Modern open-source C# .NET 10 Asheron's Call client. Faithful port of retail client behaviour to Silk.NET with a plugin API.
Find a file
Erik 466272ec55 fix(plugin-ui): Slice B review fixes — no magenta for bad DIDs, contract threshold, integral icon bindings, nearest did art, loud markup errors
Two Opus reviews of commit 8217a349e (Slice B: DAT icons in plugin
markup) found one BLOCKING defect and 14 SHOULD-FIX findings. All 15
fixed here in one commit per the review contract.

BLOCKING (finding 1): an unresolvable did painted a magenta square.
TextureCache.GetOrUploadRenderSurface's 1x1 magenta placeholder for a
missing RenderSurface is load-bearing for authored chrome, but
RetailMarkupIconResolver.ResolveDid only short-circuited did==0, so any
other unresolvable id fell through to that placeholder and got scaled
up by UiMarkupIcon/UiMarkupList/UiSimpleButton -- the classic
resolve(0)-style footgun (claude-memory/feedback_ui_resolve_zero_magenta.md),
just triggered by a missing id instead of a literal 0. Fixed by probing
Portal/HighRes existence via IDatReaderWriter.TryGet<RenderSurface>
BEFORE ever calling GetOrUploadRenderSurface -- that TryGet already
serializes concurrent DAT access internally (DatDatabaseWrapper's own
_databaseLock), the same synchronization IconComposer.TryDecode relies
on, so no additional lock was needed. RetailMarkupIconResolver now
takes IDatReaderWriter + TextureCache directly (RetailUiAssets gained a
TextureCache field, its one construction site in
InteractionRetainedUiComposition.cs updated) instead of the old
resolveSprite delegate, since it also needs the nearest-sampled upload
path for finding 6 below.

Finding 2 -- Smoke panel wiring bugs: its list fed iconkind="spell"
raw art DIDs (PluginSpellInfo.IconId) instead of spell ids, so
IMarkupIconResolver.ResolveSpell composited the wrong (or no) badge
every row. SmokeIconPanel.Binding.SpellIds now yields SpellId (the
printed text still shows IconId alongside). The bare-index demo and the
descriptor both moved from the unverified literal 7735 to 0x165 --
retail's real Melee Defense skill icon (SampleData.cs:64,
0x06000165) -- so the owner's visual gate proves real art, not a guess.
StartVisible flipped true, and a character with no self-buffs known
falls back to spell 1's real catalog entry (or an honest "no spells
known" row with icon 0 if even that fails) rather than fabricating art.

Finding 3 -- PluginIcons.Normalize's threshold was silently rewritten
from the contract's 0x01000000 to 0x06000000 during Slice B. Restored
to 0x01000000; the class/method XML docs now state the number directly
(no cref to the private const); the test table adds 0x02000000 (a value
that only distinguishes the two thresholds) and 0x01000000 itself
(passes through unchanged).

Finding 4 -- an unknown iconkind (e.g. "spel") only threw when a
resolver happened to be wired, because BuildIconSource/
BuildRowIconResolve validated inside their own null-icons early return.
A new ValidateIconKind helper runs UNCONDITIONALLY before that branch,
so a malformed iconkind is a Build-time author error on every host.

