User-observed regression 2026-04-23: acdream spawned rain particles when retail showed no rain at the same server tick. Root cause: my Phase 3e shortcut mapped DayGroup.Name = "Rainy" → WeatherKind.Rain → rain particle emitter. That's not what retail does. Parallel decompile research confirms: - Agent A (2026-04-23-physicsscript.md): PhysicsScript runtime lives at FUN_0051bed0 → FUN_0051bfb0, runs per PhysicsObj; sky calls it from NOWHERE. - Agent B (2026-04-23-sky-pes-wiring.md): FUN_00508010 (sky render loop) never reads SkyObject.DefaultPesObjectId — the field is dead at render time. Rain/snow particles in retail come from a separate camera-attached weather subsystem that has NOT yet been located. So the correct behavior is: DayGroup name should only drive fog/ambient tone (via keyframes, already in the Snapshot path), never spawn particle emitters. Any retail-faithful particle rain belongs to a future phase once we find the camera-attached weather subsystem driver. Change: MapDayGroupNameToKind now maps all weathery substrings (storm/snow/rain/cloud/overcast/dark/fog) → Overcast — fog-only visuals, no particle spawn. Clear names stay Clear. The Rain, Snow, Storm enum values remain and are still accessible via ForceWeather() for debug overrides. Tests updated (WeatherSystemTests): the name→kind theory now expects Overcast for Rainy/Snowy/Stormy variants. Also commits the four research docs from this session's parallel hunt: PhysicsScript dat+runtime, sky↔PES wiring (negative finding), lightning timer (negative finding — agent #3), fog on sky (positive: retail applies fog to sky geometry). NOTE on lightning: agent #3's research only ruled out the CLIENT-SIDE RANDOM TIMER hypothesis for lightning. User confirms retail does have visible lightning + thunder. A follow-up agent (#5, in flight as of this commit) is hunting the real mechanism — PlayScript opcode, SetLight PhysicsScript hooks, AdminEnvirons side effects, or the weather-volume draw. This commit does NOT attempt to port lightning. Build + 733 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
438 lines
19 KiB
Markdown
438 lines
19 KiB
Markdown
# Lightning Flashes & Weather Crossfade — Decompile Research
|
||
|
||
**Date:** 2026-04-23
|
||
**Scope:** Answer Q1–Q5 of the lightning-crossfade hunt brief.
|
||
**Source tree:** `docs/research/decompiled/chunk_*.c` (688K lines, decompiled retail acclient.exe).
|
||
|
||
---
|
||
|
||
## TL;DR
|
||
|
||
1. **Retail has NO lightning-flash system.** Not a timer, not an RNG modulator, not a
|
||
visual spike. Storm preset 6 sets two fog-color targets (grey 0x969696) and
|
||
toggles the crossfade; that's it. "Flashes" in modern ports are an addition.
|
||
2. **Weather crossfade is driven entirely by `FUN_0055eb40`** (chunk_00550000.c:11835)
|
||
— a 7-way switch on `EnvironChangeType` (param_2). It sets fog-crossfade target
|
||
globals (`DAT_008427ac/b0/b4`, `DAT_00842784/88`), sets `DAT_008427a9 = 1`
|
||
(active), and resets `_DAT_008427b8 = 0` (progress u).
|
||
3. **Crossfade step `_DAT_007c7208`** is a single rdata constant. It's
|
||
added each time the `LightTickSize` gate fires (i.e. per sky-keyframe update,
|
||
default ~2 seconds). Progress saturates at 1.0 (`_DAT_007938b0`).
|
||
4. **AdminEnvirons (0xEA60 = 60000) arrives via `FUN_006ae870`**
|
||
(chunk_006A0000.c:13141) and unconditionally calls `FUN_0055eb40` with the
|
||
EnvironChangeType int. No auth check, no queueing.
|
||
5. **Thunder audio (0x76..0x7B)** is driven by AdminEnvirons subtypes **0x65..0x6A**
|
||
(chunk_00550000.c:11906-11994) — each calls `FUN_00551560(soundId, chanId)` ONCE.
|
||
No timer. Not auto-linked to storm preset.
|
||
|
||
---
|
||
|
||
## Q1: Lightning flash trigger — NOT PRESENT in retail
|
||
|
||
### What I searched
|
||
- `rand()` in chunk_00500000.c (sky): **3 hits, all inside `FUN_00501600` RNG-looking
|
||
macros → actually a byte-shuffle for ARGB color lerping** (`FUN_005df4c4`), not RNG.
|
||
- `rand()` in chunk_00550000.c (weather): **3 hits (lines 646, 1074, 1102) — all
|
||
sound-probability filters in `FUN_00550cf0/FUN_00551430/FUN_005514c0`**, used by
|
||
ambient-sound emission, not lightning.
|
||
- `rand()` in chunk_005B0000.c:3176-3189 — 256-entry palette shuffle init, unrelated.
|
||
- `rand()` in chunk_005C0000.c:5560-5668 — 4 particle-emitter time-jitter seeds,
|
||
unrelated.
|
||
- `fsinf`/`fcosf` in the sky-keyframe path — only used for sun-direction polar-to-
|
||
cartesian conversion (`FUN_00501600:1193-1205`). No other time-based trig.
|
||
- String literals `"lightning"|"Lightning"|"thunder"|"Thunder"|"LIGHTNING"|"THUNDER"`:
|
||
**one hit, unrelated** (chunk_004B0000.c:2283 = a character-skill UI string
|
||
`"ID_CharacterInfo_Augmentation_Resist_Lightning"`).
|
||
- Storm preset 6 in `FUN_0055eb40` — sets `*(iVar2 + 0x41) = 1` on the singleton
|
||
`DAT_00871354` (via `FUN_00564d30`). I grepped for READS of `+0x41` across the
|
||
entire decompile: **there are NONE** outside the singleton's own ctor/reset
|
||
paths (chunk_00560000.c:2902, 3105 — both writes of 0). **The storm flag is
|
||
write-only; no lightning tick consumes it.**
|
||
|
||
### Storm preset 6 body — chunk_00550000.c:11885-11896
|
||
|
||
```c
|
||
if (param_2 == 6) {
|
||
DAT_008427a9 = 1; // crossfade active
|
||
_DAT_008427b8 = 0; // progress u
|
||
DAT_008427ac = 0x3f4ccccd; // = 0.8f (target fogStart)
|
||
DAT_008427b0 = 0; // target secondary-1 (fogNear)
|
||
DAT_008427b4 = 0x42200000; // = 40.0f (target fogFar)
|
||
DAT_00842788 = 0x64969696; // target fog color ARGB: A=0x64 R=G=B=0x96 grey
|
||
DAT_00842784 = 0x64000000; // target secondary color: A=0x64 RGB=black
|
||
iVar2 = FUN_00564d30(); // get weather-mgr singleton
|
||
*(undefined1 *)(iVar2 + 0x41) = 1; // storm flag (NEVER READ elsewhere)
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
### Conclusion
|
||
|
||
The retail acclient **does not implement lightning flashes**. Storm preset 6 is
|
||
visually indistinguishable from other fog-change presets except by color and
|
||
the unread `+0x41` storm flag. The "client-side random flash" described in the
|
||
r12 deepdive is either:
|
||
(a) a later/expansion feature not present in the decompiled build, or
|
||
(b) a modern-port embellishment.
|
||
|
||
If acdream wants lightning, it's an **addition**, not a port. A faithful retail
|
||
render is pure dense grey fog during thunderstorm.
|
||
|
||
---
|
||
|
||
## Q2: Weather / DayGroup crossfade mechanics
|
||
|
||
### State variables (all in the 0x842780 cluster)
|
||
|
||
| Global | Type | Init | Role |
|
||
|---|---|---|---|
|
||
| `DAT_008427a9` | byte | 0 | **Crossfade active flag** — true = blend keyframe output toward stored weather values |
|
||
| `_DAT_008427b8` | float | 0.0 | **Crossfade progress `u`** — 0 at start, saturates at 1.0 |
|
||
| `DAT_008427ac` | float | — | **Target fogStart** (weather override) |
|
||
| `DAT_008427b0` | float | — | **Target fogNear** (secondary/starfield override) |
|
||
| `DAT_008427b4` | float | — | **Target fogFar** (secondary/starfield override) |
|
||
| `DAT_00842788` | u32 ARGB | — | **Target primary fog color** (pair with `DAT_008427ac`) |
|
||
| `DAT_00842784` | u32 ARGB | — | **Target secondary color** (pair with `DAT_008427b0/b4`) |
|
||
| `_DAT_007c7208` | float | **.rdata constant** (value unknown in decompile; see below) | **Per-tick progress step** |
|
||
| `_DAT_007938b0` | float | 1.0 (confirmed by division usage across chunk_00440000/00450000) | Upper saturation for `u` |
|
||
|
||
### Per-frame crossfade block — chunk_00500000.c:6256-6281 (primary channel)
|
||
|
||
```c
|
||
if (DAT_008427a9 != '\0') {
|
||
if (_DAT_007938b0 <= _DAT_008427b8) { // u >= 1.0: snap
|
||
local_24 = DAT_008427ac; // fogStart = target
|
||
local_20 = DAT_00842788; // fogColor ARGB = target
|
||
}
|
||
else {
|
||
// Per-byte lerp on fog color (R, G, B, A individually):
|
||
// new = current - (current - target) * u [applied to each byte]
|
||
// -- FUN_005df4c4 is the byte clamp/pack helper --
|
||
... // 4 byte channels
|
||
local_24 = local_24 - (local_24 - DAT_008427ac) * _DAT_008427b8; // fogStart lerp
|
||
_DAT_008427b8 = _DAT_008427b8 + _DAT_007c7208; // advance u
|
||
}
|
||
}
|
||
FUN_00505f30(local_24, local_20, local_c, local_18);
|
||
```
|
||
|
||
### Per-frame crossfade block — chunk_00500000.c:6297-6324 (secondary / starfield)
|
||
|
||
Same structure, but writes `local_1c` (fogNear) ← `DAT_008427b0`, `local_24`
|
||
(fogFar) ← `DAT_008427b4`, `local_20` (color) ← `DAT_00842784`. Progress `u` is
|
||
the SAME global `_DAT_008427b8` — so both channels advance in lockstep.
|
||
|
||
### Important: the crossfade step gate
|
||
|
||
`_DAT_008427b8 += _DAT_007c7208` runs ONLY when the outer "LightTickSize" gate fires
|
||
(chunk_00500000.c:6249 `if (_DAT_008427a0 < _DAT_008379a8)`). This gate reschedules
|
||
using `*(double *)(*(int *)(DAT_0084247c + 0x50) + 0x10)` = SkyDesc.LightTickSize.
|
||
Based on the ACE schema (SkyDesc.LightTickSize in the Region dat), this is
|
||
typically 2.0 seconds.
|
||
|
||
**Duration of a crossfade**: if `_DAT_007c7208` is, say, 1/30 (0.033), then
|
||
crossfade completes in 30 light-ticks × 2s = 60 seconds. If it's 1/8 = 0.125,
|
||
it's 16 seconds. If it's 1.0, it's 2 seconds (instant within one keyframe step).
|
||
The literal value is in .rdata at offset 0x007c7208 and isn't visible in the
|
||
decompile — acdream should either (a) start with a tuning-chosen constant
|
||
(e.g. 0.1 for 20 s fade) and expose it as a config, or (b) disassemble the
|
||
retail binary's .rdata to get the ground-truth value.
|
||
|
||
### Note on retail client behavior
|
||
|
||
Because the crossfade step advances at the LightTickSize cadence (not per-frame),
|
||
retail's weather change visibly "steps" in ~2-second increments rather than
|
||
appearing silky smooth at 60 fps. This matches the known retail look —
|
||
"the sky is updating in chunks" rather than continuously.
|
||
|
||
---
|
||
|
||
## Q3: AdminEnvirons (0xEA60 = 60000) handler
|
||
|
||
### Dispatcher — chunk_006A0000.c:13141-13153
|
||
|
||
```c
|
||
undefined4 FUN_006ae870(int param_1, int *param_2)
|
||
{
|
||
undefined4 uVar1;
|
||
if (((param_1 != 0) && (*(int *)(param_1 + 0x40) != 0)) && (*param_2 == 60000)) {
|
||
uVar1 = FUN_0055eb40(param_2[1]); // param_2[1] = EnvironChangeType (int)
|
||
return uVar1;
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
Wire format: `[u32 opcode=0xEA60][u32 environChangeType]` — just a single int payload.
|
||
|
||
### `FUN_0055eb40` — EnvironChangeType dispatcher (chunk_00550000.c:11839)
|
||
|
||
| EnvChangeType | Action | Crossfade? |
|
||
|---|---|---|
|
||
| 0 (Clear) | Zero all targets; set 008427a9 = 0 (crossfade OFF) | N (off) |
|
||
| 1 (RedFog / preset 1) | fogStart 0.4, fogFar 50, color 0x64_R_96_00 | Y |
|
||
| 2 (preset 2) | fogStart 0.3, fogFar 50, color 0x64_32_00_96 | Y |
|
||
| 3 (preset 3) | fogStart 0.4, fogFar 30, color 0x64_64_64_64 (grey) | Y |
|
||
| 4 (preset 4) | fogStart 0.3, fogFar 50, color 0x64_1E_64_00 | Y |
|
||
| 5 (preset 5) | fogStart 0.8, fogFar 40, color 0x64_96_96_96 | Y |
|
||
| **6 (Storm)** | fogStart 0.8, fogFar 40, color 0x64_96_96_96 + `singleton+0x41 = 1` | Y |
|
||
| 0x65..0x72 | Play thunder/ambient sound via `FUN_00551560(soundId 0x76..0x83, chanObj)` | N (sound only) |
|
||
| 0x75..0x7B | Play thunder/ambient sound (0x84..0x8A) | N (sound only) |
|
||
| 9999 (preset 9999) | fogFar 30, color 0x32_64_64_64, same as preset 3 branch | Y |
|
||
|
||
All "crossfade" branches set `DAT_008427a9 = 1` and `_DAT_008427b8 = 0` via the
|
||
common `LAB_0055f050` tail.
|
||
|
||
The common tail (chunk_00550000.c:12009-12015):
|
||
```c
|
||
DAT_008427a9 = 1;
|
||
LAB_0055f050:
|
||
_DAT_008427b8 = 0;
|
||
DAT_008427b0 = 0; // reset fogNear target
|
||
iVar2 = FUN_00564d30();
|
||
*(undefined1 *)(iVar2 + 0x41) = 0; // clear storm flag
|
||
return 0;
|
||
```
|
||
|
||
### AdminEnvirons → crossfade trigger
|
||
|
||
The server's `AdminEnvirons(EnvironChangeType = 6)` path:
|
||
1. Client wire: opcode 0xEA60 followed by u32=6.
|
||
2. `FUN_006ae870` dispatches on opcode, calls `FUN_0055eb40(6)`.
|
||
3. `FUN_0055eb40` writes the storm targets + sets the crossfade flag.
|
||
4. Next `FUN_005062e0` tick (gated by `LightTickSize`) lerps toward the targets.
|
||
5. Crossfade continues at step `_DAT_007c7208` per tick until `u >= 1`.
|
||
|
||
---
|
||
|
||
## Q4: Thunder sound wiring
|
||
|
||
### Direct, not timer-driven
|
||
|
||
`FUN_00551560(soundId, chanObj)` is the play-sound-now call. `FUN_00564d50(singleton)`
|
||
lazily instantiates the channel object `FUN_00415730(0x10000003, 7, 0x22)` and
|
||
caches it at `singleton + 0x34`. Each EnvironChangeType 0x65..0x6A plays a
|
||
DIFFERENT thunder/ambient sound:
|
||
|
||
| EnvChangeType | soundId | Likely meaning |
|
||
|---|---|---|
|
||
| 0x65 (101) | 0x76 | Thunder1Sound |
|
||
| 0x66 (102) | 0x77 | Thunder2Sound |
|
||
| 0x67 (103) | 0x78 | Thunder3Sound |
|
||
| 0x68 (104) | 0x79 | Thunder4Sound |
|
||
| 0x69 (105) | 0x7A | Thunder5Sound |
|
||
| 0x6A (106) | 0x7B | Thunder6Sound |
|
||
| 0x6B..0x72 (107-114) | 0x7C..0x83 | other ambient sounds |
|
||
| 0x75..0x7B (117-123) | 0x84..0x8A | more ambient sounds |
|
||
|
||
**There is NO periodic "play thunder" call.** The retail client plays thunder
|
||
ONLY when the server sends `AdminEnvirons(0x65..0x6A)`. No client-side RNG
|
||
picks a sound, no tick schedules anything. If the server wants "thunder every
|
||
10-20 seconds during storm", **the server must send it explicitly.**
|
||
|
||
Cross-confirmation: `FUN_00551560(0x76, ...)` appears in the full decompile
|
||
only ONCE (chunk_00550000.c:11909). Every other thunder/ambient sound is also
|
||
a single-site dispatch from `FUN_0055eb40`. There is no storm-active loop.
|
||
|
||
---
|
||
|
||
## Q5: Port-ready C# pseudocode
|
||
|
||
### 1. Crossfade state machine
|
||
|
||
```csharp
|
||
// Source of truth: ACE/retail AC EnvironChangeType enum + the 7 cases of FUN_0055eb40
|
||
// (chunk_00550000.c:11839-12016).
|
||
public enum EnvironChangeType : uint
|
||
{
|
||
Clear = 0,
|
||
// Preset 1..6 are the historical fog presets. Values match FUN_0055eb40 switch.
|
||
Fog1 = 1, // 0x64_B2_96_00-ish, fogStart 0.4, fogFar 50
|
||
Fog2 = 2,
|
||
Fog3 = 3,
|
||
Fog4 = 4,
|
||
Fog5 = 5,
|
||
Storm = 6, // fogStart 0.8, fogFar 40, grey
|
||
Thunder1 = 0x65,
|
||
Thunder2 = 0x65 + 1,
|
||
// ...through 0x7B
|
||
Fog9999 = 9999,
|
||
}
|
||
|
||
internal sealed class WeatherCrossfade
|
||
{
|
||
// Retail globals (DAT_008427a9, DAT_008427ac, DAT_008427b0, DAT_008427b4, _DAT_008427b8,
|
||
// DAT_00842788, DAT_00842784).
|
||
private bool _active;
|
||
private float _progressU;
|
||
private float _targetFogStart;
|
||
private float _targetFogNear;
|
||
private float _targetFogFar;
|
||
private uint _targetFogColorArgb;
|
||
private uint _targetSecondaryArgb;
|
||
|
||
// Retail constant _DAT_007c7208 (.rdata). Per light-tick increment. Literal value is
|
||
// not in the decompile; 0.1 gives ~20s crossfade at default LightTickSize=2s.
|
||
// TODO(acdream): disassemble retail .rdata @ 0x007c7208 to pin the exact value.
|
||
public float ProgressStep { get; set; } = 0.1f;
|
||
|
||
/// <summary>FUN_0055eb40 — server AdminEnvirons(opcode 0xEA60) handler.</summary>
|
||
public void ApplyEnviron(EnvironChangeType type)
|
||
{
|
||
switch (type)
|
||
{
|
||
case EnvironChangeType.Clear:
|
||
_active = false;
|
||
_targetFogStart = 0f;
|
||
_targetFogFar = 0f;
|
||
_targetFogColorArgb = 0;
|
||
_targetSecondaryArgb = 0;
|
||
// fall through to reset tail
|
||
ResetTail();
|
||
return;
|
||
case EnvironChangeType.Fog1:
|
||
_targetFogStart = 0.4f; _targetFogFar = 50f;
|
||
_targetFogColorArgb = 0x64B29600; _targetSecondaryArgb = 0x64B29600; break;
|
||
case EnvironChangeType.Fog2:
|
||
_targetFogStart = 0.3f; _targetFogFar = 50f;
|
||
_targetFogColorArgb = 0x64320096; _targetSecondaryArgb = 0x64320096; break;
|
||
case EnvironChangeType.Fog3:
|
||
_targetFogStart = 0.4f; _targetFogFar = 30f;
|
||
_targetFogColorArgb = 0x64646464; _targetSecondaryArgb = 0x64646464; break;
|
||
case EnvironChangeType.Fog4:
|
||
_targetFogStart = 0.3f; _targetFogFar = 50f;
|
||
_targetFogColorArgb = 0x641E6400; _targetSecondaryArgb = 0x641E6400; break;
|
||
case EnvironChangeType.Fog5:
|
||
_targetFogStart = 0.8f; _targetFogFar = 40f;
|
||
_targetFogColorArgb = 0x64969696; _targetSecondaryArgb = 0x64000000; break;
|
||
case EnvironChangeType.Storm:
|
||
_targetFogStart = 0.8f; _targetFogFar = 40f;
|
||
_targetFogColorArgb = 0x64969696; _targetSecondaryArgb = 0x64000000;
|
||
StormFlag = true; // singleton+0x41; noted but unused by rendering
|
||
break;
|
||
case EnvironChangeType.Fog9999:
|
||
_targetFogStart = 0.4f; _targetFogFar = 30f;
|
||
_targetFogColorArgb = 0x32646464; _targetSecondaryArgb = 0x32646464; break;
|
||
default:
|
||
if ((int)type >= 0x65 && (int)type <= 0x7B) { PlayThunderFor(type); return; }
|
||
return;
|
||
}
|
||
_active = true;
|
||
_progressU = 0f;
|
||
_targetFogNear = 0f;
|
||
}
|
||
|
||
private void ResetTail()
|
||
{
|
||
_progressU = 0f;
|
||
_targetFogNear = 0f;
|
||
StormFlag = false;
|
||
}
|
||
|
||
public bool StormFlag { get; private set; }
|
||
|
||
/// <summary>Called each time the LightTickSize gate fires (~every 2 s).</summary>
|
||
public void AdvanceCrossfade(ref float curFogStart, ref uint curFogColorArgb,
|
||
ref float curFogNear, ref float curFogFar,
|
||
ref uint curSecondaryArgb)
|
||
{
|
||
if (!_active) return;
|
||
if (_progressU >= 1f)
|
||
{
|
||
// snap
|
||
curFogStart = _targetFogStart;
|
||
curFogColorArgb = _targetFogColorArgb;
|
||
curFogNear = _targetFogNear;
|
||
curFogFar = _targetFogFar;
|
||
curSecondaryArgb = _targetSecondaryArgb;
|
||
return;
|
||
}
|
||
curFogStart = curFogStart - (curFogStart - _targetFogStart) * _progressU;
|
||
curFogNear = curFogNear - (curFogNear - _targetFogNear) * _progressU;
|
||
curFogFar = curFogFar - (curFogFar - _targetFogFar) * _progressU;
|
||
curFogColorArgb = LerpArgbBytes(curFogColorArgb, _targetFogColorArgb, _progressU);
|
||
curSecondaryArgb = LerpArgbBytes(curSecondaryArgb, _targetSecondaryArgb, _progressU);
|
||
_progressU += ProgressStep;
|
||
}
|
||
|
||
private static uint LerpArgbBytes(uint a, uint b, float t)
|
||
{
|
||
// matches the per-byte pattern in FUN_005062e0:6262-6277
|
||
byte La(int s) => (byte)((a >> s) & 0xff);
|
||
byte Lb(int s) => (byte)((b >> s) & 0xff);
|
||
byte Lerp(int s) { float d = La(s) - Lb(s); return (byte)(La(s) - d * t); }
|
||
return (uint)(Lerp(0) | (Lerp(8) << 8) | (Lerp(16) << 16) | (Lerp(24) << 24));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. AdminEnvirons → crossfade network binding (F.1 dispatcher)
|
||
|
||
```csharp
|
||
// src/AcDream.Core/Events/GameEventDispatcher.cs (existing pattern from Session 2026-04-18)
|
||
// Opcode 0xEA60 = 60000 = AdminEnvirons.
|
||
// Wire format: [u32 opcode][u32 environChangeType]
|
||
public void OnAdminEnvirons(BinaryReader r)
|
||
{
|
||
uint envType = r.ReadUInt32();
|
||
_world.Weather.ApplyEnviron((EnvironChangeType)envType);
|
||
// If envType is in 0x65..0x7B the above call plays a thunder sound and returns
|
||
// without setting the crossfade.
|
||
}
|
||
```
|
||
|
||
### 3. Thunder sound wiring
|
||
|
||
```csharp
|
||
// chunk_00550000.c:11906-11994 maps AdminEnvirons -> sound.
|
||
// soundId = (int)envType - 0x65 + 0x76 (i.e. 0x65→0x76, 0x66→0x77, ..., 0x72→0x83)
|
||
// second range 0x75..0x7B → 0x84..0x8A
|
||
// Route via the already-shipped OpenAL SoundPlayer (Phase E.2).
|
||
private void PlayThunderFor(EnvironChangeType type)
|
||
{
|
||
int et = (int)type;
|
||
int soundId = et switch
|
||
{
|
||
>= 0x65 and <= 0x72 => et - 0x65 + 0x76,
|
||
>= 0x75 and <= 0x7B => et - 0x75 + 0x84,
|
||
_ => 0,
|
||
};
|
||
if (soundId != 0) _audio.Play2D((uint)soundId);
|
||
}
|
||
```
|
||
|
||
### 4. Lightning flash
|
||
|
||
**Do not port.** Retail has none. If acdream *adds* it as a client-side visual
|
||
enhancement, it should be an explicit new system behind a feature flag — not
|
||
advertised as "matches retail." Document clearly in commit message.
|
||
|
||
---
|
||
|
||
## Citations
|
||
|
||
- `docs/research/decompiled/chunk_00500000.c:6249-6333` — `FUN_005062e0` per-frame sky+crossfade
|
||
- `docs/research/decompiled/chunk_00550000.c:11835-12016` — `FUN_0055eb40` EnvironChangeType dispatcher
|
||
- `docs/research/decompiled/chunk_00550000.c:11906-11994` — thunder/ambient sound cases
|
||
- `docs/research/decompiled/chunk_006A0000.c:13141-13153` — `FUN_006ae870` AdminEnvirons (0xEA60) network handler
|
||
- `docs/research/decompiled/chunk_00560000.c:2461-2467` — `FUN_00564d30` singleton getter for the weather manager
|
||
- `docs/research/decompiled/chunk_00560000.c:2890-2914` — weather-mgr ctor (+0x41 init = 0)
|
||
- `docs/research/decompiled/chunk_00550000.c:1114-1136` — `FUN_00551560` play-sound-by-id utility
|
||
- `docs/research/decompiled/chunk_00500000.c:6280, 6322` — only writers of `_DAT_008427b8 += _DAT_007c7208`
|
||
- `docs/research/decompiled/chunk_00550000.c:11887, 12011` — only other writers of `_DAT_008427b8` (reset to 0)
|
||
|
||
## Gaps / Unresolved
|
||
|
||
1. **`_DAT_007c7208` literal value.** It's an .rdata constant not inlined in any
|
||
decompile site. Acdream should either pick a tuning value (e.g. 0.1 for
|
||
~20 s crossfade at default LightTickSize=2 s) or disassemble the retail
|
||
binary `.rdata` at address 0x007c7208 to pin the exact value.
|
||
2. **Storm flag `singleton+0x41`.** Written to 1 in preset 6, but no reader in
|
||
the full 688K-line decompile. Likely a vestigial/dead field from an earlier
|
||
retail build, or consumed by a debug path that was stripped. Safe to ignore.
|
||
3. **Exact bit-layout of fog-color targets.** The constants like `0x64B29600`
|
||
are given in mixed ARGB/BGRA order in the decompile — the apply-byte-lerp
|
||
at 6262-6277 reads them in the same byte order as the runtime current value,
|
||
so as long as acdream consistently treats them as "retail-native ARGB", the
|
||
lerp math and final D3D state push will match. Validation: compare rendered
|
||
fog color side-by-side with retail under AdminEnvirons 1..5.
|