Two audit-verified caches grew monotonically for process lifetime, which is fatal for the 30-bot long-uptime headless-fleet goal: - DatSoundCache._waves (Core) memoized every decoded PCM WaveData forever in a bare ConcurrentDictionary — no LRU, no byte budget. - OpenAlAudioEngine._bufferByWaveId (App) retained a native OpenAL buffer copy of the same PCM per wave id until engine disposal — a second, independent unbounded cache. DatSoundCache now bounds payload residency with a 32 MiB byte-budget LRU (decoded PCM waves run ~100-500 KB each, so this holds a comfortable working set). Missing/unsupported-format waves are memoized separately in an unbounded-but-cheap negative-result set (bounded by the finite Wave dat id space) so they can never compete with or get evicted alongside payload entries. Concurrent first-touch decodes of the same wave id are deduped via a shared Lazy<T> so racing callers don't pay for WaveDecoder.Decode twice. AcDream.Core cannot reference AcDream.Content (code-structure rule 2), so the LRU is a local reimplementation mirroring BoundedDatObjectCache/DecodedTextureCache's shape rather than a shared dependency. OpenAlAudioEngine._bufferByWaveId now bounds native buffer residency with a 48 MiB byte-budget LRU (AlBufferBudgetTracker), evicting least-recently- used buffers once oversized. alDeleteBuffers fails on a buffer still attached to a source, so eviction queries live AL per-source state (GetSourceInteger.Buffer) rather than tracking a second, easily-stale copy — several call sites (Play3DWave, PlayUiWave) set a source's buffer directly. The buffer EnsureBuffer just created is explicitly protected from its own eviction pass, since the caller hasn't attached it to a source yet at that point. Evicted waves simply replay through DatSoundCache -> EnsureBuffer on next use, identical to a first play. Verified before implementing: AudioHookSink is the only GetWave caller (single per-frame render-thread path per AnimationHookRouter's own threading doc), and PcmBytes is read only at DatSoundCache.Admit (byte accounting) and EnsureBuffer's first-upload branch — confirmed dead after AL upload on the steady-state replay path, so bounding either cache independently is correctness-safe; a cold replay after both evict simply falls back to a full re-decode + re-upload, identical to a first play. AlBufferBudgetTracker's eviction/budget decision is extracted as pure logic (no AL dependency) specifically so it's unit-testable: the existing OpenAlResourceLifetimeTests fake exposes a null AL, which short-circuits every native buffer call before it runs, so the engine's actual AL wiring isn't testable headless. Tests: 9 new DatSoundCacheTests (Core.Tests) covering eviction order, byte accounting, negative-result memoization, oversize-single-entry handling, and concurrent-access smoke tests; 10 new AlBufferBudgetTrackerTests (App.Tests) covering the pure LRU/budget/protection logic. Full suite: 3214/2 skip (Core.Tests), 3471/3 skip (App.Tests) plus one pre-existing, unrelated failure (LandblockBuildOriginTests.FarLoad_..., reproduces identically with these changes stashed out — landblock streaming, not audio). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 76c880d35bcf30b50e3f7b4cb8635dc9ab9e3ec7) |
||
|---|---|---|
| .github | ||
| .vscode | ||
| docs | ||
| memory | ||
| references | ||
| src | ||
| tests | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| .gitmodules | ||
| AcDream.slnx | ||
| AGENTS.md | ||
| analyze_flap_live.py | ||
| CLAUDE.md | ||
| find_burst.py | ||
| 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 | ||
| README.md | ||
acdream
A modern open-source C# / .NET 10 Asheron's Call client.
Faithful port of the retail client's behaviour to Silk.NET with a modern, plugin-friendly architecture. The code is modern; the behaviour is retail.
Status: playable pre-alpha; M3 ("Cast a spell") is active. The connected client supports the complete M2 combat/loot/inventory loop plus retained spellbook, component-book, favorite-spell, enchantment, portal-space, radar, chat, character, and inventory UI. Current work is verification and retail conformance, not a first-playable bring-up.
Start at the documentation map for the current milestone, roadmap, tactical issues, architecture, retail-divergence register, research, and durable memory.
Stack
- Language: C# .NET 10
- Graphics: Silk.NET (OpenGL 4.3
context;
GL_ARB_bindless_textureandGL_ARB_shader_draw_parametersare mandatory) - Audio: OpenAL via Silk.NET
- Dat parsing: Chorizite.DatReaderWriter
- Networking: Custom UDP + ISAAC cipher + game-message layer, wire-compatible with ACEmulator server
What works today
- ACE login, character selection, world entry, chat, client commands, and graceful logout.
- Outdoor, building, cellar, and dungeon streaming with portal-space travel, terrain/scenery/EnvCell rendering, collision, lighting, sky, fog, audio, and day/night presentation.
- Local and observed player/NPC/monster movement, animation, selection, radar, combat stances, melee and missile presentation, death, corpses, chests, and looting.
- Inventory bags, stack splitting, paperdoll/equipment, weapon switching, quick bars, item use, giving items to NPCs, and server-authoritative item movement.
- Retained retail-style gameplay UI for vitals, chat, toolbar, inventory, character/skills, spellbook/components, helpful/harmful effects, combat and spell bars, radar/compass, indicator pages, and confirmation dialogs.
- Spell catalog, learned/favorite spells, component preflight, connected casts, enchantments, DAT-driven missiles/effects, recall, Hidden/UnHide, and portal materialization presentation.
- Plugin host, shared input/command abstractions, and permanent ImGui developer
tools behind
ACDREAM_DEVTOOLS=1.
Active gaps
- R6's complete object-frame update path still needs the next local visual comparison; the final M3 gate is a two-client portal-out/materialization observer comparison.
- Issue
#225still needs the lifestone/particle alpha-order visual check, although its connected lifetime/performance gates pass. - Narrow behavior debt remains in issue
#153(an unstreamed far-teleport edge case), issue#116(slide feel), and registered TS-50/TS-51 effect timing. - Many later retail systems remain roadmap work: advanced social/vendor/trade/ crafting UI, full weather/lightning parity, deeper combat/stat systems, graphical Linux, and a true headless multi-session bot host.
See docs/README.md for document authority and current links.
Building + running
Requires:
- .NET 10 SDK
- A retail Asheron's Call dat directory (Turbine/Microsoft property —
supply your own). Contains
client_portal.dat,client_cell_1.dat,client_highres.dat,client_local_English.dat. - A running ACE (ACEmulator) server on
127.0.0.1:9000(or override via env var)
Launch (PowerShell on Windows — bash has trouble with the apostrophe in "Asheron's Call"):
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$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"
$env:ACDREAM_RETAIL_UI = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Release
Offline CLI dat inspector (no server needed):
dotnet run --project src/AcDream.Cli -- "C:\path\to\Asheron's Call"
Diagnostic env vars
| Variable | Effect |
|---|---|
ACDREAM_DUMP_SKY=1 |
Per-second dump of the interpolated SkyKeyframe values + per-SkyObject draw info + texture alpha histograms |
ACDREAM_DUMP_MOTION=1 |
Dump every inbound UpdateMotion + resulting SetCycle |
ACDREAM_STREAM_RADIUS=N |
Legacy override for the configured two-tier streaming radii |
ACDREAM_NO_AUDIO=1 |
Suppress OpenAL init |
ACDREAM_RETAIL_UI=1 |
Enable retained retail gameplay UI |
ACDREAM_DEVTOOLS=1 |
Enable ImGui developer tools |
ACDREAM_UNCAPPED_RENDER=1 |
Disable normal presentation pacing for throughput measurement |
ACDREAM_DAY_GROUP=N |
Force a specific DayGroup index for A/B-testing weather presets |
ACDREAM_RUN_SKILL=N / ACDREAM_JUMP_SKILL=N |
Client-side run/jump skill (default 200) |
Layout
src/
AcDream.App/ window, retained UI, rendering, audio, runtime wiring
AcDream.Content/ GL-free DAT mesh/texture extraction
AcDream.Core/ game state, physics, movement, world, gameplay logic
AcDream.Core.Net/ UDP + ISAAC + game-message layer
AcDream.UI.Abstractions/ shared UI/input ViewModels, commands, contracts
AcDream.UI.ImGui/ permanent developer-tool renderer
AcDream.Cli/ offline dat-inspector console app
AcDream.Bake/ offline content bake tooling
AcDream.Plugin.Abstractions/ plugin host interfaces
AcDream.Plugins.Smoke/ example plugin
tests/
AcDream.*.Tests/ layer-matched xUnit projects (6,452 passing,
5 intentional skips at the 2026-07-20 baseline)
tools/
RetailTimeProbe/ Win32 P/Invoke ReadProcessMemory probe of
the live retail acclient.exe — dumps
TimeOfDay + sky-lighting globals so we
can compare against acdream's state
SkyObjectInspect/ dat-inspector for Region sky objects
references/ vendored read-only reference code — ACE,
ACViewer, WorldBuilder, holtburger,
AC2D, Chorizite, DatReaderWriter.
Gitignored.
docs/
README.md documentation map + authority order
architecture/ architecture, structure, divergence, WB inventory
plans/ phase roadmaps + per-phase specs
research/ decompile-derived research, per-phase
findings, deep-dive agent reports
audit/ phase-completion audits
Development workflow
All AC-specific behaviour is ported from the named retail client oracle
(docs/research/named-retail/). The workflow is:
- Grep named retail first. Find the matching
class::methodin the Sept 2013 named pseudo-C and headers. - Decompile only as fallback. Use the older Ghidra chunks when the named oracle lacks the function.
- Cross-reference. Check against ACE's C# port and ACViewer / WorldBuilder.
- Write pseudocode. Translate C to readable pseudocode first.
- Port faithfully. Translate line-by-line, preserving variable names and control flow.
- Conformance test. Add tests using golden values from retail.
- Integrate surgically. Minimise churn in the surrounding pipeline.
Guessing at AC-specific algorithms is explicitly forbidden — see
CLAUDE.md for the full workflow rationale and the list of failure
modes we've paid for in the past.
Reference repos
We cross-reference six external projects for retail behavior:
- ACE (ACEmulator) — authoritative server-side protocol
- ACViewer — MonoGame dat viewer; good for character appearance
- WorldBuilder — Silk.NET dat editor; matches our stack
- Chorizite.ACProtocol — clean-room C# protocol library
- holtburger — most complete non-retail client; Rust TUI, full client-side behaviour
- AC2D — C++ AC-client emulator; has the real terrain split formula and 0xF61C movement packet format
See CLAUDE.md for which reference is authoritative for which domain.
Licence
Not yet chosen. All external reference code is vendored under its own
licence; see references/*/LICENSE. The acdream source code itself is
unreleased — not yet distributed to the public. Once the licence
choice is made it will go in a top-level LICENSE file.
The AC dat files and the game's intellectual property remain the property of Microsoft / Turbine. This project does not distribute any of those files or assets — you must supply your own retail install.