Files
deathwatch/totals.go
T
emmatherock 5a2c3272e9 Translate remaining Go source and overlay.html comments to English
Identifiers, comments, and log/console messages in auth.go, i18n.go,
counter.go, totals.go, and overlay.html's script/CSS comments — the
files the TLS commit didn't already touch. No functional changes.

locales/*.json and overlay.html's own FALLBACK/data-i18n strings stay
bilingual on purpose: that's end-user text for the overlay/panel, a
different audience than the source code, served by the existing i18n
system rather than this convention.
2026-09-17 21:51:33 -03:00

142 lines
3.9 KiB
Go

// totals.go: the totals, one per character.
//
// The game already tracks each character's death count separately (it's
// in the save, not on the PC), so the program does the same: for each
// character it saves a "base", and the number shown is always
// total = deaths read from the game + base.
//
// The number shown is, with nothing in between, whatever the game tracks
// for that character. What's saved to disk is NOT an adjustment: it's
// just the last thing seen, so there's something to show if you open the
// counter before the game and there's nothing to read yet.
//
// There were two earlier models. First a single lifetime total that
// never went down, which with multiple characters left the number stuck
// until the next death. Then a per-character "base" added to the
// reading, inherited from when we didn't trust what we were reading:
// once the total comes straight from the game on every read, correcting
// it by hand stopped fixing anything and just made the number lie quietly.
package main
import (
"encoding/json"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
)
type charTotal struct {
Name string `json:"name"`
Total int64 `json:"total"` // last total seen: shows something before the game is read
}
type totalsFile struct {
LastCharacter string `json:"lastCharacter"`
Characters map[string]*charTotal `json:"characters"`
}
type totalsStore struct {
mu sync.Mutex
path string
data totalsFile
}
// testExeDir lets tests point this at a temp directory.
var testExeDir string
func exeDir() (string, bool) {
if testExeDir != "" {
return testExeDir, true
}
if exe, err := os.Executable(); err == nil {
return filepath.Dir(exe), true
}
return "", false
}
func totalsPath() string {
if dir, ok := exeDir(); ok {
return filepath.Join(dir, "totals.json")
}
return "totals.json"
}
func newTotalsStore() *totalsStore {
s := &totalsStore{path: totalsPath()}
s.data.Characters = map[string]*charTotal{}
data, err := os.ReadFile(s.path)
if err == nil {
if err := json.Unmarshal(data, &s.data); err != nil {
log.Printf("totals.json is unreadable (%v): starting fresh, won't overwrite it until the next write", err)
s.data = totalsFile{Characters: map[string]*charTotal{}}
}
if s.data.Characters == nil {
s.data.Characters = map[string]*charTotal{}
}
}
return s
}
func (s *totalsStore) get(key string) (*charTotal, bool) {
s.mu.Lock()
defer s.mu.Unlock()
c, ok := s.data.Characters[key]
if !ok {
return nil, false
}
cp := *c
return &cp, true
}
// lastKnownTotal: what to show while the game isn't open.
func (s *totalsStore) lastKnownTotal() (key, name string, total int64) {
s.mu.Lock()
defer s.mu.Unlock()
if c, ok := s.data.Characters[s.data.LastCharacter]; ok {
return s.data.LastCharacter, c.Name, c.Total
}
return "", "", 0
}
func (s *totalsStore) save(key string, c charTotal) {
s.mu.Lock()
s.data.Characters[key] = &c
s.data.LastCharacter = key
blob, err := json.MarshalIndent(s.data, "", " ")
s.mu.Unlock()
if err != nil {
log.Printf("couldn't serialize totals.json: %v", err)
return
}
// Atomic write: if the power cuts mid-write, the old file stays intact
// instead of ending up as a JSON file cut in half.
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, blob, 0644); err != nil {
log.Printf("couldn't write totals.json: %v", err)
return
}
if err := os.Rename(tmp, s.path); err != nil {
log.Printf("couldn't replace totals.json: %v", err)
}
}
// characterKey: a character's identity.
//
// The save slot (GameMan+0xAC0) is the good identity: it never changes,
// and two characters with the same name stay distinct. The name is only
// a fallback for when we can't read the slot, with the known limitation
// that there, two homonyms DO share a counter.
func characterKey(slot int, name string) string {
if slot >= 0 {
return "slot" + strconv.Itoa(slot)
}
if n := strings.ToLower(strings.TrimSpace(name)); n != "" {
return "name:" + n
}
return ""
}