use std::{
cell::RefCell,
collections::hash_map::Entry,
hash::Hasher,
io::{Cursor, Seek, SeekFrom, Write},
rc::Rc,
sync::Mutex,
};
use binrw::prelude::*;
use rustc_hash::FxHashMap;
use super::*;
use crate::{util::align, Result};
impl ParameterIO {
pub fn write<W: Write + Seek>(&self, writer: W) -> Result<()> {
let mut ctx = WriteContext {
writer,
list_count: Default::default(),
object_count: Default::default(),
param_count: Default::default(),
param_queue: Default::default(),
string_param_queue: Default::default(),
offsets: Default::default(),
string_offsets: Default::default(),
buffer_offsets: Default::default(),
};
ctx.writer.seek(SeekFrom::Start(0x30))?;
ctx.writer.write_le(&self.data_type.as_bytes())?;
ctx.writer.write_le(&0u8)?;
ctx.align()?;
let pio_offset = ctx.writer.stream_position()?;
let root = &self.param_root;
ctx.write_lists(self)?;
ctx.write_objects(root)?;
ctx.collect_parameters(self);
ctx.write_parameters(root)?;
let data_section_begin = ctx.writer.stream_position()?;
ctx.write_data_section()?;
let string_section_begin = ctx.writer.stream_position()?;
ctx.write_string_section()?;
let unknown_section_begin = ctx.writer.stream_position()?;
ctx.align()?;
let header = ResHeader {
version: 2,
flags: 3,
file_size: ctx.writer.stream_position()? as u32,
pio_version: self.version,
pio_offset: (pio_offset - 0x30) as u32,
list_count: ctx.list_count,
object_count: ctx.object_count,
param_count: ctx.param_count,
data_section_size: (string_section_begin - data_section_begin) as u32,
string_section_size: (unknown_section_begin - string_section_begin) as u32,
unknown_section_size: 0,
};
ctx.writer.seek(SeekFrom::Start(0))?;
ctx.writer.write_le(&header)?;
ctx.writer.flush()?;
Ok(())
}
pub fn to_binary(&self) -> Vec<u8> {
let mut buf = Vec::new();
self.write(Cursor::new(&mut buf))
.expect("Parameter IO should serialize to binary without error");
buf
}
}
#[inline]
fn write_buffer<W: Write + Seek, T: BinWrite<Args = ()>>(
writer: &mut W,
buffer: &[T],
) -> BinResult<()> {
writer.write_le(&(buffer.len() as u32))?;
writer.write_le(&buffer)?;
Ok(())
}
#[inline]
fn hash_param_data(param: &Parameter) -> u64 {
let mut hasher = rustc_hash::FxHasher::default();
std::hash::Hash::hash(param, &mut hasher);
hasher.finish()
}
struct WriteContext<'pio, W: Write + Seek> {
writer: W,
list_count: u32,
object_count: u32,
param_count: u32,
param_queue: Vec<&'pio Parameter>,
string_param_queue: Vec<&'pio Parameter>,
offsets: FxHashMap<usize, u32>,
string_offsets: FxHashMap<&'pio str, u32>,
buffer_offsets: FxHashMap<u64, u32>,
}
impl<'pio, W: Write + Seek> WriteContext<'pio, W> {
#[inline(always)]
fn get_offset<T: std::fmt::Debug>(&mut self, data: &T) -> u32 {
self.offsets[&(data as *const _ as usize)]
}
#[inline(always)]
fn align(&mut self) -> BinResult<()> {
let pos = self.writer.stream_position()? as u32;
let aligned = align(pos, 4);
self.writer.seek(SeekFrom::Start(aligned as u64))?;
Ok(())
}
#[inline]
fn write_at<T: BinWrite<Args = ()>>(&mut self, offset: u32, data: T) -> BinResult<()> {
let old_pos = self.writer.stream_position()?;
self.writer.seek(SeekFrom::Start(offset as u64))?;
self.writer.write_le(&data)?;
self.writer.seek(SeekFrom::Start(old_pos))?;
Ok(())
}
fn write_lists(&mut self, pio: &'pio ParameterIO) -> BinResult<()> {
fn write<W: Write + Seek>(
ctx: &mut WriteContext<W>,
list: &ParameterList,
) -> BinResult<()> {
ctx.write_offset_for_parent(list, 0x4)?;
for (name, list) in &list.lists.0 {
ctx.write_list(*name, list)?;
}
for list in list.lists.0.values() {
write(ctx, list)?;
}
Ok(())
}
self.write_list(ROOT_KEY, &pio.param_root)?;
write(self, &pio.param_root)?;
Ok(())
}
fn write_objects(&mut self, list: &ParameterList) -> BinResult<()> {
self.write_offset_for_parent(list, 0x8)?;
for (name, object) in &list.objects.0 {
self.write_object(*name, object)?;
}
for list in list.lists.0.values() {
self.write_objects(list)?;
}
Ok(())
}
fn write_parameters(&mut self, list: &ParameterList) -> BinResult<()> {
for list in list.lists.0.values() {
self.write_parameters(list)?;
}
for object in list.objects.0.values() {
self.write_offset_for_parent(object, 0x4)?;
for (name, param) in &object.0 {
self.write_parameter(*name, param)?;
}
}
Ok(())
}
fn collect_parameters(&mut self, pio: &'pio ParameterIO) {
fn do_collect<'ctx, 'pio, W: Write + Seek>(
ctx: Rc<Mutex<&mut WriteContext<'pio, W>>>,
list: &'pio ParameterList,
process_top_objects_first: bool,
) where
'pio: 'ctx,
{
let mut obj_iter = list.objects.0.values();
let object = RefCell::new(obj_iter.next());
let mut process_one_object = || {
if let Some(obj) = object.borrow().as_ref() {
for param in obj.0.values() {
let mut ctx = ctx.lock().expect("Context should unlock");
if param.is_string_type() {
ctx.string_param_queue.push(param);
} else {
ctx.param_queue.push(param);
}
}
}
object.replace(obj_iter.next());
};
let is_botw_aiprog = !list.objects.is_empty()
&& list.objects.0.keys().next() == Some(&Name::from_str("DemoAIActionIdx"));
if process_top_objects_first && !is_botw_aiprog {
let mut i = 0;
while object.borrow().is_some() && i < 7 {
process_one_object();
i += 1;
}
}
for (i, child_list) in list.lists.0.values().enumerate() {
if !is_botw_aiprog && i % 2 == 0 && object.borrow().is_some() {
process_one_object();
}
do_collect(ctx.clone(), child_list, false);
}
while object.borrow().is_some() {
process_one_object();
}
}
do_collect(Rc::new(Mutex::new(self)), &pio.param_root, true)
}
fn write_data_section(&mut self) -> BinResult<()> {
let queue = std::mem::take(&mut self.param_queue);
for param in queue {
self.write_parameter_data(param)?;
}
self.align()?;
Ok(())
}
fn write_string_section(&mut self) -> BinResult<()> {
let queue = std::mem::take(&mut self.string_param_queue);
for param in queue {
self.write_string(param)?;
}
self.align()?;
Ok(())
}
fn write_parameter_data(&mut self, param: &Parameter) -> BinResult<()> {
debug_assert!(
!param.is_string_type(),
"`write_parameter_data` called with string parameter"
);
let parent_offset = self.get_offset(param);
let mut data_offset =
self.writer.stream_position()? as u32 + if param.is_buffer_type() { 4 } else { 0 };
let mut existed = true;
let hash = hash_param_data(param);
data_offset = match self.buffer_offsets.entry(hash) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let mut tmp_writer = Cursor::new(Vec::<u8>::with_capacity(0x200));
match param {
Parameter::Bool(b) => tmp_writer.write_le(&u32::from(*b))?,
Parameter::F32(v) => tmp_writer.write_le(&v.to_bits())?,
Parameter::I32(v) => tmp_writer.write_le(&v)?,
Parameter::Vec2(v) => tmp_writer.write_le(&v)?,
Parameter::Vec3(v) => tmp_writer.write_le(&v)?,
Parameter::Vec4(v) => tmp_writer.write_le(&v)?,
Parameter::Color(v) => tmp_writer.write_le(&v)?,
Parameter::Curve1(v) => tmp_writer.write_le(&v)?,
Parameter::Curve2(v) => tmp_writer.write_le(&v)?,
Parameter::Curve3(v) => tmp_writer.write_le(&v)?,
Parameter::Curve4(v) => tmp_writer.write_le(&v)?,
Parameter::Quat(v) => tmp_writer.write_le(&v)?,
Parameter::U32(v) => tmp_writer.write_le(&v)?,
Parameter::BufferInt(v) => write_buffer(&mut tmp_writer, v)?,
Parameter::BufferU32(v) => write_buffer(&mut tmp_writer, v)?,
Parameter::BufferF32(v) => {
tmp_writer.write_le(&(v.len() as u32))?;
for f in v {
tmp_writer.write_le(f)?;
}
}
Parameter::BufferBinary(v) => write_buffer(&mut tmp_writer, v)?,
_ => unreachable!("unhandled parameter type"),
}
self.writer.write_all(tmp_writer.into_inner().as_slice())?;
existed = false;
*entry.insert(data_offset)
}
};
self.write_at(parent_offset + 0x4, u24((data_offset - parent_offset) / 4))?;
if !existed {
self.align()?;
}
Ok(())
}
fn write_string(&mut self, param: &'pio Parameter) -> BinResult<()> {
let parent_offset = self.get_offset(param);
let string_ = param.as_str().expect("Parameter should be a string");
let pos = self.writer.stream_position()? as u32;
let mut existed = true;
let offset = *self.string_offsets.entry(string_).or_insert_with(|| {
existed = false;
pos
});
self.write_at(parent_offset + 0x4, u24((offset - parent_offset) / 4))?;
if !existed {
self.writer.write_le(&string_.as_bytes())?;
self.writer.write_le(&0u8)?;
self.align()?;
}
Ok(())
}
fn write_list(&mut self, name: Name, list: &ParameterList) -> BinResult<()> {
let offset = self.writer.stream_position()? as u32;
self.offsets.insert(list as *const _ as usize, offset);
self.list_count += 1;
self.writer.write_le(&ResParameterList {
name,
list_count: list.lists.len() as u16,
lists_rel_offset: 0,
object_count: list.objects.len() as u16,
objects_rel_offset: 0,
})?;
Ok(())
}
fn write_object(&mut self, name: Name, object: &ParameterObject) -> BinResult<()> {
let offset = self.writer.stream_position()? as u32;
self.offsets.insert(object as *const _ as usize, offset);
self.object_count += 1;
self.writer.write_le(&ResParameterObj {
name,
param_count: object.len() as u16,
params_rel_offset: 0,
})?;
Ok(())
}
fn write_parameter(&mut self, name: Name, param: &Parameter) -> BinResult<()> {
let offset = self.writer.stream_position()? as u32;
self.offsets.insert(param as *const _ as usize, offset);
self.param_count += 1;
self.writer.write_le(&ResParameter {
name,
type_: param.get_type(),
data_rel_offset: u24(0),
})?;
Ok(())
}
fn write_offset_for_parent<T: std::fmt::Debug>(
&mut self,
parent: &T,
offset_in_parent: u32,
) -> BinResult<()> {
let parent_offset = self.get_offset(parent);
let current_rel_offset = (self.writer.stream_position()? as u32 - parent_offset) / 4;
self.write_at(parent_offset + offset_in_parent, current_rel_offset as u16)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_roundtrip() {
for file in jwalk::WalkDir::new("test/aamp")
.into_iter()
.filter_map(|f| {
f.ok().and_then(|f| {
(f.file_type().is_file() && !f.file_name().to_str().unwrap().ends_with("yml"))
.then(|| f.path())
})
})
{
println!("{}", file.display());
let data = std::fs::read(&file).unwrap();
let pio = ParameterIO::from_binary(data).unwrap();
let new_bytes = pio.to_binary();
let new_pio = ParameterIO::from_binary(new_bytes).unwrap();
assert_eq!(pio, new_pio);
}
}
}