Elden Ring under Proton on Linux is the same Windows binary, so every AOB signature and memory offset is unchanged — only how the process gets found and read differs. Split main.go/i18n.go (previously Windows-only) into a portable process.go (signature scanning, pointer resolution, the poll loop) plus process_windows.go/process_linux.go behind a small boundary: findProcessID, openProcess, closeProcessHandle, readMemory, findModuleBase, productVersion, systemLang. Linux side: finds the process by walking /proc/*/maps for a mapping ending in eldenring.exe (Proton runs several helper processes, so matching by name alone isn't reliable), reads memory via /proc/<pid>/mem (stdlib only, no external deps), and has no productVersion equivalent (returns ok=false — this was always just a hint for which PlayerIns offset to try first; the real one is confirmed by a live memory read regardless). openProcess probes /proc/<pid>/mem up front so a ptrace_scope permission failure surfaces immediately with the exact `sudo setcap cap_sys_ptrace+ep <path>` fix, never suggesting the system-wide ptrace_scope=0 weakening or running as root. main.go and i18n.go are fully portable now, no build tags. Verified: Windows build/vet/test plus a real run (no regression from moving ~500 lines). Linux is cross-compile build/vet only in this session — not yet run against a real Proton process.
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
// i18n.go: interface languages.
|
|
//
|
|
// 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.
|
|
//
|
|
// 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.
|
|
//
|
|
// systemLang(), which this file calls to pick the default language, is
|
|
// the one OS-specific piece — implemented in process_windows.go/
|
|
// process_linux.go, not here, since this file's own job (embedding and
|
|
// picking a dictionary) doesn't depend on the OS at all.
|
|
package main
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"log"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed locales/*.json
|
|
var localeFS embed.FS
|
|
|
|
const fallbackLang = "en"
|
|
|
|
var locales = map[string]map[string]string{}
|
|
|
|
func loadLocales() {
|
|
entries, err := localeFS.ReadDir("locales")
|
|
if err != nil {
|
|
log.Printf("couldn't read the embedded languages: %v", err)
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if !strings.HasSuffix(name, ".json") {
|
|
continue
|
|
}
|
|
data, err := localeFS.ReadFile(path.Join("locales", name))
|
|
if err != nil {
|
|
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 has invalid JSON: %v", name, err)
|
|
continue
|
|
}
|
|
locales[strings.TrimSuffix(name, ".json")] = m
|
|
}
|
|
}
|
|
|
|
func availableLangs() []string {
|
|
out := make([]string, 0, len(locales))
|
|
for k := range locales {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// resolveLang decides the final language. "auto" (or empty) uses the
|
|
// system'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" {
|
|
want = systemLang()
|
|
}
|
|
if want == "" {
|
|
return fallbackLang
|
|
}
|
|
if _, ok := locales[want]; ok {
|
|
return want
|
|
}
|
|
// "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
|
|
}
|
|
}
|
|
return fallbackLang
|
|
}
|
|
|
|
// 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] {
|
|
out[k] = v
|
|
}
|
|
if lang != fallbackLang {
|
|
for k, v := range locales[lang] {
|
|
if v != "" {
|
|
out[k] = v
|
|
}
|
|
}
|
|
}
|
|
out["_lang"] = lang
|
|
return out
|
|
}
|