From 49d743f88a7f6f6ea535f925df368f2e2a2d25e5 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 24 Jun 2026 13:33:06 +0200 Subject: [PATCH] =?UTF-8?q?fix(physics):=20#146=20=E2=80=94=20re-base=20bu?= =?UTF-8?q?ilding=20collision=20per=20landblock=20apply=20(lost=20after=20?= =?UTF-8?q?portal-in)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static building/house-wall collision worked on a fresh login to a town but was lost after logging in elsewhere and PORTALING in: the foot-sphere clipped straight through walls while server-spawned doors (own collision) still blocked. Root (capture-confirmed, ACDREAM_PROBE_BUILDING bldOrigin): the building shell BSP's WorldTransform is computed from the streaming-relative origin (origin = (lb − _liveCenter)·192) AT CACHE TIME, and CacheBuilding is idempotent (first-wins per cell). A teleport recenters _liveCenter, but the idempotent guard never re-bases the cached transform — so the shell sat at a stale world offset (login Arwic → portal Holtburg: bldOrigin=(-5488,2149), ~5.5 km from the player at (83,24)), and FindBuildingCollisions never penetrated → result=OK everywhere → no block. Terrain doesn't suffer this because AddLandblock overwrites its WorldOffset on every apply. Fix: clear a landblock's cached buildings at the start of each ApplyLoadedTerrain (PhysicsDataCache.RemoveBuildingsForLandblock), then let the existing loop re-populate fresh with the CURRENT origin — the per-apply re-base terrain already gets, while keeping CacheBuilding's per-cell first-wins within each fresh pass (retail CSortCell::add_building). Also adds bldOrigin to the [bldg-channel] probe. Verified on the exact repro (Arwic login → Holtburg portal → walk into wall): bldOrigin now (107.5,36.0) at the wall (was -5488,2149); channel returns Collided → Slid (block + wall-slide). Suites green: Core 1568(+2 skip), App 468(+2 skip), UI 425, Net 317. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 11 ++++++++++ src/AcDream.Core/Physics/PhysicsDataCache.cs | 22 ++++++++++++++++++++ src/AcDream.Core/Physics/TransitionTypes.cs | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e4f61dc3..733d454a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -6943,6 +6943,17 @@ public sealed class GameWindow : IDisposable // formula (gridX * 8 + gridY + 1) within the 192m × 192m landblock. if (lbInfo is not null && lbInfo.Buildings.Count > 0) { + // #146 (2026-06-24): clear this landblock's prior building cache so + // the loop re-bases each building's STREAMING-RELATIVE WorldTransform + // with the CURRENT _liveCenter (origin, recomputed above per apply). + // CacheBuilding's per-cell first-wins then applies fresh within this + // pass — mirrors terrain's per-apply WorldOffset re-base (AddLandblock). + // Without it, CacheBuilding's idempotent guard locks the transform at + // the frame it was first cached with, so a teleport recenter strands + // the shell BSP at a stale world offset and house walls stop colliding + // (login at Arwic → portal to Holtburg → walls clip; bldOrigin ~5.5km off). + _physicsDataCache.RemoveBuildingsForLandblock(lb.LandblockId); + uint lbPrefix = lb.LandblockId & 0xFFFF0000u; foreach (var building in lbInfo.Buildings) { diff --git a/src/AcDream.Core/Physics/PhysicsDataCache.cs b/src/AcDream.Core/Physics/PhysicsDataCache.cs index 7218e016..fbd2ce67 100644 --- a/src/AcDream.Core/Physics/PhysicsDataCache.cs +++ b/src/AcDream.Core/Physics/PhysicsDataCache.cs @@ -455,6 +455,28 @@ public sealed class PhysicsDataCache }; } + /// + /// #146 (2026-06-24): drop every cached building belonging to a landblock so + /// the next pass re-bases their STREAMING-RELATIVE + /// WorldTransform against the CURRENT _liveCenter. Without this, + /// CacheBuilding's per-cell first-wins guard LOCKS the transform at whatever + /// frame it was first cached with; a teleport recenter then leaves the shell + /// BSP at a stale world offset (~the source↔dest landblock distance), so the + /// foot-sphere walks through where the wall visually is and collision is lost + /// (login at Arwic → portal to Holtburg → house walls clip; bldOrigin probed + /// ~5.5 km off). Terrain avoids this because AddLandblock overwrites its + /// WorldOffset every apply; buildings get the equivalent per-apply re-base via + /// the caller clearing-then-repopulating (which keeps first-wins within each + /// fresh pass — retail CSortCell::add_building semantics preserved). + /// + public void RemoveBuildingsForLandblock(uint landblockId) + { + uint prefix = landblockId & 0xFFFF0000u; + foreach (var key in _buildings.Keys) + if ((key & 0xFFFF0000u) == prefix) + _buildings.TryRemove(key, out _); + } + public BuildingPhysics? GetBuilding(uint landcellId) => _buildings.TryGetValue(landcellId, out var b) ? b : null; diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 1e39e4a4..c052541f 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -2874,7 +2874,7 @@ public sealed class Transition if (PhysicsDiagnostics.ProbeBuildingEnabled) { Console.WriteLine(System.FormattableString.Invariant( - $"[bldg-channel] cell=0x{cellId:X8} model=0x{building.ModelId:X8} wpos=({sp.GlobalSphere[0].Origin.X:F3},{sp.GlobalSphere[0].Origin.Y:F3},{sp.GlobalSphere[0].Origin.Z:F3}) hitsInterior={sp.HitsInteriorCell} result={result}")); + $"[bldg-channel] cell=0x{cellId:X8} model=0x{building.ModelId:X8} wpos=({sp.GlobalSphere[0].Origin.X:F3},{sp.GlobalSphere[0].Origin.Y:F3},{sp.GlobalSphere[0].Origin.Z:F3}) bldOrigin=({bldOrigin.X:F3},{bldOrigin.Y:F3},{bldOrigin.Z:F3}) hitsInterior={sp.HitsInteriorCell} result={result}")); } // 0x006b5338: non-OK + non-Contact mover → environment attribution.