fix #173: remote jump arcs get the retail collision-velocity response
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 <noreply@anthropic.com>
This commit is contained in:
parent
6ab269894a
commit
012d5c7bf7
3 changed files with 90 additions and 1 deletions
|
|
@ -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)
|
## #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).
|
**Status:** 🟡 FIX SHIPPED 2026-07-05 (this commit) — pending user visual gate (walk up onto the Holtburg portal platform, then the 0x0007 dungeon run).
|
||||||
|
|
|
||||||
|
|
@ -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-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-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-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-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-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` |
|
| 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` |
|
||||||
|
|
|
||||||
|
|
@ -10454,6 +10454,55 @@ public sealed class GameWindow : IDisposable
|
||||||
if (resolveResult.CellId != 0)
|
if (resolveResult.CellId != 0)
|
||||||
rm.CellId = resolveResult.CellId;
|
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
|
// K-fix15 (2026-04-26): post-resolve landing
|
||||||
// detection for airborne remotes. Mirrors
|
// detection for airborne remotes. Mirrors
|
||||||
// PlayerMovementController's local-player landing
|
// PlayerMovementController's local-player landing
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue