diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..5228d48
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github: xan105
+custom: https://www.paypal.me/xan105
+patreon: xan105
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2bc1051
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+build/
+src/pkg/
+src/*.syso
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4397ac7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Anthony Beaumont
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index fdaa656..b886114 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,166 @@
-# RA3-Launcher
-Red Alert 3 Launcher (RA3.exe re-implementation / alternative)
+Welcome back, Commander 🫡.
+
+Red Alert 3 is one of my favorite game but it's in a sorry state since EA shut down their servers for the game.
+
+This project is an open source re-implementation of the original game launcher `RA3.exe` aimed at addressing and mitigating some of the issues that have emerged since EA servers were shut down.
+
+### Issues due to EA shutting down their servers
+
+1. Game Startup is (very) slow
+
+ The game takes awfully long to start because the official launcher tries to get the latest "Comrade News" from files.ea.com
+ whether or not the gui interface was requested (-ui). And wait until the connection times out, before starting the game.
+
+2. LAN play: "CD key is already in use" (%CDKEY%)
+
+ LAN play requires each player to have a different CD Key.
+ Steam no longer populates the `%CDKEY%` variable in the registry _(is it due to EA's servers shutdown ?)_.
+
+ This is a problem because each time the game is run by Steam; Steam will write "%CDKEY%" as the user's CD key.
+ Every (Steam) players will therefore end up with the same CD Key: "%CDKEY%" and are unable to LAN play with each others.
+
+ The registry value is read on game launch. It is uneffective to change it after the game is ran by Steam.
+
+3. Online play: emulation and DLL injection
+
+ You can no longer play online nor co-op without using 3rd party service that emulate gamespy such as [revora/CnC:Online](https://cnc-online.net/en/)
+
+ Requiring an additional launcher to be able to use their service.
+
+ While _"Launchers inception"_ (Launcher which starts another Launcher) is despised by many it remains a matter of personal preference.
+ But for me the core issue was that their launcher didn't work with the Steam version nor with Linux/Proton when I tried it.
+
+## Features
+
+- Start Red Alert 3 process almost instantly _(Issue 1)_
+
+- Fix the CD Key registry value if needed for LAN play before starting the game _(Issue 2)_
+
+- Addons: revora/CnC:Online without their launcher _(Issue 3)_
+
+ See [xan105/CnC-Online](https://github.com/xan105/CnC-Online) for more details.
+
+- Compatible with 🐧 Linux/Proton
+
+- Splash screen customisation: none, random, ...
+
+## Installation
+
+Copy the files in the game directory and replace `RA3.exe`
+
+#### Steam
+
+> [!NOTE]
+> In order to fix the issue with `%CDKEY%` _(Issue 2)_ `RA3.exe` will need elevated privileges.
+> Because the CD Key value is in the registry under `HKLM` which is write access protected.
+>
+> If you run the game via Steam, you may want to consider always running `RA3.exe` with elevated privileges
+> by right clicking RA3.exe > properties > Compatibility > check "Run this program as an administrator"
+
+Wait... why does it need admin right and Steam does not ?!
+
+Steam has its own windows service running in the background with system privileges (`steamservice.exe`) to do these kind of operations silently without the end user noticing.
+
+
+## Command Line Arguments
+
+`RA3.exe --help` to display all arguments.
+
+Most of the orignal `RA3.exe` cmdline arguments were kept:
+
+|flag|type|description|
+|----|----|-----------|
+|xres|number|Sets resolution width|
+|yres|number|Sets resolution height|
+|xpos|number|Sets horizontal offset of the window|
+|ypos|number|Sets vertical offset of the window|
+|win|boolean|Runs the game in windowed mode|
+|fullscreen|boolean|Runs the game in fullscreen mode. Combine with -win for borderless windowed mode|
+|noaudio|boolean|Disables game audio|
+|noAudioMusic|boolean|Disables game music|
+|silentLogin|boolean|Forces the game to immediately log in to a multiplayer account|
+|modConfig|string|Runs the game with selected mod (has to point to its .skudef file)|
+|replayGame|string|Plays replay file|
+
+> [!TIP]
+> `-modConfig path`
+>
+> If "path" is **not** an absolute path then it will look for any `.skudef` file corresponding in `%Documents%/Red Alert 3/Mods`
+>
+> eg: `%Documents%/Red Alert 3/Mods/Upheaval/Upheaval_1.16.skudef` > `-modConfig Upheaval`
+
+## Config File
+
+📄 `RA3.json` (required):
+
+- `lang?: string (auto)`
+
+ Which language to start Red Alert 3 with.
+
+ Default (auto) will query the registry value `HKCU/Software/Electronic Arts/Electronic Arts/Red Alert 3/language`.
+ If no value is set then it defaults to "english".
+
+- `borderless?: boolean (false)`
+
+ Runs Red Alert 3 in borderless fullscreen.
+ This superseeds the cmdline arguments `-win` and `-fullscreen`
+
+- `upheaval?: boolean (false)`
+
+ Starts Red Alert 3 with the infamous mod "upheaval" if it's present in the `%GAMEDIR%` or in `%Documents%/Red Alert 3/Mods/Upheaval`
+
+- `keygen?: boolean (true)`
+
+ Check the CD Key value in the registry.
+ If it's empty or equals to "%CDKEY% generates a random CD Key and write it to the registry.
+
+> [!CAUTION]
+> Unfortunately requires elevated privileges because the key is located under `HKLM` which is write access protected.
+
+- `splash?: boolean (true)`
+
+ Display a splash screen while the game is loading similar to the original `RA3.exe`
+
+- `splash_image?: []string (["Launcher/splash.bmp"])`
+
+ Splash screen filepath. When more than one, a splash screen is selected at random.
+
+ Either absolute or relative path.
+ _NB: relative to `RA3.exe` and **not** the current working dir_
+
+> [!NOTE]
+> Image should be a 640x480 BMP file.
+
+- `addons?: []{ path: string, required?: boolean }`
+
+ List of addons to inject to the game process.
+ When `required` is set to `true` and if the injection failed, alert the user and kill the game process.
+
+ Either absolute or relative path.
+ _NB: relative to `RA3.exe` and **not** the current working dir_
+
+> [!TIP]
+> Example: You can use this option to load [xan105/CnC-Online](https://github.com/xan105/CnC-Online).
+>
+> Restoring the online features of Red Alert 3 without relying on the revora/CnC:Online launcher.
+
+ ```json
+ {
+ "addons": [
+ { "path": "Launcher/opencnconline.dll", "required": true }
+ ]
+ }
+ ```
+
+
+
+ Connected to C&C:Online under 🐧 Linux/Proton 9.0-2 (Fedora)
+
+
+## Building
+
+- Golang v1.23
+- [go-winres](https://github.com/tc-hib/go-winres) installed in `%PATH%` env var for win32 manifest & cie
+
+Run `build.cmd`
+Output files are located in `./build`
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 0000000..b482cea
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,9 @@
+@echo off
+cd %~dp0src
+set GOOS=windows
+set GOARCH=386
+go-winres make --in "..\winres\winres.json"
+echo Compiling x86 (DEBUG)...
+go build -o "..\build\Debug\RA3.exe" launcher
+echo Compiling x86 (RELEASE)...
+go build -ldflags "-w -s -H windowsgui" -o "..\build\Release\RA3.exe" launcher
\ No newline at end of file
diff --git a/screenshot/linux_proton.png b/screenshot/linux_proton.png
new file mode 100644
index 0000000..8594e24
Binary files /dev/null and b/screenshot/linux_proton.png differ
diff --git a/src/alert.go b/src/alert.go
new file mode 100644
index 0000000..8f1ddf4
--- /dev/null
+++ b/src/alert.go
@@ -0,0 +1,69 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package main
+
+import (
+ "os"
+ "golang.org/x/sys/windows"
+ "log/slog"
+)
+
+func alert(message string){
+ slog.Error(message);
+ windows.MessageBox(
+ windows.HWND(uintptr(0)),
+ windows.StringToUTF16Ptr(message),
+ windows.StringToUTF16Ptr("Red Alert 3"),
+ windows.MB_OK | windows.MB_ICONERROR)
+ os.Exit(1)
+}
+
+func displayHelp(){
+ windows.MessageBox(
+ windows.HWND(uintptr(0)),
+ windows.StringToUTF16Ptr(
+ "-win\n" +
+ "Runs the game in windowed mode\n" +
+ "\n" +
+ "-fullscreen\n" +
+ "Runs the game in fullscreen mode.\n" +
+ "Combine with -win for borderless windowed mode\n" +
+ "\n" +
+ "-modConfig filePath\n" +
+ "Runs the game with selected mod (has to point to its .skudef file)\n" +
+ "\n" +
+ "-replayGame filePath\n" +
+ "Plays replay file\n" +
+ "\n" +
+ "-noaudio\n" +
+ "Disables game audio\n" +
+ "\n" +
+ "-noAudioMusic\n" +
+ "Disables game music\n" +
+ "\n" +
+ "-silentLogin\n" +
+ "Forces the game to immediately log in to a multiplayer account\n" +
+ "\n" +
+ "-xres number\n" +
+ "Sets resolution width\n" +
+ "\n" +
+ "-yres number\n" +
+ "Sets resolution height\n" +
+ "\n" +
+ "-xpos number\n" +
+ "Sets horizontal offset of the window\n" +
+ "\n" +
+ "-ypos number\n" +
+ "Sets vertical offset of the window\n" +
+ "\n" +
+ "-help\n" +
+ "Show list of all arguments\n",
+ ),
+ windows.StringToUTF16Ptr("Red Alert 3"),
+ windows.MB_OK,
+ )
+}
\ No newline at end of file
diff --git a/src/fs.go b/src/fs.go
new file mode 100644
index 0000000..800e2ae
--- /dev/null
+++ b/src/fs.go
@@ -0,0 +1,99 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package main
+
+import(
+ "os"
+ "io"
+ "io/fs"
+ "path/filepath"
+ "encoding/json"
+ "errors"
+ "launcher/internal/regedit"
+)
+
+func locate() string{
+ process, err := os.Executable()
+ if err != nil { alert(err.Error()) }
+ return filepath.Dir(process)
+}
+
+func readJSON(filepath string) (config Config, err error) {
+
+ //default values
+ config.Version = "1.12"
+ config.Lang = "auto"
+ config.Upheaval = false
+ config.Keygen = true
+ config.Borderless = false
+ config.Splash = true
+ config.SplashImage = []string{ "Launcher/splash.bmp" }
+ config.Addons = []Addon{}
+
+ file, err := os.Open(filepath)
+ if err != nil { return }
+ defer file.Close()
+
+ bytes, err := io.ReadAll(file)
+ if err != nil { return }
+
+ err = json.Unmarshal(bytes, &config)
+ if err != nil { return }
+
+ if config.Version == "" {
+ config.Version = "1.12"
+ }
+ if config.Lang == "" {
+ config.Version = "auto"
+ }
+
+ if len(config.SplashImage) == 0 {
+ config.SplashImage = []string{ "Launcher/splash.bmp" }
+ }
+
+ return
+}
+
+func fileExist(path string) bool {
+ target, err := os.Stat(path)
+ if err == nil {
+ return !target.IsDir()
+ }
+ if errors.Is(err, os.ErrNotExist) {
+ return false
+ }
+ return false
+}
+
+func findFilesWithExt(dirpath string, ext string) []string {
+ var matches []string
+ filepath.WalkDir(dirpath, func(path string, info fs.DirEntry, e error) error {
+ if e != nil { return e }
+ if filepath.Ext(info.Name()) == ext {
+ matches = append(matches, path)
+ }
+ return nil
+ })
+ return matches
+}
+
+func getUserProfilePath() string {
+
+ const PATH = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders"
+ keys := []string{
+ "{F42EE2D3-909F-4907-8871-4C22FC0BF756}", //win10
+ "Personal"}
+
+ for _, key := range keys {
+ value := regedit.RegQueryStringValueAndExpand("HKCU", PATH, key)
+ if len(value) > 0 {
+ return value
+ }
+ }
+
+ return filepath.Join(os.Getenv("USERPROFILE"), "Documents")
+}
\ No newline at end of file
diff --git a/src/go.mod b/src/go.mod
new file mode 100644
index 0000000..6a3df2e
--- /dev/null
+++ b/src/go.mod
@@ -0,0 +1,5 @@
+module launcher
+
+go 1.23.0
+
+require golang.org/x/sys v0.24.0 // indirect
diff --git a/src/go.sum b/src/go.sum
new file mode 100644
index 0000000..d88e7bd
--- /dev/null
+++ b/src/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/src/internal/hook/hook.go b/src/internal/hook/hook.go
new file mode 100644
index 0000000..7530ce5
--- /dev/null
+++ b/src/internal/hook/hook.go
@@ -0,0 +1,79 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package hook
+
+import (
+ "syscall"
+ "golang.org/x/sys/windows"
+)
+
+var (
+ kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+ pVirtualAllocEx = kernel32.NewProc("VirtualAllocEx")
+)
+
+func CreateRemoteThread(pid uintptr, path string) error {
+
+ //Opens a handle to the target process with the needed permissions
+ hProcess, err := windows.OpenProcess(
+ windows.PROCESS_CREATE_THREAD |
+ windows.PROCESS_VM_OPERATION |
+ windows.PROCESS_VM_WRITE |
+ windows.PROCESS_VM_READ |
+ windows.PROCESS_QUERY_INFORMATION,
+ false,
+ uint32(pid),
+ )
+ if err != nil {
+ return err
+ }
+
+ //Allocates virtual memory for the file path
+ lpBaseAddress, _, err := pVirtualAllocEx.Call(
+ uintptr(hProcess),
+ 0,
+ uintptr(len(path)+1),
+ windows.MEM_RESERVE | windows.MEM_COMMIT,
+ windows.PAGE_EXECUTE_READWRITE,
+ )
+
+ //Converts the file path to type *byte
+ lpBuffer, err := windows.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+
+ //Writes the filename to the previously allocated space
+ lpNumberOfBytesWritten:= uintptr(0)
+ err = windows.WriteProcessMemory(
+ hProcess,
+ lpBaseAddress,
+ lpBuffer,
+ uintptr(len(path)+1),
+ &lpNumberOfBytesWritten,
+ )
+ if err != nil {
+ return err
+ }
+
+ //Gets a pointer to the LoadLibrary function
+ LoadLibAddr, err := syscall.GetProcAddress(syscall.Handle(kernel32.Handle()), "LoadLibraryA")
+ if err != nil {
+ return err
+ }
+
+ //Creates a remote thread that loads the DLL triggering it
+ handle, _, err := kernel32.NewProc("CreateRemoteThread").Call(uintptr(hProcess), 0, 0, LoadLibAddr, lpBaseAddress, 0, 0)
+ if handle == 0 {
+ return err
+ }
+
+ defer syscall.CloseHandle(syscall.Handle(handle))
+
+ return nil
+}
\ No newline at end of file
diff --git a/src/internal/regedit/regedit.go b/src/internal/regedit/regedit.go
new file mode 100644
index 0000000..01dcd4b
--- /dev/null
+++ b/src/internal/regedit/regedit.go
@@ -0,0 +1,60 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package regedit
+
+import (
+ "golang.org/x/sys/windows/registry"
+)
+
+func getHKEY(root string) registry.Key {
+
+ var HKEY registry.Key
+
+ switch root {
+ case "HKCU": HKEY = registry.CURRENT_USER
+ case "HKLM": HKEY = registry.LOCAL_MACHINE
+ case "HKU": HKEY = registry.USERS
+ case "HKCC": HKEY = registry.CURRENT_CONFIG
+ case "HKCR": HKEY = registry.CLASSES_ROOT
+ }
+
+ return HKEY
+}
+
+func RegQueryStringValue(root string, path string, key string) string { // REG_SZ & REG_EXPAND_SZ
+
+ var result string
+ HKEY := getHKEY(root)
+
+ k, _ := registry.OpenKey(HKEY , path, registry.QUERY_VALUE)
+ defer k.Close()
+ result, _, _ = k.GetStringValue(key)
+
+ return result
+}
+
+func RegQueryStringValueAndExpand(root string, path string, key string) string { // REG_EXPAND_SZ (expands environment-variable strings)
+
+ var result string
+ HKEY := getHKEY(root)
+
+ k, _ := registry.OpenKey(HKEY , path, registry.QUERY_VALUE)
+ defer k.Close()
+ str, _, _ := k.GetStringValue(key)
+ result, _ = registry.ExpandString(str)
+
+ return result
+}
+
+func RegWriteStringValue(root string, path string, key string, value string) {
+
+ HKEY := getHKEY(root)
+
+ k, _, _ := registry.CreateKey(HKEY, path, registry.ALL_ACCESS)
+ defer k.Close()
+ k.SetStringValue(key, value)
+}
\ No newline at end of file
diff --git a/src/internal/splash/splash.go b/src/internal/splash/splash.go
new file mode 100644
index 0000000..3cc7136
--- /dev/null
+++ b/src/internal/splash/splash.go
@@ -0,0 +1,399 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package splash
+
+import (
+ "log/slog"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+ pGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
+)
+
+func getModuleHandle() (syscall.Handle, error) {
+ ret, _, err := pGetModuleHandleW.Call(uintptr(0))
+ if ret == 0 {
+ return 0, err
+ }
+ return syscall.Handle(ret), nil
+}
+
+var (
+ gdi32 = syscall.NewLazyDLL("Gdi32.dll")
+
+ pGetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
+ pCreatePatternBrush = gdi32.NewProc("CreatePatternBrush")
+)
+
+const (
+ HORZRES = 8
+ VERTRES = 10
+)
+
+func getDeviceCaps(hDC syscall.Handle, index int32) uint32 {
+ ret, _, _ := pGetDeviceCaps.Call(
+ uintptr(hDC),
+ uintptr(index),
+ )
+
+ return uint32(ret)
+}
+
+func createPatternBrush(hbm syscall.Handle) (syscall.Handle, error) {
+ ret, _, err := pCreatePatternBrush.Call(uintptr(hbm))
+ if ret == 0 {
+ return 0, err
+ }
+ return syscall.Handle(ret), nil
+}
+
+var (
+ user32 = syscall.NewLazyDLL("user32.dll")
+
+ pCreateWindowExW = user32.NewProc("CreateWindowExW")
+ pDefWindowProcW = user32.NewProc("DefWindowProcW")
+ pDestroyWindow = user32.NewProc("DestroyWindow")
+ pDispatchMessageW = user32.NewProc("DispatchMessageW")
+ pGetMessageW = user32.NewProc("GetMessageW")
+ pLoadCursorW = user32.NewProc("LoadCursorW")
+ pPostQuitMessage = user32.NewProc("PostQuitMessage")
+ pRegisterClassExW = user32.NewProc("RegisterClassExW")
+ pTranslateMessage = user32.NewProc("TranslateMessage")
+ pLoadImageW = user32.NewProc("LoadImageW")
+ pSetWinEventHook = user32.NewProc("SetWinEventHook")
+ pUnhookWinEvent = user32.NewProc("UnhookWinEvent") //todo
+ pGetDC = user32.NewProc("GetDC")
+ pReleaseDC = user32.NewProc("ReleaseDC")
+
+)
+
+func getDC(hWnd syscall.Handle) (syscall.Handle, error) {
+ ret, _, err := pGetDC.Call(
+ uintptr(hWnd),
+ )
+ if ret == 0 {
+ return 0, err
+ }
+ return syscall.Handle(ret), nil
+}
+
+func releaseDC (hWnd syscall.Handle, hDC syscall.Handle) bool {
+ ret, _, _ := pReleaseDC.Call(
+ uintptr(hWnd),
+ uintptr(hDC),
+ )
+ return ret != 0
+}
+
+const (
+ cWS_VISIBLE = 0x10000000
+ cWS_EX_TOPMOST = 0x00000008
+ cWS_POPUP = 0x80000000
+ cWS_EX_TOOLWINDOW = 0x000000080
+ cWS_TABSTOP = 0x00010000
+)
+
+func createWindow(className, windowName string, style, style_ext uint32, x, y, width, height uint32, parent, menu, instance syscall.Handle) (syscall.Handle, error) {
+ ret, _, err := pCreateWindowExW.Call(
+ uintptr(style),
+ uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))),
+ uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(windowName))),
+ uintptr(style_ext),
+ uintptr(x),
+ uintptr(y),
+ uintptr(width),
+ uintptr(height),
+ uintptr(parent),
+ uintptr(menu),
+ uintptr(instance),
+ uintptr(0),
+ )
+ if ret == 0 {
+ return 0, err
+ }
+ return syscall.Handle(ret), nil
+}
+
+const (
+ EVENT_SYSTEM_FOREGROUND = 0x0003
+ EVENT_OBJECT_CREATE = 0x8000
+ EVENT_OBJECT_SHOW = 0x8002
+ WINEVENT_OUTOFCONTEXT = 0x0000
+ WINEVENT_INCONTEXT = 0x0004
+ WINEVENT_SKIPOWNPROCESS = 0x0002
+ WINEVENT_SKIPOWNTHREAD = 0x0001
+ OBJID_WINDOW = 0
+ OBJID_CURSOR = -9
+ OBJID_CLIENT = -4;
+)
+
+func setWinEventHook(eventMin, eventMax uint32, hmodWinEventProc syscall.Handle, pfnWinEventProc uintptr, idProcess int, idThread, dwFlags uint32) (syscall.Handle, error) {
+ ret, _, err := pSetWinEventHook.Call(
+ uintptr(eventMin),
+ uintptr(eventMax),
+ uintptr(hmodWinEventProc),
+ pfnWinEventProc,
+ uintptr(idProcess),
+ uintptr(idThread),
+ uintptr(dwFlags),
+ )
+ if ret == 0 {
+ return 0, err
+ }
+ return syscall.Handle(ret), nil
+}
+
+func unhookWinEvent(hWinEventHook syscall.Handle) bool {
+ ret, _, _ := pUnhookWinEvent.Call(
+ uintptr(hWinEventHook),
+ )
+ return ret != 0
+}
+
+const (
+ IMAGE_BITMAP = 0x00
+ LR_LOADFROMFILE = 0x00000010
+)
+
+func loadImage(imagePath string) (syscall.Handle, error) {
+ ret, _, err := pLoadImageW.Call(
+ uintptr(0),
+ uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(imagePath))),
+ uintptr(IMAGE_BITMAP),
+ uintptr(0),
+ uintptr(0),
+ uintptr(LR_LOADFROMFILE),
+ )
+ if ret == 0 {
+ return 0, err
+ }
+ return syscall.Handle(ret), nil
+}
+
+const (
+ WM_CREATE = 0x0001
+ WM_DESTROY = 0x0002
+ WM_SHOWWINDOW = 0x0018
+)
+
+func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
+ ret, _, _ := pDefWindowProcW.Call(
+ uintptr(hwnd),
+ uintptr(msg),
+ uintptr(wparam),
+ uintptr(lparam),
+ )
+ return uintptr(ret)
+}
+
+func destroyWindow(hwnd syscall.Handle) error {
+ ret, _, err := pDestroyWindow.Call(uintptr(hwnd))
+ if ret == 0 {
+ return err
+ }
+ return nil
+}
+
+type tPOINT struct {
+ x, y int32
+}
+
+type tMSG struct {
+ hwnd syscall.Handle
+ message uint32
+ wParam uintptr
+ lParam uintptr
+ time uint32
+ pt tPOINT
+}
+
+func dispatchMessage(msg *tMSG) {
+ pDispatchMessageW.Call(uintptr(unsafe.Pointer(msg)))
+}
+
+func getMessage(msg *tMSG, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) {
+ ret, _, err := pGetMessageW.Call(
+ uintptr(unsafe.Pointer(msg)),
+ uintptr(hwnd),
+ uintptr(msgFilterMin),
+ uintptr(msgFilterMax),
+ )
+ if int32(ret) == -1 {
+ return false, err
+ }
+ return int32(ret) != 0, nil
+}
+
+func postQuitMessage(exitCode int32) {
+ pPostQuitMessage.Call(uintptr(exitCode))
+}
+
+type WNDCLASSEXW struct {
+ size uint32
+ style uint32
+ wndProc uintptr
+ clsExtra int32
+ wndExtra int32
+ instance syscall.Handle
+ icon syscall.Handle
+ cursor syscall.Handle
+ background syscall.Handle
+ menuName *uint16
+ className *uint16
+ iconSm syscall.Handle
+}
+
+func registerClassEx(wcx *WNDCLASSEXW) (uint16, error) {
+ ret, _, err := pRegisterClassExW.Call(
+ uintptr(unsafe.Pointer(wcx)),
+ )
+ if ret == 0 {
+ return 0, err
+ }
+ return uint16(ret), nil
+}
+
+func translateMessage(msg *tMSG) {
+ pTranslateMessage.Call(uintptr(unsafe.Pointer(msg)))
+}
+
+func CreateWindow(exit chan bool, pid int, splashImage string, width, height uint32) {
+
+ slog.Info("splash")
+
+ className := "E39055F1-BFCB-4FB9-983F-CDC766E39B93" //Random GUID
+
+ instance, err := getModuleHandle()
+ if err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ return
+ }
+
+ var win syscall.Handle
+
+ activeWinEventHook := func(hWinEventHook syscall.Handle, event uint32, hwnd syscall.Handle, idObject int32, idChild int32, idEventThread uint32, dwmsEventTime uint32) uintptr {
+
+ if event == EVENT_OBJECT_SHOW &&
+ (idObject == OBJID_CURSOR || idObject == OBJID_WINDOW){
+ slog.Info("Splash bye bye")
+ destroyWindow(win)
+ unhookWinEvent(hWinEventHook)
+ }
+
+ return 0
+ }
+
+ lpfnWndProc := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
+ switch msg {
+ case WM_DESTROY:
+ postQuitMessage(0)
+ exit <- true
+ case WM_SHOWWINDOW: {
+ _, err := setWinEventHook(
+ EVENT_SYSTEM_FOREGROUND,
+ EVENT_OBJECT_SHOW,
+ 0,
+ syscall.NewCallback(activeWinEventHook),
+ pid,
+ 0,
+ WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
+ )
+ if err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ }
+
+ }
+ default:
+ ret := defWindowProc(hwnd, msg, wparam, lparam)
+ return ret
+ }
+ return 0
+ }
+
+
+ //load bitmap
+ hbm , err := loadImage(splashImage)
+ if err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ return
+ }
+ //and create an HBRUSH around it using CreatePatternBrush(), and then assign that to the WNDCLASS::hbrBackground
+ hbrush, err := createPatternBrush(hbm)
+ if err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ return
+ }
+
+ wcx := WNDCLASSEXW{
+ wndProc: syscall.NewCallback(lpfnWndProc),
+ instance: instance,
+ background: hbrush,
+ className: syscall.StringToUTF16Ptr(className),
+ }
+ wcx.size = uint32(unsafe.Sizeof(wcx))
+
+ if _, err = registerClassEx(&wcx); err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ return
+ }
+
+ //Get screen resolution
+ hDC, err := getDC(0)
+ if err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ return
+ }
+ defer releaseDC(0, hDC)
+ screenWidth := getDeviceCaps(hDC, HORZRES)
+ screenHeight := getDeviceCaps(hDC, VERTRES)
+
+ //create window
+ win, err = createWindow(
+ className,
+ "Red Alert 3",
+ cWS_EX_TOOLWINDOW | cWS_EX_TOPMOST,
+ cWS_VISIBLE | cWS_POPUP | cWS_TABSTOP,
+ (screenWidth - width) / 2, //center X
+ (screenHeight - height) / 2, //center Y
+ width,
+ height,
+ 0,
+ 0,
+ instance,
+ )
+ if err != nil {
+ slog.Error(err.Error())
+ exit <- true
+ return
+ }
+
+ for {
+ msg := tMSG{}
+ gotMessage, err := getMessage(&msg, 0, 0, 0)
+ if err != nil {
+ slog.Error(err.Error())
+ return
+ }
+
+ if gotMessage {
+ translateMessage(&msg)
+ dispatchMessage(&msg)
+ } else {
+ break
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/keygen.go b/src/keygen.go
new file mode 100644
index 0000000..11b7bac
--- /dev/null
+++ b/src/keygen.go
@@ -0,0 +1,68 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package main
+
+import(
+ "math/rand"
+ "log/slog"
+ "launcher/internal/regedit"
+)
+
+func randAlphaNumString(length int) string {
+ //cf: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
+
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ const IdxBits = 6
+ const IdxMask = 1<= 0; {
+ if remain == 0 {
+ cache, remain = rand.Int63(), IdxMax
+ }
+ if idx := int(cache & IdxMask); idx < len(charset) {
+ bytes[i] = charset[idx]
+ i--
+ }
+ cache >>= IdxBits
+ remain--
+ }
+ return string(bytes)
+}
+
+func keygen(){ //Elevated perm required
+
+ /*
+ LAN play requires each player to have a different CD Key.
+ Steam no longer populates the %CDKEY% variable in the registry (is it due to EA's servers shutdown ?)
+ This is a problem because each time the game is ran by Steam; Steam will write "%CDKEY%" as the user's CD key.
+ Every (Steam) players will therefore end up with the same CD Key: "%CDKEY%" and are unable to LAN play with each others.
+ The registry value is read on game launch. It is uneffective to change it after the game is ran by Steam.
+
+ Since EA shutdown their servers for this game. Having a "real" and legit CD Key is of no concerns here.
+ Generate a random alphanum string and write it to the registry if the CD Key is "%CDKEY%" or there is none.
+
+ Unfortunately writing to HKLM (registry) requires elevated privileges (UAC/Adming rights).
+ */
+
+ registration := regedit.RegQueryStringValue("HKLM", DEFAULT_REG_PATH, "Registration")
+ if registration == "" {
+ registration = DEFAULT_REG_PATH + "\\ergc"
+ regedit.RegWriteStringValue("HKLM", DEFAULT_REG_PATH, "Registration", registration) //elevated
+ }
+
+ regkey := regedit.RegQueryStringValue("HKLM", registration, "")
+
+ if regkey == "%CDKEY%" || regkey == "" {
+ slog.Warn("RA3 CD Key not found ! This will interfere with LAN play")
+
+ key:= randAlphaNumString(20)
+ regedit.RegWriteStringValue("HKLM", registration, "", key) //elevated
+ slog.Info("Generated random CD Key to enable LAN play")
+ }
+}
\ No newline at end of file
diff --git a/src/launcher.go b/src/launcher.go
new file mode 100644
index 0000000..39f4cc6
--- /dev/null
+++ b/src/launcher.go
@@ -0,0 +1,222 @@
+/*
+Copyright (c) Anthony Beaumont
+This source code is licensed under the MIT License
+found in the LICENSE file in the root directory of this source tree.
+*/
+
+package main
+
+import(
+ "flag"
+ "os"
+ "path/filepath"
+ "strings"
+ "strconv"
+ "os/exec"
+ "syscall"
+ "math/rand"
+ "log/slog"
+ "launcher/internal/regedit"
+ "launcher/internal/splash"
+ "launcher/internal/hook"
+)
+
+type Args struct {
+ xres int
+ yres int
+ xpos int
+ ypos int
+ win bool
+ fullscreen bool
+ noaudio bool
+ noAudioMusic bool
+ silentLogin bool
+ help bool
+ modConfig string
+ replayGame string
+ //ui bool //UNIMPLEMENTED: Opens the autorun feature otherwise called by inserting the game disc > no gui but splash screen is implemented
+ //getPatch bool //DEPRECATED: Forces check for official updates > currently useless as EA's servers have been shut down
+ //runver int //DEPRECATED: Select which version of the game to run > does not work with Steam version (ra3game.dat)
+}
+
+type Addon struct {
+ Path string `json:"path"`
+ Required bool `json:"required"`
+}
+
+type Config struct {
+ Version string `json:"version"`
+ Lang string `json:"lang"`
+ Upheaval bool `json:"upheaval"`
+ Keygen bool `json:"keygen"`
+ Borderless bool `json:"borderless"`
+ Splash bool `json:"splash"`
+ SplashImage []string `json:"splash_image"`
+ Addons []Addon `json:"addons"`
+}
+
+const DEFAULT_REG_PATH = "SOFTWARE\\Electronic Arts\\Electronic Arts\\Red Alert 3"
+
+func parseArgs() (args Args) {
+ flag.IntVar(&args.xres, "xres", 0, "Sets resolution width")
+ flag.IntVar(&args.yres, "yres", 0, "Sets resolution height")
+ flag.IntVar(&args.xpos, "xpos", 0, "Sets horizontal offset of the window")
+ flag.IntVar(&args.ypos, "ypos", 0, "Sets vertical offset of the window")
+ flag.BoolVar(&args.win, "win", false, "Runs the game in windowed mode")
+ flag.BoolVar(&args.fullscreen, "fullscreen", false, "Runs the game in fullscreen mode. Combine with -win for borderless windowed mode")
+ flag.BoolVar(&args.noaudio, "noaudio", false, "Disables game audio")
+ flag.BoolVar(&args.noAudioMusic, "noAudioMusic", false, "Disables game music")
+ flag.BoolVar(&args.silentLogin, "silentLogin", false, "Forces the game to immediately log in to a multiplayer account")
+ flag.BoolVar(&args.help, "help", false, "Show list of all arguments")
+ flag.StringVar(&args.modConfig, "modConfig", "", "Runs the game with selected mod (has to point to its .skudef file)")
+ flag.StringVar(&args.replayGame, "replayGame", "", "Plays replay file")
+ flag.Parse()
+
+ if args.help { displayHelp() }
+
+ return
+}
+
+func buildCommandLine(root string, args *Args, config *Config) string {
+
+ if(config.Lang == "auto"){
+ reglang := regedit.RegQueryStringValue("HKCU", DEFAULT_REG_PATH, "language")
+ if len(reglang) > 0 {
+ config.Lang = strings.ToLower(reglang)
+ } else {
+ slog.Warn("Unable to determine game's language... defaulting to \"english\"")
+ config.Lang = "english"
+ }
+ }
+ slog.Info("Language: " + config.Lang)
+
+ skudef := "RA3_" + config.Lang + "_" + config.Version + ".SkuDef"
+ cmdLine := []string{ "-config", "\"" + filepath.Join(root, skudef) + "\"" }
+
+ if len(args.modConfig) > 0 {
+ if !filepath.IsAbs(args.modConfig) {
+ slog.Info("Looking for mod \"" + args.modConfig + "\"...")
+ dirpath := filepath.Join(getUserProfilePath(),"Red Alert 3/Mods", args.modConfig)
+ for _, modconfig := range findFilesWithExt(dirpath, ".skudef") {
+ if strings.Contains(filepath.Base(modconfig), args.modConfig) {
+ slog.Info("Found mod at \"" + modconfig + "\"")
+ cmdLine = append(cmdLine, "-modConfig " + "\"" + modconfig + "\"")
+ break
+ }
+ }
+ } else {
+ cmdLine = append(cmdLine, "-modConfig " + "\"" + args.modConfig + "\"")
+ }
+ } else if config.Upheaval {
+ locations := []string{
+ filepath.Join(root, "Upheaval_1.16.SkuDef"),
+ filepath.Join(getUserProfilePath(), "Red Alert 3/Mods/Upheaval/Upheaval_1.16.SkuDef")}
+
+ for _, location := range locations {
+ if fileExist(location) {
+ cmdLine = append(cmdLine, "-modConfig " + "\"" + location + "\"")
+ break
+ }
+ }
+ }
+
+ if config.Borderless {
+ cmdLine = append(cmdLine, "-win", "-fullscreen")
+ }
+
+ //passthrough
+ if args.xres > 0 && args.yres > 0 {
+ cmdLine = append(cmdLine,
+ "-xres " + strconv.Itoa(args.xres),
+ "-yres " + strconv.Itoa(args.yres))
+ }
+ if args.xpos > 0 && args.ypos > 0 {
+ cmdLine = append(cmdLine,
+ "-xpos " + strconv.Itoa(args.xpos),
+ "-ypos " + strconv.Itoa(args.ypos))
+ }
+ if args.win && !config.Borderless {
+ cmdLine = append(cmdLine, "-win")
+ }
+ if args.fullscreen && !config.Borderless {
+ cmdLine = append(cmdLine, "-fullscreen")
+ }
+ if args.noaudio {
+ cmdLine = append(cmdLine, "-noaudio")
+ }
+ if args.noAudioMusic {
+ cmdLine = append(cmdLine, "-noAudioMusic")
+ }
+ if args.silentLogin {
+ cmdLine = append(cmdLine, "-silentLogin")
+ }
+ if len(args.replayGame) > 0 {
+ cmdLine = append(cmdLine, "-replayGame " + args.replayGame)
+ }
+
+ return strings.Join(cmdLine, " ")
+}
+
+func main(){
+
+ args := parseArgs()
+ root := locate()
+
+ config, err := readJSON(filepath.Join(root, "RA3.json"))
+ if err != nil { alert(err.Error()) }
+
+ binary := filepath.Join(root, "/Data/", "ra3_" + config.Version + ".game")
+ cmdLine := buildCommandLine(root, &args, &config)
+
+ if config.Keygen { keygen() }
+
+ cmd := exec.Command(binary)
+ argv := []string{ "\"" + binary + "\"", cmdLine }
+ cmd.SysProcAttr = &syscall.SysProcAttr{ CmdLine: strings.Join(argv, " ") } //verbatim arguments
+ cmd.Dir = root
+ cmd.Env = os.Environ()
+ cmd.Stdin = nil
+ cmd.Stdout = nil
+ cmd.Stderr = nil
+ err = cmd.Start()
+ if err != nil { alert(err.Error()) }
+
+ //splash screen
+ exit := make(chan bool)
+ if config.Splash {
+ splashImage := config.SplashImage[rand.Intn(len(config.SplashImage))]
+ if !filepath.IsAbs(splashImage) {
+ splashImage = filepath.Join(root, splashImage)
+ }
+ go splash.CreateWindow(exit, cmd.Process.Pid, splashImage, 640, 480)
+ } else {
+ go func(exit chan bool){
+ exit <- true
+ }(exit)
+ }
+
+ //Addons
+ if len(config.Addons) > 0 {
+ for _, addon := range config.Addons {
+
+ dylib := addon.Path
+ if !filepath.IsAbs(dylib) {
+ dylib = filepath.Join(root, dylib)
+ }
+
+ if fileExist(dylib){
+ err = hook.CreateRemoteThread(uintptr(cmd.Process.Pid), dylib)
+ if err != nil {
+ if addon.Required {
+ cmd.Process.Kill()
+ alert(err.Error())
+ } else {
+ slog.Error(err.Error())
+ }
+ }
+ }
+ }
+ }
+
+ <-exit
+}
\ No newline at end of file
diff --git a/winres/icon.ico b/winres/icon.ico
new file mode 100644
index 0000000..67a984a
Binary files /dev/null and b/winres/icon.ico differ
diff --git a/winres/icon.png b/winres/icon.png
new file mode 100644
index 0000000..44f8e05
Binary files /dev/null and b/winres/icon.png differ
diff --git a/winres/winres.json b/winres/winres.json
new file mode 100644
index 0000000..7a5d7e6
--- /dev/null
+++ b/winres/winres.json
@@ -0,0 +1,58 @@
+{
+ "RT_GROUP_ICON": {
+ "#42": {
+ "0409": "icon.ico"
+ }
+ },
+ "RT_MANIFEST": {
+ "#1": {
+ "0409": {
+ "identity": {
+ "name": "",
+ "version": ""
+ },
+ "description": "Red Alert 3 Launcher",
+ "minimum-os": "win10",
+ "execution-level": "as invoker",
+ "ui-access": false,
+ "auto-elevate": false,
+ "dpi-awareness": "per monitor v2",
+ "disable-theming": false,
+ "disable-window-filtering": false,
+ "high-resolution-scrolling-aware": false,
+ "ultra-high-resolution-scrolling-aware": false,
+ "long-path-aware": false,
+ "printer-driver-isolation": false,
+ "gdi-scaling": false,
+ "segment-heap": false,
+ "use-common-controls-v6": false
+ }
+ }
+ },
+ "RT_VERSION": {
+ "#1": {
+ "0000": {
+ "fixed": {
+ "file_version": "1.0.0.0",
+ "product_version": "1.0.0.0"
+ },
+ "info": {
+ "0409": {
+ "Comments": "https://xan105.com",
+ "CompanyName": "Xan",
+ "FileDescription": "Red Alert 3 Launcher",
+ "FileVersion": "1.0.0.0",
+ "InternalName": "RA3.exe",
+ "LegalCopyright": "Copyright (C) 2024",
+ "LegalTrademarks": "",
+ "OriginalFilename": "RA3.exe",
+ "PrivateBuild": "",
+ "ProductName": "Command & Conquer™ Red Alert™ 3",
+ "ProductVersion": "1.0.0.0",
+ "SpecialBuild": ""
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file