From ba33dd323a3d320e41a206198bcde3c3a654578e Mon Sep 17 00:00:00 2001 From: Trsdy <914137150@qq.com> Date: Wed, 26 Jun 2024 19:16:45 +0800 Subject: [PATCH 1/5] merge --- src/Misc/SavedGamesInSubdir.cpp | 187 ++++++++++++++++++++++++++++++++ src/Spawner/Spawner.Config.cpp | 1 + src/Spawner/Spawner.Config.h | 2 + src/Spawner/Spawner.cpp | 14 ++- 4 files changed, 203 insertions(+), 1 deletion(-) diff --git a/src/Misc/SavedGamesInSubdir.cpp b/src/Misc/SavedGamesInSubdir.cpp index 1d179e54..1da559d9 100644 --- a/src/Misc/SavedGamesInSubdir.cpp +++ b/src/Misc/SavedGamesInSubdir.cpp @@ -22,7 +22,11 @@ #include #include +#include +#include + #include +#include namespace SavedGames { @@ -161,3 +165,186 @@ DEFINE_HOOK(0x67FD26, LoadOptionsClass_ReadSaveInfo_SGInSubdir, 0x5) return 0; } + + +//issue #18 : Save game filter for 3rd party campaigns +namespace SavedGames +{ + struct CustomMissionID + { + static constexpr wchar_t* SaveName = L"CustomMissionID"; + + int Number; + + CustomMissionID() : Number { Spawner::GetConfig()->CustomMissionID } { } + + CustomMissionID(int num) : Number { num } { } + + operator int() const { return Number; } + }; + + // More fun + + struct ExtraTestInfo + { + static constexpr wchar_t* SaveName = L"Spawner test info"; + + int CurrentFrame; + int TechnoCount; + + explicit ExtraTestInfo() + :CurrentFrame { Unsorted::CurrentFrame } + , TechnoCount { TechnoClass::Array->Count } + { + } + }; + + template + bool AppendToStorage(IStorage* pStorage) + { + IStream* 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); + pStream->Release(); + } + + return ret; + } + + + template + std::optional ReadFromStorage(IStorage* pStorage) + { + IStream* 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); + + pStream->Release(); + } + + 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)); + IStorage* 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 (pStorage) + pStorage->Release(); + + 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); + if (AppendToStorage(pStorage)) + Debug::Log("[Spawner] Extra meta info appended on sav file\n"); + } + + return 0; +} + +// Read : Au debut +DEFINE_HOOK(0x67E4DC, LoadGame_AdditionalInfoForClient, 0x7) +{ + LEA_STACK(const wchar_t*, filename, STACK_OFFSET(0x518, -0x4F4)); + IStorage* 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; + } + + if (auto info = ReadFromStorage(pStorage)) + { + Debug::Log("[Spawner] CurrentFrame = %d, TechnoCount = %d\n" + , info->CurrentFrame + , info->TechnoCount + ); + } + } + if (pStorage) + pStorage->Release(); + + return 0; +} diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index eca1549a..62c098a7 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -55,6 +55,7 @@ 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); { // Scenario Options Seed = pINI->ReadInteger(pSettingsSection, "Seed", Seed); diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h index 73f5cceb..fafbb77d 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -94,6 +94,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; // Scenario Options int Seed; @@ -161,6 +162,7 @@ class SpawnerConfig , LoadSaveGame { false } , SavedGameDir { "Saved Games" } , SaveGameName { "" } + , CustomMissionID { 0 } // Scenario Options , Seed { 0 } diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index 0163a93b..79bc0d7f 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -24,6 +24,7 @@ #include "ProtocolZero.LatencyLevel.h" #include #include +#include #include #include @@ -294,7 +295,18 @@ bool Spawner::StartNewScenario(const char* pScenarioName) if (SessionClass::IsCampaign()) { pGameModeOptions->Crates = true; - return ScenarioClass::StartScenario(pScenarioName, 1, 0); + + // Rename MISSIONMD.INI to this + // because Ares has LoadScreenText.Color and Phobos has Starkku's PR #1145 + if (Spawner::Config->CustomMissionID) // before parsing + Patch::Apply_RAW(0x839724, "Spawn.ini"); + + bool result = ScenarioClass::StartScenario(pScenarioName, 1, 0); + + if (Spawner::Config->CustomMissionID) // after parsing + ScenarioClass::Instance->EndOfGame = true; + + return result; } else if (SessionClass::IsSkirmish()) { From 0fdde39b223a1d493269bbc61e8167f0ba9a4161 Mon Sep 17 00:00:00 2001 From: Starkku Date: Wed, 28 May 2025 17:10:07 +0300 Subject: [PATCH 2/5] Fixup --- src/Misc/SavedGamesInSubdir.cpp | 6 +++--- src/Spawner/Spawner.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Misc/SavedGamesInSubdir.cpp b/src/Misc/SavedGamesInSubdir.cpp index 1da559d9..2feedb39 100644 --- a/src/Misc/SavedGamesInSubdir.cpp +++ b/src/Misc/SavedGamesInSubdir.cpp @@ -172,7 +172,7 @@ namespace SavedGames { struct CustomMissionID { - static constexpr wchar_t* SaveName = L"CustomMissionID"; + static constexpr const wchar_t* SaveName = L"CustomMissionID"; int Number; @@ -187,14 +187,14 @@ namespace SavedGames struct ExtraTestInfo { - static constexpr wchar_t* SaveName = L"Spawner test info"; + static constexpr const wchar_t* SaveName = L"Spawner test info"; int CurrentFrame; int TechnoCount; explicit ExtraTestInfo() :CurrentFrame { Unsorted::CurrentFrame } - , TechnoCount { TechnoClass::Array->Count } + , TechnoCount { TechnoClass::Array.Count } { } }; diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index bdbb21a3..d8e1183b 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -312,12 +312,12 @@ bool Spawner::StartScenario(const char* pScenarioName) // Rename MISSIONMD.INI to this // because Ares has LoadScreenText.Color and Phobos has Starkku's PR #1145 - if (Spawner::Config->CustomMissionID) // before parsing + if (Spawner::Config->CustomMissionID != 0) // before parsing Patch::Apply_RAW(0x839724, "Spawn.ini"); bool result = ScenarioClass::StartScenario(pScenarioName, 1, 0); - if (Spawner::Config->CustomMissionID) // after parsing + if (Spawner::Config->CustomMissionID != 0) // after parsing ScenarioClass::Instance->EndOfGame = true; return result; From d227059a7cc7da2b09be68753fc12a2887e6e2c1 Mon Sep 17 00:00:00 2001 From: Starkku Date: Wed, 28 May 2025 20:19:17 +0300 Subject: [PATCH 3/5] Fixes to spawn.ini mission data parsing - Split mission data parsing from custom mission ID - Use hooks and fix briefing not being parsed correctly from spawn.ini --- src/Spawner/Spawner.Config.cpp | 15 ++++++++------- src/Spawner/Spawner.Config.h | 2 ++ src/Spawner/Spawner.Hook.cpp | 27 +++++++++++++++++++++++++++ src/Spawner/Spawner.cpp | 5 +++-- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index 7491522b..cc130af2 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -63,13 +63,14 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI) } { // 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 db9906f0..42b9b6de 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -112,6 +112,7 @@ class SpawnerConfig char ScenarioName[260]; char MapHash[0xff]; wchar_t UIMapName[45]; + bool ReadMissionSection; // Network Options int Protocol; @@ -183,6 +184,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 83d3f90b..ab893f0b 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) { @@ -235,3 +236,29 @@ DEFINE_HOOK(0x67E6DA, LoadGame_AfterInit, 0x6) } #pragma endregion + +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 d8e1183b..f32c0be3 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -312,8 +312,9 @@ bool Spawner::StartScenario(const char* pScenarioName) // Rename MISSIONMD.INI to this // because Ares has LoadScreenText.Color and Phobos has Starkku's PR #1145 - if (Spawner::Config->CustomMissionID != 0) // before parsing - Patch::Apply_RAW(0x839724, "Spawn.ini"); + // 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); From 50a53dece3ec03b658c5bbf8b2148359748377cf Mon Sep 17 00:00:00 2001 From: Trsdy <914137150@qq.com> Date: Fri, 30 May 2025 20:21:51 +0800 Subject: [PATCH 4/5] merge #20 before talking s'il vous plait --- src/Misc/SavedGamesInSubdir.cpp | 41 ++++----------------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/src/Misc/SavedGamesInSubdir.cpp b/src/Misc/SavedGamesInSubdir.cpp index 2feedb39..1730b32a 100644 --- a/src/Misc/SavedGamesInSubdir.cpp +++ b/src/Misc/SavedGamesInSubdir.cpp @@ -183,26 +183,11 @@ namespace SavedGames operator int() const { return Number; } }; - // More fun - - struct ExtraTestInfo - { - static constexpr const wchar_t* SaveName = L"Spawner test info"; - - int CurrentFrame; - int TechnoCount; - - explicit ExtraTestInfo() - :CurrentFrame { Unsorted::CurrentFrame } - , TechnoCount { TechnoClass::Array.Count } - { - } - }; template bool AppendToStorage(IStorage* pStorage) { - IStream* pStream = nullptr; + IStreamPtr pStream = nullptr; bool ret = false; HRESULT hr = pStorage->CreateStream( T::SaveName, @@ -218,7 +203,6 @@ namespace SavedGames ULONG written = 0; hr = pStream->Write(&info, sizeof(info), &written); ret = SUCCEEDED(hr) && written == sizeof(info); - pStream->Release(); } return ret; @@ -228,7 +212,7 @@ namespace SavedGames template std::optional ReadFromStorage(IStorage* pStorage) { - IStream* pStream = nullptr; + IStreamPtr pStream = nullptr; bool hasValue = false; HRESULT hr = pStorage->OpenStream( T::SaveName, @@ -245,8 +229,6 @@ namespace SavedGames ULONG read = 0; hr = pStream->Read(&info, sizeof(info), &read); hasValue = SUCCEEDED(hr) && read == sizeof(info); - - pStream->Release(); } return hasValue ? std::make_optional(info) : std::nullopt; @@ -269,7 +251,7 @@ DEFINE_HOOK(0x559921, LoadOptionsClass_FillList_FilterFiles, 0x6) OLECHAR wNameBuffer[0x100] {}; SavedGames::FormatPath(Main::readBuffer, pEntry->Filename.data()); MultiByteToWideChar(CP_UTF8, 0, Main::readBuffer, -1, wNameBuffer, std::size(wNameBuffer)); - IStorage* pStorage = nullptr; + IStoragePtr pStorage = nullptr; bool shouldDelete = false; if (SUCCEEDED(StgOpenStorage(wNameBuffer, NULL, STGM_READWRITE | STGM_SHARE_EXCLUSIVE, @@ -282,9 +264,6 @@ DEFINE_HOOK(0x559921, LoadOptionsClass_FillList_FilterFiles, 0x6) shouldDelete = true; } - if (pStorage) - pStorage->Release(); - if (shouldDelete) { GameDelete(pEntry); @@ -304,8 +283,6 @@ DEFINE_HOOK(0x67D2E3, SaveGame_AdditionalInfoForClient, 0x6) { if (SessionClass::IsCampaign() && Spawner::GetConfig()->CustomMissionID) AppendToStorage(pStorage); - if (AppendToStorage(pStorage)) - Debug::Log("[Spawner] Extra meta info appended on sav file\n"); } return 0; @@ -315,7 +292,7 @@ DEFINE_HOOK(0x67D2E3, SaveGame_AdditionalInfoForClient, 0x6) DEFINE_HOOK(0x67E4DC, LoadGame_AdditionalInfoForClient, 0x7) { LEA_STACK(const wchar_t*, filename, STACK_OFFSET(0x518, -0x4F4)); - IStorage* pStorage = nullptr; + IStoragePtr pStorage = nullptr; using namespace SavedGames; if (SUCCEEDED(StgOpenStorage(filename, NULL, @@ -334,17 +311,7 @@ DEFINE_HOOK(0x67E4DC, LoadGame_AdditionalInfoForClient, 0x7) { Spawner::GetConfig()->CustomMissionID = 0; } - - if (auto info = ReadFromStorage(pStorage)) - { - Debug::Log("[Spawner] CurrentFrame = %d, TechnoCount = %d\n" - , info->CurrentFrame - , info->TechnoCount - ); - } } - if (pStorage) - pStorage->Release(); return 0; } From 314684c9d1e855c33fa67a120b445f235a66be61 Mon Sep 17 00:00:00 2001 From: Starkku Date: Mon, 10 Nov 2025 16:56:49 +0200 Subject: [PATCH 5/5] Fix issue with save game filenames containing paths --- src/Misc/SavedGamesInSubdir.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Misc/SavedGamesInSubdir.cpp b/src/Misc/SavedGamesInSubdir.cpp index 1730b32a..0a9f3f35 100644 --- a/src/Misc/SavedGamesInSubdir.cpp +++ b/src/Misc/SavedGamesInSubdir.cpp @@ -315,3 +315,30 @@ DEFINE_HOOK(0x67E4DC, LoadGame_AdditionalInfoForClient, 0x7) 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; +}