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:
2026-07-07 00:03:45 +02:00
parent 3ed7931676
commit 5ba9fe8f66
49 changed files with 1965 additions and 410 deletions

View File

@@ -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
}