150 lines
4.7 KiB
Go
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
|
|
}
|