feat(runtime): project native Control object classes
Docs Deploy / Build and Deploy MkDocs (push) Successful in 50s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 50s
This commit is contained in:
5 files changed
+295
-5
No files matched your search
@@ -41,6 +41,11 @@ pub use fparkan_assets::MissionAssets;
|
||||
const MISSION_DECODED_PAYLOAD_CACHE_ENTRIES: usize = 256;
|
||||
const FALLBACK_SCRIPT_VARSET_PATH: &str = "MISSIONS/SCRIPTS/varset.var";
|
||||
const INITIAL_BEHAVIOR_RNG_STATE: u32 = 1;
|
||||
const UNIT_DAT_CONTROL_CLASS_HIGH_BIT: u32 = 0x8000_0000;
|
||||
const CONTROL_CLASS_SOURCE_BTLU: u32 = u32::from_le_bytes(*b"BTLU");
|
||||
const CONTROL_CLASS_SOURCE_BULL: u32 = u32::from_le_bytes(*b"BULL");
|
||||
const CONTROL_CLASS_SOURCE_WPNS: u32 = u32::from_le_bytes(*b"WPNS");
|
||||
const CONTROL_CLASS_SOURCE_STAT: u32 = u32::from_le_bytes(*b"STAT");
|
||||
|
||||
/// Injectable engine services used by composition roots.
|
||||
#[derive(Clone, Default)]
|
||||
@@ -454,6 +459,8 @@ pub struct MissionControlDeviceTableState {
|
||||
pub handle: ObjectHandle,
|
||||
/// Complete base projection, or an explicit unsupported boundary.
|
||||
pub status: MissionControlDeviceTableStatus,
|
||||
/// Native Control object class chosen by the full Unit DAT load route.
|
||||
pub native_control_class: Option<MissionControlClassProjection>,
|
||||
/// Raw CTLD profile retained by native component ID zero and its per-owner copy.
|
||||
pub owner_profile: Option<MissionControlOwnerProfile>,
|
||||
/// Global NDP provider rows used by indexed Control selectors.
|
||||
@@ -472,6 +479,24 @@ pub struct MissionControlDeviceTableState {
|
||||
pub current_output_rate_5e0_bits: u32,
|
||||
}
|
||||
|
||||
/// Provenance for the class passed to Control's constructor.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MissionControlClassSource {
|
||||
/// The Unit DAT class word's high bit selected the World -> Terrain route.
|
||||
UnitDatHighBit { header_flags: u32 },
|
||||
/// The root prototype NRes registry entry selected the direct AniMesh route.
|
||||
RootPrototypeFourCc { source_type: u32 },
|
||||
}
|
||||
|
||||
/// Native Control object class selected during full Unit DAT loading.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MissionControlClassProjection {
|
||||
/// The class argument received by `Control.dll!LoadControlSystem`.
|
||||
pub class_id: u32,
|
||||
/// Source branch and raw value that selected the class.
|
||||
pub source: MissionControlClassSource,
|
||||
}
|
||||
|
||||
/// CTLD owner profile selected by native component ID zero.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MissionControlOwnerProfile {
|
||||
@@ -1518,13 +1543,19 @@ fn load_mission_with_options(
|
||||
.zip(&handles)
|
||||
.enumerate()
|
||||
.map(|(object_index, (draft, handle))| {
|
||||
let prototype_span = prototype_graph
|
||||
.root_prototype_request_spans
|
||||
.get(object_index);
|
||||
let native_control_class = native_control_class_for_root(
|
||||
draft.unit_dat_header_flags,
|
||||
prototype_span,
|
||||
&resolved_prototypes,
|
||||
);
|
||||
let projection = build_control_device_projection(
|
||||
object_index,
|
||||
draft,
|
||||
&mission_assets,
|
||||
prototype_graph
|
||||
.root_prototype_request_spans
|
||||
.get(object_index),
|
||||
prototype_span,
|
||||
&resolved_prototypes,
|
||||
);
|
||||
match projection {
|
||||
@@ -1532,6 +1563,7 @@ fn load_mission_with_options(
|
||||
original_id: draft.original_id,
|
||||
handle: *handle,
|
||||
status: MissionControlDeviceTableStatus::Available(projection.devices),
|
||||
native_control_class,
|
||||
owner_profile: projection.owner_profile,
|
||||
node_provider_maximum_amount_bits: initial_node_provider_maximum_amount(
|
||||
&projection.node_provider_status,
|
||||
@@ -1547,6 +1579,7 @@ fn load_mission_with_options(
|
||||
original_id: draft.original_id,
|
||||
handle: *handle,
|
||||
status: MissionControlDeviceTableStatus::Unavailable(reason.clone()),
|
||||
native_control_class,
|
||||
owner_profile: None,
|
||||
node_provider_maximum_amount_bits: None,
|
||||
node_provider_amount_bits: None,
|
||||
@@ -4996,6 +5029,37 @@ fn native_ex_to_append_key(
|
||||
Some(raw_key.wrapping_add(first_global_node).wrapping_sub(1))
|
||||
}
|
||||
|
||||
fn native_control_class_for_root(
|
||||
unit_dat_header_flags: Option<u32>,
|
||||
prototype_span: Option<&std::ops::Range<usize>>,
|
||||
prototypes: &[fparkan_prototype::EffectivePrototype],
|
||||
) -> Option<MissionControlClassProjection> {
|
||||
let header_flags = unit_dat_header_flags?;
|
||||
if header_flags & UNIT_DAT_CONTROL_CLASS_HIGH_BIT != 0 {
|
||||
return Some(MissionControlClassProjection {
|
||||
class_id: 3,
|
||||
source: MissionControlClassSource::UnitDatHighBit { header_flags },
|
||||
});
|
||||
}
|
||||
|
||||
let span = prototype_span?;
|
||||
if span.start >= span.end {
|
||||
return None;
|
||||
}
|
||||
let source_type = prototypes.get(span.start)?.source_type?;
|
||||
let class_id = match source_type {
|
||||
CONTROL_CLASS_SOURCE_BTLU => 4,
|
||||
CONTROL_CLASS_SOURCE_BULL => 9,
|
||||
CONTROL_CLASS_SOURCE_WPNS => 2,
|
||||
CONTROL_CLASS_SOURCE_STAT => 10,
|
||||
_ => 0,
|
||||
};
|
||||
Some(MissionControlClassProjection {
|
||||
class_id,
|
||||
source: MissionControlClassSource::RootPrototypeFourCc { source_type },
|
||||
})
|
||||
}
|
||||
|
||||
fn active_unit_component_order(
|
||||
records: &[UnitComponentRecord],
|
||||
tree: &[UnitComponentTreeNode],
|
||||
@@ -6293,6 +6357,29 @@ mod tests {
|
||||
(draft, assets, prototypes)
|
||||
}
|
||||
|
||||
fn synthetic_control_class_fixture(
|
||||
source_types: &[Option<u32>],
|
||||
) -> (
|
||||
MissionObjectDraft,
|
||||
Vec<fparkan_prototype::EffectivePrototype>,
|
||||
) {
|
||||
let component_count = source_types.len();
|
||||
let mut child_counts = vec![0; component_count];
|
||||
if let Some(root_child_count) = child_counts.first_mut() {
|
||||
*root_child_count =
|
||||
u32::try_from(component_count.saturating_sub(1)).expect("child count fits u32");
|
||||
}
|
||||
let (draft, _, prototypes) = synthetic_control_fixture(
|
||||
source_types,
|
||||
&vec![1; component_count],
|
||||
&child_counts,
|
||||
&vec![0; component_count],
|
||||
&vec![1; component_count],
|
||||
vec![None; component_count],
|
||||
);
|
||||
(draft, prototypes)
|
||||
}
|
||||
|
||||
fn synthetic_control_table(
|
||||
source_types: &[Option<u32>],
|
||||
kinds: &[u32],
|
||||
@@ -6419,6 +6506,7 @@ mod tests {
|
||||
slot: 7,
|
||||
},
|
||||
status,
|
||||
native_control_class: None,
|
||||
owner_profile: None,
|
||||
node_provider_maximum_amount_bits: initial_node_provider_maximum_amount(
|
||||
&node_provider_status,
|
||||
@@ -9021,6 +9109,82 @@ mod tests {
|
||||
assert_eq!(native_into_append_key(0x1234), 0x1234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_control_class_highbit_selects_class_three_before_root_fourcc() {
|
||||
let btlu = u32::from_le_bytes(*b"BTLU");
|
||||
let unknown = u32::from_le_bytes(*b"ZZZZ");
|
||||
for root_source_type in [Some(btlu), Some(unknown), None] {
|
||||
let (mut draft, prototypes) =
|
||||
synthetic_control_class_fixture(&[root_source_type, Some(btlu)]);
|
||||
draft.unit_dat_header_flags = Some(0x8000_0004);
|
||||
let span = 0..prototypes.len();
|
||||
assert_eq!(
|
||||
native_control_class_for_root(
|
||||
draft.unit_dat_header_flags,
|
||||
Some(&span),
|
||||
&prototypes
|
||||
),
|
||||
Some(MissionControlClassProjection {
|
||||
class_id: 3,
|
||||
source: MissionControlClassSource::UnitDatHighBit {
|
||||
header_flags: 0x8000_0004,
|
||||
},
|
||||
}),
|
||||
"Unit DAT high-bit route overrides root source type {root_source_type:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_control_class_clear_route_uses_root_registry_fourcc() {
|
||||
let source_types = [
|
||||
u32::from_le_bytes(*b"BTLU"),
|
||||
u32::from_le_bytes(*b"BULL"),
|
||||
u32::from_le_bytes(*b"WPNS"),
|
||||
u32::from_le_bytes(*b"STAT"),
|
||||
u32::from_le_bytes(*b"ZZZZ"),
|
||||
];
|
||||
let expected_classes = [4, 9, 2, 10, 0];
|
||||
let (draft, prototypes) = synthetic_control_class_fixture(&source_types.map(Some));
|
||||
|
||||
for (index, (source_type, class_id)) in
|
||||
source_types.into_iter().zip(expected_classes).enumerate()
|
||||
{
|
||||
let span = index..index + 1;
|
||||
assert_eq!(
|
||||
native_control_class_for_root(
|
||||
draft.unit_dat_header_flags,
|
||||
Some(&span),
|
||||
&prototypes
|
||||
),
|
||||
Some(MissionControlClassProjection {
|
||||
class_id,
|
||||
source: MissionControlClassSource::RootPrototypeFourCc { source_type },
|
||||
}),
|
||||
"source type {source_type:08x} comes from the span's root entry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_control_class_requires_root_metadata_and_never_uses_child_fourcc() {
|
||||
let btlu = u32::from_le_bytes(*b"BTLU");
|
||||
let (mut draft, prototypes) = synthetic_control_class_fixture(&[None, Some(btlu)]);
|
||||
let span = 0..prototypes.len();
|
||||
|
||||
assert_eq!(
|
||||
native_control_class_for_root(draft.unit_dat_header_flags, Some(&span), &prototypes),
|
||||
None,
|
||||
"a child entry cannot substitute for missing root registry metadata"
|
||||
);
|
||||
draft.unit_dat_header_flags = None;
|
||||
assert_eq!(
|
||||
native_control_class_for_root(draft.unit_dat_header_flags, Some(&span), &prototypes),
|
||||
None,
|
||||
"missing Unit DAT class word must remain explicit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exto_key_mapping_obeys_native_component_and_mount_sentinels() {
|
||||
assert_eq!(native_ex_to_append_key(2, 0, None, Some(20)), Some(2));
|
||||
@@ -9896,6 +10060,15 @@ mod tests {
|
||||
let mine_control_before = loaded_mission_control_device_tables(&mine_engine)
|
||||
.expect("licensed mine Control table")
|
||||
.to_vec();
|
||||
assert_eq!(
|
||||
mine_control_before[0].native_control_class,
|
||||
Some(MissionControlClassProjection {
|
||||
class_id: 3,
|
||||
source: MissionControlClassSource::UnitDatHighBit {
|
||||
header_flags: 0x8000_0004,
|
||||
},
|
||||
})
|
||||
);
|
||||
let mine_prepared_controls = &mine_engine
|
||||
.loaded
|
||||
.as_ref()
|
||||
@@ -11820,6 +11993,7 @@ mod tests {
|
||||
original_id: Some(OriginalObjectId(original_id)),
|
||||
handle,
|
||||
status: MissionControlDeviceTableStatus::Available(vec![device]),
|
||||
native_control_class: None,
|
||||
owner_profile: None,
|
||||
node_provider_status: MissionControlNodeProviderTableStatus::Available(Vec::new()),
|
||||
node_provider_maximum_amount_bits: Some(0),
|
||||
@@ -11929,6 +12103,7 @@ mod tests {
|
||||
),
|
||||
ore_device,
|
||||
]),
|
||||
native_control_class: None,
|
||||
owner_profile: None,
|
||||
node_provider_status: MissionControlNodeProviderTableStatus::Available(vec![
|
||||
synthetic_node_provider_row(0, 0, 1.0_f32.to_bits()),
|
||||
@@ -11984,6 +12159,7 @@ mod tests {
|
||||
original_id: Some(object.original_id),
|
||||
handle: object.handle,
|
||||
status: MissionControlDeviceTableStatus::Available(Vec::new()),
|
||||
native_control_class: None,
|
||||
owner_profile: None,
|
||||
node_provider_status: MissionControlNodeProviderTableStatus::Available(Vec::new()),
|
||||
node_provider_maximum_amount_bits: Some(0),
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Text;
|
||||
// slots, and bounded vector backing are substituted; tested module code remains installed.
|
||||
internal static class BehaviorDistributorTickOracle {
|
||||
private const uint LoadWithAlteredSearchPath = 0x00000008;
|
||||
private const uint DontResolveDllReferences = 0x00000001;
|
||||
private const uint MemCommitReserve = 0x00003000;
|
||||
private const uint PageExecuteReadWrite = 0x40;
|
||||
private const uint MemRelease = 0x00008000;
|
||||
@@ -18,7 +19,9 @@ internal static class BehaviorDistributorTickOracle {
|
||||
private const string ExpectedControlSha256 = "bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e";
|
||||
private const string ExpectedArealMapSha256 = "a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea";
|
||||
private const string ExpectedAniMeshSha256 = "65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e";
|
||||
private const string ExpectedTerrainSha256 = "af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead";
|
||||
private const int TickRva = 0x19e80;
|
||||
private const int TerrainClassGetterRva = 0x33ae0;
|
||||
private const int DistributorScheduleRva = 0x1adb0;
|
||||
private const int AddObjectRva = 0x1aac0;
|
||||
private const int NormalizeRva = 0x1ac90;
|
||||
@@ -96,6 +99,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void X87ControlWordReader();
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void X87ControlWordWriter();
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void TickDelegate(IntPtr self, float deltaSeconds);
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate uint TerrainClassGetter(IntPtr self);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeBehaviorSchedule(IntPtr self);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr QueueGetter();
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint QueueClockGetter(IntPtr self);
|
||||
@@ -579,8 +583,31 @@ internal static class BehaviorDistributorTickOracle {
|
||||
return sequence[Math.Min(index, sequence.Length - 1)];
|
||||
}
|
||||
|
||||
private static int ProbeTerrainClass() {
|
||||
string terrainPath = Environment.GetEnvironmentVariable("FPARKAN_ORIGINAL_TERRAIN_DLL");
|
||||
if (String.IsNullOrEmpty(terrainPath)) terrainPath = @"C:\GOG Games\Parkan - Iron Strategy\Terrain.dll";
|
||||
if (!File.Exists(terrainPath)) { Console.Error.WriteLine("Set FPARKAN_ORIGINAL_TERRAIN_DLL to the installed Terrain.dll."); return 3; }
|
||||
string hash = Hash(terrainPath);
|
||||
if (hash != ExpectedTerrainSha256) { Console.Error.WriteLine("Unexpected Terrain.dll SHA-256: " + hash); return 4; }
|
||||
IntPtr module = LoadLibraryEx(terrainPath, IntPtr.Zero, DontResolveDllReferences);
|
||||
if (module == IntPtr.Zero) { Console.Error.WriteLine("LoadLibraryEx(Terrain.dll) failed: " + Marshal.GetLastWin32Error()); return 5; }
|
||||
try {
|
||||
IntPtr getterAddress = IntPtr.Add(module, TerrainClassGetterRva);
|
||||
byte[] getterBytes = new byte[6]; Marshal.Copy(getterAddress, getterBytes, 0, getterBytes.Length);
|
||||
string code = BitConverter.ToString(getterBytes).Replace("-", "");
|
||||
if (code != "B803000000C3") { Console.Error.WriteLine("Unexpected Terrain getter bytes: " + code); return 6; }
|
||||
TerrainClassGetter getter = (TerrainClassGetter)Marshal.GetDelegateForFunctionPointer(getterAddress, typeof(TerrainClassGetter));
|
||||
uint classId = getter(IntPtr.Zero);
|
||||
if (classId != 3) { Console.Error.WriteLine("Unexpected Terrain class ID: " + classId); return 7; }
|
||||
Console.WriteLine("PASS sha256=" + hash + " rva=0x33AE0 bytes=" + code + " this=null class=3");
|
||||
return 0;
|
||||
} finally { FreeLibrary(module); }
|
||||
}
|
||||
|
||||
private static int Main() {
|
||||
if (IntPtr.Size != 4) { Console.Error.WriteLine("Build with /platform:x86."); return 2; }
|
||||
int terrainClass = ProbeTerrainClass();
|
||||
if (terrainClass != 0) return terrainClass;
|
||||
return RunWithIsolatedWorkingDirectory();
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +202,8 @@ Unit DAT создаёт тот же Behavior-компонент во всех ho
|
||||
prototype roots без Unit DAT и отложенные spawn после исходной загрузки вне
|
||||
этой модели. Путь в `ArealMap.dll` (SHA-256
|
||||
`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`) к
|
||||
`CreateObject` по RVA `0x14fd0`/`0x15290` показывает выбор World3D class 3 или 4
|
||||
по старшему биту. Для ветки GOG `AniMesh.dll` (SHA-256
|
||||
`CreateObject` по RVA `0x14fd0`/`0x15290` показывает выбор World3D factory
|
||||
selector `3` или `4` по старшему биту. Для ветки GOG `AniMesh.dll` (SHA-256
|
||||
`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) RVA
|
||||
`0x2ff0` показывает одну ветку создания: после QueryInterface для IID `0x204`
|
||||
выполняется отдельный импорт `GetQueue()`, а на возвращённом объекте вызывается
|
||||
@@ -247,6 +247,24 @@ CTLD и связанные resources структурно читаются, coun
|
||||
известны. Не названы все секции, shape types, coefficients и точный contact
|
||||
solver. То же относится к редким MSH auxiliary streams и части CTPT/NDPR flags.
|
||||
|
||||
### Класс Control при загрузке Unit DAT
|
||||
|
||||
Для двух подтверждённых полных Unit-DAT путей Rust теперь сохраняет класс,
|
||||
который передаётся конструктору Control, вместе с ветвью-источником. Лицензированный
|
||||
`min_l_n1.dat` проходит high-bit route и даёт класс `3`; synthetic-тесты покрывают
|
||||
оба маршрута, известные FourCC и отсутствующие root metadata. Отдельное
|
||||
исполнение проверило только чистый getter `Terrain.dll+0x33ae0` (SHA-256 и точный
|
||||
RVA приведены в технической главе). Остальная цепочка через ArealMap, World3D,
|
||||
Terrain/AniMesh и создание controller восстановлена по статическим инструкциям,
|
||||
таблицам и конфигурации. Она не означает исполнения оригинального полного
|
||||
loader-а, Control activation, движения или периодического tick.
|
||||
|
||||
Не моделируются произвольный ненулевой host с неудачным QueryInterface, preview
|
||||
без полного Unit DAT, отложенный spawn и другие host/factory routes. В частности,
|
||||
generic failed-query путь native оставляет class `0` и не читает FourCC; это не
|
||||
эквивалентно canonical no-host lookup корневой registry entry. Остаточные вопросы
|
||||
об активации и игровом поведении описаны в технической главе.
|
||||
|
||||
Одна цепочка Unit DAT → AniMesh descriptor → Control теперь восстановлена
|
||||
статически в [главе о Behavior и Control](../tomes/06-behavior.md): raw
|
||||
`parent_or_link` копируется в descriptor. В non-`EXTO` registry path parent ID
|
||||
|
||||
@@ -977,6 +977,61 @@ Ignored Rust-тест также проходит полный неотфиль
|
||||
профилю. Это подтверждает декодирование и CPU-проекцию, но не исполнение native
|
||||
полного загрузчика.
|
||||
|
||||
#### Входной класс Control при загрузке Unit DAT
|
||||
|
||||
Этот класс задаётся до разбора CTLD и не выводится из device tag или MSH-
|
||||
геометрии. В GOG `ArealMap.dll` (SHA-256
|
||||
`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`)
|
||||
`FUN_10014fd0` (RVA `0x14fd0`) вызывает полный путь `FUN_10015290` (RVA
|
||||
`0x15290`), который читает class word разобранного корня Unit DAT по `+0x78`;
|
||||
это смещение поля в структуре, а не raw file offset. Установленный старший бит
|
||||
выбирает `World3D.CreateObject` selector `3`, снятый — selector `4`. В GOG
|
||||
`World3D.dll` (SHA-256
|
||||
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`) RVA
|
||||
`0x7cb0` ведёт к внутренней реализации `0x7a50`, где selectors `3` и `4`
|
||||
направляются соответственно в registered CID `1` и `3`.
|
||||
`Comp.ini` (SHA-256
|
||||
`3ce88fc077fcc9659d7b87ed0caaa6885ba6d2e2307250cbc0eb75661c295df8`)
|
||||
связывает CID `1` с `Terrain.LoadBuilding`, а CID `3` — с `AniMesh.LoadAgent`.
|
||||
Selector, CID и класс Control — разные значения, хотя первый route приводит к
|
||||
совпадающему числу `3`.
|
||||
|
||||
У маршрута selector `3` внешний host-аргумент `Terrain.LoadBuilding` пуст.
|
||||
`Terrain.dll` (SHA-256
|
||||
`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`)
|
||||
создаёт `CBuilding` через RVA `0x32490`/`0x324d0`, записывает пустой внешний
|
||||
родитель в `+0x14`, затем передаёт сам `CBuilding` в `AniMesh.LoadAgent`.
|
||||
`AniMesh+0x2ff0` вызывает `QueryInterface(6)` и получает обратно primary
|
||||
указатель `CBuilding`. Его vtable slot `+0x2c` находится по Terrain RVA
|
||||
`0x65548` и указывает на `Terrain.dll+0x33ae0`. Вызванные отдельно машинные инструкции
|
||||
этого getter-а — `B8 03 00 00 00 C3`, то есть он возвращает класс `3`.
|
||||
Постоянный x86 harness проверяет SHA-256, эти байты и прямой вызов getter-а с
|
||||
`this = null`; это исполненная проверка getter-а. Связь от Unit DAT через
|
||||
wrapper и vtable восстановлена статически, а не прогнана целиком в оригинальной
|
||||
игре.
|
||||
|
||||
При selector `4` `AniMesh.LoadAgent` получает пустой host. Конструктор начинает
|
||||
Agent class field `+0x6d8` с нуля; путь `AniMesh.dll` (SHA-256
|
||||
`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) RVA
|
||||
`0x2ff0` берёт FourCC из корневой записи NRes registry. Дальше вызов
|
||||
`LoadControlSystem` по AniMesh RVA `0x32e7` передаёт значение `+0x6d8` в качестве
|
||||
последнего аргумента; `Control.dll` (SHA-256
|
||||
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) RVA
|
||||
`0x32280` выбирает для класса `9` конструктор `0x7330`, а для остальных —
|
||||
`0x31490`.
|
||||
|
||||
Правило прямого no-host пути: `BTLU → 4`, `BULL → 9`, `WPNS → 2`, `STAT → 10`;
|
||||
другой root FourCC даёт `0`. Источник — NRes registry metadata именно корневой
|
||||
записи (`EffectivePrototype.source_type`), не первый DWORD CPT payload и не
|
||||
запись ребёнка. Если заголовок отсутствует, класс Rust оставляет неопределённым
|
||||
(`None`); при установленном старшем бите выбирается `3`, даже если FourCC нет.
|
||||
Только в прямом no-host пути отсутствие root FourCC даёт `None`. У произвольного
|
||||
ненулевого host, который не возвращает
|
||||
нужный интерфейс, native field также остаётся `0`, но FourCC в этой ветви не
|
||||
читается; этот generic route не считается одним из двух полных Unit-DAT путей.
|
||||
Runtime сохраняет класс и источник выбора, не запуская Control clock или
|
||||
конструкторы gameplay state.
|
||||
|
||||
#### Активация и начальные часы
|
||||
|
||||
Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490`
|
||||
|
||||
@@ -662,6 +662,20 @@ parent ID через Control IID `0x202`, virtual slot `+0x44`, только е
|
||||
исходный ключ `1` становится `22`. Этот ключ выбирает модельный узел, а не
|
||||
строку Control. Записи корня остаются в исходном виде.
|
||||
|
||||
#### Входной класс контроллера
|
||||
|
||||
Таблица устройств и класс самого Control-контроллера выбираются разными
|
||||
частями загрузки. У полного объекта из Unit DAT старший бит флагов корня решает,
|
||||
каким путём создать host. В ветви с этим битом объект проходит через Terrain;
|
||||
эта оболочка передаёт AniMesh native-класс Control `3`. Если бит не установлен,
|
||||
класс выбирает FourCC записи корневого ресурса: `BTLU` даёт `4`, `BULL` — `9`,
|
||||
`WPNS` — `2`, `STAT` — `10`, а неизвестный FourCC оставляет класс `0`.
|
||||
|
||||
Например, `min_l_n1.dat` хранит флаги `0x80000004`, поэтому его проекция
|
||||
получает класс `3`. Это аргумент конструктора Control, а не номер строки
|
||||
устройства и не само по себе включение движения или обновлений. Техническая
|
||||
цепочка и границы проверки описаны в [приложении](../appendices/script-vm.md#входной-класс-control-при-загрузке-unit-dat).
|
||||
|
||||
### Как Control добавляет и переопределяет устройства
|
||||
|
||||
В `Control.dll` (SHA-256
|
||||
|
||||
Reference in new issue
Block a user