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.
This commit is contained in:
@@ -6,6 +6,3 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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>(
|
||||
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,
|
||||
@@ -2450,66 +2197,6 @@ mod tests {
|
||||
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs};
|
||||
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(&[]);
|
||||
@@ -3315,7 +3002,6 @@ mod tests {
|
||||
&prototypes,
|
||||
policy,
|
||||
AssetPreparationLimits::default(),
|
||||
&mut |_| {},
|
||||
)
|
||||
.expect_err("collision should fail");
|
||||
|
||||
|
||||
@@ -6,6 +6,3 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -6,6 +6,3 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,6 +7,3 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
raw-window-handle = "0.6"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,3 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,7 +583,14 @@ pub fn load_mission_static_preview(
|
||||
engine: &mut Engine,
|
||||
request: MissionRequest,
|
||||
) -> Result<LoadedMission, EngineError> {
|
||||
load_mission_static_preview_with_progress(engine, request, |_| {})
|
||||
load_mission_with_options(
|
||||
engine,
|
||||
request,
|
||||
MissionLoadOptions {
|
||||
asset_scope: MissionAssetScope::FirstMeshPreview,
|
||||
..MissionLoadOptions::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads a static preview for the first requested non-zero count of mission roots.
|
||||
@@ -779,107 +606,25 @@ pub fn load_mission_static_preview_roots(
|
||||
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),
|
||||
..MissionLoadOptions::default()
|
||||
},
|
||||
Some(&mut on_phase),
|
||||
)
|
||||
.map(|(loaded, _trace)| loaded)
|
||||
}
|
||||
|
||||
/// Loads a static preview while synchronously reporting entered loading phases.
|
||||
///
|
||||
/// This has the same bounded asset scope as [`load_mission_static_preview`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`EngineError`] under the same conditions as
|
||||
/// [`load_mission_static_preview`].
|
||||
pub fn load_mission_static_preview_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 {
|
||||
asset_scope: MissionAssetScope::FirstMeshPreview,
|
||||
..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,
|
||||
@@ -1778,30 +1345,6 @@ mod tests {
|
||||
assert_eq!(before.snapshot.objects, after.snapshot.objects);
|
||||
}
|
||||
|
||||
#[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();
|
||||
@@ -1996,88 +1539,9 @@ 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)
|
||||
@@ -2093,61 +1557,12 @@ 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);
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user