Skip to content

Commit 5575434

Browse files
authored
Merge pull request #19 from chaserli/feature/savegameinfo
Add "CustomMissionID" for saved games
2 parents 6065e12 + 4b3f484 commit 5575434

File tree

5 files changed

+230
-3
lines changed

5 files changed

+230
-3
lines changed

src/Misc/SavedGamesInSubdir.cpp

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
#include <Utilities/Macro.h>
2323
#include <Spawner/Spawner.h>
2424

25+
#include <HouseClass.h>
26+
#include <LoadOptionsClass.h>
27+
2528
#include <filesystem>
29+
#include <optional>
2630

2731
namespace SavedGames
2832
{
@@ -162,3 +166,180 @@ DEFINE_HOOK(0x67FD26, LoadOptionsClass_ReadSaveInfo_SGInSubdir, 0x5)
162166

163167
return 0;
164168
}
169+
170+
171+
//issue #18 : Save game filter for 3rd party campaigns
172+
namespace SavedGames
173+
{
174+
struct CustomMissionID
175+
{
176+
static constexpr const wchar_t* SaveName = L"CustomMissionID";
177+
178+
int Number;
179+
180+
CustomMissionID() : Number { Spawner::GetConfig()->CustomMissionID } { }
181+
182+
CustomMissionID(int num) : Number { num } { }
183+
184+
operator int() const { return Number; }
185+
};
186+
187+
188+
template<typename T> requires std::is_trivially_copyable_v<T>
189+
bool WriteToStorage(IStorage* pStorage)
190+
{
191+
IStreamPtr pStream = nullptr;
192+
bool ret = false;
193+
HRESULT hr = pStorage->CreateStream(
194+
T::SaveName,
195+
STGM_WRITE | STGM_CREATE | STGM_SHARE_EXCLUSIVE,
196+
0,
197+
0,
198+
&pStream
199+
);
200+
201+
if (SUCCEEDED(hr) && pStream != nullptr)
202+
{
203+
T info {};
204+
ULONG written = 0;
205+
hr = pStream->Write(&info, sizeof(info), &written);
206+
ret = SUCCEEDED(hr) && written == sizeof(info);
207+
}
208+
209+
return ret;
210+
}
211+
212+
213+
template<typename T> requires std::is_trivially_copyable_v<T>
214+
std::optional<T> ReadFromStorage(IStorage* pStorage)
215+
{
216+
IStreamPtr pStream = nullptr;
217+
bool hasValue = false;
218+
HRESULT hr = pStorage->OpenStream(
219+
T::SaveName,
220+
NULL,
221+
STGM_READ | STGM_SHARE_EXCLUSIVE,
222+
0,
223+
&pStream
224+
);
225+
226+
T info {};
227+
228+
if (SUCCEEDED(hr) && pStream != nullptr)
229+
{
230+
ULONG read = 0;
231+
hr = pStream->Read(&info, sizeof(info), &read);
232+
hasValue = SUCCEEDED(hr) && read == sizeof(info);
233+
}
234+
235+
return hasValue ? std::make_optional(info) : std::nullopt;
236+
}
237+
238+
}
239+
240+
DEFINE_HOOK(0x559921, LoadOptionsClass_FillList_FilterFiles, 0x6)
241+
{
242+
GET(FileEntryClass*, pEntry, EBP);
243+
enum { NullThisEntry = 0x559959 };
244+
/*
245+
// there was a qsort later and filters out these but we could have just removed them right here
246+
if (pEntry->IsWrongVersion || !pEntry->IsValid)
247+
{
248+
GameDelete(pEntry);
249+
return NullThisEntry;
250+
};
251+
*/
252+
OLECHAR wNameBuffer[0x100] {};
253+
SavedGames::FormatPath(Main::readBuffer, pEntry->Filename.data());
254+
MultiByteToWideChar(CP_UTF8, 0, Main::readBuffer, -1, wNameBuffer, std::size(wNameBuffer));
255+
IStoragePtr pStorage = nullptr;
256+
bool shouldDelete = false;
257+
if (SUCCEEDED(StgOpenStorage(wNameBuffer, NULL,
258+
STGM_READWRITE | STGM_SHARE_EXCLUSIVE,
259+
0, 0, &pStorage)
260+
))
261+
{
262+
auto id = SavedGames::ReadFromStorage<SavedGames::CustomMissionID>(pStorage);
263+
264+
if (Spawner::GetConfig()->CustomMissionID != id.value_or(0))
265+
shouldDelete = true;
266+
}
267+
268+
if (shouldDelete)
269+
{
270+
GameDelete(pEntry);
271+
return NullThisEntry;
272+
}
273+
274+
return 0;
275+
}
276+
277+
// Write : A la fin
278+
DEFINE_HOOK(0x67D2E3, SaveGame_AdditionalInfoForClient, 0x6)
279+
{
280+
GET_STACK(IStorage*, pStorage, STACK_OFFSET(0x4A0, -0x490));
281+
using namespace SavedGames;
282+
283+
if (pStorage && SessionClass::IsCampaign())
284+
{
285+
if (Spawner::GetConfig()->CustomMissionID)
286+
WriteToStorage<CustomMissionID>(pStorage);
287+
}
288+
289+
return 0;
290+
}
291+
292+
// Read : Au debut
293+
DEFINE_HOOK(0x67E4DC, LoadGame_AdditionalInfoForClient, 0x7)
294+
{
295+
LEA_STACK(const wchar_t*, filename, STACK_OFFSET(0x518, -0x4F4));
296+
IStoragePtr pStorage = nullptr;
297+
using namespace SavedGames;
298+
299+
if (SUCCEEDED(StgOpenStorage(filename, NULL,
300+
STGM_READWRITE | STGM_SHARE_EXCLUSIVE,
301+
0, 0, &pStorage)
302+
))
303+
{
304+
if (auto id = ReadFromStorage<CustomMissionID>(pStorage))
305+
{
306+
int num = id->Number;
307+
Debug::Log("sav file CustomMissionID = %d\n", num);
308+
Spawner::GetConfig()->CustomMissionID = num;
309+
ScenarioClass::Instance->EndOfGame = true;
310+
}
311+
else
312+
{
313+
Spawner::GetConfig()->CustomMissionID = 0;
314+
}
315+
}
316+
317+
return 0;
318+
}
319+
320+
// Custom missions especially can contain paths in scenario filenames which cause
321+
// the initial save game to fail, remove the paths before filename and make the
322+
// filename uppercase to match with usual savegame names.
323+
DEFINE_HOOK(0x55DC85, MainLoop_SaveGame_SanitizeFilename, 0x7)
324+
{
325+
LEA_STACK(char*, pFilename, STACK_OFFSET(0x1C4, -0x178));
326+
LEA_STACK(const wchar_t*, pDescription, STACK_OFFSET(0x1C4, -0x70));
327+
328+
char* slash1 = strrchr(pFilename, '/');
329+
char* slash2 = strrchr(pFilename, '\\');
330+
char* lastSlash = (slash1 > slash2) ? slash1 : slash2;
331+
332+
if (lastSlash != NULL)
333+
{
334+
pFilename = lastSlash + 1;
335+
*lastSlash = '\0';
336+
}
337+
338+
for (char* p = pFilename; *p; ++p)
339+
*p = (char)toupper((unsigned char)*p);
340+
341+
R->ECX(pFilename);
342+
R->EDX(pDescription);
343+
344+
return 0x55DC90;
345+
}

