A native webfishing installer for macos

1.2.0 - Build system that allows full modding

+289 -99
+3 -3
.gitignore
··· 1 - /target 2 - /build 3 - /.idea 1 + **/target 2 + **/build 3 + **/.idea
+30
Cargo.lock
··· 174 174 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 175 175 176 176 [[package]] 177 + name = "binary-reader" 178 + version = "0.4.5" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "1d173c51941d642588ed6a13d464617e3a9176b8fe00dc2de182434c36812a5e" 181 + dependencies = [ 182 + "byteorder", 183 + ] 184 + 185 + [[package]] 177 186 name = "bitflags" 178 187 version = "1.3.2" 179 188 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 212 221 version = "3.16.0" 213 222 source = "registry+https://github.com/rust-lang/crates.io-index" 214 223 checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 224 + 225 + [[package]] 226 + name = "byteorder" 227 + version = "1.5.0" 228 + source = "registry+https://github.com/rust-lang/crates.io-index" 229 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 215 230 216 231 [[package]] 217 232 name = "bytes" ··· 566 581 ] 567 582 568 583 [[package]] 584 + name = "godot_pck" 585 + version = "0.1.0" 586 + dependencies = [ 587 + "binary-reader", 588 + "md5", 589 + ] 590 + 591 + [[package]] 569 592 name = "h2" 570 593 version = "0.4.7" 571 594 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 961 984 dependencies = [ 962 985 "value-bag", 963 986 ] 987 + 988 + [[package]] 989 + name = "md5" 990 + version = "0.7.0" 991 + source = "registry+https://github.com/rust-lang/crates.io-index" 992 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 964 993 965 994 [[package]] 966 995 name = "memchr" ··· 1963 1992 dependencies = [ 1964 1993 "asky", 1965 1994 "async-std", 1995 + "godot_pck", 1966 1996 "reqwest", 1967 1997 "steamlocate", 1968 1998 "sudo",
+2 -1
Cargo.toml
··· 10 10 sysinfo = "0.33.0" 11 11 async-std = "1.13.0" 12 12 sudo = "0.6.0" 13 - asky = "0.1.1" 13 + asky = "0.1.1" 14 + godot_pck = {path = "./src/godot_pck"}
+32
src/godot_pck/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "binary-reader" 7 + version = "0.4.5" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "1d173c51941d642588ed6a13d464617e3a9176b8fe00dc2de182434c36812a5e" 10 + dependencies = [ 11 + "byteorder", 12 + ] 13 + 14 + [[package]] 15 + name = "byteorder" 16 + version = "1.5.0" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 19 + 20 + [[package]] 21 + name = "godot_pck" 22 + version = "0.1.0" 23 + dependencies = [ 24 + "binary-reader", 25 + "md5", 26 + ] 27 + 28 + [[package]] 29 + name = "md5" 30 + version = "0.7.0" 31 + source = "registry+https://github.com/rust-lang/crates.io-index" 32 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+8
src/godot_pck/Cargo.toml
··· 1 + [package] 2 + name = "godot_pck" 3 + version = "0.1.0" 4 + edition = "2021" 5 + 6 + [dependencies] 7 + binary-reader = "0.4.5" 8 + md5 = "0.7.0"
+1
src/godot_pck/src/lib.rs
··· 1 + pub mod structs;
+153
src/godot_pck/src/structs.rs
··· 1 + use binary_reader::{BinaryReader, Endian}; 2 + 3 + #[derive(Debug)] 4 + pub struct PCK { 5 + version: u8, 6 + major_version: u8, 7 + minor_version: u8, 8 + patch_version: u8, 9 + files: Vec<PckFile>, 10 + } 11 + 12 + #[derive(Debug, Clone)] 13 + pub struct PckFile { 14 + path: String, 15 + offset: u64, 16 + content: Vec<u8>, 17 + md5: [u8; 16], 18 + } 19 + 20 + impl PCK { 21 + /// Converts pck file bytes to a PCK struct 22 + pub fn from_bytes(bytes: &[u8]) -> Result<PCK, &str> { 23 + let chunked_start = bytes.chunks(16).position(|chunk| chunk.starts_with("GDPC".as_ref())); 24 + if let None = chunked_start { 25 + return Err("Invalid PCK"); 26 + }; 27 + 28 + let start_offset = chunked_start.unwrap() * 16; 29 + let pck: Vec<u8> = bytes.to_vec().into_iter().skip(start_offset).collect(); 30 + let mut pck_reader = BinaryReader::from_vec(&pck); 31 + pck_reader.set_endian(Endian::Little); 32 + 33 + let _magic = pck_reader.read_u32().unwrap(); 34 + let version = pck_reader.read_u32().unwrap(); 35 + let major_version = pck_reader.read_u32().unwrap(); 36 + let minor_version = pck_reader.read_u32().unwrap(); 37 + let patch_version = pck_reader.read_u32().unwrap(); 38 + 39 + // Skip unused 40 + let _ = pck_reader.read(64); 41 + 42 + let num_files = pck_reader.read_u32().unwrap(); 43 + let mut files = vec![]; 44 + for _ in 0..num_files { 45 + files.push(PckFile::from_bytes(&mut pck_reader, bytes)); 46 + } 47 + 48 + Ok(PCK { 49 + version: version as u8, 50 + major_version: major_version as u8, 51 + minor_version: minor_version as u8, 52 + patch_version: patch_version as u8, 53 + files 54 + }) 55 + } 56 + 57 + /// Converts a PCK struct to the byte vector representing a pck file 58 + pub fn to_bytes(&self) -> Vec<u8> { 59 + let mut bytes = vec![]; 60 + bytes.append(&mut "GDPC".as_bytes().to_vec()); 61 + bytes.append((self.version as u32).to_le_bytes().to_vec().as_mut()); 62 + bytes.append((self.major_version as u32).to_le_bytes().to_vec().as_mut()); 63 + bytes.append((self.minor_version as u32).to_le_bytes().to_vec().as_mut()); 64 + bytes.append((self.patch_version as u32).to_le_bytes().to_vec().as_mut()); 65 + bytes.append([0u8; 16*4].to_vec().as_mut()); 66 + bytes.append((self.files.len() as u32).to_le_bytes().to_vec().as_mut()); 67 + 68 + let mut file_offset = bytes.len() 69 + + self.files.len() * (4 + 8 + 8 + 16) 70 + + self.files.iter().map(|x| x.path.len() + (4 - (x.path.len() % 4))).sum::<usize>(); 71 + 72 + if file_offset % 16 != 0 { 73 + file_offset = file_offset + (16 - (file_offset % 16)); 74 + } 75 + 76 + let file_base_offset = file_offset; 77 + 78 + let mut content : Vec<u8> = vec![]; 79 + 80 + for file in &self.files { 81 + content.extend(&file.content); 82 + 83 + let mut padded_content_len = file.content.len(); 84 + if padded_content_len % 16 != 0 { 85 + padded_content_len = padded_content_len + (16 - (padded_content_len % 16)); 86 + } 87 + 88 + for _ in file.content.len()..padded_content_len { 89 + content.push(0); 90 + } 91 + 92 + let padded_length = file.path.len() + (4 - (file.path.len() % 4)); 93 + 94 + bytes.append((padded_length as u32).to_le_bytes().to_vec().as_mut()); 95 + bytes.append(file.path.as_bytes().to_vec().as_mut()); 96 + for _ in file.path.len()..padded_length { 97 + bytes.push(0); 98 + } 99 + bytes.append((file_offset as u64).to_le_bytes().to_vec().as_mut()); 100 + bytes.append((padded_content_len as u64).to_le_bytes().to_vec().as_mut()); 101 + bytes.extend(file.md5); 102 + 103 + file_offset += padded_content_len; 104 + } 105 + 106 + for _ in bytes.len()..file_base_offset { 107 + bytes.push(0); 108 + } 109 + 110 + bytes.extend(content); 111 + bytes 112 + } 113 + 114 + pub fn get_file_by_path(&self, path: &str) -> Option<&PckFile> { 115 + self.files.iter().find(|x| x.path == path) 116 + } 117 + 118 + pub fn get_file_by_path_mut(&mut self, path: &str) -> Option<&mut PckFile> { 119 + self.files.iter_mut().find(|x| x.path == path) 120 + } 121 + 122 + pub fn add_file(&mut self, new_file: PckFile) { 123 + self.files.retain(|x| x.path != new_file.path); 124 + self.files.push(new_file); 125 + } 126 + } 127 + 128 + impl PckFile { 129 + pub fn from_bytes(pck_reader: &mut BinaryReader, file_bytes: &[u8]) -> PckFile { 130 + let path_length = pck_reader.read_u32().unwrap(); 131 + let path_bytes= pck_reader.read(path_length as usize).unwrap(); 132 + let path: String = String::from_utf8_lossy(path_bytes).replace("\0", "").to_string(); 133 + let offset = pck_reader.read_u64().unwrap(); 134 + let size = pck_reader.read_u64().unwrap(); 135 + let md5 = pck_reader.read(16).unwrap(); 136 + let content: Vec<u8> = file_bytes.iter().skip(offset as usize).take(size as usize).cloned().collect(); 137 + PckFile { 138 + path, 139 + offset, 140 + content, 141 + md5: <[u8; 16]>::try_from(md5).unwrap(), 142 + } 143 + } 144 + 145 + pub fn get_content(&self) -> &[u8] { 146 + &self.content 147 + } 148 + 149 + pub fn set_content(&mut self, content: Vec<u8>) { 150 + self.md5 = *md5::compute(&content); 151 + self.content = content; 152 + } 153 + }
+13 -17
src/main.rs
··· 2 2 mod patches; 3 3 4 4 use std::fs::File; 5 - use std::io::{Write}; 5 + use std::io::{Read, Write}; 6 6 use std::path::Path; 7 7 use std::process::Command; 8 8 use std::time::Duration; ··· 11 11 use steamlocate::SteamDir; 12 12 use sudo::RunningAs; 13 13 use sysinfo::ProcessesToUpdate; 14 + use godot_pck::structs::PCK; 14 15 15 16 static WEBFISHING_APPID: u32 = 3146520; 16 17 ··· 101 102 .output().expect("Could not copy webfishing.app"); 102 103 } 103 104 104 - fn decomp_game() { 105 - let webfishing_pck_path = Path::new("build").join("webfishing.app").join("Contents").join("Resources").join("webfishing.pck"); 106 - let decomp_command = "build/Godot RE Tools.app/Contents/MacOS/Godot RE Tools"; 107 - Command::new(decomp_command) 108 - .arg("--headless") 109 - .arg(format!("--extract={}", webfishing_pck_path.display())) 110 - .arg("--include=\"*options_menu*\"") 111 - .arg("--include=\"*SteamNetwork*\"") 112 - .arg("--output-dir=build/webfishing-export") 113 - .output().expect("Could not extract game"); 114 - } 115 - 116 105 #[tokio::main] 117 106 async fn main() { 118 107 if !Path::exists("build".as_ref()) { ··· 164 153 build_webfishing_macos(webfishing_path); 165 154 } 166 155 167 - if sudo::check() != RunningAs::Root { 168 - decomp_game(); 169 - patches::steam_network_patch::patch().await; 170 - patches::options_menu_patch::patch().await; 156 + if sudo::check()!= RunningAs::Root { 157 + let _ = create_dir("build/webfishing-export").await; 158 + let mut bytes = vec![]; 159 + File::open(webfishing_path.join("webfishing.exe")).unwrap().read_to_end(&mut bytes).unwrap(); 160 + let mut pck = PCK::from_bytes(&*bytes).unwrap(); 161 + 162 + patches::steam_network_patch::patch(&mut pck).await; 163 + patches::options_menu_patch::patch(&mut pck).await; 171 164 println!("Root permissions needed to sign webfishing"); 165 + 166 + let bytes = &pck.to_bytes(); 167 + File::create("build/webfishing.app/Contents/Resources/webfishing.pck").unwrap().write(bytes).expect("Could not write to webfishing.pck"); 172 168 } 173 169 174 170 sudo::escalate_if_needed().expect("Could not escalate to sign the app");
+17 -45
src/patches/options_menu_patch.rs
··· 1 1 use async_std::fs::File; 2 2 use async_std::io::{ReadExt, WriteExt}; 3 - use crate::utils::gd_utils::replace_slice; 3 + use godot_pck::structs::PCK; 4 4 5 + const RESOURCE_PATH: &str = "res://Scenes/Singletons/OptionsMenu/options_menu.gdc"; 6 + const FILE_PATH: &str = "build/webfishing-export/options_menu.gdc"; 5 7 const SCRIPT_PATH: &str = "build/webfishing-decomp/options_menu.gd"; 6 8 const COMPILED_PATH: &str = "build/webfishing-recomp/options_menu.gdc"; 7 - const GAME_PCK: &str = "build/webfishing.app/Contents/Resources/webfishing.pck"; 9 + pub(crate) async fn patch(pck: &mut PCK) { 10 + println!("Patching {} files...", RESOURCE_PATH); 11 + let mut pck_file = pck.get_file_by_path_mut(RESOURCE_PATH).expect("Couldn't find options_menu.gdc file"); 8 12 9 - pub(crate) async fn patch() { 10 - crate::utils::gd_utils::decomp_file("build/webfishing-export/Scenes/Singletons/OptionsMenu/options_menu.gdc"); 13 + let content = pck_file.get_content(); 14 + let mut exported_file = File::create(FILE_PATH).await.expect("Couldn't create file"); 15 + exported_file.write_all(content).await.unwrap(); 16 + drop(exported_file); 17 + 18 + crate::utils::gd_utils::decomp_file(FILE_PATH); 11 19 12 20 let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 13 21 let mut script_txt = String::new(); 14 22 script.read_to_string(&mut script_txt).await.expect("Cannot read script"); 15 23 drop(script); 16 24 17 - let patched_script = script_txt.replace("OS.window_borderless = PlayerData.player_options.fullscreen == 1", "OS.window_borderless\n"); 25 + let patched_script = script_txt.replace("OS.window_borderless = PlayerData.player_options.fullscreen == 1", ""); 18 26 let mut script = File::create(SCRIPT_PATH).await.expect("Cannot open script"); 19 27 script.write_all(patched_script.as_bytes()).await.expect("Cannot write"); 20 28 drop(script); 21 29 22 30 crate::utils::gd_utils::recomp_file(SCRIPT_PATH); 23 31 24 - let mut compiled_script_bytes = Vec::new(); 25 - let mut compiled_script = File::open(COMPILED_PATH).await.expect("Cannot open script"); 26 - compiled_script.read_to_end(&mut compiled_script_bytes).await.expect("Cannot read"); 27 - drop(compiled_script); 28 - 29 - let mut compiled_pck_bytes = Vec::new(); 30 - let mut compiled_pck = File::open(GAME_PCK).await.expect("Cannot open pck"); 31 - compiled_pck.read_to_end(&mut compiled_pck_bytes).await.expect("Cannot read"); 32 - drop(compiled_pck); 33 - let mut compiled_pck_bytes: Vec<u8> = compiled_pck_bytes.into_iter().rev().skip_while(|b| (*b) == 0).collect::<Vec<u8>>().into_iter().rev().collect(); 34 - 35 - if compiled_script_bytes.len() % 16 > 0 { 36 - let to_add = 16 - (compiled_script_bytes.len() % 16); 37 - for _ in 0..to_add { 38 - compiled_script_bytes.push(0); 39 - } 40 - } 41 - 42 - let mut tsc_bytes = Vec::new(); 43 - let mut tsc = File::open("build/webfishing-export/Scenes/Singletons/OptionsMenu/options_menu.tscn").await.expect("Cannot open options menu"); 44 - tsc.read_to_end(&mut tsc_bytes).await.expect("Cannot read"); 45 - drop(tsc); 32 + let mut file = File::open(COMPILED_PATH).await.expect("Cannot open compiled script"); 33 + let mut new_content = vec![]; 34 + file.read_to_end(&mut new_content).await.unwrap(); 46 35 47 - compiled_script_bytes.append(&mut tsc_bytes); 48 - let mut compiled_pck_bytes: Vec<u8> = compiled_pck_bytes.into_iter().rev().skip_while(|b| (*b) == 0).collect::<Vec<u8>>().into_iter().rev().collect(); 49 - if compiled_script_bytes.len() % 16 > 0 { 50 - let to_add = 16 - (compiled_script_bytes.len() % 16); 51 - for _ in 0..to_add { 52 - compiled_script_bytes.push(0); 53 - } 54 - } 55 - 56 - replace_slice(&mut compiled_pck_bytes, 57 - &[0x47, 0x44, 0x53, 0x43, 0x0D, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00], 58 - "GDSC".as_ref(), 59 - &compiled_script_bytes 60 - ); 61 - 62 - 63 - let mut compiled_pck = File::create(GAME_PCK).await.expect("Cannot open pck"); 64 - compiled_pck.write_all(compiled_pck_bytes.as_slice()).await.expect("Cannot write"); 36 + pck_file.set_content(new_content); 65 37 }
+18 -21
src/patches/steam_network_patch.rs
··· 1 + use godot_pck::structs::PckFile; 1 2 use async_std::fs::File; 2 3 use async_std::io::{ReadExt, WriteExt}; 3 - use crate::utils::gd_utils::replace_slice; 4 + use godot_pck::structs::PCK; 4 5 6 + const RESOURCE_PATH: &str = "res://Scenes/Singletons/SteamNetwork.gdc"; 7 + const FILE_PATH: &str = "build/webfishing-export/SteamNetwork.gdc"; 5 8 const SCRIPT_PATH: &str = "build/webfishing-decomp/SteamNetwork.gd"; 6 9 const COMPILED_PATH: &str = "build/webfishing-recomp/SteamNetwork.gdc"; 7 - const GAME_PCK: &str = "build/webfishing.app/Contents/Resources/webfishing.pck"; 8 10 9 - pub(crate) async fn patch() { 10 - crate::utils::gd_utils::decomp_file("build/webfishing-export/Scenes/Singletons/SteamNetwork.gdc"); 11 + pub(crate) async fn patch(pck: &mut PCK) { 12 + println!("Patching {} files...", RESOURCE_PATH); 13 + let mut pck_file: &mut PckFile = pck.get_file_by_path_mut(RESOURCE_PATH).expect("Couldn't find options_menu.gdc file"); 14 + 15 + let content = pck_file.get_content(); 16 + let mut exported_file = File::create(FILE_PATH).await.expect("Couldn't create file"); 17 + exported_file.write_all(content).await.expect("Couldn't write file"); 18 + drop(exported_file); 19 + 20 + crate::utils::gd_utils::decomp_file(FILE_PATH); 11 21 12 22 let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 13 23 let mut script_txt = String::new(); ··· 21 31 22 32 crate::utils::gd_utils::recomp_file(SCRIPT_PATH); 23 33 24 - let mut compiled_script_bytes = Vec::new(); 25 - let mut compiled_script = File::open(COMPILED_PATH).await.expect("Cannot open script"); 26 - compiled_script.read_to_end(&mut compiled_script_bytes).await.expect("Cannot read"); 27 - drop(compiled_script); 34 + let mut file = File::open(COMPILED_PATH).await.expect("Cannot open compiled script"); 35 + let mut new_content = vec![]; 36 + file.read_to_end(&mut new_content).await.unwrap(); 28 37 29 - let mut compiled_pck_bytes = Vec::new(); 30 - let mut compiled_pck = File::open(GAME_PCK).await.expect("Cannot open pck"); 31 - compiled_pck.read_to_end(&mut compiled_pck_bytes).await.expect("Cannot read"); 32 - drop(compiled_pck); 33 - 34 - replace_slice(&mut compiled_pck_bytes, 35 - &[0x47, 0x44, 0x53, 0x43, 0x0D, 0x00, 0x00, 0x00, 0x5B, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00], 36 - "GDSC".as_ref(), 37 - &compiled_script_bytes 38 - ); 39 - 40 - let mut compiled_pck = File::create(GAME_PCK).await.expect("Cannot open pck"); 41 - compiled_pck.write_all(compiled_pck_bytes.as_slice()).await.expect("Cannot write"); 38 + pck_file.set_content(new_content); 42 39 }
+12 -12
src/utils/gd_utils.rs
··· 3 3 const RE_TOOLS: &str = "build/Godot RE Tools.app/Contents/MacOS/Godot RE Tools"; 4 4 5 5 // https://stackoverflow.com/a/54152901 6 - pub(crate) fn replace_slice<T>(buf: &mut [T], from: &[T], to: &[T], replace_with: &[T]) 6 + pub(crate) fn replace_slice<T>(buf: &[T], from: &[T], to: &[T], replace_with: &mut [T]) -> Vec<T> 7 7 where 8 8 T: Clone + PartialEq + From<u8>, 9 9 { 10 - for mut i in 0..=buf.len() - replace_with.len() { 10 + let mut last_j = 0; 11 + let mut res : Vec<T> = Vec::new(); 12 + for i in 0..=buf.len() { 11 13 if buf[i..].starts_with(from) { 14 + res.append(&mut buf[last_j..i].to_vec()); 12 15 for j in (i + 1)..=buf.len() { 13 16 if buf[j..].starts_with(to) { 14 - let mut vec = Vec::new(); 15 - vec.extend_from_slice(replace_with); 16 - if replace_with.len() < j-i { 17 - for _ in 0.. (j-i-replace_with.len()) { 18 - vec.push(T::try_from(0).expect("Failed to convert from usize")); 19 - } 20 - } 21 - buf[i..j].clone_from_slice(vec.as_slice()); 17 + res.append(replace_with.to_vec().as_mut()); 18 + last_j = j; 22 19 break; 23 20 } 24 21 } 25 22 } 26 23 } 24 + 25 + res.append(&mut buf[last_j..].to_vec()); 26 + res 27 27 } 28 28 29 29 pub(crate) fn decomp_file(path: &str) { 30 - Command::new(RE_TOOLS) 30 + dbg!(Command::new(RE_TOOLS) 31 31 .arg("--headless") 32 32 .arg(format!("--decompile=\"{}\"", path)) 33 33 .arg("--bytecode=3.5.0") 34 34 .arg("--output-dir=build/webfishing-decomp") 35 - .output().expect(format!("Failed to decompile file: {}", path).as_str()); 35 + .output().expect(format!("Failed to decompile file: {}", path).as_str())); 36 36 } 37 37 38 38 pub(crate) fn recomp_file(path: &str) {