feat(script): model handler two scheduler
This commit is contained in:
@@ -67,6 +67,151 @@ pub enum ScriptDispatchSelector {
|
|||||||
Unknown(u32),
|
Unknown(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
/// Initial and per-refresh counter word from resolved slot 2.
|
||||||
|
pub counter: u32,
|
||||||
|
/// 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
|
||||||
|
/// update replaces `scalar_1` and `scalar_6`, then adds the record's own
|
||||||
|
/// slot-2 counter word with x86 wrapping arithmetic.
|
||||||
|
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;
|
||||||
|
record.counter = record.counter.wrapping_add(input.word_2);
|
||||||
|
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,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
impl ScriptInstruction {
|
impl ScriptInstruction {
|
||||||
/// Returns the recovered dispatch selector from the first disk word.
|
/// Returns the recovered dispatch selector from the first disk word.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -206,10 +351,98 @@ fn read_instruction(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
decode, decode_with_limits, ScriptDispatchSelector, GOG_HANDLER_COUNT, INSTRUCTION_WORDS,
|
decode, decode_with_limits, Handler2RecordInput, Handler2RecordScheduler,
|
||||||
|
ScriptDispatchSelector, GOG_HANDLER_COUNT, INSTRUCTION_WORDS,
|
||||||
};
|
};
|
||||||
use fparkan_binary::{DecodeError, Limits};
|
use fparkan_binary::{DecodeError, Limits};
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.records()[0].counter, 103);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.records()[0].scalar_6.to_bits(),
|
||||||
|
12.0_f32.to_bits()
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
assert_eq!(scheduler.records()[0].counter, 1);
|
||||||
|
assert_eq!(scheduler.records()[0].scalar_6.to_bits(), 4.0_f32.to_bits());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
|
||||||
#[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();
|
||||||
|
|||||||
@@ -97,10 +97,20 @@ record fields `+0x0c` и `+0x14`, затем вызывает его refresh pat
|
|||||||
`<base>_Continue`, сохраняет их IDs в indexed state и materializes новый
|
`<base>_Continue`, сохраняет их IDs в indexed state и materializes новый
|
||||||
internal record. В этой ветке не видно прямого World3D/Behavior call, поэтому
|
internal record. В этой ветке не видно прямого World3D/Behavior call, поэтому
|
||||||
это доказанная scheduler/event-record boundary, а не команда движения, атаки
|
это доказанная scheduler/event-record boundary, а не команда движения, атаки
|
||||||
или строительства. Semantic names семи slots, key equality и consumer нового
|
или строительства. Semantic names семи slots и consumer нового record остаются
|
||||||
record остаются открытыми; до dynamic capture Rust возвращает явный
|
открытыми; до dynamic capture Rust возвращает явный
|
||||||
unsupported result, а не «примерный» game command.
|
unsupported result, а не «примерный» game command.
|
||||||
|
|
||||||
|
Следующий static pass закрывает equality/update policy. Identity ровно равна
|
||||||
|
`(slot0 word, slot4 IEEE-754 bits, slot5 IEEE-754 bits)`, поэтому `-0.0` и
|
||||||
|
`+0.0` различаются. Новый 100-byte record получает slot1 в поле `+0x14`,
|
||||||
|
slot2 одновременно в `+0x24/+0x28`, slot3 в `+0x2c` и slot6 в `+0x0c`. При
|
||||||
|
совпавшем key refresh случается только когда slot1 сравнивается unequal
|
||||||
|
(включая NaN); он заменяет `+0x14` и `+0x0c`, затем прибавляет сохранённый
|
||||||
|
`+0x28` к `+0x24` с x86 wrapping arithmetic. `fparkan-script` отражает эту
|
||||||
|
изолированную часть как `Handler2RecordScheduler`; он не выполняет bytecode,
|
||||||
|
не назначает игровых имён и не делает event lookup за original VM.
|
||||||
|
|
||||||
На границе mission runtime выбранный TMA clan `first_resource` теперь
|
На границе mission runtime выбранный TMA clan `first_resource` теперь
|
||||||
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
||||||
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Emits record construction, equality, refresh and insertion helpers called by
|
||||||
|
// the corpus-reachable AI VM Handler(2) scheduler boundary.
|
||||||
|
// Run through Ghidra headless analysis; 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 ExportAiVmHandler2SchedulerHelpers extends GhidraScript {
|
||||||
|
private static final long[] ADDRESSES = {
|
||||||
|
0x10004e50L, 0x10004c50L, 0x10005070L, 0x100073e0L
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
DecompInterface decompiler = new DecompInterface();
|
||||||
|
decompiler.openProgram(currentProgram);
|
||||||
|
for (long value : ADDRESSES) {
|
||||||
|
Address address = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||||
|
.getAddress(value);
|
||||||
|
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||||
|
println("===== AI Handler(2) scheduler helper " + address + " =====");
|
||||||
|
if (function == null) { println("missing"); continue; }
|
||||||
|
println(decompiler.decompileFunction(function, 60, monitor).getDecompiledFunction().getC());
|
||||||
|
}
|
||||||
|
decompiler.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user