From 86a06f61009611b2b1c28da7e22318255827f0d4 Mon Sep 17 00:00:00 2001
From: DexalGT
Date: Mon, 23 Mar 2026 19:31:39 +0300
Subject: [PATCH 1/5] feat(cli): improve Windows CMD compatibility and batch
processing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Enable ANSI virtual terminal on Windows for proper color support
- Replace UTF-8 symbols (✓, ✗, •, ═, →) with ASCII equivalents (=>, X, >, =, ->)
- Add batch progress indicators for folder processing
- Show [N/M] progress counter and per-file status
- Update help text to clarify auto-fix behavior
- Improve error handling in batch mode (continue on file errors)
Drag-and-drop folders now work seamlessly with colored output on Windows.
---
crates/hematite-cli/src/args.rs | 9 +-
crates/hematite-cli/src/logging.rs | 161 +++++++++++++++++++++--------
crates/hematite-cli/src/process.rs | 67 ++++++++----
3 files changed, 176 insertions(+), 61 deletions(-)
diff --git a/crates/hematite-cli/src/args.rs b/crates/hematite-cli/src/args.rs
index 378fb98..113efa6 100644
--- a/crates/hematite-cli/src/args.rs
+++ b/crates/hematite-cli/src/args.rs
@@ -26,9 +26,16 @@ use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "hematite-cli")]
#[command(about = "League of Legends custom skin fixer")]
+#[command(
+ long_about = "League of Legends custom skin fixer\n\n\
+ Automatically detects and fixes common issues in custom skins.\n\
+ Supports .bin, .wad.client, .fantome, and .zip files.\n\n\
+ By default, all fixes are applied automatically when no specific flags are provided.\n\
+ Drag and drop files or folders to process multiple skins at once."
+)]
#[command(version)]
pub struct Cli {
- /// Input file or directory to process
+ /// Input file or directory to process (.bin, .wad.client, .fantome, .zip, or folder)
pub input: PathBuf,
/// Output path (default: overwrite input)
diff --git a/crates/hematite-cli/src/logging.rs b/crates/hematite-cli/src/logging.rs
index 884925d..3b455e8 100644
--- a/crates/hematite-cli/src/logging.rs
+++ b/crates/hematite-cli/src/logging.rs
@@ -7,6 +7,12 @@ use tracing_subscriber::EnvFilter;
/// Initialize the tracing subscriber based on verbosity level.
pub fn init(verbosity: &Verbosity, json_mode: bool) {
+ // Enable ANSI color support on Windows
+ #[cfg(windows)]
+ {
+ let _ = colored::control::set_virtual_terminal(true);
+ }
+
let level = match verbosity {
Verbosity::Quiet => Level::ERROR,
Verbosity::Normal => Level::INFO,
@@ -50,117 +56,190 @@ pub fn init(verbosity: &Verbosity, json_mode: bool) {
/// Log a session start banner (human-readable mode only).
pub fn log_session_start(input: &str, selected_fixes: &[String]) {
- println!("{}", "═".repeat(60).bright_cyan());
+ println!();
+ println!("{}", "=".repeat(70).bright_cyan());
println!(
"{}",
- " Hematite — League of Legends Skin Fixer"
+ " Hematite - League of Legends Skin Fixer"
.bright_cyan()
.bold()
);
- println!("{}", "═".repeat(60).bright_cyan());
- println!("{}: {}", "Input".bright_white().bold(), input);
+ println!("{}", "=".repeat(70).bright_cyan());
+ println!();
+ println!(" {}: {}", "Input".bright_white().bold(), input.bright_yellow());
if selected_fixes.is_empty() {
- println!(
- "{}: {}",
- "Mode".bright_white().bold(),
- "Auto-detect (all fixes)".yellow()
- );
+ println!(" {}: {}", "Mode".bright_white().bold(), "Auto-detect (all fixes)".green());
} else {
println!(
- "{}: {} selected",
+ " {}: {} selected",
"Fixes".bright_white().bold(),
- selected_fixes.len()
+ selected_fixes.len().to_string().cyan()
);
for fix_id in selected_fixes {
- println!(" {} {}", "•".bright_cyan(), fix_id.bright_white());
+ println!(" {} {}", ">".bright_cyan(), fix_id.bright_white());
}
}
println!();
+ println!("{}", "=".repeat(70).bright_cyan());
+ println!();
}
/// Log session summary.
pub fn log_session_summary(result: &hematite_types::result::ProcessResult, duration: f64) {
println!();
- println!("{}", "═".repeat(60).bright_cyan());
+ println!("{}", "=".repeat(70).bright_cyan());
println!("{}", " Summary".bright_cyan().bold());
- println!("{}", "═".repeat(60).bright_cyan());
+ println!("{}", "=".repeat(70).bright_cyan());
+ println!();
println!(
- "{}: {}",
+ " {}: {}",
"Files processed".bright_white().bold(),
- result.files_processed
+ result.files_processed.to_string().cyan()
);
println!(
- "{}: {}",
+ " {}: {}",
"Fixes applied".bright_white().bold(),
- result.fixes_applied.to_string().green()
- );
- println!(
- "{}: {}",
- "Fixes failed".bright_white().bold(),
- result.fixes_failed.to_string().red()
+ result.fixes_applied.to_string().green().bold()
);
+ if result.fixes_failed > 0 {
+ println!(
+ " {}: {}",
+ "Fixes failed".bright_white().bold(),
+ result.fixes_failed.to_string().red().bold()
+ );
+ }
+
if !result.errors.is_empty() {
- println!("\n{}:", "Errors".red().bold());
+ println!();
+ println!(" {}:", "Errors".red().bold());
for error in &result.errors {
- println!(" {} {}", "•".red(), error);
+ println!(" {} {}", "X".red().bold(), error);
}
}
- println!("\n{}: {:.2}s", "Duration".bright_white().bold(), duration);
- println!("{}", "═".repeat(60).bright_cyan());
+ println!();
+ println!(" {}: {:.2}s", "Duration".bright_white().bold(), duration.to_string().yellow());
+ println!();
+ println!("{}", "=".repeat(70).bright_cyan());
+ println!();
+
+ // Final status message
+ if result.errors.is_empty() && result.fixes_applied > 0 {
+ println!("{}", " Success! All fixes applied.".green().bold());
+ } else if result.errors.is_empty() {
+ println!("{}", " Complete! No issues detected.".green().bold());
+ } else {
+ println!("{}", " Completed with errors.".yellow().bold());
+ }
+ println!();
}
/// Log check-mode summary (human-readable).
pub fn log_check_summary(result: &hematite_types::result::ProcessResult) {
println!();
- println!("{}", "═".repeat(60).bright_cyan());
+ println!("{}", "=".repeat(70).bright_cyan());
println!("{}", " Check Mode Results".bright_cyan().bold());
- println!("{}", "═".repeat(60).bright_cyan());
+ println!("{}", "=".repeat(70).bright_cyan());
+ println!();
if let Some(info) = &result.check_info {
println!(
- "{}: {}",
+ " {}: {}",
"Champion".bright_white().bold(),
- info.champion.as_deref().unwrap_or("unknown").yellow()
+ info.champion.as_deref().unwrap_or("unknown").yellow().bold()
);
println!(
- "{}: {}",
+ " {}: {}",
"Skin Number".bright_white().bold(),
info.skin_number
.map(|n| n.to_string())
.unwrap_or_else(|| "none".to_string())
.yellow()
+ .bold()
);
println!(
- "{}: {}",
+ " {}: {}",
"Binless Mod".bright_white().bold(),
if info.is_binless {
- "yes".red().to_string()
+ "yes".red().bold()
} else {
- "no".green().to_string()
+ "no".green().bold()
}
);
+ println!();
+
if info.detected_issues.is_empty() {
println!(
- "\n{}",
- "No issues detected — mod looks clean!".green().bold()
+ " {}",
+ "No issues detected - mod looks clean!".green().bold()
);
} else {
println!(
- "\n{} ({}):",
+ " {} ({}):",
"Detected Issues".red().bold(),
info.detected_issues.len()
);
+ println!();
for issue in &info.detected_issues {
- println!(" {} {}", "•".red(), issue.bright_white());
+ println!(" {} {}", ">".red().bold(), issue.bright_white());
}
}
} else {
- println!("{}", "No check info available".yellow());
+ println!(" {}", "No check info available".yellow());
}
- println!("{}", "═".repeat(60).bright_cyan());
+ println!();
+ println!("{}", "=".repeat(70).bright_cyan());
+ println!();
+}
+
+/// Log batch processing start.
+pub fn log_batch_start(count: usize) {
+ println!();
+ println!(
+ " {} {}",
+ "Found".bright_white(),
+ format!("{} file(s) to process", count).cyan().bold()
+ );
+ println!();
+}
+
+/// Log individual file processing in batch mode.
+pub fn log_file_progress(current: usize, total: usize, path: &str) {
+ let progress = format!("[{}/{}]", current, total);
+ println!(
+ " {} {} {}",
+ progress.bright_cyan().bold(),
+ "Processing:".bright_white(),
+ path.yellow()
+ );
+}
+
+/// Log file completion in batch mode.
+pub fn log_file_complete(path: &str, fixes_applied: u32, success: bool) {
+ if success {
+ if fixes_applied > 0 {
+ println!(
+ " {} {} ({} fixes applied)",
+ "OK".green().bold(),
+ path.bright_white(),
+ fixes_applied.to_string().green()
+ );
+ } else {
+ println!(
+ " {} {} (no issues found)",
+ "OK".green().bold(),
+ path.bright_white()
+ );
+ }
+ } else {
+ println!(
+ " {} {}",
+ "FAILED".red().bold(),
+ path.bright_white()
+ );
+ }
}
diff --git a/crates/hematite-cli/src/process.rs b/crates/hematite-cli/src/process.rs
index f03e9fb..da6047b 100644
--- a/crates/hematite-cli/src/process.rs
+++ b/crates/hematite-cli/src/process.rs
@@ -59,21 +59,50 @@ pub fn process_input(
let mut total_result = ProcessResult::default();
if input.is_dir() {
- for entry in WalkDir::new(input) {
- let entry = entry.context("Failed to read directory entry")?;
- let path = entry.path();
-
- if path.is_file() && is_supported_file(path) {
- let result = process_file_with_hashes(
- path,
- config,
- selected_fixes,
- champions,
- dry_run,
- &hash_provider,
- check,
- )?;
- total_result.merge(result);
+ // Collect all files first to show progress
+ let files: Vec<_> = WalkDir::new(input)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.path().is_file() && is_supported_file(e.path()))
+ .map(|e| e.path().to_path_buf())
+ .collect();
+
+ if files.is_empty() {
+ tracing::warn!("No supported files found in directory");
+ return Ok(total_result);
+ }
+
+ // Log batch processing start
+ crate::logging::log_batch_start(files.len());
+
+ // Process each file with progress
+ for (index, path) in files.iter().enumerate() {
+ crate::logging::log_file_progress(index + 1, files.len(), &path.display().to_string());
+
+ match process_file_with_hashes(
+ path,
+ config,
+ selected_fixes,
+ champions,
+ dry_run,
+ &hash_provider,
+ check,
+ ) {
+ Ok(result) => {
+ let fixes_applied = result.fixes_applied;
+ let success = result.errors.is_empty();
+ total_result.merge(result);
+ crate::logging::log_file_complete(
+ &path.display().to_string(),
+ fixes_applied,
+ success,
+ );
+ }
+ Err(e) => {
+ tracing::error!("Failed to process {}: {}", path.display(), e);
+ total_result.errors.push(format!("{}: {}", path.display(), e));
+ crate::logging::log_file_complete(&path.display().to_string(), 0, false);
+ }
}
}
} else {
@@ -244,7 +273,7 @@ fn process_bin_file(
std::fs::write(&output_path, &modified_bytes)
.context("Failed to save modified BIN file")?;
- tracing::info!("✓ Wrote fixed BIN to: {}", output_path.display());
+ tracing::info!("=> Wrote fixed BIN to: {}", output_path.display());
tracing::info!(
" {} fixes applied, {} bytes written",
result.fixes_applied,
@@ -339,7 +368,7 @@ fn process_wad_file(
*bytes = converted_bytes;
conversion_count += 1;
tracing::info!(
- "✓ Converted {} from .{} to .{} ({} → {} bytes)",
+ "=> Converted {} from .{} to .{} ({} -> {} bytes)",
conversion.path,
conversion.from_ext,
conversion.to_ext,
@@ -349,7 +378,7 @@ fn process_wad_file(
}
Err(e) => {
tracing::warn!(
- "✗ Converter '{}' failed for {}: {}",
+ "X Converter '{}' failed for {}: {}",
conversion.converter,
conversion.path,
e
@@ -547,7 +576,7 @@ fn process_wad_file(
Ok(())
})?;
- tracing::info!("✓ Wrote fixed WAD to: {}", output_path.display());
+ tracing::info!("=> Wrote fixed WAD to: {}", output_path.display());
tracing::info!(
" {} chunks included, {} files removed",
chunks_included,
From 65e29eb149cbfe76d56536f59475802f365c231a Mon Sep 17 00:00:00 2001
From: DexalGT
Date: Mon, 23 Mar 2026 19:38:31 +0300
Subject: [PATCH 2/5] fix(cli): add fantome rebuild and press-any-key prompt
- Rebuild .fantome/.zip archives with fixed WAD files
- Create .fixed.fantome output with all non-WAD files preserved
- Add 'Press any key to continue' prompt before exit (except JSON mode)
- Track fixed WAD paths and replace them in the archive
- Preserve original compression methods and file permissions
Fantome files are now properly modified with fix timestamp updated.
---
crates/hematite-cli/src/main.rs | 17 ++++++
crates/hematite-cli/src/process.rs | 98 ++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+)
diff --git a/crates/hematite-cli/src/main.rs b/crates/hematite-cli/src/main.rs
index 7dd35ee..aaa103b 100644
--- a/crates/hematite-cli/src/main.rs
+++ b/crates/hematite-cli/src/main.rs
@@ -78,6 +78,11 @@ fn main() -> Result<()> {
logging::log_session_summary(&result, duration);
}
+ // Wait for user input before exiting (only in non-JSON mode)
+ if !cli.json {
+ wait_for_keypress();
+ }
+
// Exit with appropriate code
if result.errors.is_empty() {
Ok(())
@@ -86,6 +91,18 @@ fn main() -> Result<()> {
}
}
+/// Wait for user to press any key before exiting.
+fn wait_for_keypress() {
+ use std::io::{self, Read, Write};
+
+ print!("\nPress any key to continue...");
+ let _ = io::stdout().flush();
+
+ // Read a single byte (works on both Windows and Unix)
+ let mut buffer = [0u8; 1];
+ let _ = io::stdin().read(&mut buffer);
+}
+
/// Output check-mode results as JSON.
fn output_check_json(result: &hematite_types::result::ProcessResult) -> Result<()> {
if let Some(check_info) = &result.check_info {
diff --git a/crates/hematite-cli/src/process.rs b/crates/hematite-cli/src/process.rs
index da6047b..4e88d2c 100644
--- a/crates/hematite-cli/src/process.rs
+++ b/crates/hematite-cli/src/process.rs
@@ -692,6 +692,8 @@ fn process_fantome_file(
tracing::info!("Found {} WAD file(s) in archive", wad_paths.len());
let mut total_result = ProcessResult::default();
+ let mut fixed_wad_paths = Vec::new();
+
for wad_path in &wad_paths {
let result = process_wad_file(
wad_path,
@@ -702,8 +704,104 @@ fn process_fantome_file(
hash_provider,
check,
)?;
+
+ // Track the fixed WAD path (original.wad.client -> original.fixed.wad.client)
+ let has_fixes = result.fixes_applied > 0;
total_result.merge(result);
+
+ if !dry_run && has_fixes {
+ let fixed_path = wad_path.with_extension("fixed.wad.client");
+ if fixed_path.exists() {
+ fixed_wad_paths.push((wad_path.clone(), fixed_path));
+ }
+ }
+ }
+
+ // Rebuild the fantome/zip archive with fixed WAD files
+ if !dry_run && !fixed_wad_paths.is_empty() && !check {
+ rebuild_fantome_archive(file, &temp_dir, &fixed_wad_paths)?;
+ tracing::info!(
+ "=> Rebuilt fantome with {} fixed WAD file(s)",
+ fixed_wad_paths.len()
+ );
}
Ok(total_result)
}
+
+/// Rebuild a fantome/zip archive, replacing WAD files with their fixed versions.
+fn rebuild_fantome_archive(
+ original_file: &Path,
+ _temp_dir: &tempfile::TempDir,
+ fixed_wads: &[(std::path::PathBuf, std::path::PathBuf)],
+) -> Result<()> {
+ use std::io::{Read, Write};
+
+ // Read the original archive to copy non-WAD files
+ let original_zip = std::fs::File::open(original_file)?;
+ let mut original_archive = zip::ZipArchive::new(std::io::BufReader::new(original_zip))?;
+
+ // Create output path: original.fantome -> original.fixed.fantome
+ let output_path = if original_file.extension().and_then(|e| e.to_str()) == Some("fantome") {
+ original_file.with_extension("fixed.fantome")
+ } else {
+ original_file.with_extension("fixed.zip")
+ };
+
+ let output_file = std::fs::File::create(&output_path)?;
+ let mut output_archive = zip::ZipWriter::new(output_file);
+
+ // Create a map of original WAD paths to fixed WAD paths
+ let fixed_map: std::collections::HashMap = fixed_wads
+ .iter()
+ .map(|(orig, fixed)| {
+ (
+ orig.file_name()
+ .unwrap()
+ .to_string_lossy()
+ .to_string()
+ .replace(".wad.client", "")
+ + ".wad.client",
+ fixed.as_path(),
+ )
+ })
+ .collect();
+
+ // Copy all files from original archive, replacing WADs with fixed versions
+ for i in 0..original_archive.len() {
+ let mut entry = original_archive.by_index(i)?;
+ let entry_name = entry.name().to_string();
+
+ // Use same compression method as original
+ let options = zip::write::FileOptions::default()
+ .compression_method(entry.compression())
+ .unix_permissions(entry.unix_mode().unwrap_or(0o644));
+
+ output_archive.start_file(&entry_name, options)?;
+
+ // Check if this is a WAD file that was fixed
+ let is_wad = entry_name.to_lowercase().ends_with(".wad.client");
+ let fixed_wad = if is_wad {
+ fixed_map.get(&entry_name.to_lowercase())
+ } else {
+ None
+ };
+
+ if let Some(fixed_path) = fixed_wad {
+ // Write the fixed WAD instead of the original
+ let fixed_data = std::fs::read(fixed_path)?;
+ output_archive.write_all(&fixed_data)?;
+ tracing::debug!("Replaced {} with fixed version", entry_name);
+ } else {
+ // Copy original file as-is
+ let mut buffer = Vec::new();
+ entry.read_to_end(&mut buffer)?;
+ output_archive.write_all(&buffer)?;
+ }
+ }
+
+ output_archive.finish()?;
+ tracing::info!("=> Wrote fixed fantome to: {}", output_path.display());
+
+ Ok(())
+}
From e63d65daf147e9aa796ff6d35935d0a891b0dacc Mon Sep 17 00:00:00 2001
From: DexalGT
Date: Mon, 23 Mar 2026 19:41:26 +0300
Subject: [PATCH 3/5] fix(cli): separate WAD-level and BIN-level fixes
- Add filter_bin_fixes() to exclude WAD-level operations from BIN pipeline
- Prevents 'Fix rule not found' errors for bnk_remover, anm_remover, dds_to_tex
- WAD-level fixes: bnk_remover, anm_remover, dds_to_tex (file operations)
- BIN-level fixes: all others (property tree modifications)
Fixes error spam when processing files with all fixes enabled.
---
crates/hematite-cli/src/args.rs | 22 ++++++++++++++++++++++
crates/hematite-cli/src/process.rs | 9 ++++++---
2 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/crates/hematite-cli/src/args.rs b/crates/hematite-cli/src/args.rs
index 113efa6..ad4d5c6 100644
--- a/crates/hematite-cli/src/args.rs
+++ b/crates/hematite-cli/src/args.rs
@@ -128,6 +128,28 @@ const ALL_FIX_IDS: &[&str] = &[
"entry_validator",
];
+/// WAD-level fixes (operate on file lists, not BIN property trees).
+const WAD_LEVEL_FIXES: &[&str] = &["bnk_remover", "anm_remover", "dds_to_tex"];
+
+/// Filter out WAD-level fixes from a fix list (for BIN-only processing).
+pub fn filter_bin_fixes(fixes: &[String]) -> Vec {
+ fixes
+ .iter()
+ .filter(|f| !WAD_LEVEL_FIXES.contains(&f.as_str()))
+ .cloned()
+ .collect()
+}
+
+/// Filter to only WAD-level fixes.
+#[allow(dead_code)]
+pub fn filter_wad_fixes(fixes: &[String]) -> Vec {
+ fixes
+ .iter()
+ .filter(|f| WAD_LEVEL_FIXES.contains(&f.as_str()))
+ .cloned()
+ .collect()
+}
+
/// Collect selected fix IDs based on CLI flags.
///
/// If `--all` is set or no flags are passed, returns all fix IDs.
diff --git a/crates/hematite-cli/src/process.rs b/crates/hematite-cli/src/process.rs
index 4e88d2c..aa79dd7 100644
--- a/crates/hematite-cli/src/process.rs
+++ b/crates/hematite-cli/src/process.rs
@@ -244,8 +244,9 @@ fn process_bin_file(
shader_validator: shader_validator.as_ref(),
};
- // Run fixes
- let mut result = apply_fixes(&mut ctx, config, selected_fixes, dry_run);
+ // Run fixes (filter out WAD-level fixes for standalone BIN)
+ let bin_fixes = crate::args::filter_bin_fixes(selected_fixes);
+ let mut result = apply_fixes(&mut ctx, config, &bin_fixes, dry_run);
// In check mode, populate CheckInfo from detected issues
if check {
@@ -472,7 +473,9 @@ fn process_wad_file(
shader_validator: shader_validator.as_ref(),
};
- let result = apply_fixes(&mut ctx, config, selected_fixes, dry_run);
+ // Filter out WAD-level fixes (they're handled by wad_pipeline above)
+ let bin_fixes = crate::args::filter_bin_fixes(selected_fixes);
+ let result = apply_fixes(&mut ctx, config, &bin_fixes, dry_run);
let fixes_applied = result.fixes_applied;
total_result.merge(result);
From 4c9e79379d7841824e17613eae6461fdb24a7636 Mon Sep 17 00:00:00 2001
From: DexalGT
Date: Mon, 23 Mar 2026 19:46:10 +0300
Subject: [PATCH 4/5] feat(cli): clean up logging output with verbosity levels
- Normal mode: clean output (INFO only), no debug spam
- Verbose mode (-v verbose): show DEBUG logs with timestamps
- Trace mode (-v trace): show TRACE logs for deep debugging
- Quiet mode (-v quiet): errors only
Changes:
- Remove timestamps in normal mode for cleaner output
- Demote chatty logs to DEBUG level (hash loading, file processing steps)
- Only show important INFO logs: 'Saved: ' messages
- Update help text to clarify verbosity levels
Before: 50+ lines of debug spam
After: ~10 lines of clean, colored output
---
crates/hematite-cli/src/args.rs | 7 +++-
crates/hematite-cli/src/logging.rs | 55 ++++++++++++++++--------------
crates/hematite-cli/src/process.rs | 48 +++++++++++++-------------
3 files changed, 60 insertions(+), 50 deletions(-)
diff --git a/crates/hematite-cli/src/args.rs b/crates/hematite-cli/src/args.rs
index ad4d5c6..0244075 100644
--- a/crates/hematite-cli/src/args.rs
+++ b/crates/hematite-cli/src/args.rs
@@ -101,7 +101,12 @@ pub struct Cli {
#[arg(long, help = "Process all skins found in mod (not just primary skin)")]
pub all_skins: bool,
- #[arg(short = 'v', long, default_value = "normal", help = "Verbosity level")]
+ #[arg(
+ short = 'v',
+ long,
+ default_value = "normal",
+ help = "Verbosity: quiet (errors only), normal (clean output), verbose (debug info), trace (all logs)"
+ )]
pub verbosity: Verbosity,
}
diff --git a/crates/hematite-cli/src/logging.rs b/crates/hematite-cli/src/logging.rs
index 3b455e8..6138d28 100644
--- a/crates/hematite-cli/src/logging.rs
+++ b/crates/hematite-cli/src/logging.rs
@@ -13,31 +13,35 @@ pub fn init(verbosity: &Verbosity, json_mode: bool) {
let _ = colored::control::set_virtual_terminal(true);
}
- let level = match verbosity {
- Verbosity::Quiet => Level::ERROR,
- Verbosity::Normal => Level::INFO,
- Verbosity::Verbose => Level::DEBUG,
- Verbosity::Trace => Level::TRACE,
+ let filter = match verbosity {
+ Verbosity::Quiet => {
+ // Only errors
+ EnvFilter::from_default_env()
+ .add_directive(Level::ERROR.into())
+ }
+ Verbosity::Normal => {
+ // INFO and above, no debug spam
+ EnvFilter::from_default_env()
+ .add_directive(Level::INFO.into())
+ }
+ Verbosity::Verbose => {
+ // DEBUG level for hematite crates
+ EnvFilter::from_default_env()
+ .add_directive(Level::INFO.into())
+ .add_directive("hematite_cli=debug".parse().unwrap())
+ .add_directive("hematite_core=debug".parse().unwrap())
+ .add_directive("hematite_ltk=debug".parse().unwrap())
+ }
+ Verbosity::Trace => {
+ // TRACE everything
+ EnvFilter::from_default_env()
+ .add_directive(Level::TRACE.into())
+ .add_directive("hematite_cli=trace".parse().unwrap())
+ .add_directive("hematite_core=trace".parse().unwrap())
+ .add_directive("hematite_ltk=trace".parse().unwrap())
+ }
};
- let filter = EnvFilter::from_default_env()
- .add_directive(level.into())
- .add_directive(
- "hematite_cli=debug"
- .parse()
- .expect("BUG: hardcoded directive is invalid"),
- )
- .add_directive(
- "hematite_core=debug"
- .parse()
- .expect("BUG: hardcoded directive is invalid"),
- )
- .add_directive(
- "hematite_ltk=debug"
- .parse()
- .expect("BUG: hardcoded directive is invalid"),
- );
-
if json_mode {
// JSON output for automation
tracing_subscriber::fmt()
@@ -45,11 +49,12 @@ pub fn init(verbosity: &Verbosity, json_mode: bool) {
.with_env_filter(filter)
.init();
} else {
- // Human-readable colored output
+ // Human-readable colored output (hide timestamps in normal mode)
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
- .with_level(true)
+ .with_level(matches!(verbosity, Verbosity::Verbose | Verbosity::Trace))
+ .without_time()
.init();
}
}
diff --git a/crates/hematite-cli/src/process.rs b/crates/hematite-cli/src/process.rs
index aa79dd7..26632c5 100644
--- a/crates/hematite-cli/src/process.rs
+++ b/crates/hematite-cli/src/process.rs
@@ -29,12 +29,12 @@ fn load_hash_provider() -> Result> {
// Try LMDB first
match LmdbHashProvider::load_from_appdata() {
Ok(provider) => {
- tracing::info!("Using LMDB hash provider");
+ tracing::debug!("Using LMDB hash provider");
return Ok(Arc::new(provider));
}
Err(e) => {
tracing::warn!("LMDB hash provider unavailable: {}", e);
- tracing::info!("Falling back to TXT hash provider");
+ tracing::debug!("Falling back to TXT hash provider");
}
}
@@ -204,7 +204,7 @@ fn process_bin_file(
hash_provider: &Arc,
check: bool,
) -> Result {
- tracing::info!("Processing BIN: {}", file.display());
+ tracing::debug!("Processing BIN: {}", file.display());
// Initialize BIN provider
let bin_provider = LtkBinProvider;
@@ -274,8 +274,8 @@ fn process_bin_file(
std::fs::write(&output_path, &modified_bytes)
.context("Failed to save modified BIN file")?;
- tracing::info!("=> Wrote fixed BIN to: {}", output_path.display());
- tracing::info!(
+ tracing::info!("Saved: {}", output_path.display());
+ tracing::debug!(
" {} fixes applied, {} bytes written",
result.fixes_applied,
modified_bytes.len()
@@ -301,7 +301,7 @@ fn process_wad_file(
use hematite_core::wad_pipeline;
use hematite_ltk::wad_adapter::WadFile;
- tracing::info!("Processing WAD: {}", file.display());
+ tracing::debug!("Processing WAD: {}", file.display());
let bin_provider = LtkBinProvider;
@@ -320,7 +320,7 @@ fn process_wad_file(
.cloned()
.collect();
- tracing::info!(
+ tracing::debug!(
"WAD has {} total entries, {} BIN files",
wad_provider.hash_count(),
bin_chunks.len()
@@ -339,7 +339,7 @@ fn process_wad_file(
// Track WAD-level fixes applied
for wad_fix in &wad_output.applied_fixes {
- tracing::info!(
+ tracing::debug!(
"WAD-level fix '{}' affected {} files",
wad_fix.fix_name,
wad_fix.files_affected
@@ -355,7 +355,7 @@ fn process_wad_file(
let mut conversion_count = 0u32;
if !wad_output.files_to_convert.is_empty() {
- tracing::info!(
+ tracing::debug!(
"Converting {} file formats...",
wad_output.files_to_convert.len()
);
@@ -368,8 +368,8 @@ fn process_wad_file(
let old_size = bytes.len();
*bytes = converted_bytes;
conversion_count += 1;
- tracing::info!(
- "=> Converted {} from .{} to .{} ({} -> {} bytes)",
+ tracing::debug!(
+ "Converted {} from .{} to .{} ({} -> {} bytes)",
conversion.path,
conversion.from_ext,
conversion.to_ext,
@@ -379,7 +379,7 @@ fn process_wad_file(
}
Err(e) => {
tracing::warn!(
- "X Converter '{}' failed for {}: {}",
+ "Converter '{}' failed for {}: {}",
conversion.converter,
conversion.path,
e
@@ -545,7 +545,7 @@ fn process_wad_file(
use std::io::Write;
use xxhash_rust::xxh64::xxh64;
- tracing::info!("Building modified WAD...");
+ tracing::debug!("Building modified WAD...");
let mut builder = WadBuilder::default();
let mut chunks_included = 0;
@@ -579,15 +579,15 @@ fn process_wad_file(
Ok(())
})?;
- tracing::info!("=> Wrote fixed WAD to: {}", output_path.display());
- tracing::info!(
- " {} chunks included, {} files removed",
+ tracing::info!("Saved: {}", output_path.display());
+ tracing::debug!(
+ " {} chunks included, {} files removed, {} fixes applied",
chunks_included,
- shared_files_to_remove.len()
+ shared_files_to_remove.len(),
+ total_result.fixes_applied
);
- tracing::info!(" {} total fixes applied", total_result.fixes_applied);
} else if !dry_run {
- tracing::info!("No changes detected - WAD not modified");
+ tracing::debug!("No changes detected - WAD not modified");
}
Ok(total_result)
@@ -605,7 +605,7 @@ fn process_fantome_file(
hash_provider: &Arc,
check: bool,
) -> Result {
- tracing::info!("Processing Fantome: {}", file.display());
+ tracing::debug!("Processing Fantome: {}", file.display());
let zip_file = std::fs::File::open(file).context("Failed to open fantome/zip file")?;
let mut archive = zip::ZipArchive::new(std::io::BufReader::new(zip_file))
@@ -692,7 +692,7 @@ fn process_fantome_file(
return Ok(ProcessResult::default());
}
- tracing::info!("Found {} WAD file(s) in archive", wad_paths.len());
+ tracing::debug!("Found {} WAD file(s) in archive", wad_paths.len());
let mut total_result = ProcessResult::default();
let mut fixed_wad_paths = Vec::new();
@@ -723,8 +723,8 @@ fn process_fantome_file(
// Rebuild the fantome/zip archive with fixed WAD files
if !dry_run && !fixed_wad_paths.is_empty() && !check {
rebuild_fantome_archive(file, &temp_dir, &fixed_wad_paths)?;
- tracing::info!(
- "=> Rebuilt fantome with {} fixed WAD file(s)",
+ tracing::debug!(
+ "Rebuilt fantome with {} fixed WAD file(s)",
fixed_wad_paths.len()
);
}
@@ -804,7 +804,7 @@ fn rebuild_fantome_archive(
}
output_archive.finish()?;
- tracing::info!("=> Wrote fixed fantome to: {}", output_path.display());
+ tracing::info!("Saved: {}", output_path.display());
Ok(())
}
From 2ed1e792c5c1cddb1a1052076c4564b8044b8e92 Mon Sep 17 00:00:00 2001
From: DexalGT
Date: Mon, 23 Mar 2026 19:51:22 +0300
Subject: [PATCH 5/5] chore: bump version to 0.3.0
- Update README with new features and CLI flags
- Add check mode, animation remover, shader fallback, entry validation
- Update test count to 84 passing tests
- Improve feature list with Windows CMD optimization and batch processing
- Update CLI reference with all new flags
---
Cargo.toml | 2 +-
README.md | 22 ++++++++++++++++------
2 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index e00d79a..b450cd1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
-version = "0.2.0"
+version = "0.3.0"
edition = "2021"
authors = ["RitoShark"]
diff --git a/README.md b/README.md
index 69a21e8..c5a221f 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
-
+
@@ -23,9 +23,12 @@ Fix rules are defined in JSON config, so new fixes can be added without recompil
- **Auto-detect mode** — runs all applicable fixes with zero configuration
- **Config-driven fixes** — add new fix rules via JSON, no code changes needed
- **Full write-back** — modified files are written back to disk (BIN, WAD, Fantome)
-- **Batch processing** — process entire directories of skin files at once
+- **Batch processing** — drag-and-drop folders, process multiple files with progress tracking
+- **Windows CMD optimized** — colored output, ASCII-friendly symbols, proper ANSI support
- **LMDB hash system** — loads 1.8M game hashes in under 1 second
- **Remote config** — fetches latest fix rules from GitHub with offline fallback
+- **Check mode** — detect issues without modifying files, shows champion/skin info
+- **Verbosity levels** — clean output by default, verbose mode for debugging
- **Security hardened** — ZIP bomb protection, path traversal prevention, size limits
- **Dry-run mode** — preview what would be fixed before modifying files
- **JSON output** — machine-readable results for automation pipelines
@@ -61,7 +64,10 @@ hematite-cli "skin.fantome" --json > results.json
| **Broken Particles** | Fixes particle texture paths recursively | `--particles` |
| **Champion Data** | Removes outdated champion BIN entries | `--remove-champion-bins` |
| **Audio Files** | Removes BNK files with incompatible Wwise versions | `--remove-bnk` |
+| **Animations** | Removes .anm animation files from mod | `--remove-anm` |
| **VFX Shape** | Migrates VFX shape data to 14.1+ format | `--vfx-shape` |
+| **Invalid Shaders** | Replaces invalid shader references with closest match | `--fix-shaders` |
+| **Unreferenced Entries** | Removes CAD/AnimGraph/GearSkinUpgrade entries | `--validate-entries` |
Use `--all` or pass no flags to apply everything.
@@ -79,12 +85,13 @@ Use `--all` or pass no flags to apply everything.
hematite-cli [OPTIONS]
Arguments:
- File or directory to process
+ File or directory to process (.bin, .wad.client, .fantome, .zip, or folder)
Options:
-o, --output Output path (default: creates .fixed.* next to input)
-a, --all Enable all fixes
--dry-run Show what would be fixed without modifying files
+ --check Check mode: detect issues without fixing, show skin info
--json JSON output for automation
-v, --verbosity Verbosity: quiet, normal, verbose, trace [default: normal]
--small-mod Skip fallback assets (for texture-only mods)
@@ -97,7 +104,10 @@ Fix flags:
--particles Fix broken particle textures
--remove-champion-bins Remove outdated champion data
--remove-bnk Remove incompatible audio files
+ --remove-anm Remove .anm animation files
--vfx-shape Fix VFX shape format (14.1+)
+ --fix-shaders Fix invalid shader references
+ --validate-entries Remove unreferenced entries
-h, --help Print help
-V, --version Print version
@@ -191,7 +201,7 @@ cargo build --release --bin hematite-cli
### Test
```bash
-cargo test --workspace # 76 tests
+cargo test --workspace # 84 tests
cargo clippy --workspace # Lint check
cargo fmt --all -- --check # Format check
```
@@ -201,8 +211,8 @@ cargo fmt --all -- --check # Format check
Releases are automated via GitHub Actions. Push a version tag to trigger:
```bash
-git tag v0.2.0
-git push origin v0.2.0
+git tag v0.3.0
+git push origin v0.3.0
# CI: generates changelog (git-cliff) → builds binary → creates GitHub Release
```