aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-09-06 04:22:12 +0300
committerValentin Popov <valentin@popov.link>2026-09-06 04:22:12 +0300
commitaa51f3574d3d4d4e0fa59653eedd718fceb5d1bd (patch)
tree9a43f6a3f3c86767e4e2b7316051562d96789f06 /crates
parentda03e77f53bb33b2fe203ac45578289953a1b524 (diff)
downloadfparkan-aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd.tar.xz
fparkan-aa51f3574d3d4d4e0fa59653eedd718fceb5d1bd.zip
chore: simplify project to engine docs and tests
Remove planning and acceptance scaffolding while retaining the native Vulkan mission preview, format readers, runtime algorithms, and ordinary Rust tests. Keep the book aligned with the runnable project and validate checked-in shaders without generated tool metadata.
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;