aboutsummaryrefslogtreecommitdiff
path: root/cmd/thoi
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 15:35:52 -0400
committerhistoria <[not public]>2026-09-15 15:35:52 -0400
commit0b0b25222b3131ada79ff78c28eb8bf07d977deb (patch)
tree2ceaef5203595044f66a8f00064cd8e017f8f48d /cmd/thoi
parent396c3facb8287b9a34076cae8f3d7b300d366d99 (diff)
downloadthehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'cmd/thoi')
-rw-r--r--cmd/thoi/main.go36
1 files changed, 29 insertions, 7 deletions
diff --git a/cmd/thoi/main.go b/cmd/thoi/main.go
index cc4785d..2031cb6 100644
--- a/cmd/thoi/main.go
+++ b/cmd/thoi/main.go
@@ -51,7 +51,9 @@ func main() {
// body — the canonical per-tick system sequence — is enforced and tested in
// internal/game/tick_systems.go and documented in
// building_guide/tick_order.md. Reordering it is a breaking change.
- g.Ticks.Subscribe(1, func() bool {
+ // SubscribeTicks executes the body on the game's owner goroutine so the
+ // whole tick sequence serializes against session input and admin tasks.
+ g.SubscribeTicks(1, func() bool {
g.RunTickSystems()
return true
})
@@ -62,8 +64,23 @@ func main() {
}
g.SetHub(server.Hub())
+ // Admin mutation callbacks execute on the game's owner goroutine: the
+ // admin HTTP goroutine must not touch live players or world state.
+ // MigrateRoomIDs is the complete room-ID migration (players, Hub
+ // membership, casino room keys, world state, mob instances).
+ adminRewriteRoomIDs := g.MigrateRoomIDs
+ adminRebuildCraftIndex := func() {
+ g.RunOnOwner(func() { g.RebuildCraftIndex() })
+ }
+ adminEvictRoom := func(roomID int) {
+ g.RunOnOwner(func() { g.EvictCasinoRoom(roomID) })
+ }
+ adminReloadCourses := func() {
+ g.RunOnOwner(func() { g.CourseStore.ReloadAll() })
+ }
+
if cfg.AdminHTTP.Enabled {
- adminSrv, err := admin.NewServer(cfg, false, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir, g.CourseStore.ReloadAll, g.RewriteRoomIDs, g.RebuildCraftIndex, *configPath)
+ adminSrv, err := admin.NewServer(cfg, false, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir, adminReloadCourses, adminRewriteRoomIDs, adminEvictRoom, adminRebuildCraftIndex, *configPath)
if err != nil {
log.Fatalf("failed to start admin HTTP server: %v", err)
}
@@ -76,7 +93,7 @@ func main() {
}
if cfg.AdminHTTPS.Enabled {
- adminSrv, err := admin.NewServer(cfg, true, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir, g.CourseStore.ReloadAll, g.RewriteRoomIDs, g.RebuildCraftIndex, *configPath)
+ adminSrv, err := admin.NewServer(cfg, true, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir, adminReloadCourses, adminRewriteRoomIDs, adminEvictRoom, adminRebuildCraftIndex, *configPath)
if err != nil {
log.Fatalf("failed to start admin HTTPS server: %v", err)
}
@@ -105,14 +122,19 @@ func main() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+ // 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).
+ g.SetTransportStop(server.Shutdown)
go func() {
<-sigCh
- log.Println("Shutting down...")
- g.Ticks.Stop()
- os.Exit(0)
+ g.RequestShutdown("signal")
}()
- if err := server.ListenAndServe(g.HandleSession); err != nil {
+ if err := server.ListenAndServe(g.HandleSessionOnOwner); err != nil {
log.Fatalf("server error: %v", err)
}
}