aboutsummaryrefslogtreecommitdiff
path: root/cmd/thoi
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /cmd/thoi
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'cmd/thoi')
-rw-r--r--cmd/thoi/main.go26
1 files changed, 21 insertions, 5 deletions
diff --git a/cmd/thoi/main.go b/cmd/thoi/main.go
index a07fbcc..cff6eb6 100644
--- a/cmd/thoi/main.go
+++ b/cmd/thoi/main.go
@@ -42,8 +42,6 @@ func main() {
}
g := game.New(dataDir, &cfg.DefaultColors, &cfg.ConstantColors, cfg.StartupValidation, cfg.StartingRoom)
- g.Ticks.Start(cfg.TickLength)
- defer g.Ticks.Stop()
// The master per-tick subscriber. Registered as engine subscription ID 1,
// the engine fires it before any dynamically-registered combat/aggro/respawn
@@ -63,6 +61,12 @@ func main() {
log.Fatalf("failed to start server: %v", err)
}
g.SetHub(server.Hub())
+ // Start the tick engine only after the hub exists: every tick system reads
+ // g.Hub on the owner goroutine, so SetHub must happen-before the first
+ // tick. Starting earlier left a window (net.NewServer generates its RSA
+ // keys in between) in which a tick could observe a nil hub.
+ g.Ticks.Start(cfg.TickLength)
+ defer g.Ticks.Stop()
// The entire editor API request is one owner task, including file commits,
// runtime migration, undo history and reloads. These callbacks already run
@@ -77,6 +81,9 @@ func main() {
if err != nil {
log.Fatalf("failed to start admin HTTP server: %v", err)
}
+ // The raw file editor must refuse a file whose character/account is
+ // online; wired before ListenAndServe so the first request sees it.
+ adminSrv.SetPlayerFileOnlineCheck(g.AdminPlayerFileOnline)
go func() {
if err := adminSrv.ListenAndServe(); err != nil {
log.Printf("admin HTTP server error: %v", err)
@@ -90,6 +97,9 @@ func main() {
if err != nil {
log.Fatalf("failed to start admin HTTPS server: %v", err)
}
+ // Same online-file refusal as the HTTP editor: both servers expose
+ // the same raw file API.
+ adminSrv.SetPlayerFileOnlineCheck(g.AdminPlayerFileOnline)
go func() {
if err := adminSrv.ListenAndServe(); err != nil {
log.Printf("admin HTTPS server error: %v", err)
@@ -118,9 +128,8 @@ func main() {
// One shutdown request handler: signals and the in-game shutdown
// command both converge on RequestShutdown, which drains work,
// resolves outstanding wagers, saves owned state, then closes the
- // transports. ListenAndServe returns after Shutdown, and main returns
- // normally (no os.Exit; a final-save failure is logged and the exit
- // code reports it via the transport error path).
+ // transports. ListenAndServe returns after Shutdown; main then reports
+ // a failed final save with a non-zero exit (see below).
g.SetTransportStop(server.Shutdown)
go func() {
<-sigCh
@@ -130,4 +139,11 @@ func main() {
if err := server.ListenAndServe(g.HandleSessionOnOwner); err != nil {
log.Fatalf("server error: %v", err)
}
+ // The transports close inside the shutdown sequence, so a clean return
+ // from ListenAndServe is where the final save is known to have finished.
+ // It needs its own report: the transport error path can only say that
+ // the server stopped, never that online state failed to persist.
+ if err := g.ShutdownSaveError(); err != nil {
+ log.Fatalf("shutdown: final save failed: %v", err)
+ }
}