Parse the sky block: 149 sub-objects with a bounding sphere, and correct the count field
Range measured: all 27 levels, each sky region from the core index's sky offset to the next region. Method: read the 32-bit offset table at +0x20, bounded by staying inside the region, and parse each sub-object it names. Result: 149 sub-objects across 27 levels. Each opens with a count, then a bounding sphere of four floats at +0x10, then a texture offset at +0x20 and dimensions at +0x24/+0x26: +000: 26 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 +010: e0 8d b2 c1 | 9f d6 f6 3d | ef fc fb be | 1c 61 4e 41 +020: 90 99 05 00 | 1d 00 | 1d 00 | 00 00 e8 00 ... Count 1 to 38, width 3 to 81, radius 1.95 to 14.56, and the radius is positive on 149 of 149 -- the same sphere shape and the same reasoning as the shrub class header's. Correction to an earlier claim: the word at +0x0c is not the sub-object count. It reads 5 on level 0 where the table holds 6 entries, 3 on level 1 against 4, 7 on level 3 against 8, and 196617 on level 2 against 4. Across the disc offsets.len() == count+1 holds on 10 of 27 levels. The field is reported as SkyHeader::field_0c and nothing depends on it; the offset table enumerates the sub-objects and is self-limiting. That earlier claim came from level 0 alone, where 5 against 6 looked like a consistent off-by-one. It is not an off-by-one -- it is a field that happens to be small on most levels and is not a count at all. gc_formats::sky parses the header and the sub-objects. The gated test asserts the sphere is finite, the radius positive where positive at all, the dimensions non-zero, and the sub-object list non-empty on every level.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
//! 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<SkyHeader> {
|
||||
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<SkySubObject<'a>> {
|
||||
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<SkySubObject<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Sky<'a> {
|
||||
/// Parse the sky region at `bytes`, whose length bounds it.
|
||||
pub fn parse(bytes: &'a [u8]) -> Result<Sky<'a>> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user