feat(runtime): apply mission script outcomes

This commit is contained in:
2026-07-18 22:36:38 +04:00
parent 70c7afe25c
commit f49010c723
2 changed files with 141 additions and 0 deletions
+134
View File
@@ -298,6 +298,31 @@ pub struct MissionScriptVarSetState {
pub values: Vec<VarSetDefault>,
}
/// Recovered mission outcome controlled by the `Iron3D` script host callback.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum MissionScriptOutcome {
/// No recovered callback has ended the mission.
#[default]
Active,
/// `Handler(30)` reached the proven mission-complete callback branch.
Completed,
/// `Handler(30)` reached the proven mission-fail callback branch.
Failed,
}
/// Result of applying one recovered script host callback command.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MissionScriptHostEffect {
/// The callback did not match a currently recovered host branch.
Unhandled,
/// A previously active or failed mission became completed.
MissionCompleted,
/// A previously active or completed mission became failed.
MissionFailed,
/// The recovered target state was already current, so `Iron3D` has no state transition.
NoChange,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct MissionLoadOptions {
fail_after_registered_objects: Option<usize>,
@@ -443,6 +468,7 @@ struct LoadedMissionState {
script_varset: Option<MissionScriptVarSet>,
script_init_states: Vec<MissionScriptInitState>,
script_varset_states: Vec<MissionScriptVarSetState>,
script_outcome: MissionScriptOutcome,
}
/// Engine error.
@@ -1146,6 +1172,7 @@ fn load_mission_with_options_and_progress(
script_varset: loaded_scripts.varset,
script_init_states: loaded_scripts.init_states,
script_varset_states: loaded_scripts.varset_states,
script_outcome: MissionScriptOutcome::Active,
});
Ok((summary, trace))
}
@@ -1306,6 +1333,62 @@ pub fn resolve_loaded_handler30(
resolve_handler30_from_varset_state(declarations, values, instruction)
}
/// Returns the currently recovered mission outcome, if a mission is loaded.
#[must_use]
pub fn loaded_mission_script_outcome(engine: &Engine) -> Option<MissionScriptOutcome> {
engine.loaded.as_ref().map(|state| state.script_outcome)
}
/// Applies the recovered `Iron3D` host-callback mission branch.
///
/// Only `(mode=0, command=0, payload=0|1)` has a proven independent meaning:
/// payload zero marks failure and payload one marks completion. Other callback
/// commands are retained as [`MissionScriptHostEffect::Unhandled`] until their
/// `Iron3D` consumer branches are recovered.
///
/// # Errors
///
/// Returns an error if no mission is loaded.
pub fn apply_loaded_script_host_callback(
engine: &mut Engine,
command: VmHostCallbackCommand,
) -> Result<MissionScriptHostEffect, EngineError> {
let state = engine
.loaded
.as_mut()
.ok_or_else(|| EngineError::ScriptRuntime {
clan_index: 0,
message: "mission script host state is unavailable".to_string(),
})?;
Ok(apply_recovered_script_host_callback(
&mut state.script_outcome,
command,
))
}
fn apply_recovered_script_host_callback(
outcome: &mut MissionScriptOutcome,
command: VmHostCallbackCommand,
) -> MissionScriptHostEffect {
if command.mode != 0 || command.first != 0 {
return MissionScriptHostEffect::Unhandled;
}
let next = match command.second {
0 => MissionScriptOutcome::Failed,
1 => MissionScriptOutcome::Completed,
_ => return MissionScriptHostEffect::Unhandled,
};
if *outcome == next {
return MissionScriptHostEffect::NoChange;
}
*outcome = next;
match next {
MissionScriptOutcome::Completed => MissionScriptHostEffect::MissionCompleted,
MissionScriptOutcome::Failed => MissionScriptHostEffect::MissionFailed,
MissionScriptOutcome::Active => MissionScriptHostEffect::Unhandled,
}
}
fn resolve_handler30_from_varset_state(
declarations: &MissionScriptVarSet,
values: &MissionScriptVarSetState,
@@ -1862,6 +1945,57 @@ mod tests {
));
}
#[test]
fn recovered_handler_thirty_host_callback_transitions_mission_outcome() {
let mut outcome = MissionScriptOutcome::Active;
assert_eq!(
apply_recovered_script_host_callback(
&mut outcome,
VmHostCallbackCommand {
mode: 0,
first: 0,
second: 0,
},
),
MissionScriptHostEffect::MissionFailed
);
assert_eq!(outcome, MissionScriptOutcome::Failed);
assert_eq!(
apply_recovered_script_host_callback(
&mut outcome,
VmHostCallbackCommand {
mode: 0,
first: 0,
second: 1,
},
),
MissionScriptHostEffect::MissionCompleted
);
assert_eq!(outcome, MissionScriptOutcome::Completed);
assert_eq!(
apply_recovered_script_host_callback(
&mut outcome,
VmHostCallbackCommand {
mode: 0,
first: 0,
second: 1,
},
),
MissionScriptHostEffect::NoChange
);
assert_eq!(
apply_recovered_script_host_callback(
&mut outcome,
VmHostCallbackCommand {
mode: 0,
first: 3,
second: 42,
},
),
MissionScriptHostEffect::Unhandled
);
}
#[test]
fn headless_scheduler_trace_skips_presentation_phases() {
let mut engine = create(
+7
View File
@@ -252,6 +252,13 @@ call. Those branches are not yet assigned Rust gameplay meanings. Reproduce
the current evidence with `capture-ai-init.ps1` and
`ExportIron3dAiCallback.java`.
Runtime now applies only this recovered branch as
`apply_loaded_script_host_callback`: `(0, 0, 0)` transitions a loaded mission
to `Failed`, `(0, 0, 1)` transitions it to `Completed`, and a repeated target
state is a no-op just as the Iron3D guards require. The effect is deliberately
separate from audio/UI playback; commands 1, 3, 4, 5 and mode 2 return
`Unhandled` until their consumers are recovered.
### Handler(8): problem-record state write
`Handler(8)` is the ninth VM-table entry at GOG `ai.dll` VA `0x10009b0d`.