mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-03 03:15:28 -05:00
Everything but the no-shared flag is working right now. Getting no-shared to work properly will take a little more work because right now there's nothing to guarantee that the content records are synced between the TMD and content objects in a title. This means that updating any content in a title will result in the records being out of sync and the written TMD will not match the actual state of the content when it was dumped. To mitigate this, I intend on making the content records in the content struct a reference to the content records in the TMD, so that they are the same object and therefore always in sync.
174 lines
8.6 KiB
Rust
174 lines
8.6 KiB
Rust
// archive/theme.rs from ruswtii (c) 2025 NinjaCheetah & Contributors
|
|
// https://github.com/NinjaCheetah/rustwii
|
|
//
|
|
// Code for the theme building commands in the rustwii CLI.
|
|
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::io::Cursor;
|
|
use std::path::{Path, PathBuf};
|
|
use anyhow::{bail, Context, Result};
|
|
use clap::Subcommand;
|
|
use ini::{Ini, ParseOption};
|
|
use tempfile::Builder;
|
|
use zip::ZipArchive;
|
|
use rustwii::archive::{ash, lz77, u8};
|
|
use crate::archive::u8::{pack_dir_recursive, unpack_dir_recursive};
|
|
|
|
#[derive(Subcommand)]
|
|
#[command(arg_required_else_help = true)]
|
|
pub enum Commands {
|
|
/// Apply an MYM theme to the Wii Menu
|
|
ApplyMym {
|
|
/// The path to the source MYM file to apply
|
|
mym: String,
|
|
/// The path to the base Wii Menu asset archive (000000xx.app)
|
|
base: String,
|
|
/// The file to output the finished theme to (<filename>.csm)
|
|
output: String,
|
|
}
|
|
}
|
|
|
|
pub fn theme_apply_mym(mym: &str, base: &str, output: &str) -> Result<()> {
|
|
let mym_path = Path::new(mym);
|
|
if !mym_path.exists() {
|
|
bail!("Theme file \"{}\" could not be found.", mym);
|
|
}
|
|
|
|
let base_path = Path::new(base);
|
|
if !base_path.exists() {
|
|
bail!("Base asset file \"{}\" could not be found.", base);
|
|
}
|
|
|
|
let out_path = PathBuf::from(output);
|
|
|
|
// Create the temporary work directory and extract the mym file to it.
|
|
let work_dir = Builder::new().prefix("mym_apply_").tempdir()?;
|
|
let mym_dir = work_dir.path().join("mym_work");
|
|
let mym_buf = fs::read(mym_path).with_context(|| format!("Failed to open theme file \"{}\" for reading.", mym_path.display()))?;
|
|
ZipArchive::extract(&mut ZipArchive::new(Cursor::new(mym_buf))?, &mym_dir)?;
|
|
|
|
// Load the mym ini file. Escapes have to be disabled so that Windows-formatted paths are
|
|
// loaded correct.
|
|
let mym_ini = Ini::load_from_file_opt(
|
|
mym_dir.join("mym.ini"),
|
|
ParseOption { enabled_escape: false, ..Default::default() }
|
|
).with_context(|| "Failed to load theme config file. This theme may be invalid!")?;
|
|
|
|
// Extract the base asset archive to the temporary dir.
|
|
let base_dir = work_dir.path().join("base_work");
|
|
fs::create_dir(&base_dir)?;
|
|
let assets_u8 = u8::U8Directory::from_bytes(fs::read(base_path).with_context(|| format!("Base asset file \"{}\" could not be read.", base_path.display()))?.into_boxed_slice())?;
|
|
unpack_dir_recursive(&assets_u8, base_dir.clone()).expect("Failed to extract base assets, they may be invalid!");
|
|
|
|
// Store any nested containers that we extract so that they can be re-packed later.
|
|
let mut extracted_containers: HashMap<String, PathBuf> = HashMap::new();
|
|
|
|
// Iterate through the ini file and apply modifications as necessary.
|
|
for (sec, prop) in mym_ini.iter() {
|
|
if let Some(sec) = sec {
|
|
if sec.contains("sdta") {
|
|
// Validate that the file and source keys exist, and then build a path to the
|
|
// source file.
|
|
if !prop.contains_key("file") || !prop.contains_key("source") {
|
|
bail!("Theme config entry \"{}\" is invalid and cannot be applied.", sec)
|
|
}
|
|
let source_parts: Vec<&str> = prop.get("source").unwrap().split("\\").collect();
|
|
let mut source_path = mym_dir.clone();
|
|
source_path.extend(source_parts);
|
|
|
|
if !source_path.exists() {
|
|
bail!("Required source file \"{}\" could not be found! This theme may be invalid.", prop.get("source").unwrap())
|
|
}
|
|
|
|
println!("Applying static data file \"{}\" from theme...", source_path.file_name().unwrap().to_str().unwrap());
|
|
let target_parts: Vec<&str> = prop.get("file").unwrap().split("\\").collect();
|
|
let mut target_path = base_dir.clone();
|
|
target_path.extend(target_parts);
|
|
fs::copy(source_path, target_path).expect("Failed to copy asset from theme.");
|
|
} else if sec.contains("cont") {
|
|
// Validate that the file key exists and that container specified exists.
|
|
if !prop.contains_key("file") {
|
|
bail!("Theme config entry \"{}\" is invalid and cannot be applied.", sec)
|
|
}
|
|
let container_parts: Vec<&str> = prop.get("file").unwrap().split("\\").collect();
|
|
let mut container_path = base_dir.clone();
|
|
container_path.extend(container_parts);
|
|
|
|
if !container_path.exists() {
|
|
bail!("Required base container \"{}\" could not be found! The base assets or theme may be invalid.", prop.get("file").unwrap())
|
|
}
|
|
|
|
// Buffer in the container file, check its magic number, and decompress it if
|
|
// necessary.
|
|
println!("Unpacking base container \"{}\" for modification...", container_path.file_name().unwrap().to_str().unwrap());
|
|
let container_data = fs::read(&container_path)?;
|
|
let decompressed_container = if &container_data[0..4] == b"LZ77" {
|
|
println!(" - Decompressing LZ77 data...");
|
|
lz77::decompress_lz77(&container_data)?
|
|
} else if &container_data[0..4] == b"ASH0" {
|
|
println!(" - Decompressing ASH data...");
|
|
ash::decompress_ash(&container_data, None, None)?
|
|
} else {
|
|
container_data
|
|
};
|
|
|
|
// Load the unpacked archive, bailing if it still isn't a U8 archive.
|
|
if &decompressed_container[0..4] != b"\x55\xAA\x38\x2D" {
|
|
bail!("Required base container \"{}\" is not a U8 archive. The base assets may be invalid.", container_path.file_name().unwrap().display())
|
|
}
|
|
|
|
// Extracted container name should follow the format:
|
|
// <file_name>_<extension>_out
|
|
let extracted_container_name = container_path
|
|
.file_name().unwrap()
|
|
.to_str().unwrap().replace(".", "_")
|
|
+ "_out";
|
|
let extracted_container_path = container_path.parent().unwrap().join(extracted_container_name);
|
|
fs::create_dir(&extracted_container_path)?;
|
|
let u8_root = u8::U8Directory::from_bytes(decompressed_container.into_boxed_slice()).with_context(|| "Failed to extract base container! The base assets may be invalid.")?;
|
|
|
|
// Finally, unpack the specified container to the created path and register it as
|
|
// an extracted container so that we can repack it later.
|
|
unpack_dir_recursive(&u8_root, extracted_container_path.clone())?;
|
|
extracted_containers.insert(
|
|
container_path.file_name().unwrap().to_str().unwrap().to_owned(),
|
|
extracted_container_path
|
|
);
|
|
println!(" - Done.");
|
|
} else {
|
|
bail!("Theme config file contains unknown or unsupported key \"{}\"!", sec)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iterate over any containers we unpacked so we can repack them and clean up the unpacked
|
|
// folder.
|
|
println!("Repacking extracted containers...");
|
|
for container in extracted_containers {
|
|
// Add the original file name to the parent of the extracted dir, and that's where the
|
|
// repacked container should go.
|
|
println!(" - Repacking container \"{}\"...", container.0);
|
|
let repacked_container_path = container.1.parent().unwrap().join(container.0.clone());
|
|
let mut u8_root = u8::U8Directory::new(String::new());
|
|
pack_dir_recursive(&mut u8_root, container.1.clone()).with_context(|| format!("Failed to repack extracted base container \"{}\". An unknown error occurred.", container.0))?;
|
|
|
|
// Always compress the repacked archive with LZ77 compression.
|
|
let compressed_container = lz77::compress_lz77(&u8_root.to_bytes()?)?;
|
|
fs::write(repacked_container_path, compressed_container)?;
|
|
|
|
// Erase the extracted container directory so it doesn't get packed into the final themed
|
|
// archive.
|
|
fs::remove_dir_all(container.1)?;
|
|
println!(" - Done.");
|
|
}
|
|
|
|
// Theme applied, re-pack the base dir and write it out to the specified path.
|
|
let mut finished_u8 = u8::U8Directory::new(String::new());
|
|
pack_dir_recursive(&mut finished_u8, base_dir).expect("Failed to pack finalized theme!");
|
|
fs::write(&out_path, &finished_u8.to_bytes()?).with_context(|| format!("Could not open output file \"{}\" for writing.", out_path.display()))?;
|
|
println!("\nSuccessfully applied theme \"{}\" to output file \"{}\"!", mym, output);
|
|
|
|
Ok(())
|
|
}
|