2026-07-18 20:40:17 +04:00
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
|
//! Lossless, bounded reader for compiled AI `.scr` packages.
|
|
|
|
|
//!
|
|
|
|
|
//! This module preserves the layout proven by the GOG `ai.dll` reader. It
|
2026-10-20 04:11:35 +04:00
|
|
|
//! deliberately does not implement a general bytecode VM. It contains a
|
|
|
|
|
//! narrow, handler-specific resolver for the confirmed `Init` path.
|
2026-07-18 20:40:17 +04:00
|
|
|
|
|
|
|
|
use fparkan_binary::{checked_allocation_len, Cursor, DecodeError, Limits};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
const INSTRUCTION_HEADER_BYTES: u64 = 28;
|
|
|
|
|
const INSTRUCTION_WORDS: usize = 7;
|
2026-07-18 20:44:10 +04:00
|
|
|
const GOG_HANDLER_COUNT: u32 = 73;
|
2026-07-18 21:17:57 +04:00
|
|
|
const MAX_VARSET_DECLARATIONS: usize = 4096;
|
|
|
|
|
|
|
|
|
|
/// Parsed defaults from the text `varset.var` source shared by AI packages.
|
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
|
|
|
pub struct VarSet {
|
|
|
|
|
/// Declarations in original source order.
|
|
|
|
|
pub declarations: Vec<VarSetDeclaration>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One supported `VAR(...)` declaration from `varset.var`.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct VarSetDeclaration {
|
|
|
|
|
/// Variable type spelling used by the original source.
|
|
|
|
|
pub type_name: VarSetType,
|
|
|
|
|
/// ASCII variable name.
|
|
|
|
|
pub name: String,
|
|
|
|
|
/// Typed default value.
|
|
|
|
|
pub default_value: VarSetDefault,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Numeric declaration types present in the shipped GOG `varset.var`.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum VarSetType {
|
|
|
|
|
/// `VAR(float, ...)`.
|
|
|
|
|
Float,
|
|
|
|
|
/// `VAR(DWORD, ...)`.
|
|
|
|
|
Dword,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A lossless-in-meaning numeric default from `varset.var`.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum VarSetDefault {
|
|
|
|
|
/// IEEE-754 bits parsed from a `float` literal.
|
|
|
|
|
FloatBits(u32),
|
|
|
|
|
/// Unsigned 32-bit integer parsed from a decimal or hexadecimal `DWORD` literal.
|
|
|
|
|
Dword(u32),
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:33:14 +04:00
|
|
|
/// One opaque command delivered by a VM handler to the host-supplied callback.
|
|
|
|
|
///
|
|
|
|
|
/// The numeric mode belongs to the callback ABI; it has no inferred gameplay
|
|
|
|
|
/// meaning yet.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct VmHostCallbackCommand {
|
|
|
|
|
/// First callback ABI word.
|
|
|
|
|
pub mode: u32,
|
|
|
|
|
/// First resolved callback payload word.
|
|
|
|
|
pub first: u32,
|
|
|
|
|
/// Second resolved callback payload word.
|
|
|
|
|
pub second: u32,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:47:33 +04:00
|
|
|
/// The optional target-lookup payload selected by the recovered `Handler(15)`
|
|
|
|
|
/// mode word. Names follow the shipped `varset.var` constants; they do not
|
|
|
|
|
/// assign a gameplay operation to the later opaque virtual call.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum Handler15TargetPayload {
|
|
|
|
|
/// `NONE` (`0`): no additional reference is consumed.
|
|
|
|
|
None,
|
|
|
|
|
/// `TARGET_BY_LOGIC_ID` (`0x0201`): one DWORD payload.
|
|
|
|
|
ByLogicId(u32),
|
|
|
|
|
/// `TARGET_BY_PLACE` (`0x0202`): two DWORD payloads.
|
|
|
|
|
ByPlace {
|
|
|
|
|
/// First DWORD payload in source order.
|
|
|
|
|
first: u32,
|
|
|
|
|
/// Second DWORD payload in source order.
|
|
|
|
|
second: u32,
|
|
|
|
|
},
|
|
|
|
|
/// `TARGET_BY_TYPE` (`0x0203`): one DWORD payload.
|
|
|
|
|
ByType(u32),
|
|
|
|
|
/// `TARGET_NOT_DEFINED` (`0x0204`): one DWORD payload.
|
|
|
|
|
NotDefined(u32),
|
|
|
|
|
/// `TARGET_BY_NAME` (`0x0205`): one DWORD payload.
|
|
|
|
|
ByName(u32),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolved fixed-width operands delivered by `Handler(15)` to its target
|
|
|
|
|
/// interface.
|
|
|
|
|
///
|
|
|
|
|
/// Slot names preserve disk/reference order. The original first looks up an
|
|
|
|
|
/// opaque target through `word_0`, then calls a target vtable slot with this
|
|
|
|
|
/// packed data and `word_2`; neither operation is named here.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
|
|
|
pub struct Handler15Invocation {
|
|
|
|
|
/// Resolved DWORD reference 0, used for the preceding target lookup.
|
|
|
|
|
pub word_0: u32,
|
|
|
|
|
/// Resolved DWORD reference 1.
|
|
|
|
|
pub word_1: u32,
|
|
|
|
|
/// Resolved DWORD reference 2, passed separately to the target call.
|
|
|
|
|
pub word_2: u32,
|
|
|
|
|
/// Resolved DWORD reference 3.
|
|
|
|
|
pub word_3: u32,
|
|
|
|
|
/// Resolved scalar reference 4.
|
|
|
|
|
pub scalar_4: f32,
|
|
|
|
|
/// Resolved scalar reference 5.
|
|
|
|
|
pub scalar_5: f32,
|
|
|
|
|
/// Resolved scalar reference 6.
|
|
|
|
|
pub scalar_6: f32,
|
|
|
|
|
/// Resolved scalar reference 7.
|
|
|
|
|
pub scalar_7: f32,
|
|
|
|
|
/// Target mode and its exactly consumed trailing DWORD references.
|
|
|
|
|
pub target: Handler15TargetPayload,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:53:09 +04:00
|
|
|
/// 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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:04:10 +04:00
|
|
|
/// One raw DWORD write made by the corpus-proven `Handler(19)` Init path.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct Handler19DwordWrite {
|
|
|
|
|
/// Compiled varset record index selected by the instruction reference.
|
|
|
|
|
pub index: u32,
|
|
|
|
|
/// Exact 32-bit payload passed to the original common varset setter.
|
|
|
|
|
pub value: u32,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 04:11:35 +04:00
|
|
|
/// Result of resolving the first event whose case-sensitive C-string name is
|
|
|
|
|
/// `Init` (bytes through the first NUL) with the supported
|
|
|
|
|
/// `Handler(19)` action subset.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum Handler19InitResolution {
|
|
|
|
|
/// No event has the case-sensitive C-string name `Init`; the native lookup
|
|
|
|
|
/// makes no call.
|
|
|
|
|
Missing,
|
|
|
|
|
/// The first `Init\0` event was walked in source order.
|
|
|
|
|
Executed {
|
|
|
|
|
/// Zero-based event table index selected by the native name lookup.
|
|
|
|
|
event_index: usize,
|
|
|
|
|
/// Opaque event word retained from the selected record.
|
|
|
|
|
event_word: u32,
|
|
|
|
|
/// Ordered writes emitted by every supported instruction in the event.
|
|
|
|
|
writes: Vec<Handler19DwordWrite>,
|
|
|
|
|
},
|
|
|
|
|
/// The first unsupported instruction stopped this restricted resolver.
|
|
|
|
|
/// Any earlier local writes are discarded because the event did not finish.
|
|
|
|
|
Unsupported {
|
|
|
|
|
/// Zero-based event table index selected by the native name lookup.
|
|
|
|
|
event_index: usize,
|
|
|
|
|
/// Zero-based instruction index within that event.
|
|
|
|
|
instruction_index: usize,
|
|
|
|
|
/// Unsupported action shape.
|
|
|
|
|
error: Handler19ResolveError,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:04:10 +04:00
|
|
|
/// Inputs already expressed at the original `Handler(19)` setter ABI boundary.
|
|
|
|
|
///
|
|
|
|
|
/// The first two words are results of original x87 `__ftol` calls. Keeping
|
|
|
|
|
/// them as words prevents this package from silently substituting Rust's float
|
|
|
|
|
/// conversion policy before captured x87 vectors are available.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct Handler19InitInput {
|
|
|
|
|
/// x87-converted value from VM field `+0x80`.
|
|
|
|
|
pub first_x87_word: u32,
|
|
|
|
|
/// x87-converted value from VM field `+0x84`.
|
|
|
|
|
pub second_x87_word: u32,
|
|
|
|
|
/// Raw word from VM field `+0x7c`.
|
|
|
|
|
pub third_word: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Error resolving the three-target `Handler(19)` Init contract.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum Handler19ResolveError {
|
2026-10-20 04:11:35 +04:00
|
|
|
/// The selected action is not the recovered `Handler(19)` slot.
|
|
|
|
|
UnsupportedSelector {
|
|
|
|
|
/// Recovered selector shape.
|
|
|
|
|
found: ScriptDispatchSelector,
|
|
|
|
|
},
|
|
|
|
|
/// Only action instructions with control word `0xffff_ffff` are supported.
|
|
|
|
|
UnsupportedControlWord {
|
|
|
|
|
/// Opaque control word from instruction header word one.
|
|
|
|
|
found: u32,
|
|
|
|
|
},
|
|
|
|
|
/// Only instructions with no result-save reference are supported.
|
|
|
|
|
UnsupportedResultSave {
|
|
|
|
|
/// Opaque variable reference from instruction header word two.
|
|
|
|
|
found: u32,
|
|
|
|
|
},
|
2026-07-18 22:04:10 +04:00
|
|
|
/// One of the three required references was absent.
|
|
|
|
|
MissingReference {
|
|
|
|
|
/// Zero-based required target position.
|
|
|
|
|
position: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A compiled reference indexed outside the loaded varset.
|
|
|
|
|
VarSetIndexOutOfBounds {
|
|
|
|
|
/// Referenced compiled-varset index.
|
|
|
|
|
index: u32,
|
|
|
|
|
/// Available declaration count.
|
|
|
|
|
declarations: usize,
|
|
|
|
|
},
|
|
|
|
|
/// The `AutoDemo` Init target was not a `DWORD` declaration.
|
|
|
|
|
UnexpectedType {
|
|
|
|
|
/// Zero-based target position.
|
|
|
|
|
position: usize,
|
|
|
|
|
/// Observed declaration type.
|
|
|
|
|
found: VarSetType,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for Handler19ResolveError {
|
|
|
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
2026-10-20 04:11:35 +04:00
|
|
|
Self::UnsupportedSelector { found } => {
|
|
|
|
|
write!(formatter, "unsupported script selector {found:?} in Init")
|
|
|
|
|
}
|
|
|
|
|
Self::UnsupportedControlWord { found } => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"unsupported Init control word 0x{found:08x}; expected 0xffffffff"
|
|
|
|
|
),
|
|
|
|
|
Self::UnsupportedResultSave { found } => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"unsupported Init result-save reference 0x{found:08x}; expected 0xffffffff"
|
|
|
|
|
),
|
2026-07-18 22:04:10 +04:00
|
|
|
Self::MissingReference { position } => {
|
|
|
|
|
write!(formatter, "Handler(19) is missing reference {position}")
|
|
|
|
|
}
|
|
|
|
|
Self::VarSetIndexOutOfBounds {
|
|
|
|
|
index,
|
|
|
|
|
declarations,
|
|
|
|
|
} => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(19) reference {index} is outside {declarations} varset declarations"
|
|
|
|
|
),
|
|
|
|
|
Self::UnexpectedType { position, found } => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(19) reference {position} has {found:?}, expected Dword"
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for Handler19ResolveError {}
|
|
|
|
|
|
2026-07-18 21:53:09 +04:00
|
|
|
/// 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 {}
|
|
|
|
|
|
2026-07-18 21:47:33 +04:00
|
|
|
/// Error resolving the corpus-proven `Handler(15)` input layout.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum Handler15ResolveError {
|
|
|
|
|
/// One of the mode-specific required references was absent.
|
|
|
|
|
MissingReference {
|
|
|
|
|
/// Zero-based required reference position.
|
|
|
|
|
position: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A compiled reference indexed outside the loaded varset.
|
|
|
|
|
VarSetIndexOutOfBounds {
|
|
|
|
|
/// Referenced compiled-varset index.
|
|
|
|
|
index: u32,
|
|
|
|
|
/// Available declaration count.
|
|
|
|
|
declarations: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A reference's declaration type differed from the observed contract.
|
|
|
|
|
UnexpectedType {
|
|
|
|
|
/// Zero-based reference position.
|
|
|
|
|
position: usize,
|
|
|
|
|
/// Type required by the recovered handler contract.
|
|
|
|
|
expected: VarSetType,
|
|
|
|
|
/// Type actually declared in the loaded varset.
|
|
|
|
|
found: VarSetType,
|
|
|
|
|
},
|
|
|
|
|
/// The target-mode value was not among the modes observed in the GOG corpus.
|
|
|
|
|
UnsupportedTargetMode(u32),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for Handler15ResolveError {
|
|
|
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
Self::MissingReference { position } => {
|
|
|
|
|
write!(formatter, "Handler(15) is missing reference {position}")
|
|
|
|
|
}
|
|
|
|
|
Self::VarSetIndexOutOfBounds {
|
|
|
|
|
index,
|
|
|
|
|
declarations,
|
|
|
|
|
} => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(15) reference {index} is outside {declarations} varset declarations"
|
|
|
|
|
),
|
|
|
|
|
Self::UnexpectedType {
|
|
|
|
|
position,
|
|
|
|
|
expected,
|
|
|
|
|
found,
|
|
|
|
|
} => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(15) reference {position} has {found:?}, expected {expected:?}"
|
|
|
|
|
),
|
|
|
|
|
Self::UnsupportedTargetMode(mode) => {
|
|
|
|
|
write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(15) has unsupported target mode 0x{mode:08x}"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for Handler15ResolveError {}
|
|
|
|
|
|
2026-07-18 21:33:14 +04:00
|
|
|
/// Error resolving the proven `Handler(30)` callback contract.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum Handler30ResolveError {
|
|
|
|
|
/// The instruction did not contain one of Handler(30)'s required references.
|
|
|
|
|
MissingReference {
|
|
|
|
|
/// Zero-based required reference position.
|
|
|
|
|
position: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A reference index was outside the loaded varset.
|
|
|
|
|
VarSetIndexOutOfBounds {
|
|
|
|
|
/// Referenced index from the compiled instruction.
|
|
|
|
|
index: u32,
|
|
|
|
|
/// Available declaration count.
|
|
|
|
|
declarations: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A `float` declaration would require the still-unrecovered x87 `__ftol` policy.
|
|
|
|
|
FloatRequiresX87 {
|
|
|
|
|
/// Referenced index from the compiled instruction.
|
|
|
|
|
index: u32,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for Handler30ResolveError {
|
|
|
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
Self::MissingReference { position } => {
|
|
|
|
|
write!(formatter, "Handler(30) is missing reference {position}")
|
|
|
|
|
}
|
|
|
|
|
Self::VarSetIndexOutOfBounds {
|
|
|
|
|
index,
|
|
|
|
|
declarations,
|
|
|
|
|
} => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(30) reference {index} is outside {declarations} varset declarations"
|
|
|
|
|
),
|
|
|
|
|
Self::FloatRequiresX87 { index } => write!(
|
|
|
|
|
formatter,
|
|
|
|
|
"Handler(30) reference {index} requires unrecovered x87 float-to-u32 conversion"
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for Handler30ResolveError {}
|
|
|
|
|
|
|
|
|
|
impl VarSet {
|
|
|
|
|
/// Resolves the two index operands consumed by the GOG `Handler(30)`.
|
|
|
|
|
///
|
|
|
|
|
/// The original invokes its host callback with mode zero after resolving
|
|
|
|
|
/// both operands through `FUN_10013570`. The shipped GOG corpus uses only
|
|
|
|
|
/// `DWORD` declarations at these positions. A float is rejected until a
|
|
|
|
|
/// captured x87 `__ftol` conversion profile exists.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns a typed error for missing references, out-of-range varset
|
|
|
|
|
/// indices, or a float operand requiring unrecovered x87 behavior.
|
|
|
|
|
pub fn resolve_handler30(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
) -> Result<VmHostCallbackCommand, Handler30ResolveError> {
|
2026-07-18 22:25:28 +04:00
|
|
|
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)?;
|
2026-07-18 21:33:14 +04:00
|
|
|
Ok(VmHostCallbackCommand {
|
|
|
|
|
mode: 0,
|
|
|
|
|
first,
|
|
|
|
|
second,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:25:28 +04:00
|
|
|
fn resolve_handler30_operand_with_values(
|
2026-07-18 21:33:14 +04:00
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
2026-07-18 22:25:28 +04:00
|
|
|
values: &[VarSetDefault],
|
2026-07-18 21:33:14 +04:00
|
|
|
position: usize,
|
|
|
|
|
) -> Result<u32, Handler30ResolveError> {
|
|
|
|
|
let index = *instruction
|
|
|
|
|
.references
|
|
|
|
|
.get(position)
|
|
|
|
|
.ok_or(Handler30ResolveError::MissingReference { position })?;
|
2026-07-18 22:25:28 +04:00
|
|
|
let declaration = self.declarations.get(index as usize).ok_or(
|
|
|
|
|
Handler30ResolveError::VarSetIndexOutOfBounds {
|
2026-07-18 21:33:14 +04:00
|
|
|
index,
|
|
|
|
|
declarations: self.declarations.len(),
|
2026-07-18 22:25:28 +04:00
|
|
|
},
|
|
|
|
|
)?;
|
|
|
|
|
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 })
|
|
|
|
|
}
|
2026-07-18 21:33:14 +04:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-18 21:47:33 +04:00
|
|
|
|
2026-07-18 21:53:09 +04:00
|
|
|
/// 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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:04:10 +04:00
|
|
|
/// Resolves the three DWORD targets written by GOG `Handler(19)`.
|
|
|
|
|
///
|
|
|
|
|
/// The caller supplies values at the original setter ABI boundary: the
|
|
|
|
|
/// first two are already x87-converted `__ftol` results and the third is
|
|
|
|
|
/// the raw VM word. This preserves the exact default-script Init path
|
|
|
|
|
/// without claiming an unrecovered portable x87 policy.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns a typed failure for missing/out-of-range targets or a target
|
|
|
|
|
/// type that differs from the observed `AutoDemo` `DWORD` contract.
|
|
|
|
|
pub fn resolve_handler19(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
input: Handler19InitInput,
|
|
|
|
|
) -> Result<[Handler19DwordWrite; 3], Handler19ResolveError> {
|
|
|
|
|
let targets = [
|
|
|
|
|
self.resolve_handler19_target(instruction, 0)?,
|
|
|
|
|
self.resolve_handler19_target(instruction, 1)?,
|
|
|
|
|
self.resolve_handler19_target(instruction, 2)?,
|
|
|
|
|
];
|
|
|
|
|
Ok([
|
|
|
|
|
Handler19DwordWrite {
|
|
|
|
|
index: targets[0],
|
|
|
|
|
value: input.first_x87_word,
|
|
|
|
|
},
|
|
|
|
|
Handler19DwordWrite {
|
|
|
|
|
index: targets[1],
|
|
|
|
|
value: input.second_x87_word,
|
|
|
|
|
},
|
|
|
|
|
Handler19DwordWrite {
|
|
|
|
|
index: targets[2],
|
|
|
|
|
value: input.third_word,
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 04:11:35 +04:00
|
|
|
/// Resolves only the first event whose case-sensitive C-string name is
|
|
|
|
|
/// `Init` (bytes through the first NUL), and its supported
|
|
|
|
|
/// `Handler(19)` action instructions in source order.
|
|
|
|
|
///
|
|
|
|
|
/// A missing event is a normal no-write result, matching the native event
|
|
|
|
|
/// lookup. Unsupported selector/control/result-save forms stop at their
|
|
|
|
|
/// first instruction and return no partial writes. Malformed handler
|
|
|
|
|
/// references and declaration types remain errors.
|
|
|
|
|
pub fn resolve_handler19_init(
|
|
|
|
|
&self,
|
|
|
|
|
package: &ScriptPackage,
|
|
|
|
|
input: Handler19InitInput,
|
|
|
|
|
) -> Result<Handler19InitResolution, Handler19ResolveError> {
|
|
|
|
|
let Some((event_index, event)) = package.events.iter().enumerate().find(|(_, event)| {
|
|
|
|
|
event.name_raw.split(|byte| *byte == 0).next() == Some(&b"Init"[..])
|
|
|
|
|
}) else {
|
|
|
|
|
return Ok(Handler19InitResolution::Missing);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut writes = Vec::new();
|
|
|
|
|
for (instruction_index, instruction) in event.instructions.iter().enumerate() {
|
|
|
|
|
let instruction_writes =
|
|
|
|
|
match self.resolve_handler19_init_instruction(instruction, input) {
|
|
|
|
|
Ok(writes) => writes,
|
|
|
|
|
Err(error @ Handler19ResolveError::UnsupportedSelector { .. })
|
|
|
|
|
| Err(error @ Handler19ResolveError::UnsupportedControlWord { .. })
|
|
|
|
|
| Err(error @ Handler19ResolveError::UnsupportedResultSave { .. }) => {
|
|
|
|
|
return Ok(Handler19InitResolution::Unsupported {
|
|
|
|
|
event_index,
|
|
|
|
|
instruction_index,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Err(error) => return Err(error),
|
|
|
|
|
};
|
|
|
|
|
writes.extend(instruction_writes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Handler19InitResolution::Executed {
|
|
|
|
|
event_index,
|
|
|
|
|
event_word: event.event_word,
|
|
|
|
|
writes,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_handler19_init_instruction(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
input: Handler19InitInput,
|
|
|
|
|
) -> Result<[Handler19DwordWrite; 3], Handler19ResolveError> {
|
|
|
|
|
let selector = instruction.dispatch_selector();
|
|
|
|
|
if selector != ScriptDispatchSelector::Handler(19) {
|
|
|
|
|
return Err(Handler19ResolveError::UnsupportedSelector { found: selector });
|
|
|
|
|
}
|
|
|
|
|
if instruction.header_words[1] != u32::MAX {
|
|
|
|
|
return Err(Handler19ResolveError::UnsupportedControlWord {
|
|
|
|
|
found: instruction.header_words[1],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if instruction.header_words[2] != u32::MAX {
|
|
|
|
|
return Err(Handler19ResolveError::UnsupportedResultSave {
|
|
|
|
|
found: instruction.header_words[2],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
self.resolve_handler19(instruction, input)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:04:10 +04:00
|
|
|
fn resolve_handler19_target(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
position: usize,
|
|
|
|
|
) -> Result<u32, Handler19ResolveError> {
|
|
|
|
|
let index = *instruction
|
|
|
|
|
.references
|
|
|
|
|
.get(position)
|
|
|
|
|
.ok_or(Handler19ResolveError::MissingReference { position })?;
|
|
|
|
|
match self.declarations.get(index as usize) {
|
|
|
|
|
Some(VarSetDeclaration {
|
|
|
|
|
type_name: VarSetType::Dword,
|
|
|
|
|
..
|
|
|
|
|
}) => Ok(index),
|
|
|
|
|
Some(declaration) => Err(Handler19ResolveError::UnexpectedType {
|
|
|
|
|
position,
|
|
|
|
|
found: declaration.type_name,
|
|
|
|
|
}),
|
|
|
|
|
None => Err(Handler19ResolveError::VarSetIndexOutOfBounds {
|
|
|
|
|
index,
|
|
|
|
|
declarations: self.declarations.len(),
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:47:33 +04:00
|
|
|
/// Resolves the fixed-width, corpus-proven operands of GOG `Handler(15)`.
|
|
|
|
|
///
|
|
|
|
|
/// Static analysis of `ai.dll` at `0x10008054` shows four DWORD slots,
|
|
|
|
|
/// four scalar slots, then a DWORD mode. The mode selects zero, one, or
|
|
|
|
|
/// two additional DWORD references. This method deliberately stops before
|
|
|
|
|
/// the original target lookup and virtual call because their object and
|
|
|
|
|
/// gameplay semantics are not yet recovered.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns typed failures for missing/out-of-range operands, type changes,
|
|
|
|
|
/// or an unobserved target mode rather than silently inventing a command.
|
|
|
|
|
pub fn resolve_handler15(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
) -> Result<Handler15Invocation, Handler15ResolveError> {
|
|
|
|
|
let word_0 = self.resolve_handler15_dword(instruction, 0)?;
|
|
|
|
|
let word_1 = self.resolve_handler15_dword(instruction, 1)?;
|
|
|
|
|
let word_2 = self.resolve_handler15_dword(instruction, 2)?;
|
|
|
|
|
let word_3 = self.resolve_handler15_dword(instruction, 3)?;
|
|
|
|
|
let scalar_4 = self.resolve_handler15_float(instruction, 4)?;
|
|
|
|
|
let scalar_5 = self.resolve_handler15_float(instruction, 5)?;
|
|
|
|
|
let scalar_6 = self.resolve_handler15_float(instruction, 6)?;
|
|
|
|
|
let scalar_7 = self.resolve_handler15_float(instruction, 7)?;
|
|
|
|
|
let target = match self.resolve_handler15_dword(instruction, 8)? {
|
|
|
|
|
0 => Handler15TargetPayload::None,
|
|
|
|
|
0x0201 => {
|
|
|
|
|
Handler15TargetPayload::ByLogicId(self.resolve_handler15_dword(instruction, 9)?)
|
|
|
|
|
}
|
|
|
|
|
0x0202 => Handler15TargetPayload::ByPlace {
|
|
|
|
|
first: self.resolve_handler15_dword(instruction, 9)?,
|
|
|
|
|
second: self.resolve_handler15_dword(instruction, 10)?,
|
|
|
|
|
},
|
|
|
|
|
0x0203 => Handler15TargetPayload::ByType(self.resolve_handler15_dword(instruction, 9)?),
|
|
|
|
|
0x0204 => {
|
|
|
|
|
Handler15TargetPayload::NotDefined(self.resolve_handler15_dword(instruction, 9)?)
|
|
|
|
|
}
|
|
|
|
|
0x0205 => Handler15TargetPayload::ByName(self.resolve_handler15_dword(instruction, 9)?),
|
|
|
|
|
mode => return Err(Handler15ResolveError::UnsupportedTargetMode(mode)),
|
|
|
|
|
};
|
|
|
|
|
Ok(Handler15Invocation {
|
|
|
|
|
word_0,
|
|
|
|
|
word_1,
|
|
|
|
|
word_2,
|
|
|
|
|
word_3,
|
|
|
|
|
scalar_4,
|
|
|
|
|
scalar_5,
|
|
|
|
|
scalar_6,
|
|
|
|
|
scalar_7,
|
|
|
|
|
target,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_handler15_dword(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
position: usize,
|
|
|
|
|
) -> Result<u32, Handler15ResolveError> {
|
|
|
|
|
match self.resolve_handler15_declaration(instruction, position)? {
|
|
|
|
|
VarSetDeclaration {
|
|
|
|
|
type_name: VarSetType::Dword,
|
|
|
|
|
default_value: VarSetDefault::Dword(value),
|
|
|
|
|
..
|
|
|
|
|
} => Ok(*value),
|
|
|
|
|
declaration => Err(Handler15ResolveError::UnexpectedType {
|
|
|
|
|
position,
|
|
|
|
|
expected: VarSetType::Dword,
|
|
|
|
|
found: declaration.type_name,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_handler15_float(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
position: usize,
|
|
|
|
|
) -> Result<f32, Handler15ResolveError> {
|
|
|
|
|
match self.resolve_handler15_declaration(instruction, position)? {
|
|
|
|
|
VarSetDeclaration {
|
|
|
|
|
type_name: VarSetType::Float,
|
|
|
|
|
default_value: VarSetDefault::FloatBits(bits),
|
|
|
|
|
..
|
|
|
|
|
} => Ok(f32::from_bits(*bits)),
|
|
|
|
|
declaration => Err(Handler15ResolveError::UnexpectedType {
|
|
|
|
|
position,
|
|
|
|
|
expected: VarSetType::Float,
|
|
|
|
|
found: declaration.type_name,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_handler15_declaration(
|
|
|
|
|
&self,
|
|
|
|
|
instruction: &ScriptInstruction,
|
|
|
|
|
position: usize,
|
|
|
|
|
) -> Result<&VarSetDeclaration, Handler15ResolveError> {
|
|
|
|
|
let index = *instruction
|
|
|
|
|
.references
|
|
|
|
|
.get(position)
|
|
|
|
|
.ok_or(Handler15ResolveError::MissingReference { position })?;
|
|
|
|
|
self.declarations
|
|
|
|
|
.get(index as usize)
|
|
|
|
|
.ok_or(Handler15ResolveError::VarSetIndexOutOfBounds {
|
|
|
|
|
index,
|
|
|
|
|
declarations: self.declarations.len(),
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-07-18 21:33:14 +04:00
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:17:57 +04:00
|
|
|
impl VarSetDefault {
|
|
|
|
|
/// Returns the float value when this is a `float` default.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn as_float(self) -> Option<f32> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::FloatBits(bits) => Some(f32::from_bits(bits)),
|
|
|
|
|
Self::Dword(_) => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the integer value when this is a `DWORD` default.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn as_dword(self) -> Option<u32> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::FloatBits(_) => None,
|
|
|
|
|
Self::Dword(value) => Some(value),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Error while parsing the documented `varset.var` declaration subset.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum VarSetError {
|
|
|
|
|
/// A line beginning with `VAR(` did not have a closing parenthesis.
|
|
|
|
|
UnterminatedDeclaration {
|
|
|
|
|
/// One-based source line containing the declaration.
|
|
|
|
|
line: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A declaration omitted one of its first three fields.
|
|
|
|
|
MissingField {
|
|
|
|
|
/// One-based source line containing the declaration.
|
|
|
|
|
line: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A declaration type is not one of the observed GOG numeric types.
|
|
|
|
|
UnsupportedType {
|
|
|
|
|
/// One-based source line containing the declaration.
|
|
|
|
|
line: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A declaration name or numeric literal was not ASCII text.
|
|
|
|
|
NonAsciiField {
|
|
|
|
|
/// One-based source line containing the declaration.
|
|
|
|
|
line: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A `float` default was not a finite Rust-compatible decimal literal.
|
|
|
|
|
InvalidFloat {
|
|
|
|
|
/// One-based source line containing the declaration.
|
|
|
|
|
line: usize,
|
|
|
|
|
},
|
|
|
|
|
/// A `DWORD` default was neither a decimal nor a hexadecimal `u32`.
|
|
|
|
|
InvalidDword {
|
|
|
|
|
/// One-based source line containing the declaration.
|
|
|
|
|
line: usize,
|
|
|
|
|
},
|
|
|
|
|
/// The input exceeded the bounded declaration count.
|
|
|
|
|
TooManyDeclarations {
|
|
|
|
|
/// Maximum accepted declaration count.
|
|
|
|
|
limit: usize,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for VarSetError {
|
|
|
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
Self::UnterminatedDeclaration { line } => {
|
|
|
|
|
write!(formatter, "unterminated VAR declaration at line {line}")
|
|
|
|
|
}
|
|
|
|
|
Self::MissingField { line } => write!(formatter, "missing VAR field at line {line}"),
|
|
|
|
|
Self::UnsupportedType { line } => {
|
|
|
|
|
write!(formatter, "unsupported VAR type at line {line}")
|
|
|
|
|
}
|
|
|
|
|
Self::NonAsciiField { line } => write!(formatter, "non-ASCII VAR field at line {line}"),
|
|
|
|
|
Self::InvalidFloat { line } => {
|
|
|
|
|
write!(formatter, "invalid float default at line {line}")
|
|
|
|
|
}
|
|
|
|
|
Self::InvalidDword { line } => {
|
|
|
|
|
write!(formatter, "invalid DWORD default at line {line}")
|
|
|
|
|
}
|
|
|
|
|
Self::TooManyDeclarations { limit } => {
|
|
|
|
|
write!(formatter, "VAR declaration count exceeds limit {limit}")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for VarSetError {}
|
2026-07-18 20:40:17 +04:00
|
|
|
|
|
|
|
|
/// A compiled `.scr` package.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct ScriptPackage {
|
|
|
|
|
/// Number of opcode handlers expected by the package.
|
|
|
|
|
pub opcode_handler_count: u32,
|
|
|
|
|
/// Named events in original file order.
|
|
|
|
|
pub events: Vec<ScriptEvent>,
|
|
|
|
|
/// Bytes not consumed by the recovered framing.
|
|
|
|
|
pub trailing_bytes: Vec<u8>,
|
|
|
|
|
/// Original package bytes.
|
|
|
|
|
pub raw: Arc<[u8]>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One named event record.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct ScriptEvent {
|
|
|
|
|
/// Declared byte count excluding the NUL terminator.
|
|
|
|
|
pub name_len: u32,
|
|
|
|
|
/// Name bytes including its mandatory NUL terminator.
|
|
|
|
|
pub name_raw: Vec<u8>,
|
|
|
|
|
/// Opaque event word following the name.
|
|
|
|
|
pub event_word: u32,
|
|
|
|
|
/// Nested instruction records in original file order.
|
|
|
|
|
pub instructions: Vec<ScriptInstruction>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A lossless instruction record.
|
|
|
|
|
///
|
|
|
|
|
/// Seven header words are retained in their on-disk order. The sixth word
|
|
|
|
|
/// declares the number of following references; the seventh follows them.
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct ScriptInstruction {
|
|
|
|
|
/// Opaque header words in original file order.
|
|
|
|
|
pub header_words: [u32; INSTRUCTION_WORDS],
|
|
|
|
|
/// References stored after header word five and before word six.
|
|
|
|
|
pub references: Vec<u32>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 20:44:10 +04:00
|
|
|
/// The recovered selector for an instruction's installed handler table.
|
|
|
|
|
///
|
|
|
|
|
/// The GOG AI loader copies 73 function pointers in order. Across all checked
|
|
|
|
|
/// GOG packages the first disk word is either one of these indices or the
|
|
|
|
|
/// explicit `u32::MAX` sentinel. This is a disassembly contract, not an
|
|
|
|
|
/// instruction executor.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum ScriptDispatchSelector {
|
|
|
|
|
/// One of the ordered handlers installed by `ai.dll`.
|
|
|
|
|
Handler(u8),
|
|
|
|
|
/// The on-disk `0xffff_ffff` sentinel.
|
|
|
|
|
Sentinel,
|
|
|
|
|
/// A value not yet observed or accepted by the GOG handler table.
|
|
|
|
|
Unknown(u32),
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:11:10 +04:00
|
|
|
/// Raw inputs resolved by the corpus-reachable `Handler(2)` before it reaches
|
|
|
|
|
/// the original event-record scheduler.
|
|
|
|
|
///
|
|
|
|
|
/// The field names preserve handler slot order, not guessed gameplay meaning.
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
|
|
|
pub struct Handler2RecordInput {
|
|
|
|
|
/// Resolved slot 0 word.
|
|
|
|
|
pub word_0: u32,
|
|
|
|
|
/// Resolved slot 1 scalar.
|
|
|
|
|
pub scalar_1: f32,
|
|
|
|
|
/// Resolved slot 2 word.
|
|
|
|
|
pub word_2: u32,
|
|
|
|
|
/// Resolved slot 3 word.
|
|
|
|
|
pub word_3: u32,
|
|
|
|
|
/// Resolved slot 4 scalar.
|
|
|
|
|
pub scalar_4: f32,
|
|
|
|
|
/// Resolved slot 5 scalar.
|
|
|
|
|
pub scalar_5: f32,
|
|
|
|
|
/// Resolved slot 6 scalar.
|
|
|
|
|
pub scalar_6: f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The exact three-word identity used by the original `Handler(2)` scheduler.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct Handler2RecordKey {
|
|
|
|
|
/// First identity word from resolved slot 0.
|
|
|
|
|
pub word_0: u32,
|
|
|
|
|
/// IEEE-754 bits of resolved slot 4.
|
|
|
|
|
pub scalar_4_bits: u32,
|
|
|
|
|
/// IEEE-754 bits of resolved slot 5.
|
|
|
|
|
pub scalar_5_bits: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Handler2RecordInput> for Handler2RecordKey {
|
|
|
|
|
fn from(input: Handler2RecordInput) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
word_0: input.word_0,
|
|
|
|
|
scalar_4_bits: input.scalar_4.to_bits(),
|
|
|
|
|
scalar_5_bits: input.scalar_5.to_bits(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A single backend-neutral event record created by `Handler(2)`.
|
|
|
|
|
///
|
|
|
|
|
/// This mirrors only the fields whose construction and update rules are
|
|
|
|
|
/// statically recovered. Event-name lookup and the downstream consumer remain
|
|
|
|
|
/// separate runtime work.
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
|
pub struct Handler2Record {
|
|
|
|
|
/// The three-word scheduler identity.
|
|
|
|
|
pub key: Handler2RecordKey,
|
|
|
|
|
/// Resolved slot 1 scalar.
|
|
|
|
|
pub scalar_1: f32,
|
2026-10-20 01:31:26 +04:00
|
|
|
/// Current counter word stored at original record offset `+0x24`.
|
2026-07-18 21:11:10 +04:00
|
|
|
pub counter: u32,
|
2026-10-20 01:31:26 +04:00
|
|
|
/// Original record offset `+0x28`: slot 2 captured when the record was created.
|
|
|
|
|
increment: u32,
|
2026-07-18 21:11:10 +04:00
|
|
|
/// Resolved slot 3 word.
|
|
|
|
|
pub word_3: u32,
|
|
|
|
|
/// Resolved slot 6 scalar.
|
|
|
|
|
pub scalar_6: f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The result of submitting one resolved `Handler(2)` record.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct Handler2RecordUpdate {
|
|
|
|
|
/// Stable record position in insertion order.
|
|
|
|
|
pub index: usize,
|
|
|
|
|
/// Whether a new record was created.
|
|
|
|
|
pub created: bool,
|
|
|
|
|
/// Whether an existing record took the original refresh path.
|
|
|
|
|
pub refreshed: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Deterministic model of the original `Handler(2)` event-record collection.
|
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
|
|
|
pub struct Handler2RecordScheduler {
|
|
|
|
|
records: Vec<Handler2Record>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Handler2RecordScheduler {
|
|
|
|
|
/// Returns event records in original insertion order.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn records(&self) -> &[Handler2Record] {
|
|
|
|
|
&self.records
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Inserts or refreshes one resolved handler input.
|
|
|
|
|
///
|
|
|
|
|
/// The original compares the three identity words bit-for-bit. On an
|
|
|
|
|
/// existing key, it refreshes only when `scalar_1` compares unequal; that
|
2026-10-20 01:31:26 +04:00
|
|
|
/// update replaces `scalar_1` and `scalar_6`, then adds the slot-2 word
|
|
|
|
|
/// captured when that record was created, with x86 wrapping arithmetic.
|
2026-07-18 21:11:10 +04:00
|
|
|
pub fn submit(&mut self, input: Handler2RecordInput) -> Handler2RecordUpdate {
|
|
|
|
|
let key = Handler2RecordKey::from(input);
|
|
|
|
|
if let Some((index, record)) = self
|
|
|
|
|
.records
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find(|(_, record)| record.key == key)
|
|
|
|
|
{
|
|
|
|
|
if !handler_two_scalar_equal(record.scalar_1, input.scalar_1) {
|
|
|
|
|
record.scalar_1 = input.scalar_1;
|
|
|
|
|
record.scalar_6 = input.scalar_6;
|
2026-10-20 01:31:26 +04:00
|
|
|
record.counter = record.counter.wrapping_add(record.increment);
|
2026-07-18 21:11:10 +04:00
|
|
|
return Handler2RecordUpdate {
|
|
|
|
|
index,
|
|
|
|
|
created: false,
|
|
|
|
|
refreshed: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return Handler2RecordUpdate {
|
|
|
|
|
index,
|
|
|
|
|
created: false,
|
|
|
|
|
refreshed: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
let index = self.records.len();
|
|
|
|
|
self.records.push(Handler2Record {
|
|
|
|
|
key,
|
|
|
|
|
scalar_1: input.scalar_1,
|
|
|
|
|
counter: input.word_2,
|
2026-10-20 01:31:26 +04:00
|
|
|
increment: input.word_2,
|
2026-07-18 21:11:10 +04:00
|
|
|
word_3: input.word_3,
|
|
|
|
|
scalar_6: input.scalar_6,
|
|
|
|
|
});
|
|
|
|
|
Handler2RecordUpdate {
|
|
|
|
|
index,
|
|
|
|
|
created: true,
|
|
|
|
|
refreshed: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handler_two_scalar_equal(left: f32, right: f32) -> bool {
|
|
|
|
|
if left.is_nan() || right.is_nan() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let left_bits = left.to_bits();
|
|
|
|
|
let right_bits = right.to_bits();
|
|
|
|
|
left_bits == right_bits || (is_f32_zero_bits(left_bits) && is_f32_zero_bits(right_bits))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_f32_zero_bits(bits: u32) -> bool {
|
|
|
|
|
matches!(bits, 0 | 0x8000_0000)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:17:57 +04:00
|
|
|
/// Parses the numeric `VAR(...)` declarations from a legacy `varset.var` file.
|
|
|
|
|
///
|
|
|
|
|
/// Parsing is line-oriented and byte-safe: non-UTF-8 comments remain opaque,
|
|
|
|
|
/// while the declaration head, type, name, and default value must be ASCII.
|
|
|
|
|
/// `STRING(...)`, `FUNCTION(...)`, and all other non-`VAR` lines remain outside
|
|
|
|
|
/// this recovered numeric-default contract.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns a typed error for malformed supported declarations or inputs above
|
|
|
|
|
/// the fixed declaration limit.
|
|
|
|
|
pub fn parse_varset(bytes: &[u8]) -> Result<VarSet, VarSetError> {
|
|
|
|
|
let mut declarations = Vec::new();
|
|
|
|
|
for (line_index, raw_line) in bytes.split(|byte| *byte == b'\n').enumerate() {
|
|
|
|
|
let line = trim_ascii(strip_line_comment(raw_line));
|
|
|
|
|
if !line.starts_with(b"VAR(") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if declarations.len() == MAX_VARSET_DECLARATIONS {
|
|
|
|
|
return Err(VarSetError::TooManyDeclarations {
|
|
|
|
|
limit: MAX_VARSET_DECLARATIONS,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
declarations.push(parse_varset_declaration(line, line_index + 1)?);
|
|
|
|
|
}
|
|
|
|
|
Ok(VarSet { declarations })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn strip_line_comment(line: &[u8]) -> &[u8] {
|
|
|
|
|
line.windows(2)
|
|
|
|
|
.position(|window| window == b"//")
|
|
|
|
|
.map_or(line, |index| &line[..index])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn trim_ascii(bytes: &[u8]) -> &[u8] {
|
|
|
|
|
let start = bytes
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|byte| !byte.is_ascii_whitespace())
|
|
|
|
|
.unwrap_or(bytes.len());
|
|
|
|
|
let end = bytes
|
|
|
|
|
.iter()
|
|
|
|
|
.rposition(|byte| !byte.is_ascii_whitespace())
|
|
|
|
|
.map_or(start, |index| index + 1);
|
|
|
|
|
&bytes[start..end]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_varset_declaration(
|
|
|
|
|
line: &[u8],
|
|
|
|
|
line_number: usize,
|
|
|
|
|
) -> Result<VarSetDeclaration, VarSetError> {
|
|
|
|
|
let declaration = line
|
|
|
|
|
.strip_prefix(b"VAR(")
|
|
|
|
|
.and_then(|body| body.strip_suffix(b")"))
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
line.strip_prefix(b"VAR(")
|
|
|
|
|
.and_then(|body| body.strip_suffix(b");"))
|
|
|
|
|
})
|
|
|
|
|
.ok_or(VarSetError::UnterminatedDeclaration { line: line_number })?;
|
|
|
|
|
let mut fields = declaration.split(|byte| *byte == b',').map(trim_ascii);
|
|
|
|
|
let type_raw = fields
|
|
|
|
|
.next()
|
|
|
|
|
.filter(|field| !field.is_empty())
|
|
|
|
|
.ok_or(VarSetError::MissingField { line: line_number })?;
|
|
|
|
|
let name_raw = fields
|
|
|
|
|
.next()
|
|
|
|
|
.filter(|field| !field.is_empty())
|
|
|
|
|
.ok_or(VarSetError::MissingField { line: line_number })?;
|
|
|
|
|
let default_raw = fields
|
|
|
|
|
.next()
|
|
|
|
|
.filter(|field| !field.is_empty())
|
|
|
|
|
.ok_or(VarSetError::MissingField { line: line_number })?;
|
|
|
|
|
let type_text = std::str::from_utf8(type_raw)
|
|
|
|
|
.map_err(|_| VarSetError::NonAsciiField { line: line_number })?;
|
|
|
|
|
let name = std::str::from_utf8(name_raw)
|
|
|
|
|
.map_err(|_| VarSetError::NonAsciiField { line: line_number })?
|
|
|
|
|
.to_owned();
|
|
|
|
|
let default_text = std::str::from_utf8(default_raw)
|
|
|
|
|
.map_err(|_| VarSetError::NonAsciiField { line: line_number })?;
|
|
|
|
|
let (type_name, default_value) = match type_text {
|
|
|
|
|
"float" => {
|
|
|
|
|
let value = default_text
|
|
|
|
|
.parse::<f32>()
|
|
|
|
|
.ok()
|
|
|
|
|
.filter(|value| value.is_finite())
|
|
|
|
|
.ok_or(VarSetError::InvalidFloat { line: line_number })?;
|
|
|
|
|
(VarSetType::Float, VarSetDefault::FloatBits(value.to_bits()))
|
|
|
|
|
}
|
|
|
|
|
"DWORD" => (
|
|
|
|
|
VarSetType::Dword,
|
|
|
|
|
VarSetDefault::Dword(parse_varset_dword(default_text, line_number)?),
|
|
|
|
|
),
|
|
|
|
|
_ => return Err(VarSetError::UnsupportedType { line: line_number }),
|
|
|
|
|
};
|
|
|
|
|
Ok(VarSetDeclaration {
|
|
|
|
|
type_name,
|
|
|
|
|
name,
|
|
|
|
|
default_value,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_varset_dword(value: &str, line: usize) -> Result<u32, VarSetError> {
|
|
|
|
|
let (radix, digits) = value
|
|
|
|
|
.strip_prefix("0x")
|
|
|
|
|
.or_else(|| value.strip_prefix("0X"))
|
|
|
|
|
.map_or((10, value), |digits| (16, digits));
|
|
|
|
|
u32::from_str_radix(digits, radix).map_err(|_| VarSetError::InvalidDword { line })
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 20:44:10 +04:00
|
|
|
impl ScriptInstruction {
|
|
|
|
|
/// Returns the recovered dispatch selector from the first disk word.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn dispatch_selector(&self) -> ScriptDispatchSelector {
|
|
|
|
|
match self.header_words[0] {
|
2026-09-06 05:22:12 +04:00
|
|
|
value if value < GOG_HANDLER_COUNT => {
|
2026-07-18 20:44:10 +04:00
|
|
|
ScriptDispatchSelector::Handler(self.header_words[0] as u8)
|
|
|
|
|
}
|
|
|
|
|
u32::MAX => ScriptDispatchSelector::Sentinel,
|
|
|
|
|
value => ScriptDispatchSelector::Unknown(value),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 20:40:17 +04:00
|
|
|
/// Decodes a compiled AI package using default safety limits.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns a bounded [`DecodeError`] on truncated or oversized input.
|
|
|
|
|
pub fn decode(bytes: &[u8]) -> Result<ScriptPackage, DecodeError> {
|
|
|
|
|
decode_with_limits(bytes, Limits::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decodes a compiled AI package using explicit safety limits.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns a bounded [`DecodeError`] on truncated or oversized input.
|
|
|
|
|
pub fn decode_with_limits(bytes: &[u8], limits: Limits) -> Result<ScriptPackage, DecodeError> {
|
|
|
|
|
if u64::try_from(bytes.len()).map_err(|_| DecodeError::IntegerOverflow)? > limits.max_file_bytes
|
|
|
|
|
{
|
|
|
|
|
return Err(DecodeError::LimitExceeded {
|
|
|
|
|
count: u64::try_from(bytes.len()).map_err(|_| DecodeError::IntegerOverflow)?,
|
|
|
|
|
limit: limits.max_file_bytes,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let mut cursor = Cursor::new(bytes);
|
|
|
|
|
let opcode_handler_count = cursor.read_u32_le()?;
|
|
|
|
|
let event_count = cursor.read_u32_le()?;
|
|
|
|
|
checked_allocation_len(u64::from(event_count), u64::from(limits.max_entries))?;
|
|
|
|
|
let mut events =
|
|
|
|
|
Vec::with_capacity(usize::try_from(event_count).map_err(|_| DecodeError::IntegerOverflow)?);
|
|
|
|
|
for _ in 0..event_count {
|
|
|
|
|
events.push(read_event(&mut cursor, limits)?);
|
|
|
|
|
}
|
|
|
|
|
let trailing_bytes = cursor.read_exact(cursor.remaining())?.to_vec();
|
|
|
|
|
Ok(ScriptPackage {
|
|
|
|
|
opcode_handler_count,
|
|
|
|
|
events,
|
|
|
|
|
trailing_bytes,
|
|
|
|
|
raw: Arc::from(bytes),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_event(cursor: &mut Cursor<'_>, limits: Limits) -> Result<ScriptEvent, DecodeError> {
|
|
|
|
|
let name_len = cursor.read_u32_le()?;
|
|
|
|
|
let name_bytes = u64::from(name_len)
|
|
|
|
|
.checked_add(1)
|
|
|
|
|
.ok_or(DecodeError::IntegerOverflow)?;
|
|
|
|
|
let name_len_usize = checked_allocation_len(name_bytes, u64::from(limits.max_string_bytes))?;
|
|
|
|
|
let name_raw = cursor.read_exact(name_len_usize)?.to_vec();
|
|
|
|
|
if name_raw.last().copied() != Some(0) {
|
|
|
|
|
return Err(DecodeError::Invalid(
|
|
|
|
|
"script event name is not NUL terminated",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let event_word = cursor.read_u32_le()?;
|
|
|
|
|
let instruction_count = cursor.read_u32_le()?;
|
|
|
|
|
checked_allocation_len(u64::from(instruction_count), u64::from(limits.max_entries))?;
|
|
|
|
|
let minimum = u64::from(instruction_count)
|
|
|
|
|
.checked_mul(INSTRUCTION_HEADER_BYTES)
|
|
|
|
|
.ok_or(DecodeError::IntegerOverflow)?;
|
|
|
|
|
if minimum > u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)? {
|
|
|
|
|
return Err(DecodeError::UnexpectedEof {
|
|
|
|
|
offset: cursor.offset(),
|
|
|
|
|
needed: minimum,
|
|
|
|
|
remaining: u64::try_from(cursor.remaining())
|
|
|
|
|
.map_err(|_| DecodeError::IntegerOverflow)?,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
let mut instructions = Vec::with_capacity(
|
|
|
|
|
usize::try_from(instruction_count).map_err(|_| DecodeError::IntegerOverflow)?,
|
|
|
|
|
);
|
|
|
|
|
for _ in 0..instruction_count {
|
|
|
|
|
instructions.push(read_instruction(cursor, limits)?);
|
|
|
|
|
}
|
|
|
|
|
Ok(ScriptEvent {
|
|
|
|
|
name_len,
|
|
|
|
|
name_raw,
|
|
|
|
|
event_word,
|
|
|
|
|
instructions,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_instruction(
|
|
|
|
|
cursor: &mut Cursor<'_>,
|
|
|
|
|
limits: Limits,
|
|
|
|
|
) -> Result<ScriptInstruction, DecodeError> {
|
|
|
|
|
let mut header_words = [0; INSTRUCTION_WORDS];
|
|
|
|
|
for word in &mut header_words[..5] {
|
|
|
|
|
*word = cursor.read_u32_le()?;
|
|
|
|
|
}
|
|
|
|
|
header_words[5] = cursor.read_u32_le()?;
|
|
|
|
|
let reference_count = header_words[5];
|
|
|
|
|
let reference_bytes = u64::from(reference_count)
|
|
|
|
|
.checked_mul(4)
|
|
|
|
|
.ok_or(DecodeError::IntegerOverflow)?;
|
|
|
|
|
if reference_bytes
|
|
|
|
|
> u64::try_from(cursor.remaining()).map_err(|_| DecodeError::IntegerOverflow)?
|
|
|
|
|
{
|
|
|
|
|
return Err(DecodeError::UnexpectedEof {
|
|
|
|
|
offset: cursor.offset(),
|
|
|
|
|
needed: reference_bytes,
|
|
|
|
|
remaining: u64::try_from(cursor.remaining())
|
|
|
|
|
.map_err(|_| DecodeError::IntegerOverflow)?,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
checked_allocation_len(
|
|
|
|
|
u64::from(reference_count),
|
|
|
|
|
u64::from(limits.max_array_items),
|
|
|
|
|
)?;
|
|
|
|
|
let mut references = Vec::with_capacity(
|
|
|
|
|
usize::try_from(reference_count).map_err(|_| DecodeError::IntegerOverflow)?,
|
|
|
|
|
);
|
|
|
|
|
for _ in 0..reference_count {
|
|
|
|
|
references.push(cursor.read_u32_le()?);
|
|
|
|
|
}
|
|
|
|
|
header_words[6] = cursor.read_u32_le()?;
|
|
|
|
|
Ok(ScriptInstruction {
|
|
|
|
|
header_words,
|
|
|
|
|
references,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-07-18 20:44:10 +04:00
|
|
|
use super::{
|
2026-07-18 21:47:33 +04:00
|
|
|
decode, decode_with_limits, parse_varset, Handler15ResolveError, Handler15TargetPayload,
|
2026-10-20 04:11:35 +04:00
|
|
|
Handler19DwordWrite, Handler19InitInput, Handler19InitResolution, Handler19ResolveError,
|
|
|
|
|
Handler2RecordInput, Handler2RecordScheduler, Handler30ResolveError, Handler8Reset,
|
|
|
|
|
Handler8ResolveError, ScriptDispatchSelector, ScriptEvent, ScriptInstruction,
|
|
|
|
|
ScriptPackage, VarSetDefault, VarSetError, VarSetType, VmHostCallbackCommand,
|
|
|
|
|
GOG_HANDLER_COUNT, INSTRUCTION_WORDS,
|
2026-07-18 20:44:10 +04:00
|
|
|
};
|
2026-07-18 20:40:17 +04:00
|
|
|
use fparkan_binary::{DecodeError, Limits};
|
|
|
|
|
|
2026-07-18 21:11:10 +04:00
|
|
|
fn handler_two_input(
|
|
|
|
|
scalar_1: f32,
|
|
|
|
|
scalar_4: f32,
|
|
|
|
|
scalar_5: f32,
|
|
|
|
|
scalar_6: f32,
|
|
|
|
|
word_2: u32,
|
|
|
|
|
) -> Handler2RecordInput {
|
|
|
|
|
Handler2RecordInput {
|
|
|
|
|
word_0: 7,
|
|
|
|
|
scalar_1,
|
|
|
|
|
word_2,
|
|
|
|
|
word_3: 11,
|
|
|
|
|
scalar_4,
|
|
|
|
|
scalar_5,
|
|
|
|
|
scalar_6,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_two_scheduler_uses_three_word_bit_identity_and_refresh_contract() {
|
|
|
|
|
let mut scheduler = Handler2RecordScheduler::default();
|
|
|
|
|
let first = handler_two_input(1.5, -0.0, 3.0, 9.0, 4);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
scheduler.submit(first),
|
|
|
|
|
super::Handler2RecordUpdate {
|
|
|
|
|
index: 0,
|
|
|
|
|
created: true,
|
|
|
|
|
refreshed: false,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(scheduler.records().len(), 1);
|
|
|
|
|
assert_eq!(scheduler.records()[0].counter, 4);
|
|
|
|
|
|
|
|
|
|
let unchanged = handler_two_input(1.5, -0.0, 3.0, 12.0, 99);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
scheduler.submit(unchanged),
|
|
|
|
|
super::Handler2RecordUpdate {
|
|
|
|
|
index: 0,
|
|
|
|
|
created: false,
|
|
|
|
|
refreshed: false,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(scheduler.records()[0].counter, 4);
|
|
|
|
|
assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 9.0_f32.to_bits());
|
|
|
|
|
|
|
|
|
|
let refreshed = handler_two_input(2.5, -0.0, 3.0, 12.0, 99);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
scheduler.submit(refreshed),
|
|
|
|
|
super::Handler2RecordUpdate {
|
|
|
|
|
index: 0,
|
|
|
|
|
created: false,
|
|
|
|
|
refreshed: true,
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-10-20 01:31:26 +04:00
|
|
|
assert_eq!(scheduler.records()[0].counter, 8);
|
2026-07-18 21:11:10 +04:00
|
|
|
assert_eq!(
|
|
|
|
|
scheduler.records()[0].scalar_6.to_bits(),
|
|
|
|
|
12.0_f32.to_bits()
|
|
|
|
|
);
|
|
|
|
|
|
2026-10-20 01:31:26 +04:00
|
|
|
let refreshed_again = handler_two_input(3.5, -0.0, 3.0, 13.0, 7);
|
|
|
|
|
assert!(scheduler.submit(refreshed_again).refreshed);
|
|
|
|
|
assert_eq!(scheduler.records()[0].counter, 12);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
scheduler.records()[0].scalar_6.to_bits(),
|
|
|
|
|
13.0_f32.to_bits()
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-18 21:11:10 +04:00
|
|
|
let positive_zero_key = handler_two_input(2.5, 0.0, 3.0, 12.0, 1);
|
|
|
|
|
assert_eq!(scheduler.submit(positive_zero_key).index, 1);
|
|
|
|
|
assert_eq!(scheduler.records().len(), 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_two_scheduler_refreshes_nan_and_wraps_counter() {
|
|
|
|
|
let mut scheduler = Handler2RecordScheduler::default();
|
|
|
|
|
scheduler.submit(handler_two_input(f32::NAN, 1.0, 2.0, 3.0, u32::MAX));
|
|
|
|
|
let update = scheduler.submit(handler_two_input(f32::NAN, 1.0, 2.0, 4.0, 2));
|
|
|
|
|
assert_eq!(update.index, 0);
|
|
|
|
|
assert!(update.refreshed);
|
2026-10-20 01:31:26 +04:00
|
|
|
assert_eq!(scheduler.records()[0].counter, u32::MAX - 1);
|
2026-07-18 21:11:10 +04:00
|
|
|
assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 4.0_f32.to_bits());
|
2026-10-20 01:31:26 +04:00
|
|
|
|
|
|
|
|
let update = scheduler.submit(handler_two_input(f32::NAN, 1.0, 2.0, 5.0, 17));
|
|
|
|
|
assert!(update.refreshed);
|
|
|
|
|
assert_eq!(scheduler.records()[0].counter, u32::MAX - 2);
|
|
|
|
|
assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 5.0_f32.to_bits());
|
2026-07-18 21:11:10 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_two_scheduler_treats_signed_zero_value_as_unchanged() {
|
|
|
|
|
let mut scheduler = Handler2RecordScheduler::default();
|
|
|
|
|
scheduler.submit(handler_two_input(-0.0, 1.0, 2.0, 3.0, 5));
|
|
|
|
|
let update = scheduler.submit(handler_two_input(0.0, 1.0, 2.0, 4.0, 9));
|
|
|
|
|
assert!(!update.created);
|
|
|
|
|
assert!(!update.refreshed);
|
|
|
|
|
assert_eq!(scheduler.records()[0].counter, 5);
|
|
|
|
|
assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 3.0_f32.to_bits());
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:17:57 +04:00
|
|
|
#[test]
|
|
|
|
|
fn varset_parser_preserves_typed_defaults_and_legacy_comment_bytes() {
|
|
|
|
|
let source = b"// \xff legacy comment\r\n\
|
|
|
|
|
VAR( float, fDifficulty, 0.5) // ignored\r\n\
|
|
|
|
|
VAR( DWORD, CLASS_BUILDING, 0x80000000);\r\n\
|
|
|
|
|
STRING( 8, ignored, ignored, ignored)\r\n";
|
|
|
|
|
let parsed = parse_varset(source).expect("valid varset declarations");
|
|
|
|
|
assert_eq!(parsed.declarations.len(), 2);
|
|
|
|
|
assert_eq!(parsed.declarations[0].type_name, VarSetType::Float);
|
|
|
|
|
assert_eq!(parsed.declarations[0].name, "fDifficulty");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parsed.declarations[0].default_value,
|
|
|
|
|
VarSetDefault::FloatBits(0.5_f32.to_bits())
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(parsed.declarations[1].type_name, VarSetType::Dword);
|
|
|
|
|
assert_eq!(parsed.declarations[1].name, "CLASS_BUILDING");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parsed.declarations[1].default_value.as_dword(),
|
|
|
|
|
Some(0x8000_0000)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(parsed.declarations[0].default_value.as_float(), Some(0.5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn varset_parser_rejects_malformed_supported_declarations() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_varset(b"VAR( float, f0, nope)\n"),
|
|
|
|
|
Err(VarSetError::InvalidFloat { line: 1 })
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_varset(b"VAR( BYTE, b0, 1)\n"),
|
|
|
|
|
Err(VarSetError::UnsupportedType { line: 1 })
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_varset(b"VAR( DWORD, d0, 0x100000000)\n"),
|
|
|
|
|
Err(VarSetError::InvalidDword { line: 1 })
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_varset(b"VAR( DWORD, d0, 1\n"),
|
|
|
|
|
Err(VarSetError::UnterminatedDeclaration { line: 1 })
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:33:14 +04:00
|
|
|
#[test]
|
|
|
|
|
fn handler_thirty_resolves_only_proven_dword_operands() {
|
|
|
|
|
let varset = parse_varset(
|
|
|
|
|
b"VAR( float, f0, 0.5)\nVAR( DWORD, first, 0x12)\nVAR( DWORD, second, 9)\n",
|
|
|
|
|
)
|
|
|
|
|
.expect("varset");
|
|
|
|
|
let instruction = ScriptInstruction {
|
|
|
|
|
header_words: [30, 0, 0, 0, 0, 2, 0],
|
|
|
|
|
references: vec![1, 2],
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
varset.resolve_handler30(&instruction),
|
|
|
|
|
Ok(VmHostCallbackCommand {
|
|
|
|
|
mode: 0,
|
|
|
|
|
first: 0x12,
|
|
|
|
|
second: 9,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:25:28 +04:00
|
|
|
#[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,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:33:14 +04:00
|
|
|
#[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 {
|
|
|
|
|
header_words: [30, 0, 0, 0, 0, 2, 0],
|
|
|
|
|
references: vec![0, 0],
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
varset.resolve_handler30(&float_operand),
|
|
|
|
|
Err(Handler30ResolveError::FloatRequiresX87 { index: 0 })
|
|
|
|
|
);
|
|
|
|
|
let dword_varset = parse_varset(b"VAR( DWORD, d0, 1)\n").expect("dword varset");
|
|
|
|
|
let missing_operand = ScriptInstruction {
|
|
|
|
|
header_words: [30, 0, 0, 0, 0, 1, 0],
|
|
|
|
|
references: vec![0],
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
dword_varset.resolve_handler30(&missing_operand),
|
|
|
|
|
Err(Handler30ResolveError::MissingReference { position: 1 })
|
|
|
|
|
);
|
|
|
|
|
let out_of_range = ScriptInstruction {
|
|
|
|
|
header_words: [30, 0, 0, 0, 0, 2, 0],
|
|
|
|
|
references: vec![1, 1],
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
dword_varset.resolve_handler30(&out_of_range),
|
|
|
|
|
Err(Handler30ResolveError::VarSetIndexOutOfBounds {
|
|
|
|
|
index: 1,
|
|
|
|
|
declarations: 1,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:53:09 +04:00
|
|
|
#[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,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:04:10 +04:00
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_writes_exact_default_init_dwords_without_float_coercion() {
|
|
|
|
|
let varset = parse_varset(
|
|
|
|
|
b"VAR( DWORD, ClanBaseX, 950)\nVAR( DWORD, ClanBaseY, 1000)\nVAR( DWORD, ClanID, 0)\n",
|
|
|
|
|
)
|
|
|
|
|
.expect("default Init targets");
|
|
|
|
|
let writes = varset
|
|
|
|
|
.resolve_handler19(
|
|
|
|
|
&ScriptInstruction {
|
|
|
|
|
header_words: [19, 0, 0, 0, 0, 3, 0],
|
|
|
|
|
references: vec![0, 1, 2],
|
|
|
|
|
},
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 500,
|
|
|
|
|
second_x87_word: 752,
|
|
|
|
|
third_word: 0,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.expect("resolved Init writes");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
writes,
|
|
|
|
|
[
|
|
|
|
|
Handler19DwordWrite {
|
|
|
|
|
index: 0,
|
|
|
|
|
value: 500
|
|
|
|
|
},
|
|
|
|
|
Handler19DwordWrite {
|
|
|
|
|
index: 1,
|
|
|
|
|
value: 752
|
|
|
|
|
},
|
|
|
|
|
Handler19DwordWrite { index: 2, value: 0 },
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_keeps_target_errors_explicit() {
|
|
|
|
|
let float = parse_varset(b"VAR( float, f0, 1.0)\n").expect("float target");
|
|
|
|
|
let instruction = ScriptInstruction {
|
|
|
|
|
header_words: [19, 0, 0, 0, 0, 1, 0],
|
|
|
|
|
references: vec![0],
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
float.resolve_handler19(
|
|
|
|
|
&instruction,
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 0,
|
|
|
|
|
second_x87_word: 0,
|
|
|
|
|
third_word: 0,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Err(Handler19ResolveError::UnexpectedType {
|
|
|
|
|
position: 0,
|
|
|
|
|
found: VarSetType::Float,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
let empty = parse_varset(b"").expect("empty varset");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
empty.resolve_handler19(
|
|
|
|
|
&ScriptInstruction {
|
|
|
|
|
header_words: [19, 0, 0, 0, 0, 0, 0],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 0,
|
|
|
|
|
second_x87_word: 0,
|
|
|
|
|
third_word: 0,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Err(Handler19ResolveError::MissingReference { position: 0 })
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 04:11:35 +04:00
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_init_uses_first_exact_event_and_keeps_instruction_order() {
|
|
|
|
|
let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n")
|
|
|
|
|
.expect("DWORD targets");
|
|
|
|
|
let action = |references: Vec<u32>| ScriptInstruction {
|
|
|
|
|
header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0],
|
|
|
|
|
references,
|
|
|
|
|
};
|
|
|
|
|
let package = ScriptPackage {
|
|
|
|
|
opcode_handler_count: 73,
|
|
|
|
|
events: vec![
|
|
|
|
|
ScriptEvent {
|
|
|
|
|
name_len: 11,
|
|
|
|
|
name_raw: b"Init\0suffix\0".to_vec(),
|
|
|
|
|
event_word: 0x1234,
|
|
|
|
|
instructions: vec![action(vec![0, 1, 2]), action(vec![1, 0, 2])],
|
|
|
|
|
},
|
|
|
|
|
ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"Init\0".to_vec(),
|
|
|
|
|
event_word: 0x5678,
|
|
|
|
|
instructions: vec![ScriptInstruction {
|
|
|
|
|
header_words: [18, u32::MAX, u32::MAX, 0, 0, 0, 0],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
trailing_bytes: Vec::new(),
|
|
|
|
|
raw: std::sync::Arc::from(&b""[..]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let result = varset
|
|
|
|
|
.resolve_handler19_init(
|
|
|
|
|
&package,
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 7,
|
|
|
|
|
second_x87_word: 8,
|
|
|
|
|
third_word: 9,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.expect("valid first Init event");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result,
|
|
|
|
|
Handler19InitResolution::Executed {
|
|
|
|
|
event_index: 0,
|
|
|
|
|
event_word: 0x1234,
|
|
|
|
|
writes: vec![
|
|
|
|
|
Handler19DwordWrite { index: 0, value: 7 },
|
|
|
|
|
Handler19DwordWrite { index: 1, value: 8 },
|
|
|
|
|
Handler19DwordWrite { index: 2, value: 9 },
|
|
|
|
|
Handler19DwordWrite { index: 1, value: 7 },
|
|
|
|
|
Handler19DwordWrite { index: 0, value: 8 },
|
|
|
|
|
Handler19DwordWrite { index: 2, value: 9 },
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_init_missing_exact_name_has_no_writes() {
|
|
|
|
|
let varset = parse_varset(b"").expect("empty varset");
|
|
|
|
|
let package = ScriptPackage {
|
|
|
|
|
opcode_handler_count: 73,
|
|
|
|
|
events: vec![
|
|
|
|
|
ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"init\0".to_vec(),
|
|
|
|
|
event_word: 1,
|
|
|
|
|
instructions: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"INIT\0".to_vec(),
|
|
|
|
|
event_word: 2,
|
|
|
|
|
instructions: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
trailing_bytes: Vec::new(),
|
|
|
|
|
raw: std::sync::Arc::from(&b""[..]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
varset.resolve_handler19_init(
|
|
|
|
|
&package,
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 7,
|
|
|
|
|
second_x87_word: 8,
|
|
|
|
|
third_word: 9,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Ok(Handler19InitResolution::Missing)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_init_accepts_empty_first_event_as_executed_with_no_writes() {
|
|
|
|
|
let varset = parse_varset(b"").expect("empty varset");
|
|
|
|
|
let package = ScriptPackage {
|
|
|
|
|
opcode_handler_count: 73,
|
|
|
|
|
events: vec![
|
|
|
|
|
ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"Init\0".to_vec(),
|
|
|
|
|
event_word: 0x1234,
|
|
|
|
|
instructions: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"Init\0".to_vec(),
|
|
|
|
|
event_word: 0x5678,
|
|
|
|
|
instructions: vec![ScriptInstruction {
|
|
|
|
|
header_words: [18, u32::MAX, u32::MAX, 0, 0, 0, 0],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
trailing_bytes: Vec::new(),
|
|
|
|
|
raw: std::sync::Arc::from(&b""[..]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
varset.resolve_handler19_init(
|
|
|
|
|
&package,
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 7,
|
|
|
|
|
second_x87_word: 8,
|
|
|
|
|
third_word: 9,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Ok(Handler19InitResolution::Executed {
|
|
|
|
|
event_index: 0,
|
|
|
|
|
event_word: 0x1234,
|
|
|
|
|
writes: Vec::new(),
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_init_reports_unsupported_selector_and_discards_prior_writes() {
|
|
|
|
|
let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n")
|
|
|
|
|
.expect("DWORD targets");
|
|
|
|
|
let package = ScriptPackage {
|
|
|
|
|
opcode_handler_count: 73,
|
|
|
|
|
events: vec![ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"Init\0".to_vec(),
|
|
|
|
|
event_word: 0,
|
|
|
|
|
instructions: vec![
|
|
|
|
|
ScriptInstruction {
|
|
|
|
|
header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0],
|
|
|
|
|
references: vec![0, 1, 2],
|
|
|
|
|
},
|
|
|
|
|
ScriptInstruction {
|
|
|
|
|
header_words: [18, u32::MAX, u32::MAX, 0, 0, 0, 0],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
ScriptInstruction {
|
|
|
|
|
header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0],
|
|
|
|
|
references: vec![0, 1, 2],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}],
|
|
|
|
|
trailing_bytes: Vec::new(),
|
|
|
|
|
raw: std::sync::Arc::from(&b""[..]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
varset.resolve_handler19_init(
|
|
|
|
|
&package,
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 7,
|
|
|
|
|
second_x87_word: 8,
|
|
|
|
|
third_word: 9,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Ok(Handler19InitResolution::Unsupported {
|
|
|
|
|
event_index: 0,
|
|
|
|
|
instruction_index: 1,
|
|
|
|
|
error: Handler19ResolveError::UnsupportedSelector {
|
|
|
|
|
found: super::ScriptDispatchSelector::Handler(18),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_init_stops_at_unsupported_selector_before_any_write() {
|
|
|
|
|
let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n")
|
|
|
|
|
.expect("DWORD targets");
|
|
|
|
|
let package = ScriptPackage {
|
|
|
|
|
opcode_handler_count: 73,
|
|
|
|
|
events: vec![ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"Init\0".to_vec(),
|
|
|
|
|
event_word: 0,
|
|
|
|
|
instructions: vec![
|
|
|
|
|
ScriptInstruction {
|
|
|
|
|
header_words: [18, u32::MAX, u32::MAX, 0, 0, 0, 0],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
ScriptInstruction {
|
|
|
|
|
header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0],
|
|
|
|
|
references: vec![0, 1, 2],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}],
|
|
|
|
|
trailing_bytes: Vec::new(),
|
|
|
|
|
raw: std::sync::Arc::from(&b""[..]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
varset.resolve_handler19_init(
|
|
|
|
|
&package,
|
|
|
|
|
Handler19InitInput {
|
|
|
|
|
first_x87_word: 7,
|
|
|
|
|
second_x87_word: 8,
|
|
|
|
|
third_word: 9,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Ok(Handler19InitResolution::Unsupported {
|
|
|
|
|
event_index: 0,
|
|
|
|
|
instruction_index: 0,
|
|
|
|
|
error: Handler19ResolveError::UnsupportedSelector {
|
|
|
|
|
found: super::ScriptDispatchSelector::Handler(18),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_nineteen_init_types_unsupported_control_and_result_save() {
|
|
|
|
|
let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n")
|
|
|
|
|
.expect("DWORD targets");
|
|
|
|
|
let input = Handler19InitInput {
|
|
|
|
|
first_x87_word: 7,
|
|
|
|
|
second_x87_word: 8,
|
|
|
|
|
third_word: 9,
|
|
|
|
|
};
|
|
|
|
|
let instruction = |control_word, result_word| ScriptInstruction {
|
|
|
|
|
header_words: [19, control_word, result_word, 0, 0, 3, 0],
|
|
|
|
|
references: vec![0, 1, 2],
|
|
|
|
|
};
|
|
|
|
|
let resolve = |instruction| {
|
|
|
|
|
let package = ScriptPackage {
|
|
|
|
|
opcode_handler_count: 73,
|
|
|
|
|
events: vec![ScriptEvent {
|
|
|
|
|
name_len: 4,
|
|
|
|
|
name_raw: b"Init\0".to_vec(),
|
|
|
|
|
event_word: 0,
|
|
|
|
|
instructions: vec![instruction],
|
|
|
|
|
}],
|
|
|
|
|
trailing_bytes: Vec::new(),
|
|
|
|
|
raw: std::sync::Arc::from(&b""[..]),
|
|
|
|
|
};
|
|
|
|
|
varset.resolve_handler19_init(&package, input)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolve(instruction(0, u32::MAX)),
|
|
|
|
|
Ok(Handler19InitResolution::Unsupported {
|
|
|
|
|
event_index: 0,
|
|
|
|
|
instruction_index: 0,
|
|
|
|
|
error: Handler19ResolveError::UnsupportedControlWord { found: 0 },
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolve(instruction(u32::MAX, 2)),
|
|
|
|
|
Ok(Handler19InitResolution::Unsupported {
|
|
|
|
|
event_index: 0,
|
|
|
|
|
instruction_index: 0,
|
|
|
|
|
error: Handler19ResolveError::UnsupportedResultSave { found: 2 },
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 21:47:33 +04:00
|
|
|
#[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",
|
|
|
|
|
)
|
|
|
|
|
.expect("valid Handler(15) varset");
|
|
|
|
|
let instruction = ScriptInstruction {
|
|
|
|
|
header_words: [15, 0, 0, 0, 0, 11, 0],
|
|
|
|
|
references: (0..11).collect(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let resolved = varset
|
|
|
|
|
.resolve_handler15(&instruction)
|
|
|
|
|
.expect("exact Handler(15) layout");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
[
|
|
|
|
|
resolved.word_0,
|
|
|
|
|
resolved.word_1,
|
|
|
|
|
resolved.word_2,
|
|
|
|
|
resolved.word_3
|
|
|
|
|
],
|
|
|
|
|
[1, 2, 3, 4]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
[
|
|
|
|
|
resolved.scalar_4.to_bits(),
|
|
|
|
|
resolved.scalar_5.to_bits(),
|
|
|
|
|
resolved.scalar_6.to_bits(),
|
|
|
|
|
resolved.scalar_7.to_bits()
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
1.5_f32.to_bits(),
|
|
|
|
|
2.5_f32.to_bits(),
|
|
|
|
|
3.5_f32.to_bits(),
|
|
|
|
|
4.5_f32.to_bits()
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
resolved.target,
|
|
|
|
|
Handler15TargetPayload::ByPlace {
|
|
|
|
|
first: 12,
|
|
|
|
|
second: 13
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handler_fifteen_rejects_missing_and_wrongly_typed_operands() {
|
|
|
|
|
let wrong_scalar = parse_varset(
|
|
|
|
|
b"VAR( DWORD, d0, 1)\nVAR( DWORD, d1, 2)\nVAR( DWORD, d2, 3)\nVAR( DWORD, d3, 4)\nVAR( DWORD, wrong, 5)\n",
|
|
|
|
|
)
|
|
|
|
|
.expect("valid varset");
|
|
|
|
|
let instruction = ScriptInstruction {
|
|
|
|
|
header_words: [15, 0, 0, 0, 0, 5, 0],
|
|
|
|
|
references: (0..5).collect(),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
wrong_scalar.resolve_handler15(&instruction),
|
|
|
|
|
Err(Handler15ResolveError::UnexpectedType {
|
|
|
|
|
position: 4,
|
|
|
|
|
expected: VarSetType::Float,
|
|
|
|
|
found: VarSetType::Dword,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
let empty = parse_varset(b"").expect("empty varset");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
empty.resolve_handler15(&ScriptInstruction {
|
|
|
|
|
header_words: [15, 0, 0, 0, 0, 0, 0],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
}),
|
|
|
|
|
Err(Handler15ResolveError::MissingReference { position: 0 })
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 20:40:17 +04:00
|
|
|
#[test]
|
|
|
|
|
fn decodes_lossless_event_and_instruction_records() {
|
|
|
|
|
let mut bytes = Vec::new();
|
|
|
|
|
bytes.extend_from_slice(&73_u32.to_le_bytes());
|
|
|
|
|
bytes.extend_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
bytes.extend_from_slice(&4_u32.to_le_bytes());
|
|
|
|
|
bytes.extend_from_slice(b"Init\0");
|
|
|
|
|
bytes.extend_from_slice(&9_u32.to_le_bytes());
|
|
|
|
|
bytes.extend_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
for word in [1_u32, 2, 3, 4, 5, 2] {
|
|
|
|
|
bytes.extend_from_slice(&word.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
for reference in [7_u32, 8] {
|
|
|
|
|
bytes.extend_from_slice(&reference.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
bytes.extend_from_slice(&6_u32.to_le_bytes());
|
|
|
|
|
bytes.extend_from_slice(&[0xaa, 0xbb]);
|
|
|
|
|
|
|
|
|
|
let package = decode(&bytes).expect("valid script package");
|
|
|
|
|
assert_eq!(package.opcode_handler_count, 73);
|
|
|
|
|
assert_eq!(package.events.len(), 1);
|
|
|
|
|
assert_eq!(package.events[0].name_raw, b"Init\0");
|
|
|
|
|
assert_eq!(package.events[0].event_word, 9);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
package.events[0].instructions[0].header_words,
|
|
|
|
|
[1, 2, 3, 4, 5, 2, 6]
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(package.events[0].instructions[0].references, [7, 8]);
|
2026-07-18 20:44:10 +04:00
|
|
|
assert_eq!(
|
|
|
|
|
package.events[0].instructions[0].dispatch_selector(),
|
|
|
|
|
ScriptDispatchSelector::Handler(1)
|
|
|
|
|
);
|
2026-07-18 20:40:17 +04:00
|
|
|
assert_eq!(package.trailing_bytes, [0xaa, 0xbb]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rejects_missing_event_nul_and_truncated_references() {
|
|
|
|
|
let mut missing_nul = Vec::new();
|
|
|
|
|
missing_nul.extend_from_slice(&0_u32.to_le_bytes());
|
|
|
|
|
missing_nul.extend_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
missing_nul.extend_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
missing_nul.extend_from_slice(b"AB");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
decode(&missing_nul),
|
|
|
|
|
Err(DecodeError::Invalid(
|
|
|
|
|
"script event name is not NUL terminated"
|
|
|
|
|
))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut truncated = Vec::new();
|
|
|
|
|
truncated.extend_from_slice(&0_u32.to_le_bytes());
|
|
|
|
|
truncated.extend_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
truncated.extend_from_slice(&0_u32.to_le_bytes());
|
|
|
|
|
truncated.push(0);
|
|
|
|
|
truncated.extend_from_slice(&0_u32.to_le_bytes());
|
|
|
|
|
truncated.extend_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
for word in [0_u32, 0, 0, 0, 0, 1] {
|
|
|
|
|
truncated.extend_from_slice(&word.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode(&truncated),
|
|
|
|
|
Err(DecodeError::UnexpectedEof { .. })
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn explicit_limits_bound_event_allocations() {
|
|
|
|
|
let bytes = [0_u8; 8];
|
|
|
|
|
let limits = Limits {
|
|
|
|
|
max_entries: 0,
|
|
|
|
|
..Limits::default()
|
|
|
|
|
};
|
|
|
|
|
assert!(decode_with_limits(&bytes, limits).is_ok());
|
|
|
|
|
|
|
|
|
|
let bytes = [0_u8, 0, 0, 0, 1, 0, 0, 0];
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_with_limits(&bytes, limits),
|
|
|
|
|
Err(DecodeError::LimitExceeded { .. })
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-07-18 20:44:10 +04:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dispatch_selector_preserves_sentinel_and_unobserved_values() {
|
|
|
|
|
let mut instruction = super::ScriptInstruction {
|
|
|
|
|
header_words: [u32::MAX; INSTRUCTION_WORDS],
|
|
|
|
|
references: Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
instruction.dispatch_selector(),
|
|
|
|
|
ScriptDispatchSelector::Sentinel
|
|
|
|
|
);
|
|
|
|
|
instruction.header_words[0] = GOG_HANDLER_COUNT;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
instruction.dispatch_selector(),
|
|
|
|
|
ScriptDispatchSelector::Unknown(GOG_HANDLER_COUNT)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-18 20:40:17 +04:00
|
|
|
}
|