#![feature(concat_bytes)]
#![feature(int_roundings)]
mod openingtitle_filename_constants;
mod u8_fnt;
mod util;
use std::collections::HashMap;
use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
use std::str::FromStr;
use enumflags2::{bitflags, BitFlags};
use log::{debug, info, trace};
use thiserror::Error;
use crate::openingtitle_filename_constants::ALL_FILENAMES;
use crate::u8_fnt::{U8FileNode, U8FolderNode, U8Node};
pub use crate::u8_fnt::ParseU8Error;
const ANIM_FOLDER_PATH: &str = "/arc/anim";
const BLYT_FOLDER_PATH: &str = "/arc/blyt";
#[bitflags]
#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum Region {
P,
E,
J,
K,
W,
C,
}
pub type RegionBitFlags = BitFlags<Region, u32>;
impl From<&Region> for RegionBitFlags {
fn from(region: &Region) -> RegionBitFlags {
RegionBitFlags::from(*region)
}
}
impl Region {
pub const DEFAULT_ORDER: [Self; 6] = [Self::P, Self::E, Self::J, Self::K, Self::W, Self::C];
}
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ParseRegionError {
#[error("unknown region name {0:?}")]
UnknownRegionName(String),
}
impl FromStr for Region {
type Err = ParseRegionError;
fn from_str(s: &str) -> Result<Self, ParseRegionError> {
match &s.to_uppercase() as &str {
"P" => Ok(Self::P),
"E" => Ok(Self::E),
"J" => Ok(Self::J),
"K" => Ok(Self::K),
"W" => Ok(Self::W),
"C" => Ok(Self::C),
_ => Err(ParseRegionError::UnknownRegionName(s.to_owned())),
}
}
}
impl From<Region> for &str {
fn from(value: Region) -> Self {
match value {
Region::P => "P",
Region::E => "E",
Region::J => "J",
Region::K => "K",
Region::W => "W",
Region::C => "C",
}
}
}
impl From<&Region> for &str {
fn from(value: &Region) -> Self {
Self::from(*value)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub enum ConflictStrategy {
#[default]
Fail,
Overwrite,
}
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ConvertOpeningTitleBetweenRegionsError {
#[error("invalid U8 file")]
InvalidU8File(#[from] u8_fnt::ParseU8Error),
#[error("{0}")]
InvalidOpeningTitleStructure(String),
#[error("{0} not found")]
MissingFiles(String),
#[error("conflicting files: {0:?} and {1:?} are different")]
FileDataConflict(String, String),
#[error("{0:?} already exists")]
FilenameAlreadyExists(String),
#[error("I/O error")]
IoError(#[from] io::Error),
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct NamedU8FileNode {
node: U8FileNode,
filename: String,
}
#[derive(Debug, PartialEq, Eq)]
struct OptionalNamedRegionalFiles {
in_press_brlan: Option<NamedU8FileNode>,
in_title_brlan: Option<NamedU8FileNode>,
loop_press_brlan: Option<NamedU8FileNode>,
out_press_brlan: Option<NamedU8FileNode>,
brlyt: Option<NamedU8FileNode>,
}
#[derive(Debug, PartialEq, Eq)]
struct RegionalFiles {
in_press_brlan: U8FileNode,
in_title_brlan: U8FileNode,
loop_press_brlan: U8FileNode,
out_press_brlan: U8FileNode,
brlyt: U8FileNode,
}
#[allow(dead_code)] fn get_anim_folder(fnt: &U8Node) -> Result<&U8FolderNode, ConvertOpeningTitleBetweenRegionsError> {
fnt.get(ANIM_FOLDER_PATH)
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} folder not found",
ANIM_FOLDER_PATH
))
})?
.as_folder()
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} wasn't a folder",
ANIM_FOLDER_PATH
))
})
}
#[allow(dead_code)]
fn get_mut_anim_folder(
fnt: &mut U8Node,
) -> Result<&mut U8FolderNode, ConvertOpeningTitleBetweenRegionsError> {
fnt.get_mut(ANIM_FOLDER_PATH)
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} folder not found",
ANIM_FOLDER_PATH
))
})?
.as_mut_folder()
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} wasn't a folder",
ANIM_FOLDER_PATH
))
})
}
#[allow(dead_code)] fn get_blyt_folder(fnt: &U8Node) -> Result<&U8FolderNode, ConvertOpeningTitleBetweenRegionsError> {
fnt.get(BLYT_FOLDER_PATH)
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} folder not found",
BLYT_FOLDER_PATH
))
})?
.as_folder()
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} wasn't a folder",
BLYT_FOLDER_PATH
))
})
}
#[allow(dead_code)]
fn get_mut_blyt_folder(
fnt: &mut U8Node,
) -> Result<&mut U8FolderNode, ConvertOpeningTitleBetweenRegionsError> {
fnt.get_mut(BLYT_FOLDER_PATH)
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} folder not found",
BLYT_FOLDER_PATH
))
})?
.as_mut_folder()
.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::InvalidOpeningTitleStructure(format!(
"{} wasn't a folder",
BLYT_FOLDER_PATH
))
})
}
fn remove_regional_files(
fnt: &mut U8Node,
regions: RegionBitFlags,
) -> Result<HashMap<Region, OptionalNamedRegionalFiles>, ConvertOpeningTitleBetweenRegionsError> {
let mut map = HashMap::new();
for region in regions {
map.insert(
region,
OptionalNamedRegionalFiles {
in_press_brlan: None,
in_title_brlan: None,
loop_press_brlan: None,
out_press_brlan: None,
brlyt: None,
},
);
}
let folder = get_mut_anim_folder(fnt)?;
for region in regions {
let region_name = region.into();
let mut region_files = map.get_mut(®ion).unwrap();
let filename = ALL_FILENAMES[region_name].in_press_brlan;
if let Some(U8Node::File(file_node)) = folder.remove(filename) {
trace!("Removing {:?}", filename);
region_files.in_press_brlan = Some(NamedU8FileNode {
node: file_node,
filename: filename.to_owned(),
});
}
let filename = ALL_FILENAMES[region_name].in_title_brlan;
if let Some(U8Node::File(file_node)) = folder.remove(filename) {
trace!("Removing {:?}", filename);
region_files.in_title_brlan = Some(NamedU8FileNode {
node: file_node,
filename: filename.to_owned(),
});
}
let filename = ALL_FILENAMES[region_name].loop_press_brlan;
if let Some(U8Node::File(file_node)) = folder.remove(filename) {
trace!("Removing {:?}", filename);
region_files.loop_press_brlan = Some(NamedU8FileNode {
node: file_node,
filename: filename.to_owned(),
});
}
let filename = ALL_FILENAMES[region_name].out_press_brlan;
if let Some(U8Node::File(file_node)) = folder.remove(filename) {
trace!("Removing {:?}", filename);
region_files.out_press_brlan = Some(NamedU8FileNode {
node: file_node,
filename: filename.to_owned(),
});
}
}
let folder = get_mut_blyt_folder(fnt)?;
for region in regions {
let region_name = region.into();
let mut region_files = map.get_mut(®ion).unwrap();
let filename = ALL_FILENAMES[region_name].brlyt;
if let Some(U8Node::File(file_node)) = folder.remove(filename) {
trace!("Removing {:?}", filename);
region_files.brlyt = Some(NamedU8FileNode {
node: file_node,
filename: filename.to_owned(),
});
}
}
Ok(map)
}
fn check_file_pair_for_conflicts<SR: Seek + Read>(
previous_found_file: &mut Option<NamedU8FileNode>,
previous_found_file_hash: &mut u64,
found_file: &NamedU8FileNode,
data_table_offs: u32,
reader: &mut SR,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
if let Some(previous_found_file) = &*previous_found_file {
if found_file.node == previous_found_file.node {
return Ok(());
}
if *previous_found_file_hash == 0 {
trace!(
"Calculating hash of {:?} ({:#x}-{:#x})",
previous_found_file.filename,
previous_found_file.node.offset,
previous_found_file.node.offset + previous_found_file.node.size
);
*previous_found_file_hash = util::calc_hash_from_file_slice(
reader,
(data_table_offs + previous_found_file.node.offset).into(),
previous_found_file.node.size.try_into().unwrap(),
)?;
}
trace!(
"Calculating hash of {:?} ({:#x}-{:#x})",
found_file.filename,
found_file.node.offset,
found_file.node.offset + found_file.node.size
);
let new_hash = util::calc_hash_from_file_slice(
reader,
(data_table_offs + found_file.node.offset).into(),
found_file.node.size.try_into().unwrap(),
)?;
if *previous_found_file_hash != new_hash {
return Err(ConvertOpeningTitleBetweenRegionsError::FileDataConflict(
previous_found_file.filename.clone(),
found_file.filename.clone(),
));
}
} else {
*previous_found_file = Some(found_file.clone());
}
Ok(())
}
fn check_all_files_for_conflicts<SR: Seek + Read>(
all_regional_files: &HashMap<Region, OptionalNamedRegionalFiles>,
data_table_offs: u32,
mut reader: &mut SR,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
let mut in_press_brlan = None;
let mut in_title_brlan = None;
let mut loop_press_brlan = None;
let mut out_press_brlan = None;
let mut brlyt = None;
let mut in_press_brlan_hash = 0;
let mut in_title_brlan_hash = 0;
let mut loop_press_brlan_hash = 0;
let mut out_press_brlan_hash = 0;
let mut brlyt_hash = 0;
for regional_files in all_regional_files.values() {
if let Some(file) = ®ional_files.in_press_brlan {
check_file_pair_for_conflicts(
&mut in_press_brlan,
&mut in_press_brlan_hash,
file,
data_table_offs,
&mut reader,
)?;
}
if let Some(file) = ®ional_files.in_title_brlan {
check_file_pair_for_conflicts(
&mut in_title_brlan,
&mut in_title_brlan_hash,
file,
data_table_offs,
&mut reader,
)?;
}
if let Some(file) = ®ional_files.loop_press_brlan {
check_file_pair_for_conflicts(
&mut loop_press_brlan,
&mut loop_press_brlan_hash,
file,
data_table_offs,
&mut reader,
)?;
}
if let Some(file) = ®ional_files.out_press_brlan {
check_file_pair_for_conflicts(
&mut out_press_brlan,
&mut out_press_brlan_hash,
file,
data_table_offs,
&mut reader,
)?;
}
if let Some(file) = ®ional_files.brlyt {
check_file_pair_for_conflicts(
&mut brlyt,
&mut brlyt_hash,
file,
data_table_offs,
&mut reader,
)?;
}
}
Ok(())
}
fn select_regional_files(
all_regional_files: &HashMap<Region, OptionalNamedRegionalFiles>,
from_regions: &[Region],
) -> Result<RegionalFiles, ConvertOpeningTitleBetweenRegionsError> {
let mut in_press_brlan = None;
let mut in_title_brlan = None;
let mut loop_press_brlan = None;
let mut out_press_brlan = None;
let mut brlyt = None;
for region in from_regions {
if let Some(regional_files) = all_regional_files.get(region) {
if let Some(file_node) = ®ional_files.in_press_brlan {
in_press_brlan = in_press_brlan.or_else(|| Some(file_node.node.clone()));
}
if let Some(file_node) = ®ional_files.in_title_brlan {
in_title_brlan = in_title_brlan.or_else(|| Some(file_node.node.clone()));
}
if let Some(file_node) = ®ional_files.loop_press_brlan {
loop_press_brlan = loop_press_brlan.or_else(|| Some(file_node.node.clone()));
}
if let Some(file_node) = ®ional_files.out_press_brlan {
out_press_brlan = out_press_brlan.or_else(|| Some(file_node.node.clone()));
}
if let Some(file_node) = ®ional_files.brlyt {
brlyt = brlyt.or_else(|| Some(file_node.node.clone()));
}
}
}
Ok(RegionalFiles {
in_press_brlan: in_press_brlan.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::MissingFiles("inPress brlan".to_owned())
})?,
in_title_brlan: in_title_brlan.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::MissingFiles("inTitle brlan".to_owned())
})?,
loop_press_brlan: loop_press_brlan.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::MissingFiles("loopPress brlan".to_owned())
})?,
out_press_brlan: out_press_brlan.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::MissingFiles("outPress brlan".to_owned())
})?,
brlyt: brlyt.ok_or_else(|| {
ConvertOpeningTitleBetweenRegionsError::MissingFiles("brlyt".to_owned())
})?,
})
}
fn add_new_filenames(
fnt: &mut U8Node,
regional_files: &RegionalFiles,
regions: RegionBitFlags,
filename_conflict_strategy: ConflictStrategy,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
fn insert(
folder: &mut U8FolderNode,
filename: &str,
file_node: &U8FileNode,
filename_conflict_strategy: ConflictStrategy,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
if filename_conflict_strategy == ConflictStrategy::Fail && folder.contains_key(filename) {
return Err(
ConvertOpeningTitleBetweenRegionsError::FilenameAlreadyExists(filename.to_owned()),
);
}
folder.insert(filename.to_owned(), U8Node::File(file_node.clone()));
Ok(())
}
for this_region in regions {
let this_region_name = this_region.into();
trace!("Adding filenames for {this_region_name:?}");
let folder = get_mut_anim_folder(fnt)?;
insert(
folder,
ALL_FILENAMES[this_region_name].in_press_brlan,
®ional_files.in_press_brlan,
filename_conflict_strategy,
)?;
insert(
folder,
ALL_FILENAMES[this_region_name].in_title_brlan,
®ional_files.in_title_brlan,
filename_conflict_strategy,
)?;
insert(
folder,
ALL_FILENAMES[this_region_name].loop_press_brlan,
®ional_files.loop_press_brlan,
filename_conflict_strategy,
)?;
insert(
folder,
ALL_FILENAMES[this_region_name].out_press_brlan,
®ional_files.out_press_brlan,
filename_conflict_strategy,
)?;
let folder = get_mut_blyt_folder(fnt)?;
insert(
folder,
ALL_FILENAMES[this_region_name].brlyt,
®ional_files.brlyt,
filename_conflict_strategy,
)?;
}
Ok(())
}
fn build_new_fat<SR: Seek + Read, SW: Seek + Write>(
fnt: &mut U8Node,
data_table_offs: u32,
in_file: &mut SR,
out_file: &mut SW,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
let initial_fat_offset = out_file.stream_position()?;
let mut offset_remapping = HashMap::new();
fn visit_node<SR: Seek + Read, SW: Seek + Write>(
name: &str,
node: &mut U8Node,
data_table_offs: u32,
in_file: &mut SR,
out_file: &mut SW,
initial_fat_offset: u64,
offset_remapping: &mut HashMap<u32, u32>,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
trace!("Visiting {name:?}");
match node {
U8Node::File(U8FileNode {
ref mut offset,
size,
}) => {
*offset = if let Some(value) = offset_remapping.get(offset) {
*value
} else {
util::write_zeros_to_align_to(out_file, 0x20, 0)?;
let new_file_pos: u32 = (out_file.stream_position()? - initial_fat_offset)
.try_into()
.unwrap();
trace!("Moved from {:#x} to {:#x}", *offset, new_file_pos);
in_file.seek(SeekFrom::Start((data_table_offs + *offset).into()))?;
util::read_from_into(in_file, out_file, (*size).try_into().unwrap())?;
offset_remapping.insert(*offset, new_file_pos);
new_file_pos
}
}
U8Node::Folder(_) => {
trace!("Visiting children of {name:?}");
for (child_name, child) in node.iter_mut() {
visit_node(
child_name,
child,
data_table_offs,
in_file,
out_file,
initial_fat_offset,
offset_remapping,
)?;
}
trace!("Returning to parent dir ({name:?})");
}
};
Ok(())
}
visit_node(
"(root)",
fnt,
data_table_offs,
in_file,
out_file,
initial_fat_offset,
&mut offset_remapping,
)
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct ConvertOpeningTitleBetweenRegionsConflictStrategies {
pub file_contents: ConflictStrategy,
pub filenames: ConflictStrategy,
}
pub fn convert_openingtitle_between_regions<SR: Seek + Read, SW: Seek + Write>(
mut in_file: SR,
mut out_file: SW,
from_regions: Option<&[Region]>,
to_regions: RegionBitFlags,
conflict_strategies: &ConvertOpeningTitleBetweenRegionsConflictStrategies,
) -> Result<(), ConvertOpeningTitleBetweenRegionsError> {
info!("Converting an openingTitle to regions: {to_regions:?}");
const TOTAL_STEPS: u32 = 9;
let from_regions = match from_regions {
Some(regions) => regions,
None => &Region::DEFAULT_ORDER,
};
info!("[1/{TOTAL_STEPS}] Reading original FNT...");
let (mut fnt, data_table_offs) = u8_fnt::read(&mut in_file)?;
debug!("\n{fnt}");
info!("[2/{TOTAL_STEPS}] Removing all regional files...");
let all_regional_files =
remove_regional_files(&mut fnt, RegionBitFlags::from_iter(from_regions))?;
if conflict_strategies.file_contents == ConflictStrategy::Fail {
info!("[3/{TOTAL_STEPS}] Checking for conflicts...");
check_all_files_for_conflicts(&all_regional_files, data_table_offs, &mut in_file)?;
}
info!("[4/{TOTAL_STEPS}] Selecting regional files...");
let regional_files = select_regional_files(&all_regional_files, from_regions)?;
debug!("\n{fnt}");
info!("[5/{TOTAL_STEPS}] Adding new regional filenames...");
add_new_filenames(
&mut fnt,
®ional_files,
to_regions,
conflict_strategies.filenames,
)?;
debug!("\n{fnt}");
info!("[6/{TOTAL_STEPS}] Predicting size of new FNT...");
let mut tmp_cursor = Cursor::new(Vec::new());
u8_fnt::write(&mut tmp_cursor, &fnt)?;
let fnt_length = tmp_cursor.into_inner().len();
info!("...new FNT size will be {fnt_length:#x}");
info!("[7/{TOTAL_STEPS}] Writing nulls to reserve space for FNT...");
out_file.write_all(&vec![0; fnt_length])?;
info!("[8/{TOTAL_STEPS}] Building new FAT and updating FNT...");
build_new_fat(&mut fnt, data_table_offs, &mut in_file, &mut out_file)?;
debug!("\n{fnt}");
info!("[9/{TOTAL_STEPS}] Writing final FNT...");
out_file.seek(SeekFrom::Start(0))?;
u8_fnt::write(&mut out_file, &fnt)?;
info!("Done switching regions!");
Ok(())
}
#[cfg(test)]
#[allow(clippy::unnecessary_wraps)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn make_openingtitle_fnt(regions: RegionBitFlags, regional_files: &RegionalFiles) -> U8Node {
let mut anim = U8FolderNode::new();
let mut blyt = U8FolderNode::new();
for region in regions {
anim.extend([
(
ALL_FILENAMES[region.into()].in_press_brlan.to_owned(),
U8Node::File(regional_files.in_press_brlan.clone()),
),
(
ALL_FILENAMES[region.into()].in_title_brlan.to_owned(),
U8Node::File(regional_files.in_title_brlan.clone()),
),
(
ALL_FILENAMES[region.into()].loop_press_brlan.to_owned(),
U8Node::File(regional_files.loop_press_brlan.clone()),
),
(
ALL_FILENAMES[region.into()].out_press_brlan.to_owned(),
U8Node::File(regional_files.out_press_brlan.clone()),
),
]);
blyt.insert(
ALL_FILENAMES[region.into()].brlyt.to_owned(),
U8Node::File(regional_files.brlyt.clone()),
);
}
U8Node::Folder(U8FolderNode::from([(
"arc".to_owned(),
U8Node::Folder(U8FolderNode::from([
("anim".to_owned(), U8Node::Folder(anim)),
("blyt".to_owned(), U8Node::Folder(blyt)),
])),
)]))
}
fn make_hash_map_to_optional_named_regional_files(
regions: RegionBitFlags,
regional_files: &RegionalFiles,
) -> HashMap<Region, OptionalNamedRegionalFiles> {
let mut map = HashMap::new();
for region in regions {
let region_name = region.into();
map.insert(
region,
OptionalNamedRegionalFiles {
in_press_brlan: Some(NamedU8FileNode {
node: regional_files.in_press_brlan.clone(),
filename: ALL_FILENAMES[region_name].in_press_brlan.to_owned(),
}),
in_title_brlan: Some(NamedU8FileNode {
node: regional_files.in_title_brlan.clone(),
filename: ALL_FILENAMES[region_name].in_title_brlan.to_owned(),
}),
loop_press_brlan: Some(NamedU8FileNode {
node: regional_files.loop_press_brlan.clone(),
filename: ALL_FILENAMES[region_name].loop_press_brlan.to_owned(),
}),
out_press_brlan: Some(NamedU8FileNode {
node: regional_files.out_press_brlan.clone(),
filename: ALL_FILENAMES[region_name].out_press_brlan.to_owned(),
}),
brlyt: Some(NamedU8FileNode {
node: regional_files.brlyt.clone(),
filename: ALL_FILENAMES[region_name].brlyt.to_owned(),
}),
},
);
}
map
}
mod remove_regional_files {
use super::*;
#[test]
fn test_empty() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::new());
let regional_files = remove_regional_files(&mut root, RegionBitFlags::from(Region::P));
assert!(regional_files.is_err());
Ok(())
}
#[test]
fn test_simple() -> TestResult {
let regional_files = RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
};
let mut root =
make_openingtitle_fnt(Region::P | Region::E | Region::J, ®ional_files);
get_mut_anim_folder(&mut root)?.insert(
"some other random thing".to_owned(),
U8Node::File(U8FileNode {
offset: 0x10,
size: 0x10,
}),
);
let removed_files = remove_regional_files(&mut root, Region::P | Region::E)?;
let mut expected_out_root =
make_openingtitle_fnt(RegionBitFlags::from(Region::J), ®ional_files);
get_mut_anim_folder(&mut expected_out_root)?.insert(
"some other random thing".to_owned(),
U8Node::File(U8FileNode {
offset: 0x10,
size: 0x10,
}),
);
assert_eq!(root, expected_out_root);
assert_eq!(
removed_files,
make_hash_map_to_optional_named_regional_files(
Region::P | Region::E,
®ional_files
)
);
Ok(())
}
}
mod check_file_pair_for_conflicts {
use super::*;
#[test]
fn test_nodes_identical() -> TestResult {
let old = NamedU8FileNode {
node: U8FileNode {
offset: 8,
size: 10,
},
filename: "old".to_owned(),
};
let new = NamedU8FileNode {
node: U8FileNode {
offset: 8,
size: 10,
},
filename: "new".to_owned(),
};
let mut cursor = Cursor::new((0..255).collect::<Vec<u8>>());
let mut current_file = None;
let mut current_hash = 0;
check_file_pair_for_conflicts(
&mut current_file,
&mut current_hash,
&old,
10,
&mut cursor,
)?;
check_file_pair_for_conflicts(
&mut current_file,
&mut current_hash,
&new,
10,
&mut cursor,
)?;
assert_eq!(current_hash, 0);
Ok(())
}
#[test]
fn test_data_identical() -> TestResult {
let old = NamedU8FileNode {
node: U8FileNode { offset: 0, size: 8 },
filename: "old".to_owned(),
};
let new = NamedU8FileNode {
node: U8FileNode { offset: 8, size: 8 },
filename: "new".to_owned(),
};
let data = b"XXXAAAABBBBAAAABBBBCCCCDDDD";
let mut cursor = Cursor::new(data.to_vec());
let mut current_file = None;
let mut current_hash = 0;
check_file_pair_for_conflicts(
&mut current_file,
&mut current_hash,
&old,
3,
&mut cursor,
)?;
check_file_pair_for_conflicts(
&mut current_file,
&mut current_hash,
&new,
3,
&mut cursor,
)?;
assert_ne!(current_hash, 0);
Ok(())
}
#[test]
fn test_data_different() -> TestResult {
let old = NamedU8FileNode {
node: U8FileNode { offset: 0, size: 8 },
filename: "old".to_owned(),
};
let new = NamedU8FileNode {
node: U8FileNode { offset: 8, size: 8 },
filename: "new".to_owned(),
};
let data = b"XXXAAAABBBBCCCCDDDDAAAABBBB";
let mut cursor = Cursor::new(data.to_vec());
let mut current_file = None;
let mut current_hash = 0;
check_file_pair_for_conflicts(
&mut current_file,
&mut current_hash,
&old,
3,
&mut cursor,
)?;
assert!(check_file_pair_for_conflicts(
&mut current_file,
&mut current_hash,
&new,
3,
&mut cursor
)
.is_err());
assert_ne!(current_hash, 0);
Ok(())
}
}
mod check_all_files_for_conflicts {
use super::*;
#[test]
fn test_no_conflicts() -> TestResult {
let mut files = make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::P),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
},
);
files.extend(make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::E),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x11,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x12,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x13,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x14,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x15,
size: 0x5,
},
},
));
let files = files;
let data = b"0123456789abcdef0123456789abcdef";
let mut cursor = Cursor::new(data.to_vec());
check_all_files_for_conflicts(&files, 0, &mut cursor)?;
Ok(())
}
#[test]
fn test_conflicts() -> TestResult {
let mut files = make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::P),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
},
);
files.extend(make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::E),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x11,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x12,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x13,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x14,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x15,
size: 0x5,
},
},
));
let files = files;
let data = b"0123456789abcdef0123_56789abcdef";
let mut cursor = Cursor::new(data.to_vec());
assert!(check_all_files_for_conflicts(&files, 0, &mut cursor).is_err());
Ok(())
}
}
mod select_regional_files {
use super::*;
#[test]
fn test_simple() -> TestResult {
let mut files = make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::K),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
},
);
files.extend(make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::E),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x11,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x12,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x13,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x14,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x15,
size: 0x5,
},
},
));
files.get_mut(&Region::K).unwrap().in_title_brlan = None;
let files = files;
let selected = select_regional_files(&files, &[Region::K, Region::E])?;
assert_eq!(
selected,
RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1
},
in_title_brlan: U8FileNode {
offset: 0x12,
size: 0x2
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5
},
}
);
Ok(())
}
#[test]
fn test_missing_files() -> TestResult {
let mut files = make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::K),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
},
);
files.extend(make_hash_map_to_optional_named_regional_files(
RegionBitFlags::from(Region::E),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x11,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x12,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x13,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x14,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x15,
size: 0x5,
},
},
));
files.get_mut(&Region::K).unwrap().in_title_brlan = None;
files.get_mut(&Region::E).unwrap().in_title_brlan = None;
let files = files;
assert!(select_regional_files(&files, &[Region::K, Region::E]).is_err());
Ok(())
}
}
mod add_new_filenames {
use super::*;
#[test]
fn test_individual() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::from([(
"arc".to_owned(),
U8Node::Folder(U8FolderNode::from([
("anim".to_owned(), U8Node::Folder(U8FolderNode::new())),
("blyt".to_owned(), U8Node::Folder(U8FolderNode::new())),
])),
)]));
let regional_files = RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
};
add_new_filenames(
&mut root,
®ional_files,
RegionBitFlags::from(Region::K),
ConflictStrategy::default(),
)?;
assert_eq!(
root,
make_openingtitle_fnt(RegionBitFlags::from(Region::K), ®ional_files)
);
Ok(())
}
#[test]
fn test_region_free() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::from([(
"arc".to_owned(),
U8Node::Folder(U8FolderNode::from([
("anim".to_owned(), U8Node::Folder(U8FolderNode::new())),
("blyt".to_owned(), U8Node::Folder(U8FolderNode::new())),
])),
)]));
let regional_files = RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
};
add_new_filenames(
&mut root,
®ional_files,
RegionBitFlags::ALL,
ConflictStrategy::default(),
)?;
assert_eq!(
get_anim_folder(&root)?.len(),
4 * Region::DEFAULT_ORDER.len()
);
assert_eq!(get_blyt_folder(&root)?.len(), Region::DEFAULT_ORDER.len());
Ok(())
}
#[test]
fn test_forbidden_conflicts() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::from([(
"arc".to_owned(),
U8Node::Folder(U8FolderNode::from([
(
"anim".to_owned(),
U8Node::Folder(U8FolderNode::from([(
ALL_FILENAMES["W"].loop_press_brlan.to_owned(),
U8Node::File(U8FileNode { offset: 0, size: 0 }),
)])),
),
("blyt".to_owned(), U8Node::Folder(U8FolderNode::new())),
])),
)]));
let regional_files = RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
};
assert!(add_new_filenames(
&mut root,
®ional_files,
RegionBitFlags::from(Region::W),
ConflictStrategy::Fail
)
.is_err());
Ok(())
}
#[test]
fn test_allowed_conflicts() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::from([(
"arc".to_owned(),
U8Node::Folder(U8FolderNode::from([
(
"anim".to_owned(),
U8Node::Folder(U8FolderNode::from([(
ALL_FILENAMES["W"].loop_press_brlan.to_owned(),
U8Node::File(U8FileNode { offset: 0, size: 0 }),
)])),
),
("blyt".to_owned(), U8Node::Folder(U8FolderNode::new())),
])),
)]));
let regional_files = RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x1,
size: 0x1,
},
in_title_brlan: U8FileNode {
offset: 0x2,
size: 0x2,
},
loop_press_brlan: U8FileNode {
offset: 0x3,
size: 0x3,
},
out_press_brlan: U8FileNode {
offset: 0x4,
size: 0x4,
},
brlyt: U8FileNode {
offset: 0x5,
size: 0x5,
},
};
add_new_filenames(
&mut root,
®ional_files,
RegionBitFlags::from(Region::W),
ConflictStrategy::Overwrite,
)?;
assert_eq!(
root,
make_openingtitle_fnt(RegionBitFlags::from(Region::W), ®ional_files)
);
Ok(())
}
}
mod build_new_fat {
use super::*;
#[test]
fn test_empty() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::new());
let mut in_buf = Cursor::new(Vec::new());
let mut out_buf = Cursor::new(Vec::new());
build_new_fat(&mut root, 0, &mut in_buf, &mut out_buf)?;
assert_eq!(out_buf.stream_position()?, 0);
Ok(())
}
#[test]
fn test_simple() -> TestResult {
let mut root = U8Node::Folder(U8FolderNode::from([
(
"a".to_owned(),
U8Node::File(U8FileNode {
offset: 0x40,
size: 0x10,
}),
),
(
"b".to_owned(),
U8Node::File(U8FileNode {
offset: 0x20,
size: 0x10,
}),
),
(
"c".to_owned(),
U8Node::File(U8FileNode {
offset: 0x10,
size: 0x10,
}),
),
(
"d".to_owned(),
U8Node::File(U8FileNode {
offset: 0x40,
size: 0x10,
}),
),
(
"e".to_owned(),
U8Node::File(U8FileNode {
offset: 0x30,
size: 0x10,
}),
),
(
"f".to_owned(),
U8Node::File(U8FileNode {
offset: 0x20,
size: 0x10,
}),
),
]));
let mut in_buf = Cursor::new((0..255).collect::<Vec<u8>>());
let mut out_buf = Cursor::new(Vec::new());
build_new_fat(&mut root, 0, &mut in_buf, &mut out_buf)?;
assert_eq!(
root,
U8Node::Folder(U8FolderNode::from([
(
"a".to_owned(),
U8Node::File(U8FileNode {
offset: 0x00,
size: 0x10
})
),
(
"b".to_owned(),
U8Node::File(U8FileNode {
offset: 0x20,
size: 0x10
})
),
(
"c".to_owned(),
U8Node::File(U8FileNode {
offset: 0x40,
size: 0x10
})
),
(
"d".to_owned(),
U8Node::File(U8FileNode {
offset: 0x00,
size: 0x10
})
),
(
"e".to_owned(),
U8Node::File(U8FileNode {
offset: 0x60,
size: 0x10
})
),
(
"f".to_owned(),
U8Node::File(U8FileNode {
offset: 0x20,
size: 0x10
})
),
]))
);
assert_eq!(
&out_buf.into_inner(),
concat_bytes!(
b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f",
)
);
Ok(())
}
}
mod convert_openingtitle_between_regions {
use super::*;
#[test]
fn test_individual() -> TestResult {
let mut in_root = make_openingtitle_fnt(
RegionBitFlags::from(Region::W),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x00,
size: 0x8,
},
in_title_brlan: U8FileNode {
offset: 0x20,
size: 0x8,
},
loop_press_brlan: U8FileNode {
offset: 0x40,
size: 0x8,
},
out_press_brlan: U8FileNode {
offset: 0x60,
size: 0x8,
},
brlyt: U8FileNode {
offset: 0xa0,
size: 0x8,
},
},
);
get_mut_anim_folder(&mut in_root)?.insert(
"something else".to_owned(),
U8Node::File(U8FileNode {
offset: 0x80,
size: 0x8,
}),
);
let mut in_buf = Cursor::new(Vec::new());
u8_fnt::write(&mut in_buf, &in_root)?;
in_buf.write_all(concat_bytes!(
b"INPBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"INTBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"LPPBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"OTPBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"SMTHELSE\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"THEBRLYT\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
))?;
in_buf.seek(SeekFrom::Start(0))?;
let mut out_buf = Cursor::new(Vec::new());
convert_openingtitle_between_regions(
&mut in_buf,
&mut out_buf,
None,
RegionBitFlags::from(Region::J),
&ConvertOpeningTitleBetweenRegionsConflictStrategies::default(),
)?;
out_buf.seek(SeekFrom::Start(0))?;
let (out_root, out_data_offset) = u8_fnt::read(&mut out_buf)?;
let mut expected_out_root = make_openingtitle_fnt(
RegionBitFlags::from(Region::J),
&RegionalFiles {
in_press_brlan: U8FileNode {
offset: 0x00,
size: 0x8,
},
in_title_brlan: U8FileNode {
offset: 0x20,
size: 0x8,
},
loop_press_brlan: U8FileNode {
offset: 0x40,
size: 0x8,
},
out_press_brlan: U8FileNode {
offset: 0x60,
size: 0x8,
},
brlyt: U8FileNode {
offset: 0xa0,
size: 0x8,
},
},
);
get_mut_anim_folder(&mut expected_out_root)?.insert(
"something else".to_owned(),
U8Node::File(U8FileNode {
offset: 0x80,
size: 0x8,
}),
);
assert_eq!(out_root, expected_out_root);
assert_eq!(out_data_offset, 0x160);
Ok(())
}
#[test]
fn test_region_free() -> TestResult {
let in_root = U8Node::Folder(U8FolderNode::from([(
"arc".to_owned(),
U8Node::Folder(U8FolderNode::from([
(
"anim".to_owned(),
U8Node::Folder(U8FolderNode::from([
(
ALL_FILENAMES["P"].in_press_brlan.to_owned(),
U8Node::File(U8FileNode {
offset: 0x00,
size: 0x8,
}),
),
(
ALL_FILENAMES["E"].in_title_brlan.to_owned(),
U8Node::File(U8FileNode {
offset: 0x20,
size: 0x8,
}),
),
(
ALL_FILENAMES["J"].loop_press_brlan.to_owned(),
U8Node::File(U8FileNode {
offset: 0x40,
size: 0x8,
}),
),
(
ALL_FILENAMES["K"].out_press_brlan.to_owned(),
U8Node::File(U8FileNode {
offset: 0x60,
size: 0x8,
}),
),
])),
),
(
"blyt".to_owned(),
U8Node::Folder(U8FolderNode::from([(
ALL_FILENAMES["W"].brlyt.to_owned(),
U8Node::File(U8FileNode {
offset: 0x80,
size: 0x8,
}),
)])),
),
])),
)]));
let mut in_buf = Cursor::new(Vec::new());
u8_fnt::write(&mut in_buf, &in_root)?;
in_buf.write_all(concat_bytes!(
b"INPBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"INTBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"LPPBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"OTPBRLAN\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"THEBRLYT\0\0\0\0\0\0\0\0",
b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
))?;
in_buf.seek(SeekFrom::Start(0))?;
let mut out_buf = Cursor::new(Vec::new());
convert_openingtitle_between_regions(
&mut in_buf,
&mut out_buf,
None,
RegionBitFlags::ALL,
&ConvertOpeningTitleBetweenRegionsConflictStrategies::default(),
)?;
out_buf.seek(SeekFrom::Start(0))?;
let (out_root, out_data_offset) = u8_fnt::read(&mut out_buf)?;
let anim_folder = get_anim_folder(&out_root)?;
let blyt_folder = get_blyt_folder(&out_root)?;
for region in &Region::DEFAULT_ORDER {
let region_name = region.into();
assert!(anim_folder
.get(ALL_FILENAMES[region_name].in_press_brlan)
.is_some());
assert!(anim_folder
.get(ALL_FILENAMES[region_name].in_title_brlan)
.is_some());
assert!(anim_folder
.get(ALL_FILENAMES[region_name].loop_press_brlan)
.is_some());
assert!(anim_folder
.get(ALL_FILENAMES[region_name].out_press_brlan)
.is_some());
assert!(blyt_folder.get(ALL_FILENAMES[region_name].brlyt).is_some());
}
assert_eq!(out_data_offset, 0x580);
Ok(())
}
}
}