From 9743537e62f2832fd1f4bf39cab0398dc4baa68e Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 24 Jun 2026 16:24:48 +0200 Subject: [PATCH] =?UTF-8?q?fix(physics):=20#147=20=E2=80=94=20far-town=20c?= =?UTF-8?q?ity/perimeter=20walls=20had=20no=20collision=20(portal-less=20b?= =?UTF-8?q?uildings=20skipped)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A town's outer/perimeter walls have NO collision even on a fresh login: the player walks straight through them while houses block fine. Confirmed NOT a frame issue — #146's bldOrigin probe showed Arwic buildings are correctly framed (~12 m from the player, not km off), so the "#145 far-town frame" premise was wrong here. Root cause (dat-confirmed, Issue147ArwicBuildingsDumpTests): a perimeter wall is stored in LandBlockInfo.Buildings as a doorless shell — 16 of Arwic's 30 buildings are PORTAL-LESS, ringing the town at 24 m intervals (x=12/132, y=12/108). The building-collision cache loop skipped them via `if (building.Portals.Count == 0) continue;` — a filter meant only for the transit/entry feature (CellTransit.CheckBuildingTransit) that also dropped the collision shell. Retail's find_building_collisions (0x006b5300) tests the shell BSP independent of the portal list, so a doorless wall still collides. Fix: don't skip portal-less buildings — cache them with an empty portal list (no transit) but their collision shell (ModelId) intact. User-verified: Arwic perimeter walls now block (Collided/Slid). Adds the dat-dump fixture test. Suites green: Core 1569(+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 | 14 +++- .../Issue147ArwicBuildingsDumpTests.cs | 64 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/AcDream.Core.Tests/Physics/Issue147ArwicBuildingsDumpTests.cs diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 733d454a..5846d6bd 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -6957,7 +6957,19 @@ public sealed class GameWindow : IDisposable uint lbPrefix = lb.LandblockId & 0xFFFF0000u; foreach (var building in lbInfo.Buildings) { - if (building.Portals.Count == 0) continue; + // #147 (2026-06-24): do NOT skip portal-less buildings. A town's + // PERIMETER WALL is a building with NO portals (you don't enter a + // wall segment through a door) — but it still needs its shell BSP + // registered for COLLISION. The original skip existed for the + // transit/entry feature (CellTransit.CheckBuildingTransit); dropping + // the whole building also dropped its collision shell, so a far + // town's city walls had ZERO collision and the player walked through + // them (Arwic: 16 of 30 buildings are portal-less wall segments + // ringing the town at 24 m intervals). Retail collides with a + // building's shell regardless of portals — find_building_collisions + // (0x006b5300) tests parts[0], independent of the portal list. So a + // portal-less building is now cached with an EMPTY portal list (no + // transit) but its collision shell (ModelId) intact. var bldPortals = new System.Collections.Generic.List( building.Portals.Count); diff --git a/tests/AcDream.Core.Tests/Physics/Issue147ArwicBuildingsDumpTests.cs b/tests/AcDream.Core.Tests/Physics/Issue147ArwicBuildingsDumpTests.cs new file mode 100644 index 00000000..610b87d2 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/Issue147ArwicBuildingsDumpTests.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Text; +using DatReaderWriter; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Options; +using Xunit; +using Xunit.Abstractions; +using Env = System.Environment; + +namespace AcDream.Core.Tests.Physics; + +/// +/// #147 dat fixture: dump Arwic's (0xC6A9FFFE) LandBlockInfo Buildings + Objects +/// to decide what the city/perimeter WALLS are — buildings WITHOUT portals +/// (skipped by the building-collision cache loop's +/// if (building.Portals.Count == 0) continue;) vs landblock-static stabs +/// (a separate shadow-object collision path). Dat-data-dependent; SKIPs cleanly +/// without ACDREAM_DAT_DIR / the real dats. +/// +public sealed class Issue147ArwicBuildingsDumpTests +{ + private readonly ITestOutputHelper _out; + public Issue147ArwicBuildingsDumpTests(ITestOutputHelper o) => _out = o; + + [Fact] + public void DumpArwicBuildingsAndObjects() + { + var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile), + "Documents", "Asheron's Call"); + if (!Directory.Exists(datDir)) { _out.WriteLine($"SKIP: no dats at {datDir}"); return; } + + using var dats = new DatCollection(datDir, DatAccessType.Read); + var info = dats.Get(0xC6A9FFFEu); // Arwic landblock info + Assert.NotNull(info); + + var sb = new StringBuilder(); + sb.AppendLine($"Arwic 0xC6A9FFFE: Buildings={info.Buildings.Count} Objects={info.Objects.Count}"); + + int portalless = 0; + sb.AppendLine("--- Buildings (ModelId, portals, origin) ---"); + foreach (var b in info.Buildings) + { + if (b.Portals.Count == 0) portalless++; + sb.AppendLine(System.FormattableString.Invariant( + $" model=0x{b.ModelId:X8} portals={b.Portals.Count} origin=({b.Frame.Origin.X:F1},{b.Frame.Origin.Y:F1},{b.Frame.Origin.Z:F1})")); + } + sb.AppendLine($"--- portal-less buildings (skipped by the collision cache): {portalless} ---"); + + sb.AppendLine("--- Objects / stabs (first 50: Id, origin) ---"); + int n = 0; + foreach (var o in info.Objects) + { + if (n++ >= 50) break; + sb.AppendLine(System.FormattableString.Invariant( + $" id=0x{o.Id:X8} origin=({o.Frame.Origin.X:F1},{o.Frame.Origin.Y:F1},{o.Frame.Origin.Z:F1})")); + } + + var outPath = Path.Combine(Path.GetTempPath(), "arwic-buildings-dump.txt"); + File.WriteAllText(outPath, sb.ToString()); + _out.WriteLine(sb.ToString()); + _out.WriteLine($"(also written to {outPath})"); + } +}