feat(vfx): port retail hidden and teleport presentation
Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
parent
a51ebc66e9
commit
1e98d81448
46 changed files with 4883 additions and 127 deletions
|
|
@ -181,6 +181,14 @@ src/
|
|||
AcDream.App/ Layer 1 + Layer 4 wiring
|
||||
Physics/
|
||||
ProjectileController.cs -> live-record projectile orchestration/corrections
|
||||
RemotePhysicsUpdater.cs -> ordinary/Hidden remote narrow-tick integration
|
||||
RemoteTeleportController.cs -> incarnation-scoped loaded/pending placement owner
|
||||
RemoteTeleportHook.cs -> ordered retail teleport teardown seam
|
||||
RemoteTeleportPlacement.cs -> collision-seated SetPosition transition commit
|
||||
World/
|
||||
LiveEntityRuntime.cs -> canonical logical identity/state/spatial ownership
|
||||
LiveEntityPresentationController.cs -> Hidden/NoDraw/effect/collision presentation
|
||||
LiveEntityTeardown.cs -> failure-isolated multi-owner lifecycle drain
|
||||
Rendering/
|
||||
GameWindow.cs -> still owns too much runtime wiring
|
||||
TerrainRenderer.cs -> done
|
||||
|
|
@ -248,6 +256,36 @@ What exists and is active:
|
|||
owns the frame;
|
||||
delete, generation replacement, pickup/parent leave-world, and session reset
|
||||
use `LiveEntityRuntime`'s normal lifecycle and never create a second GUID map.
|
||||
- `RemoteTeleportController` owns the placement half of a fresh remote
|
||||
teleport after `RemoteTeleportHook` has torn down movement/target state. It
|
||||
collision-seats loaded destinations through `RemoteTeleportPlacement`; an
|
||||
unloaded destination retains one generation- and PositionSequence-scoped
|
||||
pending placement and resolves the latest accepted frame when that same
|
||||
projection becomes visible. It neither owns GUID identity nor reconstructs
|
||||
an entity. A placement failure after hydration restores the captured source
|
||||
frame/cell/contact rather than leaving a visible collisionless projection;
|
||||
source-resident shadows restore immediately, while an unloaded source
|
||||
delegates one incarnation-scoped restore to
|
||||
`LiveEntityPresentationController`, shared with Hidden/UnHide. A newer
|
||||
placement transfers that marker into an explicit active-placement generation
|
||||
before rebucketing even while Hidden. That generation suppresses every
|
||||
intervening Hidden/UnHide and projection restore until the controller reaches
|
||||
a stable result, then it either restores after collision seating, re-defers
|
||||
its rollback source, or hands a Hidden result back for UnHide. The typed
|
||||
`ILiveEntityRemotePlacementRuntime` seam permits a same-incarnation wrapper
|
||||
rebind only around the canonical body; pending placement adopts that wrapper
|
||||
before hydration and cannot silently lose ownership. Clearing a motion or
|
||||
projectile component retains the incarnation's body/contract identity until
|
||||
logical teardown. The production wrapper exposes an immutable body, while
|
||||
hydration defensively validates arbitrary implementations against the record
|
||||
and rolls the retained body back on mismatch. Runtime binding snapshots the
|
||||
interface Body getter once for validation, assignment, and state mutation.
|
||||
`GpuWorldState`
|
||||
rebuckets atomically and commits spatial visibility before draining its
|
||||
transition FIFO; `LiveEntityRuntime` rejects delayed duplicate edges. A
|
||||
rollback/rebucket inside a visibility observer therefore cannot expose a
|
||||
remove/add pulse, leave stale final visibility, or reorder the final
|
||||
presentation edge.
|
||||
- `BSPQuery` contains the partial retail-style BSP collision dispatcher used by
|
||||
the transition path.
|
||||
- `TransitionTypes` carries `SpherePath`, `CollisionInfo`, `ObjectInfo`,
|
||||
|
|
@ -296,7 +334,15 @@ owns spatial buckets only: register/rebucket/unregister are separate operations,
|
|||
and landblock reloads reuse the same `WorldEntity` without replaying renderer or
|
||||
script creation. Its canonical materialized view remains stable across pending
|
||||
landblocks, while a separate visible-only view feeds radar, picking, status, and
|
||||
targeting. Pickup/parent leave-world clears cell membership and pauses root
|
||||
targeting. Raw server PhysicsState and the final state produced by retail's
|
||||
ordered Lighting/NoDraw/Hidden side effects are stored separately.
|
||||
`LiveEntityPresentationController` drains those accepted transitions only after
|
||||
the renderer/effect owner is ready: Hidden suppresses the retained root's mesh,
|
||||
collision, interaction, radar, and target eligibility while preserving the
|
||||
logical record, scripts, particles, lights, full cell, and local identity.
|
||||
Direct attached children receive retail's NoDraw mutation, and typed
|
||||
Hidden/UnHide effects resolve through the live PhysicsScriptTable.
|
||||
Pickup/parent leave-world clears cell membership and pauses root
|
||||
movement/animation without destroying retained owners. `GameWindow` retains
|
||||
storage-free typed views while its large feature loops are extracted.
|
||||
|
||||
|
|
|
|||
|
|
@ -187,9 +187,17 @@ src/AcDream.App/
|
|||
│ └── Vfx/ # (already exists)
|
||||
├── Net/
|
||||
│ └── LiveSessionController.cs # owns WorldSession lifecycle, login/handshake, reconnect
|
||||
├── Physics/
|
||||
│ ├── ProjectileController.cs # canonical live-record projectile orchestration
|
||||
│ ├── RemotePhysicsUpdater.cs # ordinary/Hidden remote narrow-tick integration
|
||||
│ ├── RemoteTeleportController.cs # loaded/pending teleport placement ownership
|
||||
│ ├── RemoteTeleportHook.cs # ordered retail teleport teardown actions
|
||||
│ └── RemoteTeleportPlacement.cs # collision-seated SetPosition transition commit
|
||||
├── World/
|
||||
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
|
||||
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
|
||||
│ ├── LiveEntityPresentationController.cs # ordered Hidden/NoDraw/effect/collision side effects
|
||||
│ ├── LiveEntityTeardown.cs # failure-isolated multi-owner lifecycle drain
|
||||
│ └── ParentAttachmentState.cs # parent generations + pending ParentEvent relations
|
||||
├── Interaction/
|
||||
│ └── SelectionInteractionController.cs # owns WorldPicker, selection state, Use/PickUp dispatch
|
||||
|
|
@ -231,7 +239,11 @@ instead of spread across `GameWindow`.
|
|||
`InboundPhysicsStateController` for the nine-channel retail timestamp gates and
|
||||
latest accepted immutable CreateObject snapshot, owns the canonical local ID
|
||||
and optional runtime components, and separates logical registration from
|
||||
spatial projection. `ParentAttachmentState` is runtime-owned and keys unresolved
|
||||
spatial projection. It also retains raw PhysicsState separately from the final
|
||||
state produced by retail's ordered side effects;
|
||||
`LiveEntityPresentationController` projects those transitions into draw,
|
||||
collision, effect, child-NoDraw, and target visibility without becoming a
|
||||
second lifetime or GUID owner. `ParentAttachmentState` is runtime-owned and keys unresolved
|
||||
relations by child and parent generation. `Rendering/Vfx/EntityEffectController`
|
||||
owns the focused mixed F754/F755 pending FIFO, effect profiles, typed-table
|
||||
resolution, and a readiness set; canonical ServerGuid-to-local-ID translation
|
||||
|
|
@ -241,6 +253,35 @@ allocators fail fast instead of wrapping into another landblock's namespace.
|
|||
All other non-Parent packet
|
||||
families still need the future general queue tracked by divergence AD-32.
|
||||
|
||||
Remote teleport placement is bounded in `Physics/RemoteTeleportController`,
|
||||
not `GameWindow`: it retains at most one pending request per materialized
|
||||
incarnation, scopes it by the live generation and accepted PositionSequence,
|
||||
and asks `RemoteTeleportPlacement` to collision-seat the current body when the
|
||||
destination projection is available. `GameWindow` supplies lifecycle and
|
||||
shadow-sync callbacks only; canonical identity remains in `LiveEntityRuntime`.
|
||||
Failed hydration restores the captured source and delegates an
|
||||
incarnation-scoped shadow restore to `LiveEntityPresentationController` while
|
||||
that source is unloaded, so Hidden/UnHide and teleport never become competing
|
||||
restore owners. A newer placement transfers that restore into an explicit
|
||||
generation-scoped active-placement state before its rebucket visibility edge
|
||||
even while Hidden. All intervening Hidden/UnHide and projection edges defer to
|
||||
that owner until stable success or rollback completes; only then can it restore,
|
||||
re-defer the source, or hand a Hidden result back for UnHide. The
|
||||
`ILiveEntityRemotePlacementRuntime` seam keeps the complete cell/contact
|
||||
handoff available across same-body runtime-wrapper replacement; replacing the
|
||||
canonical body or dropping the placement contract within one incarnation is
|
||||
rejected even after an operational component clear. `RemoteMotion.Body` is
|
||||
constructor-owned; hydration compares pending/current wrappers directly to the
|
||||
record body rather than trusting wrapper-to-wrapper equality. Binding reads an
|
||||
interface Body getter once and reuses that snapshot. `GpuWorldState`
|
||||
performs remove+place as one spatial rebucket,
|
||||
then commits and serially drains visibility edges; `LiveEntityRuntime` filters
|
||||
delayed duplicates. A rollback inside an observer cannot race the outer
|
||||
destination-visible notification or expose an intermediate false pulse.
|
||||
`LiveEntityTeardown` executes those independent owner callbacks to completion
|
||||
and aggregates failures afterwards, so a throwing effect/plugin sink cannot
|
||||
strand teleport, movement, shadow, light, or GUID-scoped state.
|
||||
|
||||
---
|
||||
|
||||
## 4. Extraction sequence — safest first
|
||||
|
|
|
|||
|
|
@ -253,9 +253,9 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| TS-44 | NPC UpdatePosition reconciliation (position + orientation) is SUPPRESSED while the entity is stuck (`PositionManager.GetStickyObjectId() != 0`). **#184 (2026-07-07) UNIFIED the NPC path onto the interp queue** — the retire-condition mechanism is now ported: the grounded UP routes through `CPhysicsObj::MoveOrTeleport` (Enqueue) and the per-tick catch-up SEEDS the `PositionManager::adjust_offset` chain where `StickyManager::adjust_offset` OVERWRITES it while armed (0x00555190 / 0x00555430 assigns m_fOrigin), exactly like the player-remote branch — so POSITION is now handled retail-faithfully by the compose-overwrite whether the gate is on or off. The `snapSuppressedByStick` gate is RETAINED for two residuals: (a) it suppresses the during-stick **Enqueue** so a stale server waypoint never enters the queue, and (b) it gates the **orientation** hard-snap (still outside the interp chain — retail's sticky owns facing). Bookkeeping (`LastServerPos/Time`, cell) still records; server truth reasserts on the first UP after unstick | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC section, `snapSuppressedByStick` gate) | Retail's position mechanism (sticky-overwrites-interp) is now REACHABLE for NPCs (#184 gave them the interp-queue architecture); the gate remains only for orientation + during-stick Enqueue cleanliness | A stick that stays armed while ACE moves the monster far (shouldn't happen — sticks follow the target) keeps a stale orientation until unstick+next UP; bounded by the 1 s lease | `PositionManager::adjust_offset` 0x00555190; `CPhysicsObj::MoveOrTeleport` 0x00516330 (NPC UP routing, #184); retire the orientation residual in R6 |
|
||||
| TS-46 | Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by `SpherePath.InitPath` (foot center at `radius`, head center at `height − radius`) — retail passes the Setup's SPHERE LIST verbatim (`CPhysicsObj::transition` 0x00512dc0 → `init_sphere(GetNumSphere, GetSphere, m_scale)`, ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). **#184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (`GetSetupCylinder` = `setup.Radius`/`setup.Height` × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale.** RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's `stepUpHeight`/`stepDownHeight` stay a hardcoded 0.4 m, where retail derives them from `setup->step_up_height`/`step_down_height` (0x005180d0/0x005180f0, 0.04 m fallback, `radius×0.5` clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`InitPath`); `src/AcDream.App/Input/PlayerMovementController.cs` (player, human — correct as-is); `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep now `GetSetupCylinder`-fed with a shapeless→human fallback) | The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii | Marginal r−ε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers | `CPhysicsObj::transition` 0x00512dc0; `SPHEREPATH::init_sphere` 0x0050c670 (≤2, ×m_scale); `set_description` 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into `InitPath` |
|
||||
| ~~TS-45~~ | **RETIRED 2026-07-07** — the hand-rolled `SphereCollision` (forced `combinedR+1 cm` radial de-penetration + leaked `SetSlidingNormal` + always-Slid, head-sphere ignored) is REPLACED by the faithful `CSphere::intersects_sphere` family port (branch dispatcher 0x00537A80 + `step_sphere_up`/`slide_sphere`/`land_on_sphere`/`collide_with_point`/`step_sphere_down`), routing the grounded slide through the shared crease `SlideSphere` (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. `SphereCollisionFamilyTests` (slide-around, block, ethereal) + `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`. Residual `AP-84` (PerfectClip TOI dead in M1.5). | — | — | — | `CSphere::intersects_sphere` 0x00537A80 (pc:321678) |
|
||||
| TS-43 | Remote teleport has no `teleport_hook` equivalent: retail tears down the position managers on every teleport (`CPhysicsObj::teleport_hook` 0x00514ed0 — `CancelMoveTo(0x3c)` @0x00514edf, `PositionManager::UnStick` @0x00514eee, `StopInterpolating`/`UnConstrain`); acdream's remote teleport is a bare UP hard-snap, so a stuck/chasing remote that the server teleports keeps its stick/moveto for up to the 1 s sticky lease / next UM. The LOCAL player side IS wired (R5-V3: `PlayerMovementController.SetPosition` → `PositionManager.UnStick`; the moveto cancel was already there via `StopCompletely`; the teleport-arrival site also fires the hook's tail — `EntityPhysicsHost.NotifyTeleported` = `TargetManager::ClearTarget` + `NotifyVoyeurOfEvent(Teleported)` @0x00514f1b-0x00514f28, which is what makes mobs stuck to the player drop their sticks on a recall) | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` — teleport freshness is gated, but no remote manager teardown) | Remote teleports are rare (recalls/summons); the sticky 1 s lease + UP hard-snaps self-correct within a second. The retail timestamp distinction now exists; Step 8 must connect its accepted-teleport disposition to the hook teardown. | A teleported-away attacker briefly steers toward its pre-teleport target from the new location (≤1 s) before the lease/next-UM corrects it | `CPhysicsObj::teleport_hook` 0x00514ed0; `SmartBox::HandleReceivedPosition` 0x00453FD0 |
|
||||
| TS-47 | **NARROWED 2026-07-13** — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core.Net/Messages/ClientCommandRequests.cs` | The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`; `docs/research/2026-07-13-retail-client-command-families-pseudocode.md` |
|
||||
| TS-48 | Dragging an item onto another player honors the authoritative `DragItemOnPlayerOpensSecureTrade` option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through `GiveObjectRequest 0x00CD` works when the option is disabled; NPC giving is complete. | `src/AcDream.App/UI/ItemInteractionController.cs` (`PlaceIn3D`, `PolicyActionMessage`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs` | The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | `ItemHolder::AttemptPlaceIn3D @ 0x00588600`; `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`; `ClientTradeSystem`; `docs/research/2026-07-13-retail-give-item-pseudocode.md` |
|
||||
| TS-49 | Hidden-object availability is bridged through `TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld)` because acdream has not ported retail's DetectionManager. Retail `CObjCell::hide_object` sends `LeftDetection` to detection voyeurs; acdream instead uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. | `src/AcDream.App/Rendering/EntityPhysicsHost.cs` (`NotifyHidden`); `src/AcDream.Core/Physics/Motion/TargetManager.cs` (`NotifyVoyeurOfEventAndClear`) | The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. | Plugins or future systems listening specifically for retail detection enter/leave events receive no `LeftDetection`; only movement/sticky target consumers observe the equivalent availability loss. | `CObjCell::hide_object @ 0x0052BE30`; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through `LeftDetection` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -517,6 +517,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
|
||||
- **Missile/portal VFX campaign Step 6 implemented and independently reviewed 2026-07-14.** Pure Core now owns the retail projectile clock/integration/sweep primitive: 0.2-second catch-up quanta, 50-unit/second clamp, final-state acceleration, world-space omega, complete 3-D AlignPath, scaled Setup-local collision spheres, terrain/BSP/object continuous sweeps, exact `OBJECTINFO::missile_ignore`, self-shadow rejection, elastic/inelastic response, and correct full-cell rebasing across loaded landblocks. App live ownership and authoritative corrections remain Step 7. TS-2 is retired; ordinary missiles add PathClipped but not PerfectClip, so AP-83/AP-91 remain dormant.
|
||||
- **Missile/portal VFX campaign Step 7 implemented and independently reviewed 2026-07-14.** `ProjectileController` now attaches the Step 6 body to the canonical `LiveEntityRecord` after materialization and advances arrows, bolts, and spell missiles without another GUID map, renderer registration, or stale-spawn reconstruction. It commits predicted frames through `WorldEntity.SetPosition`, republishes static effect roots, and keeps collision shadows plus canonical full-cell buckets synchronized. A predicted crossing into an unloaded bucket suspends the body and shadow in that same quantum; pending/pickup residence retains the shadow registration for exact re-entry, and hydration plus the 96-unit retail activity gate restart at the current clock without backlog. Fresh State/Vector/Position/Movement packets correct or stop the same body after retail timestamp gates; SetState reaches the canonical body before optional projectile acquisition, and a non-finite local receipt clock cannot consume first classification. Removing Missile retains ordinary active-body physics (delegating to MovementManager when present), while both owners share the body and incarnation-scoped live-record cell. CreateObject vectors are installed only at body construction, never replayed by later SetState. Delete, reset, and GUID reuse use normal logical teardown; ACE remains authoritative for impact, damage, effects, and deletion.
|
||||
- **Missile/portal VFX campaign Step 8 implemented and independently reviewed 2026-07-14.** `LiveEntityRuntime` now distinguishes raw server PhysicsState from retail's side-effect-derived final state, beginning from constructor state `0x00400C08` and applying `set_state` in Lighting → NoDraw → Hidden order. `LiveEntityPresentationController` keeps Hidden objects logically alive while suppressing root mesh, collision, picking, radar, status, and new target acquisition; typed `PS_Hidden`/`PS_UnHide` plays remain DAT-driven, direct equipped children inherit NoDraw, particles/lights survive, and repeated/stale state cannot replay a transition. A normal visible CreateObject never fabricates UnHide. Effect-owner preparation is separate from pending F754/F755 replay so construction-state effects always run first. Remote fresh-teleport/cell-less placement now executes the exact ordered `teleport_hook` action bundle and a full-cell hard placement before contact/airborne routing, preventing an airborne correction from restoring the old location. Projectile Hidden state pauses the retained body without consuming its active identity or accumulating a clock backlog. TS-43 is retired; AP-69 remains because the separate 25-second/384-metre liveness cull is still unported.
|
||||
|
||||
**Reference docs:** `docs/research/retail-ui/00-master-synthesis.md` + slices 01-06. Every AC-specific behavior has a decompiled FUN_ / DAT_ citation.
|
||||
|
||||
|
|
|
|||
|
|
@ -471,8 +471,8 @@ include dungeons.
|
|||
`PlayerDescription.Options1` preserves retail's player secure-trade
|
||||
preference. See
|
||||
`docs/research/2026-07-13-retail-give-item-pseudocode.md` and issue #216.
|
||||
- **M2/M3 missile, effect, and portal presentation campaign (Steps 0–7
|
||||
implemented and independently reviewed 2026-07-14)** — named-retail projectile and
|
||||
- **M2/M3 missile, effect, and portal presentation campaign (Steps 0–8
|
||||
independently reviewed 2026-07-14)** — named-retail projectile and
|
||||
physics-script behavior is pinned in one oracle, the complete `PhysicsDesc`
|
||||
and F754/F755 wire surfaces are parsed with retail timestamp gates, and
|
||||
`LiveEntityRuntime` now separates logical lifetime from spatial rebucketing.
|
||||
|
|
@ -526,8 +526,15 @@ include dungeons.
|
|||
if Missile classification changes while pending; static missile effect
|
||||
roots follow every predicted frame, and materialized rehydration never uses a
|
||||
stale spawn cell. No impact, damage, effect, or delete event is synthesized
|
||||
client-side. The remaining steps port
|
||||
Hidden/UnHide portal presentation and run final hardening/visual gates.
|
||||
client-side. Step 8 ports retail's constructor-state → PhysicsDesc
|
||||
transition, ordered Lighting/NoDraw/Hidden state side effects, DAT-driven
|
||||
typed Hidden/UnHide effects, direct-child NoDraw propagation, retained
|
||||
particles/lights, mesh/collision/interaction suppression, and exact remote
|
||||
`teleport_hook` teardown followed by a placement snap before the airborne
|
||||
gate. Ordinary visible CreateObject does not fabricate UnHide; pending
|
||||
F754/F755 packets replay only after construction-state effects. TS-43 is
|
||||
retired. The remaining Step 9 is final hardening plus the two-client visual
|
||||
gate.
|
||||
- **L.1c local attack receive path (implemented 2026-07-11; live gate pending)** —
|
||||
local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted
|
||||
funnel and action-stamp gate, so ACE's server-selected melee/missile action
|
||||
|
|
|
|||
|
|
@ -698,8 +698,110 @@ The recall Animation's direct DAT hooks must be executed rather than replaced
|
|||
with a guessed cloud. Hidden/UnHide state transitions and explicit typed
|
||||
scripts remain distinct packet-driven events. A normal visible spawn alone is
|
||||
not evidence for an UnHide materialization effect; the observer portal-in
|
||||
sequence must be validated against its actual inbound state/effect packets
|
||||
during Step 8.
|
||||
sequence remains a final two-client packet/visual gate.
|
||||
|
||||
### 11.5 Step 8 implementation boundary
|
||||
|
||||
The port keeps raw wire state and side-effect-derived final state separate.
|
||||
`LiveEntityPresentationController` drains accepted state transitions only after
|
||||
the renderer, effect profile, and PhysicsScript owner are ready; pending mixed
|
||||
F754/F755 packets replay after that construction-state drain. Hidden removes
|
||||
the retained entity from drawing, collision, radar, picking, status, and new
|
||||
target acquisition, but does not unregister its record, cell, script queue,
|
||||
particles, lights, or canonical local ID. Attached children receive the exact
|
||||
direct NoDraw mutation.
|
||||
|
||||
The Hidden update is a narrow tick, not a frozen entity. In
|
||||
`CPhysicsObj::UpdatePositionInternal` (`0x00512C30`) Hidden skips PartArray
|
||||
root-motion/animation advancement and `UpdatePhysicsInternal`, but still
|
||||
composes `PositionManager::adjust_offset` and processes timed object hooks.
|
||||
The surrounding `UpdateObjectInternal` (`0x005156B0`) then continues the
|
||||
target, movement, and position-manager tails. A moving Hidden local or remote
|
||||
therefore updates its root/cell and effect-pose snapshot without accepting
|
||||
input, gravity, or ordinary body integration.
|
||||
|
||||
Retail `CObjCell::hide_object` (`0x0052BE30`) also sends DetectionManager
|
||||
`LeftDetection`. DetectionManager is not yet ported, so TS-49 records the one
|
||||
explicit adaptation: acdream sends the existing TargetManager non-Ok
|
||||
availability edge to tear down MoveTo/Sticky consumers and clears the hidden
|
||||
object's watched-role subscriptions.
|
||||
|
||||
Fresh remote `TELEPORT_TS`, or first placement while the canonical cell is
|
||||
zero, follows `MoveOrTeleport` Branch A before its contact test:
|
||||
|
||||
```text
|
||||
CancelMoveTo(0x3c)
|
||||
UnStick
|
||||
StopInterpolating
|
||||
UnConstrain
|
||||
ClearTarget + NotifyVoyeurOfEvent(Teleported)
|
||||
report_collision_end
|
||||
SetPosition the same body at received full cell + frame through CTransition
|
||||
derive Contact/OnWalkable/contact plane and ground-transition state
|
||||
```
|
||||
|
||||
This order is required even for an airborne packet; the ordinary airborne
|
||||
no-op is only a Branch B/C decision and cannot undo a genuine teleport. A
|
||||
canonical nonzero cell whose spatial projection is unavailable is classified
|
||||
as cell-less for this decision: it cannot take the ordinary interpolation path
|
||||
without a resident cell.
|
||||
|
||||
If the destination landblock is pending, `RemoteTeleportController` parks the
|
||||
authoritative frame without contact, retains the original contact edge, and
|
||||
waits for projection visibility. The request is scoped to live generation and
|
||||
accepted PositionSequence. Every newer accepted Position packet refreshes that
|
||||
same request; hydration collision-seats only the latest sequence, while delete,
|
||||
reset, or GUID reuse discards the old incarnation's placement.
|
||||
|
||||
`SetPositionInternal` captures the old Contact/OnWalkable flags at entry. It
|
||||
writes destination Contact while retaining the source OnWalkable bit for the
|
||||
first `calc_acceleration`, then invokes `set_on_walkable` (therefore
|
||||
HitGround/LeaveGround), and only then calls `handle_all_collisions` at
|
||||
`0x005154FE`. Retaining source walkability through that first calculation
|
||||
matters for a grounded source that hydrates onto a steep contact: retail clears
|
||||
grounded angular drift before installing the destination's non-walkable state.
|
||||
The callback may change velocity, so collision response must consume that
|
||||
post-callback velocity while still receiving the captured source flags. This
|
||||
ordering is shared by loaded teleports and local/remote Hidden PositionManager
|
||||
movement. A Hidden projectile that shares the same RemoteMotion body also uses
|
||||
this narrow manager tick once; projectile integration remains paused.
|
||||
|
||||
If projection hydration occurs but placement resolution fails, the visibility
|
||||
edge cannot be treated as a retry clock. The controller restores the captured
|
||||
source body frame, full cell, contact state, and projection, then drops the
|
||||
pending request. A resident source restores its shadow immediately; an unloaded
|
||||
source delegates one generation-scoped restore to
|
||||
`LiveEntityPresentationController`, the same owner used by Hidden/UnHide, until
|
||||
that projection returns. Before any newer accepted placement rebuckets, it
|
||||
transfers the deferred restore into an explicit active-placement generation,
|
||||
including while Hidden. That ownership remains active for the complete pending
|
||||
lifetime, so any number of intervening Hidden/UnHide or visibility edges cannot
|
||||
restore an unresolved pose. Only the controller's stable completion clears it:
|
||||
the placement either restores after successful collision seating, re-defers its
|
||||
captured source after failure, or hands a stable Hidden result back for UnHide.
|
||||
A same-incarnation MovementManager/runtime rebind cannot replace the canonical
|
||||
physics body or discard the typed placement contract. A wrapper replacement
|
||||
around that same body is adopted by the pending request before hydration, so
|
||||
component rebinding cannot strand active-placement ownership. Operational
|
||||
component clearing removes the wrapper, not the incarnation's body identity or
|
||||
placement requirement; only logical teardown releases those invariants. The
|
||||
production wrapper's body is constructor-owned and immutable, and hydration
|
||||
validates both pending/current wrappers against the record's captured body. A
|
||||
malformed dynamic wrapper is detached and the canonical body is rolled back.
|
||||
Every runtime-binding boundary reads the wrapper's Body exactly once, validates
|
||||
that local snapshot, and installs the same reference into the live record.
|
||||
A cell-less source withdraws the projection and publishes the final
|
||||
invisible presentation edge. Spatial rebucketing removes
|
||||
and places atomically, visibility transitions commit their new set first and
|
||||
drain through a non-reentrant FIFO, and `LiveEntityRuntime` rejects delayed
|
||||
duplicates against its last logical state. A rollback triggered by a
|
||||
destination-visible callback therefore cannot expose remove/add pulses or be
|
||||
overwritten by the outer notification.
|
||||
A newer loaded destination that fails placement consumes the original pending
|
||||
request and performs the same rollback. Logical teardown is independently
|
||||
failure-isolated across presentation, effects, teleport, movement/target,
|
||||
shadow, and light owners; session reset clears pending teleport state even when
|
||||
another teardown callback throws.
|
||||
|
||||
## 12. Secondary-reference cross-checks
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue