From 7f8a37d22abbedf487666132a4857e13c213c944 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 31 Oct 2026 01:57:56 +0400 Subject: fix(script): read Init guards in native disk order --- crates/fparkan-runtime/src/lib.rs | 37 +++++++++++++---- crates/fparkan-script/src/lib.rs | 72 +++++++++++++++++++-------------- docs/appendices/knowledge-boundaries.md | 15 ++++--- docs/appendices/script-vm.md | 22 +++++++--- 4 files changed, 99 insertions(+), 47 deletions(-) diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 50123de..086966f 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -12283,7 +12283,7 @@ mod tests { name_raw: b"Init\0".to_vec(), event_word: 0x1234_5678, instructions: vec![fparkan_script::ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }], }], @@ -12370,7 +12370,7 @@ mod tests { event_word: 0x1234, instructions: vec![ fparkan_script::ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, fparkan_script::ScriptInstruction { @@ -12784,11 +12784,24 @@ mod tests { assert_eq!(loaded.script_bundle_count, 2); assert_eq!(loaded.script_init_state_count, 2); assert_eq!(loaded.script_varset_state_count, 2); - assert!(loaded_mission_script_bundles(&engine) - .expect("script bundles") - .iter() - .all(|bundle| bundle.init_execution - == MissionScriptInitExecution::Executed { event_index: 0 })); + let script_bundles = loaded_mission_script_bundles(&engine).expect("script bundles"); + assert_eq!(script_bundles.len(), 2); + assert!(script_bundles.iter().all(|bundle| bundle.init_execution + == MissionScriptInitExecution::Executed { event_index: 0 })); + for bundle in script_bundles { + let init = bundle + .package + .events + .iter() + .find(|event| event.name_raw.split(|byte| *byte == 0).next() == Some(&b"Init"[..])) + .expect("first Init event"); + let instruction = init.instructions.first().expect("Handler(19) action"); + assert_eq!( + instruction.header_words, + [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX] + ); + assert_eq!(instruction.references.as_slice(), &[224, 225, 226]); + } assert_eq!(engine.behavior_rng_state, 0xc823_f683); assert_eq!( loaded_mission_script_bundles(&engine) @@ -13755,6 +13768,16 @@ mod tests { ) }) .expect("campaign package with an unsupported Init instruction"); + assert_eq!( + &unsupported.init_execution, + &MissionScriptInitExecution::Unsupported { + event_index: 0, + instruction_index: 1, + error: Handler19ResolveError::UnsupportedSelector { + found: fparkan_script::ScriptDispatchSelector::Sentinel, + }, + } + ); let clan_index = unsupported.clan_index; let states = loaded_mission_script_init_states(&engine).expect("Init writes"); assert!(states.iter().all(|state| state.clan_index != clan_index)); diff --git a/crates/fparkan-script/src/lib.rs b/crates/fparkan-script/src/lib.rs index 088b51f..e574489 100644 --- a/crates/fparkan-script/src/lib.rs +++ b/crates/fparkan-script/src/lib.rs @@ -205,12 +205,12 @@ pub enum Handler19ResolveError { }, /// Only action instructions with control word `0xffff_ffff` are supported. UnsupportedControlWord { - /// Opaque control word from instruction header word one. + /// Opaque control word from disk-order header index 3. found: u32, }, /// Only instructions with no result-save reference are supported. UnsupportedResultSave { - /// Opaque variable reference from instruction header word two. + /// Opaque variable reference from disk-order header index 1. found: u32, }, /// One of the three required references was absent. @@ -637,14 +637,14 @@ impl VarSet { if selector != ScriptDispatchSelector::Handler(19) { return Err(Handler19ResolveError::UnsupportedSelector { found: selector }); } - if instruction.header_words[1] != u32::MAX { + if instruction.header_words[3] != u32::MAX { return Err(Handler19ResolveError::UnsupportedControlWord { - found: instruction.header_words[1], + found: instruction.header_words[3], }); } - if instruction.header_words[2] != u32::MAX { + if instruction.header_words[1] != u32::MAX { return Err(Handler19ResolveError::UnsupportedResultSave { - found: instruction.header_words[2], + found: instruction.header_words[1], }); } self.resolve_handler19(instruction, input) @@ -1688,7 +1688,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; 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| ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references, }; let package = ScriptPackage { @@ -1832,7 +1832,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; event_word: 0, instructions: vec![ ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, ScriptInstruction { @@ -1840,7 +1840,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; references: Vec::new(), }, ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, ], @@ -1884,7 +1884,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; references: Vec::new(), }, ScriptInstruction { - header_words: [19, u32::MAX, u32::MAX, 0, 0, 3, 0], + header_words: [19, u32::MAX, u32::MAX, u32::MAX, 6, 3, u32::MAX], references: vec![0, 1, 2], }, ], @@ -1913,7 +1913,7 @@ STRING( 8, ignored, ignored, ignored)\r\n"; } #[test] - fn handler_nineteen_init_types_unsupported_control_and_result_save() { + fn handler_nineteen_init_uses_disk_control_and_result_fields() { let varset = parse_varset(b"VAR( DWORD, x, 0)\nVAR( DWORD, y, 0)\nVAR( DWORD, id, 0)\n") .expect("DWORD targets"); let input = Handler19InitInput { @@ -1921,18 +1921,17 @@ STRING( 8, ignored, ignored, ignored)\r\n"; 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 resolve = |header_words| { 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], + instructions: vec![ScriptInstruction { + header_words, + references: vec![0, 1, 2], + }], }], trailing_bytes: Vec::new(), raw: std::sync::Arc::from(&b""[..]), @@ -1940,21 +1939,34 @@ STRING( 8, ignored, ignored, ignored)\r\n"; 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 }, - }) + let actual = ( + resolve([19, u32::MAX, u32::MAX, 2, 6, 3, u32::MAX]), + resolve([19, 0x1234, u32::MAX, u32::MAX, 6, 3, u32::MAX]), + resolve([19, u32::MAX, 0xdecafbad, u32::MAX, 6, 3, u32::MAX]), ); assert_eq!( - resolve(instruction(u32::MAX, 2)), - Ok(Handler19InitResolution::Unsupported { - event_index: 0, - instruction_index: 0, - error: Handler19ResolveError::UnsupportedResultSave { found: 2 }, - }) + actual, + ( + Ok(Handler19InitResolution::Unsupported { + event_index: 0, + instruction_index: 0, + error: Handler19ResolveError::UnsupportedControlWord { found: 2 }, + }), + Ok(Handler19InitResolution::Unsupported { + event_index: 0, + instruction_index: 0, + error: Handler19ResolveError::UnsupportedResultSave { found: 0x1234 }, + }), + Ok(Handler19InitResolution::Executed { + event_index: 0, + event_word: 0, + writes: vec![ + Handler19DwordWrite { index: 0, value: 7 }, + Handler19DwordWrite { index: 1, value: 8 }, + Handler19DwordWrite { index: 2, value: 9 }, + ], + }), + ) ); } diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 29c9480..ee40121 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -141,11 +141,16 @@ Rust. продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение; на первом `FISTP qword` он явно завершает загрузку ошибкой для NaN, infinity и значений вне signed-qword диапазона. В первом событии `Init` Rust поддерживает -только action-форму `Handler(19)` (`control word` и `result reference` равны -`0xffffffff`). Неизвестная инструкция даёт `Unsupported` в preview, не применяя -накопленные частичные записи; при полной загрузке это ошибка. Это ограничение -Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные ссылки и -типы остаются ошибками в обоих режимах. +только action-форму `Handler(19)`: в семи disk header words control находится в +`header_words[3]`, а result-save reference — в `header_words[1]`; для поддержанной +формы оба равны `0xffffffff`. Это подтверждается перестановкой записи в +`ai.dll!FUN_10011b20` (RVA `0x11b20`) и чтениями в исполнителе +`FUN_10012020` (RVA `0x12020`) той же GOG-сборки. `Handler(19)` (`FUN_1000aa38`, +RVA `0xaa38`) использует references `0..2`; `header_words[2]` его result-save +guard-ом не проверяется. Неизвестная инструкция даёт `Unsupported` в preview, +не применяя накопленные частичные записи; при полной загрузке это ошибка. Это +ограничение Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные +ссылки и типы остаются ошибками в обоих режимах. Для GOG-сборки также найден статический host call path в `Init`. В `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 3de442d..7ebf344 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -16,7 +16,8 @@ handlers, второй — числом event records. Каждый event хра `name_len + 1` raw bytes с обязательным NUL, opaque event word и count вложенных records. Вложенный record сохраняет семь `u32` header words (в disk order), список `u32` references после шестого header word и trailing seventh word. -Никакой из этих words ещё не получает semantic name. `.fml` — текстовый +Семантика остальных слов остаётся открытой; поля control и result-save описаны +в узком `Init` contract ниже. `.fml` — текстовый symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaults. `fparkan-script::parse_varset` уже читает подтверждённые numeric `VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются @@ -302,10 +303,21 @@ image base `0x10000000`. В ней `FUN_10011f40` (RVA `0x11f40`) ищет им `FUN_10011f10` передаёт выбранный индекс внутреннему циклу `FUN_10011fb0` (RVA `0x11fb0`). Цикл начинает с первой вложенной инструкции, сбрасывает индекс инструкции и глубину стека условий, затем идёт по записям в порядке файла. -В исходном `default.scr` событие `Init` содержит одну инструкцию selector `19`, -control word `0xffffffff` (действие) и result reference `0xffffffff` (результат -не сохраняется). Это наблюдаемая форма пакета; поведение оригинала для остальных -форм здесь не утверждается. +Читатель `FUN_10011b20` (RVA `0x11b20`) переставляет семь disk header words в +runtime record: `[0, 1, 2, 3, 4, 5, 6]` переходят в offsets +`[+0, +8, +0x0c, +4, +0x10, +0x18, +0x1c]`; указатель references лежит в `+0x14`. +Исполнитель `FUN_10012020` (RVA `0x12020`) берёт control из runtime `+0x04`, то +есть из `header_words[3]`, а result-save reference — из runtime `+0x08`, то есть +из `header_words[1]`. Disk word `header_words[2]` попадает в `+0x0c` и участвует +в особой ветви selector `-1`, но не является result-save полем `Handler(19)`. + +В исходном `default.scr` событие `Init` содержит действие selector `19` с +заголовком `[19, 0xffffffff, 0xffffffff, 0xffffffff, 6, 3, 0xffffffff]` и +references `224, 225, 226`. Для этого действия `header_words[3] = 0xffffffff` +(action control), а `header_words[1] = 0xffffffff` (результат не сохраняется). +Одинаковое значение в `header_words[2]` — лишь содержимое этой записи, оно не +проверяется guard-ом `Handler(19)`. Это наблюдаемая форма пакета; поведение +оригинала для остальных форм здесь не утверждается. В GOG `default.scr` события `Init` в двух AutoDemo bundles имеют по одной такой инструкции. Её ссылки `224`, `225`, `226` ведут к объявлениям `DWORD` -- cgit v1.2.3