A native webfishing installer for macos

1.3.0 - Modding implemented

+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"}
+27
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 + ## How to make a mod? 29 + 30 + 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: 31 + ```json 32 + { 33 + "name": "Ship Mod", // Mod name 34 + "author": "Estym", // Author 35 + 36 + "pck_info": { // (Optional) 37 + "directory": "pck", // Relative folder path to where the mod resources are 38 + "resource_prefix": "res://Mods/Ship/" // Resource path prefix for the mod resources 39 + }, 40 + 41 + "mod_patches": [ // Array of patches 42 + { 43 + "resource": "res://Scenes/Entities/Player/player.gdc", // Resource to patch 44 + "patch_file": "patch/player.patch" // relative file path to the patch file 45 + } 46 + ], 47 + 48 + "deps": [] // Dependencies for the mod (Optional) 49 + } 50 + ``` 51 + 52 + ### Notes: 53 + - The patch files are made by using `$ git diff [original_file] [modded_file] > file.patch` 54 + 55 ## Credits 56 57 [@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 + "mod_patches": [ 9 + { 10 + "resource": "res://Scenes/Entities/Player/player.gdc", 11 + "patch_file": "patch/player.patch" 12 + } 13 + ], 14 + "deps": [] 15 + }
+58
example_mods/ship/patch/player.patch
···
··· 1 + diff --git a/webfishing/Scenes/Entities/Player/player.gd b/Users/evann/Godot/Webfishing/Scenes/Entities/Player/player.gd 2 + index 3879ff8..cc20048 100644 3 + --- a/webfishing/Scenes/Entities/Player/player.gd 4 + +++ b/Users/evann/Godot/Webfishing/Scenes/Entities/Player/player.gd 5 + @@ -30,6 +30,7 @@ export var NPC_cosmetics = {"species": "species_cat", "pattern": "pattern_none" 6 + export var NPC_name = "NPC Test" 7 + export var NPC_title = "npc title here" 8 + 9 + +var ship_mod_instance = preload("res://Mods/Ship/ship.gd").new() 10 + var camera_zoom = 5.0 11 + 12 + var direction = Vector3() 13 + @@ -341,6 +342,7 @@ func _process(delta): 14 + 15 + func _controlled_process(delta): 16 + _get_input() 17 + + 18 + _process_movement(delta) 19 + _process_timers() 20 + _interact_check() 21 + @@ -480,7 +482,7 @@ func _process_timers(): 22 + 23 + func _get_input(): 24 + direction = Vector3.ZERO 25 + - 26 + + 27 + if Input.is_action_just_released("primary_action"): _primary_action_release() 28 + if Input.is_action_pressed("primary_action"): _primary_action_hold() 29 + else: primary_hold_timer = 0 30 + @@ -538,8 +540,10 @@ func _get_input(): 31 + 32 + mouse_look = false 33 + 34 + + if ship_mod_instance.is_sitting_on_ship(self): 35 + + ship_mod_instance.process_ship(self, get_world()) 36 + + return 37 + if sitting: return 38 + - 39 + if Input.is_action_pressed("move_forward"): direction -= cam_base.transform.basis.z 40 + if Input.is_action_pressed("move_back"): direction += cam_base.transform.basis.z 41 + if Input.is_action_pressed("move_right"): direction += cam_base.transform.basis.x 42 + @@ -1405,16 +1409,6 @@ func _create_prop(ref, offset = Vector3(0, 1, 0), restrict_to_one = false): 43 + return false 44 + 45 + 46 + - if $detection_zones / prop_detect.get_overlapping_bodies().size() > 0 or not is_on_floor() or not $detection_zones / prop_ray.is_colliding(): 47 + - PlayerData._send_notification("invalid prop placement", 1) 48 + - return false 49 + - 50 + - 51 + - if prop_ids.size() > 4: 52 + - PlayerData._send_notification("prop limit reached", 1) 53 + - return false 54 + - 55 + - 56 + var item = PlayerData._find_item_code(ref) 57 + var data = Globals.item_data[item["id"]]["file"] 58 + 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 +
+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
+7 -18
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; ··· 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(); ··· 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"); ··· 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(); ··· 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 utils; 2 mod patches; 3 + mod mods; 4 5 + use std::env::{current_exe, set_current_dir}; 6 use std::fs::File; 7 use std::io::{Read, Write}; 8 use std::path::Path; 9 use std::process::Command; 10 use std::time::Duration; 11 + use asky::Text; 12 use async_std::fs::create_dir; 13 use steamlocate::SteamDir; 14 use sudo::RunningAs; ··· 16 use godot_pck::structs::PCK; 17 18 static WEBFISHING_APPID: u32 = 3146520; 19 20 async fn install_webfishing(location: &SteamDir) { 21 let steam_location = location.path(); ··· 104 105 #[tokio::main] 106 async fn main() { 107 + set_current_dir(current_exe().unwrap().parent().expect("Could not get current dir")).expect("Could not set current dir"); 108 if !Path::exists("build".as_ref()) { 109 println!("Creating build folder"); 110 create_dir("build").await.expect("could not create build folder"); ··· 162 163 patches::steam_network_patch::patch(&mut pck).await; 164 patches::options_menu_patch::patch(&mut pck).await; 165 + mods::mods::process_mods(&mut pck); 166 println!("Root permissions needed to sign webfishing"); 167 168 let bytes = &pck.to_bytes(); ··· 177 .output() 178 .expect("Could not execute xattr"); 179 180 + println!("Webfishing is in the build folder !"); 181 182 + Text::new("Press Enter to quit").prompt().expect("Could not confirm to quit"); 183 }
+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();
+2 -2
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();
··· 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();
+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))