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.
142 lines
3.9 KiB
Go
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 ""
|
|
}
|