Skip to content

Commit c5775b1

Browse files
committed
feat: 热重载 (reload_mod API + reload.flag 监视)
1 parent 424202d commit c5775b1

7 files changed

Lines changed: 336 additions & 0 deletions

File tree

src/api_impl/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ static mut G_API: ChuModAPI = ChuModAPI {
4848
toml_get_float: None,
4949
toml_get_string: None,
5050
get_manifest_path: None,
51+
reload_mod: None,
5152
};
5253

5354
fn make_api() -> ChuModAPI {
@@ -85,6 +86,7 @@ fn make_api() -> ChuModAPI {
8586
toml_get_float: Some(config_toml::api_toml_get_float),
8687
toml_get_string: Some(config_toml::api_toml_get_string),
8788
get_manifest_path: Some(manifest::api_get_manifest_path),
89+
reload_mod: Some(crate::loader::hot_reload::api_reload_mod),
8890
}
8991
}
9092

src/loader/dependency.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use super::state::HMODULE;
99

1010
pub struct PendingMod {
1111
pub file_name: String,
12+
pub full_path: String,
1213
pub handle: HMODULE,
1314
pub display_name: String,
1415
pub dependencies: Vec<String>,

src/loader/frame_hook.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ unsafe extern "system" fn frame_thread(_param: *mut std::ffi::c_void) -> u32 {
6767
}
6868

6969
pub unsafe fn tick() {
70+
if super::hot_reload::is_reloading() {
71+
return;
72+
}
73+
7074
// 复制回调后释放锁,避免 Mod 回调里调用 Loader API 时发生锁重入。
7175
let frame_mods: Vec<_> = STATE
7276
.lock()

src/loader/hot_reload.rs

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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+
}

src/loader/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod dependency;
22
pub mod frame_hook;
3+
pub mod hot_reload;
34
pub mod log;
45
pub mod metadata;
56
pub mod pe;
@@ -127,6 +128,7 @@ pub unsafe fn load_mods() {
127128
let dependencies = read_dependencies(mod_handle);
128129
pending_mods.push(PendingMod {
129130
file_name: mod_name,
131+
full_path,
130132
handle: mod_handle,
131133
display_name,
132134
dependencies,
@@ -135,6 +137,7 @@ pub unsafe fn load_mods() {
135137

136138
for pending in sort_mods(pending_mods) {
137139
let mod_name = pending.file_name;
140+
let full_path = pending.full_path;
138141
let mod_handle = pending.handle;
139142
let display_name = pending.display_name;
140143

@@ -222,6 +225,8 @@ pub unsafe fn load_mods() {
222225
on_ready,
223226
on_frame,
224227
shutdown,
228+
file_name: mod_name.clone(),
229+
full_path,
225230
name: display_name.clone(),
226231
});
227232
write_log_inner(&mut state, &format!("loaded mod: {}", display_name));
@@ -242,9 +247,11 @@ pub unsafe fn load_mods() {
242247
}
243248

244249
frame_hook::start_if_needed();
250+
hot_reload::start_monitor();
245251
}
246252

247253
pub unsafe fn unload_mods() {
254+
hot_reload::stop_monitor();
248255
frame_hook::stop();
249256
let mut state = STATE.lock().unwrap();
250257
while let Some(m) = state.mods.pop() {

0 commit comments

Comments
 (0)