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