diff options
| author | Valentin Popov <valentin@popov.link> | 2026-07-03 16:11:49 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-07-03 16:11:49 +0300 |
| commit | b2e193f23461cfa0e687ed96001edf034d64348f (patch) | |
| tree | c59139f7484c0bc1d8ae2ce6295d2d8ad199c3bf | |
| parent | 66804e0bd2821e12ba0ed41de1133cd2b48febfa (diff) | |
| download | fparkan-b2e193f23461cfa0e687ed96001edf034d64348f.tar.xz fparkan-b2e193f23461cfa0e687ed96001edf034d64348f.zip | |
feat(stage2): persist prepared mission assets and reports
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 1 | ||||
| -rw-r--r-- | apps/fparkan-cli/src/main.rs | 206 | ||||
| -rw-r--r-- | crates/fparkan-assets/src/lib.rs | 351 | ||||
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 75 |
4 files changed, 509 insertions, 124 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index e92a7a0..528f55d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -17,6 +17,7 @@ use super::{ use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION; use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest}; +#[cfg(test)] fn take_runtime_owners_in_dependency_order<Instance, Validation, Surface, Device, Swapchain>( instance: &mut Option<Instance>, validation: &mut Option<Validation>, diff --git a/apps/fparkan-cli/src/main.rs b/apps/fparkan-cli/src/main.rs index b1d3f36..e96f787 100644 --- a/apps/fparkan-cli/src/main.rs +++ b/apps/fparkan-cli/src/main.rs @@ -32,11 +32,12 @@ use fparkan_runtime::{ }; use fparkan_vfs::DirectoryVfs; use serde::Serialize; -use std::fmt::Write; 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-v1"; +const MISSION_GRAPH_SCHEMA: &str = "fparkan-mission-graph-v1"; #[derive(Serialize)] struct ArchiveInspectOutput<'a> { @@ -48,6 +49,63 @@ struct ArchiveInspectOutput<'a> { lookup_order_valid: Option<bool>, } +#[derive(Serialize)] +struct PrototypeInspectOutput { + schema_version: &'static str, + key: String, + roots: usize, + node_count: usize, + edge_count: usize, + 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 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>, +} + fn main() { let args: Vec<String> = std::env::args().skip(1).collect(); let result = run(&args); @@ -168,7 +226,10 @@ fn inspect_prototype(args: &[String]) -> Result<(), String> { 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)); + println!( + "{}", + prototype_inspect_json(&key, &graph, &report).map_err(|err| err.to_string())? + ); Ok(()) } @@ -176,22 +237,26 @@ fn prototype_inspect_json( key: &str, graph: &fparkan_prototype::PrototypeGraph, report: &fparkan_prototype::PrototypeGraphReport, -) -> String { - format!( - "{{\"schema_version\":\"fparkan-prototype-inspect-v1\",\"key\":{},\"roots\":{},\"prototype_requests\":{},\"resolved\":{},\"unit_references\":{},\"unit_components\":{},\"direct_references\":{},\"wear\":{},\"materials\":{},\"textures\":{},\"lightmaps\":{},\"failures\":{}}}", - json_string(key), - report.root_count, - graph.prototype_requests.len(), - report.resolved_count, - report.unit_reference_count, - report.unit_component_count, - report.direct_reference_count, - report.wear_resolved_count, - report.material_resolved_count, - report.texture_resolved_count, - report.lightmap_resolved_count, - report.failures.len() - ) +) -> 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(), + 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> { @@ -213,22 +278,29 @@ fn graph_mission(args: &[String]) -> Result<(), String> { ) .map_err(|err| err.to_string())?; println!( - "{{\"schema_version\":\"fparkan-mission-graph-v1\",\"mission\":{},\"objects\":{},\"paths\":{},\"clans\":{},\"extras\":{},\"roots\":{},\"direct_references\":{},\"unit_references\":{},\"unit_components\":{},\"prototype_requests\":{},\"wear\":{},\"materials\":{},\"textures\":{},\"lightmaps\":{},\"failures\":{}}}", - json_string(&mission), - loaded.object_count, - loaded.path_count, - loaded.clan_count, - loaded.extra_count, - loaded.graph_root_count, - loaded.graph_direct_reference_count, - loaded.graph_unit_reference_count, - loaded.graph_unit_component_count, - loaded.graph_resolved_count, - loaded.graph_wear_resolved_count, - loaded.graph_material_resolved_count, - loaded.graph_texture_resolved_count, - loaded.graph_lightmap_resolved_count, - loaded.graph_failure_count + "{}", + 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(()) } @@ -290,28 +362,54 @@ fn parse_archive_path(args: &[String]) -> Result<PathBuf, String> { } } -fn json_string(value: &str) -> String { - let mut out = String::with_capacity(value.len() + 2); - out.push('"'); - for ch in value.chars() { - match ch { - '"' => out.push_str("\\\""), - '\\' => out.push_str("\\\\"), - '\n' => out.push_str("\\n"), - '\r' => out.push_str("\\r"), - '\t' => out.push_str("\\t"), - c if c.is_control() => { - let _ = write!(out, "\\u{:04x}", c as u32); - } - c => out.push(c), +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 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", } - out.push('"'); - out } -fn serialize_json<T: Serialize>(value: &T) -> Result<String, String> { - serde_json::to_string(value).map_err(|err| err.to_string()) +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 { @@ -367,11 +465,11 @@ mod tests { ..fparkan_prototype::PrototypeGraphReport::default() }; - let json = prototype_inspect_json("root", &graph, &report); + let json = prototype_inspect_json("root", &graph, &report).expect("serialize"); assert_eq!( json, - "{\"schema_version\":\"fparkan-prototype-inspect-v1\",\"key\":\"root\",\"roots\":1,\"prototype_requests\":1,\"resolved\":1,\"unit_references\":0,\"unit_components\":0,\"direct_references\":1,\"wear\":0,\"materials\":0,\"textures\":0,\"lightmaps\":0,\"failures\":0}" + "{\"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\":[]}" ); } } diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index ddf5e65..460b3c6 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -20,10 +20,12 @@ )] //! Asset manager ports and transactional preparation models. -use fparkan_material::{decode_wear, resolve_material, MaterialError, MAT0_KIND, WEAR_KIND}; +use fparkan_material::{ + decode_wear, resolve_material, Mat0Document, MaterialError, WearTable, MAT0_KIND, WEAR_KIND, +}; use fparkan_mission_format::{decode_tma, decode_tma_land_path}; pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile}; -use fparkan_msh::{decode_msh, validate_msh, MshError}; +use fparkan_msh::{decode_msh, validate_msh, ModelAsset, MshError}; use fparkan_nres::{decode as decode_nres, ReadProfile}; pub use fparkan_nres::{NresDocument, NresError}; use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy, ResourceName}; @@ -36,7 +38,7 @@ use fparkan_resource::{ResourceError, ResourceKey, ResourceRepository}; pub use fparkan_terrain::{TerrainError, TerrainWorld}; use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh}; pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError}; -use fparkan_texm::{decode_texm, TexmError}; +use fparkan_texm::{decode_texm, TexmDocument, TexmError}; use std::collections::{HashMap, HashSet}; use std::fmt; use std::hash::{Hash, Hasher}; @@ -217,6 +219,10 @@ pub struct PreparedVisual { pub id: AssetId<PreparedVisual>, /// Optional mesh resource backing the visual. pub mesh: Option<ResourceKey>, + /// Prepared model backing the visual, when geometry is present. + pub model_id: Option<AssetId<PreparedModel>>, + /// Prepared WEAR table backing the visual, when geometry is present. + pub wear_id: Option<AssetId<PreparedWear>>, /// Number of validated model nodes. pub model_nodes: usize, /// Number of validated material slots on the model. @@ -227,19 +233,77 @@ pub struct PreparedVisual { pub material_count: usize, /// Typed material IDs available from the resolved visual. pub material_ids: Vec<AssetId<PreparedMaterial>>, + /// Typed texture IDs available from the resolved visual. + pub texture_ids: Vec<AssetId<PreparedTexture>>, + /// Typed lightmap IDs available from the resolved visual. + pub lightmap_ids: Vec<AssetId<PreparedTexture>>, /// Number of texture phase requests decoded as TEXM. pub texture_count: usize, /// Number of lightmap requests decoded as TEXM. pub lightmap_count: usize, } +/// CPU-side validated model ready for a renderer upload path. +#[derive(Clone, Debug, PartialEq)] +pub struct PreparedModel { + /// Stable id derived from the visual source. + pub id: AssetId<PreparedModel>, + /// Source mesh resource. + pub source: ResourceKey, + /// Fully validated model payload. + pub validated: ModelAsset, + /// Mesh dependencies that led to this prepared model. + pub mesh_dependencies: Vec<ResourceKey>, +} + +/// CPU-side WEAR table resolved for a visual. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PreparedWear { + /// Stable id derived from the visual source. + pub id: AssetId<PreparedWear>, + /// Source WEAR resource. + pub source: ResourceKey, + /// Decoded WEAR table. + pub table: WearTable, +} + /// CPU-side data needed before a material can be handed to a renderer. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PreparedMaterial { /// Stable id derived from the visual and material selector. pub id: AssetId<PreparedMaterial>, - /// Parsed material key. + /// Source MAT0 resource. + pub source: ResourceKey, + /// Parsed material key retained for compatibility with older callers. pub name: ResourceName, + /// Decoded MAT0 payload. + pub mat0: Mat0Document, + /// Texture requests declared by MAT0 phases. + pub texture_requests: Vec<ResourceName>, + /// Lightmap requests associated with the owning WEAR table. + pub lightmap_requests: Vec<ResourceName>, +} + +/// Texture usage role inside a prepared visual. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PreparedTextureUsage { + /// Standard diffuse/albedo texture. + Diffuse, + /// Lightmap texture. + Lightmap, +} + +/// CPU-side TEXM texture ready for a renderer upload path. +#[derive(Clone, Debug)] +pub struct PreparedTexture { + /// Stable id derived from the texture source and usage. + pub id: AssetId<PreparedTexture>, + /// Source TEXM resource. + pub source: ResourceKey, + /// Decoded TEXM payload. + pub texm: TexmDocument, + /// Usage role in the prepared visual. + pub usage: PreparedTextureUsage, } impl PreparedVisual { @@ -251,8 +315,16 @@ impl PreparedVisual { } /// Immutable prepared mission assets for rendering and game setup. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct MissionAssets { + /// Mesh-backed models prepared for reachable visuals. + pub models: Vec<PreparedModel>, + /// WEAR tables prepared for reachable visuals. + pub wears: Vec<PreparedWear>, + /// MAT0 materials prepared for reachable visuals. + pub materials: Vec<PreparedMaterial>, + /// TEXM textures prepared for reachable visuals. + pub textures: Vec<PreparedTexture>, /// Visuals prepared for all reachable prototype requests. pub visuals: Vec<PreparedVisual>, /// Visual ids available for each mission object index. @@ -286,32 +358,41 @@ impl MissionAssets { self.visuals.iter().find(|visual| visual.id == id) } + /// Finds a prepared model by id. + #[must_use] + pub fn model_by_id(&self, id: AssetId<PreparedModel>) -> Option<&PreparedModel> { + self.models.iter().find(|model| model.id == id) + } + + /// Finds a prepared material by id. + #[must_use] + pub fn material_by_id(&self, id: AssetId<PreparedMaterial>) -> Option<&PreparedMaterial> { + self.materials.iter().find(|material| material.id == id) + } + + /// Finds a prepared texture by id. + #[must_use] + pub fn texture_by_id(&self, id: AssetId<PreparedTexture>) -> Option<&PreparedTexture> { + self.textures.iter().find(|texture| texture.id == id) + } + /// Converts mission assets into a coarse mission plan. #[must_use] pub fn to_plan(&self) -> MissionAssetPlan { - let visual_count = self.visuals.len(); - let model_count = self - .visuals - .iter() - .filter(|visual| visual.mesh.is_some()) - .count(); - let material_count = self - .visuals - .iter() - .map(|visual| visual.material_count) - .sum(); - let texture_count = self.visuals.iter().map(|visual| visual.texture_count).sum(); - let lightmap_count = self - .visuals - .iter() - .map(|visual| visual.lightmap_count) - .sum(); MissionAssetPlan { - visual_count, - model_count, - material_count, - texture_count, - lightmap_count, + visual_count: self.visuals.len(), + model_count: self.models.len(), + material_count: self.materials.len(), + texture_count: self + .textures + .iter() + .filter(|texture| texture.usage == PreparedTextureUsage::Diffuse) + .count(), + lightmap_count: self + .textures + .iter() + .filter(|texture| texture.usage == PreparedTextureUsage::Lightmap) + .count(), } } } @@ -510,7 +591,14 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>( } let mut visual_index_by_id: HashMap<AssetId<PreparedVisual>, PreparedVisualSignature> = HashMap::new(); - let mut material_signature_by_id: HashMap<AssetId<PreparedMaterial>, Vec<u8>> = HashMap::new(); + let mut model_ids = HashSet::new(); + let mut wear_ids = HashSet::new(); + let mut material_ids = HashSet::new(); + let mut texture_ids = HashSet::new(); + let mut models = Vec::new(); + let mut wears = Vec::new(); + let mut materials = Vec::new(); + let mut textures = Vec::new(); let mut visuals = Vec::new(); let mut prototype_visual_ids = Vec::with_capacity(prototypes.len()); @@ -526,18 +614,34 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>( Some(_) => {} None => { visual_index_by_id.insert(visual_id, signature); - let visual = prepare_visual_with_repository_internal( - repository, - proto, - Some(&mut material_signature_by_id), - )?; - if visual.id != visual_id { + let bundle = prepare_visual_with_repository_internal(repository, proto)?; + if bundle.visual.id != visual_id { // Defensive check. stable IDs are deterministic for the same inputs. return Err(AssetError::InvalidPrototype( "prepared visual id changed during preparation".to_string(), )); } - visuals.push(visual); + if let Some(model) = bundle.model { + if model_ids.insert(model.id) { + models.push(model); + } + } + if let Some(wear) = bundle.wear { + if wear_ids.insert(wear.id) { + wears.push(wear); + } + } + for material in bundle.materials { + if material_ids.insert(material.id) { + materials.push(material); + } + } + for texture in bundle.textures { + if texture_ids.insert(texture.id) { + textures.push(texture); + } + } + visuals.push(bundle.visual); } } prototype_visual_ids.push(visual_id); @@ -562,6 +666,10 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>( } Ok(MissionAssets { + models, + wears, + materials, + textures, visuals, object_visuals, }) @@ -919,11 +1027,15 @@ pub fn prepare_visual(proto: &EffectivePrototype) -> Result<PreparedVisual, Asse Ok(PreparedVisual { id: AssetId::new(id), mesh, + model_id: None, + wear_id: None, model_nodes: 0, model_slots: 0, model_batches: 0, material_count: 0, material_ids: Vec::new(), + texture_ids: Vec::new(), + lightmap_ids: Vec::new(), texture_count: 0, lightmap_count: 0, }) @@ -939,16 +1051,29 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>( repository: &R, proto: &EffectivePrototype, ) -> Result<PreparedVisual, AssetError> { - prepare_visual_with_repository_internal(repository, proto, None) + Ok(prepare_visual_with_repository_internal(repository, proto)?.visual) +} + +struct PreparedVisualBundle { + visual: PreparedVisual, + model: Option<PreparedModel>, + wear: Option<PreparedWear>, + materials: Vec<PreparedMaterial>, + textures: Vec<PreparedTexture>, } fn prepare_visual_with_repository_internal<R: ResourceRepository>( repository: &R, proto: &EffectivePrototype, - mut material_signature_by_id: Option<&mut HashMap<AssetId<PreparedMaterial>, Vec<u8>>>, -) -> Result<PreparedVisual, AssetError> { +) -> Result<PreparedVisualBundle, AssetError> { let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else { - return prepare_visual(proto); + return Ok(PreparedVisualBundle { + visual: prepare_visual(proto)?, + model: None, + wear: None, + materials: Vec::new(), + textures: Vec::new(), + }); }; let nres = decode_nres( @@ -958,6 +1083,13 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>( .map_err(AssetError::Nres)?; let msh_document = decode_msh(&nres).map_err(AssetError::Msh)?; let model = validate_msh(&msh_document).map_err(AssetError::Msh)?; + let model_id = AssetId::new(stable_model_id(proto)); + let prepared_model = PreparedModel { + id: model_id, + source: mesh_key.clone(), + validated: model.clone(), + mesh_dependencies: proto.dependencies.clone(), + }; let wear_name = sibling_name(mesh_key, "wea")?; let wear_key = ResourceKey { @@ -967,11 +1099,26 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>( }; let wear = decode_wear(&read_key(repository, &wear_key, Some("wear"))?) .map_err(AssetError::Material)?; + let wear_id = AssetId::new(stable_wear_id(proto)); + let prepared_wear = PreparedWear { + id: wear_id, + source: wear_key.clone(), + table: wear.clone(), + }; let mut material_count = 0; let mut material_ids = Vec::with_capacity(wear.entries.len()); + let mut prepared_materials = Vec::with_capacity(wear.entries.len()); + let mut prepared_textures = Vec::new(); + let mut texture_ids = Vec::new(); + let mut lightmap_ids = Vec::new(); let mut texture_count = 0; let mut lightmap_count = 0; + let lightmap_requests: Vec<_> = wear + .lightmaps + .iter() + .map(|lightmap| lightmap.lightmap.clone()) + .collect(); for material_index in 0..wear.entries.len() { let material_index = u16::try_from(material_index).map_err(|_| { AssetError::InvalidPrototype("material index does not fit archive format".to_string()) @@ -981,42 +1128,64 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>( material_count += 1; let material_id = AssetId::new(stable_material_id(proto, material_index, &material.name)); material_ids.push(material_id); - if let Some(registry) = material_signature_by_id.as_deref_mut() { - match registry.get(&material_id) { - Some(existing_name) => { - if existing_name != &material.name.0 { - return Err(AssetError::InvalidPrototype( - "stable material id collision between unrelated materials".to_string(), - )); - } - } - None => { - registry.insert(material_id, material.name.0.clone()); - } - } - } - - for texture in material.document.texture_requests() { - resolve_texture(repository, &texture)?; + let material_key = ResourceKey { + archive: parse_path("material.lib")?, + name: material.name.clone(), + type_id: Some(MAT0_KIND), + }; + let texture_requests = material.document.texture_requests(); + prepared_materials.push(PreparedMaterial { + id: material_id, + source: material_key, + name: material.name.clone(), + mat0: material.document.clone(), + texture_requests: texture_requests.clone(), + lightmap_requests: lightmap_requests.clone(), + }); + + for texture in texture_requests { + let prepared_texture = prepare_texture( + repository, + &texture, + PreparedTextureUsage::Diffuse, + )?; + texture_ids.push(prepared_texture.id); + prepared_textures.push(prepared_texture); texture_count += 1; } } for lightmap in &wear.lightmaps { - resolve_lightmap(repository, &lightmap.lightmap)?; + let prepared_lightmap = prepare_texture( + repository, + &lightmap.lightmap, + PreparedTextureUsage::Lightmap, + )?; + lightmap_ids.push(prepared_lightmap.id); + prepared_textures.push(prepared_lightmap); lightmap_count += 1; } - Ok(PreparedVisual { - id: AssetId::new(stable_visual_id(proto)), - mesh: Some(mesh_key.clone()), - model_nodes: model.node_count, - model_slots: model.slots.len(), - model_batches: model.batches.len(), - material_count, - material_ids, - texture_count, - lightmap_count, + Ok(PreparedVisualBundle { + visual: PreparedVisual { + id: AssetId::new(stable_visual_id(proto)), + mesh: Some(mesh_key.clone()), + model_id: Some(model_id), + wear_id: Some(wear_id), + model_nodes: model.node_count, + model_slots: model.slots.len(), + model_batches: model.batches.len(), + material_count, + material_ids, + texture_ids, + lightmap_ids, + texture_count, + lightmap_count, + }, + model: Some(prepared_model), + wear: Some(prepared_wear), + materials: prepared_materials, + textures: prepared_textures, }) } @@ -1248,6 +1417,32 @@ fn resolve_lightmap<R: ResourceRepository>( resolve_texm(repository, name, LIGHTMAP_ARCHIVE, "lightmap") } +fn prepare_texture<R: ResourceRepository>( + repository: &R, + name: &ResourceName, + usage: PreparedTextureUsage, +) -> Result<PreparedTexture, AssetError> { + let (archive, label) = match usage { + PreparedTextureUsage::Diffuse => (TEXTURES_ARCHIVE, "texture"), + PreparedTextureUsage::Lightmap => (LIGHTMAP_ARCHIVE, "lightmap"), + }; + let key = ResourceKey { + archive: parse_path(archive)?, + name: name.clone(), + type_id: None, + }; + let Some(bytes) = read_optional_key(repository, &key, Some(label))? else { + return Err(AssetError::MissingDependency(format!("{label} {name:?}"))); + }; + let texm = decode_texm(bytes).map_err(AssetError::Texture)?; + Ok(PreparedTexture { + id: AssetId::new(stable_texture_id(&key, usage)), + source: key, + texm, + usage, + }) +} + fn resolve_texm<R: ResourceRepository>( repository: &R, name: &ResourceName, @@ -1323,6 +1518,17 @@ fn stable_visual_id(proto: &EffectivePrototype) -> u64 { hasher.finish() } +fn stable_model_id(proto: &EffectivePrototype) -> u64 { + stable_visual_id(proto) +} + +fn stable_wear_id(proto: &EffectivePrototype) -> u64 { + let mut hasher = StableHasher::default(); + stable_visual_id(proto).hash(&mut hasher); + b"wear".hash(&mut hasher); + hasher.finish() +} + fn stable_material_id( proto: &EffectivePrototype, material_index: u16, @@ -1335,6 +1541,17 @@ fn stable_material_id( hasher.finish() } +fn stable_texture_id(key: &ResourceKey, usage: PreparedTextureUsage) -> u64 { + let mut hasher = StableHasher::default(); + key.archive.identity_bytes().hash(&mut hasher); + key.name.0.hash(&mut hasher); + match usage { + PreparedTextureUsage::Diffuse => 0_u8.hash(&mut hasher), + PreparedTextureUsage::Lightmap => 1_u8.hash(&mut hasher), + } + hasher.finish() +} + fn parse_path(value: &str) -> Result<NormalizedPath, AssetError> { normalize_relative(value.as_bytes(), PathPolicy::HostCompatible) .map_err(|err| AssetError::InvalidPrototype(format!("{err}"))) diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index ca97409..d10647d 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -23,9 +23,9 @@ use fparkan_assets::{ decode_mission_land_path, decode_mission_payload, decode_nres_payload, derive_mission_land_paths, extend_graph_report_with_visual_dependencies, prepare_terrain_world, - AssetError as AssetPreparationError, AssetManager, BuildCategory, MissionAssetPlan, - MissionDocument, MissionError, MissionTerrainPaths, NresError, TerrainFormatError, - TerrainPreparationError, TerrainWorld, TmaProfile, + AssetError as AssetPreparationError, AssetId, AssetManager, BuildCategory, MissionAssetPlan, + MissionDocument, MissionError, MissionTerrainPaths, NresError, PreparedVisual, + TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile, }; use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy}; use fparkan_prototype::{ @@ -149,6 +149,36 @@ pub struct MissionLoadTrace { pub transforms: Vec<PlacedTransformProfile>, } +/// Ordered mission property preserved for later runtime stages. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MissionObjectProperty { + /// Raw property value words. + pub raw_value: [u32; 4], + /// Raw property name bytes. + pub name_raw: Vec<u8>, +} + +/// Mission-derived object draft preserved for Stage 3 viewer/player work. +#[derive(Clone, Debug, PartialEq)] +pub struct MissionObjectDraft { + /// Original mission object id. + pub original_id: Option<OriginalObjectId>, + /// Raw mission resource reference. + pub resource_name_raw: Vec<u8>, + /// Raw identity/clan word from the mission. + pub identity_or_clan_raw: u32, + /// Raw position vector. + pub position: [f32; 3], + /// Raw orientation vector. + pub orientation_raw: [f32; 3], + /// Raw scale vector. + pub scale: [f32; 3], + /// Prepared visuals reachable from this mission object. + pub visual_ids: Vec<AssetId<PreparedVisual>>, + /// Ordered mission properties. + pub properties: Vec<MissionObjectProperty>, +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] struct MissionLoadOptions { fail_after_registered_objects: Option<usize>, @@ -187,6 +217,10 @@ pub struct LoadedMission { pub graph_unit_component_count: usize, /// Mission prototype graph root count. pub graph_root_count: usize, + /// Total materialized graph node count after visual dependency expansion. + pub graph_node_count: usize, + /// Total materialized graph edge count after visual dependency expansion. + pub graph_edge_count: usize, /// Mission asset plan visual count after dependency preparation. pub asset_visual_count: usize, /// Expanded prototype requests resolved to effective prototypes. @@ -254,6 +288,7 @@ struct LoadedMissionState { prototype_report: PrototypeGraphReport, mission_assets: MissionAssets, asset_plan: MissionAssetPlan, + object_drafts: Vec<MissionObjectDraft>, } /// Engine error. @@ -539,6 +574,28 @@ fn load_mission_with_options( source, })?; let mission_asset_plan = mission_assets.to_plan(); + let object_drafts: Vec<_> = mission + .objects + .iter() + .enumerate() + .map(|(index, object)| MissionObjectDraft { + original_id: u32::try_from(index).ok().map(OriginalObjectId), + resource_name_raw: object.resource_raw.clone(), + identity_or_clan_raw: object.identity_or_clan_raw, + position: object.position, + orientation_raw: object.orientation, + scale: object.scale, + visual_ids: mission_assets.visuals_for_object(index).to_vec(), + properties: object + .properties + .iter() + .map(|property| MissionObjectProperty { + raw_value: property.raw_value, + name_raw: property.name_raw.clone(), + }) + .collect(), + }) + .collect(); trace.phases.push(MissionLoadPhase::Assets); let mut new_runtime_world = new_world(WorldConfig); @@ -580,6 +637,8 @@ fn load_mission_with_options( graph_direct_reference_count: prototype_report.direct_reference_count, graph_unit_component_count: prototype_report.unit_component_count, graph_root_count: prototype_report.root_count, + graph_node_count: prototype_graph.nodes.len(), + graph_edge_count: prototype_graph.edges.len(), asset_visual_count: mission_asset_plan.visual_count, graph_resolved_count: prototype_report.resolved_count, graph_mesh_dependency_count: prototype_report.mesh_dependency_count, @@ -608,6 +667,7 @@ fn load_mission_with_options( prototype_report, mission_assets, asset_plan: mission_asset_plan, + object_drafts, }); Ok((summary, trace)) } @@ -697,6 +757,15 @@ pub fn loaded_mission_assets(engine: &Engine) -> Option<&MissionAssets> { engine.loaded.as_ref().map(|state| &state.mission_assets) } +/// Returns mission-derived object drafts preserved for later runtime stages. +#[must_use] +pub fn loaded_mission_object_drafts(engine: &Engine) -> Option<&[MissionObjectDraft]> { + engine + .loaded + .as_ref() + .map(|state| state.object_drafts.as_slice()) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum SchedulerPresentation { Headless, |
