diff --git a/README.md b/README.md
index 7ac37497..4fb8e704 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,16 @@ Credits
- **[Kerbiter (Metadorius)](https://github.com/Metadorius)**
- Further maintenance
- Event verification checks
+ - Save button for multiplayer pause menu
+ - Beacon crash fix for multiplayer save/load
+- **[TaranDahl](https://github.com/TaranDahl)**
+ - Porting of multiplayer save/load
+ - Porting of autosaves
- **[Rampastring](https://github.com/Rampastring)**
- Original event verification checks
+- **[Vinifera](https://github.com/Vinifera-Developers/Vinifera) Contributors and [TS Patches](https://github.com/CnCNet/ts-patches) Contributors**
+ - Original TS implementation of multiplayer save/load
+ - Original TS implementation of autosaves
- **[CnCNet](https://github.com/CnCNet) Contributors** - the [original spawner](https://github.com/CnCNet/yr-patches)
- **[Ares](https://github.com/Ares-Developers/Ares) and [Phobos](https://github.com/Phobos-developers/Phobos) Contributors** - [YRpp](https://github.com/Phobos-developers/yrpp) and [Syringe](https://github.com/Ares-Developers/Syringe) which are used and some code snippets
diff --git a/Spawner.vcxproj b/Spawner.vcxproj
index 3aff89a0..b59cbe45 100644
--- a/Spawner.vcxproj
+++ b/Spawner.vcxproj
@@ -17,6 +17,8 @@
+
+
@@ -63,6 +65,8 @@
+
+
@@ -76,6 +80,7 @@
+
diff --git a/src/Ext/Event/Body.cpp b/src/Ext/Event/Body.cpp
index 0377c836..ced60a96 100644
--- a/src/Ext/Event/Body.cpp
+++ b/src/Ext/Event/Body.cpp
@@ -19,6 +19,7 @@
#include "Body.h"
#include
+#include
#include
#include
diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp
index 0310fe45..889a45d5 100644
--- a/src/Spawner/Spawner.Config.cpp
+++ b/src/Spawner/Spawner.Config.cpp
@@ -52,10 +52,14 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI)
MultiByteToWideChar(CP_UTF8, 0, Main::readBuffer, strlen(Main::readBuffer), UIGameMode, std::size(UIGameMode));
}
- // 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));
+ {// 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));
+ AutoSaveCount = pINI->ReadInteger(pSettingsSection, "AutoSaveCount", AutoSaveCount);
+ AutoSaveInterval = pINI->ReadInteger(pSettingsSection, "AutoSaveInterval", AutoSaveInterval);
+ NextAutoSaveNumber = pINI->ReadInteger(pSettingsSection, "NextAutoSaveNumber", NextAutoSaveNumber);
+ }
{ // Scenario Options
Seed = pINI->ReadInteger(pSettingsSection, "Seed", Seed);
diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h
index 73f5cceb..6c31dde3 100644
--- a/src/Spawner/Spawner.Config.h
+++ b/src/Spawner/Spawner.Config.h
@@ -94,6 +94,9 @@ class SpawnerConfig
bool LoadSaveGame;
char SavedGameDir[MAX_PATH]; // Nested paths are also supported, e.g. "Saved Games\\Yuri's Revenge"
char SaveGameName[60];
+ int AutoSaveCount;
+ int AutoSaveInterval;
+ int NextAutoSaveNumber;
// Scenario Options
int Seed;
@@ -161,6 +164,9 @@ class SpawnerConfig
, LoadSaveGame { false }
, SavedGameDir { "Saved Games" }
, SaveGameName { "" }
+ , AutoSaveCount { 5 }
+ , AutoSaveInterval { 7200 }
+ , NextAutoSaveNumber { 0 }
// Scenario Options
, Seed { 0 }
diff --git a/src/Spawner/Spawner.Hook.cpp b/src/Spawner/Spawner.Hook.cpp
index 672d0a6d..3eb10e9b 100644
--- a/src/Spawner/Spawner.Hook.cpp
+++ b/src/Spawner/Spawner.Hook.cpp
@@ -22,8 +22,10 @@
#include
#include
+#include
#include
#include
+#include
DEFINE_HOOK(0x6BD7C5, WinMain_SpawnerInit, 0x6)
{
@@ -179,3 +181,57 @@ DEFINE_HOOK(0x4FC57C, HouseClass__MPlayerDefeated_CheckAliveAndHumans, 0x7)
}
#pragma endregion MPlayerDefeated
+
+#pragma region Save&Load
+
+DEFINE_HOOK_AGAIN(0x624271, SomeFunc_InterceptMainLoop, 0x5);
+DEFINE_HOOK_AGAIN(0x623D72, SomeFunc_InterceptMainLoop, 0x5);
+DEFINE_HOOK_AGAIN(0x62314E, SomeFunc_InterceptMainLoop, 0x5);
+DEFINE_HOOK_AGAIN(0x60D407, SomeFunc_InterceptMainLoop, 0x5);
+DEFINE_HOOK_AGAIN(0x608206, SomeFunc_InterceptMainLoop, 0x5);
+DEFINE_HOOK(0x48CE8A, SomeFunc_InterceptMainLoop, 0x5)
+{
+ /**
+ * Main loop.
+ */
+ Game::MainLoop();
+
+ /**
+ * After loop.
+ */
+ Spawner::After_Main_Loop();
+ return R->Origin() + 0x5;
+}
+
+DEFINE_HOOK(0x52DAED, Game_Start_ResetGlobal, 0x7)
+{
+ Spawner::DoSave = false;
+ Spawner::NextAutoSaveFrame = -1;
+ Spawner::NextAutoSaveNumber = 0;
+ return 0;
+}
+
+DEFINE_HOOK(0x686B20, INIClass_ReadScenario_AutoSave, 0x6)
+{
+ /**
+ * Schedule the next autosave.
+ */
+ Spawner::NextAutoSaveFrame = Unsorted::CurrentFrame;
+ Spawner::NextAutoSaveFrame += Spawner::GetConfig()->AutoSaveInterval;
+ return 0;
+}
+
+DEFINE_HOOK(0x4C7A14, EventClass_RespondToEvent_SaveGame, 0x5)
+{
+ Spawner::DoSave = true;
+ return 0x4C7B42;
+}
+
+// for some reason beacons are only inited on scenario init, which doesn't happen on load
+DEFINE_HOOK(0x67E6DA, LoadGame_AfterInit, 0x6)
+{
+ BeaconManagerClass::Instance.LoadArt();
+ return 0;
+}
+
+#pragma endregion
diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp
index 10edfc75..1682bf4c 100644
--- a/src/Spawner/Spawner.cpp
+++ b/src/Spawner/Spawner.cpp
@@ -41,6 +41,9 @@
bool Spawner::Enabled = false;
bool Spawner::Active = false;
std::unique_ptr Spawner::Config = nullptr;
+bool Spawner::DoSave = false;
+int Spawner::NextAutoSaveFrame = -1;
+int Spawner::NextAutoSaveNumber = 0;
void Spawner::Init()
{
@@ -78,9 +81,7 @@ bool Spawner::StartGame()
Spawner::LoadSidesStuff();
- bool result = Config->LoadSaveGame
- ? LoadSavedGame(Config->SaveGameName)
- : StartNewScenario(pScenarioName);
+ bool result = StartScenario(pScenarioName);
if (Main::GetConfig()->DumpTypes)
DumperTypes::Dump();
@@ -165,9 +166,9 @@ void Spawner::AssignHouses()
}
}
-bool Spawner::StartNewScenario(const char* pScenarioName)
+bool Spawner::StartScenario(const char* pScenarioName)
{
- if (pScenarioName[0] == 0)
+ if (pScenarioName[0] == 0 && !Config->LoadSaveGame)
{
Debug::Log("[Spawner] Failed Read Scenario [%s]\n", pScenarioName);
@@ -218,6 +219,8 @@ bool Spawner::StartNewScenario(const char* pScenarioName)
Game::TechLevel = Spawner::Config->TechLevel;
Game::PlayerColor = Spawner::Config->Players[0].Color;
GameOptionsClass::Instance.GameSpeed = Spawner::Config->GameSpeed;
+
+ Spawner::NextAutoSaveNumber = Spawner::Config->NextAutoSaveNumber;
}
{ // Added AI Players
@@ -294,20 +297,27 @@ bool Spawner::StartNewScenario(const char* pScenarioName)
if (SessionClass::IsCampaign())
{
pGameModeOptions->Crates = true;
- return ScenarioClass::StartScenario(pScenarioName, 1, 0);
+ return Config->LoadSaveGame ? Spawner::LoadSavedGame(Config->SaveGameName) : ScenarioClass::StartScenario(pScenarioName, 1, 0);
}
else if (SessionClass::IsSkirmish())
{
- return ScenarioClass::StartScenario(pScenarioName, 0, -1);
+ return Config->LoadSaveGame ? Spawner::LoadSavedGame(Config->SaveGameName) : ScenarioClass::StartScenario(pScenarioName, 0, -1);
}
else /* if (SessionClass::IsMultiplayer()) */
{
Spawner::InitNetwork();
- if (!ScenarioClass::StartScenario(pScenarioName, 0, -1))
+ bool result = Config->LoadSaveGame ? Spawner::LoadSavedGame(Config->SaveGameName) : ScenarioClass::StartScenario(pScenarioName, 0, -1);
+
+ if (!result)
return false;
pSession->GameMode = GameMode::LAN;
- pSession->CreateConnections();
+
+ if (Config->LoadSaveGame && !Spawner::Reconcile_Players())
+ return false;
+
+ if (!pSession->CreateConnections())
+ return false;
if (Main::GetConfig()->AllowChat == false)
{
@@ -402,6 +412,128 @@ void Spawner::InitNetwork()
Game::Network::Init();
}
+/**
+ * Reconciles loaded data with the "Players" vector.
+ *
+ * This function is for supporting loading a saved multiplayer game.
+ * When the game is loaded, we have to figure out which house goes with
+ * which entry in the Players vector. We also have to figure out if
+ * everyone who was originally in the game is still with us, and if not,
+ * turn their stuff over to the computer.
+ *
+ * Original author: Vinifera Project
+ * Migration: TaranDahl
+ */
+bool Spawner::Reconcile_Players()
+{
+ int i;
+ bool found;
+ int house;
+ HouseClass* pHouse;
+
+ // Just use this as Playernodes.
+ auto players = SessionClass::Instance.StartSpots;
+
+ /**
+ * If there are no players, there's nothing to do.
+ */
+ if (players.Count == 0)
+ return true;
+
+ /**
+ * Make sure every name we're connected to can be found in a House.
+ */
+ for (i = 0; i < players.Count; i++)
+ {
+ found = false;
+
+ for (house = 0; house < players.Count; house++)
+ {
+ pHouse = HouseClass::Array.Items[house];
+ if (!pHouse)
+ continue;
+
+ for (wchar_t c : players.Items[i]->Name)
+ Debug::LogAndMessage("%c", (char)c);
+
+ Debug::LogAndMessage("\n");
+
+ for (wchar_t c : pHouse->UIName)
+ Debug::LogAndMessage("%c", (char)c);
+
+ Debug::LogAndMessage("\n");
+
+ if (!wcscmp(players.Items[i]->Name, pHouse->UIName))
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ return false;
+ }
+
+ /**
+ * Loop through all Houses; if we find a human-owned house that we're
+ * not connected to, turn it over to the computer.
+ */
+ for (house = 0; house < players.Count; house++)
+ {
+ pHouse = HouseClass::Array.Items[house];
+
+ if (!pHouse)
+ continue;
+
+ /**
+ * Skip this house if it wasn't human to start with.
+ */
+ if (!pHouse->IsHumanPlayer)
+ continue;
+
+ /**
+ * Try to find this name in the Players vector; if it's found, set
+ * its ID to this house.
+ */
+ found = false;
+ for (i = 0; i < players.Count; i++)
+ {
+ if (!wcscmp(players.Items[i]->Name, pHouse->UIName))
+ {
+ found = true;
+ players.Items[i]->HouseIndex = house;
+ break;
+ }
+ }
+
+ /**
+ * If this name wasn't found, remove it
+ */
+ if (!found)
+ {
+ /**
+ * Turn the player's house over to the computer's AI
+ */
+ pHouse->IsHumanPlayer = false;
+ pHouse->Production = true;
+ pHouse->IQLevel = RulesClass::Instance->MaxIQLevels;
+
+ static wchar_t buffer[21];
+ std::swprintf(buffer, sizeof(buffer), L"%s (AI)", pHouse->UIName);
+ std::wcscpy(pHouse->UIName, buffer);
+ //strcpy(pHouse->IniName, Fetch_String(TXT_COMPUTER));
+
+ SessionClass::Instance.MPlayerCount--;
+ }
+ }
+
+ /**
+ * If all went well, our Session.NumPlayers value should now equal the value
+ * from the saved game, minus any players we removed.
+ */
+ return SessionClass::Instance.MPlayerCount == players.Count;
+}
+
void Spawner::LoadSidesStuff()
{
RulesClass* pRules = RulesClass::Instance;
@@ -413,3 +545,126 @@ void Spawner::LoadSidesStuff()
for (auto const& pItem : HouseTypeClass::Array)
pItem->LoadFromINI(pINI);
}
+
+void Spawner::RespondToSaveGame(EventExt* event)
+{
+ /**
+ * Mark that we'd like to save the game.
+ */
+ Spawner::DoSave = true;
+}
+
+/**
+ * Prints a message that there's an autosave happening.
+ *
+ * Original author: Vinifera Project
+ * Migration: TaranDahl
+ */
+void Print_Saving_Game_Message()
+{
+ /**
+ * Calculate the message delay.
+ */
+ const int message_delay = (int)(RulesClass::Instance->MessageDelay * 900);
+
+ /**
+ * Send the message.
+ */
+ MessageListClass::Instance.AddMessage(nullptr, 0, L"Saving game...", 4, TextPrintType::Point6Grad | TextPrintType::UseGradPal | TextPrintType::FullShadow, message_delay, false);
+
+ /**
+ * Force a redraw so that our message gets printed.
+ */
+ MapClass::Instance.MarkNeedsRedraw(2);
+ MapClass::Instance.Render();
+}
+
+/**
+ * We do it by ourselves here instead of letting original Westwood code save when
+ * the event is executed, because saving mid-frame before Remove_All_Inactive()
+ * has been called can lead to save corruption
+ * In other words, by doing it here we fix a Westwood bug/oversight
+ *
+ * Original author: Rampastring, ZivDero
+ * Migration: TaranDahl
+ */
+void Spawner::After_Main_Loop()
+{
+ auto pConfig = Spawner::GetConfig();
+
+ const bool doSaveCampaign = SessionClass::Instance.GameMode == GameMode::Campaign && pConfig->AutoSaveCount > 0 && pConfig->AutoSaveInterval > 0;
+ const bool doSaveMP = Spawner::Active && SessionClass::Instance.GameMode == GameMode::LAN && pConfig->AutoSaveInterval > 0;
+
+ /**
+ * Schedule to make a save if it's time to autosave.
+ */
+ if (doSaveCampaign || doSaveMP)
+ {
+ if (Unsorted::CurrentFrame == Spawner::NextAutoSaveFrame)
+ {
+ Spawner::DoSave = true;
+ }
+ }
+
+ if (Spawner::DoSave)
+ {
+
+ Print_Saving_Game_Message();
+
+ /**
+ * Campaign autosave.
+ */
+ if (SessionClass::Instance.GameMode == GameMode::Campaign)
+ {
+ static char saveFileName[32];
+ static wchar_t saveDescription[32];
+
+ /**
+ * Prepare the save name and description.
+ */
+ std::sprintf(saveFileName, "AUTOSAVE%d.SAV", Spawner::NextAutoSaveNumber + 1);
+ std::swprintf(saveDescription, L"Mission Auto-Save (Slot %d)", Spawner::NextAutoSaveNumber + 1);
+
+ /**
+ * Pause the mission timer.
+ */
+ ScenarioClass::PauseGame();
+ Game::CallBack();
+
+ /**
+ * Save!
+ */
+ ScenarioClass::Instance->SaveGame(saveFileName, saveDescription);
+
+ /**
+ * Unpause the mission timer.
+ */
+ ScenarioClass::ResumeGame();
+
+ /**
+ * Increment the autosave number.
+ */
+ Spawner::NextAutoSaveNumber = (Spawner::NextAutoSaveNumber + 1) % pConfig->AutoSaveCount;
+
+ /**
+ * Schedule the next autosave.
+ */
+ Spawner::NextAutoSaveFrame = Unsorted::CurrentFrame + pConfig->AutoSaveInterval;
+ }
+ else if (SessionClass::Instance.GameMode == GameMode::LAN)
+ {
+
+ /**
+ * Save!
+ */
+ ScenarioClass::Instance->SaveGame("SAVEGAME.NET", L"Multiplayer Game");
+
+ /**
+ * Schedule the next autosave.
+ */
+ Spawner::NextAutoSaveFrame = Unsorted::CurrentFrame + pConfig->AutoSaveInterval;
+ }
+
+ Spawner::DoSave = false;
+ }
+}
diff --git a/src/Spawner/Spawner.h b/src/Spawner/Spawner.h
index 325edde2..f960457d 100644
--- a/src/Spawner/Spawner.h
+++ b/src/Spawner/Spawner.h
@@ -19,6 +19,7 @@
#pragma once
#include "Spawner.Config.h"
+#include
#include
class Spawner
@@ -26,6 +27,9 @@ class Spawner
public:
static bool Enabled;
static bool Active;
+ static bool DoSave;
+ static int NextAutoSaveFrame;
+ static int NextAutoSaveNumber;
private:
static std::unique_ptr Config;
@@ -39,11 +43,14 @@ class Spawner
static void Init();
static bool StartGame();
static void AssignHouses();
+ static void After_Main_Loop();
+ static void RespondToSaveGame(EventExt* event);
private:
- static bool StartNewScenario(const char* scenarioName);
+ static bool StartScenario(const char* scenarioName);
static bool LoadSavedGame(const char* scenarioName);
static void InitNetwork();
+ static bool Reconcile_Players();
static void LoadSidesStuff();
};
diff --git a/src/UI/Dialogs.cpp b/src/UI/Dialogs.cpp
new file mode 100644
index 00000000..913a8aa3
--- /dev/null
+++ b/src/UI/Dialogs.cpp
@@ -0,0 +1,97 @@
+/*
+ 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)
+{
+ 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
new file mode 100644
index 00000000..4f02a04d
--- /dev/null
+++ b/src/UI/Dialogs.h
@@ -0,0 +1,23 @@
+#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.cpp b/src/UI/Hooks.cpp
new file mode 100644
index 00000000..de704592
--- /dev/null
+++ b/src/UI/Hooks.cpp
@@ -0,0 +1,13 @@
+
+#include "Dialogs.h"
+
+#include
+#include
+
+DEFINE_HOOK(0x609299, UI_IsStaticAndOrOwnerDraw_MultiplayerGameOptionsDialog, 0x5)
+{
+ 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
new file mode 100644
index 00000000..d19c8cbd
--- /dev/null
+++ b/src/UI/MultiplayerGameOptionsDialog.rc
@@ -0,0 +1,14 @@
+#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