diff options
| author | Valentin Popov <valentin@popov.link> | 2026-07-18 20:53:09 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-07-18 20:53:09 +0300 |
| commit | 5d40ba3968d73bbe08808ff0b64daf6661b137f8 (patch) | |
| tree | 43af3426bb61611ece81790763fa58d20e7d7bac /crates | |
| parent | d1edf4ba65e46546e4a2c1ef6792e68d19d247fd (diff) | |
| download | fparkan-5d40ba3968d73bbe08808ff0b64daf6661b137f8.tar.xz fparkan-5d40ba3968d73bbe08808ff0b64daf6661b137f8.zip | |
feat(script): resolve handler eight states
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/fparkan-script/src/lib.rs | 192 |
1 files changed, 189 insertions, 3 deletions
diff --git a/crates/fparkan-script/src/lib.rs b/crates/fparkan-script/src/lib.rs index ab2e814..2f8b703 100644 --- a/crates/fparkan-script/src/lib.rs +++ b/crates/fparkan-script/src/lib.rs @@ -116,6 +116,75 @@ pub struct Handler15Invocation { pub target: Handler15TargetPayload, } +/// The proven reset branch selected before a `Handler(8)` state write. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Handler8Reset { + /// No reset helper is called before the state field is written. + None, + /// State `2`: helper resets record words `0..=3` and `6`, then invokes + /// two still-opaque callback slots. + StateTwo, + /// State `3`: helper resets record words `0..=2` and `6`, then invokes + /// the same callback slots. + StateThree, +} + +/// One opaque 100-byte AI-record state transition requested by `Handler(8)`. +/// +/// The record index is the runtime value of original `dCurrentProblem`; it is +/// intentionally separate from a compiled instruction reference. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Handler8StateChange { + /// Checked `dCurrentProblem` record index in the handler's `this + 0xa0` table. + pub record_index: u32, + /// Resolved instruction DWORD written at record offset `+0x18`. + pub next_state: u32, + /// Proven pre-write reset branch. + pub reset: Handler8Reset, +} + +/// Error resolving the one-operand `Handler(8)` contract. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Handler8ResolveError { + /// The instruction omitted its required DWORD reference. + MissingReference, + /// The instruction reference is outside the loaded varset. + VarSetIndexOutOfBounds { + /// Referenced compiled-varset index. + index: u32, + /// Available declaration count. + declarations: usize, + }, + /// The reference was not a DWORD declaration. + UnexpectedType { + /// Observed declaration type. + found: VarSetType, + }, +} + +impl std::fmt::Display for Handler8ResolveError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingReference => write!(formatter, "Handler(8) is missing reference 0"), + Self::VarSetIndexOutOfBounds { + index, + declarations, + } => write!( + formatter, + "Handler(8) reference {index} is outside {declarations} varset declarations" + ), + Self::UnexpectedType { found } => { + write!( + formatter, + "Handler(8) reference 0 has {found:?}, expected Dword" + ) + } + } + } +} + +impl std::error::Error for Handler8ResolveError {} + /// Error resolving the corpus-proven `Handler(15)` input layout. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Handler15ResolveError { @@ -272,6 +341,56 @@ impl VarSet { } } + /// Resolves the one DWORD operand consumed by GOG `Handler(8)`. + /// + /// `current_problem_record_index` is the caller's live value of the + /// loader-bound `dCurrentProblem` varset entry. The original bounds-checks + /// it against a table at `this + 0xa0`; table ownership and callback + /// semantics remain outside this package-level resolver. + /// + /// # Errors + /// + /// Returns a typed error instead of coercing a missing, invalid, or float + /// reference into a state transition. + pub fn resolve_handler8( + &self, + instruction: &ScriptInstruction, + current_problem_record_index: u32, + ) -> Result<Handler8StateChange, Handler8ResolveError> { + let index = *instruction + .references + .first() + .ok_or(Handler8ResolveError::MissingReference)?; + let declaration = self.declarations.get(index as usize).ok_or( + Handler8ResolveError::VarSetIndexOutOfBounds { + index, + declarations: self.declarations.len(), + }, + )?; + let next_state = match declaration { + VarSetDeclaration { + type_name: VarSetType::Dword, + default_value: VarSetDefault::Dword(value), + .. + } => *value, + declaration => { + return Err(Handler8ResolveError::UnexpectedType { + found: declaration.type_name, + }); + } + }; + let reset = match next_state { + 2 => Handler8Reset::StateTwo, + 3 => Handler8Reset::StateThree, + _ => Handler8Reset::None, + }; + Ok(Handler8StateChange { + record_index: current_problem_record_index, + next_state, + reset, + }) + } + /// Resolves the fixed-width, corpus-proven operands of GOG `Handler(15)`. /// /// Static analysis of `ai.dll` at `0x10008054` shows four DWORD slots, @@ -914,9 +1033,9 @@ fn read_instruction( mod tests { use super::{ decode, decode_with_limits, parse_varset, Handler15ResolveError, Handler15TargetPayload, - Handler2RecordInput, Handler2RecordScheduler, Handler30ResolveError, - ScriptDispatchSelector, ScriptInstruction, VarSetDefault, VarSetError, VarSetType, - VmHostCallbackCommand, GOG_HANDLER_COUNT, INSTRUCTION_WORDS, + Handler2RecordInput, Handler2RecordScheduler, Handler30ResolveError, Handler8Reset, + Handler8ResolveError, ScriptDispatchSelector, ScriptInstruction, VarSetDefault, + VarSetError, VarSetType, VmHostCallbackCommand, GOG_HANDLER_COUNT, INSTRUCTION_WORDS, }; use fparkan_binary::{DecodeError, Limits}; @@ -1104,6 +1223,73 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] + fn handler_eight_resolves_dword_state_and_proven_reset_branches() { + let varset = parse_varset( + b"VAR( DWORD, solving, 1)\nVAR( DWORD, solved, 2)\nVAR( DWORD, state_three, 3)\n", + ) + .expect("valid Handler(8) varset"); + for (reference, reset) in [ + (0, Handler8Reset::None), + (1, Handler8Reset::StateTwo), + (2, Handler8Reset::StateThree), + ] { + let change = varset + .resolve_handler8( + &ScriptInstruction { + header_words: [8, 0, 0, 0, 0, 1, 0], + references: vec![reference], + }, + 17, + ) + .expect("typed Handler(8) state"); + assert_eq!(change.record_index, 17); + assert_eq!(change.next_state, reference + 1); + assert_eq!(change.reset, reset); + } + } + + #[test] + fn handler_eight_rejects_missing_float_and_out_of_range_operands() { + let empty = parse_varset(b"").expect("empty varset"); + assert_eq!( + empty.resolve_handler8( + &ScriptInstruction { + header_words: [8, 0, 0, 0, 0, 0, 0], + references: Vec::new(), + }, + 0, + ), + Err(Handler8ResolveError::MissingReference) + ); + let float = parse_varset(b"VAR( float, value, 1.0)\n").expect("float varset"); + assert_eq!( + float.resolve_handler8( + &ScriptInstruction { + header_words: [8, 0, 0, 0, 0, 1, 0], + references: vec![0], + }, + 0, + ), + Err(Handler8ResolveError::UnexpectedType { + found: VarSetType::Float, + }) + ); + assert_eq!( + float.resolve_handler8( + &ScriptInstruction { + header_words: [8, 0, 0, 0, 0, 1, 0], + references: vec![1], + }, + 0, + ), + Err(Handler8ResolveError::VarSetIndexOutOfBounds { + index: 1, + declarations: 1, + }) + ); + } + + #[test] fn handler_fifteen_resolves_exact_typed_target_by_place_operands() { let varset = parse_varset( b"VAR( DWORD, d0, 1)\nVAR( DWORD, d1, 2)\nVAR( DWORD, d2, 3)\nVAR( DWORD, d3, 4)\nVAR( float, f4, 1.5)\nVAR( float, f5, 2.5)\nVAR( float, f6, 3.5)\nVAR( float, f7, 4.5)\nVAR( DWORD, mode, 0x0202)\nVAR( DWORD, place0, 12)\nVAR( DWORD, place1, 13)\n", |
