raze/source/common/menu/savegamemanager.cpp
Christoph Oelckers 0e19d4262e - savegame and mapinfo cleanup in DN3D and RR frontends.
Also added an "end game" flag to the mapinfo. For those who like to assemble single levels into custom episodes. More features for that to come. ;)
2019-12-10 22:22:59 +01:00

707 lines
17 KiB
C++

/*
** 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)
{
if (gi->LoadGame(node))
{
}
}
void FSavegameManager::SaveGame(FSaveGameNode* node, bool ok4q, bool forceq)
{
if (gi->SaveGame(node))
{
FString fn = node->Filename;
FString desc = node->SaveTitle;
NotifyNewSave(fn, desc, ok4q, forceq);
}
}
//=============================================================================
//
// Save data maintenance
//
//=============================================================================
void FSavegameManager::ClearSaveGames()
{
for (unsigned i = 0; i<SaveGames.Size(); i++)
{
if (!SaveGames[i]->bNoDelete)
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, true);
fr.Close();
delete savegame;
if (check != 0)
{
FSaveGameNode *node = new FSaveGameNode;
node->Filename = filepath;
node->bOldVersion = check == -1;
node->bMissingWads = check == -2;
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; i<SaveGames.Size(); i++)
{
FSaveGameNode *node = SaveGames[i];
#ifdef __unix__
if (node->Filename.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)
{
auto sel = savegameManager.GetSavegame(Selected);
if (sel && !sel->bOldVersion && !sel->bMissingWads)
{
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, true, false);
}
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, true, false);
}
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);
fr.Close();
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 fcomment = sjson_get_string(root, "Map Label", "");
FString ncomment = sjson_get_string(root, "Map Name", "");
FStringf pcomment("%s - %s\n", fcomment.GetChars(), ncomment.GetChars());
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<uint8_t> &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, false, false);
}
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, false, false);
}
//=============================================================================
//
//
//
//=============================================================================
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, true, true);
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, true, true);
}
});
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);
}