Initial commit: DeathWatch, an Elden Ring death counter for OBS
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.
This commit is contained in:
commit
e1f7e6f529
18 files changed
+3690
No files matched your search
+198
@@ -0,0 +1,198 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user