feat(script): bind callbacks to runtime varsets
This commit is contained in:
@@ -35,8 +35,8 @@ 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, ScriptInstruction,
|
||||||
VarSetDefault,
|
ScriptPackage, VarSet, VarSetDefault, VmHostCallbackCommand,
|
||||||
};
|
};
|
||||||
use fparkan_terrain::SurfaceQuery;
|
use fparkan_terrain::SurfaceQuery;
|
||||||
use fparkan_vfs::{Vfs, VfsError};
|
use fparkan_vfs::{Vfs, VfsError};
|
||||||
@@ -487,6 +487,13 @@ pub enum EngineError {
|
|||||||
/// Proven-contract diagnostic.
|
/// Proven-contract diagnostic.
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
|
/// A recovered script handler could not resolve against runtime state.
|
||||||
|
ScriptRuntime {
|
||||||
|
/// Owning TMA clan index.
|
||||||
|
clan_index: usize,
|
||||||
|
/// Handler-specific diagnostic.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
/// `NRes` decode error.
|
/// `NRes` decode error.
|
||||||
Nres {
|
Nres {
|
||||||
/// Resource path.
|
/// Resource path.
|
||||||
@@ -627,6 +634,10 @@ impl std::fmt::Display for EngineError {
|
|||||||
clan_index,
|
clan_index,
|
||||||
message,
|
message,
|
||||||
} => write!(f, "clan {clan_index}: script Init failed: {message}"),
|
} => write!(f, "clan {clan_index}: script Init failed: {message}"),
|
||||||
|
Self::ScriptRuntime {
|
||||||
|
clan_index,
|
||||||
|
message,
|
||||||
|
} => write!(f, "clan {clan_index}: script runtime failed: {message}"),
|
||||||
Self::Nres { path, source } => write!(f, "{path}: {source}"),
|
Self::Nres { path, source } => write!(f, "{path}: {source}"),
|
||||||
Self::Mission { path, source } => write!(f, "{path}: {source}"),
|
Self::Mission { path, source } => write!(f, "{path}: {source}"),
|
||||||
Self::TerrainFormat { path, source } => write!(f, "{path}: {source}"),
|
Self::TerrainFormat { path, source } => write!(f, "{path}: {source}"),
|
||||||
@@ -670,6 +681,7 @@ impl std::error::Error for EngineError {
|
|||||||
| Self::Script { .. }
|
| Self::Script { .. }
|
||||||
| Self::VarSet { .. }
|
| Self::VarSet { .. }
|
||||||
| Self::ScriptInit { .. }
|
| Self::ScriptInit { .. }
|
||||||
|
| Self::ScriptRuntime { .. }
|
||||||
| Self::Movement(_)
|
| Self::Movement(_)
|
||||||
| Self::PrototypeGraph { .. }
|
| Self::PrototypeGraph { .. }
|
||||||
| Self::SchedulerPhaseOrder { .. }
|
| Self::SchedulerPhaseOrder { .. }
|
||||||
@@ -1253,6 +1265,61 @@ pub fn loaded_mission_script_varset_states(engine: &Engine) -> Option<&[MissionS
|
|||||||
.map(|state| state.script_varset_states.as_slice())
|
.map(|state| state.script_varset_states.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the proven opaque callback command for one `Handler(30)` record.
|
||||||
|
///
|
||||||
|
/// Unlike the loader-only resolver, operands are read from the selected
|
||||||
|
/// clan's instantiated varset cells. Dispatching the returned host callback
|
||||||
|
/// remains outside this function because its game-side consumer is not yet
|
||||||
|
/// recovered.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an explicit error when no mission, declaration table, or clan state
|
||||||
|
/// is available, or if the instruction violates the proven handler contract.
|
||||||
|
pub fn resolve_loaded_handler30(
|
||||||
|
engine: &Engine,
|
||||||
|
clan_index: usize,
|
||||||
|
instruction: &ScriptInstruction,
|
||||||
|
) -> Result<VmHostCallbackCommand, EngineError> {
|
||||||
|
let state = engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| EngineError::ScriptRuntime {
|
||||||
|
clan_index,
|
||||||
|
message: "mission script state is unavailable".to_string(),
|
||||||
|
})?;
|
||||||
|
let declarations = state
|
||||||
|
.script_varset
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| EngineError::ScriptRuntime {
|
||||||
|
clan_index,
|
||||||
|
message: "mission varset declarations are unavailable".to_string(),
|
||||||
|
})?;
|
||||||
|
let values = state
|
||||||
|
.script_varset_states
|
||||||
|
.iter()
|
||||||
|
.find(|varset| varset.clan_index == clan_index)
|
||||||
|
.ok_or_else(|| EngineError::ScriptRuntime {
|
||||||
|
clan_index,
|
||||||
|
message: "instantiated mission varset is unavailable".to_string(),
|
||||||
|
})?;
|
||||||
|
resolve_handler30_from_varset_state(declarations, values, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_handler30_from_varset_state(
|
||||||
|
declarations: &MissionScriptVarSet,
|
||||||
|
values: &MissionScriptVarSetState,
|
||||||
|
instruction: &ScriptInstruction,
|
||||||
|
) -> Result<VmHostCallbackCommand, EngineError> {
|
||||||
|
declarations
|
||||||
|
.declarations
|
||||||
|
.resolve_handler30_with_values(instruction, &values.values)
|
||||||
|
.map_err(|source| EngineError::ScriptRuntime {
|
||||||
|
clan_index: values.clan_index,
|
||||||
|
message: source.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 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> {
|
||||||
@@ -1766,6 +1833,23 @@ mod tests {
|
|||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
let callback = resolve_handler30_from_varset_state(
|
||||||
|
&varset,
|
||||||
|
&runtime_varsets[0],
|
||||||
|
&ScriptInstruction {
|
||||||
|
header_words: [30, 0, 0, 0, 0, 2, 0],
|
||||||
|
references: vec![0, 2],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("runtime Handler(30) callback");
|
||||||
|
assert_eq!(
|
||||||
|
callback,
|
||||||
|
VmHostCallbackCommand {
|
||||||
|
mode: 0,
|
||||||
|
first: 728,
|
||||||
|
second: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -378,8 +378,32 @@ impl VarSet {
|
|||||||
&self,
|
&self,
|
||||||
instruction: &ScriptInstruction,
|
instruction: &ScriptInstruction,
|
||||||
) -> Result<VmHostCallbackCommand, Handler30ResolveError> {
|
) -> Result<VmHostCallbackCommand, Handler30ResolveError> {
|
||||||
let first = self.resolve_handler30_operand(instruction, 0)?;
|
let values: Vec<_> = self
|
||||||
let second = self.resolve_handler30_operand(instruction, 1)?;
|
.declarations
|
||||||
|
.iter()
|
||||||
|
.map(|declaration| declaration.default_value)
|
||||||
|
.collect();
|
||||||
|
self.resolve_handler30_with_values(instruction, &values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves `Handler(30)` against instantiated runtime varset values.
|
||||||
|
///
|
||||||
|
/// The declaration table still establishes the original numeric kind; the
|
||||||
|
/// supplied cells carry state after prior recovered handlers have written
|
||||||
|
/// it. This is the execution-time counterpart of [`Self::resolve_handler30`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the same bounded errors as [`Self::resolve_handler30`] when an
|
||||||
|
/// instruction, declaration, or runtime cell cannot satisfy the proven
|
||||||
|
/// DWORD operand contract.
|
||||||
|
pub fn resolve_handler30_with_values(
|
||||||
|
&self,
|
||||||
|
instruction: &ScriptInstruction,
|
||||||
|
values: &[VarSetDefault],
|
||||||
|
) -> Result<VmHostCallbackCommand, Handler30ResolveError> {
|
||||||
|
let first = self.resolve_handler30_operand_with_values(instruction, values, 0)?;
|
||||||
|
let second = self.resolve_handler30_operand_with_values(instruction, values, 1)?;
|
||||||
Ok(VmHostCallbackCommand {
|
Ok(VmHostCallbackCommand {
|
||||||
mode: 0,
|
mode: 0,
|
||||||
first,
|
first,
|
||||||
@@ -387,28 +411,34 @@ impl VarSet {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_handler30_operand(
|
fn resolve_handler30_operand_with_values(
|
||||||
&self,
|
&self,
|
||||||
instruction: &ScriptInstruction,
|
instruction: &ScriptInstruction,
|
||||||
|
values: &[VarSetDefault],
|
||||||
position: usize,
|
position: usize,
|
||||||
) -> Result<u32, Handler30ResolveError> {
|
) -> Result<u32, Handler30ResolveError> {
|
||||||
let index = *instruction
|
let index = *instruction
|
||||||
.references
|
.references
|
||||||
.get(position)
|
.get(position)
|
||||||
.ok_or(Handler30ResolveError::MissingReference { position })?;
|
.ok_or(Handler30ResolveError::MissingReference { position })?;
|
||||||
match self.declarations.get(index as usize) {
|
let declaration = self.declarations.get(index as usize).ok_or(
|
||||||
Some(VarSetDeclaration {
|
Handler30ResolveError::VarSetIndexOutOfBounds {
|
||||||
default_value: VarSetDefault::Dword(value),
|
|
||||||
..
|
|
||||||
}) => Ok(*value),
|
|
||||||
Some(VarSetDeclaration {
|
|
||||||
default_value: VarSetDefault::FloatBits(_),
|
|
||||||
..
|
|
||||||
}) => Err(Handler30ResolveError::FloatRequiresX87 { index }),
|
|
||||||
None => Err(Handler30ResolveError::VarSetIndexOutOfBounds {
|
|
||||||
index,
|
index,
|
||||||
declarations: self.declarations.len(),
|
declarations: self.declarations.len(),
|
||||||
}),
|
},
|
||||||
|
)?;
|
||||||
|
let value =
|
||||||
|
values
|
||||||
|
.get(index as usize)
|
||||||
|
.ok_or(Handler30ResolveError::VarSetIndexOutOfBounds {
|
||||||
|
index,
|
||||||
|
declarations: values.len(),
|
||||||
|
})?;
|
||||||
|
match (declaration.type_name, value) {
|
||||||
|
(VarSetType::Dword, VarSetDefault::Dword(value)) => Ok(*value),
|
||||||
|
(VarSetType::Float, _) | (_, VarSetDefault::FloatBits(_)) => {
|
||||||
|
Err(Handler30ResolveError::FloatRequiresX87 { index })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1323,6 +1353,27 @@ STRING( 8, ignored, ignored, ignored)\r\n";
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handler_thirty_reads_instantiated_dword_cells_instead_of_defaults() {
|
||||||
|
let varset =
|
||||||
|
parse_varset(b"VAR( DWORD, first, 0x12)\nVAR( DWORD, second, 9)\n").expect("varset");
|
||||||
|
let instruction = ScriptInstruction {
|
||||||
|
header_words: [30, 0, 0, 0, 0, 2, 0],
|
||||||
|
references: vec![0, 1],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
varset.resolve_handler30_with_values(
|
||||||
|
&instruction,
|
||||||
|
&[VarSetDefault::Dword(728), VarSetDefault::Dword(449)],
|
||||||
|
),
|
||||||
|
Ok(VmHostCallbackCommand {
|
||||||
|
mode: 0,
|
||||||
|
first: 728,
|
||||||
|
second: 449,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handler_thirty_keeps_float_and_malformed_references_explicit() {
|
fn handler_thirty_keeps_float_and_malformed_references_explicit() {
|
||||||
let varset = parse_varset(b"VAR( float, f0, 0.5)\n").expect("varset");
|
let varset = parse_varset(b"VAR( float, f0, 0.5)\n").expect("varset");
|
||||||
|
|||||||
@@ -231,6 +231,15 @@ The creation and conversion boundaries are reproducible with
|
|||||||
`ExportAiCreateSuperAi.java`, `ExportAiSuperAiConstructor.java`, and
|
`ExportAiCreateSuperAi.java`, `ExportAiSuperAiConstructor.java`, and
|
||||||
`ExportAiFtol.java`.
|
`ExportAiFtol.java`.
|
||||||
|
|
||||||
|
### Runtime Handler(30) operand binding
|
||||||
|
|
||||||
|
`resolve_handler30_with_values` preserves the recovered declaration-kind ABI
|
||||||
|
but reads operands from instantiated per-clan cells rather than textual
|
||||||
|
defaults. Runtime exposes `resolve_loaded_handler30` for the exact opaque
|
||||||
|
`(mode=0, first, second)` callback command. It intentionally returns that
|
||||||
|
command without invoking a guessed game-side consumer: the tenth
|
||||||
|
`CreateSuperAI` callback argument still needs its own recovery.
|
||||||
|
|
||||||
### Handler(8): problem-record state write
|
### Handler(8): problem-record state write
|
||||||
|
|
||||||
`Handler(8)` is the ninth VM-table entry at GOG `ai.dll` VA `0x10009b0d`.
|
`Handler(8)` is the ninth VM-table entry at GOG `ai.dll` VA `0x10009b0d`.
|
||||||
|
|||||||
Reference in New Issue
Block a user