Files
deathwatch/counter.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

197 lines
4.8 KiB
Go

// counter.go: death accounting.
//
// Deliberately has NOTHING Windows-specific: all memory reading lives in
// main.go. That keeps this part — which holds the actual rules (which
// character a death belongs to, what happens on a character switch, what
// to show with the game closed) — buildable and testable on any platform.
// All three times something counted wrong, the bug was here.
package main
import (
"log"
"strings"
"sync"
)
// The total shown IS the one the game keeps for the loaded character. No
// adjustments or accumulators of our own: switching characters jumps the
// number to theirs on the very same reading.
type state struct {
mu sync.Mutex
connected bool
playerLoaded bool
rawDeaths int32
haveReading bool
bossFight bool
charName string
charKey string
slot int
total int64
announced bool
lastErr string
}
type stateSnapshot struct {
RawDeaths int32
Slot int
Total int64
BossFight bool
Connected bool
PlayerLoaded bool
CharName string
}
var st state
var totals *totalsStore
// init starts by showing the last known character, so the overlay has a
// sensible number before you even open the game.
func (s *state) init(store *totalsStore) {
key, name, total := store.lastKnownTotal()
s.mu.Lock()
s.charName = name
s.charKey = key
s.slot = -1
s.total = total
s.mu.Unlock()
if name != "" {
log.Printf("last known character: %s with %d deaths", name, total)
}
}
func (s *state) snapshot() stateSnapshot {
s.mu.Lock()
defer s.mu.Unlock()
total := s.total
if s.haveReading {
total = int64(s.rawDeaths)
}
return stateSnapshot{
RawDeaths: s.rawDeaths,
Slot: s.slot,
Total: total,
BossFight: s.bossFight,
Connected: s.connected,
PlayerLoaded: s.playerLoaded,
CharName: s.charName,
}
}
func (s *state) name() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.charName
}
// displayName: whatever the config says wins; if it's empty, fall back to
// the character name read from the game.
func displayName(cfg config) string {
if n := strings.TrimSpace(cfg.Name); n != "" {
return n
}
if n := st.name(); n != "" {
return n
}
// Empty on purpose: let the UI pick the label, since it's the one that
// knows whether it's showing one player or two.
return ""
}
// setCharacter is called when the loaded character changes: the total
// becomes whatever the game tracks for that character.
func (s *state) setCharacter(slot int, name string, raw int32) {
key := characterKey(slot, name)
if key == "" {
return // neither slot nor name: no one to attribute this to
}
s.mu.Lock()
if key == s.charKey && s.haveReading {
// Same character: the name may have just shown up, since the slot
// is read before the name.
if name != "" && name != s.charName {
s.charName = name
total := s.total
s.mu.Unlock()
totals.save(key, charTotal{Name: name, Total: total})
return
}
s.mu.Unlock()
return
}
s.charKey = key
s.slot = slot
if name != "" {
s.charName = name
}
name = s.charName
// Adopt the new character's reading right away: otherwise the snapshot
// would keep showing the PREVIOUS character's deaths until the next one.
s.rawDeaths = raw
s.haveReading = true
s.total = int64(raw)
total := s.total
s.mu.Unlock()
log.Printf("character: %s (slot %d) -> %d deaths", name, slot, total)
totals.save(key, charTotal{Name: name, Total: total})
}
func (s *state) setReading(raw int32, boss bool) {
s.mu.Lock()
s.connected = true
s.playerLoaded = true
s.announced = false
changed := !s.haveReading || s.rawDeaths != raw
s.haveReading = true
s.rawDeaths = raw
s.bossFight = boss
s.lastErr = ""
total := int64(raw)
grew := total > s.total
s.total = total
key, name := s.charKey, s.charName
s.mu.Unlock()
if !changed {
return
}
if grew {
log.Printf("new death for %s -> %d", name, total)
}
totals.save(key, charTotal{Name: name, Total: total})
}
// setPlayerUnloaded: the game is still open but there's no character in
// the world (main menu or a loading screen). Unlike a disconnect, this
// does NOT clear connected: the overlay has to keep showing the total
// instead of a dash, or it would flicker on every loading screen (i.e.
// every death).
func (s *state) setPlayerUnloaded(reason string) {
s.mu.Lock()
was := s.playerLoaded
first := !s.announced
s.announced = true
s.connected = true
s.playerLoaded = false
s.bossFight = false
s.lastErr = reason
s.mu.Unlock()
if was || first {
log.Printf("no character loaded (%s) - total stays frozen until you're back in the world", reason)
}
}
func (s *state) setDisconnected(reason string) {
s.mu.Lock()
wasConnected := s.connected
s.connected = false
s.playerLoaded = false
s.bossFight = false
s.lastErr = reason
s.mu.Unlock()
if wasConnected {
log.Printf("lost the connection to the game: %s", reason)
}
}