diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-22 10:45:23 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-22 10:45:23 +0300 |
| commit | dcc64598916dba92de74c6e27a2359fa08b72e84 (patch) | |
| tree | 51b9d767e1650ebfff3d0fa7ddb378e37f5bcd6b | |
| parent | 374db10daf5db88a52596edebc6cf5f2d560065f (diff) | |
| download | fparkan-dcc64598916dba92de74c6e27a2359fa08b72e84.tar.xz fparkan-dcc64598916dba92de74c6e27a2359fa08b72e84.zip | |
feat(runtime): initialize mission ore propertiestrunk
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 157 | ||||
| -rw-r--r-- | crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs | 128 | ||||
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 36 | ||||
| -rw-r--r-- | docs/reference/tma.md | 12 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 50 |
5 files changed, 379 insertions, 4 deletions
diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 3964f78..baba3b3 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -134,6 +134,21 @@ pub struct BehaviorRegisteredObject { pub native_tail: [u32; 5], } +/// Native `CurrentOre`/`MaximumOre` state for one full Unit DAT mission object. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BehaviorOreState { + /// Stable identity from the source TMA object order. + pub original_id: OriginalObjectId, + /// Corresponding live Rust world object. + pub handle: ObjectHandle, + /// Class word recovered from the root Unit DAT header. + pub class_flags: u32, + /// Raw bits of the `CurrentOre` float property. + pub current_ore_bits: u32, + /// Raw bits of the read-only `MaximumOre` float property. + pub maximum_ore_bits: u32, +} + /// Resource distributor state associated with one TMA clan's constructed AI. #[derive(Clone, Debug, PartialEq)] pub struct BehaviorDistributorState { @@ -464,6 +479,7 @@ struct LoadedMissionState { asset_plan: MissionAssetPlan, object_drafts: Vec<MissionObjectDraft>, behavior_distributors: Vec<BehaviorDistributorState>, + behavior_ore_states: Vec<BehaviorOreState>, script_bundles: Vec<MissionScriptBundle>, script_varset: Option<MissionScriptVarSet>, script_init_states: Vec<MissionScriptInitState>, @@ -1045,6 +1061,24 @@ fn load_mission_with_options( Vec::new() }; + let behavior_ore_states = if options.asset_scope == MissionAssetScope::Full { + // Property callbacks also run for Unit DAT objects that do not join a distributor. + object_drafts + .iter() + .zip(&handles) + .filter_map(|(draft, handle)| { + Some(initialize_behavior_ore_state( + draft.original_id?, + *handle, + draft.unit_dat_header_flags?, + &draft.properties, + )) + }) + .collect() + } else { + Vec::new() + }; + let mut next_behavior_rng_state = engine.behavior_rng_state; if options.asset_scope == MissionAssetScope::Full { let queue_time_ms = queue_time_ms.expect("full mission loading requires a queue time"); @@ -1120,6 +1154,7 @@ fn load_mission_with_options( asset_plan: mission_asset_plan, object_drafts, behavior_distributors, + behavior_ore_states, script_bundles: loaded_scripts.bundles, script_varset: loaded_scripts.varset, script_init_states: loaded_scripts.init_states, @@ -1402,6 +1437,15 @@ pub fn loaded_mission_behavior_distributors( .map(|state| state.behavior_distributors.as_slice()) } +/// Returns native ore properties attached to full Unit DAT mission objects. +#[must_use] +pub fn loaded_mission_behavior_ore_states(engine: &Engine) -> Option<&[BehaviorOreState]> { + engine + .loaded + .as_ref() + .map(|state| state.behavior_ore_states.as_slice()) +} + fn behavior_clan_for_selector( selector: u32, clan_count: usize, @@ -1418,6 +1462,38 @@ fn has_behavior_registration_key(class_flags: u32) -> bool { class_flags & BEHAVIOR_REGISTRATION_KEY_FLAG != 0 } +fn native_behavior_ore_defaults(class_flags: u32) -> (u32, u32) { + match class_flags { + 0x8000_0004 => (0, 0x43fa_0000), + 0x8000_0008 => (0, 0x457a_0000), + 0x0100_2000 => (0, 0x44fa_0000), + 0x0100_4000 => (0x44fa_0000, 0x44fa_0000), + _ => (0, 0), + } +} + +fn initialize_behavior_ore_state( + original_id: OriginalObjectId, + handle: ObjectHandle, + class_flags: u32, + properties: &[MissionObjectProperty], +) -> BehaviorOreState { + let (mut current_ore_bits, maximum_ore_bits) = native_behavior_ore_defaults(class_flags); + if let Some(property) = properties + .iter() + .find(|property| property.name_raw.as_slice() == b"CurrentOre") + { + current_ore_bits = property.raw_value[1]; + } + BehaviorOreState { + original_id, + handle, + class_flags, + current_ore_bits, + maximum_ore_bits, + } +} + fn add_behavior_registered_object( distributor: &mut BehaviorDistributorState, mut object: BehaviorRegisteredObject, @@ -2470,6 +2546,26 @@ mod tests { .collect::<Vec<_>>(), vec![(0, true), (1, true)] ); + let ore_states = loaded_mission_behavior_ore_states(&engine).expect("Behavior ore state"); + let unit_dat_drafts = drafts + .iter() + .filter(|draft| draft.unit_dat_header_flags.is_some()) + .collect::<Vec<_>>(); + assert_eq!(ore_states.len(), unit_dat_drafts.len()); + assert_eq!(ore_states.len(), 8); + for state in ore_states { + let draft = unit_dat_drafts + .iter() + .find(|draft| draft.original_id == Some(state.original_id)) + .expect("ore state has a Unit DAT source"); + assert_eq!( + state.handle, + handle_by_original_id(&engine.world, state.original_id).unwrap() + ); + assert_eq!(state.class_flags, draft.unit_dat_header_flags.unwrap()); + assert_eq!(state.current_ore_bits, 0); + assert_eq!(state.maximum_ore_bits, 0); + } let registered = distributors .iter() .flat_map(|distributor| { @@ -2538,6 +2634,15 @@ mod tests { ] ); assert_eq!(registered.len(), 4); + assert!(ore_states.iter().any(|state| { + state.class_flags == 0x0100_8000 + && !distributors.iter().any(|distributor| { + distributor + .registered_objects + .iter() + .any(|object| object.handle == state.handle) + }) + })); for (_, object) in registered { assert_eq!( handle_by_original_id(&engine.world, object.original_id), @@ -2620,6 +2725,55 @@ mod tests { } #[test] + fn behavior_ore_initialization_uses_first_exact_current_value_and_read_only_maximum() { + let handle = ObjectHandle { + generation: 7, + slot: 19, + }; + let state = initialize_behavior_ore_state( + OriginalObjectId(19), + handle, + 0x0100_2000, + &[ + MissionObjectProperty { + name_raw: b"currentore".to_vec(), + raw_value: [0x1111_1111, 0xdead_beef, 0, 0], + }, + MissionObjectProperty { + name_raw: b"CurrentOre".to_vec(), + raw_value: [0x3f80_0000, 0x7fc1_2345, 0, 0], + }, + MissionObjectProperty { + name_raw: b"CurrentOre".to_vec(), + raw_value: [0, 0x4400_0000, 0, 0], + }, + MissionObjectProperty { + name_raw: b"MaximumOre".to_vec(), + raw_value: [0, 0x4123_4567, 0, 0], + }, + ], + ); + + assert_eq!(state.original_id, OriginalObjectId(19)); + assert_eq!(state.handle, handle); + assert_eq!(state.class_flags, 0x0100_2000); + assert_eq!(state.current_ore_bits, 0x7fc1_2345); + assert_eq!(state.maximum_ore_bits, 0x44fa_0000); + } + + #[test] + fn behavior_ore_defaults_follow_native_class_dispatch() { + assert_eq!(native_behavior_ore_defaults(0x8000_0004), (0, 0x43fa_0000)); + assert_eq!(native_behavior_ore_defaults(0x8000_0008), (0, 0x457a_0000)); + assert_eq!(native_behavior_ore_defaults(0x0100_2000), (0, 0x44fa_0000)); + assert_eq!( + native_behavior_ore_defaults(0x0100_4000), + (0x44fa_0000, 0x44fa_0000) + ); + assert_eq!(native_behavior_ore_defaults(0x0100_8000), (0, 0)); + } + + #[test] fn behavior_distributor_add_uses_handle_and_preserves_duplicate_tail() { let mut distributor = BehaviorDistributorState { clan_index: 0, @@ -2767,6 +2921,9 @@ mod tests { assert!(loaded_mission_behavior_distributors(&engine) .expect("preview Behavior distributors") .is_empty()); + assert!(loaded_mission_behavior_ore_states(&engine) + .expect("preview Behavior ore state") + .is_empty()); assert!(bundles .iter() .all(|bundle| bundle.behavior_warm_start.is_none())); diff --git a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs index 5c7c4f2..8532d1a 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -5,9 +5,10 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; -// Calls the original x86 Behavior.dll distributor Add, normalize, and tick paths -// against synthetic storage. Only GetSystemArealMap and the bounded vector backing -// are substituted; Behavior's corresponding instructions remain the installed DLL. +// Calls the original x86 Behavior.dll distributor Add, normalize, indexed resource +// property, and tick paths against synthetic storage. Only GetSystemArealMap and the +// bounded vector backing are substituted; Behavior's corresponding instructions +// remain the installed DLL. internal static class BehaviorDistributorTickOracle { private const uint LoadWithAlteredSearchPath = 0x00000008; private const uint MemCommitReserve = 0x00003000; @@ -18,6 +19,9 @@ internal static class BehaviorDistributorTickOracle { private const int TickRva = 0x19e80; private const int AddObjectRva = 0x1aac0; private const int NormalizeRva = 0x1ac90; + private const int IndexedPropertySetRva = 0x0b470; + private const int IndexedPropertyGetRva = 0x0b5b0; + private const int OreInitializationRva = 0x08790; private const int GetSystemArealMapIatRva = 0x59008; private const uint ResourceProperty = 0x02000100; private static Fixture active; @@ -45,6 +49,9 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void ObjectSetProperty(IntPtr self, uint property, float value, IntPtr result); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceGetProperty(IntPtr self, uint selector, IntPtr result); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int InterfaceSetProperty(IntPtr self, uint selector, IntPtr value); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void IndexedPropertySet(IntPtr self, uint index, uint valueBits); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int IndexedPropertyGet(IntPtr self, uint index, IntPtr resultBits); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeOreInitialization(IntPtr self); private static readonly AreaMapGetter AreaMapGetterCallback = GetAreaMap; private static readonly VectorInsert VectorInsertCallback = InsertVector; @@ -158,7 +165,7 @@ internal static class BehaviorDistributorTickOracle { Marshal.WriteInt32(entry, 0x04, BitConverter.ToInt32(BitConverter.GetBytes(1.0f), 0)); } } - private IntPtr Allocate(int size) { + public IntPtr Allocate(int size) { IntPtr value = Marshal.AllocHGlobal(size); Zero(value, size); Allocations.Add(value); @@ -365,6 +372,8 @@ internal static class BehaviorDistributorTickOracle { SetX87ControlWord(savedControlWord); Console.WriteLine("x87-control-word-restored=0x" + CurrentX87ControlWord().ToString("x4")); } + RunNativeIndexedPropertyCases(module); + RunNativeOreInitializationCases(module); TickDelegate tick = (TickDelegate)Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, TickRva), typeof(TickDelegate)); using (Fixture empty = NewFixture()) { empty.MakeDistributor(); active = empty; tick(empty.Distributor, 0.0f); @@ -387,6 +396,117 @@ internal static class BehaviorDistributorTickOracle { RunSkippedCase(tick); } + private static void RunNativeIndexedPropertyCases(IntPtr module) { + IndexedPropertySet set = (IndexedPropertySet)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, IndexedPropertySetRva), typeof(IndexedPropertySet)); + IndexedPropertyGet get = (IndexedPropertyGet)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, IndexedPropertyGetRva), typeof(IndexedPropertyGet)); + using (Fixture fixture = NewFixture()) { + // The handlers receive the embedded interface at owner+4. Its descriptor + // vector is at interface+0xa3c and its keyed property store at interface+0x5e4. + IntPtr owner = fixture.Allocate(0x0b20); + IntPtr iface = IntPtr.Add(owner, 4); + IntPtr descriptors = fixture.Allocate(6 * 0x18); + IntPtr propertyEntry = fixture.Allocate(0x18); + IntPtr maximumOutput = fixture.Allocate(sizeof(uint)); + IntPtr currentOutput = fixture.Allocate(sizeof(uint)); + + Marshal.WriteInt32(iface, 0xa3c, 6); + Marshal.WriteIntPtr(iface, 0xa44, descriptors); + Marshal.WriteInt32(IntPtr.Add(descriptors, 4 * 0x18), 4, 4); + Marshal.WriteInt32(IntPtr.Add(descriptors, 5 * 0x18), 4, 5); + + Marshal.WriteInt32(iface, 0x5e4, 0); // vector vtable is not used by these callbacks + Marshal.WriteInt32(iface, 0x5e8, 1); // count + Marshal.WriteInt32(iface, 0x5ec, 1); // capacity + Marshal.WriteIntPtr(iface, 0x5f0, propertyEntry); + Marshal.WriteInt32(propertyEntry, 0x00, unchecked((int)ResourceProperty)); + Marshal.WriteInt32(propertyEntry, 0x04, unchecked((int)0x11223344)); + Marshal.WriteInt32(propertyEntry, 0x08, FloatBits(2500.0f)); // auxiliary maximum + Marshal.WriteInt32(propertyEntry, 0x0c, unchecked((int)0x55667788)); + Marshal.WriteInt32(propertyEntry, 0x10, unchecked((int)0x99aabbcc)); + Marshal.WriteInt32(propertyEntry, 0x14, FloatBits(12.5f)); // primary current amount + + Expect(get(iface, 4, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f), + "descriptor 4 reads keyed auxiliary value as MaximumOre"); + Expect(get(iface, 5, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(12.5f), + "descriptor 5 reads keyed primary float as CurrentOre"); + + set(iface, 5, unchecked((uint)FloatBits(21.75f))); + Expect(Marshal.ReadInt32(propertyEntry, 0x14) == FloatBits(21.75f) && + Marshal.ReadInt32(propertyEntry, 0x08) == FloatBits(2500.0f), + "descriptor 5 updates CurrentOre and preserves MaximumOre auxiliary value"); + set(iface, 4, unchecked((uint)FloatBits(999.0f))); + Expect(Marshal.ReadInt32(propertyEntry, 0x14) == FloatBits(21.75f) && + Marshal.ReadInt32(propertyEntry, 0x08) == FloatBits(2500.0f), + "descriptor 4 is read-only through the indexed setter"); + Expect(get(iface, 4, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f) && + get(iface, 5, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(21.75f), + "indexed resource values remain independently readable after CurrentOre write"); + + IntPtr reorderedDescriptors = fixture.Allocate(2 * 0x18); + Marshal.WriteInt32(iface, 0xa3c, 2); + Marshal.WriteIntPtr(iface, 0xa44, reorderedDescriptors); + Marshal.WriteInt32(reorderedDescriptors, 4, 5); + Marshal.WriteInt32(IntPtr.Add(reorderedDescriptors, 0x18), 4, 4); + Expect(get(iface, 0, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(21.75f), + "descriptor index zero follows its CurrentOre code 5 row"); + Expect(get(iface, 1, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f), + "descriptor index one follows its MaximumOre code 4 row"); + set(iface, 0, unchecked((uint)FloatBits(31.25f))); + set(iface, 1, unchecked((uint)FloatBits(999.0f))); + Expect(Marshal.ReadInt32(propertyEntry, 0x14) == FloatBits(31.25f) && + Marshal.ReadInt32(propertyEntry, 0x08) == FloatBits(2500.0f), + "reordered descriptor rows preserve CurrentOre write and MaximumOre read-only behavior"); + Expect(get(iface, 0, currentOutput) == 1 && Marshal.ReadInt32(currentOutput) == FloatBits(31.25f) && + get(iface, 1, maximumOutput) == 1 && Marshal.ReadInt32(maximumOutput) == FloatBits(2500.0f), + "reordered descriptor rows remain readable after their indexed writes"); + Console.WriteLine("native-indexed-resource-property: maximum=" + Float(unchecked((uint)Marshal.ReadInt32(maximumOutput))) + + " current=" + Float(unchecked((uint)Marshal.ReadInt32(currentOutput))) + + "; descriptor4-write=no-op descriptor5-write=primary-only; reordered-indices=verified"); + } + } + + private static void RunNativeOreInitializationCases(IntPtr module) { + NativeOreInitialization initialize = (NativeOreInitialization)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, OreInitializationRva), typeof(NativeOreInitialization)); + uint[][] cases = new uint[][] { + new uint[] { 0x80000004, 0x00000000, 0x43fa0000 }, + new uint[] { 0x80000008, 0x00000000, 0x457a0000 }, + new uint[] { 0x01002000, 0x00000000, 0x44fa0000 }, + new uint[] { 0x01004000, 0x44fa0000, 0x44fa0000 }, + new uint[] { 0x01008000, 0x00000000, 0x00000000 } + }; + IntPtr vtable = IntPtr.Add(module, 0x59250); + foreach (uint[] test in cases) { + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x0b20); + IntPtr propertyEntry = fixture.Allocate(0x18); + Marshal.WriteIntPtr(owner, vtable); + Marshal.WriteIntPtr(owner, 0x30, owner); + Marshal.WriteInt32(owner, 0xa04, 2); + Marshal.WriteInt32(owner, 0xa64, 1); // suppress the wrapper's deferred side effect + Marshal.WriteInt32(owner, 0xafc, unchecked((int)test[0])); + Marshal.WriteInt32(owner, 0x70c, FloatBits(500.0f)); + Marshal.WriteInt32(owner, 0x710, FloatBits(4000.0f)); + Marshal.WriteInt32(owner, 0x720, FloatBits(2000.0f)); + Marshal.WriteInt32(propertyEntry, 0x00, unchecked((int)ResourceProperty)); + Marshal.WriteInt32(propertyEntry, 0x08, unchecked((int)0xbf800000)); // native add default auxiliary + Marshal.WriteInt32(propertyEntry, 0x14, FloatBits(123.0f)); // prove initialization overwrites primary + Marshal.WriteInt32(owner, 0x5ec, 1); // property count + Marshal.WriteInt32(owner, 0x5f0, 1); // property capacity + Marshal.WriteIntPtr(owner, 0x5f4, propertyEntry); + + initialize(owner); + + Expect(unchecked((uint)Marshal.ReadInt32(propertyEntry, 0x14)) == test[1] && + unchecked((uint)Marshal.ReadInt32(propertyEntry, 0x08)) == test[2], + "native ore defaults for class 0x" + test[0].ToString("x8")); + } + } + Console.WriteLine("native-behavior-ore-initialization: classes=80000004,80000008,01002000,01004000,01008000; defaults=verified"); + } + private static ushort CurrentX87ControlWord() { if (x87ControlWordReader == null || x87ControlWordCell == IntPtr.Zero) throw new InvalidOperationException("x87 control-word reader is not initialized"); diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 873497c..c1993ca 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -169,6 +169,42 @@ Behavior (`Behavior.dll`, RVA `0x3600`) вызывает `World3D!stdGetGameMode с реальными интерфейсами ArealMap, нужными `FUN_10019e80`; полный Behavior tick flow и общий dispatcher событий также остаются открытыми. +Начальные свойства `CurrentOre`/`MaximumOre` восстановлены отдельно от +регулярного обмена. В проверенных `Behavior.dll` (SHA-256 +`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) RVA +`0x8790` выбирает начальные значения по class word, `0xb110` регистрирует +дескрипторы кодов 4/5, `0xb470` пишет только код 5, а `0xb5b0` читает код 4 +как дополнительное слово и код 5 как основное. `MisLoad.dll` (SHA-256 +`c607762bb67db14ca530927c8e1347339b62b0a163ac90c708ee83142292e2bf`) RVA +`0x40b0` выбирает первое свойство TMA с точным именем `CurrentOre` и передаёт +его `raw_value[1]`; setter сохраняет биты напрямую. В AutoDemo snapshot на этих +входах было восемь объектов Unit DAT, у всех raw `CurrentOre` равен нулю; +четыре объекта класса `0x01008000` не вошли в distributor, но остались среди +инициализированных Rust-состояний. Поэтому путь нулевого значения наблюдался +наживую, но ненулевое TMA-значение в живой миссии пока не проверено. + +Синтетический x86 oracle вызвал `Behavior.dll!FUN_10008790` напрямую для классов +`0x80000004`, `0x80000008`, `0x01002000`, `0x01004000` и `0x01008000`; перед +вызовом в записи были значения `CurrentOre=123` и `MaximumOre=-1`, так что +проверено их перезаписывание. Тест выставляет `a64=1`, чтобы отключить отложенный +side effect обёртки setter-а. Это подтверждает ветви initializer-а, но не весь +путь его запуска и связи с остальными фазами объекта. Другой изолированный x86 +oracle проверяет ненулевое raw-слово `CurrentOre`, только чтение `MaximumOre` и +перестановку строк дескрипторов на синтетических записях свойств. + +Не проверен селектор 4 для внешнего config-like ресурса: в наблюдавшемся +AutoDemo его вызова нет, а установленный `Behavior.ini` не содержит +`Mine_MaxOre`, `Storage_MaxOre` или `Transport_MaxOre`. Поэтому Rust пока +воспроизводит доказанные class defaults (500, 4000 и 2000) и не заявляет +поддержку конфигурационного переопределения. Отдельно не доказано, что каждый +Unit DAT создаёт тот же Behavior-компонент во всех host factory paths. Rust +ограничивает новую запись полными TMA-объектами с Unit DAT; preview, прямые +prototype roots без Unit DAT и отложенные spawn после исходной загрузки вне +этой модели. Путь в `ArealMap.dll` (SHA-256 +`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`) к +`CreateObject` по RVA `0x14fd0`/`0x15290` показывает выбор World3D class 3 или 4 +по старшему биту, но не всю цепочку их Behavior attachment. + Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA `0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`), начальные поля IGame и проверки режима/состояния. Обработчик выбора миссии diff --git a/docs/reference/tma.md b/docs/reference/tma.md index 2a84d6d..8eb6f66 100644 --- a/docs/reference/tma.md +++ b/docs/reference/tma.md @@ -92,3 +92,15 @@ slot `+0x40` в путь выбора AI в Behavior. Поэтому Rust сох `Property` состоит из четырёх raw `u32` и имени. Typed views разрешены только для доказанных property names и consumers. + +Для свойств объекта `MisLoad.dll!FUN_10003ab0` (RVA `0x3ab0`) переставляет +дисковые слова в runtime-запись, а `FUN_100040b0` (RVA `0x40b0`) проходит её +таблицу в порядке runtime-дескрипторов. Поиск имени учитывает длину и байты с +точным регистром и использует первое совпадение. В подтверждённом пути +дисковое `raw_value[1]` становится первым DWORD аргумента setter-а, поэтому +значение передаётся как биты, без промежуточного float-преобразования. +Например, Behavior регистрирует `CurrentOre` под кодом `5`; его setter кладёт +полученные биты в основное слово свойства `0x02000100`. `MaximumOre` с кодом +`4` читает дополнительное слово той же записи, а setter этого дескриптора +ничего не меняет. Подтверждение классов и native getter/setter находится в +[главе о Behavior](../tomes/06-behavior.md#начальные-свойства-запасов-руды). diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index c00705b..16ea21a 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -255,6 +255,56 @@ Rust хранит этот небольшой начальный список с опирается на уже созданный `ObjectHandle`. Регулярный обмен ресурсами пока остаётся отдельным этапом, потому что для него нужны интерфейсы игровых объектов. +### Начальные свойства запасов руды + +Behavior хранит `CurrentOre` и `MaximumOre` в одной записи свойства с ключом +`0x02000100`: текущее количество лежит в основном float-слове, предел — в +дополнительном DWORD. В TMA это не означает, что оба значения свободно задаются +файлом. При загрузке `MisLoad.dll` ищет первое свойство с точным, чувствительным +к регистру совпадением имени и передаёт его второе raw-слово в setter. В +Behavior дескриптор `CurrentOre` записывает это слово в основное значение; +дескриптор `MaximumOre` читает дополнительное слово, но его setter ничего не +меняет. + +Оба дескриптора регистрируются как коды `4` и `5`. Это коды обработчика, а не +позиции в таблице дескрипторов: исходная DLL корректно работает и после +перестановки строк, потому что callback выбирает обработчик по коду строки. +Поэтому начальное состояние можно описать так, сохраняя биты без пересчёта +через `float`: + +```text +current = first_exact("CurrentOre").raw_value[1] ?? class_default_current +maximum = class_default_maximum +``` + +Класс задаёт значения по умолчанию в Behavior RVA `0x8790`: + +| Слово класса | `CurrentOre` | `MaximumOre` | +| --- | ---: | ---: | +| `0x80000004` | 0 | 500 | +| `0x80000008` | 0 | 4000 | +| `0x01002000` | 0 | 2000 | +| `0x01004000` | 2000 | 2000 | +| прочие слова в этой ветви | 0 | 0 | + +Вызов RVA `0x8790` на синтетической структуре подтвердил эти значения для +классов таблицы, включая `0x01008000`; он проверяет тело native initializer-а, +но не его полный жизненный цикл в игре. + +В полной загрузке Rust сохраняет эти два raw-значения рядом с исходным TMA ID +и `ObjectHandle` каждого объекта с Unit DAT; список участников распределителя +для этого не требуется. Это важно для объектов, у которых есть компонент +Behavior, но нет членства в ресурсном distributor. Preview намеренно не создаёт +начальное состояние. Лицензированная AutoDemo показывает восемь объектов с +Unit DAT, включая объекты класса `0x01008000` вне списков distributor; в этом +снимке TMA `CurrentOre` равен нулю у всех восьми. Нативный x86 oracle отдельно +проверяет ненулевое raw-слово, чтение предела, отсутствие записи в `MaximumOre` +и переставленные строки дескрипторов. + +Эта модель покрывает именно начальную загрузку полных TMA-объектов. Она не +утверждает, что уже восстановлены все пути создания Behavior, отложенное +создание объектов или внешняя подмена class defaults конфигурацией. + ### Регулярная передача ресурсов Resource Distributor работает со списком зарегистрированных объектов. За один |
