From c208d2166437cb8d7366257b4bf96738288f2120 Mon Sep 17 00:00:00 2001
From: Belonit <54427022+Belonit@users.noreply.github.com>
Date: Mon, 12 Oct 2026 09:48:47 +0300
Subject: [PATCH] Add the multiplayer save button dynamically
---
Spawner.vcxproj | 5 +-
cspell.json | 1 +
src/Spawner/Spawner.Config.cpp | 15 ++--
src/Spawner/Spawner.Config.h | 2 +
src/UI/Dialogs.cpp | 100 -------------------------
src/UI/Dialogs.h | 23 ------
src/UI/Hooks.SaveButton.cpp | 80 ++++++++++++++++++++
src/UI/Hooks.cpp | 17 -----
src/UI/MultiplayerGameOptionsDialog.rc | 14 ----
9 files changed, 92 insertions(+), 165 deletions(-)
delete mode 100644 src/UI/Dialogs.cpp
delete mode 100644 src/UI/Dialogs.h
create mode 100644 src/UI/Hooks.SaveButton.cpp
delete mode 100644 src/UI/Hooks.cpp
delete mode 100644 src/UI/MultiplayerGameOptionsDialog.rc
diff --git a/Spawner.vcxproj b/Spawner.vcxproj
index ebb20a7c..a3280b9d 100644
--- a/Spawner.vcxproj
+++ b/Spawner.vcxproj
@@ -67,9 +67,7 @@
-
-
-
+
@@ -96,7 +94,6 @@
-
diff --git a/cspell.json b/cspell.json
index 57fd1320..b537d52b 100644
--- a/cspell.json
+++ b/cspell.json
@@ -354,6 +354,7 @@
"NOINTERFACE",
"NOMINMAX",
"NOWINDOWCHANGES",
+ "NOZORDER",
"ntdll",
"ntfnce",
"NTRLMD",
diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp
index 1b4faedc..33004f97 100644
--- a/src/Spawner/Spawner.Config.cpp
+++ b/src/Spawner/Spawner.Config.cpp
@@ -54,13 +54,14 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI)
}
{ // SaveGame Options
- LoadSaveGame = pINI->ReadBool(pSettingsSection, "LoadSaveGame", LoadSaveGame);
- /* SavedGameDir */ pINI->ReadString(pSettingsSection, "SavedGameDir", SavedGameDir, SavedGameDir, sizeof(SavedGameDir));
- /* SaveGameName */ pINI->ReadString(pSettingsSection, "SaveGameName", SaveGameName, SaveGameName, sizeof(SaveGameName));
- CustomMissionID = pINI->ReadInteger(pSettingsSection, "CustomMissionID", 0);
- AutoSaveCount = pINI->ReadInteger(pSettingsSection, "AutoSaveCount", AutoSaveCount);
- AutoSaveInterval = pINI->ReadInteger(pSettingsSection, "AutoSaveInterval", AutoSaveInterval);
- NextAutoSaveNumber = pINI->ReadInteger(pSettingsSection, "NextAutoSaveNumber", NextAutoSaveNumber);
+ LoadSaveGame = pINI->ReadBool(pSettingsSection, "LoadSaveGame", LoadSaveGame);
+ /* SavedGameDir */ pINI->ReadString(pSettingsSection, "SavedGameDir", SavedGameDir, SavedGameDir, sizeof(SavedGameDir));
+ /* SaveGameName */ pINI->ReadString(pSettingsSection, "SaveGameName", SaveGameName, SaveGameName, sizeof(SaveGameName));
+ CustomMissionID = pINI->ReadInteger(pSettingsSection, "CustomMissionID", 0);
+ AutoSaveCount = pINI->ReadInteger(pSettingsSection, "AutoSaveCount", AutoSaveCount);
+ AutoSaveInterval = pINI->ReadInteger(pSettingsSection, "AutoSaveInterval", AutoSaveInterval);
+ NextAutoSaveNumber = pINI->ReadInteger(pSettingsSection, "NextAutoSaveNumber", NextAutoSaveNumber);
+ ShowMPSaveGameButton = pINI->ReadBool(pSettingsSection, "ShowMPSaveGameButton", ShowMPSaveGameButton);
}
{ // Scenario Options
diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h
index 1432587e..f7834af8 100644
--- a/src/Spawner/Spawner.Config.h
+++ b/src/Spawner/Spawner.Config.h
@@ -103,6 +103,7 @@ class SpawnerConfig
int AutoSaveCount;
int AutoSaveInterval;
int NextAutoSaveNumber;
+ bool ShowMPSaveGameButton;
// Scenario Options
int Seed;
@@ -179,6 +180,7 @@ class SpawnerConfig
, AutoSaveCount { 5 }
, AutoSaveInterval { 7200 }
, NextAutoSaveNumber { 0 }
+ , ShowMPSaveGameButton { true }
// Scenario Options
, Seed { 0 }
diff --git a/src/UI/Dialogs.cpp b/src/UI/Dialogs.cpp
deleted file mode 100644
index 856019be..00000000
--- a/src/UI/Dialogs.cpp
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- So here's a first example on how to modify the game's dialogs! :3
-
- To port a dialog from YR to the DLL, add a new .rc file and don't bother editing it
- using MSVC's inbuilt tools.
- Open gamemd.exe in ResHacker, open the selected dialog and copy the whole resource script to
- the new .rc file.
-
- Don't edit the dialog ID. That would be adding a new dialog and that's a pain to get working
- apparently (I don't know how to do that yet).
- Add #include to the first line of the script.
- If STYLE doesn't list WS_CAPTION, remove the CAPTION "" line.
-
- Adding new controls is best done in ResHacker, as MSVC wants to add too much stupid
- stuff to things (resource.h and crap like that).
- Also remember to set the control properties like the game originals to avoid problems.
- Finally, be aware that MSVC will automatically add WS_VISIBLE to the controls.
- I don't know why, but I know it sucks, as you'll see when you use the RMG with this version.
- I've yet to find a way to suppress this behavior.
-
- If you add buttons, they'll look like shit until you register them in the dialog function.
-
- Adding new dialogs requires many funny hacks I'll try to figure later.
-
- To make our systems consitent:
- New control IDs within a dialog start with 5000.
-
- Each dialog code should get its own cpp file to avoid major confusion.
- This central source file contains an addition to YR's "FetchResource"
- that's able to find resources within this DLL as well.
- It's important for it to be just an addition so other DLLs can do this as well!
-
- Happy GUI modding!
- -pd
-*/
-
-#include "Dialogs.h"
-
-#include
-#include
-#include
-#include
-#include
-
-//4A3B4B, 9 - NOTE: This overrides a call, but it's absolute, so don't worry.
-DEFINE_HOOK(0x4A3B4B, FetchResource, 0x9)
-{
- if (!Spawner::Enabled)
- return 0;
-
- HMODULE hModule = static_cast(Main::hInstance); //hModule and hInstance are technically the same...
- GET(LPCTSTR, lpName, ECX);
- GET(LPCTSTR, lpType, EDX);
-
- if(HRSRC hResInfo = FindResource(hModule, lpName, lpType)) {
- if(HGLOBAL hResData = LoadResource(hModule, hResInfo)) {
- LockResource(hResData);
- R->EAX(hResData);
-
- return 0x4A3B73; //Resource locked and loaded (omg what a pun), return!
- }
- }
- return 0; //Nothing was found, try the game's own resources.
-}
-
-/*
-DEFINE_HOOK(0x60411B, Game_DialogFunc_Subtext_Load, 0x5)
-{
- GET(int, DlgItemID, EAX);
-
- Dialogs::StatusString = nullptr;
- if(DlgItemID == -1) {
- return 0x604120;
- }
- if(DlgItemID >= ARES_GUI_START) {
- switch(DlgItemID) {
- case ARES_CHK_RMG_URBAN_AREAS:
- Dialogs::StatusString = "STT:RMGUrbanAreas";
- break;
- case ARES_CHK_MULTIENGINEER:
- Dialogs::StatusString = "STT:MultiEngineer";
- break;
- default:
- Dialogs::StatusString = "GUI:Debug";
- }
- return 0x604135;
- }
- return 0x604126;
-}
-
-
-DEFINE_HOOK(0x604136, Game_DialogFunc_Subtext_Propagate, 0x5)
-{
- if(Dialogs::StatusString) {
- R->EAX(Dialogs::StatusString);
- return 0x60413B;
- }
- return 0;
-}
-*/
diff --git a/src/UI/Dialogs.h b/src/UI/Dialogs.h
deleted file mode 100644
index 4f02a04d..00000000
--- a/src/UI/Dialogs.h
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-
-#include
-/*
-enum VanillaDialogs : uint16_t
-{
- SingleplayerGameOptionsDialog = 181,
- MultiplayerGameOptionsDialog = 3002
-};
-*/
-// For now this can only contain copies of the original dialogs with same IDs
-enum SpawnerCustomDialogs : uint16_t
-{
- MultiplayerGameOptionsDialog = 3002, // added a save button
-
- First = 3002,
- Last = 3002
-};
-
-class Dialogs {
-public:
-
-};
diff --git a/src/UI/Hooks.SaveButton.cpp b/src/UI/Hooks.SaveButton.cpp
new file mode 100644
index 00000000..ca72cabf
--- /dev/null
+++ b/src/UI/Hooks.SaveButton.cpp
@@ -0,0 +1,80 @@
+#include
+#include
+#include
+#include
+
+enum GameOptionsControlID
+{
+ SaveGameButton = 1311,
+ GameControlsButton = 1313,
+ AbortMissionButton = 1314
+};
+
+// Create the button before StandardWndProc registers and formats the controls.
+DEFINE_HOOK(0x4F11D6, MultiplayerGameOptionsDialog_AddSaveButton, 0x6)
+{
+ enum { MultiplayerGameOptionsDialog = 3002 };
+
+ if (!Spawner::Enabled || !Spawner::GetConfig()->ShowMPSaveGameButton)
+ return 0;
+
+ GET(UINT, message, EBX);
+ GET(HWND, hDialog, ESI);
+ GET(const WORD*, initData, EBP);
+
+ if (message != WM_INITDIALOG || !initData || *initData != MultiplayerGameOptionsDialog)
+ return 0;
+
+ const auto hExistingSave = Imports::GetDlgItem(hDialog, SaveGameButton);
+ if (hExistingSave)
+ return 0;
+
+ const auto hControls = Imports::GetDlgItem(hDialog, GameControlsButton);
+ const auto hAbort = Imports::GetDlgItem(hDialog, AbortMissionButton);
+ if (!hControls || !hAbort)
+ return 0;
+
+ RECT controlsRect;
+ RECT abortRect;
+ const auto hasControlsRect = Imports::GetWindowRect(hControls, &controlsRect);
+ const auto hasAbortRect = Imports::GetWindowRect(hAbort, &abortRect);
+ if (!hasControlsRect || !hasAbortRect)
+ return 0;
+
+ const int buttonStepY = abortRect.top - controlsRect.top;
+ MapWindowPoints(nullptr, hDialog, reinterpret_cast(&abortRect), 2);
+ const int width = abortRect.right - abortRect.left;
+ const int height = abortRect.bottom - abortRect.top;
+ const auto hInstance = reinterpret_cast(Imports::GetWindowLongA(hDialog, GWL_HINSTANCE));
+ const auto hSave = Imports::CreateWindowExA(0, "BUTTON", "GUI:SaveGame",
+ WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
+ abortRect.left, abortRect.top, width, height, hDialog,
+ reinterpret_cast(SaveGameButton), hInstance, nullptr);
+
+ if (!hSave)
+ return 0;
+
+ const auto font = Imports::SendMessageA(hAbort, WM_GETFONT, 0, 0);
+ Imports::SendMessageA(hSave, WM_SETFONT, font, FALSE);
+
+ // Place the save button after game controls in the child window order.
+ Imports::SetWindowPos(hSave, hControls, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+ Imports::SetWindowPos(hAbort, nullptr, abortRect.left, abortRect.top + buttonStepY,
+ 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+}
+
+DEFINE_HOOK(0x609299, UI_IsStaticAndOrOwnerDraw_MultiplayerGameOptionsDialog, 0x5)
+{
+ if (!Spawner::Enabled)
+ return 0;
+
+ enum { RetTrue = 0x609693 };
+ GET(int, dlgCtrlID, EAX);
+
+ if (dlgCtrlID == SaveGameButton)
+ return RetTrue;
+
+ return 0;
+}
diff --git a/src/UI/Hooks.cpp b/src/UI/Hooks.cpp
deleted file mode 100644
index c020fdee..00000000
--- a/src/UI/Hooks.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-
-#include "Dialogs.h"
-#include
-
-#include
-#include
-
-DEFINE_HOOK(0x609299, UI_IsStaticAndOrOwnerDraw_MultiplayerGameOptionsDialog, 0x5)
-{
- if (!Spawner::Enabled)
- return 0;
-
- enum { RetFalse = 0x609664, RetTrue = 0x609693 };
-
- GET(int, dlgCtrlID, EAX);
- return (dlgCtrlID == 1314 || dlgCtrlID == 1313 || dlgCtrlID == 1311) ? RetTrue : RetFalse;
-}
diff --git a/src/UI/MultiplayerGameOptionsDialog.rc b/src/UI/MultiplayerGameOptionsDialog.rc
deleted file mode 100644
index 01783b17..00000000
--- a/src/UI/MultiplayerGameOptionsDialog.rc
+++ /dev/null
@@ -1,14 +0,0 @@
-#include
-
-3002 DIALOG 0, 0, 533, 369
-STYLE DS_SETFONT | WS_CHILD
-LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
-FONT 8, "MS Sans Serif"
-{
- CONTROL "GUI:GameOptions", 1684, STATIC, SS_CENTER | WS_CHILD | WS_VISIBLE | WS_GROUP, 425, 1, 108, 10
- CONTROL "GUI:GameControls", 1313, BUTTON, BS_OWNERDRAW | WS_CHILD | WS_VISIBLE, 425, 122, 108, 23
- CONTROL "GUI:SaveGame", 1311, BUTTON, BS_OWNERDRAW | WS_CHILD | WS_VISIBLE, 425, 149, 108, 23
- CONTROL "GUI:AbortMission", 1314, BUTTON, BS_OWNERDRAW | WS_CHILD | WS_VISIBLE, 425, 176, 108, 23
- CONTROL "GUI:ResumeMission", 1670, BUTTON, BS_OWNERDRAW | WS_CHILD | WS_VISIBLE, 425, 346, 108, 23
- CONTROL "GUI:Blank", 1685, STATIC, SS_LEFT | SS_CENTERIMAGE | WS_CHILD | WS_VISIBLE, 2, 355, 303, 12
-}
\ No newline at end of file