feat(runtime): restore indexed Control provider query
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
4 files changed
+313
-19
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -11890,6 +11896,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(
|
||||
@@ -13834,6 +13905,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() {
|
||||
|
||||
@@ -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` и
|
||||
|
||||
@@ -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 не меняют эти три поля
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user