diff --git a/tests/AcDream.Core.Tests/Physics/RestrictionObjPrevalenceInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/RestrictionObjPrevalenceInspectionTests.cs
new file mode 100644
index 00000000..dbd8b71d
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/RestrictionObjPrevalenceInspectionTests.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using AcDream.Core.Tests.Conformance;
+using DatReaderWriter;
+using DatReaderWriter.Options;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace AcDream.Core.Tests.Physics;
+
+///
+/// AP-129 inspection (Campaign P P4 Opus review, 2026-07-30): measures how
+/// many EnvCells in the INSTALLED cell DAT actually carry a baked
+/// RestrictionObj. Decides the blast radius of the AP-71
+/// fail-closed gate: with CanMoveInto unmodeled (AP-129), every
+/// restricted cell refuses entry for every non-bypass player — harmless
+/// if the flag is rare/authored-for-housing-only, a live lockout if it is
+/// broadly baked into ordinary interiors. Inspection only; skips cleanly
+/// when the installed DATs are absent (CI), following the
+/// DoorSetupGfxObjInspectionTests pattern.
+///
+public sealed class RestrictionObjPrevalenceInspectionTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public RestrictionObjPrevalenceInspectionTests(ITestOutputHelper output)
+ => _output = output;
+
+ [Fact]
+ public void CountRestrictionObjCells_InstalledCellDat()
+ {
+ string? datDir = ConformanceDats.ResolveDatDir();
+ if (datDir is null)
+ return; // installed dats absent (CI) — skip cleanly
+
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+
+ // Bake-tool enumeration shape (BakeRunner.EnumerateEnvCellIds):
+ // LandBlockInfo (xxxxFFFE) NumCells ranges name every EnvCell id.
+ var landblockInfoIds = new List();
+ foreach (var file in dats.Cell.Tree)
+ {
+ if ((file.Id & 0xFFFFu) == 0xFFFEu)
+ landblockInfoIds.Add(file.Id);
+ }
+
+ int totalCells = 0;
+ int restricted = 0;
+ var restrictedLandblocks = new HashSet();
+ var samples = new List();
+
+ foreach (uint infoId in landblockInfoIds)
+ {
+ if (!dats.Cell.TryGet(
+ infoId, out var info)
+ || info is null
+ || info.NumCells == 0)
+ {
+ continue;
+ }
+
+ uint firstCellId = (infoId & 0xFFFF_0000u) | 0x0100u;
+ for (uint offset = 0; offset < info.NumCells; offset++)
+ {
+ uint cellId = firstCellId + offset;
+ if (!dats.Cell.TryGet(
+ cellId, out var cell)
+ || cell is null)
+ {
+ continue;
+ }
+
+ totalCells++;
+ if (cell.RestrictionObj != 0)
+ {
+ restricted++;
+ restrictedLandblocks.Add(cellId & 0xFFFF_0000u);
+ if (samples.Count < 40)
+ {
+ samples.Add(
+ $"cell 0x{cellId:X8} restrictionObj 0x{cell.RestrictionObj:X8}");
+ }
+ }
+ }
+ }
+
+ _output.WriteLine(
+ $"EnvCells scanned: {totalCells}; with RestrictionObj != 0: "
+ + $"{restricted}; distinct landblocks: {restrictedLandblocks.Count}");
+ foreach (string s in samples)
+ _output.WriteLine(s);
+
+ Assert.True(totalCells > 0, "installed cell DAT enumerated no EnvCells");
+ }
+}