/* ** loadsavemenu.cpp ** The load game and save game menus ** **--------------------------------------------------------------------------- ** Copyright 2001-2010 Randy Heit ** Copyright 2010 Christoph Oelckers ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ #include "menu/menu.h" #include "version.h" #include "m_png.h" #include "filesystem.h" #include "v_text.h" #include "d_event.h" #include "gstrings.h" #include "d_gui.h" #include "v_draw.h" #include "files.h" #include "resourcefile.h" #include "sjson.h" #include "cmdlib.h" #include "files.h" #include "savegamehelp.h" #include "i_specialpaths.h" #include "c_dispatch.h" #include "../../platform/win32/i_findfile.h" // This is a temporary direct path. Needs to be fixed when stuff gets cleaned up. FSavegameManager savegameManager; void FSavegameManager::LoadGame(FSaveGameNode* node, bool ok4q, bool forceq) { if (gi->LoadGame(node)) { FString fn = node->Filename; FString desc = node->SaveTitle; NotifyNewSave(fn, desc, ok4q, forceq); } } void FSavegameManager::SaveGame(FSaveGameNode* node) { gi->SaveGame(node); } //============================================================================= // // Save data maintenance // //============================================================================= void FSavegameManager::ClearSaveGames() { for (unsigned i = 0; ibNoDelete) delete SaveGames[i]; } SaveGames.Clear(); } FSavegameManager::~FSavegameManager() { ClearSaveGames(); } //============================================================================= // // Save data maintenance // //============================================================================= int FSavegameManager::RemoveSaveSlot(int index) { int listindex = SaveGames[0]->bNoDelete ? index - 1 : index; if (listindex < 0) return index; remove(SaveGames[index]->Filename.GetChars()); UnloadSaveData(); FSaveGameNode *file = SaveGames[index]; if (quickSaveSlot == SaveGames[index]) { quickSaveSlot = nullptr; } if (!file->bNoDelete) delete file; if (LastSaved == listindex) LastSaved = -1; else if (LastSaved > listindex) LastSaved--; if (LastAccessed == listindex) LastAccessed = -1; else if (LastAccessed > listindex) LastAccessed--; SaveGames.Delete(index); if ((unsigned)index >= SaveGames.Size()) index--; ExtractSaveData(index); return index; } //============================================================================= // // // //============================================================================= int FSavegameManager::InsertSaveNode(FSaveGameNode *node) { if (SaveGames.Size() == 0) { return SaveGames.Push(node); } if (node->bOldVersion) { // Add node at bottom of list return SaveGames.Push(node); } else { // Add node at top of list unsigned int i; for (i = 0; i < SaveGames.Size(); i++) { if (SaveGames[i]->bOldVersion || node->SaveTitle.CompareNoCase(SaveGames[i]->SaveTitle) <= 0) { break; } } SaveGames.Insert(i, node); return i; } } //============================================================================= // // M_ReadSaveStrings // // Find savegames and read their titles // //============================================================================= void FSavegameManager::ReadSaveStrings() { if (SaveGames.Size() == 0) { void *filefirst; findstate_t c_file; FString filter; LastSaved = LastAccessed = -1; quickSaveSlot = nullptr; filter = G_BuildSaveName("*"); filefirst = I_FindFirst(filter.GetChars(), &c_file); if (filefirst != ((void *)(-1))) { do { // I_FindName only returns the file's name and not its full path FString filepath = G_BuildSaveName(I_FindName(&c_file)); FResourceFile *savegame = FResourceFile::OpenResourceFile(filepath, true, true); if (savegame != nullptr) { FResourceLump *info = savegame->FindLump("info.json"); if (info == nullptr) { // savegame info not found. This is not a savegame so leave it alone. delete savegame; continue; } auto fr = info->NewReader(); FString title; int check = G_ValidateSavegame(fr, &title); delete savegame; if (check != 0) { FSaveGameNode *node = new FSaveGameNode; node->Filename = filepath; node->bOldVersion = true; node->bMissingWads = false; node->SaveTitle = title; InsertSaveNode(node); } } } while (I_FindNext (filefirst, &c_file) == 0); I_FindClose (filefirst); } } } //============================================================================= // // // //============================================================================= void FSavegameManager::NotifyNewSave(const FString &file, const FString &title, bool okForQuicksave, bool forceQuicksave) { FSaveGameNode *node; if (file.IsEmpty()) return; ReadSaveStrings(); // See if the file is already in our list for (unsigned i = 0; iFilename.Compare(file) == 0) #else if (node->Filename.CompareNoCase(file) == 0) #endif { node->SaveTitle = title; node->bOldVersion = false; node->bMissingWads = false; if (okForQuicksave) { if (quickSaveSlot == nullptr || quickSaveSlot == (FSaveGameNode*)1 || forceQuicksave) quickSaveSlot = node; LastAccessed = LastSaved = i; } return; } } node = new FSaveGameNode; node->SaveTitle = title; node->Filename = file; node->bOldVersion = false; node->bMissingWads = false; int index = InsertSaveNode(node); if (okForQuicksave) { if (quickSaveSlot == nullptr || quickSaveSlot == (FSaveGameNode*)1 || forceQuicksave) quickSaveSlot = node; LastAccessed = LastSaved = index; } else { LastAccessed = ++LastSaved; } } //============================================================================= // // Loads the savegame // //============================================================================= void FSavegameManager::LoadSavegame(int Selected) { savegameManager.LoadGame(SaveGames[Selected]); if (quickSaveSlot == (FSaveGameNode*)1) { quickSaveSlot = SaveGames[Selected]; } M_ClearMenus(); LastAccessed = Selected; } //============================================================================= // // // //============================================================================= void FSavegameManager::DoSave(int Selected, const char *savegamestring) { if (Selected != 0) { auto node = *SaveGames[Selected]; node.SaveTitle = savegamestring; savegameManager.SaveGame(&node); } else { // Find an unused filename and save as that FString filename; int i; for (i = 0;; ++i) { filename = G_BuildSaveName(FStringf("save%04d", i)); if (!FileExists(filename)) { break; } } FSaveGameNode sg{ savegamestring, filename }; savegameManager.SaveGame(&sg); } M_ClearMenus(); } //============================================================================= // // // //============================================================================= unsigned FSavegameManager::ExtractSaveData(int index) { FResourceFile *resf; FSaveGameNode *node; if (index == -1) { if (SaveGames.Size() > 0 && SaveGames[0]->bNoDelete) { index = LastSaved + 1; } else { index = LastAccessed < 0? 0 : LastAccessed; } } UnloadSaveData(); if ((unsigned)index < SaveGames.Size() && (node = SaveGames[index]) && !node->Filename.IsEmpty() && !node->bOldVersion && (resf = FResourceFile::OpenResourceFile(node->Filename.GetChars(), true)) != nullptr) { FResourceLump *info = resf->FindLump("info.json"); if (info == nullptr) { // this should not happen because the file has already been verified. return index; } auto fr = info->NewReader(); auto data = fr.ReadPadded(1); sjson_context* ctx = sjson_create_context(0, 0, NULL); if (ctx) { sjson_node* root = sjson_decode(ctx, (const char*)data.Data()); FString comment = sjson_get_string(root, "Creation Time", ""); FString pcomment = sjson_get_string(root, "Comment", ""); if (comment.Len() > 0) comment += "\n"; comment += pcomment; SaveCommentString = comment; // Extract pic (todo: let the renderer write a proper PNG file instead of a raw canvas dunp of the software renderer - and make it work for all games.) FResourceLump *pic = resf->FindLump("savepic.png"); if (pic != nullptr) { FileReader picreader; picreader.OpenMemoryArray([=](TArray &array) { auto cache = pic->Lock(); array.Resize(pic->LumpSize); memcpy(&array[0], cache, pic->LumpSize); pic->Unlock(); return true; }); PNGHandle *png = M_VerifyPNG(picreader); if (png != nullptr) { SavePic = nullptr; // not yet implemented: PNGTexture_CreateFromFile(png, node->Filename); delete png; if (SavePic && SavePic->GetWidth() == 1 && SavePic->GetHeight() == 1) { delete SavePic; SavePic = nullptr; SavePicData.Clear(); } } } } delete resf; } return index; } //============================================================================= // // // //============================================================================= void FSavegameManager::UnloadSaveData() { if (SavePic != nullptr) { delete SavePic; } SaveCommentString = ""; SavePic = nullptr; SavePicData.Clear(); } //============================================================================= // // // //============================================================================= void FSavegameManager::ClearSaveStuff() { UnloadSaveData(); if (quickSaveSlot == (FSaveGameNode*)1) { quickSaveSlot = nullptr; } } //============================================================================= // // // //============================================================================= bool FSavegameManager::DrawSavePic(int x, int y, int w, int h) { if (SavePic == nullptr) return false; DrawTexture(&twod, SavePic, x, y, DTA_DestWidth, w, DTA_DestHeight, h, DTA_Masked, false, TAG_DONE); return true; } //============================================================================= // // // //============================================================================= void FSavegameManager::SetFileInfo(int Selected) { if (!SaveGames[Selected]->Filename.IsEmpty()) { SaveCommentString.Format("File on disk:\n%s", SaveGames[Selected]->Filename.GetChars()); } } //============================================================================= // // // //============================================================================= unsigned FSavegameManager::SavegameCount() { return SaveGames.Size(); } //============================================================================= // // // //============================================================================= FSaveGameNode *FSavegameManager::GetSavegame(int i) { return SaveGames[i]; } //============================================================================= // // // //============================================================================= void FSavegameManager::InsertNewSaveNode() { NewSaveNode.SaveTitle = GStrings["NEWSAVE"]; NewSaveNode.bNoDelete = true; SaveGames.Insert(0, &NewSaveNode); } //============================================================================= // // // //============================================================================= bool FSavegameManager::RemoveNewSaveNode() { if (SaveGames[0] == &NewSaveNode) { SaveGames.Delete(0); return true; } return false; } //============================================================================= // // // //============================================================================= CVAR(Bool, saveloadconfirmation, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Int, autosavenum, 0, CVAR_NOSET | CVAR_ARCHIVE | CVAR_GLOBALCONFIG) static int nextautosave = -1; CVAR(Int, disableautosave, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CUSTOM_CVAR(Int, autosavecount, 4, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) { if (self < 1) self = 1; } CVAR(Int, quicksavenum, 0, CVAR_NOSET | CVAR_ARCHIVE | CVAR_GLOBALCONFIG) static int nextquicksave = -1; CUSTOM_CVAR(Int, quicksavecount, 4, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) { if (self < 1) self = 1; } void M_Autosave() { if (!gi->CanSave()) return; FString description; FString file; // Keep a rotating sets of autosaves UCVarValue num; const char* readableTime; int count = autosavecount != 0 ? autosavecount : 1; if (nextautosave == -1) { nextautosave = (autosavenum + 1) % count; } num.Int = nextautosave; autosavenum.ForceSet(num, CVAR_Int); FSaveGameNode sg; sg.Filename = G_BuildSaveName(FStringf("auto%04d", nextautosave)); readableTime = myasctime(); sg.SaveTitle.Format("Autosave %s", readableTime); nextautosave = (nextautosave + 1) % count; savegameManager.SaveGame(&sg); } CCMD(autosave) { if (disableautosave) return; M_Autosave(); } CCMD(rotatingquicksave) { if (!gi->CanSave()) return; FString description; FString file; // Keep a rotating sets of quicksaves UCVarValue num; const char* readableTime; int count = quicksavecount != 0 ? quicksavecount : 1; if (nextquicksave == -1) { nextquicksave = (quicksavenum + 1) % count; } num.Int = nextquicksave; quicksavenum.ForceSet(num, CVAR_Int); FSaveGameNode sg; sg.Filename = G_BuildSaveName(FStringf("quick%04d", nextquicksave)); readableTime = myasctime(); sg.SaveTitle.Format("Quicksave %s", readableTime); nextquicksave = (nextquicksave + 1) % count; savegameManager.SaveGame(&sg); } //============================================================================= // // // //============================================================================= CCMD(quicksave) { // F6 if (!gi->CanSave()) return; if (savegameManager.quickSaveSlot == NULL || savegameManager.quickSaveSlot == (FSaveGameNode*)1) { M_StartControlPanel(true); M_SetMenu(NAME_SaveGameMenu); return; } auto slot = savegameManager.quickSaveSlot; // [mxd]. Just save the game, no questions asked. if (!saveloadconfirmation) { savegameManager.SaveGame(savegameManager.quickSaveSlot); return; } gi->MenuSound(ActivateSound); FString tempstring = GStrings("QSPROMPT"); tempstring.Substitute("%s", slot->SaveTitle.GetChars()); DMenu* newmenu = CreateMessageBoxMenu(DMenu::CurrentMenu, tempstring, 0, INT_MAX, false, NAME_None, [](bool res) { if (res) { savegameManager.SaveGame(savegameManager.quickSaveSlot); } }); M_ActivateMenu(newmenu); } //============================================================================= // // // //============================================================================= CCMD(quickload) { // F9 #if 0 if (netgame) { M_StartControlPanel(true); M_StartMessage(GStrings("QLOADNET"), 1); return; } #endif if (savegameManager.quickSaveSlot == nullptr || savegameManager.quickSaveSlot == (FSaveGameNode*)1) { M_StartControlPanel(true); // signal that whatever gets loaded should be the new quicksave savegameManager.quickSaveSlot = (FSaveGameNode*)1; M_SetMenu(NAME_LoadGameMenu); return; } // [mxd]. Just load the game, no questions asked. if (!saveloadconfirmation) { savegameManager.LoadGame(savegameManager.quickSaveSlot); return; } FString tempstring = GStrings("QLPROMPT"); tempstring.Substitute("%s", savegameManager.quickSaveSlot->SaveTitle.GetChars()); M_StartControlPanel(true); DMenu* newmenu = CreateMessageBoxMenu(DMenu::CurrentMenu, tempstring, 0, INT_MAX, false, NAME_None, [](bool res) { if (res) { savegameManager.LoadGame(savegameManager.quickSaveSlot); } }); M_ActivateMenu(newmenu); } #if 0 // Duke if ((buttonMap.ButtonDown(gamefunc_Quick_Load) || g_doQuickSave == 2) && (myplayer.gm & MODE_GAME)) { buttonMap.ClearButton(gamefunc_Quick_Load); g_doQuickSave = 0; if (g_quickload == nullptr || !g_quickload->isValid()) { C_DoCommand("openloadmenu"); } else if (g_quickload->isValid()) { inputState.keyFlushChars(); inputState.ClearKeysDown(); if (G_LoadPlayerMaybeMulti(*g_quickload) != 0) g_quickload->reset(); } } if ((buttonMap.ButtonDown(gamefunc_Quick_Save) || g_doQuickSave == 1) && (myplayer.gm & MODE_GAME)) { buttonMap.ClearButton(gamefunc_Quick_Save); g_doQuickSave = 0; if (!g_lastusersave.isValid()) { C_DoCommand("opensavemenu"); return; } inputState.keyFlushChars(); if (sprite[myplayer.i].extra <= 0) { P_DoQuote(QUOTE_SAVE_DEAD, &myplayer); return; } g_screenCapture = 1; G_DrawRooms(myconnectindex, 65536); g_screenCapture = 0; if (g_lastusersave.isValid()) { savebrief_t& sv = g_lastusersave; // dirty hack... char 127 in last position indicates an auto-filled name if (sv.name[MAXSAVEGAMENAME] == 127) { strncpy(sv.name, g_mapInfo[ud.volume_number * MAXLEVELS + ud.level_number].name, MAXSAVEGAMENAME); sv.name[MAXSAVEGAMENAME] = 127; } g_quickload = &sv; G_SavePlayerMaybeMulti(sv); } } // handle CON_SAVE and CON_SAVENN if (g_saveRequested) { inputState.keyFlushChars(); videoNextPage(); g_screenCapture = 1; G_DrawRooms(myconnectindex, 65536); g_screenCapture = 0; G_SavePlayerMaybeMulti(g_lastautosave, true); g_quickload = &g_lastautosave; OSD_Printf("Saved: %s\n", g_lastautosave.path); g_saveRequested = false; } // RR if ((buttonMap.ButtonDown(gamefunc_Quick_Load) || g_doQuickSave == 2) && (!RRRA || ud.player_skill != 4) && (!RR || RRRA || ud.player_skill != 5) && (g_player[myconnectindex].ps->gm & MODE_GAME)) { buttonMap.ClearButton(gamefunc_Quick_Load); g_doQuickSave = 0; if (g_quickload == nullptr || !g_quickload->isValid()) { C_DoCommand("openloadmenu"); } else if (g_quickload->isValid()) { inputState.keyFlushChars(); inputState.ClearKeysDown(); S_PauseSounds(true); if (G_LoadPlayerMaybeMulti(*g_quickload) != 0) g_quickload->reset(); } } if ((buttonMap.ButtonDown(gamefunc_Quick_Save) || g_doQuickSave == 1) && (!RRRA || ud.player_skill != 4) && (!RR || RRRA || ud.player_skill != 5) && (g_player[myconnectindex].ps->gm & MODE_GAME)) { buttonMap.ClearButton(gamefunc_Quick_Save); g_doQuickSave = 0; if (!g_lastusersave.isValid()) { C_DoCommand("opensavemenu"); return; } inputState.keyFlushChars(); if (sprite[g_player[myconnectindex].ps->i].extra <= 0) { P_DoQuote(QUOTE_SAVE_DEAD, g_player[myconnectindex].ps); return; } g_screenCapture = 1; G_DrawRooms(myconnectindex, 65536); g_screenCapture = 0; if (g_lastusersave.isValid()) { savebrief_t& sv = g_lastusersave; // dirty hack... char 127 in last position indicates an auto-filled name if (sv.name[MAXSAVEGAMENAME] == 127) { strncpy(sv.name, g_mapInfo[ud.volume_number * MAXLEVELS + ud.level_number].name, MAXSAVEGAMENAME); sv.name[MAXSAVEGAMENAME] = 127; } g_quickload = &sv; G_SavePlayerMaybeMulti(sv); } } // sw SWBOOL DoQuickSave(short save_num) { PauseAction(); if (SaveGame(save_num) != -1) { QuickLoadNum = save_num; LastSaveNum = -1; return FALSE; } return TRUE; } SWBOOL DoQuickLoad() { inputState.ClearKeysDown(); PauseAction(); ReloadPrompt = FALSE; if (LoadGame(QuickLoadNum) == -1) { return FALSE; } ready2send = 1; LastSaveNum = -1; return TRUE; } // F6 quick save if (inputState.GetKeyStatus(KEYSC_F6)) { inputState.ClearKeyStatus(KEYSC_F6); if (!TEST(pp->Flags, PF_DEAD)) { if (QuickLoadNum < 0) { inputState.SetKeyStatus(sc_Escape); ControlPanelType = ct_savemenu; } else { inputState.ClearAllInput(); DoQuickSave(QuickLoadNum); ResumeAction(); } } } // F9 quick load if (inputState.GetKeyStatus(KEYSC_F9)) { inputState.ClearKeyStatus(KEYSC_F9); if (!TEST(pp->Flags, PF_DEAD)) { if (QuickLoadNum < 0) { inputState.SetKeyStatus(sc_Escape); ControlPanelType = ct_loadmenu; } else { DoQuickLoad(); ResumeAction(); } } } //////////////////////////////////////////////// // Load Game menu // This function gets called whenever you // press enter on one of the load game // spots. // I'm figuring it need to do the following: // . Load the game if there is one by calling: MNU_LoadGameCustom. //////////////////////////////////////////////// SWBOOL MNU_GetLoadCustom(void) { short load_num; load_num = currentmenu->cursor; // no saved game exists - don't do anything if (SaveGameDescr[load_num][0] == '\0') return FALSE; if (InMenuLevel || DemoMode || DemoPlaying) { LoadSaveMsg("Loading..."); if (LoadGame(load_num) == -1) return FALSE; QuickLoadNum = load_num; // the (Quick)Save menu should default to the last loaded game SaveGameGroup.cursor = load_num; ExitMenus(); ExitLevel = TRUE; LoadGameOutsideMoveLoop = TRUE; if (DemoMode || DemoPlaying) LoadGameFromDemo = TRUE; return TRUE; } LoadSaveMsg("Loading..."); PauseAction(); if (LoadGame(load_num) == -1) { ResumeAction(); return FALSE; } QuickLoadNum = load_num; // the (Quick)Save menu should default to the last loaded game SaveGameGroup.cursor = load_num; ready2send = 1; LastSaveNum = -1; ExitMenus(); if (DemoMode) { ExitLevel = TRUE; DemoPlaying = FALSE; } return TRUE; } //////////////////////////////////////////////// // Save Game menu // This function gets called whenever you // press enter on one of the save game // spots. // I'm figuring it need to do the following: // . Call MNU_GetInput to allow string input of description. // . Save the game if there is one by calling: MNU_SaveGameCustom. //////////////////////////////////////////////// SWBOOL MNU_GetSaveCustom(void) { short save_num; extern SWBOOL InMenuLevel, LoadGameOutsideMoveLoop; save_num = currentmenu->cursor; if (InMenuLevel) return FALSE; if (MenuInputMode) { LoadSaveMsg("Saving..."); if (DoQuickSave(save_num) == FALSE) { LoadGameGroup.cursor = save_num; } ResumeAction(); ExitMenus(); // toggle edit mode MenuInputMode = FALSE; } else { strcpy(BackupSaveGameDescr, SaveGameDescr[save_num]); // clear keyboard buffer while (inputState.keyBufferWaiting()) { if (inputState.keyGetChar() == 0) inputState.keyGetChar(); } // toggle edit mode MenuInputMode = TRUE; } return TRUE; } // Blood static void DoQuickLoad(void) { if (!gGameMenuMgr.m_bActive) { if (gQuickLoadSlot != -1) { QuickLoadGame(); return; } if (gQuickLoadSlot == -1 && gQuickSaveSlot != -1) { gQuickLoadSlot = gQuickSaveSlot; QuickLoadGame(); return; } gGameMenuMgr.Push(&menuLoadGame, -1); } } static void DoQuickSave(void) { if (gGameStarted && !gGameMenuMgr.m_bActive && gPlayer[myconnectindex].pXSprite->health != 0) { if (gQuickSaveSlot != -1) { QuickSaveGame(); return; } gGameMenuMgr.Push(&menuSaveGame, -1); } } if (gDoQuickSave) { inputState.keyFlushScans(); switch (gDoQuickSave) { case 1: DoQuickSave(); break; case 2: DoQuickLoad(); break; } gDoQuickSave = 0; return; } case sc_F6: inputState.keyFlushScans(); DoQuickSave(); break; case sc_F9: inputState.keyFlushScans(); DoQuickLoad(); break; #endif