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:
1 parent
5a2c3272e9
commit
2ba312a833
7 files changed
+1128
-883
No files matched your search
@@ -2,19 +2,26 @@
|
||||
|
||||
Lee en **solo lectura** la memoria del proceso de Elden Ring y expone el
|
||||
contador de muertes como overlay web para OBS. Soporta co-op: cada jugador
|
||||
corre el programa en su PC y uno hace de hub.
|
||||
corre el programa en su PC y uno hace de hub. Corre nativo en Windows y
|
||||
en Linux contra el juego bajo Proton (mismos offsets y firmas: es el
|
||||
mismo binario de Windows).
|
||||
|
||||
## Comandos
|
||||
|
||||
```bash
|
||||
GOOS=windows GOARCH=amd64 go build -o deathwatch.exe . # el binario (hoy, unico soportado)
|
||||
go test ./... # tests (corren en cualquier SO)
|
||||
gofmt -l *.go # formato
|
||||
GOOS=windows GOARCH=amd64 go vet . # vet: SIEMPRE con GOOS=windows
|
||||
GOOS=windows GOARCH=amd64 go build -o deathwatch.exe . # binario Windows
|
||||
GOOS=linux GOARCH=amd64 go build -o deathwatch-linux . # binario Linux (Proton)
|
||||
go test ./... # tests (corren en cualquier SO)
|
||||
gofmt -l *.go # formato
|
||||
GOOS=windows GOARCH=amd64 go vet . # vet en los dos GOOS
|
||||
GOOS=linux GOARCH=amd64 go vet .
|
||||
```
|
||||
|
||||
`go vet` sin `GOOS=windows` falla con `syscall.Handle undefined`. No es un
|
||||
error del código: es que `main.go` e `i18n.go` solo compilan para Windows.
|
||||
Dos plataformas reales ahora: correr `go vet` (y `go build`) con los dos
|
||||
`GOOS` es la única forma de agarrar una rotura especifica de una
|
||||
plataforma antes de que la vea alguien que corre la otra. `process.go`
|
||||
es portable; `process_windows.go` y `process_linux.go` son cada uno
|
||||
solo para su SO (ver "Multiplataforma" más abajo).
|
||||
|
||||
## Idioma del código
|
||||
|
||||
@@ -35,8 +42,11 @@ idiomas del overlay, ni al revés.
|
||||
|
||||
| Archivo | Qué hace | Plataforma |
|
||||
|---|---|---|
|
||||
| `main.go` | Lectura de memoria, escaneo de firmas, loop de polling, HTTP | solo Windows |
|
||||
| `i18n.go` | Carga de `locales/*.json`, idioma del sistema | solo Windows |
|
||||
| `main.go` | HTTP, startup, arma todo | portable |
|
||||
| `i18n.go` | Carga de `locales/*.json`, elige idioma | portable |
|
||||
| `process.go` | Escaneo de firmas, resolución de punteros, loop de polling | portable |
|
||||
| `process_windows.go` | Primitivas de SO: abrir proceso, leer memoria, version.dll, idioma | solo Windows |
|
||||
| `process_linux.go` | Lo mismo que `process_windows.go`, vía `/proc/<pid>/{maps,mem}` | solo Linux |
|
||||
| `counter.go` | **Contabilidad**: a qué personaje va cada muerte | portable |
|
||||
| `names.go` | `looksLikeName`: filtra basura binaria leída como nombre | portable |
|
||||
| `totals.go` | Persistencia por personaje (`totals.json`) | portable |
|
||||
@@ -53,6 +63,71 @@ los tres bugs de conteo, y separarlo es lo que permite testearlo sin una PC
|
||||
con el juego abierto. Si agregás reglas de conteo, van ahí, con test.
|
||||
`names.go`, `tlscert.go`, `pin.go` e `invite.go` siguen el mismo principio.
|
||||
|
||||
## Multiplataforma
|
||||
|
||||
`process.go` no sabe nada de Windows ni de Linux: escanea firmas, resuelve
|
||||
punteros y corre el loop de polling contra siete funciones que cruzan la
|
||||
frontera con el SO, cada una implementada una vez por plataforma
|
||||
(`process_windows.go` / `process_linux.go`):
|
||||
|
||||
```go
|
||||
type procHandle uintptr // opaco: en Windows es el HANDLE real; en Linux, el pid
|
||||
|
||||
func findProcessID(name string) (uint32, error)
|
||||
func openProcess(pid uint32) (procHandle, error)
|
||||
func closeProcessHandle(h procHandle)
|
||||
func readMemory(h procHandle, addr uintptr, size int) ([]byte, bool)
|
||||
func findModuleBase(pid uint32, name string) (uintptr, uint32, string, error)
|
||||
func productVersion(path string) (major, minor uint16, label string, ok bool)
|
||||
func systemLang() string
|
||||
```
|
||||
|
||||
En Linux, contra el juego corriendo bajo Proton (mismo binario de
|
||||
Windows, mismas firmas y offsets):
|
||||
|
||||
- **Encontrar el proceso por sus mapeos, no por el nombre**
|
||||
(`findProcessID`/`scanMaps`): Proton levanta varios procesos: se
|
||||
recorre `/proc/*/maps` y se toma el pid que tenga mapeado un archivo
|
||||
terminado en `eldenring.exe`. Los mismos mapeos dan la base y el
|
||||
tamaño del módulo para `findModuleBase`: puede venir partido en varios
|
||||
tramos (`.text`/`.rdata`/`.data`), así que se toma el span completo
|
||||
(mínimo inicio, máximo final) — alcanza, porque el escáner ya lee por
|
||||
chunks y saltea los que no puede leer.
|
||||
- **Leer memoria vía `/proc/<pid>/mem`** (`ReadAt`, sin dependencias
|
||||
externas), no `process_vm_readv(2)` crudo: mismo resultado, sin tener
|
||||
que hacer un syscall a mano con structs `iovec` sin `golang.org/x/sys`.
|
||||
- **El obstáculo real es `ptrace_scope`.** Sin la capacidad, abrir
|
||||
`/proc/<pid>/mem` da `EPERM`. `openProcess` lo prueba una vez al
|
||||
arrancar y, si falla, el error (que sale por el mismo camino que ya
|
||||
existía: `st.setDisconnected(err.Error())` en el loop de polling) trae
|
||||
el comando exacto con la ruta real del binario:
|
||||
`sudo setcap cap_sys_ptrace+ep <ruta>`. **Nunca** sugiere
|
||||
`sysctl kernel.yama.ptrace_scope=0` ni correr como root — eso baja la
|
||||
defensa de todo el sistema, no solo la de este programa.
|
||||
- **`productVersion` devuelve `ok=false` siempre.** No hay equivalente a
|
||||
`version.dll` en Linux, pero nunca hizo falta: la versión era solo una
|
||||
corazonada para elegir qué offset de `PlayerIns` probar primero
|
||||
(`playerInsCandidates`); el que vale se confirma leyendo memoria en
|
||||
`isPlayerLoaded` igual, con o sin la corazonada.
|
||||
- **`systemLang`** sale de `$LC_ALL` / `$LC_MESSAGES` / `$LANG` en vez de
|
||||
`GetUserDefaultLocaleName`, devolviendo el mismo formato que ya
|
||||
devuelve la versión de Windows (el código corto: `"es"`, no
|
||||
`"es-AR"` ni `"es_AR.UTF-8"`), para que `resolveLang` (`i18n.go`) no
|
||||
tenga que distinguir de dónde vino.
|
||||
|
||||
**Modo sólo-hub en Linux sale gratis, sin código extra.** `main()` llama
|
||||
`go pollLoop()` sin importar el modo. Si no hay ningún `eldenring.exe`
|
||||
local (el caso de una PC con el OBS en Linux mientras se juega en otra),
|
||||
`findProcessID` simplemente no encuentra nada y `pollLoop` reintenta cada
|
||||
3s sin nunca llegar a `openProcess` — `setcap`/`ptrace_scope` no entran
|
||||
en juego para nada en ese caso.
|
||||
|
||||
**Pendiente, no parte de este cambio:** testear el escáner/poller con un
|
||||
lector de memoria falso. El split ya lo habilita, pero escribir esos
|
||||
tests (siguiendo el patrón de variable de paquete intercambiable que ya
|
||||
usa `testExeDir` en `totals.go`, no una interfaz nueva) queda para
|
||||
después — ver Pendientes.
|
||||
|
||||
## Offsets de memoria
|
||||
|
||||
Todo se resuelve escaneando firmas AOB en el módulo del juego. Los tres
|
||||
@@ -191,73 +266,16 @@ No son preferencias de estilo. Cada una costó un bug en producción.
|
||||
|
||||
## Pendientes
|
||||
|
||||
- No hay `README.md` todavía. Escribir uno en inglés (instalación, modo
|
||||
hub/peer, capturas) es lo único que falta del pendiente de idioma — el
|
||||
código ya está en inglés de punta a punta, ver "Idioma del código"
|
||||
arriba.
|
||||
- No hay `README.md` todavía. Escribir uno en inglés (instalación en
|
||||
Windows y Linux/Proton — incluyendo el paso de `setcap`, modo hub/peer,
|
||||
capturas) es lo único que falta del pendiente de idioma — el código ya
|
||||
está en inglés de punta a punta, ver "Idioma del código" arriba.
|
||||
- La identificación por nombre (respaldo cuando no se lee el slot) mezcla
|
||||
personajes homónimos. Documentado, no resuelto.
|
||||
- **Compilar y correr en Linux (Proton).** Mucha gente juega Elden Ring
|
||||
con Proton, y hoy el programa sólo existe para Windows. El juego sigue
|
||||
siendo el mismo binario de Windows corriendo bajo Wine, así que **las
|
||||
firmas AOB y todos los offsets valen igual**: lo único que cambia es
|
||||
cómo se encuentra el proceso y cómo se lee su memoria.
|
||||
|
||||
**Refactor primero.** Hoy `main.go` mezcla lo específico de Windows con
|
||||
lo que no lo es. Separar en `process_windows.go` y `process_linux.go`
|
||||
detrás de unas pocas funciones — `findProcessID`, `openProcess`,
|
||||
`readMemory`, `findModuleBase`, `productVersion`, `systemLang` — y dejar
|
||||
el resto (escaneo de firmas, resolución de punteros, loop de polling,
|
||||
lectura del nombre) en un archivo portable. Beneficio extra que vale por
|
||||
sí solo: con eso el escaneo y el polling **se pueden testear con un
|
||||
lector de memoria falso**, que es justo la parte que hoy no tiene tests.
|
||||
|
||||
Lo específico de Linux:
|
||||
|
||||
1. **Encontrar el proceso por sus mapeos, no por el nombre.** Proton
|
||||
levanta varios procesos. Lo robusto es recorrer `/proc/*/maps` y
|
||||
quedarse con el pid que tenga mapeado un archivo terminado en
|
||||
`eldenring.exe`. De paso, esos mismos mapeos dan la base y el tamaño
|
||||
del módulo, que es lo que `findModuleBase` necesita. Puede venir
|
||||
partido en varios tramos (`.text`, `.rdata`, `.data`) con permisos
|
||||
distintos: tomar el span completo alcanza, porque el escáner ya lee
|
||||
por chunks y saltea los que no puede leer.
|
||||
2. **Leer con `process_vm_readv(2)`**, que no necesita adjuntarse al
|
||||
proceso. `/proc/<pid>/mem` sirve de alternativa.
|
||||
3. **El obstáculo real es `ptrace_scope`.** En casi todas las distros
|
||||
vale `1`, y con eso `process_vm_readv` sobre un proceso ajeno falla
|
||||
con `EPERM`. La salida recomendada es darle la capacidad al binario:
|
||||
|
||||
```bash
|
||||
sudo setcap cap_sys_ptrace+ep ./deathwatch
|
||||
```
|
||||
|
||||
**No** recomendar `sysctl kernel.yama.ptrace_scope=0`, que baja la
|
||||
defensa de todo el sistema, ni correrlo como root. Y que el mensaje
|
||||
de error diga exactamente esto cuando falle: sin eso el programa
|
||||
parece simplemente roto, y es el primer problema que va a tener
|
||||
cualquiera que lo pruebe.
|
||||
4. **`productVersion` no tiene equivalente** (usa `version.dll` sobre el
|
||||
exe). En Linux devolver `ok=false` y listo: la versión es sólo una
|
||||
corazonada para decidir qué offset de `PlayerIns` probar primero, y
|
||||
el valor bueno se confirma leyendo memoria igual. Una decisión vieja
|
||||
que acá se paga sola.
|
||||
5. **`systemLang`** sale de `$LC_ALL` / `$LANG` en vez de
|
||||
`GetUserDefaultLocaleName`.
|
||||
|
||||
No hay que tocar: los offsets, las firmas, `counter.go`, `names.go`,
|
||||
`totals.go`, `duo.go`, `ws.go`, `tlscert.go`, `pin.go`, `invite.go`,
|
||||
`config.go` ni el overlay. Ya son portables.
|
||||
|
||||
**Modo sólo-hub, de regalo.** Un hub que no lee memoria —que sólo junta
|
||||
lo de los peers y sirve el overlay— no necesita `setcap` ni permiso
|
||||
alguno. Es exactamente el caso de quien tiene el OBS en una PC con Linux
|
||||
y juega en otra, y sale casi gratis una vez separado lo de arriba.
|
||||
|
||||
Referencias de gente que ya leyó memoria de juegos bajo Proton:
|
||||
[pika](https://github.com/delfianto/pika),
|
||||
[cheat-engine-linux](https://github.com/wleeaf/cheat-engine-linux),
|
||||
[un trainer para D2R en Linux paso a paso](https://axiom0x0.sh/posts/d2r-memory-trainer-part2/).
|
||||
|
||||
- Testear el escáner/poller (`process.go`) con un lector de memoria
|
||||
falso. El split multiplataforma (ver "Multiplataforma" arriba) ya lo
|
||||
habilita — falta escribir los tests en sí, con una variable de paquete
|
||||
intercambiable por función de frontera (mismo patrón que `testExeDir`
|
||||
en `totals.go`), no una interfaz nueva.
|
||||
- El README debería mencionar que meter todo adentro de una VPN sigue
|
||||
siendo una opción perfectamente válida, TLS+pinning aparte.
|
||||
Reference in new issue
Block a user