A z -> y -> x tree of u16 node offsets over 4x4x4-unit cells, each populated cell naming one 128-byte mask of 1024 visibility bits that the renderer fills from the camera's cell each frame. Two sentinels carry meaning and one of them is easy to miss: a z or y slot of 0 means "no subtree", and an x slot of 0xffff means "no cell". Reading the latter as a mask index gives a grid whose highest mask is 65535 on every single level, which is the tell. The check that makes this real is the mask array. It starts at masks_offset and runs to (highest_mask + 1) * 0x80, and on every level with a grid that ends at the block boundary exactly -- widest unexplained tail across the disc is 0 bytes. A wrong tree shape or cell size would leave the highest mask index elsewhere entirely. 412,521 populated cells over 140,939 masks, across the 24 levels that have one. An offset of 0 means the level has no occlusion data at all, which is not the same as "at the start of the file" -- that is where the tfrags block lives, and parsing it as a grid produces a mask array that fits anyway, with 1600 bytes to spare. That is what made level 10 look plausible on the first pass; three levels (10, 24, 25) carry none.
264 lines
9.4 KiB
Rust
264 lines
9.4 KiB
Rust
//! Precomputed occlusion: the potentially-visible-set grid.
|
|
//!
|
|
//! Spec: `docs/formats/occlusion.md`.
|
|
//!
|
|
//! The level ships a z → y → x tree of `u16` node offsets over 4x4x4-unit
|
|
//! cells. Populated cells name one 128-byte mask of 1024 visibility bits, which
|
|
//! the renderer fills from the camera's cell each frame and then consults per
|
|
//! object *before* any frustum test.
|
|
//!
|
|
//! Two sentinels carry meaning and are easy to miss. A z or y slot of `0`
|
|
//! means "no subtree here"; an x slot of `0xffff` means "no cell". Reading the
|
|
//! latter as a mask index produces a grid whose highest mask is 65535 on every
|
|
//! level, which is the tell.
|
|
|
|
use crate::buf::{Buf, Error, ErrorKind, Result};
|
|
|
|
/// Bytes per visibility mask (1024 bits).
|
|
pub const MASK_BYTES: usize = 0x80;
|
|
|
|
/// Cell edge in world units. The camera position is scaled by 0.25 and
|
|
/// truncated, so cell 0 spans -4 < p < 4.
|
|
pub const CELL_SIZE: f32 = 4.0;
|
|
|
|
/// An x slot of this value means "no cell".
|
|
const NO_CELL: u16 = 0xffff;
|
|
|
|
/// One populated grid cell, in cell indices.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct OcclusionCell {
|
|
pub x: u16,
|
|
pub y: u16,
|
|
pub z: u16,
|
|
/// Index into [`Occlusion::masks`].
|
|
pub mask: u16,
|
|
}
|
|
|
|
/// A parsed occlusion block.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct Occlusion {
|
|
/// Byte offset of the mask array from the start of the block.
|
|
pub masks_offset: u32,
|
|
/// Every populated cell, in tree order (z, then y, then x).
|
|
pub cells: Vec<OcclusionCell>,
|
|
/// Masks `0 ..= highest index any cell names`. The game stores no count.
|
|
pub masks: Vec<[u8; MASK_BYTES]>,
|
|
}
|
|
|
|
/// Cell indices for a camera position, as the game computes them: scale by
|
|
/// 0.25 and truncate toward zero.
|
|
pub fn cell_coords(pos: [f32; 3]) -> [i32; 3] {
|
|
pos.map(|p| (p / CELL_SIZE) as i32)
|
|
}
|
|
|
|
impl Occlusion {
|
|
/// The mask index for a cell, or `None` if the grid has no cell there.
|
|
///
|
|
/// This is a linear scan rather than a tree walk. The tree exists so the
|
|
/// *game* can read it directly from the block; by this point every cell has
|
|
/// already been enumerated, and a hash of `(x, y, z)` would be a map to
|
|
/// maintain for a lookup that happens once per object per frame.
|
|
pub fn lookup(&self, x: i32, y: i32, z: i32) -> Option<u16> {
|
|
let (x, y, z) = (x as u16, y as u16, z as u16);
|
|
self.cells
|
|
.iter()
|
|
.find(|c| c.x == x && c.y == y && c.z == z)
|
|
.map(|c| c.mask)
|
|
}
|
|
|
|
/// Is an object with these occlusion bits visible from this mask?
|
|
///
|
|
/// The bits are `byte << 8 | 1 << bit`, folded at level load. The test is
|
|
/// the game's: `mask[byte] & bit == 0` means skip.
|
|
pub fn visible(&self, mask_index: u16, bits: u16) -> bool {
|
|
let byte = (bits >> 8) as usize;
|
|
let bit = 1u8 << (bits & 0xff);
|
|
self.masks
|
|
.get(mask_index as usize)
|
|
.is_some_and(|m| byte < MASK_BYTES && m[byte] & bit != 0)
|
|
}
|
|
}
|
|
|
|
/// Parse a raw occlusion block.
|
|
///
|
|
/// Every read is bounds-checked and returns an error rather than reading past
|
|
/// the end; the game reads unchecked, and retail data never needs it to.
|
|
pub fn parse_occlusion_block(block: &[u8]) -> Result<Occlusion> {
|
|
let mut b = Buf::new(block);
|
|
let masks_offset = b.i32("occlusion mask offset")?;
|
|
let (z_base, z_count) = (b.u16("occlusion z base")?, b.u16("occlusion z count")?);
|
|
if masks_offset <= 8 || masks_offset as usize > block.len() {
|
|
return Err(Error {
|
|
offset: 0,
|
|
context: "occlusion: mask offset outside the block",
|
|
kind: ErrorKind::Invalid("occlusion mask offset"),
|
|
});
|
|
}
|
|
let at = |o: usize| -> Result<u16> {
|
|
let end = o
|
|
.checked_add(2)
|
|
.filter(|&e| e <= block.len())
|
|
.ok_or(Error {
|
|
offset: o,
|
|
context: "occlusion node offset past the block",
|
|
kind: ErrorKind::OutOfBounds {
|
|
needed: 2,
|
|
available: block.len().saturating_sub(o),
|
|
},
|
|
})?;
|
|
Ok(u16::from_le_bytes([block[end - 2], block[end - 1]]))
|
|
};
|
|
|
|
let mut cells = Vec::new();
|
|
for zi in 0..z_count as usize {
|
|
let zo = at(8 + zi * 2)?;
|
|
if zo == 0 {
|
|
continue;
|
|
}
|
|
let yn = zo as usize * 4;
|
|
let y_base = at(yn)?;
|
|
let y_count = at(yn + 2)?;
|
|
for yi in 0..y_count as usize {
|
|
let yo = at(yn + 4 + yi * 2)?;
|
|
if yo == 0 {
|
|
continue;
|
|
}
|
|
let xn = yo as usize * 4;
|
|
let x_base = at(xn)?;
|
|
let x_count = at(xn + 2)?;
|
|
for xi in 0..x_count as usize {
|
|
let mask = at(xn + 4 + xi * 2)?;
|
|
if mask == NO_CELL {
|
|
continue;
|
|
}
|
|
cells.push(OcclusionCell {
|
|
x: x_base.wrapping_add(xi as u16),
|
|
y: y_base.wrapping_add(yi as u16),
|
|
z: z_base.wrapping_add(zi as u16),
|
|
mask,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let highest = cells.iter().map(|c| c.mask).max().unwrap_or(0) as usize;
|
|
let masks_start = masks_offset as usize;
|
|
let need = highest
|
|
.checked_add(1)
|
|
.and_then(|n| n.checked_mul(MASK_BYTES))
|
|
.and_then(|n| masks_start.checked_add(n))
|
|
.ok_or(Error {
|
|
offset: masks_start,
|
|
context: "occlusion mask array overflows",
|
|
kind: ErrorKind::InvalidLength("occlusion mask array"),
|
|
})?;
|
|
if need > block.len() {
|
|
return Err(Error {
|
|
offset: masks_start,
|
|
context: "occlusion mask array runs past the block",
|
|
kind: ErrorKind::OutOfBounds {
|
|
needed: need,
|
|
available: block.len(),
|
|
},
|
|
});
|
|
}
|
|
let mut masks = Vec::with_capacity(highest + 1);
|
|
for i in 0..=highest {
|
|
let at = masks_start + i * MASK_BYTES;
|
|
masks.push(block[at..at + MASK_BYTES].try_into().unwrap());
|
|
}
|
|
|
|
Ok(Occlusion {
|
|
masks_offset: masks_offset as u32,
|
|
cells,
|
|
masks,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A two-cell grid: one populated, one empty via the `0xffff` sentinel.
|
|
fn block() -> Vec<u8> {
|
|
// Masks at 0x40; the tree is tiny so only two masks are needed.
|
|
const MASKS_AT: usize = 0x40;
|
|
let mut b = vec![0u8; MASKS_AT + 2 * MASK_BYTES];
|
|
b[0x00..0x04].copy_from_slice(&(MASKS_AT as i32).to_le_bytes());
|
|
b[0x04..0x06].copy_from_slice(&0u16.to_le_bytes()); // z base
|
|
b[0x06..0x08].copy_from_slice(&1u16.to_le_bytes()); // z count
|
|
b[0x08..0x0a].copy_from_slice(&10u16.to_le_bytes()); // z node at 10*4 = 40
|
|
// y node at 40: base 5, one y slot pointing at x node 12 (48 bytes)
|
|
b[40..42].copy_from_slice(&5u16.to_le_bytes());
|
|
b[42..44].copy_from_slice(&1u16.to_le_bytes());
|
|
b[44..46].copy_from_slice(&12u16.to_le_bytes());
|
|
// x node at 48: base 7, two x slots, the second empty
|
|
b[48..50].copy_from_slice(&7u16.to_le_bytes());
|
|
b[50..52].copy_from_slice(&2u16.to_le_bytes());
|
|
b[52..54].copy_from_slice(&0u16.to_le_bytes()); // mask 0
|
|
b[54..56].copy_from_slice(&NO_CELL.to_le_bytes());
|
|
// Mask 0: visibility bit 5 of byte 0 set.
|
|
b[MASKS_AT] = 1 << 5;
|
|
b
|
|
}
|
|
|
|
#[test]
|
|
fn parses_a_two_cell_grid() {
|
|
let o = parse_occlusion_block(&block()).unwrap();
|
|
assert_eq!(o.masks_offset, 0x40);
|
|
// The empty x slot is dropped, so only one cell survives.
|
|
assert_eq!(o.cells.len(), 1);
|
|
assert_eq!(
|
|
o.cells[0],
|
|
OcclusionCell {
|
|
x: 7,
|
|
y: 5,
|
|
z: 0,
|
|
mask: 0
|
|
}
|
|
);
|
|
assert_eq!(o.masks.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn the_empty_sentinel_is_not_a_mask() {
|
|
// Without the sentinel, every level's highest mask would be 65535.
|
|
let o = parse_occlusion_block(&block()).unwrap();
|
|
assert!(o.cells.iter().all(|c| c.mask != NO_CELL));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_and_visibility() {
|
|
let o = parse_occlusion_block(&block()).unwrap();
|
|
assert_eq!(o.lookup(7, 5, 0), Some(0));
|
|
assert_eq!(o.lookup(8, 5, 0), None);
|
|
// Bits are `byte << 8 | 1 << bit`. Bit 5 of mask 0 is set.
|
|
let bits = |byte: u16, bit: u16| (byte << 8) | (1 << bit);
|
|
assert!(o.visible(0, bits(0, 5)));
|
|
assert!(!o.visible(0, bits(0, 4)));
|
|
assert!(!o.visible(0, bits(1, 5)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_malformed_block_is_an_error_not_a_panic() {
|
|
assert!(parse_occlusion_block(&[]).is_err());
|
|
assert!(parse_occlusion_block(&[0u8; 8]).is_err());
|
|
// A mask offset past the end of the buffer.
|
|
let mut b = block();
|
|
b[0x00..0x04].copy_from_slice(&9_000_000i32.to_le_bytes());
|
|
assert!(parse_occlusion_block(&b).is_err());
|
|
// A node offset pointing outside the block.
|
|
let mut b = block();
|
|
b[0x08..0x0a].copy_from_slice(&9_000u16.to_le_bytes());
|
|
assert!(parse_occlusion_block(&b).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn cells_are_four_units_across() {
|
|
assert_eq!(cell_coords([0.0, 0.0, 0.0]), [0, 0, 0]);
|
|
assert_eq!(cell_coords([5.0, -5.0, 9.0]), [1, -1, 2]);
|
|
// Cell 0 spans -4 < p < 4.
|
|
assert_eq!(cell_coords([-3.9, 3.9, 0.0]), [0, 0, 0]);
|
|
}
|
|
}
|