feat(runtime): apply clan init varsets
This commit is contained in:
@@ -56,7 +56,7 @@ fn run() -> Result<(), String> {
|
|||||||
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
let loaded = load_mission(&mut engine, MissionRequest { key: mission })
|
||||||
.map_err(|err| format!("{err}"))?;
|
.map_err(|err| format!("{err}"))?;
|
||||||
println!(
|
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.object_count,
|
||||||
loaded.areal_count,
|
loaded.areal_count,
|
||||||
loaded.surface_count,
|
loaded.surface_count,
|
||||||
@@ -70,6 +70,7 @@ fn run() -> Result<(), String> {
|
|||||||
loaded.script_event_count,
|
loaded.script_event_count,
|
||||||
loaded.script_varset_declaration_count,
|
loaded.script_varset_declaration_count,
|
||||||
loaded.script_init_state_count,
|
loaded.script_init_state_count,
|
||||||
|
loaded.script_varset_state_count,
|
||||||
loaded.graph_failure_count
|
loaded.graph_failure_count
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use fparkan_prototype::{
|
|||||||
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
||||||
use fparkan_script::{
|
use fparkan_script::{
|
||||||
Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptPackage, VarSet,
|
Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptPackage, VarSet,
|
||||||
|
VarSetDefault,
|
||||||
};
|
};
|
||||||
use fparkan_terrain::SurfaceQuery;
|
use fparkan_terrain::SurfaceQuery;
|
||||||
use fparkan_vfs::{Vfs, VfsError};
|
use fparkan_vfs::{Vfs, VfsError};
|
||||||
@@ -285,6 +286,18 @@ pub struct MissionScriptInitState {
|
|||||||
pub writes: Vec<Handler19DwordWrite>,
|
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)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
struct MissionLoadOptions {
|
struct MissionLoadOptions {
|
||||||
fail_after_registered_objects: Option<usize>,
|
fail_after_registered_objects: Option<usize>,
|
||||||
@@ -296,6 +309,7 @@ struct LoadedMissionScripts {
|
|||||||
bundles: Vec<MissionScriptBundle>,
|
bundles: Vec<MissionScriptBundle>,
|
||||||
varset: Option<MissionScriptVarSet>,
|
varset: Option<MissionScriptVarSet>,
|
||||||
init_states: Vec<MissionScriptInitState>,
|
init_states: Vec<MissionScriptInitState>,
|
||||||
|
varset_states: Vec<MissionScriptVarSetState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects how much of the resolved mission asset graph to prepare.
|
/// Selects how much of the resolved mission asset graph to prepare.
|
||||||
@@ -387,6 +401,8 @@ pub struct LoadedMission {
|
|||||||
pub script_varset_declaration_count: usize,
|
pub script_varset_declaration_count: usize,
|
||||||
/// `Handler(19)` Init results resolved for mission clans.
|
/// `Handler(19)` Init results resolved for mission clans.
|
||||||
pub script_init_state_count: usize,
|
pub script_init_state_count: usize,
|
||||||
|
/// Instantiated per-clan varsets whose Init writes have been applied.
|
||||||
|
pub script_varset_state_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frame result.
|
/// Frame result.
|
||||||
@@ -426,6 +442,7 @@ struct LoadedMissionState {
|
|||||||
script_bundles: Vec<MissionScriptBundle>,
|
script_bundles: Vec<MissionScriptBundle>,
|
||||||
script_varset: Option<MissionScriptVarSet>,
|
script_varset: Option<MissionScriptVarSet>,
|
||||||
script_init_states: Vec<MissionScriptInitState>,
|
script_init_states: Vec<MissionScriptInitState>,
|
||||||
|
script_varset_states: Vec<MissionScriptVarSetState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Engine error.
|
/// Engine error.
|
||||||
@@ -1099,6 +1116,7 @@ fn load_mission_with_options_and_progress(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0, |varset| varset.declarations.declarations.len()),
|
.map_or(0, |varset| varset.declarations.declarations.len()),
|
||||||
script_init_state_count: loaded_scripts.init_states.len(),
|
script_init_state_count: loaded_scripts.init_states.len(),
|
||||||
|
script_varset_state_count: loaded_scripts.varset_states.len(),
|
||||||
};
|
};
|
||||||
|
|
||||||
engine.world = new_runtime_world;
|
engine.world = new_runtime_world;
|
||||||
@@ -1115,6 +1133,7 @@ fn load_mission_with_options_and_progress(
|
|||||||
script_bundles: loaded_scripts.bundles,
|
script_bundles: loaded_scripts.bundles,
|
||||||
script_varset: loaded_scripts.varset,
|
script_varset: loaded_scripts.varset,
|
||||||
script_init_states: loaded_scripts.init_states,
|
script_init_states: loaded_scripts.init_states,
|
||||||
|
script_varset_states: loaded_scripts.varset_states,
|
||||||
});
|
});
|
||||||
Ok((summary, trace))
|
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())
|
.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.
|
/// Returns terrain runtime data for the loaded mission.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn loaded_terrain(engine: &Engine) -> Option<&TerrainWorld> {
|
pub fn loaded_terrain(engine: &Engine) -> Option<&TerrainWorld> {
|
||||||
@@ -1401,13 +1429,58 @@ fn load_mission_scripts(
|
|||||||
)?,
|
)?,
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
|
let varset_states = match &varset {
|
||||||
|
Some(varset) => materialize_script_varset_states(&bundles, varset, &init_states)?,
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
Ok(LoadedMissionScripts {
|
Ok(LoadedMissionScripts {
|
||||||
bundles,
|
bundles,
|
||||||
varset,
|
varset,
|
||||||
init_states,
|
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(
|
fn resolve_handler19_init_states(
|
||||||
clan_anchors: &[[f32; 2]],
|
clan_anchors: &[[f32; 2]],
|
||||||
bundles: &[MissionScriptBundle],
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -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
|
`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
|
with its zero-based clan index. For every `Init` instruction whose selector is
|
||||||
`Handler(19)`, `VarSet::resolve_handler19` produces the three per-clan DWORD
|
`Handler(19)`, `VarSet::resolve_handler19` produces the three per-clan DWORD
|
||||||
immutable `MissionScriptInitState` writes. Other Init selectors and all other events remain decoded but
|
writes. The runtime then materializes an independent declaration-ordered value
|
||||||
unexecuted. AutoDemo validates the path end-to-end: its non-integral first
|
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
|
anchor (`500.2857`) yields captured `ClanBaseX=500`, and the live GOG process
|
||||||
contains two initialized SuperAI entries `(500, 752, 0)` and `(728, 449, 1)`;
|
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
|
`GetSuperAI` returns element `n` of the 64-pointer global table at preferred
|
||||||
`ai.dll + 0x55398` for `n <= 63`. The read-only
|
`ai.dll + 0x55398` for `n <= 63`. The read-only
|
||||||
|
|||||||
Reference in New Issue
Block a user