Lands the working A8 indoor-rendering and streaming fixes accumulated this session. User has verified these visually to some degree (e.g. lifestone / translucent meshes confirmed fine under the FrontFace flip; bridge / wall / collision regressions confirmed fixed after travel); not every path has been exhaustively gated. The cellar-flap defect remains OPEN and will be solved the retail-faithful way via a dedicated brainstorm (see handoff docs). Rendering core (reviewed, high confidence): - EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of the 80B CPU InstanceData struct the shader never expected — fixes the transform/texture "explosion" for any draw with >1 instance (cells that dedupe to a shared cellGeomId). Real root cause. - WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into same-cull runs with absolute uDrawIDOffset per run). - EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) + WorldEntity.BuildingShellAnchorCellId so building shells scope to their dat-derived building cell instead of rendering everywhere. - RenderOutsideInAcdream (look into buildings from outside) + CollectVisiblePortalBuildings frustum cull of portal bounds. - Sky-when-inside-building + per-cell audit probe + GL-state probe. Streaming / perf (test-covered; not independently code-reviewed this session): - Near/far priority queues so near work wins over far; PromoteToNear carries full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids rebuilding the animated-lookup dict in the hot draw path. Fixes the bridge-not-appearing / missing-walls / broken-collision-after-travel regressions and improves post-transition FPS. Tooling + docs: - tools/A8CellAudit: offline dat cell/portal/building dumper (portals + buildings modes) — reproduces the cellar-flap investigation with no launch. - docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil double-duty finding + the WB-recursive design decision + brainstorm prompt), entity-taxonomy, replan, issue-78 visibility investigation. Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert provisional pos.w clamp, and the probe families are kept (env-var gated, zero cost when off) because the pending option-2 cellar-flap brainstorm needs them. Strip in the option-2 ship commit. Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8 visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1081 lines
49 KiB
Markdown
1081 lines
49 KiB
Markdown
# Phase A8 RE-PLAN — Indoor-cell visibility culling (taxonomy-aware integration)
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Close issue #78 (outdoor stabs/terrain visible through indoor walls) and the cellar terrain artifact by porting WorldBuilder's stencil-based `RenderInsideOut` pipeline with an entity taxonomy that correctly distinguishes **building shell stabs** (cottage walls — must render unconditionally indoors) from **outdoor scenery stabs** (trees, lampposts — stencil-gated to portal silhouettes).
|
|
|
|
**Architecture:** Tag `WorldEntity.IsBuildingShell` at the `LandblockLoader` data boundary (sourced from `LandBlockInfo.Buildings` vs `LandBlockInfo.Objects` — the dat already carries the distinction). Refactor `WbDrawDispatcher.EntitySet` from binary `IndoorOnly`/`OutdoorOnly` to a three-way `IndoorPass` (cell mesh + cell statics + building shells) / `OutdoorScenery` (trees + procedural) / `LiveDynamic` (server-spawned). Re-wire the render frame with WB's MarkAndPunch-FIRST order so indoor cell depth correctly survives. Stencil-mark only the **camera's own cell's** exit portals (skip WB Step 5 / 3-stencil-bit for first ship; the cross-cell-portal visibility loss is acceptable).
|
|
|
|
**Tech Stack:** C# .NET 10, Silk.NET (OpenGL 4.3 + GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters), xUnit.
|
|
|
|
**Predecessor context (REQUIRED reading before starting):**
|
|
- [docs/research/2026-05-26-a8-revert-handoff.md](../../research/2026-05-26-a8-revert-handoff.md) — full story of the 3-round visual verification failure + reverts
|
|
- [docs/research/2026-05-26-a8-entity-taxonomy.md](../../research/2026-05-26-a8-entity-taxonomy.md) — approved fix-shape (3 cross-references converge: retail, WB, and acdream's own GameWindow.cs:5175 comment)
|
|
- [docs/superpowers/plans/2026-05-25-phase-a8-indoor-cell-visibility-culling.md](2026-05-25-phase-a8-indoor-cell-visibility-culling.md) — original plan; **Tasks 1-6 already shipped (dormant in-tree); do NOT re-execute Task 7 as written**
|
|
|
|
**Infrastructure preserved (consume as-is):**
|
|
- `src/AcDream.App/Rendering/CellVisibility.cs` — `LoadedCell.PortalPolygons: List<Vector3[]>` (populated by `BuildLoadedCell`)
|
|
- `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs` — `PortalMeshBuilder.BuildTriangles` (pure) + `IndoorCellStencilPipeline` (GL); `UploadPortalMesh` / `MarkAndPunch` / `EnableOutdoorPass` / `DisableStencil` already implement WB's Steps 1+2+4
|
|
- `src/AcDream.App/Rendering/Shaders/portal_stencil.vert/.frag` — minimal MVP + `gl_FragDepth=1.0` writer
|
|
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — `EntitySet` enum (will be reshaped) + `WalkEntitiesForTest` test helper
|
|
- `src/AcDream.Core/Rendering/RenderingDiagnostics.cs` — `ProbeVisibilityEnabled` flag (optional, available for diagnostics)
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
| File | What changes | Why |
|
|
|---|---|---|
|
|
| `src/AcDream.Core/World/WorldEntity.cs` | Add `IsBuildingShell: bool` (init-only, default false) | Carry the dat-level distinction through to render time |
|
|
| `src/AcDream.Core/World/LandblockLoader.cs` | Set `IsBuildingShell = true` in the `info.Buildings` loop; leave `info.Objects` loop unchanged | Tag at the only point both classes are still distinguishable |
|
|
| `src/AcDream.App/Rendering/GameWindow.cs` | One-line propagation in the stab hydration copy (line 5129-5136) | Preserve flag through dat→runtime hydration |
|
|
| `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` | Rename `EntitySet.IndoorOnly`→`IndoorPass`, `OutdoorOnly`→`OutdoorScenery`; add `LiveDynamic`. Extend `WalkEntitiesInto` + `WalkEntitiesForTest` partition logic | Three-way partition reflecting the actual taxonomy |
|
|
| `src/AcDream.App/Rendering/GameWindow.cs` | Re-wire render frame inside-camera branch with WB-order: MarkAndPunch → IndoorPass → stencil-gated terrain re-draw → OutdoorScenery → LiveDynamic | Integration that respects the entity-taxonomy lesson |
|
|
| `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs` | Rebuild against new enum values; add IsBuildingShell + LiveDynamic coverage | Lock partition correctness before integration |
|
|
| `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs` | Add tests confirming `IsBuildingShell` set correctly per source array | Lock the data-layer guarantee |
|
|
| `docs/ISSUES.md` | Move #78 to Recently closed; document deferred Step 5 / cellar-from-outside artifact | Ship-docs |
|
|
| `CLAUDE.md` | Update A8 paragraph from "REVERTED" → "SHIPPED" with ship summary | Roadmap discipline |
|
|
|
|
---
|
|
|
|
## Task R1: `WorldEntity.IsBuildingShell` flag + LandblockLoader tagging
|
|
|
|
**Files:**
|
|
- Modify: `src/AcDream.Core/World/WorldEntity.cs`
|
|
- Modify: `src/AcDream.Core/World/LandblockLoader.cs:58-87`
|
|
- Modify: `src/AcDream.App/Rendering/GameWindow.cs:5129-5137`
|
|
- Test: `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs`
|
|
|
|
- [ ] **R1-S1: Write failing tests for LandblockLoader IsBuildingShell tagging**
|
|
|
|
Append these tests to `tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs`. (Read the file first to find the existing test class + how to construct a `LandBlockInfo` mock with `Objects` and `Buildings` entries — there's already a setup pattern there; reuse it.)
|
|
|
|
```csharp
|
|
[Fact]
|
|
public void BuildEntitiesFromInfo_TagsBuildingsWithIsBuildingShellTrue()
|
|
{
|
|
var info = new DatReaderWriter.DBObjs.LandBlockInfo
|
|
{
|
|
Objects = new System.Collections.Generic.List<DatReaderWriter.Types.Stab>(),
|
|
Buildings = new System.Collections.Generic.List<DatReaderWriter.Types.BuildingInfo>
|
|
{
|
|
new DatReaderWriter.Types.BuildingInfo
|
|
{
|
|
ModelId = 0x02000123u, // Setup id
|
|
Frame = new DatReaderWriter.Types.Frame
|
|
{
|
|
Origin = new System.Numerics.Vector3(10f, 20f, 30f),
|
|
Orientation = System.Numerics.Quaternion.Identity,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
var entities = AcDream.Core.World.LandblockLoader.BuildEntitiesFromInfo(info);
|
|
|
|
Assert.Single(entities);
|
|
Assert.True(entities[0].IsBuildingShell);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildEntitiesFromInfo_TagsObjectsWithIsBuildingShellFalse()
|
|
{
|
|
var info = new DatReaderWriter.DBObjs.LandBlockInfo
|
|
{
|
|
Objects = new System.Collections.Generic.List<DatReaderWriter.Types.Stab>
|
|
{
|
|
new DatReaderWriter.Types.Stab
|
|
{
|
|
Id = 0x01000123u, // GfxObj id
|
|
Frame = new DatReaderWriter.Types.Frame
|
|
{
|
|
Origin = new System.Numerics.Vector3(10f, 20f, 30f),
|
|
Orientation = System.Numerics.Quaternion.Identity,
|
|
},
|
|
},
|
|
},
|
|
Buildings = new System.Collections.Generic.List<DatReaderWriter.Types.BuildingInfo>(),
|
|
};
|
|
|
|
var entities = AcDream.Core.World.LandblockLoader.BuildEntitiesFromInfo(info);
|
|
|
|
Assert.Single(entities);
|
|
Assert.False(entities[0].IsBuildingShell);
|
|
}
|
|
```
|
|
|
|
**Note:** if the existing tests in this file use a different `Stab`/`BuildingInfo` type, mirror that pattern. The field names above match what `LandblockLoader.cs` already reads at lines 58 and 74.
|
|
|
|
- [ ] **R1-S2: Run tests to verify they fail with "IsBuildingShell does not exist"**
|
|
|
|
```bash
|
|
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~LandblockLoaderTests.BuildEntitiesFromInfo_TagsBuildings" --nologo
|
|
```
|
|
|
|
Expected: BUILD FAILURE with "'WorldEntity' does not contain a definition for 'IsBuildingShell'".
|
|
|
|
- [ ] **R1-S3: Add `IsBuildingShell` to `WorldEntity`**
|
|
|
|
Edit `src/AcDream.Core/World/WorldEntity.cs`. Add the new property just below `ParentCellId` (around current line 46):
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// True when this entity originates from <c>LandBlockInfo.Buildings[]</c>
|
|
/// (the dat array that carries building shells: cottage walls, smithy walls,
|
|
/// inn walls — every solid building enclosure). False for entities from
|
|
/// <c>LandBlockInfo.Objects[]</c> (rocks, fences, lampposts, tree clusters —
|
|
/// outdoor scenery placeholders). The two arrays are conflated through
|
|
/// hydration today but the dat itself carries the distinction; retail
|
|
/// (<c>CLandBlock::init_buildings</c>) and WorldBuilder
|
|
/// (<c>SceneryInstance.IsBuilding</c>) both preserve it.
|
|
///
|
|
/// <para>
|
|
/// Read at draw time by <see cref="AcDream.App.Rendering.Wb.WbDrawDispatcher"/>'s
|
|
/// <c>IndoorPass</c> partition so building shells render unconditionally
|
|
/// when the camera is inside their building (they ARE the indoor walls),
|
|
/// not stencil-gated as outdoor scenery would be.
|
|
/// </para>
|
|
/// </summary>
|
|
public bool IsBuildingShell { get; init; }
|
|
```
|
|
|
|
- [ ] **R1-S4: Tag Buildings loop in LandblockLoader**
|
|
|
|
Edit `src/AcDream.Core/World/LandblockLoader.cs:78-85`. Add the `IsBuildingShell = true` initializer. The Objects loop at lines 62-69 stays unchanged (default `false`).
|
|
|
|
Change this block:
|
|
|
|
```csharp
|
|
var buildingEntity = new WorldEntity
|
|
{
|
|
Id = nextId++,
|
|
SourceGfxObjOrSetupId = building.ModelId,
|
|
Position = building.Frame.Origin,
|
|
Rotation = building.Frame.Orientation,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
```
|
|
|
|
To:
|
|
|
|
```csharp
|
|
var buildingEntity = new WorldEntity
|
|
{
|
|
Id = nextId++,
|
|
SourceGfxObjOrSetupId = building.ModelId,
|
|
Position = building.Frame.Origin,
|
|
Rotation = building.Frame.Orientation,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
IsBuildingShell = true, // Phase A8: tag at source array boundary
|
|
};
|
|
```
|
|
|
|
- [ ] **R1-S5: Propagate flag through GameWindow hydration**
|
|
|
|
Edit `src/AcDream.App/Rendering/GameWindow.cs:5129-5137`. The hydration loop copies fields from `e` into a fresh `WorldEntity`; `IsBuildingShell` is dropped today. Add propagation:
|
|
|
|
Change this block:
|
|
|
|
```csharp
|
|
var entity = new AcDream.Core.World.WorldEntity
|
|
{
|
|
Id = e.Id,
|
|
SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId,
|
|
Position = e.Position + worldOffset,
|
|
Rotation = e.Rotation,
|
|
MeshRefs = meshRefs,
|
|
};
|
|
```
|
|
|
|
To:
|
|
|
|
```csharp
|
|
var entity = new AcDream.Core.World.WorldEntity
|
|
{
|
|
Id = e.Id,
|
|
SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId,
|
|
Position = e.Position + worldOffset,
|
|
Rotation = e.Rotation,
|
|
MeshRefs = meshRefs,
|
|
IsBuildingShell = e.IsBuildingShell, // Phase A8: preserve dat-level tag
|
|
};
|
|
```
|
|
|
|
- [ ] **R1-S6: Run R1 tests to verify they pass**
|
|
|
|
```bash
|
|
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~LandblockLoaderTests.BuildEntitiesFromInfo_TagsBuildings" --nologo
|
|
```
|
|
|
|
Expected: 2 tests pass.
|
|
|
|
- [ ] **R1-S7: Full build + test to verify no regression**
|
|
|
|
```bash
|
|
dotnet build -c Debug --nologo
|
|
dotnet test --nologo
|
|
```
|
|
|
|
Expected: build green (0 warnings, 0 errors). Test failures should be within the documented pre-existing ~14-23 flaky window (PhysicsResolveCapture / PhysicsDiagnostics static-leak issues) — no NEW failures attributable to this change.
|
|
|
|
- [ ] **R1-S8: Commit**
|
|
|
|
```bash
|
|
git add src/AcDream.Core/World/WorldEntity.cs src/AcDream.Core/World/LandblockLoader.cs src/AcDream.App/Rendering/GameWindow.cs tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs
|
|
git commit -m "$(cat <<'EOF'
|
|
feat(world): Phase A8 R1 — tag WorldEntity.IsBuildingShell at LandblockLoader
|
|
|
|
Adds a bool flag at the WorldEntity data layer set by LandblockLoader from
|
|
the source dat array: LandBlockInfo.Buildings → true (cottage walls, inn
|
|
walls, smithy walls); LandBlockInfo.Objects → false (trees, lampposts,
|
|
rocks, hitching posts).
|
|
|
|
Retail anchor: CLandBlock::init_buildings reads a separate BuildInfo**
|
|
array from objects (acclient.h:31893 num_buildings / buildings field;
|
|
acclient_2013_pseudo_c.txt:313854 init_buildings entry). WorldBuilder
|
|
preserves the same distinction via SceneryInstance.IsBuilding
|
|
(StaticObjectRenderManager.cs:334). Today acdream's loader reads both
|
|
arrays into the same WorldEntity pool with no tag, destroying the
|
|
distinction (the comment at GameWindow.cs:5175 already acknowledges this
|
|
gap for scenery suppression). This commit closes the gap.
|
|
|
|
Render-time consumption arrives in R2 (EntitySet partition refactor).
|
|
Two new LandblockLoader tests lock the tagging behavior.
|
|
|
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
EOF
|
|
)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task R2: Reshape `WbDrawDispatcher.EntitySet` to taxonomy-aware partition
|
|
|
|
**Files:**
|
|
- Modify: `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — enum + 3 partition-logic sites (`WalkEntitiesInto` line 360-362 + line 375-377; `WalkEntitiesForTest` line 1358-1359)
|
|
- Modify: `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs` — rebuild against new semantics + add coverage
|
|
|
|
- [ ] **R2-S1: Write failing tests for the new partition semantics**
|
|
|
|
Replace the contents of `tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs` with:
|
|
|
|
```csharp
|
|
// Phase A8 — verify the WbDrawDispatcher EntitySet partition (taxonomy-aware).
|
|
//
|
|
// The pure-data WalkEntitiesForTest helper iterates a flat entity list and
|
|
// returns the IDs that survive the EntitySet filter + visibleCellIds gate.
|
|
//
|
|
// EntitySet.IndoorPass — ParentCellId.HasValue OR IsBuildingShell,
|
|
// and NOT live-dynamic (ServerGuid == 0).
|
|
// Building shells render unconditionally indoors;
|
|
// live-dynamic flows through LiveDynamic instead.
|
|
// EntitySet.OutdoorScenery — ParentCellId == null AND !IsBuildingShell
|
|
// AND not live-dynamic.
|
|
// EntitySet.LiveDynamic — ServerGuid != 0 (player, NPCs, dropped items,
|
|
// idle doors after animation). Drawn last with
|
|
// stencil disabled.
|
|
// EntitySet.All — pre-A8 behavior (visibleCellIds gates indoor;
|
|
// outdoor entities pass through).
|
|
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.Core.World;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Rendering.Wb;
|
|
|
|
public class WbDrawDispatcherEntitySetTests
|
|
{
|
|
private static WorldEntity CellEnt(uint id, uint cellId) => new()
|
|
{
|
|
Id = id,
|
|
SourceGfxObjOrSetupId = 0x01000001u,
|
|
ParentCellId = cellId,
|
|
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
};
|
|
|
|
private static WorldEntity OutdoorScenery(uint id) => new()
|
|
{
|
|
Id = id,
|
|
SourceGfxObjOrSetupId = 0x01000001u,
|
|
ParentCellId = null,
|
|
IsBuildingShell = false,
|
|
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
};
|
|
|
|
private static WorldEntity BuildingShell(uint id) => new()
|
|
{
|
|
Id = id,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
ParentCellId = null,
|
|
IsBuildingShell = true,
|
|
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
};
|
|
|
|
private static WorldEntity LiveDynamic(uint id, uint serverGuid) => new()
|
|
{
|
|
Id = id,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
ServerGuid = serverGuid,
|
|
ParentCellId = null,
|
|
IsBuildingShell = false,
|
|
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
};
|
|
|
|
[Fact]
|
|
public void IndoorPass_IncludesCellEntities()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
CellEnt(0x10000001, 0xA9B40143),
|
|
OutdoorScenery(0x10000002),
|
|
CellEnt(0x10000003, 0xA9B40144),
|
|
};
|
|
|
|
var visible = new HashSet<uint> { 0xA9B40143u, 0xA9B40144u };
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(0x10000001u, result);
|
|
Assert.Contains(0x10000003u, result);
|
|
Assert.DoesNotContain(0x10000002u, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorPass_IncludesBuildingShells_EvenWithNullParentCellId()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
BuildingShell(0xC0000001), // cottage wall
|
|
OutdoorScenery(0xC0000002), // tree
|
|
CellEnt(0x40000001, 0xA9B40143),
|
|
};
|
|
|
|
var visible = new HashSet<uint> { 0xA9B40143u };
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(0xC0000001u, result); // building shell included
|
|
Assert.Contains(0x40000001u, result); // cell entity included
|
|
Assert.DoesNotContain(0xC0000002u, result); // tree excluded
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorPass_ExcludesLiveDynamic()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
CellEnt(0x40000001, 0xA9B40143),
|
|
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
|
|
};
|
|
|
|
var visible = new HashSet<uint> { 0xA9B40143u };
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
|
|
|
|
Assert.Single(result);
|
|
Assert.Contains(0x40000001u, result);
|
|
Assert.DoesNotContain(0x10000001u, result); // live-dynamic excluded
|
|
}
|
|
|
|
[Fact]
|
|
public void OutdoorScenery_ExcludesBuildingShells()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
BuildingShell(0xC0000001), // cottage wall — excluded
|
|
OutdoorScenery(0xC0000002), // tree — included
|
|
CellEnt(0x40000001, 0xA9B40143), // cell — excluded
|
|
};
|
|
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorScenery);
|
|
|
|
Assert.Single(result);
|
|
Assert.Contains(0xC0000002u, result);
|
|
Assert.DoesNotContain(0xC0000001u, result);
|
|
Assert.DoesNotContain(0x40000001u, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void OutdoorScenery_ExcludesLiveDynamic()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
OutdoorScenery(0xC0000001),
|
|
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
|
|
};
|
|
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorScenery);
|
|
|
|
Assert.Single(result);
|
|
Assert.Contains(0xC0000001u, result);
|
|
Assert.DoesNotContain(0x10000001u, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveDynamic_IncludesOnlyServerSpawned()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
OutdoorScenery(0xC0000001),
|
|
BuildingShell(0xC0000002),
|
|
CellEnt(0x40000001, 0xA9B40143),
|
|
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
|
|
LiveDynamic(0x10000002, serverGuid: 0x50000456u),
|
|
};
|
|
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.LiveDynamic);
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(0x10000001u, result);
|
|
Assert.Contains(0x10000002u, result);
|
|
Assert.DoesNotContain(0xC0000001u, result);
|
|
Assert.DoesNotContain(0xC0000002u, result);
|
|
Assert.DoesNotContain(0x40000001u, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void All_MatchesPreA8Behavior()
|
|
{
|
|
var entities = new List<WorldEntity>
|
|
{
|
|
CellEnt(0x40000001, 0xA9B40143),
|
|
OutdoorScenery(0xC0000001),
|
|
BuildingShell(0xC0000002),
|
|
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
|
|
CellEnt(0x40000002, 0xA9B40999), // not in visibleCellIds
|
|
};
|
|
|
|
var visible = new HashSet<uint> { 0xA9B40143u };
|
|
var result = WbDrawDispatcher.WalkEntitiesForTest(
|
|
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.All);
|
|
|
|
// Pre-A8: visibleCellIds gates indoor entities only; outdoor entities
|
|
// (regardless of building/scenery/live-dynamic) pass through.
|
|
Assert.Equal(4, result.Count);
|
|
Assert.Contains(0x40000001u, result);
|
|
Assert.Contains(0xC0000001u, result);
|
|
Assert.Contains(0xC0000002u, result);
|
|
Assert.Contains(0x10000001u, result);
|
|
Assert.DoesNotContain(0x40000002u, result);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **R2-S2: Run tests to verify they fail with "IndoorPass not found"**
|
|
|
|
```bash
|
|
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WbDrawDispatcherEntitySetTests" --nologo
|
|
```
|
|
|
|
Expected: BUILD FAILURE with "'EntitySet' does not contain a definition for 'IndoorPass'" / "'LiveDynamic'".
|
|
|
|
- [ ] **R2-S3: Rename + extend the EntitySet enum**
|
|
|
|
Edit `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:69-81`. Replace the existing enum block with:
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Phase A8 — which subset of entities to walk in a single Draw call.
|
|
/// Used to split the indoor-cell visibility pipeline into three passes
|
|
/// when the camera is inside an EnvCell.
|
|
///
|
|
/// Taxonomy reference: docs/research/2026-05-26-a8-entity-taxonomy.md.
|
|
/// </summary>
|
|
public enum EntitySet
|
|
{
|
|
/// <summary>Pre-A8 behavior: every entity walked, gated only by
|
|
/// the existing <c>ParentCellId ∈ visibleCellIds</c> filter.
|
|
/// Used when the camera is OUTSIDE any EnvCell.</summary>
|
|
All,
|
|
|
|
/// <summary>Cell mesh + cell statics (<see cref="WorldEntity.ParentCellId"/>
|
|
/// non-null) PLUS building shell stabs (<see cref="WorldEntity.IsBuildingShell"/>
|
|
/// true, regardless of ParentCellId). These render unconditionally
|
|
/// when the camera is inside their building — building shells ARE
|
|
/// the indoor walls. Live-dynamic (<c>ServerGuid != 0</c>) is
|
|
/// excluded; it flows through <see cref="LiveDynamic"/>.</summary>
|
|
IndoorPass,
|
|
|
|
/// <summary>Outdoor scenery stabs (<c>ParentCellId == null</c>,
|
|
/// <c>!IsBuildingShell</c>) plus procedurally-generated scenery.
|
|
/// Drawn stencil-gated to portal silhouettes when the camera is
|
|
/// inside. Live-dynamic excluded.</summary>
|
|
OutdoorScenery,
|
|
|
|
/// <summary>Server-spawned dynamic entities (<c>ServerGuid != 0</c>):
|
|
/// player, NPCs, monsters, dropped items, animated and idle doors.
|
|
/// Drawn last with stencil disabled so they're depth-tested against
|
|
/// everything else but not stencil-clipped.</summary>
|
|
LiveDynamic,
|
|
}
|
|
```
|
|
|
|
- [ ] **R2-S4: Extend the partition logic at the three call sites**
|
|
|
|
Edit `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`. Find the partition predicate at lines 360-362 (inside the `landblockVisible == false` branch's animated-entity loop):
|
|
|
|
```csharp
|
|
// Phase A8: EntitySet partition for indoor/outdoor split passes.
|
|
if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue;
|
|
if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue;
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```csharp
|
|
// Phase A8: EntitySet partition (taxonomy-aware).
|
|
if (!EntityMatchesSet(entity, set)) continue;
|
|
```
|
|
|
|
Do the same at lines 375-377 (the main entity-walk loop). Replace:
|
|
|
|
```csharp
|
|
// Phase A8: EntitySet partition for indoor/outdoor split passes.
|
|
if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue;
|
|
if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue;
|
|
```
|
|
|
|
With:
|
|
|
|
```csharp
|
|
// Phase A8: EntitySet partition (taxonomy-aware).
|
|
if (!EntityMatchesSet(entity, set)) continue;
|
|
```
|
|
|
|
And at lines 1358-1359 in `WalkEntitiesForTest`, replace:
|
|
|
|
```csharp
|
|
if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue;
|
|
if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue;
|
|
```
|
|
|
|
With:
|
|
|
|
```csharp
|
|
if (!EntityMatchesSet(entity, set)) continue;
|
|
```
|
|
|
|
Then add the shared predicate as a private static method on `WbDrawDispatcher` (place it just above `WalkEntitiesForTest` near line 1344, so all three call sites can reach it):
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Phase A8 — entity-taxonomy-aware membership test for the three-way
|
|
/// EntitySet partition. See <see cref="EntitySet"/> for the doctrine.
|
|
/// </summary>
|
|
private static bool EntityMatchesSet(AcDream.Core.World.WorldEntity entity, EntitySet set)
|
|
{
|
|
if (set == EntitySet.All) return true;
|
|
|
|
bool isLiveDynamic = entity.ServerGuid != 0;
|
|
if (set == EntitySet.LiveDynamic) return isLiveDynamic;
|
|
if (isLiveDynamic) return false; // IndoorPass/OutdoorScenery exclude live-dynamic
|
|
|
|
bool isIndoor = entity.ParentCellId.HasValue || entity.IsBuildingShell;
|
|
if (set == EntitySet.IndoorPass) return isIndoor;
|
|
if (set == EntitySet.OutdoorScenery) return !isIndoor;
|
|
|
|
return true; // unreachable; defensive default = include
|
|
}
|
|
```
|
|
|
|
- [ ] **R2-S5: Run R2 tests to verify they pass**
|
|
|
|
```bash
|
|
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~WbDrawDispatcherEntitySetTests" --nologo
|
|
```
|
|
|
|
Expected: 7 tests pass.
|
|
|
|
- [ ] **R2-S6: Run full Core.Tests project to verify no regression**
|
|
|
|
```bash
|
|
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --nologo
|
|
```
|
|
|
|
Expected: pass-rate matches the documented pre-existing 14-23 flaky window. No NEW failures attributable to this change. If a test fails referencing `IndoorOnly` / `OutdoorOnly` by name (other than the EntitySetTests we just rewrote), update it inline — those are the renamed-enum references.
|
|
|
|
- [ ] **R2-S7: Full build to verify App project compiles**
|
|
|
|
```bash
|
|
dotnet build -c Debug --nologo
|
|
```
|
|
|
|
Expected: build green.
|
|
|
|
- [ ] **R2-S8: Commit**
|
|
|
|
```bash
|
|
git add src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherEntitySetTests.cs
|
|
git commit -m "$(cat <<'EOF'
|
|
feat(render): Phase A8 R2 — WbDrawDispatcher.EntitySet taxonomy partition
|
|
|
|
Reshapes the dormant EntitySet enum from binary IndoorOnly/OutdoorOnly to
|
|
a three-way taxonomy-aware partition:
|
|
|
|
IndoorPass — cell mesh + cell statics + building shells
|
|
(ParentCellId.HasValue OR IsBuildingShell), live-dynamic
|
|
excluded
|
|
OutdoorScenery — outdoor scenery only (ParentCellId == null AND
|
|
!IsBuildingShell), live-dynamic excluded
|
|
LiveDynamic — ServerGuid != 0 (player, NPCs, dropped items)
|
|
|
|
Centralizes the membership predicate in EntityMatchesSet to keep the three
|
|
call sites (two in WalkEntitiesInto, one in WalkEntitiesForTest) DRY.
|
|
|
|
R1's IsBuildingShell flag is now consumed at render time. Integration into
|
|
the render frame ships in R3.
|
|
|
|
Tests rebuilt from scratch — 7 cases cover the new partition truth table.
|
|
Existing dispatcher tests (Tier 1 cache, etc.) continue to pass under the
|
|
default EntitySet.All.
|
|
|
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
EOF
|
|
)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task R3: Wire stencil pipeline into the render frame (WB order)
|
|
|
|
**Files:**
|
|
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` — render frame inside-camera branch (around lines 7079-7170, depending on where the dispatcher call sits after R2)
|
|
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` — `IndoorCellStencilPipeline` field + ctor wiring (find the ctor that owns rendering pipelines; today it sits near the `WbDrawDispatcher` instantiation)
|
|
- No new tests (GL integration is visual-verification only; the partition logic + stencil math are already covered by existing unit tests from Tasks 1-6)
|
|
|
|
- [ ] **R3-S1: Locate the dispatcher field + add IndoorCellStencilPipeline field**
|
|
|
|
Grep for the existing `WbDrawDispatcher` field declaration to find the GameWindow ctor's pipeline-init block:
|
|
|
|
```bash
|
|
grep -n "_wbDrawDispatcher" src/AcDream.App/Rendering/GameWindow.cs | head -5
|
|
```
|
|
|
|
Find the line declaring `private readonly WbDrawDispatcher? _wbDrawDispatcher;` (or similar). Add a sibling field just below it:
|
|
|
|
```csharp
|
|
private readonly IndoorCellStencilPipeline? _indoorStencilPipeline;
|
|
```
|
|
|
|
And in the ctor where `_wbDrawDispatcher` is constructed, instantiate the pipeline. The shader path follows the existing pattern for other shaders in the same ctor (search for `portal_stencil` in source — the dormant infrastructure already references it):
|
|
|
|
```csharp
|
|
_indoorStencilPipeline = new IndoorCellStencilPipeline(
|
|
_gl,
|
|
System.IO.Path.Combine(shaderDir, "portal_stencil.vert"),
|
|
System.IO.Path.Combine(shaderDir, "portal_stencil.frag"));
|
|
```
|
|
|
|
Add a Dispose call in the `Dispose()` method alongside the other pipeline disposes:
|
|
|
|
```csharp
|
|
_indoorStencilPipeline?.Dispose();
|
|
```
|
|
|
|
- [ ] **R3-S2: Re-wire the render frame inside-camera branch**
|
|
|
|
Find the existing render flow in `GameWindow.cs` around the `cameraInsideCell` references (currently lines 7001, 7079, 7118, 7159). The current structure is:
|
|
|
|
```csharp
|
|
// Step 4: portal visibility — compute BEFORE the UBO upload so
|
|
// the indoor flag drives the sun's intensity to zero for dungeons.
|
|
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
|
bool cameraInsideCell = visibility?.CameraCell is not null;
|
|
// ... [unchanged: lighting, audio, fog setup] ...
|
|
|
|
// Sky (skipped when inside)
|
|
if (!cameraInsideCell)
|
|
{
|
|
_skyRenderer?.RenderSky(...);
|
|
_particleRenderer?.Draw(..., SkyPreScene);
|
|
}
|
|
|
|
// Terrain
|
|
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
|
|
|
// Conditional depth clear (depth only, keep terrain color)
|
|
if (cameraInsideCell)
|
|
_gl!.Clear(ClearBufferMask.DepthBufferBit);
|
|
|
|
// Animated-id set (unchanged)
|
|
HashSet<uint>? animatedIds = ...;
|
|
|
|
// Single dispatcher call (all entities)
|
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
|
neverCullLandblockId: playerLb,
|
|
visibleCellIds: visibility?.VisibleCellIds,
|
|
animatedEntityIds: animatedIds);
|
|
|
|
// Particles + weather (skipped when inside)
|
|
```
|
|
|
|
Replace the entity-draw section (from the dispatcher call down through `animatedIds` build) with this branching version. The terrain draw stays where it is (above the depth clear). After the depth clear:
|
|
|
|
```csharp
|
|
// L-fix1 (2026-04-28): animated-entity id set (unchanged from
|
|
// pre-A8). Required by both the cameraInsideCell branch (to
|
|
// route them to LiveDynamic pass) and the outdoor path (where
|
|
// it preserves visibility across landblock frustum culling).
|
|
HashSet<uint>? animatedIds = null;
|
|
if (_animatedEntities.Count > 0)
|
|
{
|
|
animatedIds = new HashSet<uint>(_animatedEntities.Count);
|
|
foreach (var k in _animatedEntities.Keys)
|
|
animatedIds.Add(k);
|
|
}
|
|
|
|
if (cameraInsideCell && _indoorStencilPipeline is not null
|
|
&& visibility?.CameraCell is not null)
|
|
{
|
|
// Phase A8: WB RenderInsideOut order.
|
|
//
|
|
// 1. Terrain has already drawn (color + depth).
|
|
// 2. Depth-clear-if-inside has already cleared depth to 1.0
|
|
// (above this branch). Punch at portal silhouettes is a
|
|
// no-op against that 1.0 baseline — left in for symmetry
|
|
// with WB's reference and to handle the unusual case
|
|
// where depth-clear is later dropped.
|
|
// 3. MarkAndPunch — stencil bit 1 at camera-cell exit portals.
|
|
// Step 5 (cross-cell-portal visibility via 3-stencil-bit
|
|
// pipeline) is DEFERRED — we mark ONLY the camera's own
|
|
// cell's portals, not the BFS-extended VisibleCellIds.
|
|
// Trade-off: cross-cell visibility loss (rare visually);
|
|
// correctness in the common case (no see-through-wall to
|
|
// far-side portal openings).
|
|
var cameraCells = new[] { visibility.CameraCell };
|
|
_indoorStencilPipeline.UploadPortalMesh(cameraCells);
|
|
|
|
var viewProjection = camera.View * camera.Projection;
|
|
_indoorStencilPipeline.MarkAndPunch(viewProjection);
|
|
|
|
// 4. IndoorPass — cell mesh + cell statics + building shells
|
|
// (R1's IsBuildingShell flag drives the partition).
|
|
// Stencil OFF (MarkAndPunch's cleanup restored that).
|
|
// Depth test normal; building shells write the wall depth
|
|
// that protects the indoor from outdoor visibility.
|
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
|
neverCullLandblockId: playerLb,
|
|
visibleCellIds: visibility.VisibleCellIds,
|
|
animatedEntityIds: animatedIds,
|
|
set: WbDrawDispatcher.EntitySet.IndoorPass);
|
|
|
|
// 5. Stencil-gated outdoor: enable stencil read-only.
|
|
_indoorStencilPipeline.EnableOutdoorPass();
|
|
|
|
// 5a. Re-draw terrain — at portal-silhouette pixels only,
|
|
// terrain Z (with the f48c74a -0.01 nudge) wins over the
|
|
// punched 1.0 depth. Color writes through window.
|
|
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
|
|
|
// 5b. Outdoor scenery — same stencil gating.
|
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
|
neverCullLandblockId: playerLb,
|
|
visibleCellIds: visibility.VisibleCellIds,
|
|
animatedEntityIds: animatedIds,
|
|
set: WbDrawDispatcher.EntitySet.OutdoorScenery);
|
|
|
|
// 6. Stencil OFF — live dynamic entities draw freely with
|
|
// depth test only.
|
|
_indoorStencilPipeline.DisableStencil();
|
|
|
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
|
neverCullLandblockId: playerLb,
|
|
visibleCellIds: visibility.VisibleCellIds,
|
|
animatedEntityIds: animatedIds,
|
|
set: WbDrawDispatcher.EntitySet.LiveDynamic);
|
|
}
|
|
else
|
|
{
|
|
// Outdoor path — unchanged from pre-A8: single dispatcher
|
|
// call walks every entity with default partition.
|
|
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
|
neverCullLandblockId: playerLb,
|
|
visibleCellIds: visibility?.VisibleCellIds,
|
|
animatedEntityIds: animatedIds);
|
|
}
|
|
```
|
|
|
|
- [ ] **R3-S3: Build to verify the integration compiles**
|
|
|
|
```bash
|
|
dotnet build -c Debug --nologo 2>&1 | tail -10
|
|
```
|
|
|
|
Expected: green. If the `camera.View * camera.Projection` expression is wrong for the actual `camera` type in scope, substitute with the correct accessor (read 5 lines above the integration point — the rest of the render frame already uses `camera` for view/projection access; mirror that style).
|
|
|
|
- [ ] **R3-S4: Run full test suite — must stay within the documented flaky window**
|
|
|
|
```bash
|
|
dotnet test --nologo
|
|
```
|
|
|
|
Expected: failures within the documented 14-23 flaky window only. No new failures attributable to GameWindow.cs changes (GL integration is not unit-tested, but build alone catches compile errors and pre-existing tests catch any unintended logic changes).
|
|
|
|
- [ ] **R3-S5: Commit (no visual verification yet — that's R4)**
|
|
|
|
```bash
|
|
git add src/AcDream.App/Rendering/GameWindow.cs
|
|
git commit -m "$(cat <<'EOF'
|
|
feat(render): Phase A8 R3 — wire stencil pipeline into render frame (WB order)
|
|
|
|
Replaces the pre-A8 single dispatcher call with the WB RenderInsideOut
|
|
order when cameraInsideCell:
|
|
|
|
1. Terrain draws normally (color + depth)
|
|
2. depth-clear-if-inside (depth = 1.0 globally)
|
|
3. MarkAndPunch — stencil bit 1 at camera's-own-cell exit portals
|
|
4. IndoorPass — cell mesh + cell statics + building shells, stencil OFF
|
|
5. EnableOutdoorPass + re-draw terrain + OutdoorScenery, stencil-gated
|
|
6. DisableStencil + LiveDynamic, depth-test only
|
|
|
|
Outdoor (cameraInsideCell == false) path unchanged: single Draw(set: All).
|
|
|
|
Step 5 (WB's 3-stencil-bit cross-cell-portal pipeline) is DEFERRED — we
|
|
mark only the camera's own cell's exit portals via [visibility.CameraCell],
|
|
not the BFS-extended VisibleCellIds. Trade-off documented in
|
|
docs/research/2026-05-26-a8-entity-taxonomy.md §"open questions".
|
|
|
|
Visual verification at cottage interior / cottage cellar / inn interior /
|
|
dungeon is R4.
|
|
|
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
EOF
|
|
)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task R4: Visual verification matrix
|
|
|
|
**Files:** none modified in this task; only logs collected and a verification report appended to the plan.
|
|
|
|
This task ships nothing on its own — it's the visual-gate before R5 (ship docs). The verification scenarios are chosen because each surfaced different bugs in the original A8 attempt and they exercise different parts of the entity taxonomy.
|
|
|
|
- [ ] **R4-S1: Build for verification**
|
|
|
|
```bash
|
|
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug --nologo
|
|
```
|
|
|
|
Expected: green.
|
|
|
|
- [ ] **R4-S2: Pre-launch — close any running client; launch**
|
|
|
|
PowerShell:
|
|
|
|
```powershell
|
|
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
|
|
if ($proc) {
|
|
$proc.CloseMainWindow() | Out-Null
|
|
if (-not $proc.WaitForExit(5000)) { $proc | Stop-Process -Force }
|
|
}
|
|
Start-Sleep -Seconds 3
|
|
|
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
|
$env:ACDREAM_LIVE = "1"
|
|
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
|
$env:ACDREAM_TEST_PORT = "9000"
|
|
$env:ACDREAM_TEST_USER = "testaccount"
|
|
$env:ACDREAM_TEST_PASS = "testpassword"
|
|
|
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
|
Tee-Object -FilePath "launch-a8-verify.log"
|
|
```
|
|
|
|
(If running via Claude, use `run_in_background: true` so the tool returns immediately.)
|
|
|
|
- [ ] **R4-S3: Scenario A — Holtburg cottage interior (ground floor)**
|
|
|
|
Walk `+Acdream` into one of the Holtburg cottages (any of the cottages near the network portal). Stand in the middle of the room.
|
|
|
|
**Acceptance:**
|
|
- All walls SOLID — no see-through to outdoor terrain, no see-through to neighboring cottages.
|
|
- Outdoor terrain visible ONLY through windows / open doors.
|
|
- Player character body visible (no head-backwards, no missing limbs, no flickering on enter).
|
|
- No "transparent rectangles around buildings" regression (#100 stays closed).
|
|
|
|
If the wall opposite a window shows outdoor terrain bleed-through, that's the cross-cell-portal issue (deferred WB Step 5); document but **don't** treat as a R4 blocker if it's faint and rare.
|
|
|
|
If walls are TOTALLY MISSING (Round 3 regression) — building shells are not being included in IndoorPass. STOP, investigate `EntityMatchesSet` and the hydration propagation in R1-S5; do not proceed.
|
|
|
|
- [ ] **R4-S4: Scenario B — Holtburg cottage cellar**
|
|
|
|
Walk to a cottage that has a cellar (per #98 saga, the cottage near Holtburg Town's small green; descend the stairs).
|
|
|
|
**Acceptance:**
|
|
- Cellar walls + floor + ceiling SOLID.
|
|
- Cellar stairs SOLID — no grass/terrain overlay through the stair geometry from INSIDE (the in-to-out half of the cellar artifact).
|
|
- **Known limitation (NOT an R4 blocker):** grass/terrain may still be visible through the stair geometry when looking from OUTSIDE the cellar (out-to-in half). That's the deep-cell terrain Z-fight artifact noted in the predecessor handoff — NOT A8 scope; file separately in R5.
|
|
|
|
- [ ] **R4-S5: Scenario C — Holtburg Inn (multi-room indoor)**
|
|
|
|
Walk into the Holtburg inn (the larger building near the town network portal). Move through its rooms.
|
|
|
|
**Acceptance:**
|
|
- All inn walls SOLID.
|
|
- Adjacent rooms not visible through walls (no "I can see the door of the next room" regression from Round 2).
|
|
- The inn's interior uses cell mesh more than cottages — confirms the `ParentCellId.HasValue` path in IndoorPass works.
|
|
- Furniture (cell statics) visible and properly positioned.
|
|
|
|
- [ ] **R4-S6: Scenario D — A dungeon (portal-entry indoor world)**
|
|
|
|
Pick any reachable dungeon. The closest from Holtburg is Holtburg Sewer (if mapped on this server) — otherwise the network portal can teleport to any dungeon zone.
|
|
|
|
**Acceptance:**
|
|
- Corridor walls SOLID.
|
|
- Adjacent corridors not visible through walls.
|
|
- Lighting reads as indoor (sun zeroed, indoor ambient applied).
|
|
- No outdoor stab/terrain leak.
|
|
|
|
Dungeons exercise cell mesh + cell statics ONLY (no building shells; dungeons aren't building-baked). If dungeons regress while cottages work, the bug is in the IndoorPass partition predicate's interaction with cell-mesh entities — not the IsBuildingShell flag.
|
|
|
|
- [ ] **R4-S7: Graceful close + collect log**
|
|
|
|
```powershell
|
|
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
|
|
if ($proc) {
|
|
$proc.CloseMainWindow() | Out-Null
|
|
if (-not $proc.WaitForExit(5000)) { $proc | Stop-Process -Force }
|
|
}
|
|
```
|
|
|
|
Append verification notes to the plan or to a follow-up handoff doc:
|
|
- Scenario A result: PASS / FAIL with notes
|
|
- Scenario B result: PASS / FAIL with notes
|
|
- Scenario C result: PASS / FAIL with notes
|
|
- Scenario D result: PASS / FAIL with notes
|
|
|
|
- [ ] **R4-S8: Gate decision**
|
|
|
|
- If all four scenarios PASS or have only documented known limitations → proceed to R5.
|
|
- If ANY scenario fails an acceptance criterion → STOP, do not commit ship docs. Open a new investigation (`/investigate` skill) to triage the failure. The taxonomy fix is correct in principle; failures here are integration-detail bugs (GL state, ordering, missed entity class) that need narrow fixes rather than a re-revert.
|
|
|
|
---
|
|
|
|
## Task R5: Ship docs (close #78, update CLAUDE.md, file deferrals)
|
|
|
|
**Files:**
|
|
- Modify: `docs/ISSUES.md` — move #78 to Recently closed; file new ISSUES for the known limitations
|
|
- Modify: `CLAUDE.md` — update the A8 paragraph from "REVERTED" → "SHIPPED"
|
|
|
|
- [ ] **R5-S1: Update ISSUES.md**
|
|
|
|
Read `docs/ISSUES.md` and find the #78 entry (currently OPEN). Move it to "Recently closed" with a commit ref:
|
|
|
|
```markdown
|
|
**#78 — Outdoor stabs/buildings visible through the rendered floor** — CLOSED by R3 (commit <SHA>). Phase A8 re-plan ported WB's RenderInsideOut stencil pipeline with a corrected entity taxonomy (WorldEntity.IsBuildingShell flag distinguishing building shells from outdoor scenery stabs). Visual-verified at cottage interior, cottage cellar, Holtburg inn, dungeon.
|
|
```
|
|
|
|
In OPEN issues, file the two known limitations as new issues (assign next sequential numbers — read the doc to find the highest current ID and add to it):
|
|
|
|
```markdown
|
|
## #102 — Far-side portal visibility through walls (WB Step 5 deferral)
|
|
|
|
**Status:** OPEN (low priority; first ship of A8 deferred this).
|
|
|
|
**Description:** When standing inside a multi-room building, looking at a wall between rooms, portals on the FAR side of the room (e.g. a doorway opening to outdoors on the other side of the wall) may have their silhouette stencil-marked by Phase A8. This lets outdoor terrain leak through the wall at that silhouette. The first-ship approximation in A8 R3 stencil-marks ONLY the camera's own cell's exit portals (not BFS-extended VisibleCellIds), which AVOIDS the leak in most cases but loses cross-cell-portal visibility.
|
|
|
|
**Acceptance:** Inside Holtburg Inn looking at the wall between two rooms, no visible terrain or scenery shows through. WB Step 5's 3-stencil-bit cross-building pipeline is the reference fix.
|
|
|
|
**Files:**
|
|
- `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs` — currently single-bit stencil; would extend to bits 1+2.
|
|
- `src/AcDream.App/Rendering/GameWindow.cs` — render frame would gain a per-far-portal pass.
|
|
|
|
## #103 — Outdoor-to-indoor cellar terrain Z-fight (out-to-in artifact)
|
|
|
|
**Status:** OPEN (low priority; pre-existing).
|
|
|
|
**Description:** Looking from OUTSIDE a cottage cellar at the stair geometry from above, grass/terrain may overlap the stair triangles. Pre-existing; not addressed by A8 (no stencil work runs when the camera is outside). #100's 1cm terrain Z nudge is insufficient because cellar geometry sits multiple meters below terrain Z — depth-precision artifacts persist at oblique angles.
|
|
|
|
**Acceptance:** From outside a cottage, looking at the cellar entrance, stair geometry reads as solid stone (no grass overlay) regardless of camera angle.
|
|
|
|
**Files:** likely a deeper terrain-occlusion mechanism (per-cell terrain mask, or proper outdoor portal culling) — beyond the scope of A8.
|
|
```
|
|
|
|
- [ ] **R5-S2: Update CLAUDE.md A8 paragraph**
|
|
|
|
Read CLAUDE.md and find the current A8 paragraph. It currently begins "Phase A8 — REVERTED 2026-05-26..." (or similar). Replace with a SHIPPED summary along the lines of:
|
|
|
|
```markdown
|
|
**Phase A8 — Indoor-cell visibility culling — SHIPPED 2026-05-26.** Closes
|
|
issue #78. Five commits across the re-plan:
|
|
- R1: `WorldEntity.IsBuildingShell` flag set at `LandblockLoader` from
|
|
`LandBlockInfo.Buildings` vs `LandBlockInfo.Objects` — the dat-level
|
|
distinction acdream's loader was destroying.
|
|
- R2: `WbDrawDispatcher.EntitySet` reshape to taxonomy-aware partition
|
|
(`IndoorPass` / `OutdoorScenery` / `LiveDynamic`).
|
|
- R3: Render frame re-wired with WB's RenderInsideOut order — MarkAndPunch
|
|
before indoor draw; stencil-gated outdoor re-draw; live dynamic last
|
|
with stencil disabled. Camera's-own-cell-portals-only approximation
|
|
(WB Step 5 deferred as #102).
|
|
- Tasks 1-6 infrastructure (`PortalPolygons`, `IndoorCellStencilPipeline`,
|
|
`portal_stencil` shaders, dormant `EntitySet` enum) shipped 2026-05-25
|
|
and consumed as-is.
|
|
|
|
Visual-verified at Holtburg cottage interior, cottage cellar (in-to-out
|
|
half), Holtburg Inn (multi-room), and a dungeon. Two deferrals filed as
|
|
#102 (cross-cell-portal far-side visibility) and #103 (cellar terrain
|
|
Z-fight from outside).
|
|
|
|
Full re-plan: [docs/superpowers/plans/2026-05-26-phase-a8-replan.md](docs/superpowers/plans/2026-05-26-phase-a8-replan.md).
|
|
Taxonomy reference: [docs/research/2026-05-26-a8-entity-taxonomy.md](docs/research/2026-05-26-a8-entity-taxonomy.md).
|
|
Revert handoff (now historical): [docs/research/2026-05-26-a8-revert-handoff.md](docs/research/2026-05-26-a8-revert-handoff.md).
|
|
```
|
|
|
|
Find any other A8 references in CLAUDE.md (e.g. the "Currently working toward" line if it says A8) and update them to reflect ship.
|
|
|
|
- [ ] **R5-S3: Commit ship docs**
|
|
|
|
```bash
|
|
git add docs/ISSUES.md CLAUDE.md
|
|
git commit -m "$(cat <<'EOF'
|
|
ship(render): Phase A8 — indoor-cell visibility culling SHIPPED
|
|
|
|
Closes #78 (outdoor stabs/terrain visible through indoor walls). Files
|
|
#102 (cross-cell-portal far-side visibility, WB Step 5 deferral) and
|
|
#103 (cellar terrain Z-fight from outside; pre-existing, not A8 scope).
|
|
|
|
Visual-verified at Holtburg cottage interior, cottage cellar, Holtburg
|
|
Inn, and dungeon.
|
|
|
|
Architecture: entity taxonomy partition (WorldEntity.IsBuildingShell
|
|
tagged at LandblockLoader) drives WbDrawDispatcher.EntitySet into a
|
|
three-way IndoorPass / OutdoorScenery / LiveDynamic split. Render frame
|
|
follows WB's RenderInsideOut order with the camera's-own-cell-portals-only
|
|
approximation.
|
|
|
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
EOF
|
|
)"
|
|
```
|
|
|
|
---
|
|
|
|
## Self-review (run after writing this plan, before execution)
|
|
|
|
- **R1 covers:** the IsBuildingShell flag, loader tagging, hydration propagation, two LandblockLoader tests. ✓
|
|
- **R2 covers:** enum reshape, partition predicate, three call sites updated, seven tests covering the truth table. ✓
|
|
- **R3 covers:** pipeline field + ctor wiring, render frame branching, both inside + outside paths. ✓
|
|
- **R4 covers:** four visual scenarios chosen to surface different bug classes (cottage = building shell + cell mesh; cellar = building shell + sloped; inn = multi-room cell mesh; dungeon = cell mesh only). ✓
|
|
- **R5 covers:** ISSUES.md move + two deferral filings, CLAUDE.md ship summary, commit. ✓
|
|
|
|
**Cross-task type consistency check:** `IsBuildingShell` (bool, init-only on WorldEntity) is used consistently in R1 (declaration + set), R2 (EntityMatchesSet predicate), and R3 (no direct reference; goes through the predicate). `EntitySet.IndoorPass` / `OutdoorScenery` / `LiveDynamic` names match across R2 (definition) and R3 (consumption). `_indoorStencilPipeline` is the field name introduced in R3-S1 and referenced through R3-S2. No drift detected.
|
|
|
|
**Placeholder scan:** no "TBD" / "implement later" / "similar to" / generic-validation strings. Every code step has actual code. Every command step has an actual command.
|
|
|
|
**Spec coverage:** original handoff's pickup prompt Phase 2 expected R1-R6 tasks. This plan ships R1 (entity distinguisher) + R2 (EntitySet partition) + R3 (render frame re-wire) + R4 (visual verification matrix) + R5 (ship docs). R6 (deferred Step 5 etc.) is folded into R5 as filed issues #102 + #103 — no separate task needed since "decide to defer" was already approved in the entity-taxonomy fix-shape.
|