fix(runtime): load robot Control device tables
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
This commit is contained in:
5 files changed
+264
-34
No files matched your search
@@ -3863,7 +3863,8 @@ fn control_i32_at(payload: &[u8], offset: usize) -> Option<i32> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] = &[1, 2, 5, 8, 9, 12, 13, 15, 19, 21, 25, 26];
|
const CONTROL_TYPED_COMMON_PARSER_TAGS: &[u32] =
|
||||||
|
&[1, 2, 3, 4, 5, 8, 9, 10, 12, 13, 15, 19, 21, 25, 26, 27];
|
||||||
const CONTROL_TYPED_RECORD_FIXED_BYTES: usize = 0xB0;
|
const CONTROL_TYPED_RECORD_FIXED_BYTES: usize = 0xB0;
|
||||||
const CONTROL_TYPED_RECORD_COUNT_OFFSET: usize = 0xAC;
|
const CONTROL_TYPED_RECORD_COUNT_OFFSET: usize = 0xAC;
|
||||||
const CONTROL_TYPED_RECORD_LIMIT: usize = 4096;
|
const CONTROL_TYPED_RECORD_LIMIT: usize = 4096;
|
||||||
@@ -4950,6 +4951,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_control_extended_common_tags_preserve_variable_tails_and_next_record() {
|
||||||
|
let source = ResourceKey {
|
||||||
|
archive: parse_path("controls.rlb").unwrap(),
|
||||||
|
name: resource_name(b"parts.ctl"),
|
||||||
|
type_id: None,
|
||||||
|
};
|
||||||
|
let mut payload = component_control_payload(&[(1, 0.0, 1.0, 0.25, 0)]);
|
||||||
|
payload[12..16].copy_from_slice(&4_u32.to_le_bytes());
|
||||||
|
for tag in [3_u32, 4, 10, 27] {
|
||||||
|
let start = payload.len();
|
||||||
|
append_typed_record(&mut payload, tag, 2);
|
||||||
|
payload[start + 0xac..start + 0xb0].copy_from_slice(&2_u32.to_le_bytes());
|
||||||
|
let values = [tag.to_le_bytes(), (tag + 1).to_le_bytes()].concat();
|
||||||
|
payload.splice(start + 0xb0..start + 0xb0, values);
|
||||||
|
payload.extend_from_slice(b"fx\0");
|
||||||
|
}
|
||||||
|
let control = decode_component_control_payload(&payload, source, 2, 0).unwrap();
|
||||||
|
assert_eq!(control.typed_records.len(), 4);
|
||||||
|
for (record, tag) in control.typed_records.iter().zip([3, 4, 10, 27]) {
|
||||||
|
assert_eq!(record.tag, tag);
|
||||||
|
assert_eq!(record.values, [tag, tag + 1]);
|
||||||
|
assert_eq!(record.string_bytes, b"fx");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
control.effect_section,
|
||||||
|
PreparedControlEffectSection::Parsed(Vec::new())
|
||||||
|
);
|
||||||
|
payload.pop();
|
||||||
|
assert!(
|
||||||
|
decode_component_control_payload(&payload, control.source, 2, 0).is_err(),
|
||||||
|
"recognized final record must validate its string terminator"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn component_control_malformed_recognized_typed_tail_is_rejected() {
|
fn component_control_malformed_recognized_typed_tail_is_rejected() {
|
||||||
let source = ResourceKey {
|
let source = ResourceKey {
|
||||||
|
|||||||
@@ -3154,16 +3154,17 @@ fn apply_effective_control_override(
|
|||||||
fn control_factory_uses_common_setter(factory_tag: u32) -> bool {
|
fn control_factory_uses_common_setter(factory_tag: u32) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
factory_tag,
|
factory_tag,
|
||||||
1 | 2 | 4 | 5 | 8 | 9 | 10 | 15 | 17 | 19 | 25 | 26 | 30
|
1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 15 | 17 | 19 | 21 | 25 | 26 | 27 | 30
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn native_control_factory_defaults(factory_tag: u32) -> (Option<u32>, Option<u32>) {
|
fn native_control_factory_defaults(factory_tag: u32) -> (Option<u32>, Option<u32>) {
|
||||||
match factory_tag {
|
match factory_tag {
|
||||||
1 | 4 => (Some(0x3f80_0000), Some(0x200)),
|
1 | 4 => (Some(0x3f80_0000), Some(0x200)),
|
||||||
5 | 8 | 26 => (Some(0x3f80_0000), Some(0x5)),
|
3 | 5 | 8 | 21 | 26 => (Some(0x3f80_0000), Some(0x5)),
|
||||||
10 => (Some(0x3f80_0000), Some(0x2000)),
|
10 => (Some(0x3f80_0000), Some(0x2000)),
|
||||||
17 | 19 => (Some(0x3f80_0000), Some(0x20)),
|
17 | 19 => (Some(0x3f80_0000), Some(0x20)),
|
||||||
|
2 | 27 => (Some(0x3f80_0000), Some(0)),
|
||||||
9 | 15 | 25 => (Some(0x3f80_0000), None),
|
9 | 15 | 25 => (Some(0x3f80_0000), None),
|
||||||
_ => (None, None),
|
_ => (None, None),
|
||||||
}
|
}
|
||||||
@@ -3534,7 +3535,10 @@ fn build_control_device_projection(
|
|||||||
target_index,
|
target_index,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
if !matches!(device.factory_tag, 9 | 15 | 19 | 25) {
|
if !matches!(
|
||||||
|
device.factory_tag,
|
||||||
|
2 | 3 | 4 | 5 | 8 | 9 | 10 | 15 | 19 | 21 | 25 | 27
|
||||||
|
) {
|
||||||
return Err(Unavailable::UnsupportedOverrideReceiver {
|
return Err(Unavailable::UnsupportedOverrideReceiver {
|
||||||
factory_tag: device.factory_tag,
|
factory_tag: device.factory_tag,
|
||||||
});
|
});
|
||||||
@@ -4960,6 +4964,57 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn common_factory_into_overrides_preserve_native_constructor_defaults() {
|
||||||
|
const INTO: u32 = u32::from_le_bytes(*b"INTO");
|
||||||
|
for (factory_tag, word_50) in [
|
||||||
|
(2, 0),
|
||||||
|
(3, 5),
|
||||||
|
(4, 0x200),
|
||||||
|
(5, 5),
|
||||||
|
(8, 5),
|
||||||
|
(10, 0x2000),
|
||||||
|
(21, 5),
|
||||||
|
(27, 0),
|
||||||
|
] {
|
||||||
|
let mut root = synthetic_control_record(factory_tag, 2, 0x20, None);
|
||||||
|
root.fixed_words[6] = u32::MAX;
|
||||||
|
let mut replacement = synthetic_control_record(19, 99, 0x30, None);
|
||||||
|
replacement.fixed_words[6] = u32::MAX;
|
||||||
|
let status = synthetic_control_table(
|
||||||
|
&[None, Some(INTO)],
|
||||||
|
&[1, 1],
|
||||||
|
&[1, 0],
|
||||||
|
&[0, 0],
|
||||||
|
&[1, 1],
|
||||||
|
vec![
|
||||||
|
Some(synthetic_component_control(
|
||||||
|
0,
|
||||||
|
vec![root],
|
||||||
|
PreparedControlEffectSection::Parsed(Vec::new()),
|
||||||
|
)),
|
||||||
|
Some(synthetic_component_control(
|
||||||
|
1,
|
||||||
|
vec![replacement],
|
||||||
|
PreparedControlEffectSection::Parsed(Vec::new()),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let MissionControlDeviceTableStatus::Available(devices) = status else {
|
||||||
|
panic!("{status:?}")
|
||||||
|
};
|
||||||
|
assert_eq!(devices.len(), 1);
|
||||||
|
let device = &devices[0];
|
||||||
|
assert_eq!(
|
||||||
|
(device.factory_tag, device.descriptor.tag, device.key),
|
||||||
|
(factory_tag, 19, 2)
|
||||||
|
);
|
||||||
|
assert_eq!(device.multiplier_bits, Some(1.0_f32.to_bits()));
|
||||||
|
assert_eq!(device.native_word_50_bits, Some(word_50));
|
||||||
|
assert_eq!(device.coefficient_bits.unwrap()[0], 0x30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn control_factory_multiplier_defaults_preserve_unknown_word_50() {
|
fn control_factory_multiplier_defaults_preserve_unknown_word_50() {
|
||||||
for factory_tag in [9, 15, 25] {
|
for factory_tag in [9, 15, 25] {
|
||||||
@@ -6970,6 +7025,38 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires licensed corpus"]
|
||||||
|
fn full_load_builds_control_tables_for_autodemo_robots_and_buildings() {
|
||||||
|
let mut engine = create(EngineServices::new(Arc::new(DirectoryVfs::new(
|
||||||
|
licensed_root("IS"),
|
||||||
|
))))
|
||||||
|
.unwrap();
|
||||||
|
load_mission(
|
||||||
|
&mut engine,
|
||||||
|
MissionRequest {
|
||||||
|
key: "MISSIONS/Autodemo.00/data.tma".into(),
|
||||||
|
mode: MissionMode::AutoDemo,
|
||||||
|
},
|
||||||
|
100_000,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tables = loaded_mission_control_device_tables(&engine).unwrap();
|
||||||
|
let expected_counts = [14, 12, 15, 13, 12, 12, 11, 11];
|
||||||
|
assert_eq!(tables.len(), expected_counts.len());
|
||||||
|
for (table, expected_count) in tables.iter().zip(expected_counts) {
|
||||||
|
let MissionControlDeviceTableStatus::Available(devices) = &table.status else {
|
||||||
|
panic!("object {:?}: {:?}", table.original_id, table.status);
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
devices.len(),
|
||||||
|
expected_count,
|
||||||
|
"object {:?}",
|
||||||
|
table.original_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires licensed corpus"]
|
#[ignore = "requires licensed corpus"]
|
||||||
fn headless_step_runs_resource_schedule_and_rolls_back_both_error_paths() {
|
fn headless_step_runs_resource_schedule_and_rolls_back_both_error_paths() {
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceRate(IntPtr self, float deltaSeconds);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceRate(IntPtr self, float deltaSeconds);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeControlResourceDeviceGate(IntPtr self);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeControlResourceDeviceGate(IntPtr self);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceAvailability(IntPtr self, byte flags);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceAvailability(IntPtr self, byte flags);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlDeviceFactory(IntPtr self, uint componentId, uint keyOffset, IntPtr record);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
|
||||||
@@ -2253,6 +2254,22 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
ControlResourceTag15VtableRva, ControlResourceNoOpStringPairRva, applyOverride);
|
ControlResourceTag15VtableRva, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
RunNativeControlOverrideReceiverCase(module, 25, 0xa4, ControlResourceTag25ConstructorRva,
|
RunNativeControlOverrideReceiverCase(module, 25, 0xa4, ControlResourceTag25ConstructorRva,
|
||||||
ControlResourceTag25VtableRva, ControlResourceNoOpStringPairRva, applyOverride);
|
ControlResourceTag25VtableRva, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 4, 0xdc, 0x233d0,
|
||||||
|
0x3c720, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 10, 0x94, 0x26470,
|
||||||
|
0x3ca20, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 27, 0x94, 0x218b0,
|
||||||
|
0x3c528, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 5, 0xa4, 0x20800,
|
||||||
|
0x3cdf8, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 8, 0xbc, 0x24310,
|
||||||
|
0x3c800, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 21, 0xa4, 0x20800,
|
||||||
|
0x3c448, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 2, 0x184, 0x294c0,
|
||||||
|
0x3cbd8, 0x29940, applyOverride);
|
||||||
|
RunNativeControlOverrideReceiverCase(module, 3, 0xa4, 0x20800,
|
||||||
|
0x3c448, ControlResourceNoOpStringPairRva, applyOverride);
|
||||||
GC.KeepAlive(applyOverride);
|
GC.KeepAlive(applyOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2262,17 +2279,37 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
NativeControlResourceDeviceConstructor construct = (NativeControlResourceDeviceConstructor)Marshal.GetDelegateForFunctionPointer(
|
NativeControlResourceDeviceConstructor construct = (NativeControlResourceDeviceConstructor)Marshal.GetDelegateForFunctionPointer(
|
||||||
IntPtr.Add(module, constructorRva), typeof(NativeControlResourceDeviceConstructor));
|
IntPtr.Add(module, constructorRva), typeof(NativeControlResourceDeviceConstructor));
|
||||||
IntPtr expectedVtable = IntPtr.Add(module, vtableRva);
|
IntPtr expectedVtable = IntPtr.Add(module, vtableRva);
|
||||||
Expect(Marshal.ReadIntPtr(expectedVtable, 0x1c) == IntPtr.Add(module, ControlResourceCommonSetterRva),
|
Expect(Marshal.ReadIntPtr(expectedVtable, 0x1c) == IntPtr.Add(module, tag == 2 ? 0x29650 : ControlResourceCommonSetterRva),
|
||||||
"tag-" + tag + " vtable common setter slot resolves to native RVA 0x21d50");
|
"tag-" + tag + " vtable reader resolves to its native implementation");
|
||||||
Expect(Marshal.ReadIntPtr(expectedVtable, 0x24) == IntPtr.Add(module, stringPairRva),
|
Expect(Marshal.ReadIntPtr(expectedVtable, 0x24) == IntPtr.Add(module, stringPairRva),
|
||||||
"tag-" + tag + " vtable string-pair slot matches its native callback");
|
"tag-" + tag + " vtable string-pair slot matches its native callback");
|
||||||
using (Fixture fixture = NewFixture()) {
|
using (Fixture fixture = NewFixture()) {
|
||||||
IntPtr owner = fixture.Allocate(0x5c4);
|
IntPtr owner = fixture.Allocate(0x5c8);
|
||||||
IntPtr deviceArray = fixture.Allocate(IntPtr.Size);
|
IntPtr deviceArray = fixture.Allocate(2 * IntPtr.Size);
|
||||||
IntPtr targetDevice = fixture.Allocate(objectSize);
|
IntPtr targetDevice = (tag == 3 || tag == 5) ? IntPtr.Zero : fixture.Allocate(objectSize);
|
||||||
IntPtr typedRecord = fixture.Allocate(0xb4);
|
bool variableTail = tag == 2 || tag == 3 || tag == 4 || tag == 5 || tag == 8 || tag == 10 || tag == 21 || tag == 27;
|
||||||
Expect(construct(targetDevice) == targetDevice,
|
IntPtr typedRecord = fixture.Allocate(variableTail ? 0xc0 : 0xb4);
|
||||||
"native tag-" + tag + " constructor returns its exact-size device object");
|
if (tag == 3 || tag == 5) {
|
||||||
|
// Tag 3 follows the default factory; case 5 also assigns a derived vtable.
|
||||||
|
Marshal.WriteIntPtr(owner, 0x5bc, deviceArray);
|
||||||
|
Marshal.WriteInt32(owner, 0x5c4, 2);
|
||||||
|
Marshal.WriteInt32(typedRecord, 0, tag);
|
||||||
|
Marshal.WriteInt32(typedRecord, 0x18, -1);
|
||||||
|
NativeControlDeviceFactory create = (NativeControlDeviceFactory)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, 0x2d4b0), typeof(NativeControlDeviceFactory));
|
||||||
|
Expect(create(owner, 0, 0, typedRecord) == IntPtr.Add(typedRecord, 0xb4), "native tag-" + tag + " factory cursor");
|
||||||
|
targetDevice = Marshal.ReadIntPtr(deviceArray);
|
||||||
|
GC.KeepAlive(create);
|
||||||
|
} else {
|
||||||
|
Expect(construct(targetDevice) == targetDevice,
|
||||||
|
"native tag-" + tag + " constructor returns its exact-size device object");
|
||||||
|
}
|
||||||
|
if (variableTail) {
|
||||||
|
int expectedWord = tag == 4 ? 0x200 : (tag == 3 || tag == 5 || tag == 8 || tag == 21) ? 5 : tag == 10 ? 0x2000 : 0;
|
||||||
|
Expect(Marshal.ReadInt32(targetDevice, 0x50) == expectedWord &&
|
||||||
|
Marshal.ReadInt32(targetDevice, 0x4c) == FloatBits(1.0f),
|
||||||
|
"native extended factory constructor defaults");
|
||||||
|
}
|
||||||
Marshal.WriteIntPtr(deviceArray, 0, targetDevice);
|
Marshal.WriteIntPtr(deviceArray, 0, targetDevice);
|
||||||
Marshal.WriteIntPtr(owner, 0x5bc, deviceArray);
|
Marshal.WriteIntPtr(owner, 0x5bc, deviceArray);
|
||||||
Marshal.WriteInt32(owner, 0x5c0, 1);
|
Marshal.WriteInt32(owner, 0x5c0, 1);
|
||||||
@@ -2282,7 +2319,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
Marshal.WriteInt32(targetDevice, 0x4c, unchecked((int)0x40200000)); // multiplier 2.5
|
Marshal.WriteInt32(targetDevice, 0x4c, unchecked((int)0x40200000)); // multiplier 2.5
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
Marshal.WriteInt32(targetDevice, 0x54 + i * 4, unchecked((int)(0xb2000000u + (uint)i)));
|
Marshal.WriteInt32(targetDevice, 0x54 + i * 4, unchecked((int)(0xb2000000u + (uint)i)));
|
||||||
Marshal.WriteInt32(typedRecord, 0x2c + i * 4, unchecked((int)(0xc3000000u + (uint)i)));
|
Marshal.WriteInt32(typedRecord, 0x2c + i * 4, unchecked((int)(tag == 2 && i < 2 ? (uint)FloatBits(i == 0 ? 3.5f : 1.25f) : 0xc3000000u + (uint)i)));
|
||||||
}
|
}
|
||||||
Marshal.WriteInt32(typedRecord, 0x00, 19);
|
Marshal.WriteInt32(typedRecord, 0x00, 19);
|
||||||
Marshal.WriteInt32(typedRecord, 0x04, unchecked((int)0x77777777)); // differs from the existing device key
|
Marshal.WriteInt32(typedRecord, 0x04, unchecked((int)0x77777777)); // differs from the existing device key
|
||||||
@@ -2292,10 +2329,19 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
// This synthetic receiver has no FX host and the descriptor has no map rows.
|
// This synthetic receiver has no FX host and the descriptor has no map rows.
|
||||||
Marshal.WriteIntPtr(targetDevice, 0x3c, IntPtr.Zero);
|
Marshal.WriteIntPtr(targetDevice, 0x3c, IntPtr.Zero);
|
||||||
}
|
}
|
||||||
|
if (variableTail) {
|
||||||
|
Marshal.WriteInt32(typedRecord, 0xac, 2);
|
||||||
|
Marshal.WriteInt32(typedRecord, 0xb0, 7);
|
||||||
|
Marshal.WriteInt32(typedRecord, 0xb4, 9);
|
||||||
|
Marshal.WriteInt32(typedRecord, 0xb8, 2);
|
||||||
|
Marshal.WriteByte(typedRecord, 0xbc, (byte)'f');
|
||||||
|
Marshal.WriteByte(typedRecord, 0xbd, (byte)'x');
|
||||||
|
Marshal.WriteByte(typedRecord, 0xbe, 0);
|
||||||
|
}
|
||||||
int priorWord50 = Marshal.ReadInt32(targetDevice, 0x50);
|
int priorWord50 = Marshal.ReadInt32(targetDevice, 0x50);
|
||||||
|
|
||||||
IntPtr result = applyOverride(owner, 0, typedRecord);
|
IntPtr result = applyOverride(owner, 0, typedRecord);
|
||||||
Expect(result == IntPtr.Add(typedRecord, 0xb4),
|
Expect(result == IntPtr.Add(typedRecord, variableTail ? 0xbf : 0xb4),
|
||||||
"Control indexed override completes one tag-" + tag + " typed record");
|
"Control indexed override completes one tag-" + tag + " typed record");
|
||||||
Expect(Marshal.ReadInt32(owner, 0x5c0) == 1 && Marshal.ReadIntPtr(owner, 0x5bc) == deviceArray &&
|
Expect(Marshal.ReadInt32(owner, 0x5c0) == 1 && Marshal.ReadIntPtr(owner, 0x5bc) == deviceArray &&
|
||||||
Marshal.ReadIntPtr(deviceArray, 0) == targetDevice,
|
Marshal.ReadIntPtr(deviceArray, 0) == targetDevice,
|
||||||
@@ -2311,13 +2357,36 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
Expect(Marshal.ReadIntPtr(targetDevice, 0x3c) == IntPtr.Zero,
|
Expect(Marshal.ReadIntPtr(targetDevice, 0x3c) == IntPtr.Zero,
|
||||||
"tag-9 string callback runs with no FX host on the synthetic receiver");
|
"tag-9 string callback runs with no FX host on the synthetic receiver");
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
Expect(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x54 + i * 4)) == 0xc3000000u + (uint)i,
|
Expect(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x54 + i * 4)) == (tag == 2 && i < 2 ? (uint)FloatBits(i == 0 ? 3.5f : 1.25f) : 0xc3000000u + (uint)i),
|
||||||
"Control replaces selected tag-" + tag + " device coefficient " + i);
|
"Control replaces selected tag-" + tag + " device coefficient " + i);
|
||||||
}
|
}
|
||||||
|
if (variableTail) {
|
||||||
|
IntPtr map = Marshal.ReadIntPtr(targetDevice, 0x1c);
|
||||||
|
Expect(Marshal.ReadInt32(targetDevice, 0x20) == 2 && Marshal.ReadInt32(map) == 7 &&
|
||||||
|
Marshal.ReadInt32(map, 0x14) == 9, "native common reader retains both variable map rows");
|
||||||
|
}
|
||||||
|
if (tag == 2) {
|
||||||
|
IntPtr rowState = Marshal.ReadIntPtr(targetDevice, 0x12c);
|
||||||
|
Expect(Marshal.ReadInt32(targetDevice, 0x120) == FloatBits(1.25f) &&
|
||||||
|
Marshal.ReadInt32(targetDevice, 0x124) == 4 &&
|
||||||
|
Marshal.ReadInt32(rowState) == 0 && Marshal.ReadInt32(rowState, 4) == 0,
|
||||||
|
"tag-2 wrapper caches selector 1, rounds selector 0 with x87, and zeros map state");
|
||||||
|
Marshal.WriteInt32(rowState, 1);
|
||||||
|
Marshal.WriteInt32(rowState, 4, 2);
|
||||||
|
Expect(applyOverride(owner, 0, typedRecord) == result &&
|
||||||
|
Marshal.ReadInt32(targetDevice, 0x20) == 4,
|
||||||
|
"tag-2 repeated override retains common cursor and appends map rows");
|
||||||
|
rowState = Marshal.ReadIntPtr(targetDevice, 0x12c);
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
Expect(Marshal.ReadInt32(rowState, i * 4) == 0,
|
||||||
|
"tag-2 repeated override clears state for every accumulated map row");
|
||||||
|
Console.WriteLine("native-control-tag2-wrapper: selector1=1.25 selector0=3.5 integer=4 rows=4 state=zero");
|
||||||
|
}
|
||||||
Console.WriteLine("native-control-tag" + tag + "-into-override: size=0x" + objectSize.ToString("x") +
|
Console.WriteLine("native-control-tag" + tag + "-into-override: size=0x" + objectSize.ToString("x") +
|
||||||
" index=0 count=1 key=22222222 component=12345678 multiplier=" +
|
" index=0 count=1 key=22222222 component=12345678 multiplier=" +
|
||||||
Float(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x4c))) +
|
Float(unchecked((uint)Marshal.ReadInt32(targetDevice, 0x4c))) +
|
||||||
" original-class=preserved descriptor=attached coefficients=replaced");
|
" original-class=preserved descriptor=attached coefficients=replaced cursor=" +
|
||||||
|
(variableTail ? "0xbf map=[7,9] string=fx" : "0xb4"));
|
||||||
GC.KeepAlive(construct);
|
GC.KeepAlive(construct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,9 +268,14 @@ Control для объектов с Unit DAT: корневые записи до
|
|||||||
|
|
||||||
Control различает добавление устройства и применение одной записи к уже
|
Control различает добавление устройства и применение одной записи к уже
|
||||||
выбранному устройству. Исполнение оригинальной 32-битной DLL на искусственных
|
выбранному устройству. Исполнение оригинальной 32-битной DLL на искусственных
|
||||||
данных подтверждает общую ветвь переопределения для получателей tags `9`, `15`,
|
данных подтверждает общие поля переопределения для получателей tags `2`, `3`,
|
||||||
`19` и `25`: остаются прежними слот, ключ, ID компонента, множитель и класс,
|
`4`, `5`, `8`, `9`, `10`, `15`, `19`, `21`, `25` и `27`: остаются прежними слот,
|
||||||
а коэффициенты копируются из новой записи. Искусственный tag `9`-объект создан
|
ключ, ID компонента, множитель и класс, а коэффициенты копируются из новой
|
||||||
|
записи. Обёртка tag `2` по RVA `0x29650` дополнительно пересчитывает два кэша
|
||||||
|
и обнуляет массив состояния map-строк; это выполнено на native DLL, но пока
|
||||||
|
не моделируется в базовой Rust-проекции. Её callback `0x29940` проверен только
|
||||||
|
с неизменной пустой парой ресурсов; стрельба не проверена.
|
||||||
|
Искусственный tag `9`-объект создан
|
||||||
без FX-хоста (`+0x3c == 0`), а запись не содержит строк таблицы map; поэтому
|
без FX-хоста (`+0x3c == 0`), а запись не содержит строк таблицы map; поэтому
|
||||||
работа FX-хоста здесь не проверялась. В Rust parser-е
|
работа FX-хоста здесь не проверялась. В Rust parser-е
|
||||||
`Unsupported { typed_record_index: Some(_) }` означает, что разбор остановился
|
`Unsupported { typed_record_index: Some(_) }` означает, что разбор остановился
|
||||||
@@ -299,7 +304,9 @@ Global Control getters `1/2` и setter `1` проверены в native x86 orac
|
|||||||
Control resource у non-geometric prototype и некоторых неразрешённых EXTO node
|
Control resource у non-geometric prototype и некоторых неразрешённых EXTO node
|
||||||
mappings; preview и объекты без Unit DAT root её не строят. Её соответствие
|
mappings; preview и объекты без Unit DAT root её не строят. Её соответствие
|
||||||
полной загрузке игры для всех типов компонентов и factory paths ещё не
|
полной загрузке игры для всех типов компонентов и factory paths ещё не
|
||||||
установлено. Setter `1` меняет multiplier `+0x4c` выбранных строк, но расписание
|
установлено. Полный Rust load AutoDemo проверяет доступность всех восьми таблиц
|
||||||
|
и числа строк `14, 12, 15, 13, 12, 12, 11, 11`; это не сравнение с живой
|
||||||
|
таблицей оригинала. Setter `1` меняет multiplier `+0x4c` выбранных строк, но расписание
|
||||||
и lifetime этих объектов после конструктора и в отложенной загрузке остаются
|
и lifetime этих объектов после конструктора и в отложенной загрузке остаются
|
||||||
непроверенными.
|
непроверенными.
|
||||||
|
|
||||||
|
|||||||
+45
-14
@@ -703,15 +703,43 @@ Rust full-loader теста на оригинальных ресурсах. Ве
|
|||||||
сбрасывают их. Поэтому «переопределение» здесь означает обновить часть данных
|
сбрасывают их. Поэтому «переопределение» здесь означает обновить часть данных
|
||||||
того же устройства, с ограничениями конкретного метода.
|
того же устройства, с ограничениями конкретного метода.
|
||||||
|
|
||||||
Native x86 oracle для тех же GOG DLL выполняет конструкторы и этот общий setter
|
Native x86 oracle для тех же GOG DLL проверяет переопределение получателей
|
||||||
на уже созданных получателях factory tags `9`, `15`, `19` и `25` (размеры
|
factory tags `2`, `3`, `4`, `5`, `8`, `9`, `10`, `15`, `19`, `21`, `25` и `27`.
|
||||||
объектов `0x120`, `0x98`, `0x94` и `0xa4`). У всех проверенных vtable slot
|
Тест подтверждает сохранение существующего слота, класса/vtable, ключа,
|
||||||
`+0x1c` указывает на RVA `0x21d50`; тест подтверждает, что override сохраняет
|
component ID и multiplier `2.5` при копировании новых коэффициентов. Для новых
|
||||||
существующий слот, класс/vtable, ключ, component ID и multiplier `2.5`, копируя
|
получателей конструкторы и начальное слово `+0x50` связаны с такими RVA:
|
||||||
новые коэффициенты. Slot `+0x24` у tag `9` указывает на отдельный обработчик
|
|
||||||
`0x25600`, а у остальных трёх — на no-op `0xbcd0`. Tag `9` проверяется с
|
| Factory tag | Конструктор | Vtable | Начальное `+0x50` |
|
||||||
нулевым pointer `+0x3c` и записью без map rows: FX host намеренно не подключён,
|
| --- | --- | --- | --- |
|
||||||
поэтому его поведение этим опытом не подтверждено.
|
| `2` | `0x294c0` | `0x3cbd8` | `0` |
|
||||||
|
| `3`, `21` | `0x20800` | `0x3c448` | `5` |
|
||||||
|
| `4` | `0x233d0` | `0x3c720` | `0x200` |
|
||||||
|
| `5` | `0x20800`, затем замена vtable фабрикой | `0x3cdf8` | `5` |
|
||||||
|
| `8` | `0x24310` | `0x3c800` | `5` |
|
||||||
|
| `10` | `0x26470` | `0x3ca20` | `0x2000` |
|
||||||
|
| `27` | `0x218b0` | `0x3c528` | `0` |
|
||||||
|
|
||||||
|
У всех строк таблицы multiplier начинается с `1.0`. Tags `3` и `5` проверены
|
||||||
|
через настоящую фабрику `0x2d4b0`; tag `3` попадает в её default-ветвь.
|
||||||
|
Общий reader `0x21d50` сохраняет переменный хвост: две map-строки `7` и `9`
|
||||||
|
и дополнительная строка `fx` возвращают cursor `record + 0xbf`.
|
||||||
|
Слово CTLD `+0x18 == -1` сохраняет фабричное `+0x50` и при INTO-замене.
|
||||||
|
|
||||||
|
У tag `2` slot `+0x1c` указывает на обёртку `0x29650`. Она вызывает тот же
|
||||||
|
reader, затем сохраняет selector `1` в `+0x120`, округляет selector `0` через
|
||||||
|
x87 `FISTP` в `+0x124` и заново обнуляет массив `+0x12c` по накопленному числу
|
||||||
|
map-строк. Native тест получает `1.25`, округление `3.5 → 4` и четыре нулевых
|
||||||
|
элемента после повторного override. Rust хранит общую проекцию устройства;
|
||||||
|
эти оружейные кэши и дальнейшая стрельба в неё пока не входят. Slot `+0x24`
|
||||||
|
этого класса (`0x29940`) проверен только с неизменной пустой парой ресурсов.
|
||||||
|
Tag `9` проверяется без FX-хоста (`+0x3c == 0`) и без map-строк; его FX-путь
|
||||||
|
этим опытом также не подтверждён.
|
||||||
|
|
||||||
|
Полный Rust loader на исходном `MISSIONS/Autodemo.00/data.tma` строит доступные
|
||||||
|
таблицы для всех восьми объектов: четыре робота содержат `14`, `12`, `15`, `13`
|
||||||
|
устройств, затем два завода по `12` и два генератора по `11`. Это проверка
|
||||||
|
подготовки таблиц на настоящих ресурсах, а не сравнение полного игрового Control
|
||||||
|
с запущенным оригиналом.
|
||||||
|
|
||||||
Статический разбор глобального Control interface показывает aggregate getters
|
Статический разбор глобального Control interface показывает aggregate getters
|
||||||
`1/2` (`FUN_1002b410`, RVA `0x2b410`) и setter `1` (`FUN_1002ba30`, RVA
|
`1/2` (`FUN_1002b410`, RVA `0x2b410`) и setter `1` (`FUN_1002ba30`, RVA
|
||||||
@@ -1050,13 +1078,16 @@ offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, вк
|
|||||||
`f_gener_ball` с offset 85.
|
`f_gener_ball` с offset 85.
|
||||||
|
|
||||||
Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
|
Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
|
||||||
Теги 2 и 5 направляются в общий читатель `+0x21D50`: таблица виртуальных методов
|
Для tags `2`, `3`, `4`, `5`, `8`, `10`, `21` и `27` подтверждён общий читатель
|
||||||
тега 2 (`+0x3CBD8`) вызывает его через обёртку `+0x29650`, а таблица тега 5
|
`+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через
|
||||||
(`+0x3CDF8`) указывает прямо на него в позиции `+0x1C`. Читатель потребляет
|
обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в
|
||||||
|
позиции `+0x1C`. Читатель потребляет
|
||||||
заголовок размером `0xB0` байт со счётчиком `u32` по `+0xAC`, затем `count`
|
заголовок размером `0xB0` байт со счётчиком `u32` по `+0xAC`, затем `count`
|
||||||
значений `u32`, слово длины и, при ненулевой длине, строку с завершающим NUL;
|
значений `u32`, слово длины и, при ненулевой длине, строку с завершающим NUL;
|
||||||
указатель возврата указывает на следующую запись. Поэтому для тегов 2 и 5 подтверждён общий
|
указатель возврата указывает на следующую запись. Rust-тест с tags `3`, `4`,
|
||||||
структурный формат; смысл их полей пока не назначен.
|
`10`, `27` проверяет переход между записями после непустых values и строк,
|
||||||
|
а также отказ при оборванном завершающем NUL. Это подтверждает структурный
|
||||||
|
формат, но не назначает смысл всем коэффициентам устройств.
|
||||||
|
|
||||||
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
|
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
|
||||||
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
|
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
|
||||||
|
|||||||
Reference in new issue
Block a user