Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions src/Misc/SavedGamesInSubdir.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
#include <Utilities/Macro.h>
#include <Spawner/Spawner.h>

#include <HouseClass.h>
#include <LoadOptionsClass.h>

#include <filesystem>
#include <optional>

namespace SavedGames
{
Expand Down Expand Up @@ -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<typename T>
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<typename T>
std::optional<T> 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<SavedGames::CustomMissionID>(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<CustomMissionID>(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<CustomMissionID>(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;
}
16 changes: 9 additions & 7 deletions src/Spawner/Spawner.Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions src/Spawner/Spawner.Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -112,6 +113,7 @@ class SpawnerConfig
char ScenarioName[260];
char MapHash[0xff];
wchar_t UIMapName[45];
bool ReadMissionSection;

// Network Options
int Protocol;
Expand Down Expand Up @@ -172,6 +174,7 @@ class SpawnerConfig
, LoadSaveGame { false }
, SavedGameDir { "Saved Games" }
, SaveGameName { "" }
, CustomMissionID { 0 }
, AutoSaveCount { 5 }
, AutoSaveInterval { 7200 }
, NextAutoSaveNumber { 0 }
Expand All @@ -185,6 +188,7 @@ class SpawnerConfig
, ScenarioName { "spawnmap.ini" }
, MapHash { "" }
, UIMapName { L"" }
, ReadMissionSection { false }

// Network Options
, Protocol { 2 }
Expand Down
27 changes: 27 additions & 0 deletions src/Spawner/Spawner.Hook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <Utilities/Debug.h>
#include <Utilities/Macro.h>
#include <Unsorted.h>
#include <CCINIClass.h>

DEFINE_HOOK(0x6BD7C5, WinMain_SpawnerInit, 0x6)
{
Expand Down Expand Up @@ -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;
}
20 changes: 17 additions & 3 deletions src/Spawner/Spawner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "ProtocolZero.LatencyLevel.h"
#include <Utilities/Debug.h>
#include <Utilities/DumperTypes.h>
#include <Utilities/Patch.h>

#include <GameOptionsClass.h>
#include <GameStrings.h>
Expand Down Expand Up @@ -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())
{
Expand Down