src/Spawner/Spawner.Config.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI)
5757
LoadSaveGame = pINI->ReadBool(pSettingsSection, "LoadSaveGame", LoadSaveGame);
5858
/* SavedGameDir */ pINI->ReadString(pSettingsSection, "SavedGameDir", SavedGameDir, SavedGameDir, sizeof(SavedGameDir));
5959
/* SaveGameName */ pINI->ReadString(pSettingsSection, "SaveGameName", SaveGameName, SaveGameName, sizeof(SaveGameName));
60+
CustomMissionID = pINI->ReadInteger(pSettingsSection, "CustomMissionID", 0);
6061
AutoSaveCount = pINI->ReadInteger(pSettingsSection, "AutoSaveCount", AutoSaveCount);
6162
AutoSaveInterval = pINI->ReadInteger(pSettingsSection, "AutoSaveInterval", AutoSaveInterval);
6263
NextAutoSaveNumber = pINI->ReadInteger(pSettingsSection, "NextAutoSaveNumber", NextAutoSaveNumber);
@@ -70,6 +71,7 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI)
7071
WOLGameID = pINI->ReadInteger(pSettingsSection, "GameID", WOLGameID);
7172
/* ScenarioName */ pINI->ReadString(pSettingsSection, "Scenario", ScenarioName, ScenarioName, sizeof(ScenarioName));
7273
/* MapHash */ pINI->ReadString(pSettingsSection, "MapHash", MapHash, MapHash, sizeof(MapHash));
74+
ReadMissionSection = pINI->ReadBool(pSettingsSection, "ReadMissionSection", ReadMissionSection);
7375

