Reads the game's process memory read-only (AOB signature scanning, re-resolved every tick — never a cached pointer) and serves a live death counter as a browser-source overlay plus a status panel. Supports co-op: each player runs the program locally, one acts as hub. The peer link is authenticated (HMAC challenge/response, no token on the wire, constant-time compare) but not yet encrypted — see CLAUDE.md's TODO section for the planned TLS+pinning split. Zero external dependencies — including a hand-rolled WebSocket implementation — so the whole thing ships as one .exe.
199 lines
4.9 KiB
Go
199 lines
4.9 KiB
Go
// counter.go: la contabilidad de muertes.
|
|
//
|
|
// A proposito NO tiene nada especifico de Windows: toda la lectura de
|
|
// memoria vive en main.go. Asi esta parte, que es la que concentra las
|
|
// reglas (a que personaje va cada muerte, que pasa al cambiar de
|
|
// partida, que se muestra con el juego cerrado), se puede compilar y
|
|
// testear en cualquier plataforma. Las tres veces que algo conto mal, el
|
|
// error estuvo aca.
|
|
package main
|
|
|
|
import (
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// El total que se muestra ES el que lleva el juego para el personaje
|
|
// cargado. No hay ajustes ni acumuladores propios: al cambiar de
|
|
// personaje el numero salta al suyo en la misma lectura.
|
|
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 arranca mostrando el ultimo personaje conocido, para que el
|
|
// overlay tenga un numero con sentido antes de que abras el juego.
|
|
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("ultimo personaje conocido: %s con %d muertes", 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
|
|
}
|
|
|
|
// nombreMostrado: lo que diga el config manda; si esta vacio usamos el
|
|
// nombre del personaje que leemos del juego.
|
|
func nombreMostrado(cfg config) string {
|
|
if n := strings.TrimSpace(cfg.Name); n != "" {
|
|
return n
|
|
}
|
|
if n := st.name(); n != "" {
|
|
return n
|
|
}
|
|
// Vacio a proposito: que la etiqueta la elija la interfaz, que es la
|
|
// que sabe si esta mostrando un jugador o dos.
|
|
return ""
|
|
}
|
|
|
|
// setCharacter se llama cuando cambia el personaje cargado: el total
|
|
// pasa a ser el que el juego lleva para ese personaje.
|
|
func (s *state) setCharacter(slot int, name string, raw int32) {
|
|
key := characterKey(slot, name)
|
|
if key == "" {
|
|
return // ni slot ni nombre: no sabemos a quien imputarle esto
|
|
}
|
|
|
|
s.mu.Lock()
|
|
if key == s.charKey && s.haveReading {
|
|
// Mismo personaje: el nombre puede haber aparecido recien, porque
|
|
// el slot se lee antes que el nombre.
|
|
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
|
|
// Adoptamos ya la lectura del personaje nuevo: si no, el snapshot
|
|
// seguiria mostrando las muertes del personaje ANTERIOR hasta la
|
|
// siguiente muerte.
|
|
s.rawDeaths = raw
|
|
s.haveReading = true
|
|
s.total = int64(raw)
|
|
total := s.total
|
|
s.mu.Unlock()
|
|
|
|
log.Printf("personaje: %s (slot %d) -> %d muertes", 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("nueva muerte de %s -> %d", name, total)
|
|
}
|
|
totals.save(key, charTotal{Name: name, Total: total})
|
|
}
|
|
|
|
// setPlayerUnloaded: el juego sigue abierto pero no hay personaje en el
|
|
// mundo (menu principal o pantalla de carga). A diferencia de una
|
|
// desconexion, NO apagamos connected: el overlay tiene que seguir
|
|
// mostrando el total y no un guion, porque si no parpadearia en cada
|
|
// pantalla de carga (o sea, en cada muerte).
|
|
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("sin personaje cargado (%s) - el total queda congelado hasta que vuelvas al mundo", 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("se perdio la conexion con el juego: %s", reason)
|
|
}
|
|
}
|