//! 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, /// 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 { 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 high /// byte selects the mask byte and the low byte is *already* the shifted /// bit, not an index. Shifting the low byte again is both wrong and a /// panic, since any byte above 8 overflows a `u8` shift. /// /// 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 = (bits & 0xff) as u8; 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 { 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 { 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 { // 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]); } }