The end-to-end pipeline. acdream can now connect to a live ACE server,
complete the full handshake + character-select + enter-world flow, and
stream CreateObject messages straight into the existing IGameState and
static mesh renderer. Gated behind ACDREAM_LIVE=1 so the default
offline run path is untouched.
Added:
- AcDream.Core.Net.WorldSession: high-level session type that owns a
NetClient, drives the 3-leg handshake, parses CharacterList, sends
CharacterEnterWorldRequest + CharacterEnterWorld, and converts the
post-login fragment stream into C# events. State machine:
Disconnected → Handshaking → InCharacterSelect → EnteringWorld →
InWorld (or Failed). Public API:
* Connect(user, pass) — blocks until CharacterList received
* EnterWorld(user, characterIndex) — blocks until ServerReady
* Tick() — non-blocking, call per game-loop frame
* event EntitySpawned
* event StateChanged
* Characters property (populated after Connect)
- NetClient.TryReceive: non-blocking variant that returns immediately
with null if the kernel buffer is empty. Enables draining packets
per frame from the main thread without stalling.
- GameWindow live-mode hookup:
* AcDream.Core.Net project reference
* TryStartLiveSession() called after dat hydration, gated behind
ACDREAM_LIVE=1 + ACDREAM_TEST_USER/ACDREAM_TEST_PASS env vars
* Subscribes EntitySpawned to OnLiveEntitySpawned
* Calls Connect() then EnterWorld(0) synchronously on startup
* OnLiveEntitySpawned hydrates mesh refs from the Setup dat
(same SetupMesh.Flatten + GfxObjMesh.Build + StaticMesh.EnsureUploaded
path used by scenery), publishes a WorldEntitySnapshot via
_worldGameState.Add + _worldEvents.FireEntitySpawned, and
appends to _entities so the next frame picks it up
* OnUpdate calls _liveSession?.Tick() each frame
* OnClosing disposes the session
* Position translation: server sends (LandblockId, local XYZ +
quaternion); we map landblock to world origin relative to the
rendered 3x3 center, add local XYZ, translate AC's (W,X,Y,Z)
quaternion wire order to System.Numerics.Quaternion (X,Y,Z,W)
LIVE RUN OUTPUT (ACDREAM_LIVE=1 against localhost ACE, testaccount):
[dats loaded, 1133 static entities hydrated]
live: connecting to 127.0.0.1:9000 as testaccount
live: entering world as 0x5000000A +Acdream
live: in world — CreateObject stream active (so far: 0 received, 0 hydrated)
live: spawned guid=0x5000000A setup=0x02000001 world=(104.9,15.1,94.0)
live: spawned guid=0x7A9B4013 setup=0x0200007C world=(135.7,9.9,97.0)
live: spawned guid=0x7A9B4014 setup=0x0200007C world=(132.5,9.9,97.0)
live: spawned guid=0x7A9B4015 setup=0x020019FF world=(132.6,17.1,94.1)
live: spawned guid=0x7A9B4016 setup=0x020019FF world=(136.3,5.2,94.1)
live: spawned guid=0x7A9B4017 setup=0x020019FF world=(104.1,31.0,94.1)
live: spawned guid=0x7A9B4037 setup=0x02000975 world=(109.7,33.0,95.0)
live: spawned guid=0x7A9B4018 setup=0x020019FF world=(110.9,31.0,94.1)
live: spawned guid=0x7A9B4019 setup=0x020019FF world=(107.5,31.5,94.1)
live: spawned guid=0x7A9B403B setup=0x02000B8E world=(150.5,17.9,94.0)
live: (suppressing further spawn logs)
First line: +Acdream himself. setup=0x02000001 is ACE's default humanoid
player mesh. world coords match Holtburg (landblock 0xA9B4 local
space). Subsequent spawns are weenies at various setup ids — likely
the foundry statue, street lamps, drums, etc. The 0x7A9B4xxx GUID
pattern is ACE's convention: scenery-type (0x7) + landblock (0xA9B4) +
per-object index.
All spawns flow through the SAME SetupMesh/GfxObjMesh/StaticMeshRenderer
pipeline used by scenery and interiors today. The plugin system's
EntitySpawned event fires on every new entity, so plugins can see
them without any networking awareness.
Tests: 160 passing offline (77 core + 83 net). The live handshake and
enter-world tests are gated and still pass when ACDREAM_LIVE=1.
User visual verification is the final acceptance for Phase 4. Run
with ACDREAM_DAT_DIR + ACDREAM_LIVE=1 + ACDREAM_TEST_USER=testaccount
+ ACDREAM_TEST_PASS=testpassword and look for +Acdream's model + the
foundry statue standing on top of the Holtburg foundry.
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>