Files
bd2/go/internal/client/app/platform_windows.go
T

150 lines
4.7 KiB
Go

//go:build windows
package app
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
var (
platformUser32DLL = syscall.NewLazyDLL("user32.dll")
messageBoxW = platformUser32DLL.NewProc("MessageBoxW")
enumWindowsProc = platformUser32DLL.NewProc("EnumWindows")
getWindowThreadProcessIDProc = platformUser32DLL.NewProc("GetWindowThreadProcessId")
isWindowVisibleProc = platformUser32DLL.NewProc("IsWindowVisible")
isIconicProc = platformUser32DLL.NewProc("IsIconic")
showWindowAsyncProc = platformUser32DLL.NewProc("ShowWindowAsync")
setForegroundWindowProc = platformUser32DLL.NewProc("SetForegroundWindow")
)
// ShowFatalError keeps startup failures visible even though the release
// executable uses the Windows GUI subsystem and therefore has no console.
func ShowFatalError(err error) {
if err == nil {
return
}
message, conversionErr := syscall.UTF16PtrFromString(fmt.Sprintf(
"BD2 Client Studio could not start:\n\n%s\n\nSee the logs directory next to bd2client.exe for details.", err,
))
if conversionErr != nil {
return
}
title, conversionErr := syscall.UTF16PtrFromString("BD2 Client Studio")
if conversionErr != nil {
return
}
messageBoxW.Call(0, uintptr(unsafe.Pointer(message)), uintptr(unsafe.Pointer(title)), 0x10)
}
// CREATE_NO_WINDOW prevents console-subsystem helpers such as powershell.exe
// from allocating a visible console when bd2client is built as a Windows GUI
// executable. HideWindow also covers helpers that elect to create a window
// despite inheriting no console from the parent process.
const createNoWindow = 0x08000000
func hiddenCommand(name string, args ...string) *exec.Cmd {
command := exec.Command(name, args...)
command.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: createNoWindow,
}
return command
}
// visibleCommand suppresses a console allocation without hiding the GUI
// window created by the child process. It must be used for the game itself;
// hiddenCommand is reserved for background helper processes.
func visibleCommand(name string, args ...string) *exec.Cmd {
command := exec.Command(name, args...)
command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
return command
}
func launchGame(target string) error {
if processID, running, err := windowsExecutableProcessID(filepath.Base(target)); err != nil {
return err
} else if running {
if !activateProcessWindow(processID, 5*time.Second) {
return fmt.Errorf("Brown Dust II is running, but its window could not be restored")
}
return errGameAlreadyRunning
}
command := visibleCommand(target, gameLaunchArguments()...)
command.Dir = filepath.Dir(target)
command.Env = gameProxyEnvironment(os.Environ(), systemGameProxy())
if err := command.Start(); err != nil {
return err
}
// Unity creates the top-level window asynchronously. Best-effort foreground
// activation prevents the new window from opening behind Client Studio.
activateProcessWindow(uint32(command.Process.Pid), 15*time.Second)
return nil
}
func windowsExecutableProcessID(name string) (uint32, bool, error) {
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
if err != nil {
return 0, false, err
}
defer windows.CloseHandle(snapshot)
entry := windows.ProcessEntry32{Size: uint32(unsafe.Sizeof(windows.ProcessEntry32{}))}
if err := windows.Process32First(snapshot, &entry); err != nil {
return 0, false, err
}
for {
if strings.EqualFold(windows.UTF16ToString(entry.ExeFile[:]), name) {
return entry.ProcessID, true, nil
}
if err := windows.Process32Next(snapshot, &entry); err != nil {
if err == windows.ERROR_NO_MORE_FILES {
return 0, false, nil
}
return 0, false, err
}
}
}
func activateProcessWindow(processID uint32, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for {
if window := topLevelWindowForProcess(processID); window != 0 {
iconic, _, _ := isIconicProc.Call(window)
if iconic != 0 {
const swRestore = 9
showWindowAsyncProc.Call(window, swRestore)
}
setForegroundWindowProc.Call(window)
return true
}
if time.Now().After(deadline) {
return false
}
time.Sleep(100 * time.Millisecond)
}
}
func topLevelWindowForProcess(processID uint32) uintptr {
var found uintptr
callback := syscall.NewCallback(func(window uintptr, _ uintptr) uintptr {
var owner uint32
getWindowThreadProcessIDProc.Call(window, uintptr(unsafe.Pointer(&owner)))
visible, _, _ := isWindowVisibleProc.Call(window)
if owner == processID && visible != 0 {
found = window
return 0
}
return 1
})
enumWindowsProc.Call(callback, 0)
return found
}