From 35ddd0ad2215760bb1d3dc7724480f179ea33fa6 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 21:05:39 -0400 Subject: fix: login account race, casino chip double-refund, room-create rollback, rename collision, fatal transport errors --- internal/admin/api_rooms.go | 47 +++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) (limited to 'internal/admin/api_rooms.go') diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go index 16fc7a0..f9508f6 100644 --- a/internal/admin/api_rooms.go +++ b/internal/admin/api_rooms.go @@ -144,11 +144,38 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { var extraFiles []ExtraFile + // Prepare the reciprocal exit in the destination map BEFORE the + // destination file is committed, so the written file is complete. + if linkFrom != nil && linkDir != nil && *linkDir != "" { + if dir := world.ExitDir(strings.ToLower(*linkDir)); dir != "" { + if opp, ok := world.OppositeExit[dir]; ok && !linkOneWay { + exits[string(opp)] = map[string]any{"room": *linkFrom} + } + } + } + + // The destination room is committed FIRST: if it cannot be written, + // nothing else has been mutated yet. The old order wrote the + // source room's new exit first, so a failed destination write left + // a dangling exit pointing at a room that never existed, with no + // undo entry to repair it. + path := filepath.Join(subdir, strconv.Itoa(id)+".yaml") + newContent, writeErr := writeMapAsYAML(path, m) + if writeErr != nil { + failJSON(w, fmt.Sprintf("write room: %v", writeErr)) + return + } + if linkFrom != nil && linkDir != nil && *linkDir != "" { dir := world.ExitDir(strings.ToLower(*linkDir)) - if opp, ok := world.OppositeExit[dir]; ok { + if _, ok := world.OppositeExit[dir]; ok { srcRoom, loadErr := s.world.LoadRoom(*linkFrom) if loadErr != nil { + // Roll back the destination so a failed create leaves + // no half-created room behind. + if rmErr := os.Remove(path); rmErr != nil { + log.Printf("room create rollback: remove %s: %v", path, rmErr) + } failJSON(w, fmt.Sprintf("load link_from room %d: %v", *linkFrom, loadErr)) return } @@ -160,31 +187,23 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { srcPath, srcOk := s.world.GetRoomPath(*linkFrom) if srcOk { oldContent, _ := snapshotFile(srcPath) - newContent, writeErr := writeYAMLFile(srcPath, srcRoom) + srcContent, writeErr := writeYAMLFile(srcPath, srcRoom) if writeErr != nil { + if rmErr := os.Remove(path); rmErr != nil { + log.Printf("room create rollback: remove %s: %v", path, rmErr) + } failJSON(w, fmt.Sprintf("write source room: %v", writeErr)) return } extraFiles = append(extraFiles, ExtraFile{ FilePath: srcPath, OldContent: oldContent, - NewContent: newContent, + NewContent: srcContent, }) } - - if !linkOneWay { - exits[string(opp)] = map[string]any{"room": *linkFrom} - } } } - path := filepath.Join(subdir, strconv.Itoa(id)+".yaml") - newContent, writeErr := writeMapAsYAML(path, m) - if writeErr != nil { - failJSON(w, fmt.Sprintf("write room: %v", writeErr)) - return - } - s.world.AddRoomPath(id, path) s.undoStack.Push(ChangeDesc{ Description: fmt.Sprintf("create room %d", id), -- cgit v1.2.3