-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathweather.go
More file actions
155 lines (139 loc) · 5.36 KB
/
weather.go
File metadata and controls
155 lines (139 loc) · 5.36 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
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.1.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() {
m.cfg = loadConfig(m.plug)
if !m.cfg.Enabled {
return
}
m.plug.Callbacks.SetOnSave(m.onSave)
events.RegisterListener(events.NewRound{}, m.onNewRound)
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)
}
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.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 {
engine.Reconcile(m.state.Weather)
if m.seasonsOn {
m.zoneSeasons = seasons.ZoneSeasons(m.graph, m.climate, m.tracks, engine.CalendarNow())
engine.ReconcileSeasons(m.graph, m.zoneSeasons)
}
}
m.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)
}