The root weather package is the GoMud plugin entry point. It wires the full
module lifecycle: plugin registration, config, geography-graph management,
weather-simulation tick, ambient emotes, admin/player commands, and the exported
API. It imports internal/* for plugin infrastructure
(plugins/events/users/mudlog/util/rooms); engine-world calls live in engine/;
pure algorithms live in sim/crawler; data-file parsing lives in content/.
All fields of weatherModule are touched only from the single game-loop
goroutine — no synchronization needed.
- weather.go: the
filesembed.FS (//go:embed files/*— the config overlay plusdatafiles/mutator specs and emote tables; the engine loadsmutators/*from it via the plugin registry,contentloaders read the rest).weatherModulestruct (plug, cfg, graph, started, simReady, simCfg, climate, tables, state, nextTick, nextEmote; plustracks seasons.Tracks,seasonsOn bool,zoneSeasons map[sim.ZoneId]seasons.ZoneSeason, andseasonalTables content.SeasonalTables(the standalone seasonal-ambience emote tables) for the seasons layer; andlastAdminAction stringcarrying the most recent admin-page action result for the snapshot).init()→plugins.New+AttachFileSystem+SetOnLoad, then registers theweathercommand as a player command (not admin-only; admin subcommands are gated in-handler), the exports, and the admin web surface (registerAdminWeb()). Command, export, and web registration MUST happen ininit(), notonLoad:plugins.Load()harvests the plugin's command map and admin web surface into the engine registry BEFORE invokingonLoad, so anything registered inonLoadis lost. Behavior is gated oncfg.Enabled/simReadyin-handler instead.onLoad: loads config, then (when enabled) registersSetOnSave, aNewRoundlistener, and the two admin event listeners (WeatherAdminAction,WeatherConfigChanged).onNewRound: one-time startup (loadOrBuildGraph + startSim), the jittered ambient-emote pass (engine.EmitAmbient(m.state.Weather, m.zoneSeasons, m.tables, m.seasonalTables, util.Rand)— the single arbiter; passes nil season maps harmlessly when seasons are off), and the coarse weather tick.loadOrBuildGraph/rebuildGraph: cache-or-crawl;rebuildGraphalso callsstartSim,engine.Reconcile, and (whenseasonsOn) recomputesm.zoneSeasonsand callsengine.ReconcileSeasons(post-rebuild heal — prevents stale-zone seasons surviving a graph rebuild).sendLineis the SOLEuser.SendTextcall site. - weather_events.go: exports
WeatherSeasonChanged{Zone, Track, From, To}— queued on the engine event bus when a zone's resolved season flips. Never emitted on the first (baseline) resolution after boot, so reboots do not replay a flood of events. Other modules listen by importing this type:events.RegisterListener(weather.WeatherSeasonChanged{}, handler). Also defines the two internal admin bridges:WeatherAdminAction{Action, Weather, Zone, Intensity}— queued by HTTP handlers, executed on the game loop through the same paths as the in-game admin commands (spawn/clear/rebuild); andWeatherConfigChanged{Key}— queued after a config write is persisted, causing the game loop to re-read the config and run the changed key's live applier. All three implementevents.EventviaType() string. - weather_tick.go:
startSim(idempotent; graceful degradation — logs once and stays idle when no graph exists).loadContent(climate overrides, weather emote tables, and the seasonal-ambience tables —content.LoadSeasonalEmotesintom.seasonalTables— from the embedded FS, all fail-soft).loadSeasons— fail-soft ladder:SeasonsEnabled: false→ skip; no usable calendar → skip; no/invalid tracks → skip; each rejection leavesseasonsOn = falseso weather runs exactly as v1. On success setsm.seasonsOn = true, stores tracks, and callsseasons.ZoneSeasonsto establish the baselinezoneSeasonsmap, immediately followed byengine.ReconcileSeasons(m.graph, m.zoneSeasons)(boot assert — re-assertsseason-*mutators after a reboot since zone mutators do not survive reboots; no events emitted).loadOrInitState(restore fromengine.DecodeState, orsim.NewState/sim.DeriveSeedon a fresh start).tick— whenseasonsOn, callsseasons.EffectiveClimateto produce the climate input forsim.Step(the seasonsOn gate); then Step →engine.Reconcile(rather than bare Apply so engine-sidedecayratedrift self-corrects within one tick); thenresolveSeasonsifseasonsOn.resolveSeasons— re-resolves all zone seasons and queues aWeatherSeasonChangedevent for each flip since the previous tick; callsengine.ReconcileSeasons(m.graph, zs)after storing the new map (per-tick assert — keepsseason-*mutators live against the specs'decayratesafety net); cross-track-change suppression: season-change events are only emitted whenprev.Track == cur.Track && prev.Season != cur.Season, so a zone whose biome was reassigned by an admin rebuild emits nothing (listeners may assumeFrom/Toare seasons of the same track).persistState(cheap; called per-tick, from onSave, and from every command/export mutation path).onSave(plugins.Save hook).scheduleEmote(±25% jitter so ambience doesn't metronome). - weather_commands.go: bare
weathershows local conditions (player view; includes the dominant front viasim.Covering; whenseasonsOnalso prints the zone's current season). Subcommandszones,fronts,spawn <type> <zone> [intensity],clear [zone],graph [zone],rebuild,status,seasonsare admin/mod-gated viaHasRolePermission("weather", true).spawnandclearcallsim.ForceSpawn/sim.ClearZonesthenengine.Reconcile+persistState.seasons(printSeasons) lists every loaded track with its current season and blend percentage when inside a transition window; reports "off" whenseasonsOnis false. - weather_api.go:
registerExportsexposesGetWeather,GetFronts,SpawnFront,GetSeasonviaplugin.ExportFunction. All four guardsimReadyso callers during boot get empty-but-valid answers. The MainWorker-goroutine guarantee applies to mutating exports (same as commands).SpawnFrontcallsengine.Reconcile+persistState.GetSeason(zone) map[string]anyreturns{"track": string, "season": string, "blend": float64}fromm.zoneSeasons; empty strings when seasons are off, the zone is unknown, or its biome is unbound. - weather_admin.go: the read-side bridge between the game loop and the HTTP
layer.
AdminSnapshotstruct — an immutable deep-copy of module state (SimReady,SeasonsOn,Round,NextTickRound, graph summary,Fronts,Zones,Configrows,LastAction) serialized to JSON for the status endpoint. Package-leveladminSnapshot atomic.Pointer[AdminSnapshot]— written only from the game loop; HTTP handlers callloadSnapshot()to read it and never touch live module fields. Snapshot refresh sites:publishSnapshot()is called at the end ofstartSim, at the end oftick, at the end ofrebuildGraph, and at the end of everyapplyAdminActionandapplyConfigChangeinvocation.configKeyMeta map[string]configKeyApplier— the single source of truth for every public config key: maps each key to its badge text (shown on the page viaconfigRows()) and its optionalLiveApplyfunction (run on the game loop when the key changes).configRows()readsconfigKeyMetato build theConfigslice in the snapshot so the page renders exactly what the module will do.applyAdminAction(WeatherAdminAction)— executes spawn / clear / rebuild on the game loop, mirrors the in-game command paths, then refreshes the snapshot.applyConfigChange(Config, key)— adopts a freshly re-read config, runs the key'sLiveApplyif present and the sim is ready, then refreshes the snapshot.onAdminAction/onConfigChanged— the listener glue wiring the two event types to the appliers; both registered inonLoad, both run on the game loop. - weather_admin_api.go: the HTTP registration and handler layer.
registerAdminWeb()— called frominit()(the harvest rule: the engine harvests admin web surface atplugins.Load(), beforeonLoad, same as commands). Wires:AdminPage("Weather", "weather", "html/admin/weather.html", ...)— thehtmlFileargument is relative todatafiles/(NOTdatafiles/html/admin/as the engine doc comment implies; the loader reads the plugin-FS key verbatim — seeplugins.go:535); the page is served at/admin/weather. Three endpoints:GET weather/status(open to any admin session),POST weather/config(requiresweather.write),POST weather/action(requiresweather.write).RegisterPermissions(weather.write). Handlers are strictly limited to three touches: (1)loadSnapshot()on the atomic pointer (read-only); (2)m.plug.Config.SetinhandleAdminConfig(the engine config layer, which is internally locked); (3)events.AddToQueuein both write handlers (the event queue is thread-safe). Handlers never access any otherweatherModulefield. - weather_config.go:
Configstruct (Enabled, IncludeSecretExits, RebuildGraphOnBoot, Seed, TickEveryGameHours, MaxActiveFronts, SpawnRateScale, EmoteMode, EmoteEveryRounds, BuffsEnabled, Persist,SeasonsEnabled). Keys are flat because plugin config lookup reads flattened scalar leaves.SeasonsEnableddefaultstrue; setting it tofalsecausesloadSeasonsto return immediately, leavingseasonsOn = falseand weather running exactly as v1.buildConfig(getter)(testable, applies defaults and sanity clamps).simConfig()maps module config ontosim.Config.loadConfig(*plugins.Plugin).
internal/plugins, events, users, mudlog, util, rooms(engine, plugin infra).modules/weather/{sim,crawler,engine,content,seasons}.
GoMud runs a single game-loop goroutine (MainWorker) for both NewRound listeners
and command handlers, so weatherModule fields need no synchronization.
Exported functions are invoked on the same goroutine. Designed exception
surface — HTTP handlers: handleAdminStatus, handleAdminConfig, and
handleAdminAction in weather_admin_api.go run on web goroutines outside
MainWorker. They are permitted to touch exactly three things: the
adminSnapshot atomic pointer (read-only via loadSnapshot()), the engine
config layer (via m.plug.Config.Set, which is internally locked), and the
engine event queue (via events.AddToQueue, which is thread-safe). Any access
to other weatherModule fields from a handler is a concurrency bug.
Compiles only inside a GoMud checkout (imports internal/*). weather_config_test.go
covers buildConfig; the registration/command/tick/export paths are verified by
the in-checkout build and a boot smoke test (first-round build → state persist →
reload → tick).
Only user.SendText differs (DOGMud takes a message category). It is isolated in
sendLine — a one-line change to backport. See CONTRIBUTING.md.