diff --git a/src/Misc/SavedGamesInSubdir.cpp b/src/Misc/SavedGamesInSubdir.cpp index 3307cc96..1d8449c6 100644 --- a/src/Misc/SavedGamesInSubdir.cpp +++ b/src/Misc/SavedGamesInSubdir.cpp @@ -22,7 +22,11 @@ #include #include +#include +#include + #include +#include namespace SavedGames { @@ -162,3 +166,180 @@ DEFINE_HOOK(0x67FD26, LoadOptionsClass_ReadSaveInfo_SGInSubdir, 0x5) return 0; } + + +//issue #18 : Save game filter for 3rd party campaigns +namespace SavedGames +{ + struct CustomMissionID + { + static constexpr const wchar_t* SaveName = L"CustomMissionID"; + + int Number; + + CustomMissionID() : Number { Spawner::GetConfig()->CustomMissionID } { } + + CustomMissionID(int num) : Number { num } { } + + operator int() const { return Number; } + }; + + + template + bool AppendToStorage(IStorage* pStorage) + { + IStreamPtr pStream = nullptr; + bool ret = false; + HRESULT hr = pStorage->CreateStream( + T::SaveName, + STGM_WRITE | STGM_CREATE | STGM_SHARE_EXCLUSIVE, + 0, + 0, + &pStream + ); + + if (SUCCEEDED(hr) && pStream != nullptr) + { + T info {}; + ULONG written = 0; + hr = pStream->Write(&info, sizeof(info), &written); + ret = SUCCEEDED(hr) && written == sizeof(info); + } + + return ret; + } + + + template + std::optional ReadFromStorage(IStorage* pStorage) + { + IStreamPtr pStream = nullptr; + bool hasValue = false; + HRESULT hr = pStorage->OpenStream( + T::SaveName, + NULL, + STGM_READ | STGM_SHARE_EXCLUSIVE, + 0, + &pStream + ); + + T info; + + if (SUCCEEDED(hr) && pStream != nullptr) + { + ULONG read = 0; + hr = pStream->Read(&info, sizeof(info), &read); + hasValue = SUCCEEDED(hr) && read == sizeof(info); + } + + return hasValue ? std::make_optional(info) : std::nullopt; + } + +} + +DEFINE_HOOK(0x559921, LoadOptionsClass_FillList_FilterFiles, 0x6) +{ + GET(FileEntryClass*, pEntry, EBP); + enum { NullThisEntry = 0x559959 }; + /* + // there was a qsort later and filters out these but we could have just removed them right here + if (pEntry->IsWrongVersion || !pEntry->IsValid) + { + GameDelete(pEntry); + return NullThisEntry; + }; + */ + OLECHAR wNameBuffer[0x100] {}; + SavedGames::FormatPath(Main::readBuffer, pEntry->Filename.data()); + MultiByteToWideChar(CP_UTF8, 0, Main::readBuffer, -1, wNameBuffer, std::size(wNameBuffer)); + IStoragePtr pStorage = nullptr; + bool shouldDelete = false; + if (SUCCEEDED(StgOpenStorage(wNameBuffer, NULL, + STGM_READWRITE | STGM_SHARE_EXCLUSIVE, + 0, 0, &pStorage) + )) + { + auto id = SavedGames::ReadFromStorage(pStorage); + + if (Spawner::GetConfig()->CustomMissionID != id.value_or(0)) + shouldDelete = true; + } + + if (shouldDelete) + { + GameDelete(pEntry); + return NullThisEntry; + } + + return 0; +} + +// Write : A la fin +DEFINE_HOOK(0x67D2E3, SaveGame_AdditionalInfoForClient, 0x6) +{ + GET_STACK(IStorage*, pStorage, STACK_OFFSET(0x4A0, -0x490)); + using namespace SavedGames; + + if (pStorage) + { + if (SessionClass::IsCampaign() && Spawner::GetConfig()->CustomMissionID) + AppendToStorage(pStorage); + } + + return 0; +} + +// Read : Au debut +DEFINE_HOOK(0x67E4DC, LoadGame_AdditionalInfoForClient, 0x7) +{ + LEA_STACK(const wchar_t*, filename, STACK_OFFSET(0x518, -0x4F4)); + IStoragePtr pStorage = nullptr; + using namespace SavedGames; + + if (SUCCEEDED(StgOpenStorage(filename, NULL, + STGM_READWRITE | STGM_SHARE_EXCLUSIVE, + 0, 0, &pStorage) + )) + { + if (auto id = ReadFromStorage(pStorage)) + { + int num = id->Number; + Debug::Log("[Spawner] sav file CustomMissionID = %d\n", num); + Spawner::GetConfig()->CustomMissionID = num; + ScenarioClass::Instance->EndOfGame = true; + } + else + { + Spawner::GetConfig()->CustomMissionID = 0; + } + } + + return 0; +} + +// Custom missions especially can contain paths in scenario filenames which cause +// the initial save game to fail, remove the paths before filename and make the +// filename uppercase to match with usual savegame names. +DEFINE_HOOK(0x55DC85, MainLoop_SaveGame_SanitizeFilename, 0x7) +{ + LEA_STACK(char*, pFilename, STACK_OFFSET(0x1C4, -0x178)); + LEA_STACK(const wchar_t*, pDescription, STACK_OFFSET(0x1C4, -0x70)); + + char* slash1 = strrchr(pFilename, '/'); + char* slash2 = strrchr(pFilename, '\\'); + char* lastSlash = (slash1 > slash2) ? slash1 : slash2; + + if (lastSlash != NULL) + { + pFilename = lastSlash + 1; + *lastSlash = '\0'; + } + + for (char* p = pFilename; *p; ++p) + *p = (char)toupper((unsigned char)*p); + + R->ECX(pFilename); + R->EDX(pDescription); + + return 0x55DC90; +} diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index 430d242f..0e2e9d66 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -57,19 +57,21 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI) 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); } { // Scenario Options - Seed = pINI->ReadInteger(pSettingsSection, "Seed", Seed); - TechLevel = pINI->ReadInteger(pSettingsSection, "TechLevel", TechLevel); - IsCampaign = pINI->ReadBool(pSettingsSection, "IsSinglePlayer", IsCampaign); - Tournament = pINI->ReadInteger(pSettingsSection, "Tournament", Tournament); - WOLGameID = pINI->ReadInteger(pSettingsSection, "GameID", WOLGameID); - /* ScenarioName */ pINI->ReadString(pSettingsSection, "Scenario", ScenarioName, ScenarioName, sizeof(ScenarioName)); - /* MapHash */ pINI->ReadString(pSettingsSection, "MapHash", MapHash, MapHash, sizeof(MapHash)); + Seed = pINI->ReadInteger(pSettingsSection, "Seed", Seed); + TechLevel = pINI->ReadInteger(pSettingsSection, "TechLevel", TechLevel); + IsCampaign = pINI->ReadBool(pSettingsSection, "IsSinglePlayer", IsCampaign); + Tournament = pINI->ReadInteger(pSettingsSection, "Tournament", Tournament); + WOLGameID = pINI->ReadInteger(pSettingsSection, "GameID", WOLGameID); + /* ScenarioName */ pINI->ReadString(pSettingsSection, "Scenario", ScenarioName, ScenarioName, sizeof(ScenarioName)); + /* MapHash */ pINI->ReadString(pSettingsSection, "MapHash", MapHash, MapHash, sizeof(MapHash)); + ReadMissionSection = pINI->ReadBool(pSettingsSection, "ReadMissionSection", ReadMissionSection); if (INIClassExt::ReadString_WithoutAresHook(pINI, pSettingsSection, "UIMapName", "", Main::readBuffer, sizeof(Main::readBuffer)) > 0) MultiByteToWideChar(CP_UTF8, 0, Main::readBuffer, strlen(Main::readBuffer), UIMapName, std::size(UIMapName)); diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h index a390baaf..44c7248b 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -99,6 +99,7 @@ class SpawnerConfig bool LoadSaveGame; char SavedGameDir[MAX_PATH]; // Nested paths are also supported, e.g. "Saved Games\\Yuri's Revenge" char SaveGameName[60]; + int CustomMissionID; int AutoSaveCount; int AutoSaveInterval; int NextAutoSaveNumber; @@ -112,6 +113,7 @@ class SpawnerConfig char ScenarioName[260]; char MapHash[0xff]; wchar_t UIMapName[45]; + bool ReadMissionSection; // Network Options int Protocol; @@ -172,6 +174,7 @@ class SpawnerConfig , LoadSaveGame { false } , SavedGameDir { "Saved Games" } , SaveGameName { "" } + , CustomMissionID { 0 } , AutoSaveCount { 5 } , AutoSaveInterval { 7200 } , NextAutoSaveNumber { 0 } @@ -185,6 +188,7 @@ class SpawnerConfig , ScenarioName { "spawnmap.ini" } , MapHash { "" } , UIMapName { L"" } + , ReadMissionSection { false } // Network Options , Protocol { 2 } diff --git a/src/Spawner/Spawner.Hook.cpp b/src/Spawner/Spawner.Hook.cpp index 79113992..0659dcad 100644 --- a/src/Spawner/Spawner.Hook.cpp +++ b/src/Spawner/Spawner.Hook.cpp @@ -26,6 +26,7 @@ #include #include #include +#include DEFINE_HOOK(0x6BD7C5, WinMain_SpawnerInit, 0x6) { @@ -264,3 +265,29 @@ DEFINE_HOOK(0x686A9E, ReadScenario_InitSomeThings_SpecialHouseIsAlly, 0x6) return 0x686AC6; } + +DEFINE_HOOK(0x686D46, ReadScenarioINI_MissionININame, 0x5) +{ + LEA_STACK(CCFileClass*, pFile, STACK_OFFSET(0x174, -0xF0)); + + if (Spawner::GetConfig()->ReadMissionSection) + { + pFile->SetFileName("SPAWN.INI"); + return 0x686D57; + } + + return 0; +} + +DEFINE_HOOK(0x65F57F, BriefingDialog_MissionININame, 0x6) +{ + LEA_STACK(CCFileClass*, pFile, STACK_OFFSET(0x1D4, -0x16C)); + + if (Spawner::GetConfig()->ReadMissionSection) + { + pFile->SetFileName("SPAWN.INI"); + return 0x65F58F; + } + + return 0; +} diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index afe4ada4..3577f5d4 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -24,6 +24,7 @@ #include "ProtocolZero.LatencyLevel.h" #include #include +#include #include #include @@ -303,9 +304,22 @@ bool Spawner::StartScenario(const char* pScenarioName) if (SessionClass::IsCampaign()) { pGameModeOptions->Crates = true; - return Config->LoadSaveGame - ? Spawner::LoadSavedGame(Config->SaveGameName) - : ScenarioClass::StartScenario(pScenarioName, 1, 0); + + if (Config->LoadSaveGame) + return Spawner::LoadSavedGame(Config->SaveGameName); + + // Rename MISSIONMD.INI to this + // because Ares has LoadScreenText.Color and Phobos has Starkku's PR #1145 + // 2025-05-28: Moved to a hook in Spawner.Hook.cpp - Starkku + // if (Spawner::Config->ReadMissionSection) // before parsing + // Patch::Apply_RAW(0x839724, "Spawn.ini"); + + bool result = ScenarioClass::StartScenario(pScenarioName, 1, 0); + + if (Spawner::Config->CustomMissionID != 0) // after parsing + ScenarioClass::Instance->EndOfGame = true; + + return result; } else if (SessionClass::IsSkirmish()) {