# Hard-Teleport (Branch A) + Sequence-Number Plumbing — Retail Pseudo-C Extract **Date:** 2026-05-04 **Source:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR PDB-named pseudo-C) **Cross-reference:** `docs/research/named-retail/acclient.h`, `references/ACE/Source/ACE.Server/Network/Structure/PositionPack.cs`, `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Types/PositionPack.generated.cs` **Companion:** `03-up-routing.md` (the standard tri-state router) This note drills into Branch A (hard-teleport) of `MoveOrTeleport`, the `newer_event` 16-bit-wrap helper, the `update_times[]` slot map, the `SmartBox::HandleReceivedPosition` instance-stamp gate, the `SetPosition`/`SetPositionInternal` flag-bit decoder used by Branch A, the wire layout of the four PositionPack u16 stamps, and acdream's current end-to-end gap. --- ## 1. `CPhysicsObj::newer_event` — the 16-bit-wrap stamp comparator **File line:** 90712 — `0x00451b10` Verbatim retail (decompiler abs-delta noise replaced with the arithmetic that's actually emitted): ```c 90712 int32_t __thiscall CPhysicsObj::newer_event( 90712 class CPhysicsObj* this, 90712 enum PhysicsTimeStamp arg2, // slot index 0..8 90712 uint16_t arg3) // wire-side new stamp 90712 { 90716 esi = this->update_times[arg2]; // stored stamp 90718 edi = arg3; // received stamp 90721 // signed delta: diff = (int32)((uint32)edi - (uint32)esi) 90722 // abs(diff) → eax_4 90723 eax_4 = abs(diff); 90726 if (eax_4 > 0x7fff) 90727 c = (edi < esi); // wrapped: received is OLDER 90728 else 90729 c = (esi < edi); // not wrapped: received is NEWER if c 90731 if (diff == 0) 90732 return 0; // EQUAL → not newer 90734 this->update_times[arg2] = edi; // STORE new value 90735 return 1; // NEWER → 1 90712 } ``` **Decoder note.** The Binary Ninja output expands the abs(delta) into `HIGHD/LOWD ^ - -` salad (lines 90721–90723) and the early-return at 90731 reads as `-((eax_4 - eax_4)) == 0`, which the optimizer collapses into `if (diff == 0) return 0;`. The stripped semantic equivalent (matches ACE `WorldObject_Networking.cs::is_newer_event`, `PhysicsObj.cs::CheckIsNewer`): ```csharp static bool IsNewer16(ushort prev, ushort received) { int diff = (ushort)(received - prev); // unsigned 16-bit subtract if (diff == 0) return false; // equal → not newer return diff < 0x8000; // <32k forward → newer; ≥32k → older } ``` **Side effect.** This is **NOT pure** — the function writes `update_times[arg2] = edi` when it returns 1. So the four `newer_event` calls inside `HandleReceivedPosition` / `MoveOrTeleport` simultaneously test *and* commit the new stamp. Any port must preserve "test-and-set" semantics or the next UP will re-fire as if it were the first. --- ## 2. `update_times[9]` slot map **Header:** `acclient.h:6084` (verbatim): ```c enum PhysicsTimeStamp { POSITION_TS = 0x0, MOVEMENT_TS = 0x1, STATE_TS = 0x2, VECTOR_TS = 0x3, TELEPORT_TS = 0x4, SERVER_CONTROLLED_MOVE_TS = 0x5, FORCE_POSITION_TS = 0x6, OBJDESC_TS = 0x7, INSTANCE_TS = 0x8, NUM_PHYSICS_TS = 0x9, }; ``` `CPhysicsObj` carries `unsigned __int16 update_times[9]` (`acclient.h:30738`). The four PositionPack u16s map onto **four** of these slots: | Wire field (PositionPack order) | Slot | Slot index | Used in MoveOrTeleport? | |---|---|---|---| | `instance_timestamp` | INSTANCE_TS | 8 | gate in UnpackPositionEvent (must EQUAL) | | `position_timestamp` | POSITION_TS | 0 | gate in HandleReceivedPosition (must be newer) | | `teleport_timestamp` | TELEPORT_TS | 4 | **Branch A trigger** in MoveOrTeleport | | `force_position_timestamp` | FORCE_POSITION_TS | 6 | local-player BlipPlayer trigger | The remaining slots (MOVEMENT_TS, STATE_TS, VECTOR_TS, SERVER_CONTROLLED_MOVE_TS, OBJDESC_TS) are stamped by separate opcodes — UpdateMotion (0xF74C), VectorUpdate (0xF74E), ObjDesc, etc. — not by 0xF748. --- ## 3. `SmartBox::HandleReceivedPosition` (full) — staleness gates around Branch A **File line:** 92896 — `0x00453fd0` Verbatim retail (de-noised from line numbers preserved): ```c 92896 void __thiscall SmartBox::HandleReceivedPosition( 92896 class SmartBox* this, 92896 class CPhysicsObj* arg2, // target object 92896 class Position const* arg3, // received position 92896 uint32_t arg4, // placement_id 92896 int32_t arg5, // has_contact (0=air, !=0 grounded) 92896 class AC1Legacy::Vector3 const* arg6, // velocity 92896 uint16_t arg7, // POSITION_TS (position_timestamp) 92896 uint16_t arg8, // TELEPORT_TS (teleport_timestamp / move-seq) 92896 uint16_t arg9) // FORCE_POSITION_TS 92898 { 92901 objcell_id = arg3->objcell_id; 92902 var_48 = 0x796910; // Position vtable 92904 Frame::operator=(&var_40, &arg3->frame); // local copy of frame 92905 player = this->player; // ───────── (1) LOCAL PLAYER force-position blip ───────── 92907 if (arg2 == player && newer_event(player, FORCE_POSITION_TS, arg9) != 0) { 92910 ebp = player->update_times[TELEPORT_TS]; // peek-only: is the teleport_ts EQUAL to ours? 92923 if (signed_delta_is_zero(ebp, arg8)) { 92925 CPhysicsObj::get_heading(player); 92927 Frame::set_heading(&var_40, currentHeading); 92928 SmartBox::BlipPlayer(this, &var_48); // server forced our pos 92929 player->update_times[POSITION_TS] = arg7; 92931 cmdinterp->vtable->SendPositionEvent(cmdinterp); 92932 return; } } // ───────── (2) PEEK ebp = current POSITION_TS, run TEST-AND-SET ───────── 92936 ebp = arg2->update_times[POSITION_TS]; // save old stamp 92938 if (newer_event(arg2, POSITION_TS, arg7) == 0) { // not newer — STALE position; nothing to do unless the teleport // stamp is somehow different (logging only). 92941 esi = arg2->update_times[TELEPORT_TS]; 92954 if (signed_delta_nonzero(arg8, esi)) 92955 ++error_count; 92957 return; // ← stale UP: no body change } // ───────── (3) TELEPORT_TS sanity vs received arg8 ───────── 92961 ecx_4 = arg2->update_times[TELEPORT_TS]; 92974 if (signed_delta_nonzero(ecx_4, arg8)) { // received teleport_ts is OLDER than recorded — rewind position // stamp & bail (this branch is safety, not a normal path). 92976 arg2->update_times[POSITION_TS] = ebp; 92977 return; } // ───────── (4) Detach from any parent + re-place ───────── 92982 parent = arg2->parent; 92982 if (parent != 0 && parent->id != this->player_id) { 92984 weenie = CObjectMaint::GetWeenieObject(arg2->id); 92986 if (weenie != 0) 92987 weenie->vtable->SetParentedState(weenie, 0); } 92990 CPhysicsObj::unset_parent(arg2); 92992 if (CPhysicsObj::HasAnims(arg2) == 0) 92993 CPhysicsObj::SetPlacementFrame(arg2, arg4, 1); // ───────── (5) REMOTE OBJECT branch (the L.3 target) ───────── 92995 if (arg2 != this->player) { 92997 if (CPhysicsObj::MoveOrTeleport(arg2, &var_48, arg8, arg5, arg6) != 0) { // … ConstrainTo with start/max constraint distances … 93007 CPhysicsObj::ConstrainTo(arg2, &arg2->m_position, …); } 93010 return; } // ───────── (6) LOCAL PLAYER teleport branch ───────── 93013 if (CPhysicsObj::newer_event(arg2, TELEPORT_TS, arg8) != 0) { 93015 SmartBox::TeleportPlayer(this, &var_48); // ConstrainTo + zero velocity 93024 CPhysicsObj::ConstrainTo(arg2, &var_48, …); 93029 CPhysicsObj::set_velocity(player, &zero, 1); 93030 return; } // ───────── (7) LOCAL PLAYER soft-correct ───────── 93041 CPhysicsObj::ConstrainTo(this->player, &var_48, …); 93044 if (cmdinterp->UsePositionFromServer() != 0 && arg5 != 0) { 93047 autonomyLevel = cmdinterp->GetAutonomyLevel(); 93049 CPhysicsObj::InterpolateTo(arg2, &var_48, autonomyLevel != 0); } 92896 } ``` **Critical: the order of stamp updates.** 1. `INSTANCE_TS` is gated equality-only in `UnpackPositionEvent` (line 93081) — **never** stored as "newer", just verified equal. 2. `POSITION_TS` is test-and-set at line 92938 by `newer_event(POSITION_TS, arg7)`. 3. `TELEPORT_TS` is **peeked** at line 92974 (sanity), then test-and-set at line 93013 (player branch) **or** line 284325 (remote branch via MoveOrTeleport). 4. `FORCE_POSITION_TS` is test-and-set at line 92907. So a single 0xF748 stamps up to **3** slots: POSITION_TS always (if newer), and either TELEPORT_TS (if teleport advanced) or FORCE_POSITION_TS (if force advanced) but generally not both. --- ## 4. `SmartBox::UnpackPositionEvent` — INSTANCE_TS staleness gate **File line:** 93055 — `0x004542c0` Already covered in `03-up-routing.md` Section 2. The salient points re-stated: ```c 93055 enum NetBlobProcessedStatus SmartBox::UnpackPositionEvent(...) 93055 { 93059 PositionPack::PositionPack(&var_68); 93060 PositionPack::UnPack(&var_68, arg3, arg4); // ← reads bytes 93061 eax_1 = CObjectMaint::GetObjectA(this->m_pObjMaint, arg2); 93063 if (eax_1 != 0) { 93065 ecx_4 = eax_1->update_times[INSTANCE_TS=8]; 93081 if (signed_delta_is_zero(received_inst, ecx_4)) { // ★ EQUAL ⇒ proceed; UN-EQUAL is the loggedout / queue path 93083 if (ecx_4 != received_inst) 93084 return NETBLOB_LOGGED_OUT; // (= 2) 93092 SmartBox::HandleReceivedPosition(this, eax_1, 93092 &recvPos, placement_id, has_contact, &velocity, 93092 position_ts, teleport_ts, force_position_ts); 93093 return NETBLOB_PROCESSED_OK; // (= 1) } 93095 } 93097 return NETBLOB_QUEUED; // (= 4) 93055 } ``` The instance gate is "equality-or-drop." Reasoning: instance stamp counts character logins. If the server has bumped it (player relogged) the client's still-cached object is the OLD instance — defer or drop. UnPack consumed the bytes, so the buffer pointer advanced either way; only the side effect on the body is gated. The 16-bit-wrap math is identical to `newer_event`'s, but **without** the test-and-set side effect — INSTANCE_TS is bumped elsewhere (CreateObject path). --- ## 5. `CPhysicsObj::MoveOrTeleport` — Branch A dissection **File line:** 284304 — `0x00516330` (full extract in `03-up-routing.md`) Branch A specifically: ```c 284321 if (signed_delta_is_zero(this_1->update_times[TELEPORT_TS], arg3)) // (a) TELEPORT_TS sanity 284321 { 284325 eax_8 = CPhysicsObj::newer_event(this_1, TELEPORT_TS, arg3); // (b) test-and-set 284325 // ↑ 284325 // writes update_times[4]=arg3 if newer 284327 if (eax_8 != 0 || this_1->cell == 0) // (c) Branch A predicate 284327 { // ───── BRANCH A: HARD TELEPORT ───── 284329 int32_t var_70_3 = 1; // unused local 284330 CPhysicsObj::teleport_hook(this_1, edx_2); // (d) teleport_hook 284331 SetPositionStruct sps; 284332 SetPositionStruct::SetPositionStruct(&sps); 284333 SetPositionStruct::SetPosition(&sps, arg2); // copy received Position 284334 SetPositionStruct::SetFlags(&sps, 0x1012); // (e) Slide+Placement+SendPositionEvent 284335 CPhysicsObj::SetPosition(this_1, &sps); // run CTransition + place 284336 SetPositionStruct::~SetPositionStruct(&sps); 284337 return 1; } /* …Branch B / Branch C (see 03-up-routing.md) … */ } ``` **Important nuance — sanity vs trigger.** The `signed_delta_is_zero` at line 284321 is a *sanity* gate — it actually means "abs(delta) == 0", i.e. "the recorded TELEPORT_TS minus the received arg3 is zero, OR the abs-equality compare path was taken". In retail this path is taken when the wire stamp is **equal to or newer than** recorded (the OLDER case is filtered via the `signed_delta_nonzero` at 92974 in HandleReceivedPosition before MoveOrTeleport is called). Inside MoveOrTeleport, line 284325's `newer_event` is the **actual** "is-newer" decision, with side-effect. So in the abstract: | Wire `arg3` vs recorded `update_times[TELEPORT_TS]` | Path | |---|---| | OLDER (wrap-aware) | Already filtered at 92974 — never reaches MoveOrTeleport | | EQUAL | `newer_event` returns 0 ⇒ Branch A predicate evaluates `0 \|\| (cell==0)` — Branch A ONLY if cell unset; otherwise fall through to Branch B/C | | NEWER | `newer_event` returns 1, stores arg3 ⇒ Branch A predicate true ⇒ Branch A fires | And **`cell == 0`** is the bootstrap case — a `CPhysicsObj` that has been allocated but not yet placed (e.g., we got an UP for an entity mid-teleport, before its initial cell entry). Forces a hard placement even with equal stamp, because there's nowhere else to put it. ### `teleport_hook` (line 283115 — `0x00514ed0`) ```c 283115 void CPhysicsObj::teleport_hook(class CPhysicsObj* this, int32_t arg2) 283115 { 283118 if (this->movement_manager != 0) 283124 MovementManager::CancelMoveTo(this->movement_manager, ctx=0x3c); 283129 if (this->position_manager != 0) 283130 PositionManager::UnStick(this->position_manager); 283134 if (this->position_manager != 0) 283135 PositionManager::StopInterpolating(this->position_manager); // ← drops queue 283139 if (this->position_manager != 0) 283140 PositionManager::UnConstrain(this->position_manager); 283144 if (this->target_manager != 0) { 283146 TargetManager::ClearTarget(this->target_manager); 283147 TargetManager::NotifyVoyeurOfEvent(Teleported_TargetStatus); } 283150 CPhysicsObj::report_collision_end(this, 1); 283115 } ``` **What teleport_hook does (for the port):** 1. Cancels any in-progress MoveTo route (the AI's "go to door" command). 2. UnStick: clear the "stuck against wall" recovery state. 3. **StopInterpolating: clear the position queue** — required before the hard-snap so no stale waypoint pulls the body away. 4. UnConstrain: clear distance-from-anchor constraints. 5. Clear target lock; notify nearby observers we teleported. 6. Report a "collision end" so adjacent collision listeners stop tracking us. Steps 3 and 6 are the L.3-relevant ones for acdream. Steps 1, 2, 4, 5 are tangential to position routing. --- ## 6. `SetPosition` / `SetPositionInternal` flag bit decoder (0x1012) **Outer** `SetPosition` (line 284137 — `0x005160c0`) builds a CTransition over the body's spheres and forwards to the flag-decoder `SetPositionInternal`. The **flag-decoder** `SetPositionInternal` at line 284117 — `0x00516040`: ```c 284117 enum SetPositionError SetPositionInternal(class SetPositionStruct const* arg2, 284117 class CTransition* arg3) 284117 { 284120 if ((arg2->flags & 0x0200) != 0) // bit 9 = SCATTER 284121 return SetScatterPositionInternal(this, arg2, arg3); 284123 objcell_id = arg2->pos.objcell_id; 284124 var_48 = 0x796910; 284126 Frame::operator=(&var_40, &arg2->pos.frame); 284127 result = SetPositionInternal(this, &recvPos, arg2, arg3); // run normal path 284129 if (result != OK_SPE && (arg2->flags & 0x0100) != 0) // bit 8 = ALLOW_SCATTER_FALLBACK 284130 return SetScatterPositionInternal(this, arg2, arg3); 284132 return result; 284117 } ``` The middle dispatcher `SetPositionInternal(this, Position*, sps, trans)` (line 283892) runs `AdjustPosition` then dispatches based on `sps->flags & 0x20` and other bits, and ultimately calls the inner `SetPositionInternal(this, CTransition*)` (line 283399) to commit the body state. Inside `CheckPositionInternal` (line 280070 — `0x00511e90`) and the middle dispatcher we see the bit checks: | Bit | Hex | Used at | Semantic (cross-checked with ACE `SetPositionFlags.cs`) | |----:|----:|---|---| | 0 | 0x0001 | 284129 | **ALLOW_SCATTER_FALLBACK** — if the precise placement fails, retry as a scatter (±xrad/yrad) placement. | | 1 | 0x0002 | 284120 | **SCATTER** — go straight to scatter placement (used by `SetScatterPositionInternal`). | | 4 | 0x0010 | 280075, 280080 | **PLACEMENT_ALLOW_SLIDING** — the sphere will slide along walls during the placement search instead of being rejected on first contact. **Set in MoveOrTeleport's 0x1012.** | | 5 | 0x0020 | 283929 | **DO_NOT_LOAD_CELLS** — the cell array is left as "do_not_load_cells = 1"; used when streaming hasn't committed the cell yet. | | 8 | 0x0100 | 284129 | (see bit 0 — same word, different reading) | | 9 | 0x0200 | 284120 | (see bit 1 — same word, different reading) | | 11 | 0x0800 | — | **IS_PORTAL_TRAVEL** (per ACE) — not seen on the MoveOrTeleport paths. | | 12 | 0x1000 | always present in MoveOrTeleport flags | **SEND_POSITION_EVENT** — after placement, fire the position-event broadcast back through cmdinterp. **Set in MoveOrTeleport's 0x1012 and SetPositionSimple's 0x1002/0x1012.** | Note that ACE's `SetPositionFlags.cs` and the older WorldBuilder references use **different** symbolic names; the bit assignments above match what's actually decoded in the retail pseudo-C against `SetPositionStruct::flags` field (a `uint32_t`). **Decoded `0x1012`** = `0x1000 | 0x0010 | 0x0002`. Hmm — **bit 1 (0x0002)** is set too. Re-checking: the OR is **0x1000 + 0x0010 + 0x0002 = 0x1012**. This means MoveOrTeleport's Branch A also sets the **SCATTER** bit. That's the inverse of what prior research note (`03-up-routing.md` Section 5) recorded — let's re-derive from the source. Re-parse: - `0x1012` = binary `0001 0000 0001 0010`. Set bits: 1, 4, 12. - Bit 1 = SCATTER (line 284120 takes the scatter path on `& 0x0200`). - Wait — `0x0200` is bit **9**, not bit 1. Let me reread: ```c 284120 if ((*(uint8_t*)((char*)((int16_t)arg2->flags))[1] & 2) != 0) ``` The decompiler is reading byte 1 (bits 8-15) of `flags` and ANDing that byte with `2` — meaning it's testing **bit 9** of `flags`, not bit 1. So `0x0200` is the SCATTER bit. With `0x1012` = bits 1, 4, 12, the bit-1 (`0x0002`) is **NOT** the scatter bit — it's something else entirely. Updated table: | Decimal bit | Hex | Semantic | |--:|--:|---| | 0 | 0x0001 | (unknown — possibly SLIDE; see ACE) | | 1 | 0x0002 | **PLACEMENT** — the position is a fresh placement (vs. continuation of motion). | | 4 | 0x0010 | **PLACEMENT_ALLOW_SLIDING** — sphere slides during placement search (line 280075). | | 8 | 0x0100 | **ALLOW_SCATTER_FALLBACK** — retry scatter on placement failure (line 284129). | | 9 | 0x0200 | **SCATTER** — initial scatter placement (line 284120). | | 12 | 0x1000 | **SEND_POSITION_EVENT** — broadcast position after place. | So `0x1012 = SEND_POSITION_EVENT | PLACEMENT_ALLOW_SLIDING | PLACEMENT`. This is consistent with retail's `SetPositionSimple(slide=1)` which emits `0x1012` and the prior `03-up-routing.md` characterization (Slide + Placement + SendPositionEvent). `0x1002` (used by `SetPositionSimple(slide=0)`) = `SEND_POSITION_EVENT | PLACEMENT` — **without** the PLACEMENT_ALLOW_SLIDING bit, so the sphere must fit at the exact spot or fail. `0x0011` (used by `enter_world` line 284208) = `PLACEMENT | PLACEMENT_ALLOW_SLIDING` — enter without broadcasting an event. `0x0001` (used by `enter_world` line 284205 default) = `(unknown)` or possibly `SLIDE` — only one bit set; behaves as the bare-minimum placement. **Cross-reference recommendation:** ACE's `Source/ACE.Server/Physics/SetPositionFlags.cs` should be the ground truth for symbolic names. The retail PDB decompile shows the bit positions but not the names — Turbine compiled the enum out. --- ## 7. Wire format — PositionPack on 0xF748 **ACE `Source/ACE.Server/Network/Structure/PositionPack.cs:90-115`:** ```csharp public static void Write(this BinaryWriter writer, PositionPack position) { writer.Write((uint)position.Flags); // u32 PositionFlags writer.Write(position.Origin); // u32 cellId + Vector3 pos if ((flags & OrientationHasNoW) == 0) writer.Write(Rotation.W); if ((flags & OrientationHasNoX) == 0) writer.Write(Rotation.X); if ((flags & OrientationHasNoY) == 0) writer.Write(Rotation.Y); if ((flags & OrientationHasNoZ) == 0) writer.Write(Rotation.Z); if ((flags & HasVelocity) != 0) writer.Write(position.Velocity); // 3xf32 if ((flags & HasPlacementID) != 0) writer.Write((uint)position.PlacementID); writer.Write(position.InstanceSequence); // u16 writer.Write(position.PositionSequence); // u16 writer.Write(position.TeleportSequence); // u16 writer.Write(position.ForcePositionSequence); // u16 } ``` **Chorizite generated parser (`PositionPack.generated.cs:65-91`) matches.** The wire order at the end is: ``` ... ObjectInstanceSequence u16 ... ObjectPositionSequence u16 ... ObjectTeleportSequence u16 ... ObjectForcePositionSequence u16 ``` **ACE's TeleportSequence advance logic** (`PositionPack.cs:46-54`): ```csharp InstanceSequence = wo.Sequences.GetCurrentSequence(SequenceType.ObjectInstance); PositionSequence = wo.Sequences.GetNextSequence(SequenceType.ObjectPosition); // ← always advanced if (adminMove) TeleportSequence = wo.Sequences.GetNextSequence(SequenceType.ObjectTeleport); else TeleportSequence = wo.Sequences.GetCurrentSequence(SequenceType.ObjectTeleport); ForcePositionSequence = wo.Sequences.GetCurrentSequence(SequenceType.ObjectForcePosition); ``` **Critical: TeleportSequence advances ONLY when `adminMove=true`.** `adminMove` is a parameter of `WorldObject.SendUpdatePosition(bool adminMove = false)` (`WorldObject_Networking.cs:430`). Searching the ACE codebase for `SendUpdatePosition(true)` returns 0 hits (verified via grep). The parameter is documented as `"only used if admin is teleporting a non-player object"` (line 429 comment) — i.e., GM `@teleto` a creature. **For player teleport (portal travel, recall, lifestone, death respawn):** ACE sends the **dedicated** `GameMessagePlayerTeleport` (opcode **0xF751**) which carries only the next ObjectTeleport stamp (`GameMessagePlayerTeleport.cs:10`): ```csharp Writer.Write(player.Sequences.GetNextSequence(Sequence.SequenceType.ObjectTeleport)); ``` …then immediately follows with `SendUpdatePosition()` carrying the *new* (already-advanced) TeleportSequence (`Player_Location.cs:686-694`). **Net effect for remote-observer ports (acdream's case): the TeleportSequence in 0xF748 advances when:** 1. The remote was a player who teleported (PlayerTeleport advanced their own seq, then their next UP carries the new value). 2. A GM `@teleto`-ed a creature (admin code path sets `adminMove=true`). **The TeleportSequence does NOT advance for:** - Normal walking / running movement - Normal AI patrol - Mob-hunt path updates - Position-only correction broadcasts - Force-position blips (those use ForcePositionSequence) So Branch A in retail fires on remotes specifically when a player just portal-jumped, GM-teleported, lifestone-recalled, or respawned. **Test cases for the L.3 port:** 1. Cast a portal recall spell while a remote observer watches you. 2. Step into a portal while another character is nearby. 3. Die — respawn at lifestone with a remote watching. 4. `@teleto` your character via GM command while another's nearby. --- ## 8. Cross-check: acdream's current sequence-number plumbing ### 8a. Inbound parser `src/AcDream.Core.Net/Messages/UpdatePosition.cs:68-70, 152-159`: ```csharp public readonly record struct Parsed( uint Guid, CreateObject.ServerPosition Position, System.Numerics.Vector3? Velocity, uint? PlacementId, bool IsGrounded, ushort InstanceSequence = 0, ushort TeleportSequence = 0, ushort ForcePositionSequence = 0); ``` The four u16s are read at parse (file lines 152-159). PositionSequence is consumed for buffer alignment but **not stored** (comment: "not tracked by movement"). ### 8b. WorldSession dispatch `src/AcDream.Core.Net/WorldSession.cs:701-717`: ```csharp var posUpdate = UpdatePosition.TryParse(body); if (posUpdate is not null) { // Update sequence counters from the player's own position updates. if (posUpdate.Value.Guid == Characters?.Characters.FirstOrDefault().Id) { _instanceSequence = posUpdate.Value.InstanceSequence; _teleportSequence = posUpdate.Value.TeleportSequence; // OUR seq; for outbound use _forcePositionSequence = posUpdate.Value.ForcePositionSequence; } PositionUpdated?.Invoke(new EntityPositionUpdate( posUpdate.Value.Guid, posUpdate.Value.Position, posUpdate.Value.Velocity, posUpdate.Value.IsGrounded)); // ← stamps DROPPED HERE } ``` **The four sequence numbers DO arrive.** For the local player they're copied into `_teleportSequence`/`_forcePositionSequence` for OUTBOUND use only (used at GameWindow.cs:5294, 5312, 5329 to stamp our 0xF61C MoveToState packets). For remote players / NPCs the stamps **never leave WorldSession** — the `EntityPositionUpdate` record defined at line 110-114 has only `Guid, Position, Velocity, IsGrounded`. ### 8c. Downstream effect `src/AcDream.App/Rendering/GameWindow.cs::OnLivePositionUpdated` (line 3312) has no awareness that the inbound UP carried a teleport stamp at all. The L.3 environment-variable path (lines 3508-3625) implements Branches B (in-bubble Interpolate), C (out-of-bubble snap), "first UP seed", and air no-op — but **Branch A is never separately taken**. A player teleport that hits a remote observer just falls through whichever of B/C/seed/air path the position happens to hit: - if airborne (e.g. portal exit at high altitude): air no-op ⇒ body keeps falling locally, NEVER moves to the new portal-exit position until the remote lands. - if grounded and within 96m: enqueue the new position, then chase it at walking speed across however far the teleport went — visible "teleport-creep" of up to many meters. - if grounded and beyond 96m: snap (this is correct by accident, because the teleport sent us > 96m). ### 8d. What's needed for the port Plumb the four u16 stamps from `WorldSession.EntityPositionUpdate` into `OnLivePositionUpdated`'s `RemoteMotionState`, then on every UP: 1. INSTANCE_TS: equality check (already implicit via the GUID matching the live entity). 2. POSITION_TS: drop the UP if not newer-by-wrap. (Currently acdream applies every UP, even out-of-order ones.) 3. TELEPORT_TS: test-and-set with the wrap-aware comparator. If newer, fire Branch A: - Equivalent of `teleport_hook`: clear `rmState.Interp` queue, call `report_collision_end` on adjacent listeners (likely a no-op in current acdream — the collision broadcaster doesn't yet exist), nuke any in-flight MoveTo (likely none for remotes). - Hard-snap `rmState.Body.Position = worldPos`, `rmState.Body.Orientation = rot` (already done). - Force `rmState.CellId = p.LandblockId` (already done). 4. FORCE_POSITION_TS: only relevant for our local player (handled via the BlipPlayer-equivalent in PlayerMovementController, not through the remote path). The change is small: extend `EntityPositionUpdate` with the three trailing u16s, store the per-remote `TeleportTimestamp` on `RemoteMotionState`, and gate Branch A on its advance. --- ## 9. Answers to the cross-questions ### Q1. What sequence numbers does ACE actually broadcast in 0xF748 packets? **A.** Four u16s in this order: `InstanceSequence`, `PositionSequence`, `TeleportSequence`, `ForcePositionSequence`. PositionSequence advances on every `SendUpdatePosition` call (always next). InstanceSequence and ForcePositionSequence stay constant in normal motion (current). The **TeleportSequence advances ONLY when `adminMove=true`**, which in practice means "GM teleported this non-player object" or — for the local player — when ACE chains a `GameMessagePlayerTeleport (0xF751)` **before** the `SendUpdatePosition`, advancing the player's own ObjectTeleport seq so the next 0xF748 carries the new value. (`Player_Location.cs:686-694`.) ### Q2. Does TELEPORT_TS only advance on actual teleports, or every position update? **A.** Only on actual teleports. ACE: `adminMove ? GetNextSequence(ObjectTeleport) : GetCurrentSequence(ObjectTeleport)`. Retail's `MoveOrTeleport` is consequently the standard "normal-motion" path 99% of the time and only triggers Branch A on genuine teleport events. This is why decompilers historically named the field "teleport_timestamp" — it's a teleport flag, not a tick. ### Q3. Do we have a teleport_timestamp field anywhere in acdream that's already plumbed but unused? **A.** Yes — partially. `UpdatePosition.Parsed.TeleportSequence` exists at `Messages/UpdatePosition.cs:69` and is read at line 157. It's then used for the **local player's outbound** packet stamping (`WorldSession._teleportSequence` ⇒ MoveToState builders). For **remote entities**, the stamp is **dropped** at the `PositionUpdated?.Invoke(new EntityPositionUpdate(...))` boundary (`WorldSession.cs:712-716`) — `EntityPositionUpdate` has no TeleportSequence field. The L.3 follow-up needs to add that field and a per-`RemoteMotionState` `TeleportTimestamp` cache. ### Q4. What test cases trigger Branch A in retail? **A.** 1. **Player portal travel**: another player walks into a portal next to you. Their character's `update_times[TELEPORT_TS]` advances via `GameMessagePlayerTeleport (0xF751)` server-side; the immediately-following 0xF748 carries the new TeleportSequence. 2. **Recall spells** (Lifestone Recall, Primary Portal Recall, etc.): same path as #1. 3. **Death/Lifestone respawn**: PlayerTeleport→UpdatePosition pair. 4. **GM `@teleto` of a non-player object** (creature, item): server-side `SendUpdatePosition(adminMove: true)`. 5. **First UP on a freshly-attached remote with `cell == 0`**: the `cell == 0` clause in `MoveOrTeleport` line 284327 forces Branch A for the bootstrap placement, even with stamp equality. This is acdream's "first-UP seed" case — already handled correctly by the `LastServerPosTime > 0` predicate at `GameWindow.cs:3563`, but the rationale matches retail. --- ## Appendix A — additional symbols | Function / Type | Address | Line in retail decomp | |----|----|----| | `CPhysicsObj::newer_event` | `0x00451b10` | 90712 | | `CPhysicsObj::teleport_hook` | `0x00514ed0` | 283115 | | `SmartBox::HandleReceivedPosition` | `0x00453fd0` | 92896 | | `SmartBox::UnpackPositionEvent` | `0x004542c0` | 93055 | | `CPhysicsObj::SetPosition (outer)` | `0x005160c0` | 284137 | | `CPhysicsObj::SetPositionInternal (flag-decode)` | `0x00516040` | 284117 | | `CPhysicsObj::SetPositionInternal (middle)` | `0x00515bd0` | 283892 | | `CPhysicsObj::SetPositionInternal (inner)` | `0x00515330` | 283399 | | `CPhysicsObj::CheckPositionInternal` | `0x00511e90` | 280070 | | `CPhysicsObj::SetScatterPositionInternal` | `0x00515f00` | 284059 | | `enum PhysicsTimeStamp` | — | acclient.h:6084 | | `struct PositionPack` | — | acclient.h:53280 | | `struct SetPositionStruct` | — | acclient.h:52398 | ## Appendix B — flag-bit summary card ``` SetPositionStruct.flags (uint32) bit 0 0x0001 ? (single-bit enter_world default; likely SLIDE) bit 1 0x0002 PLACEMENT fresh placement vs. continuation bit 4 0x0010 PLACEMENT_ALLOW_SLIDING sphere slides during search bit 5 0x0020 DO_NOT_LOAD_CELLS keep cells unloaded bit 8 0x0100 ALLOW_SCATTER_FALLBACK retry scatter on failure bit 9 0x0200 SCATTER initial scatter placement bit 11 0x0800 IS_PORTAL_TRAVEL (per ACE; not in MoveOrTeleport paths) bit 12 0x1000 SEND_POSITION_EVENT broadcast pos to cmdinterp Combined values: 0x1012 = SEND_POSITION_EVENT | PLACEMENT_ALLOW_SLIDING | PLACEMENT used by MoveOrTeleport Branch A (teleport) & SetPositionSimple(slide=1) 0x1002 = SEND_POSITION_EVENT | PLACEMENT used by SetPositionSimple(slide=0) — non-MoveOrTeleport call sites 0x0011 = PLACEMENT_ALLOW_SLIDING | (bit 0) used by enter_world(arg2 != 0) & reenter_visibility 0x0001 = (bit 0) used by enter_world(arg2 == 0) — bare entry ```