7476
if (INIClassExt::ReadString_WithoutAresHook(pINI, pSettingsSection, "UIMapName", "", Main::readBuffer, sizeof(Main::readBuffer)) > 0)
7577
MultiByteToWideChar(CP_UTF8, 0, Main::readBuffer, strlen(Main::readBuffer), UIMapName, std::size(UIMapName));

src/Spawner/Spawner.Config.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class SpawnerConfig
9999
bool LoadSaveGame;
100100
char SavedGameDir[MAX_PATH]; // Nested paths are also supported, e.g. "Saved Games\\Yuri's Revenge"
101101
char SaveGameName[60];
102+
int CustomMissionID;
102103
int AutoSaveCount;
103104
int AutoSaveInterval;
104105
int NextAutoSaveNumber;
@@ -112,6 +113,7 @@ class SpawnerConfig
112113
char ScenarioName[260];
113114
char MapHash[0xff];
114115
wchar_t UIMapName[45];
116+
bool ReadMissionSection;
115117

116118
// Network Options
117119
int Protocol;
@@ -172,6 +174,7 @@ class SpawnerConfig
172174
, LoadSaveGame { false }
173175
, SavedGameDir { "Saved Games" }
174176
, SaveGameName { "" }
177+
, CustomMissionID { 0 }
175178
, AutoSaveCount { 5 }
176179
, AutoSaveInterval { 7200 }
177180
, NextAutoSaveNumber { 0 }
@@ -185,6 +188,7 @@ class SpawnerConfig
185188
, ScenarioName { "spawnmap.ini" }
186189
, MapHash { "" }
187190
, UIMapName { L"" }
191+
, ReadMissionSection { false }
188192

189193
// Network Options
190194
, Protocol { 2 }

src/Spawner/Spawner.Hook.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <Utilities/Debug.h>
2727
#include <Utilities/Macro.h>
2828
#include <Unsorted.h>
29+
#include <CCINIClass.h>
2930

3031
DEFINE_HOOK(0x6BD7C5, WinMain_SpawnerInit, 0x6)
3132
{
@@ -264,3 +265,29 @@ DEFINE_HOOK(0x686A9E, ReadScenario_InitSomeThings_SpecialHouseIsAlly, 0x6)
264265

265266
return 0x686AC6;
266267
}
268+
269+
DEFINE_HOOK(0x686D46, ReadScenarioINI_MissionININame, 0x5)
270+
{
271+
LEA_STACK(CCFileClass*, pFile, STACK_OFFSET(0x174, -0xF0));
272+
273+
if (Spawner::GetConfig()->ReadMissionSection)
274+
{
275+
pFile->SetFileName("SPAWN.INI");
276+
return 0x686D57;
277+
}
278+
279+
return 0;
280+
}
281+
282+
DEFINE_HOOK(0x65F57F, BriefingDialog_MissionININame, 0x6)
283+
{
284+
LEA_STACK(CCFileClass*, pFile, STACK_OFFSET(0x1D4, -0x16C));
285+
286+
if (Spawner::GetConfig()->ReadMissionSection)
287+
{
288+
pFile->SetFileName("SPAWN.INI");
289+
return 0x65F58F;
290+
}
291+
292+
return 0;
293+
}

src/Spawner/Spawner.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,22 @@ bool Spawner::StartScenario(const char* pScenarioName)
303303
if (SessionClass::IsCampaign())
304304
{
305305
pGameModeOptions->Crates = true;
306-
return Config->LoadSaveGame
307-
? Spawner::LoadSavedGame(Config->SaveGameName)
308-
: ScenarioClass::StartScenario(pScenarioName, 1, 0);
306+
307+
if (Config->LoadSaveGame)
308+
return Spawner::LoadSavedGame(Config->SaveGameName);
309+
310+
// Rename MISSIONMD.INI to this
311+
// because Ares has LoadScreenText.Color and Phobos has Starkku's PR #1145
312+
// 2025-05-28: Moved to a hook in Spawner.Hook.cpp - Starkku
313+
// if (Spawner::Config->ReadMissionSection) // before parsing
314+
// Patch::Apply_RAW(0x839724, "Spawn.ini");
315+
316+
bool result = ScenarioClass::StartScenario(pScenarioName, 1, 0);
317+
318+
if (Spawner::Config->CustomMissionID != 0) // after parsing
319+
ScenarioClass::Instance->EndOfGame = true;
320+
321+
return result;
309322
}
310323
else if (SessionClass::IsSkirmish())
311324
{

0 commit comments

Comments
 (0)