diff options
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 183 | ||||
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 48 | ||||
| -rw-r--r-- | docs/appendices/script-vm.md | 74 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 27 |
4 files changed, 313 insertions, 19 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 440c623..9013019 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -2256,20 +2256,22 @@ pub fn query_loaded_mission_control_resource( query_control_device_table_resource(table, selector) } -/// Queries one loaded Control device through its original factory getter. +/// Queries one loaded Control device through its indexed selector interface. /// -/// Selector `0xe00` is modeled for current descriptor tags 5 and 26 with -/// factory tags 5 and 26. Both common factory readers use the device key to -/// read the current provider value and multiply it by coefficient 0. Only -/// current tag 26 also multiplies by `M`; current tag 5 ignores `M` and -/// descriptor word 8 for this selector. Explicit Control update calls can -/// change current provider or multiplier values; the mission clock does not -/// advance them automatically. +/// Selector `0x400` returns the current type-1 value from the provider row +/// selected by the device key, including signed zero, regardless of current +/// descriptor or factory tag. Selector `0xe00` is modeled for current +/// descriptor tags 5 and 26 with factory tags 5 and 26. Both common factory +/// readers use the device key to read the current provider value and multiply +/// it by coefficient 0. Only current tag 26 also multiplies by `M`; current +/// tag 5 ignores `M` and descriptor word 8 for this selector. Explicit Control +/// update calls can change current provider or multiplier values; the mission +/// clock does not advance them automatically. /// /// # Errors /// Returns an error for an invalid handle, unavailable mission/table/provider, -/// an out-of-range device/provider key, or a factory/tag/float path that is not -/// supported by the current reconstruction. +/// an out-of-range device/provider key, an unsupported float range, or an +/// unsupported current/factory tag on selector `0xe00`. pub fn query_loaded_mission_control_device_selector( engine: &Engine, handle: ObjectHandle, @@ -4371,7 +4373,7 @@ fn query_control_device_table_selector( device_index: usize, selector: u32, ) -> Result<Option<f32>, ControlResourceAccessError> { - if selector != 0xe00 { + if !matches!(selector, 0x400 | 0xe00) { return Err(ControlResourceAccessError::UnsupportedSelector(selector)); } let devices = match &table.status { @@ -4387,6 +4389,10 @@ fn query_control_device_table_selector( device_index, device_count: devices.len(), })?; + if selector == 0x400 { + return control_device_provider_value_from_status(&table.node_provider_status, device) + .map(Some); + } query_control_device_selector(device, &table.node_provider_status) } @@ -11891,6 +11897,71 @@ mod tests { } #[test] + fn indexed_provider_value_query_400_reads_current_value_by_device_key() { + let mut device = synthetic_effective_control_device(2, 99, 9.0_f32.to_bits(), None, None); + device.key = 1; + device.native_word_50_bits = Some(0); + device.coefficient_bits = None; + device.descriptor.fixed_words[8] = 13.0_f32.to_bits(); + let table = synthetic_control_device_table_with_providers( + MissionControlDeviceTableStatus::Available(vec![device]), + MissionControlNodeProviderTableStatus::Available(vec![ + synthetic_node_provider_row(0, 0, 0.125_f32.to_bits()), + synthetic_node_provider_row(0, 1, 0.625_f32.to_bits()), + synthetic_node_provider_row(0, 2, 0.875_f32.to_bits()), + ]), + ); + + assert_eq!( + query_control_device_table_selector(&table, 0, 0x400), + Ok(Some(0.625_f32)), + "selector 0x400 returns the current type-1 value at the device's provider key" + ); + + let mut negative_zero = table.clone(); + let MissionControlNodeProviderTableStatus::Available(rows) = + &mut negative_zero.node_provider_status + else { + unreachable!(); + }; + rows[1].current_type1_value_bits = (-0.0_f32).to_bits(); + assert_eq!( + query_control_device_table_selector(&negative_zero, 0, 0x400) + .expect("negative-zero provider value") + .map(f32::to_bits), + Some((-0.0_f32).to_bits()) + ); + + let reason = MissionControlNodeProviderTableUnavailableReason::MissingNodeData { + unit_component_index: 0, + source: None, + }; + let unavailable = synthetic_control_device_table_with_providers( + table.status.clone(), + MissionControlNodeProviderTableStatus::Unavailable(reason.clone()), + ); + assert_eq!( + query_control_device_table_selector(&unavailable, 0, 0x400), + Err(ControlResourceAccessError::NodeProviderTableUnavailable( + reason + )) + ); + + let mut out_of_range = table.clone(); + let MissionControlDeviceTableStatus::Available(devices) = &mut out_of_range.status else { + unreachable!(); + }; + devices[0].key = 3; + assert_eq!( + query_control_device_table_selector(&out_of_range, 0, 0x400), + Err(ControlResourceAccessError::NodeProviderKeyOutOfRange { + key: 3, + row_count: 3, + }) + ); + } + + #[test] fn synthetic_into_builder_appends_all_rows_and_overrides_only_first_record() { let append = synthetic_control_table( &[None, Some(PROTOTYPE_TYPE_INTO)], @@ -13836,6 +13907,96 @@ mod tests { #[test] #[ignore = "requires licensed corpus"] + fn full_load_queries_indexed_control_selector_400_for_all_autodemo_devices() { + let mut engine = create(EngineServices::new(Arc::new(DirectoryVfs::new( + licensed_root("IS"), + )))) + .expect("engine"); + load_mission( + &mut engine, + MissionRequest { + key: "MISSIONS/Autodemo.00/data.tma".into(), + mode: MissionMode::AutoDemo, + }, + MissionStartupInput { + queue_time_ms: 100_000, + research_creation_gate: false, + }, + ) + .expect("load licensed AutoDemo with explicit startup inputs"); + + let tables = loaded_mission_control_device_tables(&engine).expect("Control tables"); + let expected: [(u32, usize, &[u32]); 8] = [ + (0, 33, &[0, 0, 0, 0, 0, 0, 16, 19, 16, 18, 27, 30, 31, 32]), + (1, 23, &[0, 0, 0, 0, 0, 0, 10, 13, 10, 11, 19, 22]), + (2, 26, &[0, 0, 0, 0, 0, 0, 0, 0, 8, 9, 8, 10, 19, 22, 25]), + (3, 29, &[0, 0, 0, 0, 0, 0, 16, 21, 16, 20, 26, 27, 28]), + (4, 32, &[5, 5, 1, 23, 3, 22, 7, 19, 16, 23, 5, 29]), + (5, 32, &[5, 5, 1, 23, 3, 22, 7, 19, 16, 23, 5, 29]), + (6, 22, &[17, 19, 19, 15, 13, 14, 10, 11, 4, 5, 7]), + (7, 22, &[17, 19, 19, 15, 13, 14, 10, 11, 4, 5, 7]), + ]; + assert_eq!(tables.len(), expected.len()); + + let mut queried_devices = 0; + for (original_id, expected_provider_rows, expected_keys) in expected { + let table = tables + .iter() + .find(|table| table.original_id == Some(OriginalObjectId(original_id))) + .expect("AutoDemo Control table for original object"); + let devices = match &table.status { + MissionControlDeviceTableStatus::Available(devices) => devices, + MissionControlDeviceTableStatus::Unavailable(reason) => { + panic!("Control table for object {original_id} is unavailable: {reason:?}"); + } + }; + let provider_rows = match &table.node_provider_status { + MissionControlNodeProviderTableStatus::Available(rows) => rows, + MissionControlNodeProviderTableStatus::Unavailable(reason) => { + panic!("provider rows for object {original_id} are unavailable: {reason:?}"); + } + }; + assert_eq!( + devices.len(), + expected_keys.len(), + "device count for {original_id}" + ); + assert_eq!( + provider_rows.len(), + expected_provider_rows, + "provider row count for {original_id}" + ); + + for (device_index, expected_key) in expected_keys.iter().copied().enumerate() { + let device = &devices[device_index]; + assert_eq!( + device.key, expected_key, + "provider key for object {original_id}, device {device_index}" + ); + assert!( + (expected_key as usize) < provider_rows.len(), + "provider key for object {original_id}, device {device_index}" + ); + let actual = query_loaded_mission_control_device_selector( + &engine, + table.handle, + device_index, + 0x400, + ) + .expect("selector 0x400 query"); + assert_eq!( + actual.map(f32::to_bits), + Some(0x3f80_0000), + "current provider value for object {original_id}, device {device_index}, key {expected_key}" + ); + queried_devices += 1; + } + } + assert_eq!(queried_devices, 100); + } + + #[test] + #[ignore = "requires licensed corpus"] fn headless_step_runs_resource_schedule_and_rolls_back_both_error_paths() { let mut engine = create(EngineServices::new(Arc::new(DirectoryVfs::new( licensed_root("IS"), diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 59b2ffd..bda4d49 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -532,12 +532,48 @@ callbacks. Licensed двухстадийный узел шахты подтвеÑ новый API при доле `0.5`, без FX и window path. Гибель root/critical узла и неизвестные flags/aggregate также остаются явными ошибками. -Rust query обращается к загруженному объекту и поддерживает `0xe00` только для -текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и -factory paths остаются явными границами. Запрос читает текущее type-1 значение. -Licensed `min_l_n1.dat` подтверждает изменение `0xe00` и Behavior `0x1002` с -`1.0` до `0.5` после явного уменьшения количества вдвое; чтение получает -текущее значение provider, который также меняет явный бюджетный шаг. +Rust query обращается к загруженному объекту и поддерживает `0xe00` для пар +текущего descriptor tag и factory tag `(5,5)`, `(5,26)`, `(26,5)` и `(26,26)`; +прочие специализированные tag/factory пути остаются явными границами. Запрос +читает текущее type-1 значение provider. Licensed `min_l_n1.dat` подтверждает +изменение `0xe00` и Behavior `0x1002` с `1.0` до `0.5` после явного уменьшения +количества вдвое; чтение получает текущее значение provider, который также +меняет явный бюджетный шаг. + +Indexed selector `0x400` в том же Control IID `0x204` возвращает текущее type-1 +значение строки provider, выбранной через key устройства, при любом descriptor +и factory tag. Rust принимает конечное normal-or-zero значение, включая оба +знака нуля, и сохраняет его биты; недоступная таблица, отсутствующий key и +значения вне этой численной области дают типизированную ошибку. В оригинальном +`Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` ветвь +`FUN_1002bb40` (RVA `0x2bc3c`) читает provider pointer `device+0x44` и key +`device+0x04`, вызывает provider slot `+0x0c` в режиме `1` и не проверяет +descriptor/factory tag. Rust-проекция читает сохранённый снимок; она не вычисляет +value из amount, maximum, coefficients или множителя `M`. Native selector-cell +alias/fallback за пределами валидной записи и поведение native при невалидном +key остаются неисследованными; Rust не подставляет для них `0.0` или `None`. +Это покрывает перечисленные query-пути. Полный интерфейс IID `0x204`, его +взаимодействие со всеми Behavior-вызовами и полный игровой tick остаются за +границей подтверждённого поведения. + +Объём свидетельств для `0x400` ограничен и состоит из трёх частей. Native +oracle на настоящем `Control.dll` проверил шесть выходов по raw bits для +ограждённого синтетического owner/provider table; он вызвал indexed query и +реальный provider getter, но не создавал оригинальные factory-объекты. Матрица +выходов находится в [техническом разборе](script-vm.md#индексный-control-getter-сырой-селектор-0x400). +Независимый разбор AutoDemo обнаружил восемь таблиц и 100 устройств с +доступными ключами, но все выбранные provider values равны `1.0`; такой набор +подтверждает загрузку и область ключей, однако не различает, какую именно +строку выбирает алгоритм. Игнорируемый Rust-тест опрашивает эти 100 устройств и +проверяет те же биты, не выдавая равенство ресурсных данных за доказательство +индексирования. + +Вместе native-векторы и resource acceptance не подтверждают состояние +оригинальных factory instances, полный контракт IID `0x204`, полный +Behavior-call flow или живую миссию/gameplay. Поведение при ошибочных native +указателях и ключах, а также selector-cell alias/fallback вне валидной строки +по-прежнему не описано; Rust сохраняет typed errors на этой границе. Native oracle проверил изолированные пути распределения, обновления класса `4`, стадийную арифметику `0x11220`, обход дочерних provider-строк `0x11130` на interleaved parents `[-1, 0, 0, 1, 3]` с callback-порядком `1, 3, 4, 2` и diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 790729b..6e48445 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -1429,6 +1429,80 @@ key из поля устройства `+0x04`. Provider getter `FUN_1000dc40` ( строку `table + key*0x2c`; текущее type-1 значение находится по `+0x14` строки. Индекс устройства и ключ provider row — разные значения с разными задачами. +#### Индексный Control getter: сырой селектор `0x400` + +В той же сборке `Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` +индексный query для `QueryInterface(0x204)` входит через слот `+0x18` vtable +`0x3d1b4`: `FUN_1002bb40` (RVA `0x2bb40`, `RET 8`, `ECX=owner+0x0c`, ordinal +устройства в `EDX`, selector и output pointer на стеке). При сыром selector +`0x400` функция попадает в ветвь RVA `0x2bc3c`. +Ветка получает provider pointer из `device+0x44`, отдельно читает provider key +из `device+0x04` и вызывает provider vtable slot `+0x0c` в режиме `1`, передавая +адрес ячейки selector как storage результата. После вызова значение переносится +в выходной `f32` caller-а; ветвь возвращает `AL=1` без проверки текущего +descriptor tag или factory tag. + +Поле `device+0x44` указывает на provider interface `owner+0x04` с vtable +`0x3d254`; slot `+0x0c` ведёт к `FUN_1000dc40` (RVA `0x0dc40`). Getter +использует key для строки общей таблицы, а не ordinal устройства. Для валидной +строки selector `1` читает её текущее type-1 поле `+0x14` напрямую: +`0x400` поэтому не пересчитывает долю из amount и maximum и не умножает её на +коэффициент устройства, `M` или descriptor word. В Rust тот же маршрут выглядит +так: + +```text +device = devices[device_index] +value = providers[device.key].current_type1_value +return Some(value) +``` + +Индекс устройства сначала выбирает запись, её key затем выбирает provider row; +этот indexed-вызов не является глобальным запросом `0x0f` и не является +metadata getter IID `0x16`. Проекция Rust принимает конечные нормальные значения +и точные `+0`/`-0`, сохраняя биты нулевого значения; остальные float-диапазоны и +недоступные table/key остаются типизированными ошибками. Native selector-cell +alias/fallback вне валидной записи provider не реализован: Rust не подставляет +для такого пути `0.0` или `None`. + +Шесть изолированных вызовов оригинала проверяют именно этот валидный +indexed-вызов. +В каждом ordinal устройства равен `2`, а ключ provider равен `1`, `3` или `4`; +все значения строк различаются, поэтому ключ не совпадает с ordinal и не +подменяется соседней строкой. + +| Вектор | Ordinal | Key | Raw output | AL | +| --- | ---: | ---: | ---: | ---: | +| Значение из корпуса `1.0` | 2 | 1 | `0x3f800000` | 1 | +| Доля `0.625` | 2 | 1 | `0x3f200000` | 1 | +| Отрицательное конечное `-2.5` | 2 | 3 | `0xc0200000` | 1 | +| `+0` | 2 | 1 | `0x00000000` | 1 | +| `-0` | 2 | 3 | `0x80000000` | 1 | +| Отличающаяся строка `0.0625` | 2 | 4 | `0x3d800000` | 1 | + +Вектор `0.625` специально задаёт native word `device+0x50 = 0`; он всё равно +возвращает текущее поле provider. Таблица устройства, provider и остальные +строки были синтетическими, ограждёнными входами; исполнены оригинальные +`FUN_1002bb40` и provider getter `FUN_1000dc40`, без constructors, ControlSystem +или mock callbacks. Все входные записи остались неизменными, проверены canary +области, а изменился только DWORD выхода. Для DLL с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` входной +control word `0x027f` был временно заменён на `0x007f` и восстановлен после +вызова; у всех шести ветвей `AL=1`. Выходы совпали с прямыми значениями выбранных +synthetic rows. Это не снимок состояния оригинальной фабрики или миссии: полный +IID `0x204`, интеграция Behavior и gameplay-трасса не проверены. + +Независимый разбор лицензированного AutoDemo дал восемь таблиц и 100 пар +устройство/key, но все выбранные type-1 значения там равны `1.0`. Такой набор +проверяет загрузку таблицы, ключи и доступность запросов, но не позволяет +различить выбор строки по key и по ordinal. Игнорируемый Rust acceptance test +проверяет все 100 запросов на этом корпусе и ожидает `0x3f800000`. В тесте явно +заданы `MissionStartupInput { queue_time_ms: 100_000, +research_creation_gate: false }`; эти значения являются входами Rust-проверки, +а не доказательством штатного gate оригинала. Native синтетические строки дают +отдельное доказательство выбора key. Оба результата имеют ограниченную область +и не являются сравнением полного поведения игры. + Конструктор owner `FUN_10006bf0` (RVA `0x6bf0`) задаёт scalar `+0x1a8 = 1.0` и вызывает `FUN_10014260`, который записывает `1.0` в `+0x354/+0x358`. Проверенные ветви канонического event `4/null` и FAC0 не меняют эти три поля diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 3af2c21..b9aefb5 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -1089,8 +1089,31 @@ NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую та последовательность provider-строк: её длина берётся из NDP, она не дополняется до числа узлов MSH и не участвует во второй перенумерации ключа. -Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке -`Behavior.dll` (SHA-256 +Индексный selector `0x400` выдаёт значение выбранной строки provider как есть. +Сначала общий индекс устройств выбирает дескриптор, затем его ключ выбирает +строку NDP; это разные номера. Результатом становится текущее type-1 значение +этой строки, а не запас `CurrentOre`, предел или значение после умножения на +коэффициент и `M`. Например, provider-строка с `0.625` возвращает `0.625`, а +записанный в ней ноль тоже остаётся успешным результатом. Rust использует уже +подготовленную таблицу и сообщает отдельную ошибку, если provider-таблица или +строка по ключу недоступна. Этот запрос читает снимок состояния и сам не меняет +таблицу и не запускает её обновление. + +У правила есть два разных подтверждения. Разбор AutoDemo нашёл восемь таблиц +Control и 100 устройств; все 100 значений по ключам устройств в этом корпусе +равны `1.0`, поэтому Rust-проверка подтверждает реальные ключи и доступность +строк, но сама по себе не различает выбор по ключу и по номеру устройства. +Отдельная x86-проверка исходного `Control.dll` включала шесть вызовов на +синтетической таблице, где значения строк были заданы вручную и различались. +Например, при номере устройства `2`, ключе `1` и нулевом слове +`device+0x50` оригинал вернул `0.625` из выбранной строки. Точные биты ответов +приведены в [техническом разборе](../appendices/script-vm.md#индексный-control-getter-сырой-селектор-0x400). +Этот опыт не создавал оригинальные factory-объекты и не исполнял полную миссию: +полный интерфейс IID `0x204`, интеграция Behavior и игровое поведение остаются +непроверенными. + +В цепочке добычи руды оригинал использует описанный выше selector `0x300`. +В той же сборке `Behavior.dll` (SHA-256 `ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) `FUN_100186b0` (RVA `0x186b0`) перечисляет устройства типа `0x1a`, а `FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector |
