docs: phase 2 design — static meshes, textures, neighbors
Locks the design decisions from brainstorming: full scope (geometry + textures + 3x3 neighbor grid), dual cameras (orbit default + FlyCamera toggled by F), minimal plugin API growth (IGameState.Entities snapshot + IEvents.EntitySpawned). BCnEncoder.Net handles DXT/BCn decoding, matching WorldBuilder's approach. Adds three new namespaces under AcDream.Core (World, Meshing, Textures) and three new rendering components under AcDream.App (StaticMeshRenderer, TextureCache, FlyCamera + ICamera). Entities are deduplicated by GfxObj id at the GPU layer; textures dedup by Surface id. Plugins get immutable snapshots, not live references. Rough 18-task sequence captured. Full bite-sized plan comes next via superpowers:writing-plans. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2089bf3d56
commit
7e601073d4
1 changed files with 441 additions and 0 deletions
441
docs/plans/2026-04-10-phase-2-static-meshes-design.md
Normal file
441
docs/plans/2026-04-10-phase-2-static-meshes-design.md
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
# acdream Phase 2 design: static meshes, textures, neighbor landblocks
|
||||||
|
|
||||||
|
**Date:** 2026-04-10
|
||||||
|
**Status:** Approved — design locked, ready for implementation planning.
|
||||||
|
**Builds on:** Phase 1 (`docs/plans/2026-04-10-phase-1-terrain-and-plugin-scaffold.md`, merged as commit `2089bf3`).
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Turn the single flat-shaded Holtburg terrain patch from Phase 1 into a recognizable textured 3×3 landblock world with all static objects (buildings, trees, rocks) rendered from their GfxObj / Setup sources, explorable by both orbit and free-fly cameras, with a minimal plugin API exposing the world entity list.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Lighting (flat color + diffuse texture only). Deferred to later phase.
|
||||||
|
- Frustum culling. 9 landblocks is small enough that drawing everything every frame is fine.
|
||||||
|
- Skeletal animation. Static objects only. Monsters/NPCs/players are Phase 3+.
|
||||||
|
- Collision. Free-fly clips through walls.
|
||||||
|
- Level-of-detail (LOD) swaps. Draw the full-res mesh at all distances.
|
||||||
|
- Hot world changes (`IGameState.Entities` is a one-shot snapshot built at load time).
|
||||||
|
- Per-cell terrain texture blending. Hard edges between adjacent terrain types are acceptable.
|
||||||
|
|
||||||
|
## Locked decisions (from brainstorming)
|
||||||
|
|
||||||
|
1. **Full scope:** geometry + textures + 3×3 neighbor landblocks.
|
||||||
|
2. **Both cameras:** `OrbitCamera` remains default, `FlyCamera` added, `F` key toggles. `ICamera` interface unifies them.
|
||||||
|
3. **Minimal plugin API:** `IGameState.Entities` (snapshot) + `IEvents.EntitySpawned` (per-entity-at-load). No overlay yet.
|
||||||
|
4. **BCnEncoder.Net** for DXT/BCn texture decoding (matches WorldBuilder's choice).
|
||||||
|
|
||||||
|
## Architecture overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ acdream client │
|
||||||
|
│ │
|
||||||
|
│ dat directory │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ AcDream.Core │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │ │
|
||||||
|
│ │ │ Terrain │ │ World │ │ Textures │ │ │
|
||||||
|
│ │ │ (P1) │ │ │ │ │ │ │
|
||||||
|
│ │ │ │ │ LandblockLdr │ │ Surface │ │ │
|
||||||
|
│ │ │ Landblock│ │ WorldView │ │ Decoder │ │ │
|
||||||
|
│ │ │ Mesh │ │ (3x3 grid) │ │ (BCn) │ │ │
|
||||||
|
│ │ └──────────┘ │ WorldEntity │ └────────────┘ │ │
|
||||||
|
│ │ └──────────────┘ │ │
|
||||||
|
│ │ ┌──────────────────────────────┐ │ │
|
||||||
|
│ │ │ Meshing │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ GfxObjMesh.Build │ │ │
|
||||||
|
│ │ │ SetupMesh.Flatten │ │ │
|
||||||
|
│ │ └──────────────────────────────┘ │ │
|
||||||
|
│ └──────┬───────────────┬──────────────┬────────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ AcDream.App / Rendering │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ TerrainRenderer(updated) StaticMeshRenderer(new) │ │
|
||||||
|
│ │ ↳ atlas sampling ↳ one VAO per GfxObj │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ TextureCache(new) │ │
|
||||||
|
│ │ ↳ Surface.id → GL handle │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Shader: terrain.vert/frag (updated, samples atlas) │ │
|
||||||
|
│ │ Shader: mesh.vert/frag (new) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ICamera(new) │ │
|
||||||
|
│ │ ├── OrbitCamera (P1, refactored behind interface) │ │
|
||||||
|
│ │ └── FlyCamera (new, WASD + mouse look) │ │
|
||||||
|
│ │ CameraController (F key toggles) │ │
|
||||||
|
│ └──────────────┬───────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────┐ │
|
||||||
|
│ │ Plugin pipeline (P1) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ + IGameState.Entities │ (snapshot) │
|
||||||
|
│ │ + IEvents.EntitySpawned │ (per-entity, at load) │
|
||||||
|
│ └─────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key principles:**
|
||||||
|
|
||||||
|
1. Pure CPU work (`World/`, `Meshing/`, `Textures/`) lives under `AcDream.Core` and is TDD-tested.
|
||||||
|
2. GPU work (`StaticMeshRenderer`, `TextureCache`, shaders, cameras) lives under `AcDream.App` and is manual-smoke tested against real retail dats.
|
||||||
|
3. Meshes are **deduplicated by GfxObj id.** One VAO per unique mesh. Entities sharing a GfxObj share its VAO — the draw loop iterates entities and sets per-entity model matrices.
|
||||||
|
4. Textures are **deduplicated by Surface id.** `TextureCache` lazily uploads on miss.
|
||||||
|
5. The 3×3 neighbor grid is a concern of `WorldView`, not `LandblockLoader`. The loader loads one landblock; the view composes nine.
|
||||||
|
6. Plugins receive **immutable snapshots** (`WorldEntitySnapshot`), not live `WorldEntity` references. Protects core state.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
startup
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Load center landblock ID (Holtburg 0xA9B4)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
For each of 9 landblocks in 3x3 grid around center:
|
||||||
|
│
|
||||||
|
├─► LandBlock (heightmap) ──► LandblockMesh.Build (Phase 1)
|
||||||
|
│
|
||||||
|
└─► LandBlockInfo (static objects)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
For each "stab" (static object placement):
|
||||||
|
│
|
||||||
|
├─► Read object id + position + rotation + scale
|
||||||
|
│
|
||||||
|
├─► Resolve the id to a GfxObj or a Setup
|
||||||
|
│ │
|
||||||
|
│ ├── GfxObj ──► extract one CPU mesh
|
||||||
|
│ │
|
||||||
|
│ └── Setup ──► recursively flatten into N (GfxObjId, PartTransform)
|
||||||
|
│ tuples — one per part
|
||||||
|
│
|
||||||
|
├─► Create WorldEntity { Id, SourceId, Position, Rotation, Scale, MeshRefs[] }
|
||||||
|
│
|
||||||
|
└─► Fire IEvents.EntitySpawned(snapshot)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
For each unique GfxObj referenced by any entity:
|
||||||
|
│
|
||||||
|
├─► Decode its Surfaces through Surface → SurfaceTexture → RenderSurface
|
||||||
|
│ using BCnEncoder.Net for DXT, palette lookup for indexed textures
|
||||||
|
│
|
||||||
|
├─► Upload RGBA8 bytes as a GL texture (via TextureCache)
|
||||||
|
│
|
||||||
|
└─► Build VAO/VBO/EBO for the mesh (via StaticMeshRenderer)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Main loop:
|
||||||
|
│
|
||||||
|
├─► camera (orbit or fly, via ICamera)
|
||||||
|
│
|
||||||
|
├─► TerrainRenderer.Draw(camera, LandblockMeshData[] + origins)
|
||||||
|
│ for each of 9 landblock meshes:
|
||||||
|
│ bind terrain shader + terrain atlas texture
|
||||||
|
│ set model matrix (translate by landblock origin)
|
||||||
|
│ draw
|
||||||
|
│
|
||||||
|
└─► StaticMeshRenderer.Draw(camera, IEnumerable<WorldEntity>)
|
||||||
|
group entities by GfxObj id
|
||||||
|
for each group:
|
||||||
|
bind mesh shader
|
||||||
|
bind the GfxObj's diffuse texture
|
||||||
|
for each entity in group:
|
||||||
|
set model matrix (translate + rotate + scale)
|
||||||
|
draw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### `AcDream.Core/World/`
|
||||||
|
|
||||||
|
**`WorldEntity`** — sealed class holding per-spawn state:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class WorldEntity
|
||||||
|
{
|
||||||
|
public required uint Id { get; init; }
|
||||||
|
public required uint SourceGfxObjOrSetupId { get; init; }
|
||||||
|
public required Vector3 Position { get; init; }
|
||||||
|
public required Quaternion Rotation { get; init; }
|
||||||
|
public required Vector3 Scale { get; init; }
|
||||||
|
public required IReadOnlyList<MeshRef> MeshRefs { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct MeshRef(uint GfxObjId, Matrix4x4 PartTransform);
|
||||||
|
```
|
||||||
|
|
||||||
|
`Id` is a monotonic counter assigned at load time (no wire protocol yet). `SourceGfxObjOrSetupId` is the dat id that this entity was built from; used for deduplication in rendering.
|
||||||
|
|
||||||
|
**`LandblockLoader`** — static class with `Load(DatCollection dats, uint landblockId) → LoadedLandblock`. Returns the heightmap `LandBlock` + the flat list of `WorldEntity` from `LandBlockInfo`. No GPU state. Testable.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record LoadedLandblock(
|
||||||
|
uint LandblockId,
|
||||||
|
LandBlock Heightmap,
|
||||||
|
IReadOnlyList<WorldEntity> Entities);
|
||||||
|
```
|
||||||
|
|
||||||
|
**`WorldView`** — the 3×3 aggregate:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class WorldView
|
||||||
|
{
|
||||||
|
public uint CenterLandblockId { get; }
|
||||||
|
public IReadOnlyList<LoadedLandblock> Landblocks { get; }
|
||||||
|
public IEnumerable<WorldEntity> AllEntities => Landblocks.SelectMany(lb => lb.Entities);
|
||||||
|
|
||||||
|
public static WorldView Load(DatCollection dats, uint centerLandblockId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes neighbor ids from the center: the high 16 bits of a landblock id are an `0xXXYY` coordinate, so neighbors are `(x-1..x+1, y-1..y+1)`. Missing neighbors (edges of the world, or missing in the dat) are silently skipped.
|
||||||
|
|
||||||
|
### `AcDream.Core/Meshing/`
|
||||||
|
|
||||||
|
**`GfxObjMesh.Build(GfxObj) → LandblockMeshData`** — walks `GfxObj.VertexArray` + `GfxObj.Polygons` into a vertex+index mesh. This is the hardest piece of Phase 2.
|
||||||
|
|
||||||
|
Key challenges:
|
||||||
|
- AC's GfxObj uses per-polygon vertex indexing with separate UV indices per vertex per polygon. A single vertex position may appear multiple times with different UVs — we need to either duplicate vertices or build a `(posIdx, uvIdx)` → output-vertex-idx map.
|
||||||
|
- Polygons can have more than 3 vertices (4, 5, ...). Simple fan triangulation is fine for convex polygons.
|
||||||
|
- Polygons reference a `Surface` index; for Phase 2, we record that as vertex data so the shader knows which texture to sample. For now, we assume each GfxObj uses one surface — if multiple, we pick the first (Phase 3 handles multi-surface properly).
|
||||||
|
|
||||||
|
We reuse the `Vertex` struct from Phase 1 (`Position`, `Normal`, `TexCoord`) even though `Normal` will be flat `UnitZ` for now.
|
||||||
|
|
||||||
|
**`SetupMesh.Flatten(Setup, DatCollection) → IReadOnlyList<(uint GfxObjId, Matrix4x4 PartTransform)>`** — walks a `Setup`'s parts and placement frames, yields the flat list of (GfxObjId, transform) tuples for a single `WorldEntity` to reference.
|
||||||
|
|
||||||
|
### `AcDream.Core/Textures/`
|
||||||
|
|
||||||
|
**`SurfaceDecoder.Decode(Surface, DatCollection) → DecodedTexture`** — resolves Surface → SurfaceTexture → RenderSurface, decodes compressed or palette-indexed pixel data.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record DecodedTexture(byte[] Rgba8, int Width, int Height);
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses `BCnEncoder.Net` for BC1/BC2/BC3. Handles palette indexed via `Palette` lookup. Falls back to a 1×1 magenta texture on any failure.
|
||||||
|
|
||||||
|
Testable: feed it a synthetic uncompressed `RenderSurface` and assert byte-exact output.
|
||||||
|
|
||||||
|
### `AcDream.App/Rendering/`
|
||||||
|
|
||||||
|
**`ICamera`** — interface:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ICamera
|
||||||
|
{
|
||||||
|
Matrix4x4 View { get; }
|
||||||
|
Matrix4x4 Projection { get; }
|
||||||
|
float Aspect { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`OrbitCamera`** — refactored to implement `ICamera` (existing behavior preserved).
|
||||||
|
|
||||||
|
**`FlyCamera`** — new implementation:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class FlyCamera : ICamera
|
||||||
|
{
|
||||||
|
public Vector3 Position { get; set; } = new(96, 96, 100);
|
||||||
|
public float Yaw { get; set; }
|
||||||
|
public float Pitch { get; set; }
|
||||||
|
public float MoveSpeed { get; set; } = 80f; // world units / sec
|
||||||
|
public float MouseSensitivity { get; set; } = 0.002f;
|
||||||
|
public float FovY { get; set; } = MathF.PI / 3f;
|
||||||
|
public float Aspect { get; set; } = 16f / 9f;
|
||||||
|
|
||||||
|
public Matrix4x4 View { get; }
|
||||||
|
public Matrix4x4 Projection { get; }
|
||||||
|
|
||||||
|
public void Update(double dt, bool w, bool a, bool s, bool d, bool up, bool down);
|
||||||
|
public void Look(float deltaX, float deltaY);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
WASD moves in the horizontal plane relative to yaw. Space/Ctrl (or up/down) move vertically. Mouse captured with `CursorMode.Raw`.
|
||||||
|
|
||||||
|
**`CameraController`** — owns both cameras and the active selection:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class CameraController
|
||||||
|
{
|
||||||
|
public ICamera Active { get; private set; }
|
||||||
|
public OrbitCamera Orbit { get; }
|
||||||
|
public FlyCamera Fly { get; }
|
||||||
|
|
||||||
|
public void Toggle(); // flips Active, also flips cursor capture
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`StaticMeshRenderer`** — draws all `WorldEntity` via their `MeshRefs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed unsafe class StaticMeshRenderer : IDisposable
|
||||||
|
{
|
||||||
|
public StaticMeshRenderer(GL gl, Shader meshShader, TextureCache textures);
|
||||||
|
|
||||||
|
// Upload a mesh for a given GfxObj id (idempotent; cached).
|
||||||
|
public void EnsureUploaded(uint gfxObjId, LandblockMeshData meshData, uint surfaceId);
|
||||||
|
|
||||||
|
// Draw all entities grouped by GfxObj for batched state changes.
|
||||||
|
public void Draw(ICamera camera, IEnumerable<WorldEntity> entities);
|
||||||
|
|
||||||
|
public void Dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally holds a `Dictionary<uint, GfxObjGpu>` where `GfxObjGpu` bundles VAO + index count + surface id.
|
||||||
|
|
||||||
|
**`TextureCache`** — App-side GL texture ownership:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class TextureCache : IDisposable
|
||||||
|
{
|
||||||
|
public TextureCache(GL gl, DatCollection dats);
|
||||||
|
|
||||||
|
// Get or upload the texture for a Surface id. Returns GL texture handle.
|
||||||
|
public uint GetOrUpload(uint surfaceId);
|
||||||
|
|
||||||
|
public void Dispose(); // delete all handles
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On cache miss, calls `SurfaceDecoder.Decode`, uploads the RGBA8 bytes as a `GL_RGBA8` texture, stores the handle.
|
||||||
|
|
||||||
|
**Terrain atlas** — separate concern from per-GfxObj textures. At load time we gather the unique terrain-type texture ids from the loaded landblocks, decode each, and upload as a `GL_TEXTURE_2D_ARRAY` with each terrain type as a layer. The updated terrain shader samples the array with the per-vertex `aTerrainIndex` (a byte value taken from `TerrainInfo[i]`).
|
||||||
|
|
||||||
|
**Shader updates:**
|
||||||
|
|
||||||
|
`terrain.vert` — gains `in uint aTerrainIndex; out flat uint vTerrainIndex; uniform mat4 uModel;`. Position transformed by `uModel` before view/projection.
|
||||||
|
|
||||||
|
`terrain.frag` — gains `uniform sampler2DArray uTerrainAtlas;`. Samples atlas at `(vTexCoord, vTerrainIndex)`. Falls back to the Phase 1 height ramp if texture is missing.
|
||||||
|
|
||||||
|
`mesh.vert` / `mesh.frag` — new pair for static meshes. Position × model × view × projection. Single `sampler2D uDiffuse`. No lighting.
|
||||||
|
|
||||||
|
### `AcDream.Plugin.Abstractions/`
|
||||||
|
|
||||||
|
**`IGameState`** — new interface:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IGameState
|
||||||
|
{
|
||||||
|
IReadOnlyList<WorldEntitySnapshot> Entities { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct WorldEntitySnapshot(
|
||||||
|
uint Id,
|
||||||
|
uint SourceId,
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Rotation,
|
||||||
|
Vector3 Scale);
|
||||||
|
```
|
||||||
|
|
||||||
|
**`IEvents`** — new interface:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IEvents
|
||||||
|
{
|
||||||
|
event Action<WorldEntitySnapshot> EntitySpawned;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`IPluginHost`** — gains two properties:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPluginHost
|
||||||
|
{
|
||||||
|
IPluginLogger Log { get; }
|
||||||
|
IGameState State { get; } // NEW
|
||||||
|
IEvents Events { get; } // NEW
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins get snapshots, not live `WorldEntity` references — this protects core state from plugin mutation and prevents coupling to rendering details like `MeshRef`.
|
||||||
|
|
||||||
|
### `AcDream.App/Plugins/`
|
||||||
|
|
||||||
|
**`WorldGameState`** — sealed class implementing `IGameState`. Built from a `WorldView`. `Entities` returns a cached snapshot list rebuilt lazily when the world changes (in Phase 2, only at initial load).
|
||||||
|
|
||||||
|
**`WorldEvents`** — sealed class implementing `IEvents`. Holds the `EntitySpawned` event. Fired by `WorldGameState` during snapshot building.
|
||||||
|
|
||||||
|
**`AppPluginHost`** extended to own `State` and `Events`, handed to all plugins via `Initialize`.
|
||||||
|
|
||||||
|
## Testing strategy
|
||||||
|
|
||||||
|
**TDD (pure CPU, xUnit):**
|
||||||
|
- `GfxObjMesh.Build` — synthetic GfxObj, vertex/index count assertions, UV forwarding, triangulation of N-gons
|
||||||
|
- `SetupMesh.Flatten` — synthetic Setup with 2-3 parts, assert flattened (GfxObjId, Matrix4x4) tuples
|
||||||
|
- `SurfaceDecoder.Decode` — synthetic uncompressed RenderSurface, RGBA8 byte-exact assertion; BC1 decode exercised with a known small blob
|
||||||
|
- `LandblockLoader.Load` — synthetic or real Holtburg, entity count assertion
|
||||||
|
- `WorldView` — neighbor id math, edge handling
|
||||||
|
|
||||||
|
**Manual smoke (against retail dats):**
|
||||||
|
- `StaticMeshRenderer` — do buildings appear?
|
||||||
|
- `TextureCache` — do textures look right?
|
||||||
|
- `FlyCamera` — does WASD move, does mouse look work?
|
||||||
|
- `CameraController` — does F toggle?
|
||||||
|
- Plugin round-trip — `SmokePlugin` subscribes to `EntitySpawned` and logs entity count at `Enable` time
|
||||||
|
|
||||||
|
**Expected test count:** ~30-35 (17 from Phase 1 + 13-18 new).
|
||||||
|
|
||||||
|
## Error handling & failure modes
|
||||||
|
|
||||||
|
**Dat-level (load):**
|
||||||
|
- Missing neighbor → silent skip
|
||||||
|
- `LandBlockInfo` missing → log warning, render terrain without objects
|
||||||
|
- GfxObj/Setup unresolvable → log warning, skip entity, continue
|
||||||
|
- Surface/RenderSurface missing/corrupt → 1×1 magenta fallback texture
|
||||||
|
|
||||||
|
**GL-level (runtime):**
|
||||||
|
- Texture upload fails → log error, magenta fallback
|
||||||
|
- Shader compile fails → throw at load time (same as Phase 1)
|
||||||
|
- Degenerate mesh (zero triangles) → log warning, skip rendering
|
||||||
|
|
||||||
|
**Plugin-level:**
|
||||||
|
- `EntitySpawned` handler throws → catch, log, continue (don't mark plugin faulted — too disruptive)
|
||||||
|
- `IGameState.Entities` accessed during load → returns current snapshot, may be mid-build
|
||||||
|
|
||||||
|
**Primary risk:** `GfxObjMesh.Build` is the piece most likely to produce visually wrong output. The test suite catches gross errors (counts, triangulation); subtle bugs (flipped triangles, wrong UV mapping) only show visually. Mitigation: smoke-test each sub-step separately — wireframe first, then UVs, then textures.
|
||||||
|
|
||||||
|
## Task sequence (rough, full bite-sized plan in a separate doc)
|
||||||
|
|
||||||
|
1. Add `Meshing/`, `World/`, `Textures/` folder scaffolding under `AcDream.Core`
|
||||||
|
2. `GfxObjMesh.Build` (TDD, pure CPU — the hardest piece)
|
||||||
|
3. `SurfaceDecoder.Decode` (TDD, BCnEncoder.Net)
|
||||||
|
4. `LandblockLoader.Load` (TDD)
|
||||||
|
5. `SetupMesh.Flatten` (TDD)
|
||||||
|
6. `WorldView` 3×3 grid + neighbor id math (TDD)
|
||||||
|
7. `TextureCache` (App-side GL wrapper)
|
||||||
|
8. `StaticMeshRenderer` + new mesh shader (manual smoke)
|
||||||
|
9. Integrate `StaticMeshRenderer` into `GameWindow` — buildings flat white
|
||||||
|
10. Hook textures into `StaticMeshRenderer` — buildings now textured
|
||||||
|
11. Terrain atlas texture + updated terrain shader — ground textured
|
||||||
|
12. Render neighbor landblocks — horizon no longer cliffs
|
||||||
|
13. Extract `ICamera`, refactor `OrbitCamera` behind it
|
||||||
|
14. `FlyCamera` implementation
|
||||||
|
15. `CameraController` + F toggle
|
||||||
|
16. Add `IGameState` + `IEvents` to abstractions
|
||||||
|
17. `WorldGameState` + `WorldEvents` + `AppPluginHost` wire-up
|
||||||
|
18. Extend `SmokePlugin` to subscribe to `EntitySpawned` — end-to-end smoke
|
||||||
|
|
||||||
|
**Ordering rationale:**
|
||||||
|
- Pure CPU (1-6) first so TDD works without GL
|
||||||
|
- Static meshes (7-10) before terrain textures (11) because they're the bigger visual reward
|
||||||
|
- Neighbors (12) after single-landblock works — isolates the neighbor logic
|
||||||
|
- Camera (13-15) late because independent and doesn't block visual work
|
||||||
|
- Plugin API (16-18) last — discipline requires it, touches less than the rest
|
||||||
|
|
||||||
|
## Open questions (deferred, not blocking)
|
||||||
|
|
||||||
|
- Does `GfxObj` always reference one `Surface`, or do we need per-vertex surface indexing? If the latter, Phase 2 might need per-polygon draw batching. Defer investigation to Task 2.
|
||||||
|
- Terrain atlas size — 16 layers enough, or more? Deferred to Task 11 once we know the retail terrain type count.
|
||||||
|
- Should `WorldEntitySnapshot` be sent as a single list or as an `IEnumerable` (lazy)? Probably list — plugins want indexed access. Locked: list.
|
||||||
|
- Collision, animation, hot reload of plugins — all Phase 3+.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue