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 bd6ad0c..1371b1f 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -127,6 +127,8 @@ internal static class BehaviorDistributorTickOracle { [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountDistribute(IntPtr self, float delta, uint mode, uint flags); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlContactRateUpdate(IntPtr self, float deltaSeconds); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlEvent(IntPtr self, uint eventCode, uint payload); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlScaleQuery(IntPtr self, IntPtr result); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlTimedUpdate(IntPtr self, float inputTime); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderRowConstructor(IntPtr self, IntPtr sourceNdpRow); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlOwnerConstructor(IntPtr self, IntPtr host, uint classCode); @@ -702,6 +704,7 @@ internal static class BehaviorDistributorTickOracle { RunNativeControlTimedUpdateBaselineCases(controlModule); RunNativeControlAggregateResourceCases(controlModule); RunNativeControlResourceBudgetTag15Case(controlModule); + RunNativeControlActivationCase(controlModule); RunNativeBehaviorProfileMapCases(module); RunNativeBehaviorVarSyntheticCase(module); RunNativeBehaviorRealProfileCases(module); @@ -1816,6 +1819,7 @@ internal static class BehaviorDistributorTickOracle { IntPtr randomState = IntPtr.Add(module, 0x43228); int savedPeriod = Marshal.ReadInt32(period); int savedRandom = Marshal.ReadInt32(randomState); + Expect(savedPeriod == FloatBits(250.0f), "native Control settings constructor default resource period"); try { Marshal.WriteInt32(period, FloatBits(500.0f)); Marshal.WriteInt32(randomState, 0); // Fixed point: random offset is -period/8. @@ -1849,6 +1853,81 @@ internal static class BehaviorDistributorTickOracle { } } + private static void RunNativeControlActivationCase(IntPtr module) { + NativeControlOwnerConstructor construct = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer( + IntPtr.Add(module, 0x31490), typeof(NativeControlOwnerConstructor)); + using (Fixture fixture = NewFixture()) { + IntPtr owner = fixture.Allocate(0x670); + Expect(construct(owner, IntPtr.Zero, 4) == owner, "native derived Control constructor"); + IntPtr host = fixture.Allocate(4); + IntPtr hostVtable = fixture.Allocate(0x24); + IntPtr ani = fixture.Allocate(4); + IntPtr aniVtable = fixture.Allocate(0x84); + IntPtr scale = fixture.Allocate(4); + IntPtr scaleVtable = fixture.Allocate(0x3c); + IntPtr matrix = fixture.Allocate(64); + for (int i = 0; i < 16; i++) Marshal.WriteInt32(matrix, i * 4, FloatBits(i % 5 == 0 ? 1.0f : 0.0f)); + Marshal.WriteIntPtr(host, hostVtable); + Marshal.WriteIntPtr(ani, aniVtable); + Marshal.WriteIntPtr(scale, scaleVtable); + List trace = new List(); + bool wrongArguments = false; + InterfaceGetProperty transform = delegate(IntPtr self, uint selector, IntPtr result) { + wrongArguments |= self != host || selector != 2; + trace.Add("transform"); Marshal.WriteIntPtr(result, matrix); return 1; + }; + NativeControlScaleQuery queryScale = delegate(IntPtr self, IntPtr result) { + wrongArguments |= self != scale; trace.Add("scale"); + for (int i = 0; i < 3; i++) Marshal.WriteInt32(result, i * 4, FloatBits(1.0f)); + }; + // Explicit zero-geometry host mocks: capture parameters and supply outputs. + InterfaceGetProperty geometryState = delegate(IntPtr self, uint first, IntPtr second) { + wrongArguments |= self != ani; trace.Add("geometry-state"); + Marshal.WriteInt32(new IntPtr(unchecked((int)first)), 0); + Marshal.WriteInt32(second, 0); return 1; + }; + InterfaceGetProperty geometrySet = delegate(IntPtr self, uint node, IntPtr value) { + wrongArguments |= self != ani || node != 0; + trace.Add("geometry-set:" + Hex(unchecked((uint)value.ToInt32()))); return 1; + }; + InterfaceGetProperty geometryPoint = delegate(IntPtr self, uint node, IntPtr result) { + wrongArguments |= self != ani || node != 0; trace.Add("geometry-point"); + for (int i = 0; i < 3; i++) Marshal.WriteInt32(result, i * 4, 0); return 1; + }; + Marshal.WriteIntPtr(hostVtable, 0x20, Marshal.GetFunctionPointerForDelegate(transform)); + Marshal.WriteIntPtr(scaleVtable, 0x38, Marshal.GetFunctionPointerForDelegate(queryScale)); + Marshal.WriteIntPtr(aniVtable, 0x68, Marshal.GetFunctionPointerForDelegate(geometryState)); + Marshal.WriteIntPtr(aniVtable, 0x64, Marshal.GetFunctionPointerForDelegate(geometrySet)); + Marshal.WriteIntPtr(aniVtable, 0x80, Marshal.GetFunctionPointerForDelegate(geometryPoint)); + Marshal.WriteIntPtr(owner, 0x1c, host); + Marshal.WriteIntPtr(owner, 0x20, ani); + Marshal.WriteIntPtr(owner, 0x24, scale); + IntPtr eventVtable = Marshal.ReadIntPtr(owner, 4); + Expect(Marshal.ReadIntPtr(eventVtable, 8) == IntPtr.Add(module, 0x31ae0), "native activation event interface"); + NativeControlEvent activate = (NativeControlEvent)Marshal.GetDelegateForFunctionPointer( + WriteStdCallToFastcallThunk(0x380, Marshal.ReadIntPtr(eventVtable, 8)), typeof(NativeControlEvent)); + foreach (uint clock in new uint[] { 1000, 33554433 }) { + trace.Clear(); + activate(IntPtr.Add(owner, 4), 4, clock); + float absolute = (float)clock; + float baseline = absolute - 100.0f; + Expect(Marshal.ReadByte(owner, 0xe0) == 1 && + ReadFloat(owner, 0xe4) == baseline && ReadFloat(owner, 0xe8) == baseline && + ReadFloat(owner, 0xec) == baseline && ReadFloat(owner, 0xd8) == absolute && ReadFloat(owner, 0xdc) == absolute, + "native event 4 activates the object and initializes absolute clock baselines"); + Expect(!wrongArguments && String.Join(",", trace.ToArray()) == + "transform,scale,geometry-state,geometry-set:0x00000000,geometry-point,geometry-state,geometry-set:0x3f800000", + "native activation zero-geometry host callback order and arguments"); + Expect(Marshal.ReadInt32(owner, 0x370) == 1, "only first activation queues the initial command"); + Console.WriteLine("native-control-activation: eventRva=0x31ae0 handlerRva=0x314f0 class=4 clock=" + clock + + " absolute=" + Float(absolute) + " e4/e8/ec=" + Float(baseline) + " e0=1 pending=1 callbacks=" + String.Join(",", trace.ToArray())); + } + GC.KeepAlive(transform); GC.KeepAlive(queryScale); GC.KeepAlive(geometryState); + GC.KeepAlive(geometrySet); GC.KeepAlive(geometryPoint); GC.KeepAlive(activate); + } + GC.KeepAlive(construct); + } + private static void RunNativeArealMapComponentBuilderCase(IntPtr module) { using (ArealMapFixture fixture = new ArealMapFixture()) { fixture.OwnerVtable = fixture.Allocate(0x40); diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 3dc0e6b..58deb31 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -360,7 +360,11 @@ Control выставлен flag `0x102`, слово флагов ANI `+0x14` и часть активного `0xbcf0` исполнена на синтетическом хосте класса `10`: при периоде `500` и нулевом RNG проверены пропуск до deadline и на равенстве `437.5`, затем ресурсный `dt = 0.5` при времени `500`. Движение и очереди -исключены условиями вектора; активация `+0xe0` задана явно. Источник времени +исключены условиями вектора; активация `+0xe0` задана явно. Отдельный вектор +класса `4` исполняет событие `4` через `0x31ae0` → `0x314f0`, проверяя +активность, начальные часы `time - 100`, округление DWORD в `f32` и одну +начальную команду. Геометрические callbacks подставлены явно, таблицы +устройств и shapes пусты; полный загрузочный путь не проверен. Источник времени и доставка прослежены статически: World3D RVA `0x6bf0` вычисляет часы из `timeGetTime()` с игровым смещением и поправкой на паузу, затем выполняет первый обход `6/1`, шаг мировой физики и @@ -368,7 +372,8 @@ World3D RVA `0x6bf0` вычисляет часы из `timeGetTime()` с игр обработчик Control RVA `0x7830` численно преобразует unsigned DWORD в `f32` перед primary slot `+0x58`. Фабрика выбирает RVA `0xcbb0` для класса `9` и RVA `0xbcf0` для остальных. Это подтверждает статическую цепочку вызовов, но не -её исполнение на миссии: загрузочная активация, остальные ветви `0xbcf0` и +её исполнение на миссии: связывание интерфейсов, активация устройств и shapes, +остальные ветви `0xbcf0` и изменение provider-снимков Rust остаются открытыми. Источник контактного коэффициента прослежен статически, но не исполнен. Подробные векторы и статические границы приведены в [техническом подразделе о provider-состоянии Control](script-vm.md#изменение-provider-состояния-control). diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index d031158..69ea54f 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -573,7 +573,9 @@ Tick-векторы проверяют нулевой шаг, обе ветви равенство спросу, разные веса и достаточную/недостаточную мощность. Тот же harness проверяет в оригинальном Control пропорциональное и последовательное распределение provider amount, коэффициент класса `4` с интервалом `2`, а также -неактивную временную ветвь с входами `0.5`, `1` и `2`. Вызовы Control используют +неактивную временную ветвь с входами `0.5`, `1` и `2`. Отдельные векторы +проверяют активацию класса `4` и бюджетную часть активной ветви класса `10`. +Вызовы Control используют синтетические строки и хостовые callbacks; ArealMap тоже получает синтетические объекты. Harness сверяет SHA-256 всех трёх DLL; ArealMap загружается из изолированной временной рабочей папки, которую процесс затем удаляет. Эти тесты @@ -676,6 +678,36 @@ provider getter и изменение количества не подменен категории и выполнение миссии этим не проверены. Rust пока не переносит этот изменяемый бюджетный цикл. +#### Активация и начальные часы + +Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490` +и обработчик интерфейса RVA `0x31ae0` → `0x314f0` с событием `4` и ненулевым +DWORD времени. Он выставляет `+0xe0` по условию `class != 0`, копирует матрицу +хоста в три transform-поля и численно преобразует беззнаковое время в `f32`. +Поля `+0xe4`, `+0xe8` и `+0xec` получают `time - 100`, а командные часы +`+0xd8` и `+0xdc` — `time`. Это разные базовые значения: + +| Вход DWORD | Абсолютное `f32` | `+0xe4`, `+0xe8`, `+0xec` | +|---|---|---| +| `1000` | `1000` | `900` | +| `33554433` | `33554432` | `33554332` | + +Первый вызов при старом `+0xe4 == 0` добавляет начальную команду в очередь; +повторный вызов в этом векторе сохраняет одну запись. Хост явно подставляет +identity transform, единичный масштаб и нулевую геометрию. Oracle проверяет +аргументы и порядок этих callbacks, а функции Control и изменения его часов +остаются исходными. Таблицы устройств, shapes и дорожек пусты: их активация, +полный object loader и ветвь события `4` с нулевым payload здесь не исполнены. +Нулевой payload статически выбирает отдельную фазу связывания интерфейсов, +поэтому его нельзя считать обычным ненулевым временем активации. + +Период ресурсного обновления — поле `+8` глобального объекта настроек +RVA `0x42478`. Инициализатор RVA `0x6350` и reset-метод RVA `0x6420` задают +ему `250` миллисекунд (`0x437a0000`); oracle проверяет значение после загрузки +DLL. Экспорт `InitializeSettings` (RVA `0x32260`) регистрирует объект с ключом +`0x15` в `World3D::CreateGameSettings`. Это default, а не доказательство +неизменности настройки во всех игровых режимах. + #### Активный ресурсный clock Тот же native budget-вектор проходит через `Control.dll` RVA `0xbcf0` при @@ -703,8 +735,9 @@ Rust пока не переносит этот изменяемый бюджет до `0x42206666`, долю до `0x3ecd4fdf`; mock-генератор получает `dt = 0.5` ровно один раз. После вызова dirty равен `1`, а агрегат `+0x588` ещё не пересчитан: бюджетное изменение происходит после предшествующего `0x12a40`. -Активация через загрузочные события, ненулевые состояния RNG, принудительный -цикл по `+0xd4`, движение и непустые очереди остаются отдельными границами. +Событие активации проверено отдельно выше. Ненулевые состояния RNG, +принудительный цикл по `+0xd4`, движение и непустые очереди остаются отдельными +границами. #### Доставка игровых часов в Control