aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-terrain-format
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 15:52:46 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 15:52:46 +0300
commit83caf4d6abf95b47046e1b3cc8c3203323ee0eff (patch)
tree3a7b6efb3226647df701683bfa2dd6803d016cd5 /crates/fparkan-terrain-format
parent4df46865091fe12a3e04606d4710521516f35d85 (diff)
downloadfparkan-83caf4d6abf95b47046e1b3cc8c3203323ee0eff.tar.xz
fparkan-83caf4d6abf95b47046e1b3cc8c3203323ee0eff.zip
feat(terrain): recover material layer contract
Diffstat (limited to 'crates/fparkan-terrain-format')
-rw-r--r--crates/fparkan-terrain-format/src/lib.rs56
1 files changed, 55 insertions, 1 deletions
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<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>,
@@ -1387,6 +1414,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");
let document = decode_land_map(&nres).expect("land map");