fix(runtime): load research tables for every AI owner
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
This commit is contained in:
3 files changed
+108
-62
No files matched your search
@@ -1510,7 +1510,7 @@ fn load_mission_with_options(
|
|||||||
let research_trf_tables = if options.asset_scope == MissionAssetScope::Full
|
let research_trf_tables = if options.asset_scope == MissionAssetScope::Full
|
||||||
&& startup_input.is_some_and(|input| input.research_creation_gate)
|
&& startup_input.is_some_and(|input| input.research_creation_gate)
|
||||||
{
|
{
|
||||||
load_mission_research_trf_tables(&vfs, &mission, &loaded_scripts.bundles)?
|
load_mission_research_trf_tables(&vfs, &mission, &loaded_scripts.ai_startup_states)?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
@@ -6613,14 +6613,14 @@ fn load_script_formula_table(
|
|||||||
fn load_mission_research_trf_tables(
|
fn load_mission_research_trf_tables(
|
||||||
vfs: &Arc<dyn Vfs>,
|
vfs: &Arc<dyn Vfs>,
|
||||||
mission: &MissionDocument,
|
mission: &MissionDocument,
|
||||||
bundles: &[MissionScriptBundle],
|
ai_startup_states: &[MissionAiStartupState],
|
||||||
) -> Result<Vec<MissionResearchTrfTable>, EngineError> {
|
) -> Result<Vec<MissionResearchTrfTable>, EngineError> {
|
||||||
let mut tables = Vec::with_capacity(bundles.len());
|
let mut tables = Vec::with_capacity(ai_startup_states.len());
|
||||||
for bundle in bundles {
|
for ai_state in ai_startup_states {
|
||||||
let Some(clan) = mission.clans.get(bundle.clan_index) else {
|
let Some(clan) = mission.clans.get(ai_state.clan_index) else {
|
||||||
return Err(EngineError::ScriptRuntime {
|
return Err(EngineError::ScriptRuntime {
|
||||||
clan_index: bundle.clan_index,
|
clan_index: ai_state.clan_index,
|
||||||
message: "script bundle clan index is outside the decoded mission".to_string(),
|
message: "AI startup state clan index is outside the decoded mission".to_string(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
let second_resource = match &clan.body {
|
let second_resource = match &clan.body {
|
||||||
@@ -6642,7 +6642,7 @@ fn load_mission_research_trf_tables(
|
|||||||
}
|
}
|
||||||
if !raw_path.is_ascii() {
|
if !raw_path.is_ascii() {
|
||||||
return Err(EngineError::ResearchTrfPath {
|
return Err(EngineError::ResearchTrfPath {
|
||||||
clan_index: bundle.clan_index,
|
clan_index: ai_state.clan_index,
|
||||||
path: raw_path.to_vec(),
|
path: raw_path.to_vec(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -6651,12 +6651,12 @@ fn load_mission_research_trf_tables(
|
|||||||
let bytes = read_vfs(vfs, &path)?;
|
let bytes = read_vfs(vfs, &path)?;
|
||||||
let document =
|
let document =
|
||||||
ResearchTrfDocument::decode(bytes).map_err(|source| EngineError::ResearchTrf {
|
ResearchTrfDocument::decode(bytes).map_err(|source| EngineError::ResearchTrf {
|
||||||
clan_index: bundle.clan_index,
|
clan_index: ai_state.clan_index,
|
||||||
path: path.as_str().to_string(),
|
path: path.as_str().to_string(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
tables.push(MissionResearchTrfTable {
|
tables.push(MissionResearchTrfTable {
|
||||||
clan_index: bundle.clan_index,
|
clan_index: ai_state.clan_index,
|
||||||
path: path.as_str().to_string(),
|
path: path.as_str().to_string(),
|
||||||
document,
|
document,
|
||||||
});
|
});
|
||||||
@@ -16428,12 +16428,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_load_keeps_independent_trf_tables_for_each_script_bundle_clan() {
|
fn full_load_keeps_independent_trf_tables_for_each_ai_state_clan() {
|
||||||
let vfs = research_startup_test_vfs(
|
let vfs = research_startup_test_vfs(
|
||||||
&[
|
&[Some(b"MISSIONS/SCRIPTS/shared"), None],
|
||||||
Some(b"MISSIONS/SCRIPTS/shared"),
|
|
||||||
Some(b"MISSIONS/SCRIPTS/shared"),
|
|
||||||
],
|
|
||||||
&[
|
&[
|
||||||
b"DATA/Research/shared.trf\0ignored.trf",
|
b"DATA/Research/shared.trf\0ignored.trf",
|
||||||
b"DATA/Research/shared.trf",
|
b"DATA/Research/shared.trf",
|
||||||
@@ -16456,13 +16453,21 @@ mod tests {
|
|||||||
.expect("full load with explicit research gate");
|
.expect("full load with explicit research gate");
|
||||||
|
|
||||||
let tables = loaded_mission_research_trf_tables(&engine).expect("TRF tables");
|
let tables = loaded_mission_research_trf_tables(&engine).expect("TRF tables");
|
||||||
assert_eq!(tables.len(), 2, "each loaded script clan owns a table");
|
assert_eq!(tables.len(), 2, "each AI state owner gets a table");
|
||||||
|
let bundles = loaded_mission_script_bundles(&engine).expect("script bundles");
|
||||||
|
assert_eq!(bundles.len(), 1, "only the scripted clan has a bundle");
|
||||||
|
assert_eq!(bundles[0].clan_index, 0);
|
||||||
|
let ai_states = loaded_mission_ai_startup_states(&engine).expect("AI states");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded_mission_script_bundles(&engine)
|
ai_states
|
||||||
.expect("script bundles")
|
.iter()
|
||||||
.len(),
|
.map(|state| state.clan_index)
|
||||||
2
|
.collect::<Vec<_>>(),
|
||||||
|
[0, 1]
|
||||||
);
|
);
|
||||||
|
assert!(ai_states
|
||||||
|
.iter()
|
||||||
|
.all(|state| state.record_slots == [u32::MAX; 9]));
|
||||||
assert_eq!(tables[0].clan_index, 0);
|
assert_eq!(tables[0].clan_index, 0);
|
||||||
assert_eq!(tables[0].path, "DATA/Research/shared.trf");
|
assert_eq!(tables[0].path, "DATA/Research/shared.trf");
|
||||||
assert_eq!(tables[1].clan_index, 1);
|
assert_eq!(tables[1].clan_index, 1);
|
||||||
@@ -16480,7 +16485,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_load_skips_research_trf_when_gate_bundle_or_c_string_is_missing() {
|
fn full_load_skips_research_trf_when_gate_or_c_string_is_missing() {
|
||||||
for (script_bases, research_paths, gate, mode) in [
|
for (script_bases, research_paths, gate, mode) in [
|
||||||
(
|
(
|
||||||
vec![Some(&b"MISSIONS/SCRIPTS/shared"[..])],
|
vec![Some(&b"MISSIONS/SCRIPTS/shared"[..])],
|
||||||
@@ -16500,12 +16505,6 @@ mod tests {
|
|||||||
true,
|
true,
|
||||||
MissionMode::SingleMission,
|
MissionMode::SingleMission,
|
||||||
),
|
),
|
||||||
(
|
|
||||||
vec![None],
|
|
||||||
vec![&b"DATA/Research/missing.trf"[..]],
|
|
||||||
true,
|
|
||||||
MissionMode::SingleMission,
|
|
||||||
),
|
|
||||||
] {
|
] {
|
||||||
let vfs = research_startup_test_vfs(&script_bases, &research_paths, None);
|
let vfs = research_startup_test_vfs(&script_bases, &research_paths, None);
|
||||||
let mut engine =
|
let mut engine =
|
||||||
@@ -16529,10 +16528,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn research_trf_read_and_decode_errors_keep_the_loaded_world_and_rng() {
|
fn static_preview_skips_research_trf_reads() {
|
||||||
let initial_vfs = research_startup_test_vfs(
|
let vfs = research_startup_test_vfs(
|
||||||
&[Some(b"MISSIONS/SCRIPTS/shared")],
|
&[Some(b"MISSIONS/SCRIPTS/shared")],
|
||||||
&[b"DATA/Research/shared.trf"],
|
&[b"DATA/Research/missing.trf"],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let mut engine = create(EngineServices::new(vfs)).expect("research startup test engine");
|
||||||
|
|
||||||
|
load_mission_static_preview(
|
||||||
|
&mut engine,
|
||||||
|
MissionRequest {
|
||||||
|
key: "MISSIONS/TEST/data.tma".to_string(),
|
||||||
|
mode: MissionMode::SingleMission,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("preview must not read research TRF paths");
|
||||||
|
|
||||||
|
assert!(loaded_mission_research_trf_tables(&engine)
|
||||||
|
.expect("TRF state")
|
||||||
|
.is_empty());
|
||||||
|
assert!(loaded_mission_ai_startup_states(&engine)
|
||||||
|
.expect("AI state")
|
||||||
|
.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_script_ai_research_trf_read_and_decode_errors_keep_loaded_world_and_rng() {
|
||||||
|
let initial_vfs = research_startup_test_vfs(
|
||||||
|
&[Some(b"MISSIONS/SCRIPTS/shared"), None],
|
||||||
|
&[b"", b"DATA/Research/shared.trf"],
|
||||||
Some(&research_test_trf()),
|
Some(&research_test_trf()),
|
||||||
);
|
);
|
||||||
let mut engine = create(EngineServices::new(initial_vfs)).expect("research startup engine");
|
let mut engine = create(EngineServices::new(initial_vfs)).expect("research startup engine");
|
||||||
@@ -16564,8 +16589,8 @@ mod tests {
|
|||||||
|
|
||||||
for trf in [None, Some(&b"malformed"[..])] {
|
for trf in [None, Some(&b"malformed"[..])] {
|
||||||
let vfs = research_startup_test_vfs(
|
let vfs = research_startup_test_vfs(
|
||||||
&[Some(b"MISSIONS/SCRIPTS/shared")],
|
&[Some(b"MISSIONS/SCRIPTS/shared"), None],
|
||||||
&[b"DATA/Research/shared.trf"],
|
&[b"", b"DATA/Research/shared.trf"],
|
||||||
trf,
|
trf,
|
||||||
);
|
);
|
||||||
engine.services.vfs = Some(vfs);
|
engine.services.vfs = Some(vfs);
|
||||||
@@ -16609,7 +16634,7 @@ mod tests {
|
|||||||
clan_index, path, ..
|
clan_index, path, ..
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
assert_eq!(clan_index, 0);
|
assert_eq!(clan_index, 1);
|
||||||
assert_eq!(path, "DATA/Research/shared.trf");
|
assert_eq!(path, "DATA/Research/shared.trf");
|
||||||
}
|
}
|
||||||
(_, error) => panic!("unexpected TRF failure: {error:?}"),
|
(_, error) => panic!("unexpected TRF failure: {error:?}"),
|
||||||
@@ -16620,8 +16645,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn research_trf_c_string_paths_reject_non_ascii_bytes() {
|
fn research_trf_c_string_paths_reject_non_ascii_bytes() {
|
||||||
let vfs = research_startup_test_vfs(
|
let vfs = research_startup_test_vfs(
|
||||||
&[Some(b"MISSIONS/SCRIPTS/shared")],
|
&[Some(b"MISSIONS/SCRIPTS/shared"), None],
|
||||||
&[b"DATA/Research/\xe9.trf"],
|
&[b"", b"DATA/Research/\xe9.trf"],
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
let mut engine = create(EngineServices::new(vfs)).expect("research startup test engine");
|
let mut engine = create(EngineServices::new(vfs)).expect("research startup test engine");
|
||||||
@@ -16639,14 +16664,14 @@ mod tests {
|
|||||||
.expect_err("non-ASCII TRF path must fail explicitly");
|
.expect_err("non-ASCII TRF path must fail explicitly");
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
error,
|
error,
|
||||||
EngineError::ResearchTrfPath { clan_index: 0, path }
|
EngineError::ResearchTrfPath { clan_index: 1, path }
|
||||||
if path == b"DATA/Research/\xe9.trf"
|
if path == b"DATA/Research/\xe9.trf"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn licensed_campaign_full_load_creates_trf_tables_for_script_clan_owners() {
|
fn licensed_campaign_full_load_creates_trf_tables_for_ai_clan_owners() {
|
||||||
let root = licensed_root("IS");
|
let root = licensed_root("IS");
|
||||||
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root));
|
||||||
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
let mut engine = create(EngineServices::new(vfs)).expect("engine");
|
||||||
|
|||||||
@@ -905,22 +905,34 @@ arg1 следующего вызова. `AddNewObjectToGame` принимает
|
|||||||
`FUN_10002a30` (RVA `0x2a30`, `RET 8`) записывает его в `inner+8`. TMA tag
|
`FUN_10002a30` (RVA `0x2a30`, `RET 8`) записывает его в `inner+8`. TMA tag
|
||||||
`row+0x20` ограничивает число вызовов AI slot `+0x38`, но не задаёт владельца.
|
`row+0x20` ограничивает число вызовов AI slot `+0x38`, но не задаёт владельца.
|
||||||
|
|
||||||
|
Создание AI не фильтруется по наличию сценарного пути. В `iron3d.dll` SHA-256
|
||||||
|
`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`
|
||||||
|
`FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов и вызывает для каждой
|
||||||
|
`FUN_100392e0` (RVA `0x392e0`); в `ai.dll` SHA-256
|
||||||
|
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`
|
||||||
|
`CreateSuperAI` (RVA `0xf710`) выделяет AI и вызывает конструктор по RVA
|
||||||
|
`0x1000` без проверки пути сценария. Создание исследовательского контекста
|
||||||
|
следует за успешным созданием AI и требует включённого флага создания
|
||||||
|
исследований и непустого `second_resource.path` до первого NUL. Переданный в
|
||||||
|
`descriptor[0]` индекс клана, а не `second_resource.tag`, задаёт владельца;
|
||||||
|
тег остаётся счётчиком начальных вызовов AI slot `+0x38`.
|
||||||
|
|
||||||
В Rust эта граница представлена только загрузкой таблиц. Полный
|
В Rust эта граница представлена только загрузкой таблиц. Полный
|
||||||
`load_mission` требует явный `MissionStartupInput { queue_time_ms,
|
`load_mission` требует явный `MissionStartupInput { queue_time_ms,
|
||||||
research_creation_gate }`; gate не выводится из `MissionMode` или имени пути.
|
research_creation_gate }`; этот флаг не выводится из `MissionMode` или имени
|
||||||
При `true` загрузчик проходит по успешно разобранным пакетам сценариев, берёт
|
пути. Если флаг равен `true`, загрузчик проходит по существующим
|
||||||
реальный `second_resource.path` соответствующего TMA-клана до первого NUL,
|
`MissionAiStartupState` всех TMA-кланов, в том числе без сценарного пакета,
|
||||||
отклоняет не-ASCII путь, затем сохраняет отдельный `ResearchTrfDocument` с
|
читает их `second_resource.path` до первого NUL и отклоняет не-ASCII путь.
|
||||||
`clan_index` и нормализованным путём. Пустая строка, выключенный gate или
|
Пустая строка пропускается; выключенный флаг и статический preview не читают
|
||||||
отсутствие пакета сценариев пропускают чтение. Повтор одного пути разными
|
TRF. На каждого
|
||||||
владельцами приводит к отдельным буферам строк TRF0. Обычные тесты на
|
владельца сохраняется отдельный `ResearchTrfDocument` с нормализованным путём,
|
||||||
искусственных данных проверяют владельцев, повторное декодирование, пропуски,
|
даже когда разные кланы указывают на один файл. Обычные тесты на искусственных
|
||||||
ошибки и откат; игнорируемая проверка оригинальных ресурсов связывает реальные
|
данных проверяют владение у кланов со сценариями и без них, повторное
|
||||||
`Campaign.00/Mission.01` пары путей с индексами владельцев. Это не загружает
|
декодирование, пропуски, ошибки и откат; игнорируемая проверка оригинальных
|
||||||
полный исследовательский контекст оригинала.
|
ресурсов связывает реальные `Campaign.00/Mission.01` пары путей с индексами владельцев.
|
||||||
Успешно разобранный пакет сценариев здесь обозначает только границу текущей
|
Rust пока сохраняет только декодированные данные и связь с владельцем: он не
|
||||||
настройки Rust, а не совпадение с полным нативным жизненным циклом
|
создаёт полный исследовательский контекст, не строит AI-каталог и не исполняет
|
||||||
`CreateSuperAI`.
|
`Handler(43)`.
|
||||||
|
|
||||||
Нативный metadata loader нельзя считать простым копированием таблицы. В GOG
|
Нативный metadata loader нельзя считать простым копированием таблицы. В GOG
|
||||||
`Behavior.dll` (SHA-256
|
`Behavior.dll` (SHA-256
|
||||||
|
|||||||
+20
-11
@@ -579,8 +579,14 @@ let cost = calc_full_research_cost(&candidate, &table)?;
|
|||||||
штатный Handler(43) и запуск миссии здесь не проверялись.
|
штатный Handler(43) и запуск миссии здесь не проверялись.
|
||||||
|
|
||||||
Владелец начальной записи исследования — индекс текущего прохода по кланам.
|
Владелец начальной записи исследования — индекс текущего прохода по кланам.
|
||||||
Движок сохраняет его в `descriptor[0]`, а затем переносит в контекст, чтобы
|
В GOG-сборке `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов
|
||||||
связать запрос с AI-данными того же клана.
|
и вызывает для каждой `FUN_100392e0` (RVA `0x392e0`), которая создаёт AI;
|
||||||
|
`ai.dll!CreateSuperAI` (RVA `0xf710`) выделяет объект и вызывает конструктор
|
||||||
|
по RVA `0x1000`, не проверяя наличие сценарного пути. Для исследовательского
|
||||||
|
пути нужны отдельный флаг включения и непустой `second_resource.path`; наличие
|
||||||
|
сценарного пакета условием не является. Индекс клана сохраняется в
|
||||||
|
`descriptor[0]` и переносится в контекст, чтобы связать запрос с AI-данными
|
||||||
|
того же клана.
|
||||||
|
|
||||||
Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из
|
Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из
|
||||||
`GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов
|
`GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов
|
||||||
@@ -590,12 +596,14 @@ Handler(43) и штатный запуск миссии остаются неп
|
|||||||
|
|
||||||
Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди
|
Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди
|
||||||
и отдельный `research_creation_gate`. Режим миссии и имя файла этот
|
и отдельный `research_creation_gate`. Режим миссии и имя файла этот
|
||||||
флаг не заменяют. При включённом флаге таблица загружается только для клана,
|
флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся
|
||||||
чей сценарный пакет уже прочитан и успешно разобран; путь берётся из второго
|
`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного
|
||||||
ресурса этого клана как C-строка. Пустая C-строка пропускается, а не-ASCII
|
пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой
|
||||||
байты дают явную ошибку. Клан индексируется по порядку в TMA, тег ресурса на
|
путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку.
|
||||||
владение не влияет. Если несколько кланов указывают на один файл, каждый
|
Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег
|
||||||
получает отдельно декодированную таблицу с собственными строками TRF0.
|
`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов
|
||||||
|
указывают на один файл, каждый получает отдельно декодированную таблицу со
|
||||||
|
своими строками TRF0.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
MissionStartupInput {
|
MissionStartupInput {
|
||||||
@@ -604,9 +612,10 @@ MissionStartupInput {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Это сохраняет исходные таблицы и их связь с владельцами для следующих этапов.
|
Это сохраняет декодированные таблицы и связь с владельцами для следующих
|
||||||
Оно ещё не создаёт полный исследовательский контекст оригинала и не запускает
|
этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не
|
||||||
`Handler(43)`; эта обработка пока остаётся явно неподдержанной.
|
строит AI-каталог и не запускает `Handler(43)`; эти части пока не
|
||||||
|
восстановлены.
|
||||||
|
|
||||||
Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256
|
Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256
|
||||||
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция
|
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция
|
||||||
|
|||||||
Reference in new issue
Block a user