diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 835c623f4..29bb9f5ad 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -39,6 +39,27 @@ confirmed closed by the owner, 11 need a focused live gate, and 43 are safe to
remain closed. See
[`docs/research/2026-08-28-owner-closed-issue-validity-audit.md`](research/2026-08-28-owner-closed-issue-validity-audit.md).
+## #482 — Some exterior cathedral walls do not stop the player
+
+**Status:** IN-PROGRESS — scoped repair, automated gates and lead graphical
+wall gate PASS/provisional; owner acceptance pending. At F4180012 [48.002960,39.257545,160.004990],
+AcDream can remain halfway through the wall. Retail teleported to that position
+pushes out to x48.480000. Read-only retail capture identifies the neighboring
+F418000A building Gfx01001FB3;0012 has no building pointer. Paired native code
+confirms that a missing terrain polygon does not suppress the building/object
+channels. The repaired CheckOtherCells no longer skips both on a terrain-cell
+mismatch. Full installed-scene replay proves the failure, fixed25focused tests
+pass and restoring the old skip fails5regressions. Release0W0E/full17036/0/0,
+lead installed4/4. Owner accepts working collision without exact final-position
+equality; original unmatched placement-vs-retail-settle comparison is preserved.
+Owner authorized closing both clients; both closed gracefully before testing.
+Lead graphical30724 proves outside placement, wall stopping and movement away;
+unchanged33696 checks settled cathedral/Holtburg geometry. Both exited0 with
+graceful logout, nine PNGs inspected with narrow verdicts/limitations in report.
+Scope/evidence: `research/2026-09-01-overhaul/482-cathedral-exterior-collision-contract.md`
+and `482-cathedral-exterior-collision-verification.md` alongside it.
+Separate from AP159 ramp membership and #477 AMD device loss; FPS remains deferred.
+
## #481 — Low initial FPS after entering the world
**Status:** OPEN, owner-reported during the `de427d2c0` gate on2026-09-05.
diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md
index be2530e0b..758be2cbd 100644
--- a/docs/architecture/retail-divergence-register.md
+++ b/docs/architecture/retail-divergence-register.md
@@ -560,6 +560,7 @@ equivalence argument (promote to AD/AP) or a fix.
| # | Divergence | Where (file:line) | Recorded justification (deficient) | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
+| ~~UN-10~~ | **Filed and RETIRED2026-09-05 (#482).** `CheckOtherCells` skipped an entire outdoor candidate when its terrain sample was absent/belonged to the center's other cell, suppressing that available cell's building and object collision. Availability now comes from `CellGraph.GetVisible`; terrain is optional and building→objects still execute on OK. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CheckOtherCells`) | No retail basis for the old whole-cell continue. Paired native return/dispatch proof and full prepared cathedral scene reproduce the omission; restoring it fails the building/object and approach/placement regressions. | Some exterior walls can be entered while the sphere center is in an adjacent landcell; repaired by restoring collision dispatch, not moving geometry or clamping coordinates. Owner accepts functional collision, not exact post-teleport settle coordinates; graphical acceptance remains separately recorded. | `CTransition::check_other_cells`0050AE50; `CLandCell::find_env_collisions`00532F20/0053305E; `CLandCell::find_collisions`00532D60; `CSortCell::find_collisions`005340A0; `docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-verification.md` |
| UN-1 | `CheckOtherCells` iterates the overlap set SORTED by cell id; retail walks the CELLARRAY in build order — and the loop halts on the first non-OK result, so order is behavior-bearing | `src/AcDream.Core/Physics/CellTransit.cs:1718` | Justified only as "deterministic order for greppable probe logs" — no equivalence argument vs retail's array order recorded | A sphere straddling two cells that would each return a different non-OK result halts on a different cell than retail — different collision normal / slide direction at multi-cell straddles | `CTransition::check_other_cells` pc:272717-272798 |
| UN-4 | GfxObj double-sided/negative-surface handling keeps WB's legacy logic (cull-mode double-siding, no reversed-winding duplicate, different neg-surface predicate) while the CellStruct path follows the retail-cited `ConstructMesh` reading | `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs:1059` (CellStruct contrast :1396-1410) | No recorded justification on the GfxObj side — it is the unmodified WB extraction; the retail citation was added only to the CellStruct path | GfxObj models retail draws via duplicated-reversed-winding get wrong back-face lighting (normals not inverted) or missing/extra negative faces — dark or absent faces from behind | `D3DPolyRender::ConstructMesh` 0x0059dfa0 |
| UN-6 | Fixed 200 ms sleep between ConnectRequest and ConnectResponse; retail inserts no delay. Annotated only as "with 200ms race delay"; the 2026-06-04 audit flagged it, the follow-up refuted "forbidden workaround" but wrote no fuller rationale back | `src/AcDream.Core.Net/WorldSession.cs:484` | Presumed ACE port+1 listener race guard — four words, no citation | Every login eats a flat 200 ms; if the race needs longer on a loaded server, the handshake fails intermittently (ConnectResponse ignored → CharacterList never arrives, exit-29 shape) with no retry — a timing constant masking an unconfirmed root cause | (none recorded) |
diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
index 778df3e31..a80fca40e 100644
--- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
+++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
@@ -872,6 +872,25 @@ its bounded scope/input/containment proof; no production change or new
intentional deviation, no AMD-cause inference. Debugger detached, both
owner-held clients remain open. FPS deferred; C1a/#477/G4 remain unclosed.
+**Exterior wall #482,2026-09-05:** owner reports missing collision at
+F4180012 [48.002960,39.257545,160.004990], with retail correcting to x48.48.
+Paired capture identifies neighbor000A's direct Gfx01001FB3 shell; native
+dispatch preserves building/object checks when terrain has no contribution.
+Full production prepared/flat scene replay reproduces both approach and
+initial-overlap failures, clear-space control passes. Bounded scoped repair
+underway, not yet accepted; contract/verification482-cathedral-exterior-
+collision-{contract,verification}.md, packet§62. Owner authorized closure;
+both exact clients gracefully closed, no debugger remains, ACE stays up.
+Owner clarified functional collision rather than exact final coordinate;
+prepared human/direct flat-shell oracle replaces the unmatched settle
+comparison, which is preserved. Frozen review PASS,25focused tests PASS,
+5discriminating sabotage failures. Lead Release0W0E/full17036/0/0 and
+installed4/4 PASS; UN-10 retired with repair. Lead graphical wall30724 and
+settled-controls33696 both exit0/gracefully: wall blocking/correction/escape
+PASS provisional, nine PNGs inspected with control limits in verification
+report. No running clients/debugger; ACE up. Owner wall acceptance pending.
+No FPS work; AP159, AMD#477 and final G4 remain separate open tails.
+
## 14. Risk register (condensed)
- **R1 second scene graph.** Graph records own only topology/membership facts;
diff --git a/docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-contract.md b/docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-contract.md
new file mode 100644
index 000000000..219a9f91f
--- /dev/null
+++ b/docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-contract.md
@@ -0,0 +1,114 @@
+# #482 — cathedral exterior neighboring-cell collision
+
+Bounded chunk, 2026-09-05; base508c6014e. Owner's continued goal explicitly
+requires resolving cathedral issues. Owner authorized graceful closure of
+both clients before implementation/testing; PID25724 logout confirmed and
+PID35048 absent. ACE UDP9000/PID13340 remains running. FPS is excluded.
+
+**Owner acceptance clarification during implementation:** "might not be exact
+as retail does it. As long as it is fixed." Functional acceptance is preventing
+entry through the wall, moving an initially overlapping character outside,
+and allowing movement away. Exact equality with retail's final x48.48 is NOT
+required. This changes the acceptance oracle, not the authorized production
+algorithm: retain the named/paired retail dispatch fix, no coordinate clamp,
+scene special case or placement-algorithm rewrite. A bare SetPosition request
+is not established as the same phase/state as the owner's later-settled retail
+observation; preserve that failed comparison as an unmatched diagnostic,
+never relabel it retail-exact. Actual graphical confirmation remains owed.
+
+## Reproduction and independently checked retail facts
+
+Owner walks into some exterior wall sections at F4180012,
+[48.002960,39.257545,160.004990], orientation[1,0,0,0]. Retail teleported to
+the same spot initially overlaps, then moves to[48.480000,39.257545,160.004990]
+with orientation[-0.992006,0,0,-0.126192]. Screenshots are the supplied
+codex-clipboard-f29dec37-5578-4de6-a046-a6db75e11b97.png and
+codex-clipboard-57dcb1d8-6638-4fc7-856c-7fc0b7681b06.png in Local/Temp.
+This is collision failure, not just different projected character positions.
+
+Root's paired2013 retail snapshot identifies F418000A's sole building:
+owner1A4A0F20, part0 at113E6EC8, GfxObj01001FB3 (449 physics polygons and a
+non-null physics BSP), root origin[36,36,160],180-degree Z rotation. Root and
+part0 Position bytes are identical. F4180012 has no building pointer. The
+reported center is east of x48; a radius0.48 human sphere overlaps000A.
+No Setup-part placement repair is indicated by this direct-Gfx shell.
+
+Named+paired facts: CTransition::check_other_cells0050AE50 dispatches every
+non-null, non-primary candidate via vtable+88 (0050AE98–0050AEAF).
+CLandCell::find_env_collisions00532F20 retains entry-restriction OK in EBX;
+no terrain polygon takes00532F96→0053305E and returns OK via00533061.
+CLandCell::find_collisions00532D60 then calls CSortCell005340A0 at00532D79,
+and objects0052B750 at00532D86 if still OK. CSortCell invokes the building's
+006B5300 channel. Thus no terrain contribution must NOT suppress that cell's
+building/object checks. Root verified named bodies; independent OpenAI audit
+checked paired native spans. Root independently verified the raw spans;
+`482-cathedral-exterior-collision-verification.md` records their hashes.
+
+Production Transition.CheckOtherCells currently continues the entire outdoor
+cell at the SampleTerrainWalkable cell-id mismatch, before building/object
+dispatch. This is a confirmed dispatch divergence; pre-fix installed repro
+must establish that it actually explains this reported wall failure.
+
+## Allowed implementation
+
+One OpenAI implementer, shared campaign worktree; lead owns documentation,
+register, integration and graphical runs. Implementer may edit only:
+
+- src/AcDream.Core/Physics/TransitionTypes.cs, CheckOtherCells outdoor branch.
+- tests/AcDream.Core.Tests/Physics/TransitionCheckOtherCellsTests.cs (or one
+ focused sibling test file if cleaner).
+- one new tests/AcDream.App.Tests/Physics/CathedralExteriorWallCollisionInstalledDatTests.cs.
+
+First write/run a discriminating pre-fix regression and exact installed-scene
+replay. Use current full F418 building/terrain/EnvCell publication or an
+existing production factory/publisher fixture; do not omit CacheBuildings.
+Inspect the preserved s5-c1a-geometry fixture as a recipe, not a golden source;
+do not modify that scratch or import its unpinned membership hash assertion.
+Use prepared/flat collision in the installed witness. Assert actual shell
+identity/anchor and absence of an0012 building, overlapping000A candidate,
+then actual transition response. No silent DAT skips or new production probe.
+
+If the reproduced failure is this dispatch gate, fix only its scope: retain
+the existing loaded-cell/null-owner semantics (CellGraph.GetVisible is the
+explicit availability source), evaluate terrain only where applicable, and
+always continue through building then objects for an available other cell
+after terrain OK. Reuse SampleTerrainWalkableInCell if appropriate. A missing
+terrain polygon is not an unavailable cell. Do not permit stale cached
+buildings/objects under an unavailable candidate. Preserve non-OK stopping,
+live sphere refresh, water/contact semantics, primary exclusion and existing
+iteration order. UN-1 sorting, AP159 ramp bridge, transforms, package/shaders,
+placement shortcuts and FPS remain out of scope for this chunk.
+
+Tests must demonstrate adjacent building dispatch, neighboring object dispatch
+after terrain miss, nonintersecting neighbor control, unavailable-cell inertness,
+and existing terrain-only behavior. Exercise prepared/flat production traversal,
+not only a mocked return. Exact installed wall witness must assert actual
+blocked/corrected position and preserve a clear-position control. Use actual
+prepared standard-human Setup collision properties, explicitly a scale1
+control unless the current avatar's inputs were captured. Run both
+approach from outside and owner-reported initial overlap if the production
+placement/resolve seam supports it; report differences, never weaken an
+expectation merely because approach passes. Per the owner's clarification,
+the initial-overlap assertion is functional nonpenetration plus a real
+movement-away control, not the unmatched exact post-settle coordinate oracle.
+Mutation/sabotage must make the
+new regression fail when the old whole-cell skip is reinstated.
+
+## Gate / stop / integration
+
+Implementer alone owns build/test execution until handback (both client names
+must remain absent). Release build and focused whole test classes; keep
+pre-fix failed TRX, fixed TRX, sabotage failed TRX, restore the fixed source
+using apply_patch, rerun green and report exact diff/hash freeze. No commits
+or graphical launches by implementer. Root independently verifies claims
+against named retail/paired binary and actual artifacts, then runs retail and
+production review lenses. Maximum two correction rounds; third required fix
+round stops with write-up. If installed replay points to another mechanism,
+STOP and return evidence before changing additional production scope.
+
+Root files #482 and register UN-10 (never reused), records retirement only on
+verified repair, and commits code/evidence/ledger together. No new intentional
+deviation is authorized. Root rebuilds green, runs exact exterior/doorway
+graphical checks with PNG PASS/FAIL (provisional until owner), closes own
+client gracefully. Overall G4 remains FAIL until cathedral tails and final
+lifecycle/stability/owner sign-off are genuinely complete. No main merge.
diff --git a/docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-verification.md b/docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-verification.md
new file mode 100644
index 000000000..48c83fe7f
--- /dev/null
+++ b/docs/research/2026-09-01-overhaul/482-cathedral-exterior-collision-verification.md
@@ -0,0 +1,223 @@
+# #482 — exterior wall evidence and verification
+
+2026-09-05, base508c6014e. Implementation and gates are not yet accepted.
+Bounded scope: `482-cathedral-exterior-collision-contract.md`.
+
+## Owner failure and read-only retail capture
+
+Owner FAIL: AcDream remains embedded at F4180012
+[48.002960,39.257545,160.004990]. Retail initially overlaps when teleported
+there, then moves outward to x48.480000 with unchanged y/z. Different heading
+does not explain missing physical collision. PNGs below are owner evidence,
+not a lead self-gate.
+
+Non-invasive cdb snapshots of paired2013 retail PID35048 identify the loaded
+F418 landblock and all nine building anchors. F418000A's building is
+owner1A4A0F20, part0 at113E6EC8, direct Gfx01001FB3,449 physics polygons and
+non-null physics BSP. Origin[36,36,160], rotation approximately180 degrees
+around Z; all18 Position dwords of root/part0 agree. F4180012 has no building.
+The radius0.48 human sphere at the owner center overlaps000A. This rules out
+a Setup-part transform explanation for this particular direct-Gfx shell,
+but actual AcDream candidate inclusion and collision response need testing.
+
+Captures used read-only inspection, no breakpoints/calls/memory writes, and
+ended in qd. After owner authorization, both exact clients were closed via
+CloseMainWindow; AcDream log confirms graceful logout requested/confirmed.
+No forced kill, no remaining client/debugger; ACE UDP9000/PID13340 remained up.
+
+Archive: `artifacts/482-cathedral-wall-retail-20260905/`. Copies hash-checked
+against their original Temp files; names below share that directory.
+
+| File | SHA-256 |
+|---|---|
+| cathedral-wall-retail-roots-20260905.log | 8319545BE95A7C5A9E3287265482A03AA157CECCEB4BD5F797BFE8F8D04A8F17 |
+| cathedral-wall-retail-roster-20260905.cdb | 399D2EB9E78E4621A68C9F53F61270E993471B93718DE546AA1FA9B4452041FC |
+| cathedral-wall-retail-roster-20260905.log | 16815962339A8B31E5236617C0D8E48E09BBE453767F5A933357BE013B924767 |
+| cathedral-wall-retail-geometry-20260905.log | DCD2D6E9990B6EE495D538D96C00AED0D7561A9EE49648F95E490247318FAE32 |
+| codex-clipboard-f29dec37-5578-4de6-a046-a6db75e11b97.png | 255FE90F98C8C6517969DC58FB38859BF34DB4861E77A84F04A7AD167FCA3270 |
+| codex-clipboard-57dcb1d8-6638-4fc7-856c-7fc0b7681b06.png | CAB385A1EF032CC108FA1413DFD5AF22F8F67D31D5BF69515516EDDC119BAB00 |
+
+## Named and paired native dispatch
+
+Oracle: named-retail/acclient_2013_pseudo_c.txt, paired executable
+C:/Turbine/Asheron's Call/acclient.exe, SHA-256
+006FFEADC5D679C871497112A5BD1F87714D0E273E2166BAE5052DDE369297B1,
+PDB GUID9e847e2f-777c-4bd9-886c-22256bb87f32 age1. Root and independent retail
+audit checked the dispatch/return branches; no new decompilation required.
+For these .text addresses, file offset = VA minus00400000. These are span
+hashes, not whole-function hashes.
+
+| VA, byte count | Meaning | SHA-256 |
+|---|---|---|
+| 0050AE98,28 | Non-null/non-primary candidate virtual find_collisions | FE5504232907D37E9FC16252C9E80250C16A5880FCE115D9ED21F963A0601A75 |
+| 00532D60,47 | LandCell env, then sort/building, then object on OK | 91989F38F67E681C677420E46992F0AE89F3DBA3CB12D8D158B33AD345C477F4 |
+| 00532F20,36 | Entry-restriction result retained in EBX | AF70C2C2267E51532F3606C53A21782AD70CE86282CA012F6639F64820732E5F |
+| 00532F8C,18 | No terrain polygon branches to return | 73D9BFCBBE689BEB742237FA31929263BE0D9B4A7F7DB1738D4CB039EEF6408C |
+| 0053305E,12 | Return EAX=EBX, including OK=1 | DA523DD257160ECF834F1006428D192E671AF37ADE7559B8B49AC3F60DD2759C |
+| 005340A0,23 | Non-null building dispatch to006B5300 | 4F4A92F32C3C71BD771E21B0A2411FDB9CFD571CD8DA3C4DA28F4D11E1AE87A7 |
+| 006B5300,72 | Building part0 collision with BldgCheck bracket | D3C1035DB94955DBDFD50AB6119C3A5DED0187D39511C66896B5AAAC4A29438D |
+
+Faithful dispatch pseudocode (existing candidate-order deviation UN-1 remains
+outside this repair): for each available non-primary candidate, check its
+environment contribution; no terrain polygon means OK, not skip the cell.
+On OK check its building; on OK check its objects; propagate a non-OK result
+without evaluating later channels. Refresh the live sphere for each candidate.
+Secondary ACE LandCell and ACViewer SortCell ports agree; paired retail wins.
+
+Current AcDream whole-cell continue after terrain sample mismatch suppresses
+both building and object channels. This dispatch divergence is confirmed;
+the complete owner repro cause still requires the full installed-scene test.
+
+## Initial replay and owner acceptance clarification
+
+Pre-fix full near-build/publication + v10 prepared/flat replay confirms real
+candidates[F4180012,F418000A], shell01001FB3/449 polygons and no0012 building.
+Both outside approach and generic initial-overlap placement wrongly commit
+x48.002960; clear-space control passes. Exact artifact:
+`artifacts/482-wall/482-prefixed-installed-replay.trx` (2failed/1passed/0skipped).
+
+With the scoped dispatch repair, approach stops at x48.6 and the clear control
+passes. Generic nonethereal Placement|Slide placement finds
+[48.772762,39.70199,160.00499], failing the initial strict x48.48 assertion.
+`482-fixed-installed.trx` preserves this. Production review correctly rejects
+that exact oracle: the request had hand-authored human geometry/default state,
+different flags and no later settle simulation; it was not matched to the
+owner's post-teleport observation. Compass-search placement can produce a
+diagonal correction. No production placement defect or retail equality follows.
+
+Owner then explicitly clarified: "might not be exact as retail does it. As
+long as it is fixed." The acceptance criterion is functional wall blocking,
+nonpenetrating overlap correction and free movement away, not bit/exact final
+position equality. The corrected fixture must use prepared standard-human
+geometry as an explicitly labeled control, verify actual collision clearance,
+and preserve the original unmatched failure evidence. No extra production
+algorithm change is authorized or made by this criterion amendment.
+
+## Frozen implementation and discriminating tests
+
+Production change is only the available outdoor candidate dispatch in
+TransitionTypes.CheckOtherCells. It introduces no new owner, scene ID, clamp,
+physics model, placement shortcut, shader/package or FPS change. UN-10 is
+filed/retired with the repair; existing UN-1 and AP159 are unchanged.
+
+Final source SHA-256 (root independently read/hash-checked):
+
+- TransitionTypes.cs: C83818C07137EE32214FB32996BA67C42D2ECAB319C8CB7D7ADFC8CF0A4156BE.
+- TransitionCheckOtherCellsTests.cs: 636A67E192960B7C4EA852B0E8D781BBA6AF7E30E45B7D35E290C58A4A8E35BC.
+- CathedralExteriorWallCollisionInstalledDatTests.cs: E3A420FCE7D86B3C0E5942CE95BBD1301015FD135409B26F505526AB45737A77.
+
+Prepared Setup02000001 confirms spheres z0.475/1.35,r0.48,height1.835,
+step-up0.6/step-down1.5. Scale1 is an explicit standard-human control, not a
+captured current-avatar state. Full production publication includes terrain,
+buildings, prepared objects and EnvCells. Direct flat-shell queries prove
+the reported point collides and the corrected placement does not. Approach
+blocks, clear-position motion and movement away remain unobstructed.
+
+Root read the actual TRXs, results and outputs, not just reported counts.
+Artifacts below are under `artifacts/482-wall/`:
+
+| Artifact | Outcome | SHA-256 |
+|---|---|---|
+| 482-restored-core.trx | 21PASS/0FAIL/0skip | F28DFFA37997BCF09A63908A759650A3F09C5CC84C4AD6702D7EBFE220341C3F |
+| 482-restored-installed.trx | 4PASS/0FAIL/0skip | 31EDDFCF3CCEA007BD6230E4C7108C0F8C80B5E3271EA835DBF08202FF58233F |
+| 482-sabotage-core.trx | 18PASS/3expectedFAIL/0skip | 88A9D96C1E8BA1C418D15EC13045EDCE43F5596758D64254C00DD3071DFAB0C6 |
+| 482-sabotage-installed.trx | 2PASS/2expectedFAIL/0skip | F3E5D4AC89ED62053B0050559A11EFC4424CCAACB78C4A2147C06DEA4D1D74E0 |
+| unmatched-strict-placement-fixture.cs.txt | Preserved unmatched oracle, not accepted parity test | 042426F8EB81DEEAD0B91CE030169C0AE172CD20CF92798A8359C88C2DC3AE90 |
+| 482-prefixed-installed-replay.trx | 1PASS/2expectedFAIL/0skip | 0872F992C4A4E0C1C17DF785A2FBB37D088111CF4BBA6CA741B7A2914A7E880C |
+| 482-fixed-installed.trx | Initial unmatched comparison,1FAIL | C7BD387185B1DD09E38E3C8A2520475F0606E039F116F5931BB6905EE908299B |
+| 482-final-implementation.diff | Archived implementation snapshot | 3707EDC9B64079AE28482970D53987EDDB69B6E849F08730E4F148F6B814AA04 |
+
+Restoring the old whole-cell skip fails the real building/object/stop-order
+regressions and both installed approach/placement cases. Clear/escape controls
+stay green. Restored final code passes all25 focused tests. Implementer's
+Release solution build succeeds0warnings/0errors; lead integrated gate follows.
+
+## Review closure
+
+One fixture-proof correction round: independent review found the unmatched
+exact-coordinate oracle, not a production dispatch defect. Owner explicitly
+changed acceptance to working collision. Final combined retail/production
+review PASS on the three frozen hashes above, root independently checked
+named bodies/seven native byte spans and actual TRXs. No further production
+correction, no third fix round. Review does not claim exact teleport parity
+or movement away specifically from the returned placement pose; the graphical
+route checks that actual post-placement escape as an additional observation.
+
+## Lead integration gate
+
+Fresh locked restore and full Release build0W0E pass, followed by the literal
+hermetic gate17036PASS/0FAIL/0skip across14assemblies. Filter unchanged from
+the prior full gate: Lane!=InstalledDat&Lane!=Linux&Lane!=Manual&Lane!=Timing&
+Lane!=Live&Lane!=PreparedPackage. Evidence:
+`artifacts/482-integrated-hermetic/release-gate-summary.json`; root independently
+verified all32 SHA256SUMS entries. This ran on base508c6014e plus the frozen
+delta, honestly recorded dirty, before the integration commit. The lead's
+fresh no-build InstalledDat rerun passes4/4/0skip:
+`artifacts/482-wall/482-lead-installed.trx`.
+
+Exact graphical candidate DLL SHA-256:
+
+- App: FEF0E3717F5B0A526A1F40A2645F61BCC31AF316A2E4F456D4CD3BD04FC131D8.
+- Core: F20220D51B6BFFBEDED128061D9D1662EC492D5D3C55D197648CD7BA3D9BB04F.
+
+Graphical route `tools/overhaul-selfgate/route-482-cathedral-wall.txt` preflight
+checks all78verbs/actions, balanced held input and terminal close-client.
+SHA-256734B3F9E659B159A65E75DBD12FF33699B6AB51D630F5A89790BF02422999FAA.
+Both client names/debugger absent and ACE UDP9000/PID13340 verified before
+launch. One controlled Release process, no concurrent builds/tests.
+High/MSAA4,1280x720 and the existing v10 session/package; automation adds
+checkpoint/referee and borderless exact-framebuffer instrumentation, so this
+is not an ordinary-profile stability or FPS measurement.
+
+## Graphical check — PASS for wall functionality, provisional until owner
+
+Run `logs/selfgate-20260905-183651-482-cathedral-wall-functional/`,PID30724,
+started18:36:51+02, unchanged DLL hashes above. All78commands completed;
+exit0 and graceful logout requested/confirmed. All seven checkpoints report
+completed/visible, waitCueShown=false, invariant failures0 and idle transit.
+No logged ErrorDeviceLost/unhandled exception; this short, unvalidated run
+does not clear AMD#477. Existing missing PhysicsScriptTable notices remain.
+
+All PNGs below were opened. Paths share that run's `artifacts/screenshots/`.
+Positions are read directly from the visible /loc text, not inferred from
+camera perspective. Results are narrow/provisional, not final G4.
+
+| PNG | Observation / verdict |
+|---|---|
+| 482-01-overlap.png | PASS: requested embedded point places visibly outside, x48.891850,y39.257545,z160.004990. |
+| 482-01b-overlap-escape.png | PASS: movement away from that actual returned placement reaches x51.650181, same y/z. |
+| 482-02-approach.png | PASS: westward input from x49 stops outside at x48.497448, same y/z. |
+| 482-03-escape.png | PASS: backing away reaches x53.011257, same y/z. |
+| 482-04-adjacent-wall.png | PASS: second wall point at y41 stops outside at x48.829643,z160.004990. |
+| 482-05-cathedral-interior.png | Geometry visible; arrival caption still present. Settled control below supersedes presentation acceptance. |
+| 482-06-holtburg-doorway.png | Ground/building visible, close-camera avatar translucent. Not proof of avatar-opacity parity; settled control below. |
+
+Log SHA-256A860FC230DF4311E6764A73DF00D0C395C51A7B3A9FBF370A9C6D5304BA2C37B;
+checkpoint streamBF2CDA69B818A26FB174B38B94F3DB8F849D4B2AC611A4A45D00E8C682D7E32F.
+
+Separate unchanged-binary controls:
+`logs/selfgate-20260905-183912-482-settled-controls/`,PID33696,
+started18:39:12+02,20seconds after each world-visible edge. Script
+`tools/overhaul-selfgate/route-482-controls.txt`; exit0/graceful logout, both
+checkpoints completed/visible, cuefalse/invariants0/transit idle. PNGs opened:
+
+- `artifacts/screenshots/482-control-cathedral-settled.png`: narrow PASS for
+ stable cathedral floor/geometry; caption cleared, /loc F4180104
+ [36.900002,48.700001,169.804993]. SHA-256
+ A0EC44D1D42303B09F1AB5E79CDF89DA2A30A0E70695F8CAB40563E8F1E0F648.
+- `artifacts/screenshots/482-control-holtburg-settled.png`: narrow PASS for
+ stable outdoor ground/doorway geometry, /loc A9B40029
+ [133.988495,17.614059,94.005005], not an ongoing fall. Camera remains close
+ with translucent avatar; no avatar-opacity/retail-camera parity claim.
+ SHA-256567C92060A6255550ED9EDA68F050EFBC5239BFEFEA755D413CB0AE0FD4BCDD0.
+
+Second log SHA-2561F9076DEF0F5DAAD3DDB5DF6674261F8633E2097DC5BC7773BD8BBCE0571AA72;
+checkpoint streamB0D004349487174BC008073249215D262D2EDF8C2A6262D78EDBAD1F2F7A58EA.
+Both own clients closed; no AcDream.App/acclient/cdb remains. ACE stays up.
+
+## Acceptance remaining
+
+Owner visual acceptance is still pending. The older owner screenshots remain
+FAIL evidence for the pre-fix build, never relabeled as passing.
+No link to AP159 ramp membership or AMD#477 is established. FPS is deferred,
+G4 remains FAIL, and nothing merges to main.
diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
index 0f0569dfd..f775101f6 100644
--- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
+++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
@@ -5074,3 +5074,35 @@ a production AP159 repair. Neither a visual-cause nor AMD-cause claim follows.
No source/package/FPS change or new intentional deviation. Debugger detached;
AcDream25724 and retail35048 remain open for owner, ACE13340 stays up. No
builds/tests/closure while owner checks. C1a/AP159 and #477/G4 remain open.
+
+## 62. Exterior cathedral wall collision #482 — 2026-09-05
+
+Owner reports physical wall penetration at F4180012 x48.002960,y39.257545,
+z160.004990; retail corrects x to48.480000. Snapshot identifies000A's single
+direct-Gfx01001FB3 shell, part0/root frames identical,449 physics polygons,
+and no0012 building. Named/paired native dispatch confirms no terrain polygon
+does not suppress the later building/object channels. AcDream's whole-cell
+continue does. Full production near-build/publication with prepared flat
+collision reproduces both walking-in and initially-overlapping failures,
+clear-space control passes. artifacts/482-wall/482-prefixed-installed-replay.trx:
+2failed/1passed/0skipped, actual candidates0012/000A and embedded x48.002960.
+
+Owner explicitly authorized closing both and continuing. Both exact clients
+closed gracefully; AcDream logout confirmed, no debugger, ACE9000 still up.
+Bounded OpenAI implementation owns builds/tests; lead owns integration and
+graphical acceptance. `482-cathedral-exterior-collision-contract.md` and
+`482-cathedral-exterior-collision-verification.md` record scope/native evidence.
+Owner subsequently clarified functional collision, not exact final retail
+coordinates. Original unmatched strict-placement comparison preserved;
+prepared human control/direct flat-shell clearance supplies the functional
+oracle. Frozen retail/production review PASS after one fixture correction,
+25focused PASS, old-skip sabotage5expectedFAIL, restored green. Lead full
+Release0W0E/hermetic17036/0/0 and installed4/4 pass. UN-10 filed/retired with
+the scoped repair, no new intentional deviation. Exact graphical candidate
+and artifacts are in the verification report. Lead wall30724 PASS provisional
+for actual outside placement, stopped approach and escape; separate settled
+controls33696 narrow PASS for ground/geometry with camera-opacity limits.
+Nine PNGs opened, both clients exit0/gracefully, checkpoints completed/visible/
+cuefalse/invariants0/transit idle. No clients/debugger remain, ACE up. Owner
+wall acceptance remains pending; the short runs do not clear stability.
+AP159 ramp, AMD#477 and G4 stay open, FPS remains deferred.
diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs
index c6a801646..d45df607f 100644
--- a/src/AcDream.Core/Physics/TransitionTypes.cs
+++ b/src/AcDream.Core/Physics/TransitionTypes.cs
@@ -2878,37 +2878,46 @@ public sealed class Transition
if ((cellId & 0xFFFFu) < 0x0100u)
{
- var terrainWalkable = engine.SampleTerrainWalkable(footCenter.X, footCenter.Y);
- if (terrainWalkable is null || terrainWalkable.Value.CellId != cellId)
+ // CTransition::check_other_cells (0x0050ae98): a null cell
+ // is unavailable. CLandCell::find_env_collisions
+ // (0x00532f96 -> 0x0053305e) instead returns OK when its
+ // terrain polygon misses; the available cell must still
+ // run its building and object channels (0x00532d79/86).
+ if (engine.DataCache.CellGraph.GetVisible(cellId) is null)
continue;
- var terrainState = ValidateWalkable(
- footCenter,
- sphereRadius,
- terrainWalkable.Value.Plane,
- terrainWalkable.Value.IsWater,
- terrainWalkable.Value.WaterDepth,
- cellId: terrainWalkable.Value.CellId,
- walkableVertices: terrainWalkable.Value.Vertices);
-
- if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
+ var terrainWalkable = engine.SampleTerrainWalkableInCell(
+ cellId, footCenter.X, footCenter.Y);
+ if (terrainWalkable is { } terrain)
{
- var plane = terrainWalkable.Value.Plane;
- Console.WriteLine(System.FormattableString.Invariant(
- $"[other-cells] primary=0x{sp.CheckCellId:X8} iter=0x{cellId:X8} terrain wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) r={sphereRadius:F3} result={terrainState} n=({plane.Normal.X:F3},{plane.Normal.Y:F3},{plane.Normal.Z:F3}) d={plane.D:F3}"));
- }
+ var terrainState = ValidateWalkable(
+ footCenter,
+ sphereRadius,
+ terrain.Plane,
+ terrain.IsWater,
+ terrain.WaterDepth,
+ cellId: terrain.CellId,
+ walkableVertices: terrain.Vertices);
- if (PhysicsDiagnostics.ProbePushBackEnabled)
- {
- PhysicsDiagnostics.LogPushBackCellTransit(
- primaryCellId: sp.CheckCellId,
- otherCellId: cellId,
- bspResult: (int)terrainState,
- halted: false);
- }
+ if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
+ {
+ var plane = terrain.Plane;
+ Console.WriteLine(System.FormattableString.Invariant(
+ $"[other-cells] primary=0x{sp.CheckCellId:X8} iter=0x{cellId:X8} terrain wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) r={sphereRadius:F3} result={terrainState} n=({plane.Normal.X:F3},{plane.Normal.Y:F3},{plane.Normal.Z:F3}) d={plane.D:F3}"));
+ }
- if (ApplyOtherCellResult(terrainState, out var terrainHalted))
- return terrainHalted;
+ if (PhysicsDiagnostics.ProbePushBackEnabled)
+ {
+ PhysicsDiagnostics.LogPushBackCellTransit(
+ primaryCellId: sp.CheckCellId,
+ otherCellId: cellId,
+ bspResult: (int)terrainState,
+ halted: false);
+ }
+
+ if (ApplyOtherCellResult(terrainState, out var terrainHalted))
+ return terrainHalted;
+ }
// BR-7 / A6.P4 (2026-06-11): retail's per-other-cell
// find_collisions on a LandCell is env → building → objects
diff --git a/tests/AcDream.App.Tests/Physics/CathedralExteriorWallCollisionInstalledDatTests.cs b/tests/AcDream.App.Tests/Physics/CathedralExteriorWallCollisionInstalledDatTests.cs
new file mode 100644
index 000000000..561cf95db
--- /dev/null
+++ b/tests/AcDream.App.Tests/Physics/CathedralExteriorWallCollisionInstalledDatTests.cs
@@ -0,0 +1,217 @@
+using System.Collections.Immutable;
+using System.Numerics;
+using AcDream.App.Streaming;
+using AcDream.Content;
+using AcDream.Content.Pak;
+using AcDream.Core.Physics;
+using AcDream.Core.World;
+using DatReaderWriter.DBObjs;
+using Xunit.Abstractions;
+
+namespace AcDream.App.Tests.Physics;
+
+///
+/// #482: the F418000A cathedral shell must still collide when the mover's
+/// center belongs to F4180012 and only its sphere overlaps the shell's cell.
+/// Uses the complete production near-build/publication and prepared package.
+///
+[Trait("Lane", "InstalledDat")]
+public sealed class CathedralExteriorWallCollisionInstalledDatTests
+ : IClassFixture
+{
+ private const uint PrimaryCell = 0xF4180012u;
+ private const uint BuildingCell = 0xF418000Au;
+ private const uint ShellGfx = 0x01001FB3u;
+ // Prepared standard-human control, not an uncaptured current-avatar
+ // snapshot. RuntimeSetPositionMoverPreparer uses these authored fields
+ // multiplied by the object's scale; this witness explicitly uses scale 1.
+ private const uint HumanSetup = 0x02000001u;
+ private const float MoverScale = 1f;
+ private static readonly Vector3 Reported = new(48.002960f, 39.257545f, 160.004990f);
+ private ImmutableArray HumanSpheres => _scene.Mover.Spheres;
+ private float Radius => HumanSpheres[0].Radius * MoverScale;
+ private float Height => _scene.Mover.Height * MoverScale;
+ private float StepUp => _scene.Mover.StepUpHeight * MoverScale;
+ private float StepDown => _scene.Mover.StepDownHeight * MoverScale;
+ private readonly Scene _scene;
+ private readonly ITestOutputHelper _output;
+
+ public CathedralExteriorWallCollisionInstalledDatTests(Scene scene, ITestOutputHelper output)
+ => (_scene, _output) = (scene, output);
+
+ [Fact]
+ public void OutsideApproach_IsBlockedByNeighboringPreparedShell()
+ {
+ AssertSceneAndCandidate();
+ Vector3 from = Reported with { X = 48.6f };
+ ResolveResult result = _scene.Engine.ResolveWithTransition(
+ from, Reported, PrimaryCell, Radius, Height, StepUp, StepDown,
+ isOnGround: true, body: GroundedBody(from),
+ moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
+ movingEntityId: 0x000F4243u, sphereList: HumanSpheres);
+ _output.WriteLine($"approach from={from} requested={Reported} actual={result.Position} ok={result.Ok}");
+ Assert.True(result.Ok);
+ Assert.InRange(result.Position.X, 48.479f, 48.601f);
+ Assert.InRange(MathF.Abs(result.Position.Y - Reported.Y), 0f, 0.001f);
+ Assert.InRange(MathF.Abs(result.Position.Z - Reported.Z), 0f, 0.01f);
+ }
+
+ [Fact]
+ public void ReportedInitialOverlap_PlacementFindsClearPositionOutsideWall()
+ {
+ AssertSceneAndCandidate();
+ PhysicsSetPositionResult result = _scene.Engine.SetPosition(new PhysicsSetPositionRequest(
+ Reported, Quaternion.Identity, PrimaryCell, Reported, HumanSpheres,
+ Scale: MoverScale, StepUpHeight: StepUp, StepDownHeight: StepDown,
+ MoverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
+ MovingEntityId: 0x000F4243u,
+ Flags: PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide
+ | PhysicsSetPositionFlags.DoNotCreateCells));
+ _output.WriteLine($"overlap requested={Reported} actual={result.Position} error={result.Error} residence={result.Residence}");
+ Assert.True(result.IsCommitted);
+ // Owner's acceptance is functional wall collision, not equality to
+ // retail's separately observed later settle position. The original
+ // strict/unmatched placement fixture and failed TRX are preserved.
+ Assert.True(result.Position.X >= 48f + Radius - 0.001f);
+ Assert.Equal(TransitionState.OK, QueryShellPlacement(result.Position));
+ }
+
+ [Fact]
+ public void ClearExteriorPosition_PreservesFreeMovement()
+ {
+ AssertSceneAndCandidate();
+ Vector3 from = Reported with { X = 49f };
+ Vector3 target = from + new Vector3(0f, 0.1f, 0f);
+ ResolveResult result = _scene.Engine.ResolveWithTransition(
+ from, target, PrimaryCell, Radius, Height, StepUp, StepDown,
+ isOnGround: true, body: GroundedBody(from),
+ moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
+ movingEntityId: 0x000F4243u, sphereList: HumanSpheres);
+ _output.WriteLine($"clear requested={target} actual={result.Position} ok={result.Ok}");
+ Assert.True(result.Ok);
+ Assert.InRange(Vector3.Distance(target, result.Position), 0f, 0.001f);
+ }
+
+ [Fact]
+ public void MovementAwayFromExteriorWall_IsUnobstructed()
+ {
+ AssertSceneAndCandidate();
+ Vector3 from = Reported with { X = 48.6f };
+ Vector3 target = from + new Vector3(0.2f, 0f, 0f);
+ ResolveResult result = _scene.Engine.ResolveWithTransition(
+ from, target, PrimaryCell, Radius, Height, StepUp, StepDown,
+ isOnGround: true, body: GroundedBody(from),
+ moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
+ movingEntityId: 0x000F4243u, sphereList: HumanSpheres);
+ _output.WriteLine($"away requested={target} actual={result.Position} ok={result.Ok}");
+ Assert.True(result.Ok);
+ Assert.InRange(Vector3.Distance(target, result.Position), 0f, 0.001f);
+ Assert.Equal(TransitionState.OK, QueryShellPlacement(result.Position));
+ }
+
+ private void AssertSceneAndCandidate()
+ {
+ PhysicsDataCache cache = _scene.Cache;
+ Assert.Equal(CollisionTraversalMode.Flat, cache.CollisionTraversalMode);
+ BuildingPhysics building = Assert.IsType(cache.GetBuilding(BuildingCell));
+ Assert.Equal(ShellGfx, building.ModelId);
+ Assert.Equal(new Vector3(36f, 36f, 160f), building.WorldTransform.Translation);
+ Assert.InRange(Vector3.Distance(-Vector3.UnitX,
+ Vector3.TransformNormal(Vector3.UnitX, building.WorldTransform)), 0f, 0.000001f);
+ Assert.InRange(Vector3.Distance(-Vector3.UnitY,
+ Vector3.TransformNormal(Vector3.UnitY, building.WorldTransform)), 0f, 0.000001f);
+ Assert.Null(cache.GetBuilding(PrimaryCell));
+ GfxObjPhysics physics = Assert.IsType(cache.GetGfxObj(ShellGfx));
+ Assert.Null(physics.BSP);
+ Assert.NotNull(physics.FlatPhysicsBsp);
+ Assert.True(physics.FlatPhysicsBsp.RootIndex >= 0);
+ Assert.Equal(449, physics.FlatPhysicsBsp.PolygonTable.Polygons.Length);
+ Assert.NotNull(cache.CellGraph.GetVisible(BuildingCell));
+ _output.WriteLine($"mover standard-human={HumanSetup:X8} scale={MoverScale} height={Height} steps={StepUp}/{StepDown} spheres={string.Join(';', HumanSpheres)}");
+ Vector3 center = Reported + HumanSpheres[0].Origin * MoverScale;
+ uint containing = CellTransit.FindCellSet(cache, center, Radius, PrimaryCell, out var cells);
+ _output.WriteLine($"candidate containing={containing:X8} cells={string.Join(',', cells.Select(id => id.ToString("X8")))} shell={building.ModelId:X8} anchor={building.WorldTransform.Translation}");
+ Assert.Equal(PrimaryCell, containing);
+ Assert.Contains(BuildingCell, cells);
+ Assert.Equal(PrimaryCell, _scene.Engine.SampleTerrainWalkable(center.X, center.Y)!.Value.CellId);
+ Assert.Null(_scene.Engine.SampleTerrainWalkableInCell(BuildingCell, center.X, center.Y));
+ Assert.Equal(TransitionState.Collided, QueryShellPlacement(Reported));
+ }
+
+ private TransitionState QueryShellPlacement(Vector3 position)
+ {
+ var transition = new Transition();
+ transition.SpherePath.InitPath(position, position, PrimaryCell, HumanSpheres, MoverScale);
+ transition.SpherePath.InsertType = InsertType.Placement;
+ transition.SpherePath.BldgCheck = true;
+ BuildingPhysics building = _scene.Cache.GetBuilding(BuildingCell)!;
+ Assert.True(Matrix4x4.Decompose(building.WorldTransform, out _, out var rotation, out var origin));
+ var inverse = Quaternion.Inverse(rotation);
+ Vector3 local0 = Vector3.Transform(transition.SpherePath.GlobalSphere[0].Origin - origin, inverse);
+ Vector3 local1 = Vector3.Transform(transition.SpherePath.GlobalSphere[1].Origin - origin, inverse);
+ return CollisionTraversal.FindCollisions(_scene.Cache, _scene.Cache.GetGfxObj(ShellGfx)!,
+ transition, local0, Radius, true, local1, HumanSpheres[1].Radius * MoverScale,
+ local0, Vector3.Transform(Vector3.UnitZ, inverse), 1f, rotation, _scene.Engine, origin);
+ }
+
+ private static PhysicsBody GroundedBody(Vector3 position) => new()
+ {
+ Position = position,
+ Orientation = Quaternion.Identity,
+ ContactPlaneValid = true,
+ ContactPlane = new Plane(Vector3.UnitZ, -160f),
+ ContactPlaneCellId = PrimaryCell,
+ TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
+ };
+
+ public sealed class Scene : IDisposable
+ {
+ private readonly BoundedTestDatCollection _dat;
+ private readonly PakPreparedAssetSource _prepared;
+ public PhysicsDataCache Cache { get; }
+ public PhysicsEngine Engine { get; }
+ public FlatSetupCollision Mover { get; }
+
+ public Scene()
+ {
+ Assert.Equal("1", System.Environment.GetEnvironmentVariable("ACDREAM_RUN_INSTALLED_DAT_TESTS"));
+ string? directory = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
+ string? package = System.Environment.GetEnvironmentVariable("ACDREAM_PAK_PATH");
+ Assert.True(Directory.Exists(directory), "An explicit installed ACDREAM_DAT_DIR is required.");
+ Assert.True(File.Exists(package), "An explicit validated ACDREAM_PAK_PATH is required.");
+ _dat = new BoundedTestDatCollection(directory!);
+ var bounded = (IDatReaderWriter)_dat;
+ _prepared = new PakPreparedAssetSource(package!, bounded);
+ PreparedCollisionReadResult mover = _prepared.ReadSetupCollision(HumanSetup);
+ Assert.Equal(PreparedAssetReadStatus.Loaded, mover.Status);
+ Mover = Assert.IsType(mover.Data);
+ Assert.Equal(2, Mover.Spheres.Length);
+ float[] heights = Assert.IsType(bounded.Get(0x13000000u)).LandDefs.LandHeightTable;
+ var factory = new LandblockBuildFactory(bounded, _prepared, new object(), heights);
+ LandblockBuild build = Assert.IsType(factory.Build(new LandblockBuildRequest(
+ 0xF418FFFFu, LandblockStreamJobKind.LoadNear, Generation: 1,
+ new LandblockBuildOrigin(0xF4, 0x18))));
+ Assert.NotNull(build.EnvCells);
+ LandblockCollisionBuild collisions = Assert.IsType(build.Collisions);
+ Assert.NotEmpty(collisions.CellStructures);
+ Cache = PhysicsDataCache.CreateProduction();
+ Engine = new PhysicsEngine { DataCache = Cache };
+ TerrainSurface terrain = LandblockPhysicsContentBuilder.BuildTerrainSurface(build.Landblock, heights);
+ var surfaces = new List();
+ var portals = new List();
+ LandblockPhysicsContentBuilder.PublishPreparedCells(Cache, build.Landblock, collisions,
+ Vector3.Zero, surfaces, portals);
+ LandblockPhysicsContentBuilder.CacheBuildings(Cache, build.Landblock, terrain, Vector3.Zero);
+ LandblockPhysicsContentBuilder.CachePreparedObjects(Cache, collisions);
+ Engine.AddLandblock(build.LandblockId, terrain, surfaces, portals, 0f, 0f);
+ _ = LandblockPhysicsContentBuilder.PublishStaticCollision(Engine, Cache,
+ build.Landblock, collisions, Vector3.Zero);
+ }
+
+ public void Dispose()
+ {
+ _prepared.Dispose();
+ _dat.Dispose();
+ }
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Physics/TransitionCheckOtherCellsTests.cs b/tests/AcDream.Core.Tests/Physics/TransitionCheckOtherCellsTests.cs
index 644d5f04f..e852acbba 100644
--- a/tests/AcDream.Core.Tests/Physics/TransitionCheckOtherCellsTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/TransitionCheckOtherCellsTests.cs
@@ -9,12 +9,10 @@ using Xunit;
namespace AcDream.Core.Tests.Physics;
///
-/// Unit tests for the result-combine helper used by
-/// . The iteration / per-cell
-/// BSP-query parts are covered end-to-end by
-/// ; this file pins the
-/// retail-faithful halt semantics that
-/// acclient_2013_pseudo_c.txt:272739-272752 spells out.
+/// Pins the retail halt semantics of
+/// (0x0050ae50), plus the available outdoor cell's terrain → building →
+/// objects dispatch (CLandCell::find_collisions 0x00532d60). Neighbor fixtures
+/// use prepared-flat production traversal with no replacement result hooks.
///
public class TransitionCheckOtherCellsTests
{
@@ -168,4 +166,165 @@ public class TransitionCheckOtherCellsTests
Assert.Equal(0xA9B40001u, t.CollisionInfo.ContactPlaneCellId);
Assert.True(t.SpherePath.CheckPos.Z > -0.28f);
}
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void OutdoorTerrainMiss_RetainsPreparedBuildingAndObjectCollision(bool objectChannel)
+ {
+ PhysicsEngine engine = NeighborWall(objectChannel);
+ Transition transition = NeighborTransition();
+ Assert.NotNull(engine.DataCache!.CellGraph.GetVisible(NeighborCell));
+ Assert.Null(engine.SampleTerrainWalkableInCell(NeighborCell, 23.8f, 12f));
+
+ TransitionState result = transition.CheckOtherCells(engine,
+ transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell });
+
+ // Actual flat BSP Path 6 calls SetCollide and returns Adjusted for
+ // an unclipped airborne sphere; its normal belongs to StepUpNormal.
+ Assert.Equal(TransitionState.Adjusted, result);
+ Assert.True(transition.SpherePath.Collide);
+ Assert.Equal(-Vector3.UnitX, transition.SpherePath.StepUpNormal);
+ if (objectChannel)
+ Assert.Equal(NeighborObject, transition.CollisionInfo.LastCollidedObjectGuid);
+ else
+ Assert.True(transition.CollisionInfo.CollidedWithEnvironment);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void OutdoorTerrainMiss_NonintersectingPreparedNeighborRemainsClear(bool objectChannel)
+ {
+ PhysicsEngine engine = NeighborWall(objectChannel, wallOriginX: 30f);
+ Transition transition = NeighborTransition();
+ Vector3 target = transition.SpherePath.CheckPos;
+
+ TransitionState result = transition.CheckOtherCells(engine,
+ transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell });
+
+ Assert.Equal(TransitionState.OK, result);
+ Assert.Equal(target, transition.SpherePath.CheckPos);
+ Assert.False(transition.CollisionInfo.CollisionNormalValid);
+ Assert.Null(transition.CollisionInfo.LastCollidedObjectGuid);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void UnavailableOutdoorCell_DoesNotDispatchRetainedBuildingOrObjects(bool objectChannel)
+ {
+ PhysicsEngine engine = NeighborWall(objectChannel, wallOriginX: 24f);
+ Transition available = NeighborTransition(targetX: 24.3f);
+ Assert.Equal(NeighborCell, engine.SampleTerrainWalkable(24.3f, 12f)!.Value.CellId);
+ Assert.Equal(TransitionState.Adjusted, available.CheckOtherCells(engine,
+ available.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell }));
+ // Deliberately leave stale cached geometry and engine terrain behind:
+ // CellGraph availability, not a terrain sample, governs dispatch.
+ engine.DataCache!.CellGraph.RemoveLandblock(NeighborCell);
+ Assert.Null(engine.DataCache.CellGraph.GetVisible(NeighborCell));
+ Assert.NotNull(engine.DataCache.GetGfxObj(NeighborGfx));
+ if (objectChannel)
+ Assert.NotEmpty(engine.ShadowObjects.GetObjectsInCell(NeighborCell));
+ else
+ Assert.NotNull(engine.DataCache.GetBuilding(NeighborCell));
+ Transition transition = NeighborTransition(targetX: 24.3f);
+ Vector3 target = transition.SpherePath.CheckPos;
+
+ TransitionState result = transition.CheckOtherCells(engine,
+ transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell });
+
+ Assert.Equal(TransitionState.OK, result);
+ Assert.Equal(target, transition.SpherePath.CheckPos);
+ Assert.False(transition.CollisionInfo.CollisionNormalValid);
+ Assert.Null(transition.CollisionInfo.LastCollidedObjectGuid);
+ }
+
+ [Fact]
+ public void OutdoorPrimaryCell_IsNotDispatchedAgain()
+ {
+ PhysicsEngine engine = NeighborWall(objectChannel: false);
+ Transition transition = NeighborTransition();
+ transition.SpherePath.SetCheckPos(transition.SpherePath.CheckPos, NeighborCell);
+
+ Assert.Equal(TransitionState.OK, transition.CheckOtherCells(engine,
+ transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell }));
+ Assert.False(transition.CollisionInfo.CollisionNormalValid);
+ }
+
+ [Fact]
+ public void NeighborBuildingNonOk_StopsBeforeNeighborObjects()
+ {
+ PhysicsEngine engine = NeighborWall(objectChannel: false);
+ engine.ShadowObjects.Register(NeighborObject, NeighborGfx, new Vector3(23.5f, 12f, 0f),
+ Quaternion.Identity, 10f, 0f, 0f, 0xA9B40000u,
+ collisionType: ShadowCollisionType.BSP, state: 0x00010000u,
+ seedCellId: NeighborCell, isStatic: true);
+ Assert.NotEmpty(engine.ShadowObjects.GetObjectsInCell(NeighborCell));
+ Transition transition = NeighborTransition();
+
+ Assert.Equal(TransitionState.Adjusted, transition.CheckOtherCells(engine,
+ transition.SpherePath.GlobalSphere[0].Origin, 0.48f, new[] { NeighborCell }));
+ Assert.True(transition.CollisionInfo.CollidedWithEnvironment);
+ Assert.Null(transition.CollisionInfo.LastCollidedObjectGuid);
+ }
+
+ private const uint NeighborCell = 0xA9B40009u;
+ private const uint NeighborGfx = 0x0100F482u;
+ private const uint NeighborObject = 0xCA9B4482u;
+
+ private static Transition NeighborTransition(float targetX = 23.8f)
+ {
+ Vector3 target = new(targetX, 12f, 1f);
+ var transition = new Transition();
+ transition.SpherePath.InitPath(new Vector3(targetX - 0.4f, 12f, 1f), target,
+ 0xA9B40001u, 0.48f);
+ transition.SpherePath.SetCheckPos(target, 0xA9B40001u);
+ return transition;
+ }
+
+ private static PhysicsEngine NeighborWall(bool objectChannel, float wallOriginX = 23.5f)
+ {
+ var (root, polygons) = BSPStepUpFixtures.TallWall();
+ var normalized = new Dictionary();
+ foreach ((ushort id, ResolvedPolygon polygon) in polygons)
+ normalized.Add(id, new ResolvedPolygon
+ {
+ Id = id, Vertices = polygon.Vertices, Plane = polygon.Plane,
+ NumPoints = polygon.NumPoints, SidesType = polygon.SidesType,
+ });
+ var graph = new GfxObjPhysics
+ {
+ SourceId = NeighborGfx,
+ BSP = new PhysicsBSPTree { Root = root },
+ Resolved = normalized,
+ BoundingSphere = root.BoundingSphere,
+ };
+ var cache = PhysicsDataCache.CreateProduction();
+ cache.CacheGfxObj(NeighborGfx, FlatCollisionAssetBuilder.FlattenGfxObj(graph));
+ Assert.Equal(CollisionTraversalMode.Flat, cache.CollisionTraversalMode);
+ Assert.Null(cache.GetGfxObj(NeighborGfx)!.BSP);
+ Assert.NotNull(cache.GetGfxObj(NeighborGfx)!.FlatPhysicsBsp);
+ var engine = new PhysicsEngine { DataCache = cache };
+ var heightTable = new float[256];
+ Array.Fill(heightTable, -1000f);
+ engine.AddLandblock(0xA9B4FFFFu, new TerrainSurface(new byte[81], heightTable),
+ Array.Empty(), Array.Empty(), 0f, 0f);
+ Vector3 origin = new(wallOriginX, 12f, 0f);
+ if (objectChannel)
+ {
+ engine.ShadowObjects.Register(NeighborObject, NeighborGfx, origin,
+ Quaternion.Identity, 10f, 0f, 0f, 0xA9B40000u,
+ collisionType: ShadowCollisionType.BSP, state: 0x00010000u,
+ seedCellId: NeighborCell, isStatic: true);
+ Assert.Contains(engine.ShadowObjects.GetObjectsInCell(NeighborCell),
+ entry => entry.EntityId == NeighborObject);
+ }
+ else
+ {
+ cache.CacheBuilding(NeighborCell, Array.Empty(),
+ Matrix4x4.CreateTranslation(origin), NeighborGfx);
+ }
+ return engine;
+ }
}
diff --git a/tools/overhaul-selfgate/route-482-cathedral-wall.txt b/tools/overhaul-selfgate/route-482-cathedral-wall.txt
new file mode 100644
index 000000000..dcb105907
--- /dev/null
+++ b/tools/overhaul-selfgate/route-482-cathedral-wall.txt
@@ -0,0 +1,95 @@
+# #482: exact owner overlap, outside approach, escape and doorway controls.
+# Release green first; no other client; normal High/MSAA4 profile, not FPS work.
+# /loc output and PNGs are observations; inspect them before assigning PASS.
+wait world-ready 180000
+wait world-visible 90000
+resize 1280 720
+wait framebuffer 1280 720 30000
+renderpack select high
+wait render-pack high 90000
+sleep 4000
+
+# Owner-reported overlap: a successful placement should push east to x48.48.
+command /teleloc F4180012 48.002960 39.257545 160.004990 1 0 0 0
+sleep 2000
+wait materialized 1 180000
+wait world-visible 90000
+sleep 4000
+input press CameraViewDefault
+command /loc
+sleep 500
+checkpoint 482_01_overlap
+screenshot 482-01-overlap 15000
+
+# From the actual returned placement, step east away from the wall.
+input down MovementStrafeRight
+sleep 750
+input up MovementStrafeRight
+sleep 1000
+command /loc
+sleep 500
+checkpoint 482_01b_overlap_escape
+screenshot 482-01b-overlap-escape 15000
+
+# Face west and walk into the same wall from clear exterior space.
+command /teleloc F4180012 49 39.257545 160.004990 0.707107 0 0 0.707107
+sleep 2000
+wait materialized 2 180000
+wait world-visible 90000
+sleep 3000
+input press CameraViewDefault
+input down MovementForward
+sleep 750
+input up MovementForward
+sleep 1000
+command /loc
+sleep 500
+checkpoint 482_02_approach
+screenshot 482-02-approach 15000
+
+# Backing away must remain possible; no wall-sticking workaround.
+input down MovementBackup
+sleep 750
+input up MovementBackup
+sleep 1000
+command /loc
+sleep 500
+checkpoint 482_03_escape
+screenshot 482-03-escape 15000
+
+# A second point along the same exterior wall, with a clear exterior start.
+command /teleloc F4180012 49 41 160.004990 0.707107 0 0 0.707107
+sleep 2000
+wait materialized 3 180000
+wait world-visible 90000
+sleep 3000
+input press CameraViewDefault
+input down MovementForward
+sleep 750
+input up MovementForward
+sleep 1000
+command /loc
+sleep 500
+checkpoint 482_04_adjacent_wall
+screenshot 482-04-adjacent-wall 15000
+
+# Known valid cathedral interior arrival; no AP159 membership claim.
+command /teleloc F4180104 36.9 48.7 169.805 1 0 0 0
+sleep 2000
+wait materialized 4 180000
+wait world-visible 90000
+sleep 4000
+input press CameraViewDefault
+checkpoint 482_05_cathedral_interior
+screenshot 482-05-cathedral-interior 15000
+
+# Previously gated valid standing point outside the Holtburg doorway.
+command /teleloc A9B40029 133.603592 17.391838 96.330009 0.977940 -0.143531 -0.022038 0.150157
+sleep 2000
+wait materialized 5 180000
+wait world-visible 90000
+sleep 4000
+input press CameraViewDefault
+checkpoint 482_06_holtburg_doorway
+screenshot 482-06-holtburg-doorway 15000
+close-client
diff --git a/tools/overhaul-selfgate/route-482-controls.txt b/tools/overhaul-selfgate/route-482-controls.txt
new file mode 100644
index 000000000..2696d4499
--- /dev/null
+++ b/tools/overhaul-selfgate/route-482-controls.txt
@@ -0,0 +1,29 @@
+# #482 unchanged-binary controls after the portal-out presentation has settled.
+wait world-ready 180000
+wait world-visible 90000
+resize 1280 720
+wait framebuffer 1280 720 30000
+renderpack select high
+wait render-pack high 90000
+sleep 4000
+command /teleloc F4180104 36.9 48.7 169.805 1 0 0 0
+sleep 2000
+wait materialized 1 180000
+wait world-visible 90000
+sleep 20000
+input press CameraViewDefault
+command /loc
+sleep 500
+checkpoint 482_control_cathedral_settled
+screenshot 482-control-cathedral-settled 15000
+command /teleloc A9B40029 133.603592 17.391838 96.330009 0.977940 -0.143531 -0.022038 0.150157
+sleep 2000
+wait materialized 2 180000
+wait world-visible 90000
+sleep 20000
+input press CameraViewDefault
+command /loc
+sleep 500
+checkpoint 482_control_holtburg_settled
+screenshot 482-control-holtburg-settled 15000
+close-client