Addresses the 'doors, windows, and alpha-keyed parts render bright
pink' issue the user observed after the Phase 2a visual checkpoint.
SurfaceDecoder gains a second overload taking an optional Palette
parameter. When the render surface format is PFID_INDEX16 and a
palette is supplied, each 16-bit value in SourceData is treated as
an index into Palette.Colors (a List<ColorARGB>) and the corresponding
ARGB color's channels are written to the output buffer. The original
no-palette overload is preserved so the Task 3 unit tests that
confirm INDEX16 -> magenta fallback still describe their behavior
correctly (INDEX16 without a palette still returns magenta).
TextureCache now resolves the RenderSurface's DefaultPaletteId via
the dats and passes the resulting Palette (or null) to the decoder.
mesh.frag adds an alpha cutout: fragments with sampled alpha < 0.5
are discarded. Without this, transparent regions of alpha-keyed
textures (doors, windows, foliage cutouts) would render as opaque
rectangles using the texture's background color. This is the
standard alpha-tested approach, simpler than full alpha blending
and matches how AC's original client rendered these surfaces.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 simplified per-vertex height as byte * 2.0f, but AC stores
heights as byte indices into a 256-entry non-linear float lookup
(Region.LandDefs.LandHeightTable). Static object placements in
LandBlockInfo use the real table, so terrain rendered with the
simplified scale left buildings floating or buried.
LandblockMesh.Build now takes an explicit float[] heightTable so
the core code stays testable without a DatCollection. GameWindow
loads Region id 0x13000000 once at startup and passes its
LandDefs.LandHeightTable into every landblock mesh build. The
Phase 1 tests use an identity table (i * 2f for i in 0..255) so
their expectations remain unchanged.
Addresses the 'buildings buried and floating' issue the user
observed after the Phase 2a visual checkpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tasks 1-10 are fully specified TDD-bite-sized and cover Phase 2a:
scaffold + GfxObjMesh.Build + SurfaceDecoder + LandblockLoader +
SetupMesh.Flatten + WorldView + TextureCache + StaticMeshRenderer +
debug + visual verification. End state: Holtburg terrain with buildings
rendered and at least some textures resolved.
Tasks 11-18 are sketches for Phase 2b in a future session: terrain
atlas, 3x3 neighbor rendering, ICamera/FlyCamera/CameraController, and
the IGameState/IEvents plugin API growth. These will be re-planned
with fresh context once Phase 2a is known to work.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verified the real GfxObj data shape against DatReaderWriter's generated
types and WorldBuilder's ObjectMeshManager. Three corrections:
- GfxObj.Surfaces is a list and Polygon.PosSurface indexes into it;
multiple surfaces per GfxObj are the norm. GfxObjMesh.Build now
returns IReadOnlyList<GfxObjSubMesh> — one sub-mesh per referenced
surface. StaticMeshRenderer draws entities by grouping entities by
GfxObj and then by sub-mesh within each GfxObj. Matches the approach
WorldBuilder takes.
- LandBlockInfo has both Objects (Stabs — small decorations, rocks,
trees) AND Buildings (BuildingInfo). LandblockLoader loads both
lists and maps them uniformly to WorldEntity since both types share
the Frame shape.
- Stab.Frame and BuildingInfo.Frame carry position+orientation only,
no scale component. WorldEntity.Scale dropped. WorldEntitySnapshot
loses its Scale field too. Phase 3+ can add it back if needed.
Closes the one open design question flagged in the original doc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Addresses final code review of phase-1 branch (Important I-1, I-3):
- Move plugin Enable() loop inside the same try block as GameWindow.Run,
and wrap each Enable() in per-plugin try/catch mirroring the Disable
loop. Previously, a plugin Enable() throwing would skip the finally
block entirely: plugins that had already enabled would never get
disabled, Serilog would never flush, and the exception would escape
ungracefully. Now Enable failures are logged and contained, and
shutdown always runs.
- Add a comment at the Get<LandBlock> call in GameWindow.OnLoad explaining
why TryGet was avoided (the [MaybeNullWhen(false)] nullable-generic
analysis trips TreatWarningsAsErrors).
I-2 (camera aspect doesn't update on window resize) deferred to Phase 2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 MVP end-to-end. Program.cs initializes Serilog, builds an
AppPluginHost that hands plugins a SerilogAdapter (IPluginLogger),
discovers plugins from the App's output plugins/ dir, loads each via
PluginLoader, calls Enable on all of them before opening the GameWindow,
and calls Disable in a finally block on shutdown.
AcDream.Plugins.Smoke is a new first-party plugin that logs through
the host during Initialize / Enable / Disable. Its csproj references
the abstractions with Private=false + ExcludeAssets=runtime to avoid
shipping a second copy of AcDream.Plugin.Abstractions.dll (which would
break ALC type identity). An MSBuild Target on the App project copies
the plugin DLL into plugins/AcDream.Plugins.Smoke/ and writes the
plugin.json manifest next to it.
Smoke verified against real dats. Console output observed:
[INF] scanning plugins in ...\plugins
[INF] smoke plugin initialized
[INF] loaded plugin acdream.smoke (Smoke Plugin)
[INF] smoke plugin enabled
loaded landblock 0xA9B4FFFF
<window renders terrain>
[INF] smoke plugin disabled (on shutdown)
Phase 1 done.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a 2-stage GLSL shader (vertex + fragment), a Shader helper that
compiles/links and exposes SetMatrix4 for uniforms, and an OrbitCamera
with yaw/pitch/distance and a 192-unit-centered target for a single
landblock. TerrainRenderer now takes a Shader and issues an actual
DrawElements call with uView + uProjection uniforms. GameWindow owns
the Shader and Camera, routes mouse drag to camera yaw/pitch, and
scroll wheel to camera distance.
The fragment shader maps world Z to a green-brown-white ramp so
lowlands read green, midlands brown, and peaks white — no textures
yet, but enough to visually confirm the terrain shape.
Shaders are copied to the output dir via a <None Update> item group.
Smoke verified against real dats: process stays alive with no GL
errors, no shader compile/link failures, and no exception trail.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GameWindow now owns a DatCollection + TerrainRenderer. On load it
opens the dat directory passed as argv[0] (or ACDREAM_DAT_DIR), finds
Holtburg (landblock 0xA9B4FFFF) by default with a fallback to the
first landblock in the cell b-tree, builds the CPU mesh from
LandblockMesh.Build, and uploads VBO+EBO+VAO with a 3f/3f/2f attribute
layout. No draw call yet — shader and matrix uniforms land in Task 9.
Enabled AllowUnsafeBlocks on the App csproj so the fixed-buffer upload
in TerrainRenderer compiles. Uses dats.Get<LandBlock>(id) instead of
TryGet(..., out T) to sidestep the [MaybeNullWhen(false)] analysis that
TreatWarningsAsErrors was flagging.
Smoke verified against the real retail dats: prints
"loaded landblock 0xA9B4FFFF" and the window stays alive with no GL
errors or exceptions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Minimal Silk.NET window wiring: 1280x720, OpenGL 4.3 core profile,
VSync, dark navy clear color, Escape to close. No rendering beyond
the clear call — terrain and shader land in Tasks 8 and 9.
Manual smoke: process starts, stays alive past GL context creation,
produces no stderr, no uncaught exceptions. Actual visual check
will happen end-to-end after Task 10.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pure CPU mesh generator: takes a DatReaderWriter LandBlock DBObj and
produces 81 vertices + 128 triangles covering 192x192 world units.
Vertices are a readonly record struct (position, normal, texcoord)
so the upcoming GPU upload in Task 8 can sizeof() them directly.
Height byte -> world z uses a simple 2x scale; the real AC height
lookup table is a Phase 2+ concern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses code quality review of a7f0732:
- LoadedPlugin now holds the AssemblyLoadContext explicitly so Task 10
can call Unload() for hot reload (Critical)
- LoadedPlugin.Error is Exception? to match PluginDiscoveryResult and
preserve stack traces; synthetic failures build FileNotFoundException
and InvalidOperationException (Important)
- PluginLoader falls back to ReflectionTypeLoadException.Types if
GetTypes() can't fully resolve (Important)
- Hardcoded abstractions assembly name is now a const (Minor)
Addresses code quality review of ed1c2d0:
- ILogger would collide with Microsoft.Extensions.Logging.ILogger and Serilog.ILogger
in any plugin file that imports both namespaces; renamed to IPluginLogger
- IAcDreamPlugin.Initialize now has an XML doc clarifying its lifecycle contract
Addresses code quality review of 9161868:
- PluginDiscoveryResult.Error is now Exception? rather than string?,
preserving stack traces across the plugin boundary for logging
- PluginDiscovery.Scan orders subdirectories by ordinal string comparison
so plugin load order is reproducible across platforms
Addresses code quality review of c082ecf:
- Require takes a literal JSON field name, no more fragile PascalCase->camelCase transform
- Parse_MissingRequiredField_Throws asserts exact message, not substring
- Remove unused using System.Text.Json.Serialization
Ten bite-sized tasks covering: solution reorg into four projects,
TDD-driven PluginManifest parsing + discovery + collectible-ALC
loader (with a real fixture DLL), LandblockMesh pure generator,
Silk.NET window smoke, dat-backed landblock mesh upload, height-
ramp shader + orbit camera, and finally the end-to-end smoke
plugin round-trip. Manual visual smoke only for the GL bits;
xUnit TDD for everything testable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Captures the day-1 plugin + scripting contract for acdream:
C# plugins via collectible AssemblyLoadContext with a stable
IPluginHost API, Lua macros via a first-party Macros plugin that
embeds MoonSharp, and a four-stage packet pipeline for raw and
parsed traffic in both directions. Extends the phased MVP so
every core system is exposed through the plugin API in the same
commit that adds it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brand-new solution targeting .NET 10, using Chorizite.DatReaderWriter 2.1.4
to walk a retail AC dat directory and print how many of each asset type live
in client_portal / client_cell_1 / client_highres / client_local_English.
Opens the four dats in ~16 ms and counts 887,381 indexed assets across 40+
tracked DBObj types. Cell-database terrain (LandBlock, LandBlockInfo, EnvCell)
uses mask-based IDs that DatReaderWriter 2.1.4's GetAllIdsOfType<T> does not
support; worked around with a manual b-tree walk in CountCellByLow16.
Sanity check: LandBlock count is 65,025 = 255 x 255, exactly the AC world grid.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>