feat(terrain): recover material layer contract

This commit is contained in:
2026-07-18 16:52:46 +04:00
parent 4df4686509
commit 83caf4d6ab
2 changed files with 66 additions and 1 deletions
+55 -1
View File
@@ -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<u8>,
/// 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<TerrainFace28>,
@@ -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");
+11
View File
@@ -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