Warcraft III Weather
Contract
Warcraft III weather is a map/JASS-owned presentation effect. The authoritative
visual definition is TerrainArt\Weather.slk; a weather rawcode such as RAhr
selects one SLK row. It is not an MDX model entity and it does not change
pathfinding, fog-of-war simulation, combat, or unit state.
OpenRealm has three sources of weather instances:
war3map.w3iglobalweatherID-> one enabled effect coveringCM_GetWorldBounds();war3map.w3rv5 region weather IDs -> one enabled effect per authored weather region;- JASS
AddWeatherEffect(rect, id)-> one disabled handle which becomes visible afterEnableWeatherEffect(handle, true)and is destroyed byRemoveWeatherEffect(handle).
The explicit enable step matches normal Warcraft GUI/JASS usage: creating a weather effect and turning it on are separate operations.
Data Flow
W3I global weather / W3R region weather / JASS weather native
|
v
games/warcraft-3/game/g_weather.c
stable level weather registry
|
| generic reliable GameCommand payload
v
client/cl_parse.c -> refExport_t.GameCommand
|
v
games/warcraft-3/renderer/r_weather.c
|
+-> TerrainArt\\Weather.slk typed row
+-> map-scoped texDir\\texFile.blp texture
+-> shared cparticle_t pool
v
alpha pass
The shared client does not interpret wc3_weather; it forwards opaque game
commands through the selected renderer callback. This follows the game-boundary
rule in docs/architecture/server-selected-effects.md.
W3R Weather Fields
games/warcraft-3/common/world_w3.c reads the version-5 war3map.w3r layout.
Only bounds and non-zero weather rawcodes are retained because the region name,
ambient-sound string, editor ID, and colour are not needed by the weather
renderer.
A v5 disk record is:
float left, bottom, right, top
cstring name
int region_id
fourcc weather_id
cstring ambient_sound
byte blue, green, red, alpha/reserved
The parser validates version 5 and bounds the allocation from the remaining
file size before reading map-controlled counts. Format reference:
ChiefOfGxBxL/WC3MapSpecification Regions/5.md.
Weather.slk
The renderer owns a typed Weather.slk schema because these rows are
presentation data, not gameplay unit metadata. It first tries the map-scoped
TerrainArt\Weather.slk, then falls back to the base game file. This preserves
map-import overrides without adding WC3 paths to shared renderer code.
texFile is the Warcraft texture basename rather than a filename with an extension; the renderer appends .blp after combining it with texDir.
The schema includes:
effectID,texDir,texFile;alphaMode,useFog;height,angx,angy;emrate,lifespan,particles;veloc,accel,var;texr,texc;head,tail,taillen;lati,long,midTime;- start/mid/end RGB, alpha, and scale;
- head/tail UV stage fields;
AmbientSound,version.
Representative shipped rain (RAhr) uses rainTail, a negative velocity, a
short lifespan, and tail=1; do not hard-code those values. Community extracts
of Blizzard's Weather.slk document the column names and shipped values; the
map's SLK remains authoritative at runtime.
Particle Rendering
r_weather.c emits only inside the intersection of the weather rectangle and a
camera-local world window. Large map-wide weather therefore does not allocate
particles across the entire map. Emission accrues the authored emrate across
frame time. Retail tracing confirms that emrate is a per-second rate for each
weather emitter. OpenRealm applies that rate directly to its single renderer
effect; retail's multiple emitter fan-out is not yet reconstructed. The
authored particles count caps logical weather particles per effect.
Spawn height is terrain height plus the authored height. angx/angy rotate
the base vertical direction, veloc supplies signed speed, var applies the
same symmetric per-particle speed variation used by WC3 ParticleEmitter2, and
lati spreads each particle inside a cone around that authored direction.
accel changes speed along the selected particle direction. Weather uses
renderer-local xorshift state rather than gameplay RNG.
The shared particle representation has an optional generic world-space tail
vector. A zero vector keeps the camera-facing billboard path. A non-zero
vector renders a camera-facing quad from position - tail to position; WC3
weather sets that vector from velocity * taillen. head=1, tail=1 now
creates both primitives from the same spawned weather particle state instead of
discarding the authored head.
Start/mid/end RGB/alpha and scale reuse the shared particle interpolation.
Weather also supplies separate head/tail hUV* / tUV* atlas-frame curves,
interpolated across the same authored midTime; ordinary shared particles keep
the existing full-lifetime atlas animation unless they opt into that curve.
OpenRealm currently multiplies weather texture and particle color and uses
ordinary source-alpha blending; it parses alphaMode but does not map its
values. A retail trace of Prologue01's RLhr (alphaMode=0) confirms those
parts of the path and also reveals a retail alpha-test cutoff that OpenRealm
does not currently reproduce. See Retail Rain Alpha for
the evidence, parity gap, and reproducible probe. Behavior for other
alphaMode values remains unverified.
Retail Rain Alpha
Confirmed Prologue01 path
The evidence chain starts in the retail map. Prologue01.w3m is in
War3Local.mpq; its war3map.j creates and enables RLhr over
Starting_Area_Rain near map initialization. The War3.mpq
TerrainArt\\Weather.slk row authors alphaMode=0, white RGB, and
alphaStart=alphaMid=alphaEnd=150. That row names
ReplaceableTextures\\Weather\\rainTail.blp, a 32x16 BLP1 image whose source
header declares an 8-bit alpha channel.
A read-only Frida trace of the retail OpenGL draw path observed the rain batch with these values:
| State at the draw | Observed value | Meaning |
|---|---|---|
| Vertex color | (255, 255, 255, 150) |
White particle color, authored alpha 150/255 (about 0.588) |
| Texture environment | GL_MODULATE (8448) |
Texture RGBA modulates the vertex RGBA |
| Texture | 32x16, GL_TEXTURE_ALPHA_SIZE=4 |
The bound rain-sized texture reports 4-bit internal alpha precision |
| Alpha test | enabled, GL_GEQUAL (518), ref 0.0156863 |
Fragment alpha must be at least 4/255 to pass |
| Blend | enabled, GL_SRC_ALPHA (770), GL_ONE_MINUS_SRC_ALPHA (771) |
Standard source-alpha compositing |
| Primitive batch | GL_TRIANGLES, 468 or 474 indices |
Repeated weather draw observed during the cinematic |
For this mode-0 rain path, the effective fragment alpha is the modulated texture
alpha times the particle vertex alpha: A = A_texture * (150/255). The alpha
test discards fragments where A < 4/255. Passing fragments use ordinary
source-alpha compositing: C_out = C_src * A + C_dst * (1-A). The white vertex
RGB leaves the sampled texture RGB unchanged under modulation. The 4-bit
precision is the OpenGL texture object's reported internal alpha size; the BLP
source itself declares 8-bit alpha. The trace records the bound texture object
and its dimensions, not its asset path. Prologue01's active opening effect and
the row's texture dimensions and vertex alpha make the draw a rain-matching
candidate, but the probe does not independently recover or prove the texture
filename.
The controlled Prologue01 scene and matching row data make this a strong
correlation with RLhr (alphaMode=0), but the draw probe does not recover a
texture filename and dimensions plus vertex alpha are not unique identifiers.
Treat the GL state as observed on the rain-matching batch, not as a definitive
asset-to-draw link. It does not establish what alphaMode=1 or other weather
rows do. OpenRealm's weather path already multiplies texture and vertex color
and uses the same
source-alpha blend factors, but it does not apply the observed 4/255 alpha
test. In the implementation, R_WeatherSpawn
selects BLEND_MODE_BLEND; the shared particle shader
discards only for its separate alpha-key mode. The missing 4/255 cutoff is a
confirmed parity gap. The effect of nonzero alphaMode values remains
unverified.
Reproduce the trace
The controller refuses to attach unless the --exe file has SHA-256
3f2ed0120d80578bf07e4423296dade1adfb959d59a2d20a7584224559570eed; it then
attaches to the Warcraft III.exe process on the Frida endpoint. Run the game
from that same file. The setup needs the matching Frida 17.19.0 client/server
pair, Wine, and Xvfb. The trace used Frida's x86_64 Wine server with an ia32
game process. To recreate the client/server install, download
frida-server-17.19.0-windows-x86_64.exe.xz from the
official 17.19.0 release,
then run:
FRIDA_TOOLS="$HOME/.local/share/open-realm/tools"
python3 -m venv "$FRIDA_TOOLS/frida-venv"
"$FRIDA_TOOLS/frida-venv/bin/python" -m pip install 'frida==17.19.0'
mkdir -p "$FRIDA_TOOLS/frida"
xz -dc frida-server-17.19.0-windows-x86_64.exe.xz > "$FRIDA_TOOLS/frida/server.exe"
Extract the original map without rebuilding or modifying its MPQ:
WC3DATA="$PWD/data/warcraft-3"
build/bin/mpqtool -mpq "$WC3DATA/War3Local.mpq" cat Maps/Campaign/Prologue01.w3m > /tmp/Prologue01.w3m
build/bin/mpqtool -mpq /tmp/Prologue01.w3m cat war3map.j | rg -n -C2 'Starting_Area_Rain|AddWeatherEffect'
sha256sum "$WC3DATA/Warcraft III.exe"
build/bin/mpqtool -mpq "$WC3DATA/War3.mpq" cat TerrainArt/Weather.slk | rg -n -A45 'Y1;K"effectID"|Y13;K"RLhr"'
build/bin/mpqtool -mpq "$WC3DATA/War3.mpq" imginfo ReplaceableTextures/Weather/rainTail.blp
The map script identifies the active opening effect; the SLK row supplies its
authored color/alpha and texture path; imginfo confirms the source BLP
dimensions and alpha bits. This separates authored source data from the
runtime GL state observed by the probe.
Use the matching Frida 17.19.0 client/server pair. The server path below is the
local tool installation used for this trace. If display :97 is not already
running, start Xvfb in its own terminal and leave it running:
export WINEPREFIX="$HOME/.local/share/open-realm/wine-wc3"
export DISPLAY=:97
DISPLAY=:97 WAYLAND_DISPLAY= WINEDEBUG=-all \
wine "$HOME/.local/share/open-realm/tools/frida/server.exe" --listen=127.0.0.1:27043
In another terminal on that display, launch the extracted map:
export WINEPREFIX="$HOME/.local/share/open-realm/wine-wc3"
export DISPLAY=:97
WC3DATA="$PWD/data/warcraft-3"
DISPLAY=:97 WAYLAND_DISPLAY= WINEDEBUG=-all wine "$WC3DATA/Warcraft III.exe" -window -graphicsapi OpenGL2 \
-loadfile "$(winepath -w /tmp/Prologue01.w3m)"
Dismiss the chapter screen (send Escape to the focused game with
xdotool key --clearmodifiers Escape if needed) and wait until the opening
rain is visible. Attach for a bounded 10-second capture; the game remains
running after the probe detaches:
WC3DATA="$PWD/data/warcraft-3"
"$HOME/.local/share/open-realm/tools/frida-venv/bin/python" \
tools/frida/trace_wc3_weather_alpha.py --exe "$WC3DATA/Warcraft III.exe" \
--seconds 10 --output /tmp/wc3-weather-alpha.jsonl
The probe hooks opengl32.dll's glColorPointer and glDrawElements, then
queries the current texture dimensions/alpha size, texture environment, alpha
test, and blend state at matching 32x16 rain-texture draws. A rain-draw JSONL
row should report colorArray.sample containing [255,255,255,150],
textureEnvMode=8448, blendSrc=770, blendDst=771,
alphaTestFunc=518, and alphaTestRef near 0.0156863. The script is
read-only and does not call game functions or change OpenGL state.
The GL enum values are recorded numerically so the output is machine-readable:
8448=GL_MODULATE, 770=GL_SRC_ALPHA, 771=GL_ONE_MINUS_SRC_ALPHA, and
518=GL_GEQUAL.
Retail Rain Emission Density
Undead01's JASS in Maps/Campaign/Undead01.w3m (inside War3Local.mpq)
enables RLlr (Lordaeron light rain). The base War3.mpq row has emrate=40,
lifespan=1.1, particles=880, alphaMode=0, alphaStart/Mid/End=150,
head=0, and tail=1. Prologue01's opening uses RLhr, whose row has
emrate=100, lifespan=0.9, and particles=1800.
These commands inspect the installed source data without relying on an extracted working-tree file:
build/bin/mpqtool -mpq "data/warcraft-3/War3Local.mpq" cat Maps/Campaign/Undead01.w3m > /tmp/Undead01.w3m
build/bin/mpqtool -mpq /tmp/Undead01.w3m cat war3map.j | rg -n "RLlr|SetSkyModel"
build/bin/mpqtool -mpq "data/warcraft-3/War3.mpq" cat TerrainArt/Weather.slk | rg -n "RLlr"
The particles / (emrate * lifespan) = 20 relationship in shipped rain rows
initially suggested that retail applied emrate once per 50 ms tick. Retail
executable analysis and a live trace resolve the rate semantics: each emitter
accumulates emrate as a per-second rate. The aggregate rate of a map's whole
weather effect also depends on how many emitters retail creates for its region.
In the hash-verified retail executable
(3f2ed0120d80578bf07e4423296dade1adfb959d59a2d20a7584224559570eed), the
generic emitter update at VA 0x0097d170 loads the rate from this + 0xa8,
multiplies it by the clamped frame delta, and adds that amount to the fractional
emission accumulator at this + 8. The helper at VA 0x009794b0 supplies
1.0, so it adds no tick-rate scale. The update then emits the accumulated
whole particles and keeps the fractional remainder. The weather-specific
particle initializer is at VA 0x0097f1c0.
A read-only Frida trace of the unmodified Prologue01 scene hooked that
initializer and observed 23 active weather emitter objects with rate 100.
Their initializer argument was 0.025 seconds, matching the update's frame
delta: each emitter accrues 100 * 0.025 = 2.5 particle credits per update,
which is 100 particles per second per emitter. The sampled live counts were
mostly 80 or 81 per emitter. Sequential per-object sums were 1,845 and 1,858 in
two captures; these are non-atomic reads, not proof of the exact global cap.
The roughly 1.85K live-particle total is consistent with the row's
particles=1800 value. The 20x row ratio therefore should not be read as an
engine tick rate; Prologue01 has many concurrent weather emitters. This probe
does not establish emitter placement, the rule that chooses emitter count, or
how counts are divided at region edges.
Reproduce the density trace
Launch the hash-verified retail executable on the unmodified Prologue01 map and reach the opening rain scene using the setup in Reproduce the alpha trace. Then attach the read-only rate probe:
WC3DATA="$PWD/data/warcraft-3"
"$HOME/.local/share/open-realm/tools/frida-venv/bin/python" \
tools/frida/trace_wc3_weather_density.py --exe "$WC3DATA/Warcraft III.exe" \
--seconds 10 --rate 100 --output /tmp/wc3-weather-density.jsonl
The controller checks the executable SHA-256 and bounds the capture. The probe
hooks the weather initializer at RVA 0x57f1c0; each matching JSONL row reports
the emitter's rate at this + 0xa8, frame delta, and live count. The companion
Ghidra check is the rate-times-delta sequence in ParticleEmitter_Update at
VA 0x0097d170.
This confirms the rate semantics for RLhr and the shared retail weather
emitter path. It strongly supports the same interpretation for RLlr, though
that row has not had its own runtime capture. Nominal single-emitter rates are
100 * 0.9 = 90 live particles for RLhr and 40 * 1.1 = 44 for RLlr, before
effect-wide limits and emitter multiplicity. OpenRealm intentionally uses the
confirmed per-emitter rate without guessing the number or placement of retail
emitters; aggregate density can therefore be lower than retail until that
fan-out is reconstructed.
RLlr also authors alphaMode=0 and alpha 150. The live alpha trace above used
Prologue01's RLhr; the RLlr row has not had a separate runtime capture.
The shipped RLlr row has head=0,tail=1; the renderer draws its tail
primitive. Rows with both flags clear have no authored primitive and are
skipped with a diagnostic instead of being silently converted to a head.
Implementation status: OpenRealm uses the authored per-second emrate
directly and enforces particles as a logical live-particle cap per weather
effect. It does not yet reproduce retail's multiple spatial weather emitters,
so aggregate density can be low. Do not restore a fixed ×20 multiplier: the
Prologue01 trace establishes 23 emitters in that scene, not a universal
fan-out rule or an exact effect-wide cap. An RLlr runtime capture remains
outstanding. Keep those questions separate from the confirmed mode-0 alpha
path above; alphaMode values other than zero remain unverified.
Lifecycle And Networking
level.weather_effects[] owns stable weather handles for the map lifetime.
JASS gets light handles to those slots; renderer objects are not JASS objects.
Each svc_frame carries the complete registered weather set in its game-owned
datagram. The client caches that set in client_state and passes it through
viewDef; the renderer reconciles its particle runtime while rendering, so
packet loss and reconnects converge without a renderer command API.
Map-authored W3I/W3R weather starts enabled. JASS-created weather starts
disabled until EnableWeatherEffect(..., true).
The fixed weather registry and next_weather_id are part of WC3 save format
version 10. weathereffect JASS values snapshot as stable registry-slot
indexes, and load/reconnect replays the restored authoritative set to the
renderer. See Save/Load.
Disabling/removing stops new emission. Already-spawned cosmetic particles are allowed to expire by their authored lifespan rather than being converted into networked state.
Deliberate Gaps
The initial renderer intentionally does not guess behavior that was not confirmed strongly enough:
useFogis parsed but is not mapped to a new weather-specific fog policy; the existing shared particle FOW behavior remains unchanged;longis parsed but not yet used because its exact legacy emitter-extent transform is not verified;AmbientSoundis parsed but weather ambient audio is not yet wired throughAmbienceSounds.slk;- the camera-local emission window is a bounded renderer performance policy; the exact retail camera footprint and weather-rectangle edge behavior are not claimed yet;
angx/angycurrently use the conventional vertical-vector X/Y rotation; exact retail sign/order quirks have not been claimed;- overlapping weather effects are independently composited; retail edge/fade quirks have not been claimed;
- only
war3map.w3rversion 5 is accepted by the new region-weather parser.
These are compatibility follow-ups, not reasons to put guesses into the current renderer.
Verification
In-engine tests in games/warcraft-3/game/tests/t_api.c cover:
- JASS add/enable preserving rawcode and rectangle;
- JASS remove releasing the runtime slot;
- W3I global and W3R-derived region weather starting enabled;
- weather light-handle save-codec identity and a full save/load round trip of rawcode, rectangle, enable state, handle ID, and JASS global identity.
For visual verification, use a map with shipped heavy/light rain and test:
- global map weather;
- a rectangular weather region while panning across its edge;
- JASS create -> enable -> disable -> re-enable -> remove;
- a map-imported
TerrainArt\Weather.slk/rain texture override; - different frame rates to confirm emission speed/density remains stable;
- a custom
Weather.slkrow with non-zerovar/latiand distinct head/tail UV stages, includinghead=1,tail=1, to confirm the two primitives share motion while selecting their independent atlas curves.
The renderer-level test-mdx-ui suite directly exercises weather emission,
alpha, tail geometry, and rejection of a row with neither primitive flag.
Do not add weather debug logging to do this; use the authored-map cases above as the acceptance checks.