Decompose save.go (refactor step 7)

saveGame splits into askDefaultSave/saveFileName/saveCheckOverwrite/
askOverwrite around a saveAnswer tri-state; snapshot gains destRefFor;
applySnapshot gains applyMonsters/applyDests. save.go is
complexity-clean. Behavior unchanged.
This commit is contained in:
2026-07-22 21:53:02 +07:00
parent 389db14bbf
commit 43b4fbe746

View File

@@ -275,43 +275,44 @@ func (g *RogueGame) snapshot() *SaveState {
}
st.Monsters = append(st.Monsters, sc)
ref := destRef{}
switch {
case m.Dest == nil:
case m.Dest == &p.Pos:
ref = destRef{Kind: 1}
default:
for mi, om := range g.Level.Monsters {
if m.Dest == &om.Pos {
ref = destRef{Kind: 2, Idx: mi}
}
}
if ref.Kind == 0 {
for _, oo := range g.Level.Objects {
if m.Dest == &oo.Pos {
ref = destRef{Kind: 3, Idx: objAt[oo]}
}
}
}
if ref.Kind == 0 {
for ri := range g.Level.Rooms {
if m.Dest == &g.Level.Rooms[ri].Gold {
ref = destRef{Kind: 4, Idx: ri}
}
}
}
}
st.Dests = append(st.Dests, ref)
st.Dests = append(st.Dests, g.destRefFor(m, objAt))
}
return st
}
// destRefFor encodes a monster's chase target as a (kind, index)
// reference: the hero, another monster, a level object, or room gold
// (state.c rs_write_thing).
func (g *RogueGame) destRefFor(m *Monster, objAt map[*Object]int) destRef {
switch {
case m.Dest == nil:
return destRef{}
case m.Dest == &g.Player.Pos:
return destRef{Kind: 1}
}
for mi, om := range g.Level.Monsters {
if m.Dest == &om.Pos {
return destRef{Kind: 2, Idx: mi}
}
}
for _, oo := range g.Level.Objects {
if m.Dest == &oo.Pos {
return destRef{Kind: 3, Idx: objAt[oo]}
}
}
for ri := range g.Level.Rooms {
if m.Dest == &g.Level.Rooms[ri].Gold {
return destRef{Kind: 4, Idx: ri}
}
}
return destRef{}
}
// applySnapshot rebuilds live game state from a snapshot.
func (g *RogueGame) applySnapshot(st *SaveState) {
p := &g.Player
@@ -412,7 +413,15 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
p.MaxStats = sp.MaxStats
p.VfHit = sp.VfHit
// monsters, their map index, and their chase targets
g.applyMonsters(st)
g.applyDests(st)
g.scr.Std.SetContents(st.Screen)
}
// applyMonsters rebuilds the monster list and its map index from a
// snapshot (the monster half of applySnapshot).
func (g *RogueGame) applyMonsters(st *SaveState) {
g.Level.Monsters = nil
for i := range st.Monsters {
sc := &st.Monsters[i]
@@ -430,13 +439,17 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
g.Level.Monsters = append(g.Level.Monsters, m)
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
}
}
// applyDests re-aims the monsters' chase targets from their (kind,
// index) references (the fixup half of applySnapshot).
func (g *RogueGame) applyDests(st *SaveState) {
for i, ref := range st.Dests {
m := g.Level.Monsters[i]
switch ref.Kind {
case 1:
m.Dest = &p.Pos
m.Dest = &g.Player.Pos
case 2:
m.Dest = &g.Level.Monsters[ref.Idx].Pos
case 3:
@@ -445,13 +458,20 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
m.Dest = &g.Level.Rooms[ref.Idx].Gold
}
}
g.scr.Std.SetContents(st.Screen)
}
// saveAnswer is a yes/no/escape prompt result in the save-game flow.
type saveAnswer int
// The saveGame prompt outcomes.
const (
saveYes saveAnswer = iota
saveNo
saveAbort
)
// saveGame implements the "save game" command (save.c save_game). The
// labeled prompt loop and the useDefault flag stand in for the C
// goto over/gotfile flow.
// labeled prompt loop stands in for the C goto over/gotfile flow.
func (g *RogueGame) saveGame() {
g.Msgs.Mpos = 0
@@ -460,84 +480,34 @@ prompt:
useDefault := false
if g.FileName != "" {
var c byte
for {
g.msg("save file (%s)? ", g.FileName)
c = g.readchar()
g.Msgs.Mpos = 0
if c == Escape {
g.msg("")
return
}
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
break
}
g.msg("please answer Y or N")
a := g.askDefaultSave()
if a == saveAbort {
return
}
if c == 'y' || c == 'Y' {
g.addstr("Yes\n")
g.refresh()
useDefault = true
}
useDefault = a == saveYes
}
for {
var buf string
if useDefault {
buf = g.FileName
useDefault = false
} else {
g.Msgs.Mpos = 0
g.msg("file name: ")
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
return
}
g.Msgs.Mpos = 0
buf, ok := g.saveFileName(useDefault)
if !ok {
return
}
// test to see if the file exists
_, statErr := os.Stat(buf)
if statErr == nil {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
c := g.readchar()
if c == Escape {
g.msg("")
useDefault = false
return
}
a := g.saveCheckOverwrite(buf)
if a == saveAbort {
return
}
if c == 'y' || c == 'Y' {
break
}
if c == 'n' || c == 'N' {
continue prompt // the C goto over: start again
}
g.msg("Please answer Y or N")
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
if a == saveNo {
continue prompt // the C goto over: start again
}
g.FileName = buf
err := g.saveFile(g.FileName)
if err != nil {
if err := g.saveFile(g.FileName); err != nil {
g.msg("%s", err.Error())
continue
@@ -550,6 +520,96 @@ prompt:
g.myExit()
}
// askDefaultSave asks whether to save to the current file name (save.c
// save_game).
func (g *RogueGame) askDefaultSave() saveAnswer {
for {
g.msg("save file (%s)? ", g.FileName)
c := g.readchar()
g.Msgs.Mpos = 0
switch c {
case Escape:
g.msg("")
return saveAbort
case 'y', 'Y':
g.addstr("Yes\n")
g.refresh()
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("please answer Y or N")
}
}
// saveFileName picks the save path: the default, or a prompted one; ok
// is false when the player quit the prompt (save.c save_game).
func (g *RogueGame) saveFileName(useDefault bool) (string, bool) {
if useDefault {
return g.FileName, true
}
g.Msgs.Mpos = 0
g.msg("file name: ")
buf := ""
if g.getStr(&buf, g.scr.Std) == Quit {
g.msg("")
return "", false
}
g.Msgs.Mpos = 0
return buf, true
}
// saveCheckOverwrite guards an existing file: saveNo restarts the whole
// prompt, saveAbort quits (save.c save_game).
func (g *RogueGame) saveCheckOverwrite(buf string) saveAnswer {
// test to see if the file exists
if _, statErr := os.Stat(buf); statErr != nil {
return saveYes
}
answer := g.askOverwrite()
if answer != saveYes {
return answer
}
g.msg("file name: %s", buf)
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
return saveYes
}
// askOverwrite asks whether to overwrite the existing file (save.c
// save_game).
func (g *RogueGame) askOverwrite() saveAnswer {
for {
g.msg("File exists. Do you wish to overwrite it?")
g.Msgs.Mpos = 0
switch g.readchar() {
case Escape:
g.msg("")
return saveAbort
case 'y', 'Y':
return saveYes
case 'n', 'N':
return saveNo
}
g.msg("Please answer Y or N")
}
}
// saveFile writes the saved game (save.c save_file). A failed write means
// a corrupt save, so the file is removed before reporting the error.
func (g *RogueGame) saveFile(path string) error {