The retail movement-manager family the R4 MoveToManager port left as do-not-invent seams (decomp §9f/§9g). Faithful C# ports of retail's PositionManager facade + StickyManager + ConstraintManager + the TargetManager voyeur system, with full conformance tests. NO wiring yet — purely additive, no behavior change. Wiring (retiring TS-39 sticky + AP-79 target adapter) is R5-V2/V3. New Core classes (src/AcDream.Core/Physics/Motion/): - StickyManager (0x00555400): follow-a-target steering. adjust_offset's dense x87 mush decoded via ACE (StickyRadius 0.3, StickyTime 1.0, follow speed ×5 / fallback 15) — speed-clamped signed-distance steer + bounded turn-to-face; 1 s watchdog; Ok→initialized / non-Ok→teardown. - ConstraintManager (0x00556090): the server-position rubber-band leash. 90% IsFullyConstrained jump gate + grounded linear brake taper. Structural only — acdream never ARMS it (retail arms from SmartBox::HandleReceivedPosition, which acdream lacks, with two x87 constants BN elided). IsFullyConstrained stays false = TS-35 behavior; leash-arming + the unknown constants are a deferred issue. - PositionManager facade (0x00555160): lazy Sticky/Constraint + fan-out. - TargetManager (0x0051a370) + TargettedVoyeurInfo: the peer-to-peer voyeur subscription system (0.5 s throttle, 10 s staleness, send-on-drift-past-radius, dead-reckon GetInterpolatedPosition). A faithful superset of the AP-79 adapter — SetTarget subscribes ON the target; the target's HandleTargetting pushes updates back. - IPhysicsObjHost: the CPhysicsObj back-pointer seam (position/velocity/ radius/contact/GetObjectA + target-tracking fan-out) the App wires per entity in V2/V3. MotionDeltaFrame: mutable retail-Frame delta accumulator. Supporting: - TargetInfo extended to the full retail 10-field struct (additive defaults keep the R4 4-arg call sites compiling). - MoveToMath: signed CylinderDistanceNoZ, NormalizeCheckSmall, GlobalToLocalVec. - Rename: the misnamed AcDream.Core.Physics.PositionManager (a remote anim+interp per-frame combiner, NOT the retail facade) → RemoteMotion Combiner, freeing the name and removing the ambiguity that breaks every file importing both Physics + Physics.Motion (GameWindow will in V2/V3). Tests: 42 new conformance cases (Sticky/Constraint/Position facade + TargetManager incl. the full cross-entity voyeur round-trip). Full suite 4006 green (+2 skipped), no regressions. Decomp + ACE cross-ref + port plan: docs/research/2026-07-03-r5-managers/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1256 lines
54 KiB
Markdown
1256 lines
54 KiB
Markdown
# TargetManager (voyeur-subscription system) — retail decomp extract
|
|
|
|
Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build, PDB-named).
|
|
Structs: `docs/research/named-retail/acclient.h`.
|
|
|
|
## Struct definitions (acclient.h)
|
|
|
|
### `enum TargetStatus` (acclient.h:7264)
|
|
|
|
```c
|
|
enum TargetStatus
|
|
{
|
|
Undef_TargetStatus = 0x0,
|
|
Ok_TargetStatus = 0x1,
|
|
ExitWorld_TargetStatus = 0x2,
|
|
Teleported_TargetStatus = 0x3,
|
|
Contained_TargetStatus = 0x4,
|
|
Parented_TargetStatus = 0x5,
|
|
TimedOut_TargetStatus = 0x6,
|
|
FORCE_TargetStatus_32_BIT = 0x7FFFFFFF,
|
|
};
|
|
```
|
|
|
|
### `struct TargetManager` (acclient.h:31024, id 3484)
|
|
|
|
```c
|
|
struct __cppobj TargetManager
|
|
{
|
|
CPhysicsObj *physobj;
|
|
TargetInfo *target_info;
|
|
LongNIHash<TargettedVoyeurInfo> *voyeur_table;
|
|
long double last_update_time;
|
|
};
|
|
```
|
|
|
|
### `struct TargetInfo` (acclient.h:31591)
|
|
|
|
```c
|
|
struct __cppobj TargetInfo
|
|
{
|
|
unsigned int context_id;
|
|
unsigned int object_id;
|
|
float radius;
|
|
long double quantum;
|
|
Position target_position;
|
|
Position interpolated_position;
|
|
AC1Legacy::Vector3 interpolated_heading;
|
|
AC1Legacy::Vector3 velocity;
|
|
TargetStatus status;
|
|
long double last_update_time;
|
|
};
|
|
```
|
|
|
|
### `struct TargettedVoyeurInfo` (acclient.h:52807, id 5801, 8-byte aligned)
|
|
|
|
```c
|
|
struct __cppobj __declspec(align(8)) TargettedVoyeurInfo
|
|
{
|
|
unsigned int object_id;
|
|
long double quantum;
|
|
float radius;
|
|
Position last_sent_position;
|
|
};
|
|
```
|
|
|
|
### `struct LongNIHash<TargettedVoyeurInfo>` (acclient.h:31606, id 3483)
|
|
|
|
```c
|
|
struct __cppobj LongNIHash<TargettedVoyeurInfo>
|
|
{
|
|
LongNIHashData **buckets;
|
|
int table_size;
|
|
};
|
|
```
|
|
|
|
### `struct LongNIHashIter<TargettedVoyeurInfo>` (acclient.h:57648)
|
|
|
|
```c
|
|
struct __cppobj LongNIHashIter<TargettedVoyeurInfo>
|
|
{
|
|
LongNIHash<TargettedVoyeurInfo> *hash;
|
|
/* + bucketNo, curDat, fDone per the operator++ body below */
|
|
};
|
|
```
|
|
|
|
NOTE: field layout for `LongNIHashIter` beyond `hash` isn't in the struct
|
|
dump captured, but `operator++`'s pseudo-C shows `bucketNo`, `curDat`,
|
|
`fDone` members (see below).
|
|
|
|
---
|
|
|
|
## Functions
|
|
|
|
### `TargetManager::TargetManager` (ctor) — 0051a370
|
|
|
|
```c
|
|
0051a370 void __thiscall TargetManager::TargetManager(class TargetManager* this, class CPhysicsObj* arg2)
|
|
|
|
0051a370 {
|
|
0051a376 this->physobj = arg2;
|
|
0051a37a this->target_info = nullptr;
|
|
0051a37d this->voyeur_table = nullptr;
|
|
0051a380 this->last_update_time = 0f;
|
|
0051a383 *(uint32_t*)((char*)this->last_update_time)[4] = 0;
|
|
0051a370 }
|
|
```
|
|
|
|
NOTE: `*(uint32_t*)((char*)this->last_update_time)[4] = 0` is BN's mangled
|
|
way of zeroing the high dword of the 80-bit `long double last_update_time`
|
|
field (two-dword store split across a `long double`). Same pattern recurs
|
|
throughout this file for every `long double` field/parameter assignment —
|
|
not a bug, just how BN represents x87 80-bit extended stores split into a
|
|
32-bit low/high pair. Flagging once here; not re-flagging every occurrence
|
|
below.
|
|
|
|
### `TargetInfo::TargetInfo` (ctor, bonus — used pervasively by callers) — 0051a420
|
|
|
|
```c
|
|
0051a420 void __fastcall TargetInfo::TargetInfo(class TargetInfo* this)
|
|
|
|
0051a420 {
|
|
0051a429 this->context_id = 0;
|
|
0051a42b this->object_id = 0;
|
|
0051a42e this->radius = 0f;
|
|
0051a431 this->quantum = 0f;
|
|
0051a434 *(uint32_t*)((char*)this->quantum)[4] = 0;
|
|
0051a437 this->target_position.vtable = 0x796910;
|
|
0051a43e this->target_position.objcell_id = 0;
|
|
0051a423 this->target_position.frame.qw = 0x3f800000;
|
|
0051a423 this->target_position.frame.qx = 0f;
|
|
0051a423 this->target_position.frame.qy = 0f;
|
|
0051a423 this->target_position.frame.qz = 0f;
|
|
0051a423 this->target_position.frame.m_fOrigin.x = 0;
|
|
0051a423 this->target_position.frame.m_fOrigin.y = 0f;
|
|
0051a423 this->target_position.frame.m_fOrigin.z = 0f;
|
|
0051a459 Frame::cache(&this->target_position.frame);
|
|
0051a461 this->interpolated_position.vtable = 0x796910;
|
|
0051a468 this->interpolated_position.objcell_id = 0;
|
|
0051a45e this->interpolated_position.frame.qw = 0x3f800000;
|
|
0051a45e this->interpolated_position.frame.qx = 0f;
|
|
0051a45e this->interpolated_position.frame.qy = 0f;
|
|
0051a45e this->interpolated_position.frame.qz = 0f;
|
|
0051a45e this->interpolated_position.frame.m_fOrigin.x = 0;
|
|
0051a45e this->interpolated_position.frame.m_fOrigin.y = 0f;
|
|
0051a45e this->interpolated_position.frame.m_fOrigin.z = 0f;
|
|
0051a483 Frame::cache(&this->interpolated_position.frame);
|
|
0051a488 this->status = Undef_TargetStatus;
|
|
0051a48e this->last_update_time = 0f;
|
|
0051a494 *(uint32_t*)((char*)this->last_update_time)[4] = 0;
|
|
0051a420 }
|
|
```
|
|
|
|
Default-constructs both `Position` members to identity quaternion at
|
|
origin, `status = Undef_TargetStatus`, everything else zeroed.
|
|
`velocity`/`interpolated_heading` are NOT explicitly zeroed here (they're
|
|
POD `AC1Legacy::Vector3` — likely left uninitialized by this particular
|
|
ctor overload, or zeroed by a base/member ctor not captured in this
|
|
pseudo-C region).
|
|
|
|
### `TargetManager::SetTargetQuantum` — 0051a4a0
|
|
|
|
```c
|
|
0051a4a0 void __thiscall TargetManager::SetTargetQuantum(class TargetManager* this, double arg2)
|
|
|
|
0051a4a0 {
|
|
0051a4a3 class TargetInfo* target_info = this->target_info;
|
|
0051a4a3
|
|
0051a4a8 if (target_info != 0)
|
|
0051a4a8 {
|
|
0051a4b2 target_info->quantum = arg2;
|
|
0051a4b5 *(uint32_t*)((char*)target_info->quantum)[4] = *(uint32_t*)((char*)arg2)[4];
|
|
0051a4bf class CPhysicsObj* eax_1 = CPhysicsObj::GetObjectA(this->target_info->object_id);
|
|
0051a4bf
|
|
0051a4c9 if (eax_1 != 0)
|
|
0051a4c9 {
|
|
0051a4cb class TargetInfo* target_info_1 = this->target_info;
|
|
0051a4e6 CPhysicsObj::add_voyeur(eax_1, this->physobj->id, ((float)((long double)target_info_1->radius)), ((float)((long double)target_info_1->quantum)));
|
|
0051a4c9 }
|
|
0051a4a8 }
|
|
0051a4a0 }
|
|
```
|
|
|
|
Only acts if we currently HAVE a target (`target_info != 0`). Updates our
|
|
own `quantum` (the resend interval), then re-registers ourselves as a
|
|
voyeur of the target object with the new quantum — i.e. quantum changes
|
|
propagate by re-adding the voyeur subscription (not a separate "update
|
|
quantum" RPC on the remote side).
|
|
|
|
### `TargetManager::SendVoyeurUpdate` — 0051a4f0
|
|
|
|
```c
|
|
0051a4f0 void __thiscall TargetManager::SendVoyeurUpdate(class TargetManager* this, class TargettedVoyeurInfo* arg2, class Position const* arg3, enum TargetStatus arg4)
|
|
|
|
0051a4f0 {
|
|
0051a503 int32_t* esi = arg2;
|
|
0051a514 esi[6] = arg3->objcell_id;
|
|
0051a517 Frame::operator=(&esi[7], &arg3->frame);
|
|
0051a520 int32_t var_d0;
|
|
0051a520 TargetInfo::TargetInfo(&var_d0);
|
|
0051a525 class CPhysicsObj* physobj = this->physobj;
|
|
0051a527 int32_t eax_1 = esi[2];
|
|
0051a52a int32_t edx = esi[4];
|
|
0051a52d var_d0 = 0;
|
|
0051a538 uint32_t id = physobj->id;
|
|
0051a53f int32_t var_c0 = eax_1;
|
|
0051a543 int32_t var_bc = esi[3];
|
|
0051a547 int32_t var_c8 = edx;
|
|
0051a556 uint32_t objcell_id = physobj->m_position.objcell_id;
|
|
0051a55a void var_b0;
|
|
0051a55a Frame::operator=(&var_b0, &physobj->m_position.frame);
|
|
0051a562 uint32_t objcell_id_1 = arg3->objcell_id;
|
|
0051a571 void var_68;
|
|
0051a571 Frame::operator=(&var_68, &arg3->frame);
|
|
0051a57d void __return;
|
|
0051a57d class AC1Legacy::Vector3* eax_3 = CPhysicsObj::get_velocity(physobj, &__return);
|
|
0051a586 float x = eax_3->x;
|
|
0051a597 float y = eax_3->y;
|
|
0051a5a2 float z = eax_3->z;
|
|
0051a5a9 enum TargetStatus var_10 = arg4;
|
|
0051a5b0 class CPhysicsObj* eax_5 = CPhysicsObj::GetObjectA(*(uint32_t*)esi);
|
|
0051a5b0
|
|
0051a5be if (eax_5 != 0)
|
|
0051a5c7 CPhysicsObj::receive_target_update(eax_5, &var_d0);
|
|
0051a4f0 }
|
|
```
|
|
|
|
Mechanically: (1) stashes `arg3` (the caller-computed interpolated
|
|
position) into the voyeur record's `last_sent_position` field (`esi[6]`
|
|
= objcell_id, `esi[7..]` = frame — matches `TargettedVoyeurInfo::last_sent_position`
|
|
being the 3rd/4th field after `object_id`/`quantum`/`radius`); (2) builds
|
|
a fresh `TargetInfo` on the stack (`var_d0`) populated with: our own
|
|
`context_id`? (actually `esi[2]`/`esi[3]`/`esi[4]` = voyeur's own
|
|
`quantum`/`radius` fields per struct layout — field-name binding here is
|
|
approximate since BN shows raw offsets, not member names), our physobj's
|
|
`id` as `object_id`, our physobj's CURRENT position (not the interpolated
|
|
`arg3`!) as `target_position`, `arg3`'s position as `interpolated_position`,
|
|
our CURRENT velocity, and `arg4` as `status`; (3) looks up the voyeur's
|
|
`object_id` (`*(uint32_t*)esi`, i.e. `esi[0]`) via `CPhysicsObj::GetObjectA`
|
|
and if resolved, tail-calls `CPhysicsObj::receive_target_update` on THAT
|
|
object — delivering the update directly into the voyeur's own
|
|
`TargetManager::ReceiveUpdate`.
|
|
|
|
NOTE (field-offset ambiguity): the exact mapping of `esi[2]`/`esi[3]`/`esi[4]`
|
|
into `var_c0`/`var_bc`/`var_c8` (populating fields of the constructed
|
|
`TargetInfo`) is inferred from struct order, not verified field-by-field —
|
|
BN shows raw stack offsets (`var_c0`, `var_bc`, `var_c8`) copied from raw
|
|
struct offsets (`esi[2]`, `esi[3]`, `esi[4]`) with no field names attached.
|
|
Cross-check against `TargettedVoyeurInfo` layout (`object_id`@0, `quantum`@4
|
|
(long double, 12 bytes on this compiler?), `radius`@0x10, `last_sent_position`@0x14)
|
|
before porting exact field semantics.
|
|
|
|
### `TargetManager::GetInterpolatedPosition` — 0051a5e0
|
|
|
|
```c
|
|
0051a5e0 void __thiscall TargetManager::GetInterpolatedPosition(class TargetManager* this, double arg2, class Position* arg3)
|
|
|
|
0051a5e0 {
|
|
0051a5e4 class Position* esi = arg3;
|
|
0051a5eb class CPhysicsObj* physobj = this->physobj;
|
|
0051a5f6 esi->objcell_id = physobj->m_position.objcell_id;
|
|
0051a5fd Frame::operator=(&esi->frame, &physobj->m_position.frame);
|
|
0051a608 arg3 = ((float)((long double)arg2));
|
|
0051a611 void __return;
|
|
0051a611 class AC1Legacy::Vector3* eax_2 = CPhysicsObj::get_velocity(this->physobj, &__return);
|
|
0051a634 esi->frame.m_fOrigin.x = ((float)((((long double)arg3) * ((long double)eax_2->x)) + ((long double)esi->frame.m_fOrigin.x)));
|
|
0051a63a esi->frame.m_fOrigin.y = ((float)((((long double)arg3) * ((long double)eax_2->y)) + ((long double)esi->frame.m_fOrigin.y)));
|
|
0051a644 esi->frame.m_fOrigin.z = ((float)(((long double)((float)(((long double)arg3) * ((long double)eax_2->z)))) + ((long double)esi->frame.m_fOrigin.z)));
|
|
0051a5e0 }
|
|
```
|
|
|
|
Straightforward dead-reckoning extrapolation: `out = physobj.position`,
|
|
then `out.origin += arg2 (a time-delta / quantum, as double) * velocity`
|
|
per-axis. This is our own physobj's position extrapolated forward by
|
|
`arg2` seconds using our own current velocity — used to predict where WE
|
|
will be by the time a voyeur update actually lands, so the voyeur update
|
|
carries a "where I'll be at quantum-from-now" position rather than
|
|
"where I am right now."
|
|
|
|
### `TargetManager::CheckAndUpdateVoyeur` — 0051a650
|
|
|
|
```c
|
|
0051a650 void __thiscall TargetManager::CheckAndUpdateVoyeur(class TargetManager* this, class TargettedVoyeurInfo* arg2)
|
|
|
|
0051a650 {
|
|
0051a65b int32_t var_48 = 0x796910;
|
|
0051a663 int32_t var_44 = 0;
|
|
0051a66b int32_t var_40 = 0x3f800000;
|
|
0051a673 int32_t var_3c = 0;
|
|
0051a67b int32_t var_38 = 0;
|
|
0051a683 int32_t var_34 = 0;
|
|
0051a68b int32_t var_c = 0;
|
|
0051a693 int32_t var_8 = 0;
|
|
0051a69b int32_t var_4 = 0;
|
|
0051a6a3 Frame::cache(&var_40);
|
|
0051a6b7 int32_t var_58 = *(uint32_t*)((char*)arg2->quantum)[4];
|
|
0051a6bb TargetManager::GetInterpolatedPosition(this, arg2->quantum, &var_48);
|
|
0051a6c8 long double st0 = Position::distance(&var_48, &arg2->last_sent_position);
|
|
0051a6cd long double temp1 = ((long double)arg2->radius);
|
|
0051a6cd (st0 - temp1);
|
|
0051a6cd
|
|
0051a6d5 if ((*(uint8_t*)((char*)((((st0 < temp1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(st0, temp1))) ? 1 : 0) << 0xa) | ((((st0 == temp1) ? 1 : 0) << 0xe) | 0)))))[1] & 0x41) == 0)
|
|
0051a6e1 TargetManager::SendVoyeurUpdate(this, arg2, &var_48, Ok_TargetStatus);
|
|
0051a650 }
|
|
```
|
|
|
|
NOTE (x87 FCMP mush): the trailing `if (...)` block is a garbled x87
|
|
`FCOMI`/`fnstsw`+`sahf`-style comparison of `st0` (the computed distance)
|
|
against `temp1` (`arg2->radius`), reconstructed by BN into a synthetic
|
|
FLAGS byte then masked with `0x41` (ZF|CF). The condition as literally
|
|
written computes `(distance < radius) OR unordered OR (distance == radius)`
|
|
into bits 8/0xa/0xe of a byte, then tests bits `0x41` = `0b0100_0001`
|
|
(bit0 + bit6) of the HIGH byte of that packed value — which does not
|
|
line up cleanly with the bits set (8/0xa/0xe are all ABOVE bit 7, so the
|
|
high byte holds bits 8,10,14 shifted down by 8 → i.e. bit0=orig bit8=`st0<temp1`,
|
|
bit2=orig bit0xa=unordered, bit6=orig bit0xe=`st0==temp1`). So `& 0x41`
|
|
tests `(distance<radius) OR (distance==radius)`, i.e. **`distance <= radius`**
|
|
(ignoring the unordered bit which lands on bit2, not covered by 0x41).
|
|
**Net semantic: `if (distance <= radius) { /* skip send */ } else { SendVoyeurUpdate(... Ok) }`**
|
|
— inverted from a naive reading. In other words: only send the voyeur
|
|
update when the extrapolated position has drifted MORE than `radius`
|
|
from the `last_sent_position` we already told them about. This is the
|
|
classic "send-on-significant-movement" dead-reckoning threshold, and the
|
|
mush decodes to the expected sense. Flag as CONFIRMED-BUT-MUSH; the lead
|
|
should re-derive from the bit math above rather than trust my paraphrase
|
|
blindly.
|
|
|
|
### `TargetManager::NotifyVoyeurOfEvent` — 0051a6f0
|
|
|
|
```c
|
|
0051a6f0 void __thiscall TargetManager::NotifyVoyeurOfEvent(class TargetManager* this, enum TargetStatus arg2)
|
|
|
|
0051a6f0 {
|
|
0051a6f6 class LongNIHash<TargettedVoyeurInfo>* voyeur_table = this->voyeur_table;
|
|
0051a6f6
|
|
0051a6fb if (voyeur_table != 0)
|
|
0051a6fb {
|
|
0051a703 int32_t* var_10;
|
|
0051a703 LongNIHashIter<DetectionCylsphere>::LongNIHashIter<DetectionCylsphere>(&var_10, voyeur_table);
|
|
0051a708 int32_t i_1;
|
|
0051a708 int32_t i = i_1;
|
|
0051a708
|
|
0051a70e if (i == 0)
|
|
0051a70e {
|
|
0051a711 void** j_1;
|
|
0051a711 void** j = j_1;
|
|
0051a716 int32_t var_c;
|
|
0051a716 int32_t edi_1 = var_c;
|
|
0051a716
|
|
0051a762 do
|
|
0051a762 {
|
|
0051a722 class TargettedVoyeurInfo* eax;
|
|
0051a722
|
|
0051a722 if (j == 0)
|
|
0051a729 eax = nullptr;
|
|
0051a722 else
|
|
0051a724 eax = j[1];
|
|
0051a724
|
|
0051a72d if (i == 0)
|
|
0051a72d {
|
|
0051a733 int32_t* ecx_1;
|
|
0051a733
|
|
0051a733 for (j = *(uint32_t*)j; j == 0; j = *(uint32_t*)(*(uint32_t*)ecx_1 + (edi_1 << 2)))
|
|
0051a733 {
|
|
0051a735 ecx_1 = var_10;
|
|
0051a73c edi_1 += 1;
|
|
0051a73c
|
|
0051a73f if (edi_1 >= ecx_1[1])
|
|
0051a73f {
|
|
0051a748 i = 1;
|
|
0051a748 break;
|
|
0051a73f }
|
|
0051a733 }
|
|
0051a72d }
|
|
0051a72d
|
|
0051a75b TargetManager::SendVoyeurUpdate(this, eax, &this->physobj->m_position, arg2);
|
|
0051a762 } while (i == 0);
|
|
0051a70e }
|
|
0051a6fb }
|
|
0051a6f0 }
|
|
```
|
|
|
|
Iterates every entry in `voyeur_table` (all subscribers watching US) and
|
|
calls `SendVoyeurUpdate` for each, using our CURRENT (non-interpolated)
|
|
position and the caller-supplied `arg2` status (e.g. `ExitWorld_TargetStatus`,
|
|
`Teleported_TargetStatus`). This is the "broadcast a status event to
|
|
everyone watching me" path, distinct from the per-tick distance-gated
|
|
`CheckAndUpdateVoyeur` path.
|
|
|
|
NOTE (BN mislabel): the ctor call `LongNIHashIter<DetectionCylsphere>::LongNIHashIter<DetectionCylsphere>(&var_10, voyeur_table)`
|
|
is BN reusing the `LongNIHashIter<DetectionCylsphere>` template
|
|
instantiation's mangled name for what is actually iterating
|
|
`this->voyeur_table`, a `LongNIHash<TargettedVoyeurInfo>*`. The real
|
|
type is `LongNIHashIter<TargettedVoyeurInfo>` (confirmed: the struct
|
|
exists at acclient.h:57648 and `TargetManager::HandleTargetting` below
|
|
uses the identical pattern with the same mislabeled ctor name). Treat
|
|
every `LongNIHashIter<DetectionCylsphere>` symbol in this file's
|
|
TargetManager functions as actually `LongNIHashIter<TargettedVoyeurInfo>`.
|
|
|
|
### `TargetManager::ClearTarget` — 0051a7e0
|
|
|
|
```c
|
|
0051a7e0 void __fastcall TargetManager::ClearTarget(class TargetManager* this)
|
|
|
|
0051a7e0 {
|
|
0051a7e3 class TargetInfo* target_info = this->target_info;
|
|
0051a7e3
|
|
0051a7e8 if (target_info != 0)
|
|
0051a7e8 {
|
|
0051a7ee class CPhysicsObj* eax_1 = CPhysicsObj::GetObjectA(target_info->object_id);
|
|
0051a7ee
|
|
0051a7f8 if (eax_1 != 0)
|
|
0051a802 CPhysicsObj::remove_voyeur(eax_1, this->physobj->id);
|
|
0051a802
|
|
0051a807 class TargetInfo* target_info_1 = this->target_info;
|
|
0051a807
|
|
0051a80c if (target_info_1 != 0)
|
|
0051a80c {
|
|
0051a814 target_info_1->interpolated_position.vtable = 0x79285c;
|
|
0051a817 target_info_1->target_position.vtable = 0x79285c;
|
|
0051a81a operator delete(target_info_1);
|
|
0051a80c }
|
|
0051a80c
|
|
0051a822 this->target_info = nullptr;
|
|
0051a7e8 }
|
|
0051a7e0 }
|
|
```
|
|
|
|
If we currently have a target: resolve the target object, if resolved
|
|
tell IT to `remove_voyeur(our_id)` (unsubscribe us from their voyeur
|
|
table), then free our local `TargetInfo` and null the pointer. Mirror
|
|
image of `SetTarget`'s subscribe path.
|
|
|
|
### `TargetManager::AddVoyeur` — 0051a830
|
|
|
|
```c
|
|
0051a830 void __thiscall TargetManager::AddVoyeur(class TargetManager* this, uint32_t arg2, float arg3, double arg4)
|
|
|
|
0051a830 {
|
|
0051a838 class LongNIHash<TargettedVoyeurInfo>* voyeur_table = this->voyeur_table;
|
|
0051a838
|
|
0051a840 if (voyeur_table == 0)
|
|
0051a840 {
|
|
0051a88f void* eax_5 = operator new(8);
|
|
0051a899 class LongNIHash<TargettedVoyeurInfo>* eax_6;
|
|
0051a899
|
|
0051a899 if (eax_5 == 0)
|
|
0051a8a6 eax_6 = nullptr;
|
|
0051a899 else
|
|
0051a89f eax_6 = LongNIHash<DetectionInfo>::LongNIHash<DetectionInfo>(eax_5, 4);
|
|
0051a89f
|
|
0051a8a8 this->voyeur_table = eax_6;
|
|
0051a840 }
|
|
0051a840 else
|
|
0051a840 {
|
|
0051a850 void** edx_3 = voyeur_table->buckets[(COMBINE(0, ((arg2 >> 0x10) ^ arg2)) % voyeur_table->table_size)];
|
|
0051a850
|
|
0051a855 if (edx_3 != 0)
|
|
0051a855 {
|
|
0051a85a while (edx_3[2] != arg2)
|
|
0051a85a {
|
|
0051a860 edx_3 = *(uint32_t*)edx_3;
|
|
0051a860
|
|
0051a864 if (edx_3 == 0)
|
|
0051a864 goto label_51a8b3;
|
|
0051a85a }
|
|
0051a85a
|
|
0051a86b void* edx_4 = edx_3[1];
|
|
0051a86b
|
|
0051a870 if (edx_4 != 0)
|
|
0051a870 {
|
|
0051a87b *(uint32_t*)((char*)edx_4 + 0x10) = arg3;
|
|
0051a883 *(uint32_t*)((char*)edx_4 + 8) = arg4;
|
|
0051a886 *(uint32_t*)((char*)edx_4 + 0xc) = *(uint32_t*)((char*)arg4)[4];
|
|
0051a88a return;
|
|
0051a870 }
|
|
0051a855 }
|
|
0051a840 }
|
|
0051a840
|
|
0051a8b3 label_51a8b3:
|
|
0051a8b3 void* esi = operator new(0x60);
|
|
0051a8b3
|
|
0051a8ba if (esi == 0)
|
|
0051a8f3 esi = nullptr;
|
|
0051a8ba else
|
|
0051a8ba {
|
|
0051a8bc *(uint32_t*)esi = 0;
|
|
0051a8be *(uint32_t*)((char*)esi + 8) = 0;
|
|
0051a8c1 *(uint32_t*)((char*)esi + 0xc) = 0;
|
|
0051a8c4 *(uint32_t*)((char*)esi + 0x10) = 0;
|
|
0051a8ca *(uint32_t*)((char*)esi + 0x14) = 0x796910;
|
|
0051a8d1 *(uint32_t*)((char*)esi + 0x18) = 0;
|
|
0051a8d4 *(uint32_t*)((char*)esi + 0x1c) = 0x3f800000;
|
|
0051a8da *(uint32_t*)((char*)esi + 0x20) = 0;
|
|
0051a8dd *(uint32_t*)((char*)esi + 0x24) = 0;
|
|
0051a8e0 *(uint32_t*)((char*)esi + 0x28) = 0;
|
|
0051a8e3 *(uint32_t*)((char*)esi + 0x50) = 0;
|
|
0051a8e6 *(uint32_t*)((char*)esi + 0x54) = 0;
|
|
0051a8e9 *(uint32_t*)((char*)esi + 0x58) = 0;
|
|
0051a8ec Frame::cache(((char*)esi + 0x1c));
|
|
0051a8ba }
|
|
0051a8ba
|
|
0051a902 *(uint32_t*)esi = arg2;
|
|
0051a904 *(uint32_t*)((char*)esi + 0x10) = arg3;
|
|
0051a907 *(uint32_t*)((char*)esi + 8) = arg4;
|
|
0051a90a *(uint32_t*)((char*)esi + 0xc) = *(uint32_t*)((char*)arg4)[4];
|
|
0051a911 LongNIHash<DetectionInfo>::add(this->voyeur_table, esi, arg2);
|
|
0051a922 TargetManager::SendVoyeurUpdate(this, esi, &this->physobj->m_position, Ok_TargetStatus);
|
|
0051a830 }
|
|
```
|
|
|
|
`arg2` = voyeur's object id, `arg3` = radius (float), `arg4` = quantum
|
|
(double). Lazily creates `voyeur_table` (4 buckets) on first use.
|
|
Hand-rolled hash-bucket lookup: if an existing `TargettedVoyeurInfo` for
|
|
`arg2` is found, just updates its `radius`/`quantum` in place and
|
|
returns early (no immediate send). Otherwise allocates a fresh
|
|
`TargettedVoyeurInfo` (0x60 bytes), zero-inits it (including
|
|
`last_sent_position` via `Frame::cache`), sets `object_id`/`radius`/`quantum`,
|
|
inserts into the hash, then immediately calls `SendVoyeurUpdate` with our
|
|
CURRENT position and `Ok_TargetStatus` — i.e. brand-new voyeurs get an
|
|
immediate snapshot rather than waiting for the next `HandleTargetting` tick.
|
|
|
|
NOTE (BN mislabel): `LongNIHash<DetectionInfo>::LongNIHash<DetectionInfo>`
|
|
and `LongNIHash<DetectionInfo>::add` are BN reusing the `DetectionInfo`
|
|
hash template instantiation's mangled symbol for what is actually
|
|
`LongNIHash<TargettedVoyeurInfo>`'s ctor/add — same class of mislabel as
|
|
above (`voyeur_table` is declared `LongNIHash<TargettedVoyeurInfo>*`).
|
|
|
|
### `TargetManager::ReceiveUpdate` — 0051a930
|
|
|
|
```c
|
|
0051a930 void __thiscall TargetManager::ReceiveUpdate(class TargetManager* this, class TargetInfo const* arg2)
|
|
|
|
0051a930 {
|
|
0051a936 class TargetInfo* target_info = this->target_info;
|
|
0051a936
|
|
0051a93c if (target_info != 0)
|
|
0051a93c {
|
|
0051a946 uint32_t object_id = arg2->object_id;
|
|
0051a946
|
|
0051a94c if (object_id == target_info->object_id)
|
|
0051a94c {
|
|
0051a952 target_info->object_id = object_id;
|
|
0051a955 this->target_info->radius = arg2->radius;
|
|
0051a961 class TargetInfo* target_info_2 = this->target_info;
|
|
0051a964 target_info_2->quantum = arg2->quantum;
|
|
0051a96a *(uint32_t*)((char*)target_info_2->quantum)[4] = *(uint32_t*)((char*)arg2->quantum)[4];
|
|
0051a973 class Position* eax_3 = &this->target_info->target_position;
|
|
0051a97d eax_3->objcell_id = arg2->target_position.objcell_id;
|
|
0051a980 Frame::operator=(&eax_3->frame, &arg2->target_position.frame);
|
|
0051a98b class Position* eax_5 = &this->target_info->interpolated_position;
|
|
0051a995 eax_5->objcell_id = arg2->interpolated_position.objcell_id;
|
|
0051a998 Frame::operator=(&eax_5->frame, &arg2->interpolated_position.frame);
|
|
0051a9a0 class AC1Legacy::Vector3* eax_7 = &this->target_info->velocity;
|
|
0051a9ad eax_7->x = arg2->velocity.x;
|
|
0051a9b2 eax_7->y = arg2->velocity.y;
|
|
0051a9b8 eax_7->z = arg2->velocity.z;
|
|
0051a9bb this->target_info->status = arg2->status;
|
|
0051a9cf class TargetInfo* target_info_3 = this->target_info;
|
|
0051a9d8 target_info_3->last_update_time = (*(uint32_t*)Timer::cur_time);
|
|
0051a9de *(uint32_t*)((char*)target_info_3->last_update_time)[4] = *(int32_t*)((char*)Timer::cur_time + 4);
|
|
0051a9f5 int32_t __return;
|
|
0051a9f5 class AC1Legacy::Vector3* eax_10 = Position::get_offset(&this->physobj->m_position, &__return, &this->target_info->interpolated_position);
|
|
0051a9ff class AC1Legacy::Vector3* ecx_13 = &this->target_info->interpolated_heading;
|
|
0051aa05 ecx_13->x = eax_10->x;
|
|
0051aa0a ecx_13->y = eax_10->y;
|
|
0051aa10 ecx_13->z = eax_10->z;
|
|
0051aa10
|
|
0051aa23 if (AC1Legacy::Vector3::normalize_check_small(&this->target_info->interpolated_heading) != 0)
|
|
0051aa23 {
|
|
0051aa25 class TargetInfo* target_info_1 = this->target_info;
|
|
0051aa28 __return = 0;
|
|
0051aa34 target_info_1->interpolated_heading.x = __return;
|
|
0051aa34 target_info_1->interpolated_heading.y = 0f;
|
|
0051aa34 target_info_1->interpolated_heading.z = 1f;
|
|
0051aa23 }
|
|
0051aa23
|
|
0051aa66 void var_e4;
|
|
0051aa66 TargetInfo::TargetInfo(&var_e4, this->target_info);
|
|
0051aa6d CPhysicsObj::HandleUpdateTarget(this->physobj, var_e4);
|
|
0051aa6d
|
|
0051aa79 if (arg2->status == ExitWorld_TargetStatus)
|
|
0051aa7d TargetManager::ClearTarget(this);
|
|
0051a94c }
|
|
0051a93c }
|
|
0051a930 }
|
|
```
|
|
|
|
This is the "I am a voyeur and just got an update FROM the thing I'm
|
|
watching" handler — called via `CPhysicsObj::receive_target_update` from
|
|
`SendVoyeurUpdate`'s tail-call on the SENDER side. Only processes if we
|
|
still have an active `target_info` AND the incoming `object_id` matches
|
|
what we're tracking (stale/mismatched updates dropped silently). Copies
|
|
`radius`/`quantum`/`target_position`/`interpolated_position`/`velocity`/`status`
|
|
wholesale from the wire payload, stamps `last_update_time = Timer::cur_time`,
|
|
then recomputes `interpolated_heading` as the normalized offset from OUR
|
|
position to the target's `interpolated_position` (falls back to
|
|
`(0,0,1)` — i.e. forward/+Z — if the offset vector is too small to
|
|
normalize, via `normalize_check_small`). Finally fans the whole
|
|
`TargetInfo` snapshot out to `CPhysicsObj::HandleUpdateTarget` (which
|
|
forwards to `MovementManager::HandleUpdateTarget` +
|
|
`PositionManager::HandleUpdateTarget` — see below), and if the target's
|
|
status says `ExitWorld_TargetStatus`, immediately clears our own
|
|
subscription (target left the world → give up watching it).
|
|
|
|
### `TargetManager::HandleTargetting` — 0051aa90
|
|
|
|
```c
|
|
0051aa90 void __fastcall TargetManager::HandleTargetting(class TargetManager* this)
|
|
|
|
0051aa90 {
|
|
0051aa9d long double x87_r7 = (((long double)PhysicsTimer::curr_time) - ((long double)this->last_update_time));
|
|
0051aaa0 long double temp0 = ((long double)0.5);
|
|
0051aaa0 (x87_r7 - temp0);
|
|
0051aaa6 int32_t eax;
|
|
0051aaa6 eax = ((((x87_r7 < temp0) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7, temp0))) ? 1 : 0) << 0xa) | ((((x87_r7 == temp0) ? 1 : 0) << 0xe) | 0))));
|
|
0051aaa8 bool p = /* bool p = unimplemented {test ah, 0x5} */;
|
|
0051aaa8
|
|
0051aaab if (p)
|
|
0051aaab {
|
|
0051aab1 class TargetInfo* target_info = this->target_info;
|
|
0051aab1
|
|
0051aac0 if ((target_info != 0 && target_info->status == Undef_TargetStatus))
|
|
0051aac0 {
|
|
0051aac8 long double x87_r7_2 = (((long double)10.0) + ((long double)target_info->last_update_time));
|
|
0051aace long double temp1_1 = ((long double)Timer::cur_time);
|
|
0051aace (x87_r7_2 - temp1_1);
|
|
0051aad4 enum TargetStatus eax_1;
|
|
0051aad4 eax_1 = ((((x87_r7_2 < temp1_1) ? 1 : 0) << 8) | ((((0) ? 1 : 0) << 9) | (((((FCMP_UO(x87_r7_2, temp1_1))) ? 1 : 0) << 0xa) | ((((x87_r7_2 == temp1_1) ? 1 : 0) << 0xe) | 0))));
|
|
0051aad6 bool p_1 = /* bool p_1 = unimplemented {test ah, 0x5} */;
|
|
0051aad6
|
|
0051aad9 if (!(p_1))
|
|
0051aad9 {
|
|
0051aadb target_info->status = TimedOut_TargetStatus;
|
|
0051aaf1 void var_e8;
|
|
0051aaf1 TargetInfo::TargetInfo(&var_e8, this->target_info);
|
|
0051aaf8 CPhysicsObj::HandleUpdateTarget(this->physobj, var_e8);
|
|
0051aad9 }
|
|
0051aac0 }
|
|
0051aac0
|
|
0051aafd class LongNIHash<TargettedVoyeurInfo>* voyeur_table = this->voyeur_table;
|
|
0051aafd
|
|
0051ab02 if (voyeur_table != 0)
|
|
0051ab02 {
|
|
0051ab09 void var_10;
|
|
0051ab09 int32_t edx_1 = LongNIHashIter<DetectionCylsphere>::LongNIHashIter<DetectionCylsphere>(&var_10, voyeur_table);
|
|
0051ab14 int32_t i;
|
|
0051ab14
|
|
0051ab14 while (i == 0)
|
|
0051ab14 {
|
|
0051ab1c void* var_8;
|
|
0051ab1c class TargettedVoyeurInfo* esi_1;
|
|
0051ab1c
|
|
0051ab1c if (var_8 == 0)
|
|
0051ab23 esi_1 = nullptr;
|
|
0051ab1c else
|
|
0051ab1e esi_1 = *(uint32_t*)((char*)var_8 + 4);
|
|
0051ab1e
|
|
0051ab25 int32_t var_1c_2 = 0;
|
|
0051ab2b LongNIHashIter<TargettedVoyeurInfo>::operator++(&var_10, edx_1);
|
|
0051ab33 edx_1 = TargetManager::CheckAndUpdateVoyeur(this, esi_1);
|
|
0051ab14 }
|
|
0051ab02 }
|
|
0051ab02
|
|
0051ab4c this->last_update_time = (*(uint32_t*)PhysicsTimer::curr_time);
|
|
0051ab4f *(uint32_t*)((char*)this->last_update_time)[4] = *(int32_t*)((char*)PhysicsTimer::curr_time + 4);
|
|
0051aaab }
|
|
0051aa90 }
|
|
```
|
|
|
|
**This is the per-tick driver entry point** — called unconditionally
|
|
once per physics tick from `CPhysicsObj::UpdateObjectInternal` (see
|
|
Callers below) whenever `this->target_manager != 0`. There is **no
|
|
`TargetManager::UseTime` function** — `HandleTargetting` IS the tick
|
|
driver, gated by its own internal quantum check rather than an outer
|
|
`UseTime` wrapper (unlike `MovementManager::UseTime` / `PositionManager::UseTime`
|
|
/ `StickyManager::UseTime` which sit as siblings in the same call chain).
|
|
|
|
NOTE (x87 FCMP mush, two occurrences): both `p` and `p_1` are
|
|
`unimplemented {test ah, 0x5}` — BN couldn't lower the FLAGS test after
|
|
the synthetic FCOMI byte pack. Bit pattern is IDENTICAL shape to the one
|
|
decoded in `CheckAndUpdateVoyeur` above: `test ah, 0x5` tests bits
|
|
`0b101` = bit0+bit2 of the high byte, i.e. (in the same bit-index scheme
|
|
as before) orig-bit-8 (`<`) and orig-bit-0xa (unordered). So:
|
|
`p = (x87_r7 < temp0) OR unordered(x87_r7, temp0)`, i.e.
|
|
**`p = !(PhysicsTimer::curr_time - last_update_time >= 0.5)`** ... wait,
|
|
sign check: `x87_r7 = curr_time - last_update_time`, compared `< 0.5`.
|
|
`p` true means `elapsed < 0.5` (or unordered) → guard body runs when `p`
|
|
is true. Net: **the whole per-tick body (voyeur re-check + timeout-status
|
|
loop) only runs when `elapsed_since_last_update < 0.5s`... ** that reads
|
|
backwards for a "tick every N seconds" gate (normally you'd expect the
|
|
body to run when elapsed >= threshold, then reset the timer). **FLAG FOR
|
|
LEAD**: re-derive this comparison by hand against x86 FCOMI/SAHF
|
|
semantics before porting — my bit-algebra above may have the sense
|
|
inverted (in `CheckAndUpdateVoyeur` the same `0x41`-style mask decoded
|
|
to `<=`, but here it's a bare `test ah,0x5` i.e. `0x05` mask = bit0+bit2,
|
|
different mask than `CheckAndUpdateVoyeur`'s `0x41` = bit0+bit6 — these
|
|
are NOT the same test and I do not have confident hand-verified parity
|
|
between them). The inner 10-second timeout check (`p_1`, same `0x5` mask
|
|
pattern on `(10.0 + last_update_time) vs Timer::cur_time`, guarded by
|
|
`!p_1`) reads as "target status still Undef 10s after last update →
|
|
promote to TimedOut" which is directionally sane (timeout-if-stale), so
|
|
by symmetry `p` at the top is very likely the equivalent "run this tick's
|
|
logic once quantum-time has elapsed" gate — but get x86 semantics
|
|
verified rather than trusting this narrative.
|
|
|
|
NOTE (BN mislabel): same `LongNIHashIter<DetectionCylsphere>::LongNIHashIter<DetectionCylsphere>`
|
|
mislabel as in `NotifyVoyeurOfEvent` — actually `LongNIHashIter<TargettedVoyeurInfo>`.
|
|
The subsequent `LongNIHashIter<TargettedVoyeurInfo>::operator++` call
|
|
(correctly named this time) confirms the iterator's real element type.
|
|
|
|
Per-voyeur, the tick body calls `CheckAndUpdateVoyeur` (the
|
|
distance-threshold gated sender) for every entry, then stamps
|
|
`this->last_update_time = PhysicsTimer::curr_time` at the end (used for
|
|
next tick's `p` gate).
|
|
|
|
### `TargetManager::SetTarget` — 0051ac30
|
|
|
|
```c
|
|
0051ac30 void __thiscall TargetManager::SetTarget(class TargetManager* this, uint32_t arg2, uint32_t arg3, float arg4, double arg5)
|
|
|
|
0051ac30 {
|
|
0051ac39 class TargetInfo* target_info = this->target_info;
|
|
0051ac39
|
|
0051ac3f if (target_info != 0)
|
|
0051ac3f {
|
|
0051ac45 class CPhysicsObj* eax_1 = CPhysicsObj::GetObjectA(target_info->object_id);
|
|
0051ac45
|
|
0051ac4f if (eax_1 != 0)
|
|
0051ac59 CPhysicsObj::remove_voyeur(eax_1, this->physobj->id);
|
|
0051ac59
|
|
0051ac5e class TargetInfo* target_info_1 = this->target_info;
|
|
0051ac5e
|
|
0051ac63 if (target_info_1 != 0)
|
|
0051ac63 {
|
|
0051ac6b target_info_1->interpolated_position.vtable = 0x79285c;
|
|
0051ac6e target_info_1->target_position.vtable = 0x79285c;
|
|
0051ac71 operator delete(target_info_1);
|
|
0051ac63 }
|
|
0051ac63
|
|
0051ac79 this->target_info = nullptr;
|
|
0051ac3f }
|
|
0051ac3f
|
|
0051ac89 if (arg3 == 0)
|
|
0051ac89 {
|
|
0051ad3d uint32_t var_d0;
|
|
0051ad3d TargetInfo::TargetInfo(&var_d0);
|
|
0051ad59 var_d0 = arg2;
|
|
0051ad60 int32_t var_cc_1 = 0;
|
|
0051ad6b int32_t var_10_1 = 6;
|
|
0051ad76 void var_1a8;
|
|
0051ad76 TargetInfo::TargetInfo(&var_1a8, &var_d0);
|
|
0051ad7d CPhysicsObj::HandleUpdateTarget(this->physobj, var_1a8);
|
|
0051ad7d return;
|
|
0051ac89 }
|
|
0051ac89
|
|
0051ac94 void* eax_2 = operator new(0xd0);
|
|
0051ac9e uint32_t* eax_3;
|
|
0051ac9e
|
|
0051ac9e if (eax_2 == 0)
|
|
0051aca9 eax_3 = nullptr;
|
|
0051ac9e else
|
|
0051aca2 eax_3 = TargetInfo::TargetInfo(eax_2);
|
|
0051aca2
|
|
0051acb2 this->target_info = eax_3;
|
|
0051acb5 *(uint32_t*)eax_3 = arg2;
|
|
0051acb7 this->target_info->object_id = arg3;
|
|
0051acc4 this->target_info->radius = arg4;
|
|
0051acca class TargetInfo* target_info_3 = this->target_info;
|
|
0051acdb target_info_3->quantum = arg5;
|
|
0051acde *(uint32_t*)((char*)target_info_3->quantum)[4] = *(uint32_t*)((char*)arg5)[4];
|
|
0051ace6 class TargetInfo* target_info_4 = this->target_info;
|
|
0051acef target_info_4->last_update_time = (*(uint32_t*)Timer::cur_time);
|
|
0051acf5 *(uint32_t*)((char*)target_info_4->last_update_time)[4] = *(int32_t*)((char*)Timer::cur_time + 4);
|
|
0051ad02 class CPhysicsObj* eax_8 = CPhysicsObj::GetObjectA(this->target_info->object_id);
|
|
0051ad02
|
|
0051ad0c if (eax_8 != 0)
|
|
0051ad0c {
|
|
0051ad0e class TargetInfo* target_info_2 = this->target_info;
|
|
0051ad29 CPhysicsObj::add_voyeur(eax_8, this->physobj->id, ((float)((long double)target_info_2->radius)), ((float)((long double)target_info_2->quantum)));
|
|
0051ad0c }
|
|
0051ac30 }
|
|
```
|
|
|
|
`arg2` = context_id, `arg3` = new target object_id, `arg4` = radius,
|
|
`arg5` = quantum. First unconditionally tears down any EXISTING target
|
|
(unsubscribe voyeur from old target, free old `TargetInfo`) — same body
|
|
as `ClearTarget` inlined. Then: **if `arg3 == 0`** (clearing to "no
|
|
target"), synthesizes a `TargetInfo` with `context_id = arg2`,
|
|
`status = 6` (`TimedOut_TargetStatus`, NOTE: value 6 is written directly
|
|
as a raw int at `var_10_1 = 6` rather than the named enum constant — same
|
|
value as `TimedOut_TargetStatus`, so this "clear" path reports itself as
|
|
a timeout-status update to `HandleUpdateTarget`) and returns early
|
|
WITHOUT allocating `this->target_info` (stays null). Otherwise
|
|
(`arg3 != 0`): allocates a fresh heap `TargetInfo` (0xd0 bytes),
|
|
populates `context_id`/`object_id`/`radius`/`quantum`/`last_update_time
|
|
= Timer::cur_time`, resolves the target object, and if found calls
|
|
`add_voyeur` on it to subscribe. Note this does NOT immediately fetch/
|
|
apply the target's current position — that arrives asynchronously via
|
|
the target's own `SendVoyeurUpdate` → `receive_target_update` →
|
|
`ReceiveUpdate` round-trip.
|
|
|
|
### `TargetManager::RemoveVoyeur` — 0051ad90
|
|
|
|
```c
|
|
0051ad90 int32_t __thiscall TargetManager::RemoveVoyeur(class TargetManager* this, uint32_t arg2)
|
|
|
|
0051ad90 {
|
|
0051ad90 class LongNIHash<TargettedVoyeurInfo>* voyeur_table = this->voyeur_table;
|
|
0051ad90
|
|
0051ad95 if (voyeur_table != 0)
|
|
0051ad95 {
|
|
0051ad9c class DetectionCylsphere* eax_2 = LongNIHash<DetectionCylsphere>::remove(voyeur_table, arg2);
|
|
0051ad9c
|
|
0051ada3 if (eax_2 != 0)
|
|
0051ada3 {
|
|
0051ada6 eax_2->detection_type = 0x79285c;
|
|
0051adad operator delete(eax_2);
|
|
0051adba return 1;
|
|
0051ada3 }
|
|
0051ad95 }
|
|
0051ad95
|
|
0051adbf return 0;
|
|
0051ad90 }
|
|
```
|
|
|
|
Removes voyeur `arg2` from `voyeur_table` and frees the
|
|
`TargettedVoyeurInfo` record; returns 1 if one was found/removed, 0
|
|
otherwise.
|
|
|
|
NOTE (BN mislabel): `LongNIHash<DetectionCylsphere>::remove` — same
|
|
mislabel pattern, actually operating on `LongNIHash<TargettedVoyeurInfo>`
|
|
(confirmed by the `this` parameter type `LongNIHash<TargettedVoyeurInfo>*`).
|
|
Also `eax_2->detection_type = 0x79285c` is writing a vtable-pointer-looking
|
|
constant into a field named by BN as `detection_type` — this is almost
|
|
certainly the vtable-poison pattern used elsewhere in this file right
|
|
before `operator delete` (see `ClearTarget`'s `->vtable = 0x79285c` and
|
|
`SetTarget`'s identical pattern) applied to what should be
|
|
`TargettedVoyeurInfo`'s embedded `Position last_sent_position`'s vtable
|
|
slot, mislabeled as `detection_type` because BN is treating the freed
|
|
object as a `DetectionCylsphere*` per the mislabel above.
|
|
|
|
---
|
|
|
|
## `TargetManager::~TargetManager` (dtor, bonus) — 0051abd0
|
|
|
|
```c
|
|
0051abd0 void __fastcall TargetManager::~TargetManager(class TargetManager* this)
|
|
|
|
0051abd0 {
|
|
0051abd3 class TargetInfo* target_info = this->target_info;
|
|
0051abd3
|
|
0051abd8 if (target_info != 0)
|
|
0051abd8 {
|
|
0051abe0 target_info->interpolated_position.vtable = 0x79285c;
|
|
0051abe3 target_info->target_position.vtable = 0x79285c;
|
|
0051abe6 operator delete(target_info);
|
|
0051abee this->target_info = nullptr;
|
|
0051abd8 }
|
|
0051abd8
|
|
0051abf5 class LongNIHash<TargettedVoyeurInfo>* voyeur_table = this->voyeur_table;
|
|
0051abf5
|
|
0051abfa if (voyeur_table != 0)
|
|
0051abfa {
|
|
0051abfc LongNIHash<TargettedVoyeurInfo>::destroy_contents(voyeur_table);
|
|
0051ac01 class LongNIHash<TargettedVoyeurInfo>* voyeur_table_1 = this->voyeur_table;
|
|
0051ac01
|
|
0051ac06 if (voyeur_table_1 != 0)
|
|
0051ac06 {
|
|
0051ac0a LongNIHash<DetectionCylsphere>::flush(voyeur_table_1);
|
|
0051ac12 operator delete[](voyeur_table_1->buckets);
|
|
0051ac18 voyeur_table_1->buckets = 0;
|
|
0051ac1e operator delete(voyeur_table_1);
|
|
0051ac06 }
|
|
0051abfa }
|
|
0051abd0 }
|
|
```
|
|
|
|
Frees `target_info` (if any), then destroys+flushes+frees the entire
|
|
`voyeur_table` hash (all subscriber records). Does NOT notify anyone
|
|
(no `NotifyVoyeurOfEvent` call) — pure teardown, unlike `exit_world`
|
|
which explicitly notifies before the `TargetManager` itself is destroyed
|
|
elsewhere (line 281877, `CPhysicsObj`'s own dtor/cleanup path).
|
|
|
|
NOTE (BN mislabel): `LongNIHash<DetectionCylsphere>::flush` at 0051ac0a
|
|
— actually `LongNIHash<TargettedVoyeurInfo>::flush` (`voyeur_table_1` is
|
|
typed `LongNIHash<TargettedVoyeurInfo>*`).
|
|
|
|
---
|
|
|
|
## `CPhysicsObj`-level seams (no dedicated `MakeTargetManager`)
|
|
|
|
Grep for `MakeTargetManager` returned nothing — **retail does not have a
|
|
factory function by that name.** Instead, `TargetManager` construction is
|
|
**lazily inlined** at the two call sites that first need it:
|
|
`CPhysicsObj::set_target` (becoming a voyeur OF something) and
|
|
`CPhysicsObj::add_voyeur` (gaining a voyeur watching us). Both follow the
|
|
same `if (this->target_manager == 0) { alloc + placement-new
|
|
TargetManager(this) }` pattern.
|
|
|
|
### `CPhysicsObj::set_target` — 0050ed30
|
|
|
|
```c
|
|
0050ed30 void __thiscall CPhysicsObj::set_target(class CPhysicsObj* this, uint32_t arg2, uint32_t arg3, float arg4, double arg5)
|
|
|
|
0050ed30 {
|
|
0050ed3b if (this->target_manager == 0)
|
|
0050ed3b {
|
|
0050ed3f void* eax_1 = operator new(0x18);
|
|
0050ed49 class TargetManager* eax_2;
|
|
0050ed49
|
|
0050ed49 if (eax_1 == 0)
|
|
0050ed55 eax_2 = nullptr;
|
|
0050ed49 else
|
|
0050ed4e eax_2 = TargetManager::TargetManager(eax_1, this);
|
|
0050ed4e
|
|
0050ed57 this->target_manager = eax_2;
|
|
0050ed3b }
|
|
0050ed3b
|
|
0050ed69 int32_t var_8_2 = *(uint32_t*)((char*)arg5)[4];
|
|
0050ed7c TargetManager::SetTarget(this->target_manager, arg2, arg3, arg4, arg5);
|
|
0050ed30 }
|
|
```
|
|
|
|
Lazy-construct (0x18 = 24 bytes, matches `TargetManager`'s 4 fields:
|
|
ptr+ptr+ptr+long double(?) — actually `physobj`+`target_info`+`voyeur_table`
|
|
= 12 bytes + `last_update_time` long double = likely 8-12 bytes padded to
|
|
24), then forward to `TargetManager::SetTarget`.
|
|
|
|
### `CPhysicsObj::clear_target` — 0050ed90
|
|
|
|
```c
|
|
0050ed90 void __fastcall CPhysicsObj::clear_target(class CPhysicsObj* this)
|
|
|
|
0050ed90 {
|
|
0050ed90 class TargetManager* target_manager = this->target_manager;
|
|
0050ed90
|
|
0050ed98 if (target_manager == 0)
|
|
0050ed9f return;
|
|
0050ed9f
|
|
0050ed9a /* tailcall */
|
|
0050ed9a return TargetManager::ClearTarget(target_manager);
|
|
0050ed90 }
|
|
```
|
|
|
|
No-op if no `target_manager` exists yet (does NOT lazily construct just
|
|
to clear). Tail-calls `TargetManager::ClearTarget` otherwise.
|
|
|
|
### `CPhysicsObj::set_target_quantum` — 0050eda0
|
|
|
|
```c
|
|
0050eda0 void __thiscall CPhysicsObj::set_target_quantum(class CPhysicsObj* this, double arg2)
|
|
|
|
0050eda0 {
|
|
0050eda0 class TargetManager* target_manager = this->target_manager;
|
|
0050eda0
|
|
0050eda8 if (target_manager != 0)
|
|
0050eda8 {
|
|
0050edb2 int32_t var_4_1 = *(uint32_t*)((char*)arg2)[4];
|
|
0050edb4 TargetManager::SetTargetQuantum(target_manager, arg2);
|
|
0050eda8 }
|
|
0050eda0 }
|
|
```
|
|
|
|
### `CPhysicsObj::get_target_quantum` — 0050edc0
|
|
|
|
```c
|
|
0050edc0 class TargetManager* __fastcall CPhysicsObj::get_target_quantum(class CPhysicsObj const* this)
|
|
|
|
0050edc0 {
|
|
0050edc0 class TargetManager* result = this->target_manager;
|
|
0050edc0
|
|
0050edc8 if (result != 0)
|
|
0050edc8 {
|
|
0050edca result = result->target_info;
|
|
0050edca
|
|
0050edcf if (result != 0)
|
|
0050edd1 result->last_update_time;
|
|
0050edc8 }
|
|
0050edc8
|
|
0050eddb return result;
|
|
0050edc0 }
|
|
```
|
|
|
|
NOTE: despite the name `get_target_quantum`, the return-type/body as
|
|
decompiled reads `target_info->last_update_time` in an expression-statement
|
|
with the result DISCARDED, then returns `result` which by that point
|
|
holds `this->target_manager->target_info` (a pointer!), not a `quantum`
|
|
double. This strongly looks like **BN mis-decompiling a `long double`
|
|
return through EAX/x87** — the real function almost certainly returns
|
|
`target_info->quantum` (a `long double`, returned via `st0`, which BN's
|
|
pseudo-C sometimes fails to model and instead shows the last pointer left
|
|
in a GPR). The caller at `MoveToManager::UseTime` (0052a07e,
|
|
`fsubr st0, [esp+8]` immediately after the call) treats the return value
|
|
as an x87 float on the FPU stack, confirming this reads a `long double`
|
|
via `st0`, NOT a pointer. **FLAG FOR LEAD: this function's decompiled
|
|
body/return type is unreliable; trust the CALLER's x87 usage (treats
|
|
return as `long double quantum`) over the shown C.**
|
|
|
|
### `CPhysicsObj::receive_target_update` — 0050ede0
|
|
|
|
```c
|
|
0050ede0 void __thiscall CPhysicsObj::receive_target_update(class CPhysicsObj* this, class TargetInfo const* arg2)
|
|
|
|
0050ede0 {
|
|
0050ede0 class TargetManager* target_manager = this->target_manager;
|
|
0050ede0
|
|
0050ede8 if (target_manager == 0)
|
|
0050edef return;
|
|
0050edef
|
|
0050edea /* tailcall */
|
|
0050edea return TargetManager::ReceiveUpdate(target_manager, arg2);
|
|
0050ede0 }
|
|
```
|
|
|
|
No-op if no manager. This is the entry point `SendVoyeurUpdate` tail-calls
|
|
into on the recipient object.
|
|
|
|
### `CPhysicsObj::add_voyeur` — 0050ee00
|
|
|
|
```c
|
|
0050ee00 void __thiscall CPhysicsObj::add_voyeur(class CPhysicsObj* this, uint32_t arg2, float arg3, float arg4)
|
|
|
|
0050ee00 {
|
|
0050ee00 int32_t __saved_esi_1;
|
|
0050ee00 int32_t __saved_esi = __saved_esi_1;
|
|
0050ee00
|
|
0050ee0b if (this->target_manager == 0)
|
|
0050ee0b {
|
|
0050ee0d int64_t var_c;
|
|
0050ee0d *(uint32_t*)((char*)var_c)[4] = 0x18;
|
|
0050ee0f int32_t eax_1 = operator new();
|
|
0050ee19 class TargetManager* eax_2;
|
|
0050ee19
|
|
0050ee19 if (eax_1 == 0)
|
|
0050ee25 eax_2 = nullptr;
|
|
0050ee19 else
|
|
0050ee19 {
|
|
0050ee1b *(uint32_t*)((char*)var_c)[4] = this;
|
|
0050ee1e eax_2 = TargetManager::TargetManager(eax_1);
|
|
0050ee19 }
|
|
0050ee19
|
|
0050ee27 this->target_manager = eax_2;
|
|
0050ee0b }
|
|
0050ee0b
|
|
0050ee47 TargetManager::AddVoyeur(this->target_manager, arg2, arg3, ((double)((long double)arg4)));
|
|
0050ee00 }
|
|
```
|
|
|
|
Same lazy-construct pattern as `set_target`, then forward to
|
|
`TargetManager::AddVoyeur`. `arg2` = voyeur object id, `arg3` = radius,
|
|
`arg4` = quantum (float, widened to double for the call).
|
|
|
|
### `CPhysicsObj::remove_voyeur` — 0050ee50
|
|
|
|
```c
|
|
0050ee50 int32_t __thiscall CPhysicsObj::remove_voyeur(class CPhysicsObj* this, uint32_t arg2)
|
|
|
|
0050ee50 {
|
|
0050ee50 class TargetManager* target_manager = this->target_manager;
|
|
0050ee50
|
|
0050ee58 if (target_manager == 0)
|
|
0050ee61 return 0;
|
|
0050ee61
|
|
0050ee5a /* tailcall */
|
|
0050ee5a return TargetManager::RemoveVoyeur(target_manager, arg2);
|
|
0050ee50 }
|
|
```
|
|
|
|
---
|
|
|
|
## Callers (per-tick driver + `SetTarget` producers)
|
|
|
|
### Per-tick driver: `CPhysicsObj::UpdateObjectInternal` — 005156b0 (excerpt)
|
|
|
|
```c
|
|
005159a2 if (part_array_1 != 0)
|
|
005159a4 CPartArray::HandleMovement(part_array_1);
|
|
005159a4
|
|
005159a9 class PositionManager* position_manager = this->position_manager;
|
|
005159a9
|
|
005159b1 if (position_manager != 0)
|
|
005159b3 PositionManager::UseTime(position_manager);
|
|
```
|
|
|
|
and immediately preceding (full excerpt, lines 283730-283753):
|
|
|
|
```c
|
|
00515970 class DetectionManager* detection_manager = this->detection_manager;
|
|
00515970
|
|
00515978 if (detection_manager != 0)
|
|
0051597a DetectionManager::CheckDetection(detection_manager);
|
|
0051597a
|
|
0051597f class TargetManager* target_manager = this->target_manager;
|
|
0051597f
|
|
00515987 if (target_manager != 0)
|
|
00515989 TargetManager::HandleTargetting(target_manager);
|
|
00515989
|
|
0051598e class MovementManager* movement_manager = this->movement_manager;
|
|
0051598e
|
|
00515996 if (movement_manager != 0)
|
|
00515998 MovementManager::UseTime(movement_manager);
|
|
00515998
|
|
0051599d class CPartArray* part_array_1 = this->part_array;
|
|
0051599d
|
|
005159a2 if (part_array_1 != 0)
|
|
005159a4 CPartArray::HandleMovement(part_array_1);
|
|
005159a4
|
|
005159a9 class PositionManager* position_manager = this->position_manager;
|
|
005159a9
|
|
005159b1 if (position_manager != 0)
|
|
005159b3 PositionManager::UseTime(position_manager);
|
|
```
|
|
|
|
**Confirms the per-tick fan-out order inside `CPhysicsObj::UpdateObjectInternal`
|
|
(the retail per-object physics-tick function, called every physics tick
|
|
per live `CPhysicsObj`):**
|
|
1. `DetectionManager::CheckDetection`
|
|
2. `TargetManager::HandleTargetting` ← the voyeur-subscription tick (no separate `UseTime`)
|
|
3. `MovementManager::UseTime`
|
|
4. `CPartArray::HandleMovement`
|
|
5. `PositionManager::UseTime`
|
|
|
|
This ordering matters for R5's facade design: TargetManager ticks BEFORE
|
|
MovementManager and PositionManager each frame, so any target-driven
|
|
`HandleUpdateTarget` callback into those two managers (see next section)
|
|
happens with THIS tick's fresh distance-check data, ahead of movement/position
|
|
processing the same tick.
|
|
|
|
### `CPhysicsObj::HandleUpdateTarget` — 00512bc0 (fan-out target)
|
|
|
|
```c
|
|
00512bc0 void __thiscall CPhysicsObj::HandleUpdateTarget(class CPhysicsObj* this, class TargetInfo arg2)
|
|
|
|
00512bc0 {
|
|
00512bc9 if (arg2.context_id == 0)
|
|
00512bc9 {
|
|
00512bd3 void var_d4;
|
|
00512bd3
|
|
00512bd3 if (this->movement_manager != 0)
|
|
00512bd3 {
|
|
00512be5 TargetInfo::TargetInfo(&var_d4, &arg2);
|
|
00512bf0 MovementManager::HandleUpdateTarget(this->movement_manager, var_d4);
|
|
00512bd3 }
|
|
00512bd3
|
|
00512bfd if (this->position_manager != 0)
|
|
00512bfd {
|
|
00512c0f TargetInfo::TargetInfo(&var_d4, &arg2);
|
|
00512c1a PositionManager::HandleUpdateTarget(this->position_manager, var_d4);
|
|
00512bfd }
|
|
00512bc9 }
|
|
00512bc0 }
|
|
```
|
|
|
|
Only fans out when `arg2.context_id == 0` (context_id nonzero presumably
|
|
reserved for a different consumer not wired here, e.g. quest/AI scripted
|
|
targeting — not confirmed in this extract). Copy-constructs a fresh
|
|
`TargetInfo` per recipient and forwards to **both**
|
|
`MovementManager::HandleUpdateTarget` and `PositionManager::HandleUpdateTarget`
|
|
unconditionally (both get the callback if both managers exist — not an
|
|
either/or). `TargetManager::ReceiveUpdate`, `TargetManager::HandleTargetting`
|
|
(timeout path), and `TargetManager::SetTarget` (the `arg3==0` clear path)
|
|
are the three call sites feeding this fan-out.
|
|
|
|
### `SetTarget` producers
|
|
|
|
**`MoveToManager::MoveToObject`** — 00529680 (excerpt, the `set_target` call):
|
|
|
|
```c
|
|
00529791 if (arg3 == physics_obj_2->id)
|
|
00529791 {
|
|
00529795 edx = MoveToManager::CleanUp(this);
|
|
00529795 goto label_52979a;
|
|
00529791 }
|
|
00529791
|
|
005297b8 int32_t var_50_4 = 0;
|
|
005297c4 CPhysicsObj::set_target(physics_obj_2, 0, this->top_level_object_id, 0.5f, 0f);
|
|
```
|
|
|
|
**`MoveToManager::TurnToObject`** — 005297d0 (excerpt):
|
|
|
|
```c
|
|
00529900 int32_t __saved_edi_2 = 0;
|
|
0052990c this->initialized = 0;
|
|
00529916 CPhysicsObj::set_target(physics_obj_1, 0, arg3, 0.5f, 0f);
|
|
```
|
|
|
|
Both `MoveToManager` entry points that establish "move to / turn to a
|
|
specific object" call `CPhysicsObj::set_target(physobj, context_id=0,
|
|
target_object_id, radius=0.5, quantum=0.0)` — a **zero quantum**,
|
|
meaning (per `HandleTargetting`'s gate) the tick-driven resend is
|
|
effectively "as fast as the 0.5s-ish quantum-check tick allows" rather
|
|
than throttled further; radius is a fixed 0.5 (game units — i.e. "notify
|
|
me if the target moves more than half a unit from what I last knew").
|
|
|
|
**`MoveToManager::CleanUp`** — clears the target when movement ends
|
|
(excerpt, line 306731-306732):
|
|
|
|
```c
|
|
0052962c if ((this->top_level_object_id != 0 && this->movement_type != Invalid))
|
|
00529634 CPhysicsObj::clear_target(this->physics_obj);
|
|
```
|
|
|
|
**`MoveToManager::UseTime`** — adaptive quantum re-tuning (heavily
|
|
x87-mangled excerpt, lines 307376-307434): computes a candidate quantum
|
|
from `Position::distance(starting_position, physics_obj->m_position)` and
|
|
`get_velocity`-derived speed via a sqrt-based formula (`fsqrt` visible at
|
|
0052a057), compares the delta between the new candidate and the current
|
|
`CPhysicsObj::get_target_quantum()` value against a `1.0` threshold and
|
|
`0.1` epsilon guard, and if the change is significant enough calls
|
|
`CPhysicsObj::set_target_quantum(physics_obj, var_88_3)`. **NOTE: this
|
|
whole block is FCMP/x87-mangled past reliable hand-decoding** — several
|
|
lines are literal `/* unimplemented {fld/fmul/fsqrt/...} */` placeholders
|
|
BN could not lower into pseudo-C at all. Treat as "adaptive quantum
|
|
retuning happens somewhere in `MoveToManager::UseTime` based on distance
|
|
traveled since move-start and current speed" — do NOT port the exact
|
|
formula from this extract without a fresh, careful manual disassembly
|
|
pass; the pseudo-C here is not trustworthy enough to line-port.
|
|
|
|
### `StickyManager` — a second, independent `TargetManager` consumer
|
|
|
|
Not explicitly requested but discovered while tracing `clear_target`
|
|
callers — **directly relevant to R5's PositionManager/Sticky scope**:
|
|
|
|
**`StickyManager::StickTo`** — 00555710 (excerpt): tears down any
|
|
existing target then calls
|
|
`CPhysicsObj::set_target(physics_obj_1, 0, arg2, 0.5f, 0.5)` — same
|
|
radius (0.5) as `MoveToManager` but a **nonzero 0.5s quantum** this time
|
|
(throttled resend, unlike MoveToManager's 0.0).
|
|
|
|
**`StickyManager::UseTime`** — 00555610: on a `Timer::cur_time >=
|
|
sticky_timeout_time` gate (mangled FCMP, same `0x41`-mask shape as
|
|
`CheckAndUpdateVoyeur` → reads as `>=`), clears `target_id` and calls
|
|
`CPhysicsObj::clear_target` + `CPhysicsObj::interrupt_current_movement`.
|
|
|
|
**`StickyManager::Destroy`** — 00555650: unconditional `clear_target` if
|
|
`target_id != 0` during teardown.
|
|
|
|
**`StickyManager::HandleUpdateTarget`** — 00555780: the `TargetInfo`
|
|
consumer callback fanned out from `CPhysicsObj::HandleUpdateTarget`.
|
|
Ignores updates whose `object_id` doesn't match `this->target_id`. On a
|
|
match: if `status == Ok_TargetStatus`, copies `target_position` into its
|
|
own tracked position and marks `initialized = 1`; otherwise (any other
|
|
status, including timeout/exit/teleport) tears itself down
|
|
(`clear_target` + `interrupt_current_movement`) exactly like `UseTime`'s
|
|
timeout path.
|
|
|
|
This confirms **two independent per-`CPhysicsObj` consumers of the same
|
|
`TargetManager`/voyeur machinery**: `MoveToManager` (move/turn-to-object,
|
|
quantum=0, immediate-as-tick-allows resend) and `StickyManager`
|
|
(quantum=0.5s throttled resend, timeout-driven unstick). Both receive
|
|
their updates through the identical `CPhysicsObj::HandleUpdateTarget` →
|
|
`{MovementManager,PositionManager}::HandleUpdateTarget` fan-out — i.e.
|
|
`StickyManager::HandleUpdateTarget` is presumably reached VIA
|
|
`PositionManager::HandleUpdateTarget` (StickyManager is a PositionManager
|
|
sub-component per the project's existing R5 handoff doc), not directly
|
|
off `CPhysicsObj`.
|