+123
-5
Cargo.lock
+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
+7
-1
Cargo.toml
+38
README.md
+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
+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
+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
+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
+7
modlist.md
+8
-2
src/godot_pck/src/structs.rs
+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
+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
}
+154
src/mods/mods.rs
+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
+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
-1
src/patches/mod.rs
+3
-3
src/patches/steam_network_patch.rs
+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
+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))