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:
parent
2e9a836f08
commit
e4cf3a9b6b
2 changed files with 103 additions and 0 deletions
|
|
@ -1084,6 +1084,14 @@ public sealed class GameWindow : IDisposable
|
||||||
// flashes during stormy weather.
|
// flashes during stormy weather.
|
||||||
_liveSession.PlayScriptReceived += OnPlayScriptReceived;
|
_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
|
// Phase G.1: keep the client's day/night clock in sync with
|
||||||
// server time. Fires once from ConnectRequest (initial seed)
|
// server time. Fires once from ConnectRequest (initial seed)
|
||||||
// and repeatedly on TimeSync-flagged packets.
|
// and repeatedly on TimeSync-flagged packets.
|
||||||
|
|
@ -2274,6 +2282,61 @@ public sealed class GameWindow : IDisposable
|
||||||
_scriptRunner.Play(scriptId, guid, camWorldPos);
|
_scriptRunner.Play(scriptId, guid, camWorldPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 5d — retail <c>AdminEnvirons</c> (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
|
||||||
|
/// <c>EnvironChangeType</c> → wave asset, which we don't yet
|
||||||
|
/// have dat-indexed; follow-up will wire the thunder wave ids.
|
||||||
|
/// </summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A.1: streaming load delegate, runs on the worker thread.
|
/// Phase A.1: streaming load delegate, runs on the worker thread.
|
||||||
/// Reads the landblock from the dats, hydrates its stab entities (same
|
/// Reads the landblock from the dats, hydrates its stab entities (same
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,33 @@ public sealed class WorldSession : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<uint /*guid*/, uint /*scriptId*/>? PlayScriptReceived;
|
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>
|
/// <summary>
|
||||||
/// Phase G.1: latest server Portal Year tick count. Seeded from the
|
/// Phase G.1: latest server Portal Year tick count. Seeded from the
|
||||||
/// ConnectRequest handshake (r12 §1.3 — server sends absolute game
|
/// ConnectRequest handshake (r12 §1.3 — server sends absolute game
|
||||||
|
|
@ -571,6 +598,19 @@ public sealed class WorldSession : IDisposable
|
||||||
var env = GameEventEnvelope.TryParse(body);
|
var env = GameEventEnvelope.TryParse(body);
|
||||||
if (env is not null) GameEvents.Dispatch(env.Value);
|
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
|
else if (op == 0xF754u) // PlayScript — server triggers a PhysicsScript on a target guid
|
||||||
{
|
{
|
||||||
// Phase 6b: wire format `[u32 opcode][u32 guid][u32 scriptId]`
|
// Phase 6b: wire format `[u32 opcode][u32 guid][u32 scriptId]`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue