Skip to content

Commit 76afc18

Browse files
authored
Merge pull request #31 from LeagueToolkit/feat/wad-builder
feat: wad builder
2 parents 59eb073 + e201a14 commit 76afc18

File tree

6 files changed

+404
-9
lines changed

6 files changed

+404
-9
lines changed

crates/league-toolkit/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ io-ext = { path = "../io-ext" }
2929
league-primitives = { path = "../league-primitives" }
3030
zstd = { version = "0.13", default-features = false, optional = true }
3131
ruzstd = { version = "0.7", optional = true }
32-
32+
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
33+
itertools = "0.14.0"
3334
serde = { version = "1.0.204", features = ["derive"], optional = true }
3435
paste = "1.0.15"
3536
miette = "7.2.0"
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
use std::io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
2+
3+
use byteorder::{WriteBytesExt, LE};
4+
use flate2::read::GzEncoder;
5+
use io_ext::measure;
6+
use itertools::Itertools;
7+
use xxhash_rust::{xxh3, xxh64};
8+
9+
use crate::league_file::LeagueFileKind;
10+
11+
use super::{WadChunk, WadChunkCompression, WadError};
12+
13+
#[derive(Debug, thiserror::Error)]
14+
pub enum WadBuilderError {
15+
#[error("wad error")]
16+
WadError(#[from] WadError),
17+
18+
#[error("io error")]
19+
IoError(#[from] io::Error),
20+
21+
#[error("unsupported compression type: {0}")]
22+
UnsupportedCompressionType(WadChunkCompression),
23+
}
24+
25+
/// Implements a builder interface for creating WAD files.
26+
///
27+
/// ## This example builds a WAD file in memory
28+
/// ```
29+
/// # use league_toolkit::core::wad::*;
30+
/// # use std::io::{Cursor, Write};
31+
///
32+
/// let mut builder = WadBuilder::default();
33+
/// let scratch = Vec::new();
34+
/// let mut wad_cursor = Cursor::new(scratch);
35+
///
36+
/// builder = builder.with_chunk(WadChunkBuilder::default().with_path("path/to/chunk"));
37+
/// builder.build_to_writer(&mut wad_cursor, |path, cursor| {
38+
/// cursor.write_all(&[0xAA; 100])?;
39+
///
40+
/// Ok(())
41+
/// })
42+
/// .expect("Failed to build WAD");
43+
/// ```
44+
#[derive(Debug, Default)]
45+
pub struct WadBuilder {
46+
chunk_builders: Vec<WadChunkBuilder>,
47+
}
48+
49+
impl WadBuilder {
50+
pub fn with_chunk(mut self, chunk: WadChunkBuilder) -> Self {
51+
self.chunk_builders.push(chunk);
52+
self
53+
}
54+
55+
/// Build the WAD file and write it to the given writer.
56+
///
57+
/// * `writer` - The writer to write the WAD file to.
58+
/// * `provide_chunk_data` - A function that provides the rawdata for each chunk.
59+
pub fn build_to_writer<
60+
TWriter: io::Write + io::Seek,
61+
TChunkDataProvider: Fn(u64, &mut Cursor<Vec<u8>>) -> Result<(), WadBuilderError>,
62+
>(
63+
self,
64+
writer: &mut TWriter,
65+
provide_chunk_data: TChunkDataProvider,
66+
) -> Result<(), WadBuilderError> {
67+
// First we need to write a dummy header and TOC, so we can calculate from where to start writing the chunks
68+
let mut writer = BufWriter::new(writer);
69+
70+
let (_, toc_offset) = self.write_dummy_toc::<TWriter>(&mut writer)?;
71+
72+
// Sort the chunks by path hash, otherwise League wont load the WAD
73+
let ordered_chunks = self
74+
.chunk_builders
75+
.iter()
76+
.sorted_by_key(|chunk| chunk.path)
77+
.collect::<Vec<_>>();
78+
79+
let mut final_chunks = Vec::new();
80+
81+
for chunk in ordered_chunks {
82+
let mut cursor = Cursor::new(Vec::new());
83+
provide_chunk_data(chunk.path, &mut cursor)?;
84+
85+
let chunk_data_size = cursor.get_ref().len();
86+
let (compressed_data, compression) =
87+
Self::compress_chunk_data(cursor.get_ref(), chunk.force_compression)?;
88+
let compressed_data_size = compressed_data.len();
89+
let compressed_checksum = xxh3::xxh3_64(&compressed_data);
90+
91+
let chunk_data_offset = writer.stream_position()?;
92+
writer.write_all(&compressed_data)?;
93+
94+
final_chunks.push(WadChunk {
95+
path_hash: chunk.path,
96+
data_offset: chunk_data_offset as usize,
97+
compressed_size: compressed_data_size,
98+
uncompressed_size: chunk_data_size,
99+
compression_type: compression,
100+
is_duplicated: false,
101+
frame_count: 0,
102+
start_frame: 0,
103+
checksum: compressed_checksum,
104+
});
105+
}
106+
107+
writer.seek(SeekFrom::Start(toc_offset))?;
108+
109+
for chunk in final_chunks {
110+
chunk.write_v3_4(&mut writer)?;
111+
}
112+
113+
Ok(())
114+
}
115+
116+
fn write_dummy_toc<W: io::Write + io::Seek>(
117+
&self,
118+
writer: &mut BufWriter<&mut W>,
119+
) -> Result<(u64, u64), WadBuilderError> {
120+
let (header_toc_size, toc_offset) = measure(writer, |writer| {
121+
// Write the header
122+
writer.write_u16::<LE>(0x5752)?;
123+
writer.write_u8(3)?; // major
124+
writer.write_u8(4)?; // minor
125+
126+
// Write dummy ECDSA signature
127+
writer.write_all(&[0; 256])?;
128+
writer.write_u64::<LE>(0)?;
129+
130+
// Write dummy TOC
131+
writer.write_u32::<LE>(self.chunk_builders.len() as u32)?;
132+
let toc_offset = writer.stream_position()?;
133+
for _ in self.chunk_builders.iter() {
134+
writer.write_all(&[0; 32])?;
135+
}
136+
137+
Ok::<_, WadBuilderError>(toc_offset)
138+
})?;
139+
140+
Ok((header_toc_size, toc_offset))
141+
}
142+
143+
fn compress_chunk_data(
144+
data: &[u8],
145+
force_compression: Option<WadChunkCompression>,
146+
) -> Result<(Vec<u8>, WadChunkCompression), WadBuilderError> {
147+
let (compressed_data, compression) = match force_compression {
148+
Some(compression) => (
149+
Self::compress_chunk_data_by_compression(data, compression)?,
150+
compression,
151+
),
152+
None => {
153+
let kind = LeagueFileKind::identify_from_bytes(data);
154+
let compression = kind.ideal_compression();
155+
let compressed_data = Self::compress_chunk_data_by_compression(data, compression)?;
156+
157+
(compressed_data, compression)
158+
}
159+
};
160+
161+
Ok((compressed_data, compression))
162+
}
163+
164+
fn compress_chunk_data_by_compression(
165+
data: &[u8],
166+
compression: WadChunkCompression,
167+
) -> Result<Vec<u8>, WadBuilderError> {
168+
let mut compressed_data = Vec::new();
169+
match compression {
170+
WadChunkCompression::None => {
171+
compressed_data = data.to_vec();
172+
}
173+
WadChunkCompression::GZip => {
174+
let reader = BufReader::new(data);
175+
let mut encoder = GzEncoder::new(reader, flate2::Compression::default());
176+
177+
encoder.read_to_end(&mut compressed_data)?;
178+
}
179+
WadChunkCompression::Zstd => {
180+
let mut encoder = zstd::Encoder::new(BufWriter::new(&mut compressed_data), 3)?;
181+
encoder.write_all(data)?;
182+
encoder.finish()?;
183+
}
184+
WadChunkCompression::Satellite => {
185+
return Err(WadBuilderError::UnsupportedCompressionType(compression));
186+
}
187+
WadChunkCompression::ZstdMulti => {
188+
return Err(WadBuilderError::UnsupportedCompressionType(compression));
189+
}
190+
}
191+
192+
Ok(compressed_data)
193+
}
194+
}
195+
196+
/// Implements a builder interface for creating WAD chunks.
197+
///
198+
/// # Examples
199+
/// ```
200+
/// # use league_toolkit::core::wad::*;
201+
/// #
202+
/// let builder = WadChunkBuilder::default();
203+
/// builder.with_path("path/to/chunk");
204+
/// builder.with_force_compression(WadChunkCompression::Zstd);
205+
/// ```
206+
#[derive(Debug, Clone, Copy, Default)]
207+
pub struct WadChunkBuilder {
208+
/// The path hash of the chunk. Hashed using xxhash64.
209+
path: u64,
210+
211+
/// If provided, the chunk will be compressed using the given compression type, otherwise the ideal compression will be used.
212+
force_compression: Option<WadChunkCompression>,
213+
}
214+
215+
impl WadChunkBuilder {
216+
pub fn with_path(mut self, path: impl AsRef<str>) -> Self {
217+
self.path = xxh64::xxh64(path.as_ref().to_lowercase().as_bytes(), 0);
218+
self
219+
}
220+
221+
pub fn with_force_compression(mut self, compression: WadChunkCompression) -> Self {
222+
self.force_compression = Some(compression);
223+
self
224+
}
225+
}
226+
227+
#[cfg(test)]
228+
mod tests {
229+
use crate::core::wad::Wad;
230+
231+
use super::*;
232+
233+
#[test]
234+
fn test_wad_builder() {
235+
let scratch = Vec::new();
236+
let mut cursor = Cursor::new(scratch);
237+
238+
let mut builder = WadBuilder::default();
239+
builder = builder.with_chunk(WadChunkBuilder::default().with_path("test1"));
240+
builder = builder.with_chunk(WadChunkBuilder::default().with_path("test2"));
241+
builder = builder.with_chunk(WadChunkBuilder::default().with_path("test3"));
242+
243+
builder
244+
.build_to_writer(&mut cursor, |path, cursor| {
245+
cursor.write_all(&[0xAA; 100])?;
246+
247+
Ok(())
248+
})
249+
.expect("Failed to build WAD");
250+
251+
cursor.set_position(0);
252+
253+
let wad = Wad::mount(cursor).expect("Failed to mount WAD");
254+
assert_eq!(wad.chunks().len(), 3);
255+
256+
let chunk = wad.chunks.get(&xxh64::xxh64(b"test1", 0)).unwrap();
257+
assert_eq!(chunk.path_hash, xxh64::xxh64(b"test1", 0));
258+
assert_eq!(chunk.compressed_size, 17);
259+
assert_eq!(chunk.uncompressed_size, 100);
260+
assert_eq!(chunk.compression_type, WadChunkCompression::Zstd);
261+
}
262+
}

0 commit comments

Comments
 (0)