From 012d5c7bf7bcfd7cfebff8a4536645227c49267c Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 5 Jul 2026 15:34:25 +0200 Subject: [PATCH] fix #173: remote jump arcs get the retail collision-velocity response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An observed character jumping into a dungeon ceiling hovered at the roof until its ballistic arc decayed, landing visibly late (user report, 0x0007). The remote DR tick sweeps collision (position pinned at the ceiling — no clip-through) but retail's post-transition velocity response, CPhysicsObj::handle_all_collisions (pc:282699-282715: v -= (1+elasticity)*dot(v,n)*n), was only ported for the LOCAL player (L.3a). The remote body kept its +Z launch velocity and re-integrated it into the roof every tick — the position was clamped but the timeline was pure ballistics. Retail runs handle_all_collisions after every SetPositionInternal for every physics object, remotes included. Mirror the local reflection block in the remote sweep's post-resolve path: same formula, same AD-25 airborne-before-AND-after suppression (corridor slides and landings don't reflect; the landing snap's Velocity.Z <= 0 gate stays intact), same Inelastic zero-out for future missiles. AD-25 register row extended to cover both sites. Suites green: Core 2533 / App 713 / UI 425 / Net 385. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 40 +++++++++++++++ .../retail-divergence-register.md | 2 +- src/AcDream.App/Rendering/GameWindow.cs | 49 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 0e973558..751b621e 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,46 @@ Copy this block when adding a new issue: --- +## #173 — Observed character jumping into a ceiling hovers at the roof until the arc decays (no collision-velocity response on remotes) + +**Status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (watch a second client jump into the 0x0007 dungeon roof; it should bounce down immediately like the local player). +**Severity:** MEDIUM (remote-motion fidelity indoors; lands visibly late) +**Filed:** 2026-07-05 +**Component:** physics — remote dead-reckoning collision response + +**Description (user, 0x0007 dungeon):** watching another character jump into +the dungeon roof, the observed char sticks to the ceiling until the jump arc +would naturally have come down — "like we are calculating the entire jump +instead of actually checking the collision" — and lands later than retail, +with the animation pinned at the roof. The LOCAL player's own jump bounces +off the roof immediately. + +**Root cause (code-confirmed):** the remote DR tick integrates the +VectorUpdate launch ballistically and DOES sweep collision +(`ResolveWithTransition`, GameWindow remote block) — the sweep pins the +POSITION at the ceiling — but the retail post-transition velocity response +(`CPhysicsObj::handle_all_collisions`, pc:282699-282715: reflect +`v −= (1+elasticity)·dot(v,n)·n`) was only ever ported for the LOCAL player +(L.3a, `PlayerMovementController` ~:940). The remote body kept its +Z launch +velocity, re-integrated it into the roof every tick, and only descended once +gravity burned the arc off. Retail runs handle_all_collisions after every +SetPositionInternal for every physics object — remotes included. + +**Fix (this commit):** mirror the local L.3a reflection block in the remote +sweep's post-resolve path (same formula, same AD-25 airborne-before-AND-after +suppression so corridor slides and landings don't reflect, same Inelastic +zero-out). Register AD-25 extended to cover both sites. + +**Files:** `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep, #173 +block after `rm.Body.Position = resolveResult.Position`). + +**Acceptance:** from acdream, watch a second client jump into a dungeon +ceiling: the observed char deflects off the roof immediately and lands at +retail timing; grounded remote movement (corridor wall slides, NPC chases) +unchanged. + +--- + ## #172 — Town-network portal platform blocks instead of stepping up (CCylSphere family was never ported) **Status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (walk up onto the Holtburg portal platform, then the 0x0007 dungeon run). diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 470348f0..1d4be70e 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -85,7 +85,7 @@ accepted-divergence entries (#96, #49, #50). | AD-22 | Async streamed mesh loading with point-of-use self-heal (`EnsureLoaded` re-request in the dispatcher's per-frame meshMissing path, **#128**); retail loads synchronously — geometry is never absent | `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs:211` | Documented convergence argument: the self-heal makes absence transient, converging the async pipeline to retail's never-absent guarantee | A missing mesh referenced OUTSIDE the dispatcher's walk (a future consumer not touching meshMissing) stays permanently invisible — the #119/#128 broken-stairs class; best case, late pop-in | retail synchronous content load (note at WbMeshAdapter.cs:211) | | AD-23 | Live entities with `ServerGuid != 0` and null `ParentCellId` are culled (ClipSlotCull) while indoor clip routing is active; retail objects are always cell-resident (synchronous add-to-cell at creation) | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:484` | Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, *given membership resolves promptly* | An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight | retail per-cell object lists in PView traversal | | AD-24 | EnvCell shell geometry hash-deduplicated ((environmentId, structure, surface overrides) → 31-multiplier hash) and instanced; retail draws each CEnvCell's own structure directly | `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs:276` | Verbatim WB EnvCellRenderManager port (Phase A8); dedup is what makes the single-VAO MDI cell pipeline cheap; intended visuals identical | A hash collision between distinct tuples renders the wrong interior shell in some room with NO diagnostic firing — wrong walls/floor in a dungeon room | retail `PView::DrawCells` → per-cell drawing_bsp (cited at :319) | -| AD-25 | Wall-bounce velocity reflection suppressed on landing (fires only airborne-before AND airborne-after); retail bounces unless grounded→grounded-and-not-sledding | `src/AcDream.App/Input/PlayerMovementController.cs:874` | Our per-frame architecture amplifies the artifact (post-reflection +Z defeats the `Velocity.Z <= 0` landing-snap gate → micro-bounce death spiral); at elasticity 0.05 retail's landing bounce is imperceptible; sledding reverts to retail rule | Landing-reflection-dependent behavior (slope-landing momentum, high-elasticity surfaces) won't reproduce; the suppression masks the landing-snap gate fragility and could outlive its reason | `handle_all_collisions` pc:282699-282715; ACE PhysicsObj.cs:2656-2721 | +| AD-25 | Wall-bounce velocity reflection suppressed on landing (fires only airborne-before AND airborne-after); retail bounces unless grounded→grounded-and-not-sledding. **2026-07-05 (#173): the same reflection + suppression now also runs in the remote DR sweep** (remote jumps hitting ceilings reflect like the local player; both sites share the rule and this row) | `src/AcDream.App/Input/PlayerMovementController.cs:874`; `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep post-resolve, #173 block) | Our per-frame architecture amplifies the artifact (post-reflection +Z defeats the `Velocity.Z <= 0` landing-snap gate → micro-bounce death spiral — both the local and remote landing snaps use that gate); at elasticity 0.05 retail's landing bounce is imperceptible; sledding reverts to retail rule | Landing-reflection-dependent behavior (slope-landing momentum, high-elasticity surfaces) won't reproduce; the suppression masks the landing-snap gate fragility and could outlive its reason | `handle_all_collisions` pc:282699-282715; ACE PhysicsObj.cs:2656-2721 | | AD-27 | Use/PickUp action fired on natural moveto completion via the `MoveToComplete` client-addition seam (retail's `CleanUpAndCallWeenie` contains no weenie call in this build and notifies nothing on arrival); retail sends the action once (server MoveToChain callback completes it) | `src/AcDream.App/Rendering/GameWindow.cs:12939` (MoveToComplete subscription) + `src/AcDream.Core/Physics/Motion/MoveToManager.cs` (`MoveToComplete` seam doc) | ACE's server-side chain may have timed out by the time our body arrives; the close-range deferred send hits ACE's WithinUseRadius fast-path. R4-V5 re-anchored from the deleted B.6 `AutoWalkArrived` event — same fires-on-arrival-only contract (never on cancel) | If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius; `MoveToManager::CleanUpAndCallWeenie` 00529650 §7e (no weenie call) | | AD-28 | Chat transcript (`UiText`) and input (`UiChatInput`) are two separate widget classes placed inside their dat-authored container panels; retail's `ChatInterface` uses a single mode-flagged `UIElement_Text` (Type-12) that switches between read and edit mode | `src/AcDream.App/UI/Layout/ChatWindowController.cs:135` (transcript) + `:150` (input) | `UIElement_Text` is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture | A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | `UIElement_Text` (Type-12) @ keystone.dll; `gmMainChatUI::PostInit` @0x4ce130 | | AD-29 | `ClientObjectTable` fires global `ObjectAdded`/`ObjectUpdated`/`ObjectRemoved` events; consumers filter by guid on their end. Retail dispatches per-object via `NoticeRegistrar` observer dispatch — each UI cell observes only its specific object guid | `src/AcDream.Core/Items/ClientObjectTable.cs:48` (events); `src/AcDream.App/UI/Layout/ToolbarController.cs:115` (guid filter) | `NoticeRegistrar` is inside keystone.dll with no PDB/decomp; global broadcast + consumer-side filter is functionally equivalent for the current panel count and object volumes seen in practice | At high object counts (>1 000 objects), every `ObjectUpdated` wakes every subscribed consumer — O(n·m) notification cost instead of retail's O(1) per-observer dispatch; a consumer that forgets the guid filter processes all objects (a latent correctness bug) | `NoticeRegistrar` (keystone.dll, no PDB); retail per-object observer registration in `CObjectMaint` | diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 6b59c300..d470d95a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -10454,6 +10454,55 @@ public sealed class GameWindow : IDisposable if (resolveResult.CellId != 0) rm.CellId = resolveResult.CellId; + // #173 (2026-07-05): retail CPhysicsObj::handle_all_collisions + // (pc:282699-282715) runs after EVERY SetPositionInternal — + // remote objects included; a VectorUpdate-launched jump arc + // is ordinary object physics in retail. acdream ported the + // velocity reflection for the LOCAL player only (L.3a, + // PlayerMovementController ~:940), so a remote jumping into + // a dungeon ceiling had its POSITION pinned by the sweep + // while its +Z velocity kept integrating — the char hovered + // at the roof until gravity burned the arc off, landing + // late (user report, 0x0007 dungeon). Mirror the local + // site exactly: + // v_new = v − (1 + elasticity)·dot(v, n)·n + // with the AD-25 suppression (bounce only when airborne + // before AND after — corridor slides and landings don't + // reflect; the landing snap below keeps its + // `Velocity.Z <= 0` gate intact). Inelastic movers + // (missiles, later) zero out instead. + if (resolveResult.CollisionNormalValid) + { + bool prevOnWalkable = rm.Body.OnWalkable; + bool nowOnWalkable = resolveResult.IsOnGround; + bool applyBounce = rm.Body.State.HasFlag( + AcDream.Core.Physics.PhysicsStateFlags.Sledding) + ? !(prevOnWalkable && nowOnWalkable) + : (!prevOnWalkable && !nowOnWalkable); + if (applyBounce) + { + if (rm.Body.State.HasFlag( + AcDream.Core.Physics.PhysicsStateFlags.Inelastic)) + { + rm.Body.Velocity = System.Numerics.Vector3.Zero; + } + else + { + var vRem = rm.Body.Velocity; + var nRem = resolveResult.CollisionNormal; + float dotVN = System.Numerics.Vector3.Dot(vRem, nRem); + if (dotVN < 0f) + { + rm.Body.Velocity = + vRem + nRem * (-(dotVN * (rm.Body.Elasticity + 1f))); + if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1") + Console.WriteLine( + $"VU.bounce guid=0x{serverGuid:X8} n=({nRem.X:F2},{nRem.Y:F2},{nRem.Z:F2}) vZ {vRem.Z:F2}->{rm.Body.Velocity.Z:F2}"); + } + } + } + } + // K-fix15 (2026-04-26): post-resolve landing // detection for airborne remotes. Mirrors // PlayerMovementController's local-player landing