fix(terrain): preserve opaque shade lookup keys
This commit is contained in:
@@ -27,7 +27,7 @@ use fparkan_assets::{
|
|||||||
use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
|
use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions};
|
||||||
use fparkan_inspection::{
|
use fparkan_inspection::{
|
||||||
inspect_archive_file, inspect_land_msh_bounds_file, inspect_model_from_root,
|
inspect_archive_file, inspect_land_msh_bounds_file, inspect_model_from_root,
|
||||||
load_land_msh_from_path, ArchiveInspection, ModelInspection,
|
inspect_wear_from_root, load_land_msh_from_path, ArchiveInspection, ModelInspection,
|
||||||
};
|
};
|
||||||
use fparkan_path::{normalize_relative, PathPolicy};
|
use fparkan_path::{normalize_relative, PathPolicy};
|
||||||
use fparkan_prototype::build_prototype_graph_report;
|
use fparkan_prototype::build_prototype_graph_report;
|
||||||
@@ -46,6 +46,7 @@ const MISSION_GRAPH_SCHEMA: &str = "fparkan-mission-graph-v1";
|
|||||||
const MISSION_INSPECT_SCHEMA: &str = "fparkan-mission-inspect-v1";
|
const MISSION_INSPECT_SCHEMA: &str = "fparkan-mission-inspect-v1";
|
||||||
const TERRAIN_INSPECT_SCHEMA: &str = "fparkan-terrain-inspect-v1";
|
const TERRAIN_INSPECT_SCHEMA: &str = "fparkan-terrain-inspect-v1";
|
||||||
const MODEL_INSPECT_SCHEMA: &str = "fparkan-model-inspect-v1";
|
const MODEL_INSPECT_SCHEMA: &str = "fparkan-model-inspect-v1";
|
||||||
|
const WEAR_INSPECT_SCHEMA: &str = "fparkan-wear-inspect-v1";
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct ArchiveInspectOutput<'a> {
|
struct ArchiveInspectOutput<'a> {
|
||||||
@@ -128,11 +129,11 @@ struct TerrainInspectOutput {
|
|||||||
faces: usize,
|
faces: usize,
|
||||||
slots: usize,
|
slots: usize,
|
||||||
material_tags: Vec<TerrainMaterialTagCount>,
|
material_tags: Vec<TerrainMaterialTagCount>,
|
||||||
shade_material_pairs: usize,
|
shade_pairs: usize,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
shade_material_lookup_min: Option<u16>,
|
shade_lookup_key_min: Option<u16>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
shade_material_lookup_max: Option<u16>,
|
shade_lookup_key_max: Option<u16>,
|
||||||
shade_batch_boundaries: usize,
|
shade_batch_boundaries: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +143,19 @@ struct TerrainMaterialTagCount {
|
|||||||
faces: usize,
|
faces: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct WearInspectOutput {
|
||||||
|
schema_version: &'static str,
|
||||||
|
archive: String,
|
||||||
|
resource: String,
|
||||||
|
materials: usize,
|
||||||
|
lightmaps: usize,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
first_material: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
last_material: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct ModelInspectOutput<'a> {
|
struct ModelInspectOutput<'a> {
|
||||||
schema_version: &'static str,
|
schema_version: &'static str,
|
||||||
@@ -238,6 +252,10 @@ fn run(args: &[String]) -> Result<(), String> {
|
|||||||
let rest = strip_format_json(rest)?;
|
let rest = strip_format_json(rest)?;
|
||||||
inspect_terrain(&rest)
|
inspect_terrain(&rest)
|
||||||
}
|
}
|
||||||
|
[domain, command, rest @ ..] if domain == "wear" && command == "inspect" => {
|
||||||
|
let rest = strip_format_json(rest)?;
|
||||||
|
inspect_wear(&rest)
|
||||||
|
}
|
||||||
[domain, command, rest @ ..] if domain == "model" && command == "inspect" => {
|
[domain, command, rest @ ..] if domain == "model" && command == "inspect" => {
|
||||||
let rest = strip_format_json(rest)?;
|
let rest = strip_format_json(rest)?;
|
||||||
inspect_model(&rest)
|
inspect_model(&rest)
|
||||||
@@ -479,19 +497,13 @@ fn inspect_terrain(args: &[String]) -> Result<(), String> {
|
|||||||
.map(|(tag, faces)| TerrainMaterialTagCount { tag, faces })
|
.map(|(tag, faces)| TerrainMaterialTagCount { tag, faces })
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
material_tags.sort_unstable_by_key(|entry| entry.tag);
|
material_tags.sort_unstable_by_key(|entry| entry.tag);
|
||||||
let shade_material_pairs = (0..terrain.slots.slots_raw.len())
|
let shade_pairs = (0..terrain.slots.slots_raw.len())
|
||||||
.filter_map(|slot_index| terrain.slot_material_pairs(slot_index))
|
.filter_map(|slot_index| terrain.slot_material_pairs(slot_index))
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let shade_material_lookup_min = shade_material_pairs
|
let shade_lookup_key_min = shade_pairs.iter().map(|pair| pair.shade_lookup_key()).min();
|
||||||
.iter()
|
let shade_lookup_key_max = shade_pairs.iter().map(|pair| pair.shade_lookup_key()).max();
|
||||||
.map(|pair| pair.shade_selection().material_index)
|
let shade_batch_boundaries = shade_pairs
|
||||||
.min();
|
|
||||||
let shade_material_lookup_max = shade_material_pairs
|
|
||||||
.iter()
|
|
||||||
.map(|pair| pair.shade_selection().material_index)
|
|
||||||
.max();
|
|
||||||
let shade_batch_boundaries = shade_material_pairs
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|pair| pair.flags & 0x0010 != 0)
|
.filter(|pair| pair.flags & 0x0010 != 0)
|
||||||
.count();
|
.count();
|
||||||
@@ -506,15 +518,35 @@ fn inspect_terrain(args: &[String]) -> Result<(), String> {
|
|||||||
faces: terrain.faces.len(),
|
faces: terrain.faces.len(),
|
||||||
slots: terrain.slots.slots_raw.len(),
|
slots: terrain.slots.slots_raw.len(),
|
||||||
material_tags,
|
material_tags,
|
||||||
shade_material_pairs: shade_material_pairs.len(),
|
shade_pairs: shade_pairs.len(),
|
||||||
shade_material_lookup_min,
|
shade_lookup_key_min,
|
||||||
shade_material_lookup_max,
|
shade_lookup_key_max,
|
||||||
shade_batch_boundaries,
|
shade_batch_boundaries,
|
||||||
})?
|
})?
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inspect_wear(args: &[String]) -> Result<(), String> {
|
||||||
|
let root = parse_root_alias(args)?;
|
||||||
|
let archive = parse_required(args, &["--archive"], "--archive")?;
|
||||||
|
let resource = parse_required(args, &["--resource"], "--resource")?;
|
||||||
|
let inspection = inspect_wear_from_root(&root, &archive, &resource)?;
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serialize_json(&WearInspectOutput {
|
||||||
|
schema_version: WEAR_INSPECT_SCHEMA,
|
||||||
|
archive,
|
||||||
|
resource,
|
||||||
|
materials: inspection.materials,
|
||||||
|
lightmaps: inspection.lightmaps,
|
||||||
|
first_material: inspection.first_material,
|
||||||
|
last_material: inspection.last_material,
|
||||||
|
})?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn inspect_model(args: &[String]) -> Result<(), String> {
|
fn inspect_model(args: &[String]) -> Result<(), String> {
|
||||||
let root = parse_root_alias(args)?;
|
let root = parse_root_alias(args)?;
|
||||||
let archive = parse_required(args, &["--archive"], "--archive")?;
|
let archive = parse_required(args, &["--archive"], "--archive")?;
|
||||||
@@ -632,7 +664,7 @@ fn prototype_graph_requiredness_label(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: fparkan corpus discover|validate --root <path> [--format json] | archive inspect <file> [--format json] | terrain inspect <Land.msh> [--format json] | model inspect --root <path> --archive <archive> --resource <model.msh> [--format json] | prototype inspect --root <path> --key <key> [--format json] | mission graph|inspect --root <path> --mission <path> [--format json]".to_string()
|
"usage: fparkan corpus discover|validate --root <path> [--format json] | archive inspect <file> [--format json] | terrain inspect <Land.msh> [--format json] | wear inspect --root <path> --archive <archive> --resource <wear.wea> [--format json] | model inspect --root <path> --archive <archive> --resource <model.msh> [--format json] | prototype inspect --root <path> --key <key> [--format json] | mission graph|inspect --root <path> --mission <path> [--format json]".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -759,16 +791,35 @@ mod tests {
|
|||||||
TerrainMaterialTagCount { tag: 0, faces: 1 },
|
TerrainMaterialTagCount { tag: 0, faces: 1 },
|
||||||
TerrainMaterialTagCount { tag: 3, faces: 1 },
|
TerrainMaterialTagCount { tag: 3, faces: 1 },
|
||||||
],
|
],
|
||||||
shade_material_pairs: 2,
|
shade_pairs: 2,
|
||||||
shade_material_lookup_min: Some(1),
|
shade_lookup_key_min: Some(1),
|
||||||
shade_material_lookup_max: Some(2),
|
shade_lookup_key_max: Some(2),
|
||||||
shade_batch_boundaries: 1,
|
shade_batch_boundaries: 1,
|
||||||
})
|
})
|
||||||
.expect("serialize terrain inspection");
|
.expect("serialize terrain inspection");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
json,
|
json,
|
||||||
"{\"schema_version\":\"fparkan-terrain-inspect-v1\",\"path\":\"DATA/MAPS/AutoMAP/Land.msh\",\"positions\":3,\"min\":[-1.0,-2.0,-3.0],\"max\":[4.0,5.0,6.0],\"faces\":2,\"slots\":4,\"material_tags\":[{\"tag\":0,\"faces\":1},{\"tag\":3,\"faces\":1}],\"shade_material_pairs\":2,\"shade_material_lookup_min\":1,\"shade_material_lookup_max\":2,\"shade_batch_boundaries\":1}"
|
"{\"schema_version\":\"fparkan-terrain-inspect-v1\",\"path\":\"DATA/MAPS/AutoMAP/Land.msh\",\"positions\":3,\"min\":[-1.0,-2.0,-3.0],\"max\":[4.0,5.0,6.0],\"faces\":2,\"slots\":4,\"material_tags\":[{\"tag\":0,\"faces\":1},{\"tag\":3,\"faces\":1}],\"shade_pairs\":2,\"shade_lookup_key_min\":1,\"shade_lookup_key_max\":2,\"shade_batch_boundaries\":1}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wear_inspect_output_retains_material_bounds() {
|
||||||
|
let json = serialize_json(&WearInspectOutput {
|
||||||
|
schema_version: WEAR_INSPECT_SCHEMA,
|
||||||
|
archive: "system.rlb".to_string(),
|
||||||
|
resource: "SHADE.WEA".to_string(),
|
||||||
|
materials: 1,
|
||||||
|
lightmaps: 0,
|
||||||
|
first_material: Some("LIGHT1".to_string()),
|
||||||
|
last_material: Some("LIGHT1".to_string()),
|
||||||
|
})
|
||||||
|
.expect("serialize wear inspection");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
json,
|
||||||
|
"{\"schema_version\":\"fparkan-wear-inspect-v1\",\"archive\":\"system.rlb\",\"resource\":\"SHADE.WEA\",\"materials\":1,\"lightmaps\":0,\"first_material\":\"LIGHT1\",\"last_material\":\"LIGHT1\"}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,19 @@ pub struct WearMaterialTexture {
|
|||||||
pub image: fparkan_texm::RgbaImage,
|
pub image: fparkan_texm::RgbaImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compact inspection summary of a WEAR resource stored in an archive.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WearInspection {
|
||||||
|
/// Number of material rows.
|
||||||
|
pub materials: usize,
|
||||||
|
/// Number of lightmap rows.
|
||||||
|
pub lightmaps: usize,
|
||||||
|
/// First material resource name, when present.
|
||||||
|
pub first_material: Option<String>,
|
||||||
|
/// Last material resource name, when present.
|
||||||
|
pub last_material: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Land map/msh inspection payload.
|
/// Land map/msh inspection payload.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MapInspection {
|
pub struct MapInspection {
|
||||||
@@ -374,6 +387,34 @@ pub fn inspect_model_from_root(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inspects a WEAR resource through repository-backed lookup.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a string error when the resource cannot be resolved or parsed as a
|
||||||
|
/// valid WEAR payload.
|
||||||
|
pub fn inspect_wear_from_root(
|
||||||
|
root: &Path,
|
||||||
|
archive: &str,
|
||||||
|
resource: &str,
|
||||||
|
) -> Result<WearInspection, String> {
|
||||||
|
let bytes = read_resource_bytes_diagnostic(root, archive, resource)
|
||||||
|
.map_err(|err| render_human(&err))?;
|
||||||
|
let wear = decode_wear(&bytes).map_err(|err| err.to_string())?;
|
||||||
|
Ok(WearInspection {
|
||||||
|
materials: wear.entries.len(),
|
||||||
|
lightmaps: wear.lightmaps.len(),
|
||||||
|
first_material: wear
|
||||||
|
.entries
|
||||||
|
.first()
|
||||||
|
.map(|entry| String::from_utf8_lossy(&entry.material.0).into_owned()),
|
||||||
|
last_material: wear
|
||||||
|
.entries
|
||||||
|
.last()
|
||||||
|
.map(|entry| String::from_utf8_lossy(&entry.material.0).into_owned()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads and validates a model resource through repository-backed lookup.
|
/// Loads and validates a model resource through repository-backed lookup.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|||||||
@@ -212,18 +212,15 @@ pub struct TerrainMaterialPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TerrainMaterialPair {
|
impl TerrainMaterialPair {
|
||||||
/// Returns the material-manager selection used by `GetShade`.
|
/// Returns the opaque lookup key supplied to `GetShade`.
|
||||||
///
|
///
|
||||||
/// `GetShade` owns a manager loaded only with `Shade.wea`, so its
|
/// This is deliberately separate from the `Land1.wea`/`Land2.wea`
|
||||||
/// 16-bit lookup occupies row bits of that manager's table zero. It is
|
/// selectors carried by [`TerrainMaterialLayers`]. Although `GetShade`
|
||||||
/// deliberately separate from the `Land1.wea`/`Land2.wea` selections
|
/// owns a manager loaded from `Shade.wea`, this key is not a WEAR row:
|
||||||
/// carried by [`TerrainMaterialLayers`].
|
/// observed map values exceed that table's row count.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn shade_selection(self) -> TerrainMaterialSelection {
|
pub const fn shade_lookup_key(self) -> u16 {
|
||||||
TerrainMaterialSelection {
|
self.material_lookup
|
||||||
table_index: 0,
|
|
||||||
material_index: self.material_lookup,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,11 +1583,8 @@ mod tests {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
document.slot_material_pairs(0).expect("material pairs")[0].shade_selection(),
|
document.slot_material_pairs(0).expect("material pairs")[0].shade_lookup_key(),
|
||||||
TerrainMaterialSelection {
|
0x0102
|
||||||
table_index: 0,
|
|
||||||
material_index: 0x0102,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
assert_eq!(document.slot_material_pairs(1), None);
|
assert_eq!(document.slot_material_pairs(1), None);
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ use std::collections::VecDeque;
|
|||||||
///
|
///
|
||||||
/// Applications access this semantic terrain API through this terrain crate
|
/// Applications access this semantic terrain API through this terrain crate
|
||||||
/// rather than taking a direct dependency on the binary-format parser.
|
/// rather than taking a direct dependency on the binary-format parser.
|
||||||
pub use fparkan_terrain_format::{
|
pub use fparkan_terrain_format::{TerrainMaterialLayers, TerrainMaterialSelection};
|
||||||
TerrainMaterialLayers, TerrainMaterialPair, TerrainMaterialSelection,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Terrain world.
|
/// Terrain world.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
|||||||
@@ -1586,12 +1586,13 @@ fields, while `LandMeshDocument::slot_material_pairs` decodes the selected
|
|||||||
type-11 entries as `{ material_lookup: u16, flags: u16 }`. Flag `0x0010` is a
|
type-11 entries as `{ material_lookup: u16, flags: u16 }`. Flag `0x0010` is a
|
||||||
proven batch boundary in `GetShade`. Crucially, this lookup does **not** select
|
proven batch boundary in `GetShade`. Crucially, this lookup does **not** select
|
||||||
either map-local Land table: the `GetShade` constructor loads its own one-table
|
either map-local Land table: the `GetShade` constructor loads its own one-table
|
||||||
manager from `system.rlb` resource `Shade.wea`, so the 16-bit value is that
|
manager from `system.rlb` resource `Shade.wea`. That WEAR table has one row,
|
||||||
manager's table-zero row. `TerrainMaterialPair::shade_selection` records this
|
`LIGHT1`, whereas AutoDemo's 3,174 pairs use keys `0..3173`; therefore the
|
||||||
separate selector instead of conflating it with `Land1.wea`/`Land2.wea`. The
|
16-bit value is an opaque manager lookup/cache key, **not** a table-row
|
||||||
AutoDemo inspector now reports 128 slots, 3,174 addressed shade pairs, selector
|
selector. `TerrainMaterialPair::shade_lookup_key` records this separate key
|
||||||
range `0..3173`, and 392 `0x0010` batch boundaries. The meanings of remaining
|
without conflating it with `Land1.wea`/`Land2.wea`. The AutoDemo inspector now
|
||||||
flags and the final blend operation remain unassigned.
|
reports 128 slots and 392 `0x0010` batch boundaries. The meanings of remaining
|
||||||
|
flags, this cache mapping, and the final blend operation remain unassigned.
|
||||||
|
|
||||||
The static Vulkan bridge now carries each contiguous face run as a separate
|
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
|
draw range keyed by the original packed `material_tag`; it neither reorders
|
||||||
|
|||||||
Reference in New Issue
Block a user