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` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue