Skip to content

Commit 9d582d5

Browse files
pack-bundle command + write callback
1 parent 5de4bcc commit 9d582d5

File tree

3 files changed

+168
-20
lines changed

3 files changed

+168
-20
lines changed

src/bin/ffbuildtool.rs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ enum Commands {
2727
ReadBundle(ReadBundleArgs),
2828
#[cfg(feature = "lzma")]
2929
ExtractBundle(ExtractBundleArgs),
30+
#[cfg(feature = "lzma")]
31+
PackBundle(PackBundleArgs),
3032
}
3133

3234
#[derive(Args, Debug)]
@@ -101,8 +103,8 @@ struct ValidateBuildArgs {
101103
#[derive(Args, Debug)]
102104
struct ReadBundleArgs {
103105
/// Path to the compressed asset bundle
104-
#[clap(short = 'b', long)]
105-
bundle_path: String,
106+
#[clap(short = 'i', long)]
107+
input_bundle: String,
106108

107109
/// Whether to calculate the hashes of each file in the bundle
108110
#[clap(short = 'c', long, action)]
@@ -113,14 +115,30 @@ struct ReadBundleArgs {
113115
#[derive(Args, Debug)]
114116
struct ExtractBundleArgs {
115117
/// Path to the compressed asset bundle
116-
#[clap(short = 'b', long)]
117-
bundle_path: String,
118+
#[clap(short = 'i', long)]
119+
input_bundle: String,
118120

119121
/// Path to the output directory. If not specified, will be extracted to a directory named after the bundle.
120122
#[clap(short = 'o', long)]
121123
output_dir: Option<String>,
122124
}
123125

126+
#[cfg(feature = "lzma")]
127+
#[derive(Args, Debug)]
128+
struct PackBundleArgs {
129+
/// Path to the input directory
130+
#[clap(short = 'i', long)]
131+
input_dir: String,
132+
133+
/// Path to the output bundle
134+
#[clap(short = 'o', long)]
135+
output_bundle: String,
136+
137+
/// Compression level to use
138+
#[clap(short = 'l', long, default_value = "6")]
139+
compression_level: u32,
140+
}
141+
124142
#[derive(Debug, PartialEq, Eq)]
125143
enum ItemState {
126144
Downloading,
@@ -236,6 +254,8 @@ async fn main() -> Result<(), String> {
236254
Commands::ReadBundle(args) => read_bundle(args).await,
237255
#[cfg(feature = "lzma")]
238256
Commands::ExtractBundle(args) => extract_bundle(args).await,
257+
#[cfg(feature = "lzma")]
258+
Commands::PackBundle(args) => pack_bundle(args).await,
239259
}
240260
}
241261

@@ -359,8 +379,10 @@ async fn validate_build(args: ValidateBuildArgs) -> Result<(), String> {
359379
async fn read_bundle(args: ReadBundleArgs) -> Result<(), String> {
360380
use std::time::Instant;
361381

382+
use ffbuildtool::bundle::AssetBundle;
383+
362384
let start = Instant::now();
363-
let (header, mut bundle) = ffbuildtool::bundle::AssetBundle::from_file(&args.bundle_path)?;
385+
let (header, mut bundle) = AssetBundle::from_file(&args.input_bundle)?;
364386
println!("Bundle read in {}ms", start.elapsed().as_millis());
365387

366388
if args.calculate_hashes {
@@ -380,26 +402,71 @@ async fn read_bundle(args: ReadBundleArgs) -> Result<(), String> {
380402
async fn extract_bundle(args: ExtractBundleArgs) -> Result<(), String> {
381403
use std::{path::PathBuf, time::Instant};
382404

405+
use ffbuildtool::{bundle::AssetBundle, util};
406+
383407
let start = Instant::now();
384-
let (_, bundle) = ffbuildtool::bundle::AssetBundle::from_file(&args.bundle_path)?;
408+
let (header, bundle) = AssetBundle::from_file(&args.input_bundle)?;
385409
println!("Bundle read in {}ms", start.elapsed().as_millis());
410+
println!(
411+
"------------------------\n{}\n------------------------\n{}",
412+
header, bundle
413+
);
386414

387415
let output_dir = args.output_dir.unwrap_or({
388-
let bundle_name = ffbuildtool::util::get_file_name_without_parent(&args.bundle_path);
389-
let bundle_name_url_encoded = ffbuildtool::util::url_encode(bundle_name);
390-
let bundle_path = PathBuf::from(&args.bundle_path);
416+
let bundle_name = util::get_file_name_without_parent(&args.input_bundle);
417+
let bundle_name_url_encoded = util::url_encode(bundle_name);
418+
let bundle_path = PathBuf::from(&args.input_bundle);
391419
bundle_path
392420
.parent()
393421
.unwrap_or(&bundle_path)
394422
.join(bundle_name_url_encoded)
395423
.to_string_lossy()
396424
.to_string()
397425
});
398-
println!("Extracting bundle {} to {}", args.bundle_path, output_dir);
426+
println!("Extracting bundle {} to {}", args.input_bundle, output_dir);
399427

400428
let start = Instant::now();
401429
bundle.extract_files(&output_dir)?;
402430
println!("Bundle extracted in {}ms", start.elapsed().as_millis());
403431

404432
Ok(())
405433
}
434+
435+
#[cfg(feature = "lzma")]
436+
async fn pack_bundle(args: PackBundleArgs) -> Result<(), String> {
437+
use std::{sync::LazyLock, time::Instant};
438+
439+
use ffbuildtool::bundle::AssetBundle;
440+
441+
fn cb(level_idx: usize, file: usize, total_files: usize, current_file_name: String) {
442+
static PBS: OnceLock<Mutex<HashMap<usize, ProgressBar>>> = OnceLock::new();
443+
static PB_TEMPLATE: LazyLock<ProgressStyle> = LazyLock::new(|| {
444+
ProgressStyle::default_bar()
445+
.template("{bar:40} {pos} / {len} {msg}")
446+
.unwrap()
447+
});
448+
449+
let mut pbs = PBS
450+
.get_or_init(|| Mutex::new(HashMap::new()))
451+
.lock()
452+
.unwrap();
453+
let pb = pbs.entry(level_idx).or_insert_with(|| {
454+
let pb = ProgressBar::new(total_files as u64);
455+
pb.set_style(PB_TEMPLATE.clone());
456+
pb
457+
});
458+
459+
pb.set_position(file as u64);
460+
pb.set_message(current_file_name);
461+
}
462+
463+
let start = Instant::now();
464+
let bundle = AssetBundle::from_directory(&args.input_dir)?;
465+
println!("Files read in {}ms", start.elapsed().as_millis());
466+
467+
let start = Instant::now();
468+
bundle.to_file(&args.output_bundle, args.compression_level, Some(cb))?;
469+
println!("Bundle created in {}ms", start.elapsed().as_millis());
470+
471+
Ok(())
472+
}

src/bundle.rs

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use lzma::{LzmaReader, LzmaWriter};
1111

1212
use crate::{util, Error, FileInfo};
1313

14+
// level index, file index, total files, file name
15+
pub type CompressionCallback = fn(usize, usize, usize, String);
16+
1417
fn read_u32<T: Read>(reader: &mut T) -> Result<u32, Error> {
1518
let mut buf = [0; 4];
1619
reader.read_exact(&mut buf)?;
@@ -92,7 +95,12 @@ impl std::fmt::Display for AssetBundleHeader {
9295
i, level.compressed_end, level.uncompressed_end
9396
)?;
9497
}
95-
write!(f, "Bundle size: {}", self.bundle_size)
98+
write!(
99+
f,
100+
"Bundle size: {} ({} bytes)",
101+
util::bytes_to_human_readable(self.bundle_size),
102+
self.bundle_size
103+
)
96104
}
97105
}
98106
impl AssetBundleHeader {
@@ -292,15 +300,15 @@ impl std::fmt::Display for LevelFile {
292300
f,
293301
"{} - {} ({} bytes) - {}",
294302
self.name,
295-
util::bytes_to_human_readable(self.data.len() as u64),
303+
util::bytes_to_human_readable(self.data.len() as u32),
296304
self.data.len(),
297305
hash
298306
),
299307
None => write!(
300308
f,
301309
"{} - {} ({} bytes)",
302310
self.name,
303-
util::bytes_to_human_readable(self.data.len() as u64),
311+
util::bytes_to_human_readable(self.data.len() as u32),
304312
self.data.len()
305313
),
306314
}
@@ -336,15 +344,29 @@ impl Level {
336344
Ok(Self { files })
337345
}
338346

339-
fn write<W: Write>(&self, writer: &mut W, compression: u32) -> Result<usize, Error> {
347+
fn write<W: Write>(
348+
&self,
349+
writer: &mut W,
350+
compression: u32,
351+
level_idx: usize,
352+
callback: Option<CompressionCallback>,
353+
) -> Result<usize, Error> {
340354
let mut writer = Counter::new(LzmaWriter::new_compressor(writer, compression)?);
341355
let header = self.gen_header();
342356
header.write(&mut writer)?;
343357

358+
let num_files = header.files.len();
344359
for (idx, file) in header.files.iter().enumerate() {
345360
let padding_size = file.offset as usize - writer.writer_bytes();
346361
writer.write_all(&vec![0; padding_size])?;
347362
writer.write_all(&self.files[idx].data)?;
363+
if let Some(callback) = callback {
364+
callback(level_idx, idx, num_files, file.name.clone());
365+
}
366+
}
367+
368+
if let Some(callback) = callback {
369+
callback(level_idx, num_files, num_files, "Done".to_string());
348370
}
349371

350372
// pad to 4 bytes
@@ -442,14 +464,20 @@ impl AssetBundle {
442464
Ok((header, Self { levels }))
443465
}
444466

445-
fn write<W: Write>(&self, writer: &mut W, compression: u32) -> Result<(), Error> {
467+
fn write<W: Write>(
468+
&self,
469+
writer: &mut W,
470+
compression: u32,
471+
callback: Option<CompressionCallback>,
472+
) -> Result<(), Error> {
446473
let mut buf = Vec::new();
447474
let mut buf_writer = Counter::new(&mut buf);
448475
let mut uncompressed_bytes_written = 0;
449476

450477
let mut level_ends = Vec::new();
451-
for level in &self.levels {
452-
uncompressed_bytes_written += level.write(&mut buf_writer, compression)?;
478+
for (idx, level) in self.levels.iter().enumerate() {
479+
uncompressed_bytes_written +=
480+
level.write(&mut buf_writer, compression, idx, callback)?;
453481
let uncompressed_end = uncompressed_bytes_written as u32;
454482
let compressed_end = buf_writer.writer_bytes() as u32;
455483
level_ends.push(LevelEnds {
@@ -492,11 +520,57 @@ impl AssetBundle {
492520
.map_err(|e| format!("Couldn't read bundle: {}", e))
493521
}
494522

495-
pub fn to_file(&self, path: &str, compression_level: u32) -> Result<(), String> {
523+
pub fn from_directory(path: &str) -> Result<Self, String> {
524+
// each subdirectory with the name `levelX` contains the files for that level.
525+
// they must be in order-- starting from level0-- for their files to be included.
526+
// all loose files get put at the end of level0.
527+
let root_path = PathBuf::from(path);
528+
if !root_path.is_dir() {
529+
return Err(format!("Invalid root directory: {}", path));
530+
}
531+
532+
let mut levels = Vec::new();
533+
for i in 0.. {
534+
let level_dir = root_path.join(format!("level{}", i));
535+
if !level_dir.as_path().is_dir() {
536+
break;
537+
}
538+
539+
let Ok(files) = Self::get_level_files_from_dir(&level_dir) else {
540+
return Err(format!(
541+
"Couldn't read files in dir: {}",
542+
level_dir.display()
543+
));
544+
};
545+
546+
levels.push(Level { files });
547+
}
548+
549+
let Ok(loose_files) = Self::get_level_files_from_dir(&root_path) else {
550+
return Err(format!(
551+
"Couldn't read files in dir: {}",
552+
root_path.display()
553+
));
554+
};
555+
if levels.is_empty() {
556+
levels.push(Level { files: loose_files });
557+
} else {
558+
levels[0].files.extend(loose_files);
559+
}
560+
561+
Ok(Self { levels })
562+
}
563+
564+
pub fn to_file(
565+
&self,
566+
path: &str,
567+
compression_level: u32,
568+
callback: Option<CompressionCallback>,
569+
) -> Result<(), String> {
496570
let file =
497571
File::create(path).map_err(|e| format!("Couldn't create file {}: {}", path, e))?;
498572
let mut writer = BufWriter::new(file);
499-
self.write(&mut writer, compression_level)
573+
self.write(&mut writer, compression_level, callback)
500574
.map_err(|e| format!("Couldn't write bundle: {}", e))?;
501575
writer
502576
.flush()
@@ -553,4 +627,11 @@ impl AssetBundle {
553627

554628
Ok(result)
555629
}
630+
631+
pub fn get_num_files(&self, level: usize) -> Result<usize, Error> {
632+
if level >= self.levels.len() {
633+
return Err(format!("Level {} does not exist", level).into());
634+
}
635+
Ok(self.levels[level].files.len())
636+
}
556637
}

src/util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ pub fn file_path_to_uri(file_path: &str) -> String {
266266
format!("file:///{}", path)
267267
}
268268

269-
pub fn bytes_to_human_readable(bytes: u64) -> String {
269+
pub fn bytes_to_human_readable(bytes: u32) -> String {
270270
let units = ["B", "KB", "MB", "GB"];
271271
let mut bytes = bytes as f64;
272272
let mut unit = 0;

0 commit comments

Comments
 (0)