A native webfishing installer for macos

Compare changes

Choose any two refs to compare.

+123 -5
Cargo.lock
··· 18 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 20 [[package]] 21 name = "asky" 22 version = "0.1.1" 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 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" ··· 250 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 251 252 [[package]] 253 name = "colored" 254 version = "2.2.0" 255 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 741 ] 742 743 [[package]] 744 name = "icu_collections" 745 version = "1.5.0" 746 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1002 version = "0.3.17" 1003 source = "registry+https://github.com/rust-lang/crates.io-index" 1004 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1005 1006 [[package]] 1007 name = "miniz_oxide" ··· 1053 ] 1054 1055 [[package]] 1056 name = "ntapi" 1057 version = "0.4.1" 1058 source = "registry+https://github.com/rust-lang/crates.io-index" 1059 checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 1060 dependencies = [ 1061 "winapi", 1062 ] 1063 1064 [[package]] ··· 1147 "redox_syscall", 1148 "smallvec", 1149 "windows-targets 0.52.6", 1150 ] 1151 1152 [[package]] ··· 1453 1454 [[package]] 1455 name = "serde" 1456 - version = "1.0.216" 1457 source = "registry+https://github.com/rust-lang/crates.io-index" 1458 - checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 1459 dependencies = [ 1460 "serde_derive", 1461 ] 1462 1463 [[package]] 1464 name = "serde_derive" 1465 - version = "1.0.216" 1466 source = "registry+https://github.com/rust-lang/crates.io-index" 1467 - checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 1468 dependencies = [ 1469 "proc-macro2", 1470 "quote", ··· 1993 "asky", 1994 "async-std", 1995 "godot_pck", 1996 "reqwest", 1997 "steamlocate", 1998 "sudo", 1999 "sysinfo", ··· 2028 source = "registry+https://github.com/rust-lang/crates.io-index" 2029 checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 2030 dependencies = [ 2031 - "windows-core", 2032 "windows-targets 0.52.6", 2033 ] 2034
··· 18 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 20 [[package]] 21 + name = "android-tzdata" 22 + version = "0.1.1" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 + 26 + [[package]] 27 + name = "android_system_properties" 28 + version = "0.1.5" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 + dependencies = [ 32 + "libc", 33 + ] 34 + 35 + [[package]] 36 name = "asky" 37 version = "0.1.1" 38 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 238 checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 239 240 [[package]] 241 + name = "bytecount" 242 + version = "0.6.8" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" 245 + 246 + [[package]] 247 name = "byteorder" 248 version = "1.5.0" 249 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 271 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 272 273 [[package]] 274 + name = "chrono" 275 + version = "0.4.39" 276 + source = "registry+https://github.com/rust-lang/crates.io-index" 277 + checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 278 + dependencies = [ 279 + "android-tzdata", 280 + "iana-time-zone", 281 + "js-sys", 282 + "num-traits", 283 + "wasm-bindgen", 284 + "windows-targets 0.52.6", 285 + ] 286 + 287 + [[package]] 288 name = "colored" 289 version = "2.2.0" 290 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 776 ] 777 778 [[package]] 779 + name = "iana-time-zone" 780 + version = "0.1.61" 781 + source = "registry+https://github.com/rust-lang/crates.io-index" 782 + checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 783 + dependencies = [ 784 + "android_system_properties", 785 + "core-foundation-sys", 786 + "iana-time-zone-haiku", 787 + "js-sys", 788 + "wasm-bindgen", 789 + "windows-core 0.52.0", 790 + ] 791 + 792 + [[package]] 793 + name = "iana-time-zone-haiku" 794 + version = "0.1.2" 795 + source = "registry+https://github.com/rust-lang/crates.io-index" 796 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 797 + dependencies = [ 798 + "cc", 799 + ] 800 + 801 + [[package]] 802 name = "icu_collections" 803 version = "1.5.0" 804 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1060 version = "0.3.17" 1061 source = "registry+https://github.com/rust-lang/crates.io-index" 1062 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1063 + 1064 + [[package]] 1065 + name = "minimal-lexical" 1066 + version = "0.2.1" 1067 + source = "registry+https://github.com/rust-lang/crates.io-index" 1068 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1069 1070 [[package]] 1071 name = "miniz_oxide" ··· 1117 ] 1118 1119 [[package]] 1120 + name = "nom" 1121 + version = "7.1.3" 1122 + source = "registry+https://github.com/rust-lang/crates.io-index" 1123 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1124 + dependencies = [ 1125 + "memchr", 1126 + "minimal-lexical", 1127 + ] 1128 + 1129 + [[package]] 1130 + name = "nom_locate" 1131 + version = "4.2.0" 1132 + source = "registry+https://github.com/rust-lang/crates.io-index" 1133 + checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" 1134 + dependencies = [ 1135 + "bytecount", 1136 + "memchr", 1137 + "nom", 1138 + ] 1139 + 1140 + [[package]] 1141 name = "ntapi" 1142 version = "0.4.1" 1143 source = "registry+https://github.com/rust-lang/crates.io-index" 1144 checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 1145 dependencies = [ 1146 "winapi", 1147 + ] 1148 + 1149 + [[package]] 1150 + name = "num-traits" 1151 + version = "0.2.19" 1152 + source = "registry+https://github.com/rust-lang/crates.io-index" 1153 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1154 + dependencies = [ 1155 + "autocfg", 1156 ] 1157 1158 [[package]] ··· 1241 "redox_syscall", 1242 "smallvec", 1243 "windows-targets 0.52.6", 1244 + ] 1245 + 1246 + [[package]] 1247 + name = "patch-apply" 1248 + version = "0.8.3" 1249 + source = "registry+https://github.com/rust-lang/crates.io-index" 1250 + checksum = "7fe95476ec50a4e9b95ed12a4677ff5996aba4329bf2535fb21c49afaad20809" 1251 + dependencies = [ 1252 + "chrono", 1253 + "nom", 1254 + "nom_locate", 1255 ] 1256 1257 [[package]] ··· 1558 1559 [[package]] 1560 name = "serde" 1561 + version = "1.0.217" 1562 source = "registry+https://github.com/rust-lang/crates.io-index" 1563 + checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1564 dependencies = [ 1565 "serde_derive", 1566 ] 1567 1568 [[package]] 1569 name = "serde_derive" 1570 + version = "1.0.217" 1571 source = "registry+https://github.com/rust-lang/crates.io-index" 1572 + checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1573 dependencies = [ 1574 "proc-macro2", 1575 "quote", ··· 2098 "asky", 2099 "async-std", 2100 "godot_pck", 2101 + "patch-apply", 2102 "reqwest", 2103 + "serde", 2104 + "serde_derive", 2105 + "serde_json", 2106 "steamlocate", 2107 "sudo", 2108 "sysinfo", ··· 2137 source = "registry+https://github.com/rust-lang/crates.io-index" 2138 checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 2139 dependencies = [ 2140 + "windows-core 0.57.0", 2141 + "windows-targets 0.52.6", 2142 + ] 2143 + 2144 + [[package]] 2145 + name = "windows-core" 2146 + version = "0.52.0" 2147 + source = "registry+https://github.com/rust-lang/crates.io-index" 2148 + checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2149 + dependencies = [ 2150 "windows-targets 0.52.6", 2151 ] 2152
+7 -1
Cargo.toml
··· 11 async-std = "1.13.0" 12 sudo = "0.6.0" 13 asky = "0.1.1" 14 - godot_pck = {path = "./src/godot_pck"}
··· 11 async-std = "1.13.0" 12 sudo = "0.6.0" 13 asky = "0.1.1" 14 + patch-apply = "0.8.3" 15 + serde = { version = "1.0.217" } 16 + serde_json = { version = "1.0.134" } 17 + serde_derive = { version = "1.0.217" } 18 + 19 + 20 + godot_pck = {path = "./src/godot_pck"}
+38
README.md
··· 25 - renaming `steam_id_remote` dictionnary key to `remote_steam_id` to fix network spam detection that resulted in timeouts 26 - prevent the game from crashing when saving the options by not setting any values to `OS.windowed_borderless` because setting a value to it crashes the game somehow 27 28 ## Credits 29 30 [@vimaexd](https://github.com/vimaexd) for their blog post !
··· 25 - renaming `steam_id_remote` dictionnary key to `remote_steam_id` to fix network spam detection that resulted in timeouts 26 - prevent the game from crashing when saving the options by not setting any values to `OS.windowed_borderless` because setting a value to it crashes the game somehow 27 28 + 29 + ## How to install a mod? 30 + 31 + When running the software for the first time and building webfishing, you'll notice that a `mods` folder has appeared in the folder where the installer is. 32 + 33 + In order to install a mod, just copy a mod folder in it, a mod folder has a `manifest.json` file in it. 34 + 35 + After that, run the installer again ! It will tell you in the terminal if the mod is installed or if something went wrong. 36 + 37 + Here's a small mod list : [link to the mod list](modlist.md) 38 + 39 + ## How to make a mod? 40 + 41 + As you can see in the `example_mods` folder, a mod has typically two folders and a single `manifest.json` file having the following structure: 42 + ```jsonc 43 + { 44 + "name": "Ship Mod", // Mod name 45 + "author": "Estym", // Author 46 + 47 + "pck_info": { // (Optional) 48 + "directory": "pck", // Relative folder path to where the mod resources are 49 + "resource_prefix": "res://Mods/Ship/" // Resource path prefix for the mod resources 50 + }, 51 + 52 + "patches": [ // Array of patches 53 + { 54 + "resource": "res://Scenes/Entities/Player/player.gdc", // Resource to patch 55 + "patch_file": "patch/player.patch" // relative file path to the patch file 56 + } 57 + ], 58 + 59 + "deps": [] // Dependencies for the mod (Optional) 60 + } 61 + ``` 62 + 63 + ### Notes: 64 + - The patch files are made by using `$ git diff [original_file] [modded_file] > file.patch` 65 + 66 ## Credits 67 68 [@vimaexd](https://github.com/vimaexd) for their blog post !
+15
example_mods/ship/manifest.json
···
··· 1 + { 2 + "name": "Ship Mod", 3 + "author": "Estym", 4 + "pck_info": { 5 + "directory": "pck", 6 + "resource_prefix": "res://Mods/Ship/" 7 + }, 8 + "patches": [ 9 + { 10 + "resource": "res://Scenes/Entities/Player/player.gdc", 11 + "patch_file": "patch/player.patch" 12 + } 13 + ], 14 + "deps": [] 15 + }
+49
example_mods/ship/patch/player.patch
···
··· 1 + diff --git a/player.gd b/player copie.gd 2 + index c84af29..4cb41f9 100644 3 + --- a/player.gd 4 + +++ b/player copie.gd 5 + @@ -23,6 +23,7 @@ const PARTICLE_DATA = { 6 + "music": preload("res://Scenes/Particles/music_particle.tscn"), 7 + "kiss": preload("res://Scenes/Particles/kiss.tscn"), 8 + } 9 + +var ship_mod_instance = preload("res://Mods/Ship/ship.gd").new() 10 + 11 + export (NodePath) var hand_sprite_node 12 + export (NodePath) var hand_bone_node 13 + @@ -31,6 +32,7 @@ export var NPC_cosmetics = {"species": "species_cat", "pattern": "pattern_none" 14 + export var NPC_name = "NPC Test" 15 + export var NPC_title = "npc title here" 16 + 17 + + 18 + var camera_zoom = 5.0 19 + 20 + var direction = Vector3() 21 + @@ -531,6 +533,10 @@ func _get_input(): 22 + 23 + mouse_look = false 24 + 25 + + if ship_mod_instance.is_sitting_on_ship(self): 26 + + ship_mod_instance.process_ship(self, get_world()) 27 + + return 28 + + 29 + if sitting: return 30 + 31 + if Input.is_action_pressed("move_forward"): direction -= cam_base.transform.basis.z 32 + @@ -1389,17 +1395,6 @@ func _create_prop(ref, offset = Vector3(0, 1, 0), restrict_to_one = false): 33 + PlayerData.emit_signal("_prop_update") 34 + return false 35 + 36 + - 37 + - if $detection_zones / prop_detect.get_overlapping_bodies().size() > 0 or not is_on_floor() or not $detection_zones / prop_ray.is_colliding(): 38 + - PlayerData._send_notification("invalid prop placement", 1) 39 + - return false 40 + - 41 + - 42 + - if prop_ids.size() > 4: 43 + - PlayerData._send_notification("prop limit reached", 1) 44 + - return false 45 + - 46 + - 47 + var item = PlayerData._find_item_code(ref) 48 + var data = Globals.item_data[item["id"]]["file"] 49 + var ver_offset = Vector3(0, 1.0, 0) * (1.0 - player_scale)
+42
example_mods/ship/pck/ship.gd
···
··· 1 + extends Node 2 + 3 + var ship 4 + # Declare member variables here. Examples: 5 + # var a = 2 6 + # var b = "text" 7 + var entity_node 8 + 9 + # Called when the node enters the scene tree for the first time. 10 + func _ready(): 11 + 12 + pass # Replace with function body. 13 + 14 + func process_ship(player, world): 15 + var entities = player.get_parent() 16 + for i in entities.get_children(): 17 + if i.actor_type == "table": 18 + if Input.is_key_pressed(KEY_SHIFT): 19 + i.translation.y += 0.05 20 + i.packet_cooldown = 0 21 + i._network_share() 22 + elif Input.is_key_pressed(KEY_CONTROL): 23 + i.translation.y -= 0.05 24 + i.packet_cooldown = 0 25 + i._network_share() 26 + 27 + 28 + func get_prop_by_ref(ref): 29 + for obj in PlayerData.inventory: 30 + if obj.ref == ref: 31 + return obj 32 + return null 33 + 34 + func is_sitting_on_ship(player): 35 + 36 + for prop in PlayerData.props_placed: 37 + var obj = get_prop_by_ref(prop.ref) 38 + if obj != null && obj.id == 'prop_table': 39 + ship = obj 40 + return player.sitting 41 + return false; 42 +
+7
modlist.md
···
··· 1 + # List of currently existing mods compatible with the webfishing-macos-installer 2 + 3 + ## AtProto Webfishing 4 + [Repository](https://forgejo.regnault.dev/estym/webfishing-macos-atproto) 5 + 6 + A mod that adds remote saving using a Bluesky account (or a self-hosted PDS) 7 +
+8 -2
src/godot_pck/src/structs.rs
··· 12 #[derive(Debug, Clone)] 13 pub struct PckFile { 14 path: String, 15 - offset: u64, 16 content: Vec<u8>, 17 md5: [u8; 16], 18 } ··· 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
··· 12 #[derive(Debug, Clone)] 13 pub struct PckFile { 14 path: String, 15 content: Vec<u8>, 16 md5: [u8; 16], 17 } ··· 135 let content: Vec<u8> = file_bytes.iter().skip(offset as usize).take(size as usize).cloned().collect(); 136 PckFile { 137 path, 138 content, 139 md5: <[u8; 16]>::try_from(md5).unwrap(), 140 + } 141 + } 142 + 143 + pub fn new_file(path: String, content: Vec<u8>) -> PckFile { 144 + PckFile { 145 + path, 146 + md5: *md5::compute(&content), 147 + content 148 } 149 } 150
+141 -78
src/main.rs
··· 1 - mod utils; 2 mod patches; 3 4 use std::fs::File; 5 use std::io::{Read, Write}; 6 use std::path::Path; 7 - use std::process::Command; 8 use std::time::Duration; 9 - use asky::Confirm; 10 - use async_std::fs::create_dir; 11 use steamlocate::SteamDir; 12 use sudo::RunningAs; 13 use sysinfo::ProcessesToUpdate; 14 - use godot_pck::structs::PCK; 15 16 static WEBFISHING_APPID: u32 = 3146520; 17 - 18 - 19 20 async fn install_webfishing(location: &SteamDir) { 21 let steam_location = location.path(); 22 - let acf_path = steam_location.join("steamapps").join(format!("appmanifest_{}.acf", WEBFISHING_APPID)); 23 24 println!("Creating Webfishing ACF"); 25 - File::create(acf_path).unwrap().write(include_str!("../res/webfishing.acf").as_bytes()).expect("could not write acf"); 26 27 println!("Waiting for steam to close"); 28 let mut system = sysinfo::System::new_all(); ··· 40 } 41 42 println!("Steam launched, downloading webfishing"); 43 - let download_path = steam_location.join("steamapps").join("downloading").join(format!("{}", WEBFISHING_APPID)); 44 45 while Path::exists(download_path.as_path()) { 46 println!("Downloading webfishing..."); ··· 50 51 async fn download_godot_steam_template() { 52 println!("Downloading GodotSteam template..."); 53 - let res = reqwest::get("https://github.com/GodotSteam/GodotSteam/releases/download/v3.27/macos-g36-s160-gs327.zip").await.expect("Could not download godotsteam template"); 54 let body = res.bytes().await.expect("Could not read body"); 55 56 - let mut file = File::create("build/godot_steam_template.zip").expect("Could not create godotsteam template"); 57 - file.write_all(&body).expect("Could not write godotsteam template"); 58 } 59 60 async fn download_gd_decomp() { ··· 69 Command::new("unzip") 70 .arg("decompiler.zip") 71 .current_dir("build") 72 - .output().expect("Could not unzip godotsteam template"); 73 } 74 75 fn build_webfishing_macos(webfishing_path: &Path) { 76 let template_path = Path::new("build/osx_template.app"); 77 Command::new("rm") 78 .current_dir(template_path) 79 - .arg("Contents/MacOS/godot_osx_debug.64") 80 - .output().expect("Could not remove delete godot_osx_debug.64"); 81 82 Command::new("mv") 83 .current_dir(template_path) 84 - .arg("Contents/MacOS/godot_osx_release.64") 85 .arg("Contents/MacOS/webfishing") 86 - .output().expect("Could not rename godot_osc_release.64"); 87 88 - let mut steamappid = File::create(template_path.join("Contents").join("MacOS").join("steam_appid.txt")).expect("could not create steam_appid.txt file"); 89 - steamappid.write(include_str!("../res/steam_appid.txt").as_bytes()).expect("could not write steam_appid.txt"); 90 91 Command::new("cp") 92 .arg(webfishing_path.join("webfishing.exe")) 93 - .arg(template_path.join("Contents").join("Resources").join("webfishing.pck")) 94 - .output().expect("Could not copy webfishing.exe"); 95 96 - let mut info_plist = File::create(template_path.join("Contents").join("Info.plist")).expect("Could not open Info.plist"); 97 - info_plist.write_all(include_str!("../res/Info.plist").as_bytes()).expect("could not write Info.plist"); 98 99 Command::new("mv") 100 .arg(template_path) 101 .arg(Path::new("build/webfishing.app")) 102 - .output().expect("Could not copy webfishing.app"); 103 } 104 105 #[tokio::main] 106 async fn main() { 107 if !Path::exists("build".as_ref()) { 108 println!("Creating build folder"); 109 - create_dir("build").await.expect("could not create build folder"); 110 } 111 112 let location = SteamDir::locate().expect("could not locate steam directory"); ··· 117 install_webfishing(&location).await; 118 } 119 120 - let (app, library) = location.find_app(WEBFISHING_APPID).unwrap().unwrap(); 121 122 if !Path::exists("build/decompiler.zip".as_ref()) { 123 download_gd_decomp().await; 124 } 125 126 - if !Path::exists("build/godot_steam_template.zip".as_ref()) { 127 download_godot_steam_template().await; 128 } 129 130 - if !Path::exists("build/macos.zip".as_ref()) { 131 - println!("Unzipping template"); 132 - Command::new("unzip") 133 - .arg("-o") 134 - .arg("godot_steam_template.zip") 135 - .current_dir("./build") 136 - .output().expect("Could not unzip godot_steam_template.zip"); 137 - } 138 139 - if !Path::exists("build/osx_template.app".as_ref()) && !Path::exists("build/webfishing.app".as_ref()) { 140 - println!("Unzipping template"); 141 - Command::new("unzip") 142 - .arg("-o") 143 - .arg("macos.zip") 144 - .current_dir("./build") 145 - .output() 146 - .expect("Could not unzip macos.zip"); 147 - } 148 149 150 let binding = library.resolve_app_dir(&app); 151 let webfishing_path = binding.as_path(); ··· 153 build_webfishing_macos(webfishing_path); 154 } 155 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; 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"); 168 - } 169 - 170 - sudo::escalate_if_needed().expect("Could not escalate to sign the app"); 171 - 172 - Command::new("xattr") 173 - .arg("-cr") 174 - .arg("build/webfishing.app") 175 - .output() 176 - .expect("Could not execute xattr"); 177 178 - if Confirm::new("Do you wanna install Webfishing in the app folder?").prompt().expect("Could not confirm to install the webfishing") { 179 - Command::new("rsync") 180 - .arg("-a") 181 - .arg("build/webfishing.app") 182 - .current_dir("/Applications/") 183 - .output().expect("Could not execute rsync"); 184 185 - Command::new("rm") 186 - .arg("-r") 187 - .arg("build/webfishing.app") 188 - .output().expect("Could not remove webfishing.app"); 189 190 - println!("Successfully installed webfishing !"); 191 - } else { 192 - println!("Webfishing is in the build folder !") 193 } 194 }
··· 1 + mod mods; 2 mod patches; 3 + mod utils; 4 5 + use asky::Text; 6 + use async_std::fs::create_dir; 7 + use godot_pck::structs::PCK; 8 + use std::env::{current_exe, set_current_dir}; 9 use std::fs::File; 10 use std::io::{Read, Write}; 11 use std::path::Path; 12 + use std::process::{exit, Command}; 13 use std::time::Duration; 14 use steamlocate::SteamDir; 15 use sudo::RunningAs; 16 use sysinfo::ProcessesToUpdate; 17 18 static WEBFISHING_APPID: u32 = 3146520; 19 20 async fn install_webfishing(location: &SteamDir) { 21 let steam_location = location.path(); 22 + let acf_path = steam_location 23 + .join("steamapps") 24 + .join(format!("appmanifest_{}.acf", WEBFISHING_APPID)); 25 26 println!("Creating Webfishing ACF"); 27 + File::create(acf_path) 28 + .unwrap() 29 + .write(include_str!("../res/webfishing.acf").as_bytes()) 30 + .expect("could not write acf"); 31 32 println!("Waiting for steam to close"); 33 let mut system = sysinfo::System::new_all(); ··· 45 } 46 47 println!("Steam launched, downloading webfishing"); 48 + let download_path = steam_location 49 + .join("steamapps") 50 + .join("downloading") 51 + .join(format!("{}", WEBFISHING_APPID)); 52 53 while Path::exists(download_path.as_path()) { 54 println!("Downloading webfishing..."); ··· 58 59 async fn download_godot_steam_template() { 60 println!("Downloading GodotSteam template..."); 61 + let res = reqwest::get( 62 + "https://codeberg.org/godotsteam/godotsteam/releases/download/v3.24/macos-g353-s159-gs324.zip", 63 + ) 64 + .await 65 + .expect("Could not download godotsteam template"); 66 let body = res.bytes().await.expect("Could not read body"); 67 68 + let mut file = File::create("build/godot_steam_template_324.zip") 69 + .expect("Could not create godotsteam template"); 70 + file.write_all(&body) 71 + .expect("Could not write godotsteam template"); 72 } 73 74 async fn download_gd_decomp() { ··· 83 Command::new("unzip") 84 .arg("decompiler.zip") 85 .current_dir("build") 86 + .output() 87 + .expect("Could not unzip godotsteam template"); 88 } 89 90 fn build_webfishing_macos(webfishing_path: &Path) { 91 let template_path = Path::new("build/osx_template.app"); 92 + 93 Command::new("rm") 94 .current_dir(template_path) 95 + .arg("Contents/MacOS/godot_osx_debug.universal") 96 + .output() 97 + .expect("Could not remove delete godot_osx_debug.universal"); 98 99 Command::new("mv") 100 .current_dir(template_path) 101 + .arg("Contents/MacOS/godot_osx_release.universal") 102 .arg("Contents/MacOS/webfishing") 103 + .output() 104 + .expect("Could not rename godot_osc_release.universal"); 105 106 + let mut steamappid = File::create( 107 + template_path 108 + .join("Contents") 109 + .join("MacOS") 110 + .join("steam_appid.txt"), 111 + ) 112 + .expect("could not create steam_appid.txt file"); 113 + steamappid 114 + .write(include_str!("../res/steam_appid.txt").as_bytes()) 115 + .expect("could not write steam_appid.txt"); 116 117 Command::new("cp") 118 .arg(webfishing_path.join("webfishing.exe")) 119 + .arg( 120 + template_path 121 + .join("Contents") 122 + .join("Resources") 123 + .join("webfishing.pck"), 124 + ) 125 + .output() 126 + .expect("Could not copy webfishing.exe"); 127 128 + let mut info_plist = File::create(template_path.join("Contents").join("Info.plist")) 129 + .expect("Could not open Info.plist"); 130 + info_plist 131 + .write_all(include_str!("../res/Info.plist").as_bytes()) 132 + .expect("could not write Info.plist"); 133 134 Command::new("mv") 135 .arg(template_path) 136 .arg(Path::new("build/webfishing.app")) 137 + .output() 138 + .expect("Could not copy webfishing.app"); 139 + } 140 + 141 + fn sign_webfishing() { 142 + Command::new("xattr") 143 + .arg("-cr") 144 + .arg("build/webfishing.app") 145 + .output() 146 + .expect("Could not execute xattr"); 147 + 148 + Command::new("rm") 149 + .arg("build/signing-step") 150 + .output() 151 + .expect("Could not remove signing-step file"); 152 + 153 + println!("Webfishing is in the build folder !"); 154 + 155 + Text::new("Press Enter to quit") 156 + .prompt() 157 + .expect("Could not confirm to quit"); 158 + 159 + 160 } 161 162 #[tokio::main] 163 async fn main() { 164 + if sudo::check() == RunningAs::Root && Path::new("build/signing-step").exists() { 165 + sign_webfishing(); 166 + exit(0); 167 + } 168 + 169 + set_current_dir( 170 + current_exe() 171 + .unwrap() 172 + .parent() 173 + .expect("Could not get current dir"), 174 + ) 175 + .expect("Could not set current dir"); 176 if !Path::exists("build".as_ref()) { 177 println!("Creating build folder"); 178 + create_dir("build") 179 + .await 180 + .expect("could not create build folder"); 181 } 182 183 let location = SteamDir::locate().expect("could not locate steam directory"); ··· 188 install_webfishing(&location).await; 189 } 190 191 + let (app, library) = location.find_app(WEBFISHING_APPID).unwrap().unwrap(); 192 193 if !Path::exists("build/decompiler.zip".as_ref()) { 194 download_gd_decomp().await; 195 } 196 197 + if !Path::exists("build/godot_steam_template_324.zip".as_ref()) { 198 download_godot_steam_template().await; 199 } 200 201 + println!("Unzipping template 1/2"); 202 + Command::new("unzip") 203 + .arg("-o") 204 + .arg("godot_steam_template_324.zip") 205 + .current_dir("./build") 206 + .output() 207 + .expect("Could not unzip godot_steam_template_324.zip"); 208 209 + Command::new("mv") 210 + .arg("build/godot_steam_template_324/macos.zip") 211 + .arg("build/macos.zip") 212 + .current_dir("./build") 213 + .output() 214 + .expect("Could not copy godot_steam_template_324/macos.zip"); 215 216 + println!("Unzipping template 2/2"); 217 + Command::new("unzip") 218 + .arg("-o") 219 + .arg("macos.zip") 220 + .current_dir("./build") 221 + .output() 222 + .expect("Could not unzip macos.zip"); 223 224 let binding = library.resolve_app_dir(&app); 225 let webfishing_path = binding.as_path(); ··· 227 build_webfishing_macos(webfishing_path); 228 } 229 230 + let _ = create_dir("build/webfishing-export").await; 231 + let mut bytes = vec![]; 232 + File::open(webfishing_path.join("webfishing.exe")) 233 + .unwrap() 234 + .read_to_end(&mut bytes) 235 + .unwrap(); 236 + let mut pck = PCK::from_bytes(&*bytes).unwrap(); 237 238 + patches::steam_network_patch::patch(&mut pck).await; 239 + patches::options_menu_patch::patch(&mut pck).await; 240 + mods::mods::process_mods(&mut pck); 241 242 + let bytes = &pck.to_bytes(); 243 + File::create("build/webfishing.app/Contents/Resources/webfishing.pck") 244 + .unwrap() 245 + .write(bytes) 246 + .expect("Could not write to webfishing.pck"); 247 248 + File::create("build/signing-step").expect("Could not create signing step file"); 249 250 + if sudo::check() != RunningAs::Root { 251 + println!("In order to sign the app, you need to be root"); 252 + sudo::escalate_if_needed().expect("Could not escalate"); 253 + exit(1); 254 } 255 + 256 + sign_webfishing(); 257 }
+2
src/mods/mod.rs
···
··· 1 + mod structs; 2 + pub mod mods;
+154
src/mods/mods.rs
···
··· 1 + use std::fs; 2 + use std::fs::{read_dir, File}; 3 + use std::io::{Read, Write}; 4 + use std::path::{Path, PathBuf}; 5 + use godot_pck::structs::{PckFile, PCK}; 6 + use crate::mods::structs::{Manifest, Patch, PckInfo}; 7 + use crate::utils; 8 + 9 + fn read_manifests() -> Vec<Manifest> { 10 + let mut manifests = Vec::new(); 11 + let mods_dir = fs::read_dir("mods").expect("Cannot read mods directory"); 12 + for mod_dir in mods_dir { 13 + let mod_dir = mod_dir.expect("Failed to read mod directory"); 14 + if !mod_dir.path().is_dir() {continue} 15 + 16 + if !mod_dir.path().join("manifest.json").exists() { 17 + println!("No manifest found in {:?}", mod_dir.path()); 18 + continue; 19 + } 20 + 21 + let mut manifest: Manifest = serde_json::from_reader(fs::File::open(mod_dir.path().join("manifest.json")).unwrap()).unwrap(); 22 + manifest.path = mod_dir.path().display().to_string(); 23 + 24 + manifests.push(manifest); 25 + } 26 + manifests 27 + } 28 + 29 + fn check_mods(manifests: Vec<Manifest>) -> Vec<Manifest> { 30 + let mod_names: Vec<String> = manifests.iter().map(|m| m.get_name()).collect(); 31 + 32 + manifests.into_iter().filter(|manifest: &Manifest| { 33 + let mut res = true; 34 + println!("Checking {}", manifest.get_name()); 35 + for mod_name in manifest.get_dependencies() { 36 + if !mod_names.contains(&mod_name) { 37 + println!("\t- missing dependency {}", mod_name); 38 + res = false; 39 + } 40 + } 41 + res 42 + }).collect::<Vec<Manifest>>() 43 + } 44 + 45 + fn apply_patch(manifest: &Manifest, pck: &mut PCK, patch: &&Patch) { 46 + let resource_path = patch.get_resource(); 47 + let resource_path = resource_path.as_str(); 48 + let resource = pck.get_file_by_path_mut(resource_path).expect(&format!("Could not patch resource {}", resource_path)); 49 + let resource_name = resource_path.split("res://").last().expect("could not remove res://").split("/").last().expect("Could not get last part of resource"); 50 + if resource_path.ends_with(".gdc") { 51 + let mut resource_exported = File::create(format!("build/webfishing-export/{}", resource_name)).expect(&format!("Could not create mod resource {}", resource_name)); 52 + resource_exported.write_all(resource.get_content()).expect(&format!("Could not write mod resource {}", resource_name)); 53 + drop(resource_exported); 54 + utils::gd_utils::decomp_script(&format!("build/webfishing-export/{}", resource_name)) 55 + } else { 56 + let mut resource_exported = File::create(format!("build/webfishing-decomp/{}", resource_name)).expect(&format!("Could not create mod resource {}", resource_name)); 57 + resource_exported.write_all(resource.get_content()).expect(&format!("Could not write mod resource {}", resource_name)); 58 + drop(resource_exported); 59 + } 60 + 61 + let mut patch_file = File::open(format!("{}/{}", manifest.path, patch.get_patch_file())).expect(&format!("Could not open patch file {}", patch.get_patch_file())); 62 + let mut patch_file_content = String::new(); 63 + patch_file.read_to_string(&mut patch_file_content).expect(&format!("Could not read patch file {}", patch.get_patch_file())); 64 + drop(patch_file); 65 + 66 + let patch_struct = patch_apply::Patch::from_single(patch_file_content.as_str()).expect(&format!("Could not create patch {}", patch.get_patch_file())); 67 + 68 + let mut file_to_patch = File::open(format!("build/webfishing-decomp/{}", resource_name.replace(".gdc", ".gd"))).expect(&format!("Could not open file {}", resource_name)); 69 + let mut file_to_patch_content = String::new(); 70 + file_to_patch.read_to_string(&mut file_to_patch_content).expect(&format!("Could not read patch file {}", resource_name)); 71 + drop(file_to_patch); 72 + 73 + let patched_content = patch_apply::apply(file_to_patch_content, patch_struct); 74 + 75 + if resource_path.ends_with(".gdc") { 76 + let mut patched_file = File::create(format!("build/webfishing-decomp/{}", resource_name.replace(".gdc", ".gd"))).unwrap(); 77 + patched_file.write_all(patched_content.as_bytes()).unwrap(); 78 + drop(patched_file); 79 + 80 + utils::gd_utils::recomp_file(&format!("build/webfishing-decomp/{}", resource_name.replace(".gdc", ".gd"))); 81 + 82 + let mut recompiled_patched_file = File::open(format!("build/webfishing-recomp/{}", resource_name)).expect("Could not open recompiled patched file"); 83 + let mut recompiled_patched_content = Vec::new(); 84 + recompiled_patched_file.read_to_end(&mut recompiled_patched_content).expect(&format!("Could not read patched file {}", resource_name)); 85 + 86 + resource.set_content(recompiled_patched_content); 87 + } else { 88 + resource.set_content(Vec::from(patched_content.as_bytes())); 89 + } 90 + } 91 + 92 + fn add_recursive_files_to_pck(dir: String, pck: &mut PCK, pck_info: &PckInfo) { 93 + let directory = read_dir(Path::new(dir.as_str())).expect("read_dir failed"); 94 + for entry in directory { 95 + let entry = entry.expect("Failed to read entry"); 96 + if entry.path().is_file() { 97 + let path: PathBuf = entry.path().iter() 98 + .skip_while(|s| *s != pck_info.get_directory().as_str()) 99 + .skip(1) 100 + .collect(); 101 + 102 + let resource_path = format!("{}/{}", pck_info.get_resource_prefix(), path.display()).replace("//", "/").replace("res:/", "res://"); 103 + 104 + let mut resource_file = File::open(entry.path()).expect("Cannot open resource file"); 105 + let mut resource_file_content = Vec::new(); 106 + resource_file.read_to_end(&mut resource_file_content).expect("Cannot read resource file"); 107 + drop(resource_file); 108 + 109 + let pck_file = PckFile::new_file(resource_path, resource_file_content); 110 + pck.add_file(pck_file); 111 + } else { 112 + add_recursive_files_to_pck(entry.path().display().to_string(), pck, pck_info); 113 + } 114 + } 115 + } 116 + 117 + fn apply_mod(manifest: &Manifest, pck: &mut PCK) { 118 + println!("Applying {} by {}", manifest.get_name(), manifest.get_author()); 119 + // Apply game patches 120 + for patch in manifest.get_patches() { 121 + apply_patch(manifest, pck, &patch); 122 + }; 123 + 124 + // Add pck data 125 + match manifest.get_pck_info() { 126 + Some(pck_info) => { 127 + add_recursive_files_to_pck(format!("{}/{}",manifest.path,pck_info.get_directory()), pck, pck_info); 128 + } 129 + None => {} 130 + } 131 + } 132 + 133 + 134 + pub fn process_mods(pck: &mut PCK) { 135 + if !Path::exists("mods".as_ref()) { 136 + fs::create_dir(Path::new("mods")).expect("Failed to create mods directory"); 137 + } 138 + 139 + let manifests = read_manifests(); 140 + println!("Found {} mods", manifests.len()); 141 + 142 + // Dependency checking 143 + println!("Checking mod dependencies"); 144 + let mut checked_manifests = check_mods(manifests); 145 + while !checked_manifests.clone().into_iter().map(|x| x.get_name()).eq(check_mods(checked_manifests.clone()).into_iter().map(|x| x.get_name())) { 146 + checked_manifests = check_mods(checked_manifests); 147 + } 148 + 149 + println!("Checked all mod dependencies, loading {} mods", checked_manifests.len()); 150 + 151 + for manifest in checked_manifests { 152 + apply_mod(&manifest, pck) 153 + } 154 + }
+73
src/mods/structs.rs
···
··· 1 + use serde_derive::{Deserialize}; 2 + 3 + #[derive(Deserialize, Debug, Default, Clone)] 4 + pub struct Manifest { 5 + name: String, 6 + author: String, 7 + pck_info: Option<PckInfo>, 8 + patches: Vec<Patch>, 9 + deps: Option<Vec<String>>, 10 + 11 + // Reserved 12 + #[serde(skip_serializing)] 13 + #[serde(default)] 14 + pub path: String, 15 + } 16 + 17 + #[derive(Deserialize, Debug, Clone)] 18 + pub struct Patch { 19 + resource: String, 20 + patch_file: String, 21 + } 22 + 23 + #[derive(Deserialize, Debug, Default, Clone)] 24 + pub struct PckInfo { 25 + directory: String, 26 + resource_prefix: String 27 + } 28 + 29 + 30 + impl Manifest { 31 + pub fn get_dependencies(&self) -> Vec<String> { 32 + match self.deps { 33 + Some(ref deps) => deps.clone(), 34 + None => Vec::new() 35 + } 36 + } 37 + 38 + pub fn get_author(&self) -> String { 39 + self.author.clone() 40 + } 41 + 42 + pub fn get_name(&self) -> String { 43 + self.name.clone() 44 + } 45 + 46 + pub fn get_patches(&self) -> &Vec<Patch> { 47 + &self.patches 48 + } 49 + 50 + pub fn get_pck_info(&self) -> &Option<PckInfo> { 51 + &self.pck_info 52 + } 53 + } 54 + 55 + impl Patch { 56 + pub fn get_resource(&self) -> String { 57 + self.resource.clone() 58 + } 59 + 60 + pub fn get_patch_file(&self) -> String { 61 + self.patch_file.clone() 62 + } 63 + } 64 + 65 + impl PckInfo { 66 + pub fn get_directory(&self) -> String { 67 + self.directory.clone() 68 + } 69 + 70 + pub fn get_resource_prefix(&self) -> String { 71 + self.resource_prefix.clone() 72 + } 73 + }
+1 -1
src/patches/mod.rs
··· 1 pub mod steam_network_patch; 2 - pub mod options_menu_patch;
··· 1 pub mod steam_network_patch; 2 + pub mod options_menu_patch;
+2 -2
src/patches/options_menu_patch.rs
··· 8 const COMPILED_PATH: &str = "build/webfishing-recomp/options_menu.gdc"; 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"); 12 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); 19 20 let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 21 let mut script_txt = String::new();
··· 8 const COMPILED_PATH: &str = "build/webfishing-recomp/options_menu.gdc"; 9 pub(crate) async fn patch(pck: &mut PCK) { 10 println!("Patching {} files...", RESOURCE_PATH); 11 + let pck_file = pck.get_file_by_path_mut(RESOURCE_PATH).expect("Couldn't find options_menu.gdc file"); 12 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_script(FILE_PATH); 19 20 let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 21 let mut script_txt = String::new();
+3 -3
src/patches/steam_network_patch.rs
··· 10 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); 21 22 let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 23 let mut script_txt = String::new(); 24 script.read_to_string(&mut script_txt).await.expect("Cannot read script"); 25 drop(script); 26 27 - let patched_script = script_txt.replace("steam_id_remote", "remote_steam_id"); 28 let mut script = File::create(SCRIPT_PATH).await.expect("Cannot open script"); 29 script.write_all(patched_script.as_bytes()).await.expect("Cannot write"); 30 drop(script);
··· 10 11 pub(crate) async fn patch(pck: &mut PCK) { 12 println!("Patching {} files...", RESOURCE_PATH); 13 + let 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_script(FILE_PATH); 21 22 let mut script = File::open(SCRIPT_PATH).await.expect("Cannot open script"); 23 let mut script_txt = String::new(); 24 script.read_to_string(&mut script_txt).await.expect("Cannot read script"); 25 drop(script); 26 27 + let patched_script = script_txt.replace(".LOBBY_COMPARISON_EQUAL_TO_GREATER_THAN", ".OBBY_COMPARISON_EQUAL_TO_GREATER_THAN"); 28 let mut script = File::create(SCRIPT_PATH).await.expect("Cannot open script"); 29 script.write_all(patched_script.as_bytes()).await.expect("Cannot write"); 30 drop(script);
+1 -25
src/utils/gd_utils.rs
··· 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: &[T], from: &[T], to: &[T], replace_with: &mut [T]) -> Vec<T> 7 - where 8 - T: Clone + PartialEq + From<u8>, 9 - { 10 - let mut last_j = 0; 11 - let mut res : Vec<T> = Vec::new(); 12 - for i in 0..=buf.len() { 13 - if buf[i..].starts_with(from) { 14 - res.append(&mut buf[last_j..i].to_vec()); 15 - for j in (i + 1)..=buf.len() { 16 - if buf[j..].starts_with(to) { 17 - res.append(replace_with.to_vec().as_mut()); 18 - last_j = j; 19 - break; 20 - } 21 - } 22 - } 23 - } 24 - 25 - res.append(&mut buf[last_j..].to_vec()); 26 - res 27 - } 28 - 29 - pub(crate) fn decomp_file(path: &str) { 30 Command::new(RE_TOOLS) 31 .arg("--headless") 32 .arg(format!("--decompile=\"{}\"", path))
··· 2 3 const RE_TOOLS: &str = "build/Godot RE Tools.app/Contents/MacOS/Godot RE Tools"; 4 5 + pub(crate) fn decomp_script(path: &str) { 6 Command::new(RE_TOOLS) 7 .arg("--headless") 8 .arg(format!("--decompile=\"{}\"", path))