//! The sky block: its header and its sub-objects. //! //! Spec: `docs/formats/core-data-regions.md`. //! //! The sky region runs from the core index's `sky` offset to the next region. //! Its first 0x20 bytes are a header, and the sub-objects are named by a table //! of 32-bit offsets starting at `+0x20`. //! //! **The sub-object count is not at `+0x0c`.** That field reads 5 on level 0 //! and the table there holds 6 entries, 3 on level 1 against 4 entries, 7 on //! level 3 against 8 -- and across the disc `offsets.len() == count + 1` holds //! on only **10 of 27** levels, with level 2 reading 196,617. The field is //! reported as [`SkyHeader::field_0c`] and nothing depends on it. The offset //! table is what actually enumerates the sub-objects, and it is self-limiting: //! entries run while they stay strictly inside the region. //! //! Each sub-object opens with a count, twelve zero bytes, a four-float bounding //! sphere, then its texture dimensions. The sphere is the same shape as the //! shrub class header's: three floats and a radius, with the fourth positive //! where the first three can be either sign. use crate::buf::{Buf, Error, ErrorKind, Result}; /// Where the sub-object offset table starts within the sky region. pub const SKY_OFFSET_TABLE_OFFSET: usize = 0x20; /// Most sub-objects any level's table holds. const MAX_SUB_OBJECTS: usize = 64; /// The sky region's fixed header. #[derive(Debug, Clone, Copy, PartialEq)] pub struct SkyHeader { /// Bytes `+0x04..+0x0c`. Meaning not established. pub field_04: [u32; 2], /// The word at `+0x0c`. /// /// Reported, never relied on. It does **not** equal the sub-object count: /// see the module docs. Level 2 reads 196,617 against 4 sub-objects. pub field_0c: u32, /// The word at `+0x10`. Constant `64` on level 0. pub field_10: u32, /// The word at `+0x14`. Constant `192` on level 0. pub field_14: u32, /// The word at `+0x18`. Constant `64` on level 0. pub field_18: u32, } impl SkyHeader { /// Parse the sky header, bounds-checked against `bytes`. pub fn parse(bytes: &[u8]) -> Result { let mut b = Buf::new(bytes); b.skip(4, "sky header")?; Ok(SkyHeader { field_04: [b.u32("sky +0x04")?, b.u32("sky +0x08")?], field_0c: b.u32("sky +0x0c")?, field_10: b.u32("sky +0x10")?, field_14: b.u32("sky +0x14")?, field_18: b.u32("sky +0x18")?, }) } } /// One sky sub-object. #[derive(Debug, Clone, Copy, PartialEq)] pub struct SkySubObject<'a> { /// The leading count. `1` to `38` on level 0. pub count: u32, /// Bytes `+0x04..+0x10`. Zero on every sub-object measured. pub reserved: [u8; 12], /// The bounding sphere: three floats and a radius at `+0x10`. /// /// The fourth float is positive wherever the first three are negative on /// level 0, which is the reading that makes it a radius. Shrub uses the /// same shape and the same reasoning. pub sphere: [f32; 4], /// A memory offset at `+0x20`, distinct per sub-object. pub texture_offset: u32, /// Texture width at `+0x24`. `29`, `14`, `15`, `26`, `81` on level 0. pub width: u16, /// Texture height at `+0x26`. pub height: u16, /// The sub-object's bytes. pub data: &'a [u8], } impl<'a> SkySubObject<'a> { /// Parse one sub-object, bounds-checked against `bytes`. pub fn parse(bytes: &'a [u8]) -> Result> { if bytes.len() < 0x2c { return Err(Error { offset: 0, context: "sky sub-object is shorter than its header", kind: ErrorKind::OutOfBounds { needed: 0x2c, available: bytes.len(), }, }); } let mut b = Buf::new(bytes); let count = b.u32("sky sub-object count")?; let mut reserved = [0u8; 12]; reserved.copy_from_slice(b.read_bytes(12, "sky sub-object reserved")?); let sphere = [ b.f32("sky sphere x")?, b.f32("sky sphere y")?, b.f32("sky sphere z")?, b.f32("sky sphere radius")?, ]; let texture_offset = b.u32("sky texture offset")?; let width = b.u16("sky texture width")?; let height = b.u16("sky texture height")?; Ok(SkySubObject { count, reserved, sphere, texture_offset, width, height, data: bytes, }) } } /// A parsed sky region: its header and its sub-objects. pub struct Sky<'a> { pub header: SkyHeader, pub sub_objects: Vec>, } impl<'a> Sky<'a> { /// Parse the sky region at `bytes`, whose length bounds it. pub fn parse(bytes: &'a [u8]) -> Result> { let header = SkyHeader::parse(bytes)?; let mut offsets = Vec::new(); let mut b = Buf::new(bytes); b.seek(SKY_OFFSET_TABLE_OFFSET); // Entries run while they stay strictly inside the region. This is the // enumeration; the word at +0x0c is not. for _ in 0..MAX_SUB_OBJECTS { let at = b.pos(); if at + 4 > bytes.len() { break; } let v = b.u32("sky sub-object offset")?; if v == 0 || v as usize >= bytes.len() { break; } offsets.push(v as usize); } let mut sub_objects = Vec::with_capacity(offsets.len()); for (i, &start) in offsets.iter().enumerate() { let end = offsets.get(i + 1).copied().unwrap_or(bytes.len()); let end = end.min(bytes.len()); let Some(seg) = bytes.get(start..end) else { continue; }; sub_objects.push(SkySubObject::parse(seg)?); } Ok(Sky { header, sub_objects, }) } }