# 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 *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` (acclient.h:31606, id 3483) ```c struct __cppobj LongNIHash { LongNIHashData **buckets; int table_size; }; ``` ### `struct LongNIHashIter` (acclient.h:57648) ```c struct __cppobj LongNIHashIter { LongNIHash *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* voyeur_table = this->voyeur_table; 0051a6f6 0051a6fb if (voyeur_table != 0) 0051a6fb { 0051a703 int32_t* var_10; 0051a703 LongNIHashIter::LongNIHashIter(&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::LongNIHashIter(&var_10, voyeur_table)` is BN reusing the `LongNIHashIter` template instantiation's mangled name for what is actually iterating `this->voyeur_table`, a `LongNIHash*`. The real type is `LongNIHashIter` (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` symbol in this file's TargetManager functions as actually `LongNIHashIter`. ### `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* voyeur_table = this->voyeur_table; 0051a838 0051a840 if (voyeur_table == 0) 0051a840 { 0051a88f void* eax_5 = operator new(8); 0051a899 class LongNIHash* eax_6; 0051a899 0051a899 if (eax_5 == 0) 0051a8a6 eax_6 = nullptr; 0051a899 else 0051a89f eax_6 = LongNIHash::LongNIHash(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::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::LongNIHash` and `LongNIHash::add` are BN reusing the `DetectionInfo` hash template instantiation's mangled symbol for what is actually `LongNIHash`'s ctor/add — same class of mislabel as above (`voyeur_table` is declared `LongNIHash*`). ### `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* voyeur_table = this->voyeur_table; 0051aafd 0051ab02 if (voyeur_table != 0) 0051ab02 { 0051ab09 void var_10; 0051ab09 int32_t edx_1 = LongNIHashIter::LongNIHashIter(&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::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::LongNIHashIter` mislabel as in `NotifyVoyeurOfEvent` — actually `LongNIHashIter`. The subsequent `LongNIHashIter::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* voyeur_table = this->voyeur_table; 0051ad90 0051ad95 if (voyeur_table != 0) 0051ad95 { 0051ad9c class DetectionCylsphere* eax_2 = LongNIHash::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::remove` — same mislabel pattern, actually operating on `LongNIHash` (confirmed by the `this` parameter type `LongNIHash*`). 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* voyeur_table = this->voyeur_table; 0051abf5 0051abfa if (voyeur_table != 0) 0051abfa { 0051abfc LongNIHash::destroy_contents(voyeur_table); 0051ac01 class LongNIHash* voyeur_table_1 = this->voyeur_table; 0051ac01 0051ac06 if (voyeur_table_1 != 0) 0051ac06 { 0051ac0a LongNIHash::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::flush` at 0051ac0a — actually `LongNIHash::flush` (`voyeur_table_1` is typed `LongNIHash*`). --- ## `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`.