use rootcause::Report;
use thiserror::Error;
use winnow::Parser;
use winnow::binary::le_f32;
use winnow::binary::le_i64;
use winnow::binary::le_u64;
use winnow::combinator::repeat;
use winnow::error::ContextError;
use winnow::error::ErrMode;
use crate::data::parser_utils::WResult;
use crate::data::parser_utils::resolve_relptr;
const INSTANCE_SIZE: usize = 16;
#[derive(Debug, Error)]
pub enum ForestError {
#[error("data too short: need {need} bytes at offset 0x{offset:X}, have {have}")]
DataTooShort { offset: usize, need: usize, have: usize },
#[error("parse error: {0}")]
ParseError(String),
#[error("LOD0 species table not found in header")]
NoLod0Table,
}
#[derive(Debug, Clone, Copy)]
pub struct ForestInstance {
pub species_index: usize,
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug)]
pub struct Forest {
pub species: Vec<String>,
pub instances: Vec<ForestInstance>,
}
fn parse_string_table_entry(input: &mut &[u8]) -> WResult<(u64, i64)> {
let len = le_u64.parse_next(input)?;
let relptr = le_i64.parse_next(input)?;
Ok((len, relptr))
}
struct RawInstance {
x: f32,
y: f32,
z: f32,
}
fn parse_raw_instance(input: &mut &[u8]) -> WResult<RawInstance> {
let x = le_f32.parse_next(input)?;
let y = le_f32.parse_next(input)?;
let z = le_f32.parse_next(input)?;
let _w = le_f32.parse_next(input)?;
Ok(RawInstance { x, y, z })
}
fn find_lod0_species_table(data: &[u8], num_species: usize, search_end: usize) -> Option<(usize, Vec<(usize, usize)>)> {
let ns = num_species as u32;
let marker = [ns.to_le_bytes(), 0u32.to_le_bytes(), ns.to_le_bytes(), 1u32.to_le_bytes()].concat();
let end = search_end.min(data.len());
let marker_pos = data[..end].windows(marker.len()).position(|w| w == marker)?;
let table_start = marker_pos + marker.len();
let table_end = table_start + num_species * 8;
if table_end > end {
return None;
}
let mut entries = Vec::with_capacity(num_species);
for i in 0..num_species {
let off = table_start + i * 8;
let start = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as usize;
let count = u32::from_le_bytes(data[off + 4..off + 8].try_into().unwrap()) as usize;
entries.push((start, count));
}
let total: usize = entries.iter().map(|(_, c)| *c).sum();
Some((total, entries))
}
pub fn parse_forest(file_data: &[u8]) -> Result<Forest, Report<ForestError>> {
if file_data.len() < 32 {
return Err(Report::new(ForestError::DataTooShort { offset: 0, need: 32, have: file_data.len() }));
}
let header_input = &mut &file_data[0x00..];
let num_species = le_u64
.parse_next(header_input)
.map_err(|e: ErrMode<ContextError>| Report::new(ForestError::ParseError(format!("{e}"))))?
as usize;
let string_table_offset = le_u64
.parse_next(header_input)
.map_err(|e: ErrMode<ContextError>| Report::new(ForestError::ParseError(format!("{e}"))))?
as usize;
if num_species == 0 {
return Ok(Forest { species: Vec::new(), instances: Vec::new() });
}
if num_species > 1000 {
return Err(Report::new(ForestError::ParseError(format!("unreasonable species count: {num_species}"))));
}
let string_table_end = string_table_offset + num_species * 16;
if string_table_end > file_data.len() {
return Err(Report::new(ForestError::DataTooShort {
offset: string_table_offset,
need: num_species * 16,
have: file_data.len().saturating_sub(string_table_offset),
}));
}
let mut species = Vec::with_capacity(num_species);
let mut data_start = string_table_end;
for i in 0..num_species {
let entry_off = string_table_offset + i * 16;
let input = &mut &file_data[entry_off..];
let (str_len, str_relptr) = parse_string_table_entry(input)
.map_err(|e: ErrMode<ContextError>| Report::new(ForestError::ParseError(format!("{e}"))))?;
let str_len = str_len as usize;
let str_abs = resolve_relptr(entry_off, str_relptr);
if str_len == 0 || str_abs + str_len > file_data.len() {
species.push(format!("species_{i}"));
continue;
}
let name_bytes = &file_data[str_abs..str_abs + str_len - 1];
let name = String::from_utf8_lossy(name_bytes).into_owned();
species.push(name);
let str_end = str_abs + str_len;
if str_end > data_start {
data_start = str_end;
}
}
let (lod0_total, species_table) = find_lod0_species_table(file_data, num_species, string_table_offset)
.ok_or_else(|| Report::new(ForestError::NoLod0Table))?;
let remaining = file_data.len() - data_start;
let max_instances = remaining / INSTANCE_SIZE;
let input = &mut &file_data[data_start..data_start + max_instances * INSTANCE_SIZE];
let raw_instances: Vec<RawInstance> = repeat(max_instances, parse_raw_instance)
.parse_next(input)
.map_err(|e: ErrMode<ContextError>| Report::new(ForestError::ParseError(format!("{e}"))))?;
let mut instances = Vec::with_capacity(lod0_total);
for (sp_idx, &(start, count)) in species_table.iter().enumerate() {
if count == 0 {
continue;
}
let end = start + count;
if end > raw_instances.len() {
eprintln!(
"Warning: forest species {sp_idx} range {start}..{end} exceeds instance count {}",
raw_instances.len()
);
continue;
}
for raw in &raw_instances[start..end] {
instances.push(ForestInstance { species_index: sp_idx, x: raw.x, y: raw.y, z: raw.z });
}
}
Ok(Forest { species, instances })
}