feat(script): resolve handler fifteen inputs
This commit is contained in:
@@ -64,6 +64,119 @@ pub struct VmHostCallbackCommand {
|
||||
pub second: u32,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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 {}
|
||||
|
||||
/// Error resolving the proven `Handler(30)` callback contract.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Handler30ResolveError {
|
||||
@@ -158,6 +271,114 @@ impl VarSet {
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VarSetDefault {
|
||||
@@ -692,9 +913,10 @@ fn read_instruction(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
decode, decode_with_limits, parse_varset, Handler2RecordInput, Handler2RecordScheduler,
|
||||
Handler30ResolveError, ScriptDispatchSelector, ScriptInstruction, VarSetDefault,
|
||||
VarSetError, VarSetType, VmHostCallbackCommand, GOG_HANDLER_COUNT, INSTRUCTION_WORDS,
|
||||
decode, decode_with_limits, parse_varset, Handler15ResolveError, Handler15TargetPayload,
|
||||
Handler2RecordInput, Handler2RecordScheduler, Handler30ResolveError,
|
||||
ScriptDispatchSelector, ScriptInstruction, VarSetDefault, VarSetError, VarSetType,
|
||||
VmHostCallbackCommand, GOG_HANDLER_COUNT, INSTRUCTION_WORDS,
|
||||
};
|
||||
use fparkan_binary::{DecodeError, Limits};
|
||||
|
||||
@@ -881,6 +1103,80 @@ STRING( 8, ignored, ignored, ignored)\r\n";
|
||||
);
|
||||
}
|
||||
|
||||
#[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 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_lossless_event_and_instruction_records() {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
@@ -173,6 +173,35 @@ layout prerequisites/modifiers/unlocks. Formula evaluator требует strict
|
||||
grammar/version, typed operands, deterministic numeric policy, bounded stack и
|
||||
явных errors; x87-compatible rounding нужен там, где оно выбирает ветку.
|
||||
|
||||
### Handler(15): typed target-call boundary
|
||||
|
||||
`Handler(15)` is the sixteenth VM-table entry at GOG `ai.dll` VA `0x10008054`
|
||||
and the second most frequent non-sentinel selector: 236 records in 28 of 58
|
||||
GOG packages. It is not a one-word opcode. Across the complete corpus,
|
||||
references `0..3` are `DWORD`, `4..7` are `float`, and `8` is the DWORD mode.
|
||||
The original resolves the first four through `0x10013570`, the next four
|
||||
through x87 scalar helper `0x10013190`, then reads the mode through
|
||||
`0x10013570`.
|
||||
|
||||
The shipped `varset.var` names the only observed mode values: `NONE=0` uses
|
||||
9 total references; `TARGET_BY_LOGIC_ID=0x0201`, `TARGET_BY_TYPE=0x0203`,
|
||||
`TARGET_NOT_DEFINED=0x0204`, and `TARGET_BY_NAME=0x0205` use 10; and
|
||||
`TARGET_BY_PLACE=0x0202` uses 11. The corpus contains 34/122/80 records in
|
||||
these three arities. The trailing references at positions 9 and, only for
|
||||
`TARGET_BY_PLACE`, 10 are all in-range DWORD declarations.
|
||||
|
||||
After resolving those inputs, the handler looks up an opaque target through
|
||||
virtual slot `+0x1c` on `this+0x3d8` using the first word. A missing target
|
||||
sets VM flag `+0x50=5`; otherwise the handler builds a temporary call record,
|
||||
applies the mode-specific tail, and invokes the target's `+0x0c` virtual slot
|
||||
with that record and the third word. Its zero/non-zero result becomes
|
||||
`+0x50=0/1`. The target type, the virtual method's semantic action, and its
|
||||
return value remain unproven. Accordingly `VarSet::resolve_handler15` only
|
||||
materializes a type-checked `Handler15Invocation` and `Handler15TargetPayload`;
|
||||
it never executes the opaque target call. Missing, out-of-range, wrong-type,
|
||||
and unobserved-mode inputs are explicit errors. Reproduce the static evidence
|
||||
with `tools/ghidra/ExportAiVmHandler15.java`.
|
||||
|
||||
## Готовность
|
||||
|
||||
Все demo packages должны проходить package/version checks, offsets оставаться
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Emits Handler(15), a frequent non-sentinel selector in the GOG compiled
|
||||
// script corpus. Run headless; the original PE remains read only.
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
|
||||
public class ExportAiVmHandler15 extends GhidraScript {
|
||||
private static final long ADDRESS = 0x10008054L;
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||
.getAddress(ADDRESS);
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
println("===== AI VM Handler(15) =====");
|
||||
if (function == null) { println("missing"); return; }
|
||||
DecompInterface decompiler = new DecompInterface();
|
||||
decompiler.openProgram(currentProgram);
|
||||
println(decompiler.decompileFunction(function, 60, monitor).getDecompiledFunction().getC());
|
||||
decompiler.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user