aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-assets
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-assets')
-rw-r--r--crates/fparkan-assets/Cargo.toml27
-rw-r--r--crates/fparkan-assets/src/lib.rs390
2 files changed, 49 insertions, 368 deletions
diff --git a/crates/fparkan-assets/Cargo.toml b/crates/fparkan-assets/Cargo.toml
index 468698b..ce3cb61 100644
--- a/crates/fparkan-assets/Cargo.toml
+++ b/crates/fparkan-assets/Cargo.toml
@@ -6,19 +6,14 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-material = { path = "../fparkan-material", version = "0.1.0" }
-fparkan-msh = { path = "../fparkan-msh", version = "0.1.0" }
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-mission-format = { path = "../fparkan-mission-format", version = "0.1.0" }
-fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
-fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
-fparkan-texm = { path = "../fparkan-texm", version = "0.1.0" }
-fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
-fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
-
-[dev-dependencies]
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-material = { path = "../fparkan-material" }
+fparkan-msh = { path = "../fparkan-msh" }
+fparkan-nres = { path = "../fparkan-nres" }
+fparkan-path = { path = "../fparkan-path" }
+fparkan-mission-format = { path = "../fparkan-mission-format" }
+fparkan-prototype = { path = "../fparkan-prototype" }
+fparkan-resource = { path = "../fparkan-resource" }
+fparkan-texm = { path = "../fparkan-texm" }
+fparkan-terrain = { path = "../fparkan-terrain" }
+fparkan-terrain-format = { path = "../fparkan-terrain-format" }
+fparkan-vfs = { path = "../fparkan-vfs" }
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 708993f..3193e3f 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1,23 +1,4 @@
#![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
- )
-)]
//! Asset manager ports and transactional preparation models.
use fparkan_material::{
@@ -35,24 +16,24 @@ use fparkan_prototype::{
PrototypeGraphFailure, PrototypeGraphNodeKind, PrototypeGraphProvenance, PrototypeGraphReport,
PrototypeGraphRequiredness,
};
-use fparkan_resource::{ResourceError, ResourceKey, ResourceRepository};
+use fparkan_resource::{CachedResourceRepository, 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_mip_rgba8, decode_texm, RgbaImage, TexmDocument, TexmError};
+use fparkan_vfs::DirectoryVfs;
use std::collections::{hash_map::Entry, HashMap, HashSet};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
+use std::path::Path;
use std::sync::Arc;
const TEXTURES_ARCHIVE: &str = "textures.lib";
const LIGHTMAP_ARCHIVE: &str = "lightmap.lib";
-const VISUAL_DEPENDENCY_PROGRESS_INTERVAL: usize = 64;
type WearValidationCache =
HashMap<(NormalizedPath, Vec<u8>), Result<fparkan_material::WearTable, String>>;
-type TextureValidationCache = HashMap<Vec<u8>, Result<(), String>>;
/// Canonical terrain archive paths derived from a mission land reference.
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -188,6 +169,37 @@ pub fn decode_nres_payload(
decode_nres(bytes, ReadProfile::Compatible)
}
+/// Loads a phase-zero diffuse texture from a map-local standalone WEAR file.
+///
+/// The sidecar stays beside `Land.msh`; MAT0 and TEXM remain in the normal
+/// game-root archives.
+pub fn load_standalone_wear_material_texture_mip0_rgba8_from_root(
+ root: &Path,
+ wear_path: &Path,
+ material_index: u16,
+) -> Result<RgbaImage, String> {
+ let wear = decode_wear(
+ &std::fs::read(wear_path).map_err(|err| format!("{}: {err}", wear_path.display()))?,
+ )
+ .map_err(|err| err.to_string())?;
+ let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
+ let material =
+ resolve_material(&repository, &wear, material_index).map_err(|err| err.to_string())?;
+ let texture = material.document.primary_texture().ok_or_else(|| {
+ "MAT0 phase zero declares an intentionally untextured material".to_string()
+ })?;
+ let key = ResourceKey {
+ archive: parse_path(TEXTURES_ARCHIVE).map_err(|err| err.to_string())?,
+ name: texture.clone(),
+ type_id: None,
+ };
+ let bytes = read_optional_key(&repository, &key, Some("texture"))
+ .map_err(|err| err.to_string())?
+ .ok_or_else(|| format!("missing texture {texture:?}"))?;
+ let document = decode_texm(bytes).map_err(|err| err.to_string())?;
+ decode_mip_rgba8(&document, 0).map_err(|err| err.to_string())
+}
+
/// Decodes terrain documents and builds immutable terrain state.
///
/// # Errors
@@ -494,56 +506,6 @@ pub struct AssetPreparationReport {
pub texture_pixels: u64,
}
-/// Asset preparation checkpoint emitted while resolving visual dependencies.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum AssetPreparationPhase {
- /// Decode and validate an MSH model.
- ModelMesh,
- /// Decode the corresponding WEAR table.
- WearTable,
- /// Resolve MAT0 material documents referenced by a WEAR table.
- Materials,
- /// Decode diffuse textures and baked lightmaps.
- Textures,
-}
-
-/// Visual dependency class currently being expanded into a prototype graph.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum VisualDependencyPhase {
- /// Resolve a mesh's WEAR table.
- Wear,
- /// Resolve MAT0 material documents selected by WEAR.
- Material,
- /// Validate diffuse TEXM textures and lightmaps.
- Texture,
-}
-
-/// Validation-cache entries retained while expanding visual dependencies.
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub struct VisualDependencyCacheCounts {
- /// Distinct WEAR archive/name keys resolved so far.
- pub wear_entries: usize,
- /// Distinct MAT0 raw names resolved so far.
- pub material_entries: usize,
- /// Distinct TEXM archive/name keys resolved so far.
- pub texture_entries: usize,
-}
-
-/// Throttled visual-dependency expansion progress.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct VisualDependencyProgress {
- /// Dependency class currently being expanded.
- pub phase: VisualDependencyPhase,
- /// Number of requests reached in this class during the current expansion.
- pub request_count: usize,
- /// Resource graph nodes materialized before this request is resolved.
- pub graph_node_count: usize,
- /// Resource graph edges materialized before this request is resolved.
- pub graph_edge_count: usize,
- /// Validation-cache entries retained before this request is resolved.
- pub cache_entries: VisualDependencyCacheCounts,
-}
-
/// Errors raised while preparing CPU-side assets.
#[derive(Debug)]
pub enum AssetError {
@@ -660,25 +622,6 @@ impl<R: ResourceRepository> AssetManager<R> {
prepare_mission_assets_with_repository(&self.repository, root_prototype_spans, prototypes)
}
- /// Builds mission assets and reports the active dependency class.
- ///
- /// # Errors
- ///
- /// Returns [`AssetError`] if any visual dependency is missing or malformed.
- pub fn prepare_mission_assets_with_progress(
- &self,
- root_prototype_spans: &[std::ops::Range<usize>],
- prototypes: &[EffectivePrototype],
- on_phase: impl FnMut(AssetPreparationPhase),
- ) -> Result<MissionAssets, AssetError> {
- prepare_mission_assets_with_repository_with_progress(
- &self.repository,
- root_prototype_spans,
- prototypes,
- on_phase,
- )
- }
-
/// Builds mission assets together with a bounded preparation report.
///
/// # Errors
@@ -764,28 +707,6 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>(
.0)
}
-/// Builds mission assets and reports the active dependency class.
-///
-/// # Errors
-///
-/// Returns [`AssetError`] if any visual dependency is missing or malformed.
-pub fn prepare_mission_assets_with_repository_with_progress<R: ResourceRepository>(
- repository: &R,
- root_prototype_spans: &[std::ops::Range<usize>],
- prototypes: &[EffectivePrototype],
- mut on_phase: impl FnMut(AssetPreparationPhase),
-) -> Result<MissionAssets, AssetError> {
- Ok(prepare_mission_assets_with_repository_internal(
- repository,
- root_prototype_spans,
- prototypes,
- AssetIdentityPolicy::default(),
- AssetPreparationLimits::default(),
- &mut on_phase,
- )?
- .0)
-}
-
/// Builds mission assets while enforcing explicit preparation limits.
///
/// # Errors
@@ -798,27 +719,24 @@ pub fn prepare_mission_assets_profiled_with_repository<R: ResourceRepository>(
prototypes: &[EffectivePrototype],
limits: AssetPreparationLimits,
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
- let mut ignore_phase = |_| {};
prepare_mission_assets_with_repository_internal(
repository,
root_prototype_spans,
prototypes,
AssetIdentityPolicy::default(),
limits,
- &mut ignore_phase,
)
}
// This orchestration intentionally keeps preparation, deduplication, and limit enforcement
// together so all fallible asset construction remains transactional.
-#[allow(clippy::too_many_lines)]
+
fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
repository: &R,
root_prototype_spans: &[std::ops::Range<usize>],
prototypes: &[EffectivePrototype],
identity_policy: AssetIdentityPolicy,
limits: AssetPreparationLimits,
- on_phase: &mut dyn FnMut(AssetPreparationPhase),
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
if prototypes.is_empty() {
return Ok((MissionAssets::default(), AssetPreparationReport::default()));
@@ -858,7 +776,6 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
proto,
identity_policy,
&mut preparation_cache,
- on_phase,
)?;
if bundle.visual.id != visual_id {
// Defensive check. stable IDs are deterministic for the same inputs.
@@ -1173,73 +1090,17 @@ fn enforce_asset_preparation_limits(
/// the path policy, which indicates a programming error in this module.
// The graph expansion is deliberately a single transactional traversal so every generated
// edge receives the correct parent provenance and node/edge sequence.
-#[allow(clippy::too_many_lines)]
-pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
- repository: &R,
- report: &mut PrototypeGraphReport,
- graph: &mut PrototypeGraph,
- prototypes: &[EffectivePrototype],
-) {
- extend_graph_report_with_visual_dependencies_and_progress(
- repository,
- report,
- graph,
- prototypes,
- |_| {},
- );
-}
-/// Extends a prototype dependency report while reporting each visual dependency class.
-///
-/// # Panics
-///
-/// Panics only if the hard-coded, host-compatible `material.lib` path stops
-/// satisfying the path policy, which indicates a programming error in this module.
-#[allow(clippy::too_many_lines)]
-pub fn extend_graph_report_with_visual_dependencies_and_progress<R: ResourceRepository>(
- repository: &R,
- report: &mut PrototypeGraphReport,
- graph: &mut PrototypeGraph,
- prototypes: &[EffectivePrototype],
- mut on_phase: impl FnMut(VisualDependencyPhase),
-) {
- let mut last_phase = None;
- extend_graph_report_with_visual_dependencies_with_progress(
- repository,
- report,
- graph,
- prototypes,
- |progress| {
- if last_phase != Some(progress.phase) {
- on_phase(progress.phase);
- last_phase = Some(progress.phase);
- }
- },
- );
-}
-
-/// Extends a prototype dependency report with throttled request progress.
-///
-/// The callback receives each dependency-class entrance, its first request,
-/// and every subsequent multiple of 64 requests. Graph traversal, edge order
-/// and validation remain unchanged.
-///
-/// # Panics
-///
-/// Panics only if the hard-coded, host-compatible `material.lib` path stops
-/// satisfying the path policy, which indicates a programming error in this module.
-#[allow(clippy::too_many_lines)]
-pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRepository>(
+pub fn extend_graph_report_with_visual_dependencies<R: ResourceRepository>(
repository: &R,
report: &mut PrototypeGraphReport,
graph: &mut PrototypeGraph,
prototypes: &[EffectivePrototype],
- mut on_progress: impl FnMut(VisualDependencyProgress),
) {
if graph.visual_dependencies_expanded {
return;
}
- #[allow(clippy::expect_used)]
+
let material_archive = parse_path("material.lib")
.expect("static material archive path must satisfy host-compatible normalization");
let mut next_node = graph
@@ -1258,9 +1119,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
let mut lightmap_texture_validation = HashMap::new();
let mut material_validation = HashMap::new();
let mut wear_validation = HashMap::new();
- let mut wear_requests = 0usize;
- let mut material_requests = 0usize;
- let mut texture_requests = 0usize;
// Capture all base-graph anchors before this traversal appends visual
// resources and edges. Looking them up again from the growing graph would
// make each later prototype scan an ever larger collection.
@@ -1288,22 +1146,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
let unit_component_index =
mesh_parent_edge.and_then(|edge| unit_component_index_for_edge(graph, edge));
- wear_requests = wear_requests.saturating_add(1);
- report_visual_dependency_progress(
- &mut on_progress,
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Wear,
- request_count: wear_requests,
- graph_node_count: graph.nodes.len(),
- graph_edge_count: graph.edges.len(),
- cache_entries: visual_dependency_cache_counts(
- &wear_validation,
- &material_validation,
- &diffuse_texture_validation,
- &lightmap_texture_validation,
- ),
- },
- );
match resolve_wear_table_cached(repository, mesh, &mut wear_validation) {
Ok(table) => {
report.wear_resolved_count += 1;
@@ -1343,35 +1185,7 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
&mut next_edge,
);
report.material_slot_count += table.entries.len();
- on_progress(VisualDependencyProgress {
- phase: VisualDependencyPhase::Material,
- request_count: material_requests,
- graph_node_count: graph.nodes.len(),
- graph_edge_count: graph.edges.len(),
- cache_entries: visual_dependency_cache_counts(
- &wear_validation,
- &material_validation,
- &diffuse_texture_validation,
- &lightmap_texture_validation,
- ),
- });
for (material_index, _entry) in table.entries.iter().enumerate() {
- material_requests = material_requests.saturating_add(1);
- report_visual_dependency_progress(
- &mut on_progress,
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Material,
- request_count: material_requests,
- graph_node_count: graph.nodes.len(),
- graph_edge_count: graph.edges.len(),
- cache_entries: visual_dependency_cache_counts(
- &wear_validation,
- &material_validation,
- &diffuse_texture_validation,
- &lightmap_texture_validation,
- ),
- },
- );
let Ok(material_index) = u16::try_from(material_index) else {
push_visual_failure(
report,
@@ -1418,22 +1232,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
&mut next_edge,
);
for texture in material.document.texture_requests() {
- texture_requests = texture_requests.saturating_add(1);
- report_visual_dependency_progress(
- &mut on_progress,
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Texture,
- request_count: texture_requests,
- graph_node_count: graph.nodes.len(),
- graph_edge_count: graph.edges.len(),
- cache_entries: visual_dependency_cache_counts(
- &wear_validation,
- &material_validation,
- &diffuse_texture_validation,
- &lightmap_texture_validation,
- ),
- },
- );
report.texture_request_count += 1;
match resolve_texm_validation_cached(
repository,
@@ -1495,22 +1293,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
}
}
for lightmap in &table.lightmaps {
- texture_requests = texture_requests.saturating_add(1);
- report_visual_dependency_progress(
- &mut on_progress,
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Texture,
- request_count: texture_requests,
- graph_node_count: graph.nodes.len(),
- graph_edge_count: graph.edges.len(),
- cache_entries: visual_dependency_cache_counts(
- &wear_validation,
- &material_validation,
- &diffuse_texture_validation,
- &lightmap_texture_validation,
- ),
- },
- );
report.lightmap_request_count += 1;
match resolve_texm_validation_cached(
repository,
@@ -1574,34 +1356,6 @@ pub fn extend_graph_report_with_visual_dependencies_with_progress<R: ResourceRep
graph.visual_dependencies_expanded = true;
}
-fn report_visual_dependency_progress(
- on_progress: &mut impl FnMut(VisualDependencyProgress),
- progress: VisualDependencyProgress,
-) {
- if progress.request_count == 1
- || progress
- .request_count
- .is_multiple_of(VISUAL_DEPENDENCY_PROGRESS_INTERVAL)
- {
- on_progress(progress);
- }
-}
-
-fn visual_dependency_cache_counts(
- wear_validation: &WearValidationCache,
- material_validation: &HashMap<Vec<u8>, Result<ResolvedMaterial, String>>,
- diffuse_texture_validation: &TextureValidationCache,
- lightmap_texture_validation: &TextureValidationCache,
-) -> VisualDependencyCacheCounts {
- VisualDependencyCacheCounts {
- wear_entries: wear_validation.len(),
- material_entries: material_validation.len(),
- texture_entries: diffuse_texture_validation
- .len()
- .saturating_add(lightmap_texture_validation.len()),
- }
-}
-
fn push_graph_resource_node(
graph: &mut PrototypeGraph,
kind: PrototypeGraphNodeKind,
@@ -1738,7 +1492,6 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>(
proto,
AssetIdentityPolicy::default(),
&mut preparation_cache,
- &mut |_| {},
)?
.visual)
}
@@ -1761,13 +1514,12 @@ struct PreparationCache {
}
// Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary.
-#[allow(clippy::too_many_lines)]
+
fn prepare_visual_with_repository_internal<R: ResourceRepository>(
repository: &R,
proto: &EffectivePrototype,
identity_policy: AssetIdentityPolicy,
preparation_cache: &mut PreparationCache,
- on_phase: &mut dyn FnMut(AssetPreparationPhase),
) -> Result<PreparedVisualBundle, AssetError> {
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
return Ok(PreparedVisualBundle {
@@ -1779,7 +1531,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
});
};
- on_phase(AssetPreparationPhase::ModelMesh);
let model = prepare_model_cached(repository, mesh_key, preparation_cache)?;
let model_id = AssetId::new((identity_policy.model)(proto));
let prepared_model = PreparedModel {
@@ -1795,7 +1546,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
name: wear_name,
type_id: Some(WEAR_KIND),
};
- on_phase(AssetPreparationPhase::WearTable);
let wear = prepare_wear_cached(repository, &wear_key, preparation_cache)?;
let wear_id = AssetId::new((identity_policy.wear)(proto));
let prepared_wear = PreparedWear {
@@ -1817,7 +1567,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
.iter()
.map(|lightmap| lightmap.lightmap.clone())
.collect();
- on_phase(AssetPreparationPhase::Materials);
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())
@@ -1847,7 +1596,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
});
for texture in texture_requests {
- on_phase(AssetPreparationPhase::Textures);
let prepared_texture = prepare_texture_cached(
repository,
&texture,
@@ -1862,7 +1610,6 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
}
for lightmap in &wear.lightmaps {
- on_phase(AssetPreparationPhase::Textures);
let prepared_lightmap = prepare_texture_cached(
repository,
&lightmap.lightmap,
@@ -2451,66 +2198,6 @@ mod tests {
use std::path::PathBuf;
#[test]
- fn visual_dependency_progress_is_throttled_at_fixed_request_intervals() {
- let mut progress = Vec::new();
- for request_count in 1..=129 {
- report_visual_dependency_progress(
- &mut |event| progress.push(event),
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Texture,
- request_count,
- graph_node_count: 37,
- graph_edge_count: 41,
- cache_entries: VisualDependencyCacheCounts {
- wear_entries: 3,
- material_entries: 5,
- texture_entries: 7,
- },
- },
- );
- }
-
- assert_eq!(
- progress,
- vec![
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Texture,
- request_count: 1,
- graph_node_count: 37,
- graph_edge_count: 41,
- cache_entries: VisualDependencyCacheCounts {
- wear_entries: 3,
- material_entries: 5,
- texture_entries: 7,
- },
- },
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Texture,
- request_count: 64,
- graph_node_count: 37,
- graph_edge_count: 41,
- cache_entries: VisualDependencyCacheCounts {
- wear_entries: 3,
- material_entries: 5,
- texture_entries: 7,
- },
- },
- VisualDependencyProgress {
- phase: VisualDependencyPhase::Texture,
- request_count: 128,
- graph_node_count: 37,
- graph_edge_count: 41,
- cache_entries: VisualDependencyCacheCounts {
- wear_entries: 3,
- material_entries: 5,
- texture_entries: 7,
- },
- },
- ]
- );
- }
-
- #[test]
fn wear_validation_cache_replays_archive_qualified_failure() {
let repository = repository_with_archives(&[]);
let mesh = ResourceKey {
@@ -3315,7 +3002,6 @@ mod tests {
&prototypes,
policy,
AssetPreparationLimits::default(),
- &mut |_| {},
)
.expect_err("collision should fail");