// 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) } }