feat(runtime): register mission objects with clan distributors
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
This commit is contained in:
5 files changed
+318
-18
No files matched your search
@@ -23,8 +23,8 @@ use fparkan_terrain::SurfaceQuery;
|
|||||||
use fparkan_vfs::{Vfs, VfsError};
|
use fparkan_vfs::{Vfs, VfsError};
|
||||||
use fparkan_world::{
|
use fparkan_world::{
|
||||||
construct_object, handle_by_original_id, new as new_world, register_object, set_transform,
|
construct_object, handle_by_original_id, new as new_world, register_object, set_transform,
|
||||||
step, transform_state, InputSnapshot, ObjectDraft, OriginalObjectId, TransformState, World,
|
step, transform_state, InputSnapshot, ObjectDraft, ObjectHandle, OriginalObjectId,
|
||||||
WorldConfig, WorldSnapshot,
|
TransformState, World, WorldConfig, WorldSnapshot,
|
||||||
};
|
};
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -115,6 +115,34 @@ pub struct MissionObjectDraft {
|
|||||||
pub unit_components: Vec<UnitComponentRecord>,
|
pub unit_components: Vec<UnitComponentRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A mission object registered with one clan's native Behavior distributor.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BehaviorRegisteredObject {
|
||||||
|
/// Stable identity from the source TMA object order.
|
||||||
|
pub original_id: OriginalObjectId,
|
||||||
|
/// Corresponding live Rust world object.
|
||||||
|
pub handle: ObjectHandle,
|
||||||
|
/// Raw DWORD that selects the `GetSuperAI` table slot.
|
||||||
|
pub selector: u32,
|
||||||
|
/// Class word recovered from the root Unit DAT header.
|
||||||
|
pub class_flags: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resource distributor state associated with one TMA clan's constructed AI.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BehaviorDistributorState {
|
||||||
|
/// TMA clan index; it is not an index into the filtered script bundle list.
|
||||||
|
pub clan_index: usize,
|
||||||
|
/// Whether a successfully loaded script package published this AI to
|
||||||
|
/// `GetSuperAI` in the standard host path.
|
||||||
|
pub super_ai_visible: bool,
|
||||||
|
/// Objects admitted by the currently recovered standard-load registration path.
|
||||||
|
pub registered_objects: Vec<BehaviorRegisteredObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAST_SUPER_AI_TABLE_INDEX: u32 = 63;
|
||||||
|
const BEHAVIOR_REGISTRATION_KEY_FLAG: u32 = 0x8000_0000;
|
||||||
|
|
||||||
/// Native terrain adjustment selected for a mission object's BAS geometry.
|
/// Native terrain adjustment selected for a mission object's BAS geometry.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum BasementPlacementMode {
|
pub enum BasementPlacementMode {
|
||||||
@@ -429,6 +457,7 @@ struct LoadedMissionState {
|
|||||||
mission_assets: MissionAssets,
|
mission_assets: MissionAssets,
|
||||||
asset_plan: MissionAssetPlan,
|
asset_plan: MissionAssetPlan,
|
||||||
object_drafts: Vec<MissionObjectDraft>,
|
object_drafts: Vec<MissionObjectDraft>,
|
||||||
|
behavior_distributors: Vec<BehaviorDistributorState>,
|
||||||
script_bundles: Vec<MissionScriptBundle>,
|
script_bundles: Vec<MissionScriptBundle>,
|
||||||
script_varset: Option<MissionScriptVarSet>,
|
script_varset: Option<MissionScriptVarSet>,
|
||||||
script_init_states: Vec<MissionScriptInitState>,
|
script_init_states: Vec<MissionScriptInitState>,
|
||||||
@@ -955,6 +984,58 @@ fn load_mission_with_options(
|
|||||||
registered_objects += 1;
|
registered_objects += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let behavior_distributors = if options.asset_scope == MissionAssetScope::Full {
|
||||||
|
let visible_clan_indices = loaded_scripts
|
||||||
|
.bundles
|
||||||
|
.iter()
|
||||||
|
.map(|bundle| bundle.clan_index)
|
||||||
|
.filter(|clan_index| {
|
||||||
|
*clan_index < mission.clans.len()
|
||||||
|
&& *clan_index <= LAST_SUPER_AI_TABLE_INDEX as usize
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut distributors = mission
|
||||||
|
.clans
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(clan_index, _)| BehaviorDistributorState {
|
||||||
|
clan_index,
|
||||||
|
super_ai_visible: visible_clan_indices.contains(&clan_index),
|
||||||
|
registered_objects: Vec::new(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (draft, handle) in object_drafts.iter().zip(&handles) {
|
||||||
|
let Some(class_flags) = draft.unit_dat_header_flags else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !has_behavior_registration_key(class_flags) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(clan_index) = behavior_clan_for_selector(
|
||||||
|
draft.raw_after_resource,
|
||||||
|
mission.clans.len(),
|
||||||
|
&visible_clan_indices,
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(original_id) = draft.original_id else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
distributors[clan_index]
|
||||||
|
.registered_objects
|
||||||
|
.push(BehaviorRegisteredObject {
|
||||||
|
original_id,
|
||||||
|
handle: *handle,
|
||||||
|
selector: draft.raw_after_resource,
|
||||||
|
class_flags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
distributors
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
let mut next_behavior_rng_state = engine.behavior_rng_state;
|
let mut next_behavior_rng_state = engine.behavior_rng_state;
|
||||||
if options.asset_scope == MissionAssetScope::Full {
|
if options.asset_scope == MissionAssetScope::Full {
|
||||||
let queue_time_ms = queue_time_ms.expect("full mission loading requires a queue time");
|
let queue_time_ms = queue_time_ms.expect("full mission loading requires a queue time");
|
||||||
@@ -1029,6 +1110,7 @@ fn load_mission_with_options(
|
|||||||
mission_assets,
|
mission_assets,
|
||||||
asset_plan: mission_asset_plan,
|
asset_plan: mission_asset_plan,
|
||||||
object_drafts,
|
object_drafts,
|
||||||
|
behavior_distributors,
|
||||||
script_bundles: loaded_scripts.bundles,
|
script_bundles: loaded_scripts.bundles,
|
||||||
script_varset: loaded_scripts.varset,
|
script_varset: loaded_scripts.varset,
|
||||||
script_init_states: loaded_scripts.init_states,
|
script_init_states: loaded_scripts.init_states,
|
||||||
@@ -1300,6 +1382,33 @@ pub fn loaded_mission_object_drafts(engine: &Engine) -> Option<&[MissionObjectDr
|
|||||||
.map(|state| state.object_drafts.as_slice())
|
.map(|state| state.object_drafts.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the clan-indexed Behavior distributors and their registered objects.
|
||||||
|
#[must_use]
|
||||||
|
pub fn loaded_mission_behavior_distributors(
|
||||||
|
engine: &Engine,
|
||||||
|
) -> Option<&[BehaviorDistributorState]> {
|
||||||
|
engine
|
||||||
|
.loaded
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| state.behavior_distributors.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn behavior_clan_for_selector(
|
||||||
|
selector: u32,
|
||||||
|
clan_count: usize,
|
||||||
|
super_ai_clan_indices: &[usize],
|
||||||
|
) -> Option<usize> {
|
||||||
|
if selector > LAST_SUPER_AI_TABLE_INDEX {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let clan_index = usize::try_from(selector).ok()?;
|
||||||
|
(clan_index < clan_count && super_ai_clan_indices.contains(&clan_index)).then_some(clan_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_behavior_registration_key(class_flags: u32) -> bool {
|
||||||
|
class_flags & BEHAVIOR_REGISTRATION_KEY_FLAG != 0
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
|
fn normalize_engine_path(role: &'static str, value: &str) -> Result<NormalizedPath, EngineError> {
|
||||||
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
|
normalize_relative(value.as_bytes(), PathPolicy::StrictLegacy).map_err(|source| {
|
||||||
EngineError::Path {
|
EngineError::Path {
|
||||||
@@ -1666,6 +1775,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_selector_requires_an_exact_visible_clan_slot() {
|
||||||
|
let sparse_slots = [1, 63];
|
||||||
|
assert_eq!(behavior_clan_for_selector(1, 64, &sparse_slots), Some(1));
|
||||||
|
assert_eq!(behavior_clan_for_selector(63, 64, &[63]), Some(63));
|
||||||
|
assert_eq!(behavior_clan_for_selector(0, 64, &[1, 63]), None);
|
||||||
|
assert_eq!(behavior_clan_for_selector(64, 65, &[64]), None);
|
||||||
|
assert_eq!(behavior_clan_for_selector(0xfffe, 65_535, &[0xfffe]), None);
|
||||||
|
assert_eq!(behavior_clan_for_selector(63, 63, &[63]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavior_registration_uses_the_class_high_bit() {
|
||||||
|
assert!(has_behavior_registration_key(0x8000_0010));
|
||||||
|
assert!(has_behavior_registration_key(0x8000_0002));
|
||||||
|
assert!(has_behavior_registration_key(0x8000_0123));
|
||||||
|
assert!(!has_behavior_registration_key(0x0000_0123));
|
||||||
|
assert!(!has_behavior_registration_key(0x7fff_ffff));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn behavior_warm_start_uses_native_clock_and_shared_rng_vectors() {
|
fn behavior_warm_start_uses_native_clock_and_shared_rng_vectors() {
|
||||||
let mut rng_state = 1;
|
let mut rng_state = 1;
|
||||||
@@ -2260,6 +2389,80 @@ mod tests {
|
|||||||
assert_eq!(draft.identity_or_clan_raw, object.identity_or_clan_raw);
|
assert_eq!(draft.identity_or_clan_raw, object.identity_or_clan_raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let distributors =
|
||||||
|
loaded_mission_behavior_distributors(&engine).expect("Behavior distributors");
|
||||||
|
assert_eq!(
|
||||||
|
distributors
|
||||||
|
.iter()
|
||||||
|
.map(|distributor| (distributor.clan_index, distributor.super_ai_visible))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![(0, true), (1, true)]
|
||||||
|
);
|
||||||
|
let registered = distributors
|
||||||
|
.iter()
|
||||||
|
.flat_map(|distributor| {
|
||||||
|
distributor
|
||||||
|
.registered_objects
|
||||||
|
.iter()
|
||||||
|
.map(move |object| (distributor.clan_index, object))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
registered
|
||||||
|
.iter()
|
||||||
|
.map(|(clan_index, object)| {
|
||||||
|
let draft = drafts
|
||||||
|
.iter()
|
||||||
|
.find(|draft| draft.original_id == Some(object.original_id))
|
||||||
|
.expect("registered object draft");
|
||||||
|
(
|
||||||
|
*clan_index,
|
||||||
|
object.original_id,
|
||||||
|
draft.resource_name_raw.as_slice(),
|
||||||
|
object.selector,
|
||||||
|
object.class_flags,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
OriginalObjectId(4),
|
||||||
|
&b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..],
|
||||||
|
0,
|
||||||
|
0x8000_0010,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
OriginalObjectId(6),
|
||||||
|
&b"UNITS\\BUILDS\\GENER\\gener01.dat"[..],
|
||||||
|
0,
|
||||||
|
0x8000_0002,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
OriginalObjectId(5),
|
||||||
|
&b"UNITS\\BUILDS\\PLANT\\wor_l_n1.dat"[..],
|
||||||
|
1,
|
||||||
|
0x8000_0010,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
OriginalObjectId(7),
|
||||||
|
&b"UNITS\\BUILDS\\GENER\\gener01.dat"[..],
|
||||||
|
1,
|
||||||
|
0x8000_0002,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(registered.len(), 4);
|
||||||
|
for (_, object) in registered {
|
||||||
|
assert_eq!(
|
||||||
|
handle_by_original_id(&engine.world, object.original_id),
|
||||||
|
Some(object.handle)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mission
|
mission
|
||||||
.clans
|
.clans
|
||||||
@@ -2333,6 +2536,9 @@ mod tests {
|
|||||||
.expect("load licensed campaign preview");
|
.expect("load licensed campaign preview");
|
||||||
|
|
||||||
let bundles = loaded_mission_script_bundles(&engine).expect("script bundles");
|
let bundles = loaded_mission_script_bundles(&engine).expect("script bundles");
|
||||||
|
assert!(loaded_mission_behavior_distributors(&engine)
|
||||||
|
.expect("preview Behavior distributors")
|
||||||
|
.is_empty());
|
||||||
assert!(bundles
|
assert!(bundles
|
||||||
.iter()
|
.iter()
|
||||||
.all(|bundle| bundle.behavior_warm_start.is_none()));
|
.all(|bundle| bundle.behavior_warm_start.is_none()));
|
||||||
@@ -2639,6 +2845,9 @@ mod tests {
|
|||||||
let previous_bundles = loaded_mission_script_bundles(&engine)
|
let previous_bundles = loaded_mission_script_bundles(&engine)
|
||||||
.expect("previous bundles")
|
.expect("previous bundles")
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
let previous_distributors = loaded_mission_behavior_distributors(&engine)
|
||||||
|
.expect("previous Behavior distributors")
|
||||||
|
.to_vec();
|
||||||
let previous_rng = engine.behavior_rng_state;
|
let previous_rng = engine.behavior_rng_state;
|
||||||
let before = step_headless(&mut engine, InputSnapshot)
|
let before = step_headless(&mut engine, InputSnapshot)
|
||||||
.expect("step previous world")
|
.expect("step previous world")
|
||||||
@@ -2662,6 +2871,10 @@ mod tests {
|
|||||||
loaded_mission_script_bundles(&engine),
|
loaded_mission_script_bundles(&engine),
|
||||||
Some(previous_bundles.as_slice())
|
Some(previous_bundles.as_slice())
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
loaded_mission_behavior_distributors(&engine),
|
||||||
|
Some(previous_distributors.as_slice())
|
||||||
|
);
|
||||||
assert_eq!(engine.behavior_rng_state, previous_rng);
|
assert_eq!(engine.behavior_rng_state, previous_rng);
|
||||||
let after = step_headless(&mut engine, InputSnapshot)
|
let after = step_headless(&mut engine, InputSnapshot)
|
||||||
.expect("step retained world")
|
.expect("step retained world")
|
||||||
|
|||||||
@@ -129,10 +129,41 @@ Rust `load_mission` принимает явный `queue_time_ms`, а не чи
|
|||||||
host construction phases. Не проверена другая лицензированная миссия с
|
host construction phases. Не проверена другая лицензированная миссия с
|
||||||
непустым `Init`. Вызов `Behavior.dll!FUN_10019e80` (RVA `0x19e80`, SHA-256
|
непустым `Init`. Вызов `Behavior.dll!FUN_10019e80` (RVA `0x19e80`, SHA-256
|
||||||
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) исполнялся
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) исполнялся
|
||||||
на синтетических интерфейсах, но runtime пока не вызывает его в игровом цикле:
|
на синтетических интерфейсах. Путь членства в реальных distributor-списках
|
||||||
не доказаны создание реальных записей distributor и интерфейсов ArealMap,
|
теперь связан с полями TMA и Unit DAT: `World3D!CreateObject` передаёт selector
|
||||||
которые ему нужны. Остальной Behavior tick flow и общий dispatcher событий
|
и class word через дескриптор в Behavior (RVA `0x7e55`, `0x7e62`), а
|
||||||
также не восстановлены.
|
`Behavior.dll!FUN_10005e60` (RVA `0x5e60`) проверяет высокий бит registration
|
||||||
|
key, AI и distributor. Selector из первого DWORD после имени ресурса выбирает
|
||||||
|
слот `GetSuperAI`; таблица этой функции находится в `ai.dll` RVA `0x55398`.
|
||||||
|
Хеши `MisLoad.dll`, `iron3d.dll`, `ArealMap.dll`, `World3D.dll` и
|
||||||
|
`Behavior.dll`, использованных для цепочки, перечислены в
|
||||||
|
[справочнике TMA](../reference/tma.md).
|
||||||
|
|
||||||
|
Отдельный режимный бит Behavior нельзя выводить из `MissionMode`. Конструктор
|
||||||
|
Behavior (`Behavior.dll`, RVA `0x3600`) вызывает `World3D!stdGetGameMode` (RVA
|
||||||
|
`0x14750`), который читает host-global по RVA `0x795130` в `World3D.dll` (SHA-256
|
||||||
|
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`). В
|
||||||
|
обычном пути `stdInitGame` (RVA `0x13e90`, запись режима около `0x14040`)
|
||||||
|
формируется отдельное host-значение; в наблюдавшемся запуске AutoDemo getter
|
||||||
|
вернул `4`, и бит `0x2` был снят. Это объясняет соответствующий gate при
|
||||||
|
создании Behavior, но не устанавливает отображение между host-режимом и Rust
|
||||||
|
`MissionMode`.
|
||||||
|
|
||||||
|
Наблюдение обычного запуска лицензированной GOG AutoDemo подтвердило четыре
|
||||||
|
добавления с ключами `0x80000003..0x80000006` в два клановых списка. Полная
|
||||||
|
загрузка Rust сохраняет соответствующие `ObjectHandle` в точном доступном слоте
|
||||||
|
`GetSuperAI` до стартового `Init`; preview не создаёт членств. Лицензированный
|
||||||
|
тест связывает эти записи с TMA ID 4–7, selector и Unit DAT class. Это
|
||||||
|
подтверждает только состав списков, не точную эквивалентность всей загрузки или
|
||||||
|
регулярного шага Behavior.
|
||||||
|
|
||||||
|
Нативный `Behavior.dll!Add` (RVA `0x1aac0`) создаёт для каждого участника запись
|
||||||
|
размером `0x1c` с начальной долей `1 / count`, а `normalize` (RVA `0x1ac90`)
|
||||||
|
перенормирует прежние доли после добавления. Rust пока хранит только членство:
|
||||||
|
поля записи, доли и их изменение ещё не исполняются. Не восстановлено и
|
||||||
|
соединение с реальными интерфейсами ArealMap, необходимыми для регулярного
|
||||||
|
вызова `FUN_10019e80`; остальной Behavior tick flow и общий dispatcher событий
|
||||||
|
также остаются открытыми.
|
||||||
|
|
||||||
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
|
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
|
||||||
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
|
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ slot `+0x40` в путь выбора AI в Behavior. Поэтому Rust сох
|
|||||||
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e` и
|
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e` и
|
||||||
`Behavior.dll`
|
`Behavior.dll`
|
||||||
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`.
|
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`.
|
||||||
|
Таблица `GetSuperAI` в `ai.dll` находится по RVA `0x55398`; SHA-256 файла —
|
||||||
|
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`.
|
||||||
|
|
||||||
`Property` состоит из четырёх raw `u32` и имени. Typed views разрешены только
|
`Property` состоит из четырёх raw `u32` и имени. Typed views разрешены только
|
||||||
для доказанных property names и consumers.
|
для доказанных property names и consumers.
|
||||||
@@ -215,6 +215,26 @@ MissionObjectDraft {
|
|||||||
`fparkan-world` не подтверждено. Смещения, условия версий и контрольные суммы исследованных
|
`fparkan-world` не подтверждено. Смещения, условия версий и контрольные суммы исследованных
|
||||||
сборок приведены в [справочнике TMA](../reference/tma.md).
|
сборок приведены в [справочнике TMA](../reference/tma.md).
|
||||||
|
|
||||||
|
Это слово влияет не только на сценарий. Успешно загруженный пакет клана
|
||||||
|
публикует его `SuperAI` в таблице `GetSuperAI`; World3D передаёт первый DWORD
|
||||||
|
дескриптора создания через Behavior, где он выбирает тот же номер таблицы.
|
||||||
|
Поэтому при создании полного мира можно отнести объект к конкретному клановому
|
||||||
|
диспетчеру, если его номер лежит в диапазоне `0..63` и сценарий этого клана
|
||||||
|
загружен. Rust хранит такую связь отдельно от владельца объекта:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TMA raw_after_resource = 1
|
||||||
|
-> GetSuperAI[1] доступен после загрузки script bundle клана 1
|
||||||
|
-> запись объекта относится к distributor клана 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Для объектов с Unit DAT используется старший бит DWORD по смещению `+4` от
|
||||||
|
начала DAT как общий признак, по которому оригинальный World3D создаёт
|
||||||
|
registration key для Behavior. Это второе 32-битное слово заголовка. Из него
|
||||||
|
нельзя вывести сторону или тип владельца. В AutoDemo это связывает поля TMA,
|
||||||
|
class word из Unit DAT и выбранный клановый AI, не меняя `OwnerId` и хеш
|
||||||
|
игрового состояния.
|
||||||
|
|
||||||
Свойства хранятся как ordered property bag:
|
Свойства хранятся как ordered property bag:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
+46
-12
@@ -204,11 +204,47 @@ SuperAI для записей кланов, затем обновляет Behavi
|
|||||||
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
|
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
|
||||||
входа `AI+0x78`, одноразовую метку `AI+0x8a8` или обновления Behavior на игровых
|
входа `AI+0x78`, одноразовую метку `AI+0x8a8` или обновления Behavior на игровых
|
||||||
тиках. При этом один конкретный шаг Behavior — распределение ресурсов — уже
|
тиках. При этом один конкретный шаг Behavior — распределение ресурсов — уже
|
||||||
исполнен на исходной DLL с искусственными объектами; FParkan пока не вызывает
|
исполнен на исходной DLL с искусственными объектами. Rust пока не вызывает
|
||||||
его, потому что игровой мир Rust ещё не предоставляет нужные ресурсные
|
регулярный шаг: игровой мир ещё не предоставляет нужные ресурсные интерфейсы.
|
||||||
интерфейсы. Runtime также не подменяет очередь оригинала: вызывающая сторона
|
При полной загрузке Rust уже восстанавливает членство объектов в клановых
|
||||||
сама передаёт время, а локальный генератор Rust пока учитывает только случайные
|
списках Behavior, но сами записи распределителя и их доли пока не моделирует.
|
||||||
числа восстановленного стартового шага.
|
Runtime также не подменяет очередь оригинала: вызывающая сторона сама передаёт
|
||||||
|
время, а локальный генератор Rust пока учитывает только случайные числа
|
||||||
|
восстановленного стартового шага.
|
||||||
|
|
||||||
|
### Откуда берутся объекты распределителя
|
||||||
|
|
||||||
|
Для каждого клана Iron3D создаёт AI независимо от того, найден ли его `.scr`.
|
||||||
|
Таблица `GetSuperAI` получает указатель только для клана, чей пакет сценария
|
||||||
|
успешно загружен. У размещённого объекта первое DWORD после имени ресурса
|
||||||
|
выбирает индекс этой таблицы; второй соседний DWORD для этой цели не используется.
|
||||||
|
World3D получает индекс через дескриптор создания объекта и передаёт его в
|
||||||
|
Behavior. Отдельно DWORD по смещению `+4` от начала Unit DAT задаёт class word
|
||||||
|
объекта. Это второе 32-битное слово файла; его старший бит приводит к созданию
|
||||||
|
registration key, который проходит защиту Behavior вместе с проверками AI и
|
||||||
|
distributor.
|
||||||
|
|
||||||
|
Для обычной полной загрузки Rust сохраняет по одной записи состояния на клан.
|
||||||
|
Каждый размещённый объект с известным Unit DAT и установленным старшим битом
|
||||||
|
получает существующий `ObjectHandle` и попадает в список только при точном
|
||||||
|
совпадении индекса `raw_after_resource` с доступной записью `GetSuperAI`. Индекс
|
||||||
|
не перенумеровывается: например, пакет клана 1 остаётся слотом 1 даже если
|
||||||
|
пакет клана 0 отсутствует. Preview сохраняет свои статические данные, но не
|
||||||
|
создаёт эти связи.
|
||||||
|
|
||||||
|
В лицензированной AutoDemo это даёт четыре членства: TMA объекты 4 и 6 идут
|
||||||
|
в слот 0, а объекты 5 и 7 — в слот 1. У парных записей первого клана raw
|
||||||
|
селектор равен 0, у второго — 1; классы из Unit DAT равны `0x80000010` для
|
||||||
|
`wor_l_n1.dat` и `0x80000002` для `gener01.dat`. Тот же обычный путь в исходном
|
||||||
|
Behavior добавил четыре ключа `0x80000003..0x80000006` в два distributor-а.
|
||||||
|
Rust-тест повторно читает лицензированную миссию, связывает результаты с её
|
||||||
|
TMA-записями и проверяет реальные `ObjectHandle`, но помечен `#[ignore]`, чтобы
|
||||||
|
не требовать оригинальных ресурсов от остальных разработчиков.
|
||||||
|
|
||||||
|
Это пока только перечень участников. Оригинальный `Behavior.dll!Add` создаёт для
|
||||||
|
каждого участника запись размером `0x1c` с долей `1 / count`, после чего
|
||||||
|
нормирует прежние доли. Rust ещё не хранит эти поля и не исполняет их регулярный
|
||||||
|
обмен ресурсами. Поля не выводятся из TMA-соседнего слова или `OwnerId`.
|
||||||
|
|
||||||
### Регулярная передача ресурсов
|
### Регулярная передача ресурсов
|
||||||
|
|
||||||
@@ -227,13 +263,11 @@ Resource Distributor работает со списком зарегистрир
|
|||||||
исходной `Behavior.dll`, но объекты и ответы интерфейсов заданы тестом; это ещё
|
исходной `Behavior.dll`, но объекты и ответы интерфейсов заданы тестом; это ещё
|
||||||
не воспроизведение реальной миссии.
|
не воспроизведение реальной миссии.
|
||||||
|
|
||||||
Пока мы знаем точные границы интерфейсных запросов и некоторые ветви классов
|
Членство списка связано с реальным путём регистрации, но выполнение этой
|
||||||
объектов, но не все доменные значения полей записи и не происхождение реальных
|
миссии не сравнивалось с тем, как исходная DLL заполняет поля записи `0x1c` и
|
||||||
строк списка. Загрузчик FParkan не должен создавать такие строки из похожих
|
нормирует доли. Rust пока не хранит эти поля, а игровому миру ещё не хватает
|
||||||
полей миссии: сначала нужно доказать путь регистрации и связать его с типами
|
ресурсных интерфейсов ArealMap для регулярного шага. Открытые вопросы и границы
|
||||||
ресурсных объектов. Технические идентификаторы и численные векторы приведены
|
проверки приведены в [границах знания](../appendices/knowledge-boundaries.md).
|
||||||
в [приложении о сценарной машине](../appendices/script-vm.md), а оставшиеся
|
|
||||||
вопросы перечислены в [границах знания](../appendices/knowledge-boundaries.md).
|
|
||||||
|
|
||||||
Первый часто встречающийся table entry с side effect — `Handler(2)` (176
|
Первый часто встречающийся table entry с side effect — `Handler(2)` (176
|
||||||
records в corpus). Он берёт active instruction, разрешает семь slots через
|
records в corpus). Он берёт active instruction, разрешает семь slots через
|
||||||
|
|||||||
Reference in new issue
Block a user