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