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.
197 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|