A native webfishing installer for macos

New patch system - Patches the option menu crash

+35 -26
src/main.rs
··· 1 use std::fs::File; 2 - use std::io::{Read, Write}; 3 use std::path::Path; 4 use std::process::Command; 5 use std::time::Duration; ··· 12 static WEBFISHING_APPID: u32 = 3146520; 13 14 15 - // https://stackoverflow.com/a/54152901 16 - fn replace_slice<T>(buf: &mut [T], from: &[T], to: &[T]) 17 - where 18 - T: Clone + PartialEq, 19 - { 20 - for i in 0..=buf.len() - from.len() { 21 - if buf[i..].starts_with(from) { 22 - buf[i..(i + from.len())].clone_from_slice(to); 23 - } 24 - } 25 - } 26 27 async fn install_webfishing(location: &SteamDir) { 28 let steam_location = location.path(); ··· 62 63 let mut file = File::create("build/godot_steam_template.zip").expect("Could not create godotsteam template"); 64 file.write_all(&body).expect("Could not write godotsteam template"); 65 } 66 67 fn build_webfishing_macos(webfishing_path: &Path) { ··· 94 .output().expect("Could not copy webfishing.app"); 95 } 96 97 - fn patch_webfishing_pck() { 98 - println!("Patching webfishing PCK"); 99 let webfishing_pck_path = Path::new("build").join("webfishing.app").join("Contents").join("Resources").join("webfishing.pck"); 100 - 101 - let mut webfishing_pck_read = File::open(&webfishing_pck_path).expect("Could not open webfishing.pck file"); 102 - let mut bytes = Vec::new(); 103 - webfishing_pck_read.read_to_end(&mut bytes).expect("Could not read webfishing.pck"); 104 - drop(webfishing_pck_read); 105 - 106 - // PATCH 107 - replace_slice(&mut bytes, "steam_id_remote".as_bytes(), "remote_steam_id".as_bytes()); 108 - 109 - let mut webfishing_pck_write = File::create(webfishing_pck_path).expect("Could not open webfishing.pck file"); 110 - webfishing_pck_write.write_all(bytes.as_slice()).expect("Could not write webfishing.pck"); 111 } 112 113 #[tokio::main] ··· 126 } 127 128 let (app, library) = location.find_app(WEBFISHING_APPID).unwrap().unwrap(); 129 130 if !Path::exists("build/godot_steam_template.zip".as_ref()) { 131 download_godot_steam_template().await; ··· 158 } 159 160 if sudo::check() != RunningAs::Root { 161 - patch_webfishing_pck(); 162 println!("Root permissions needed to sign webfishing"); 163 } 164
··· 1 + mod utils; 2 + mod patches; 3 + 4 use std::fs::File; 5 + use std::io::{Write}; 6 use std::path::Path; 7 use std::process::Command; 8 use std::time::Duration; ··· 15 static WEBFISHING_APPID: u32 = 3146520; 16 17 18 19 async fn install_webfishing(location: &SteamDir) { 20 let steam_location = location.path(); ··· 54 55 let mut file = File::create("build/godot_steam_template.zip").expect("Could not create godotsteam template"); 56 file.write_all(&body).expect("Could not write godotsteam template"); 57 + } 58 + 59 + async fn download_gd_decomp() { 60 + println!("Download Godot Decompiler..."); 61 + let res = reqwest::get("https://github.com/GDRETools/gdsdecomp/releases/download/v0.8.0/GDRE_tools-v0.8.0-macos.zip").await.expect("Could not download decompiler"); 62 + let body = res.bytes().await.expect("Could not read body"); 63 + 64 + println!("Unzipping GodotSteam Decompiler..."); 65 + let mut file = File::create("build/decompiler.zip").expect("Could not create decompiler"); 66 + file.write_all(&body).expect("Could not write decompiler"); 67 + 68 + Command::new("unzip") 69 + .arg("decompiler.zip") 70 + .current_dir("build") 71 + .output().expect("Could not unzip godotsteam template"); 72 } 73 74 fn build_webfishing_macos(webfishing_path: &Path) { ··· 101 .output().expect("Could not copy webfishing.app"); 102 } 103 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 #[tokio::main] ··· 129 } 130 131 let (app, library) = location.find_app(WEBFISHING_APPID).unwrap().unwrap(); 132 + 133 + if !Path::exists("build/decompiler.zip".as_ref()) { 134 + download_gd_decomp().await; 135 + } 136 137 if !Path::exists("build/godot_steam_template.zip".as_ref()) { 138 download_godot_steam_template().await; ··· 165 } 166 167 if sudo::check() != RunningAs::Root { 168 + decomp_game(); 169 + patches::steam_network_patch::patch().await; 170 + patches::options_menu_patch::patch().await; 171 println!("Root permissions needed to sign webfishing"); 172 } 173
+2
src/patches/mod.rs
···
··· 1 + pub mod steam_network_patch; 2 + pub mod options_menu_patch;
+57
src/patches/options_menu_patch.rs
···
··· 1 + use async_std::fs::File; 2 + use async_std::io::{ReadExt, WriteExt}; 3 + use crate::utils::gd_utils::replace_slice; 4 + 5 + const SCRIPT_PATH: &str = "build/webfishing-decomp/options_menu.gd"; 6 + const COMPILED_PATH: &str = "build/webfishing-recomp/options_menu.gdc"; 7 + const GAME_PCK: &str = "build/webfishing.app/Contents/Resources/webfishing.pck"; 8 + 9 + pub(crate) async fn patch() { 10 + crate::utils::gd_utils::decomp_file("build/webfishing-export/Scenes/Singletons/OptionsMenu/options_menu.gdc"); 11 + 12 + let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 13 + let mut script_txt = String::new(); 14 + script.read_to_string(&mut script_txt).await.expect("Cannot read script"); 15 + drop(script); 16 + 17 + let patched_script = script_txt.replace("OS.window_borderless = PlayerData.player_options.fullscreen == 1", "\n"); 18 + let mut script = File::create(SCRIPT_PATH).await.expect("Cannot open script"); 19 + script.write_all(patched_script.as_bytes()).await.expect("Cannot write"); 20 + drop(script); 21 + 22 + crate::utils::gd_utils::recomp_file(SCRIPT_PATH); 23 + 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 + 34 + for _ in 0..16 - (compiled_script_bytes.len() % 16) { 35 + compiled_script_bytes.push(0); 36 + } 37 + 38 + let mut tsc_bytes = Vec::new(); 39 + let mut tsc = File::open("build/webfishing-export/Scenes/Singletons/OptionsMenu/options_menu.tscn").await.expect("Cannot open options menu"); 40 + tsc.read_to_end(&mut tsc_bytes).await.expect("Cannot read"); 41 + drop(tsc); 42 + 43 + compiled_script_bytes.append(&mut tsc_bytes); 44 + for _ in 0..16 - (compiled_script_bytes.len() % 16) { 45 + compiled_script_bytes.push(0); 46 + } 47 + 48 + replace_slice(&mut compiled_pck_bytes, 49 + &[0x47, 0x44, 0x53, 0x43, 0x0D, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00], 50 + "GDSC".as_ref(), 51 + &compiled_script_bytes 52 + ); 53 + 54 + 55 + let mut compiled_pck = File::create(GAME_PCK).await.expect("Cannot open pck"); 56 + compiled_pck.write_all(compiled_pck_bytes.as_slice()).await.expect("Cannot write"); 57 + }
+42
src/patches/steam_network_patch.rs
···
··· 1 + use async_std::fs::File; 2 + use async_std::io::{ReadExt, WriteExt}; 3 + use crate::utils::gd_utils::replace_slice; 4 + 5 + const SCRIPT_PATH: &str = "build/webfishing-decomp/SteamNetwork.gd"; 6 + const COMPILED_PATH: &str = "build/webfishing-recomp/SteamNetwork.gdc"; 7 + const GAME_PCK: &str = "build/webfishing.app/Contents/Resources/webfishing.pck"; 8 + 9 + pub(crate) async fn patch() { 10 + crate::utils::gd_utils::decomp_file("build/webfishing-export/Scenes/Singletons/SteamNetwork.gdc"); 11 + 12 + let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 13 + let mut script_txt = String::new(); 14 + script.read_to_string(&mut script_txt).await.expect("Cannot read script"); 15 + drop(script); 16 + 17 + let patched_script = script_txt.replace("steam_id_remote", "remote_steam_id"); 18 + let mut script = File::create(SCRIPT_PATH).await.expect("Cannot open script"); 19 + script.write_all(patched_script.as_bytes()).await.expect("Cannot write"); 20 + drop(script); 21 + 22 + crate::utils::gd_utils::recomp_file(SCRIPT_PATH); 23 + 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 + 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"); 42 + }
+45
src/utils/gd_utils.rs
···
··· 1 + use std::process::Command; 2 + 3 + const RE_TOOLS: &str = "build/Godot RE Tools.app/Contents/MacOS/Godot RE Tools"; 4 + 5 + // https://stackoverflow.com/a/54152901 6 + pub(crate) fn replace_slice<T>(buf: &mut [T], from: &[T], to: &[T], replace_with: &[T]) 7 + where 8 + T: Clone + PartialEq + From<u8>, 9 + { 10 + for i in 0..=buf.len() - replace_with.len() { 11 + if buf[i..].starts_with(from) { 12 + for j in (i + 1)..=buf.len() { 13 + 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()); 22 + break; 23 + } 24 + } 25 + } 26 + } 27 + } 28 + 29 + pub(crate) fn decomp_file(path: &str) { 30 + Command::new(RE_TOOLS) 31 + .arg("--headless") 32 + .arg(format!("--decompile=\"{}\"", path)) 33 + .arg("--bytecode=3.5.0") 34 + .arg("--output-dir=build/webfishing-decomp") 35 + .output().expect(format!("Failed to decompile file: {}", path).as_str()); 36 + } 37 + 38 + pub(crate) fn recomp_file(path: &str) { 39 + Command::new(RE_TOOLS) 40 + .arg("--headless") 41 + .arg(format!("--compile=\"{}\"", path)) 42 + .arg("--bytecode=3.5.0") 43 + .arg("--output-dir=build/webfishing-recomp") 44 + .output().expect(format!("Failed to recompile file: {}", path).as_str()); 45 + }
+1
src/utils/mod.rs
···
··· 1 + pub mod gd_utils;