diff --git a/Cargo.toml b/Cargo.toml index a036484..a3cb5dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ windows-sys = { version = "0.59", features = [ "Win32_Graphics_Gdi", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", + "Win32_Networking_WinHttp", "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Cryptography", diff --git a/packages/applechu-schema/src/lib.rs b/packages/applechu-schema/src/lib.rs index 4ed569c..421bb99 100644 --- a/packages/applechu-schema/src/lib.rs +++ b/packages/applechu-schema/src/lib.rs @@ -1124,12 +1124,12 @@ mod tests { "RestartSong", "SkipWarning", "PCBID", + "ServerResources", "VFS", "NetEnv", ] ); } - #[test] fn rejects_malformed_entries_instead_of_silently_dropping_them() { let source = r#" diff --git a/packages/applechu/src/lib.rs b/packages/applechu/src/lib.rs index 5311707..7d2235e 100644 --- a/packages/applechu/src/lib.rs +++ b/packages/applechu/src/lib.rs @@ -33,6 +33,8 @@ pub mod platform; #[cfg(target_arch = "x86")] mod proxy; mod schema_embed; +#[cfg(target_arch = "x86")] +mod server_resources; mod slider; mod system_config; mod unlocker; diff --git a/packages/applechu/src/platform/vfs.rs b/packages/applechu/src/platform/vfs.rs index dd454b9..43057d7 100644 --- a/packages/applechu/src/platform/vfs.rs +++ b/packages/applechu/src/platform/vfs.rs @@ -1,6 +1,7 @@ use std::ffi::CString; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, OnceLock}; use once_cell::sync::OnceCell; @@ -11,6 +12,8 @@ use crate::platform::{path_hook, winapi}; use crate::util::api::Api; static CONFIG: OnceCell = OnceCell::new(); +/// option 根下的子树重定向:(小写目录名, 目标目录) +static OPTION_OVERRIDES: OnceLock>> = OnceLock::new(); static OPTION_API_LOGGED: AtomicBool = AtomicBool::new(false); static OPTION_PATH_LOGGED: AtomicBool = AtomicBool::new(false); @@ -123,6 +126,11 @@ pub fn resolve_path(path: &str) -> Option { if normalized == VFS_TMP_ICF && !config.allow_amfs_downloads { return Some(PathBuf::from(&config.appdata).join("tmpIcf.icf")); } + // 自定义 option 的挂载点优先于盘符映射:它的位置可能正好落在某个映射盘上, + // 未注册覆盖时返回 `None`,下面的分支一个都不少。 + if let Some(target) = option_override(config, &normalized) { + return Some(target); + } if let Some(tail) = strip_drive(&normalized, 'e') { return Some(join_root(&config.amfs, tail)); } @@ -136,7 +144,7 @@ pub fn resolve_path(path: &str) -> Option { return Some(join_root(&config.nthome, tail)); } if let Some(tail) = strip_prefix_dir(&normalized, VFS_OPTION) { - return Some(join_root(&config.option, tail)); + return Some(option_target(config, tail)); } if let Some(tail) = strip_prefix_dir(&normalized, VFS_APM) { return Some(join_root(&config.option, tail)); @@ -144,6 +152,64 @@ pub fn resolve_path(path: &str) -> Option { None } +/// option 源路径 → 真实路径 +/// +/// 根下第一段命中自定义 option 覆盖时,该段**被替换**为解压目录(不是简单叠加), +/// 其余路径原样接在后面;未命中时与没有覆盖时完全一致,落在配置的 option 根下。 +fn option_target(config: &VfsConfig, tail: &str) -> PathBuf { + if tail.is_empty() { + return PathBuf::from(&config.option); + } + let mut segments = tail.splitn(2, '\\'); + let name = segments.next().unwrap_or_default().to_ascii_lowercase(); + let rest = segments.next().unwrap_or_default(); + match override_dir(&name) { + Some(target) => join_root(&target, rest), + None => join_root(&config.option, tail), + } +} + +/// 命中挂载根(mount 根由本模块 hook 给出)的 option 子树 +/// +/// 与 [`option_target`] 同理:第一段被替换为解压目录。`normalized` 已小写, +/// 因此比较也用小写。 +fn option_override(config: &VfsConfig, normalized: &str) -> Option { + let option_root = config.option.trim_end_matches('\\').to_ascii_lowercase(); + let tail = strip_prefix_dir(normalized, &option_root)?; + if tail.is_empty() { + return None; + } + let mut segments = tail.splitn(2, '\\'); + let name = segments.next()?.to_ascii_lowercase(); + let rest = segments.next().unwrap_or_default(); + Some(join_root(&override_dir(&name)?, rest)) +} + +/// 查 option 根下某个子目录名的重定向目标(`None` 表示未挂载) +fn override_dir(name: &str) -> Option { + let overrides = OPTION_OVERRIDES.get()?.lock().ok()?; + overrides + .iter() + .find(|(candidate, _)| candidate == name) + .map(|(_, target)| target.clone()) +} + +/// 注册(或替换)option 根下一个子目录的重定向 +/// +/// 由服务器下发功能在解压完成后调用:`c:\mount\option\\...` 将指向 +/// `target`,读取与枚举都由游戏走真实文件 API 完成。传空 `target` 注销。 +pub fn set_option_override(name: &str, target: &str) { + let overrides = OPTION_OVERRIDES.get_or_init(|| Mutex::new(Vec::new())); + let Ok(mut overrides) = overrides.lock() else { + return; + }; + let key = name.trim_matches('\\').to_ascii_lowercase(); + overrides.retain(|(candidate, _)| *candidate != key); + if !target.is_empty() { + overrides.push((key, winapi::fixup_path(Path::new(target)))); + } +} + fn strip_drive(path: &str, drive: char) -> Option<&str> { let bytes = path.as_bytes(); if bytes.len() < 2 || bytes[0] != drive as u8 || bytes[1] != b':' { @@ -205,6 +271,14 @@ pub fn amfs_path() -> Option { CONFIG.get().map(|config| config.amfs.clone()) } +/// 游戏原始请求中 option 的**源前缀**(重定向前),规范化小写 +/// +/// 游戏访问 option 时传入的是 `c:\mount\option\...`,`resolve_path` 之后才被 +/// 改写为真实目录;虚拟包匹配必须对源前缀做判断。 +pub fn option_source_prefix() -> String { + VFS_OPTION.to_owned() +} + pub fn appdata_path() -> Option { CONFIG.get().map(|config| config.appdata.clone()) } diff --git a/packages/applechu/src/platform/vfs/tests.rs b/packages/applechu/src/platform/vfs/tests.rs index d578c15..9bfa29d 100644 --- a/packages/applechu/src/platform/vfs/tests.rs +++ b/packages/applechu/src/platform/vfs/tests.rs @@ -1,7 +1,7 @@ use std::fs; use std::time::{SystemTime, UNIX_EPOCH}; -use super::select_option_path; +use super::{option_override, option_target, select_option_path, set_option_override, VfsConfig}; #[test] fn configured_option_directory_wins_when_non_empty() { @@ -56,6 +56,71 @@ fn missing_candidates_keep_configured_option_path() { remove_test_tree(&root); } +#[test] +fn option_override_redirects_only_the_mounted_subtree() { + // Given: 一个注册了重定向的自定义 option 根。 + let config = test_config("override-only-mounted"); + set_option_override("a9z1", "D:\\cache\\ServerResources.d\\A9Z1"); + + // Then: 该子树的任何路径都指向解压目录——无论游戏走源路径 + // (`c:\mount\option\...`) 还是 mount 根 (`