aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 21:25:28 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 21:25:28 +0300
commitbbefe65a6287fad5424e3a5d2b91664b555e90dc (patch)
tree15ee8663974a8bb3efb41b75df0ada524fb59a83 /crates
parent0ee635eb77d8de656af32ca0c4a2fa9562bcbd54 (diff)
downloadfparkan-bbefe65a6287fad5424e3a5d2b91664b555e90dc.tar.xz
fparkan-bbefe65a6287fad5424e3a5d2b91664b555e90dc.zip
feat(script): bind callbacks to runtime varsets
Diffstat (limited to 'crates')
-rw-r--r--crates/fparkan-runtime/src/lib.rs88
-rw-r--r--crates/fparkan-script/src/lib.rs79
2 files changed, 151 insertions, 16 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs
index 771a1d4..cc58185 100644
--- a/crates/fparkan-runtime/src/lib.rs
+++ b/crates/fparkan-runtime/src/lib.rs
@@ -35,8 +35,8 @@ use fparkan_prototype::{
};
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
use fparkan_script::{
- Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptPackage, VarSet,
- VarSetDefault,
+ Handler19DwordWrite, Handler19InitInput, ScriptDispatchSelector, ScriptInstruction,
+ ScriptPackage, VarSet, VarSetDefault, VmHostCallbackCommand,
};
use fparkan_terrain::SurfaceQuery;
use fparkan_vfs::{Vfs, VfsError};
@@ -487,6 +487,13 @@ pub enum EngineError {
/// Proven-contract diagnostic.
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 {
/// Resource path.
@@ -627,6 +634,10 @@ impl std::fmt::Display for EngineError {
clan_index,
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::Mission { 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::VarSet { .. }
| Self::ScriptInit { .. }
+ | Self::ScriptRuntime { .. }
| Self::Movement(_)
| Self::PrototypeGraph { .. }
| 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())
}
+/// 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.
#[must_use]
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]
diff --git a/crates/fparkan-script/src/lib.rs b/crates/fparkan-script/src/lib.rs
index 9acbcb7..6887a03 100644
--- a/crates/fparkan-script/src/lib.rs
+++ b/crates/fparkan-script/src/lib.rs
@@ -378,8 +378,32 @@ impl VarSet {
&self,
instruction: &ScriptInstruction,
) -> Result<VmHostCallbackCommand, Handler30ResolveError> {
- let first = self.resolve_handler30_operand(instruction, 0)?;
- let second = self.resolve_handler30_operand(instruction, 1)?;
+ let values: Vec<_> = self
+ .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 {
mode: 0,
first,
@@ -387,28 +411,34 @@ impl VarSet {
})
}
- fn resolve_handler30_operand(
+ fn resolve_handler30_operand_with_values(
&self,
instruction: &ScriptInstruction,
+ values: &[VarSetDefault],
position: usize,
) -> Result<u32, Handler30ResolveError> {
let index = *instruction
.references
.get(position)
.ok_or(Handler30ResolveError::MissingReference { position })?;
- match self.declarations.get(index as usize) {
- Some(VarSetDeclaration {
- default_value: VarSetDefault::Dword(value),
- ..
- }) => Ok(*value),
- Some(VarSetDeclaration {
- default_value: VarSetDefault::FloatBits(_),
- ..
- }) => Err(Handler30ResolveError::FloatRequiresX87 { index }),
- None => Err(Handler30ResolveError::VarSetIndexOutOfBounds {
+ let declaration = self.declarations.get(index as usize).ok_or(
+ Handler30ResolveError::VarSetIndexOutOfBounds {
index,
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 })
+ }
}
}
@@ -1324,6 +1354,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]
fn handler_thirty_keeps_float_and_malformed_references_explicit() {
let varset = parse_varset(b"VAR( float, f0, 0.5)\n").expect("varset");
let float_operand = ScriptInstruction {