aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/fparkan-animation/Cargo.toml3
-rw-r--r--crates/fparkan-animation/src/lib.rs20
-rw-r--r--crates/fparkan-assets/Cargo.toml27
-rw-r--r--crates/fparkan-assets/src/lib.rs390
-rw-r--r--crates/fparkan-binary/Cargo.toml3
-rw-r--r--crates/fparkan-binary/src/lib.rs20
-rw-r--r--crates/fparkan-corpus/Cargo.toml22
-rw-r--r--crates/fparkan-corpus/src/lib.rs1239
-rw-r--r--crates/fparkan-diagnostics/Cargo.toml13
-rw-r--r--crates/fparkan-diagnostics/src/lib.rs249
-rw-r--r--crates/fparkan-fx/Cargo.toml7
-rw-r--r--crates/fparkan-fx/src/lib.rs19
-rw-r--r--crates/fparkan-inspection/Cargo.toml21
-rw-r--r--crates/fparkan-inspection/src/lib.rs1009
-rw-r--r--crates/fparkan-material/Cargo.toml11
-rw-r--r--crates/fparkan-material/src/lib.rs19
-rw-r--r--crates/fparkan-mission-format/Cargo.toml5
-rw-r--r--crates/fparkan-mission-format/src/lib.rs19
-rw-r--r--crates/fparkan-msh/Cargo.toml7
-rw-r--r--crates/fparkan-msh/src/lib.rs20
-rw-r--r--crates/fparkan-nres/Cargo.toml7
-rw-r--r--crates/fparkan-nres/src/lib.rs19
-rw-r--r--crates/fparkan-path/Cargo.toml3
-rw-r--r--crates/fparkan-path/src/lib.rs19
-rw-r--r--crates/fparkan-platform/Cargo.toml3
-rw-r--r--crates/fparkan-platform/src/lib.rs19
-rw-r--r--crates/fparkan-prototype/Cargo.toml13
-rw-r--r--crates/fparkan-prototype/src/lib.rs21
-rw-r--r--crates/fparkan-render/Cargo.toml4
-rw-r--r--crates/fparkan-render/src/lib.rs1165
-rw-r--r--crates/fparkan-resource/Cargo.toml13
-rw-r--r--crates/fparkan-resource/src/lib.rs19
-rw-r--r--crates/fparkan-rsli/Cargo.toml5
-rw-r--r--crates/fparkan-rsli/src/lib.rs20
-rw-r--r--crates/fparkan-runtime/Cargo.toml23
-rw-r--r--crates/fparkan-runtime/src/lib.rs710
-rw-r--r--crates/fparkan-script/Cargo.toml5
-rw-r--r--crates/fparkan-script/src/lib.rs5
-rw-r--r--crates/fparkan-terrain-format/Cargo.toml7
-rw-r--r--crates/fparkan-terrain-format/src/lib.rs19
-rw-r--r--crates/fparkan-terrain/Cargo.toml7
-rw-r--r--crates/fparkan-terrain/src/lib.rs19
-rw-r--r--crates/fparkan-test-support/Cargo.toml12
-rw-r--r--crates/fparkan-test-support/src/lib.rs44
-rw-r--r--crates/fparkan-texm/Cargo.toml5
-rw-r--r--crates/fparkan-texm/src/lib.rs19
-rw-r--r--crates/fparkan-vfs/Cargo.toml7
-rw-r--r--crates/fparkan-vfs/src/lib.rs25
-rw-r--r--crates/fparkan-world/Cargo.toml5
-rw-r--r--crates/fparkan-world/src/lib.rs19
50 files changed, 145 insertions, 5239 deletions
diff --git a/crates/fparkan-animation/Cargo.toml b/crates/fparkan-animation/Cargo.toml
index a3e5d9e..153f9f2 100644
--- a/crates/fparkan-animation/Cargo.toml
+++ b/crates/fparkan-animation/Cargo.toml
@@ -6,6 +6,3 @@ license.workspace = true
repository.workspace = true
[dependencies]
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-animation/src/lib.rs b/crates/fparkan-animation/src/lib.rs
index 34a3fa7..c07c5b8 100644
--- a/crates/fparkan-animation/src/lib.rs
+++ b/crates/fparkan-animation/src/lib.rs
@@ -1,23 +1,5 @@
#![forbid(unsafe_code)]
-#![cfg_attr(
- test,
- allow(
- clippy::cast_possible_truncation,
- clippy::cast_possible_wrap,
- 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::cast_precision_loss)]
+
//! Deterministic animation sampling contracts.
//!
//! The current sampler is a portable reference path. Compatibility profiles
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");
diff --git a/crates/fparkan-binary/Cargo.toml b/crates/fparkan-binary/Cargo.toml
index 2bab5be..cec2aa3 100644
--- a/crates/fparkan-binary/Cargo.toml
+++ b/crates/fparkan-binary/Cargo.toml
@@ -6,6 +6,3 @@ license.workspace = true
repository.workspace = true
[dependencies]
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-binary/src/lib.rs b/crates/fparkan-binary/src/lib.rs
index be9e8d9..bb9f118 100644
--- a/crates/fparkan-binary/src/lib.rs
+++ b/crates/fparkan-binary/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
- )
-)]
//! Bounded little-endian binary cursor and checked layout helpers.
use std::fmt;
@@ -399,7 +380,6 @@ pub fn sha256_hex(digest: &Sha256Digest) -> String {
out
}
-#[allow(clippy::many_single_char_names)]
fn compress_sha256_chunk(h: &mut [u32; 8], chunk: &[u8], k: &[u32; 64]) {
let mut w = [0u32; 64];
for (idx, word) in w.iter_mut().take(16).enumerate() {
diff --git a/crates/fparkan-corpus/Cargo.toml b/crates/fparkan-corpus/Cargo.toml
deleted file mode 100644
index 50bf798..0000000
--- a/crates/fparkan-corpus/Cargo.toml
+++ /dev/null
@@ -1,22 +0,0 @@
-[package]
-name = "fparkan-corpus"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-
-[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-fparkan-fx = { path = "../fparkan-fx", version = "0.1.0" }
-fparkan-material = { path = "../fparkan-material", version = "0.1.0" }
-fparkan-msh = { path = "../fparkan-msh", version = "0.1.0" }
-fparkan-mission-format = { path = "../fparkan-mission-format", version = "0.1.0" }
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-rsli = { path = "../fparkan-rsli", version = "0.1.0" }
-fparkan-texm = { path = "../fparkan-texm", version = "0.1.0" }
-fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-corpus/src/lib.rs b/crates/fparkan-corpus/src/lib.rs
deleted file mode 100644
index 48eaf9e..0000000
--- a/crates/fparkan-corpus/src/lib.rs
+++ /dev/null
@@ -1,1239 +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
- )
-)]
-//! Licensed corpus discovery and aggregate reports.
-
-use fparkan_binary::{sha256, sha256_hex, Sha256Digest};
-use fparkan_fx::{decode_fxid, FXID_KIND};
-use fparkan_material::{decode_mat0, decode_wear, MAT0_KIND, WEAR_KIND};
-use fparkan_mission_format::{decode_tma, TmaProfile};
-use fparkan_msh::{decode_msh, validate_msh};
-use fparkan_nres::NresDocument;
-use fparkan_path::{ascii_lookup_key, normalize_relative, PathPolicy};
-use fparkan_prototype::{decode_unit_dat, decode_unit_dat_binding};
-use fparkan_rsli::{decode as decode_rsli, ReadProfile};
-use fparkan_terrain_format::{decode_land_map, decode_land_msh};
-use fparkan_texm::decode_texm;
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt;
-use std::fs;
-use std::io::Write;
-#[cfg(unix)]
-use std::os::unix::ffi::OsStrExt;
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-
-const TEXM_KIND: u32 = 0x6d78_6554;
-
-/// Corpus kind.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum CorpusKind {
- /// Demo corpus.
- Demo,
- /// Part 1 full game.
- Part1,
- /// Part 2 full game.
- Part2,
- /// Unknown local directory.
- Unknown,
-}
-
-/// Corpus root.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct CorpusRoot(pub PathBuf);
-
-/// Discovery options.
-#[derive(Clone, Copy, Debug, Default)]
-pub struct DiscoverOptions {
- /// Whether symlinks may be traversed.
- pub follow_symlinks: bool,
-}
-
-/// File manifest entry.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct ManifestEntry {
- /// Normalized relative path.
- pub path: String,
- /// Byte-exact relative host path used for reopening corpus files.
- pub host_rel_path: PathBuf,
- /// File size in bytes.
- pub size: u64,
- /// SHA-256 content fingerprint.
- pub hash: Sha256Digest,
-}
-
-/// Corpus manifest.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct CorpusManifest {
- /// Kind.
- pub kind: CorpusKind,
- /// Sorted files.
- pub files: Vec<ManifestEntry>,
- /// Casefold collisions.
- pub casefold_collisions: Vec<Vec<String>>,
-}
-
-/// Aggregate report.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct CorpusReport {
- /// Schema version.
- pub schema: u32,
- /// Kind.
- pub kind: CorpusKind,
- /// Total files.
- pub files: usize,
- /// Total bytes.
- pub bytes: u64,
- /// Metrics.
- pub metrics: BTreeMap<String, u64>,
- /// Casefold collision count.
- pub casefold_collisions: usize,
- /// Manifest fingerprint.
- pub fingerprint: Sha256Digest,
- /// Per-file status records.
- pub records: Vec<CorpusFileRecord>,
- /// Number of files with report errors.
- pub failures: usize,
-}
-
-/// Per-file report status.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum CorpusFileStatus {
- /// File was inspected successfully.
- Ok,
- /// File was inspected but produced a non-fatal warning.
- Warning,
- /// File could not be inspected.
- Error,
-}
-
-/// Per-file report record.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct CorpusFileRecord {
- /// Normalized relative path.
- pub path: String,
- /// Inspection status.
- pub status: CorpusFileStatus,
- /// Detected file variant.
- pub variant: String,
- /// Optional status message.
- pub message: Option<String>,
-}
-
-/// Corpus error.
-#[derive(Debug)]
-pub enum CorpusError {
- /// I/O failure.
- Io {
- /// Path where I/O failed.
- path: PathBuf,
- /// Source error.
- source: std::io::Error,
- },
- /// Invalid root.
- InvalidRoot(PathBuf),
- /// Invalid path.
- InvalidPath(String),
- /// Aggregate report failure.
- Report {
- /// Path where reporting failed.
- path: String,
- /// Failure message.
- message: String,
- },
-}
-
-impl fmt::Display for CorpusError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Io { path, source } => write!(f, "{}: {source}", path.display()),
- Self::InvalidRoot(path) => write!(f, "invalid corpus root: {}", path.display()),
- Self::InvalidPath(path) => write!(f, "invalid corpus path: {path}"),
- Self::Report { path, message } => write!(f, "{path}: {message}"),
- }
- }
-}
-
-impl std::error::Error for CorpusError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Self::Io { source, .. } => Some(source),
- Self::InvalidRoot(_) | Self::InvalidPath(_) | Self::Report { .. } => None,
- }
- }
-}
-
-/// Discovers a corpus under a root directory.
-///
-/// # Errors
-///
-/// Returns [`CorpusError`] if the root is invalid, traversal encounters an I/O
-/// error, or a discovered path cannot be represented by the legacy path policy.
-pub fn discover(root: &Path, options: DiscoverOptions) -> Result<CorpusManifest, CorpusError> {
- if !root.is_dir() {
- return Err(CorpusError::InvalidRoot(root.to_path_buf()));
- }
- let mut files = Vec::new();
- walk(root, root, options, &mut files)?;
- files.sort_by(|a, b| a.host_rel_path.cmp(&b.host_rel_path));
-
- let kind = classify(root, &files);
- let casefold_collisions = detect_casefold_collisions(&files);
- Ok(CorpusManifest {
- kind,
- files,
- casefold_collisions,
- })
-}
-
-fn walk(
- root: &Path,
- dir: &Path,
- options: DiscoverOptions,
- out: &mut Vec<ManifestEntry>,
-) -> Result<(), CorpusError> {
- let read_dir = fs::read_dir(dir).map_err(|source| CorpusError::Io {
- path: dir.to_path_buf(),
- source,
- })?;
- let mut entries = Vec::new();
- for entry in read_dir {
- let entry = entry.map_err(|source| CorpusError::Io {
- path: dir.to_path_buf(),
- source,
- })?;
- entries.push(entry.path());
- }
- entries.sort();
- for path in entries {
- if path
- .file_name()
- .and_then(|name| name.to_str())
- .is_some_and(|name| name.starts_with('.'))
- {
- continue;
- }
- let metadata = fs::symlink_metadata(&path).map_err(|source| CorpusError::Io {
- path: path.clone(),
- source,
- })?;
- if metadata.file_type().is_symlink() && !options.follow_symlinks {
- continue;
- }
- if metadata.is_dir() {
- walk(root, &path, options, out)?;
- continue;
- }
- if !metadata.is_file() {
- continue;
- }
- let rel = path
- .strip_prefix(root)
- .map_err(|_| CorpusError::InvalidPath(path.display().to_string()))?;
- #[cfg(unix)]
- let rel_bytes = rel.as_os_str().as_bytes();
- #[cfg(not(unix))]
- let rel_bytes = rel
- .to_str()
- .ok_or_else(|| CorpusError::InvalidPath(path.display().to_string()))?
- .as_bytes();
- let normalized = normalize_relative(rel_bytes, PathPolicy::HostCompatible)
- .map_err(|_| CorpusError::InvalidPath(path.display().to_string()))?;
- let bytes = fs::read(&path).map_err(|source| CorpusError::Io {
- path: path.clone(),
- source,
- })?;
- out.push(ManifestEntry {
- path: normalized.display_lossy().to_string(),
- host_rel_path: rel.to_path_buf(),
- size: metadata.len(),
- hash: sha256(&bytes),
- });
- }
- Ok(())
-}
-
-fn classify(root: &Path, files: &[ManifestEntry]) -> CorpusKind {
- let name = root
- .file_name()
- .and_then(|v| v.to_str())
- .unwrap_or_default()
- .to_ascii_uppercase();
- if name == "IS" {
- CorpusKind::Part1
- } else if name == "IS2" {
- CorpusKind::Part2
- } else if files
- .iter()
- .any(|f| f.path.eq_ignore_ascii_case("iron_3d.exe"))
- {
- CorpusKind::Part1
- } else {
- CorpusKind::Unknown
- }
-}
-
-fn detect_casefold_collisions(files: &[ManifestEntry]) -> Vec<Vec<String>> {
- let mut grouped: BTreeMap<Vec<u8>, BTreeSet<String>> = BTreeMap::new();
- for file in files {
- grouped
- .entry(ascii_lookup_key(path_identity_bytes(&file.host_rel_path)).0)
- .or_default()
- .insert(file.path.clone());
- }
- grouped
- .into_values()
- .filter(|paths| paths.len() > 1)
- .map(|paths| paths.into_iter().collect())
- .collect()
-}
-
-/// Builds aggregate report.
-///
-/// # Errors
-///
-/// Returns [`CorpusError`] when the aggregate report cannot be constructed.
-/// Per-file inspection failures are represented in [`CorpusReport::records`]
-/// and counted in [`CorpusReport::failures`].
-pub fn report(root: &Path, manifest: &CorpusManifest) -> Result<CorpusReport, CorpusError> {
- let mut metrics = empty_report_metrics();
- let mut records = Vec::with_capacity(manifest.files.len());
- let mut failures = 0usize;
-
- for entry in &manifest.files {
- let record = inspect_report_file(root, entry, &mut metrics);
- if record.status == CorpusFileStatus::Error {
- failures = failures.saturating_add(1);
- }
- records.push(record);
- }
-
- Ok(CorpusReport {
- schema: 1,
- kind: manifest.kind,
- files: manifest.files.len(),
- bytes: manifest.files.iter().map(|f| f.size).sum(),
- metrics,
- casefold_collisions: manifest.casefold_collisions.len(),
- fingerprint: fingerprint(manifest),
- records,
- failures,
- })
-}
-
-fn empty_report_metrics() -> BTreeMap<String, u64> {
- let mut metrics = BTreeMap::new();
- metrics.insert("nres_files".to_string(), 0);
- metrics.insert("nres_entries".to_string(), 0);
- metrics.insert("rsli_files".to_string(), 0);
- metrics.insert("tma_files".to_string(), 0);
- metrics.insert("land_msh_files".to_string(), 0);
- metrics.insert("land_map_files".to_string(), 0);
- metrics.insert("unit_dat_files".to_string(), 0);
- metrics.insert("msh_entries".to_string(), 0);
- metrics.insert("mat0_entries".to_string(), 0);
- metrics.insert("texm_entries".to_string(), 0);
- metrics.insert("fxid_entries".to_string(), 0);
- metrics.insert("wear_entries".to_string(), 0);
- metrics
-}
-
-fn inspect_report_file(
- root: &Path,
- entry: &ManifestEntry,
- metrics: &mut BTreeMap<String, u64>,
-) -> CorpusFileRecord {
- let lower = entry.path.to_ascii_lowercase();
- let mut variant = inspect_path_metrics(&lower, metrics);
- let path = root.join(&entry.host_rel_path);
- let bytes = match fs::read(&path) {
- Ok(bytes) => bytes,
- Err(source) => {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(source.to_string()),
- };
- }
- };
- if bytes.starts_with(b"NRes") {
- if variant == "file" {
- variant = "nres".to_string();
- }
- bump(metrics, "nres_files", 1);
- if let Err(message) = inspect_nres_metrics(&bytes, metrics) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
- }
- if variant == "land_msh" {
- if let Err(message) = inspect_land_metrics(&bytes, false) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
- }
- }
- if variant == "land_map" {
- if let Err(message) = inspect_land_metrics(&bytes, true) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
- }
- }
- } else if bytes.starts_with(b"NL") {
- variant = "rsli".to_string();
- bump(metrics, "rsli_files", 1);
- if let Err(message) = inspect_rsli_metrics(&bytes) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
- }
- } else if lower.ends_with("data.tma") {
- if let Err(message) = inspect_tma_metrics(&bytes) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant: "tma".to_string(),
- message: Some(message),
- };
- }
- } else if has_extension(&lower, "dat")
- && (lower.starts_with("units/") || lower.contains("/units/"))
- {
- variant = "unit_dat".to_string();
- if let Err(message) = inspect_unit_dat_metrics(&bytes) {
- return CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Error,
- variant,
- message: Some(message),
- };
- }
- }
- CorpusFileRecord {
- path: entry.path.clone(),
- status: CorpusFileStatus::Ok,
- variant,
- message: None,
- }
-}
-
-fn path_identity_bytes(path: &Path) -> &[u8] {
- #[cfg(unix)]
- {
- path.as_os_str().as_bytes()
- }
- #[cfg(not(unix))]
- {
- path.to_str().unwrap_or_default().as_bytes()
- }
-}
-
-fn inspect_path_metrics(lower: &str, metrics: &mut BTreeMap<String, u64>) -> String {
- let mut variant = "file";
- if lower.ends_with("data.tma") {
- bump(metrics, "tma_files", 1);
- variant = "tma";
- }
- if lower.ends_with("land.msh") {
- bump(metrics, "land_msh_files", 1);
- variant = "land_msh";
- }
- if lower.ends_with("land.map") {
- bump(metrics, "land_map_files", 1);
- variant = "land_map";
- }
- if has_extension(lower, "dat") && (lower.starts_with("units/") || lower.contains("/units/")) {
- bump(metrics, "unit_dat_files", 1);
- variant = "unit_dat";
- }
- variant.to_string()
-}
-
-fn inspect_nres_metrics(bytes: &[u8], metrics: &mut BTreeMap<String, u64>) -> Result<(), String> {
- let document = inspect_nres_document(bytes)?;
- bump(metrics, "nres_entries", document.entries().len() as u64);
- for entry in document.entries() {
- let name = String::from_utf8_lossy(entry.name_bytes()).to_ascii_lowercase();
- if has_extension(&name, "msh") {
- bump(metrics, "msh_entries", 1);
- validate_nres_msh_payload(&document, entry)?;
- }
- match entry.meta().type_id {
- MAT0_KIND => {
- bump(metrics, "mat0_entries", 1);
- validate_nres_mat0_payload(&document, entry)?;
- }
- TEXM_KIND => {
- bump(metrics, "texm_entries", 1);
- validate_nres_texm_payload(&document, entry)?;
- }
- FXID_KIND => {
- bump(metrics, "fxid_entries", 1);
- validate_nres_fxid_payload(&document, entry)?;
- }
- WEAR_KIND => {
- bump(metrics, "wear_entries", 1);
- validate_nres_wear_payload(&document, entry)?;
- }
- _ => {}
- }
- }
- Ok(())
-}
-
-fn validate_nres_msh_payload(
- document: &NresDocument,
- entry: &fparkan_nres::NresEntry,
-) -> Result<(), String> {
- let payload = document
- .payload(entry.id())
- .map_err(|err| err.to_string())?;
- let nested = fparkan_nres::decode(
- Arc::from(payload.to_vec().into_boxed_slice()),
- fparkan_nres::ReadProfile::Compatible,
- )
- .map_err(|err| err.to_string())?;
- let model = decode_msh(&nested).map_err(|err| err.to_string())?;
- validate_msh(&model).map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn validate_nres_mat0_payload(
- document: &NresDocument,
- entry: &fparkan_nres::NresEntry,
-) -> Result<(), String> {
- let payload = document
- .payload(entry.id())
- .map_err(|err| err.to_string())?;
- decode_mat0(payload, entry.meta().attr2).map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn validate_nres_wear_payload(
- document: &NresDocument,
- entry: &fparkan_nres::NresEntry,
-) -> Result<(), String> {
- let payload = document
- .payload(entry.id())
- .map_err(|err| err.to_string())?;
- decode_wear(payload).map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn validate_nres_texm_payload(
- document: &NresDocument,
- entry: &fparkan_nres::NresEntry,
-) -> Result<(), String> {
- let payload = document
- .payload(entry.id())
- .map_err(|err| err.to_string())?;
- decode_texm(Arc::from(payload.to_vec().into_boxed_slice())).map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn validate_nres_fxid_payload(
- document: &NresDocument,
- entry: &fparkan_nres::NresEntry,
-) -> Result<(), String> {
- let payload = document
- .payload(entry.id())
- .map_err(|err| err.to_string())?;
- decode_fxid(Arc::from(payload.to_vec().into_boxed_slice())).map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn inspect_rsli_metrics(bytes: &[u8]) -> Result<(), String> {
- let _ = decode_rsli(
- Arc::from(bytes.to_vec().into_boxed_slice()),
- ReadProfile::Compatible,
- )
- .map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn inspect_tma_metrics(bytes: &[u8]) -> Result<(), String> {
- let _ = decode_tma(
- Arc::from(bytes.to_vec().into_boxed_slice()),
- TmaProfile::Strict,
- )
- .map_err(|err| err.to_string())?;
- Ok(())
-}
-
-fn inspect_unit_dat_metrics(bytes: &[u8]) -> Result<(), String> {
- if decode_unit_dat(bytes).is_err() && decode_unit_dat_binding(bytes).is_err() {
- return Err("failed to parse unit.dat payload as unit or binding format".to_string());
- }
- Ok(())
-}
-
-fn inspect_land_metrics(bytes: &[u8], is_map: bool) -> Result<(), String> {
- let document = inspect_nres_document(bytes)?;
- if is_map {
- decode_land_map(&document).map_err(|err| err.to_string())?;
- } else {
- decode_land_msh(&document).map_err(|err| err.to_string())?;
- }
- Ok(())
-}
-
-fn inspect_nres_document(bytes: &[u8]) -> Result<NresDocument, String> {
- fparkan_nres::decode(
- Arc::from(bytes.to_vec().into_boxed_slice()),
- fparkan_nres::ReadProfile::Compatible,
- )
- .map_err(|err| err.to_string())
-}
-
-fn bump(metrics: &mut BTreeMap<String, u64>, key: &str, delta: u64) {
- if let Some(value) = metrics.get_mut(key) {
- *value = value.saturating_add(delta);
- }
-}
-
-fn has_extension(path: &str, expected: &str) -> bool {
- Path::new(path)
- .extension()
- .is_some_and(|extension| extension.eq_ignore_ascii_case(expected))
-}
-
-/// Computes stable manifest fingerprint.
-#[must_use]
-pub fn fingerprint(manifest: &CorpusManifest) -> Sha256Digest {
- let mut bytes = Vec::new();
- for file in &manifest.files {
- bytes.extend_from_slice(file.path.as_bytes());
- bytes.push(0);
- bytes.extend_from_slice(&file.size.to_le_bytes());
- bytes.extend_from_slice(&file.hash);
- }
- sha256(&bytes)
-}
-
-/// Writes report atomically.
-///
-/// # Errors
-///
-/// Returns [`CorpusError`] if the parent directory, temporary file, write, or
-/// final rename operation fails.
-pub fn write_report_atomic(path: &Path, report: &CorpusReport) -> Result<(), CorpusError> {
- let tmp = path.with_extension("tmp");
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent).map_err(|source| CorpusError::Io {
- path: parent.to_path_buf(),
- source,
- })?;
- }
- let mut file = fs::File::create(&tmp).map_err(|source| CorpusError::Io {
- path: tmp.clone(),
- source,
- })?;
- file.write_all(render_report_json(report).as_bytes())
- .map_err(|source| CorpusError::Io {
- path: tmp.clone(),
- source,
- })?;
- file.sync_all().map_err(|source| CorpusError::Io {
- path: tmp.clone(),
- source,
- })?;
- fs::rename(&tmp, path).map_err(|source| CorpusError::Io {
- path: path.to_path_buf(),
- source,
- })?;
- Ok(())
-}
-
-/// Renders report JSON.
-#[must_use]
-pub fn render_report_json(report: &CorpusReport) -> String {
- let mut out = format!(
- "{{\"schema_version\":\"fparkan-corpus-report-v1\",\"schema\":{},\"kind\":\"{:?}\",\"files\":{},\"bytes\":{},\"casefold_collisions\":{},\"fingerprint\":\"{}\",\"failures\":{},\"record_count\":{},\"metrics\":{{",
- report.schema,
- report.kind,
- report.files,
- report.bytes,
- report.casefold_collisions,
- sha256_hex(&report.fingerprint),
- report.failures,
- report.records.len()
- );
- for (idx, (key, value)) in report.metrics.iter().enumerate() {
- if idx > 0 {
- out.push(',');
- }
- out.push('"');
- out.push_str(key);
- out.push_str("\":");
- out.push_str(&value.to_string());
- }
- out.push_str("}}");
- out.push('}');
- out
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use fparkan_path::join_under;
- use std::time::{SystemTime, UNIX_EPOCH};
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn report_for_testdata_roots() {
- let root = licensed_root("IS");
- let manifest = discover(&root, DiscoverOptions::default()).expect("manifest");
- let report = report(&root, &manifest).expect("report");
- assert!(report.files > 0);
- assert!(report.metrics["nres_files"] > 0);
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn licensed_part1_manifest_profile_and_counts_match_baseline() {
- let root = testdata_root("IS");
- let manifest = discover(&root, DiscoverOptions::default()).expect("part 1 manifest");
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(manifest.kind, CorpusKind::Part1);
- assert_eq!(report.files, 1_017);
- assert_eq!(report.metrics["nres_files"], 120);
- assert_eq!(report.metrics["rsli_files"], 2);
- assert_eq!(report.metrics["tma_files"], 29);
- assert_eq!(report.metrics["land_msh_files"], 33);
- assert_eq!(report.metrics["land_map_files"], 33);
- assert_eq!(report.metrics["unit_dat_files"], 425);
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn licensed_part2_manifest_profile_and_counts_match_baseline() {
- let root = testdata_root("IS2");
- let manifest = discover(&root, DiscoverOptions::default()).expect("part 2 manifest");
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(manifest.kind, CorpusKind::Part2);
- assert_eq!(report.files, 1_302);
- assert_eq!(report.metrics["nres_files"], 134);
- assert_eq!(report.metrics["rsli_files"], 2);
- assert_eq!(report.metrics["tma_files"], 31);
- assert_eq!(report.metrics["land_msh_files"], 32);
- assert_eq!(report.metrics["land_map_files"], 32);
- assert_eq!(report.metrics["unit_dat_files"], 676);
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn licensed_part1_has_no_casefold_relative_path_collisions() {
- let root = testdata_root("IS");
- let manifest = discover(&root, DiscoverOptions::default()).expect("part 1 manifest");
-
- assert!(manifest.casefold_collisions.is_empty());
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn licensed_part2_has_no_casefold_relative_path_collisions() {
- let root = testdata_root("IS2");
- let manifest = discover(&root, DiscoverOptions::default()).expect("part 2 manifest");
-
- assert!(manifest.casefold_collisions.is_empty());
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn licensed_part1_paths_stay_under_root() {
- assert_discovered_paths_stay_under_root("IS");
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
- fn licensed_part2_paths_stay_under_root() {
- assert_discovered_paths_stay_under_root("IS2");
- }
-
- #[test]
- fn report_json_contains_metrics_and_hashes_not_paths_or_payloads() {
- let manifest = CorpusManifest {
- kind: CorpusKind::Part1,
- files: vec![manifest_entry("secret/payload.bin", 4, sha256(b"DATA"))],
- casefold_collisions: Vec::new(),
- };
- let report = report(Path::new("."), &manifest).expect("report");
- let json = render_report_json(&report);
-
- assert!(json.contains("\"schema_version\":\"fparkan-corpus-report-v1\""));
- assert!(json.contains("\"fingerprint\":"));
- assert!(json.contains("\"failures\":1"));
- assert!(json.contains("\"record_count\":1"));
- assert!(json.contains("\"metrics\":"));
- assert!(!json.contains("secret/payload.bin"));
- assert!(!json.contains("DATA"));
- }
-
- #[test]
- fn report_records_missing_manifest_files_as_failures() {
- let root = temp_dir("report-missing");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry("missing.lib", 1, sha256(b"missing"))],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records.len(), 1);
- assert_eq!(report.records[0].path, "missing.lib");
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_records_malformed_nres_as_failure() {
- let root = temp_dir("report-bad-nres");
- fs::write(root.join("bad.lib"), b"NRes").expect("bad nres");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry("bad.lib", 4, sha256(b"NRes"))],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- assert_eq!(report.records[0].variant, "nres");
- assert!(report.records[0]
- .message
- .as_deref()
- .is_some_and(|message| message.contains("NRes")));
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_uses_production_nres_parser_for_entry_metrics() {
- let root = temp_dir("report-nres");
- let archive = build_nres(&[
- TestNresEntry {
- name: "mesh.msh",
- type_id: 0,
- payload: b"mesh",
- },
- TestNresEntry {
- name: "mat.bin",
- type_id: 0x3054_414D,
- payload: b"mat0",
- },
- TestNresEntry {
- name: "texture.bin",
- type_id: 0x6D78_6554,
- payload: b"texm",
- },
- ]);
- fs::write(root.join("archive.lib"), &archive).expect("archive");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry(
- "archive.lib",
- u64::try_from(archive.len()).expect("archive size"),
- sha256(&archive),
- )],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records.len(), 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- assert_eq!(report.records[0].variant, "nres");
- assert_eq!(report.metrics["nres_files"], 1);
- assert_eq!(report.metrics["nres_entries"], 3);
- assert_eq!(report.metrics["msh_entries"], 1);
- assert_eq!(report.metrics["mat0_entries"], 0);
- assert_eq!(report.metrics["texm_entries"], 0);
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_land_map_paths_use_production_land_parser() {
- let root = temp_dir("report-land-map");
- fs::create_dir_all(root.join("WORLD/MAP")).expect("land map dir");
- fs::write(root.join("WORLD/MAP/land.map"), build_nres(&[])).expect("land map");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry(
- "WORLD/MAP/land.map",
- 16,
- sha256(b"land.map"),
- )],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- assert_eq!(report.records[0].variant, "land_map");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_land_msh_paths_use_production_land_parser() {
- let root = temp_dir("report-land-msh");
- fs::create_dir_all(root.join("WORLD/MAP")).expect("land msh dir");
- fs::write(root.join("WORLD/MAP/land.msh"), build_nres(&[])).expect("land msh");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry(
- "WORLD/MAP/land.msh",
- 16,
- sha256(b"land.msh"),
- )],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- assert_eq!(report.records[0].variant, "land_msh");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_tma_paths_use_production_tma_parser() {
- let root = temp_dir("report-tma");
- fs::create_dir_all(root.join("MISSIONS/test")).expect("tma dir");
- fs::write(root.join("MISSIONS/test/data.tma"), b"malformed tma").expect("tma");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry(
- "MISSIONS/test/data.tma",
- 12,
- sha256(b"malformed tma"),
- )],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- assert_eq!(report.records[0].variant, "tma");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_unit_dat_paths_use_production_unit_parser() {
- let root = temp_dir("report-unit");
- fs::create_dir_all(root.join("units")).expect("unit dir");
- fs::write(root.join("units/unit.dat"), vec![0u8; 120]).expect("unit");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry("units/unit.dat", 120, sha256(&[0u8; 120]))],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 0);
- assert_eq!(report.records[0].status, CorpusFileStatus::Ok);
- assert_eq!(report.records[0].variant, "unit_dat");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn report_rsli_paths_use_production_rsli_parser() {
- let root = temp_dir("report-rsli");
- fs::write(root.join("patch.nl"), b"NL malformed").expect("rsli");
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry("patch.nl", 12, sha256(b"NL malformed"))],
- casefold_collisions: Vec::new(),
- };
-
- let report = report(&root, &manifest).expect("report");
-
- assert_eq!(report.failures, 1);
- assert_eq!(report.records[0].status, CorpusFileStatus::Error);
- assert_eq!(report.records[0].variant, "rsli");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn deterministic_traversal_is_creation_order_independent() {
- let first = temp_dir("order-first");
- let second = temp_dir("order-second");
- fs::create_dir_all(first.join("nested")).expect("first nested");
- fs::create_dir_all(second.join("nested")).expect("second nested");
-
- fs::write(first.join("b.bin"), b"b").expect("first b");
- fs::write(first.join("nested").join("a.bin"), b"a").expect("first a");
- fs::write(second.join("nested").join("a.bin"), b"a").expect("second a");
- fs::write(second.join("b.bin"), b"b").expect("second b");
-
- let first_manifest = discover(&first, DiscoverOptions::default()).expect("first manifest");
- let second_manifest =
- discover(&second, DiscoverOptions::default()).expect("second manifest");
-
- assert_eq!(first_manifest.files, second_manifest.files);
- let _ = fs::remove_dir_all(first);
- let _ = fs::remove_dir_all(second);
- }
-
- #[cfg(unix)]
- #[test]
- fn unreadable_directory_produces_error() {
- use std::os::unix::fs::PermissionsExt;
-
- let root = temp_dir("unreadable");
- let child = root.join("locked");
- fs::create_dir_all(&child).expect("locked dir");
- fs::set_permissions(&child, fs::Permissions::from_mode(0o000)).expect("lock dir");
-
- let result = discover(&root, DiscoverOptions::default());
-
- fs::set_permissions(&child, fs::Permissions::from_mode(0o700)).expect("unlock dir");
- let _ = fs::remove_dir_all(root);
- assert!(matches!(result, Err(CorpusError::Io { path, .. }) if path.ends_with("locked")));
- }
-
- #[cfg(unix)]
- #[test]
- fn symlink_loop_is_not_traversed_by_default() {
- use std::os::unix::fs::symlink;
-
- let root = temp_dir("symlink-loop");
- fs::write(root.join("real.bin"), b"real").expect("real file");
- symlink(&root, root.join("loop")).expect("loop symlink");
-
- let manifest = discover(&root, DiscoverOptions::default()).expect("manifest");
-
- assert_eq!(manifest.files.len(), 1);
- assert_eq!(manifest.files[0].path, "real.bin");
- let _ = fs::remove_dir_all(root);
- }
-
- #[test]
- fn casefold_collisions_are_registered() {
- let manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![
- manifest_entry("Textures/Foo.TEX", 1, sha256(b"first")),
- manifest_entry("textures/foo.tex", 1, sha256(b"second")),
- ],
- casefold_collisions: Vec::new(),
- };
-
- let collisions = detect_casefold_collisions(&manifest.files);
-
- assert_eq!(
- collisions,
- vec![vec![
- "Textures/Foo.TEX".to_string(),
- "textures/foo.tex".to_string()
- ]]
- );
- }
-
- #[test]
- fn fingerprint_changes() {
- let mut manifest = CorpusManifest {
- kind: CorpusKind::Unknown,
- files: vec![manifest_entry("a", 1, sha256(b"before"))],
- casefold_collisions: Vec::new(),
- };
- let a = fingerprint(&manifest);
- manifest.files[0].hash = sha256(b"after");
- assert_ne!(a, fingerprint(&manifest));
- }
-
- #[test]
- fn atomic_report_write() {
- let tmp = std::env::temp_dir().join(format!(
- "fparkan-report-{}.json",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("clock")
- .as_nanos()
- ));
- let report = CorpusReport {
- schema: 1,
- kind: CorpusKind::Unknown,
- files: 0,
- bytes: 0,
- metrics: BTreeMap::new(),
- casefold_collisions: 0,
- fingerprint: sha256(b"empty-report"),
- records: Vec::new(),
- failures: 0,
- };
- write_report_atomic(&tmp, &report).expect("write");
- assert!(tmp.is_file());
- let _ = fs::remove_file(tmp);
- }
-
- #[cfg(unix)]
- #[test]
- fn discover_supports_non_utf8_host_paths() {
- use std::ffi::OsString;
- use std::os::unix::ffi::OsStringExt;
-
- let root = temp_dir("non-utf8");
- let file_name = OsString::from_vec(vec![0xFF, b'.', b'b', b'i', b'n']);
- let file_path = root.join(&file_name);
- if let Err(err) = fs::write(&file_path, b"raw") {
- assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
- let _ = fs::remove_dir_all(root);
- return;
- }
-
- let manifest = discover(&root, DiscoverOptions::default()).expect("manifest");
-
- assert_eq!(manifest.files.len(), 1);
- assert_eq!(manifest.files[0].path, "\u{FFFD}.bin");
- assert_eq!(manifest.files[0].host_rel_path, PathBuf::from(&file_name));
- let _ = fs::remove_dir_all(root);
- }
-
- struct TestNresEntry<'a> {
- name: &'a str,
- type_id: u32,
- payload: &'a [u8],
- }
-
- fn build_nres(entries: &[TestNresEntry<'_>]) -> Vec<u8> {
- let mut out = vec![0; 16];
- let mut offsets = Vec::with_capacity(entries.len());
- for entry in entries {
- offsets.push(u32::try_from(out.len()).expect("offset"));
- out.extend_from_slice(entry.payload);
- let padding = (8 - (out.len() % 8)) % 8;
- out.resize(out.len() + padding, 0);
- }
- let mut order: Vec<usize> = (0..entries.len()).collect();
- order.sort_by(|left, right| {
- entries[*left]
- .name
- .as_bytes()
- .cmp(entries[*right].name.as_bytes())
- });
- for (index, entry) in entries.iter().enumerate() {
- push_u32(&mut out, entry.type_id);
- push_u32(&mut out, 0);
- push_u32(&mut out, 0);
- push_u32(
- &mut out,
- u32::try_from(entry.payload.len()).expect("payload size"),
- );
- push_u32(&mut out, 0);
- let mut name = [0; 36];
- let name_bytes = entry.name.as_bytes();
- name[..name_bytes.len()].copy_from_slice(name_bytes);
- out.extend_from_slice(&name);
- push_u32(&mut out, offsets[index]);
- push_u32(&mut out, u32::try_from(order[index]).expect("sort index"));
- }
- out[0..4].copy_from_slice(b"NRes");
- out[4..8].copy_from_slice(&0x100_u32.to_le_bytes());
- out[8..12].copy_from_slice(&u32::try_from(entries.len()).expect("count").to_le_bytes());
- let total_size = u32::try_from(out.len()).expect("total size");
- out[12..16].copy_from_slice(&total_size.to_le_bytes());
- out
- }
-
- fn manifest_entry(path: &str, size: u64, hash: Sha256Digest) -> ManifestEntry {
- ManifestEntry {
- path: path.to_string(),
- host_rel_path: PathBuf::from(path),
- size,
- hash,
- }
- }
-
- fn push_u32(out: &mut Vec<u8>, value: u32) {
- out.extend_from_slice(&value.to_le_bytes());
- }
-
- fn temp_dir(name: &str) -> PathBuf {
- let path = std::env::temp_dir().join(format!(
- "fparkan-corpus-{name}-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("clock")
- .as_nanos()
- ));
- fs::create_dir_all(&path).expect("temp dir");
- path
- }
-
- fn testdata_root(part: &str) -> PathBuf {
- licensed_root(part)
- }
-
- fn licensed_root(part: &str) -> PathBuf {
- let variable = match part {
- "IS" => "FPARKAN_CORPUS_PART1_ROOT",
- "IS2" => "FPARKAN_CORPUS_PART2_ROOT",
- _ => panic!("unknown licensed corpus part: {part}"),
- };
- let root = std::env::var_os(variable)
- .map(PathBuf::from)
- .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests"));
- assert!(
- root.is_dir(),
- "licensed corpus root is missing: {}",
- root.display()
- );
- root
- }
-
- fn assert_discovered_paths_stay_under_root(part: &str) {
- let root = testdata_root(part);
- let manifest = discover(&root, DiscoverOptions::default()).expect("licensed manifest");
-
- for entry in &manifest.files {
- let normalized = normalize_relative(entry.path.as_bytes(), PathPolicy::HostCompatible)
- .expect("discovered path should re-normalize");
- let joined = join_under(&root, &normalized).expect("discovered path should join");
- assert!(
- joined.starts_with(&root),
- "discovered path escaped root: {}",
- entry.path
- );
- }
- }
-}
diff --git a/crates/fparkan-diagnostics/Cargo.toml b/crates/fparkan-diagnostics/Cargo.toml
deleted file mode 100644
index 59b8273..0000000
--- a/crates/fparkan-diagnostics/Cargo.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-[package]
-name = "fparkan-diagnostics"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-
-[dependencies]
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-diagnostics/src/lib.rs b/crates/fparkan-diagnostics/src/lib.rs
deleted file mode 100644
index f0228ec..0000000
--- a/crates/fparkan-diagnostics/src/lib.rs
+++ /dev/null
@@ -1,249 +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
- )
-)]
-//! Structured diagnostics shared by `FParkan` crates.
-
-use serde::Serialize;
-
-/// Diagnostic severity.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum Severity {
- /// Informational note.
- Info,
- /// Recoverable warning.
- Warning,
- /// Error for the current operation.
- Error,
- /// Fatal error for the current run.
- Fatal,
-}
-
-/// Evidence level for a contract or interpretation.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum EvidenceStatus {
- /// Described by project documentation.
- Documented,
- /// Verified by synthetic fixtures.
- SyntheticVerified,
- /// Verified against the licensed corpus.
- CorpusVerified,
- /// Verified by runtime capture.
- RuntimeCaptured,
- /// Working hypothesis; not a runtime contract.
- Hypothesis,
-}
-
-/// Operation phase where a diagnostic was produced.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum Phase {
- /// Discovery.
- Discover,
- /// Read.
- Read,
- /// Parse.
- Parse,
- /// Validate.
- Validate,
- /// Resolve.
- Resolve,
- /// Prepare.
- Prepare,
- /// Construct.
- Construct,
- /// Register.
- Register,
- /// Simulate.
- Simulate,
- /// Render.
- Render,
-}
-
-/// Byte span in an input source.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
-pub struct SourceSpan {
- /// Start offset.
- pub offset: u64,
- /// Length in bytes.
- pub length: u64,
-}
-
-/// Stable diagnostic code.
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
-pub struct DiagnosticCode(pub &'static str);
-
-/// Context attached to a diagnostic.
-#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
-pub struct DiagnosticContext {
- /// Phase.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub phase: Option<Phase>,
- /// Redacted or logical path.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub path: Option<String>,
- /// Archive entry name.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub archive_entry: Option<String>,
- /// Object/prototype key.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub object_key: Option<String>,
- /// Input span.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub span: Option<SourceSpan>,
-}
-
-/// Structured diagnostic with cause chain.
-#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
-pub struct Diagnostic {
- /// Stable code.
- pub code: DiagnosticCode,
- /// Severity.
- pub severity: Severity,
- /// Human message.
- pub message: String,
- /// Context.
- pub context: DiagnosticContext,
- /// Causes.
- pub causes: Vec<Diagnostic>,
-}
-
-/// Creates a diagnostic with default error severity.
-#[must_use]
-pub fn diagnostic(code: DiagnosticCode, message: impl Into<String>) -> Diagnostic {
- Diagnostic {
- code,
- severity: Severity::Error,
- message: message.into(),
- context: DiagnosticContext::default(),
- causes: Vec::new(),
- }
-}
-
-impl Diagnostic {
- /// Returns a copy with severity changed.
- #[must_use]
- pub fn with_severity(mut self, severity: Severity) -> Self {
- self.severity = severity;
- self
- }
-
- /// Returns a copy with context changed.
- #[must_use]
- pub fn with_context(mut self, context: DiagnosticContext) -> Self {
- self.context = context;
- self
- }
-
- /// Adds a cause.
- pub fn push_cause(&mut self, cause: Diagnostic) {
- self.causes.push(cause);
- }
-}
-
-/// Renders a compact human-readable diagnostic.
-#[must_use]
-pub fn render_human(diagnostic: &Diagnostic) -> String {
- let mut out = format!(
- "{:?} {}: {}",
- diagnostic.severity, diagnostic.code.0, diagnostic.message
- );
- if let Some(path) = &diagnostic.context.path {
- out.push_str(" [");
- out.push_str(path);
- out.push(']');
- }
- out
-}
-
-/// Renders deterministic JSON using the typed diagnostic schema.
-#[must_use]
-pub fn render_json(diagnostic: &Diagnostic) -> String {
- match serde_json::to_string(diagnostic) {
- Ok(json) => json,
- Err(err) => format!("{{\"error\":\"diagnostic serialization failed: {err}\"}}"),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn json_is_stable() {
- let d = diagnostic(DiagnosticCode("S0-DIAG-001"), "keeps context").with_context(
- DiagnosticContext {
- phase: Some(Phase::Parse),
- ..DiagnosticContext::default()
- },
- );
- assert_eq!(
- render_json(&d),
- "{\"code\":\"S0-DIAG-001\",\"severity\":\"error\",\"message\":\"keeps context\",\"context\":{\"phase\":\"parse\"},\"causes\":[]}"
- );
- }
-
- #[test]
- fn diagnostic_chain_preserves_context() {
- let mut root = diagnostic(DiagnosticCode("ROOT"), "root").with_context(DiagnosticContext {
- phase: Some(Phase::Resolve),
- path: Some("archives/material.lib".to_string()),
- archive_entry: Some("MATERIAL.MAT0".to_string()),
- object_key: Some("unit/tank".to_string()),
- span: Some(SourceSpan {
- offset: 12,
- length: 4,
- }),
- });
- root.push_cause(diagnostic(DiagnosticCode("CAUSE"), "cause").with_context(
- DiagnosticContext {
- phase: Some(Phase::Parse),
- path: Some("archives/material.lib".to_string()),
- span: Some(SourceSpan {
- offset: 16,
- length: 8,
- }),
- ..DiagnosticContext::default()
- },
- ));
-
- let json = render_json(&root);
-
- assert!(json.contains("\"code\":\"ROOT\""));
- assert!(json.contains("\"phase\":\"resolve\""));
- assert!(json.contains("\"path\":\"archives/material.lib\""));
- assert!(json.contains("\"archive_entry\":\"MATERIAL.MAT0\""));
- assert!(json.contains("\"object_key\":\"unit/tank\""));
- assert!(json.contains("\"span\":{\"offset\":12,\"length\":4}"));
- assert!(json.contains("\"code\":\"CAUSE\""));
- assert!(json.contains("\"span\":{\"offset\":16,\"length\":8}"));
- }
-
- #[test]
- fn json_escapes_all_control_characters() {
- let value = diagnostic(DiagnosticCode("S1-H01"), "quote\"\u{0000}tab\tline\r\n");
- let json = render_json(&value);
- assert!(json.contains("\\u0000"));
- assert!(json.contains("\\t"));
- assert!(!json.contains('\t'));
- assert!(!json.contains('\r'));
- }
-}
diff --git a/crates/fparkan-fx/Cargo.toml b/crates/fparkan-fx/Cargo.toml
index a8cf963..89672ec 100644
--- a/crates/fparkan-fx/Cargo.toml
+++ b/crates/fparkan-fx/Cargo.toml
@@ -6,10 +6,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
+fparkan-binary = { path = "../fparkan-binary" }
[dev-dependencies]
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-fx/src/lib.rs b/crates/fparkan-fx/src/lib.rs
index 44b1a59..3fa4aae 100644
--- a/crates/fparkan-fx/src/lib.rs
+++ b/crates/fparkan-fx/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
- )
-)]
//! FXID effect contracts.
//!
//! FXID decoding and command framing are implemented as compatibility
diff --git a/crates/fparkan-inspection/Cargo.toml b/crates/fparkan-inspection/Cargo.toml
deleted file mode 100644
index 240f34d..0000000
--- a/crates/fparkan-inspection/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-name = "fparkan-inspection"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-
-[dependencies]
-fparkan-diagnostics = { path = "../fparkan-diagnostics", version = "0.1.0" }
-fparkan-msh = { path = "../fparkan-msh", version = "0.1.0" }
-fparkan-material = { path = "../fparkan-material", version = "0.1.0" }
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-rsli = { path = "../fparkan-rsli", version = "0.1.0" }
-fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
-fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
-fparkan-texm = { path = "../fparkan-texm", version = "0.1.0" }
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-inspection/src/lib.rs b/crates/fparkan-inspection/src/lib.rs
deleted file mode 100644
index be1f098..0000000
--- a/crates/fparkan-inspection/src/lib.rs
+++ /dev/null
@@ -1,1009 +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
- )
-)]
-//! Shared inspection helpers for format-backed tooling.
-
-use fparkan_diagnostics::{
- diagnostic, render_human, Diagnostic, DiagnosticCode, DiagnosticContext, Phase, SourceSpan,
-};
-use fparkan_material::{decode_wear, resolve_material, MaterialFallback};
-use fparkan_msh::{
- decode_msh, node38_metadata, selected_slot, validate_msh, Group, Lod, ModelAsset, NodeId,
-};
-use fparkan_nres::{decode as decode_nres, NresDocument, ReadProfile};
-use fparkan_path::{normalize_relative, PathPolicy};
-use fparkan_resource::{archive_path, resource_name, CachedResourceRepository, ResourceRepository};
-use fparkan_rsli::decode as decode_rsli;
-use fparkan_terrain_format::{decode_land_map, decode_land_msh, LandMeshDocument};
-use fparkan_texm::decode_texm;
-use fparkan_vfs::{DirectoryVfs, Vfs};
-use std::fs;
-#[cfg(unix)]
-use std::os::unix::ffi::OsStrExt;
-use std::path::Path;
-use std::sync::Arc;
-
-/// Archive inspection variants.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum ArchiveInspection {
- /// `NRes` inspection summary.
- Nres {
- /// Archive entry count.
- entries: usize,
- /// Lookup order validity.
- lookup_order_valid: bool,
- /// Entry samples (subject to request limit).
- sample: Vec<NresEntrySummary>,
- },
- /// `RsLi` inspection summary.
- Rsli {
- /// Archive entry count.
- entries: usize,
- },
- /// Unknown/unsupported archive magic.
- Unsupported,
-}
-
-/// `NRes` entry summary.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct NresEntrySummary {
- /// ASCII/legacy resource name.
- pub name: String,
- /// Entry type identifier.
- pub type_id: u32,
- /// Declared entry payload size.
- pub data_size: u32,
-}
-
-/// Model inspection payload.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct ModelInspection {
- /// Terrain stream/document stream count.
- pub streams: usize,
- /// Node count.
- pub nodes: usize,
- /// Slot count.
- pub slots: usize,
- /// Position count.
- pub positions: usize,
- /// Index count.
- pub indices: usize,
- /// Batch count.
- pub batches: usize,
- /// Original node record stride.
- pub node_stride: usize,
- /// Standard-node metadata in source order, when the model uses `Node38`.
- pub node38: Vec<Node38Inspection>,
- /// Number of decoded type-8 animation keys, when available.
- pub animation_keys: Option<usize>,
- /// Declared type-19 animation frame count, when available.
- pub animation_frame_count: Option<u32>,
-}
-
-/// Inspection view of a standard 38-byte model node.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Node38Inspection {
- /// Source node index.
- pub index: usize,
- /// Opaque source field at byte offset two.
- pub parent_or_link_raw: u16,
- /// Type-19 frame-map offset, or `0xFFFF`.
- pub anim_map_start: u16,
- /// Type-8 fallback key index.
- pub fallback_key: u16,
- /// Whether LOD zero/group zero selects a geometry slot.
- pub has_lod0_group0: bool,
-}
-
-/// Texture inspection payload.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct TextureInspection {
- /// Width.
- pub width: u32,
- /// Height.
- pub height: u32,
- /// Texture format debug text.
- pub format: String,
- /// Mip level count.
- pub mips: usize,
- /// Total page rectangles.
- pub pages: usize,
-}
-
-/// Diffuse TEXM selected through an original WEAR and MAT0 material chain.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct WearMaterialTexture {
- /// Source WEAR archive.
- pub wear_archive: String,
- /// Source WEAR resource.
- pub wear_resource: String,
- /// Positional WEAR selector used by an MSH batch.
- pub material_index: u16,
- /// Resolved MAT0 resource name after the original fallback chain.
- pub material_name: String,
- /// Fallback route used while resolving the MAT0 resource.
- pub material_fallback: MaterialFallback,
- /// Texture name selected from phase zero of the resolved MAT0 document.
- pub texture_name: String,
- /// Decoded RGBA8 mip zero suitable for the Vulkan upload boundary.
- pub image: fparkan_texm::RgbaImage,
-}
-
-/// Compact inspection summary of a WEAR resource stored in an archive.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct WearInspection {
- /// Number of material rows.
- pub materials: usize,
- /// Number of lightmap rows.
- pub lightmaps: usize,
- /// First material resource name, when present.
- pub first_material: Option<String>,
- /// Last material resource name, when present.
- pub last_material: Option<String>,
-}
-
-/// Land map/msh inspection payload.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct MapInspection {
- /// Mapped mesh stream count.
- pub streams: usize,
- /// Slot count.
- pub slots: usize,
- /// Position count.
- pub positions: usize,
- /// Face count.
- pub faces: usize,
- /// Terrain areals.
- pub areals: usize,
- /// Declared areal count from map metadata.
- pub declared_areals: u32,
- /// Map grid width.
- pub grid_width: u32,
- /// Map grid height.
- pub grid_height: u32,
-}
-
-/// Axis-aligned position bounds of a decoded `Land.msh`.
-#[derive(Clone, Debug, PartialEq)]
-pub struct LandMeshBoundsInspection {
- /// Number of source positions covered by the bounds.
- pub positions: usize,
- /// Per-axis inclusive minimum position.
- pub min: [f32; 3],
- /// Per-axis inclusive maximum position.
- pub max: [f32; 3],
-}
-
-/// Supported land file kinds.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum LandFileKind {
- /// `land.msh` payload.
- LandMsh,
- /// `land.map` payload.
- LandMap,
-}
-
-/// Inspects a format archive.
-///
-/// # Errors
-///
-/// Returns a string error when the archive cannot be read or decoded.
-pub fn inspect_archive_file(path: &Path, sample_limit: usize) -> Result<ArchiveInspection, String> {
- inspect_archive_file_diagnostic(path, sample_limit)
- .map_err(|diagnostic| render_human(&diagnostic))
-}
-
-/// Inspects a format archive and returns a structured diagnostic on failure.
-///
-/// # Errors
-///
-/// Returns a [`Diagnostic`] when the archive cannot be read or decoded.
-// Diagnostic is deliberately returned by value as the public structured-error contract.
-#[allow(clippy::result_large_err)]
-pub fn inspect_archive_file_diagnostic(
- path: &Path,
- sample_limit: usize,
-) -> Result<ArchiveInspection, Diagnostic> {
- let parent = path.parent().unwrap_or_else(|| Path::new("."));
- let file_name = path.file_name().ok_or_else(|| {
- diagnostic(
- DiagnosticCode("S1.VFS.PATH"),
- format!("{}: archive path has no file name", path.display()),
- )
- .with_context(DiagnosticContext {
- phase: Some(Phase::Read),
- path: Some(path.display().to_string()),
- ..DiagnosticContext::default()
- })
- })?;
- #[cfg(unix)]
- let raw_name = file_name.as_bytes();
- #[cfg(not(unix))]
- let raw_name = file_name
- .to_str()
- .ok_or_else(|| {
- diagnostic(
- DiagnosticCode("S1.VFS.PATH"),
- format!("{}: archive file name is not valid text", path.display()),
- )
- .with_context(DiagnosticContext {
- phase: Some(Phase::Read),
- path: Some(path.display().to_string()),
- ..DiagnosticContext::default()
- })
- })?
- .as_bytes();
- let normalized = normalize_relative(raw_name, PathPolicy::HostCompatible).map_err(|err| {
- diagnostic(
- DiagnosticCode("S1.VFS.PATH"),
- format!("{}: {err}", path.display()),
- )
- .with_context(DiagnosticContext {
- phase: Some(Phase::Read),
- path: Some(path.display().to_string()),
- ..DiagnosticContext::default()
- })
- })?;
- let vfs = DirectoryVfs::new(parent);
- let bytes = vfs.read(&normalized).map_err(|err| {
- diagnostic(
- DiagnosticCode("S1.VFS.READ"),
- format!("{}: {err}", path.display()),
- )
- .with_context(DiagnosticContext {
- phase: Some(Phase::Read),
- path: Some(path.display().to_string()),
- ..DiagnosticContext::default()
- })
- })?;
- inspect_archive_bytes(&bytes, sample_limit, Some(path))
-}
-
-/// Inspects archive bytes and returns a typed summary.
-// Keeps the internal diagnostic flow aligned with the public structured-error contract.
-#[allow(clippy::result_large_err)]
-fn inspect_archive_bytes(
- bytes: &[u8],
- sample_limit: usize,
- source: Option<&Path>,
-) -> Result<ArchiveInspection, Diagnostic> {
- if bytes.starts_with(b"NRes") {
- let document = decode_nres(
- Arc::from(bytes.to_vec().into_boxed_slice()),
- ReadProfile::Compatible,
- )
- .map_err(|err| {
- archive_parse_diagnostic("S1.NRES.DECODE", source, bytes, err.to_string())
- })?;
- let mut sample = Vec::new();
- for entry in document.entries().iter().take(sample_limit) {
- sample.push(NresEntrySummary {
- name: String::from_utf8_lossy(entry.name_bytes()).to_string(),
- type_id: entry.meta().type_id,
- data_size: entry.meta().data_size,
- });
- }
- Ok(ArchiveInspection::Nres {
- entries: document.entries().len(),
- lookup_order_valid: document.lookup_order_valid(),
- sample,
- })
- } else if bytes.get(0..4) == Some(b"NL\0\x01") {
- let document = decode_rsli(
- Arc::from(bytes.to_vec().into_boxed_slice()),
- fparkan_rsli::ReadProfile::Compatible,
- )
- .map_err(|err| {
- archive_parse_diagnostic("S1.RSLI.DECODE", source, bytes, err.to_string())
- })?;
- Ok(ArchiveInspection::Rsli {
- entries: document.entries().len(),
- })
- } else {
- Err(archive_parse_diagnostic(
- "S1.RESOURCE.UNSUPPORTED_ARCHIVE",
- source,
- bytes,
- "unsupported archive magic".to_string(),
- ))
- }
-}
-
-/// Inspects a model through repository-backed resource lookup.
-///
-/// # Errors
-///
-/// Returns a string error when the resource cannot be resolved or parsed as a
-/// valid model payload.
-pub fn inspect_model_from_root(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<ModelInspection, String> {
- let bytes = read_resource_bytes_diagnostic(root, archive, resource)
- .map_err(|err| render_human(&err))?;
- let document = decode_nres(bytes.clone(), ReadProfile::Compatible).map_err(|err| {
- render_human(&resource_parse_diagnostic(
- "S1.NRES.DECODE",
- archive,
- resource,
- &bytes,
- err.to_string(),
- ))
- })?;
- let msh = decode_msh(&document).map_err(|err| err.to_string())?;
- let validated = validate_msh(&msh).map_err(|err| err.to_string())?;
- let node38 = if validated.node_stride == 38 {
- (0..validated.node_count)
- .filter_map(|index| {
- let node = NodeId(u32::try_from(index).ok()?);
- let metadata = node38_metadata(&validated, node)?;
- Some(Node38Inspection {
- index,
- parent_or_link_raw: metadata.parent_or_link_raw,
- anim_map_start: metadata.anim_map_start,
- fallback_key: metadata.fallback_key,
- has_lod0_group0: selected_slot(&validated, node, Lod(0), Group(0)).is_some(),
- })
- })
- .collect()
- } else {
- Vec::new()
- };
- Ok(ModelInspection {
- streams: msh.streams().len(),
- nodes: validated.node_count,
- slots: validated.slots.len(),
- positions: validated.positions.len(),
- indices: validated.indices.len(),
- batches: validated.batches.len(),
- node_stride: validated.node_stride,
- node38,
- animation_keys: validated
- .animation
- .as_ref()
- .map(|animation| animation.keys.len()),
- animation_frame_count: validated
- .animation
- .as_ref()
- .map(|animation| animation.frame_count),
- })
-}
-
-/// Inspects a WEAR resource through repository-backed lookup.
-///
-/// # Errors
-///
-/// Returns a string error when the resource cannot be resolved or parsed as a
-/// valid WEAR payload.
-pub fn inspect_wear_from_root(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<WearInspection, String> {
- let bytes = read_resource_bytes_diagnostic(root, archive, resource)
- .map_err(|err| render_human(&err))?;
- let wear = decode_wear(&bytes).map_err(|err| err.to_string())?;
- Ok(WearInspection {
- materials: wear.entries.len(),
- lightmaps: wear.lightmaps.len(),
- first_material: wear
- .entries
- .first()
- .map(|entry| String::from_utf8_lossy(&entry.material.0).into_owned()),
- last_material: wear
- .entries
- .last()
- .map(|entry| String::from_utf8_lossy(&entry.material.0).into_owned()),
- })
-}
-
-/// Loads and validates a model resource through repository-backed lookup.
-///
-/// # Errors
-///
-/// Returns a string error when the resource cannot be resolved or parsed as a
-/// valid model payload.
-pub fn load_model_from_root(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<ModelAsset, String> {
- let document = load_model_document_from_root_diagnostic(root, archive, resource)
- .map_err(|err| render_human(&err))?;
- let msh = decode_msh(&document).map_err(|err| err.to_string())?;
- validate_msh(&msh).map_err(|err| err.to_string())
-}
-
-/// Inspects a texture through repository-backed resource lookup.
-///
-/// # Errors
-///
-/// Returns a string error when the resource cannot be resolved or parsed as a
-/// valid texture payload.
-pub fn inspect_texture_from_root(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<TextureInspection, String> {
- let bytes = read_resource_bytes_diagnostic(root, archive, resource)
- .map_err(|err| render_human(&err))?;
- let document = decode_texm(bytes).map_err(|err| err.to_string())?;
- Ok(TextureInspection {
- width: document.width(),
- height: document.height(),
- format: format!("{:?}", document.format()),
- mips: document.mip_count(),
- pages: document.page_rects().len(),
- })
-}
-
-/// Loads a decoded TEXM document through repository-backed lookup.
-///
-/// # Errors
-///
-/// Returns a string error when the resource cannot be resolved or parsed as a
-/// valid TEXM payload.
-pub fn load_texture_from_root(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<fparkan_texm::TexmDocument, String> {
- let bytes = read_resource_bytes_diagnostic(root, archive, resource)
- .map_err(|err| render_human(&err))?;
- decode_texm(bytes).map_err(|err| err.to_string())
-}
-
-/// Loads and decodes TEXM mip 0 as RGBA8 through repository-backed lookup.
-///
-/// # Errors
-///
-/// Returns a string error when the resource cannot be resolved, decoded, or
-/// converted to the shared RGBA8 upload representation.
-pub fn load_texture_mip0_rgba8_from_root(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<fparkan_texm::RgbaImage, String> {
- let document = load_texture_from_root(root, archive, resource)?;
- fparkan_texm::decode_mip_rgba8(&document, 0).map_err(|err| err.to_string())
-}
-
-/// Resolves phase-zero diffuse TEXM through `WEAR → MAT0 → Textures.lib`.
-///
-/// The selector is the positional `Batch20.material_index`, not WEAR's legacy
-/// text id. MAT0 fallback remains owned by `fparkan-material`: exact requested
-/// entry, then `DEFAULT`, then the first material entry.
-///
-/// # Errors
-///
-/// Returns a string error if WEAR/MAT0 resolution, phase selection, or TEXM
-/// decoding fails. An empty phase-zero texture name is an intentional
-/// untextured material and is reported rather than substituted with a texture.
-pub fn load_wear_material_texture_mip0_rgba8_from_root(
- root: &Path,
- wear_archive: &str,
- wear_resource: &str,
- material_index: u16,
-) -> Result<WearMaterialTexture, String> {
- let wear_bytes = read_resource_bytes_diagnostic(root, wear_archive, wear_resource)
- .map_err(|err| render_human(&err))?;
- let wear = decode_wear(&wear_bytes).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 texture_name = String::from_utf8_lossy(&texture.0).into_owned();
- let image = load_texture_mip0_rgba8_from_root(root, "Textures.lib", &texture_name)?;
- Ok(WearMaterialTexture {
- wear_archive: wear_archive.to_string(),
- wear_resource: wear_resource.to_string(),
- material_index,
- material_name: String::from_utf8_lossy(&material.name.0).into_owned(),
- material_fallback: material.fallback,
- texture_name,
- image,
- })
-}
-
-/// Resolves phase-zero diffuse TEXM through a standalone map-local WEAR file.
-///
-/// Map terrain keeps its two WEAR tables adjacent to `Land.msh`, while their
-/// MAT0 and TEXM resources remain in the game root's normal archives. This
-/// preserves that split instead of treating the sidecar as an archive entry.
-///
-/// # Errors
-///
-/// Returns a string error if the standalone WEAR file, global material
-/// resources, or selected diffuse TEXM cannot be decoded.
-pub fn load_standalone_wear_material_texture_mip0_rgba8_from_root(
- root: &Path,
- wear_path: &Path,
- material_index: u16,
-) -> Result<WearMaterialTexture, String> {
- let wear_bytes =
- fs::read(wear_path).map_err(|err| format!("{}: {err}", wear_path.display()))?;
- let wear = decode_wear(&wear_bytes).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 texture_name = String::from_utf8_lossy(&texture.0).into_owned();
- let image = load_texture_mip0_rgba8_from_root(root, "Textures.lib", &texture_name)?;
- Ok(WearMaterialTexture {
- wear_archive: "<standalone>".to_string(),
- wear_resource: wear_path.display().to_string(),
- material_index,
- material_name: String::from_utf8_lossy(&material.name.0).into_owned(),
- material_fallback: material.fallback,
- texture_name,
- image,
- })
-}
-
-/// Inspects a terrain land file by path.
-///
-/// # Errors
-///
-/// Returns a string error when the file cannot be read or parsed as the
-/// requested terrain payload kind.
-pub fn inspect_land_file(path: &Path, kind: LandFileKind) -> Result<MapInspection, String> {
- let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let document = decode_nres(Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible)
- .map_err(|err| err.to_string())?;
- match kind {
- LandFileKind::LandMsh => inspect_land_msh(&document),
- LandFileKind::LandMap => inspect_land_map(&document),
- }
-}
-
-/// Loads and validates a standalone `Land.msh` file.
-///
-/// # Errors
-///
-/// Returns a human-readable error if the file cannot be read, decoded as `NRes`,
-/// or decoded as the specialized terrain mesh format.
-pub fn load_land_msh_from_path(path: &Path) -> Result<LandMeshDocument, String> {
- let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
- let document = decode_nres(Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible)
- .map_err(|err| err.to_string())?;
- decode_land_msh(&document).map_err(|err| err.to_string())
-}
-
-/// Inspects the source-coordinate bounds of a standalone `Land.msh` file.
-///
-/// # Errors
-///
-/// Returns a string error when the file cannot be read, decoded, or contains
-/// no finite source positions.
-pub fn inspect_land_msh_bounds_file(path: &Path) -> Result<LandMeshBoundsInspection, String> {
- let mesh = load_land_msh_from_path(path)?;
- inspect_land_msh_bounds(&mesh)
-}
-
-/// Computes source-coordinate bounds for an already validated `Land.msh`.
-///
-/// # Errors
-///
-/// Returns a string error when the mesh has no finite source positions.
-pub fn inspect_land_msh_bounds(
- mesh: &LandMeshDocument,
-) -> Result<LandMeshBoundsInspection, String> {
- let mut min = [f32::INFINITY; 3];
- let mut max = [f32::NEG_INFINITY; 3];
- for position in &mesh.positions {
- if !position.iter().all(|value| value.is_finite()) {
- return Err("Land.msh contains a non-finite source position".to_string());
- }
- for axis in 0..3 {
- min[axis] = min[axis].min(position[axis]);
- max[axis] = max[axis].max(position[axis]);
- }
- }
- if mesh.positions.is_empty() {
- return Err("Land.msh contains no source positions".to_string());
- }
- Ok(LandMeshBoundsInspection {
- positions: mesh.positions.len(),
- min,
- max,
- })
-}
-
-fn inspect_land_msh(document: &NresDocument) -> Result<MapInspection, String> {
- let land_msh = decode_land_msh(document).map_err(|err| err.to_string())?;
- Ok(MapInspection {
- streams: land_msh.streams.len(),
- slots: land_msh.slots.slots_raw.len(),
- positions: land_msh.positions.len(),
- faces: land_msh.faces.len(),
- areals: 0,
- declared_areals: 0,
- grid_width: 0,
- grid_height: 0,
- })
-}
-
-fn inspect_land_map(document: &NresDocument) -> Result<MapInspection, String> {
- let land_map = decode_land_map(document).map_err(|err| err.to_string())?;
- Ok(MapInspection {
- streams: 0,
- slots: 0,
- positions: 0,
- faces: 0,
- areals: land_map.areals.len(),
- declared_areals: land_map.areal_count,
- grid_width: land_map.grid.cells_x,
- grid_height: land_map.grid.cells_y,
- })
-}
-
-// Preserves the shared structured-error type without changing callers to boxed errors.
-#[allow(clippy::result_large_err)]
-fn read_resource_bytes_diagnostic(
- root: &Path,
- archive: &str,
- name: &str,
-) -> Result<Arc<[u8]>, Diagnostic> {
- let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
- let archive_path = archive_path(archive.as_bytes()).map_err(|err| {
- diagnostic(DiagnosticCode("S1.PATH.ARCHIVE"), err.to_string()).with_context(
- DiagnosticContext {
- phase: Some(Phase::Resolve),
- path: Some(archive.to_string()),
- archive_entry: Some(name.to_string()),
- ..DiagnosticContext::default()
- },
- )
- })?;
- let resource_name = resource_name(name.as_bytes());
- let archive_handle = repository.open_archive(&archive_path).map_err(|err| {
- diagnostic(DiagnosticCode("S1.RESOURCE.OPEN_ARCHIVE"), err.to_string()).with_context(
- DiagnosticContext {
- phase: Some(Phase::Read),
- path: Some(archive.to_string()),
- archive_entry: Some(name.to_string()),
- ..DiagnosticContext::default()
- },
- )
- })?;
- let Some(handle) = repository
- .find(archive_handle, &resource_name)
- .map_err(|err| {
- diagnostic(DiagnosticCode("S1.RESOURCE.FIND"), err.to_string()).with_context(
- DiagnosticContext {
- phase: Some(Phase::Resolve),
- path: Some(archive.to_string()),
- archive_entry: Some(name.to_string()),
- ..DiagnosticContext::default()
- },
- )
- })?
- else {
- return Err(diagnostic(
- DiagnosticCode("S1.RESOURCE.MISSING_ENTRY"),
- format!(
- "resource not found: {archive}/{}",
- String::from_utf8_lossy(name.as_bytes())
- ),
- )
- .with_context(DiagnosticContext {
- phase: Some(Phase::Resolve),
- path: Some(archive.to_string()),
- archive_entry: Some(name.to_string()),
- ..DiagnosticContext::default()
- }));
- };
- let bytes = repository.read(handle).map_err(|err| {
- diagnostic(DiagnosticCode("S1.RESOURCE.READ"), err.to_string()).with_context(
- DiagnosticContext {
- phase: Some(Phase::Read),
- path: Some(archive.to_string()),
- archive_entry: Some(name.to_string()),
- ..DiagnosticContext::default()
- },
- )
- })?;
- Ok(Arc::from(bytes.into_owned()))
-}
-
-// Preserves the shared structured-error type without changing callers to boxed errors.
-#[allow(clippy::result_large_err)]
-fn load_model_document_from_root_diagnostic(
- root: &Path,
- archive: &str,
- resource: &str,
-) -> Result<NresDocument, Diagnostic> {
- let bytes = read_resource_bytes_diagnostic(root, archive, resource)?;
- decode_nres(bytes.clone(), ReadProfile::Compatible).map_err(|err| {
- resource_parse_diagnostic("S1.NRES.DECODE", archive, resource, &bytes, err.to_string())
- })
-}
-
-fn archive_parse_diagnostic(
- code: &'static str,
- source: Option<&Path>,
- bytes: &[u8],
- message: String,
-) -> Diagnostic {
- diagnostic(DiagnosticCode(code), message).with_context(DiagnosticContext {
- phase: Some(Phase::Parse),
- path: source.map(|path| path.display().to_string()),
- span: Some(SourceSpan {
- offset: 0,
- length: u64::try_from(bytes.len().min(4)).unwrap_or(4),
- }),
- ..DiagnosticContext::default()
- })
-}
-
-fn resource_parse_diagnostic(
- code: &'static str,
- archive: &str,
- resource: &str,
- bytes: &[u8],
- message: String,
-) -> Diagnostic {
- diagnostic(DiagnosticCode(code), message).with_context(DiagnosticContext {
- phase: Some(Phase::Parse),
- path: Some(archive.to_string()),
- archive_entry: Some(resource.to_string()),
- span: Some(SourceSpan {
- offset: 0,
- length: u64::try_from(bytes.len().min(4)).unwrap_or(4),
- }),
- ..DiagnosticContext::default()
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use fparkan_terrain_format::TerrainSlotTable;
- use std::io::Write as _;
- use std::path::PathBuf;
-
- const TEST_NRES_HEADER_LEN: usize = 16;
- const TEST_NRES_NAME_LEN: usize = 36;
- const TEST_NRES_VERSION_0100: u32 = 0x100;
-
- #[test]
- fn inspect_rsli_rejects_malformed_archive() {
- let dir = temp_dir("inspect");
- let path = dir.join("test.rsli");
- let mut file = fs::File::create(&path).expect("file");
- file.write_all(b"NL\0\x01").expect("magic");
- drop(file);
-
- let error = inspect_archive_file(&path, 0).expect_err("malformed archive");
- assert!(error.contains("entry table out of bounds"));
- }
-
- #[test]
- fn archive_diagnostic_preserves_source_path_phase_and_span() {
- let dir = temp_dir("inspect-diagnostic");
- let path = dir.join("broken.nres");
- fs::write(&path, b"NRes").expect("broken nres");
-
- let diagnostic = inspect_archive_file_diagnostic(&path, 0).expect_err("diagnostic failure");
-
- assert_eq!(diagnostic.code.0, "S1.NRES.DECODE");
- let expected_path = path.display().to_string();
- assert_eq!(
- diagnostic.context.path.as_deref(),
- Some(expected_path.as_str())
- );
- assert_eq!(diagnostic.context.phase, Some(Phase::Parse));
- assert_eq!(
- diagnostic.context.span,
- Some(SourceSpan {
- offset: 0,
- length: 4
- })
- );
- }
-
- #[test]
- fn nres_entry_summary_fields_are_readable() {
- let dir = temp_dir("inspect-nres");
- let archive = dir.join("test.nres");
- let payload = Vec::from("NRes\x00\x00\x00\x00");
- fs::write(&archive, &payload).expect("nres");
-
- let _ = inspect_archive_file(&archive, 2);
- }
-
- #[test]
- fn model_archive_diagnostic_preserves_archive_entry_context() {
- let dir = temp_dir("inspect-model-diagnostic");
- let archive = dir.join("models.rlb");
- fs::write(&archive, build_single_entry_nres(b"BROKEN.MSH", b"NRes")).expect("archive");
-
- let diagnostic = load_model_document_from_root_diagnostic(&dir, "models.rlb", "BROKEN.MSH")
- .expect_err("nested diagnostic failure");
-
- assert_eq!(diagnostic.code.0, "S1.NRES.DECODE");
- assert_eq!(diagnostic.context.phase, Some(Phase::Parse));
- assert_eq!(diagnostic.context.path.as_deref(), Some("models.rlb"));
- assert_eq!(
- diagnostic.context.archive_entry.as_deref(),
- Some("BROKEN.MSH")
- );
- assert_eq!(
- diagnostic.context.span,
- Some(SourceSpan {
- offset: 0,
- length: 4
- })
- );
- }
-
- #[test]
- fn wear_material_texture_loader_preserves_original_selection_provenance() {
- let dir = temp_dir("wear-material-texture");
- fs::write(
- dir.join("wear.rlb"),
- build_single_entry_nres(b"MODEL.WEA", b"1\n0 MAT\n"),
- )
- .expect("wear archive");
- let mut mat0 = vec![0; 4 + 34];
- mat0[0..2].copy_from_slice(&1_u16.to_le_bytes());
- mat0[22..25].copy_from_slice(b"TEX");
- fs::write(
- dir.join("material.lib"),
- build_single_entry_nres_with_meta(b"MAT", fparkan_material::MAT0_KIND, 0, &mat0),
- )
- .expect("material archive");
- fs::write(
- dir.join("Textures.lib"),
- build_single_entry_nres(b"TEX", &texm_argb8888_pixel([0x40, 0x11, 0x22, 0x33])),
- )
- .expect("texture archive");
-
- let selected =
- load_wear_material_texture_mip0_rgba8_from_root(&dir, "wear.rlb", "MODEL.WEA", 0)
- .expect("resolved material texture");
-
- assert_eq!(selected.material_name, "MAT");
- assert_eq!(selected.material_fallback, MaterialFallback::Exact);
- assert_eq!(selected.texture_name, "TEX");
- assert_eq!(selected.image.rgba8, vec![0x11, 0x22, 0x33, 0x40]);
- }
-
- #[test]
- fn standalone_wear_material_texture_loader_keeps_map_sidecar_separate() {
- let dir = temp_dir("standalone-wear-material-texture");
- let wear_path = dir.join("Land2.wea");
- fs::write(&wear_path, b"1\n0 MAT\n").expect("standalone wear");
- let mut mat0 = vec![0; 4 + 34];
- mat0[0..2].copy_from_slice(&1_u16.to_le_bytes());
- mat0[22..25].copy_from_slice(b"TEX");
- fs::write(
- dir.join("material.lib"),
- build_single_entry_nres_with_meta(b"MAT", fparkan_material::MAT0_KIND, 0, &mat0),
- )
- .expect("material archive");
- fs::write(
- dir.join("Textures.lib"),
- build_single_entry_nres(b"TEX", &texm_argb8888_pixel([0x40, 0x11, 0x22, 0x33])),
- )
- .expect("texture archive");
-
- let selected =
- load_standalone_wear_material_texture_mip0_rgba8_from_root(&dir, &wear_path, 0)
- .expect("resolved material texture");
-
- assert_eq!(selected.wear_archive, "<standalone>");
- assert_eq!(selected.wear_resource, wear_path.display().to_string());
- assert_eq!(selected.texture_name, "TEX");
- assert_eq!(selected.image.rgba8, vec![0x11, 0x22, 0x33, 0x40]);
- }
-
- #[test]
- fn land_mesh_bounds_preserve_each_source_axis() {
- let mesh = LandMeshDocument {
- streams: Vec::new(),
- nodes_raw: Vec::new(),
- slots: TerrainSlotTable {
- header_raw: Vec::new(),
- slots_raw: Vec::new(),
- },
- positions: vec![[4.0, -2.0, 8.0], [-3.0, 6.0, 1.0]],
- normals: Vec::new(),
- uv0: Vec::new(),
- accelerator: Vec::new(),
- aux14: Vec::new(),
- aux18: Vec::new(),
- faces: Vec::new(),
- };
-
- let bounds = inspect_land_msh_bounds(&mesh).expect("bounds");
-
- assert_eq!(bounds.positions, 2);
- assert_eq!(bounds.min, [-3.0, -2.0, 1.0]);
- assert_eq!(bounds.max, [4.0, 6.0, 8.0]);
- }
-
- fn temp_dir(name: &str) -> PathBuf {
- let base = PathBuf::from("/tmp")
- .join("fparkan-inspection-tests")
- .join(name);
- let _ = fs::remove_dir_all(&base);
- fs::create_dir_all(&base).expect("tmp dir");
- base
- }
-
- fn build_single_entry_nres(name: &[u8], payload: &[u8]) -> Vec<u8> {
- build_single_entry_nres_with_meta(name, 1, 0, payload)
- }
-
- fn build_single_entry_nres_with_meta(
- name: &[u8],
- type_id: u32,
- attr2: u32,
- payload: &[u8],
- ) -> Vec<u8> {
- let mut out = vec![0; TEST_NRES_HEADER_LEN];
- let payload_offset = u32::try_from(out.len()).expect("payload offset");
- out.extend_from_slice(payload);
- let padding = (8 - (out.len() % 8)) % 8;
- out.resize(out.len() + padding, 0);
-
- push_u32(&mut out, type_id);
- push_u32(&mut out, 0);
- push_u32(&mut out, attr2);
- push_u32(&mut out, u32::try_from(payload.len()).expect("payload len"));
- push_u32(&mut out, 0);
- let mut raw_name = [0; TEST_NRES_NAME_LEN];
- raw_name[..name.len()].copy_from_slice(name);
- out.extend_from_slice(&raw_name);
- push_u32(&mut out, payload_offset);
- push_u32(&mut out, 0);
-
- out[0..4].copy_from_slice(b"NRes");
- out[4..8].copy_from_slice(&TEST_NRES_VERSION_0100.to_le_bytes());
- out[8..12].copy_from_slice(&1_u32.to_le_bytes());
- let total_size = u32::try_from(out.len()).expect("total size");
- out[12..16].copy_from_slice(&total_size.to_le_bytes());
- out
- }
-
- fn push_u32(out: &mut Vec<u8>, value: u32) {
- out.extend_from_slice(&value.to_le_bytes());
- }
-
- fn texm_argb8888_pixel(pixel: [u8; 4]) -> Vec<u8> {
- let mut out = Vec::new();
- out.extend_from_slice(&0x6D78_6554_u32.to_le_bytes());
- out.extend_from_slice(&1_u32.to_le_bytes());
- out.extend_from_slice(&1_u32.to_le_bytes());
- out.extend_from_slice(&1_u32.to_le_bytes());
- out.extend_from_slice(&0_u32.to_le_bytes());
- out.extend_from_slice(&0_u32.to_le_bytes());
- out.extend_from_slice(&0_u32.to_le_bytes());
- out.extend_from_slice(&8888_u32.to_le_bytes());
- out.extend_from_slice(&pixel);
- out
- }
-}
diff --git a/crates/fparkan-material/Cargo.toml b/crates/fparkan-material/Cargo.toml
index 4a00a73..63174d5 100644
--- a/crates/fparkan-material/Cargo.toml
+++ b/crates/fparkan-material/Cargo.toml
@@ -7,12 +7,9 @@ repository.workspace = true
[dependencies]
encoding_rs = "0.8"
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
+fparkan-path = { path = "../fparkan-path" }
+fparkan-resource = { path = "../fparkan-resource" }
[dev-dependencies]
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-nres = { path = "../fparkan-nres" }
+fparkan-vfs = { path = "../fparkan-vfs" }
diff --git a/crates/fparkan-material/src/lib.rs b/crates/fparkan-material/src/lib.rs
index 7da9952..32d48a1 100644
--- a/crates/fparkan-material/src/lib.rs
+++ b/crates/fparkan-material/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
- )
-)]
//! WEAR/MAT0 material contracts.
use encoding_rs::WINDOWS_1251;
diff --git a/crates/fparkan-mission-format/Cargo.toml b/crates/fparkan-mission-format/Cargo.toml
index 1bfc362..3802966 100644
--- a/crates/fparkan-mission-format/Cargo.toml
+++ b/crates/fparkan-mission-format/Cargo.toml
@@ -7,7 +7,4 @@ repository.workspace = true
[dependencies]
encoding_rs = "0.8"
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
diff --git a/crates/fparkan-mission-format/src/lib.rs b/crates/fparkan-mission-format/src/lib.rs
index 1562256..e796d61 100644
--- a/crates/fparkan-mission-format/src/lib.rs
+++ b/crates/fparkan-mission-format/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
- )
-)]
//! Count-driven mission format primitives.
use encoding_rs::WINDOWS_1251;
diff --git a/crates/fparkan-msh/Cargo.toml b/crates/fparkan-msh/Cargo.toml
index 7df80f9..61ebc2b 100644
--- a/crates/fparkan-msh/Cargo.toml
+++ b/crates/fparkan-msh/Cargo.toml
@@ -7,8 +7,5 @@ repository.workspace = true
[dependencies]
encoding_rs = "0.8"
-fparkan-animation = { path = "../fparkan-animation", version = "0.1.0" }
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-animation = { path = "../fparkan-animation" }
+fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-msh/src/lib.rs b/crates/fparkan-msh/src/lib.rs
index fd222c6..2e38082 100644
--- a/crates/fparkan-msh/src/lib.rs
+++ b/crates/fparkan-msh/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
- )
-)]
//! Stage-3 MSH asset contract.
use encoding_rs::WINDOWS_1251;
@@ -2065,7 +2046,6 @@ mod tests {
out
}
- #[allow(clippy::similar_names, clippy::too_many_arguments)]
fn batch_record(
batch_flags: u16,
material_index: u16,
diff --git a/crates/fparkan-nres/Cargo.toml b/crates/fparkan-nres/Cargo.toml
index 539bf16..ae507af 100644
--- a/crates/fparkan-nres/Cargo.toml
+++ b/crates/fparkan-nres/Cargo.toml
@@ -6,8 +6,5 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
+fparkan-path = { path = "../fparkan-path" }
diff --git a/crates/fparkan-nres/src/lib.rs b/crates/fparkan-nres/src/lib.rs
index e82ca08..56c4370 100644
--- a/crates/fparkan-nres/src/lib.rs
+++ b/crates/fparkan-nres/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
- )
-)]
//! Strict and lossless `NRes` archive support.
use fparkan_binary::{checked_allocation_len, Cursor, DecodeError};
diff --git a/crates/fparkan-path/Cargo.toml b/crates/fparkan-path/Cargo.toml
index 57664b7..66749f2 100644
--- a/crates/fparkan-path/Cargo.toml
+++ b/crates/fparkan-path/Cargo.toml
@@ -6,6 +6,3 @@ license.workspace = true
repository.workspace = true
[dependencies]
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-path/src/lib.rs b/crates/fparkan-path/src/lib.rs
index 8051b0f..efde47d 100644
--- a/crates/fparkan-path/src/lib.rs
+++ b/crates/fparkan-path/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
- )
-)]
//! Legacy path normalization and ASCII lookup semantics.
use std::cmp::Ordering;
diff --git a/crates/fparkan-platform/Cargo.toml b/crates/fparkan-platform/Cargo.toml
index 3f9d593..143ae1c 100644
--- a/crates/fparkan-platform/Cargo.toml
+++ b/crates/fparkan-platform/Cargo.toml
@@ -7,6 +7,3 @@ repository.workspace = true
[dependencies]
raw-window-handle = "0.6"
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-platform/src/lib.rs b/crates/fparkan-platform/src/lib.rs
index c2a3a0f..04ea32d 100644
--- a/crates/fparkan-platform/src/lib.rs
+++ b/crates/fparkan-platform/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
- )
-)]
//! Platform ports for clocks, event sources and window descriptors.
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
diff --git a/crates/fparkan-prototype/Cargo.toml b/crates/fparkan-prototype/Cargo.toml
index 0426c23..fec907c 100644
--- a/crates/fparkan-prototype/Cargo.toml
+++ b/crates/fparkan-prototype/Cargo.toml
@@ -7,13 +7,10 @@ repository.workspace = true
[dependencies]
encoding_rs = "0.8"
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
+fparkan-path = { path = "../fparkan-path" }
+fparkan-resource = { path = "../fparkan-resource" }
+fparkan-vfs = { path = "../fparkan-vfs" }
[dev-dependencies]
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
+fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs
index f4540d4..5023df4 100644
--- a/crates/fparkan-prototype/src/lib.rs
+++ b/crates/fparkan-prototype/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
- )
-)]
//! Prototype registry and unit DAT primitives.
use encoding_rs::WINDOWS_1251;
@@ -824,7 +805,7 @@ pub fn build_prototype_graph(
///
/// This function reports per-root failures in [`PrototypeGraphReport`] instead
/// of returning early.
-#[allow(clippy::too_many_lines)]
+
pub fn build_prototype_graph_report(
repository: &dyn ResourceRepository,
vfs: &dyn Vfs,
diff --git a/crates/fparkan-render/Cargo.toml b/crates/fparkan-render/Cargo.toml
index f28496a..9485140 100644
--- a/crates/fparkan-render/Cargo.toml
+++ b/crates/fparkan-render/Cargo.toml
@@ -6,7 +6,3 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs
index 96ccc2a..7e3c1f8 100644
--- a/crates/fparkan-render/src/lib.rs
+++ b/crates/fparkan-render/src/lib.rs
@@ -1,26 +1,5 @@
#![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
- )
-)]
-//! Backend-neutral render commands and deterministic captures.
-
-use fparkan_world::OriginalObjectId;
+//! Camera mathematics and graphics pipeline state.
/// A 64-byte transform block returned by the original Terrain camera ABI.
///
@@ -217,7 +196,7 @@ impl LegacyD3d7Projection {
// Ngi32 converts these signed viewport dimensions into single-precision
// arithmetic before building the legacy matrix.
- #[allow(clippy::cast_precision_loss)]
+
let aspect = (width as f32) / (height as f32);
let half_fov = self.field_of_view_radians * 0.5;
let cosine = half_fov.cos();
@@ -338,112 +317,6 @@ impl LegacyIron3dEulerTransform {
}
}
-/// Raw camera state observed through the original Terrain camera interface.
-///
-/// Selector 0 supplies the currently active transform and selector 2 supplies
-/// its paired transform. Keeping both lets a later compatibility layer derive
-/// a view/projection convention without re-reading the legacy process.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct RawCameraPose {
- /// Transform returned by selector 0.
- pub selector0: RawCameraTransform,
- /// Transform returned by selector 2.
- pub selector2: RawCameraTransform,
-}
-
-/// Raw projection state observed through the original `CBufferingCamera` ABI.
-///
-/// The five-float context block is intentionally represented as original words:
-/// its indices are observed, but their semantic labels have not yet all been
-/// recovered. The field-of-view value is the type-0 input to `tan(fov / 2)`.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct RawCameraProjection {
- /// Viewport rectangle as `(left, top, right, bottom)`.
- pub viewport: [i32; 4],
- /// Original projection selector.
- pub projection_type: u32,
- /// Original IEEE-754 FOV value in radians.
- pub field_of_view_radians_bits: u32,
- /// Exact five-float context block returned by the primary renderer.
- pub context_words: [u32; 5],
-}
-
-impl RawCameraProjection {
- /// Returns the type-0 FOV input in radians.
- #[must_use]
- pub fn field_of_view_radians(self) -> f32 {
- f32::from_bits(self.field_of_view_radians_bits)
- }
-
- /// Returns the five context values without assigning semantic labels.
- #[must_use]
- pub fn context_values(self) -> [f32; 5] {
- self.context_words.map(f32::from_bits)
- }
-}
-
-/// Immutable camera data visible to command generation.
-#[derive(Clone, Debug, PartialEq)]
-pub struct CameraSnapshot {
- /// View matrix, row-major.
- pub view: [f32; 16],
- /// Projection matrix, row-major.
- pub projection: [f32; 16],
- /// Optional unconverted source-camera state.
- ///
- /// This does not alter rendering until the original matrix and projection
- /// conventions have been recovered; it preserves the ABI boundary for the
- /// runtime adapter and deterministic captures.
- pub raw_pose: Option<RawCameraPose>,
- /// Optional unconverted source-projection state.
- ///
- /// This is retained independently of `projection`, because its legacy
- /// context words have not yet been mapped to a Vulkan clip convention.
- pub raw_projection: Option<RawCameraProjection>,
-}
-
-impl Default for CameraSnapshot {
- fn default() -> Self {
- Self {
- view: identity_transform(),
- projection: identity_transform(),
- raw_pose: None,
- raw_projection: None,
- }
- }
-}
-
-/// Draw id.
-#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
-pub struct DrawId(pub u64);
-
-/// GPU mesh id.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct GpuMeshId(pub u64);
-
-/// GPU material id.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct GpuMaterialId(pub u64);
-
-/// Render phase.
-#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
-pub enum RenderPhase {
- /// Terrain.
- Terrain,
- /// Opaque.
- Opaque,
- /// Alpha test.
- AlphaTest,
- /// Transparent.
- Transparent,
- /// Effects.
- Effects,
- /// Debug.
- Debug,
- /// UI.
- Ui,
-}
-
/// Fixed-function blend behaviour represented without a graphics API type.
///
/// This is a compatibility contract, not yet a decoded MAT0 mapping.
@@ -531,546 +404,6 @@ impl From<LegacyPipelineState> for PipelineKey {
}
}
-/// Index range.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct IndexRange {
- /// Start.
- pub start: u32,
- /// Count.
- pub count: u32,
-}
-
-/// A draw candidate in an immutable render snapshot.
-#[derive(Clone, Debug, PartialEq)]
-pub struct RenderSnapshotDraw {
- /// Draw id.
- pub id: DrawId,
- /// Phase.
- pub phase: RenderPhase,
- /// Object id.
- pub object_id: Option<OriginalObjectId>,
- /// Mesh.
- pub mesh: GpuMeshId,
- /// Material table after WEAR/MAT0 fallback resolution.
- pub material_slots: Vec<GpuMaterialId>,
- /// Batch material index into [`Self::material_slots`].
- pub material_index: u16,
- /// Fixed-function state resolved for this draw.
- pub pipeline_state: LegacyPipelineState,
- /// Node transform matrix, row-major.
- pub transform: [f32; 16],
- /// Index range.
- pub range: IndexRange,
- /// Stable sort order.
- pub stable_order: u64,
-}
-
-/// Immutable backend-neutral render snapshot.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct RenderSnapshot {
- /// Camera data for the frame.
- pub camera: CameraSnapshot,
- /// Draw candidates gathered from world/assets.
- pub draws: Vec<RenderSnapshotDraw>,
-}
-
-/// Command generation profile.
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub struct RenderProfile {
- /// Include UI phase commands when present.
- pub include_ui: bool,
-}
-
-/// Draw command.
-#[derive(Clone, Debug, PartialEq)]
-pub struct DrawCommand {
- /// Draw id.
- pub id: DrawId,
- /// Phase.
- pub phase: RenderPhase,
- /// Object id.
- pub object_id: Option<OriginalObjectId>,
- /// Mesh.
- pub mesh: GpuMeshId,
- /// Material.
- pub material: GpuMaterialId,
- /// Canonical graphics-pipeline variant.
- pub pipeline_key: PipelineKey,
- /// Transform matrix, row-major.
- pub transform: [f32; 16],
- /// Index range.
- pub range: IndexRange,
- /// Stable sort order.
- pub stable_order: u64,
-}
-
-/// Render command.
-#[derive(Clone, Debug, PartialEq)]
-pub enum RenderCommand {
- /// Begin frame.
- BeginFrame,
- /// Draw.
- Draw(DrawCommand),
- /// End frame.
- EndFrame,
-}
-
-/// Render command list.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct RenderCommandList {
- /// Commands.
- pub commands: Vec<RenderCommand>,
-}
-
-/// Optional render command validation limits.
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub struct RenderValidationLimits {
- /// Exclusive upper bound for GPU mesh ids.
- pub mesh_count: Option<u64>,
- /// Exclusive upper bound for index ranges.
- pub index_count: Option<u32>,
-}
-
-/// Frame output.
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct FrameOutput;
-
-/// Render error.
-#[derive(Debug)]
-pub enum RenderError {
- /// Invalid range.
- InvalidRange,
- /// Invalid command stream framing or ordering.
- InvalidCommandStream {
- /// Command index.
- index: usize,
- /// Contextual error message.
- message: &'static str,
- },
- /// Invalid draw range with command-generation context.
- InvalidDrawRange {
- /// Draw id.
- draw_id: DrawId,
- /// Stable sort order.
- stable_order: u64,
- /// Range start.
- start: u32,
- /// Range count.
- count: u32,
- },
- /// Index range arithmetic overflow.
- IndexRangeOverflow {
- /// Draw id.
- draw_id: DrawId,
- /// Range start.
- start: u32,
- /// Range count.
- count: u32,
- },
- /// Index range exceeds validation limits.
- IndexRangeOutOfBounds {
- /// Draw id.
- draw_id: DrawId,
- /// Exclusive index limit.
- index_count: u32,
- /// Range end.
- end: u32,
- },
- /// Mesh id exceeds validation limits.
- MeshOutOfBounds {
- /// Draw id.
- draw_id: DrawId,
- /// Mesh id.
- mesh: GpuMeshId,
- /// Exclusive mesh limit.
- mesh_count: u64,
- },
- /// Draw transform contains a non-finite value.
- NonFiniteTransform {
- /// Draw id.
- draw_id: DrawId,
- /// Matrix element index.
- element: usize,
- },
- /// Draw commands are not ordered by phase, stable order and draw id.
- PhaseOrderViolation {
- /// Draw id.
- draw_id: DrawId,
- /// Previous phase.
- previous: RenderPhase,
- /// Current phase.
- current: RenderPhase,
- },
- /// A batch material index did not resolve through the material table.
- MaterialIndexOutOfBounds {
- /// Draw id.
- draw_id: DrawId,
- /// Requested material index.
- material_index: u16,
- /// Available material slots.
- material_count: usize,
- },
-}
-
-impl std::fmt::Display for RenderError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::InvalidRange => write!(f, "render command contains an empty index range"),
- Self::InvalidCommandStream { index, message } => {
- write!(
- f,
- "render command stream is invalid at command {index}: {message}"
- )
- }
- Self::InvalidDrawRange {
- draw_id,
- stable_order,
- start,
- count,
- } => write!(
- f,
- "draw {} has invalid index range start={} count={} at stable order {}",
- draw_id.0, start, count, stable_order
- ),
- Self::IndexRangeOverflow {
- draw_id,
- start,
- count,
- } => write!(
- f,
- "draw {} index range overflows start={} count={}",
- draw_id.0, start, count
- ),
- Self::IndexRangeOutOfBounds {
- draw_id,
- index_count,
- end,
- } => write!(
- f,
- "draw {} index range ends at {} but mesh has {} indices",
- draw_id.0, end, index_count
- ),
- Self::MeshOutOfBounds {
- draw_id,
- mesh,
- mesh_count,
- } => write!(
- f,
- "draw {} references mesh {} but only {} meshes are available",
- draw_id.0, mesh.0, mesh_count
- ),
- Self::NonFiniteTransform { draw_id, element } => write!(
- f,
- "draw {} has non-finite transform element {}",
- draw_id.0, element
- ),
- Self::PhaseOrderViolation {
- draw_id,
- previous,
- current,
- } => write!(
- f,
- "draw {} phase order regressed from {:?} to {:?}",
- draw_id.0, previous, current
- ),
- Self::MaterialIndexOutOfBounds {
- draw_id,
- material_index,
- material_count,
- } => write!(
- f,
- "draw {} references material index {} but only {} material slots are available",
- draw_id.0, material_index, material_count
- ),
- }
- }
-}
-
-impl std::error::Error for RenderError {}
-
-/// Builds a deterministic command list from an immutable render snapshot.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when a draw has an invalid index range or a material
-/// index that cannot be resolved through its material slot table.
-pub fn build_commands(
- snapshot: &RenderSnapshot,
- profile: RenderProfile,
-) -> Result<RenderCommandList, RenderError> {
- let mut draws = snapshot
- .draws
- .iter()
- .filter(|draw| profile.include_ui || draw.phase != RenderPhase::Ui)
- .collect::<Vec<_>>();
- draws.sort_by_key(|draw| (draw.phase, draw.stable_order, draw.id));
-
- let mut commands = Vec::with_capacity(draws.len() + 2);
- commands.push(RenderCommand::BeginFrame);
- for draw in draws {
- if draw.range.count == 0 {
- return Err(RenderError::InvalidDrawRange {
- draw_id: draw.id,
- stable_order: draw.stable_order,
- start: draw.range.start,
- count: draw.range.count,
- });
- }
- validate_index_range(draw.id, draw.range)?;
- validate_transform(draw.id, &draw.transform)?;
- let material = draw
- .material_slots
- .get(usize::from(draw.material_index))
- .copied()
- .ok_or(RenderError::MaterialIndexOutOfBounds {
- draw_id: draw.id,
- material_index: draw.material_index,
- material_count: draw.material_slots.len(),
- })?;
- commands.push(RenderCommand::Draw(DrawCommand {
- id: draw.id,
- phase: draw.phase,
- object_id: draw.object_id,
- mesh: draw.mesh,
- material,
- pipeline_key: draw.pipeline_state.into(),
- transform: draw.transform,
- range: draw.range,
- stable_order: draw.stable_order,
- }));
- }
- commands.push(RenderCommand::EndFrame);
- Ok(RenderCommandList { commands })
-}
-
-/// Backend port.
-pub trait RenderBackend {
- /// Executes commands.
- ///
- /// # Errors
- ///
- /// Returns [`RenderError`] when the command stream is malformed for the
- /// backend.
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError>;
-}
-
-/// Marker trait for backends that execute draws against a live GPU.
-///
-/// Planning and capture-only backends must not implement this trait.
-pub trait GpuRenderBackend: RenderBackend {}
-
-/// Backend that validates commands and intentionally produces no pixels.
-#[derive(Clone, Debug, Default)]
-pub struct NullBackend;
-
-impl RenderBackend for NullBackend {
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- validate_command_list(commands)?;
- Ok(FrameOutput)
- }
-}
-
-/// Backend that stores deterministic command captures for verification.
-#[derive(Clone, Debug, Default)]
-pub struct RecordingBackend {
- captures: Vec<Vec<u8>>,
-}
-
-impl RecordingBackend {
- /// Returns all captures in submission order.
- #[must_use]
- pub fn captures(&self) -> &[Vec<u8>] {
- &self.captures
- }
-
- /// Returns the most recent capture.
- #[must_use]
- pub fn last_capture(&self) -> Option<&[u8]> {
- self.captures.last().map(Vec::as_slice)
- }
-
- /// Clears stored captures without changing backend behavior.
- pub fn clear(&mut self) {
- self.captures.clear();
- }
-}
-
-impl RenderBackend for RecordingBackend {
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- let capture = canonical_capture(commands)?;
- self.captures.push(capture);
- Ok(FrameOutput)
- }
-}
-
-/// Builds a canonical capture.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when a draw command contains an invalid index range.
-pub fn canonical_capture(commands: &RenderCommandList) -> Result<Vec<u8>, RenderError> {
- validate_command_list(commands)?;
- let mut out = Vec::new();
- for command in &commands.commands {
- match command {
- RenderCommand::BeginFrame => out.extend_from_slice(b"B\n"),
- RenderCommand::EndFrame => out.extend_from_slice(b"E\n"),
- RenderCommand::Draw(draw) => {
- out.extend_from_slice(
- format!(
- "D,{:?},{},{},{},{},{}\n",
- draw.phase,
- draw.id.0,
- draw.mesh.0,
- draw.material.0,
- draw.pipeline_key.packed(),
- draw.stable_order
- )
- .as_bytes(),
- );
- }
- }
- }
- Ok(out)
-}
-
-/// Validates a render command list without backend-specific resource limits.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when framing, ordering or draw data is invalid.
-pub fn validate_command_list(commands: &RenderCommandList) -> Result<(), RenderError> {
- validate_command_list_with_limits(commands, RenderValidationLimits::default())
-}
-
-/// Validates a render command list with optional backend resource limits.
-///
-/// # Errors
-///
-/// Returns [`RenderError`] when framing, ordering, draw data or resource bounds
-/// are invalid.
-pub fn validate_command_list_with_limits(
- commands: &RenderCommandList,
- limits: RenderValidationLimits,
-) -> Result<(), RenderError> {
- let Some(first) = commands.commands.first() else {
- return Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "empty command list",
- });
- };
- if !matches!(first, RenderCommand::BeginFrame) {
- return Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "first command must be BeginFrame",
- });
- }
- if commands.commands.len() < 2 {
- return Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "frame must end with EndFrame",
- });
- }
- let end_index = commands.commands.len() - 1;
- if !matches!(commands.commands[end_index], RenderCommand::EndFrame) {
- return Err(RenderError::InvalidCommandStream {
- index: end_index,
- message: "last command must be EndFrame",
- });
- }
-
- let mut previous_key: Option<(RenderPhase, u64, DrawId)> = None;
- for (index, command) in commands.commands.iter().enumerate() {
- match command {
- RenderCommand::BeginFrame if index == 0 => {}
- RenderCommand::BeginFrame => {
- return Err(RenderError::InvalidCommandStream {
- index,
- message: "nested BeginFrame is not allowed",
- });
- }
- RenderCommand::EndFrame if index == end_index => {}
- RenderCommand::EndFrame => {
- return Err(RenderError::InvalidCommandStream {
- index,
- message: "EndFrame before final command is not allowed",
- });
- }
- RenderCommand::Draw(draw) => {
- validate_draw_command(draw, limits)?;
- let key = (draw.phase, draw.stable_order, draw.id);
- if let Some(previous) = previous_key {
- if key < previous {
- return Err(RenderError::PhaseOrderViolation {
- draw_id: draw.id,
- previous: previous.0,
- current: draw.phase,
- });
- }
- }
- previous_key = Some(key);
- }
- }
- }
- Ok(())
-}
-
-fn validate_draw_command(
- draw: &DrawCommand,
- limits: RenderValidationLimits,
-) -> Result<(), RenderError> {
- if draw.range.count == 0 {
- return Err(RenderError::InvalidRange);
- }
- let end = validate_index_range(draw.id, draw.range)?;
- validate_transform(draw.id, &draw.transform)?;
- if let Some(mesh_count) = limits.mesh_count {
- if draw.mesh.0 >= mesh_count {
- return Err(RenderError::MeshOutOfBounds {
- draw_id: draw.id,
- mesh: draw.mesh,
- mesh_count,
- });
- }
- }
- if let Some(index_count) = limits.index_count {
- if end > index_count {
- return Err(RenderError::IndexRangeOutOfBounds {
- draw_id: draw.id,
- index_count,
- end,
- });
- }
- }
- Ok(())
-}
-
-fn validate_index_range(draw_id: DrawId, range: IndexRange) -> Result<u32, RenderError> {
- range
- .start
- .checked_add(range.count)
- .ok_or(RenderError::IndexRangeOverflow {
- draw_id,
- start: range.start,
- count: range.count,
- })
-}
-
-fn validate_transform(draw_id: DrawId, transform: &[f32; 16]) -> Result<(), RenderError> {
- for (element, value) in transform.iter().enumerate() {
- if !value.is_finite() {
- return Err(RenderError::NonFiniteTransform { draw_id, element });
- }
- }
- Ok(())
-}
-
-fn identity_transform() -> [f32; 16] {
- [
- 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
- ]
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -1098,49 +431,6 @@ mod tests {
}
#[test]
- fn raw_camera_pose_preserves_words_and_extracts_confirmed_translation() {
- let mut active = [0_u32; 16];
- active[0] = 0x7FC0_0001;
- active[3] = 491.562_5_f32.to_bits();
- active[7] = 761.550_8_f32.to_bits();
- active[11] = 7.361_0_f32.to_bits();
- let paired = [0xA5A5_5A5A; 16];
-
- let pose = RawCameraPose {
- selector0: RawCameraTransform { words: active },
- selector2: RawCameraTransform { words: paired },
- };
-
- assert_eq!(pose.selector0.words, active);
- assert_eq!(pose.selector2.words, paired);
- assert_eq!(
- pose.selector0.translation(),
- [491.562_5, 761.550_8, 7.361_0]
- );
- assert_eq!(CameraSnapshot::default().raw_pose, None);
- }
-
- #[test]
- fn raw_camera_projection_preserves_live_context_words_without_labeling_them() {
- let projection = RawCameraProjection {
- viewport: [0, 0, 1024, 768],
- projection_type: 0,
- field_of_view_radians_bits: 1.04_f32.to_bits(),
- context_words: [
- 0.0_f32.to_bits(),
- 0.5_f32.to_bits(),
- 700.0_f32.to_bits(),
- 0.1_f32.to_bits(),
- 0.99_f32.to_bits(),
- ],
- };
-
- assert_eq!(projection.field_of_view_radians(), 1.04);
- assert_eq!(projection.context_values(), [0.0, 0.5, 700.0, 0.1, 0.99]);
- assert_eq!(CameraSnapshot::default().raw_projection, None);
- }
-
- #[test]
fn raw_camera_transform_inverts_only_non_singular_affine_blocks() {
let source = [
0.0, -1.0, 0.0, 433.544_7, 0.948_985, 0.0, 0.315_322, 652.292_5, -0.315_322, 0.0,
@@ -1259,455 +549,4 @@ mod tests {
None
);
}
-
- fn snapshot_draw(
- id: u64,
- phase: RenderPhase,
- material_index: u16,
- stable_order: u64,
- ) -> RenderSnapshotDraw {
- RenderSnapshotDraw {
- id: DrawId(id),
- phase,
- object_id: Some(OriginalObjectId(u32::try_from(id).expect("id fits"))),
- mesh: GpuMeshId(10 + id),
- material_slots: vec![GpuMaterialId(31), GpuMaterialId(37)],
- material_index,
- pipeline_state: LegacyPipelineState::default(),
- transform: identity_transform(),
- range: IndexRange { start: 0, count: 3 },
- stable_order,
- }
- }
-
- #[test]
- fn capture_is_stable() {
- let list = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(1),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: [0.0; 16],
- range: IndexRange { start: 0, count: 3 },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert_eq!(
- canonical_capture(&list).expect("capture"),
- b"B\nD,Opaque,1,2,3,0,4\nE\n"
- );
- }
-
- #[test]
- fn pipeline_key_is_explicit_stable_and_sensitive_to_pipeline_structure() {
- let base = LegacyPipelineState::default();
- assert_eq!(PipelineKey::from(base).packed(), 0);
-
- let variant = LegacyPipelineState {
- blend: LegacyBlendMode::SourceAlpha,
- depth: LegacyDepthMode::TestReadOnly,
- cull: LegacyCullMode::BackFace,
- alpha_test: true,
- };
- assert_eq!(PipelineKey::from(variant).packed(), 0b00_101_101);
- assert_ne!(PipelineKey::from(base), PipelineKey::from(variant));
- }
-
- #[test]
- fn alpha_test_flag_changes_key_without_encoding_material_threshold() {
- let opaque = LegacyPipelineState::default();
- let alpha_test = LegacyPipelineState {
- alpha_test: true,
- ..opaque
- };
- assert_eq!(PipelineKey::from(alpha_test).packed(), 0b10_0000);
- }
-
- #[test]
- fn null_backend_validates_without_capture() {
- let mut backend = NullBackend;
- let invalid = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(1),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: [0.0; 16],
- range: IndexRange { start: 0, count: 0 },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
-
- assert!(matches!(
- backend.execute(&invalid),
- Err(RenderError::InvalidRange)
- ));
- }
-
- #[test]
- fn recording_backend_stores_captures() {
- let mut backend = RecordingBackend::default();
- let list = RenderCommandList {
- commands: vec![RenderCommand::BeginFrame, RenderCommand::EndFrame],
- };
-
- backend.execute(&list).expect("execute");
- backend.execute(&list).expect("execute");
-
- assert_eq!(backend.captures().len(), 2);
- assert_eq!(backend.last_capture(), Some(&b"B\nE\n"[..]));
- backend.clear();
- assert!(backend.captures().is_empty());
- }
-
- #[test]
- fn one_snapshot_draw_produces_one_draw_command() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![snapshot_draw(1, RenderPhase::Opaque, 0, 10)],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
-
- assert!(matches!(commands.commands[0], RenderCommand::BeginFrame));
- assert!(matches!(commands.commands[2], RenderCommand::EndFrame));
- let RenderCommand::Draw(draw) = &commands.commands[1] else {
- panic!("expected draw");
- };
- assert_eq!(draw.id, DrawId(1));
- assert_eq!(draw.mesh, GpuMeshId(11));
- assert_eq!(draw.range, IndexRange { start: 0, count: 3 });
- Ok(())
- }
-
- #[test]
- fn material_index_maps_through_resolved_material_slots() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![snapshot_draw(2, RenderPhase::Opaque, 1, 10)],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
-
- let RenderCommand::Draw(draw) = &commands.commands[1] else {
- panic!("expected draw");
- };
- assert_eq!(draw.material, GpuMaterialId(37));
- Ok(())
- }
-
- #[test]
- fn node_transform_is_retained() -> Result<(), RenderError> {
- let mut draw = snapshot_draw(3, RenderPhase::Opaque, 0, 10);
- draw.transform[3] = 12.5;
- draw.transform[7] = -4.0;
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![draw],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
-
- let RenderCommand::Draw(draw) = &commands.commands[1] else {
- panic!("expected draw");
- };
- assert_eq!(draw.transform[3], 12.5);
- assert_eq!(draw.transform[7], -4.0);
- Ok(())
- }
-
- #[test]
- fn command_order_uses_phase_then_stable_key() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(3, RenderPhase::Transparent, 0, 0),
- snapshot_draw(2, RenderPhase::Opaque, 0, 20),
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- ],
- };
-
- let commands = build_commands(&snapshot, RenderProfile::default())?;
- let capture = canonical_capture(&commands)?;
-
- assert_eq!(
- capture,
- b"B\nD,Opaque,1,11,31,0,10\nD,Opaque,2,12,31,0,20\nD,Transparent,3,13,31,0,0\nE\n"
- );
- Ok(())
- }
-
- #[test]
- fn command_capture_independent_of_snapshot_construction_order() -> Result<(), RenderError> {
- let forward = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- snapshot_draw(2, RenderPhase::Opaque, 1, 20),
- ],
- };
- let reverse = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(2, RenderPhase::Opaque, 1, 20),
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- ],
- };
-
- assert_eq!(
- canonical_capture(&build_commands(&forward, RenderProfile::default())?)?,
- canonical_capture(&build_commands(&reverse, RenderProfile::default())?)?
- );
- Ok(())
- }
-
- #[test]
- fn invalid_range_returns_contextual_error() {
- let mut draw = snapshot_draw(9, RenderPhase::Opaque, 0, 10);
- draw.range = IndexRange { start: 4, count: 0 };
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![draw],
- };
-
- assert!(matches!(
- build_commands(&snapshot, RenderProfile::default()),
- Err(RenderError::InvalidDrawRange {
- draw_id: DrawId(9),
- stable_order: 10,
- start: 4,
- count: 0
- })
- ));
- }
-
- #[test]
- fn command_validation_rejects_bad_frame_framing() {
- let missing_begin = RenderCommandList {
- commands: vec![RenderCommand::EndFrame],
- };
- assert!(matches!(
- validate_command_list(&missing_begin),
- Err(RenderError::InvalidCommandStream {
- index: 0,
- message: "first command must be BeginFrame"
- })
- ));
-
- let nested = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::BeginFrame,
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list(&nested),
- Err(RenderError::InvalidCommandStream {
- index: 1,
- message: "nested BeginFrame is not allowed"
- })
- ));
- }
-
- #[test]
- fn command_validation_rejects_nonfinite_transform_and_range_overflow() {
- let mut draw = snapshot_draw(10, RenderPhase::Opaque, 0, 10);
- draw.transform[5] = f32::NAN;
- let nonfinite = build_commands(
- &RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![draw],
- },
- RenderProfile::default(),
- );
- assert!(matches!(
- nonfinite,
- Err(RenderError::NonFiniteTransform {
- draw_id: DrawId(10),
- element: 5
- })
- ));
-
- let list = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(11),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange {
- start: u32::MAX,
- count: 1,
- },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list(&list),
- Err(RenderError::IndexRangeOverflow {
- draw_id: DrawId(11),
- start: u32::MAX,
- count: 1
- })
- ));
- }
-
- #[test]
- fn command_validation_checks_order_and_resource_bounds() {
- let ordered = build_commands(
- &RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![snapshot_draw(1, RenderPhase::Opaque, 0, 10)],
- },
- RenderProfile::default(),
- )
- .expect("commands");
- assert!(matches!(
- validate_command_list_with_limits(
- &ordered,
- RenderValidationLimits {
- mesh_count: Some(5),
- index_count: Some(16)
- }
- ),
- Err(RenderError::MeshOutOfBounds {
- draw_id: DrawId(1),
- mesh: GpuMeshId(11),
- mesh_count: 5
- })
- ));
-
- let out_of_bounds = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(12),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(2),
- material: GpuMaterialId(3),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange {
- start: 14,
- count: 3,
- },
- stable_order: 4,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list_with_limits(
- &out_of_bounds,
- RenderValidationLimits {
- mesh_count: Some(5),
- index_count: Some(16)
- }
- ),
- Err(RenderError::IndexRangeOutOfBounds {
- draw_id: DrawId(12),
- index_count: 16,
- end: 17
- })
- ));
-
- let unordered = RenderCommandList {
- commands: vec![
- RenderCommand::BeginFrame,
- RenderCommand::Draw(DrawCommand {
- id: DrawId(1),
- phase: RenderPhase::Transparent,
- object_id: None,
- mesh: GpuMeshId(1),
- material: GpuMaterialId(1),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange { start: 0, count: 3 },
- stable_order: 0,
- }),
- RenderCommand::Draw(DrawCommand {
- id: DrawId(2),
- phase: RenderPhase::Opaque,
- object_id: None,
- mesh: GpuMeshId(1),
- material: GpuMaterialId(1),
- pipeline_key: LegacyPipelineState::default().into(),
- transform: identity_transform(),
- range: IndexRange { start: 0, count: 3 },
- stable_order: 0,
- }),
- RenderCommand::EndFrame,
- ],
- };
- assert!(matches!(
- validate_command_list(&unordered),
- Err(RenderError::PhaseOrderViolation {
- draw_id: DrawId(2),
- previous: RenderPhase::Transparent,
- current: RenderPhase::Opaque
- })
- ));
- }
-
- #[test]
- fn render_error_display_is_actionable() {
- assert_eq!(
- RenderError::InvalidDrawRange {
- draw_id: DrawId(9),
- stable_order: 10,
- start: 4,
- count: 0
- }
- .to_string(),
- "draw 9 has invalid index range start=4 count=0 at stable order 10"
- );
- assert_eq!(
- RenderError::MaterialIndexOutOfBounds {
- draw_id: DrawId(7),
- material_index: 3,
- material_count: 2
- }
- .to_string(),
- "draw 7 references material index 3 but only 2 material slots are available"
- );
- }
-
- #[test]
- fn ui_phase_is_excluded_until_requested() -> Result<(), RenderError> {
- let snapshot = RenderSnapshot {
- camera: CameraSnapshot::default(),
- draws: vec![
- snapshot_draw(1, RenderPhase::Opaque, 0, 10),
- snapshot_draw(2, RenderPhase::Ui, 0, 20),
- ],
- };
-
- let default_commands = build_commands(&snapshot, RenderProfile::default())?;
- let ui_commands = build_commands(&snapshot, RenderProfile { include_ui: true })?;
-
- assert_eq!(default_commands.commands.len(), 3);
- assert_eq!(ui_commands.commands.len(), 4);
- Ok(())
- }
}
diff --git a/crates/fparkan-resource/Cargo.toml b/crates/fparkan-resource/Cargo.toml
index 011583e..815bef9 100644
--- a/crates/fparkan-resource/Cargo.toml
+++ b/crates/fparkan-resource/Cargo.toml
@@ -6,11 +6,8 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-rsli = { path = "../fparkan-rsli", version = "0.1.0" }
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
+fparkan-nres = { path = "../fparkan-nres" }
+fparkan-path = { path = "../fparkan-path" }
+fparkan-rsli = { path = "../fparkan-rsli" }
+fparkan-vfs = { path = "../fparkan-vfs" }
diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs
index f996a71..c470501 100644
--- a/crates/fparkan-resource/src/lib.rs
+++ b/crates/fparkan-resource/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
- )
-)]
//! Resource identity and repository ports.
use fparkan_binary::{sha256, Sha256Digest};
diff --git a/crates/fparkan-rsli/Cargo.toml b/crates/fparkan-rsli/Cargo.toml
index 0b72705..2230dc2 100644
--- a/crates/fparkan-rsli/Cargo.toml
+++ b/crates/fparkan-rsli/Cargo.toml
@@ -6,8 +6,5 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
+fparkan-binary = { path = "../fparkan-binary" }
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-rsli/src/lib.rs b/crates/fparkan-rsli/src/lib.rs
index 0bb1498..23b1be2 100644
--- a/crates/fparkan-rsli/src/lib.rs
+++ b/crates/fparkan-rsli/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
- )
-)]
//! Stage-1 `RsLi` archive contract.
use fparkan_binary::DecodeError;
@@ -954,7 +935,6 @@ struct EntryRecord {
effective_offset: usize,
}
-#[allow(clippy::too_many_lines)]
fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliError> {
enforce_limit(
u64::try_from(bytes.len()).map_err(|_| RsliError::IntegerOverflow)?,
diff --git a/crates/fparkan-runtime/Cargo.toml b/crates/fparkan-runtime/Cargo.toml
index 858e5af..ddf09a1 100644
--- a/crates/fparkan-runtime/Cargo.toml
+++ b/crates/fparkan-runtime/Cargo.toml
@@ -6,16 +6,13 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-assets = { path = "../fparkan-assets", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-platform = { path = "../fparkan-platform", version = "0.1.0" }
-fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
-fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
-fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
-fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
-fparkan-script = { path = "../fparkan-script", version = "0.1.0" }
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-assets = { path = "../fparkan-assets" }
+fparkan-path = { path = "../fparkan-path" }
+fparkan-platform = { path = "../fparkan-platform" }
+fparkan-prototype = { path = "../fparkan-prototype" }
+fparkan-render = { path = "../fparkan-render" }
+fparkan-terrain = { path = "../fparkan-terrain" }
+fparkan-resource = { path = "../fparkan-resource" }
+fparkan-script = { path = "../fparkan-script" }
+fparkan-vfs = { path = "../fparkan-vfs" }
+fparkan-world = { path = "../fparkan-world" }
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index 2bb1be1..2fead30 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -1,32 +1,13 @@
#![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
- )
-)]
-//! Runtime orchestration for headless and rendered modes.
+//! Runtime orchestration for mission loading and world stepping.
use fparkan_assets::{
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
- derive_mission_land_paths, extend_graph_report_with_visual_dependencies_with_progress,
+ derive_mission_land_paths, extend_graph_report_with_visual_dependencies,
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
- AssetId, AssetManager, AssetPreparationPhase, BuildCategory, MissionAssetPlan, MissionDocument,
- MissionError, MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError,
- TerrainPreparationError, TerrainWorld, TmaProfile, VisualDependencyPhase,
+ AssetId, AssetManager, BuildCategory, MissionAssetPlan, MissionDocument, MissionError,
+ MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError, TerrainPreparationError,
+ TerrainWorld, TmaProfile,
};
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
use fparkan_prototype::{
@@ -53,47 +34,6 @@ pub use fparkan_assets::MissionAssets;
const MISSION_DECODED_PAYLOAD_CACHE_ENTRIES: usize = 256;
const FALLBACK_SCRIPT_VARSET_PATH: &str = "MISSIONS/SCRIPTS/varset.var";
-/// Engine mode.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum EngineMode {
- /// Headless.
- Headless,
- /// Rendered.
- Rendered,
-}
-
-/// Scheduler phase.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum SchedulerPhase {
- /// Collect platform events.
- CollectPlatformEvents,
- /// Build input snapshot.
- BuildInputSnapshot,
- /// Advance clock.
- AdvanceGameClock,
- /// Calculate world queue.
- CalculateWorldQueue,
- /// Apply deferred operations.
- ApplyDeferredOperations,
- /// Update animation/effects.
- UpdateAnimationAndEffects,
- /// Publish render snapshot.
- PublishRenderSnapshot,
- /// Render world.
- RenderWorld,
- /// End frame callbacks.
- EndFrameCallbacks,
- /// Maintenance.
- Maintenance,
-}
-
-/// Engine config.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct EngineConfig {
- /// Mode.
- pub mode: EngineMode,
-}
-
/// Injectable engine services used by composition roots.
#[derive(Clone, Default)]
pub struct EngineServices {
@@ -116,97 +56,6 @@ pub struct MissionRequest {
pub key: String,
}
-/// Mission loading phase captured for diagnostics and acceptance tests.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum MissionLoadPhase {
- /// Resolve services and mission request context.
- Context,
- /// Decode and validate TMA.
- Tma,
- /// Decode and validate terrain map assets.
- Map,
- /// Expand object roots into a prototype graph.
- Graph,
- /// Expand model-backed visual dependencies of the prototype graph.
- GraphVisuals,
- /// Resolve WEAR tables while expanding visual dependencies.
- GraphVisualWears,
- /// Resolve MAT0 documents while expanding visual dependencies.
- GraphVisualMaterials,
- /// Progress marker emitted after each 64 MAT0 validation requests.
- GraphVisualMaterialRequests {
- /// Number of MAT0 validation requests started.
- request_count: usize,
- /// Graph nodes materialized before the request.
- graph_node_count: usize,
- /// Graph edges materialized before the request.
- graph_edge_count: usize,
- /// Distinct WEAR validation keys retained before the request.
- wear_cache_entries: usize,
- /// Distinct MAT0 validation keys retained before the request.
- material_cache_entries: usize,
- /// Distinct TEXM validation keys retained before the request.
- texture_cache_entries: usize,
- },
- /// Validate TEXM documents while expanding visual dependencies.
- GraphVisualTextures,
- /// Progress marker emitted after each 64 TEXM validation requests.
- GraphVisualTextureRequests {
- /// Number of TEXM validation requests started.
- request_count: usize,
- /// Graph nodes materialized before the request.
- graph_node_count: usize,
- /// Graph edges materialized before the request.
- graph_edge_count: usize,
- /// Distinct WEAR validation keys retained before the request.
- wear_cache_entries: usize,
- /// Distinct MAT0 validation keys retained before the request.
- material_cache_entries: usize,
- /// Distinct TEXM validation keys retained before the request.
- texture_cache_entries: usize,
- },
- /// Prepare all reachable visual/resource dependencies.
- Assets,
- /// Decode and validate MSH model meshes.
- AssetModelMeshes,
- /// Decode WEAR material tables.
- AssetWearTables,
- /// Resolve MAT0 material documents.
- AssetMaterials,
- /// Decode diffuse textures and baked lightmaps.
- AssetTextures,
- /// Construct all object drafts before registration.
- Construct,
- /// Register constructed objects.
- Register,
-}
-
-/// Raw placed transform preserved by the mission loader.
-#[derive(Clone, Debug, PartialEq)]
-pub struct PlacedTransformProfile {
- /// Object index in TMA order.
- pub object_index: usize,
- /// Raw position vector.
- pub position: [f32; 3],
- /// Raw orientation vector. No Euler order is inferred here.
- pub orientation_raw: [f32; 3],
- /// Raw scale vector.
- pub scale: [f32; 3],
-}
-
-/// Mission loading trace.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct MissionLoadTrace {
- /// Observed phases in execution order.
- pub phases: Vec<MissionLoadPhase>,
- /// Number of object drafts constructed before the first registration.
- pub drafts_before_registration: usize,
- /// Number of objects registered.
- pub registered_objects: usize,
- /// Raw transform profile for placed objects.
- pub transforms: Vec<PlacedTransformProfile>,
-}
-
/// Ordered mission property preserved for later runtime stages.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MissionObjectProperty {
@@ -435,20 +284,10 @@ pub struct LoadedMission {
pub struct FrameResult {
/// Snapshot.
pub snapshot: WorldSnapshot,
- /// Scheduler phases executed for this frame.
- pub trace: FrameTrace,
-}
-
-/// Scheduler trace for a completed frame.
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct FrameTrace {
- /// Frame phases in execution order.
- pub phases: Vec<SchedulerPhase>,
}
/// Engine.
pub struct Engine {
- config: EngineConfig,
services: EngineServices,
world: World,
loaded: Option<LoadedMissionState>,
@@ -559,13 +398,6 @@ pub enum EngineError {
World(fparkan_world::WorldError),
/// Reference movement input or terrain query failed.
Movement(String),
- /// Scheduler phase order was violated.
- SchedulerPhaseOrder {
- /// Previous phase.
- previous: SchedulerPhase,
- /// Current phase.
- current: SchedulerPhase,
- },
/// Staged mission world was torn down after a registration-phase failure.
RegistrationTeardown {
/// Registered objects before the forced failure.
@@ -676,10 +508,6 @@ impl std::fmt::Display for EngineError {
}
Self::World(source) => write!(f, "{source}"),
Self::Movement(message) => write!(f, "reference movement: {message}"),
- Self::SchedulerPhaseOrder { previous, current } => write!(
- f,
- "scheduler phase order regressed from {previous:?} to {current:?}"
- ),
Self::RegistrationTeardown {
registered_objects,
released_objects,
@@ -710,22 +538,14 @@ impl std::error::Error for EngineError {
| Self::ScriptRuntime { .. }
| Self::Movement(_)
| Self::PrototypeGraph { .. }
- | Self::SchedulerPhaseOrder { .. }
| Self::RegistrationTeardown { .. } => None,
}
}
}
-/// Creates engine.
-///
-/// # Errors
-///
-/// Currently this constructor is infallible, but it returns
-/// [`EngineError`] to keep the composition-root API stable as services become
-/// mandatory.
-pub fn create(config: EngineConfig, services: EngineServices) -> Result<Engine, EngineError> {
+/// Creates an engine with the supplied services.
+pub fn create(services: EngineServices) -> Result<Engine, EngineError> {
Ok(Engine {
- config,
services,
world: new_world(WorldConfig),
loaded: None,
@@ -744,7 +564,7 @@ pub fn load_mission(
engine: &mut Engine,
request: MissionRequest,
) -> Result<LoadedMission, EngineError> {
- load_mission_with_trace(engine, request).map(|(loaded, _trace)| loaded)
+ load_mission_with_options(engine, request, MissionLoadOptions::default())
}
/// Loads a mission using the bounded static-preview asset scope.
@@ -763,123 +583,48 @@ pub fn load_mission_static_preview(
engine: &mut Engine,
request: MissionRequest,
) -> Result<LoadedMission, EngineError> {
- load_mission_static_preview_with_progress(engine, request, |_| {})
-}
-
-/// Loads a static preview for the first requested non-zero count of mission roots.
-///
-/// Normal gameplay still uses [`load_mission`] and always resolves every root.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as
-/// [`load_mission_static_preview`].
-pub fn load_mission_static_preview_roots(
- engine: &mut Engine,
- request: MissionRequest,
- root_count: NonZeroUsize,
-) -> Result<LoadedMission, EngineError> {
- load_mission_static_preview_roots_with_progress(engine, request, root_count, |_| {})
-}
-
-/// Loads selected static-preview roots while synchronously reporting phases.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as
-/// [`load_mission_static_preview_roots`].
-pub fn load_mission_static_preview_roots_with_progress(
- engine: &mut Engine,
- request: MissionRequest,
- root_count: NonZeroUsize,
- mut on_phase: impl FnMut(MissionLoadPhase),
-) -> Result<LoadedMission, EngineError> {
- load_mission_with_options_and_progress(
+ load_mission_with_options(
engine,
request,
MissionLoadOptions {
- asset_scope: MissionAssetScope::PreviewRoots(root_count),
+ asset_scope: MissionAssetScope::FirstMeshPreview,
..MissionLoadOptions::default()
},
- Some(&mut on_phase),
)
- .map(|(loaded, _trace)| loaded)
}
-/// Loads a static preview while synchronously reporting entered loading phases.
+/// Loads a static preview for the first requested non-zero count of mission roots.
///
-/// This has the same bounded asset scope as [`load_mission_static_preview`].
+/// Normal gameplay still uses [`load_mission`] and always resolves every root.
///
/// # Errors
///
/// Returns [`EngineError`] under the same conditions as
/// [`load_mission_static_preview`].
-pub fn load_mission_static_preview_with_progress(
+pub fn load_mission_static_preview_roots(
engine: &mut Engine,
request: MissionRequest,
- mut on_phase: impl FnMut(MissionLoadPhase),
+ root_count: NonZeroUsize,
) -> Result<LoadedMission, EngineError> {
- load_mission_with_options_and_progress(
+ load_mission_with_options(
engine,
request,
MissionLoadOptions {
- asset_scope: MissionAssetScope::FirstMeshPreview,
+ asset_scope: MissionAssetScope::PreviewRoots(root_count),
..MissionLoadOptions::default()
},
- Some(&mut on_phase),
)
- .map(|(loaded, _trace)| loaded)
-}
-
-/// Loads mission transactionally and returns a diagnostic trace.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as [`load_mission`].
-pub fn load_mission_with_trace(
- engine: &mut Engine,
- request: MissionRequest,
-) -> Result<(LoadedMission, MissionLoadTrace), EngineError> {
- load_mission_with_options_and_progress(engine, request, MissionLoadOptions::default(), None)
}
-/// Loads a mission while synchronously reporting each entered loading phase.
-///
-/// The observer runs immediately after the phase is recorded in the returned
-/// trace. It is intended for diagnostic progress reporting; it does not alter
-/// loader ordering, validation, or transaction behavior.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as [`load_mission`].
-pub fn load_mission_with_progress(
- engine: &mut Engine,
- request: MissionRequest,
- mut on_phase: impl FnMut(MissionLoadPhase),
-) -> Result<LoadedMission, EngineError> {
- load_mission_with_options_and_progress(
- engine,
- request,
- MissionLoadOptions::default(),
- Some(&mut on_phase),
- )
- .map(|(loaded, _trace)| loaded)
-}
-
-#[allow(clippy::too_many_lines)]
-fn load_mission_with_options_and_progress(
+fn load_mission_with_options(
engine: &mut Engine,
request: MissionRequest,
options: MissionLoadOptions,
- mut on_phase: Option<&mut dyn FnMut(MissionLoadPhase)>,
-) -> Result<(LoadedMission, MissionLoadTrace), EngineError> {
- let mut trace = MissionLoadTrace::default();
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Context);
+) -> Result<LoadedMission, EngineError> {
let vfs = engine.services.vfs.clone().ok_or(EngineError::MissingVfs)?;
let mission_path = normalize_engine_path("mission", &request.key)?;
let mission_bytes = read_vfs(&vfs, &mission_path)?;
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Map);
let land_path =
decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| {
EngineError::Mission {
@@ -919,7 +664,6 @@ fn load_mission_with_options_and_progress(
TerrainPreparationError::Runtime(source) => EngineError::Terrain(source),
}
})?;
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Tma);
let mission = decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| {
EngineError::Mission {
path: mission_path.as_str().to_string(),
@@ -927,18 +671,6 @@ fn load_mission_with_options_and_progress(
}
})?;
let loaded_scripts = load_mission_scripts(&vfs, &mission)?;
- trace.transforms = mission
- .objects
- .iter()
- .enumerate()
- .map(|(object_index, object)| PlacedTransformProfile {
- object_index,
- position: object.position,
- orientation_raw: object.orientation,
- scale: object.scale,
- })
- .collect();
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Graph);
let repository = CachedResourceRepository::with_payload_cache_budget(
vfs.clone(),
MISSION_DECODED_PAYLOAD_CACHE_ENTRIES,
@@ -957,93 +689,34 @@ fn load_mission_with_options_and_progress(
};
let (mut prototype_graph, resolved_prototypes, mut prototype_report) =
build_prototype_graph_report(&repository, vfs.as_ref(), scoped_graph_roots);
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::GraphVisuals);
- let mut last_graph_visual_phase = None;
- extend_graph_report_with_visual_dependencies_with_progress(
+ extend_graph_report_with_visual_dependencies(
&repository,
&mut prototype_report,
&mut prototype_graph,
&resolved_prototypes,
- |progress| {
- let runtime_phase = match progress.phase {
- VisualDependencyPhase::Wear => MissionLoadPhase::GraphVisualWears,
- VisualDependencyPhase::Material => MissionLoadPhase::GraphVisualMaterials,
- VisualDependencyPhase::Texture => MissionLoadPhase::GraphVisualTextures,
- };
- if last_graph_visual_phase != Some(runtime_phase) {
- record_load_phase(&mut trace, &mut on_phase, runtime_phase);
- last_graph_visual_phase = Some(runtime_phase);
- }
- if progress.request_count > 1 {
- let request_phase = match progress.phase {
- VisualDependencyPhase::Wear => None,
- VisualDependencyPhase::Material => {
- Some(MissionLoadPhase::GraphVisualMaterialRequests {
- request_count: progress.request_count,
- graph_node_count: progress.graph_node_count,
- graph_edge_count: progress.graph_edge_count,
- wear_cache_entries: progress.cache_entries.wear_entries,
- material_cache_entries: progress.cache_entries.material_entries,
- texture_cache_entries: progress.cache_entries.texture_entries,
- })
- }
- VisualDependencyPhase::Texture => {
- Some(MissionLoadPhase::GraphVisualTextureRequests {
- request_count: progress.request_count,
- graph_node_count: progress.graph_node_count,
- graph_edge_count: progress.graph_edge_count,
- wear_cache_entries: progress.cache_entries.wear_entries,
- material_cache_entries: progress.cache_entries.material_entries,
- texture_cache_entries: progress.cache_entries.texture_entries,
- })
- }
- };
- if let Some(request_phase) = request_phase {
- record_load_phase(&mut trace, &mut on_phase, request_phase);
- }
- }
- },
);
if !prototype_report.is_success() {
return Err(EngineError::PrototypeGraph {
failures: prototype_report.failures.clone(),
});
}
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Assets);
let asset_manager = AssetManager::new(repository);
- let mut last_asset_phase = None;
- let mut observe_asset_phase = |phase| {
- let runtime_phase = match phase {
- AssetPreparationPhase::ModelMesh => MissionLoadPhase::AssetModelMeshes,
- AssetPreparationPhase::WearTable => MissionLoadPhase::AssetWearTables,
- AssetPreparationPhase::Materials => MissionLoadPhase::AssetMaterials,
- AssetPreparationPhase::Textures => MissionLoadPhase::AssetTextures,
- };
- if last_asset_phase != Some(runtime_phase) {
- record_load_phase(&mut trace, &mut on_phase, runtime_phase);
- last_asset_phase = Some(runtime_phase);
- }
- };
let mission_assets = match options.asset_scope {
- MissionAssetScope::Full => asset_manager.prepare_mission_assets_with_progress(
+ MissionAssetScope::Full => asset_manager.prepare_mission_assets(
&prototype_graph.root_prototype_request_spans,
&resolved_prototypes,
- &mut observe_asset_phase,
),
MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets(
&asset_manager,
&prototype_graph.root_prototype_request_spans,
&resolved_prototypes,
- &mut observe_asset_phase,
),
- MissionAssetScope::PreviewRoots(root_count) => asset_manager
- .prepare_mission_assets_with_progress(
- &prototype_graph.root_prototype_request_spans[..root_count
- .get()
- .min(prototype_graph.root_prototype_request_spans.len())],
- &resolved_prototypes,
- &mut observe_asset_phase,
- ),
+ MissionAssetScope::PreviewRoots(root_count) => asset_manager.prepare_mission_assets(
+ &prototype_graph.root_prototype_request_spans[..root_count
+ .get()
+ .min(prototype_graph.root_prototype_request_spans.len())],
+ &resolved_prototypes,
+ ),
}
.map_err(|source| EngineError::AssetPreparation {
mission: request.key.clone(),
@@ -1079,7 +752,6 @@ fn load_mission_with_options_and_progress(
.collect();
let mut new_runtime_world = new_world(WorldConfig);
let mut handles = Vec::with_capacity(mission.objects.len());
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Construct);
for (index, object) in mission.objects.iter().enumerate() {
let original_id = u32::try_from(index).ok().map(OriginalObjectId);
let handle = construct_object(&mut new_runtime_world, ObjectDraft { original_id })?;
@@ -1094,19 +766,18 @@ fn load_mission_with_options_and_progress(
)?;
handles.push(handle);
}
- trace.drafts_before_registration = handles.len();
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Register);
+ let mut registered_objects = 0;
for handle in &handles {
- if options.fail_after_registered_objects == Some(trace.registered_objects) {
+ if options.fail_after_registered_objects == Some(registered_objects) {
let report = fparkan_world::shutdown(new_runtime_world);
return Err(EngineError::RegistrationTeardown {
- registered_objects: trace.registered_objects,
+ registered_objects,
released_objects: report.released_objects.len(),
managers_released: report.managers_released,
});
}
register_object(&mut new_runtime_world, *handle)?;
- trace.registered_objects += 1;
+ registered_objects += 1;
}
let summary = LoadedMission {
@@ -1174,32 +845,17 @@ fn load_mission_with_options_and_progress(
script_varset_states: loaded_scripts.varset_states,
script_outcome: MissionScriptOutcome::Active,
});
- Ok((summary, trace))
-}
-
-fn record_load_phase(
- trace: &mut MissionLoadTrace,
- on_phase: &mut Option<&mut dyn FnMut(MissionLoadPhase)>,
- phase: MissionLoadPhase,
-) {
- trace.phases.push(phase);
- if let Some(observer) = on_phase.as_deref_mut() {
- observer(phase);
- }
+ Ok(summary)
}
fn prepare_first_preview_assets<R: ResourceRepository>(
asset_manager: &AssetManager<R>,
root_spans: &[std::ops::Range<usize>],
prototypes: &[fparkan_prototype::EffectivePrototype],
- on_phase: &mut dyn FnMut(AssetPreparationPhase),
) -> Result<MissionAssets, AssetPreparationError> {
for span in root_spans {
- let assets = asset_manager.prepare_mission_assets_with_progress(
- std::slice::from_ref(span),
- prototypes,
- &mut *on_phase,
- )?;
+ let assets =
+ asset_manager.prepare_mission_assets(std::slice::from_ref(span), prototypes)?;
if !assets.models.is_empty() {
return Ok(assets);
}
@@ -1216,7 +872,9 @@ pub fn step_headless(
engine: &mut Engine,
input: InputSnapshot,
) -> Result<FrameResult, EngineError> {
- run_frame(engine, input, SchedulerPresentation::Headless)
+ Ok(FrameResult {
+ snapshot: step(&mut engine.world, &input)?,
+ })
}
/// Steps rendered mode.
@@ -1225,20 +883,7 @@ pub fn step_headless(
///
/// Returns [`EngineError`] when the world step fails.
pub fn frame(engine: &mut Engine) -> Result<FrameResult, EngineError> {
- match engine.config.mode {
- EngineMode::Headless => step_headless(engine, InputSnapshot),
- EngineMode::Rendered => run_frame(engine, InputSnapshot, SchedulerPresentation::Rendered),
- }
-}
-
-/// Shuts down engine.
-///
-/// # Errors
-///
-/// Currently shutdown is infallible, but the `Result` preserves the lifecycle
-/// API for future service teardown failures.
-pub fn shutdown(_engine: Engine) -> Result<(), EngineError> {
- Ok(())
+ step_headless(engine, InputSnapshot)
}
/// Returns the loaded mission summary.
@@ -1451,78 +1096,6 @@ pub fn loaded_mission_object_drafts(engine: &Engine) -> Option<&[MissionObjectDr
.map(|state| state.object_drafts.as_slice())
}
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum SchedulerPresentation {
- Headless,
- Rendered,
-}
-
-#[derive(Clone, Debug, Default)]
-struct Scheduler {
- phase: Option<SchedulerPhase>,
- trace: FrameTrace,
-}
-
-impl Scheduler {
- fn enter(&mut self, phase: SchedulerPhase) -> Result<(), EngineError> {
- if let Some(previous) = self.phase {
- if scheduler_phase_index(phase) <= scheduler_phase_index(previous) {
- return Err(EngineError::SchedulerPhaseOrder {
- previous,
- current: phase,
- });
- }
- }
- self.phase = Some(phase);
- self.trace.phases.push(phase);
- Ok(())
- }
-
- fn finish(self) -> FrameTrace {
- self.trace
- }
-}
-
-fn run_frame(
- engine: &mut Engine,
- input: InputSnapshot,
- presentation: SchedulerPresentation,
-) -> Result<FrameResult, EngineError> {
- let mut scheduler = Scheduler::default();
- scheduler.enter(SchedulerPhase::CollectPlatformEvents)?;
- scheduler.enter(SchedulerPhase::BuildInputSnapshot)?;
- scheduler.enter(SchedulerPhase::AdvanceGameClock)?;
- scheduler.enter(SchedulerPhase::CalculateWorldQueue)?;
- let snapshot = step(&mut engine.world, &input)?;
- scheduler.enter(SchedulerPhase::ApplyDeferredOperations)?;
- scheduler.enter(SchedulerPhase::UpdateAnimationAndEffects)?;
- if presentation == SchedulerPresentation::Rendered {
- scheduler.enter(SchedulerPhase::PublishRenderSnapshot)?;
- scheduler.enter(SchedulerPhase::RenderWorld)?;
- }
- scheduler.enter(SchedulerPhase::EndFrameCallbacks)?;
- scheduler.enter(SchedulerPhase::Maintenance)?;
- Ok(FrameResult {
- snapshot,
- trace: scheduler.finish(),
- })
-}
-
-fn scheduler_phase_index(phase: SchedulerPhase) -> u8 {
- match phase {
- SchedulerPhase::CollectPlatformEvents => 0,
- SchedulerPhase::BuildInputSnapshot => 1,
- SchedulerPhase::AdvanceGameClock => 2,
- SchedulerPhase::CalculateWorldQueue => 3,
- SchedulerPhase::ApplyDeferredOperations => 4,
- SchedulerPhase::UpdateAnimationAndEffects => 5,
- SchedulerPhase::PublishRenderSnapshot => 6,
- SchedulerPhase::RenderWorld => 7,
- SchedulerPhase::EndFrameCallbacks => 8,
- SchedulerPhase::Maintenance => 9,
- }
-}
-
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
EngineError::Path {
@@ -1702,7 +1275,7 @@ fn handler19_x87_truncated_u32(
}
// ai.dll's __ftol saves the x87 control word, sets rounding-control bits
// to truncate, performs `fistp qword`, and restores the control word.
- #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+
let word = value.trunc() as u32;
Ok(word)
}
@@ -1758,13 +1331,7 @@ mod tests {
#[test]
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::default()).expect("engine");
let before = step_headless(&mut engine, InputSnapshot).expect("step");
let err = load_mission(
&mut engine,
@@ -1779,30 +1346,6 @@ mod tests {
}
#[test]
- fn load_progress_reports_context_before_missing_vfs_error() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
- let mut phases = Vec::new();
-
- let err = load_mission_with_progress(
- &mut engine,
- MissionRequest {
- key: "MISSIONS/Autodemo.00/data.tma".to_string(),
- },
- |phase| phases.push(phase),
- )
- .expect_err("missing VFS");
-
- assert!(matches!(err, EngineError::MissingVfs));
- assert_eq!(phases, vec![MissionLoadPhase::Context]);
- }
-
- #[test]
fn script_varset_prefers_bundle_adjacent_file_then_uses_shared_fallback() {
let mut personal_files = MemoryVfs::default();
personal_files.insert(
@@ -1997,87 +1540,8 @@ mod tests {
}
#[test]
- fn headless_scheduler_trace_skips_presentation_phases() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
-
- let result = frame(&mut engine).expect("frame");
-
- assert_eq!(result.snapshot.tick.0, 1);
- assert_eq!(
- result.trace.phases,
- vec![
- SchedulerPhase::CollectPlatformEvents,
- SchedulerPhase::BuildInputSnapshot,
- SchedulerPhase::AdvanceGameClock,
- SchedulerPhase::CalculateWorldQueue,
- SchedulerPhase::ApplyDeferredOperations,
- SchedulerPhase::UpdateAnimationAndEffects,
- SchedulerPhase::EndFrameCallbacks,
- SchedulerPhase::Maintenance,
- ]
- );
- }
-
- #[test]
- fn rendered_scheduler_trace_includes_presentation_after_simulation() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Rendered,
- },
- EngineServices::default(),
- )
- .expect("engine");
-
- let result = frame(&mut engine).expect("frame");
-
- assert_eq!(
- result.trace.phases,
- vec![
- SchedulerPhase::CollectPlatformEvents,
- SchedulerPhase::BuildInputSnapshot,
- SchedulerPhase::AdvanceGameClock,
- SchedulerPhase::CalculateWorldQueue,
- SchedulerPhase::ApplyDeferredOperations,
- SchedulerPhase::UpdateAnimationAndEffects,
- SchedulerPhase::PublishRenderSnapshot,
- SchedulerPhase::RenderWorld,
- SchedulerPhase::EndFrameCallbacks,
- SchedulerPhase::Maintenance,
- ]
- );
- }
-
- #[test]
- fn scheduler_rejects_phase_regressions() {
- let mut scheduler = Scheduler::default();
- scheduler
- .enter(SchedulerPhase::BuildInputSnapshot)
- .expect("enter build input");
-
- assert!(matches!(
- scheduler.enter(SchedulerPhase::CollectPlatformEvents),
- Err(EngineError::SchedulerPhaseOrder {
- previous: SchedulerPhase::BuildInputSnapshot,
- current: SchedulerPhase::CollectPlatformEvents,
- })
- ));
- }
-
- #[test]
fn reference_movement_rejects_non_finite_or_non_positive_input() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::default()).expect("engine");
let err =
advance_reference_movement(&mut engine, OriginalObjectId(0), [f32::NAN, 0.0], 1.0)
@@ -2095,59 +1559,10 @@ mod tests {
#[test]
#[ignore = "requires licensed corpus"]
- fn load_trace_records_preparation_before_registration_and_raw_transforms() {
- let root = licensed_root("IS");
- let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
-
- let (loaded, trace) = load_mission_with_trace(
- &mut engine,
- MissionRequest {
- key: "MISSIONS/Autodemo.00/data.tma".to_string(),
- },
- )
- .expect("load mission with trace");
-
- assert_eq!(
- trace.phases,
- vec![
- MissionLoadPhase::Context,
- MissionLoadPhase::Map,
- MissionLoadPhase::Tma,
- MissionLoadPhase::Graph,
- MissionLoadPhase::GraphVisuals,
- MissionLoadPhase::Assets,
- MissionLoadPhase::Construct,
- MissionLoadPhase::Register,
- ]
- );
- assert_eq!(trace.drafts_before_registration, loaded.object_count);
- assert_eq!(trace.registered_objects, loaded.object_count);
- assert_eq!(trace.transforms.len(), loaded.object_count);
- assert!(trace.transforms.iter().all(|transform| transform
- .orientation_raw
- .iter()
- .all(|component| component.is_finite())));
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
fn reference_movement_snaps_a_live_mission_object_to_terrain() {
let root = licensed_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
load_mission(
&mut engine,
MissionRequest {
@@ -2221,13 +1636,7 @@ mod tests {
inner: DirectoryVfs::new(&root),
denied,
});
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let before = step_headless(&mut engine, InputSnapshot).expect("before");
let err = load_mission(&mut engine, mission).expect_err("load error");
match denied {
@@ -2248,16 +1657,10 @@ mod tests {
fn registration_phase_failure_uses_normal_teardown_and_keeps_engine_world() {
let root = licensed_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let before = step_headless(&mut engine, InputSnapshot).expect("before");
- let err = load_mission_with_options_and_progress(
+ let err = load_mission_with_options(
&mut engine,
MissionRequest {
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
@@ -2266,7 +1669,6 @@ mod tests {
fail_after_registered_objects: Some(1),
..MissionLoadOptions::default()
},
- None,
)
.expect_err("forced registration failure");
@@ -2326,13 +1728,7 @@ mod tests {
] {
let root = local_testdata_root(case.0);
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let loaded = load_mission(
&mut engine,
MissionRequest {
@@ -2440,13 +1836,7 @@ mod tests {
fn run_headless_case(case: HeadlessCase) -> WorldSnapshot {
let root = licensed_root(case.root);
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let loaded = load_mission(
&mut engine,
MissionRequest {
@@ -2474,13 +1864,7 @@ mod tests {
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
let mut totals = LoadTotals::default();
for mission in missions {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs.clone()),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs.clone())).expect("engine");
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
.expect("load mission foundation");
assert_eq!(loaded.object_count, loaded.registered_objects);
diff --git a/crates/fparkan-script/Cargo.toml b/crates/fparkan-script/Cargo.toml
index 0280be7..47ee000 100644
--- a/crates/fparkan-script/Cargo.toml
+++ b/crates/fparkan-script/Cargo.toml
@@ -6,7 +6,4 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
diff --git a/crates/fparkan-script/src/lib.rs b/crates/fparkan-script/src/lib.rs
index 6887a03..88c2d0a 100644
--- a/crates/fparkan-script/src/lib.rs
+++ b/crates/fparkan-script/src/lib.rs
@@ -1,5 +1,4 @@
#![forbid(unsafe_code)]
-#![cfg_attr(test, allow(clippy::expect_used, clippy::panic, clippy::unwrap_used))]
//! Lossless, bounded reader for compiled AI `.scr` packages.
//!
//! This module preserves the layout proven by the GOG `ai.dll` reader. It
@@ -1061,9 +1060,7 @@ impl ScriptInstruction {
#[must_use]
pub fn dispatch_selector(&self) -> ScriptDispatchSelector {
match self.header_words[0] {
- value if value < GOG_HANDLER_COUNT =>
- {
- #[allow(clippy::cast_possible_truncation)]
+ value if value < GOG_HANDLER_COUNT => {
ScriptDispatchSelector::Handler(self.header_words[0] as u8)
}
u32::MAX => ScriptDispatchSelector::Sentinel,
diff --git a/crates/fparkan-terrain-format/Cargo.toml b/crates/fparkan-terrain-format/Cargo.toml
index 28a7a9c..dcd4e9e 100644
--- a/crates/fparkan-terrain-format/Cargo.toml
+++ b/crates/fparkan-terrain-format/Cargo.toml
@@ -6,8 +6,5 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
+fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs
index 54bd3f4..3c05cfe 100644
--- a/crates/fparkan-terrain-format/src/lib.rs
+++ b/crates/fparkan-terrain-format/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
- )
-)]
//! Terrain disk format primitives.
use fparkan_binary::{checked_count_bytes, Cursor, DecodeError};
diff --git a/crates/fparkan-terrain/Cargo.toml b/crates/fparkan-terrain/Cargo.toml
index cf10191..8273f3f 100644
--- a/crates/fparkan-terrain/Cargo.toml
+++ b/crates/fparkan-terrain/Cargo.toml
@@ -6,10 +6,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-terrain-format = { path = "../fparkan-terrain-format", version = "0.1.0" }
+fparkan-terrain-format = { path = "../fparkan-terrain-format" }
[dev-dependencies]
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs
index de67fd0..7dced19 100644
--- a/crates/fparkan-terrain/src/lib.rs
+++ b/crates/fparkan-terrain/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
- )
-)]
//! Validated terrain runtime queries using XY ground coordinates and Z height.
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
diff --git a/crates/fparkan-test-support/Cargo.toml b/crates/fparkan-test-support/Cargo.toml
deleted file mode 100644
index 3649ded..0000000
--- a/crates/fparkan-test-support/Cargo.toml
+++ /dev/null
@@ -1,12 +0,0 @@
-[package]
-name = "fparkan-test-support"
-version.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-
-[dependencies]
-fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
-
-[lints]
-workspace = true
diff --git a/crates/fparkan-test-support/src/lib.rs b/crates/fparkan-test-support/src/lib.rs
deleted file mode 100644
index cc2e9e8..0000000
--- a/crates/fparkan-test-support/src/lib.rs
+++ /dev/null
@@ -1,44 +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
- )
-)]
-//! Dev-only synthetic builders and fake ports.
-
-use fparkan_render::{FrameOutput, RenderBackend, RenderCommandList, RenderError};
-
-/// Fake clock.
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-pub struct FakeClock {
- /// Current tick.
- pub tick: u64,
-}
-
-/// Recording backend.
-#[derive(Clone, Debug, Default)]
-pub struct RecordingRenderBackend {
- /// Recorded command lists.
- pub captures: Vec<RenderCommandList>,
-}
-
-impl RenderBackend for RecordingRenderBackend {
- fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
- self.captures.push(commands.clone());
- Ok(FrameOutput)
- }
-}
diff --git a/crates/fparkan-texm/Cargo.toml b/crates/fparkan-texm/Cargo.toml
index 5845efb..b6f9424 100644
--- a/crates/fparkan-texm/Cargo.toml
+++ b/crates/fparkan-texm/Cargo.toml
@@ -8,7 +8,4 @@ repository.workspace = true
[dependencies]
[dev-dependencies]
-fparkan-nres = { path = "../fparkan-nres", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-nres = { path = "../fparkan-nres" }
diff --git a/crates/fparkan-texm/src/lib.rs b/crates/fparkan-texm/src/lib.rs
index c73bb95..8747737 100644
--- a/crates/fparkan-texm/src/lib.rs
+++ b/crates/fparkan-texm/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
- )
-)]
//! Stage-3 Texm texture contract.
use std::sync::Arc;
diff --git a/crates/fparkan-vfs/Cargo.toml b/crates/fparkan-vfs/Cargo.toml
index e5fab96..2a20dda 100644
--- a/crates/fparkan-vfs/Cargo.toml
+++ b/crates/fparkan-vfs/Cargo.toml
@@ -6,8 +6,5 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
+fparkan-path = { path = "../fparkan-path" }
diff --git a/crates/fparkan-vfs/src/lib.rs b/crates/fparkan-vfs/src/lib.rs
index 0b25056..3c92fa1 100644
--- a/crates/fparkan-vfs/src/lib.rs
+++ b/crates/fparkan-vfs/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
- )
-)]
//! Virtual filesystem ports for resource loading.
use fparkan_binary::{sha256, Sha256Digest};
@@ -312,7 +293,7 @@ pub struct MemoryVfs {
impl MemoryVfs {
/// Inserts a file.
- #[allow(clippy::needless_pass_by_value)]
+
pub fn insert(&mut self, path: NormalizedPath, bytes: Arc<[u8]>) {
let path = path.as_bytes().to_vec();
self.files.insert(path, bytes);
@@ -347,13 +328,13 @@ impl MemoryVfs {
}
#[cfg(unix)]
-#[allow(clippy::unnecessary_wraps)]
+
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
Some(metadata.dev().rotate_left(32) ^ metadata.ino())
}
#[cfg(windows)]
-#[allow(clippy::unnecessary_wraps)]
+
fn file_identity(metadata: &fs::Metadata) -> Option<u64> {
// Stable Rust does not expose the Windows file index. Keep a best-effort
// replacement detector from metadata that is available through the
diff --git a/crates/fparkan-world/Cargo.toml b/crates/fparkan-world/Cargo.toml
index 0b0d900..a78fcd1 100644
--- a/crates/fparkan-world/Cargo.toml
+++ b/crates/fparkan-world/Cargo.toml
@@ -6,7 +6,4 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-binary = { path = "../fparkan-binary", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-binary = { path = "../fparkan-binary" }
diff --git a/crates/fparkan-world/src/lib.rs b/crates/fparkan-world/src/lib.rs
index e14ac97..41c2038 100644
--- a/crates/fparkan-world/src/lib.rs
+++ b/crates/fparkan-world/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
- )
-)]
//! Deterministic world identity, queue, lifecycle, and snapshots.
use fparkan_binary::sha256;