From 18fd4c0efa57bbe9e934045b770fa908e7e6c539 Mon Sep 17 00:00:00 2001 From: WuBoytH Date: Sat, 19 Sep 2026 15:55:39 -0400 Subject: [PATCH] Patch file at boot to get their size --- src/fs.rs | 13 ++---- src/modfs/handlers/bgm.rs | 4 -- src/modfs/handlers/motionlist.rs | 4 -- src/modfs/handlers/msbt.rs | 4 -- src/modfs/handlers/nus3audio.rs | 20 +-------- src/modfs/handlers/prc.rs | 5 --- src/modfs/mod.rs | 76 +++++++++++++++++++++++++++++--- src/modfs/registry.rs | 12 ----- 8 files changed, 74 insertions(+), 64 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 77954242e..09987f6e7 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -83,8 +83,6 @@ impl CachedFilesystem { /// Use the file information that was generated during file discovery to fill out a GlobalFilesystem struct pub fn make_from_promise(discovery: DiscoveryResult) -> CachedFilesystem { - let arc = resource::arc(); - // Load the default config, which we will then join with the other configs let mut config = match ModConfig::from_json(DEFAULT_CONFIG) { Ok(cfg) => cfg, @@ -105,13 +103,10 @@ impl CachedFilesystem { let mut hashed_paths: HashMap = HashMap::new(); let mut hashed_sizes: HashMap = HashMap::new(); - for hash in modfs.handlers().bound_hashes() { - if let Ok(data) = arc.get_file_data_from_hash(hash, config::region()) { - let base_size = data.decomp_size as usize; - let size = modfs.handlers().patched_size_for_hash(hash, base_size).unwrap_or(base_size); - hashed_paths.insert(hash, get_path_from_hash(hash)); - hashed_sizes.insert(hash, size); - } + // Patch every handler-bound file once now so the exact result size is what gets written into the data.arc + for (&hash, &size) in modfs.measure_patched_sizes() { + hashed_paths.insert(hash, get_path_from_hash(hash)); + hashed_sizes.insert(hash, size); } for dep in nus3audio_deps { diff --git a/src/modfs/handlers/bgm.rs b/src/modfs/handlers/bgm.rs index f93d2418f..6cac08bdd 100644 --- a/src/modfs/handlers/bgm.rs +++ b/src/modfs/handlers/bgm.rs @@ -30,10 +30,6 @@ impl FileHandler for BgmHandler { &["bgm_property.bin"] } - fn patched_size(&self, _hash: Hash40, base_size: usize) -> usize { - base_size * 30 - } - fn discover(&mut self, _ctx: &mut DiscoveryContext, full_path: &Path, local: &Path, _size: usize) -> Option { let (base_local, _region) = super::strip_regional(local); diff --git a/src/modfs/handlers/motionlist.rs b/src/modfs/handlers/motionlist.rs index a4596d343..589cb52c5 100644 --- a/src/modfs/handlers/motionlist.rs +++ b/src/modfs/handlers/motionlist.rs @@ -118,8 +118,4 @@ impl FileHandler for MotionListHandler { .map_err(|e| ModFsError::Handler(format!("failed to write patched motion_list: {:?}", e)))?; Ok(writer.into_inner()) } - - fn patched_size(&self, _hash: Hash40, base_size: usize) -> usize { - base_size * 2 - } } diff --git a/src/modfs/handlers/msbt.rs b/src/modfs/handlers/msbt.rs index 3e6778286..9474f5daa 100644 --- a/src/modfs/handlers/msbt.rs +++ b/src/modfs/handlers/msbt.rs @@ -178,10 +178,6 @@ impl FileHandler for MsbtHandler { .map_err(|e| ModFsError::Handler(format!("failed to write patched msbt: {:?}", e)))?; Ok(cursor.into_inner()) } - - fn patched_size(&self, _hash: Hash40, base_size: usize) -> usize { - base_size * 2 - } } fn to_text_data(text_type: &TextType) -> Vec { diff --git a/src/modfs/handlers/nus3audio.rs b/src/modfs/handlers/nus3audio.rs index d432dd388..5a1f51cba 100644 --- a/src/modfs/handlers/nus3audio.rs +++ b/src/modfs/handlers/nus3audio.rs @@ -11,15 +11,9 @@ use crate::{ modfs::{registry::FileHandler, DiscoveryContext, ModFsError}, }; -/// Slack added per patch file on top of its size, to cover section headers, alignment and the JUNK padding the -/// writer emits when the merged file is laid out again. -const PATCH_SLACK: usize = 0x100; - #[derive(Default)] pub struct Nus3audioHandler { patches: HashMap>, - /// Sum of the on-disk sizes of every patch bound to a target, used to bound the merged file's size - patch_sizes: HashMap, } impl FileHandler for Nus3audioHandler { @@ -35,13 +29,12 @@ impl FileHandler for Nus3audioHandler { &["patch3audio"] } - fn discover(&mut self, _ctx: &mut DiscoveryContext, full_path: &Path, local: &Path, size: usize) -> Option { + fn discover(&mut self, _ctx: &mut DiscoveryContext, full_path: &Path, local: &Path, _size: usize) -> Option { let base_local = local.with_extension("nus3audio"); let (base_local, _region) = super::strip_regional(&base_local); let hash = super::try_smash_hash(&base_local)?; self.patches.entry(hash).or_default().push(full_path.to_path_buf()); - *self.patch_sizes.entry(hash).or_default() += size + PATCH_SLACK; if let Some(s) = local.to_str() { hashes::add(s); @@ -52,17 +45,6 @@ impl FileHandler for Nus3audioHandler { Some(hash) } - fn patched_size(&self, hash: Hash40, base_size: usize) -> usize { - match self.patch_sizes.get(&hash) { - // Only allocates about as much as all of the audio patches will take up. - // This may not even be the final patched size as auto patches can - // replace audio instead of append it. - Some(extra) => (base_size + extra).next_multiple_of(0x1000), - // Fallback in case the above fails I guess - None => base_size * 2, - } - } - fn apply(&self, hash: Hash40, bytes: Vec) -> Result, ModFsError> { let Some(patches) = self.patches.get(&hash) else { return Ok(bytes); diff --git a/src/modfs/handlers/prc.rs b/src/modfs/handlers/prc.rs index 91ceb3909..0a6c02ddd 100644 --- a/src/modfs/handlers/prc.rs +++ b/src/modfs/handlers/prc.rs @@ -86,9 +86,4 @@ impl FileHandler for PrcHandler { .map_err(|e| ModFsError::Handler(format!("failed to write patched prc: {:?}", e)))?; Ok(writer.into_inner()) } - - fn patched_size(&self, _hash: Hash40, base_size: usize) -> usize { - base_size * 2 - } - } diff --git a/src/modfs/mod.rs b/src/modfs/mod.rs index 43ac4bbc6..ba0d795cf 100644 --- a/src/modfs/mod.rs +++ b/src/modfs/mod.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, io::Read, path::{Path, PathBuf}, }; @@ -71,6 +72,8 @@ pub struct ModFs { handlers: HandlerRegistry, conflict_mode: ConflictMode, dir_index: std::sync::RwLock>, + /// Exact size of every handler-patched file, measured by running its handler chain once at boot + patched_sizes: HashMap, } struct DirIndex { @@ -114,6 +117,7 @@ impl ModFs { handlers, conflict_mode: ConflictMode::default(), dir_index: std::sync::RwLock::new(None), + patched_sizes: HashMap::new(), } } @@ -187,12 +191,65 @@ impl ModFs { if let Some(s) = self.virt.max_size(hash) { return Some(s); } - let base = self.patch.get(path).map(|e| e.size)?; - return Some(self.handlers.patched_size_for_hash(hash, base).unwrap_or(base)); + if let Some(&s) = self.patched_sizes.get(&hash) { + return Some(s); + } } self.patch.get(path).map(|e| e.size) } + /// Runs the handler chain for every bound hash once and records the exact size of the result. That size is what + /// gets written into the data.arc, so the game allocates exactly what the patched file needs instead of a guessed + /// multiple of the vanilla size. The patched bytes are dropped right away and regenerated when the file loads. + /// + /// Files whose base can't be read (not in the arc, unreadable patch entry) are skipped. If a handler fails, the + /// load will fail the same way later, so the unpatched size is recorded for it. + pub fn measure_patched_sizes(&mut self) -> &HashMap { + let bound: Vec = self.handlers.bound_hashes().collect(); + let mut sizes = HashMap::with_capacity(bound.len()); + + for hash in bound { + let base = match self.read_unpatched(hash) { + Ok(bytes) => bytes, + Err(e) => { + debug!( + "modfs: not measuring '{}' ({:#x}), the base file could not be read: {:?}", + crate::hashes::find(hash), + hash.0, + e + ); + continue; + }, + }; + let base_len = base.len(); + let size = match self.handlers.apply_chain(hash, base) { + Ok(patched) => patched.len(), + Err(e) => { + warn!( + "modfs: failed to patch '{}' ({:#x}) while measuring its size, keeping the unpatched size {:#x}: {:?}", + crate::hashes::find(hash), + hash.0, + base_len, + e + ); + base_len + }, + }; + debug!( + "modfs: '{}' ({:#x}) patches to {:#x} bytes (base {:#x})", + crate::hashes::find(hash), + hash.0, + size, + base_len + ); + sizes.insert(hash, size); + } + + info!(target: "std", "modfs: measured exact sizes for {} patched files", sizes.len()); + self.patched_sizes = sizes; + &self.patched_sizes + } + pub fn read_dir<'a>(&'a self, path: &Path) -> Vec { self.with_dir_index(|index| index.children.get(path).cloned().unwrap_or_default()) } @@ -291,13 +348,18 @@ impl ModFs { } pub fn read_base(&self, hash: Hash40) -> Result, ModFsError> { - let bytes = if let Some((local, entry)) = self.patch.entry_for_hash(hash) { - std::fs::read(entry.full_path(local))? + let bytes = self.read_unpatched(hash)?; + self.handlers.apply_chain(hash, bytes) + } + + /// Reads the file a handler chain starts from: the mod's own file if one replaces it, otherwise the vanilla one + fn read_unpatched(&self, hash: Hash40) -> Result, ModFsError> { + if let Some((local, entry)) = self.patch.entry_for_hash(hash) { + Ok(std::fs::read(entry.full_path(local))?) } else { let arc = crate::resource::arc(); - arc.get_file_contents(hash, config::region())? - }; - self.handlers.apply_chain(hash, bytes) + Ok(arc.get_file_contents(hash, config::region())?) + } } pub fn resolve_stream_path(&self, hash: Hash40) -> Option<(std::path::PathBuf, usize)> { diff --git a/src/modfs/registry.rs b/src/modfs/registry.rs index 44fcff893..af86ba536 100644 --- a/src/modfs/registry.rs +++ b/src/modfs/registry.rs @@ -31,10 +31,6 @@ pub trait FileHandler: Send + Sync + 'static { Ok(bytes) } - fn patched_size(&self, _hash: Hash40, base_size: usize) -> usize { - base_size * 10 - } - fn finalize(&self, _config: &mut ModConfig) {} } @@ -108,14 +104,6 @@ impl HandlerRegistry { self.handlers_for_hash(hash).iter().any(|id| self.handlers[id.0].patches_load()) } - /// Returns the largest buffer size any handler bound to `hash` needs, or `None` if no handler is bound. - pub fn patched_size_for_hash(&self, hash: Hash40, base_size: usize) -> Option { - self.handlers_for_hash(hash) - .iter() - .map(|id| self.handlers[id.0].patched_size(hash, base_size)) - .max() - } - pub fn apply_chain(&self, hash: Hash40, mut bytes: Vec) -> Result, ModFsError> { for id in self.handlers_for_hash(hash) { bytes = self.handlers[id.0].apply(hash, bytes)?;