feat(runtime): initialize mission ore properties
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m9s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m9s
This commit is contained in:
5 files changed
+379
-4
No files matched your search
@@ -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),
|
||||
@@ -2619,6 +2724,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 {
|
||||
@@ -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()));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 и проверки режима/состояния. Обработчик выбора миссии
|
||||
|
||||
@@ -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#начальные-свойства-запасов-руды).
|
||||
@@ -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 работает со списком зарегистрированных объектов. За один
|
||||
|
||||
Reference in new issue
Block a user