aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_rooms.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 17:48:26 -0400
committerhistoria <[not public]>2026-09-18 17:48:26 -0400
commit8fe9e3c3fef0049df82281a62afe35dda516dfe2 (patch)
treedd269f2c0710f334e19f00a191260ebc46284a90 /internal/admin/api_rooms.go
parent194164a77eb4f2479b94c19e6f0e1278ab608781 (diff)
downloadthehouseoficarus-8fe9e3c3fef0049df82281a62afe35dda516dfe2.tar.gz
fix: close combat ghost, admin traversal, login throttle, journal double-pay, and quality/economy gaps
Diffstat (limited to 'internal/admin/api_rooms.go')
-rw-r--r--internal/admin/api_rooms.go70
1 files changed, 52 insertions, 18 deletions
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go
index d84449b..7f317cc 100644
--- a/internal/admin/api_rooms.go
+++ b/internal/admin/api_rooms.go
@@ -343,6 +343,11 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) {
s.world.RebuildRoomIndex(s.dataDir)
s.world.ClearRoomState(id)
+ // Drop the room's live mob instances too, or they keep wandering and
+ // are returned by MobsInRoom for a room that no longer exists.
+ if s.mobStore != nil {
+ s.mobStore.RemoveMobsInRoom(id)
+ }
// Drop any cached in-room games (tables/machines) and refund their
// outstanding wagers — same eviction semantics as bulk delete.
if s.evictRoom != nil {
@@ -490,6 +495,11 @@ func (s *AdminServer) handleRenameRoomDir(w http.ResponseWriter, r *http.Request
failJSON(w, "rename failed: "+err.Error())
return
}
+ // Undo history stores absolute paths: re-prefix them so a later undo
+ // does not recreate the old directory and write a duplicate room file.
+ if s.undoStack != nil {
+ s.undoStack.RewritePaths(oldPath, newPath)
+ }
s.world.RebuildRoomIndex(s.dataDir)
writeJSON(w, map[string]any{"ok": true, "new_dir": newRel})
}
@@ -674,39 +684,63 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, "room path not found", http.StatusInternalServerError)
return
}
-
- oldA, _ := snapshotFile(pathA)
- newA, err := writeYAMLFile(pathA, roomA)
+ newA, err := marshalYAML(roomA)
if err != nil {
- writeJSONError(w, "write A: "+err.Error(), http.StatusInternalServerError)
+ writeJSONError(w, "serialize A: "+err.Error(), http.StatusInternalServerError)
return
}
- s.undoStack.Push(ChangeDesc{
- Description: fmt.Sprintf("link %d %s %d", body.From, dir, body.To),
- FilePath: pathA,
- OldContent: oldA,
- NewContent: newA,
- })
+ oldA, _ := snapshotFile(pathA)
+ pathB := ""
+ var oldB, newB []byte
if !body.OneWay {
- pathB, okB2 := s.world.GetRoomPath(body.To)
+ var okB2 bool
+ pathB, okB2 = s.world.GetRoomPath(body.To)
if !okB2 {
writeJSONError(w, "room B path not found", http.StatusInternalServerError)
return
}
- oldB, _ := snapshotFile(pathB)
- newB, err := writeYAMLFile(pathB, roomB)
+ newB, err = marshalYAML(roomB)
if err != nil {
+ writeJSONError(w, "serialize B: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ oldB, _ = snapshotFile(pathB)
+ }
+
+ // Both sides are resolved and serialized before any write, and both
+ // are applied in one journaled transaction so a failure cannot leave
+ // an asymmetric link, and one Undo reverses the whole action.
+ txn := persistence.NewTransaction(filepath.Join(s.dataDir, ".journal"))
+ if err := txn.Replace(pathA, newA, 0644); err != nil {
+ writeJSONError(w, "write A: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if !body.OneWay {
+ if err := txn.Replace(pathB, newB, 0644); err != nil {
writeJSONError(w, "write B: "+err.Error(), http.StatusInternalServerError)
return
}
- s.undoStack.Push(ChangeDesc{
- Description: fmt.Sprintf("link %d %s %d (reverse)", body.To, oppDir, body.From),
- FilePath: pathB,
- OldContent: oldB,
- NewContent: newB,
+ }
+ if err := txn.Commit(); err != nil {
+ writeJSONError(w, "commit link: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ desc := ChangeDesc{
+ Description: fmt.Sprintf("link %d %s %d", body.From, dir, body.To),
+ FilePath: pathA,
+ OldContent: oldA,
+ NewContent: newA,
+ }
+ if !body.OneWay {
+ desc.ExtraFiles = append(desc.ExtraFiles, ExtraFile{
+ FilePath: pathB,
+ OldContent: oldB,
+ NewContent: newB,
})
}
+ s.undoStack.Push(desc)
writeJSON(w, map[string]any{"ok": true})
case http.MethodDelete: