//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) }