diff --git a/Cargo.lock b/Cargo.lock
index 2c0f2804..9ad8fe08 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -33,7 +33,7 @@ dependencies = [
"bitflags 2.9.1",
"bytes",
"bytestring",
- "derive_more",
+ "derive_more 2.0.1",
"encoding_rs",
"foldhash",
"futures-core",
@@ -131,7 +131,7 @@ dependencies = [
"bytes",
"bytestring",
"cfg-if",
- "derive_more",
+ "derive_more 2.0.1",
"encoding_rs",
"foldhash",
"futures-core",
@@ -264,6 +264,49 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "assert_type_match"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -297,6 +340,178 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+[[package]]
+name = "bevy_app"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4491cc4c718ae76b4c6883df58b94cc88b32dcd894ea8d5b603c7c7da72ca967"
+dependencies = [
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_platform",
+ "bevy_reflect",
+ "bevy_tasks",
+ "bevy_utils",
+ "cfg-if",
+ "ctrlc",
+ "downcast-rs",
+ "log",
+ "thiserror 2.0.16",
+ "variadics_please",
+]
+
+[[package]]
+name = "bevy_derive"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b837bf6c51806b10ebfa9edf1844ad80a3a0760d6c5fac4e90761df91a8901a"
+dependencies = [
+ "bevy_macro_utils",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "bevy_ecs"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c2bf6521aae57a0ec3487c4bfb59e36c4a378e834b626a4bea6a885af2fdfe7"
+dependencies = [
+ "arrayvec",
+ "bevy_ecs_macros",
+ "bevy_platform",
+ "bevy_ptr",
+ "bevy_reflect",
+ "bevy_tasks",
+ "bevy_utils",
+ "bitflags 2.9.1",
+ "bumpalo",
+ "concurrent-queue",
+ "derive_more 1.0.0",
+ "disqualified",
+ "fixedbitset",
+ "indexmap",
+ "log",
+ "nonmax",
+ "serde",
+ "smallvec",
+ "thiserror 2.0.16",
+ "variadics_please",
+]
+
+[[package]]
+name = "bevy_ecs_macros"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38748d6f3339175c582d751f410fb60a93baf2286c3deb7efebb0878dce7f413"
+dependencies = [
+ "bevy_macro_utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "bevy_macro_utils"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052eeebcb8e7e072beea5031b227d9a290f8a7fbbb947573ab6ec81df0fb94be"
+dependencies = [
+ "parking_lot",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "toml_edit",
+]
+
+[[package]]
+name = "bevy_platform"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7573dc824a1b08b4c93fdbe421c53e1e8188e9ca1dd74a414455fe571facb47"
+dependencies = [
+ "cfg-if",
+ "critical-section",
+ "foldhash",
+ "hashbrown",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+ "spin 0.9.8",
+]
+
+[[package]]
+name = "bevy_ptr"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df7370d0e46b60e071917711d0860721f5347bc958bf325975ae6913a5dfcf01"
+
+[[package]]
+name = "bevy_reflect"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daeb91a63a1a4df00aa58da8cc4ddbd4b9f16ab8bb647c5553eb156ce36fa8c2"
+dependencies = [
+ "assert_type_match",
+ "bevy_platform",
+ "bevy_ptr",
+ "bevy_reflect_derive",
+ "bevy_utils",
+ "derive_more 1.0.0",
+ "disqualified",
+ "downcast-rs",
+ "erased-serde",
+ "foldhash",
+ "glam",
+ "serde",
+ "smallvec",
+ "smol_str",
+ "thiserror 2.0.16",
+ "uuid",
+ "variadics_please",
+ "wgpu-types",
+]
+
+[[package]]
+name = "bevy_reflect_derive"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ddadc55fe16b45faaa54ab2f9cb00548013c74812e8b018aa172387103cce6"
+dependencies = [
+ "bevy_macro_utils",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "uuid",
+]
+
+[[package]]
+name = "bevy_tasks"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b674242641cab680688fc3b850243b351c1af49d4f3417a576debd6cca8dcf5"
+dependencies = [
+ "async-executor",
+ "async-task",
+ "atomic-waker",
+ "bevy_platform",
+ "cfg-if",
+ "crossbeam-queue",
+ "derive_more 1.0.0",
+ "futures-lite",
+ "heapless",
+]
+
+[[package]]
+name = "bevy_utils"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f7a8905a125d2017e8561beefb7f2f5e67e93ff6324f072ad87c5fd6ec3b99"
+dependencies = [
+ "bevy_platform",
+ "thread_local",
+]
+
[[package]]
name = "bincode"
version = "2.0.1"
@@ -534,6 +749,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+ "portable-atomic",
+]
+
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -609,6 +834,12 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -637,6 +868,15 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -722,13 +962,34 @@ dependencies = [
"powerfmt",
]
+[[package]]
+name = "derive_more"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
+dependencies = [
+ "derive_more-impl 1.0.0",
+]
+
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
- "derive_more-impl",
+ "derive_more-impl 2.0.1",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "unicode-xid",
]
[[package]]
@@ -804,6 +1065,12 @@ dependencies = [
"syn 2.0.104",
]
+[[package]]
+name = "disqualified"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd"
+
[[package]]
name = "dissimilar"
version = "1.0.10"
@@ -837,6 +1104,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+[[package]]
+name = "downcast-rs"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf"
+
[[package]]
name = "dyn-clone"
version = "1.0.19"
@@ -864,6 +1137,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "erased-serde"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
+dependencies = [
+ "serde",
+ "typeid",
+]
+
[[package]]
name = "errno"
version = "0.3.13"
@@ -927,6 +1210,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
[[package]]
name = "flate2"
version = "1.1.2"
@@ -981,6 +1270,25 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -1053,6 +1361,15 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+[[package]]
+name = "glam"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "globset"
version = "0.4.16"
@@ -1088,11 +1405,35 @@ dependencies = [
"scroll",
]
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
[[package]]
name = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+dependencies = [
+ "equivalent",
+ "serde",
+]
+
+[[package]]
+name = "heapless"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "portable-atomic",
+ "stable_deref_trait",
+]
[[package]]
name = "heck"
@@ -1679,6 +2020,9 @@ dependencies = [
name = "me3-mod-host"
version = "0.7.0"
dependencies = [
+ "bevy_app",
+ "bevy_derive",
+ "bevy_ecs",
"closure-ffi",
"color-eyre",
"crash-handler",
@@ -2108,6 +2452,12 @@ version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
version = "0.12.4"
@@ -2285,6 +2635,21 @@ dependencies = [
"time",
]
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "potential_utf"
version = "0.1.2"
@@ -3080,6 +3445,15 @@ dependencies = [
"syn 2.0.104",
]
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "socket2"
version = "0.5.10"
@@ -3095,6 +3469,9 @@ name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "portable-atomic",
+]
[[package]]
name = "spin"
@@ -3504,6 +3881,12 @@ dependencies = [
"tracing-log",
]
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
[[package]]
name = "typenum"
version = "1.18.0"
@@ -3644,6 +4027,7 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
+ "getrandom 0.3.3",
"js-sys",
"serde",
"wasm-bindgen",
@@ -3655,6 +4039,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+[[package]]
+name = "variadics_please"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -3761,6 +4156,16 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "webpki-roots"
version = "1.0.2"
@@ -3770,6 +4175,19 @@ dependencies = [
"rustls-pki-types",
]
+[[package]]
+name = "wgpu-types"
+version = "24.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c"
+dependencies = [
+ "bitflags 2.9.1",
+ "js-sys",
+ "log",
+ "serde",
+ "web-sys",
+]
+
[[package]]
name = "widestring"
version = "1.2.0"
diff --git a/crates/mod-host/Cargo.toml b/crates/mod-host/Cargo.toml
index 9fc2fc4d..20a2a244 100644
--- a/crates/mod-host/Cargo.toml
+++ b/crates/mod-host/Cargo.toml
@@ -56,6 +56,9 @@ windows = { workspace = true, features = [
"Win32_UI_WindowsAndMessaging",
] }
xxhash-rust = { version = "0.8", features = ["std", "xxh3"] }
+bevy_ecs = "0.16.1"
+bevy_app = "0.16.1"
+bevy_derive = "0.16.1"
[build-dependencies]
winresource = "0.1"
diff --git a/crates/mod-host/src/app.rs b/crates/mod-host/src/app.rs
new file mode 100644
index 00000000..70a44f89
--- /dev/null
+++ b/crates/mod-host/src/app.rs
@@ -0,0 +1,91 @@
+use std::sync::{Arc, RwLock};
+
+use bevy_derive::{Deref, DerefMut};
+use bevy_ecs::{
+ resource::Resource,
+ schedule::{ExecutorKind, IntoScheduleConfigs, Schedule, ScheduleLabel},
+ system::{Res, ResMut, ScheduleSystem},
+ world::World,
+};
+
+use crate::plugins::Plugin;
+
+#[derive(Deref, DerefMut)]
+pub struct Me3App {
+ world: World,
+}
+
+/// The `PreStartup` schedule is the first schedule to be executed during initialization. Prefer
+/// `[Startup]` unless you specifically need your code to run before any other me3 patches.
+#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct PreStartup;
+
+/// The `Startup` schedule executes before execution is passed to the game, but after
+/// `[PreStartup]`.
+#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct Startup;
+
+/// The `PostStartup` schedule executes after the game has executed its own `WinMain` startup
+/// routine and initialized Steam (if applicable).
+#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct PostStartup;
+
+/// The `Update` schedule is ran on every frame of the game.
+#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct Update;
+
+impl Default for Me3App {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Me3App {
+ pub fn new() -> Self {
+ let mut world = World::new();
+ let mut post_startup_schedule = Schedule::new(PostStartup);
+ post_startup_schedule.set_executor_kind(ExecutorKind::SingleThreaded);
+
+ let mut startup_schedule = Schedule::new(Startup);
+ startup_schedule.set_executor_kind(ExecutorKind::MultiThreaded);
+
+ let mut pre_startup_schedule = Schedule::new(PreStartup);
+ pre_startup_schedule.set_executor_kind(ExecutorKind::SingleThreaded);
+
+ world.add_schedule(pre_startup_schedule);
+ world.add_schedule(startup_schedule);
+ world.add_schedule(post_startup_schedule);
+
+ Self { world }
+ }
+
+ pub fn register_plugin
(&mut self, plugin: P)
+ where
+ P: Plugin,
+ {
+ plugin.build(self)
+ }
+
+ pub fn register_system(
+ &mut self,
+ schedule: impl ScheduleLabel,
+ systems: impl IntoScheduleConfigs,
+ ) {
+ self.world.schedule_scope(schedule, |_, sched| {
+ sched.add_systems(systems);
+ });
+ }
+
+ pub fn run_schedule(&mut self, label: impl ScheduleLabel) {
+ self.world.run_schedule(label);
+ }
+}
+
+#[derive(Deref, DerefMut, Resource)]
+pub struct ExternalResource(pub T);
+pub type ExternalRes<'w, T> = Res<'w, ExternalResource>;
+pub type ExternalResMut<'w, T> = ResMut<'w, ExternalResource>;
+
+#[derive(Deref, DerefMut, Resource)]
+pub struct SharedResource(pub Arc>);
+pub type SharedRes<'w, T> = Res<'w, SharedResource>;
diff --git a/crates/mod-host/src/asset_hooks.rs b/crates/mod-host/src/asset_hooks.rs
index 3b3b6302..4f289d52 100644
--- a/crates/mod-host/src/asset_hooks.rs
+++ b/crates/mod-host/src/asset_hooks.rs
@@ -29,7 +29,7 @@ use tracing::{debug, error, info, info_span, instrument, warn};
use windows::core::{PCSTR, PCWSTR};
use xxhash_rust::xxh3;
-use crate::{executable::Executable, host::ModHost};
+use crate::{executable::Executable, hook, host::ModHost};
static VFS_MOUNTS: Mutex = Mutex::new(VfsMounts::new());
@@ -130,9 +130,9 @@ fn hook_ebl_utility(
debug!(?make_ebl_object);
- ModHost::get_attached()
- .hook(make_ebl_object)
- .with_closure(move |p1, path, p3, trampoline| {
+ hook!(
+ pointer = make_ebl_object,
+ move |p1, path, p3, trampoline| {
let mut device_manager = DlDeviceManager::lock(device_manager);
let expanded = unsafe { device_manager.expand_path(path.as_wide()) };
@@ -147,8 +147,8 @@ fn hook_ebl_utility(
let _guard = device_manager.push_vfs_mounts(&VFS_MOUNTS.lock().unwrap());
unsafe { (trampoline)(p1, path, p3) }
- })
- .install()?;
+ }
+ )?;
info!("applied asset override hook");
@@ -189,10 +189,9 @@ fn hook_device_manager(
.is_ok()
};
- ModHost::get_attached()
- .hook(open_disk_file)
- .with_span(info_span!("hook"))
- .with_closure(move |p1, path, p3, p4, p5, p6, trampoline| {
+ hook!(
+ pointer = open_disk_file,
+ move |p1, path, p3, p4, p5, p6, trampoline| {
let file_operator = if let Some(path) = override_path(unsafe { path.as_ref() }) {
unsafe {
trampoline(
@@ -222,8 +221,8 @@ fn hook_device_manager(
.unwrap()
.try_open_file(path, p3, p4, p5, p6)
}
- })
- .install()?;
+ }
+ )?;
info!("applied asset override hook");
diff --git a/crates/mod-host/src/host.rs b/crates/mod-host/src/host.rs
index 9a0cfaf9..50b622d2 100644
--- a/crates/mod-host/src/host.rs
+++ b/crates/mod-host/src/host.rs
@@ -1,38 +1,34 @@
use std::{
collections::HashMap,
- ffi::CString,
fmt::Debug,
marker::Tuple,
- panic,
- path::Path,
sync::{Arc, Mutex, OnceLock},
- time::Duration,
};
+use bevy_ecs::{system::Res, world::Mut};
use closure_ffi::traits::FnPtr;
-use libloading::{Library, Symbol};
use me3_launcher_attach_protocol::AttachConfig;
-use me3_mod_protocol::{native::NativeInitializerCondition, Game, ModProfile};
+use me3_mod_protocol::Game;
use retour::Function;
-use tracing::{error, info, warn, Span};
+use tracing::{info, Span};
use self::hook::HookInstaller;
use crate::{
+ app::{ExternalResource, Me3App},
detour::UntypedDetour,
- native::{ModEngineConnectorShim, ModEngineExtension, ModEngineInitializer},
+ plugins::properties::GameProperties,
};
mod append;
-pub mod game_properties;
+
+#[macro_use]
pub mod hook;
static ATTACHED_INSTANCE: OnceLock = OnceLock::new();
-#[derive(Default)]
pub struct ModHost {
+ pub(crate) app: Mutex,
hooks: Mutex>>,
- native_modules: Mutex>,
- profiles: Vec,
property_overrides: Mutex, bool>>,
}
@@ -40,7 +36,6 @@ impl Debug for ModHost {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModHost")
.field("hooks", &self.hooks)
- .field("profiles", &self.profiles)
.field("property_overrides", &self.property_overrides)
.finish()
}
@@ -48,58 +43,19 @@ impl Debug for ModHost {
#[allow(unused)]
impl ModHost {
- pub fn new() -> Self {
- Self::default()
+ pub fn new(app: Me3App) -> Self {
+ Self {
+ app: Mutex::new(app),
+ hooks: Default::default(),
+ property_overrides: Default::default(),
+ }
}
- pub fn load_native(
- &self,
- path: &Path,
- condition: &Option,
- ) -> eyre::Result<()> {
- let result = panic::catch_unwind(|| {
- let module = unsafe { libloading::Library::new(path)? };
-
- match &condition {
- Some(NativeInitializerCondition::Delay { ms }) => {
- std::thread::sleep(Duration::from_millis(*ms as u64))
- }
- Some(NativeInitializerCondition::Function(symbol)) => unsafe {
- let sym_name = CString::new(symbol.as_bytes())?;
- let initializer: Symbol bool> =
- module.get(sym_name.as_bytes_with_nul())?;
-
- if initializer() {
- info!(?path, symbol, "native initialized successfully");
- } else {
- error!(?path, symbol, "native failed to initialize");
- }
- },
- None => {
- let me2_initializer: Option> =
- unsafe { module.get(b"modengine_ext_init\0").ok() };
-
- let mut extension_ptr: *mut ModEngineExtension = std::ptr::null_mut();
- if let Some(initializer) = me2_initializer {
- unsafe { initializer(&ModEngineConnectorShim, &mut extension_ptr) };
-
- info!(?path, "loaded native with me2 compatibility shim");
- }
- }
- }
-
- Ok(module)
- });
+ pub fn with_app(f: impl FnOnce(&ModHost, &mut Me3App) -> R) -> R {
+ let attached = Self::get_attached();
+ let mut app = attached.app.lock().expect("failed to lock app");
- match result {
- Err(exception) => {
- warn!("an error occurred while loading {path:?}, it may not work as expected");
- Ok(())
- }
- Ok(result) => result.map(|module| {
- self.native_modules.lock().unwrap().push(module);
- }),
- }
+ f(attached, &mut app)
}
pub fn get_attached() -> &'static ModHost {
@@ -119,14 +75,18 @@ impl ModHost {
}
pub fn override_game_property>(&self, property: S, state: bool) {
- self.property_overrides
- .lock()
- .unwrap()
- .insert(property.as_ref().encode_utf16().collect(), state);
+ let mut app = self.app.lock().expect("failed to lock app");
+
+ app.resource_scope(|world, props: Mut| {
+ props
+ .lock()
+ .unwrap()
+ .insert(property.as_ref().encode_utf16().collect(), state)
+ });
}
}
-pub fn dearxan(attach_config: &AttachConfig) {
+pub fn dearxan(attach_config: Res>) {
if !attach_config.disable_arxan && attach_config.game != Game::DarkSouls3 {
return;
}
diff --git a/crates/mod-host/src/host/hook.rs b/crates/mod-host/src/host/hook.rs
index 6ec25107..eeaa2529 100644
--- a/crates/mod-host/src/host/hook.rs
+++ b/crates/mod-host/src/host/hook.rs
@@ -12,6 +12,56 @@ use crate::{
host::append::{Append, WithAppended},
};
+#[macro_export]
+macro_rules! hook {
+ {
+ module = $module:expr,
+ symbol = $symbol:literal,
+ signature: $signature:ty,
+ $closure:expr
+ } => {{
+ let fn_ptr = unsafe {
+ let s = ::std::ffi::CString::new($symbol)?;
+ ::windows::Win32::System::LibraryLoader::GetProcAddress(
+ $module,
+ ::windows::core::PCSTR::from_raw(s.as_ptr() as _)
+ )
+ .ok_or_eyre(concat!($symbol, " not found"))?
+ };
+
+ let fn_ptr = unsafe { ::std::mem::transmute::<_, $signature>(fn_ptr) };
+
+ $crate::host::ModHost::get_attached()
+ .hook(fn_ptr)
+ .with_span(::tracing::info_span!($symbol))
+ .with_closure($closure)
+ .install()
+ }};
+
+ (
+ pointer = $ptr:expr,
+ signature = $signature:ty,
+ $closure:expr
+ ) => {{
+ let fn_ptr = $ptr as $signature;
+
+ $crate::host::ModHost::get_attached()
+ .hook(fn_ptr)
+ .with_closure($closure)
+ .install()
+ }};
+
+ (
+ pointer = $ptr:expr,
+ $closure:expr
+ ) => {{
+ $crate::host::ModHost::get_attached()
+ .hook($ptr)
+ .with_closure($closure)
+ .install()
+ }};
+}
+
pub enum HookSource {
Function(F),
Closure((F, &'static OnceCell)),
diff --git a/crates/mod-host/src/lib.rs b/crates/mod-host/src/lib.rs
index 65817985..afe79f70 100644
--- a/crates/mod-host/src/lib.rs
+++ b/crates/mod-host/src/lib.rs
@@ -9,12 +9,10 @@ use std::{
sync::{Arc, OnceLock},
};
-use me3_binary_analysis::{fd4_step::Fd4StepTables, rtti};
use me3_env::TelemetryVars;
-use me3_launcher_attach_protocol::{AttachConfig, AttachRequest, AttachResult, Attachment};
-use me3_mod_host_assets::mapping::VfsOverrideMapping;
+use me3_launcher_attach_protocol::{AttachRequest, AttachResult, Attachment};
use me3_telemetry::TelemetryConfig;
-use tracing::{error, info, warn, Span};
+use tracing::{info, warn, Span};
use windows::Win32::{
Globalization::CP_UTF8,
System::{
@@ -24,11 +22,17 @@ use windows::Win32::{
};
use crate::{
+ app::{ExternalResource, Me3App, PostStartup, PreStartup, Startup},
deferred::defer_until_init,
executable::Executable,
- host::{game_properties, ModHost},
+ host::ModHost,
+ plugins::{
+ natives::NativesPlugin, properties::GamePropertiesPlugin, save_file::SaveFilePlugin,
+ skip_logos::SkipLogosPlugin, vfs::VfsPlugin,
+ },
};
+pub mod app;
mod asset_hooks;
mod debugger;
mod deferred;
@@ -37,8 +41,7 @@ mod executable;
mod filesystem;
mod host;
mod native;
-mod savefile;
-mod skip_logos;
+pub mod plugins;
static INSTANCE: OnceLock = OnceLock::new();
static mut TELEMETRY_INSTANCE: OnceLock = OnceLock::new();
@@ -87,100 +90,54 @@ fn on_attach(request: AttachRequest) -> AttachResult {
#[allow(static_mut_refs)]
let _ = unsafe { TELEMETRY_INSTANCE.set(telemetry_guard) };
- let result = me3_telemetry::with_root_span("host", "attach", move || {
- info!("Beginning host attach");
-
- if debugger::is_debugger_present()
- && let Err(e) = debugger::prevent_hiding_threads()
- {
- warn!("error" = &*e, "may fail to debug some threads");
- }
-
- // SAFETY: process is still suspended.
- let exe = unsafe { Executable::new() };
-
- ModHost::new().attach();
-
- host::dearxan(&attach_config);
-
- skip_logos::attach_override(attach_config.clone(), exe)?;
-
- game_properties::attach_override(attach_config.clone(), exe)?;
-
- if !attach_config.start_online {
- game_properties::start_offline();
- }
-
- let mut override_mapping = VfsOverrideMapping::new()?;
-
- override_mapping.scan_directories(attach_config.packages.iter())?;
- savefile::attach_override(&attach_config, &mut override_mapping)?;
+ if debugger::is_debugger_present()
+ && let Err(e) = debugger::prevent_hiding_threads()
+ {
+ warn!("error" = &*e, "may fail to debug some threads");
+ }
- let override_mapping = Arc::new(override_mapping);
+ // SAFETY: process is still suspended.
+ let exe = unsafe { Executable::new() };
+ let mut app = Me3App::new();
- filesystem::attach_override(override_mapping.clone())?;
+ app.insert_resource(ExternalResource(exe));
+ app.insert_resource(ExternalResource(attach_config));
- info!("Host successfully attached");
+ app.register_system(PreStartup, host::dearxan);
+ // app.register_system(
+ // Startup,
+ // (
+ // game_properties::attach_override,
+ // game_properties::start_offline.run_if(|| !start_online),
+ // filesystem::attach_override,
+ // ),
+ // );
- defer_until_init(Span::current(), {
- let override_mapping = override_mapping.clone();
+ app.register_plugin(GamePropertiesPlugin);
+ app.register_plugin(NativesPlugin);
+ app.register_plugin(SaveFilePlugin);
+ app.register_plugin(SkipLogosPlugin);
+ app.register_plugin(VfsPlugin);
- move || {
- if let Err(e) = deferred_attach(attach_config, exe, override_mapping) {
- error!("error" = &*e, "deferred attach failed!")
- }
- }
- })?;
+ // TODO: could load systems from dylibs here
- info!("Deferred me3 attach");
+ app.run_schedule(PreStartup);
+ app.run_schedule(Startup);
- Ok(Attachment)
- })?;
+ let host = ModHost::new(app);
+ host.attach();
- Ok(result)
-}
+ info!("Host successfully attached");
-fn deferred_attach(
- attach_config: Arc,
- exe: Executable,
- override_mapping: Arc,
-) -> Result<(), eyre::Error> {
- let class_map = Arc::new(rtti::classes(exe)?);
- let step_tables = Fd4StepTables::from_initialized_data(exe)?;
-
- savefile::oversized_regulation_fix(
- attach_config.clone(),
- exe,
- &step_tables,
- override_mapping.clone(),
- )?;
-
- for native in &attach_config.natives {
- if let Err(e) = ModHost::get_attached().load_native(&native.path, &native.initializer) {
- warn!(
- error = &*e,
- path = %native.path.display(),
- "failed to load native mod",
- );
-
- if !native.optional {
- return Err(e);
- }
+ defer_until_init(Span::current(), {
+ move || {
+ ModHost::with_app(|_, app| {
+ app.run_schedule(PostStartup);
+ });
}
- }
-
- asset_hooks::attach_override(
- attach_config,
- exe,
- class_map,
- &step_tables,
- override_mapping,
- )
- .map_err(|e| {
- e.wrap_err("failed to attach asset override hooks; no files will be overridden")
})?;
- Ok(())
+ Ok(Attachment)
}
#[unsafe(no_mangle)]
diff --git a/crates/mod-host/src/native.rs b/crates/mod-host/src/native.rs
index ca53e336..8b137891 100644
--- a/crates/mod-host/src/native.rs
+++ b/crates/mod-host/src/native.rs
@@ -1,13 +1 @@
-use std::ffi::c_char;
-pub type ModEngineInitializer =
- unsafe extern "C" fn(&ModEngineConnectorShim, &mut *mut ModEngineExtension) -> bool;
-
-pub struct ModEngineConnectorShim;
-
-pub struct ModEngineExtension {
- _destructor: extern "C" fn(),
- _on_attach: extern "C" fn(),
- _on_detach: extern "C" fn(),
- _id: extern "C" fn() -> *const c_char,
-}
diff --git a/crates/mod-host/src/plugins.rs b/crates/mod-host/src/plugins.rs
new file mode 100644
index 00000000..df818d7a
--- /dev/null
+++ b/crates/mod-host/src/plugins.rs
@@ -0,0 +1,11 @@
+use crate::app::Me3App;
+
+pub mod natives;
+pub mod properties;
+pub mod save_file;
+pub mod skip_logos;
+pub mod vfs;
+
+pub trait Plugin {
+ fn build(&self, app: &mut Me3App);
+}
diff --git a/crates/mod-host/src/plugins/natives.rs b/crates/mod-host/src/plugins/natives.rs
new file mode 100644
index 00000000..25f05f1f
--- /dev/null
+++ b/crates/mod-host/src/plugins/natives.rs
@@ -0,0 +1,172 @@
+use bevy_app::PostStartup;
+use bevy_ecs::{
+ resource::Resource,
+ schedule::{Schedule, ScheduleLabel},
+ system::ResMut,
+};
+use libloading::{Library, Symbol};
+use me3_launcher_attach_protocol::AttachConfig;
+use me3_mod_protocol::native::{Native, NativeInitializerCondition};
+use tracing::{error, info, warn};
+
+use crate::{app::ExternalRes, host::ModHost, plugins::Plugin};
+
+pub struct NativesPlugin;
+
+use std::{
+ ffi::{c_char, CString},
+ panic,
+ time::Duration,
+};
+
+pub type ModEngineInitializer =
+ unsafe extern "C" fn(&ModEngineConnectorShim, &mut *mut ModEngineExtension) -> bool;
+
+pub struct ModEngineConnectorShim;
+
+pub struct ModEngineExtension {
+ _destructor: extern "C" fn(),
+ _on_attach: extern "C" fn(),
+ _on_detach: extern "C" fn(),
+ _id: extern "C" fn() -> *const c_char,
+}
+
+#[derive(Default, Resource)]
+pub struct NativesCollection {
+ loaded: Vec,
+ delayed: Vec,
+}
+
+#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct LoadDelayedNatives;
+
+impl NativesCollection {
+ pub fn load(&mut self, native: &Native) -> color_eyre::Result<()> {
+ let path = native.path.as_path();
+ let result = panic::catch_unwind(|| {
+ let module = unsafe { libloading::Library::new(path)? };
+
+ match &native.initializer {
+ Some(NativeInitializerCondition::Delay { ms }) => {
+ std::thread::sleep(Duration::from_millis(*ms as u64))
+ }
+ Some(NativeInitializerCondition::Function(symbol)) => unsafe {
+ let sym_name = CString::new(symbol.as_bytes())?;
+ let initializer: Symbol bool> =
+ module.get(sym_name.as_bytes_with_nul())?;
+
+ if initializer() {
+ info!(?path, symbol, "native initialized successfully");
+ } else {
+ error!(?path, symbol, "native failed to initialize");
+ }
+ },
+ None => {
+ let me2_initializer: Option> =
+ unsafe { module.get(b"modengine_ext_init\0").ok() };
+
+ let mut extension_ptr: *mut ModEngineExtension = std::ptr::null_mut();
+ if let Some(initializer) = me2_initializer {
+ unsafe { initializer(&ModEngineConnectorShim, &mut extension_ptr) };
+
+ info!(?path, "loaded native with me2 compatibility shim");
+ }
+ }
+ }
+
+ Ok(module)
+ });
+
+ match result {
+ Err(_) => {
+ warn!("an error occurred while loading {path:?}, it may not work as expected");
+ Ok(())
+ }
+ Ok(result) => result.map(|module| {
+ self.loaded.push(module);
+ }),
+ }
+ }
+}
+
+impl Plugin for NativesPlugin {
+ fn build(&self, app: &mut crate::app::Me3App) {
+ let delayed_native_schedule = Schedule::new(LoadDelayedNatives);
+ app.add_schedule(delayed_native_schedule);
+
+ app.init_resource::();
+ app.register_system(PostStartup, Self::load_natives);
+ app.register_system(LoadDelayedNatives, Self::load_delayed_natives);
+ }
+}
+
+impl NativesPlugin {
+ pub fn load_delayed_natives(mut natives: ResMut) -> bevy_ecs::error::Result {
+ let delayed: Vec<_> = natives.delayed.drain(..).collect();
+ for native in delayed {
+ if let Err(e) = natives.load(&native) {
+ warn!(
+ error = &*e,
+ path = %native.path.display(),
+ "failed to load native mod",
+ );
+
+ if !native.optional {
+ return Err(e.into());
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn load_natives(
+ config: ExternalRes,
+ mut natives: ResMut,
+ ) -> bevy_ecs::error::Result {
+ let first_delayed_offset = config
+ .natives
+ .iter()
+ .enumerate()
+ .filter_map(|(idx, native)| native.initializer.is_some().then_some(idx))
+ .next()
+ .unwrap_or(config.natives.len());
+
+ let (immediate, delayed) = config.natives.split_at(first_delayed_offset);
+
+ for native in immediate {
+ if let Err(e) = natives.load(native) {
+ warn!(
+ error = &*e,
+ path = %native.path.display(),
+ "failed to load native mod",
+ );
+
+ if !native.optional {
+ return Err(e.into());
+ }
+ }
+ }
+
+ natives.delayed.extend(delayed.iter().cloned());
+
+ std::thread::spawn(move || {
+ ModHost::with_app(|_, app| {
+ app.run_schedule(LoadDelayedNatives);
+ })
+ });
+
+ Ok(())
+ }
+}
+
+// asset_hooks::attach_override(
+// &**attach_config,
+// &**exe,
+// class_map,
+// &step_tables,
+// &**override_mapping,
+// )
+// .map_err(|e| {
+// e.wrap_err("failed to attach asset override hooks; no files will be overridden")
+// })?;
diff --git a/crates/mod-host/src/host/game_properties.rs b/crates/mod-host/src/plugins/properties.rs
similarity index 72%
rename from crates/mod-host/src/host/game_properties.rs
rename to crates/mod-host/src/plugins/properties.rs
index 7f84e6ef..b32bba4a 100644
--- a/crates/mod-host/src/host/game_properties.rs
+++ b/crates/mod-host/src/plugins/properties.rs
@@ -1,5 +1,13 @@
-use std::{collections::HashMap, mem, slice, sync::Arc};
-
+use std::{
+ collections::HashMap,
+ mem, slice,
+ sync::{Arc, Mutex},
+ vec::Vec as StdVec,
+};
+
+use bevy_app::PostStartup;
+use bevy_derive::{Deref, DerefMut};
+use bevy_ecs::{resource::Resource, system::Res};
use eyre::OptionExt;
use me3_launcher_attach_protocol::AttachConfig;
use me3_mod_host_types::string::DlUtf16String;
@@ -7,69 +15,20 @@ use me3_mod_protocol::Game;
use pelite::pe::Pe;
use rdvec::Vec;
use regex::bytes::Regex;
-use tracing::{error, instrument, Span};
use windows::core::PCWSTR;
-use crate::{deferred::defer_until_init, executable::Executable, host::ModHost};
-
-type GetBoolProperty = unsafe extern "C" fn(usize, *const (), bool) -> bool;
-
-pub fn start_offline() {
- ModHost::get_attached().override_game_property("Menu.IsEnableOnlineMode", false);
-}
-
-#[instrument(skip_all)]
-pub fn attach_override(
- attach_config: Arc,
- exe: Executable,
-) -> Result<(), eyre::Error> {
- let game = attach_config.game;
-
- let do_override = move || {
- let get_bool_property = bool_property_getter(attach_config, exe)?;
-
- ModHost::get_attached()
- .hook(get_bool_property)
- .with_closure(move |p1, name, default, trampoline| unsafe {
- if name.is_null() {
- return false;
- }
-
- let property = if game >= Game::ArmoredCore6 {
- let name = PCWSTR::from_raw(name as *const u16);
- slice::from_raw_parts(name.as_ptr(), name.len())
- } else {
- let name = &*(name as *const DlUtf16String);
- name.get().unwrap().as_slice()
- };
+use crate::{app::ExternalRes, executable::Executable, hook, plugins::Plugin};
- ModHost::get_attached()
- .property_overrides
- .lock()
- .unwrap()
- .get(property)
- .copied()
- .unwrap_or_else(|| trampoline(p1, name, default))
- })
- .install()?;
+pub struct GamePropertiesPlugin;
- eyre::Ok(())
- };
+#[derive(Default, Deref, DerefMut, Resource)]
+pub struct GameProperties(Arc, bool>>>);
- // Some games (Dark Souls 3) might employ Arxan encryption
- // that is removed after running the Arxan entrypoint.
- defer_until_init(Span::current(), move || {
- if let Err(e) = do_override() {
- error!("error" = %e, "failed to hook property getter");
- }
- })?;
-
- Ok(())
-}
+type GetBoolProperty = unsafe extern "C" fn(usize, *const (), bool) -> bool;
fn bool_property_getter(
- attach_config: Arc,
- exe: Executable,
+ attach_config: &AttachConfig,
+ exe: &Executable,
) -> Result {
// Matches callsites for the boolean DLSystemProperty getter.
//
@@ -121,3 +80,48 @@ fn bool_property_getter(
.map(|(ptr, _)| unsafe { mem::transmute::<_, GetBoolProperty>(ptr) })
.ok_or_eyre("pattern returned no matches")
}
+
+impl GamePropertiesPlugin {
+ pub fn hook_property_getter(
+ attach_config: ExternalRes,
+ exe: ExternalRes,
+ properties: Res,
+ ) -> bevy_ecs::error::Result {
+ let get_bool_property = bool_property_getter(&attach_config, &exe)?;
+ let game = attach_config.game;
+ let props = properties.clone();
+
+ hook!(
+ pointer = get_bool_property,
+ move |p1, name, default, trampoline| unsafe {
+ if name.is_null() {
+ return false;
+ }
+
+ let property = if game >= Game::ArmoredCore6 {
+ let name = PCWSTR::from_raw(name as *const u16);
+ slice::from_raw_parts(name.as_ptr(), name.len())
+ } else {
+ let name = &*(name as *const DlUtf16String);
+ name.get().unwrap().as_slice()
+ };
+
+ props
+ .lock()
+ .unwrap()
+ .get(property)
+ .copied()
+ .unwrap_or_else(|| trampoline(p1, name, default))
+ }
+ )?;
+
+ Ok(())
+ }
+}
+
+impl Plugin for GamePropertiesPlugin {
+ fn build(&self, app: &mut crate::app::Me3App) {
+ app.init_resource::();
+ app.register_system(PostStartup, Self::hook_property_getter);
+ }
+}
diff --git a/crates/mod-host/src/savefile.rs b/crates/mod-host/src/plugins/save_file.rs
similarity index 81%
rename from crates/mod-host/src/savefile.rs
rename to crates/mod-host/src/plugins/save_file.rs
index dc80fe88..b17b7b0a 100644
--- a/crates/mod-host/src/savefile.rs
+++ b/crates/mod-host/src/plugins/save_file.rs
@@ -3,9 +3,9 @@ use std::{
fs, mem,
path::{Path, PathBuf},
ptr::NonNull,
- sync::Arc,
};
+use bevy_ecs::system::{NonSend, ResMut};
use eyre::{eyre, OptionExt};
use from_singleton::FromSingleton;
use me3_binary_analysis::{fd4_step::Fd4StepTables, pe};
@@ -15,17 +15,38 @@ use me3_mod_host_types::{alloc::DlStdAllocator, vector::DlVector};
use me3_mod_protocol::Game;
use pelite::pe::{Pe, Va};
use regex::bytes::Regex;
-use tracing::{error, info, instrument, warn, Span};
+use tracing::{error, instrument, warn, Span};
-use crate::{executable::Executable, host::ModHost};
+use crate::{
+ app::{ExternalRes, ExternalResource, Me3App, Startup},
+ executable::Executable,
+ host::ModHost,
+ plugins::Plugin,
+};
+
+pub struct SaveFilePlugin;
+
+impl Plugin for SaveFilePlugin {
+ fn build(&self, app: &mut Me3App) {
+ let config = app.resource::>();
+
+ if config.game >= Game::EldenRing {
+ app.register_system(Startup, oversized_regulation_fix_after_er);
+ } else {
+ app.register_system(Startup, oversized_regulation_fix_for_sdt);
+ }
+
+ app.register_system(Startup, override_savefile);
+ }
+}
const SL_FATAL_ERROR: &str = "could not load alternative savefile location";
#[instrument(skip_all)]
-pub fn attach_override(
- attach_config: &AttachConfig,
- mapping: &mut VfsOverrideMapping,
-) -> Result<(), eyre::Error> {
+pub fn override_savefile(
+ attach_config: ExternalRes,
+ mut mapping: ResMut>,
+) -> bevy_ecs::error::Result {
if let Some(override_name) = &attach_config.savefile {
let savefile_dir = attach_config
.game
@@ -70,32 +91,15 @@ fn override_savefile_path(
Ok(override_path)
}
-#[instrument(skip_all)]
-pub fn oversized_regulation_fix(
- attach_config: Arc,
- exe: Executable,
- step_tables: &Fd4StepTables,
- _mapping: Arc,
-) -> Result<(), eyre::Error> {
- if attach_config.game >= Game::EldenRing {
- oversized_regulation_fix_after_er(exe, step_tables)?;
- } else {
- oversized_regulation_fix_for_sdt(exe)?;
- }
-
- info!("applied hooks");
-
- Ok(())
-}
-
fn oversized_regulation_fix_after_er(
- exe: Executable,
- step_tables: &Fd4StepTables,
-) -> Result<(), eyre::Error> {
+ exe: ExternalRes,
+ step_tables: NonSend,
+) -> Result<(), bevy_ecs::error::BevyError> {
let apply_fn = step_tables
.by_name("CSRegulationStep::STEP_Idle")
.ok_or_eyre("CSRegulationStep::STEP_Idle not found")?;
+ let exe = **exe;
// Intercept and free the raw regulation to prevent writing it to the savefile.
ModHost::get_attached()
.hook(apply_fn)
@@ -132,9 +136,11 @@ fn oversized_regulation_fix_after_er(
Ok(())
}
-fn oversized_regulation_fix_for_sdt(exe: Executable) -> Result<(), eyre::Error> {
+fn oversized_regulation_fix_for_sdt(
+ exe: ExternalRes,
+) -> Result<(), bevy_ecs::error::BevyError> {
let text_section =
- pe::section(exe, ".text").map_err(|e| eyre!("PE section \"{e}\" is missing"))?;
+ pe::section(**exe, ".text").map_err(|e| eyre!("PE section \"{e}\" is missing"))?;
let text = exe.get_section_bytes(text_section)?;
// matches:
diff --git a/crates/mod-host/src/skip_logos.rs b/crates/mod-host/src/plugins/skip_logos.rs
similarity index 73%
rename from crates/mod-host/src/skip_logos.rs
rename to crates/mod-host/src/plugins/skip_logos.rs
index c05130e6..09881423 100644
--- a/crates/mod-host/src/skip_logos.rs
+++ b/crates/mod-host/src/plugins/skip_logos.rs
@@ -1,13 +1,13 @@
-use std::{mem, ptr, sync::Arc};
+use std::{mem, ptr};
-use eyre::{eyre, OptionExt};
+use bevy_ecs::system::Res;
+use eyre::OptionExt;
use me3_binary_analysis::pe;
use me3_launcher_attach_protocol::AttachConfig;
use me3_mod_protocol::Game;
-use pelite::pe::Pe;
-use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
+use pelite::pe::Pe as _;
+use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _};
use regex::bytes::Regex;
-use tracing::{error, info, instrument, Span};
use windows::{
core::{s, w},
Win32::{
@@ -18,83 +18,35 @@ use windows::{
},
};
-use crate::{deferred::defer_until_init, executable::Executable, host::ModHost};
+use crate::{
+ app::{ExternalResource, PostStartup, Startup},
+ executable::Executable,
+ host::ModHost,
+ plugins::Plugin,
+};
-#[instrument(name = "skip_logos", skip_all)]
-pub fn attach_override(
- attach_config: Arc,
- exe: Executable,
-) -> Result<(), eyre::Error> {
- fix_show_window_flash()?;
+pub struct SkipLogosPlugin;
- defer_until_init(Span::current(), move || {
- if attach_config.skip_logos {
- // Different hooks depending on engine version.
- let result = if attach_config.game >= Game::EldenRing {
- skip_fd4_logos(exe)
- } else {
- skip_sprj_logos(exe)
- };
+impl Plugin for SkipLogosPlugin {
+ fn build(&self, app: &mut crate::app::Me3App) {
+ let config = app.resource::>();
- if let Err(e) = result {
- error!("error" = &*e, "can't skip logos");
+ if config.skip_logos {
+ if config.game >= Game::EldenRing {
+ app.register_system(PostStartup, skip_fd4_logos);
} else {
- info!("applied hook");
+ app.register_system(PostStartup, skip_sprj_logos);
}
}
- })?;
-
- Ok(())
-}
-/// Skip logos (ELDEN RING and later games).
-#[instrument(skip_all)]
-fn skip_fd4_logos(exe: Executable) -> Result<(), eyre::Error> {
- let [data, rdata] = pe::sections(exe, [".data", ".rdata"])
- .map_err(|e| eyre!("PE section \"{e}\" is missing"))?;
-
- let data = exe.get_section_bytes(data)?;
- let rdata = exe.get_section_bytes(rdata)?;
-
- // "TitleStep::STEP_BeginLogo" as a UTF-16 string.
- let step_name_re = Regex::new(
- r"(?s-u)T\x00i\x00t\x00l\x00e\x00S\x00t\x00e\x00p\x00:\x00:\x00S\x00T\x00E\x00P\x00_\x00B\x00e\x00g\x00i\x00n\x00L\x00o\x00g\x00o\x00",
- )
- .unwrap();
-
- // Find the string in the .rdata section.
- let step_name_ptr = step_name_re
- .find(rdata)
- .map(|m| m.as_bytes().as_ptr() as usize)
- .ok_or_eyre("pattern returned no matches")?;
-
- let (_, data_ptrs, _) = unsafe { data.align_to::() };
-
- // Find a pointer to the string in the .data section.
- let step_name_ptr = &raw const *data_ptrs
- .par_iter()
- .find_any(|ptr| **ptr == step_name_ptr)
- .ok_or_eyre("no matching step pointers")?;
-
- // Replace the pointer to the step function before the string pointer with the one after it.
- //
- // Memory layout:
- // 0x00 pointer to function TitleStep::STEP_BeginLogo step_name_ptr.sub(1)
- // 0x08 pointer to string "TitleStep::STEP_BeginLogo" ↑↑↑
- // 0x10 pointer to function TitleStep::STEP_BeginTitle step_name_ptr.add(1)
- unsafe {
- let prev_step_fn = step_name_ptr.sub(1) as *mut usize;
- let next_step_fn = step_name_ptr.add(1).read();
- prev_step_fn.write(next_step_fn);
+ app.register_system(Startup, fix_show_window_flash);
}
-
- Ok(())
}
/// Skip logos (Dark Souls 3 and Sekiro).
-fn skip_sprj_logos(exe: Executable) -> Result<(), eyre::Error> {
- let [text, data] = pe::sections(exe, [".text", ".data"])
- .map_err(|e| eyre!("PE section \"{e}\" is missing"))?;
+pub fn skip_sprj_logos(exe: Res>) -> bevy_ecs::error::Result {
+ let [text, data] = pe::sections(**exe, [".text", ".data"])
+ .map_err(|e| eyre::eyre!("PE section \"{e}\" is missing"))?;
let text = exe.get_section_bytes(text)?;
let data = exe.get_section_bytes(data)?;
@@ -142,8 +94,50 @@ fn skip_sprj_logos(exe: Executable) -> Result<(), eyre::Error> {
Ok(())
}
-/// Replaces the default WNDCLASSEXW white background color with black.
-fn fix_show_window_flash() -> Result<(), eyre::Error> {
+/// Skip logos (ELDEN RING and later games).
+pub fn skip_fd4_logos(exe: Res>) -> bevy_ecs::error::Result {
+ let [data, rdata] = pe::sections(**exe, [".data", ".rdata"])
+ .map_err(|e| eyre::eyre!("PE section \"{e}\" is missing"))?;
+
+ let data = exe.get_section_bytes(data)?;
+ let rdata = exe.get_section_bytes(rdata)?;
+
+ // "TitleStep::STEP_BeginLogo" as a UTF-16 string.
+ let step_name_re = Regex::new(
+ r"(?s-u)T\x00i\x00t\x00l\x00e\x00S\x00t\x00e\x00p\x00:\x00:\x00S\x00T\x00E\x00P\x00_\x00B\x00e\x00g\x00i\x00n\x00L\x00o\x00g\x00o\x00",
+ )
+ .unwrap();
+
+ // Find the string in the .rdata section.
+ let step_name_ptr = step_name_re
+ .find(rdata)
+ .map(|m| m.as_bytes().as_ptr() as usize)
+ .ok_or_eyre("pattern returned no matches")?;
+
+ let (_, data_ptrs, _) = unsafe { data.align_to::() };
+
+ // Find a pointer to the string in the .data section.
+ let step_name_ptr = &raw const *data_ptrs
+ .par_iter()
+ .find_any(|ptr| **ptr == step_name_ptr)
+ .ok_or_eyre("no matching step pointers")?;
+
+ // Replace the pointer to the step function before the string pointer with the one after it.
+ //
+ // Memory layout:
+ // 0x00 pointer to function TitleStep::STEP_BeginLogo step_name_ptr.sub(1)
+ // 0x08 pointer to string "TitleStep::STEP_BeginLogo" ↑↑↑
+ // 0x10 pointer to function TitleStep::STEP_BeginTitle step_name_ptr.add(1)
+ unsafe {
+ let prev_step_fn = step_name_ptr.sub(1) as *mut usize;
+ let next_step_fn = step_name_ptr.add(1).read();
+ prev_step_fn.write(next_step_fn);
+ }
+
+ Ok(())
+}
+
+fn fix_show_window_flash() -> bevy_ecs::error::Result {
unsafe {
let user32 = GetModuleHandleW(w!("user32.dll"))?;
diff --git a/crates/mod-host/src/plugins/vfs.rs b/crates/mod-host/src/plugins/vfs.rs
new file mode 100644
index 00000000..2b80ad09
--- /dev/null
+++ b/crates/mod-host/src/plugins/vfs.rs
@@ -0,0 +1,9 @@
+use crate::plugins::Plugin;
+
+pub struct VfsPlugin;
+
+impl Plugin for VfsPlugin {
+ fn build(&self, app: &mut crate::app::Me3App) {
+ todo!()
+ }
+}
diff --git a/crates/mod-host/src/plugins/vfs/assets.rs b/crates/mod-host/src/plugins/vfs/assets.rs
new file mode 100644
index 00000000..e69de29b
diff --git a/crates/mod-host/src/plugins/vfs/filesystem.rs b/crates/mod-host/src/plugins/vfs/filesystem.rs
new file mode 100644
index 00000000..e69de29b