A native webfishing installer for macos

Compare changes

Choose any two refs to compare.

+3 -3
.gitignore
··· 1 - /target 2 - /build 3 - /.idea
··· 1 + **/target 2 + **/build 3 + **/.idea
+153 -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" ··· 174 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 175 176 [[package]] 177 name = "bitflags" 178 version = "1.3.2" 179 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 214 checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 215 216 [[package]] 217 name = "bytes" 218 version = "1.9.0" 219 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 235 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 236 237 [[package]] 238 name = "colored" 239 version = "2.2.0" 240 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 566 ] 567 568 [[package]] 569 name = "h2" 570 version = "0.4.7" 571 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 718 ] 719 720 [[package]] 721 name = "icu_collections" 722 version = "1.5.0" 723 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 963 ] 964 965 [[package]] 966 name = "memchr" 967 version = "2.7.4" 968 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 973 version = "0.3.17" 974 source = "registry+https://github.com/rust-lang/crates.io-index" 975 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 976 977 [[package]] 978 name = "miniz_oxide" ··· 1024 ] 1025 1026 [[package]] 1027 name = "ntapi" 1028 version = "0.4.1" 1029 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1033 ] 1034 1035 [[package]] 1036 name = "object" 1037 version = "0.36.7" 1038 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1118 "redox_syscall", 1119 "smallvec", 1120 "windows-targets 0.52.6", 1121 ] 1122 1123 [[package]] ··· 1424 1425 [[package]] 1426 name = "serde" 1427 - version = "1.0.216" 1428 source = "registry+https://github.com/rust-lang/crates.io-index" 1429 - checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 1430 dependencies = [ 1431 "serde_derive", 1432 ] 1433 1434 [[package]] 1435 name = "serde_derive" 1436 - version = "1.0.216" 1437 source = "registry+https://github.com/rust-lang/crates.io-index" 1438 - checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 1439 dependencies = [ 1440 "proc-macro2", 1441 "quote", ··· 1963 dependencies = [ 1964 "asky", 1965 "async-std", 1966 "reqwest", 1967 "steamlocate", 1968 "sudo", 1969 "sysinfo", ··· 1998 source = "registry+https://github.com/rust-lang/crates.io-index" 1999 checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 2000 dependencies = [ 2001 - "windows-core", 2002 "windows-targets 0.52.6", 2003 ] 2004
··· 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" ··· 189 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 190 191 [[package]] 192 + name = "binary-reader" 193 + version = "0.4.5" 194 + source = "registry+https://github.com/rust-lang/crates.io-index" 195 + checksum = "1d173c51941d642588ed6a13d464617e3a9176b8fe00dc2de182434c36812a5e" 196 + dependencies = [ 197 + "byteorder", 198 + ] 199 + 200 + [[package]] 201 name = "bitflags" 202 version = "1.3.2" 203 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" 250 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 251 + 252 + [[package]] 253 name = "bytes" 254 version = "1.9.0" 255 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" ··· 616 ] 617 618 [[package]] 619 + name = "godot_pck" 620 + version = "0.1.0" 621 + dependencies = [ 622 + "binary-reader", 623 + "md5", 624 + ] 625 + 626 + [[package]] 627 name = "h2" 628 version = "0.4.7" 629 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" ··· 1044 ] 1045 1046 [[package]] 1047 + name = "md5" 1048 + version = "0.7.0" 1049 + source = "registry+https://github.com/rust-lang/crates.io-index" 1050 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 1051 + 1052 + [[package]] 1053 name = "memchr" 1054 version = "2.7.4" 1055 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" ··· 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]] 1159 name = "object" 1160 version = "0.36.7" 1161 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 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", ··· 2097 dependencies = [ 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
+8 -1
Cargo.toml
··· 10 sysinfo = "0.33.0" 11 async-std = "1.13.0" 12 sudo = "0.6.0" 13 - asky = "0.1.1"
··· 10 sysinfo = "0.33.0" 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"}
+42 -1
README.md
··· 23 ## Implemented Patches 24 25 - renaming `steam_id_remote` dictionnary key to `remote_steam_id` to fix network spam detection that resulted in timeouts 26 27 ## Credits 28 29 - [@vimaexd](https://github.com/vimaexd) for their blog post !
··· 23 ## Implemented Patches 24 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 ! 69 + 70 + [Godot RE Tools](https://github.com/GDRETools/gdsdecomp) for the amazing tooling that allows me to patch the game !
+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 +
+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;
+159
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 + content: Vec<u8>, 16 + md5: [u8; 16], 17 + } 18 + 19 + impl PCK { 20 + /// Converts pck file bytes to a PCK struct 21 + pub fn from_bytes(bytes: &[u8]) -> Result<PCK, &str> { 22 + let chunked_start = bytes.chunks(16).position(|chunk| chunk.starts_with("GDPC".as_ref())); 23 + if let None = chunked_start { 24 + return Err("Invalid PCK"); 25 + }; 26 + 27 + let start_offset = chunked_start.unwrap() * 16; 28 + let pck: Vec<u8> = bytes.to_vec().into_iter().skip(start_offset).collect(); 29 + let mut pck_reader = BinaryReader::from_vec(&pck); 30 + pck_reader.set_endian(Endian::Little); 31 + 32 + let _magic = pck_reader.read_u32().unwrap(); 33 + let version = pck_reader.read_u32().unwrap(); 34 + let major_version = pck_reader.read_u32().unwrap(); 35 + let minor_version = pck_reader.read_u32().unwrap(); 36 + let patch_version = pck_reader.read_u32().unwrap(); 37 + 38 + // Skip unused 39 + let _ = pck_reader.read(64); 40 + 41 + let num_files = pck_reader.read_u32().unwrap(); 42 + let mut files = vec![]; 43 + for _ in 0..num_files { 44 + files.push(PckFile::from_bytes(&mut pck_reader, bytes)); 45 + } 46 + 47 + Ok(PCK { 48 + version: version as u8, 49 + major_version: major_version as u8, 50 + minor_version: minor_version as u8, 51 + patch_version: patch_version as u8, 52 + files 53 + }) 54 + } 55 + 56 + /// Converts a PCK struct to the byte vector representing a pck file 57 + pub fn to_bytes(&self) -> Vec<u8> { 58 + let mut bytes = vec![]; 59 + bytes.append(&mut "GDPC".as_bytes().to_vec()); 60 + bytes.append((self.version as u32).to_le_bytes().to_vec().as_mut()); 61 + bytes.append((self.major_version as u32).to_le_bytes().to_vec().as_mut()); 62 + bytes.append((self.minor_version as u32).to_le_bytes().to_vec().as_mut()); 63 + bytes.append((self.patch_version as u32).to_le_bytes().to_vec().as_mut()); 64 + bytes.append([0u8; 16*4].to_vec().as_mut()); 65 + bytes.append((self.files.len() as u32).to_le_bytes().to_vec().as_mut()); 66 + 67 + let mut file_offset = bytes.len() 68 + + self.files.len() * (4 + 8 + 8 + 16) 69 + + self.files.iter().map(|x| x.path.len() + (4 - (x.path.len() % 4))).sum::<usize>(); 70 + 71 + if file_offset % 16 != 0 { 72 + file_offset = file_offset + (16 - (file_offset % 16)); 73 + } 74 + 75 + let file_base_offset = file_offset; 76 + 77 + let mut content : Vec<u8> = vec![]; 78 + 79 + for file in &self.files { 80 + content.extend(&file.content); 81 + 82 + let mut padded_content_len = file.content.len(); 83 + if padded_content_len % 16 != 0 { 84 + padded_content_len = padded_content_len + (16 - (padded_content_len % 16)); 85 + } 86 + 87 + for _ in file.content.len()..padded_content_len { 88 + content.push(0); 89 + } 90 + 91 + let padded_length = file.path.len() + (4 - (file.path.len() % 4)); 92 + 93 + bytes.append((padded_length as u32).to_le_bytes().to_vec().as_mut()); 94 + bytes.append(file.path.as_bytes().to_vec().as_mut()); 95 + for _ in file.path.len()..padded_length { 96 + bytes.push(0); 97 + } 98 + bytes.append((file_offset as u64).to_le_bytes().to_vec().as_mut()); 99 + bytes.append((padded_content_len as u64).to_le_bytes().to_vec().as_mut()); 100 + bytes.extend(file.md5); 101 + 102 + file_offset += padded_content_len; 103 + } 104 + 105 + for _ in bytes.len()..file_base_offset { 106 + bytes.push(0); 107 + } 108 + 109 + bytes.extend(content); 110 + bytes 111 + } 112 + 113 + pub fn get_file_by_path(&self, path: &str) -> Option<&PckFile> { 114 + self.files.iter().find(|x| x.path == path) 115 + } 116 + 117 + pub fn get_file_by_path_mut(&mut self, path: &str) -> Option<&mut PckFile> { 118 + self.files.iter_mut().find(|x| x.path == path) 119 + } 120 + 121 + pub fn add_file(&mut self, new_file: PckFile) { 122 + self.files.retain(|x| x.path != new_file.path); 123 + self.files.push(new_file); 124 + } 125 + } 126 + 127 + impl PckFile { 128 + pub fn from_bytes(pck_reader: &mut BinaryReader, file_bytes: &[u8]) -> PckFile { 129 + let path_length = pck_reader.read_u32().unwrap(); 130 + let path_bytes= pck_reader.read(path_length as usize).unwrap(); 131 + let path: String = String::from_utf8_lossy(path_bytes).replace("\0", "").to_string(); 132 + let offset = pck_reader.read_u64().unwrap(); 133 + let size = pck_reader.read_u64().unwrap(); 134 + let md5 = pck_reader.read(16).unwrap(); 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 + 151 + pub fn get_content(&self) -> &[u8] { 152 + &self.content 153 + } 154 + 155 + pub fn set_content(&mut self, content: Vec<u8>) { 156 + self.md5 = *md5::compute(&content); 157 + self.content = content; 158 + } 159 + }
+140 -81
src/main.rs
··· 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; 9 - use asky::Confirm; 10 - use async_std::fs::create_dir; 11 use steamlocate::SteamDir; 12 use sudo::RunningAs; 13 use sysinfo::ProcessesToUpdate; 14 15 static WEBFISHING_APPID: u32 = 3146520; 16 17 - 18 - 19 async fn install_webfishing(location: &SteamDir) { 20 let steam_location = location.path(); 21 - let acf_path = steam_location.join("steamapps").join(format!("appmanifest_{}.acf", WEBFISHING_APPID)); 22 23 println!("Creating Webfishing ACF"); 24 - File::create(acf_path).unwrap().write(include_str!("../res/webfishing.acf").as_bytes()).expect("could not write acf"); 25 26 println!("Waiting for steam to close"); 27 let mut system = sysinfo::System::new_all(); ··· 39 } 40 41 println!("Steam launched, downloading webfishing"); 42 - let download_path = steam_location.join("steamapps").join("downloading").join(format!("{}", WEBFISHING_APPID)); 43 44 while Path::exists(download_path.as_path()) { 45 println!("Downloading webfishing..."); ··· 49 50 async fn download_godot_steam_template() { 51 println!("Downloading GodotSteam template..."); 52 - 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"); 53 let body = res.bytes().await.expect("Could not read body"); 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() { ··· 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) { 75 let template_path = Path::new("build/osx_template.app"); 76 Command::new("rm") 77 .current_dir(template_path) 78 - .arg("Contents/MacOS/godot_osx_debug.64") 79 - .output().expect("Could not remove delete godot_osx_debug.64"); 80 81 Command::new("mv") 82 .current_dir(template_path) 83 - .arg("Contents/MacOS/godot_osx_release.64") 84 .arg("Contents/MacOS/webfishing") 85 - .output().expect("Could not rename godot_osc_release.64"); 86 87 - let mut steamappid = File::create(template_path.join("Contents").join("MacOS").join("steam_appid.txt")).expect("could not create steam_appid.txt file"); 88 - steamappid.write(include_str!("../res/steam_appid.txt").as_bytes()).expect("could not write steam_appid.txt"); 89 90 Command::new("cp") 91 .arg(webfishing_path.join("webfishing.exe")) 92 - .arg(template_path.join("Contents").join("Resources").join("webfishing.pck")) 93 - .output().expect("Could not copy webfishing.exe"); 94 95 - let mut info_plist = File::create(template_path.join("Contents").join("Info.plist")).expect("Could not open Info.plist"); 96 - info_plist.write_all(include_str!("../res/Info.plist").as_bytes()).expect("could not write Info.plist"); 97 98 Command::new("mv") 99 .arg(template_path) 100 .arg(Path::new("build/webfishing.app")) 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] 117 async fn main() { 118 if !Path::exists("build".as_ref()) { 119 println!("Creating build folder"); 120 - create_dir("build").await.expect("could not create build folder"); 121 } 122 123 let location = SteamDir::locate().expect("could not locate steam directory"); ··· 128 install_webfishing(&location).await; 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; 139 } 140 141 - if !Path::exists("build/macos.zip".as_ref()) { 142 - println!("Unzipping template"); 143 - Command::new("unzip") 144 - .arg("-o") 145 - .arg("godot_steam_template.zip") 146 - .current_dir("./build") 147 - .output().expect("Could not unzip godot_steam_template.zip"); 148 - } 149 150 - if !Path::exists("build/osx_template.app".as_ref()) && !Path::exists("build/webfishing.app".as_ref()) { 151 - println!("Unzipping template"); 152 - Command::new("unzip") 153 - .arg("-o") 154 - .arg("macos.zip") 155 - .current_dir("./build") 156 - .output() 157 - .expect("Could not unzip macos.zip"); 158 - } 159 160 161 let binding = library.resolve_app_dir(&app); 162 let webfishing_path = binding.as_path(); ··· 164 build_webfishing_macos(webfishing_path); 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 174 - sudo::escalate_if_needed().expect("Could not escalate to sign the app"); 175 176 - Command::new("xattr") 177 - .arg("-cr") 178 - .arg("build/webfishing.app") 179 - .output() 180 - .expect("Could not execute xattr"); 181 182 - if Confirm::new("Do you wanna install Webfishing in the app folder?").prompt().expect("Could not confirm to install the webfishing") { 183 - Command::new("rsync") 184 - .arg("-a") 185 - .arg("build/webfishing.app") 186 - .current_dir("/Applications/") 187 - .output().expect("Could not execute rsync"); 188 189 - Command::new("rm") 190 - .arg("-r") 191 - .arg("build/webfishing.app") 192 - .output().expect("Could not remove webfishing.app"); 193 194 - println!("Successfully installed webfishing !"); 195 - } else { 196 - println!("Webfishing is in the build folder !") 197 - } 198 }
··· 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;
+17 -45
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", "OS.window_borderless\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 - 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); 46 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"); 65 }
··· 1 use async_std::fs::File; 2 use async_std::io::{ReadExt, WriteExt}; 3 + use godot_pck::structs::PCK; 4 5 + const RESOURCE_PATH: &str = "res://Scenes/Singletons/OptionsMenu/options_menu.gdc"; 6 + const FILE_PATH: &str = "build/webfishing-export/options_menu.gdc"; 7 const SCRIPT_PATH: &str = "build/webfishing-decomp/options_menu.gd"; 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(); 22 script.read_to_string(&mut script_txt).await.expect("Cannot read script"); 23 drop(script); 24 25 + let patched_script = script_txt.replace("OS.window_borderless = PlayerData.player_options.fullscreen == 1", ""); 26 let mut script = File::create(SCRIPT_PATH).await.expect("Cannot open script"); 27 script.write_all(patched_script.as_bytes()).await.expect("Cannot write"); 28 drop(script); 29 30 crate::utils::gd_utils::recomp_file(SCRIPT_PATH); 31 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(); 35 36 + pck_file.set_content(new_content); 37 }
+19 -22
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 }
··· 1 + use godot_pck::structs::PckFile; 2 use async_std::fs::File; 3 use async_std::io::{ReadExt, WriteExt}; 4 + use godot_pck::structs::PCK; 5 6 + const RESOURCE_PATH: &str = "res://Scenes/Singletons/SteamNetwork.gdc"; 7 + const FILE_PATH: &str = "build/webfishing-export/SteamNetwork.gdc"; 8 const SCRIPT_PATH: &str = "build/webfishing-decomp/SteamNetwork.gd"; 9 const COMPILED_PATH: &str = "build/webfishing-recomp/SteamNetwork.gdc"; 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); 31 32 crate::utils::gd_utils::recomp_file(SCRIPT_PATH); 33 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(); 37 38 + pck_file.set_content(new_content); 39 }
+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: &mut [T], from: &[T], to: &[T], replace_with: &[T]) 7 - where 8 - T: Clone + PartialEq + From<u8>, 9 - { 10 - for mut 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))
··· 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))