From e4cf3a9b6b2df4ca4ddb2c0d3ad2b320b9a545a3 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Apr 2026 11:27:13 +0200 Subject: [PATCH] weather(phase-5d): AdminEnvirons packet handler + thunder sound dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports retail's AdminEnvirons (opcode 0xEA60) — the client-visible weather-event channel distinct from the PlayScript path. Wire format (chunk_006A0000.c: `[u32 opcode][u32 environChangeType]`). EnvironChangeType range: 0x00..0x06 — fog presets (Clear/Red/Blue/White/Green/Black/Black2) 0x65..0x75 — one-shot ambient sounds (Roar, Bell, Chant, etc) 0x76..0x7B — Thunder1..6 sounds (paired with a lightning PlayScript) Dispatch: - WorldSession decodes the packet, fires EnvironChanged event. - GameWindow.OnEnvironChanged: * Fog values (0x00..0x06) → WeatherSystem.Override. The enum values line up byte-for-byte with our EnvironOverride enum (deliberately mirrored from retail), so a direct cast works. * Sound values (0x65..0x7B) → console log with retail name for now. Actual OpenAL playback needs a EnvironChangeType → WaveData lookup (indexed via SoundTable dat), which is a separate follow-up. The event still fires so any future audio subscriber can plug in. Combined with Phase 6a-6c PhysicsScript/PlayScript wiring, the complete retail lightning pipeline is now: server sends PlayScript(0xF754, lightningGuid, scriptId=0x33xxxxxx) → runs the flash script via PhysicsScriptRunner → CreateParticleHook spawns the flash particles server sends AdminEnvirons(0xEA60, Thunder3Sound=0x78) → OnEnvironChanged logs; audio binding TBD Whether the user's ACE sends these packets depends on the server (ACE 2.x vanilla does NOT — Agent #5 verified no lightning opcodes in the default emit path). With the client port complete, any ACE mod or extension that emits the right packets will Just Work in acdream. Build + 742 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 63 +++++++++++++++++++++++++ src/AcDream.Core.Net/WorldSession.cs | 40 ++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ceb3c5a..44359db 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1084,6 +1084,14 @@ public sealed class GameWindow : IDisposable // flashes during stormy weather. _liveSession.PlayScriptReceived += OnPlayScriptReceived; + // Phase 5d — AdminEnvirons (0xEA60): fog presets + sound + // cues. Fog types (0x00..0x06) set WeatherSystem.Override; + // sound types (0x65..0x7B) play a one-shot audio cue. + // Lightning flashes arrive as a PAIRED PlayScript (the + // visual) + AdminEnvirons ThunderXSound (the audio) — both + // are handled here and in OnPlayScriptReceived respectively. + _liveSession.EnvironChanged += OnEnvironChanged; + // Phase G.1: keep the client's day/night clock in sync with // server time. Fires once from ConnectRequest (initial seed) // and repeatedly on TimeSync-flagged packets. @@ -2274,6 +2282,61 @@ public sealed class GameWindow : IDisposable _scriptRunner.Play(scriptId, guid, camWorldPos); } + /// + /// Phase 5d — retail AdminEnvirons (0xEA60) dispatcher. + /// Routes fog presets into the weather system's sticky override + /// slot and logs the sound cues (Thunder1..6, Roar, Bell, etc) + /// for now — actual sound playback needs a lookup table from + /// EnvironChangeType → wave asset, which we don't yet + /// have dat-indexed; follow-up will wire the thunder wave ids. + /// + private void OnEnvironChanged(uint environChangeType) + { + // Fog presets — values match AcDream.Core.World.EnvironOverride + // byte-for-byte (we deliberately mirrored retail's enum). + if (environChangeType <= 0x06u) + { + Weather.Override = (AcDream.Core.World.EnvironOverride)environChangeType; + Console.WriteLine( + $"live: AdminEnvirons fog override = " + + $"{(AcDream.Core.World.EnvironOverride)environChangeType}"); + return; + } + + // Sound cues 0x65..0x7B. Log by retail name for now; audio + // binding is a separate follow-up (needs sound-table indexing + // plus a PlaySound API on OpenAlAudioEngine that takes a + // retail sound enum → wave-id mapping). + string name = environChangeType switch + { + 0x65u => "RoarSound", + 0x66u => "BellSound", + 0x67u => "Chant1Sound", + 0x68u => "Chant2Sound", + 0x69u => "DarkWhispers1Sound", + 0x6Au => "DarkWhispers2Sound", + 0x6Bu => "DarkLaughSound", + 0x6Cu => "DarkWindSound", + 0x6Du => "DarkSpeechSound", + 0x6Eu => "DrumsSound", + 0x6Fu => "GhostSpeakSound", + 0x70u => "BreathingSound", + 0x71u => "HowlSound", + 0x72u => "LostSoulsSound", + 0x75u => "SquealSound", + 0x76u => "Thunder1Sound", + 0x77u => "Thunder2Sound", + 0x78u => "Thunder3Sound", + 0x79u => "Thunder4Sound", + 0x7Au => "Thunder5Sound", + 0x7Bu => "Thunder6Sound", + _ => $"Unknown(0x{environChangeType:X2})", + }; + Console.WriteLine( + $"live: AdminEnvirons sound cue = {name} " + + $"(0x{environChangeType:X2}) — audio binding pending"); + } + /// /// Phase A.1: streaming load delegate, runs on the worker thread. /// Reads the landblock from the dats, hydrates its stab entities (same diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 1a9fd87..9e85456 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -132,6 +132,33 @@ public sealed class WorldSession : IDisposable /// public event Action? PlayScriptReceived; + /// + /// Phase 5d — retail's AdminEnvirons packet (opcode + /// 0xEA60) — the one-and-only channel retail's server uses + /// for weather environment changes. Wire format: + /// [u32 opcode][u32 environChangeType]. The payload enum is + /// retail's EnvironChangeType: + /// + /// + /// 0x00..0x06 — fog presets (Clear/Red/Blue/White/Green/ + /// Black/Black2). Subscribers route these to a + /// . + /// + /// + /// 0x65..0x75 — one-shot ambient sound cues + /// (Roar / Bell / Chant / etc). + /// + /// + /// 0x76..0x7B — Thunder1..Thunder6 sounds. Paired with + /// a separate from the server + /// carrying the lightning-flash PhysicsScript. + /// + /// + /// See docs/research/2026-04-23-lightning-crossfade.md + + /// 2026-04-23-lightning-real.md. + /// + public event Action? EnvironChanged; + /// /// Phase G.1: latest server Portal Year tick count. Seeded from the /// ConnectRequest handshake (r12 §1.3 — server sends absolute game @@ -571,6 +598,19 @@ public sealed class WorldSession : IDisposable var env = GameEventEnvelope.TryParse(body); if (env is not null) GameEvents.Dispatch(env.Value); } + else if (op == 0xEA60u) // AdminEnvirons — server pushes a fog preset or sound cue + { + // Phase 5d: wire format `[u32 opcode][u32 environChangeType]` + // per chunk_006A0000.c. Dispatch the event; GameWindow + // subscribers route fog presets into WeatherSystem.Override + // and sound cues (thunder, roar, etc) into the audio engine. + if (body.Length >= 8) + { + uint envType = System.Buffers.Binary.BinaryPrimitives + .ReadUInt32LittleEndian(body.AsSpan(4, 4)); + EnvironChanged?.Invoke(envType); + } + } else if (op == 0xF754u) // PlayScript — server triggers a PhysicsScript on a target guid { // Phase 6b: wire format `[u32 opcode][u32 guid][u32 scriptId]`