From ea02915695740a6073771679f35590d84106254a Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 10:41:39 +0400 Subject: [PATCH] feat(cli): inspect mission transforms --- Cargo.lock | 1 + apps/fparkan-cli/Cargo.toml | 1 + apps/fparkan-cli/src/main.rs | 83 ++++++++++++++++++++++++++++++++++-- docs/tomes/05-render.md | 12 ++++++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bccb4a2..fe58adf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,7 @@ dependencies = [ "fparkan-assets", "fparkan-corpus", "fparkan-inspection", + "fparkan-path", "fparkan-prototype", "fparkan-resource", "fparkan-runtime", diff --git a/apps/fparkan-cli/Cargo.toml b/apps/fparkan-cli/Cargo.toml index 676cc29..10689b4 100644 --- a/apps/fparkan-cli/Cargo.toml +++ b/apps/fparkan-cli/Cargo.toml @@ -10,6 +10,7 @@ fparkan-assets = { path = "../../crates/fparkan-assets", version = "0.1.0" } fparkan-corpus = { path = "../../crates/fparkan-corpus", version = "0.1.0" } fparkan-prototype = { path = "../../crates/fparkan-prototype", version = "0.1.0" } fparkan-inspection = { path = "../../crates/fparkan-inspection", version = "0.1.0" } +fparkan-path = { path = "../../crates/fparkan-path", version = "0.1.0" } fparkan-resource = { path = "../../crates/fparkan-resource", version = "0.1.0" } fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" } fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" } diff --git a/apps/fparkan-cli/src/main.rs b/apps/fparkan-cli/src/main.rs index f2f35e8..6d5fb3c 100644 --- a/apps/fparkan-cli/src/main.rs +++ b/apps/fparkan-cli/src/main.rs @@ -21,16 +21,19 @@ #![allow(clippy::print_stderr, clippy::print_stdout)] //! `FParkan` command-line tools. -use fparkan_assets::extend_graph_report_with_visual_dependencies; +use fparkan_assets::{ + decode_mission_payload, extend_graph_report_with_visual_dependencies, TmaProfile, +}; use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions}; use fparkan_inspection::inspect_archive_file; use fparkan_inspection::ArchiveInspection; +use fparkan_path::{normalize_relative, PathPolicy}; use fparkan_prototype::build_prototype_graph_report; use fparkan_resource::{resource_name, CachedResourceRepository}; use fparkan_runtime::{ create, load_mission, EngineConfig, EngineMode, EngineServices, MissionRequest, }; -use fparkan_vfs::DirectoryVfs; +use fparkan_vfs::{DirectoryVfs, Vfs}; use serde::Serialize; use std::path::PathBuf; use std::sync::Arc; @@ -38,6 +41,7 @@ use std::sync::Arc; const ARCHIVE_INSPECT_SCHEMA: &str = "fparkan-archive-inspect-v1"; const PROTOTYPE_INSPECT_SCHEMA: &str = "fparkan-prototype-inspect-v1"; const MISSION_GRAPH_SCHEMA: &str = "fparkan-mission-graph-v1"; +const MISSION_INSPECT_SCHEMA: &str = "fparkan-mission-inspect-v1"; #[derive(Serialize)] struct ArchiveInspectOutput<'a> { @@ -94,6 +98,22 @@ struct MissionGraphOutput { failures: usize, } +#[derive(Serialize)] +struct MissionInspectOutput { + schema_version: &'static str, + mission: String, + objects: Vec, +} + +#[derive(Serialize)] +struct MissionObjectInspectOutput { + index: usize, + resource: String, + position: [f32; 3], + orientation_raw: [f32; 3], + scale: [f32; 3], +} + #[derive(Serialize)] struct GraphFailureOutput { root_index: usize, @@ -154,6 +174,10 @@ fn run(args: &[String]) -> Result<(), String> { let rest = strip_format_json(rest)?; graph_mission(&rest) } + [domain, command, rest @ ..] if domain == "mission" && command == "inspect" => { + let rest = strip_format_json(rest)?; + inspect_mission(&rest) + } _ => Err(usage()), } } @@ -310,6 +334,38 @@ fn graph_mission(args: &[String]) -> Result<(), String> { Ok(()) } +fn inspect_mission(args: &[String]) -> Result<(), String> { + let root = parse_root_alias(args)?; + let mission = parse_required(args, &["--mission"], "--mission")?; + let mission_path = normalize_relative(mission.as_bytes(), PathPolicy::StrictLegacy) + .map_err(|err| err.to_string())?; + let vfs = DirectoryVfs::new(root); + let bytes = vfs.read(&mission_path).map_err(|err| err.to_string())?; + let document = + decode_mission_payload(bytes, TmaProfile::Strict).map_err(|err| err.to_string())?; + let objects = document + .objects + .iter() + .enumerate() + .map(|(index, object)| MissionObjectInspectOutput { + index, + resource: String::from_utf8_lossy(&object.resource_name.raw).into_owned(), + position: object.position, + orientation_raw: object.orientation, + scale: object.scale, + }) + .collect(); + println!( + "{}", + serialize_json(&MissionInspectOutput { + schema_version: MISSION_INSPECT_SCHEMA, + mission, + objects, + })? + ); + Ok(()) +} + fn inspect_archive(args: &[String]) -> Result<(), String> { let path = parse_archive_path(args)?; let inspection = inspect_archive_file(&path, 0).map_err(|err| err.clone())?; @@ -416,7 +472,7 @@ fn prototype_graph_requiredness_label( } fn usage() -> String { - "usage: fparkan corpus discover|validate --root [--format json] | archive inspect [--format json] | prototype inspect --root --key [--format json] | mission graph --root --mission [--format json]".to_string() + "usage: fparkan corpus discover|validate --root [--format json] | archive inspect [--format json] | prototype inspect --root --key [--format json] | mission graph|inspect --root --mission [--format json]".to_string() } #[cfg(test)] @@ -507,4 +563,25 @@ mod tests { "{\"schema_version\":\"fparkan-mission-graph-v1\",\"mission\":\"MISSIONS/Autodemo.00/data.tma\",\"objects\":2,\"paths\":3,\"clans\":4,\"extras\":5,\"roots\":6,\"node_count\":7,\"edge_count\":8,\"direct_references\":9,\"unit_references\":10,\"unit_components\":11,\"prototype_requests\":12,\"wear_requests\":13,\"wear\":14,\"materials\":15,\"textures\":16,\"lightmaps\":17,\"is_success\":true,\"failures\":0}" ); } + + #[test] + fn mission_inspect_output_retains_raw_transform_fields() { + let json = serialize_json(&MissionInspectOutput { + schema_version: MISSION_INSPECT_SCHEMA, + mission: "MISSIONS/test/data.tma".to_string(), + objects: vec![MissionObjectInspectOutput { + index: 1, + resource: "unit.dat".to_string(), + position: [1.0, 2.0, 3.0], + orientation_raw: [4.0, 5.0, 6.0], + scale: [7.0, 8.0, 9.0], + }], + }) + .expect("serialize mission inspection"); + + assert_eq!( + json, + "{\"schema_version\":\"fparkan-mission-inspect-v1\",\"mission\":\"MISSIONS/test/data.tma\",\"objects\":[{\"index\":1,\"resource\":\"unit.dat\",\"position\":[1.0,2.0,3.0],\"orientation_raw\":[4.0,5.0,6.0],\"scale\":[7.0,8.0,9.0]}]}" + ); + } } diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 7ce73a6..231f15f 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1017,6 +1017,18 @@ warnings/errors `0/0`. The readback FNV-1a remained does **not** prove the second root is visible or equivalent: it is recorded as a camera/frustum/overlap investigation target, not hidden as a parity result. +The reproducible `fparkan-cli mission inspect` probe closes two tempting but +incorrect explanations for that equality. GOG `Autodemo.00` root 0 is +`w_m_wlk2.dat` at `(418.10318, 717.433, 3.0409389)` while root 1 is +`w_s_wlk1.dat` at `(479.12396, 795.95337, 1.6228507)`; therefore the TMA data +is neither duplicate nor co-located. Separately, the Vulkan static submit loop +calls `cmd_draw_indexed` for every prepared draw range and its depth comparison +is `LESS_OR_EQUAL`. The unresolved equality is consequently evidence about the +diagnostic camera/projection or visibility framing, not grounds to patch a +missing draw or a `LESS` depth bug. The inspector intentionally reports raw +orientation as well as position and scale so later camera/transform work can +be checked against the original mission bytes. + ### Camera ownership boundary from the GOG renderer The GOG `World3D.dll` export `LoadCamera` at RVA `0x1FB06` is only an import