use std::{ffi::CStr, iter, mem::ManuallyDrop, ops::Drop, ptr::NonNull, slice};
use bitfield::bitfield;
use super::FD4ResCap;
use crate::param::ParamDef;
use shared::{OwnedPtr, Subclass};
#[repr(C)]
#[derive(Subclass)]
pub struct FD4ParamResCap {
pub res_cap: FD4ResCap,
pub size: u64,
pub data: OwnedPtr<ParamFile>,
}
impl FD4ParamResCap {
pub fn struct_name(&self) -> &str {
self.data.struct_name()
}
pub unsafe fn get<P: ParamDef>(&self, id: u32) -> Option<&P> {
unsafe { self.data.get_row_by_id(id) }
}
pub unsafe fn get_mut<P: ParamDef>(&mut self, id: u32) -> Option<&mut P> {
unsafe { self.data.get_row_by_id_mut(id) }
}
}
#[repr(C)]
struct ParamFileMetadata {
after_name_offset: u32,
_un08: u64,
}
#[repr(C)]
struct RowLookupEntry {
param_id: u32,
index: u32,
}
#[repr(C)]
struct RowDescriptor<T: Into<u64> + Copy> {
id: u32,
data_offset: T,
name_offset: T,
}
impl<T: Into<u64> + Copy> RowDescriptor<T> {
pub fn data_offset(&self) -> usize {
self.data_offset.into() as usize
}
}
#[repr(C)]
struct ParamFileV2 {
_unk00: [u8; 0x8],
paramdef_version: u16,
row_count: u16,
struct_name: [u8; 0x20],
flags: ParamFileFlags,
}
#[repr(C)]
struct ParamFileV5 {
_aligned_offset_after_param_data: u32,
_unk04: u32,
paramdef_version: u16,
row_count: u16,
_unk0c: u32,
struct_name_offset: u32,
_unk18: u64,
_unk20: u64,
_unk28: u32,
flags: ParamFileFlags,
data_offset: u32,
}
pub union ParamFile {
v2: ManuallyDrop<ParamFileV2>,
v5: ManuallyDrop<ParamFileV5>,
}
impl ParamFile {
const LOOKUP_TABLE_ALIGNMENT: u32 = 0x10;
pub fn struct_name(&self) -> &str {
match self.as_enum() {
ParamFileType::V2(file) => CStr::from_bytes_until_nul(&file.struct_name)
.ok()
.and_then(|s| s.to_str().ok())
.unwrap_or(""),
ParamFileType::V5(file) => {
let offset = file.struct_name_offset;
if offset == 0 {
""
} else {
unsafe {
CStr::from_ptr(self.offset::<i8>(offset as usize).as_ptr())
.to_str()
.unwrap_or("")
}
}
}
}
}
pub fn paramdef_version(&self) -> u16 {
unsafe { self.v5.paramdef_version }
}
pub fn row_count(&self) -> usize {
unsafe { self.v5.row_count as usize }
}
pub unsafe fn get_row_by_id<P: ParamDef>(&self, id: u32) -> Option<&P> {
let index = self.find_index(id)?;
let (actual_id, data_offset) = self.row_data_offset(index)?;
debug_assert_eq!(id, actual_id, "Unexpected row ID for {}", P::NAME);
Some(unsafe { self.offset::<P>(data_offset).as_ref() })
}
pub unsafe fn get_row_by_id_mut<P: ParamDef>(&mut self, id: u32) -> Option<&mut P> {
let index = self.find_index(id)?;
let (actual_id, data_offset) = self.row_data_offset(index)?;
debug_assert_eq!(id, actual_id, "Unexpected row ID for {}", P::NAME);
Some(unsafe { self.offset::<P>(data_offset).as_mut() })
}
pub unsafe fn get_row_by_index<P: ParamDef>(&self, row_index: usize) -> Option<&P> {
let data_offset = self.row_data_offset(row_index)?.1;
Some(unsafe { self.offset::<P>(data_offset).as_ref() })
}
pub unsafe fn get_row_by_index_mut<P: ParamDef>(&mut self, row_index: usize) -> Option<&mut P> {
let data_offset = self.row_data_offset(row_index)?.1;
Some(unsafe { self.offset::<P>(data_offset).as_mut() })
}
pub unsafe fn rows<'a, P: ParamDef + 'a>(&'a self) -> impl Iterator<Item = (u32, &'a P)> + 'a {
self.lookup_table()
.iter()
.map(|l| unsafe { (l.param_id, self.get_row_by_index(l.index as usize).unwrap()) })
}
pub unsafe fn rows_mut<'a, P: ParamDef + 'a>(
&'a mut self,
) -> impl Iterator<Item = (u32, &'a mut P)> + 'a {
let mut file = NonNull::from_ref(self);
let range = self.lookup_table().as_ptr_range();
let mut ptr = range.start;
let end = range.end;
iter::from_fn(move || {
if ptr == end {
return None;
}
unsafe {
let lookup = ptr.as_ref().unwrap();
let result = file
.as_mut()
.get_row_by_index_mut(lookup.index as usize)
.unwrap();
ptr = ptr.add(1);
Some((lookup.param_id, result))
}
})
}
pub fn find_index(&self, id: u32) -> Option<usize> {
let table = self.lookup_table();
let target_index = self
.lookup_table()
.binary_search_by(|entry| entry.param_id.cmp(&id))
.ok()?;
Some(table[target_index].index as usize)
}
fn lookup_table(&self) -> &[RowLookupEntry] {
let aligned_file_size =
self.metadata()
.after_name_offset
.next_multiple_of(Self::LOOKUP_TABLE_ALIGNMENT) as usize;
unsafe {
slice::from_raw_parts(
self.offset::<RowLookupEntry>(aligned_file_size).as_ptr(),
self.row_count(),
)
}
}
const fn metadata(&self) -> &ParamFileMetadata {
unsafe {
let metadata_ptr = (self as *const Self).byte_sub(size_of::<ParamFileMetadata>())
as *const ParamFileMetadata;
&*metadata_ptr
}
}
const unsafe fn offset<T>(&self, offset: usize) -> NonNull<T> {
unsafe { NonNull::from_ref(self).cast::<u8>().add(offset).cast::<T>() }
}
fn row_descriptors<T: Into<u64> + Copy>(&self) -> &[RowDescriptor<T>] {
let offset = size_of::<ParamFile>().strict_add_signed(match self.as_enum() {
ParamFileType::V2(_) => -0x8,
ParamFileType::V5(_) => 0x8,
});
unsafe {
slice::from_raw_parts(
self.offset::<RowDescriptor<T>>(offset).as_ptr(),
self.row_count(),
)
}
}
fn row_data_offset(&self, row_index: usize) -> Option<(u32, usize)> {
if row_index >= self.row_count() {
return None;
}
match self.as_enum() {
ParamFileType::V2(_) => self
.row_descriptors::<u32>()
.get(row_index)
.map(|d| (d.id, d.data_offset())),
ParamFileType::V5(_) => {
debug_assert!(
self.flags().offset_64() && self.flags().offset_64_v5(),
"Expected all v5 param files to use 64-bit RowDescriptor offsets",
);
self.row_descriptors::<u64>()
.get(row_index)
.map(|d| (d.id, d.data_offset()))
}
}
}
fn as_enum(&self) -> ParamFileType<'_> {
unsafe {
match self.flags().file_version() {
2 => ParamFileType::V2(&self.v2),
5 => ParamFileType::V5(&self.v5),
n => panic!("Unexpected ParamFile version {n}"),
}
}
}
fn flags(&self) -> ParamFileFlags {
unsafe { self.v5.flags }
}
}
impl Drop for ParamFile {
fn drop(&mut self) {
unsafe {
match self.flags().file_version() {
2 => ManuallyDrop::drop(&mut self.v2),
5 => ManuallyDrop::drop(&mut self.v5),
n => panic!("Unexpected ParamFile version {n}"),
}
}
}
}
enum ParamFileType<'a> {
V2(&'a ParamFileV2),
V5(&'a ParamFileV5),
}
bitfield! {
#[derive(Clone, Copy, PartialEq, Eq)]
struct ParamFileFlags(u32);
impl Debug;
_, offset_64, _: 17;
_, offset_64_v5, _: 15;
pub u8, file_version, _: 14, 8;
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn proper_sizes() {
assert_eq!(0x38, size_of::<ParamFile>());
}
}