Finding 5 -- BindUintLiteralOrBinding required an exact uint property
type, rejecting the int-typed bindings Decal-facing code commonly uses
(MosswartMassacre's HudPictureBox.Image is int end to end). It now
matches BindUint's existing leniency: any property, converted via
Convert.ToUInt32 at read time. BindUintList likewise now accepts
IEnumerable<int> alongside IEnumerable<uint> (unchecked per-element
reinterpret -- icon ids never go negative in practice).

Finding 6 -- TextureCache._renderSurfaceGpuTextures was keyed by id
alone, so whichever caller asked for a given RenderSurface id FIRST won
the sampler for every later caller of the same id -- UiDatFont's glyph
atlases already request nearest:true while ResolveChrome's background
art requests nearest:false, so this was a real, reachable collision,
not hypothetical. Rekeyed to (id, nearest); RetailMarkupIconResolver.
ResolveDid now requests nearest:true (pixel-exact 32x32 icon art);
ResolveChrome is untouched (still nearest:false/linear). Audited every
other _renderSurfaceGpuTextures use site (TryGetValue/set/Dispose
iteration+Clear) plus the separate _nearestUiTextureSources/
_linearUiTwinHandles/_uploadMetadata dictionaries (all keyed by handle
or accounting name, unaffected) -- no other eviction/accounting path
assumed id-only keying.

Finding 7 -- column-reservation semantics, per the DECIDED shape:
MarkupDocument now sets button.IconSource / list.IconIdsSource +
IconResolve ONLY when a resolver (icons parameter) is actually wired --
previously button.IconSource was always assigned (even to an
always-empty func on an icons:null host); combined with this finding's
other half -- UiSimpleButton.OnDraw now reserves its icon column
whenever IconSource is non-null, regardless of a per-frame resolve miss,
so a bound id that goes briefly to 0 no longer slides the caption back
and forth -- would have permanently reserved a blank column on such a
host. UiMarkupList already reserved its column whenever IconIdsSource
was set; no draw-side change needed there.

Finding 8 -- added a with/without-icons comparison test for
UiMarkupList (mirroring the existing UiSimpleButton one): asserts the
row text quad's x is strictly greater with an icon column present, and
the icon quad itself has non-zero width.

Finding 9 -- <icon tooltip=""> (empty string) was still treated as
"has a tooltip" by a bare attribute-presence check, making the icon
swallow clicks with no visible tooltip ever appearing. Now uses
!string.IsNullOrWhiteSpace, matching ApplyCommon's own predicate for
every other element's tooltip.

Finding 10 -- PluginShelfButton.OnDraw drew nothing when a non-zero
descriptor icon id resolved to no texture (a bad Decal index, a DAT id
from a different install), rather than falling back to Initials the
way a zero id already did. Now decides once, on the first draw
(memoized, so Initials' string work never repeats every frame): a
failed resolve permanently switches Text to the initials fallback,
computed and assigned BEFORE base.OnDraw actually paints the caption.

Finding 11 -- MarkupDocument.AddElement's switch had no default arm, so
an unknown or miscased element name (<Icon>, <butotn>) silently
vanished from the built tree instead of failing loudly like every
other malformed-markup case. Added a default arm that throws
FormatException. Ran AcDream.Plugins.MossTank.Tests (337/337,
unchanged) and the full App markup suite to confirm no existing markup
relies on an unknown element.

Finding 12 -- PluginPanelDescriptor.IconSurfaceId's XML doc now states
that a bare Decal index is accepted and normalized, citing
PluginIcons.Normalize.

Finding 13 -- docs/plugin-ui-markup.md: replaced the blanket "wrong
type/missing property throws at Build" sentence with the per-attribute
truth table the review produced (which attributes are silent at
runtime vs. throw at Build, and each one's bound CLR/delegate type);
restated the icon-id boundary as 0x01000000; added the "do NOT add
0x06000000 to the four already-full IconId records" warning (citing
SkillBase._iconID / UIRegion::SetImageByDID @0x004f150e); documented
that 0x-prefixed hex is required (an unprefixed all-digit literal
parses as decimal); noted unknown element names now throw; called out
list colors (0xRRGGBB) vs. color=/background=/border= (#AARRGGBB) as
non-interchangeable grammars; documented the root <panel visible>
binding-only exception; corrected the shelf's collapse toggle glyphs
(</>, not the old doc's arrows) and the 28px collapsed-tab size; added
the IconId record-equality API-v1 note; and called out iconkind as
per-<list> (mixed id spaces need pre-normalized DIDs; the composited
spell badge has no did-space escape hatch) plus the existing
one-text-column LIMITATION being deferred to MossTank.

Coverage added for finding 14: a PluginSidePanelTests case proving the
shelf button normalizes a bare descriptor index before resolving, and a
reflection-based unit on AppAutomationSurface.ProjectWorldObject (its
public callers gate on IsAvailable, which needs a fully connected
session heavier than this mapping needs -- the plan's own documented
fallback) proving PluginWorldObject.IconId carries ClientObject.IconId
through unchanged; PluginInventoryItem.IconId uses the identical
one-line pattern inline in CaptureOwnedItems, reviewed by inspection.

Finding 15: recorded a "Review ledger" section in the plan doc with
both slices' commits, both review verdicts, and the two items
explicitly deferred to the MossTank plugin work (multi-column list,
root literal visible).

Verification: full solution builds green. Targeted filter
(Markup|PluginSidePanel|PluginIcons|AppAutomation|TextureCache|
UiDatFont) passes 131/131, including the two InstalledDat-lane tests
(RetailMarkupIconResolverInstalledDatTests,
AppAutomationSurfaceIconInstalledDatTests) actually resolving against
the real installed DAT, not skipping. AcDream.Plugins.MossTank.Tests
passes 337/337 unchanged. Full AcDream.App.Tests suite: 7351 passed /
97 skipped / 36 failed -- identical failure set/count to the
7334/97/36 baseline (the +17 passes are exactly the new/expanded
tests: 2 new PluginIconsTests.Normalize theory rows, 10 new
MarkupIconTests cases, 2 new PluginSidePanelTests cases, 1 new
AppAutomationSurfaceTests case, and the 2 new standalone test files).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 15:43:56 +02:00
.gitea/workflows fix #485: isolate console capture and align Gitea portable test lanes 2026-09-06 11:41:27 +02:00
.github test(vulkan): isolate the hardware witness lane 2026-09-05 03:19:55 +02:00
.vscode ci: add GitHub Agentic Workflows scaffolding + daily hygiene assessment 2026-05-22 23:31:13 +02:00
assets/icons fix(app): apply the window icon from Load, not beside Window.Create 2026-08-20 15:13:29 +02:00
docs fix(plugin-ui): Slice B review fixes — no magenta for bad DIDs, contract threshold, integral icon bindings, nearest did art, loud markup errors 2026-09-06 15:43:56 +02:00
memory fix(platform): Campaign LA LA0 review fixes — CI Linux lanes, arch doc, self-guard 2026-08-14 15:30:04 +02:00
references chore(submodule): advance WB to acdream-fix-floor-rendering 2026-05-19 13:23:19 +02:00
samples feat(render): implement Campaign AR and terrain fidelity 2026-08-22 13:13:29 +02:00
src fix(plugin-ui): Slice B review fixes — no magenta for bad DIDs, contract threshold, integral icon bindings, nearest did art, loud markup errors 2026-09-06 15:43:56 +02:00
tests fix(plugin-ui): Slice B review fixes — no magenta for bad DIDs, contract threshold, integral icon bindings, nearest did art, loud markup errors 2026-09-06 15:43:56 +02:00
tools docs #477: bound first-failure reporting and extended reproduction 2026-09-05 18:49:22 +02:00
.gitattributes ci: add GitHub Agentic Workflows scaffolding + daily hygiene assessment 2026-05-22 23:31:13 +02:00
.gitignore research(render) Campaign FW0: retail walk-oracle harness + 10 live trace fixtures 2026-08-30 09:10:07 +02:00
.gitmodules phase(N.0): wire up WorldBuilder fork as submodule + project refs 2026-05-08 08:51:49 +02:00
AcDream.slnx feat(render): implement Campaign AR and terrain fidelity 2026-08-22 13:13:29 +02:00
AGENTS.md fix(render): harden portal exit handoff 2026-08-25 17:39:44 +02:00
analyze_flap_live.py diag(render): flap re-diagnosed as portal-flood re-clip DRIFT; physics + camera REFUTED 2026-06-08 11:21:46 +02:00
CLAUDE.md fix #435 (part 2, closes it): attribute the unowned probes — delete 7, reclassify 8, restore 1 2026-08-24 12:32:42 +02:00
Directory.Build.props build: make release restore reproducible 2026-08-18 10:29:00 +02:00
Directory.Packages.props build: make release restore reproducible 2026-08-18 10:29:00 +02:00
find_burst.py diag(render): flap re-diagnosed as portal-flood re-clip DRIFT; physics + camera REFUTED 2026-06-08 11:21:46 +02:00
global.json ci: add bounded complete release gate 2026-08-18 09:09:38 +02:00
launch-a6-issue98-capture.ps1 docs(research): A6.P3 #98 — comparison harness findings + neighborhood fixtures 2026-05-23 20:12:43 +02:00
launch-a6-issue98-cottage-gfxobj-dump.ps1 test(phys): A6.P3 #98 — comparison harness reproduces cottage-floor cap 2026-05-23 20:44:50 +02:00
launch-a6-issue98-polydump.ps1 docs(research): A6.P3 #98 — comparison harness findings + neighborhood fixtures 2026-05-23 20:12:43 +02:00
launch-flap-capture.ps1 diag(render): flap re-diagnosed as portal-flood re-clip DRIFT; physics + camera REFUTED 2026-06-08 11:21:46 +02:00
launch-flap-churn.ps1 diag(render): launch-flap-churn.ps1 — Phase 1 portal-churn pin capture script 2026-06-08 12:56:44 +02:00
launch-flap-verify.ps1 docs(render): FLAP settled by live-retail measurement — full retail port DECIDED (Option A) + exhaustive handoff 2026-06-08 16:19:34 +02:00
NOTICE.md chore(O-T1): create Core/Rendering/Wb directory + NOTICE.md attribution 2026-05-21 14:59:56 +02:00
NuGet.Config build: make release restore reproducible 2026-08-18 10:29:00 +02:00
README.md fix: complete retail parity stability pass 2026-08-28 20:01:39 +02:00

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.pak produced by AcDream.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 GameRuntime is hosted by both the graphical client and AcDream.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 13, 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 #225 retains 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.dat
    • client_cell_1.dat
    • client_highres.dat
    • client_local_English.dat
  • A machine-local acdream.pak built 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/:

  1. Search the named retail pseudo-C and headers by class::method.
  2. Use the older Ghidra chunks only when the named oracle is insufficient.
  3. Cross-reference ACE and the relevant client/viewer implementation.
  4. Record readable pseudocode and exact constants/order.
  5. Port the retail mechanism into the correct modern owner.
  6. Add conformance, lifecycle, and failure-boundary tests.
  7. Run the automated gate and the appropriate connected or visual gate.
  8. 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.