aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/fparkan-headless/src/main.rs3
-rw-r--r--crates/fparkan-runtime/src/lib.rs86
-rw-r--r--docs/appendices/script-vm.md9
3 files changed, 94 insertions, 4 deletions
diff --git a/apps/fparkan-headless/src/main.rs b/apps/fparkan-headless/src/main.rs
index 0da9af9..9c5ab52 100644
--- a/apps/fparkan-headless/src/main.rs
+++ b/apps/fparkan-headless/src/main.rs
@@ -56,7 +56,7 @@ fn run() -> Result<(), String> {
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
.map_err(|err| format!("{err}"))?;
println!(
- "mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} scripts={} script_events={} script_varset_declarations={} script_init_states={} graph_failures={}",
+ "mission objects={} areals={} surfaces={} graph_roots={} components={} wear={} material_slots={} textures={} lightmaps={} scripts={} script_events={} script_varset_declarations={} script_init_states={} script_varset_states={} graph_failures={}",
loaded.object_count,
loaded.areal_count,
loaded.surface_count,
@@ -70,6 +70,7 @@ fn run() -> Result<(), String> {
loaded.script_event_count,
loaded.script_varset_declaration_count,
loaded.script_init_state_count,
+ loaded.script_varset_state_count,
loaded.graph_failure_count
);
}
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index 8095925..771a1d4 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -36,6 +36,7 @@ use fparkan_prototype::{
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_script::{
Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptPackage, VarSet,
+ VarSetDefault,
};
use fparkan_terrain::SurfaceQuery;
use fparkan_vfs::{Vfs, VfsError};
@@ -285,6 +286,18 @@ pub struct MissionScriptInitState {
pub writes: Vec<Handler19DwordWrite>,
}
+/// Per-clan mutable-value model of one instantiated mission varset.
+///
+/// Declarations remain shared source metadata; these values are the runtime
+/// cells that later recovered handlers will read and write.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MissionScriptVarSetState {
+ /// TMA clan index owning this instantiated varset.
+ pub clan_index: usize,
+ /// Values in the original varset declaration order.
+ pub values: Vec<VarSetDefault>,
+}
+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct MissionLoadOptions {
fail_after_registered_objects: Option<usize>,
@@ -296,6 +309,7 @@ struct LoadedMissionScripts {
bundles: Vec<MissionScriptBundle>,
varset: Option<MissionScriptVarSet>,
init_states: Vec<MissionScriptInitState>,
+ varset_states: Vec<MissionScriptVarSetState>,
}
/// Selects how much of the resolved mission asset graph to prepare.
@@ -387,6 +401,8 @@ pub struct LoadedMission {
pub script_varset_declaration_count: usize,
/// `Handler(19)` Init results resolved for mission clans.
pub script_init_state_count: usize,
+ /// Instantiated per-clan varsets whose Init writes have been applied.
+ pub script_varset_state_count: usize,
}
/// Frame result.
@@ -426,6 +442,7 @@ struct LoadedMissionState {
script_bundles: Vec<MissionScriptBundle>,
script_varset: Option<MissionScriptVarSet>,
script_init_states: Vec<MissionScriptInitState>,
+ script_varset_states: Vec<MissionScriptVarSetState>,
}
/// Engine error.
@@ -1099,6 +1116,7 @@ fn load_mission_with_options_and_progress(
.as_ref()
.map_or(0, |varset| varset.declarations.declarations.len()),
script_init_state_count: loaded_scripts.init_states.len(),
+ script_varset_state_count: loaded_scripts.varset_states.len(),
};
engine.world = new_runtime_world;
@@ -1115,6 +1133,7 @@ fn load_mission_with_options_and_progress(
script_bundles: loaded_scripts.bundles,
script_varset: loaded_scripts.varset,
script_init_states: loaded_scripts.init_states,
+ script_varset_states: loaded_scripts.varset_states,
});
Ok((summary, trace))
}
@@ -1225,6 +1244,15 @@ pub fn loaded_mission_script_init_states(engine: &Engine) -> Option<&[MissionScr
.map(|state| state.script_init_states.as_slice())
}
+/// Returns instantiated per-clan varsets after proven Init writes are applied.
+#[must_use]
+pub fn loaded_mission_script_varset_states(engine: &Engine) -> Option<&[MissionScriptVarSetState]> {
+ engine
+ .loaded
+ .as_ref()
+ .map(|state| state.script_varset_states.as_slice())
+}
+
/// Returns terrain runtime data for the loaded mission.
#[must_use]
pub fn loaded_terrain(engine: &Engine) -> Option<&TerrainWorld> {
@@ -1401,13 +1429,58 @@ fn load_mission_scripts(
)?,
None => Vec::new(),
};
+ let varset_states = match &varset {
+ Some(varset) => materialize_script_varset_states(&bundles, varset, &init_states)?,
+ None => Vec::new(),
+ };
Ok(LoadedMissionScripts {
bundles,
varset,
init_states,
+ varset_states,
})
}
+fn materialize_script_varset_states(
+ bundles: &[MissionScriptBundle],
+ varset: &MissionScriptVarSet,
+ init_states: &[MissionScriptInitState],
+) -> Result<Vec<MissionScriptVarSetState>, EngineError> {
+ let defaults: Vec<_> = varset
+ .declarations
+ .declarations
+ .iter()
+ .map(|declaration| declaration.default_value)
+ .collect();
+ bundles
+ .iter()
+ .map(|bundle| {
+ let mut values = defaults.clone();
+ for init in init_states
+ .iter()
+ .filter(|init| init.clan_index == bundle.clan_index)
+ {
+ for write in &init.writes {
+ let Some(value) = values.get_mut(write.index as usize) else {
+ return Err(EngineError::ScriptInit {
+ clan_index: bundle.clan_index,
+ message: format!(
+ "Handler(19) write index {} is outside the instantiated varset",
+ write.index
+ ),
+ });
+ };
+ *value = VarSetDefault::Dword(write.value);
+ }
+ }
+ Ok(MissionScriptVarSetState {
+ clan_index: bundle.clan_index,
+ values,
+ })
+ })
+ .collect()
+}
+
fn resolve_handler19_init_states(
clan_anchors: &[[f32; 2]],
bundles: &[MissionScriptBundle],
@@ -1680,6 +1753,19 @@ mod tests {
],
}]
);
+ let runtime_varsets = materialize_script_varset_states(&bundles, &varset, &states)
+ .expect("materialized per-clan varset");
+ assert_eq!(
+ runtime_varsets,
+ vec![MissionScriptVarSetState {
+ clan_index: 1,
+ values: vec![
+ VarSetDefault::Dword(728),
+ VarSetDefault::Dword(449),
+ VarSetDefault::Dword(1),
+ ],
+ }]
+ );
}
#[test]
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md
index dbcd539..59c1512 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -204,11 +204,14 @@ during mission loading: each selected clan's TMA anchor is accepted only in the
`0..=10000` base range, truncated through the recovered x87 rule, and paired
with its zero-based clan index. For every `Init` instruction whose selector is
`Handler(19)`, `VarSet::resolve_handler19` produces the three per-clan DWORD
-immutable `MissionScriptInitState` writes. Other Init selectors and all other events remain decoded but
-unexecuted. AutoDemo validates the path end-to-end: its non-integral first
+writes. The runtime then materializes an independent declaration-ordered value
+array for each selected clan and applies those writes, so later recovered
+handlers can consume `ClanBaseX`, `ClanBaseY`, and `ClanID` as runtime cells
+rather than loader defaults. Other Init selectors and all other events remain
+decoded but unexecuted. AutoDemo validates the path end-to-end: its non-integral first
anchor (`500.2857`) yields captured `ClanBaseX=500`, and the live GOG process
contains two initialized SuperAI entries `(500, 752, 0)` and `(728, 449, 1)`;
-the Rust loader reports `script_init_states=2`.
+the Rust loader reports `script_init_states=2` and `script_varset_states=2`.
`GetSuperAI` returns element `n` of the 64-pointer global table at preferred
`ai.dll + 0x55398` for `n <= 63`. The read-only