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 b064668b63 feat(render): Campaign V slice V4d-2 - move terrain onto the RHI
TerrainModernRenderer records through IGpuPassEncoder instead of calling GL
directly. V4d-1 already converged its two matrix uniforms; this is the plumbing.

What moved. The per-frame indirect command array became an
IGpuFrame.AllocateRing slice, which retires the three-deep per-frame-slot
indirect buffer pool outright. That pool existed so a second terrain draw within
one frame - a retail outside view can issue several - could not overwrite an
earlier draw's still-pending commands; the frame ring gives that structurally,
because every allocation within a frame is distinct memory that lives until the
frame retires. DynamicIndirectBufferCount now reports 0, which is the truth
rather than a silent change.

The vertex and index arena became an IGpuBuffer pair. AddLandblock's two
BufferSubData calls are Upload, and EnsureCapacity's grow-and-copy is
IGpuBuffer.CopyTo, still device-side so resident landblock meshes never
round-trip through system memory. The global VAO is gone: the pipeline owns one
shaped by the vertex layout, and the encoder re-issues attribute pointers on
every BindVertexBuffer.

Locations 2-5 use GpuVertexFormat.UByte4UInt, added at c7f5f251 for exactly
this. They are uvec4 in the shader and carry terrain-type, road and
split-direction codes; UByte4Normalized would have delivered [0,1] floats to an
integer input, which GL leaves undefined - garbage, not an approximation.

uTextureIndexA/uTextureIndexB became GpuPushConstants.TextureIndexA/B. Slice V2b
named those uniforms to match the pinned block, so this was the rename it was
meant to be. uTexTiling moved from a loose uniform float[36] into a std140 block
at GpuBindingModel.UniformTerrainTiling: at 144 bytes of payload it cannot ride
in the 96-byte push-constant block, and no RHI verb sets a uniform array. std140
pads each element to 16 bytes so the block is 576, but the element type is
unchanged, so uTexTiling[int(layer)] reads exactly as before. It is a long-lived
uniform buffer uploaded on the first draw, preserving the upload-once property
the linked-program uniform had.

The imperative Enable(CullFace)/CullFace(Back)/FrontFace(Ccw) triple and the
inherited depth state are baked into one pipeline. Depth compare is GL_LESS, not
the contract's LessOrEqual default: the world frame runs under GL_LESS
(RenderFrameGlStateController.RestoreFrameDefaults) and terrain never called
glDepthFunc, so it inherited it. Baking LessOrEqual would change which of two
coplanar retail surfaces wins - visible exactly where terrain meets roads and
building footings, which is what the shader's zFightTerrainAdjust nudge is
about. Blend off, alpha-to-coverage off, colour write on and depth write on come
from the same frame default, each checked against what terrain observes rather
than assumed. GL_MULTISAMPLE is untouched by pipeline binds, so MSAA does not
leak away from the still-raw-GL sky and particles.

Deliberately unmoved. The terrain clip UBO at binding 2 and the SceneLighting
UBO at binding 1 stay raw global binds - ClipFrame owns one and the viewport and
portal renderers read the other, and both are raw GL until V4h (campaign doc
5.3). The interim GlBindlessHandleTable stays, now held as an IGpuBuffer and
bound through the encoder at binding 9; retiring it is V4t. glMemoryBarrier
stays a raw call: it has no RHI verb and was already a no-op against
client-side uploads. The trailing FrontFace(CW)/Disable(CullFace) restore stays
so sky and particles see what they see today. TerrainAtlas is untouched - it
belongs to V4t. Terrain has no GPU timer to port; its diagnostics use a CPU
stopwatch.

Three consequences worth naming rather than leaving to be discovered.

The convenience constructor narrowed from public to internal, because IGpuDevice
and ICurrentGpuFrameSource are internal RHI types and a public constructor
cannot name them. The class stays public, no other member changed visibility,
and every caller was already in this assembly - EnvCellRenderer's constructor is
internal for the same reason. That is the only visibility change in the diff.

