diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs index 7552b96..ddb439d 100644 --- a/crates/fparkan-terrain-format/src/lib.rs +++ b/crates/fparkan-terrain-format/src/lib.rs @@ -62,6 +62,18 @@ pub struct CompactSurfaceMask(pub u16); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MaterialClassMask(pub u8); +/// The two positional material-table selectors packed into a terrain face tag. +/// +/// The high byte selects from map-local `Land1.wea`; `0xff` is the observed +/// no-selection sentinel. The low byte selects from `Land2.wea`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TerrainMaterialLayers { + /// Optional high-byte selector for `Land1.wea`. + pub land1_selector: Option, + /// Low-byte selector for `Land2.wea`. + pub land2_selector: u8, +} + /// Terrain face with 28-byte source layout. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TerrainFace28 { @@ -81,6 +93,18 @@ pub struct TerrainFace28 { pub raw: [u8; 28], } +impl TerrainFace28 { + /// Decodes the proven two-table selector layout of [`Self::material_tag`]. + #[must_use] + pub const fn material_layers(&self) -> TerrainMaterialLayers { + let [land2_selector, land1] = self.material_tag.to_le_bytes(); + TerrainMaterialLayers { + land1_selector: if land1 == u8::MAX { None } else { Some(land1) }, + land2_selector, + } + } +} + /// Terrain stream descriptor. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TerrainStream { @@ -131,7 +155,10 @@ pub struct LandMeshDocument { pub accelerator: Vec<[u8; 4]>, /// Type 14 auxiliary words. pub aux14: Vec<[u8; 4]>, - /// Type 18 auxiliary words. + /// Type 18 microtexture mapping words. + /// + /// `Terrain.dll!CLandscape` requires this stream and retains it as a + /// four-byte-per-entry mapping chunk. pub aux18: Vec<[u8; 4]>, /// Faces. pub faces: Vec, @@ -1386,6 +1413,33 @@ mod tests { ); } + #[test] + fn terrain_material_tag_decodes_two_sidecar_selectors() { + let mut raw_face = face([0, 1, 2], [None, None, None]); + raw_face[4..6].copy_from_slice(&0x0102_u16.to_le_bytes()); + let nres = decode_nres(&minimal_land_msh(&raw_face)).expect("nres"); + let document = decode_land_msh(&nres).expect("land mesh"); + + assert_eq!( + document.faces[0].material_layers(), + TerrainMaterialLayers { + land1_selector: Some(1), + land2_selector: 2, + } + ); + + raw_face[4..6].copy_from_slice(&0xff03_u16.to_le_bytes()); + let nres = decode_nres(&minimal_land_msh(&raw_face)).expect("nres"); + let document = decode_land_msh(&nres).expect("land mesh"); + assert_eq!( + document.faces[0].material_layers(), + TerrainMaterialLayers { + land1_selector: None, + land2_selector: 3, + } + ); + } + #[test] fn decodes_minimal_land_map() { let nres = decode_nres(&minimal_land_map([(-1, -1), (-1, -1)], 0)).expect("nres"); diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index d1b584f..e75712c 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1550,6 +1550,17 @@ This is evidence for a two-layer terrain-material contract, but not yet for its blend equation, so the Vulkan bridge intentionally remains texture-agnostic until the base/overlay composition is recovered. +Static recovery of `Terrain.dll!CLandscape` now fixes the loader contract behind +that observation. The constructor opens `Land.msh`, creates one material manager +from `Land1.wea`, then invokes that manager's second-table load with `Land2.wea`. +It requires NRes type 18 with the original diagnostic “microtexture mapping +chunk” and retains it as four-byte entries; type 14 is optional. Therefore the +two WEAR selectors must not be collapsed into an invented one-layer material, +and the type-18 `aux18` stream is now documented as microtexture mapping data. +`TerrainFace28::material_layers` exposes the evidenced high-byte `Land1` +selector (with `0xff` absent sentinel) and low-byte `Land2` selector. The +manager's actual blend/pass operation remains unrecovered. + The static Vulkan bridge now carries each contiguous face run as a separate draw range keyed by the original packed `material_tag`; it neither reorders triangles nor collapses the high byte. For the first usable visual layer, the