Three-lens review of c7ab5b6d8 found the round-0 port of
DrawPortalPolyInternal's degenerate-input guard inverted retail's
quantifier: it rejected a polygon when ANY vertex sat on ANY of the
four +/-12 planes. The PDB-paired bytes (VA 0x59BCD6-0x59BD66: four
per-plane fld/fcomp 12.0/fnstsw/test ah,0x44/jnp sequences, each
accumulating a "every vertex on this plane" boolean, cleared by the
first vertex NOT on it) and the named decomp's own nested-if reading
of the post-loop branch (0x59bd42-0x59bd6c) agree: retail rejects only
when EVERY local vertex lies on the SAME one of the four planes — a
polygon degenerate onto a plane, not one that merely touches a plane
at one vertex.
F1 — the quantifier. WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard
now tracks four booleans (everyVertexOnPlusX/MinusX/PlusY/MinusY), each
starting true and cleared by the first vertex that fails its own plane
test; the guard rejects iff any of the four survives the whole vertex
loop. T2 (WalkVisibilityMathTests.cs) is rewritten to the five §7
cases: every vertex on x=+12 -> rejected; every vertex on y=-12 ->
rejected; one vertex on x=+12, rest inside -> KEPT; every vertex at
11.999 -> kept; vertices split across x=+12 and y=+12 (no common
plane) -> kept. WalkFrameDriverTests' punch-side pin
(OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape)
carries the same correction — its old "one vertex on the plane"
polygon was wrongly rejected under round 0; it punches now, and a new
all-on-plane polygon is added as the actual rejected case. The doc
comment's "essentially never" sentence is deleted and replaced with a
fresh DAT-scan citation (see datScan below).
MUTATION (verified): restore the any-vertex form -> the one-vertex and
split-plane admit cases fail (Assert.False Expected:False Actual:True,
x2; the WalkFrameDriverTests punch pin's Assert.Equal Expected:2
Actual:1). Delete the guard (always return false) -> the all-on-plane
reject cases fail (Assert.True Expected:True Actual:False, x3 —
plusX-plane, minusY-plane, and the vacuous-empty-polygon pin; the
punch pin's Assert.Equal Expected:2 Actual:3).
datScan (DAT counts backing F1's doc comment): a throwaway xunit test
(deleted after this commit — not part of the diff) walked the full
0x0000-0xFFFF landblock-prefix space (LandBlockInfo/EnvCell carry no
usable DatCollection.GetAllIdsOfType range — both are landblock-
relative — matching the method CellStructSurfaceConstructionInstalledDatTests
already established), resolved each EnvCell's Environment/CellStruct,
and for every CellPortal's polygon tested each vertex's local x/y
against +/-12. Against the installed client_cell_1.dat: 3,405
landblocks with cells, 1,854,237 total portal polygons. 2,889 polygons
have at least one vertex on a +/-12 plane; 2,163 have EVERY vertex on
the SAME plane — all 2,163 are EXIT portals (OtherCellId==0xFFFF, out
of 16,939 exit portals total); 0 interior (non-exit) portals ever
qualify (interior: 1,837,298 total, 340 any-vertex, 0 all-on-plane).
Duration ~15s. This exactly reproduces the review's cited numbers.
F2 — seal-path count order. RetailPViewPassExecutor's exit-seal loop
dropped the `localVertices.Length < 3` pre-filter that ran BEFORE both
the boundary guard and the `submitted` increment (round 0's order:
length check -> guard -> transform -> draw -> count). Retail's real
order is guard -> transform -> clip -> count, with the counter
incremented BEFORE polyClipFinish runs (0x59BD70-0x59BD74 precedes
0x59BDB0) — the counter records accepted ATTEMPTS, not successful GPU
fans. `submitted++` now runs immediately after the guard-gated
transform+EmitSeamMask, before DrawDepthFan (whose own `< 3` guard is
this port's stand-in for retail's post-clip `var_4 >= 3` check). A new
pin (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing)
constructs a real PortalDepthMaskRenderer (RecordingGpuDevice-backed)
and a constructor-bypassed executor (RuntimeHelpers.GetUninitializedObject,
the same pattern WalkOutsideViewReassemblyTests already uses) with a
2-vertex ordinary-coordinate portal polygon: DrawExitPortalMask returns
submitted=1 while zero draw calls reach the device.
MUTATION (verified): restore the `< 3` pre-filter ahead of the guard
-> the pin's Assert.Equal fails, Expected:1 Actual:0.
F3 — the depth-event transcript gate, automated. New
WalkAlphaDepthTrace.cs (beside WalkOracleTrace.cs) parses the four
`PM poly=<ptr> mode=<0|1> counterBefore=<hex>` / `PC ov=<n>
counter=<hex> fc=<0|1>` lines per oh-capture/*.alphadepth.log frame,
using the same F-marker frame delimiting as WalkOracleTrace. New
WalkTraceConformanceTests.AlphaDepthTranscript.cs (beside
WalkTraceConformanceTests.cs, which is now `partial`) replays each
pose's frame 2 through the real WalkFrameDriver/RetailFrameWalk over
WalkLandscapeDatBuilder-built installed-DAT world data, with:
- AlphaDepthCollectSink: an IWalkEventSink decorator forwarding every
hook to the real driver unchanged, while recording mode=1 punch
events (OnPunchGeometry, counterBefore = driver.PortalsDrawnCount
at COLLECT time — Collect finishes entirely before Replay starts,
so reading the counter at Replay time would already see the SAME
turn's post-reset value; holtburg-doorway-still's own capture
proves this: its punches show counterBefore=2 immediately followed
by its seals' counterBefore=0/1 in the identical turn) and every
PC tuple (Emit, WalkEventKind.DrawCells — fires for the root's own
turn AND every building look-in's re-entrant call, matching
retail's breakpoint not distinguishing them).
- AlphaDepthLeaf: an IWalkFrameLeafRenderer whose DrawExitSeals
reproduces DrawPortalDepthWrite's real per-portal loop (F1's guard,
F2's count-before-clip order) directly over
driver.InteriorFloodCells / WalkCell.Portals / .PortalPolygons —
the identical data the render-side production path reads — since
the interface gives DrawExitSeals no cell/portal parameter at all.
- Two Collect+Replay passes per pose (a fresh driver reaches the SAME
steady-state a running session already carries by the time
retail's capture began — holtburg-doorway-still and foundry-deep
both show a stable per-frame accepted-seal count that regenerates
identically every captured frame): the first pass's own output is
discarded, only the second is compared.
Five Facts (one per pose — the spec's "four" undercounts the five
*.alphadepth.log files actually present in oh-capture/):
AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2 — PASS
AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2 — PASS
AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2 — PASS
AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2 — PASS
(proves the incrementing 0/1/2 accepted-seal sequence AND the
pre-reset counterBefore=2 punches, end to end, against real DAT
portal data)
AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2 — KnownFailure
(tagged [Trait("Status","KnownFailure")]): terrace-edge never runs
a qualifying interior turn in its own capture (every PC line reads
ov=0) — its punches' counterBefore=2 is a session value carried
over from BEFORE the capture even started (the file's own pre-"F 1"
content already reads 2; nothing inside the captured frames ever
touches the counter). A fresh two-pass replay from
PortalsDrawnCount=0 has no mechanism to derive that leftover value.
Printed divergence: expected [(mode=1,counterBefore=2) x2], actual
[(mode=1,counterBefore=0) x2]; the PC sequence (ov=0 both times)
matches exactly. An initial-condition gap in the fixture, not a
guard/count defect — the identical harness reproduces holtburg-
doorway-still's and foundry-deep's own steady states exactly from
cold starts.
MUTATION (verified): swap AlphaDepthLeaf's recorded seal mode from 0
to 1 -> AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2
fails (Assert.Equal Collections differ at pos 5: expected
(0,0),(0,1)... actual (1,0),(1,1)...).
DispatcherFixture and DrawScope (WalkFrameDriverTests.cs) are widened
from private to internal so the new gate can drive a real
WbDrawDispatcher without duplicating that fixture.
F4 — manifest comment truth. VulkanShaderManifestTests.cs's
portal_depth.vert.spv re-pin comment had the ULP direction backward
(claimed the OLD 0x3F7FFFFE literal was NEARER the camera than
retail's real 0x3F7FFFEF constant; portal_depth.vert's own shader-
source comment already had this correct — FARTHER, larger z/w) and
cited T1 as living in WalkVisibilityMathTests when
PortalDepthVert_FarPunchConstant_MatchesRetailExactBits is in this
same file. Both corrected.
Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0
errors. Hermetic lane (Lane!=InstalledDat&...&Status!=KnownFailure):
6829/6829 passed. InstalledDat lane (real client_cell_1.dat): 253
total, 247 passed, 5 failed — exactly the two #383 layout tests
(LayoutImporterMediaBearingChildSweepTests/LayoutImporterInvisibleSweepTests),
TowerAscentReplayTests' KnownFailure, Oh_doorway_still_first_frame_diff
(#458), and this round's own AlphaDepthTranscript_TerraceEdge
KnownFailure — no other divergence. Shader tests (VulkanShaderDescriptorContractTests
+ VulkanShaderManifestTests + RenderPackSpirvValidatorTests +
SkyVertexLayoutTests): 35/35. Register/divergence tests: 52/52.
dotnet build src/AcDream.App -c Release: 0 warnings/0 errors.
Register: no new AD row. F1/F2 are straight bug fixes toward retail
fidelity (not deviations); AD-119 (the C2 blend-vs-write-mask row)
does not describe the count order, so its neighbour needed no
correction per F2's own instruction.
Deviation from the fix-round spec's literal text: §7 F1 said "swap the
any-vertex hits instead of the four per-plane accumulators" as the
split-plane case's mutation — the code actually ported uses per-vertex
`if (x != 12f) flag = false;` clears, not a per-vertex OR; the mutation
verified above (restore the any-vertex FORM entirely) exercises the
same defect class and is the mutation actually run. §7 also names
"four" alphadepth.log fixtures where five exist on disk
(cathedral-arrival, cathedral-leak, foundry-deep,
holtburg-doorway-still, terrace-edge); all five are gated rather than
four, with cathedral-leak an unremarked addition alongside the four
the spec names.
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.