diff options
Diffstat (limited to 'apps/fparkan-cli/src/main.rs')
| -rw-r--r-- | apps/fparkan-cli/src/main.rs | 1070 |
1 files changed, 0 insertions, 1070 deletions
diff --git a/apps/fparkan-cli/src/main.rs b/apps/fparkan-cli/src/main.rs deleted file mode 100644 index d3e1df5..0000000 --- a/apps/fparkan-cli/src/main.rs +++ /dev/null @@ -1,1070 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr( - test, - allow( - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - clippy::cast_precision_loss, - clippy::expect_used, - clippy::float_cmp, - clippy::identity_op, - clippy::too_many_lines, - clippy::uninlined_format_args, - clippy::map_unwrap_or, - clippy::needless_raw_string_hashes, - clippy::semicolon_if_nothing_returned, - clippy::type_complexity, - clippy::panic, - clippy::unwrap_used - ) -)] -#![allow(clippy::print_stderr, clippy::print_stdout)] -//! `FParkan` command-line tools. - -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, inspect_land_msh_bounds_file, inspect_model_from_root, - inspect_wear_from_root, load_land_msh_from_path, ArchiveInspection, ModelInspection, -}; -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, Vfs}; -use serde::Serialize; -use std::fmt::Write as _; -use std::path::PathBuf; -use std::sync::Arc; - -const ARCHIVE_INSPECT_SCHEMA: &str = "fparkan-archive-inspect-v1"; -const PROTOTYPE_INSPECT_SCHEMA: &str = "fparkan-prototype-inspect-v2"; -const MISSION_GRAPH_SCHEMA: &str = "fparkan-mission-graph-v1"; -const MISSION_INSPECT_SCHEMA: &str = "fparkan-mission-inspect-v1"; -const TERRAIN_INSPECT_SCHEMA: &str = "fparkan-terrain-inspect-v1"; -const MODEL_INSPECT_SCHEMA: &str = "fparkan-model-inspect-v1"; -const WEAR_INSPECT_SCHEMA: &str = "fparkan-wear-inspect-v1"; -const SCRIPT_INSPECT_SCHEMA: &str = "fparkan-script-inspect-v2"; - -#[derive(Serialize)] -struct ArchiveInspectOutput<'a> { - schema_version: &'static str, - path: &'a str, - kind: &'a str, - entries: usize, - #[serde(skip_serializing_if = "Option::is_none")] - lookup_order_valid: Option<bool>, -} - -#[derive(Serialize)] -struct PrototypeInspectOutput { - schema_version: &'static str, - key: String, - roots: usize, - node_count: usize, - edge_count: usize, - unit_component_records: Vec<UnitComponentInspectOutput>, - edges: Vec<PrototypeGraphEdgeInspectOutput>, - prototype_requests: usize, - resolved: usize, - unit_references: usize, - unit_components: usize, - direct_references: usize, - wear_requests: usize, - wear: usize, - materials: usize, - textures: usize, - lightmaps: usize, - is_success: bool, - failures: Vec<GraphFailureOutput>, -} - -#[derive(Serialize)] -struct MissionGraphOutput { - schema_version: &'static str, - mission: String, - objects: usize, - paths: usize, - clans: usize, - extras: usize, - roots: usize, - node_count: usize, - edge_count: usize, - direct_references: usize, - unit_references: usize, - unit_components: usize, - prototype_requests: usize, - wear_requests: usize, - wear: usize, - materials: usize, - textures: usize, - lightmaps: usize, - is_success: bool, - failures: usize, -} - -#[derive(Serialize)] -struct MissionInspectOutput { - schema_version: &'static str, - mission: String, - objects: Vec<MissionObjectInspectOutput>, -} - -#[derive(Serialize)] -struct MissionObjectInspectOutput { - index: usize, - resource: String, - position: [f32; 3], - orientation_raw: [f32; 3], - scale: [f32; 3], -} - -#[derive(Serialize)] -struct TerrainInspectOutput { - schema_version: &'static str, - path: String, - positions: usize, - min: [f32; 3], - max: [f32; 3], - faces: usize, - slots: usize, - material_tags: Vec<TerrainMaterialTagCount>, - shade_pairs: usize, - #[serde(skip_serializing_if = "Option::is_none")] - shade_lookup_key_min: Option<u16>, - #[serde(skip_serializing_if = "Option::is_none")] - shade_lookup_key_max: Option<u16>, - shade_batch_boundaries: usize, -} - -#[derive(Serialize)] -struct TerrainMaterialTagCount { - tag: u16, - 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)] -struct ModelInspectOutput<'a> { - schema_version: &'static str, - archive: &'a str, - resource: &'a str, - streams: usize, - nodes: usize, - node_stride: usize, - slots: usize, - positions: usize, - indices: usize, - batches: usize, - #[serde(skip_serializing_if = "Option::is_none")] - animation_keys: Option<usize>, - #[serde(skip_serializing_if = "Option::is_none")] - animation_frame_count: Option<u32>, - node38: Vec<ModelNodeInspectOutput>, -} - -#[derive(Serialize)] -struct ModelNodeInspectOutput { - index: usize, - parent_or_link_raw: u16, - anim_map_start: u16, - fallback_key: u16, - has_lod0_group0: bool, -} - -#[derive(Serialize)] -struct GraphFailureOutput { - root_index: usize, - edge: &'static str, - requiredness: &'static str, - message: String, - #[serde(skip_serializing_if = "Option::is_none")] - archive: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - 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>, -} - -#[derive(Serialize)] -struct ScriptInspectOutput<'a> { - schema_version: &'static str, - path: &'a str, - opcode_handler_count: u32, - events: usize, - instructions: usize, - references: usize, - trailing_bytes: usize, - first_header_word_candidates: Vec<ScriptHeaderWordCount>, -} - -#[derive(Serialize)] -struct ScriptHeaderWordCount { - value: u32, - instructions: usize, -} - -#[derive(Serialize)] -struct VarSetInspectOutput<'a> { - schema_version: &'static str, - path: &'a str, - declarations: usize, - float_defaults: usize, - dword_defaults: usize, - first_name: Option<&'a str>, - last_name: Option<&'a str>, -} - -fn main() { - let args: Vec<String> = std::env::args().skip(1).collect(); - let result = run(&args); - let code = exit_code(&result); - if let Err(err) = result { - eprintln!("{err}"); - } - std::process::exit(code); -} - -fn run(args: &[String]) -> Result<(), String> { - match args { - [domain, command, rest @ ..] if domain == "corpus" && command == "discover" => { - let rest = strip_format_json(rest)?; - let root = parse_root(&rest)?; - let manifest = - discover(&root, DiscoverOptions::default()).map_err(|e| e.to_string())?; - let report = report(&root, &manifest).map_err(|e| e.to_string())?; - println!("{}", render_report_json(&report)); - Ok(()) - } - [domain, command, rest @ ..] if domain == "corpus" && command == "validate" => { - let rest = strip_format_json(rest)?; - let root = parse_root(&rest)?; - let manifest = - discover(&root, DiscoverOptions::default()).map_err(|e| e.to_string())?; - let report = report(&root, &manifest).map_err(|e| e.to_string())?; - if report.casefold_collisions > 0 { - return Err("casefold collisions found".to_string()); - } - if report.failures > 0 { - return Err(format!("corpus report found {} failures", report.failures)); - } - println!("{}", render_report_json(&report)); - Ok(()) - } - [domain, command, rest @ ..] if domain == "archive" && command == "inspect" => { - let rest = strip_format_json(rest)?; - inspect_archive(&rest) - } - [domain, command, rest @ ..] if domain == "prototype" && command == "inspect" => { - let rest = strip_format_json(rest)?; - inspect_prototype(&rest) - } - [domain, command, rest @ ..] if domain == "mission" && command == "graph" => { - 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) - } - [domain, command, rest @ ..] if domain == "terrain" && command == "inspect" => { - let rest = strip_format_json(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" => { - let rest = strip_format_json(rest)?; - inspect_model(&rest) - } - [domain, command, rest @ ..] if domain == "script" && command == "inspect" => { - let rest = strip_format_json(rest)?; - inspect_script(&rest) - } - [domain, command, rest @ ..] if domain == "varset" && command == "inspect" => { - let rest = strip_format_json(rest)?; - inspect_varset(&rest) - } - _ => Err(usage()), - } -} - -fn inspect_script(args: &[String]) -> Result<(), String> { - let path = parse_file_path(args, "script inspect")?; - let bytes = std::fs::read(&path).map_err(|err| format!("{}: {err}", path.display()))?; - let package = - fparkan_script::decode(&bytes).map_err(|err| format!("{}: {err}", path.display()))?; - let display_path = path.display().to_string(); - println!("{}", script_inspect_json(&display_path, &package)?); - Ok(()) -} - -fn script_inspect_json( - path: &str, - package: &fparkan_script::ScriptPackage, -) -> Result<String, String> { - let instructions = package - .events - .iter() - .map(|event| event.instructions.len()) - .sum(); - let references = package - .events - .iter() - .flat_map(|event| &event.instructions) - .map(|instruction| instruction.references.len()) - .sum(); - let mut candidates = std::collections::BTreeMap::<u32, usize>::new(); - for instruction in package.events.iter().flat_map(|event| &event.instructions) { - *candidates.entry(instruction.header_words[0]).or_insert(0) += 1; - } - serialize_json(&ScriptInspectOutput { - schema_version: SCRIPT_INSPECT_SCHEMA, - path, - opcode_handler_count: package.opcode_handler_count, - events: package.events.len(), - instructions, - references, - trailing_bytes: package.trailing_bytes.len(), - first_header_word_candidates: candidates - .into_iter() - .map(|(value, instructions)| ScriptHeaderWordCount { - value, - instructions, - }) - .collect(), - }) -} - -fn inspect_varset(args: &[String]) -> Result<(), String> { - let path = parse_file_path(args, "varset inspect")?; - let bytes = std::fs::read(&path).map_err(|err| format!("{}: {err}", path.display()))?; - let varset = - fparkan_script::parse_varset(&bytes).map_err(|err| format!("{}: {err}", path.display()))?; - let display_path = path.display().to_string(); - println!("{}", varset_inspect_json(&display_path, &varset)?); - Ok(()) -} - -fn varset_inspect_json(path: &str, varset: &fparkan_script::VarSet) -> Result<String, String> { - let float_defaults = varset - .declarations - .iter() - .filter(|declaration| declaration.type_name == fparkan_script::VarSetType::Float) - .count(); - let dword_defaults = varset.declarations.len() - float_defaults; - serialize_json(&VarSetInspectOutput { - schema_version: "fparkan-varset-inspect-v1", - path, - declarations: varset.declarations.len(), - float_defaults, - dword_defaults, - first_name: varset - .declarations - .first() - .map(|declaration| declaration.name.as_str()), - last_name: varset - .declarations - .last() - .map(|declaration| declaration.name.as_str()), - }) -} - -fn exit_code(result: &Result<(), String>) -> i32 { - if result.is_ok() { - 0 - } else { - 2 - } -} - -fn strip_format_json(args: &[String]) -> Result<Vec<String>, String> { - let mut stripped = Vec::with_capacity(args.len()); - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - if arg == "--format" { - let value = iter - .next() - .ok_or_else(|| "--format requires a value".to_string())?; - if value != "json" { - return Err(format!("unsupported output format: {value}")); - } - continue; - } - stripped.push(arg.clone()); - } - Ok(stripped) -} - -fn parse_root(args: &[String]) -> Result<PathBuf, String> { - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - if arg == "--root" { - return iter - .next() - .map(PathBuf::from) - .ok_or_else(|| "--root requires a path".to_string()); - } - } - Err("missing --root".to_string()) -} - -fn parse_root_alias(args: &[String]) -> Result<PathBuf, String> { - parse_option(args, &["--root", "--game-root"]) - .map(PathBuf::from) - .ok_or_else(|| "missing --root".to_string()) -} - -fn parse_required(args: &[String], names: &[&str], label: &str) -> Result<String, String> { - parse_option(args, names).ok_or_else(|| format!("missing {label}")) -} - -fn parse_option(args: &[String], names: &[&str]) -> Option<String> { - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - if names.iter().any(|name| arg == name) { - return iter.next().cloned(); - } - } - None -} - -fn inspect_prototype(args: &[String]) -> Result<(), String> { - let root = parse_root_alias(args)?; - let key = parse_required(args, &["--key"], "--key")?; - let vfs = Arc::new(DirectoryVfs::new(root)); - let repository = CachedResourceRepository::new(vfs.clone()); - let roots = [resource_name(key.as_bytes())]; - let (mut graph, resolved, mut report) = - build_prototype_graph_report(&repository, vfs.as_ref(), &roots); - extend_graph_report_with_visual_dependencies(&repository, &mut report, &mut graph, &resolved); - println!( - "{}", - prototype_inspect_json(&key, &graph, &report).map_err(|err| err.to_string())? - ); - Ok(()) -} - -fn prototype_inspect_json( - key: &str, - graph: &fparkan_prototype::PrototypeGraph, - report: &fparkan_prototype::PrototypeGraphReport, -) -> Result<String, serde_json::Error> { - serde_json::to_string(&PrototypeInspectOutput { - schema_version: PROTOTYPE_INSPECT_SCHEMA, - key: key.to_string(), - roots: report.root_count, - node_count: graph.nodes.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(), - resolved: report.resolved_count, - unit_references: report.unit_reference_count, - unit_components: report.unit_component_count, - direct_references: report.direct_reference_count, - wear_requests: report.wear_request_count, - wear: report.wear_resolved_count, - materials: report.material_resolved_count, - textures: report.texture_resolved_count, - lightmaps: report.lightmap_resolved_count, - is_success: report.is_success(), - failures: report - .failures - .iter() - .take(16) - .map(graph_failure_output) - .collect(), - }) -} - -fn graph_mission(args: &[String]) -> Result<(), String> { - let root = parse_root_alias(args)?; - let mission = parse_required(args, &["--mission"], "--mission")?; - let services = EngineServices::new(Arc::new(DirectoryVfs::new(root))); - let mut engine = create( - EngineConfig { - mode: EngineMode::Headless, - }, - services, - ) - .map_err(|err| err.to_string())?; - let loaded = load_mission( - &mut engine, - MissionRequest { - key: mission.clone(), - }, - ) - .map_err(|err| err.to_string())?; - println!( - "{}", - serialize_json(&MissionGraphOutput { - schema_version: MISSION_GRAPH_SCHEMA, - mission: mission.clone(), - objects: loaded.object_count, - paths: loaded.path_count, - clans: loaded.clan_count, - extras: loaded.extra_count, - roots: loaded.graph_root_count, - node_count: loaded.graph_node_count, - edge_count: loaded.graph_edge_count, - direct_references: loaded.graph_direct_reference_count, - unit_references: loaded.graph_unit_reference_count, - unit_components: loaded.graph_unit_component_count, - prototype_requests: loaded.graph_resolved_count, - wear_requests: loaded.graph_wear_request_count, - wear: loaded.graph_wear_resolved_count, - materials: loaded.graph_material_resolved_count, - textures: loaded.graph_texture_resolved_count, - lightmaps: loaded.graph_lightmap_resolved_count, - is_success: loaded.graph_failure_count == 0, - failures: loaded.graph_failure_count, - })? - ); - 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())?; - - match inspection { - ArchiveInspection::Nres { - entries, - lookup_order_valid, - .. - } => { - println!( - "{}", - archive_inspect_json( - &path.display().to_string(), - "NRes", - entries, - Some(lookup_order_valid), - )? - ); - Ok(()) - } - ArchiveInspection::Rsli { entries } => { - println!( - "{}", - archive_inspect_json(&path.display().to_string(), "RsLi", entries, None)? - ); - Ok(()) - } - ArchiveInspection::Unsupported => { - Err(format!("{}: unsupported archive magic", path.display())) - } - } -} - -fn inspect_terrain(args: &[String]) -> Result<(), String> { - let path = parse_file_path(args, "terrain inspect")?; - let bounds = inspect_land_msh_bounds_file(&path)?; - let terrain = load_land_msh_from_path(&path)?; - let mut material_tags = terrain - .faces - .iter() - .fold(std::collections::BTreeMap::new(), |mut counts, face| { - *counts.entry(face.material_tag).or_insert(0usize) += 1; - counts - }) - .into_iter() - .map(|(tag, faces)| TerrainMaterialTagCount { tag, faces }) - .collect::<Vec<_>>(); - material_tags.sort_unstable_by_key(|entry| entry.tag); - let shade_pairs = (0..terrain.slots.slots_raw.len()) - .filter_map(|slot_index| terrain.slot_material_pairs(slot_index)) - .flatten() - .collect::<Vec<_>>(); - let shade_lookup_key_min = shade_pairs.iter().map(|pair| pair.shade_lookup_key()).min(); - let shade_lookup_key_max = shade_pairs.iter().map(|pair| pair.shade_lookup_key()).max(); - let shade_batch_boundaries = shade_pairs - .iter() - .filter(|pair| pair.flags & 0x0010 != 0) - .count(); - println!( - "{}", - serialize_json(&TerrainInspectOutput { - schema_version: TERRAIN_INSPECT_SCHEMA, - path: path.display().to_string(), - positions: bounds.positions, - min: bounds.min, - max: bounds.max, - faces: terrain.faces.len(), - slots: terrain.slots.slots_raw.len(), - material_tags, - shade_pairs: shade_pairs.len(), - shade_lookup_key_min, - shade_lookup_key_max, - shade_batch_boundaries, - })? - ); - 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> { - let root = parse_root_alias(args)?; - let archive = parse_required(args, &["--archive"], "--archive")?; - let resource = parse_required(args, &["--resource"], "--resource")?; - let inspection = inspect_model_from_root(&root, &archive, &resource)?; - println!("{}", model_inspect_json(&archive, &resource, &inspection)?); - Ok(()) -} - -fn model_inspect_json( - archive: &str, - resource: &str, - inspection: &ModelInspection, -) -> Result<String, String> { - serialize_json(&ModelInspectOutput { - schema_version: MODEL_INSPECT_SCHEMA, - archive, - resource, - streams: inspection.streams, - nodes: inspection.nodes, - node_stride: inspection.node_stride, - slots: inspection.slots, - positions: inspection.positions, - indices: inspection.indices, - batches: inspection.batches, - animation_keys: inspection.animation_keys, - animation_frame_count: inspection.animation_frame_count, - node38: inspection - .node38 - .iter() - .map(|node| ModelNodeInspectOutput { - index: node.index, - parent_or_link_raw: node.parent_or_link_raw, - anim_map_start: node.anim_map_start, - fallback_key: node.fallback_key, - has_lod0_group0: node.has_lod0_group0, - }) - .collect(), - }) -} - -fn archive_inspect_json( - path: &str, - kind: &str, - entries: usize, - lookup_order_valid: Option<bool>, -) -> Result<String, String> { - serialize_json(&ArchiveInspectOutput { - schema_version: ARCHIVE_INSPECT_SCHEMA, - path, - kind, - entries, - lookup_order_valid, - }) -} - -fn parse_archive_path(args: &[String]) -> Result<PathBuf, String> { - parse_file_path(args, "archive inspect") -} - -fn parse_file_path(args: &[String], command: &str) -> Result<PathBuf, String> { - match args { - [path] => Ok(PathBuf::from(path)), - [flag, path] if flag == "--file" => Ok(PathBuf::from(path)), - _ => Err(format!("{command} requires <file> or --file <file>")), - } -} - -fn serialize_json<T: Serialize>(value: &T) -> Result<String, String> { - serde_json::to_string(value).map_err(|err| err.to_string()) -} - -fn graph_failure_output(failure: &fparkan_prototype::PrototypeGraphFailure) -> GraphFailureOutput { - GraphFailureOutput { - root_index: failure.root_index, - edge: prototype_graph_edge_label(failure.edge), - requiredness: prototype_graph_requiredness_label(failure.requiredness), - message: failure.message.clone(), - archive: failure - .provenance - .as_ref() - .and_then(|provenance| provenance.archive.clone()), - resource: failure.provenance.as_ref().and_then(|provenance| { - provenance - .resource - .as_ref() - .map(|raw| String::from_utf8_lossy(raw).into_owned()) - }), - } -} - -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 { - match edge { - fparkan_prototype::PrototypeGraphEdge::MissionToUnitDat => "mission_to_unit_dat", - fparkan_prototype::PrototypeGraphEdge::MissionToObjectsRegistry => { - "mission_to_objects_registry" - } - fparkan_prototype::PrototypeGraphEdge::UnitDatToComponent => "unit_dat_to_component", - fparkan_prototype::PrototypeGraphEdge::PrototypeToMesh => "prototype_to_mesh", - fparkan_prototype::PrototypeGraphEdge::MeshToWear => "mesh_to_wear", - fparkan_prototype::PrototypeGraphEdge::WearToMaterial => "wear_to_material", - fparkan_prototype::PrototypeGraphEdge::MaterialToTexture => "material_to_texture", - fparkan_prototype::PrototypeGraphEdge::WearToLightmap => "wear_to_lightmap", - } -} - -fn prototype_graph_requiredness_label( - requiredness: fparkan_prototype::PrototypeGraphRequiredness, -) -> &'static str { - match requiredness { - fparkan_prototype::PrototypeGraphRequiredness::Required => "required", - fparkan_prototype::PrototypeGraphRequiredness::Optional => "optional", - fparkan_prototype::PrototypeGraphRequiredness::Fallback => "fallback", - } -} - -fn usage() -> 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] | script inspect <file> [--format json] | varset inspect <file> [--format json] | prototype inspect --root <path> --key <key> [--format json] | mission graph|inspect --root <path> --mission <path> [--format json]".to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn strings(values: &[&str]) -> Vec<String> { - values.iter().map(|value| (*value).to_string()).collect() - } - - #[test] - fn stable_exit_codes_are_mapped() { - assert_eq!(exit_code(&Ok(())), 0); - assert_eq!(exit_code(&Err("failure".to_string())), 2); - } - - #[test] - fn accepts_json_format_option() { - assert_eq!( - strip_format_json(&strings(&["--root", "testdata", "--format", "json"])), - Ok(strings(&["--root", "testdata"])) - ); - assert_eq!( - strip_format_json(&strings(&["--format", "text"])), - Err("unsupported output format: text".to_string()) - ); - } - - #[test] - fn script_inspect_json_has_canonical_field_order() { - let package = - fparkan_script::decode(&[73, 0, 0, 0, 0, 0, 0, 0]).expect("minimal script package"); - assert_eq!( - script_inspect_json("script.scr", &package), - Ok("{\"schema_version\":\"fparkan-script-inspect-v2\",\"path\":\"script.scr\",\"opcode_handler_count\":73,\"events\":0,\"instructions\":0,\"references\":0,\"trailing_bytes\":0,\"first_header_word_candidates\":[]}".to_string()) - ); - } - - #[test] - fn varset_inspect_json_has_canonical_field_order() { - let varset = - fparkan_script::parse_varset(b"VAR( float, f0, 0)\nVAR( DWORD, d0, 0xffffffff)\n") - .expect("minimal varset"); - assert_eq!( - varset_inspect_json("varset.var", &varset), - Ok("{\"schema_version\":\"fparkan-varset-inspect-v1\",\"path\":\"varset.var\",\"declarations\":2,\"float_defaults\":1,\"dword_defaults\":1,\"first_name\":\"f0\",\"last_name\":\"d0\"}".to_string()) - ); - } - - #[test] - fn archive_json_has_schema_version() { - let json = archive_inspect_json("archive.lib", "NRes", 3, Some(true)) - .expect("serialize archive inspection"); - - assert!(json.contains("\"schema_version\":\"fparkan-archive-inspect-v1\"")); - assert!(json.contains("\"kind\":\"NRes\"")); - assert!(json.contains("\"lookup_order_valid\":true")); - } - - #[test] - fn prototype_graph_json_has_canonical_field_order() { - let mut graph = fparkan_prototype::PrototypeGraph::default(); - graph - .prototype_requests - .push(fparkan_prototype::PrototypeKey(resource_name(b"root"))); - let report = fparkan_prototype::PrototypeGraphReport { - root_count: 1, - direct_reference_count: 1, - resolved_count: 1, - ..fparkan_prototype::PrototypeGraphReport::default() - }; - - let json = prototype_inspect_json("root", &graph, &report).expect("serialize"); - - assert_eq!( - json, - "{\"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\":[]}" - ); - } - - #[test] - fn mission_graph_json_has_canonical_field_order() { - let json = serialize_json(&MissionGraphOutput { - schema_version: MISSION_GRAPH_SCHEMA, - mission: "MISSIONS/Autodemo.00/data.tma".to_string(), - 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, - }) - .expect("serialize"); - - assert_eq!( - json, - "{\"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]}]}" - ); - } - - #[test] - fn terrain_inspect_output_retains_axis_bounds() { - let json = serialize_json(&TerrainInspectOutput { - schema_version: TERRAIN_INSPECT_SCHEMA, - path: "DATA/MAPS/AutoMAP/Land.msh".to_string(), - positions: 3, - min: [-1.0, -2.0, -3.0], - max: [4.0, 5.0, 6.0], - faces: 2, - slots: 4, - material_tags: vec![ - TerrainMaterialTagCount { tag: 0, faces: 1 }, - TerrainMaterialTagCount { tag: 3, faces: 1 }, - ], - shade_pairs: 2, - shade_lookup_key_min: Some(1), - shade_lookup_key_max: Some(2), - shade_batch_boundaries: 1, - }) - .expect("serialize terrain inspection"); - - assert_eq!( - 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_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\"}" - ); - } -} |