Terrain no longer needs a Shader composed for it, since its pipeline compiles
terrain_modern from the same sources with the same shared preamble. That removes
the terrain-shader composition step, its publication, its lifetime field and the
WorldRenderCompositionPoint member. Two data-driven test cases went with it: one
InlineData row naming "terrain shader" as a publication to fail, and one case
from the theory that enumerates every composition point. App tests therefore
read 3,844 rather than the 3,846 baseline. No invariant lost coverage - both
theories still exercise every remaining resource and point; the two cases were
parameterisations over a step that no longer exists.

The renderer's own GpuRetirementLedger is gone. Every resource it held retryable
releases for is an IGpuBuffer or IGpuPipeline now, and their Dispose already
routes the physical free through the device's retirement queue. Only the
fallback clip UBO is still a raw GL name, so it is all the dispose ledger
carries. The slot allocator's separate retryable publication path is untouched.
Also dropped: a dead BindlessSupport field, assigned and never read.

Gates. Release build green with TreatWarningsAsErrors. App tests 3,844 passed /
3 skipped over four consecutive runs. Offline pixel gate against 0cb10597: 20
differing pixels of 563,200 (fraction 3.55e-05, 28x under the threshold),
against a same-commit control at this commit of 26 - the change differs from its
parent by LESS than the capture differs from itself, which is as close to proof
of no systematic shift as this gate can give. Compared against all three V4d-1
captures the numbers are 20, 32 and 34, against a same-commit V4d-1 spread of 8,
27 and 28: the same distribution. The gate run's client log has zero exceptions
and an empty stderr.

Coverage gap, stated rather than assumed: the offline gate's scene is a fixed
outdoor view. It exercises terrain heavily - terrain blending, road overlays and
the water edge are most of the frame - but it does not cover terrain seen
through a doorway clip region, which is the one terrain path with its own
binding (the clip UBO at binding 2). That wants a user visual check.

No divergence-register row: this slice changes no retail-facing behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 21:31:53 +02:00
.github feat(linux): gate graphical backends and capabilities 2026-07-27 12:26:58 +02:00
.vscode ci: add GitHub Agentic Workflows scaffolding + daily hygiene assessment 2026-05-22 23:31:13 +02:00
docs docs(render): connected verification is available; convert visual debt to a gate 2026-07-27 21:19:24 +02:00
memory docs: reconcile project state and refresh README 2026-07-27 12:53:31 +02:00
references chore(submodule): advance WB to acdream-fix-floor-rendering 2026-05-19 13:23:19 +02:00
src feat(render): Campaign V slice V4d-2 - move terrain onto the RHI 2026-07-27 21:31:53 +02:00
tests feat(render): Campaign V slice V4d-2 - move terrain onto the RHI 2026-07-27 21:31:53 +02:00
tools test(render): fail the offline pixel gate on stray camera input 2026-07-27 20:05:25 +02:00
.gitattributes ci: add GitHub Agentic Workflows scaffolding + daily hygiene assessment 2026-05-22 23:31:13 +02:00
.gitignore docs(runtime): close exact world host ownership 2026-07-26 18:41:24 +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(headless): establish portable Linux host boundary 2026-07-27 01:10:45 +02:00
AGENTS.md docs: reconcile project state and refresh README 2026-07-27 12:53:31 +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 feat(render): Campaign V slice V0 — pin the Vulkan-shaped RHI contract 2026-07-27 14:26:26 +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
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
README.md docs: reconcile project state and refresh README 2026-07-27 12:53:31 +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 modern OpenGL capabilities

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 baseline is a successful Release build with 8,826 passing tests and 5 intentional skips. The build currently reports 17 test-project warnings tracked by #228; production compilation has zero errors.

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 package is approximately 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"
$env:ACDREAM_RETAIL_UI = "1"

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=1 Enable the retained retail gameplay UI
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.