-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathweather.go
More file actions
168 lines (152 loc) · 6.21 KB
/
Copy pathweather.go
File metadata and controls
168 lines (152 loc) · 6.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package weather
import (
"embed"
"github.com/GoMudEngine/GoMud/internal/events"
"github.com/GoMudEngine/GoMud/internal/mudlog"
"github.com/GoMudEngine/GoMud/internal/plugins"
"github.com/GoMudEngine/GoMud/internal/users"
"github.com/GoMudEngine/GoMud/internal/util"
"github.com/GoMudEngine/GoMud/modules/weather/content"
"github.com/GoMudEngine/GoMud/modules/weather/crawler"
"github.com/GoMudEngine/GoMud/modules/weather/engine"
"github.com/GoMudEngine/GoMud/modules/weather/seasons"
"github.com/GoMudEngine/GoMud/modules/weather/sim"
)
//go:embed files/*
var files embed.FS
// weatherModule holds the plugin handle, resolved config, the geography graph,
// and the live simulation (state/climate/emote tables/schedule). All fields are
// touched only from the single game-loop goroutine — no synchronization needed.
type weatherModule struct {
plug *plugins.Plugin
cfg Config
graph *sim.Graph
started bool
simReady bool // graph + content + state loaded; ticking enabled
simCfg sim.Config
climate sim.Climate
tables content.Tables
seasonalTables content.SeasonalTables // seasonal-ambience emotes (track,season)-keyed
state sim.State
nextTick uint64 // round number when the next weather tick fires
nextEmote uint64 // round number when the next ambient emote pass fires
tracks seasons.Tracks // loaded season tracks (nil/empty = seasons off)
seasonsOn bool // SeasonsEnabled && tracks loaded && calendar usable
zoneSeasons map[sim.ZoneId]seasons.ZoneSeason // previous tick's resolution (event diffing)
lastAdminAction string // most recent admin-page action result (snapshot field)
}
var module weatherModule
func init() {
module = weatherModule{plug: plugins.New(`weather`, `0.2.0`)}
if err := module.plug.AttachFileSystem(files); err != nil {
panic(err)
}
module.plug.Callbacks.SetOnLoad(module.onLoad)
// Command and exports are registered at init: plugins.Load() harvests the
// command map BEFORE invoking onLoad, so anything registered there is lost.
// Behavior (not registration) is gated on cfg.Enabled / simReady.
module.plug.AddUserCommand(`weather`, module.cmdWeather, false, false)
module.registerExports()
module.registerAdminWeb()
}
// onLoad loads config and registers the save hook + NewRound listener. The
// command and exports are registered in init() (plugins.Load harvests the
// command map before onLoad). World crawling and sim startup are deferred to
// the first NewRound (engine-specific onLoad timing vs world load).
func (m *weatherModule) onLoad() {
// The self-heal MUST run before the config read below: if the engine's
// overlay merge clobbered the live Modules.weather block this boot (see
// healConfigClobber), loadConfig would otherwise adopt code defaults —
// including Enabled=true — in place of the operator's settings.
m.healConfigClobber()
m.cfg = loadConfig(m.plug)
if !m.cfg.Enabled {
// One-off snapshot so the always-registered admin page reports the
// truth instead of "module starting" forever. Nothing else ever
// publishes in this branch (no listeners are registered).
adminSnapshot.Store(&AdminSnapshot{LastAction: "module disabled by config (Enabled: false)"})
return
}
m.plug.Callbacks.SetOnSave(m.onSave)
events.RegisterListener(events.NewRound{}, m.onNewRound)
events.RegisterListener(events.RoomChange{}, m.onRoomChange)
events.RegisterListener(WeatherAdminAction{}, m.onAdminAction)
events.RegisterListener(WeatherConfigChanged{}, m.onConfigChanged)
}
// onNewRound drives everything round-based: one-time startup, the jittered
// ambient-emote pass, and the coarse weather tick.
func (m *weatherModule) onNewRound(e events.Event) events.ListenerReturn {
evt, ok := e.(events.NewRound)
if !ok {
return events.Continue
}
if !m.started {
m.started = true
m.loadOrBuildGraph()
m.startSim(evt.RoundNumber)
m.publishSnapshot() // single-publish rule: see publishSnapshot
}
if !m.simReady {
return events.Continue
}
if m.cfg.EmoteMode == EmoteModeModule && evt.RoundNumber >= m.nextEmote {
engine.EmitAmbient(m.state.Weather, m.zoneSeasons, m.tables, m.seasonalTables, util.Rand)
m.scheduleEmote(evt.RoundNumber)
}
if evt.RoundNumber >= m.nextTick {
m.tick(evt.RoundNumber)
}
return events.Continue
}
// loadOrBuildGraph uses the cached graph when present and current, otherwise
// crawls the world and persists the result.
func (m *weatherModule) loadOrBuildGraph() {
if !m.cfg.RebuildGraphOnBoot {
if b, err := m.plug.ReadBytes(engine.CacheIdentifier); err == nil {
if g, ok := engine.DecodeCache(b); ok {
m.graph = g
mudlog.Info("Weather: loaded geography cache",
"zones", len(g.Nodes), "edges", len(g.Edges))
return
}
}
}
m.rebuildGraph()
}
// rebuildGraph crawls the live world, stores the graph, and writes the cache.
func (m *weatherModule) rebuildGraph() {
opts := crawler.DefaultOptions()
opts.IncludeSecretExits = m.cfg.IncludeSecretExits
opts.ExcludeZonePatterns = m.cfg.ExcludeZonePatterns
opts.BuiltAtRound = util.GetRoundCount()
g, err := crawler.Build(engine.NewWorldReader(), opts)
if err != nil {
mudlog.Error("Weather: graph build failed", "error", err)
return
}
m.graph = g
if b, err := g.ToJSON(); err == nil {
if err := m.plug.WriteBytes(engine.CacheIdentifier, b); err != nil {
mudlog.Error("Weather: graph cache write failed", "error", err)
}
}
mudlog.Info("Weather: built geography graph",
"zones", len(g.Nodes), "edges", len(g.Edges), "components", g.Components)
m.startSim(util.GetRoundCount())
if m.simReady {
m.applyWeather()
if m.seasonsOn {
m.zoneSeasons = seasons.ZoneSeasons(m.graph, m.climate, m.tracks, engine.CalendarNow())
engine.ReconcileSeasons(m.graph, m.zoneSeasons)
}
}
// No publishSnapshot here: rebuildGraph is a helper, not an entry point
// (single-publish rule: see publishSnapshot).
}
// sendLine writes one line to a user. It is the ONLY place this module calls the
// engine's SendText, isolating the one upstream-vs-DOGMud divergence: upstream
// GoMud uses SendText(text); the DOGMud fork uses SendText(category, text).
// Backporting to DOGMud is a one-line change here.
func sendLine(user *users.UserRecord, text string) {
user.SendText(text)
}