// deathwatch: reads Elden Ring's death counter read-only, straight from // the process (the same byte pattern / offset used by LiveSplit's // "eldenring_boss_timer.asl" ASL script, verified by hand). Writes // nothing to the game's memory. Runs on Windows natively and on Linux // against the game running under Proton — same signatures and offsets // either way, since it's the same Windows binary in memory; see // process.go/process_windows.go/process_linux.go for the split. // // Exposes: // // GET / -> status panel (HTML) // GET /?view=overlay -> transparent version for OBS Browser Source // GET /deaths -> {"deaths":N,"players":[...],"character":"...","slot":N,...} package main import ( "crypto/tls" _ "embed" "encoding/json" "log" "net/http" "os" "strings" ) //go:embed overlay.html var overlayHTML []byte // portOf pulls the port out of an address like "0.0.0.0:47822", so your // partner can be told exactly what to put in their config. func portOf(addr string) string { if _, port, ok := strings.Cut(addr, ":"); ok { return port } return addr } func withCORS(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Cache-Control", "no-store") } func main() { log.SetFlags(log.Ltime) log.Println("=== Elden Ring Death Counter (local, read-only) ===") loadLocales() cfg := loadConfig() writeSampleConfig() isPeer := cfg.Mode == "peer" lang := resolveLang(cfg.Language) log.Printf("version: %s | PID %d | mode %s | language %s (available: %s)", buildTag, os.Getpid(), cfg.Mode, lang, strings.Join(availableLangs(), ", ")) totals = newTotalsStore() st.init(totals) go pollLoop() registry := newPeerRegistry() // The token is required on both ends: without it, anyone who can // reach the port could inject data into the overlay. The hub also // generates its own TLS certificate the first time: the peer pins it // by fingerprint (pin.go), not by certificate-authority trust, which // doesn't exist for a Tailscale or LAN address anyway. var token string var hubCert tls.Certificate var certFingerprint string if !isPeer { t, generated, err := resolveToken(cfg) if err != nil { log.Fatalf("couldn't prepare the token: %v", err) } token = t logTokenBanner(token, generated) cert, fp, certGenerated, err := loadOrCreateHubCert() if err != nil { log.Fatalf("couldn't prepare the TLS certificate for the peer link: %v", err) } if certGenerated { log.Println("generated a new TLS certificate for the peer link") } hubCert, certFingerprint = cert, fp candidates := candidateIPv4s() host := pickBestHost(candidates) if len(candidates) > 0 { log.Printf("detected network addresses: %s", strings.Join(candidates, ", ")) } peerPort := portOf(cfg.PeerListen) if host != "" { invite := encodeInvite(inviteCode{Host: host, Port: peerPort, Fingerprint: certFingerprint, Token: token}) log.Println("invite code for your co-op partner — paste it as invite = \"...\" in their config.toml:") log.Println(invite) log.Printf("(wrong address? they can override just the host with hub = \":%s\")", peerPort) } else { log.Println("couldn't auto-detect a network address to build an invite code with.") log.Printf("have your partner set these by hand in their config.toml: hub = \":%s\", token = \"%s\", fingerprint = \"%s\"", peerPort, token, certFingerprint) } } var peerHub string if isPeer { hub, tok, fp, ok, err := resolvePeerConn(cfg) if err != nil { log.Fatalf("peer config problem: %v", err) } if ok { peerHub = hub go peerLoop(cfg, hub, tok, fp) } else { log.Println("peer mode with nothing configured yet (no 'invite', no 'hub'+'token'+'fingerprint'): running as a local-only overlay for now") } } else { registry.declare(cfg.Partner) } mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("X-Build", buildTag) w.Write(overlayHTML) }) // The UI's text: the page requests it once on load. ?lang= lets you // force a language without touching the config, handy for having the // overlay in one language and the panel in another. mux.HandleFunc("/strings.json", func(w http.ResponseWriter, r *http.Request) { withCORS(w) want := lang if q := r.URL.Query().Get("lang"); q != "" { want = resolveLang(q) } w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(stringsFor(want)) }) mux.HandleFunc("/deaths", func(w http.ResponseWriter, r *http.Request) { withCORS(w) snap := st.snapshot() // This player first, then partners in order of appearance. players := []playerView{{ Name: displayName(cfg), Deaths: snap.Total, BossFight: snap.BossFight, PlayerLoaded: snap.PlayerLoaded, Connected: snap.Connected, Self: true, }} // Co-op mode only when a partner is (or was just recently) connected. if registry.coopMode() { players = append(players, registry.views()...) } var combined int64 for _, p := range players { combined += p.Deaths } resp := map[string]interface{}{ "players": players, "combined": combined, "build": buildTag, // Single-player-version fields: kept so nothing already // pointing at them breaks. "deaths": snap.Total, "rawDeaths": snap.RawDeaths, "character": snap.CharName, "slot": snap.Slot, "bossFight": snap.BossFight, "connected": snap.Connected, "playerLoaded": snap.PlayerLoaded, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) log.Printf("Panel: http://%s/", cfg.Listen) if isPeer { if peerHub != "" { log.Printf("Pushing the counter to the hub %s. This window needs to stay open while you play.", peerHub) } if err := http.ListenAndServe(cfg.Listen, mux); err != nil { log.Fatalf("couldn't start the local server: %v", err) } return } log.Printf("OBS URL: http://%s/?view=overlay", cfg.Listen) log.Println("Keep this window open while you stream. Ctrl+C to close.") // Panel/overlay in plain HTTP, in the background; the peer server // (TLS, with the certificate from above) blocks in the foreground as // the process's main server. go func() { if err := http.ListenAndServe(cfg.Listen, mux); err != nil { log.Fatalf("couldn't start the local server: %v", err) } }() peerMux := http.NewServeMux() peerMux.HandleFunc("/ws", registry.wsHandler(token)) peerSrv := &http.Server{ Addr: cfg.PeerListen, Handler: peerMux, TLSConfig: hubServerTLSConfig(hubCert), } log.Printf("Peer link: %s (TLS, certificate pinned)", cfg.PeerListen) if err := peerSrv.ListenAndServeTLS("", ""); err != nil { log.Fatalf("couldn't start the peer server: %v", err) } }