|
| 1 | +use std::ffi::{c_char, CStr, CString}; |
| 2 | +use std::fs::File; |
| 3 | +use std::panic::{catch_unwind, AssertUnwindSafe}; |
| 4 | +use std::path::Path; |
| 5 | +use std::sync::atomic::{AtomicBool, Ordering}; |
| 6 | + |
| 7 | +use windows_sys::Win32::Foundation::GetLastError; |
| 8 | +use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress, LoadLibraryA}; |
| 9 | +use windows_sys::Win32::System::Threading::Sleep; |
| 10 | + |
| 11 | +use crate::api_impl; |
| 12 | +use crate::types::{ |
| 13 | + ChuModFrameFunc, ChuModInfo, ChuModInitFunc, ChuModNameFunc, ChuModReadyFunc, |
| 14 | + ChuModShutdownFunc, CHUMOD_API_VERSION, |
| 15 | +}; |
| 16 | + |
| 17 | +use super::frame_hook; |
| 18 | +use super::log::{log_error, log_info, log_warn}; |
| 19 | +use super::pe::{parse_game_info, read_game_version}; |
| 20 | +use super::seh::{call_mod_init, call_mod_on_ready, call_mod_shutdown}; |
| 21 | +use super::state::{LoadedMod, STATE}; |
| 22 | + |
| 23 | +static MONITOR_STARTED: AtomicBool = AtomicBool::new(false); |
| 24 | +static MONITOR_RUNNING: AtomicBool = AtomicBool::new(false); |
| 25 | +static RELOAD_IN_PROGRESS: AtomicBool = AtomicBool::new(false); |
| 26 | + |
| 27 | +extern "system" { |
| 28 | + fn CreateDirectoryA(path: *const u8, security: *const std::ffi::c_void) -> i32; |
| 29 | + fn CreateThread( |
| 30 | + attrs: *const std::ffi::c_void, |
| 31 | + stack_size: usize, |
| 32 | + start: Option<unsafe extern "system" fn(*mut std::ffi::c_void) -> u32>, |
| 33 | + param: *mut std::ffi::c_void, |
| 34 | + flags: u32, |
| 35 | + id: *mut u32, |
| 36 | + ) -> *mut std::ffi::c_void; |
| 37 | + fn FreeLibrary(module: *mut std::ffi::c_void) -> i32; |
| 38 | +} |
| 39 | + |
| 40 | +pub fn is_reloading() -> bool { |
| 41 | + RELOAD_IN_PROGRESS.load(Ordering::SeqCst) |
| 42 | +} |
| 43 | + |
| 44 | +pub unsafe extern "C" fn api_reload_mod(mod_name: *const c_char) -> i32 { |
| 45 | + if mod_name.is_null() { |
| 46 | + return -1; |
| 47 | + } |
| 48 | + let name = CStr::from_ptr(mod_name).to_string_lossy().into_owned(); |
| 49 | + match catch_unwind(AssertUnwindSafe(|| unsafe { reload_mod_by_name(&name) })) { |
| 50 | + Ok(ret) => ret, |
| 51 | + Err(_) => { |
| 52 | + log_error(&format!("reload_mod panic caught: {}", name)); |
| 53 | + -1 |
| 54 | + } |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +pub fn start_monitor() { |
| 59 | + if MONITOR_STARTED.swap(true, Ordering::SeqCst) { |
| 60 | + return; |
| 61 | + } |
| 62 | + MONITOR_RUNNING.store(true, Ordering::SeqCst); |
| 63 | + unsafe { |
| 64 | + let handle = CreateThread( |
| 65 | + std::ptr::null(), |
| 66 | + 0, |
| 67 | + Some(reload_flag_thread), |
| 68 | + std::ptr::null_mut(), |
| 69 | + 0, |
| 70 | + std::ptr::null_mut(), |
| 71 | + ); |
| 72 | + if handle.is_null() { |
| 73 | + MONITOR_RUNNING.store(false, Ordering::SeqCst); |
| 74 | + MONITOR_STARTED.store(false, Ordering::SeqCst); |
| 75 | + log_warn("failed to start reload.flag monitor thread"); |
| 76 | + return; |
| 77 | + } |
| 78 | + } |
| 79 | + log_info("reload.flag monitor thread started"); |
| 80 | +} |
| 81 | + |
| 82 | +pub fn stop_monitor() { |
| 83 | + MONITOR_RUNNING.store(false, Ordering::SeqCst); |
| 84 | + MONITOR_STARTED.store(false, Ordering::SeqCst); |
| 85 | +} |
| 86 | + |
| 87 | +unsafe extern "system" fn reload_flag_thread(_param: *mut std::ffi::c_void) -> u32 { |
| 88 | + while MONITOR_RUNNING.load(Ordering::SeqCst) { |
| 89 | + poll_reload_flag(); |
| 90 | + Sleep(500); |
| 91 | + } |
| 92 | + 0 |
| 93 | +} |
| 94 | + |
| 95 | +pub unsafe fn poll_reload_flag() { |
| 96 | + let base_dir = STATE.lock().map(|state| state.base_dir.clone()).unwrap_or_default(); |
| 97 | + if base_dir.is_empty() { |
| 98 | + return; |
| 99 | + } |
| 100 | + let flag_path = format!("{}\\mods\\reload.flag", base_dir); |
| 101 | + if !Path::new(&flag_path).exists() { |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + log_info("reload.flag detected; reloading all mods"); |
| 106 | + let _ = reload_all_mods(); |
| 107 | + if let Err(err) = std::fs::remove_file(&flag_path) { |
| 108 | + log_warn(&format!("failed to remove reload.flag: {}", err)); |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +pub unsafe fn reload_all_mods() -> i32 { |
| 113 | + let names: Vec<String> = STATE |
| 114 | + .lock() |
| 115 | + .map(|state| state.mods.iter().map(|m| m.name.clone()).collect()) |
| 116 | + .unwrap_or_default(); |
| 117 | + let mut failed = 0; |
| 118 | + for name in names { |
| 119 | + if reload_mod_by_name(&name) != 0 { |
| 120 | + failed += 1; |
| 121 | + } |
| 122 | + } |
| 123 | + if failed == 0 { 0 } else { -1 } |
| 124 | +} |
| 125 | + |
| 126 | +pub unsafe fn reload_mod_by_name(name: &str) -> i32 { |
| 127 | + if RELOAD_IN_PROGRESS.swap(true, Ordering::SeqCst) { |
| 128 | + log_warn(&format!("reload already in progress, skip: {}", name)); |
| 129 | + return -1; |
| 130 | + } |
| 131 | + let result = reload_mod_by_name_inner(name); |
| 132 | + RELOAD_IN_PROGRESS.store(false, Ordering::SeqCst); |
| 133 | + result |
| 134 | +} |
| 135 | + |
| 136 | +unsafe fn reload_mod_by_name_inner(name: &str) -> i32 { |
| 137 | + let (index, old_mod) = { |
| 138 | + let mut state = match STATE.lock() { |
| 139 | + Ok(state) => state, |
| 140 | + Err(_) => return -1, |
| 141 | + }; |
| 142 | + let wanted = normalize_name(name); |
| 143 | + let Some(index) = state.mods.iter().position(|m| mod_matches(m, &wanted)) else { |
| 144 | + log_warn(&format!("reload target not found: {}", name)); |
| 145 | + return -1; |
| 146 | + }; |
| 147 | + let old_mod = state.mods.remove(index); |
| 148 | + (index, old_mod) |
| 149 | + }; |
| 150 | + |
| 151 | + log_info(&format!("reload start: {}", old_mod.name)); |
| 152 | + if let Some(shutdown) = old_mod.shutdown { |
| 153 | + log_info(&format!("reload shutdown: {}", old_mod.name)); |
| 154 | + call_mod_shutdown(&old_mod.name, shutdown); |
| 155 | + } |
| 156 | + if !old_mod.handle.is_null() { |
| 157 | + log_info(&format!("reload FreeLibrary: {}", old_mod.name)); |
| 158 | + FreeLibrary(old_mod.handle); |
| 159 | + } |
| 160 | + |
| 161 | + match load_replacement(&old_mod.file_name, &old_mod.full_path) { |
| 162 | + Some(new_mod) => { |
| 163 | + let ready = new_mod.on_ready.map(|on_ready| (new_mod.name.clone(), on_ready)); |
| 164 | + let new_name = new_mod.name.clone(); |
| 165 | + if let Ok(mut state) = STATE.lock() { |
| 166 | + let insert_at = index.min(state.mods.len()); |
| 167 | + state.mods.insert(insert_at, new_mod); |
| 168 | + } |
| 169 | + if let Some((ready_name, on_ready)) = ready { |
| 170 | + call_mod_on_ready(&ready_name, on_ready); |
| 171 | + } |
| 172 | + frame_hook::start_if_needed(); |
| 173 | + log_info(&format!("reload complete: {}", new_name)); |
| 174 | + 0 |
| 175 | + } |
| 176 | + None => { |
| 177 | + log_error(&format!("reload failed, mod remains unloaded: {}", old_mod.name)); |
| 178 | + -1 |
| 179 | + } |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +unsafe fn load_replacement(file_name: &str, full_path: &str) -> Option<LoadedMod> { |
| 184 | + log_info(&format!("reload LoadLibrary: {}", full_path)); |
| 185 | + let full_path_c = CString::new(full_path).ok()?; |
| 186 | + let mod_handle = LoadLibraryA(full_path_c.as_ptr() as *const u8); |
| 187 | + if mod_handle.is_null() { |
| 188 | + log_error(&format!( |
| 189 | + "reload LoadLibrary failed: {} (err={})", |
| 190 | + full_path, |
| 191 | + GetLastError() |
| 192 | + )); |
| 193 | + return None; |
| 194 | + } |
| 195 | + |
| 196 | + let display_name = read_display_name(mod_handle).unwrap_or_else(|| file_name.to_string()); |
| 197 | + let init_fn_ptr = GetProcAddress(mod_handle, b"chumod_init\0".as_ptr()); |
| 198 | + if let Some(init_fn) = init_fn_ptr { |
| 199 | + let init_fn: ChuModInitFunc = std::mem::transmute(init_fn); |
| 200 | + if call_reloaded_init(file_name, &display_name, init_fn) != Some(0) { |
| 201 | + FreeLibrary(mod_handle); |
| 202 | + return None; |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + let shutdown_ptr = GetProcAddress(mod_handle, b"chumod_shutdown\0".as_ptr()); |
| 207 | + let shutdown: Option<ChuModShutdownFunc> = shutdown_ptr.map(|f| std::mem::transmute(f)); |
| 208 | + let on_ready_ptr = GetProcAddress(mod_handle, b"chumod_on_ready\0".as_ptr()); |
| 209 | + let on_ready: Option<ChuModReadyFunc> = on_ready_ptr.map(|f| std::mem::transmute(f)); |
| 210 | + let on_frame_ptr = GetProcAddress(mod_handle, b"chumod_on_frame\0".as_ptr()); |
| 211 | + let on_frame: Option<ChuModFrameFunc> = on_frame_ptr.map(|f| std::mem::transmute(f)); |
| 212 | + |
| 213 | + Some(LoadedMod { |
| 214 | + handle: mod_handle, |
| 215 | + on_ready, |
| 216 | + on_frame, |
| 217 | + shutdown, |
| 218 | + file_name: file_name.to_string(), |
| 219 | + full_path: full_path.to_string(), |
| 220 | + name: display_name, |
| 221 | + }) |
| 222 | +} |
| 223 | + |
| 224 | +unsafe fn call_reloaded_init( |
| 225 | + file_name: &str, |
| 226 | + display_name: &str, |
| 227 | + init_fn: ChuModInitFunc, |
| 228 | +) -> Option<i32> { |
| 229 | + let base_dir = STATE.lock().map(|state| state.base_dir.clone()).unwrap_or_default(); |
| 230 | + let game = GetModuleHandleA(b"chusanApp.exe\0".as_ptr()); |
| 231 | + let (game_size, text_base, text_size, rdata_base, rdata_size) = if !game.is_null() { |
| 232 | + parse_game_info(game) |
| 233 | + } else { |
| 234 | + (0, 0, 0, 0, 0) |
| 235 | + }; |
| 236 | + let game_version = read_game_version(&base_dir).unwrap_or_default(); |
| 237 | + let game_version_c = CString::new(game_version).unwrap_or_default(); |
| 238 | + let loader_ver = concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char; |
| 239 | + let info = ChuModInfo { |
| 240 | + api_version: CHUMOD_API_VERSION, |
| 241 | + loader_version: loader_ver, |
| 242 | + game_module: if !game.is_null() { |
| 243 | + b"chusanApp.exe\0".as_ptr() as *const c_char |
| 244 | + } else { |
| 245 | + std::ptr::null() |
| 246 | + }, |
| 247 | + game_base: if !game.is_null() { game as usize } else { 0 }, |
| 248 | + game_size, |
| 249 | + text_base, |
| 250 | + text_size, |
| 251 | + rdata_base, |
| 252 | + rdata_size, |
| 253 | + game_version: game_version_c.as_ptr(), |
| 254 | + }; |
| 255 | + |
| 256 | + let mod_stem = file_stem(file_name); |
| 257 | + let config_dir = format!("{}\\mods\\config", base_dir); |
| 258 | + CreateDirectoryA(format!("{}\0", config_dir).as_ptr(), std::ptr::null()); |
| 259 | + let log_dir = format!("{}\\mods\\log", base_dir); |
| 260 | + CreateDirectoryA(format!("{}\0", log_dir).as_ptr(), std::ptr::null()); |
| 261 | + let mod_log_path = format!("{}\\{}.log", log_dir, mod_stem); |
| 262 | + let mod_log_path_c = CString::new(mod_log_path.clone()).unwrap_or_default(); |
| 263 | + let toml_config_path = format!("{}\\{}.toml", config_dir, mod_stem); |
| 264 | + let ini_config_path = format!("{}\\{}.ini", config_dir, mod_stem); |
| 265 | + let toml_config_exists = Path::new(&toml_config_path).exists(); |
| 266 | + let manifest_path = format!("{}\\mods\\manifest\\{}.toml", base_dir, mod_stem); |
| 267 | + let manifest_exists = Path::new(&manifest_path).exists(); |
| 268 | + |
| 269 | + let api = api_impl::get_api(); |
| 270 | + api_impl::set_log_path(mod_log_path_c.as_ptr()); |
| 271 | + api_impl::set_current_config(if toml_config_exists { |
| 272 | + &toml_config_path |
| 273 | + } else { |
| 274 | + &ini_config_path |
| 275 | + }); |
| 276 | + api_impl::load_current_toml_config(toml_config_exists.then_some(toml_config_path.as_str())); |
| 277 | + api_impl::set_current_manifest_path(manifest_exists.then_some(manifest_path.as_str())); |
| 278 | + |
| 279 | + if let Ok(mut state) = STATE.lock() { |
| 280 | + state.current_mod_log_file = File::create(&mod_log_path).ok(); |
| 281 | + } |
| 282 | + let ret = call_mod_init(display_name, init_fn, &info, api); |
| 283 | + if let Ok(mut state) = STATE.lock() { |
| 284 | + state.current_mod_log_file = None; |
| 285 | + } |
| 286 | + api_impl::set_log_path(std::ptr::null()); |
| 287 | + api_impl::load_current_toml_config(None); |
| 288 | + api_impl::set_current_manifest_path(None); |
| 289 | + ret |
| 290 | +} |
| 291 | + |
| 292 | +unsafe fn read_display_name(handle: *mut std::ffi::c_void) -> Option<String> { |
| 293 | + let name_fn_ptr = GetProcAddress(handle, b"chumod_name\0".as_ptr())?; |
| 294 | + let name_fn: ChuModNameFunc = std::mem::transmute(name_fn_ptr); |
| 295 | + let raw = name_fn(); |
| 296 | + if raw.is_null() { |
| 297 | + None |
| 298 | + } else { |
| 299 | + Some(CStr::from_ptr(raw).to_string_lossy().into_owned()) |
| 300 | + } |
| 301 | +} |
| 302 | + |
| 303 | +fn mod_matches(loaded: &LoadedMod, wanted: &str) -> bool { |
| 304 | + normalize_name(&loaded.name) == wanted |
| 305 | + || normalize_name(&loaded.file_name) == wanted |
| 306 | + || normalize_name(&file_stem(&loaded.file_name)) == wanted |
| 307 | +} |
| 308 | + |
| 309 | +fn normalize_name(name: &str) -> String { |
| 310 | + name.trim().trim_end_matches(".dll").trim_end_matches(".DLL").to_ascii_lowercase() |
| 311 | +} |
| 312 | + |
| 313 | +fn file_stem(file_name: &str) -> String { |
| 314 | + file_name |
| 315 | + .strip_suffix(".dll") |
| 316 | + .or_else(|| file_name.strip_suffix(".DLL")) |
| 317 | + .unwrap_or(file_name) |
| 318 | + .to_string() |
| 319 | +} |
0 commit comments