From fa2f33237294ecbb03bfbc5d16c2e92b5357d41e Mon Sep 17 00:00:00 2001 From: Trsdy <914137150@qq.com> Date: Sat, 5 Sep 2026 14:33:17 +0800 Subject: [PATCH 1/7] init --- Spawner.vcxproj | 1 + src/Misc/NoSaveLoadMode.cpp | 102 +++++++++++++++++++++++++++++++++ src/Spawner/Spawner.Config.cpp | 1 + src/Spawner/Spawner.Config.h | 2 + src/Spawner/Spawner.Hook.cpp | 7 +++ 5 files changed, 113 insertions(+) create mode 100644 src/Misc/NoSaveLoadMode.cpp diff --git a/Spawner.vcxproj b/Spawner.vcxproj index 5df113d1..4633f0e2 100644 --- a/Spawner.vcxproj +++ b/Spawner.vcxproj @@ -17,6 +17,7 @@ + diff --git a/src/Misc/NoSaveLoadMode.cpp b/src/Misc/NoSaveLoadMode.cpp new file mode 100644 index 00000000..e05b129f --- /dev/null +++ b/src/Misc/NoSaveLoadMode.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include + +const wchar_t* Fetch_CSF_Text(const char* label, const wchar_t* defaultText) +{ + std::wstring_view msg = StringTable::LoadString(label); + if (msg.empty() || msg.starts_with(L"MISSING:")) + return defaultText; + return msg.data(); +} + +DEFINE_HOOK(0x686089, DoLose_RetryDialogForCampaigns, 0x7) +{ + if (!Spawner::GetConfig()->DisableSaveLoad) return 0; + enum { OK = 0x6860F6, Cancel = 0x6860EE}; + if (WWMessageBox::Instance.Process( + Fetch_CSF_Text("TXT_HARDCORE_FAILURE", L"GG"), + StringTable::LoadString("GUI:Restart"), + StringTable::LoadString("GUI:Leave"), nullptr) == WWMessageBox::Result::OK) + return OK; + + return Cancel; +} + +// disable load, save and delete buttons on the ingame menu +DEFINE_HOOK(0x4F17F6, sub_4F1720_DisableSaves, 0x6) +{ + if (!Spawner::GetConfig()->DisableSaveLoad) return 0; + GET(HWND, hDlg, EBP); + + HWND hLoad = GetDlgItem(hDlg, 1310); + SendMessageA(hLoad, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantLoad", L"Cannot Load")); + EnableWindow(hLoad, FALSE); + HWND hSave = GetDlgItem(hDlg, 1311); + SendMessageA(hSave, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantSave", L"Cannot Save")); + EnableWindow(hSave, FALSE); + HWND hDel = GetDlgItem(hDlg, 1312); + SendMessageA(hDel, 1202, 0, (LPARAM)L"----"); + EnableWindow(hDel, FALSE); + + return 0x4F1834; +} + +std::wstring HardCoreText {}; +DEFINE_HOOK(0x553076, LoadProgressMgr_Draw_ExtraText, 0x5) +{ + GET(LoadProgressManager*, self, EBP); + if (!Spawner::GetConfig()->DisableSaveLoad) return 0; + + Point2D pos + { + self->TitleBarRect.X + self->TitleBarRect.Width - 100, + self->TitleBarRect.Y + 10 + }; + if (HardCoreText.empty()) + HardCoreText = Fetch_CSF_Text("TXT_HARDCORE_MODE", L"HardCore"); + LEA_STACK(RectangleStruct*, pBnd, STACK_OFFSET(0x1268, -0x1204)); + if (auto logo = FileSystem::LoadSHPFile("hardcorelogo.shp")) + { + self->ProgressSurface->DrawSHP(FileSystem::PALETTE_PAL, logo, 0, &pos, pBnd, BlitterFlags::bf_400, 0, 0, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0); + } + else + { + self->ProgressSurface->DrawText(HardCoreText.c_str(), &pos, COLOR_RED); + } + + return 0; +} + +DEFINE_HOOK(0x6D0E20, TabClass_Draw_ccb, 0x6) +{ + if (HardCoreText.empty()) return 0; + auto rect = DSurface::Temp->GetRect(); + auto wanted = Drawing::GetTextDimensions(HardCoreText.c_str(), { 0,0 }, 0); + Point2D pt { + rect.Width - wanted.Width - 40, + rect.Height - 24 + }; + DSurface::Temp->DrawText(HardCoreText.c_str(), &pt, COLOR_WHITE); + return 0; +} + +DEFINE_HOOK(0x68A107, ScenarioCass_ReadMapINI_eog, 0x6) +{ + if (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0) + return 0; + GET(ScenarioClass*, sce, ESI); + sce->EndOfGame = true; + return 0x68A125; +} + +DEFINE_HOOK(0x6C9357, MissionEndDlg_HardCoreExitText, 0x5) +{ + if (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0) + return 0; + GET(HWND, hdlg, EBX); + auto btnCtn = GetDlgItem(hdlg, 1059); + SendMessageA(btnCtn, 1202, 0, (LPARAM)StringTable::LoadString("GUI:ExitGame")); + return 0; +} diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index 205eca9d..62d61f44 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -118,6 +118,7 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI) ContinueWithoutHumans = pINI->ReadBool(pSettingsSection, "ContinueWithoutHumans", ContinueWithoutHumans); DefeatedBecomesObserver = pINI->ReadBool(pSettingsSection, "DefeatedBecomesObserver", DefeatedBecomesObserver); Observer_ShowAIOnSidebar = pINI->ReadBool(pSettingsSection, "Observer.ShowAIOnSidebar", Observer_ShowAIOnSidebar); + DisableSaveLoad = pINI->ReadBool(pSettingsSection, "DisableSaveLoad", false) && IsCampaign && !LoadSaveGame; #ifdef IS_CNCNET_YR_VER DisableChat = pINI->ReadBool(pSettingsSection, "DisableChat", DisableChat); #endif diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h index fca7b2be..35191153 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -147,6 +147,7 @@ class SpawnerConfig bool ContinueWithoutHumans; bool DefeatedBecomesObserver; bool Observer_ShowAIOnSidebar; + bool DisableSaveLoad; #ifdef IS_CNCNET_YR_VER bool DisableChat; #endif @@ -245,6 +246,7 @@ class SpawnerConfig , ContinueWithoutHumans { false } , DefeatedBecomesObserver { false } , Observer_ShowAIOnSidebar { false } + , DisableSaveLoad { false } #ifdef IS_CNCNET_YR_VER , DisableChat { false } #endif diff --git a/src/Spawner/Spawner.Hook.cpp b/src/Spawner/Spawner.Hook.cpp index 0659dcad..0b2ddc29 100644 --- a/src/Spawner/Spawner.Hook.cpp +++ b/src/Spawner/Spawner.Hook.cpp @@ -73,6 +73,13 @@ DEFINE_HOOK(0x6BD7C5, WinMain_SpawnerInit, 0x6) // Skip load *.PKT, *.YRO and *.YRM map files Patch::Apply_LJMP(0x699AD9, 0x69A1B2); // SessionClass::Read_Scenario_Descriptions + + if (Spawner::GetConfig()->DisableSaveLoad) + { + Patch::Apply_LJMP(0x55DBCD, 0x55DC99); // Disable MainLoop_SaveGame (Spawner&Phobos) + Patch::Apply_RAW(0x67CEF0, { 0x33,0xC0,0xC3 }); // Corrupt savegame function + Patch::Apply_TYPED(0x83D560, { (DWORD)std::rand() }); // Corrupt save game magicn + } } return 0; From 9ff47a9a35fba5de4e50f2f2e317dc7ded89e184 Mon Sep 17 00:00:00 2001 From: Trsdy <914137150@qq.com> Date: Wed, 30 Sep 2026 15:35:31 +0800 Subject: [PATCH 2/7] minor adjust --- src/Misc/NoSaveLoadMode.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Misc/NoSaveLoadMode.cpp b/src/Misc/NoSaveLoadMode.cpp index e05b129f..83cc4d1c 100644 --- a/src/Misc/NoSaveLoadMode.cpp +++ b/src/Misc/NoSaveLoadMode.cpp @@ -82,21 +82,23 @@ DEFINE_HOOK(0x6D0E20, TabClass_Draw_ccb, 0x6) return 0; } -DEFINE_HOOK(0x68A107, ScenarioCass_ReadMapINI_eog, 0x6) +DEFINE_HOOK(0x68A1BD, ScenarioCass_ReadMapINI_eog, 0x6) { if (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0) return 0; GET(ScenarioClass*, sce, ESI); sce->EndOfGame = true; - return 0x68A125; + sce->SkipScore = false; + return 0x68A1DB; } -DEFINE_HOOK(0x6C9357, MissionEndDlg_HardCoreExitText, 0x5) +DEFINE_HOOK(0x6C9357, MissionEndDlg_ExitText, 0x5) { - if (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0) - return 0; - GET(HWND, hdlg, EBX); - auto btnCtn = GetDlgItem(hdlg, 1059); - SendMessageA(btnCtn, 1202, 0, (LPARAM)StringTable::LoadString("GUI:ExitGame")); + if (ScenarioClass::Instance->EndOfGame) + { + GET(HWND, hdlg, EBX); + auto btnCtn = GetDlgItem(hdlg, 1059); + SendMessageA(btnCtn, 1202, 0, (LPARAM)StringTable::LoadString("GUI:ExitGame")); + } return 0; } From debef9e7947e9501cc0500369b83d6cf3eeed577 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:13:04 +0300 Subject: [PATCH 3/7] Clean up NoSaveLoadMode code --- src/Misc/NoSaveLoadMode.cpp | 120 ++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/src/Misc/NoSaveLoadMode.cpp b/src/Misc/NoSaveLoadMode.cpp index 83cc4d1c..12579eee 100644 --- a/src/Misc/NoSaveLoadMode.cpp +++ b/src/Misc/NoSaveLoadMode.cpp @@ -11,94 +11,108 @@ const wchar_t* Fetch_CSF_Text(const char* label, const wchar_t* defaultText) return msg.data(); } -DEFINE_HOOK(0x686089, DoLose_RetryDialogForCampaigns, 0x7) +DEFINE_HOOK(0x686089, DoLose_NoSaveLoadModeRetryDialog, 0x7) { - if (!Spawner::GetConfig()->DisableSaveLoad) return 0; - enum { OK = 0x6860F6, Cancel = 0x6860EE}; - if (WWMessageBox::Instance.Process( + if (!Spawner::GetConfig()->DisableSaveLoad) + return 0; + + enum { Restart = 0x6860F6, Leave = 0x6860EE }; + + const auto result = WWMessageBox::Instance.Process( Fetch_CSF_Text("TXT_HARDCORE_FAILURE", L"GG"), StringTable::LoadString("GUI:Restart"), - StringTable::LoadString("GUI:Leave"), nullptr) == WWMessageBox::Result::OK) - return OK; + StringTable::LoadString("GUI:Leave"), nullptr); - return Cancel; + if (result == WWMessageBox::Result::OK) + return Restart; + + return Leave; } // disable load, save and delete buttons on the ingame menu -DEFINE_HOOK(0x4F17F6, sub_4F1720_DisableSaves, 0x6) +DEFINE_HOOK(0x4F17F6, GameOptionsDialog_DisableSaveLoadButtons, 0x6) { - if (!Spawner::GetConfig()->DisableSaveLoad) return 0; - GET(HWND, hDlg, EBP); - - HWND hLoad = GetDlgItem(hDlg, 1310); - SendMessageA(hLoad, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantLoad", L"Cannot Load")); - EnableWindow(hLoad, FALSE); - HWND hSave = GetDlgItem(hDlg, 1311); - SendMessageA(hSave, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantSave", L"Cannot Save")); - EnableWindow(hSave, FALSE); - HWND hDel = GetDlgItem(hDlg, 1312); - SendMessageA(hDel, 1202, 0, (LPARAM)L"----"); - EnableWindow(hDel, FALSE); + if (!Spawner::GetConfig()->DisableSaveLoad) + return 0; + + GET(HWND, hDialog, EBP); + + HWND hLoadButton = GetDlgItem(hDialog, 1310); + SendMessageA(hLoadButton, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantLoad", L"Cannot Load")); + EnableWindow(hLoadButton, FALSE); + + HWND hSaveButton = GetDlgItem(hDialog, 1311); + SendMessageA(hSaveButton, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantSave", L"Cannot Save")); + EnableWindow(hSaveButton, FALSE); + + HWND hDeleteButton = GetDlgItem(hDialog, 1312); + SendMessageA(hDeleteButton, 1202, 0, (LPARAM)L"----"); + EnableWindow(hDeleteButton, FALSE); return 0x4F1834; } -std::wstring HardCoreText {}; -DEFINE_HOOK(0x553076, LoadProgressMgr_Draw_ExtraText, 0x5) +std::wstring NoSaveLoadModeText { }; +DEFINE_HOOK(0x553076, LoadProgressManager_Draw_NoSaveLoadModeIndicator, 0x5) { - GET(LoadProgressManager*, self, EBP); - if (!Spawner::GetConfig()->DisableSaveLoad) return 0; + if (!Spawner::GetConfig()->DisableSaveLoad) + return 0; + + GET(LoadProgressManager*, pLoadProgressManager, EBP); + if (NoSaveLoadModeText.empty()) + NoSaveLoadModeText = Fetch_CSF_Text("TXT_HARDCORE_MODE", L"HardCore"); - Point2D pos + Point2D position { - self->TitleBarRect.X + self->TitleBarRect.Width - 100, - self->TitleBarRect.Y + 10 + pLoadProgressManager->TitleBarRect.X + pLoadProgressManager->TitleBarRect.Width - 100, + pLoadProgressManager->TitleBarRect.Y + 10 }; - if (HardCoreText.empty()) - HardCoreText = Fetch_CSF_Text("TXT_HARDCORE_MODE", L"HardCore"); - LEA_STACK(RectangleStruct*, pBnd, STACK_OFFSET(0x1268, -0x1204)); - if (auto logo = FileSystem::LoadSHPFile("hardcorelogo.shp")) - { - self->ProgressSurface->DrawSHP(FileSystem::PALETTE_PAL, logo, 0, &pos, pBnd, BlitterFlags::bf_400, 0, 0, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0); - } + + LEA_STACK(RectangleStruct*, pBounds, STACK_OFFSET(0x1268, -0x1204)); + if (auto pLogoSHP = FileSystem::LoadSHPFile("hardcorelogo.shp")) + pLoadProgressManager->ProgressSurface->DrawSHP(FileSystem::PALETTE_PAL, pLogoSHP, 0, &position, pBounds, BlitterFlags::bf_400, 0, 0, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0); else - { - self->ProgressSurface->DrawText(HardCoreText.c_str(), &pos, COLOR_RED); - } + pLoadProgressManager->ProgressSurface->DrawText(NoSaveLoadModeText.c_str(), &position, COLOR_RED); return 0; } -DEFINE_HOOK(0x6D0E20, TabClass_Draw_ccb, 0x6) +DEFINE_HOOK(0x6D0E20, TabClass_Draw_NoSaveLoadModeText, 0x6) { - if (HardCoreText.empty()) return 0; - auto rect = DSurface::Temp->GetRect(); - auto wanted = Drawing::GetTextDimensions(HardCoreText.c_str(), { 0,0 }, 0); - Point2D pt { - rect.Width - wanted.Width - 40, - rect.Height - 24 + if (NoSaveLoadModeText.empty()) + return 0; + + auto surfaceRect = DSurface::Temp->GetRect(); + auto textDimensions = Drawing::GetTextDimensions(NoSaveLoadModeText.c_str(), { 0,0 }, 0); + Point2D position { + surfaceRect.Width - textDimensions.Width - 40, + surfaceRect.Height - 24 }; - DSurface::Temp->DrawText(HardCoreText.c_str(), &pt, COLOR_WHITE); + DSurface::Temp->DrawText(NoSaveLoadModeText.c_str(), &position, COLOR_WHITE); + return 0; } -DEFINE_HOOK(0x68A1BD, ScenarioCass_ReadMapINI_eog, 0x6) +DEFINE_HOOK(0x68A1BD, ScenarioClass_ReadMapINI_SetEndOfGame, 0x6) { if (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0) return 0; - GET(ScenarioClass*, sce, ESI); - sce->EndOfGame = true; - sce->SkipScore = false; + + GET(ScenarioClass*, pScenario, ESI); + pScenario->EndOfGame = true; + pScenario->SkipScore = false; + return 0x68A1DB; } -DEFINE_HOOK(0x6C9357, MissionEndDlg_ExitText, 0x5) +DEFINE_HOOK(0x6C9357, MissionEndDialog_SetExitGameText, 0x5) { if (ScenarioClass::Instance->EndOfGame) { - GET(HWND, hdlg, EBX); - auto btnCtn = GetDlgItem(hdlg, 1059); - SendMessageA(btnCtn, 1202, 0, (LPARAM)StringTable::LoadString("GUI:ExitGame")); + GET(HWND, hDialog, EBX); + auto hContinueButton = GetDlgItem(hDialog, 1059); + SendMessageA(hContinueButton, 1202, 0, (LPARAM)StringTable::LoadString("GUI:ExitGame")); } + return 0; } From 2ef3434b18ee8f8557d6981e0bf6994741fd9ba7 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:13:05 +0300 Subject: [PATCH 4/7] Use TryFetchString for no-save mode text --- src/Misc/NoSaveLoadMode.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Misc/NoSaveLoadMode.cpp b/src/Misc/NoSaveLoadMode.cpp index 12579eee..49b3bfb4 100644 --- a/src/Misc/NoSaveLoadMode.cpp +++ b/src/Misc/NoSaveLoadMode.cpp @@ -3,14 +3,6 @@ #include #include -const wchar_t* Fetch_CSF_Text(const char* label, const wchar_t* defaultText) -{ - std::wstring_view msg = StringTable::LoadString(label); - if (msg.empty() || msg.starts_with(L"MISSING:")) - return defaultText; - return msg.data(); -} - DEFINE_HOOK(0x686089, DoLose_NoSaveLoadModeRetryDialog, 0x7) { if (!Spawner::GetConfig()->DisableSaveLoad) @@ -19,7 +11,7 @@ DEFINE_HOOK(0x686089, DoLose_NoSaveLoadModeRetryDialog, 0x7) enum { Restart = 0x6860F6, Leave = 0x6860EE }; const auto result = WWMessageBox::Instance.Process( - Fetch_CSF_Text("TXT_HARDCORE_FAILURE", L"GG"), + StringTable::TryFetchString("TXT_HARDCORE_FAILURE", L"GG"), StringTable::LoadString("GUI:Restart"), StringTable::LoadString("GUI:Leave"), nullptr); @@ -38,11 +30,11 @@ DEFINE_HOOK(0x4F17F6, GameOptionsDialog_DisableSaveLoadButtons, 0x6) GET(HWND, hDialog, EBP); HWND hLoadButton = GetDlgItem(hDialog, 1310); - SendMessageA(hLoadButton, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantLoad", L"Cannot Load")); + SendMessageA(hLoadButton, 1202, 0, (LPARAM)StringTable::TryFetchString("GUI:CantLoad", L"Cannot Load")); EnableWindow(hLoadButton, FALSE); HWND hSaveButton = GetDlgItem(hDialog, 1311); - SendMessageA(hSaveButton, 1202, 0, (LPARAM)Fetch_CSF_Text("GUI:CantSave", L"Cannot Save")); + SendMessageA(hSaveButton, 1202, 0, (LPARAM)StringTable::TryFetchString("GUI:CantSave", L"Cannot Save")); EnableWindow(hSaveButton, FALSE); HWND hDeleteButton = GetDlgItem(hDialog, 1312); @@ -60,7 +52,7 @@ DEFINE_HOOK(0x553076, LoadProgressManager_Draw_NoSaveLoadModeIndicator, 0x5) GET(LoadProgressManager*, pLoadProgressManager, EBP); if (NoSaveLoadModeText.empty()) - NoSaveLoadModeText = Fetch_CSF_Text("TXT_HARDCORE_MODE", L"HardCore"); + NoSaveLoadModeText = StringTable::TryFetchString("TXT_HARDCORE_MODE", L"HardCore"); Point2D position { From c134b9c5270eaf1d48ef4c2adb92e5f26de3d1b2 Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:16:10 +0300 Subject: [PATCH 5/7] Guard no-save mode hooks when spawner is disabled --- src/Misc/NoSaveLoadMode.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Misc/NoSaveLoadMode.cpp b/src/Misc/NoSaveLoadMode.cpp index 49b3bfb4..72cd17e6 100644 --- a/src/Misc/NoSaveLoadMode.cpp +++ b/src/Misc/NoSaveLoadMode.cpp @@ -3,9 +3,14 @@ #include #include +static bool IsDisableSaveLoadEnabled() +{ + return Spawner::Enabled && Spawner::GetConfig()->DisableSaveLoad; +} + DEFINE_HOOK(0x686089, DoLose_NoSaveLoadModeRetryDialog, 0x7) { - if (!Spawner::GetConfig()->DisableSaveLoad) + if (!IsDisableSaveLoadEnabled()) return 0; enum { Restart = 0x6860F6, Leave = 0x6860EE }; @@ -24,7 +29,7 @@ DEFINE_HOOK(0x686089, DoLose_NoSaveLoadModeRetryDialog, 0x7) // disable load, save and delete buttons on the ingame menu DEFINE_HOOK(0x4F17F6, GameOptionsDialog_DisableSaveLoadButtons, 0x6) { - if (!Spawner::GetConfig()->DisableSaveLoad) + if (!IsDisableSaveLoadEnabled()) return 0; GET(HWND, hDialog, EBP); @@ -47,7 +52,7 @@ DEFINE_HOOK(0x4F17F6, GameOptionsDialog_DisableSaveLoadButtons, 0x6) std::wstring NoSaveLoadModeText { }; DEFINE_HOOK(0x553076, LoadProgressManager_Draw_NoSaveLoadModeIndicator, 0x5) { - if (!Spawner::GetConfig()->DisableSaveLoad) + if (!IsDisableSaveLoadEnabled()) return 0; GET(LoadProgressManager*, pLoadProgressManager, EBP); @@ -87,7 +92,7 @@ DEFINE_HOOK(0x6D0E20, TabClass_Draw_NoSaveLoadModeText, 0x6) DEFINE_HOOK(0x68A1BD, ScenarioClass_ReadMapINI_SetEndOfGame, 0x6) { - if (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0) + if (!Spawner::Enabled || (!Spawner::GetConfig()->DisableSaveLoad && Spawner::GetConfig()->CustomMissionID == 0)) return 0; GET(ScenarioClass*, pScenario, ESI); From 0565309752be0a1be7136bf2b722431c4174044a Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:16:10 +0300 Subject: [PATCH 6/7] Terminate explicitly when saving is disabled --- src/Misc/NoSaveLoadMode.cpp | 20 ++++++++++++++++++++ src/Spawner/Spawner.Hook.cpp | 6 ------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Misc/NoSaveLoadMode.cpp b/src/Misc/NoSaveLoadMode.cpp index 72cd17e6..e51aaf48 100644 --- a/src/Misc/NoSaveLoadMode.cpp +++ b/src/Misc/NoSaveLoadMode.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -8,6 +9,25 @@ static bool IsDisableSaveLoadEnabled() return Spawner::Enabled && Spawner::GetConfig()->DisableSaveLoad; } +DEFINE_HOOK(0x6BD7CB, WinMain_InitNoSaveLoadMode, 0x5) +{ + if (IsDisableSaveLoadEnabled()) + { + Patch::Apply_LJMP(0x55DBCD, 0x55DC99); // Skip the initial save, including the Phobos autosave hook. + Patch::Apply_TYPED(0x83D560, { (DWORD)std::rand() }); // Invalidate the save game magic. + } + + return 0; +} + +DEFINE_HOOK(0x67CEF0, ScenarioClass_SaveGame_NoSaveLoadMode, 0x6) +{ + if (IsDisableSaveLoadEnabled()) + Debug::FatalErrorAndExit(Debug::ExitCode::SLFail, "Save attempted with DisableSaveLoad enabled. Terminating the game.\n"); + + return 0; +} + DEFINE_HOOK(0x686089, DoLose_NoSaveLoadModeRetryDialog, 0x7) { if (!IsDisableSaveLoadEnabled()) diff --git a/src/Spawner/Spawner.Hook.cpp b/src/Spawner/Spawner.Hook.cpp index 0b2ddc29..8fee1b70 100644 --- a/src/Spawner/Spawner.Hook.cpp +++ b/src/Spawner/Spawner.Hook.cpp @@ -74,12 +74,6 @@ DEFINE_HOOK(0x6BD7C5, WinMain_SpawnerInit, 0x6) // Skip load *.PKT, *.YRO and *.YRM map files Patch::Apply_LJMP(0x699AD9, 0x69A1B2); // SessionClass::Read_Scenario_Descriptions - if (Spawner::GetConfig()->DisableSaveLoad) - { - Patch::Apply_LJMP(0x55DBCD, 0x55DC99); // Disable MainLoop_SaveGame (Spawner&Phobos) - Patch::Apply_RAW(0x67CEF0, { 0x33,0xC0,0xC3 }); // Corrupt savegame function - Patch::Apply_TYPED(0x83D560, { (DWORD)std::rand() }); // Corrupt save game magicn - } } return 0; From 90717298450237b280d665d14b0c90f8b455d97b Mon Sep 17 00:00:00 2001 From: Belonit <54427022+Belonit@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:21:10 +0300 Subject: [PATCH 7/7] Disable autosaves when DisableSaveLoad is enabled --- src/Spawner/Spawner.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index f7c76d50..2d8305ee 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -608,7 +608,8 @@ void Spawner::After_Main_Loop() && SessionClass::Instance.GameMode == GameMode::LAN && pConfig->AutoSaveInterval > 0; - const bool isAutoSaving = (doSaveSP || doSaveMP) + const bool isAutoSaving = !pConfig->DisableSaveLoad + && (doSaveSP || doSaveMP) && Unsorted::CurrentFrame == Spawner::NextAutoSaveFrame; // Schedule to make a save if it's time to autosave.