// totals.go: los totales, uno por personaje. // // 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. // // 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. // // 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. package main import ( "encoding/json" "log" "os" "path/filepath" "strconv" "strings" "sync" ) type charTotal struct { Name string `json:"name"` Total int64 `json:"total"` // ultimo total visto: para mostrar algo antes de leer el juego } type totalsFile struct { LastCharacter string `json:"lastCharacter"` Characters map[string]*charTotal `json:"characters"` } type totalsStore struct { mu sync.Mutex path string data totalsFile } // testExeDir permite a los tests apuntar a un directorio temporal. var testExeDir string func exeDir() (string, bool) { if testExeDir != "" { return testExeDir, true } if exe, err := os.Executable(); err == nil { return filepath.Dir(exe), true } return "", false } func totalsPath() string { if dir, ok := exeDir(); ok { return filepath.Join(dir, "totals.json") } return "totals.json" } func newTotalsStore() *totalsStore { s := &totalsStore{path: totalsPath()} s.data.Characters = map[string]*charTotal{} 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) s.data = totalsFile{Characters: map[string]*charTotal{}} } if s.data.Characters == nil { s.data.Characters = map[string]*charTotal{} } } return s } func (s *totalsStore) get(key string) (*charTotal, bool) { s.mu.Lock() defer s.mu.Unlock() c, ok := s.data.Characters[key] if !ok { return nil, false } cp := *c return &cp, true } // lastKnownTotal: que mostrar mientras el juego no este abierto. func (s *totalsStore) lastKnownTotal() (key, name string, total int64) { s.mu.Lock() defer s.mu.Unlock() if c, ok := s.data.Characters[s.data.LastCharacter]; ok { return s.data.LastCharacter, c.Name, c.Total } return "", "", 0 } func (s *totalsStore) save(key string, c charTotal) { s.mu.Lock() s.data.Characters[key] = &c s.data.LastCharacter = key blob, err := json.MarshalIndent(s.data, "", " ") s.mu.Unlock() if err != nil { log.Printf("no pude serializar 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. tmp := s.path + ".tmp" if err := os.WriteFile(tmp, blob, 0644); err != nil { log.Printf("no pude escribir totals.json: %v", err) return } if err := os.Rename(tmp, s.path); err != nil { log.Printf("no pude reemplazar totals.json: %v", err) } } // characterKey: la identidad de un personaje. // // 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. func characterKey(slot int, name string) string { if slot >= 0 { return "slot" + strconv.Itoa(slot) } if n := strings.ToLower(strings.TrimSpace(name)); n != "" { return "name:" + n } return "" }