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
@@ -0,0 +1,223 @@
|
||||
//go:build windows
|
||||
|
||||
// process_windows.go: the Windows side of the portable boundary defined
|
||||
// in process.go — finding the game process, opening/closing it, reading
|
||||
// its memory, finding a loaded module, reading the exe's file version,
|
||||
// and the system's UI language. All via raw Windows API calls (no
|
||||
// external dependencies, per the project's single-.exe goal).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
processQueryInformation = 0x0400
|
||||
processVMRead = 0x0010
|
||||
th32csSnapProcess = 0x00000002
|
||||
th32csSnapModule = 0x00000008
|
||||
th32csSnapModule32 = 0x00000010
|
||||
maxPath = 260
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procOpenProcess = kernel32.NewProc("OpenProcess")
|
||||
procCloseHandle = kernel32.NewProc("CloseHandle")
|
||||
procReadProcessMemory = kernel32.NewProc("ReadProcessMemory")
|
||||
procCreateToolhelp32Snapshot = kernel32.NewProc("CreateToolhelp32Snapshot")
|
||||
procModule32FirstW = kernel32.NewProc("Module32FirstW")
|
||||
procModule32NextW = kernel32.NewProc("Module32NextW")
|
||||
procProcess32FirstW = kernel32.NewProc("Process32FirstW")
|
||||
procProcess32NextW = kernel32.NewProc("Process32NextW")
|
||||
procGetUserDefaultLocaleName = kernel32.NewProc("GetUserDefaultLocaleName")
|
||||
|
||||
versionDLL = syscall.NewLazyDLL("version.dll")
|
||||
procGetFileVersionInfoSizeW = versionDLL.NewProc("GetFileVersionInfoSizeW")
|
||||
procGetFileVersionInfoW = versionDLL.NewProc("GetFileVersionInfoW")
|
||||
procVerQueryValueW = versionDLL.NewProc("VerQueryValueW")
|
||||
)
|
||||
|
||||
// vsFixedFileInfo is Windows's VS_FIXEDFILEINFO struct: used to pull the
|
||||
// game's version straight from eldenring.exe, same as SoulMemory (which
|
||||
// reads MainModule.FileVersionInfo.ProductVersion).
|
||||
type vsFixedFileInfo struct {
|
||||
Signature uint32
|
||||
StrucVersion uint32
|
||||
FileVersionMS uint32
|
||||
FileVersionLS uint32
|
||||
ProductVersionMS uint32
|
||||
ProductVersionLS uint32
|
||||
FileFlagsMask uint32
|
||||
FileFlags uint32
|
||||
FileOS uint32
|
||||
FileType uint32
|
||||
FileSubtype uint32
|
||||
FileDateMS uint32
|
||||
FileDateLS uint32
|
||||
}
|
||||
|
||||
type processEntry32 struct {
|
||||
Size uint32
|
||||
CntUsage uint32
|
||||
ProcessID uint32
|
||||
DefaultHeapID uintptr
|
||||
ModuleID uint32
|
||||
CntThreads uint32
|
||||
ParentProcessID uint32
|
||||
PriorityClassBase int32
|
||||
Flags uint32
|
||||
ExeFile [maxPath]uint16
|
||||
}
|
||||
|
||||
type moduleEntry32 struct {
|
||||
Size uint32
|
||||
ModuleID uint32
|
||||
ProcessID uint32
|
||||
GlblcntUsage uint32
|
||||
ProccntUsage uint32
|
||||
ModBaseAddr uintptr
|
||||
ModBaseSize uint32
|
||||
HModule syscall.Handle
|
||||
ModuleName [256]uint16
|
||||
ExePath [maxPath]uint16
|
||||
}
|
||||
|
||||
func findProcessID(name string) (uint32, error) {
|
||||
snap, _, _ := procCreateToolhelp32Snapshot.Call(uintptr(th32csSnapProcess), 0)
|
||||
if snap == 0 || snap == uintptr(^uintptr(0)) {
|
||||
return 0, fmt.Errorf("couldn't take a process snapshot")
|
||||
}
|
||||
defer procCloseHandle.Call(snap)
|
||||
|
||||
var pe processEntry32
|
||||
pe.Size = uint32(unsafe.Sizeof(pe))
|
||||
r, _, _ := procProcess32FirstW.Call(snap, uintptr(unsafe.Pointer(&pe)))
|
||||
if r == 0 {
|
||||
return 0, fmt.Errorf("Process32First failed")
|
||||
}
|
||||
for {
|
||||
exe := syscall.UTF16ToString(pe.ExeFile[:])
|
||||
if strings.EqualFold(exe, name) {
|
||||
return pe.ProcessID, nil
|
||||
}
|
||||
r, _, _ := procProcess32NextW.Call(snap, uintptr(unsafe.Pointer(&pe)))
|
||||
if r == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("process not found: %s", name)
|
||||
}
|
||||
|
||||
func openProcess(pid uint32) (procHandle, error) {
|
||||
h, _, err := procOpenProcess.Call(uintptr(processQueryInformation|processVMRead), 0, uintptr(pid))
|
||||
if h == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return procHandle(h), nil
|
||||
}
|
||||
|
||||
func closeProcessHandle(h procHandle) {
|
||||
procCloseHandle.Call(uintptr(h))
|
||||
}
|
||||
|
||||
func readMemory(h procHandle, addr uintptr, size int) ([]byte, bool) {
|
||||
if addr == 0 {
|
||||
return nil, false
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
var n uintptr
|
||||
r, _, _ := procReadProcessMemory.Call(uintptr(h), addr, uintptr(unsafe.Pointer(&buf[0])), uintptr(size), uintptr(unsafe.Pointer(&n)))
|
||||
if r == 0 || int(n) != size {
|
||||
return nil, false
|
||||
}
|
||||
return buf, true
|
||||
}
|
||||
|
||||
func findModuleBase(pid uint32, name string) (uintptr, uint32, string, error) {
|
||||
snap, _, _ := procCreateToolhelp32Snapshot.Call(uintptr(th32csSnapModule|th32csSnapModule32), uintptr(pid))
|
||||
if snap == 0 || snap == uintptr(^uintptr(0)) {
|
||||
return 0, 0, "", fmt.Errorf("couldn't take a module snapshot")
|
||||
}
|
||||
defer procCloseHandle.Call(snap)
|
||||
|
||||
var me moduleEntry32
|
||||
me.Size = uint32(unsafe.Sizeof(me))
|
||||
r, _, _ := procModule32FirstW.Call(snap, uintptr(unsafe.Pointer(&me)))
|
||||
if r == 0 {
|
||||
return 0, 0, "", fmt.Errorf("Module32First failed")
|
||||
}
|
||||
for {
|
||||
mname := syscall.UTF16ToString(me.ModuleName[:])
|
||||
if strings.EqualFold(mname, name) {
|
||||
return me.ModBaseAddr, me.ModBaseSize, syscall.UTF16ToString(me.ExePath[:]), nil
|
||||
}
|
||||
r, _, _ := procModule32NextW.Call(snap, uintptr(unsafe.Pointer(&me)))
|
||||
if r == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, 0, "", fmt.Errorf("module not found: %s", name)
|
||||
}
|
||||
|
||||
// productVersion reads the game executable's version. label carries both
|
||||
// full version numbers (product and file) because they don't always
|
||||
// match, which helps diagnose whether the PlayerIns offset ever needs
|
||||
// adjusting.
|
||||
func productVersion(path string) (major, minor uint16, label string, ok bool) {
|
||||
if path == "" {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
size, _, _ := procGetFileVersionInfoSizeW.Call(uintptr(unsafe.Pointer(p)), 0)
|
||||
if size == 0 {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
buf := make([]byte, size)
|
||||
r, _, _ := procGetFileVersionInfoW.Call(uintptr(unsafe.Pointer(p)), 0, size, uintptr(unsafe.Pointer(&buf[0])))
|
||||
if r == 0 {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
sub, err := syscall.UTF16PtrFromString(`\`)
|
||||
if err != nil {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
var info *vsFixedFileInfo
|
||||
var infoLen uint32
|
||||
r, _, _ = procVerQueryValueW.Call(
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(unsafe.Pointer(sub)),
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
uintptr(unsafe.Pointer(&infoLen)),
|
||||
)
|
||||
if r == 0 || info == nil || infoLen == 0 {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
quad := func(ms, ls uint32) string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", ms>>16, ms&0xFFFF, ls>>16, ls&0xFFFF)
|
||||
}
|
||||
label = fmt.Sprintf("product %s / file %s",
|
||||
quad(info.ProductVersionMS, info.ProductVersionLS),
|
||||
quad(info.FileVersionMS, info.FileVersionLS))
|
||||
return uint16(info.ProductVersionMS >> 16), uint16(info.ProductVersionMS & 0xFFFF), label, true
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in new issue
Block a user