Skip to content

Commit b41beba

Browse files
authored
Autosave / MP save improvements (#48)
* Use reference to game's SAVEGAME.NET * Autosave / MP save improvements - Fix no autosave in skirmish - Overhaul save routine a bit, including making it more consistent with Phobos - Add explanations * Fix capitalization
1 parent 114aaab commit b41beba

File tree

2 files changed

+89
-38
lines changed

2 files changed

+89
-38
lines changed

src/Spawner/Spawner.Hook.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ DEFINE_HOOK(0x686B20, INIClass_ReadScenario_AutoSave, 0x6)
224224
return 0;
225225
}
226226

227+
// Do not change the address without adjusting Phobos handling
228+
// and reading the comments in Spawner::After_Main_Loop
227229
DEFINE_HOOK(0x4C7A14, EventClass_RespondToEvent_SaveGame, 0x5)
228230
{
229231
Spawner::RespondToSaveGame();

src/Spawner/Spawner.cpp

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
#include <Unsorted.h>
3939
#include <WWMouseClass.h>
4040

41+
#include <cassert>
42+
4143
bool Spawner::Enabled = false;
4244
bool Spawner::Active = false;
4345
std::unique_ptr<SpawnerConfig> Spawner::Config = nullptr;
@@ -570,69 +572,116 @@ void Spawner::RespondToSaveGame()
570572
*
571573
* Original author: Rampastring, ZivDero
572574
* Migration: TaranDahl
575+
* Further changes: Kerbiter
573576
*/
574577
void Spawner::After_Main_Loop()
575578
{
576579
auto pConfig = Spawner::GetConfig();
577580

578-
const bool doSaveCampaign = SessionClass::Instance.GameMode == GameMode::Campaign && pConfig->AutoSaveCount > 0 && pConfig->AutoSaveInterval > 0;
579-
const bool doSaveMP = Spawner::Active && SessionClass::Instance.GameMode == GameMode::LAN && pConfig->AutoSaveInterval > 0;
581+
const bool doSaveSP =
582+
SessionClass::IsSingleplayer()
583+
&& pConfig->AutoSaveCount > 0
584+
&& pConfig->AutoSaveInterval > 0;
585+
586+
const bool doSaveMP =
587+
Spawner::Active
588+
&& SessionClass::Instance.GameMode == GameMode::LAN
589+
&& pConfig->AutoSaveInterval > 0;
590+
591+
const bool isAutoSaving = (doSaveSP || doSaveMP)
592+
&& Unsorted::CurrentFrame == Spawner::NextAutoSaveFrame;
580593

581594
// Schedule to make a save if it's time to autosave.
582-
if (doSaveCampaign || doSaveMP)
583-
{
584-
if (Unsorted::CurrentFrame == Spawner::NextAutoSaveFrame)
585-
{
586-
Spawner::DoSave = true;
587-
}
588-
}
595+
// The save might be triggered manually, so we have to OR it.
596+
Spawner::DoSave |= isAutoSaving;
589597

590598
if (Spawner::DoSave)
591599
{
592-
// Send the message.
593-
const auto TXT_AUTOSAVE_MESSAGE = StringTable::TryFetchString("TXT_AUTOSAVE_MESSAGE", L"Saving game...");
594-
MessageListClass::Instance.PrintMessage(TXT_AUTOSAVE_MESSAGE, (int)(RulesClass::Instance->MessageDelay * 900), ColorScheme::White, true);
600+
auto PrintMessage = [](const wchar_t* pMessage)
601+
{
602+
MessageListClass::Instance.PrintMessage(
603+
pMessage,
604+
RulesClass::Instance->MessageDelay,
605+
HouseClass::CurrentPlayer->ColorSchemeIndex,
606+
/* bSilent: */ true
607+
);
608+
609+
// Force a redraw so that our message gets printed.
610+
if (Game::SpecialDialog == 0)
611+
{
612+
MapClass::Instance.MarkNeedsRedraw(2);
613+
MapClass::Instance.Render();
614+
}
615+
};
595616

596-
// Force a redraw so that our message gets printed.
597-
if (Game::SpecialDialog == 0)
617+
auto SaveGame = [PrintMessage](const char* fName, const wchar_t* description)
598618
{
599-
MapClass::Instance.MarkNeedsRedraw(2);
600-
MapClass::Instance.Render();
601-
}
619+
if (ScenarioClass::SaveGame(fName, description))
620+
PrintMessage(StringTable::LoadString(GameStrings::TXT_GAME_WAS_SAVED));
621+
else
622+
PrintMessage(StringTable::LoadString(GameStrings::TXT_ERROR_SAVING_GAME));
623+
};
624+
625+
// Send the message.
626+
PrintMessage(StringTable::LoadString(GameStrings::TXT_SAVING_GAME));
627+
628+
std::wstring saveGameDescription;
629+
if (SessionClass::IsCampaign())
630+
saveGameDescription = ScenarioClass::Instance->UINameLoaded;
631+
else
632+
saveGameDescription = ScenarioClass::Instance->Name;
633+
saveGameDescription += L" - ";
634+
635+
// This whole situation is a mess, but basically there's a myriad of ways to save
636+
// scattered across Phobos (quicksave hotkey and save trigger action) and spawner
637+
// (autosave), all in different conditions (multi- or singleplayer).
638+
639+
// Previously everything only supported singleplayer, so Phobos didn't have to
640+
// account for multiplayer. Now we have to support both singleplayer and multiplayer,
641+
// but only spawner can do proper multiplayer saves on-demand, *and* also without
642+
// spawner there is no point in doing multiplayer saves at all.
643+
644+
// What I came up with is: for synced situations (trigger action) we save on Phobos
645+
// side only if save event code (0x4C7A14) is patched (heuristic, any better ideas
646+
// are welcome), and for unsynced situations (quicksave) we also check for that patch
647+
// and emit the event that uses it.
648+
649+
// If anyone wants to untangle that mess in a nice way - be my guest.
650+
// - Kerbiter
602651

603-
// Campaign autosave.
604-
if (SessionClass::Instance.GameMode == GameMode::Campaign)
652+
// Singleplayer autosave.
653+
if (SessionClass::Instance.IsSingleplayer())
605654
{
606-
static char saveFileName[32];
607-
static wchar_t saveDescription[32];
655+
// ASSUMPTION: There will be no save events emitted in singleplayer
656+
// situations, and the only other way for the spawner to save is
657+
// through the autosave, which is what we are doing here.
608658

609-
// Prepare the save name and description.
610-
const auto TXT_AUTOSAVE_DESCRIPTION_CAMPAIGN = StringTable::TryFetchString("TXT_AUTOSAVE_DESC_SP", L"Mission Auto-Save (Slot %d)");
611-
std::sprintf(saveFileName, "AUTOSAVE%d.SAV", Spawner::NextAutoSaveNumber + 1);
612-
std::swprintf(saveDescription, TXT_AUTOSAVE_DESCRIPTION_CAMPAIGN, Spawner::NextAutoSaveNumber + 1);
659+
// If you want to fixup this - again, be my guest.
660+
// - Kerbiter
661+
662+
assert(isAutoSaving);
613663

614-
// Pause the mission timer.
615-
ScenarioClass::PauseGame();
616-
Game::CallBack();
664+
static char saveFileName[32];
665+
static wchar_t saveDescription[128];
617666

618-
// Save!
619-
ScenarioClass::Instance->SaveGame(saveFileName, saveDescription);
667+
saveGameDescription += StringTable::TryFetchString("TXT_AUTOSAVE_SUFFIX", L"Autosave (slot %d)");
668+
std::sprintf(saveFileName, "AUTOSAVE%d.SAV", Spawner::NextAutoSaveNumber + 1);
669+
std::swprintf(saveDescription, saveGameDescription.c_str(), Spawner::NextAutoSaveNumber + 1);
620670

621-
// Unpause the mission timer.
622-
ScenarioClass::ResumeGame();
671+
SaveGame(saveFileName, saveDescription);
623672

624-
// Increment the autosave number.
625673
Spawner::NextAutoSaveNumber = (Spawner::NextAutoSaveNumber + 1) % pConfig->AutoSaveCount;
626-
627-
// Schedule the next autosave.
628674
Spawner::NextAutoSaveFrame = Unsorted::CurrentFrame + pConfig->AutoSaveInterval;
629675
}
630676
else if (SessionClass::Instance.GameMode == GameMode::LAN)
631677
{
632-
// Save!
633-
ScenarioClass::Instance->SaveGame("SAVEGAME.NET", StringTable::TryFetchString("TXT_AUTOSAVE_DESC_MP", L"Multiplayer Game"));
678+
// CnCNet client follows the legacy approach of fixed save name and copies it
679+
// over to it's own directory. The description isn't read now, but we write it
680+
// regardless as it shouldn't impact anything. The suffix for it is unavailable
681+
// though as it would require a custom event (seems overkill for such).
682+
saveGameDescription += StringTable::LoadString(GameStrings::TXT_MULTIPLAYER_GAME);
683+
SaveGame(GameStrings::SAVEGAME_NET, saveGameDescription.c_str());
634684

635-
// Schedule the next autosave.
636685
Spawner::NextAutoSaveFrame = Unsorted::CurrentFrame + pConfig->AutoSaveInterval;
637686
}
638687

0 commit comments

Comments
 (0)