Fixes the three-lens review blockers against 671eb3ad4 (S3 section 9.6 F1-F6).
F1 - slot key (blocking, retail). TerrainModernRenderer.DrawLandCells
normalizes every incoming landblockId to (id & 0xFFFF0000u) | 0xFFFFu
before the _idToSlot lookup: the walk hands 0xXXYY0000
(WalkLandBlock.LandblockId) but AddLandblock stores under the DAT id
0xXXYYFFFF (LandblockRenderPublisher.LandblockId) - every walk lookup
was missing and the walk path drew NO terrain. Unit-tested end-to-end
through a real RecordingGpuDevice-backed TerrainModernRenderer
(TerrainWalkSlotKeyNormalizationTests): AddLandblock(0xA9B4FFFF, ...)
is found by a 0xA9B40000 lookup, an unknown landblock is a silent
per-entry no-op, and a batch mixing a known and unknown entry submits
only the known one.
F2 - deferred cross-block batching (blocking, driver). Retail's
DrawSortCell always follows DrawLandCell (LC/SC strictly alternate,
never two LC in a row - S3 section 9 R1), so chunk 3's "merge
consecutive same-landblock LandCell events" rule never actually
merged anything; the driver review flagged batching as inert.
WalkFrameDriver.Replay now keeps ONE pending terrain batch across
landblocks ((landblockId, side, cellIndex) entries, cleared at
Replay's own start); a LandCell event only appends; every OTHER event
kind that will itself submit GPU work (StreamMark, Sky, CellShell,
PunchFan, AlphaBarrier, LandscapeFlush, ClearInteriorDepth,
ExitSeals) flushes the pending batch first; a StaticParticles/
CellParticles turn asks the new ParticleSystem.
HasRenderableEmittersInCell (an allocation-free sibling of
CopyRenderableEmittersInCell) and, when the cell has no renderable
emitter, submits nothing and does NOT flush either - the whole point
of the deferred rule. The end of Replay flushes the remainder. This
is order-preserving by construction: a flush always lands at the
exact point the unbatched draw would have, so GPU submission order -
and therefore pixels - is identical to the unbatched baseline; only
the number of small terrain draw calls shrinks.
TerrainModernRenderer.DrawLandCellRuns becomes DrawLandCells(
viewProjection, IReadOnlyList<(uint LandblockId, int SideCellCount,
int CellIndex)>) - one MultiDrawIndexedIndirect over every entry's
runs, unknown slots skipped per-entry. IWalkFrameLeafRenderer.
DrawLandCellBatch drops its separate landblockId parameter to match
(a batch can span several landblocks now) and gains
HasRenderableEmittersInCell.
Batch-count demonstration: driven through a real WalkFrameDriver
Replay (OnLandCellTurn_MergesAcrossLandblocksOverAnEmptyParticleTurn_
RealSubmissionsSplit), 4 LandCell turns across 3 distinct landblocks,
separated only by an empty particle turn, a real StreamMark, and a
building's alpha barrier, submit as exactly 3 DrawLandCellBatch calls
(2+1+1) instead of 4 - the empty particle turn's non-flush merges two
otherwise-separate cross-landblock entries. At production scale the
same mechanism is expected to cut the terrace-edge frame's ~578
individual DrawLandCell events (S3 section 9's captured transcript
count) to "tens" of submitted batches, per the contract's own
expectation: most terrain cells have no particle owner nearby, so the
strict LC/[empty-SC]/LC/[empty-SC]/... run collapses into one batch
per region bounded by real content (a building, a StreamMark-worthy
cell, or a genuine emitter) rather than per cell.
F3 - per-frame terrain diagnostic (blocking, build/test). The walk
leaf no longer brackets each batch with TerrainDrawDiagnosticsController
.Begin()/Complete() (a per-batch Stopwatch Restart/Stop pair that was
pushing one timing SAMPLE per batch, not per frame).
RetailPViewPassExecutor.DrawWalkLandCellBatch instead times its own
call with a raw Stopwatch.GetTimestamp() delta (no allocation) and
hands the ticks to the controller's new AccumulateWalkBatch;
RetailPViewRenderer.DrawWalkDrivenStatics calls the new
CompleteWalkTerrainFrame() exactly once, immediately after
driver.Replay finishes - "the end of the walk replay", where the
deleted whole-stage terrain leaf's own Begin()/Complete() bracket
used to close - which pushes ONE elapsed-time sample (even a
zero-batch frame pushes a zero sample: one sample per frame, not per
landscape turn) and publishes on the existing 5-second cadence.
TerrainRenderDiagnosticFacts gains a Draws field alongside
VisibleSlots (both were the same field before); TerrainModernRenderer
tracks its own per-frame WalkVisibleSlotCount/WalkDrawCount (a
HashSet<int>/int cleared in BeginFrame, populated by DrawLandCells),
and the diagnostics source reports those whenever the walk drew at
least one batch this frame, falling back to the non-walk Draw()
path's VisibleSlots otherwise (the two paths never both run in the
same frame). The [TERRAIN-DIAG] line's meaning (cpu_us per frame) is
unchanged, so the S3 section 9.5 before/after compare stays valid.
F4 - driver pins for the LandCell position (major). Three RunFrame-
level pins replace the deleted TERRAIN:0 pins: an outdoor-root
sequence (SKY, then one LANDCELL, driving RetailFrameWalk.
DrawLandscape directly with a one-view/zero-vertex WalkPortalView so
WalkLandscape.CheckBlocks' admission stays the same deterministic
"CY-only" test RetailFrameWalkTests already relies on, while still
satisfying WalkFrameDriver's real >=1-active-view fail-loud guard);
an interior-root test with one real exit view and one populated
block (SKY, LANDCELL, LFLUSH, SEALS, SHELL...) built on the existing
RunFrame_InteriorFloodWithExitView_... fixture; and the T4 batching
pin re-expressed for the F2 rule (OnLandCellTurn_
MergesAcrossLandblocksOverAnEmptyParticleTurn_RealSubmissionsSplit,
described above). The fake leaf's DrawLandCellBatch now logs
LANDCELL:<lb>:<side>:<idx>[,...] per batch and gains
HasRenderableEmittersInCell backed by an opt-out CellsWithoutEmitters
set (default true - has-emitters - so every pre-existing pin in the
file keeps its old unconditional-submission behavior unchanged).
F5 - no code change: the walk's in-view gate is unchanged; no
whole-block terrain re-added.
F6 - minor/notes: DrawLandCells' own comment now states the walk's
CheckBlocks/landcell_check admission is the sole terrain culling
authority (retail has no separate terrain frustum test); the
HandleLandscapeTurn comment's inverted claim is corrected (a FARTHER
building's punch survived because NEARER terrain was drawn BEFORE
it, not after - the interleave now draws it after, matching retail);
the "flat/directional-shadow paths" claim is corrected to the one
actual caller, WorldScenePassExecutor.DrawFlatTerrain (a directional-
shadow receiver selects its pipeline inside the SAME DrawRhi call,
not through a second caller); the cathedral order-trace token gains
the LOD side/index (":LC<lb>/<side>:<idx>"); T2's vacuous "no
TERRAIN event" assertion in RetailFrameWalkTests is replaced by a
comment pointing at the F4 driver-level pins; and the stale
"Confirmed OH5 defect" row in oh1-construction-landscape-contract.md
is retired with "FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round
1)".
App hermetic lane: 6,795/6,795 (up from 671eb3ad4's 6,786 baseline -
net +9 tests: 3 F1 slot-key tests, 2 F4a/b driver RunFrame pins, 3
TerrainDrawDiagnosticsController walk-frame tests, plus the T4->F4c
rewrite and the RetailPViewPassExecutorTests split are net neutral).
InstalledDat lane: 241 passed, the same 3 accepted failures (2
pre-existing #383 layout fixture-drift tests, 1 TowerAscent
Status=KnownFailure) - unchanged from baseline. Core Vfx tests:
109/109 (108 baseline + 1 new HasRenderableEmittersInCell lifecycle
pin mirroring CopyRenderableEmittersInCell's own add/move/remove
test).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|---|---|---|
| .gitea/workflows | ||
| .github | ||
| .vscode | ||
| assets/icons | ||
| docs | ||
| memory | ||
| references | ||
| samples | ||
| src | ||
| tests | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| .gitmodules | ||
| AcDream.slnx | ||
| AGENTS.md | ||
| analyze_flap_live.py | ||
| CLAUDE.md | ||
| Directory.Build.props | ||
| Directory.Packages.props | ||
| find_burst.py | ||
| global.json | ||
| launch-a6-issue98-capture.ps1 | ||
| launch-a6-issue98-cottage-gfxobj-dump.ps1 | ||
| launch-a6-issue98-polydump.ps1 | ||
| launch-flap-capture.ps1 | ||
| launch-flap-churn.ps1 | ||
| launch-flap-verify.ps1 | ||
| NOTICE.md | ||
| NuGet.Config | ||
| README.md | ||
acdream
A modern open-source C# / .NET 10 Asheron's Call client.
acdream ports the observable behaviour of the September 2013 retail client to Silk.NET and a modern, plugin-friendly architecture. The code is modern; the behaviour is retail.
Status: playable pre-alpha. M3, “Cast a spell,” landed on 2026-07-21 and
M4, “Live in the world,” is active. The graphical client supports the connected
combat, magic, movement, portal, inventory, loot, and retained-UI loops used by
the current test characters. The presentation-independent GameRuntime and
the Linux/Windows multi-session headless host are complete. Native Linux
graphics are intentionally parked at the L1 capability checkpoint; Windows is
the currently validated graphical platform.
The documentation map is the entry point for current milestones, roadmap state, architecture, issues, retail divergences, research, and durable project memory.
Technology
- Runtime: C# and .NET 10
- Graphics: Silk.NET, OpenGL 4.3 core, bindless textures, shader draw parameters, SSBOs, and multi-draw indirect
- Audio: OpenAL through Silk.NET
- Content: retail DAT files plus a machine-local, memory-mapped
acdream.pakproduced byAcDream.Bake - Networking: custom UDP, ISAAC cipher, and game-message layers compatible with ACEmulator
- UI: retained retail gameplay UI plus opt-in ImGui developer tools
- Automation: the same presentation-independent
GameRuntimeis hosted by both the graphical client andAcDream.Headless
The modern renderer is mandatory. There is no legacy renderer fallback. Startup reports an actionable error if the required OpenGL capabilities are missing.
What works
- ACE login, character selection, world entry, chat, client commands, reconnect, and graceful logout.
- Outdoor, building, cellar, and dungeon streaming with prepared terrain, scenery, buildings, EnvCells, collision, portal visibility, sky, fog, lighting, audio, and day/night presentation.
- Local and observed movement, animation, jumping, selection, radar, combat stances, melee, bows, crossbows, spell projectiles, death, corpses, chests, and looting.
- Inventory bags, stable server ordering, stack splitting, ground drops, paperdoll equipment, weapon switching, quick bars, item use, cooldowns, and giving items to NPCs.
- Retail-style retained UI for vitals, chat, toolbar, inventory, character, attributes, skills, spellbook, components, effects, combat/spell/jump bars, radar/compass, dialogs, external containers, and assessment.
- Complete end-of-retail spell catalog, learned and favorite spells, component preflight, connected casts, enchantments, DAT-driven projectiles and effects, recall, portal-space travel, Hidden/UnHide, and remote materialization.
- One presentation-independent runtime owner for session, entities, objects, inventory, character state, selection, interactions, combat, magic, movement, physics, projectiles, world environment, and portal transit.
- A no-window Windows/Linux host with deterministic bot commands/events, shared immutable content, multi-session scheduling, isolation, reconnect, resource telemetry, and tested 1/5/10/30-session ownership.
- Plugin loading, shared command/input abstractions, retained markup panels,
and permanent ImGui developer tools behind
ACDREAM_DEVTOOLS=1.
Current boundaries
- The active M4 prelude is world interaction completion. Slices 1–3, including assessment and its final formula/icon/layout correction, are user-accepted. Equipped-child picking and vendor browse/buy/sell are the next uncompleted slices.
- Issue
#225retains the lifestone/particle shared-alpha visual comparison. Its connected lifetime and performance routes already pass. - Narrow carried behaviour debt includes issue
#153(an unstreamed far-teleport edge), issue#116(slide feel), issue#235(30 Hz capped/RDP jump presentation), and the live temporary-stopgap rows in the retail divergence register. - Native Linux graphics are deferred. L0 portability and L1 backend/capability
reporting are implemented; WSLg reaches the GPU through Mesa D3D12 but does
not expose mandatory
GL_ARB_bindless_texture. Resume with a supported physical Linux AMD/NVIDIA driver before beginning later Slice L work. - Advanced vendor/trade/crafting/social surfaces and larger M4 quest, character-creation, and emote bodies remain roadmap work.
Prerequisites
- .NET 10 SDK
- Your own retail Asheron's Call DAT directory containing:
client_portal.datclient_cell_1.datclient_highres.datclient_local_English.dat
- A machine-local
acdream.pakbuilt from those DATs - A running ACE server for connected play; the examples use
127.0.0.1:9000 - For the graphical client, a driver exposing the mandatory Vulkan capabilities validated at startup
The project does not distribute Microsoft/Turbine DAT files or derived prepared packages.
Build and test
dotnet restore AcDream.slnx
dotnet build AcDream.slnx -c Release
dotnet test AcDream.slnx -c Release --no-build
The current CI-filtered Windows baseline is a successful Release build with 16,151 passing tests and zero failures; opt-in live, installed-DAT, prepared-package, manual, timing, and platform-specific lanes run separately.
Prepare content
Production rendering and collision use the validated prepared package rather than decoding world meshes on the frame path:
dotnet run --project src\AcDream.Bake\AcDream.Bake.csproj -c Release -- `
--dat-dir "C:\Games\Asheron's Call" `
--out "C:\Games\Asheron's Call\acdream.pak"
A complete format-2 package from the standard installed DAT set is about
570 MiB (the former format-1 package was about 30 GB). It is machine-local and
must not be committed. ACDREAM_PAK_PATH overrides the default
<DAT directory>\acdream.pak.
Run the graphical client
$env:ACDREAM_DAT_DIR = "C:\Games\Asheron's Call"
$env:ACDREAM_PAK_PATH = "C:\Games\Asheron's Call\acdream.pak"
$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 -c Release
The DAT directory can instead be supplied as the first positional argument:
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Release -- `
"C:\Games\Asheron's Call"
Run a headless session
AcDream.Headless loads no App, UI, OpenGL, native-window, or audio assembly.
Create a version-1 configuration such as bot.json:
{
"version": 1,
"process": {
"content": {
"datDirectory": "/opt/ac",
"preparedAssetPath": "/opt/ac/acdream.pak"
}
},
"sessions": [
{
"id": "bot-1",
"endpoint": { "host": "127.0.0.1", "port": 9000 },
"account": "testaccount",
"character": { "index": 0 },
"policy": { "id": "idle" },
"credential": {
"provider": "environment",
"reference": "ACDREAM_BOT_PASSWORD"
}
}
]
}
Then validate and run it:
export ACDREAM_BOT_PASSWORD='testpassword'
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- \
validate --config bot.json
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- \
run --config bot.json
For a single local session, run also accepts
--user <account> --password <password>. Add uniquely identified session
entries and credential references for a multi-session process. Available
built-in policies are idle, lifecycle-smoke, observer-movement, and
portal-route-smoke.
Useful startup options
| Variable | Effect |
|---|---|
ACDREAM_DAT_DIR |
Retail DAT directory |
ACDREAM_PAK_PATH |
Prepared package path; defaults to <DAT dir>/acdream.pak |
ACDREAM_LIVE=1 |
Enable connected mode |
ACDREAM_TEST_HOST / ACDREAM_TEST_PORT |
ACE endpoint |
ACDREAM_TEST_USER / ACDREAM_TEST_PASS |
Graphical-client credentials |
ACDREAM_RETAIL_UI=0 |
Disable the retained retail gameplay UI for diagnostics; it is enabled by default |
ACDREAM_DEVTOOLS=1 |
Enable ImGui developer tools |
ACDREAM_NO_AUDIO=1 |
Suppress OpenAL initialization |
ACDREAM_UNCAPPED_RENDER=1 |
Disable normal frame pacing for diagnostics |
ACDREAM_DISPLAY_PROTOCOL=auto|x11|wayland |
Select the Linux GLFW backend |
ACDREAM_DAY_GROUP=N |
Force a day-group index for weather/lighting comparisons |
ACDREAM_STREAM_RADIUS=N |
Legacy override over configured streaming radii |
ACDREAM_DUMP_SKY=1 |
Dump sky interpolation and draw diagnostics |
ACDREAM_DUMP_MOTION=1 |
Dump inbound movement and motion-cycle decisions |
Additional diagnostic and budget controls are documented beside their typed owners and in the linked research plans; they are not stable user settings.
Repository layout
src/
AcDream.Runtime/ presentation-independent GameRuntime
AcDream.App/ graphical host, retained UI, renderer, audio
AcDream.Headless/ Windows/Linux no-window multi-session host
AcDream.Core/ retail gameplay, movement, physics, world logic
AcDream.Core.Net/ UDP, ISAAC, protocol and message routing
AcDream.Content/ GL-free DAT and prepared-package content
AcDream.Bake/ offline acdream.pak builder
AcDream.Cli/ offline DAT inspector
AcDream.UI.Abstractions/ shared UI/input models and contracts
AcDream.UI.ImGui/ developer-tool presentation
AcDream.Plugin.Abstractions/ BCL-only plugin contracts
AcDream.Plugins.Smoke/ example plugin
tests/
AcDream.*.Tests/ layer-matched xUnit projects
docs/
README.md documentation authority and current map
architecture/ ownership, structure, divergence, WB inventory
plans/ milestone, roadmap, and execution plans
research/ retail pseudocode, traces, fixtures, evidence
audit/ completion and conformance audits
memory/ durable engineering references
references/ gitignored external reference repositories
Development workflow
All AC-specific behaviour starts from the named retail oracle in
docs/research/named-retail/:
- Search the named retail pseudo-C and headers by
class::method. - Use the older Ghidra chunks only when the named oracle is insufficient.
- Cross-reference ACE and the relevant client/viewer implementation.
- Record readable pseudocode and exact constants/order.
- Port the retail mechanism into the correct modern owner.
- Add conformance, lifecycle, and failure-boundary tests.
- Run the automated gate and the appropriate connected or visual gate.
- Update architecture, roadmap, divergences, and durable memory with the same change.
Guessing at AC-specific algorithms is forbidden. See AGENTS.md, CLAUDE.md, and the architecture guide for the full rules.
Reference projects
- ACE / ACEmulator: authoritative server and protocol behaviour
- ACViewer: character appearance and DAT presentation cross-check
- WorldBuilder: extracted Silk.NET DAT/rendering foundation
- Chorizite.ACProtocol: clean-room protocol reference
- holtburger: broad non-retail client behaviour reference
- AC2D: terrain and movement-packet cross-checks
The retail binary/decomp remains the behavioural oracle when references disagree.
Licence and game assets
The acdream source has not yet been assigned a top-level licence and is not ready for public redistribution. External reference code retains its own licence.
Asheron's Call DAT files, art, names, and other game assets remain the property of Microsoft/Turbine. This repository does not distribute them; users must supply their own retail installation.