aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-runtime
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-runtime')
-rw-r--r--crates/fparkan-runtime/Cargo.toml23
-rw-r--r--crates/fparkan-runtime/src/lib.rs716
2 files changed, 62 insertions, 677 deletions
diff --git a/crates/fparkan-runtime/Cargo.toml b/crates/fparkan-runtime/Cargo.toml
index 858e5af..ddf09a1 100644
--- a/crates/fparkan-runtime/Cargo.toml
+++ b/crates/fparkan-runtime/Cargo.toml
@@ -6,16 +6,13 @@ license.workspace = true
repository.workspace = true
[dependencies]
-fparkan-assets = { path = "../fparkan-assets", version = "0.1.0" }
-fparkan-path = { path = "../fparkan-path", version = "0.1.0" }
-fparkan-platform = { path = "../fparkan-platform", version = "0.1.0" }
-fparkan-prototype = { path = "../fparkan-prototype", version = "0.1.0" }
-fparkan-render = { path = "../fparkan-render", version = "0.1.0" }
-fparkan-terrain = { path = "../fparkan-terrain", version = "0.1.0" }
-fparkan-resource = { path = "../fparkan-resource", version = "0.1.0" }
-fparkan-script = { path = "../fparkan-script", version = "0.1.0" }
-fparkan-vfs = { path = "../fparkan-vfs", version = "0.1.0" }
-fparkan-world = { path = "../fparkan-world", version = "0.1.0" }
-
-[lints]
-workspace = true
+fparkan-assets = { path = "../fparkan-assets" }
+fparkan-path = { path = "../fparkan-path" }
+fparkan-platform = { path = "../fparkan-platform" }
+fparkan-prototype = { path = "../fparkan-prototype" }
+fparkan-render = { path = "../fparkan-render" }
+fparkan-terrain = { path = "../fparkan-terrain" }
+fparkan-resource = { path = "../fparkan-resource" }
+fparkan-script = { path = "../fparkan-script" }
+fparkan-vfs = { path = "../fparkan-vfs" }
+fparkan-world = { path = "../fparkan-world" }
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index 2bb1be1..e0088a3 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -1,32 +1,13 @@
#![forbid(unsafe_code)]
-#![cfg_attr(
- test,
- allow(
- clippy::cast_possible_truncation,
- clippy::cast_possible_wrap,
- clippy::cast_precision_loss,
- clippy::expect_used,
- clippy::float_cmp,
- clippy::identity_op,
- clippy::too_many_lines,
- clippy::uninlined_format_args,
- clippy::map_unwrap_or,
- clippy::needless_raw_string_hashes,
- clippy::semicolon_if_nothing_returned,
- clippy::type_complexity,
- clippy::panic,
- clippy::unwrap_used
- )
-)]
-//! Runtime orchestration for headless and rendered modes.
+//! Runtime orchestration for mission loading and world stepping.
use fparkan_assets::{
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
- derive_mission_land_paths, extend_graph_report_with_visual_dependencies_with_progress,
+ derive_mission_land_paths, extend_graph_report_with_visual_dependencies,
mission_script_bundle_bases, prepare_terrain_world, AssetError as AssetPreparationError,
- AssetId, AssetManager, AssetPreparationPhase, BuildCategory, MissionAssetPlan, MissionDocument,
- MissionError, MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError,
- TerrainPreparationError, TerrainWorld, TmaProfile, VisualDependencyPhase,
+ AssetId, AssetManager, BuildCategory, MissionAssetPlan, MissionDocument, MissionError,
+ MissionTerrainPaths, NresError, PreparedVisual, TerrainFormatError, TerrainPreparationError,
+ TerrainWorld, TmaProfile,
};
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
use fparkan_prototype::{
@@ -53,47 +34,6 @@ pub use fparkan_assets::MissionAssets;
const MISSION_DECODED_PAYLOAD_CACHE_ENTRIES: usize = 256;
const FALLBACK_SCRIPT_VARSET_PATH: &str = "MISSIONS/SCRIPTS/varset.var";
-/// Engine mode.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum EngineMode {
- /// Headless.
- Headless,
- /// Rendered.
- Rendered,
-}
-
-/// Scheduler phase.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum SchedulerPhase {
- /// Collect platform events.
- CollectPlatformEvents,
- /// Build input snapshot.
- BuildInputSnapshot,
- /// Advance clock.
- AdvanceGameClock,
- /// Calculate world queue.
- CalculateWorldQueue,
- /// Apply deferred operations.
- ApplyDeferredOperations,
- /// Update animation/effects.
- UpdateAnimationAndEffects,
- /// Publish render snapshot.
- PublishRenderSnapshot,
- /// Render world.
- RenderWorld,
- /// End frame callbacks.
- EndFrameCallbacks,
- /// Maintenance.
- Maintenance,
-}
-
-/// Engine config.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct EngineConfig {
- /// Mode.
- pub mode: EngineMode,
-}
-
/// Injectable engine services used by composition roots.
#[derive(Clone, Default)]
pub struct EngineServices {
@@ -116,97 +56,6 @@ pub struct MissionRequest {
pub key: String,
}
-/// Mission loading phase captured for diagnostics and acceptance tests.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum MissionLoadPhase {
- /// Resolve services and mission request context.
- Context,
- /// Decode and validate TMA.
- Tma,
- /// Decode and validate terrain map assets.
- Map,
- /// Expand object roots into a prototype graph.
- Graph,
- /// Expand model-backed visual dependencies of the prototype graph.
- GraphVisuals,
- /// Resolve WEAR tables while expanding visual dependencies.
- GraphVisualWears,
- /// Resolve MAT0 documents while expanding visual dependencies.
- GraphVisualMaterials,
- /// Progress marker emitted after each 64 MAT0 validation requests.
- GraphVisualMaterialRequests {
- /// Number of MAT0 validation requests started.
- request_count: usize,
- /// Graph nodes materialized before the request.
- graph_node_count: usize,
- /// Graph edges materialized before the request.
- graph_edge_count: usize,
- /// Distinct WEAR validation keys retained before the request.
- wear_cache_entries: usize,
- /// Distinct MAT0 validation keys retained before the request.
- material_cache_entries: usize,
- /// Distinct TEXM validation keys retained before the request.
- texture_cache_entries: usize,
- },
- /// Validate TEXM documents while expanding visual dependencies.
- GraphVisualTextures,
- /// Progress marker emitted after each 64 TEXM validation requests.
- GraphVisualTextureRequests {
- /// Number of TEXM validation requests started.
- request_count: usize,
- /// Graph nodes materialized before the request.
- graph_node_count: usize,
- /// Graph edges materialized before the request.
- graph_edge_count: usize,
- /// Distinct WEAR validation keys retained before the request.
- wear_cache_entries: usize,
- /// Distinct MAT0 validation keys retained before the request.
- material_cache_entries: usize,
- /// Distinct TEXM validation keys retained before the request.
- texture_cache_entries: usize,
- },
- /// Prepare all reachable visual/resource dependencies.
- Assets,
- /// Decode and validate MSH model meshes.
- AssetModelMeshes,
- /// Decode WEAR material tables.
- AssetWearTables,
- /// Resolve MAT0 material documents.
- AssetMaterials,
- /// Decode diffuse textures and baked lightmaps.
- AssetTextures,
- /// Construct all object drafts before registration.
- Construct,
- /// Register constructed objects.
- Register,
-}
-
-/// Raw placed transform preserved by the mission loader.
-#[derive(Clone, Debug, PartialEq)]
-pub struct PlacedTransformProfile {
- /// Object index in TMA order.
- pub object_index: usize,
- /// Raw position vector.
- pub position: [f32; 3],
- /// Raw orientation vector. No Euler order is inferred here.
- pub orientation_raw: [f32; 3],
- /// Raw scale vector.
- pub scale: [f32; 3],
-}
-
-/// Mission loading trace.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct MissionLoadTrace {
- /// Observed phases in execution order.
- pub phases: Vec<MissionLoadPhase>,
- /// Number of object drafts constructed before the first registration.
- pub drafts_before_registration: usize,
- /// Number of objects registered.
- pub registered_objects: usize,
- /// Raw transform profile for placed objects.
- pub transforms: Vec<PlacedTransformProfile>,
-}
-
/// Ordered mission property preserved for later runtime stages.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MissionObjectProperty {
@@ -435,20 +284,10 @@ pub struct LoadedMission {
pub struct FrameResult {
/// Snapshot.
pub snapshot: WorldSnapshot,
- /// Scheduler phases executed for this frame.
- pub trace: FrameTrace,
-}
-
-/// Scheduler trace for a completed frame.
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct FrameTrace {
- /// Frame phases in execution order.
- pub phases: Vec<SchedulerPhase>,
}
/// Engine.
pub struct Engine {
- config: EngineConfig,
services: EngineServices,
world: World,
loaded: Option<LoadedMissionState>,
@@ -559,13 +398,6 @@ pub enum EngineError {
World(fparkan_world::WorldError),
/// Reference movement input or terrain query failed.
Movement(String),
- /// Scheduler phase order was violated.
- SchedulerPhaseOrder {
- /// Previous phase.
- previous: SchedulerPhase,
- /// Current phase.
- current: SchedulerPhase,
- },
/// Staged mission world was torn down after a registration-phase failure.
RegistrationTeardown {
/// Registered objects before the forced failure.
@@ -676,10 +508,6 @@ impl std::fmt::Display for EngineError {
}
Self::World(source) => write!(f, "{source}"),
Self::Movement(message) => write!(f, "reference movement: {message}"),
- Self::SchedulerPhaseOrder { previous, current } => write!(
- f,
- "scheduler phase order regressed from {previous:?} to {current:?}"
- ),
Self::RegistrationTeardown {
registered_objects,
released_objects,
@@ -710,22 +538,14 @@ impl std::error::Error for EngineError {
| Self::ScriptRuntime { .. }
| Self::Movement(_)
| Self::PrototypeGraph { .. }
- | Self::SchedulerPhaseOrder { .. }
| Self::RegistrationTeardown { .. } => None,
}
}
}
-/// Creates engine.
-///
-/// # Errors
-///
-/// Currently this constructor is infallible, but it returns
-/// [`EngineError`] to keep the composition-root API stable as services become
-/// mandatory.
-pub fn create(config: EngineConfig, services: EngineServices) -> Result<Engine, EngineError> {
+/// Creates an engine with the supplied services.
+pub fn create(services: EngineServices) -> Result<Engine, EngineError> {
Ok(Engine {
- config,
services,
world: new_world(WorldConfig),
loaded: None,
@@ -744,7 +564,7 @@ pub fn load_mission(
engine: &mut Engine,
request: MissionRequest,
) -> Result<LoadedMission, EngineError> {
- load_mission_with_trace(engine, request).map(|(loaded, _trace)| loaded)
+ load_mission_with_options(engine, request, MissionLoadOptions::default())
}
/// Loads a mission using the bounded static-preview asset scope.
@@ -763,123 +583,48 @@ pub fn load_mission_static_preview(
engine: &mut Engine,
request: MissionRequest,
) -> Result<LoadedMission, EngineError> {
- load_mission_static_preview_with_progress(engine, request, |_| {})
-}
-
-/// Loads a static preview for the first requested non-zero count of mission roots.
-///
-/// Normal gameplay still uses [`load_mission`] and always resolves every root.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as
-/// [`load_mission_static_preview`].
-pub fn load_mission_static_preview_roots(
- engine: &mut Engine,
- request: MissionRequest,
- root_count: NonZeroUsize,
-) -> Result<LoadedMission, EngineError> {
- load_mission_static_preview_roots_with_progress(engine, request, root_count, |_| {})
-}
-
-/// Loads selected static-preview roots while synchronously reporting phases.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as
-/// [`load_mission_static_preview_roots`].
-pub fn load_mission_static_preview_roots_with_progress(
- engine: &mut Engine,
- request: MissionRequest,
- root_count: NonZeroUsize,
- mut on_phase: impl FnMut(MissionLoadPhase),
-) -> Result<LoadedMission, EngineError> {
- load_mission_with_options_and_progress(
+ load_mission_with_options(
engine,
request,
MissionLoadOptions {
- asset_scope: MissionAssetScope::PreviewRoots(root_count),
+ asset_scope: MissionAssetScope::FirstMeshPreview,
..MissionLoadOptions::default()
},
- Some(&mut on_phase),
)
- .map(|(loaded, _trace)| loaded)
}
-/// Loads a static preview while synchronously reporting entered loading phases.
+/// Loads a static preview for the first requested non-zero count of mission roots.
///
-/// This has the same bounded asset scope as [`load_mission_static_preview`].
+/// Normal gameplay still uses [`load_mission`] and always resolves every root.
///
/// # Errors
///
/// Returns [`EngineError`] under the same conditions as
/// [`load_mission_static_preview`].
-pub fn load_mission_static_preview_with_progress(
+pub fn load_mission_static_preview_roots(
engine: &mut Engine,
request: MissionRequest,
- mut on_phase: impl FnMut(MissionLoadPhase),
+ root_count: NonZeroUsize,
) -> Result<LoadedMission, EngineError> {
- load_mission_with_options_and_progress(
+ load_mission_with_options(
engine,
request,
MissionLoadOptions {
- asset_scope: MissionAssetScope::FirstMeshPreview,
+ asset_scope: MissionAssetScope::PreviewRoots(root_count),
..MissionLoadOptions::default()
},
- Some(&mut on_phase),
- )
- .map(|(loaded, _trace)| loaded)
-}
-
-/// Loads mission transactionally and returns a diagnostic trace.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as [`load_mission`].
-pub fn load_mission_with_trace(
- engine: &mut Engine,
- request: MissionRequest,
-) -> Result<(LoadedMission, MissionLoadTrace), EngineError> {
- load_mission_with_options_and_progress(engine, request, MissionLoadOptions::default(), None)
-}
-
-/// Loads a mission while synchronously reporting each entered loading phase.
-///
-/// The observer runs immediately after the phase is recorded in the returned
-/// trace. It is intended for diagnostic progress reporting; it does not alter
-/// loader ordering, validation, or transaction behavior.
-///
-/// # Errors
-///
-/// Returns [`EngineError`] under the same conditions as [`load_mission`].
-pub fn load_mission_with_progress(
- engine: &mut Engine,
- request: MissionRequest,
- mut on_phase: impl FnMut(MissionLoadPhase),
-) -> Result<LoadedMission, EngineError> {
- load_mission_with_options_and_progress(
- engine,
- request,
- MissionLoadOptions::default(),
- Some(&mut on_phase),
)
- .map(|(loaded, _trace)| loaded)
}
-#[allow(clippy::too_many_lines)]
-fn load_mission_with_options_and_progress(
+fn load_mission_with_options(
engine: &mut Engine,
request: MissionRequest,
options: MissionLoadOptions,
- mut on_phase: Option<&mut dyn FnMut(MissionLoadPhase)>,
-) -> Result<(LoadedMission, MissionLoadTrace), EngineError> {
- let mut trace = MissionLoadTrace::default();
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Context);
+) -> Result<LoadedMission, EngineError> {
let vfs = engine.services.vfs.clone().ok_or(EngineError::MissingVfs)?;
let mission_path = normalize_engine_path("mission", &request.key)?;
let mission_bytes = read_vfs(&vfs, &mission_path)?;
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Map);
let land_path =
decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| {
EngineError::Mission {
@@ -919,7 +664,6 @@ fn load_mission_with_options_and_progress(
TerrainPreparationError::Runtime(source) => EngineError::Terrain(source),
}
})?;
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Tma);
let mission = decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| {
EngineError::Mission {
path: mission_path.as_str().to_string(),
@@ -927,18 +671,6 @@ fn load_mission_with_options_and_progress(
}
})?;
let loaded_scripts = load_mission_scripts(&vfs, &mission)?;
- trace.transforms = mission
- .objects
- .iter()
- .enumerate()
- .map(|(object_index, object)| PlacedTransformProfile {
- object_index,
- position: object.position,
- orientation_raw: object.orientation,
- scale: object.scale,
- })
- .collect();
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Graph);
let repository = CachedResourceRepository::with_payload_cache_budget(
vfs.clone(),
MISSION_DECODED_PAYLOAD_CACHE_ENTRIES,
@@ -957,93 +689,34 @@ fn load_mission_with_options_and_progress(
};
let (mut prototype_graph, resolved_prototypes, mut prototype_report) =
build_prototype_graph_report(&repository, vfs.as_ref(), scoped_graph_roots);
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::GraphVisuals);
- let mut last_graph_visual_phase = None;
- extend_graph_report_with_visual_dependencies_with_progress(
+ extend_graph_report_with_visual_dependencies(
&repository,
&mut prototype_report,
&mut prototype_graph,
&resolved_prototypes,
- |progress| {
- let runtime_phase = match progress.phase {
- VisualDependencyPhase::Wear => MissionLoadPhase::GraphVisualWears,
- VisualDependencyPhase::Material => MissionLoadPhase::GraphVisualMaterials,
- VisualDependencyPhase::Texture => MissionLoadPhase::GraphVisualTextures,
- };
- if last_graph_visual_phase != Some(runtime_phase) {
- record_load_phase(&mut trace, &mut on_phase, runtime_phase);
- last_graph_visual_phase = Some(runtime_phase);
- }
- if progress.request_count > 1 {
- let request_phase = match progress.phase {
- VisualDependencyPhase::Wear => None,
- VisualDependencyPhase::Material => {
- Some(MissionLoadPhase::GraphVisualMaterialRequests {
- request_count: progress.request_count,
- graph_node_count: progress.graph_node_count,
- graph_edge_count: progress.graph_edge_count,
- wear_cache_entries: progress.cache_entries.wear_entries,
- material_cache_entries: progress.cache_entries.material_entries,
- texture_cache_entries: progress.cache_entries.texture_entries,
- })
- }
- VisualDependencyPhase::Texture => {
- Some(MissionLoadPhase::GraphVisualTextureRequests {
- request_count: progress.request_count,
- graph_node_count: progress.graph_node_count,
- graph_edge_count: progress.graph_edge_count,
- wear_cache_entries: progress.cache_entries.wear_entries,
- material_cache_entries: progress.cache_entries.material_entries,
- texture_cache_entries: progress.cache_entries.texture_entries,
- })
- }
- };
- if let Some(request_phase) = request_phase {
- record_load_phase(&mut trace, &mut on_phase, request_phase);
- }
- }
- },
);
if !prototype_report.is_success() {
return Err(EngineError::PrototypeGraph {
failures: prototype_report.failures.clone(),
});
}
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Assets);
let asset_manager = AssetManager::new(repository);
- let mut last_asset_phase = None;
- let mut observe_asset_phase = |phase| {
- let runtime_phase = match phase {
- AssetPreparationPhase::ModelMesh => MissionLoadPhase::AssetModelMeshes,
- AssetPreparationPhase::WearTable => MissionLoadPhase::AssetWearTables,
- AssetPreparationPhase::Materials => MissionLoadPhase::AssetMaterials,
- AssetPreparationPhase::Textures => MissionLoadPhase::AssetTextures,
- };
- if last_asset_phase != Some(runtime_phase) {
- record_load_phase(&mut trace, &mut on_phase, runtime_phase);
- last_asset_phase = Some(runtime_phase);
- }
- };
let mission_assets = match options.asset_scope {
- MissionAssetScope::Full => asset_manager.prepare_mission_assets_with_progress(
+ MissionAssetScope::Full => asset_manager.prepare_mission_assets(
&prototype_graph.root_prototype_request_spans,
&resolved_prototypes,
- &mut observe_asset_phase,
),
MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets(
&asset_manager,
&prototype_graph.root_prototype_request_spans,
&resolved_prototypes,
- &mut observe_asset_phase,
),
- MissionAssetScope::PreviewRoots(root_count) => asset_manager
- .prepare_mission_assets_with_progress(
- &prototype_graph.root_prototype_request_spans[..root_count
- .get()
- .min(prototype_graph.root_prototype_request_spans.len())],
- &resolved_prototypes,
- &mut observe_asset_phase,
- ),
+ MissionAssetScope::PreviewRoots(root_count) => asset_manager.prepare_mission_assets(
+ &prototype_graph.root_prototype_request_spans[..root_count
+ .get()
+ .min(prototype_graph.root_prototype_request_spans.len())],
+ &resolved_prototypes,
+ ),
}
.map_err(|source| EngineError::AssetPreparation {
mission: request.key.clone(),
@@ -1079,7 +752,6 @@ fn load_mission_with_options_and_progress(
.collect();
let mut new_runtime_world = new_world(WorldConfig);
let mut handles = Vec::with_capacity(mission.objects.len());
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Construct);
for (index, object) in mission.objects.iter().enumerate() {
let original_id = u32::try_from(index).ok().map(OriginalObjectId);
let handle = construct_object(&mut new_runtime_world, ObjectDraft { original_id })?;
@@ -1094,19 +766,18 @@ fn load_mission_with_options_and_progress(
)?;
handles.push(handle);
}
- trace.drafts_before_registration = handles.len();
- record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Register);
+ let mut registered_objects = 0;
for handle in &handles {
- if options.fail_after_registered_objects == Some(trace.registered_objects) {
+ if options.fail_after_registered_objects == Some(registered_objects) {
let report = fparkan_world::shutdown(new_runtime_world);
return Err(EngineError::RegistrationTeardown {
- registered_objects: trace.registered_objects,
+ registered_objects,
released_objects: report.released_objects.len(),
managers_released: report.managers_released,
});
}
register_object(&mut new_runtime_world, *handle)?;
- trace.registered_objects += 1;
+ registered_objects += 1;
}
let summary = LoadedMission {
@@ -1174,32 +845,17 @@ fn load_mission_with_options_and_progress(
script_varset_states: loaded_scripts.varset_states,
script_outcome: MissionScriptOutcome::Active,
});
- Ok((summary, trace))
-}
-
-fn record_load_phase(
- trace: &mut MissionLoadTrace,
- on_phase: &mut Option<&mut dyn FnMut(MissionLoadPhase)>,
- phase: MissionLoadPhase,
-) {
- trace.phases.push(phase);
- if let Some(observer) = on_phase.as_deref_mut() {
- observer(phase);
- }
+ Ok(summary)
}
fn prepare_first_preview_assets<R: ResourceRepository>(
asset_manager: &AssetManager<R>,
root_spans: &[std::ops::Range<usize>],
prototypes: &[fparkan_prototype::EffectivePrototype],
- on_phase: &mut dyn FnMut(AssetPreparationPhase),
) -> Result<MissionAssets, AssetPreparationError> {
for span in root_spans {
- let assets = asset_manager.prepare_mission_assets_with_progress(
- std::slice::from_ref(span),
- prototypes,
- &mut *on_phase,
- )?;
+ let assets =
+ asset_manager.prepare_mission_assets(std::slice::from_ref(span), prototypes)?;
if !assets.models.is_empty() {
return Ok(assets);
}
@@ -1216,7 +872,9 @@ pub fn step_headless(
engine: &mut Engine,
input: InputSnapshot,
) -> Result<FrameResult, EngineError> {
- run_frame(engine, input, SchedulerPresentation::Headless)
+ Ok(FrameResult {
+ snapshot: step(&mut engine.world, &input)?,
+ })
}
/// Steps rendered mode.
@@ -1225,20 +883,7 @@ pub fn step_headless(
///
/// Returns [`EngineError`] when the world step fails.
pub fn frame(engine: &mut Engine) -> Result<FrameResult, EngineError> {
- match engine.config.mode {
- EngineMode::Headless => step_headless(engine, InputSnapshot),
- EngineMode::Rendered => run_frame(engine, InputSnapshot, SchedulerPresentation::Rendered),
- }
-}
-
-/// Shuts down engine.
-///
-/// # Errors
-///
-/// Currently shutdown is infallible, but the `Result` preserves the lifecycle
-/// API for future service teardown failures.
-pub fn shutdown(_engine: Engine) -> Result<(), EngineError> {
- Ok(())
+ step_headless(engine, InputSnapshot)
}
/// Returns the loaded mission summary.
@@ -1451,78 +1096,6 @@ pub fn loaded_mission_object_drafts(engine: &Engine) -> Option<&[MissionObjectDr
.map(|state| state.object_drafts.as_slice())
}
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum SchedulerPresentation {
- Headless,
- Rendered,
-}
-
-#[derive(Clone, Debug, Default)]
-struct Scheduler {
- phase: Option<SchedulerPhase>,
- trace: FrameTrace,
-}
-
-impl Scheduler {
- fn enter(&mut self, phase: SchedulerPhase) -> Result<(), EngineError> {
- if let Some(previous) = self.phase {
- if scheduler_phase_index(phase) <= scheduler_phase_index(previous) {
- return Err(EngineError::SchedulerPhaseOrder {
- previous,
- current: phase,
- });
- }
- }
- self.phase = Some(phase);
- self.trace.phases.push(phase);
- Ok(())
- }
-
- fn finish(self) -> FrameTrace {
- self.trace
- }
-}
-
-fn run_frame(
- engine: &mut Engine,
- input: InputSnapshot,
- presentation: SchedulerPresentation,
-) -> Result<FrameResult, EngineError> {
- let mut scheduler = Scheduler::default();
- scheduler.enter(SchedulerPhase::CollectPlatformEvents)?;
- scheduler.enter(SchedulerPhase::BuildInputSnapshot)?;
- scheduler.enter(SchedulerPhase::AdvanceGameClock)?;
- scheduler.enter(SchedulerPhase::CalculateWorldQueue)?;
- let snapshot = step(&mut engine.world, &input)?;
- scheduler.enter(SchedulerPhase::ApplyDeferredOperations)?;
- scheduler.enter(SchedulerPhase::UpdateAnimationAndEffects)?;
- if presentation == SchedulerPresentation::Rendered {
- scheduler.enter(SchedulerPhase::PublishRenderSnapshot)?;
- scheduler.enter(SchedulerPhase::RenderWorld)?;
- }
- scheduler.enter(SchedulerPhase::EndFrameCallbacks)?;
- scheduler.enter(SchedulerPhase::Maintenance)?;
- Ok(FrameResult {
- snapshot,
- trace: scheduler.finish(),
- })
-}
-
-fn scheduler_phase_index(phase: SchedulerPhase) -> u8 {
- match phase {
- SchedulerPhase::CollectPlatformEvents => 0,
- SchedulerPhase::BuildInputSnapshot => 1,
- SchedulerPhase::AdvanceGameClock => 2,
- SchedulerPhase::CalculateWorldQueue => 3,
- SchedulerPhase::ApplyDeferredOperations => 4,
- SchedulerPhase::UpdateAnimationAndEffects => 5,
- SchedulerPhase::PublishRenderSnapshot => 6,
- SchedulerPhase::RenderWorld => 7,
- SchedulerPhase::EndFrameCallbacks => 8,
- SchedulerPhase::Maintenance => 9,
- }
-}
-
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
EngineError::Path {
@@ -1702,7 +1275,7 @@ fn handler19_x87_truncated_u32(
}
// ai.dll's __ftol saves the x87 control word, sets rounding-control bits
// to truncate, performs `fistp qword`, and restores the control word.
- #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+
let word = value.trunc() as u32;
Ok(word)
}
@@ -1758,13 +1331,7 @@ mod tests {
#[test]
fn load_mission_requires_vfs_and_keeps_world_unchanged_on_error() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::default()).expect("engine");
let before = step_headless(&mut engine, InputSnapshot).expect("step");
let err = load_mission(
&mut engine,
@@ -1779,30 +1346,6 @@ mod tests {
}
#[test]
- fn load_progress_reports_context_before_missing_vfs_error() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
- let mut phases = Vec::new();
-
- let err = load_mission_with_progress(
- &mut engine,
- MissionRequest {
- key: "MISSIONS/Autodemo.00/data.tma".to_string(),
- },
- |phase| phases.push(phase),
- )
- .expect_err("missing VFS");
-
- assert!(matches!(err, EngineError::MissingVfs));
- assert_eq!(phases, vec![MissionLoadPhase::Context]);
- }
-
- #[test]
fn script_varset_prefers_bundle_adjacent_file_then_uses_shared_fallback() {
let mut personal_files = MemoryVfs::default();
personal_files.insert(
@@ -1997,87 +1540,8 @@ mod tests {
}
#[test]
- fn headless_scheduler_trace_skips_presentation_phases() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
-
- let result = frame(&mut engine).expect("frame");
-
- assert_eq!(result.snapshot.tick.0, 1);
- assert_eq!(
- result.trace.phases,
- vec![
- SchedulerPhase::CollectPlatformEvents,
- SchedulerPhase::BuildInputSnapshot,
- SchedulerPhase::AdvanceGameClock,
- SchedulerPhase::CalculateWorldQueue,
- SchedulerPhase::ApplyDeferredOperations,
- SchedulerPhase::UpdateAnimationAndEffects,
- SchedulerPhase::EndFrameCallbacks,
- SchedulerPhase::Maintenance,
- ]
- );
- }
-
- #[test]
- fn rendered_scheduler_trace_includes_presentation_after_simulation() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Rendered,
- },
- EngineServices::default(),
- )
- .expect("engine");
-
- let result = frame(&mut engine).expect("frame");
-
- assert_eq!(
- result.trace.phases,
- vec![
- SchedulerPhase::CollectPlatformEvents,
- SchedulerPhase::BuildInputSnapshot,
- SchedulerPhase::AdvanceGameClock,
- SchedulerPhase::CalculateWorldQueue,
- SchedulerPhase::ApplyDeferredOperations,
- SchedulerPhase::UpdateAnimationAndEffects,
- SchedulerPhase::PublishRenderSnapshot,
- SchedulerPhase::RenderWorld,
- SchedulerPhase::EndFrameCallbacks,
- SchedulerPhase::Maintenance,
- ]
- );
- }
-
- #[test]
- fn scheduler_rejects_phase_regressions() {
- let mut scheduler = Scheduler::default();
- scheduler
- .enter(SchedulerPhase::BuildInputSnapshot)
- .expect("enter build input");
-
- assert!(matches!(
- scheduler.enter(SchedulerPhase::CollectPlatformEvents),
- Err(EngineError::SchedulerPhaseOrder {
- previous: SchedulerPhase::BuildInputSnapshot,
- current: SchedulerPhase::CollectPlatformEvents,
- })
- ));
- }
-
- #[test]
fn reference_movement_rejects_non_finite_or_non_positive_input() {
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::default(),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::default()).expect("engine");
let err =
advance_reference_movement(&mut engine, OriginalObjectId(0), [f32::NAN, 0.0], 1.0)
@@ -2095,59 +1559,10 @@ mod tests {
#[test]
#[ignore = "requires licensed corpus"]
- fn load_trace_records_preparation_before_registration_and_raw_transforms() {
- let root = licensed_root("IS");
- let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
-
- let (loaded, trace) = load_mission_with_trace(
- &mut engine,
- MissionRequest {
- key: "MISSIONS/Autodemo.00/data.tma".to_string(),
- },
- )
- .expect("load mission with trace");
-
- assert_eq!(
- trace.phases,
- vec![
- MissionLoadPhase::Context,
- MissionLoadPhase::Map,
- MissionLoadPhase::Tma,
- MissionLoadPhase::Graph,
- MissionLoadPhase::GraphVisuals,
- MissionLoadPhase::Assets,
- MissionLoadPhase::Construct,
- MissionLoadPhase::Register,
- ]
- );
- assert_eq!(trace.drafts_before_registration, loaded.object_count);
- assert_eq!(trace.registered_objects, loaded.object_count);
- assert_eq!(trace.transforms.len(), loaded.object_count);
- assert!(trace.transforms.iter().all(|transform| transform
- .orientation_raw
- .iter()
- .all(|component| component.is_finite())));
- }
-
- #[test]
- #[ignore = "requires licensed corpus"]
fn reference_movement_snaps_a_live_mission_object_to_terrain() {
let root = licensed_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
load_mission(
&mut engine,
MissionRequest {
@@ -2221,13 +1636,7 @@ mod tests {
inner: DirectoryVfs::new(&root),
denied,
});
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let before = step_headless(&mut engine, InputSnapshot).expect("before");
let err = load_mission(&mut engine, mission).expect_err("load error");
match denied {
@@ -2248,16 +1657,10 @@ mod tests {
fn registration_phase_failure_uses_normal_teardown_and_keeps_engine_world() {
let root = licensed_root("IS");
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let before = step_headless(&mut engine, InputSnapshot).expect("before");
- let err = load_mission_with_options_and_progress(
+ let err = load_mission_with_options(
&mut engine,
MissionRequest {
key: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma".to_string(),
@@ -2266,7 +1669,6 @@ mod tests {
fail_after_registered_objects: Some(1),
..MissionLoadOptions::default()
},
- None,
)
.expect_err("forced registration failure");
@@ -2326,13 +1728,7 @@ mod tests {
] {
let root = local_testdata_root(case.0);
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
- let mut engine = create(
- EngineConfig {
- mode: EngineMode::Headless,
- },
- EngineServices::new(vfs),
- )
- .expect("engine");
+ let mut engine = create(EngineServices::new(vfs)).expect("engine");
let loaded = load_mission(
&mut engine,
MissionRequest {
@@ -2365,7 +1761,7 @@ mod tests {
#[test]
#[ignore = "requires licensed corpus"]
- fn licensed_corpora_load_all_mission_foundations() {
+ fn licensed_corpus_part1_loads_all_mission_foundations() {
let part1 = load_all(&licensed_root("IS"));
assert_eq!(part1.missions, 29);
assert_eq!(part1.paths, 34);
@@ -2385,7 +1781,11 @@ mod tests {
assert_eq!(part1.material_slots, part1.material_resolved);
assert_eq!(part1.texture_requests, part1.texture_resolved);
assert_eq!(part1.lightmap_requests, part1.lightmap_resolved);
+ }
+ #[test]
+ #[ignore = "requires licensed corpus"]
+ fn licensed_corpus_part2_loads_all_mission_foundations() {
let part2 = load_all(&licensed_root("IS2"));
assert_eq!(part2.missions, 31);
assert_eq!(part2.paths, 61);
@@ -2440,13 +1840,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 +1868,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);