weather(phase-5d): AdminEnvirons packet handler + thunder sound dispatch

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-24 11:27:13 +02:00
parent 2e9a836f08
commit e4cf3a9b6b
2 changed files with 103 additions and 0 deletions

View file

@ -132,6 +132,33 @@ public sealed class WorldSession : IDisposable
/// </summary>
public event Action<uint /*guid*/, uint /*scriptId*/>? PlayScriptReceived;
/// <summary>
/// Phase 5d — retail's <c>AdminEnvirons</c> packet (opcode
/// <c>0xEA60</c>) — the one-and-only channel retail's server uses
/// for weather environment changes. Wire format:
/// <c>[u32 opcode][u32 environChangeType]</c>. The payload enum is
/// retail's <c>EnvironChangeType</c>:
/// <list type="bullet">
/// <item><description>
/// <c>0x00..0x06</c> — fog presets (Clear/Red/Blue/White/Green/
/// Black/Black2). Subscribers route these to a
/// <see cref="AcDream.Core.World.WeatherSystem.Override"/>.
/// </description></item>
/// <item><description>
/// <c>0x65..0x75</c> — one-shot ambient sound cues
/// (Roar / Bell / Chant / etc).
/// </description></item>
/// <item><description>
/// <c>0x76..0x7B</c> — Thunder1..Thunder6 sounds. Paired with
/// a separate <see cref="PlayScriptReceived"/> from the server
/// carrying the lightning-flash PhysicsScript.
/// </description></item>
/// </list>
/// See <c>docs/research/2026-04-23-lightning-crossfade.md</c> +
/// <c>2026-04-23-lightning-real.md</c>.
/// </summary>
public event Action<uint /*environChangeType*/>? EnvironChanged;
/// <summary>
/// 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]`