aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/undo.go
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 /internal/admin/undo.go
parent396c3facb8287b9a34076cae8f3d7b300d366d99 (diff)
downloadthehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'internal/admin/undo.go')
-rw-r--r--internal/admin/undo.go178
1 files changed, 135 insertions, 43 deletions
diff --git a/internal/admin/undo.go b/internal/admin/undo.go
index 5e1e6a3..0f9744c 100644
--- a/internal/admin/undo.go
+++ b/internal/admin/undo.go
@@ -29,6 +29,11 @@ type ChangeDesc struct {
IsDelete bool `json:"is_delete"`
IsCreate bool `json:"is_create"`
ExtraFiles []ExtraFile `json:"extra_files,omitempty"`
+ // IDMap records a room-ID change (oldID -> newID) included in the
+ // change. Undo applies its inverse, redo the forward map, through the
+ // wired runtime migration so players, Hub membership, and casino room
+ // keys move back and forth with the file images.
+ IDMap map[int]int `json:"id_map,omitempty"`
}
func (c ChangeDesc) ShortDesc() string {
@@ -63,6 +68,9 @@ type UndoStack struct {
redo []ChangeDesc
maxSize int
filePath string
+ // migrateRoomIDs applies a room-ID map to runtime state (players, Hub
+ // membership, casino room keys) during undo/redo of room-ID changes.
+ migrateRoomIDs func(map[int]int)
}
func NewUndoStack(dataDir string) *UndoStack {
@@ -73,6 +81,14 @@ func NewUndoStack(dataDir string) *UndoStack {
}
}
+// SetMigrateRoomIDs wires the runtime room-ID migration used by undo/redo
+// of room-ID changes. Called by the server after construction.
+func (us *UndoStack) SetMigrateRoomIDs(fn func(map[int]int)) {
+ us.mu.Lock()
+ defer us.mu.Unlock()
+ us.migrateRoomIDs = fn
+}
+
func (us *UndoStack) Push(desc ChangeDesc) {
us.mu.Lock()
defer us.mu.Unlock()
@@ -98,6 +114,16 @@ func (us *UndoStack) Undo() *ChangeDesc {
log.Printf("undo: failed %s: %v", last.ShortDesc(), err)
return nil
}
+ // Runtime migration moves with the file images: undo applies the
+ // inverse room-ID map so players, Hub membership, and casino room keys
+ // return to the old location together.
+ if len(last.IDMap) > 0 && us.migrateRoomIDs != nil {
+ inverse := make(map[int]int, len(last.IDMap))
+ for from, to := range last.IDMap {
+ inverse[to] = from
+ }
+ us.migrateRoomIDs(inverse)
+ }
us.history = us.history[:len(us.history)-1]
us.redo = append(us.redo, last)
@@ -118,6 +144,14 @@ func (us *UndoStack) Redo() *ChangeDesc {
log.Printf("redo: failed %s: %v", last.ShortDesc(), err)
return nil
}
+ // Redo applies the forward room-ID map through the runtime migration.
+ if len(last.IDMap) > 0 && us.migrateRoomIDs != nil {
+ forward := make(map[int]int, len(last.IDMap))
+ for from, to := range last.IDMap {
+ forward[from] = to
+ }
+ us.migrateRoomIDs(forward)
+ }
us.redo = us.redo[:len(us.redo)-1]
us.history = append(us.history, last)
@@ -126,64 +160,80 @@ func (us *UndoStack) Redo() *ChangeDesc {
return &last
}
+// applyUndo reverts a change: the primary file, then every extra file.
+// Extra-file failures propagate: a multi-file undo that restored only part
+// of a change must be reported as failed, never as success.
func (us *UndoStack) applyUndo(c ChangeDesc) error {
if c.NewFilePath != "" {
- os.Remove(c.NewFilePath)
+ if err := os.Remove(c.NewFilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("remove %s: %w", c.NewFilePath, err)
+ }
}
if c.IsDelete {
if c.OldContent == nil {
- os.MkdirAll(c.FilePath, 0755)
+ if err := os.MkdirAll(c.FilePath, 0755); err != nil {
+ return fmt.Errorf("recreate dir %s: %w", c.FilePath, err)
+ }
} else {
- os.MkdirAll(filepath.Dir(c.FilePath), 0755)
+ if err := os.MkdirAll(filepath.Dir(c.FilePath), 0755); err != nil {
+ return fmt.Errorf("mkdir: %w", err)
+ }
if err := os.WriteFile(c.FilePath, c.OldContent, 0644); err != nil {
return fmt.Errorf("restore deleted %s: %w", c.FilePath, err)
}
}
- for _, ef := range c.ExtraFiles {
- applyUndoExtraFile(ef)
- }
} else if c.IsCreate {
- os.Remove(c.FilePath)
- for _, ef := range c.ExtraFiles {
- applyUndoExtraFile(ef)
+ if err := os.Remove(c.FilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("remove created %s: %w", c.FilePath, err)
}
} else {
- target := c.FilePath
- content := c.OldContent
- if c.NewFilePath != "" {
- target = c.FilePath
+ if err := os.MkdirAll(filepath.Dir(c.FilePath), 0755); err != nil {
+ return fmt.Errorf("mkdir: %w", err)
}
- os.MkdirAll(filepath.Dir(target), 0755)
- if err := os.WriteFile(target, content, 0644); err != nil {
- return fmt.Errorf("revert %s: %w", target, err)
+ if err := os.WriteFile(c.FilePath, c.OldContent, 0644); err != nil {
+ return fmt.Errorf("revert %s: %w", c.FilePath, err)
}
- for _, ef := range c.ExtraFiles {
- applyUndoExtraFile(ef)
+ }
+ for _, ef := range c.ExtraFiles {
+ if err := applyUndoExtraFile(ef); err != nil {
+ return err
}
}
return nil
}
+// applyRedo re-applies a change: the primary file, then every extra file.
+// Extra-file failures propagate like undo.
func (us *UndoStack) applyRedo(c ChangeDesc) error {
if c.NewFilePath != "" {
- os.Remove(c.FilePath)
+ if err := os.Remove(c.FilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("remove %s: %w", c.FilePath, err)
+ }
}
if c.IsCreate {
target := c.FilePath
if c.NewFilePath != "" {
target = c.NewFilePath
}
- os.MkdirAll(filepath.Dir(target), 0755)
+ if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
+ return fmt.Errorf("mkdir: %w", err)
+ }
if err := os.WriteFile(target, c.NewContent, 0644); err != nil {
return fmt.Errorf("recreate %s: %w", target, err)
}
for _, ef := range c.ExtraFiles {
- applyRedoExtraFile(ef)
+ if err := applyRedoExtraFile(ef); err != nil {
+ return err
+ }
}
} else if c.IsDelete {
- os.Remove(c.FilePath)
+ if err := os.Remove(c.FilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("re-delete %s: %w", c.FilePath, err)
+ }
for _, ef := range c.ExtraFiles {
- applyRedoExtraFile(ef)
+ if err := applyRedoExtraFile(ef); err != nil {
+ return err
+ }
}
} else {
target := c.FilePath
@@ -191,49 +241,87 @@ func (us *UndoStack) applyRedo(c ChangeDesc) error {
if c.NewFilePath != "" {
target = c.NewFilePath
}
- os.MkdirAll(filepath.Dir(target), 0755)
+ if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
+ return fmt.Errorf("mkdir: %w", err)
+ }
if err := os.WriteFile(target, content, 0644); err != nil {
return fmt.Errorf("reapply %s: %w", target, err)
}
for _, ef := range c.ExtraFiles {
- applyRedoExtraFile(ef)
+ if err := applyRedoExtraFile(ef); err != nil {
+ return err
+ }
}
}
return nil
}
-func applyUndoExtraFile(ef ExtraFile) {
+func applyUndoExtraFile(ef ExtraFile) error {
switch {
case ef.IsCreate:
- os.Remove(ef.FilePath)
+ if err := os.Remove(ef.FilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("undo remove %s: %w", ef.FilePath, err)
+ }
case ef.IsDelete:
- os.MkdirAll(filepath.Dir(ef.FilePath), 0755)
- os.WriteFile(ef.FilePath, ef.OldContent, 0644)
+ if err := os.MkdirAll(filepath.Dir(ef.FilePath), 0755); err != nil {
+ return fmt.Errorf("undo mkdir: %w", err)
+ }
+ if err := os.WriteFile(ef.FilePath, ef.OldContent, 0644); err != nil {
+ return fmt.Errorf("undo restore %s: %w", ef.FilePath, err)
+ }
case ef.NewFilePath != "":
- os.Remove(ef.NewFilePath)
- os.MkdirAll(filepath.Dir(ef.FilePath), 0755)
- os.WriteFile(ef.FilePath, ef.OldContent, 0644)
+ if err := os.Remove(ef.NewFilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("undo remove %s: %w", ef.NewFilePath, err)
+ }
+ if err := os.MkdirAll(filepath.Dir(ef.FilePath), 0755); err != nil {
+ return fmt.Errorf("undo mkdir: %w", err)
+ }
+ if err := os.WriteFile(ef.FilePath, ef.OldContent, 0644); err != nil {
+ return fmt.Errorf("undo restore %s: %w", ef.FilePath, err)
+ }
default:
- os.MkdirAll(filepath.Dir(ef.FilePath), 0755)
- os.WriteFile(ef.FilePath, ef.OldContent, 0644)
+ if err := os.MkdirAll(filepath.Dir(ef.FilePath), 0755); err != nil {
+ return fmt.Errorf("undo mkdir: %w", err)
+ }
+ if err := os.WriteFile(ef.FilePath, ef.OldContent, 0644); err != nil {
+ return fmt.Errorf("undo revert %s: %w", ef.FilePath, err)
+ }
}
+ return nil
}
-func applyRedoExtraFile(ef ExtraFile) {
+func applyRedoExtraFile(ef ExtraFile) error {
switch {
case ef.IsDelete:
- os.Remove(ef.FilePath)
+ if err := os.Remove(ef.FilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("redo remove %s: %w", ef.FilePath, err)
+ }
case ef.IsCreate:
- os.MkdirAll(filepath.Dir(ef.FilePath), 0755)
- os.WriteFile(ef.FilePath, ef.NewContent, 0644)
+ if err := os.MkdirAll(filepath.Dir(ef.FilePath), 0755); err != nil {
+ return fmt.Errorf("redo mkdir: %w", err)
+ }
+ if err := os.WriteFile(ef.FilePath, ef.NewContent, 0644); err != nil {
+ return fmt.Errorf("redo create %s: %w", ef.FilePath, err)
+ }
case ef.NewFilePath != "":
- os.Remove(ef.FilePath)
- os.MkdirAll(filepath.Dir(ef.NewFilePath), 0755)
- os.WriteFile(ef.NewFilePath, ef.NewContent, 0644)
+ if err := os.Remove(ef.FilePath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("redo remove %s: %w", ef.FilePath, err)
+ }
+ if err := os.MkdirAll(filepath.Dir(ef.NewFilePath), 0755); err != nil {
+ return fmt.Errorf("redo mkdir: %w", err)
+ }
+ if err := os.WriteFile(ef.NewFilePath, ef.NewContent, 0644); err != nil {
+ return fmt.Errorf("redo create %s: %w", ef.NewFilePath, err)
+ }
default:
- os.MkdirAll(filepath.Dir(ef.FilePath), 0755)
- os.WriteFile(ef.FilePath, ef.NewContent, 0644)
+ if err := os.MkdirAll(filepath.Dir(ef.FilePath), 0755); err != nil {
+ return fmt.Errorf("redo mkdir: %w", err)
+ }
+ if err := os.WriteFile(ef.FilePath, ef.NewContent, 0644); err != nil {
+ return fmt.Errorf("redo reapply %s: %w", ef.FilePath, err)
+ }
}
+ return nil
}
func (us *UndoStack) Info() UndoInfo {
@@ -273,6 +361,7 @@ func (us *UndoStack) save() {
IsDelete bool `json:"is_delete"`
IsCreate bool `json:"is_create"`
ExtraFiles []extraEntry `json:"extra_files,omitempty"`
+ IDMap map[int]int `json:"id_map,omitempty"`
}
type saveData struct {
History []entry `json:"history"`
@@ -302,6 +391,7 @@ func (us *UndoStack) save() {
IsDelete: c.IsDelete,
IsCreate: c.IsCreate,
ExtraFiles: extra,
+ IDMap: c.IDMap,
})
}
return entries
@@ -344,6 +434,7 @@ func (us *UndoStack) load() {
IsDelete bool `json:"is_delete"`
IsCreate bool `json:"is_create"`
ExtraFiles []extraEntry `json:"extra_files,omitempty"`
+ IDMap map[int]int `json:"id_map,omitempty"`
}
var saveData struct {
History []entry `json:"history"`
@@ -377,6 +468,7 @@ func (us *UndoStack) load() {
IsDelete: e.IsDelete,
IsCreate: e.IsCreate,
ExtraFiles: extra,
+ IDMap: e.IDMap,
})
}
return changes