feat(script): resolve handler thirty dword callbacks
This commit is contained in:
@@ -50,6 +50,116 @@ pub enum VarSetDefault {
|
|||||||
Dword(u32),
|
Dword(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
let first = self.resolve_handler30_operand(instruction, 0)?;
|
||||||
|
let second = self.resolve_handler30_operand(instruction, 1)?;
|
||||||
|
Ok(VmHostCallbackCommand {
|
||||||
|
mode: 0,
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_handler30_operand(
|
||||||
|
&self,
|
||||||
|
instruction: &ScriptInstruction,
|
||||||
|
position: usize,
|
||||||
|
) -> Result<u32, Handler30ResolveError> {
|
||||||
|
let index = *instruction
|
||||||
|
.references
|
||||||
|
.get(position)
|
||||||
|
.ok_or(Handler30ResolveError::MissingReference { position })?;
|
||||||
|
match self.declarations.get(index as usize) {
|
||||||
|
Some(VarSetDeclaration {
|
||||||
|
default_value: VarSetDefault::Dword(value),
|
||||||
|
..
|
||||||
|
}) => Ok(*value),
|
||||||
|
Some(VarSetDeclaration {
|
||||||
|
default_value: VarSetDefault::FloatBits(_),
|
||||||
|
..
|
||||||
|
}) => Err(Handler30ResolveError::FloatRequiresX87 { index }),
|
||||||
|
None => Err(Handler30ResolveError::VarSetIndexOutOfBounds {
|
||||||
|
index,
|
||||||
|
declarations: self.declarations.len(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl VarSetDefault {
|
impl VarSetDefault {
|
||||||
/// Returns the float value when this is a `float` default.
|
/// Returns the float value when this is a `float` default.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -583,8 +693,8 @@ fn read_instruction(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
decode, decode_with_limits, parse_varset, Handler2RecordInput, Handler2RecordScheduler,
|
decode, decode_with_limits, parse_varset, Handler2RecordInput, Handler2RecordScheduler,
|
||||||
ScriptDispatchSelector, VarSetDefault, VarSetError, VarSetType, GOG_HANDLER_COUNT,
|
Handler30ResolveError, ScriptDispatchSelector, ScriptInstruction, VarSetDefault,
|
||||||
INSTRUCTION_WORDS,
|
VarSetError, VarSetType, VmHostCallbackCommand, GOG_HANDLER_COUNT, INSTRUCTION_WORDS,
|
||||||
};
|
};
|
||||||
use fparkan_binary::{DecodeError, Limits};
|
use fparkan_binary::{DecodeError, Limits};
|
||||||
|
|
||||||
@@ -718,6 +828,59 @@ STRING( 8, ignored, ignored, ignored)\r\n";
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn decodes_lossless_event_and_instruction_records() {
|
fn decodes_lossless_event_and_instruction_records() {
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
|
|||||||
@@ -128,11 +128,22 @@ non-sentinel selector — `Handler(30)`, 246 records. Его VA `0x1000c266`
|
|||||||
у `Handler(57)` с первым word `2` и у отдельного lifecycle path с первым word
|
у `Handler(57)` с первым word `2` и у отдельного lifecycle path с первым word
|
||||||
`1`; предметная семантика этих modes ещё не доказана. В частности, это пока
|
`1`; предметная семантика этих modes ещё не доказана. В частности, это пока
|
||||||
не основание назвать Handler(30) сообщением, приказом или UI opcode. Точный
|
не основание назвать Handler(30) сообщением, приказом или UI opcode. Точный
|
||||||
text-to-varset resolver ещё расположен за wrapper `0x10011ea0` в
|
text-to-varset resolver расположен за wrapper `0x10011ea0` в
|
||||||
`0x100174a0`, поэтому Rust не dispatch-ит Handler(30) до восстановления
|
`0x100174a0`. Воспроизводимые exports: `ExportAiVmHandler30.java`,
|
||||||
индексации. Воспроизводимые exports: `ExportAiVmHandler30.java`,
|
|
||||||
`FindAiVmHandler30Callback.java`, `ExportAiVarSetLoader.java`.
|
`FindAiVmHandler30Callback.java`, `ExportAiVarSetLoader.java`.
|
||||||
|
|
||||||
|
Следующий pass восстанавливает эту индексацию. `0x100174a0` добавляет каждый
|
||||||
|
recognized source declaration в encounter order как 48-byte record; GOG shared
|
||||||
|
`varset.var` не содержит `STRING(...)`, поэтому его 231 numeric `VAR` entries
|
||||||
|
образуют точно это index space. `0x10013570` возвращает `DWORD` record kind
|
||||||
|
raw `u32`; float kind проходит `__ftol`, чей x87 rounding profile ещё требует
|
||||||
|
capture. Полный GOG scan всех 246 Handler(30) instructions показывает 492
|
||||||
|
operand references: все 492 in-range и указывают на `DWORD`. Поэтому
|
||||||
|
`VarSet::resolve_handler30` уже materializes точный opaque callback command
|
||||||
|
`(mode=0, first, second)` для данного corpus path, но явно отклоняет float,
|
||||||
|
out-of-range и incomplete instructions вместо silent coercion. Extractors:
|
||||||
|
`ExportAiVarSetParser.java`, `ExportAiVarSetU32Resolver.java`.
|
||||||
|
|
||||||
Следующий static pass закрывает equality/update policy. Identity ровно равна
|
Следующий static pass закрывает equality/update policy. Identity ровно равна
|
||||||
`(slot0 word, slot4 IEEE-754 bits, slot5 IEEE-754 bits)`, поэтому `-0.0` и
|
`(slot0 word, slot4 IEEE-754 bits, slot5 IEEE-754 bits)`, поэтому `-0.0` и
|
||||||
`+0.0` различаются. Новый 100-byte record получает slot1 в поле `+0x14`,
|
`+0.0` различаются. Новый 100-byte record получает slot1 в поле `+0x14`,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Emits the actual text-to-varset parser called by the AI varset loader.
|
||||||
|
// 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 ExportAiVarSetParser extends GhidraScript {
|
||||||
|
private static final long ADDRESS = 0x100174a0L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||||
|
.getAddress(ADDRESS);
|
||||||
|
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||||
|
println("===== AI varset parser =====");
|
||||||
|
if (function == null) { println("missing"); return; }
|
||||||
|
DecompInterface decompiler = new DecompInterface();
|
||||||
|
decompiler.openProgram(currentProgram);
|
||||||
|
println(decompiler.decompileFunction(function, 60, monitor).getDecompiledFunction().getC());
|
||||||
|
decompiler.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Emits the u32 resolver used by corpus-reachable Handler(30) for indexed
|
||||||
|
// varset records. 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 ExportAiVarSetU32Resolver extends GhidraScript {
|
||||||
|
private static final long ADDRESS = 0x10013570L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||||
|
.getAddress(ADDRESS);
|
||||||
|
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||||
|
println("===== AI varset u32 resolver =====");
|
||||||
|
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