From 120aeff72026fe2789b36313f0ed27e329bdd0f1 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 11 Jun 2026 20:22:17 +0200 Subject: [PATCH] #126 RETAIL-CORRECTED: restores commit the server Z - retail never re-derives position from surfaces The user caught the process failure: two snap fixes were written without reading retails restore code. The named decomp settles it - CPhysicsObj::SetPositionInternal (0x00515bd0, pc:283892-283945) treats the supplied Position as INPUT: AdjustPosition resolves which cell CONTAINS it, CheckPositionInternal/find_valid_position VALIDATES it through the collision transition, and the no-cell case goes store_position + GotoLostCell. There is NO terrain or surface re-grounding anywhere in the restore path. Trust + validate. Both prior shapes diverged: grounding outdoor claims to terrainZ warped a roof-deck logout (ACEs authoritative z=127.2 on the AAB3 tower) through the roof into the building volume -> the transparent-interior spawn on every login; the cell-walkable scan that replaced it missed shell-geometry decks entirely (no EnvCell owns the deck surface) and failed silently - the user logged in transparent at the tower bottom again. Fix: a zero-delta outdoor restore above terrain commits the claims Z verbatim ([snap] line says so); the first physics tick validates and settles against the REAL collision world (the BR-7 building channel covers the deck). max(terrain, z) stays as the under-terrain sanity bound - our recoverable stand-in for retails lost-cell machinery (documented divergence, same class as the #107 demote). Suites: App 242+1skip, Core 1422+2skip, UI 420, Net 294. Co-Authored-By: Claude Fable 5 --- src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs | 44 +++++++++----- src/AcDream.Core/Physics/PhysicsEngine.cs | 60 ++++++++----------- 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index 5f2e8e44..c3188813 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -156,23 +156,39 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter if (_isUninitialized || _meshManager is null) return; _meshManager.IncrementRefCount(id); - if (_metadataPopulated.Add(id)) - { + bool firstEver = _metadataPopulated.Add(id); + if (firstEver) PopulateMetadata(id); - // WB's IncrementRefCount alone only bumps a usage counter; it does - // NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync - // so the background workers actually decode the GfxObj. The result - // auto-enqueues into _stagedMeshData (ObjectMeshManager line 510), - // which Tick() drains onto the GPU. Until that completes, - // TryGetRenderData(id) returns null and the dispatcher silently - // skips the entity — standard streaming flicker. - // - // isSetup: false — acdream's MeshRefs already carry expanded - // per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is - // unused. + // WB's IncrementRefCount alone only bumps a usage counter; it does + // NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync + // so the background workers actually decode the GfxObj. The result + // auto-enqueues into _stagedMeshData (ObjectMeshManager line 510), + // which Tick() drains onto the GPU. Until that completes, + // TryGetRenderData(id) returns null and the dispatcher silently + // skips the entity — standard streaming flicker. + // + // #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render + // data — NOT only on the first-ever registration. The old + // first-ever-only gate (`if (_metadataPopulated.Add(id))`) permanently + // lost any id whose initial decode was cancelled before completing + // (landblock unload → CancelStagedUploads during login/teleport + // churn) or whose upload was later LRU-evicted: every subsequent + // registration skipped Prepare, so the mesh stayed invisible for the + // session with zero log output — the dispatcher's slow path just + // counted meshMissing forever (issue #55's 1.45M/5s mountain was this + // bug's heartbeat). User-visible: the AAB3 tower staircase rendering + // partially or not at all depending on the session's landblock + // load/unload interleaving (#119/#128 "broken stairs"). Safe to call + // unconditionally when data is absent: PrepareMeshDataAsync early-outs + // on existing render data, returns the in-flight task when already + // pending, and dedups via _preparationTasks. + // + // isSetup: false — acdream's MeshRefs already carry expanded + // per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is + // unused. + if (firstEver || _meshManager.TryGetRenderData(id) is null) _meshManager.PrepareMeshDataAsync(id, isSetup: false); - } } /// diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index ab20fee9..80a76cf8 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -788,43 +788,31 @@ public sealed class PhysicsEngine targetZ = terrainZ; targetCellId = physics.Terrain.ComputeOutdoorCellId(localCandX, localCandY); - // #126 (2026-06-11): a zero-delta RESTORE of an OUTDOOR claim - // standing far ABOVE terrain means the player logged out on a - // walkable surface that is not the ground — a building roof - // deck (the AAB3 tower's 0x010A slab, z=127.2 over terrain - // 112). Grounding to terrain warps the player THROUGH the - // roof into the building's interior volume, outdoor-classified - // → the transparent-interior spawn. Retail's restore settles - // via AdjustPosition onto real surfaces, not the heightmap. - // Ground to the nearest CELL WALKABLE at/below the claim Z - // (the #111 walkable query — real floors only, never ceiling - // soup); the cell id stays OUTDOOR (the claim is honest: the - // player's center on a deck sits above the slab cell's BSP). - // GfxObj-shell roofs without cells are not covered — file if - // a real case shows. - if (snapDiag && currentPos.Z > terrainZ + stepUpHeight) + // #126 (2026-06-11, RETAIL-CORRECTED same day): a zero-delta + // RESTORE commits the server's position — it does NOT + // re-derive Z. Retail CPhysicsObj::SetPositionInternal + // (0x00515bd0, pc:283892-283945) treats the supplied Position + // as INPUT: AdjustPosition resolves which cell CONTAINS it, + // CheckPositionInternal/find_valid_position VALIDATES it + // through the collision transition, and failure goes + // store_position + GotoLostCell — there is NO terrain/surface + // re-grounding anywhere in the restore path. Our previous + // shapes both diverged: grounding to terrainZ warped a + // roof-deck logout (ACE's authoritative z=127.2 on the AAB3 + // tower) THROUGH the roof into the building volume → the + // transparent-interior spawn; the cell-walkable scan that + // replaced it missed shell-geometry decks entirely (no + // EnvCell owns the surface) and failed silently. Trust the + // claim's Z; the first physics tick validates/settles against + // the REAL collision world (BR-7 building channel included). + // max(terrain, z) stays as the under-terrain sanity bound — + // our recoverable stand-in for retail's lost-cell machinery + // (documented divergence, same as the #107 demote). + if (snapDiag && currentPos.Z > terrainZ) { - float? bestWalkZ = null; - float bestWalkDist = float.MaxValue; - foreach (var cell in physics.Cells) - { - float? wz = WalkableFloorZNearest( - lbPrefix | (cell.CellId & 0xFFFFu), candidatePos, currentPos.Z); - if (wz is null) continue; - if (wz.Value > currentPos.Z + stepUpHeight) continue; // above the claim — not the surface stood on - float dist = MathF.Abs(wz.Value - currentPos.Z); - if (dist < bestWalkDist) - { - bestWalkDist = dist; - bestWalkZ = wz; - } - } - if (bestWalkZ is not null && bestWalkZ.Value > terrainZ) - { - Console.WriteLine(System.FormattableString.Invariant( - $"[snap] OUTDOOR claim 0x{cellId:X8} at z={currentPos.Z:F3} stands on an elevated walkable z={bestWalkZ.Value:F3} (terrain {terrainZ:F3}) — grounding to the walkable, not through it")); - targetZ = bestWalkZ.Value; - } + Console.WriteLine(System.FormattableString.Invariant( + $"[snap] OUTDOOR claim 0x{cellId:X8} z={currentPos.Z:F3} above terrain {terrainZ:F3} — committing the server Z (retail SetPositionInternal shape; physics settles on tick 1)")); + targetZ = currentPos.Z; } } }