#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 <noreply@anthropic.com>
This commit is contained in:
parent
b94a7e8017
commit
120aeff720
2 changed files with 54 additions and 50 deletions
|
|
@ -156,23 +156,39 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
if (_isUninitialized || _meshManager is null) return;
|
if (_isUninitialized || _meshManager is null) return;
|
||||||
_meshManager.IncrementRefCount(id);
|
_meshManager.IncrementRefCount(id);
|
||||||
|
|
||||||
if (_metadataPopulated.Add(id))
|
bool firstEver = _metadataPopulated.Add(id);
|
||||||
{
|
if (firstEver)
|
||||||
PopulateMetadata(id);
|
PopulateMetadata(id);
|
||||||
|
|
||||||
// WB's IncrementRefCount alone only bumps a usage counter; it does
|
// WB's IncrementRefCount alone only bumps a usage counter; it does
|
||||||
// NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync
|
// NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync
|
||||||
// so the background workers actually decode the GfxObj. The result
|
// so the background workers actually decode the GfxObj. The result
|
||||||
// auto-enqueues into _stagedMeshData (ObjectMeshManager line 510),
|
// auto-enqueues into _stagedMeshData (ObjectMeshManager line 510),
|
||||||
// which Tick() drains onto the GPU. Until that completes,
|
// which Tick() drains onto the GPU. Until that completes,
|
||||||
// TryGetRenderData(id) returns null and the dispatcher silently
|
// TryGetRenderData(id) returns null and the dispatcher silently
|
||||||
// skips the entity — standard streaming flicker.
|
// skips the entity — standard streaming flicker.
|
||||||
//
|
//
|
||||||
// isSetup: false — acdream's MeshRefs already carry expanded
|
// #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render
|
||||||
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
// data — NOT only on the first-ever registration. The old
|
||||||
// unused.
|
// 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);
|
_meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
|
||||||
|
|
@ -788,43 +788,31 @@ public sealed class PhysicsEngine
|
||||||
targetZ = terrainZ;
|
targetZ = terrainZ;
|
||||||
targetCellId = physics.Terrain.ComputeOutdoorCellId(localCandX, localCandY);
|
targetCellId = physics.Terrain.ComputeOutdoorCellId(localCandX, localCandY);
|
||||||
|
|
||||||
// #126 (2026-06-11): a zero-delta RESTORE of an OUTDOOR claim
|
// #126 (2026-06-11, RETAIL-CORRECTED same day): a zero-delta
|
||||||
// standing far ABOVE terrain means the player logged out on a
|
// RESTORE commits the server's position — it does NOT
|
||||||
// walkable surface that is not the ground — a building roof
|
// re-derive Z. Retail CPhysicsObj::SetPositionInternal
|
||||||
// deck (the AAB3 tower's 0x010A slab, z=127.2 over terrain
|
// (0x00515bd0, pc:283892-283945) treats the supplied Position
|
||||||
// 112). Grounding to terrain warps the player THROUGH the
|
// as INPUT: AdjustPosition resolves which cell CONTAINS it,
|
||||||
// roof into the building's interior volume, outdoor-classified
|
// CheckPositionInternal/find_valid_position VALIDATES it
|
||||||
// → the transparent-interior spawn. Retail's restore settles
|
// through the collision transition, and failure goes
|
||||||
// via AdjustPosition onto real surfaces, not the heightmap.
|
// store_position + GotoLostCell — there is NO terrain/surface
|
||||||
// Ground to the nearest CELL WALKABLE at/below the claim Z
|
// re-grounding anywhere in the restore path. Our previous
|
||||||
// (the #111 walkable query — real floors only, never ceiling
|
// shapes both diverged: grounding to terrainZ warped a
|
||||||
// soup); the cell id stays OUTDOOR (the claim is honest: the
|
// roof-deck logout (ACE's authoritative z=127.2 on the AAB3
|
||||||
// player's center on a deck sits above the slab cell's BSP).
|
// tower) THROUGH the roof into the building volume → the
|
||||||
// GfxObj-shell roofs without cells are not covered — file if
|
// transparent-interior spawn; the cell-walkable scan that
|
||||||
// a real case shows.
|
// replaced it missed shell-geometry decks entirely (no
|
||||||
if (snapDiag && currentPos.Z > terrainZ + stepUpHeight)
|
// 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;
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
float bestWalkDist = float.MaxValue;
|
$"[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)"));
|
||||||
foreach (var cell in physics.Cells)
|
targetZ = currentPos.Z;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue