Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
424 changes: 421 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions crates/mod-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
91 changes: 91 additions & 0 deletions crates/mod-host/src/app.rs
Original file line number Diff line number Diff line change
@@ -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<P>(&mut self, plugin: P)
where
P: Plugin,
{
plugin.build(self)
}

pub fn register_system<M>(
&mut self,
schedule: impl ScheduleLabel,
systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
) {
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<T>(pub T);
pub type ExternalRes<'w, T> = Res<'w, ExternalResource<T>>;
pub type ExternalResMut<'w, T> = ResMut<'w, ExternalResource<T>>;

#[derive(Deref, DerefMut, Resource)]
pub struct SharedResource<T>(pub Arc<RwLock<T>>);
pub type SharedRes<'w, T> = Res<'w, SharedResource<T>>;
23 changes: 11 additions & 12 deletions crates/mod-host/src/asset_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<VfsMounts> = Mutex::new(VfsMounts::new());

Expand Down Expand Up @@ -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()) };
Expand All @@ -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");

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -222,8 +221,8 @@ fn hook_device_manager(
.unwrap()
.try_open_file(path, p3, p4, p5, p6)
}
})
.install()?;
}
)?;

info!("applied asset override hook");

Expand Down
94 changes: 27 additions & 67 deletions crates/mod-host/src/host.rs
Original file line number Diff line number Diff line change
@@ -1,105 +1,61 @@
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<ModHost> = OnceLock::new();

#[derive(Default)]
pub struct ModHost {
pub(crate) app: Mutex<Me3App>,
hooks: Mutex<Vec<Arc<UntypedDetour>>>,
native_modules: Mutex<Vec<Library>>,
profiles: Vec<ModProfile>,
property_overrides: Mutex<HashMap<Vec<u16>, bool>>,
}

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()
}
}

#[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<NativeInitializerCondition>,
) -> 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<unsafe extern "C" fn() -> 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<Symbol<ModEngineInitializer>> =
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<R>(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 {
Expand All @@ -119,14 +75,18 @@ impl ModHost {
}

pub fn override_game_property<S: AsRef<str>>(&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<GameProperties>| {
props
.lock()
.unwrap()
.insert(property.as_ref().encode_utf16().collect(), state)
});
}
}

pub fn dearxan(attach_config: &AttachConfig) {
pub fn dearxan(attach_config: Res<ExternalResource<AttachConfig>>) {
if !attach_config.disable_arxan && attach_config.game != Game::DarkSouls3 {
return;
}
Expand Down
50 changes: 50 additions & 0 deletions crates/mod-host/src/host/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F: Function> {
Function(F),
Closure((F, &'static OnceCell<F>)),
Expand Down
Loading
Loading