Commit graph

574 commits

Author SHA1 Message Date
Erik
1375780e14 feat(app): render static meshes from Holtburg LandBlockInfo 2026-04-10 18:32:09 +02:00
Erik
cefc689ba8 feat(app): add TextureCache for Surface→GL texture handle caching 2026-04-10 18:03:58 +02:00
Erik
5d35f4fe46 feat(core): add WorldView with 3x3 neighbor landblock computation 2026-04-10 18:02:41 +02:00
Erik
8f5b498be6 feat(core): add SetupMesh.Flatten for single-level part hierarchy 2026-04-10 18:01:16 +02:00
Erik
473a06c534 feat(core): add LandblockLoader with Stab+Building → WorldEntity mapping 2026-04-10 17:58:30 +02:00
Erik
dbf913ebb4 feat(core): add SurfaceDecoder for A8R8G8B8 and BCn formats 2026-04-10 17:56:15 +02:00
Erik
f915a13263 feat(core): add GfxObjMesh.Build multi-surface mesh extractor 2026-04-10 17:52:09 +02:00
Erik
01745d30ab chore(core): scaffold World/Meshing/Textures + add BCnEncoder.Net 2026-04-10 17:49:14 +02:00
Erik
6a100ef6e7 refactor(app): harden shutdown per final review
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>
2026-04-10 16:52:19 +02:00
Erik
fb83e0bb6f feat(app): wire plugin host, ship smoke plugin, log lifecycle
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>
2026-04-10 16:46:25 +02:00
Erik
87c45c70ac feat(app): render landblock with height-ramp shader + orbit camera
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>
2026-04-10 16:44:08 +02:00
Erik
8356fe65a0 feat(app): load landblock from dats and upload mesh to GPU
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>
2026-04-10 16:42:13 +02:00
Erik
6d18e0bd38 feat(app): Silk.NET window smoke — clear to navy
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>
2026-04-10 16:39:40 +02:00
Erik
baf0db303d feat(core): add LandblockMesh flat-terrain generator
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>
2026-04-10 16:37:52 +02:00
Erik
f6a57cbc6c refactor(core): harden PluginLoader per code review
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)
2026-04-10 09:57:45 +02:00
Erik
a7f0732026 feat(core): add PluginLoader with collectible ALC 2026-04-10 09:51:16 +02:00
Erik
9dfbc05052 refactor(abstractions): rename ILogger to IPluginLogger, doc Initialize
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
2026-04-10 09:46:30 +02:00
Erik
ed1c2d061c feat(abstractions): add IAcDreamPlugin, IPluginHost, ILogger 2026-04-10 09:42:52 +02:00
Erik
42480cc751 refactor(core): preserve exception in PluginDiscoveryResult + order deterministically
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
2026-04-10 09:40:17 +02:00
Erik
91618682e2 feat(core): add PluginDiscovery directory scan 2026-04-10 09:35:58 +02:00
Erik
99d2702c13 refactor(core): harden PluginManifest error model
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
2026-04-10 09:33:00 +02:00
Erik
c082ecf36a feat(core): add PluginManifest json parsing 2026-04-10 09:28:08 +02:00
Erik
caf57cca3e chore: phase 1 — add Core, Abstractions, App, Tests projects 2026-04-10 09:22:33 +02:00
Erik
020ec2a35d chore: phase 0 — skeleton + dat asset inventory
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>
2026-04-10 09:02:56 +02:00