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:
emmatherock committed 2026-09-17 21:10:22 -03:00
commit e1f7e6f529
18 files changed
+3690

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
package main
import (
"os"
"path/filepath"
"testing"
)
// cada test corre en su propio directorio: totals.json y offset.txt
// se resuelven contra el ejecutable, asi que lo apuntamos al temporal.
func fresh(t *testing.T, legacy string) {
t.Helper()
dir := t.TempDir()
testExeDir = dir
if legacy != "" {
if err := os.WriteFile(filepath.Join(dir, "offset.txt"), []byte(legacy), 0644); err != nil {
t.Fatal(err)
}
}
totals = newTotalsStore()
st = state{}
st.init(totals)
}
func total(t *testing.T) int64 { t.Helper(); return st.snapshot().Total }
// EL CASO DE EMMA: cambiar de personaje tiene que mover el numero YA,
// sin esperar a que alguien se muera.
func TestCambioDePersonajeActualizaAlInstante(t *testing.T) {
fresh(t, "")
st.setCharacter(0, "emma", 40)
st.setReading(40, false)
if got := total(t); got != 40 {
t.Fatalf("emma deberia arrancar con sus 40 muertes, dio %d", got)
}
// se va al menu y carga otro personaje, con otro conteo propio
st.setPlayerUnloaded("menu")
st.setCharacter(1, "emmatherock", 7)
if got := total(t); got != 7 {
t.Fatalf("al cargar emmatherock el total tenia que saltar a 7 SIN morir, dio %d", got)
}
st.setReading(7, false)
if got := total(t); got != 7 {
t.Fatalf("tras la lectura deberia seguir en 7, dio %d", got)
}
// y volver muestra lo que el juego lleva para emma
st.setPlayerUnloaded("menu")
st.setCharacter(0, "emma", 41)
st.setReading(41, false)
if got := total(t); got != 41 {
t.Fatalf("volviendo a emma deberia dar 41, dio %d", got)
}
}
// Dos personajes con el MISMO nombre no se tienen que mezclar.
func TestHomonimosNoSeMezclan(t *testing.T) {
fresh(t, "")
st.setCharacter(0, "Tarnished", 100)
st.setReading(100, false)
st.setCharacter(3, "Tarnished", 5)
st.setReading(5, false)
if got := total(t); got != 5 {
t.Fatalf("el del slot 3 deberia dar 5, dio %d", got)
}
if c, ok := totals.get("slot0"); !ok || c.Total != 100 {
t.Fatalf("el del slot 0 deberia seguir en 100, dio %+v", c)
}
}
// El total es el del juego, sin importar que haya un offset.txt viejo
// dando vueltas al lado del programa.
func TestIgnoraElOffsetViejo(t *testing.T) {
fresh(t, "124")
st.setCharacter(0, "emma", 30)
st.setReading(30, false)
if got := total(t); got != 30 {
t.Fatalf("deberia mostrar las 30 del juego y no las 124 del archivo viejo, dio %d", got)
}
}
// Sin poder leer el slot, cae al nombre y sigue funcionando.
func TestSinSlotCaeAlNombre(t *testing.T) {
fresh(t, "")
st.setCharacter(-1, "emma", 40)
st.setReading(40, false)
if got := total(t); got != 40 {
t.Fatalf("deberia dar 40, dio %d", got)
}
if _, ok := totals.get("name:emma"); !ok {
t.Fatal("deberia haberlo guardado por nombre")
}
}
// Al reabrir el programa con el juego cerrado, muestra el ultimo
// personaje conocido en vez de un cero.
func TestRecuerdaElUltimoAlReabrir(t *testing.T) {
fresh(t, "")
st.setCharacter(2, "emma", 88)
st.setReading(88, false)
totals = newTotalsStore() // simula reabrir el programa
st = state{}
st.init(totals)
if got := total(t); got != 88 {
t.Fatalf("al reabrir deberia mostrar 88, dio %d", got)
}
if st.snapshot().CharName != "emma" {
t.Fatalf("y recordar el nombre, dio %q", st.snapshot().CharName)
}
}