From 5a2c3272e9e2f9367e40aaa1ebbb89bf050e9ab6 Mon Sep 17 00:00:00 2001 From: emmatherock Date: Thu, 17 Sep 2026 21:51:33 -0300 Subject: [PATCH] Translate remaining Go source and overlay.html comments to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- auth.go | 62 +++++++++++++++---------------- auth_test.go | 98 +++++++++++++++++++++++++------------------------ counter.go | 68 +++++++++++++++++----------------- counter_test.go | 59 ++++++++++++++--------------- i18n.go | 36 +++++++++--------- overlay.html | 42 +++++++++++---------- totals.go | 59 +++++++++++++++-------------- 7 files changed, 213 insertions(+), 211 deletions(-) diff --git a/auth.go b/auth.go index c0fe76f..f28d6ef 100644 --- a/auth.go +++ b/auth.go @@ -1,20 +1,19 @@ -// auth.go: autenticacion entre el hub y los peers. +// auth.go: authentication between the hub and its peers. // -// El transporte NO se asume confiable. Puede ser Tailscale, ZeroTier, -// WireGuard, o un puerto abierto directo a internet: eso lo decide quien -// lo usa, no nosotros. Asi que la conexion se defiende sola. +// The transport is NOT assumed trustworthy. It could be Tailscale, +// ZeroTier, WireGuard, or a port opened straight to the internet: that's +// up to whoever runs it, not us. So the connection defends itself. // -// El token es OBLIGATORIO y lo genera el programa (nadie elige "1234"), y -// nunca viaja por la red: el hub manda un desafio al azar y el peer -// responde con un HMAC del desafio usando el token como clave. Quien -// escuche el trafico no se lleva el token, y no puede repetir una -// respuesta vieja porque el desafio cambia en cada conexion. +// The token is REQUIRED and generated by the program (nobody gets to pick +// "1234"), and it never travels over the network: the hub sends a random +// challenge and the peer replies with an HMAC of it, keyed by the token. +// Anyone listening to the traffic doesn't get the token, and can't replay +// an old response because the challenge changes on every connection. // -// Lo que esto NO hace: cifrar. Los mensajes (nombre y numero de muertes) -// viajan en claro. Para un contador de muertes en un stream es -// aceptable, pero si te importa, poné la conexion adentro de una VPN o -// un tunel con TLS. Lo que el token garantiza es que nadie pueda -// INYECTAR datos falsos en tu overlay. +// This proves the PEER's identity to the hub. It doesn't encrypt +// anything by itself — that's TLS's job now, one layer up (see +// tlscert.go/pin.go), which in turn proves the HUB's identity to the +// peer via certificate pinning. Neither layer replaces the other. package main import ( @@ -33,11 +32,11 @@ import ( const ( tokenBytes = 16 // 128 bits nonceBytes = 16 - // Plazo para autenticarse. Una conexion que se queda callada tras el - // desafio se cierra en vez de ocupar un lugar para siempre. + // Deadline to authenticate. A connection that goes quiet after the + // challenge gets closed instead of holding a slot forever. authTimeoutSeconds = 10 - // Techo de conexiones simultaneas: evita que alguien deje el puerto - // sin recursos abriendo sockets. + // Cap on simultaneous connections: keeps someone from starving the + // port of resources by opening sockets. maxPeerConns = 8 ) @@ -56,12 +55,13 @@ func randomHex(n int) (string, error) { return hex.EncodeToString(b), nil } -// resolveToken decide el token efectivo. Si el config trae uno, manda ese -// (para quien quiera elegirlo a mano o compartir uno ya acordado). Si no, -// se usa el de token.txt, y si tampoco existe se genera uno y se guarda. +// resolveToken decides the effective token. If the config has one, that +// wins (for anyone who wants to pick it by hand or share a pre-agreed +// one). Otherwise it uses the one in token.txt, generating and saving one +// if that doesn't exist either. // -// Devuelve tambien si lo acaba de generar, para avisarlo fuerte en la -// consola: es lo que hay que pasarle al compañero. +// Also returns whether it was just generated, so it can be announced +// loudly on the console: that's what needs to be handed to your partner. func resolveToken(cfg config) (token string, generated bool, err error) { if t := strings.TrimSpace(cfg.Token); t != "" { return t, false, nil @@ -74,23 +74,23 @@ func resolveToken(cfg config) (token string, generated bool, err error) { } t, gerr := randomHex(tokenBytes) if gerr != nil { - return "", false, fmt.Errorf("no pude generar un token: %w", gerr) + return "", false, fmt.Errorf("couldn't generate a token: %w", gerr) } if werr := os.WriteFile(path, []byte(t), 0600); werr != nil { - return "", false, fmt.Errorf("no pude guardar token.txt: %w", werr) + return "", false, fmt.Errorf("couldn't save token.txt: %w", werr) } return t, true, nil } -// proofFor calcula la respuesta al desafio: HMAC-SHA256(token, nonce). +// proofFor computes the answer to the challenge: HMAC-SHA256(token, nonce). func proofFor(token, nonce string) string { m := hmac.New(sha256.New, []byte(token)) m.Write([]byte(nonce)) return hex.EncodeToString(m.Sum(nil)) } -// proofValid compara en tiempo constante. Con == la comparacion corta en -// el primer byte distinto, y eso filtra informacion por tiempos. +// proofValid compares in constant time. With == the comparison short- +// circuits at the first differing byte, which leaks information via timing. func proofValid(token, nonce, got string) bool { want := proofFor(token, nonce) return subtle.ConstantTimeCompare([]byte(want), []byte(got)) == 1 @@ -98,8 +98,8 @@ func proofValid(token, nonce, got string) bool { func logTokenBanner(token string, generated bool) { if generated { - log.Printf("generé un token nuevo y lo guardé en token.txt") + log.Printf("generated a new token and saved it to token.txt") } - log.Printf("token de conexion: %s", token) - log.Printf("tu compañero tiene que poner ESE token en su config.toml; sin el, su conexion se rechaza") + log.Printf("connection token: %s", token) + log.Printf("your partner needs to put THAT exact token in their config.toml; without it, their connection gets rejected") } diff --git a/auth_test.go b/auth_test.go index 597ad88..d9c259e 100644 --- a/auth_test.go +++ b/auth_test.go @@ -8,8 +8,8 @@ import ( "time" ) -// arrancaHub levanta un hub con un token dado y devuelve su direccion. -func arrancaHub(t *testing.T, token string) (*peerRegistry, string) { +// startHub spins up a hub with a given token and returns its address. +func startHub(t *testing.T, token string) (*peerRegistry, string) { t.Helper() reg := newPeerRegistry() ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -26,92 +26,94 @@ func arrancaHub(t *testing.T, token string) (*peerRegistry, string) { func peers(reg *peerRegistry) int { return len(reg.views()) } -// El token correcto entra y sus datos llegan. -func TestTokenCorrectoEntra(t *testing.T) { - reg, addr := arrancaHub(t, "el-bueno") +// The correct token gets in and its data arrives. +func TestCorrectTokenGetsIn(t *testing.T) { + reg, addr := startHub(t, "the-right-one") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() - if err := authenticate(c, config{Token: "el-bueno", clientID: "abc"}); err != nil { - t.Fatalf("deberia haber autenticado: %v", err) + if err := authenticate(c, config{Token: "the-right-one", clientID: "abc"}); err != nil { + t.Fatalf("should have authenticated: %v", err) } - msg, _ := json.Marshal(peerMessage{Type: "state", ID: "abc", Name: "Fulano", Deaths: 7}) + msg, _ := json.Marshal(peerMessage{Type: "state", ID: "abc", Name: "Somebody", Deaths: 7}) if err := c.WriteText(msg); err != nil { t.Fatal(err) } - esperarHasta(t, func() bool { return peers(reg) == 1 }, "el peer nunca aparecio") + waitUntil(t, func() bool { return peers(reg) == 1 }, "the peer never showed up") if got := reg.views()[0].Deaths; got != 7 { - t.Fatalf("esperaba 7 muertes, dio %d", got) + t.Fatalf("expected 7 deaths, got %d", got) } } -// El token equivocado NO entra y no deja rastro. -func TestTokenEquivocadoNoEntra(t *testing.T) { - reg, addr := arrancaHub(t, "el-bueno") +// The wrong token does NOT get in, and leaves no trace. +func TestWrongTokenDoesNotGetIn(t *testing.T) { + reg, addr := startHub(t, "the-right-one") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() - _ = authenticate(c, config{Token: "el-malo", clientID: "x"}) - msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruso", Deaths: 999}) + _ = authenticate(c, config{Token: "the-wrong-one", clientID: "x"}) + msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruder", Deaths: 999}) c.WriteText(msg) time.Sleep(700 * time.Millisecond) if peers(reg) != 0 { - t.Fatalf("el hub acepto datos con el token equivocado: %+v", reg.views()) + t.Fatalf("the hub accepted data with the wrong token: %+v", reg.views()) } } -// Mandar datos SIN autenticarse tampoco funciona. -func TestSinAutenticarNoEntra(t *testing.T) { - reg, addr := arrancaHub(t, "el-bueno") +// Sending data WITHOUT authenticating doesn't work either. +func TestUnauthenticatedDoesNotGetIn(t *testing.T) { + reg, addr := startHub(t, "the-right-one") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() - msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruso", Deaths: 999}) + msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruder", Deaths: 999}) c.WriteText(msg) time.Sleep(700 * time.Millisecond) if peers(reg) != 0 { - t.Fatalf("el hub acepto datos sin autenticacion: %+v", reg.views()) + t.Fatalf("the hub accepted data with no authentication: %+v", reg.views()) } } -// El token no viaja: lo unico que sale del peer es un HMAC del desafio. -func TestElTokenNoViajaPorLaRed(t *testing.T) { - _, addr := arrancaHub(t, "secreto-muy-secreto") +// The token never travels: the only thing that leaves the peer is an +// HMAC of the challenge. +func TestTheTokenNeverTravelsOverTheWire(t *testing.T) { + _, addr := startHub(t, "very-secret-secret") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() - raw, err := c.ReadMessage() // el desafio + raw, err := c.ReadMessage() // the challenge if err != nil { t.Fatal(err) } var ch authChallenge json.Unmarshal(raw, &ch) if ch.Nonce == "" { - t.Fatal("el hub no mando desafio") + t.Fatal("the hub didn't send a challenge") } - reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofFor("secreto-muy-secreto", ch.Nonce)}) + reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofFor("very-secret-secret", ch.Nonce)}) if string(reply) == "" { - t.Fatal("respuesta vacia") + t.Fatal("empty reply") } for _, b := range [][]byte{raw, reply} { - if contiene(string(b), "secreto-muy-secreto") { - t.Fatalf("el token aparece en el trafico: %s", b) + if contains(string(b), "very-secret-secret") { + t.Fatalf("the token shows up in the traffic: %s", b) } } } -// Una respuesta capturada no sirve en otra conexion: el desafio cambia. -func TestNoSePuedeRepetirUnaRespuestaVieja(t *testing.T) { - reg, addr := arrancaHub(t, "el-bueno") +// A captured response can't be replayed on another connection: the +// challenge changes. +func TestAnOldResponseCannotBeReplayed(t *testing.T) { + reg, addr := startHub(t, "the-right-one") c1, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { @@ -120,7 +122,7 @@ func TestNoSePuedeRepetirUnaRespuestaVieja(t *testing.T) { raw, _ := c1.ReadMessage() var ch1 authChallenge json.Unmarshal(raw, &ch1) - proofViejo := proofFor("el-bueno", ch1.Nonce) + oldProof := proofFor("the-right-one", ch1.Nonce) c1.Close() c2, err := wsDial(addr, "/ws", 3*time.Second) @@ -132,43 +134,43 @@ func TestNoSePuedeRepetirUnaRespuestaVieja(t *testing.T) { var ch2 authChallenge json.Unmarshal(raw2, &ch2) if ch2.Nonce == ch1.Nonce { - t.Fatal("el desafio se repitio entre conexiones: el nonce tiene que ser distinto") + t.Fatal("the challenge repeated across connections: the nonce has to differ") } - reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofViejo}) + reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: oldProof}) c2.WriteText(reply) - msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Repetidor", Deaths: 1}) + msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Replayer", Deaths: 1}) c2.WriteText(msg) time.Sleep(700 * time.Millisecond) if peers(reg) != 0 { - t.Fatalf("el hub acepto una respuesta repetida: %+v", reg.views()) + t.Fatalf("the hub accepted a replayed response: %+v", reg.views()) } } -// El token generado es aleatorio y de largo razonable. -func TestTokenGeneradoEsFuerte(t *testing.T) { +// The generated token is random and of a reasonable length. +func TestGeneratedTokenIsStrong(t *testing.T) { testExeDir = t.TempDir() a, gen, err := resolveToken(config{}) if err != nil || !gen { - t.Fatalf("deberia haber generado uno: gen=%v err=%v", gen, err) + t.Fatalf("should have generated one: gen=%v err=%v", gen, err) } if len(a) != tokenBytes*2 { - t.Fatalf("largo inesperado: %d", len(a)) + t.Fatalf("unexpected length: %d", len(a)) } - // la segunda vez lo lee del archivo, no genera otro + // the second time it reads it from the file, doesn't generate another b, gen2, _ := resolveToken(config{}) if gen2 || a != b { - t.Fatalf("deberia reusar el guardado: %q vs %q (gen=%v)", a, b, gen2) + t.Fatalf("should reuse the saved one: %q vs %q (gen=%v)", a, b, gen2) } - // y en otra instalacion sale distinto + // and a different installation gets a different one testExeDir = t.TempDir() c, _, _ := resolveToken(config{}) if c == a { - t.Fatal("dos instalaciones generaron el mismo token") + t.Fatal("two installations generated the same token") } testExeDir = "" } -func contiene(s, sub string) bool { +func contains(s, sub string) bool { return len(sub) > 0 && len(s) >= len(sub) && (func() bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { @@ -179,7 +181,7 @@ func contiene(s, sub string) bool { })() } -func esperarHasta(t *testing.T, cond func() bool, msg string) { +func waitUntil(t *testing.T, cond func() bool, msg string) { t.Helper() deadline := time.Now().Add(4 * time.Second) for time.Now().Before(deadline) { diff --git a/counter.go b/counter.go index e988077..dbb807c 100644 --- a/counter.go +++ b/counter.go @@ -1,11 +1,10 @@ -// counter.go: la contabilidad de muertes. +// counter.go: death accounting. // -// 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. +// 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 ( @@ -14,9 +13,9 @@ import ( "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. +// 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 @@ -45,8 +44,8 @@ type stateSnapshot struct { 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. +// 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() @@ -56,7 +55,7 @@ func (s *state) init(store *totalsStore) { s.total = total s.mu.Unlock() if name != "" { - log.Printf("ultimo personaje conocido: %s con %d muertes", name, total) + log.Printf("last known character: %s with %d deaths", name, total) } } @@ -84,32 +83,32 @@ func (s *state) name() string { 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 { +// 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 } - // Vacio a proposito: que la etiqueta la elija la interfaz, que es la - // que sabe si esta mostrando un jugador o dos. + // 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 se llama cuando cambia el personaje cargado: el total -// pasa a ser el que el juego lleva para ese personaje. +// 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 // ni slot ni nombre: no sabemos a quien imputarle esto + return // neither slot nor name: no one to attribute this to } 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. + // 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 @@ -126,16 +125,15 @@ func (s *state) setCharacter(slot int, name string, raw int32) { 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. + // 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("personaje: %s (slot %d) -> %d muertes", name, slot, total) + log.Printf("character: %s (slot %d) -> %d deaths", name, slot, total) totals.save(key, charTotal{Name: name, Total: total}) } @@ -159,16 +157,16 @@ func (s *state) setReading(raw int32, boss bool) { return } if grew { - log.Printf("nueva muerte de %s -> %d", name, total) + log.Printf("new death for %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). +// 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 @@ -180,7 +178,7 @@ func (s *state) setPlayerUnloaded(reason string) { 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) + log.Printf("no character loaded (%s) - total stays frozen until you're back in the world", reason) } } @@ -193,6 +191,6 @@ func (s *state) setDisconnected(reason string) { s.lastErr = reason s.mu.Unlock() if wasConnected { - log.Printf("se perdio la conexion con el juego: %s", reason) + log.Printf("lost the connection to the game: %s", reason) } } diff --git a/counter_test.go b/counter_test.go index 7cc6f54..322d2d7 100644 --- a/counter_test.go +++ b/counter_test.go @@ -6,8 +6,8 @@ import ( "testing" ) -// cada test corre en su propio directorio: totals.json y offset.txt -// se resuelven contra el ejecutable, asi que lo apuntamos al temporal. +// each test runs in its own directory: totals.json and offset.txt resolve +// relative to the executable, so we point that at the temp dir. func fresh(t *testing.T, legacy string) { t.Helper() dir := t.TempDir() @@ -24,90 +24,91 @@ func fresh(t *testing.T, legacy string) { 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) { +// THE EMMA CASE: switching characters has to move the number RIGHT AWAY, +// without waiting for anyone to die. +func TestCharacterSwitchUpdatesInstantly(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) + t.Fatalf("emma should start at her 40 deaths, got %d", got) } - // se va al menu y carga otro personaje, con otro conteo propio + // goes to the menu and loads another character, with its own count 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) + t.Fatalf("loading emmatherock should jump the total to 7 WITHOUT dying, got %d", got) } st.setReading(7, false) if got := total(t); got != 7 { - t.Fatalf("tras la lectura deberia seguir en 7, dio %d", got) + t.Fatalf("after the reading it should still be 7, got %d", got) } - // y volver muestra lo que el juego lleva para emma + // and switching back shows what the game tracks for 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) + t.Fatalf("switching back to emma should give 41, got %d", got) } } -// Dos personajes con el MISMO nombre no se tienen que mezclar. -func TestHomonimosNoSeMezclan(t *testing.T) { +// Two characters with the SAME name must not get mixed up. +func TestHomonymsDontMix(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) + t.Fatalf("slot 3's should give 5, got %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) + t.Fatalf("slot 0's should still be 100, got %+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) { +// The total is the game's, no matter if a stale offset.txt is lying +// around next to the program. +func TestIgnoresStaleOffset(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) + t.Fatalf("should show the game's 30, not the old file's 124, got %d", got) } } -// Sin poder leer el slot, cae al nombre y sigue funcionando. -func TestSinSlotCaeAlNombre(t *testing.T) { +// Without being able to read the slot, it falls back to the name and +// keeps working. +func TestNoSlotFallsBackToName(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) + t.Fatalf("should give 40, got %d", got) } if _, ok := totals.get("name:emma"); !ok { - t.Fatal("deberia haberlo guardado por nombre") + t.Fatal("should have saved it by name") } } -// Al reabrir el programa con el juego cerrado, muestra el ultimo -// personaje conocido en vez de un cero. -func TestRecuerdaElUltimoAlReabrir(t *testing.T) { +// Reopening the program with the game closed shows the last known +// character instead of a zero. +func TestRemembersTheLastOneOnReopen(t *testing.T) { fresh(t, "") st.setCharacter(2, "emma", 88) st.setReading(88, false) - totals = newTotalsStore() // simula reabrir el programa + totals = newTotalsStore() // simulates reopening the program st = state{} st.init(totals) if got := total(t); got != 88 { - t.Fatalf("al reabrir deberia mostrar 88, dio %d", got) + t.Fatalf("reopening should show 88, got %d", got) } if st.snapshot().CharName != "emma" { - t.Fatalf("y recordar el nombre, dio %q", st.snapshot().CharName) + t.Fatalf("and remember the name, got %q", st.snapshot().CharName) } } diff --git a/i18n.go b/i18n.go index 98d6c4a..f47a982 100644 --- a/i18n.go +++ b/i18n.go @@ -1,15 +1,15 @@ //go:build windows -// i18n.go: idiomas de la interfaz. +// i18n.go: interface languages. // -// Las traducciones viven en locales/*.json y se embeben en el ejecutable. -// La razon de elegir archivos sueltos en vez de un mapa en el codigo es -// que agregar un idioma sea "copiar en.json, traducirlo, y listo": quien -// traduce no necesita saber Go ni tocar una linea de codigo. +// Translations live in locales/*.json and get embedded into the binary. +// The reason for separate files instead of a map in the code is so +// adding a language is "copy en.json, translate it, done": whoever +// translates doesn't need to know Go or touch a line of code. // -// Los mensajes de la consola NO pasan por aca a proposito: son -// diagnostico, y conviene que esten siempre en el mismo idioma para que -// un log pegado en un issue se pueda leer sin importar de donde venga. +// Console messages deliberately do NOT go through here: they're +// diagnostics, and it helps if they're always in the same language so a +// log pasted into an issue reads the same no matter where it came from. package main import ( @@ -33,7 +33,7 @@ var locales = map[string]map[string]string{} func loadLocales() { entries, err := localeFS.ReadDir("locales") if err != nil { - log.Printf("no pude leer los idiomas embebidos: %v", err) + log.Printf("couldn't read the embedded languages: %v", err) return } for _, e := range entries { @@ -43,12 +43,12 @@ func loadLocales() { } data, err := localeFS.ReadFile(path.Join("locales", name)) if err != nil { - log.Printf("no pude leer locales/%s: %v", name, err) + log.Printf("couldn't read locales/%s: %v", name, err) continue } var m map[string]string if err := json.Unmarshal(data, &m); err != nil { - log.Printf("locales/%s tiene JSON invalido: %v", name, err) + log.Printf("locales/%s has invalid JSON: %v", name, err) continue } locales[strings.TrimSuffix(name, ".json")] = m @@ -66,7 +66,7 @@ func availableLangs() []string { var procGetUserDefaultLocaleName = kernel32.NewProc("GetUserDefaultLocaleName") -// systemLang devuelve el idioma de Windows ("es-AR" -> "es"). +// systemLang returns Windows's language ("es-AR" -> "es"). func systemLang() string { buf := make([]uint16, 85) // LOCALE_NAME_MAX_LENGTH r, _, _ := procGetUserDefaultLocaleName.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) @@ -80,8 +80,8 @@ func systemLang() string { return strings.ToLower(name) } -// resolveLang decide el idioma final. "auto" (o vacio) usa el de -// Windows; si ese idioma no esta traducido, cae a ingles. +// resolveLang decides the final language. "auto" (or empty) uses +// Windows's; if that language isn't translated, it falls back to English. func resolveLang(want string) string { want = strings.ToLower(strings.TrimSpace(want)) if want == "" || want == "auto" { @@ -93,7 +93,7 @@ func resolveLang(want string) string { if _, ok := locales[want]; ok { return want } - // "es-AR" o "es_AR" igual tienen que encontrar "es". + // "es-AR" or "es_AR" should still resolve to "es". if base, _, ok := strings.Cut(strings.NewReplacer("_", "-").Replace(want), "-"); ok { if _, ok := locales[base]; ok { return base @@ -102,9 +102,9 @@ func resolveLang(want string) string { return fallbackLang } -// stringsFor devuelve el diccionario del idioma pedido, completado con -// el ingles: si una traduccion quedo a medias, se ve la clave que falta -// en ingles y no la clave cruda. +// stringsFor returns the requested language's dictionary, filled in with +// English: if a translation is incomplete, the missing key shows up in +// English instead of as a raw key. func stringsFor(lang string) map[string]string { out := map[string]string{} for k, v := range locales[fallbackLang] { diff --git a/overlay.html b/overlay.html index b2a7510..d26ea8b 100644 --- a/overlay.html +++ b/overlay.html @@ -40,8 +40,8 @@ border-top:1px solid var(--gold); border-bottom:1px solid var(--gold); padding:18px 24px; width:600px; box-sizing:border-box; box-shadow:var(--shadow); } - /* Con un solo jugador la placa se angosta; el canvas de OBS no cambia, - simplemente ocupa menos. Asi las dimensiones se configuran una vez. */ + /* With a single player the plate narrows; the OBS canvas doesn't + change, it just takes up less of it. So the dimensions get set once. */ .plate.solo{width:420px;} .players{display:flex;align-items:stretch;justify-content:center;} .pcol{flex:1 1 0;min-width:0;text-align:center;} @@ -55,9 +55,10 @@ transition:color .15s ease, transform .15s ease; } .pnum.pulse{color:var(--gold-bright);transform:scale(1.06);} - /* Una sola linea bajo el numero: dice "muertes" y, cuando hay algo que - contar (jefe, sin conexion), lo agrega ahi mismo. Asi la palabra - siempre esta y el alto del overlay no cambia nunca. */ + /* A single line under the number: says "deaths" and, when there's + something else to report (boss, no connection), appends it right + there. That way the word is always present and the overlay's height + never changes. */ .pnote{ font-family:'Cinzel', Georgia, serif;font-size:.7rem;letter-spacing:.22em;text-transform:uppercase; color:var(--muted);min-height:1.5em;margin-top:.35rem; @@ -166,9 +167,9 @@ (function(){ var mode = window.__mode || 'control'; - // Textos de la interfaz. El diccionario real viene de /strings.json - // (ver i18n.go y locales/*.json). Este de aca es el bote salvavidas: - // si esa peticion falla, se ve ingles y nunca una clave cruda. + // Interface text. The real dictionary comes from /strings.json (see + // i18n.go and locales/*.json). This one here is the lifeboat: if that + // request fails, you see English and never a raw key. var FALLBACK = { "overlay.deaths": "deaths", "overlay.boss": "boss", @@ -239,18 +240,18 @@ el.className = 'status' + (cls ? ' ' + cls : ''); } - // Cache de las columnas: reconstruir el DOM en cada tick haria - // parpadear el overlay y perderia la animacion del pulso. + // Column cache: rebuilding the DOM on every tick would make the + // overlay flicker and lose the pulse animation. // - // Se indexan por POSICION, nunca por nombre. Indexarlas por nombre fue - // un bug real: al cambiar de personaje cambiaba la clave, se creaba una - // columna nueva y la vieja quedaba colgada en el DOM, asi que un mismo - // jugador aparecia dos veces. El nombre es contenido, no identidad. + // Indexed by POSITION, never by name. Indexing by name was a real bug: + // switching characters changed the key, spawning a new column and + // leaving the old one stuck in the DOM, so the same player showed up + // twice. The name is content, not identity. var slots = []; var prevByName = {}; - // ensureSlots deja exactamente n columnas, creando o sacando las que - // hagan falta (con su divisor). + // ensureSlots leaves exactly n columns, creating or removing whatever's + // needed (with its divider). function ensureSlots(list, n){ while (slots.length > n){ var old = slots.pop(); @@ -291,7 +292,7 @@ for (var i = 0; i < players.length; i++){ var p = players[i]; var slot = slots[i]; - // Sin nombre de personaje todavía: etiqueta según el modo. + // No character name yet: label based on the mode. slot.name.textContent = p.name || (players.length < 2 ? t('overlay.deaths') : (p.self ? t('overlay.you') : t('overlay.partner'))); slot.num.textContent = p.connected ? p.deaths : '–'; @@ -363,13 +364,14 @@ setTimeout(poll, 1000); }); } - // ?lang= en la URL pisa el idioma del config, comodo para tener el - // overlay en un idioma y el panel en otro sin tocar archivos. + // ?lang= in the URL overrides the config's language, handy for having + // the overlay in one language and the panel in another without + // touching any files. var langParam = new URLSearchParams(location.search).get('lang'); fetch('/strings.json' + (langParam ? '?lang=' + encodeURIComponent(langParam) : ''), {cache:'no-store'}) .then(function(r){ return r.json(); }) .then(function(d){ if (d && typeof d === 'object') STR = d; }) - .catch(function(){ /* nos quedamos con FALLBACK */ }) + .catch(function(){ /* stick with FALLBACK */ }) .finally(function(){ applyStrings(); poll(); }); })(); diff --git a/totals.go b/totals.go index f34f691..4c8df10 100644 --- a/totals.go +++ b/totals.go @@ -1,21 +1,21 @@ -// totals.go: los totales, uno por personaje. +// totals.go: the totals, one per character. // -// El juego ya lleva la cuenta de muertes de cada personaje por separado -// (esta en la partida, no en la PC), asi que el programa hace lo mismo: -// para cada personaje guarda un "base", y el numero que se muestra es -// siempre total = muertes que lee del juego + base. +// The game already tracks each character's death count separately (it's +// in the save, not on the PC), so the program does the same: for each +// character it saves a "base", and the number shown is always +// total = deaths read from the game + base. // -// El numero que se muestra es, sin intermediarios, el que lleva el juego -// para ese personaje. Lo que se guarda en disco NO es un ajuste: es solo -// lo ultimo que se vio, para poder mostrar algo cuando abris el contador -// antes que el juego y todavia no hay nada que leer. +// The number shown is, with nothing in between, whatever the game tracks +// for that character. What's saved to disk is NOT an adjustment: it's +// just the last thing seen, so there's something to show if you open the +// counter before the game and there's nothing to read yet. // -// Hubo dos modelos anteriores. Primero un unico total de por vida que -// nunca bajaba, que con varios personajes dejaba el numero quieto hasta -// la siguiente muerte. Despues un "base" por personaje que se sumaba a -// la lectura, heredado de cuando no confiabamos en lo que leiamos: una -// vez que el total sale del juego en cada lectura, corregir a mano dejo -// de arreglar nada y solo servia para que el numero mintiera en silencio. +// There were two earlier models. First a single lifetime total that +// never went down, which with multiple characters left the number stuck +// until the next death. Then a per-character "base" added to the +// reading, inherited from when we didn't trust what we were reading: +// once the total comes straight from the game on every read, correcting +// it by hand stopped fixing anything and just made the number lie quietly. package main import ( @@ -30,7 +30,7 @@ import ( type charTotal struct { Name string `json:"name"` - Total int64 `json:"total"` // ultimo total visto: para mostrar algo antes de leer el juego + Total int64 `json:"total"` // last total seen: shows something before the game is read } type totalsFile struct { @@ -44,7 +44,7 @@ type totalsStore struct { data totalsFile } -// testExeDir permite a los tests apuntar a un directorio temporal. +// testExeDir lets tests point this at a temp directory. var testExeDir string func exeDir() (string, bool) { @@ -71,7 +71,7 @@ func newTotalsStore() *totalsStore { data, err := os.ReadFile(s.path) if err == nil { if err := json.Unmarshal(data, &s.data); err != nil { - log.Printf("totals.json ilegible (%v): arranco de cero, no lo piso hasta la proxima escritura", err) + log.Printf("totals.json is unreadable (%v): starting fresh, won't overwrite it until the next write", err) s.data = totalsFile{Characters: map[string]*charTotal{}} } if s.data.Characters == nil { @@ -92,7 +92,7 @@ func (s *totalsStore) get(key string) (*charTotal, bool) { return &cp, true } -// lastKnownTotal: que mostrar mientras el juego no este abierto. +// lastKnownTotal: what to show while the game isn't open. func (s *totalsStore) lastKnownTotal() (key, name string, total int64) { s.mu.Lock() defer s.mu.Unlock() @@ -109,28 +109,27 @@ func (s *totalsStore) save(key string, c charTotal) { blob, err := json.MarshalIndent(s.data, "", " ") s.mu.Unlock() if err != nil { - log.Printf("no pude serializar totals.json: %v", err) + log.Printf("couldn't serialize totals.json: %v", err) return } - // Escritura atomica: si se corta la luz a mitad, el archivo viejo - // queda entero en vez de quedar un JSON cortado por la mitad. + // Atomic write: if the power cuts mid-write, the old file stays intact + // instead of ending up as a JSON file cut in half. tmp := s.path + ".tmp" if err := os.WriteFile(tmp, blob, 0644); err != nil { - log.Printf("no pude escribir totals.json: %v", err) + log.Printf("couldn't write totals.json: %v", err) return } if err := os.Rename(tmp, s.path); err != nil { - log.Printf("no pude reemplazar totals.json: %v", err) + log.Printf("couldn't replace totals.json: %v", err) } } -// characterKey: la identidad de un personaje. +// characterKey: a character's identity. // -// El slot de guardado (GameMan+0xAC0) es la identidad buena: no cambia -// nunca y dos personajes que se llamen igual siguen siendo distintos. El -// nombre queda solo como respaldo para cuando no podemos leer el slot, -// con la limitacion conocida de que ahi si dos homonimos comparten -// contador. +// The save slot (GameMan+0xAC0) is the good identity: it never changes, +// and two characters with the same name stay distinct. The name is only +// a fallback for when we can't read the slot, with the known limitation +// that there, two homonyms DO share a counter. func characterKey(slot int, name string) string { if slot >= 0 { return "slot" + strconv.Itoa(slot)