A weather system for GoMud worlds. Weather forms as discrete, named systems (fronts) that move across a graph of your world's geography, gather or lose strength based on the terrain they cross, and express themselves through GoMud's existing room mutators — room names and descriptions, alerts, light, ambient emotes, and curated, overridable buffs.
A storm forms over the coast, rolls inland across the plains gathering strength, climbs into the mountains where the terrain bleeds it dry, and dissipates on the far side — and players in each zone along the way feel it arrive, pass, and leave.
Built in the same spirit as the GoMud Module Playtest Harness: engine-native, compiled-in, data-driven, and testable in isolation.
Status: M3 complete; S1+S2 seasonal layer complete. Weather works
end-to-end on a stock GoMud world: install, run, and storms travel, rooms
render (storm-wracked), ambient lines play indoors and out, state survives
reboots, and seasonal description lines appear in every outdoor zone on the
stock world. Remaining before a public release: M4 (per-room indoor/biome
variants, Buffs.Overrides, polish, builder guide) and the one-time
module-registry listing. The
design spec
remains the source of truth for scope and architecture; dated status notes in
it record exactly what each milestone shipped.
- A weather simulation at zone granularity. Every zone has exactly one
current weather type (
clear,overcast,rain,storm,fog,snow,blizzard,dust,heatwaveout of the box — the set is open data, not a hardcoded enum). Fronts travel zone-to-zone along exits your world already has. - Biome-aware, in both directions. A zone's biome decides which weather can form there and how likely it is (deserts birth dust, not blizzards), and the terrain a front crosses feeds or saps it (oceans feed storms; mountains bleed them dry, so systems die crossing a range).
- Deterministic and persistent. The simulation core is a pure function over a seeded RNG: the same seed and world replay the same weather, and active fronts + RNG state are saved across reboots. Great for debugging and tests.
- Data-driven presentation. The engine owns weather state; your world owns its voice. Everything players read lives in YAML you can override: mutator specs (room name/description/alert/light/buffs per weather type) and emote tables keyed by weather × biome × indoor/outdoor.
- Zero engine changes. The module compiles in against existing GoMud APIs
(mutators, events, gametime, plugin storage, plugin data files). Nothing in
internal/is patched.
- Not per-room weather. Simulation granularity is the zone. Indoor rooms are not rained on — they get indoor presentation ("rain drums on the roof") — but two outdoor rooms in one zone always share weather.
- Not per-room seasonal variation. S1+S2 ship zone-granularity seasons
(one season per zone, one
season-*mutator per zone); biome-variant seasonal mutators and per-zone track overrides are deferred to a later milestone. - Not a wind/pressure/temperature simulation. No vector fields, no thermodynamics. Weather types carry coarse implications (a blizzard is cold) through the buffs and prose you configure.
- Not a prose author. We ship sensible default text so it works out of the box, but the defaults are meant to be replaced with your world's voice.
- Not a drop-in plugin for a running server. GoMud modules are compiled into the server binary. Installing means adding source and rebuilding — see Installation.
- Not client-side rendering / GMCP. A weather GMCP package is a listed future enhancement, not part of v1.
- Go 1.24+ (go.dev/dl) — the same minimum as the GoMud engine itself; the module needs nothing newer. You don't need to know Go to use the module, but you need the toolchain to build GoMud at all.
- A current upstream GoMud checkout. The module binds to engine features
that exist on upstream
masteras of mid-2026, most importantly plugin-filesystem data loading for mutators (the engine wiresmutators.RegisterFS(plugins.GetPluginRegistry())inmain.go). If your engine predates that, the module's weather mutators never load and every weather change logs a "no mutator spec loaded" warning — the server stays healthy, but rooms won't render weather. - The stock-world content it reuses by default: buff ids 31 (Freezing
Snow) and 33 (Thirsty), and the stock color patterns
gray,blue,mute-dblue,frost,brown,embers. Missing any of these degrades gracefully (see What can break it).
These steps assume you've never built GoMud before.
-
Install Go from go.dev/dl and confirm it works:
go version
-
Get GoMud:
git clone https://github.com/GoMudEngine/GoMud.git cd GoMud -
Install the module through GoMud's module manager (the standard path — the module is listed in the official registry):
go run . module install weatherThis downloads the release archive, verifies its checksum, and extracts it to
modules/weather/. You'll be asked to confirm a third-party install (the module is community-authored, not by the GoMud team). -
Register modules and build. From the GoMud checkout root:
go generate ./... # regenerates modules/all-modules.go to include weather go buildgo generateis the step people forget: modules are wired in by a generated import file, so without it the module silently isn't in the binary. -
Run the server:
go run . # or run the binary you just built
Connect with any telnet/MUD client to the port in
_datafiles/config.yaml(stock default: 33333).
That's the whole install. Weather is enabled by default
(Modules.weather.Enabled: true ships in the module's config overlay): on the
first game round the module crawls your world's zones and exits into a
geography graph, caches it, seeds the simulation from your zone names (stable
per world), and starts ticking once per game hour. No data authoring, no room
tagging, no world prep.
mutators.LoadDataFiles() loadedCount=24 ← stock 10 + our 8 weather + 6 season specs
Weather: built geography graph zones=15 edges=10 components=6
Weather: seasons active tracks=2 seasonalZones=N
Weather: fresh simulation state seed=17214436859030717895
On later boots: loaded geography cache and restored simulation state fronts=N instead.
Any player:
| Command | Output |
|---|---|
weather |
The weather in Frostfang is clear. — plus the dominant front and felt intensity when a system covers your zone. |
Admins (and mods granted the weather permission key):
| Command | What it does |
|---|---|
weather status |
Graph summary, active front count, next tick round, emote/buff/persist settings. |
weather zones |
Every zone and its current weather. |
weather fronts |
Active systems: id, type, center zone, intensity, moisture, age. |
weather spawn <type> <zone> [intensity] |
Force a front (e.g. weather spawn storm Frostfang 0.9). Zone names may contain spaces; intensity is an optional trailing number 0..1. |
weather clear [zone] |
Remove all fronts, or every front whose coverage reaches the named zone. |
weather graph [zone] |
A zone's graph neighbors and border weights (crawler spot-check). |
weather rebuild |
Re-crawl the world and rewrite the graph cache (run after adding zones/exits). |
Weather shows up without anyone running commands, of course: room names get a
tag like (raining), descriptions gain a weather line, severe weather adds an
alert banner and dims light, and occupied rooms hear ambient lines every ~20
rounds (indoor rooms get indoor variants).
A crawler walks every room exit once at boot and reduces your world to a
zone-adjacency graph (zone = node, "rooms in A have exits into B" = weighted
edge). A pure, seeded simulation ticks once per game hour: fronts age,
terrain feeds or saps them, they move along edges (wide borders are likelier),
their type drifts toward what the local climate supports, dead ones are
removed, new ones spawn within a budget, and every zone resolves to one
weather type (strong fronts project onto neighboring zones, so a big storm
covers an area, not a point). The engine adapter then makes the world
match: each zone's ZoneConfig.Mutators gets exactly the right weather-*
mutator (the engine merges zone mutators into every room render), and an
emote scheduler voices occupied rooms. State is saved through plugin storage
and reconciled on boot.
S1 ships one feature: climate odds shift with the calendar. Each biome
is bound to a named season track (YAML file); each tick the simulation receives
a season-adjusted climate instead of the flat biome defaults. The shipped tracks
are temperate (winter/spring/summer/autumn) and monsoon (wet/dry). S3 will
add seasonal prose; S1+S2 ship the full mechanical layer. See the
seasons design spec
for full architecture.
S2 adds a seasonal ambience layer in an independent season-* mutator
namespace, reconciled alongside the weather-* layer at boot, each tick, and
after every graph rebuild. Each season-bound zone carries exactly one
season-<track>-<season> mutator while that season is active. Six default specs
ship (season-temperate-winter/spring/summer/autumn, season-monsoon-wet/dry),
each appending one description line to every room in the zone:
Winter holds the land; frost rims every edge and breath hangs in the air.
The shipped defaults are description-only — no buffs, no exits. This is a
deliberate scope decision: zone-wide buffs that persist for an entire ~4-real-day
season are too heavy-handed to ship as defaults (buff 31 deals damage per
trigger; buff 33 is −20 all stats). Transient weather buffs from M3 remain as
shipped; season-long buffs wait for Buffs.Overrides (M4) so worlds opt in
deliberately. The BuffsEnabled: false config toggle covers both namespaces if
you want a zero-buff install.
Builder seam — seasonal exits. To add a winter-only crossing (a frozen river,
a snowed-in pass), create a world-specific override of the season spec and add
an exits: block. The field shape matches the engine's standard MutatorSpec
(verified against pushed_boulder.yaml and internal/exit/exit.go):
# my_world/mutators/season_temperate_winter.yaml (override of the shipped spec)
mutatorid: season-temperate-winter
descriptionmodifier:
behavior: append
text: Winter holds the land; frost rims every edge and breath hangs in the air.
colorpattern: frost
decayrate: 24 hours
exits:
across the ice:
roomid: 123While season-temperate-winter is active on the zone, players see the exit
across the ice leading to room 123; when the season flips, the exit
disappears. The same exits: field works for weather specs if you want a
storm-only secret passage.
Making an esoteric season (no Go required). Two optional per-season YAML fields let you introduce weather types that are completely absent from a biome's base climate — the "glass rain during the Shattering" pattern from spec §3.1a:
# files/datafiles/seasons/mystic.yaml
track: stillness
seasons:
- name: calm
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- name: shattering # a Broken-Earth-style season
months: [11, 12]
transitionDays: 2
baseWeightScale: 0.0 # suppress ALL normal weather (default 1.0)
weatherWeightAdditions: # absolute weights ADDED — may introduce absent types
glassrain: 8
ashfall: 3
spawnWeightMultiplier: 2.0
influence: { intensityDelta: 0.05 }Then bind the biome in its climate file (track: stillness) and follow the
existing new-weather-type recipe: add files/datafiles/mutators/weather_glassrain.yaml
(room name tag, description, alert, light mod) and
files/datafiles/emotes/glassrain.yaml (ambient lines per biome). That is
the entire authoring checklist — a glass-rain season is pure YAML, no Go.
All knobs live under Modules.weather.*. Defaults ship in this module's
files/data-overlays/config.yaml — to change them, edit that file and
rebuild. Gotcha (inherited from the engine's overlay mechanics): a
Modules.weather: block in your server's config-overrides.yaml will NOT
merge; module config comes from module overlays.
| Key | Default | Meaning |
|---|---|---|
Enabled |
true |
Master switch. Off = module registers nothing but an inert command. |
Seed |
0 |
RNG seed. 0 derives a stable seed from your zone names (same world ⇒ same seed; negative values are treated as 0). |
TickEveryGameHours |
1 |
Simulation cadence in game hours (minimum 1). |
MaxActiveFronts |
8 |
Global front budget (minimum 1). |
SpawnRateScale |
1.0 |
Multiplier on spawn pressure. 0 stops new fronts entirely. |
EmoteMode |
module |
module = we emit ambient lines; tag-only = we stay silent and your room scripts react to the weather mutators/alerts instead. |
EmoteEveryRounds |
20 |
Ambient emote cadence in rounds, jittered ±25% (minimum 5). |
BuffsEnabled |
true |
Apply the curated default buffs carried by weather mutators (blizzard → 31 Freezing Snow, heatwave → 33 Thirsty). false strips buff ids from the weather specs at boot. |
Persist |
true |
Save/restore fronts + RNG across reboots. |
IncludeSecretExits |
true |
Crawler counts secret/locked exits as zone adjacency (weather doesn't care about locks). |
RebuildGraphOnBoot |
false |
Force a fresh crawl each boot instead of using the cache. |
SeasonsEnabled |
true |
Master switch for the seasons layer. false = weather runs exactly as v1 (no climate shifts, no WeatherSeasonChanged events, no GetSeason response). |
Planned but not yet config keys (deferred to M4+): PrevailingWind,
PerRoomRefinement, Buffs.Overrides, ExcludeZonePatterns (the crawler
currently always skips instance_*/ephemeral_* zones).
Everything a builder would want to change is YAML under files/datafiles/,
rebuilt into the binary. No Go required.
- Prose & ambiance —
files/datafiles/emotes/<type>.yaml. Lines are keyed by biome with adefaultfallback, splitoutdoor:/indoor:(indoor never falls back to outdoor — silence beats "rain falls around you" inside a tavern). Add a biome key to give, say, forests their own storm lines. Prefer total control? SetEmoteMode: tag-onlyand react to the weather mutators from your own room scripts. - Room rendering & mechanics —
files/datafiles/mutators/weather_<type>.yaml, standard engineMutatorSpecschema: name tag, description line, alert,lightmod, buff ids. Two rules our validation tests enforce, learned the hard way against the live engine: weather specs must never setrespawnrate(it would fight the orchestrator and prevent cleanup) and never setdecayintoid(the engine'sRemoveinstantly resurrects the decay target — see What can break it).decayratestays: it's the self-heal safety net if the module is disabled mid-storm. - Climate — drop
files/datafiles/climate/<biome>.yamlto override a biome's weather weights, terrain influence, and spawn pressure (schema in the design spec §7.3). Biomes without a file use built-in defaults for the standard biomes (plains/forest/mountain/desert/tundra/swamp/ocean) plus a milddefaultprofile for everything else. Note: an override replaces the biome's profile wholesale — omitted fields become zero, includingspawnWeight. - A new weather type is just data: reference it in a climate file, add
mutators/weather_<type>.yamlandemotes/<type>.yaml. The filename must be the mutator id lowercased with non-alphanumerics as_(engine loader rule): idweather-acid-rain⇒ fileweather_acid_rain.yaml.
Via the plugin export mechanism (plugin.ExportFunction):
GetWeather(zone string) map[string]any—{"type": "storm", "intensity": 0.72}.GetFronts() []map[string]any— active systems.SpawnFront(type, zone string, intensity float64) bool— e.g. a quest that summons a storm.
All are safe to call any time (they return empty-but-valid answers before the sim finishes starting) and run on the engine's single game-loop goroutine.
The module is built to fail soft, but these are the realistic ways a customized world or forked engine changes its behavior. Roughly in order of likelihood:
-
Forking the engine (API drift). The module's only engine-coupled code is the root package and
engine/; the simulation (sim/), crawler, and data layer (content/) compile against nothing of GoMud's. Real example — DOGMud changed one signature: upstreamusers.UserRecord.SendText(text string)vs DOGMud'sSendText(category, text string). That's a compile error, and because every player-facing line in this module flows through one helper (sendLineinweather.go), the backport is a one-line change. That's the pattern to expect from forks: the damage is a compile error in the thin adapter layer, not silent misbehavior — unless the fork changes engine semantics rather than signatures (see #6). -
Reusing or removing the stock buff ids (31, 33). The default specs reference engine buffs by numeric id. If your world deleted buff 31, blizzards just apply nothing (harmless). But if you reassigned id 31 to something else — "Vampiric Frenzy", say — blizzards will cheerfully apply it, with no warning, because an id is all the spec knows. If you've renumbered buffs, set
BuffsEnabled: falseor edit the two specs. -
Replacing the stock color patterns. The specs color their text with
gray,blue,mute-dblue,frost,brown,embersfrom your world'scolor-patterns.yaml. Removing/renaming those names just renders the text uncolored — cosmetic, but easy to miss. -
Claiming the
weather-mutator namespace. All module mutator ids start withweather-, and the module enforces that namespace at runtime: its reconciler removes any liveweather-*zone mutator that doesn't match the simulation's view. If you hand-author a mutator namedweather-eclipseand place it on a zone, the module will strip it within one tick. Use a different prefix for your own mutators. (A duplicate of one of our exact ids is caught at boot — the engine logsduplicate mutator idand keeps the disk version.) -
Biome data the module doesn't know. Zones with no biome, or custom biome ids (
crystalwastes), silently fall back to the milddefaultclimate — weather still works, just blander and less varied. Fix by shipping a climate file per custom biome. Related: indoor detection is a biome-id heuristic (cave,underground,dungeon,indoor,tunnel,sewer). A customcavernbiome is treated as outdoors — players in it would see "rain patters down around you" — until M4's configurable indoor handling, the workaround is using one of the recognized ids or overriding the emote tables. -
Modifying engine internals the module's behavior depends on. The adapter binds to
internal/mutators,internal/rooms(zone configs),internal/gametime, andinternal/events(NewRound). Signature changes show up as compile errors (good). Behavioral changes are subtler — as evidence that these internals genuinely matter, two real upstream behaviors shaped this module during development:MutatorList.Removeinstantly resurrects any mutator whose spec hasdecayintoid(so our specs must not carry it), andplugins.Load()harvests a module's commands before calling itsonLoad(so registration must happen ininit()). A fork that "cleans up" mutator lifecycle or plugin loading can break weather in ways that compile fine. The boot smoke checklist in CONTRIBUTING.md is the quick way to validate a fork. -
Worlds the crawler sees differently than players do. The graph is built from room exits only. Zones reachable solely by teleport, scripted movement, or magic words have no edges — weather never travels to or from them (each island runs independent weather; that's by design for planes, surprising for a teleport-hub world). Zone names matching
instance_*orephemeral_*are skipped entirely. After adding zones or exits, runweather rebuild. -
Aggressive game-time changes. Ticks are scheduled in game hours via the engine's
gametime; emotes in rounds. If you changeRoundSecondsor the game-time calendar so an hour passes very fast or very slow, scaleTickEveryGameHours/EmoteEveryRoundsto taste. (A tick cadence far above a spec'sdecayrateis also safe — the module re-asserts mutators every tick — but between ticks a long-lived storm may briefly flicker as the safety-net decay fires.)
The repo splits along an engine boundary:
sim/ pure simulation core — graph, fronts, climate, Step() (no engine imports)
crawler/ pure geography crawler — exits → zone-adjacency graph (no engine imports)
content/ pure data-file layer — climate + emote YAML parsing (no engine imports)
seasons/ pure season resolver — tracks → effective climate transform (no engine imports)
engine/ the engine adapter — the ONLY package calling internal/* world APIs
weather*.go module root — plugin lifecycle, tick loop, commands, exports
files/ shipped data: config overlay, mutator specs, emote tables, season tracks
Pure packages are tested standalone, no server needed:
go test ./sim/... ./crawler/... ./content/... ./seasons/...(Not go test ./... — the engine-coupled packages only compile inside a GoMud
checkout.) Architecture tests fail the build if a pure package ever imports
internal/*. For the engine-coupled packages, mirror the module into a
checkout and test there:
pwsh scripts/sync-to-checkout.ps1 -Checkout <path-to-GoMud-checkout>
# then, from the checkout:
go test ./modules/weather/...(sync-to-checkout.ps1 is a development tool for iterating on this repo
against a live engine. It is not an installation mechanism — operators should
install through GoMud's module manager as described above.)
CONTRIBUTING.md covers the module/engine ownership boundary, the OOBE
requirement, architecture rules, and the boot smoke-test checklist. Each Go
package carries a context.md describing its responsibilities in detail.
- S3 — Seasonal prose & content: seasonal emote tables, optional
seasonal:weather-variant support,jungle/monsoondefault content, README/builder-guide updates. - M4 — polish & default content: per-room indoor/biome mutator variants,
Buffs.Overrides, full per-biome emote/climate coverage for every stock biome, builder guide, CI. - Registry onboarding — one-time listing so
module install weatherworks.
GPLv3, matching the GoMud engine.