Adopt house golangci-lint config; fix all approved-linter findings
.golangci.yml is the prompts-repo standard verbatim plus one approved
exception (paralleltest, sneak 2026-07-06). Roughly 1,500 findings
fixed:
- autofix sweep (wsl_v5/nlreturn/intrange/modernize formatting), with
the misspell autofix REVERTED where it rewrote authentic C game text
("missle vanishes" stays, with nolint and a comment)
- error handling: errcheck sites get real handling — saveFile now
closes explicitly and removes corrupt saves, scoreboard writes are
documented best-effort, err113 sentinel errors (ErrSaveOutOfDate,
ErrScreenTooSmall); panics allowed for unrecoverable states per
MEMORY.md policy
- gosec: real fixes (ParseInt for SEED, 0600 scorefile) and justified
per-line nolints for provably-bounded conversions (randomMonsterLetter
helper collapses eight rnd(26)+'A' sites)
- API tidying from linters: pointer receivers on flag types
(recvcheck), msg helpers renamed addmsgf/doaddf/Printwf/MvPrintwf
(goprintffuncname), myExit()/findFloor(monst)/mkGameInput(t) drop
always-constant params (unparam), gameEnd carries no status
- gocritic/staticcheck: if-else chains to switches, the pack.c
inventory filter untangled into matchesFilter, main.go split so
defers run before exit (exitAfterDefer, funlen)
- revive doc comments on all exported flag types/consts/methods
Remaining findings are confined to linters pending sneak's exception
decision (mnd, gochecknoglobals, cyclop, nestif, gocognit, exhaustive,
goconst, testpackage); TODO.md records the state. MEMORY.md added at
repo root as the project's agent working notes.
This commit is contained in:
106
game/save.go
106
game/save.go
@@ -2,6 +2,7 @@ package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
@@ -131,16 +132,19 @@ func (g *RogueGame) roomIdx(rp *Room) int {
|
||||
if rp == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := range g.Level.Rooms {
|
||||
if rp == &g.Level.Rooms[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
for i := range g.Level.Passages {
|
||||
if rp == &g.Level.Passages[i] {
|
||||
return 100 + i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -152,6 +156,7 @@ func (g *RogueGame) roomAt(i int) *Room {
|
||||
case i >= 0:
|
||||
return &g.Level.Rooms[i]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -160,11 +165,13 @@ func (g *RogueGame) packIdx(obj *Object) int {
|
||||
if obj == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
for i, o := range g.Player.Pack {
|
||||
if o == obj {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -266,9 +273,11 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
for _, o := range m.Pack {
|
||||
sc.Pack = append(sc.Pack, *o)
|
||||
}
|
||||
|
||||
st.Monsters = append(st.Monsters, sc)
|
||||
|
||||
ref := destRef{}
|
||||
|
||||
switch {
|
||||
case m.Dest == nil:
|
||||
case m.Dest == &p.Pos:
|
||||
@@ -279,6 +288,7 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
ref = destRef{Kind: 2, Idx: mi}
|
||||
}
|
||||
}
|
||||
|
||||
if ref.Kind == 0 {
|
||||
for _, oo := range g.Level.Objects {
|
||||
if m.Dest == &oo.Pos {
|
||||
@@ -286,6 +296,7 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ref.Kind == 0 {
|
||||
for ri := range g.Level.Rooms {
|
||||
if m.Dest == &g.Level.Rooms[ri].Gold {
|
||||
@@ -294,8 +305,10 @@ func (g *RogueGame) snapshot() *SaveState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
st.Dests = append(st.Dests, ref)
|
||||
}
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
@@ -371,15 +384,18 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
p.Flags = sp.Body.Flags
|
||||
p.Stats = sp.Body.Stats
|
||||
p.Room = g.roomAt(sp.Body.RoomIdx)
|
||||
|
||||
p.Pack = nil
|
||||
for i := range sp.Body.Pack {
|
||||
o := sp.Body.Pack[i]
|
||||
p.Pack = append(p.Pack, &o)
|
||||
}
|
||||
|
||||
pick := func(i int) *Object {
|
||||
if i < 0 || i >= len(p.Pack) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.Pack[i]
|
||||
}
|
||||
p.CurArmor = pick(sp.CurArmor)
|
||||
@@ -400,6 +416,7 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
g.Level.Monsters = nil
|
||||
for i := range st.Monsters {
|
||||
sc := &st.Monsters[i]
|
||||
|
||||
m := &Monster{Creature: Creature{
|
||||
Pos: sc.Pos, Turn: sc.Turn, Type: sc.Type, Disguise: sc.Disguise,
|
||||
OldCh: sc.OldCh, Flags: sc.Flags, Stats: sc.Stats,
|
||||
@@ -409,11 +426,14 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
o := sc.Pack[j]
|
||||
m.Pack = append(m.Pack, &o)
|
||||
}
|
||||
|
||||
g.Level.Monsters = append(g.Level.Monsters, m)
|
||||
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
|
||||
}
|
||||
|
||||
for i, ref := range st.Dests {
|
||||
m := g.Level.Monsters[i]
|
||||
|
||||
switch ref.Kind {
|
||||
case 1:
|
||||
m.Dest = &p.Pos
|
||||
@@ -433,26 +453,35 @@ func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
// goto over/gotfile flow becomes the useDefault flag.
|
||||
func (g *RogueGame) saveGame() {
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
over:
|
||||
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")
|
||||
}
|
||||
|
||||
if c == 'y' || c == 'Y' {
|
||||
g.addstr("Yes\n")
|
||||
g.refresh()
|
||||
|
||||
useDefault = true
|
||||
}
|
||||
}
|
||||
@@ -465,80 +494,115 @@ over:
|
||||
} else {
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("file name: ")
|
||||
|
||||
if g.getStr(&buf, g.scr.Std) == Quit {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
// test to see if the file exists
|
||||
if _, err := os.Stat(buf); err == nil {
|
||||
_, 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("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if c == 'y' || c == 'Y' {
|
||||
break
|
||||
}
|
||||
|
||||
if c == 'n' || c == 'N' {
|
||||
goto over
|
||||
}
|
||||
|
||||
g.msg("Please answer Y or N")
|
||||
}
|
||||
|
||||
g.msg("file name: %s", buf)
|
||||
os.Remove(g.FileName)
|
||||
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
|
||||
}
|
||||
|
||||
g.FileName = buf
|
||||
if err := g.saveFile(g.FileName); err != nil {
|
||||
|
||||
err := g.saveFile(g.FileName)
|
||||
if err != nil {
|
||||
g.msg("%s", err.Error())
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
g.myExit(0)
|
||||
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// saveFile writes the saved game (save.c save_file).
|
||||
// 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 {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400) //nolint:gosec,lll // G304: user-chosen save path
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := gob.NewEncoder(f).Encode(g.snapshot()); err != nil {
|
||||
os.Remove(path)
|
||||
return err
|
||||
|
||||
encErr := gob.NewEncoder(f).Encode(g.snapshot())
|
||||
closeErr := f.Close()
|
||||
|
||||
if encErr != nil || closeErr != nil {
|
||||
_ = os.Remove(path) // don't leave a corrupt save behind
|
||||
|
||||
if encErr != nil {
|
||||
return encErr
|
||||
}
|
||||
|
||||
return closeErr
|
||||
}
|
||||
|
||||
return os.Chmod(path, 0o400)
|
||||
}
|
||||
|
||||
// AutoSave silently saves to the current file name; used on SIGHUP/SIGTERM
|
||||
// (save.c auto_save).
|
||||
// (save.c auto_save). Best effort by design: it runs on the way out of a
|
||||
// dying process.
|
||||
func (g *RogueGame) AutoSave() {
|
||||
if g.FileName != "" {
|
||||
os.Remove(g.FileName)
|
||||
g.saveFile(g.FileName)
|
||||
_ = os.Remove(g.FileName)
|
||||
_ = g.saveFile(g.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSaveOutOfDate reports a save file from an incompatible version.
|
||||
var ErrSaveOutOfDate = errors.New("sorry, saved game is out of date")
|
||||
|
||||
// Restore restores a saved game from a file (save.c restore). The file is
|
||||
// deleted, as in C, to defeat restarting from the same save.
|
||||
func Restore(path string, cfg Config) (*RogueGame, error) {
|
||||
f, err := os.Open(path)
|
||||
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }() // read-only handle
|
||||
|
||||
var st SaveState
|
||||
if err := gob.NewDecoder(f).Decode(&st); err != nil {
|
||||
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w", path, err)
|
||||
|
||||
decErr := gob.NewDecoder(f).Decode(&st)
|
||||
if decErr != nil {
|
||||
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w",
|
||||
path, decErr)
|
||||
}
|
||||
|
||||
if st.Version != saveFormatVersion {
|
||||
return nil, fmt.Errorf("sorry, saved game is out of date")
|
||||
return nil, ErrSaveOutOfDate
|
||||
}
|
||||
|
||||
g := &RogueGame{
|
||||
@@ -553,8 +617,10 @@ func Restore(path string, cfg Config) (*RogueGame, error) {
|
||||
g.applySnapshot(&st)
|
||||
|
||||
// defeat multiple restarting from the same place
|
||||
if err := os.Remove(path); err != nil {
|
||||
return nil, fmt.Errorf("cannot unlink file: %w", err)
|
||||
rmErr := os.Remove(path)
|
||||
if rmErr != nil {
|
||||
return nil, fmt.Errorf("cannot unlink file: %w", rmErr)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user