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 2e9a836f08 weather(phase-6bc): wire PlayScript packet + script runner into frame loop
Phase 6b — WorldSession now dispatches the PlayScript opcode (0xF754)
that retail uses for all server-triggered client-side visual effects.
Wire format per Agent #5 decompile (chunk_006A0000.c:12320-12336):

    [u32 opcode=0xF754][u32 targetGuid][u32 scriptId]

New event `PlayScriptReceived(uint guid, uint scriptId)` fires on
every matching fragment. Unknown payloads (body < 12 bytes) are
silently ignored.

Phase 6c — GameWindow instantiates a PhysicsScriptRunner at startup,
subscribes to PlayScriptReceived, and ticks the runner every frame
BEFORE the ParticleSystem tick so a CreateParticleHook fired this
frame gets its emitter integrated in the same frame.

Anchor policy: use the camera's world position for the script anchor.
For Dereth-wide storm effects (lightning flashes) the camera is the
right reference frame — the flash is "around the player." Per-entity
effects (spell casts, emotes) dedupe by (scriptId, entityId) so
multiple simultaneous plays on different guids work; a follow-up will
look up the guid's last-known world pos from _worldState for accurate
per-entity anchoring.

The full pipeline now for a lightning flash:
  1. ACE (or other retail-emulating server) sends
     GameMessage(0xF754, lightningGuid, scriptId=0x33xxxxxx).
  2. WorldSession parses: PlayScriptReceived event fires.
  3. GameWindow.OnPlayScriptReceived routes to _scriptRunner.Play.
  4. Runner loads the PhysicsScript from the dat, schedules every
     (StartTime, AnimationHook) entry.
  5. Per-frame Tick fires each hook at its scheduled time via
     ParticleHookSink — CreateParticleHook spawns a particle emitter
     (the flash), SoundHook plays thunder audio (Phase 5d), etc.

Set ACDREAM_DUMP_PLAYSCRIPT=1 to see each inbound PlayScript and each
hook fire as `[pes]` log lines — useful for identifying which script
IDs your ACE server actually sends.

Build + 742 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:24:30 +02:00
docs sky(phase-5a): remove DayGroup-name rain hack, ship retail-only Overcast mapping 2026-04-24 11:04:36 +02:00
memory docs(memory): session 2026-04-19 handoff — remote motion port complete 2026-04-19 21:33:12 +02:00
src weather(phase-6bc): wire PlayScript packet + script runner into frame loop 2026-04-24 11:24:30 +02:00
tests weather(phase-6a): port retail PhysicsScript runtime 2026-04-24 11:20:39 +02:00
tools sky(phase-3g): fix LCG multiplier — 360 (DaysPerYear), not 7620 2026-04-24 10:17:38 +02:00
.gitignore chore: gitignore launch.log 2026-04-18 23:11:56 +02:00
AcDream.slnx sky(phase-3g): fix LCG multiplier — 360 (DaysPerYear), not 7620 2026-04-24 10:17:38 +02:00
CLAUDE.md docs(claude.md): add 'Running the client against the live server' section 2026-04-19 14:58:13 +02:00
README.md chore: phase 0 — skeleton + dat asset inventory 2026-04-10 09:02:56 +02:00

acdream

Experimental modern open-source Asheron's Call client in C# / .NET 10.

Status: pre-alpha, not playable. Phase 0 only — dat file asset inventory.

Stack: .NET 10, Chorizite.DatReaderWriter for dat parsing. Silk.NET + Avalonia planned for rendering/UI (not yet wired up).

Requires: A retail Asheron's Call install (Turbine/Microsoft property — supply your own). Set ACDREAM_DAT_DIR environment variable to the directory containing client_portal.dat, client_cell_1.dat, client_highres.dat, and client_local_English.dat, or pass it as the first CLI argument.

Layout

  • src/AcDream.Cli/ — console app that dumps asset counts from a dat directory
  • references/ — local read-only reference material (ACE, ACViewer, WorldBuilder, DatReaderWriter, holtburger, retail AC install). Gitignored.

Run

dotnet run --project src/AcDream.Cli -- "C:\path\to\Asheron's Call"

Or set ACDREAM_DAT_DIR and run without args.