diff --git a/crates/ltk_modpkg/src/builder.rs b/crates/ltk_modpkg/src/builder.rs index 6406383..707bbce 100644 --- a/crates/ltk_modpkg/src/builder.rs +++ b/crates/ltk_modpkg/src/builder.rs @@ -232,24 +232,8 @@ impl ModpkgBuilder { layer_index_map: &HashMap, wad_indices: &HashMap, ) -> Result, ModpkgBuilderError> { - // First process all meta chunks (metadata, thumbnail, etc.) let mut meta_chunks = self.process_meta_chunks(writer, chunk_path_indices)?; - - // Build a set of path hashes for all meta chunks we just emitted, - // so we can skip them when processing the remaining chunks. - let meta_path_hashes = meta_chunks - .iter() - .map(|c| c.path_hash) - .collect::>(); - - // Then process all non-meta chunks via the generic pipeline - let regular_chunks: Vec<_> = self - .chunks - .values() - .chain(self.meta_chunks.values()) - .filter(|chunk| !meta_path_hashes.contains(&chunk.path_hash)) - .collect(); - + let regular_chunks = self.collect_regular_chunks(&meta_chunks); let mut processed_regular_chunks = Self::process_chunks( ®ular_chunks, writer, @@ -264,6 +248,27 @@ impl ModpkgBuilder { Ok(meta_chunks) } + /// Collect all non-meta chunks, sorted by WAD name then layer. + /// + /// This groups related chunks physically in the file, + /// enabling more sequential I/O when reading all overrides for a WAD. + fn collect_regular_chunks(&self, meta_chunks: &[ModpkgChunk]) -> Vec<&ModpkgChunkBuilder> { + let meta_path_hashes = meta_chunks + .iter() + .map(|c| c.path_hash) + .collect::>(); + + let mut regular_chunks: Vec<_> = self + .chunks + .values() + .chain(self.meta_chunks.values()) + .filter(|chunk| !meta_path_hashes.contains(&chunk.path_hash)) + .collect(); + regular_chunks.sort_by(|a, b| a.wad.cmp(&b.wad).then(a.layer.cmp(&b.layer))); + + regular_chunks + } + fn write_chunk_toc( writer: &mut W, chunk_toc_offset: u64, @@ -586,6 +591,14 @@ impl ModpkgChunkBuilder { self } + /// Set the WAD association for this chunk. + /// + /// This enables efficient WAD-based lookups via the secondary index. + pub fn with_wad(mut self, wad: &str) -> Self { + self.wad = wad.to_lowercase(); + self + } + pub fn path_hash(&self) -> u64 { self.path_hash } diff --git a/crates/ltk_modpkg/src/decoder.rs b/crates/ltk_modpkg/src/decoder.rs index adb60a1..cba7c76 100644 --- a/crates/ltk_modpkg/src/decoder.rs +++ b/crates/ltk_modpkg/src/decoder.rs @@ -32,13 +32,10 @@ where } fn decode_zstd_chunk(&mut self, chunk: &ModpkgChunk) -> Result, ModpkgError> { - self.source.seek(SeekFrom::Start(chunk.data_offset))?; - - let mut data: Vec = vec![0; chunk.uncompressed_size as usize]; + let compressed = self.load_chunk_raw(chunk)?; - zstd::Decoder::new(&mut self.source) - .map_err(|e| ModpkgError::Io(std::io::Error::other(e)))? - .read_exact(&mut data)?; + let data = zstd::bulk::decompress(&compressed, chunk.uncompressed_size as usize) + .map_err(|e| ModpkgError::Io(std::io::Error::other(e)))?; Ok(data.into_boxed_slice()) } diff --git a/crates/ltk_modpkg/src/lib.rs b/crates/ltk_modpkg/src/lib.rs index 7300fad..364a744 100644 --- a/crates/ltk_modpkg/src/lib.rs +++ b/crates/ltk_modpkg/src/lib.rs @@ -34,6 +34,9 @@ pub use utils::*; /// The name of the base layer. pub const BASE_LAYER_NAME: &str = "base"; +/// A batch-loaded chunk entry: `(path_hash, layer_hash, decompressed_data)`. +pub type BatchChunkEntry = (u64, u64, Box<[u8]>); + /// The name of the metadata folder inside the mod package. pub const METADATA_FOLDER_NAME: &str = "_meta_"; @@ -55,6 +58,11 @@ pub struct Modpkg { /// The key is a tuple of the path hash and the layer hash respectively. pub chunks: HashMap<(u64, u64), ModpkgChunk>, + /// Secondary index: chunks grouped by (wad_index, layer_index). + /// + /// Values are chunk keys `(path_hash, layer_hash)` that can be looked up in `chunks`. + pub chunks_by_wad_layer: HashMap<(u32, u32), Vec<(u64, u64)>>, + /// The original byte source. source: TSource, } @@ -93,9 +101,27 @@ impl Modpkg { } } - fn candidate_path_hashes(path: &str) -> (u64, Option) { + /// Resolve the chunk key `(path_hash, layer_hash)` for a given path and layer, + /// handling both literal and hex-encoded chunk names. + /// + /// Returns the first matching key, or `Err` if no chunk matches. + fn resolve_chunk_key( + &self, + path: &str, + layer: Option<&str>, + ) -> Result<(u64, u64), ModpkgError> { let normalized = utils::normalize_chunk_path(path); let literal_hash = hash_chunk_name(&normalized); + let layer_hash = match layer { + Some(name) => hash_layer_name(name), + None => NO_LAYER_HASH, + }; + + if self.chunks.contains_key(&(literal_hash, layer_hash)) { + return Ok((literal_hash, layer_hash)); + } + + // Try hex-encoded chunk name fallback (e.g., "abcdef1234567890.dds") let filename_lower = Path::new(&normalized) .file_name() .and_then(|s| s.to_str()) @@ -105,12 +131,14 @@ impl Modpkg { if utils::is_hex_chunk_name(&filename_lower) { if let Some(base) = filename_lower.split('.').next() { if let Ok(parsed) = u64::from_str_radix(base, 16) { - return (literal_hash, Some(parsed)); + if self.chunks.contains_key(&(parsed, layer_hash)) { + return Ok((parsed, layer_hash)); + } } } } - (literal_hash, None) + Err(ModpkgError::MissingChunk(literal_hash)) } /// Load the raw data of a chunk using the path hash and layer hash @@ -145,19 +173,8 @@ impl Modpkg { path: &str, layer: Option<&str>, ) -> Result, ModpkgError> { - let (literal_hash, hex_hash) = Self::candidate_path_hashes(path); - let layer_hash = match layer { - Some(layer_name) => hash_layer_name(layer_name), - None => NO_LAYER_HASH, - }; - - if let Ok(data) = self.load_chunk_raw(literal_hash, layer_hash) { - return Ok(data); - } - if let Some(hh) = hex_hash { - return self.load_chunk_raw(hh, layer_hash); - } - self.load_chunk_raw(literal_hash, layer_hash) + let (ph, lh) = self.resolve_chunk_key(path, layer)?; + self.load_chunk_raw(ph, lh) } /// Load and decompress the data of a chunk by path and layer name @@ -166,38 +183,14 @@ impl Modpkg { path: &str, layer: Option<&str>, ) -> Result, ModpkgError> { - let (literal_hash, hex_hash) = Self::candidate_path_hashes(path); - let layer_hash = match layer { - Some(layer_name) => hash_layer_name(layer_name), - None => NO_LAYER_HASH, - }; - - if let Ok(data) = self.load_chunk_decompressed_by_hash(literal_hash, layer_hash) { - return Ok(data); - } - if let Some(hh) = hex_hash { - return self.load_chunk_decompressed_by_hash(hh, layer_hash); - } - self.load_chunk_decompressed_by_hash(literal_hash, layer_hash) + let (ph, lh) = self.resolve_chunk_key(path, layer)?; + self.load_chunk_decompressed_by_hash(ph, lh) } /// Get a chunk by path and layer name pub fn get_chunk(&self, path: &str, layer: Option<&str>) -> Result<&ModpkgChunk, ModpkgError> { - let (literal_hash, hex_hash) = Self::candidate_path_hashes(path); - let layer_hash = match layer { - Some(layer_name) => hash_layer_name(layer_name), - None => NO_LAYER_HASH, - }; - - if let Some(chunk) = self.chunks.get(&(literal_hash, layer_hash)) { - return Ok(chunk); - } - if let Some(hh) = hex_hash { - if let Some(chunk) = self.chunks.get(&(hh, layer_hash)) { - return Ok(chunk); - } - } - Err(ModpkgError::MissingChunk(literal_hash)) + let (ph, lh) = self.resolve_chunk_key(path, layer)?; + Ok(self.chunks.get(&(ph, lh)).unwrap()) } /// Load a chunk into memory @@ -209,20 +202,67 @@ impl Modpkg { } /// Check if a chunk exists by path and layer name - pub fn has_chunk(&self, path: &str, layer: Option<&str>) -> Result { - let (literal_hash, hex_hash) = Self::candidate_path_hashes(path); - let layer_hash = match layer { - Some(layer_name) => hash_layer_name(layer_name), - None => NO_LAYER_HASH, - }; + pub fn has_chunk(&self, path: &str, layer: Option<&str>) -> bool { + self.resolve_chunk_key(path, layer).is_ok() + } - if self.chunks.contains_key(&(literal_hash, layer_hash)) { - return Ok(true); - } - if let Some(hh) = hex_hash { - return Ok(self.chunks.contains_key(&(hh, layer_hash))); + /// Resolve a layer name to its index in the layer table. + pub fn layer_index(&self, layer: &str) -> Option { + let layer_hash = hash_layer_name(layer); + self.layer_indices + .iter() + .position(|&h| h == layer_hash) + .map(|idx| idx as u32) + } + + /// Resolve a WAD name to its index in the WAD table. + pub fn wad_index(&self, wad_name: &str) -> Option { + let wad_hash = hash_wad_name(wad_name); + self.wads_indices + .iter() + .position(|&h| h == wad_hash) + .map(|idx| idx as u32) + } + + /// Get the WAD name for a given WAD index, or `None` if the index is invalid. + pub fn wad_name_for_index(&self, wad_index: u32) -> Option<&str> { + let wad_hash = self.wads_indices.get(wad_index as usize)?; + self.wads.get(wad_hash).map(|s| s.as_str()) + } + + /// Get the chunk keys for a given (wad_index, layer_index) pair. + /// + /// Returns an empty slice if no chunks match. + pub fn chunks_for_wad_layer(&self, wad_index: u32, layer_index: u32) -> &[(u64, u64)] { + self.chunks_by_wad_layer + .get(&(wad_index, layer_index)) + .map(|v| v.as_slice()) + .unwrap_or(&[]) + } + + /// Load and decompress multiple chunks in offset-sorted order for better I/O performance. + /// + /// Returns `(path_hash, layer_hash, data)` tuples in arbitrary order. + pub fn load_chunks_batch( + &mut self, + keys: &[(u64, u64)], + ) -> Result, ModpkgError> { + // Resolve keys to chunks and sort by data_offset for sequential I/O + let mut sorted: Vec<_> = keys + .iter() + .filter_map(|&(ph, lh)| self.chunks.get(&(ph, lh)).map(|c| (ph, lh, *c))) + .collect(); + sorted.sort_by_key(|(_, _, c)| c.data_offset); + + let mut results = Vec::with_capacity(sorted.len()); + let mut decoder = ModpkgDecoder { + source: &mut self.source, + }; + for (ph, lh, chunk) in &sorted { + let data = decoder.load_chunk_decompressed(chunk)?; + results.push((*ph, *lh, data)); } - Ok(false) + Ok(results) } } @@ -394,9 +434,9 @@ mod tests { let modpkg = Modpkg::mount_from_reader(cursor).unwrap(); // Test has_chunk - assert!(modpkg.has_chunk(path, Some(layer_name)).unwrap()); - assert!(modpkg.has_chunk(hex_path, Some(layer_name)).unwrap()); - assert!(!modpkg.has_chunk("nonexistent", Some(layer_name)).unwrap()); + assert!(modpkg.has_chunk(path, Some(layer_name))); + assert!(modpkg.has_chunk(hex_path, Some(layer_name))); + assert!(!modpkg.has_chunk("nonexistent", Some(layer_name))); // Test get_chunk let chunk = modpkg.get_chunk(path, Some(layer_name)).unwrap(); diff --git a/crates/ltk_modpkg/src/project.rs b/crates/ltk_modpkg/src/project.rs index f74eede..ff525fa 100644 --- a/crates/ltk_modpkg/src/project.rs +++ b/crates/ltk_modpkg/src/project.rs @@ -344,16 +344,49 @@ fn build_chunk_from_file( .strip_prefix(layer_dir) .map_err(|e| PackError::Io(io::Error::other(e.to_string())))?; - let chunk_builder = ModpkgChunkBuilder::new() - .with_path(relative_path.as_str()) + let relative_str = relative_path.as_str(); + + // Detect WAD association from the directory structure. + // If the first path component is a WAD name (e.g., "Aatrox.wad.client"), + // strip it from the chunk path and track it via wad_index instead. + let (chunk_path, wad_name) = match relative_str.split_once('/') { + Some((first, rest)) if first.to_ascii_lowercase().ends_with(".wad.client") => { + (rest, Some(first)) + } + _ => (relative_str, None), + }; + + let compression = compression_for_extension(file_path.extension()); + + let mut chunk_builder = ModpkgChunkBuilder::new() + .with_path(chunk_path) .map_err(PackError::Builder)? - .with_compression(ModpkgCompression::Zstd) + .with_compression(compression) .with_layer(&layer.name); + if let Some(wad) = wad_name { + chunk_builder = chunk_builder.with_wad(wad); + } + let path_hash = chunk_builder.path_hash(); Ok((builder.with_chunk(chunk_builder), path_hash)) } +/// Determine the best compression strategy based on file extension. +/// +/// Pre-compressed formats (textures, audio) gain little from zstd and +/// waste CPU time at both compression and decompression. +fn compression_for_extension(ext: Option<&str>) -> ModpkgCompression { + match ext.map(|e| e.to_ascii_lowercase()).as_deref() { + // Pre-compressed textures and images + Some("dds" | "tex" | "webp" | "png" | "jpg" | "jpeg") => ModpkgCompression::None, + // Pre-compressed audio + Some("bnk" | "wpk" | "wem" | "ogg") => ModpkgCompression::None, + // Everything else benefits from compression + _ => ModpkgCompression::Zstd, + } +} + fn add_meta_chunks( mut builder: ModpkgBuilder, project_root: &Utf8Path, diff --git a/crates/ltk_modpkg/src/read.rs b/crates/ltk_modpkg/src/read.rs index 12dce39..14c1d82 100644 --- a/crates/ltk_modpkg/src/read.rs +++ b/crates/ltk_modpkg/src/read.rs @@ -17,7 +17,7 @@ impl Modpkg { const MAGIC: [u8; 8] = *b"_modpkg_"; pub fn mount_from_reader(mut source: TSource) -> Result { - let mut reader = BufReader::new(&mut source); + let mut reader = BufReader::with_capacity(64 * 1024, &mut source); let magic = reader.read_u64::()?; if magic != u64::from_le_bytes(Self::MAGIC) { @@ -44,6 +44,7 @@ impl Modpkg { reader.seek(SeekFrom::Current(((8 - (position % 8)) % 8) as i64))?; let mut chunks = HashMap::new(); + let mut chunks_by_wad_layer: HashMap<(u32, u32), Vec<(u64, u64)>> = HashMap::new(); for _ in 0..chunk_count { let chunk = ModpkgChunk::read(&mut reader)?; let layer_hash = if chunk.layer_index == NO_LAYER_INDEX { @@ -52,7 +53,12 @@ impl Modpkg { layer_indices[chunk.layer_index as usize] }; - chunks.insert((chunk.path_hash, layer_hash), chunk); + let key = (chunk.path_hash, layer_hash); + chunks_by_wad_layer + .entry((chunk.wad_index, chunk.layer_index)) + .or_default() + .push(key); + chunks.insert(key, chunk); } drop(reader); @@ -66,6 +72,7 @@ impl Modpkg { wads_indices, wads, chunks, + chunks_by_wad_layer, source, }) } diff --git a/crates/ltk_overlay/src/modpkg_content.rs b/crates/ltk_overlay/src/modpkg_content.rs index 6ca1c3e..3b69949 100644 --- a/crates/ltk_overlay/src/modpkg_content.rs +++ b/crates/ltk_overlay/src/modpkg_content.rs @@ -9,7 +9,7 @@ use crate::error::{Error, Result}; use camino::{Utf8Path, Utf8PathBuf}; use ltk_mod_project::{ModProject, ModProjectAuthor, ModProjectLayer}; use ltk_modpkg::Modpkg; -use std::collections::HashSet; +use std::collections::HashMap; use std::io::{Read, Seek}; /// Content provider that reads directly from a mounted `.modpkg` archive. @@ -83,32 +83,25 @@ impl ModContentProvider for ModpkgContent { } fn list_layer_wads(&mut self, layer: &str) -> Result> { - let layer_hash = ltk_modpkg::hash_layer_name(layer); - - let mut wad_names: HashSet = HashSet::new(); - - for &(path_hash, chunk_layer_hash) in self.modpkg.chunks.keys() { - if chunk_layer_hash != layer_hash { - continue; - } - - let path = match self.modpkg.chunk_paths.get(&path_hash) { - Some(p) => p, - None => continue, - }; - if path.starts_with("_meta_/") { - continue; - } - - // The WAD name is the first path component (e.g., "Aatrox.wad.client/data/...") - if let Some(wad_name) = path.split('/').next() { - if wad_name.to_ascii_lowercase().ends_with(".wad.client") { - wad_names.insert(wad_name.to_string()); + let layer_index = match self.modpkg.layer_index(layer) { + Some(idx) => idx, + None => return Ok(Vec::new()), + }; + + let mut wad_names = Vec::new(); + for (wad_idx, _) in self.modpkg.wads_indices.iter().enumerate() { + if !self + .modpkg + .chunks_for_wad_layer(wad_idx as u32, layer_index) + .is_empty() + { + if let Some(name) = self.modpkg.wad_name_for_index(wad_idx as u32) { + wad_names.push(name.to_string()); } } } - Ok(wad_names.into_iter().collect()) + Ok(wad_names) } fn read_wad_overrides( @@ -116,39 +109,44 @@ impl ModContentProvider for ModpkgContent { layer: &str, wad_name: &str, ) -> Result)>> { - let layer_hash = ltk_modpkg::hash_layer_name(layer); - let wad_prefix = format!("{}/", wad_name); - - // Collect (path_hash, layer_hash, relative_path) for matching chunks - let matching: Vec<(u64, u64, String)> = self + let layer_index = match self.modpkg.layer_index(layer) { + Some(idx) => idx, + None => return Ok(Vec::new()), + }; + let wad_index = match self.modpkg.wad_index(wad_name) { + Some(idx) => idx, + None => return Ok(Vec::new()), + }; + + // Use secondary index to get chunk keys for this WAD+layer + let chunk_keys: Vec<(u64, u64)> = self .modpkg - .chunks - .keys() - .filter_map(|&(path_hash, chunk_layer_hash)| { - if chunk_layer_hash != layer_hash { - return None; - } + .chunks_for_wad_layer(wad_index, layer_index) + .to_vec(); + + if chunk_keys.is_empty() { + return Ok(Vec::new()); + } + + // Collect relative paths for each chunk (before mutable borrow for batch load) + let rel_paths: HashMap = chunk_keys + .iter() + .filter_map(|&(path_hash, _)| { let path = self.modpkg.chunk_paths.get(&path_hash)?; - if path.starts_with("_meta_/") { - return None; - } - let rel = path.strip_prefix(&wad_prefix)?; - Some((path_hash, chunk_layer_hash, rel.to_string())) + Some((path_hash, path.clone())) }) .collect(); - let mut results = Vec::with_capacity(matching.len()); - for (path_hash, layer_hash, rel_path) in matching { - let bytes = self - .modpkg - .load_chunk_decompressed_by_hash(path_hash, layer_hash) - .map_err(|e| { - Error::Other(format!( - "Failed to decompress modpkg chunk {:016x}: {}", - path_hash, e - )) - })?; - results.push((Utf8PathBuf::from(rel_path), bytes.into_vec())); + // Batch load all chunks in offset-sorted order for sequential I/O + let batch = self.modpkg.load_chunks_batch(&chunk_keys).map_err(|e| { + Error::Other(format!("Failed to batch decompress modpkg chunks: {}", e)) + })?; + + let mut results = Vec::with_capacity(batch.len()); + for (path_hash, _layer_hash, data) in batch { + if let Some(rel) = rel_paths.get(&path_hash) { + results.push((Utf8PathBuf::from(rel.as_str()), data.into_vec())); + } } Ok(results) @@ -157,36 +155,27 @@ impl ModContentProvider for ModpkgContent { fn read_wad_override_file( &mut self, layer: &str, - wad_name: &str, + _wad_name: &str, rel_path: &Utf8Path, ) -> Result> { let layer_hash = ltk_modpkg::hash_layer_name(layer); - let full_path = - ltk_modpkg::utils::normalize_chunk_path(&format!("{}/{}", wad_name, rel_path)); + let normalized = ltk_modpkg::utils::normalize_chunk_path(rel_path.as_str()); + let path_hash = ltk_modpkg::utils::hash_chunk_name(&normalized); - // Compute the path hash from the full WAD-relative path - let path_hash = ltk_modpkg::utils::hash_chunk_name(&full_path); - if self.modpkg.chunks.contains_key(&(path_hash, layer_hash)) { - let bytes = self - .modpkg - .load_chunk_decompressed_by_hash(path_hash, layer_hash) - .map_err(|e| { - Error::Other(format!( - "Failed to decompress modpkg chunk {:016x}: {}", - path_hash, e - )) - })?; - return Ok(bytes.into_vec()); - } - - Err(Error::Other(format!( - "Override file not found in modpkg: {}/{}/{}", - layer, wad_name, rel_path - ))) + let bytes = self + .modpkg + .load_chunk_decompressed_by_hash(path_hash, layer_hash) + .map_err(|e| { + Error::Other(format!( + "Failed to decompress modpkg chunk {:016x}: {}", + path_hash, e + )) + })?; + + Ok(bytes.into_vec()) } fn read_raw_override_file(&mut self, _rel_path: &Utf8Path) -> Result> { - // Modpkg format doesn't have raw overrides — all content is organized by WAD Err(Error::Other( "Modpkg format does not support raw overrides".to_string(), )) @@ -208,11 +197,8 @@ mod tests { use ltk_modpkg::{Modpkg, ModpkgCompression}; use std::io::{Cursor, Write}; - /// Build a modpkg in memory with chunks whose paths use backslashes - /// (simulating a Windows-packed archive) and verify that the overlay - /// content provider can discover WADs and read overrides correctly. #[test] - fn list_layer_wads_with_backslash_paths() { + fn list_layer_wads_with_wad_index() { let scratch = Vec::new(); let mut cursor = Cursor::new(scratch); @@ -220,18 +206,19 @@ mod tests { .with_layer(ModpkgLayerBuilder::base()) .with_chunk( ModpkgChunkBuilder::new() - // Simulate a path produced by Windows glob - .with_path("Graves.wad.client\\data\\characters\\graves\\skin0.bin") + .with_path("data\\characters\\graves\\skin0.bin") .unwrap() .with_compression(ModpkgCompression::None) - .with_layer("base"), + .with_layer("base") + .with_wad("Graves.wad.client"), ) .with_chunk( ModpkgChunkBuilder::new() - .with_path("Graves.wad.client\\assets\\texture.tex") + .with_path("assets\\texture.tex") .unwrap() .with_compression(ModpkgCompression::None) - .with_layer("base"), + .with_layer("base") + .with_wad("Graves.wad.client"), ); builder @@ -251,7 +238,7 @@ mod tests { } #[test] - fn read_wad_overrides_with_backslash_paths() { + fn read_wad_overrides_with_wad_index() { let scratch = Vec::new(); let mut cursor = Cursor::new(scratch); let file_data = b"hello world"; @@ -260,10 +247,11 @@ mod tests { .with_layer(ModpkgLayerBuilder::base()) .with_chunk( ModpkgChunkBuilder::new() - .with_path("Graves.wad.client\\data\\skin0.bin") + .with_path("data\\skin0.bin") .unwrap() .with_compression(ModpkgCompression::None) - .with_layer("base"), + .with_layer("base") + .with_wad("Graves.wad.client"), ); builder @@ -286,7 +274,7 @@ mod tests { } #[test] - fn multi_layer_backslash_paths() { + fn multi_layer_wad_index() { let scratch = Vec::new(); let mut cursor = Cursor::new(scratch); @@ -295,17 +283,19 @@ mod tests { .with_layer(ModpkgLayerBuilder::new("loading-screen").with_priority(1)) .with_chunk( ModpkgChunkBuilder::new() - .with_path("Graves.wad.client\\data\\base_file.bin") + .with_path("data\\base_file.bin") .unwrap() .with_compression(ModpkgCompression::None) - .with_layer("base"), + .with_layer("base") + .with_wad("Graves.wad.client"), ) .with_chunk( ModpkgChunkBuilder::new() - .with_path("Graves.wad.client\\data\\loading.bin") + .with_path("data\\loading.bin") .unwrap() .with_compression(ModpkgCompression::None) - .with_layer("loading-screen"), + .with_layer("loading-screen") + .with_wad("Graves.wad.client"), ); builder