Add Linux/Proton support

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.
This commit is contained in:
emmatherock committed 2026-09-18 01:25:44 -03:00
1 parent 5a2c3272e9
commit 2ba312a833
7 files changed
+1128 -883

No files matched your search

+7 -22
View File
@@ -1,5 +1,3 @@
//go:build windows
// i18n.go: interface languages.
//
// Translations live in locales/*.json and get embedded into the binary.
@@ -10,6 +8,11 @@
// 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 (
@@ -19,8 +22,6 @@ import (
"path"
"sort"
"strings"
"syscall"
"unsafe"
)
//go:embed locales/*.json
@@ -64,24 +65,8 @@ func availableLangs() []string {
return out
}
var procGetUserDefaultLocaleName = kernel32.NewProc("GetUserDefaultLocaleName")
// 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)))
if r == 0 {
return ""
}
name := syscall.UTF16ToString(buf[:r])
if base, _, ok := strings.Cut(name, "-"); ok {
return strings.ToLower(base)
}
return strings.ToLower(name)
}
// resolveLang decides the final language. "auto" (or empty) uses
// Windows's; if that language isn't translated, it falls back to English.
// 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" {