feat(runtime): apply clan init varsets

This commit is contained in:
2026-07-18 22:18:49 +04:00
parent d91e63fa9a
commit 14096788fc
3 changed files with 94 additions and 4 deletions
+2 -1
View File
@@ -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
);
}
+86
View File
@@ -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]
+6 -3
View File
@@ -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