refactor(weather): delete legacy camera-attached rain/snow particle emitter

The pre-research workaround at GameWindow.UpdateWeatherParticles +
BuildRainDesc + BuildSnowDesc was acdream's stand-in for retail's
weather rendering. It emitted billboarded particles inside a 15m disk
attached to the camera ('AttachLocal'), with a broken alpha fade
(0.3 → 0 caused rain to vanish at exact ground level — Issue #1) and a
fixed disk that visibly framed the player even at speed.

Retail rain is the world-space mesh path (SkyRenderer.RenderWeather):
  GfxObj 0x01004C42 / 0x01004C44 — hollow octagonal cylinder, 113m radius,
  815m tall, anchored at player_pos + (0, 0, -120m) per
  GameSky::UpdatePosition at 0x00506dd0 — drawn AFTER the landblock pass
  per LScape::draw at 0x00506330. Snow renders identically when a Snowy
  DayGroup is active: the partition by Properties&0x04 picks up snow
  weather meshes for free.

The legacy emitter was gated behind ACDREAM_FAKE_RAIN_PARTICLES=1 in
the previous commit (3e0da49) so the world-space path could be
A/B-compared. Visual verification this session confirmed the world-
space path is correct; deleting the legacy code removes ~120 LOC plus
the env var, the gate, the _rainEmitterHandle / _snowEmitterHandle
fields, and the _lastWeatherKind state machine.

Files affected:
  GameWindow.cs: drop UpdateWeatherParticles, BuildRainDesc, BuildSnowDesc,
  emitter-handle fields, last-weather-kind state, and the gated call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-27 12:05:12 +02:00
parent 4678b3ee6b
commit d95a8d2a55

View file

@ -373,12 +373,6 @@ public sealed class GameWindow : IDisposable
private long _loadedSkyDayIndex = long.MinValue; private long _loadedSkyDayIndex = long.MinValue;
private AcDream.Core.World.DayGroupData? _activeDayGroup; private AcDream.Core.World.DayGroupData? _activeDayGroup;
// Current rain/snow emitter handles — spawned on weather-kind change
// and stopped when the kind leaves Rain/Snow. Non-zero == active.
private int _rainEmitterHandle;
private int _snowEmitterHandle;
private AcDream.Core.World.WeatherKind _lastWeatherKind =
AcDream.Core.World.WeatherKind.Clear;
private double _weatherAccum; private double _weatherAccum;
// F7 / F10 debug-cycle steps for time + weather. Initialized out of // F7 / F10 debug-cycle steps for time + weather. Initialized out of
@ -4229,20 +4223,19 @@ public sealed class GameWindow : IDisposable
Weather.Tick(nowSeconds: _weatherAccum, dayIndex: dayIndex, dtSeconds: (float)deltaSeconds); Weather.Tick(nowSeconds: _weatherAccum, dayIndex: dayIndex, dtSeconds: (float)deltaSeconds);
_weatherAccum += deltaSeconds; _weatherAccum += deltaSeconds;
// Update the rain/snow particle emitters when the weather kind // (Pre-Bug-A code spawned camera-attached rain/snow particle
// changes. Keep the emitters fed by the ParticleSystem tick so // emitters here as a workaround for missing weather-mesh
// visuals stay alive frame-over-frame. // rendering. Deleted 2026-04-26 once the retail-faithful world-
// // space mesh path landed in SkyRenderer.RenderWeather. Retail
// Bug A note (2026-04-26): retail rain is the world-space mesh // rain is GfxObj 0x01004C42/0x01004C44 — a hollow octagonal
// 0x01004C42/0x01004C44 rendered in SkyRenderer.RenderWeather // cylinder anchored at player_pos + (0, 0, -120m) per
// AFTER the scene — see docs/research/2026-04-26-sky-investigation-handoff.md. // GameSky::UpdatePosition at 0x00506dd0 — drawn after the
// The camera-attached emitter path here is acdream's old // landblock pass per LScape::draw at 0x00506330. There is no
// pre-research workaround (broken alpha fade, fixed disk around // server-driven weather event and no camera-attached emitter
// camera). It's kept gated behind ACDREAM_FAKE_RAIN_PARTICLES=1 // in retail. Snow renders identically when a Snowy DayGroup is
// so the retail-faithful path can be A/B-tested against it // active in some other Region; the partition by Properties&0x04
// without uninstalling the legacy code outright. // and the SkyRenderer.RenderWeather pass both pick up snow
if (System.Environment.GetEnvironmentVariable("ACDREAM_FAKE_RAIN_PARTICLES") == "1") // weather meshes for free.)
UpdateWeatherParticles(atmo);
// Phase E.3: advance live particle emitters AFTER animation tick // Phase E.3: advance live particle emitters AFTER animation tick
// so emitters spawned by hooks fired this frame get integrated. // so emitters spawned by hooks fired this frame get integrated.
@ -5241,114 +5234,6 @@ public sealed class GameWindow : IDisposable
} }
} }
/// <summary>
/// Keep the rain/snow camera-anchored emitters aligned with the
/// current weather state. Spawns on entry, stops on exit, with no
/// per-frame churn while the state is stable. Emitters are camera-
/// local (<see cref="AcDream.Core.Vfx.EmitterFlags.AttachLocal"/>)
/// so walking never leaves the rain volume (r12 §7).
/// </summary>
private void UpdateWeatherParticles(in AcDream.Core.World.AtmosphereSnapshot atmo)
{
if (_particleSystem is null) return;
if (atmo.Kind == _lastWeatherKind) return; // no change
// Stop any existing emitters first.
if (_rainEmitterHandle != 0)
{
_particleSystem.StopEmitter(_rainEmitterHandle, fadeOut: true);
_rainEmitterHandle = 0;
}
if (_snowEmitterHandle != 0)
{
_particleSystem.StopEmitter(_snowEmitterHandle, fadeOut: true);
_snowEmitterHandle = 0;
}
// Anchor at camera world position; AttachLocal keeps it moving.
var anchor = System.Numerics.Vector3.Zero;
if (_cameraController is not null)
{
System.Numerics.Matrix4x4.Invert(_cameraController.Active.View, out var inv);
anchor = new System.Numerics.Vector3(inv.M41, inv.M42, inv.M43);
}
switch (atmo.Kind)
{
case AcDream.Core.World.WeatherKind.Rain:
case AcDream.Core.World.WeatherKind.Storm:
_rainEmitterHandle = _particleSystem.SpawnEmitter(
BuildRainDesc(), anchor);
break;
case AcDream.Core.World.WeatherKind.Snow:
_snowEmitterHandle = _particleSystem.SpawnEmitter(
BuildSnowDesc(), anchor);
break;
}
_lastWeatherKind = atmo.Kind;
}
/// <summary>
/// Rain emitter tuned per r12 §7: streaks falling at ~50 m/s with
/// a slight wind bias, 500 drops/sec, 2000 max alive, 1.2s life so
/// drops cover the ~60m fall at terminal velocity.
/// </summary>
private static AcDream.Core.Vfx.EmitterDesc BuildRainDesc() => new()
{
DatId = 0xFFFF_0001u, // synthetic id
Type = AcDream.Core.Vfx.ParticleType.LocalVelocity,
Flags = AcDream.Core.Vfx.EmitterFlags.AttachLocal |
AcDream.Core.Vfx.EmitterFlags.Billboard,
EmitRate = 500f,
MaxParticles = 2000,
LifetimeMin = 1.0f,
LifetimeMax = 1.4f,
OffsetDir = new System.Numerics.Vector3(0, 0, 1),
MinOffset = 0f,
MaxOffset = 50f,
SpawnDiskRadius = 15f,
InitialVelocity = new System.Numerics.Vector3(0.5f, 0f, -50f),
VelocityJitter = 2f,
Gravity = System.Numerics.Vector3.Zero,
StartColorArgb = 0x40B0C0E0u,
EndColorArgb = 0x20B0C0E0u,
StartAlpha = 0.3f,
EndAlpha = 0f,
StartSize = 0.05f,
EndSize = 0.05f,
};
/// <summary>
/// Snow emitter tuned per r12 §8: slow fall at ~2 m/s, tumbling
/// sideways drift, small billboards, 100 flakes/sec, long lifespan.
/// </summary>
private static AcDream.Core.Vfx.EmitterDesc BuildSnowDesc() => new()
{
DatId = 0xFFFF_0002u,
Type = AcDream.Core.Vfx.ParticleType.LocalVelocity,
Flags = AcDream.Core.Vfx.EmitterFlags.AttachLocal |
AcDream.Core.Vfx.EmitterFlags.Billboard,
EmitRate = 100f,
MaxParticles = 1000,
LifetimeMin = 4f,
LifetimeMax = 8f,
OffsetDir = new System.Numerics.Vector3(0, 0, 1),
MinOffset = 0f,
MaxOffset = 30f,
SpawnDiskRadius = 15f,
InitialVelocity = new System.Numerics.Vector3(0.3f, 0.2f, -2f),
VelocityJitter = 0.8f,
Gravity = System.Numerics.Vector3.Zero,
StartColorArgb = 0xE0FFFFFFu,
EndColorArgb = 0x80FFFFFFu,
StartAlpha = 0.85f,
EndAlpha = 0.3f,
StartSize = 0.08f,
EndSize = 0.06f,
};
// ── Phase I.2 — DebugPanel helpers ──────────────────────────────── // ── Phase I.2 — DebugPanel helpers ────────────────────────────────
// //
// The ImGui DebugPanel reads through DebugVM closures that ask // The ImGui DebugPanel reads through DebugVM closures that ask