feat(cli): expose unit component provenance
This commit is contained in:
@@ -37,11 +37,12 @@ use fparkan_runtime::{
|
|||||||
};
|
};
|
||||||
use fparkan_vfs::{DirectoryVfs, Vfs};
|
use fparkan_vfs::{DirectoryVfs, Vfs};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::fmt::Write as _;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
const ARCHIVE_INSPECT_SCHEMA: &str = "fparkan-archive-inspect-v1";
|
const ARCHIVE_INSPECT_SCHEMA: &str = "fparkan-archive-inspect-v1";
|
||||||
const PROTOTYPE_INSPECT_SCHEMA: &str = "fparkan-prototype-inspect-v1";
|
const PROTOTYPE_INSPECT_SCHEMA: &str = "fparkan-prototype-inspect-v2";
|
||||||
const MISSION_GRAPH_SCHEMA: &str = "fparkan-mission-graph-v1";
|
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";
|
||||||
@@ -65,6 +66,8 @@ struct PrototypeInspectOutput {
|
|||||||
roots: usize,
|
roots: usize,
|
||||||
node_count: usize,
|
node_count: usize,
|
||||||
edge_count: usize,
|
edge_count: usize,
|
||||||
|
unit_component_records: Vec<UnitComponentInspectOutput>,
|
||||||
|
edges: Vec<PrototypeGraphEdgeInspectOutput>,
|
||||||
prototype_requests: usize,
|
prototype_requests: usize,
|
||||||
resolved: usize,
|
resolved: usize,
|
||||||
unit_references: usize,
|
unit_references: usize,
|
||||||
@@ -196,6 +199,33 @@ struct GraphFailureOutput {
|
|||||||
resource: Option<String>,
|
resource: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UnitComponentInspectOutput {
|
||||||
|
root_index: usize,
|
||||||
|
component_index: usize,
|
||||||
|
archive_raw_hex: String,
|
||||||
|
resource_raw_hex: String,
|
||||||
|
kind: u32,
|
||||||
|
parent_or_link: i32,
|
||||||
|
description_raw_hex: String,
|
||||||
|
tail0: u32,
|
||||||
|
tail1: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PrototypeGraphEdgeInspectOutput {
|
||||||
|
id: u32,
|
||||||
|
from: u32,
|
||||||
|
to: u32,
|
||||||
|
kind: &'static str,
|
||||||
|
requiredness: &'static str,
|
||||||
|
root_index: Option<usize>,
|
||||||
|
parent_edge: Option<u32>,
|
||||||
|
unit_component_index: Option<usize>,
|
||||||
|
archive: Option<String>,
|
||||||
|
resource_raw_hex: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let result = run(&args);
|
let result = run(&args);
|
||||||
@@ -350,6 +380,58 @@ fn prototype_inspect_json(
|
|||||||
roots: report.root_count,
|
roots: report.root_count,
|
||||||
node_count: graph.nodes.len(),
|
node_count: graph.nodes.len(),
|
||||||
edge_count: graph.edges.len(),
|
edge_count: graph.edges.len(),
|
||||||
|
unit_component_records: graph
|
||||||
|
.root_unit_components
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(root_index, records)| {
|
||||||
|
records
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(
|
||||||
|
move |(component_index, record)| UnitComponentInspectOutput {
|
||||||
|
root_index,
|
||||||
|
component_index,
|
||||||
|
archive_raw_hex: hex_bytes(&record.archive_raw),
|
||||||
|
resource_raw_hex: hex_bytes(&record.resource_raw),
|
||||||
|
kind: record.kind,
|
||||||
|
parent_or_link: record.parent_or_link,
|
||||||
|
description_raw_hex: hex_bytes(&record.description_raw),
|
||||||
|
tail0: record.tail0,
|
||||||
|
tail1: record.tail1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
edges: graph
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.map(|edge| PrototypeGraphEdgeInspectOutput {
|
||||||
|
id: edge.id.0,
|
||||||
|
from: edge.from.0,
|
||||||
|
to: edge.to.0,
|
||||||
|
kind: prototype_graph_edge_kind_label(edge.kind),
|
||||||
|
requiredness: prototype_graph_requiredness_label(edge.requiredness),
|
||||||
|
root_index: edge.provenance.as_ref().map(|value| value.root_index),
|
||||||
|
parent_edge: edge
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.parent_edge.map(|parent| parent.0)),
|
||||||
|
unit_component_index: edge
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.unit_component_index),
|
||||||
|
archive: edge
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.archive.clone()),
|
||||||
|
resource_raw_hex: edge
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.resource.as_ref())
|
||||||
|
.map(|raw| hex_bytes(raw)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
prototype_requests: graph.prototype_requests.len(),
|
prototype_requests: graph.prototype_requests.len(),
|
||||||
resolved: report.resolved_count,
|
resolved: report.resolved_count,
|
||||||
unit_references: report.unit_reference_count,
|
unit_references: report.unit_reference_count,
|
||||||
@@ -638,6 +720,29 @@ fn graph_failure_output(failure: &fparkan_prototype::PrototypeGraphFailure) -> G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hex_bytes(raw: &[u8]) -> String {
|
||||||
|
let mut output = String::with_capacity(raw.len().saturating_mul(2));
|
||||||
|
for byte in raw {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
write!(&mut output, "{byte:02x}").expect("writing into String cannot fail");
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prototype_graph_edge_kind_label(
|
||||||
|
edge: fparkan_prototype::PrototypeGraphEdgeKind,
|
||||||
|
) -> &'static str {
|
||||||
|
match edge {
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::MissionToRoot => "mission_to_root",
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::UnitDatToComponent => "unit_dat_to_component",
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::PrototypeToMesh => "prototype_to_mesh",
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::MeshToWear => "mesh_to_wear",
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::WearToMaterial => "wear_to_material",
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::MaterialToTexture => "material_to_texture",
|
||||||
|
fparkan_prototype::PrototypeGraphEdgeKind::WearToLightmap => "wear_to_lightmap",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prototype_graph_edge_label(edge: fparkan_prototype::PrototypeGraphEdge) -> &'static str {
|
fn prototype_graph_edge_label(edge: fparkan_prototype::PrototypeGraphEdge) -> &'static str {
|
||||||
match edge {
|
match edge {
|
||||||
fparkan_prototype::PrototypeGraphEdge::MissionToUnitDat => "mission_to_unit_dat",
|
fparkan_prototype::PrototypeGraphEdge::MissionToUnitDat => "mission_to_unit_dat",
|
||||||
@@ -720,7 +825,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
json,
|
json,
|
||||||
"{\"schema_version\":\"fparkan-prototype-inspect-v1\",\"key\":\"root\",\"roots\":1,\"node_count\":0,\"edge_count\":0,\"prototype_requests\":1,\"resolved\":1,\"unit_references\":0,\"unit_components\":0,\"direct_references\":1,\"wear_requests\":0,\"wear\":0,\"materials\":0,\"textures\":0,\"lightmaps\":0,\"is_success\":true,\"failures\":[]}"
|
"{\"schema_version\":\"fparkan-prototype-inspect-v2\",\"key\":\"root\",\"roots\":1,\"node_count\":0,\"edge_count\":0,\"unit_component_records\":[],\"edges\":[],\"prototype_requests\":1,\"resolved\":1,\"unit_references\":0,\"unit_components\":0,\"direct_references\":1,\"wear_requests\":0,\"wear\":0,\"materials\":0,\"textures\":0,\"lightmaps\":0,\"is_success\":true,\"failures\":[]}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -484,6 +484,21 @@ Visual dependency expansion наследует этот индекс на edges
|
|||||||
diffuse texture и lightmap. Поэтому ошибка или asset в любой из этих фаз
|
diffuse texture и lightmap. Поэтому ошибка или asset в любой из этих фаз
|
||||||
сохраняет путь до конкретной записи unit DAT, а не только до миссионного object.
|
сохраняет путь до конкретной записи unit DAT, а не только до миссионного object.
|
||||||
|
|
||||||
|
Для воспроизводимой проверки используется `fparkan-cli prototype inspect`.
|
||||||
|
Схема JSON `fparkan-prototype-inspect-v2` содержит lossless hex каждого
|
||||||
|
32-byte поля unit record и materialized graph edges с `unit_component_index`.
|
||||||
|
Например, для GOG AutoDemo:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo run -p fparkan-cli -- prototype inspect `
|
||||||
|
--root 'C:\GOG Games\Parkan - Iron Strategy' `
|
||||||
|
--key 'UNITS\UNITS\AutoDEMO\w_m_wlk2.dat' --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
Вывод фиксирует 18 component records, их MSH/WEAR/MAT0/Texm dependencies и
|
||||||
|
точные parent edges. JSON предназначен для анализа и regression evidence, а
|
||||||
|
не для интерпретации `kind`, links или opaque tails как готовой game logic.
|
||||||
|
|
||||||
## Вспомогательные форматы
|
## Вспомогательные форматы
|
||||||
|
|
||||||
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип
|
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип
|
||||||
|
|||||||
Reference in New Issue
Block a user