You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

570 lines
13 KiB

/**
* @file pfile.cpp
*
* Implementation of the save game encoding functionality.
*/
#include "pfile.h"
#include <string>
#include "codec.h"
#include "engine.h"
#include "init.h"
#include "loadsave.h"
#include "menu.h"
#include "mpq/mpq_reader.hpp"
#include "pack.h"
#include "qol/stash.h"
#include "utils/endian.hpp"
#include "utils/file_util.h"
#include "utils/language.h"
#include "utils/paths.h"
#include "utils/utf8.hpp"
namespace devilution {
#define PASSWORD_SPAWN_SINGLE "adslhfb1"
#define PASSWORD_SPAWN_MULTI "lshbkfg1"
#define PASSWORD_SINGLE "xrgyrkj1"
#define PASSWORD_MULTI "szqnlsk1"
bool gbValidSaveFile;
namespace {
MpqWriter SaveWriter;
MpqWriter StashWriter;
/** List of character names for the character selection screen. */
char hero_names[MAX_CHARACTERS][PLR_NAME_LEN];
std::string savePrefix;
std::string GetSavePath(uint32_t saveNum)
{
std::string path = paths::PrefPath();
const char *ext = ".sv";
if (gbIsHellfire)
ext = ".hsv";
path.append(savePrefix);
if (gbIsSpawn) {
if (!gbIsMultiplayer) {
path.append("spawn_");
} else {
path.append("share_");
}
} else {
if (!gbIsMultiplayer) {
path.append("single_");
} else {
path.append("multi_");
}
}
char saveNumStr[21];
snprintf(saveNumStr, sizeof(saveNumStr) / sizeof(char), "%i", saveNum);
path.append(saveNumStr);
path.append(ext);
return path;
}
std::string GetStashSavePath()
{
std::string path = paths::PrefPath();
const char *ext = ".sv";
if (gbIsHellfire)
ext = ".hsv";
if (gbIsSpawn) {
path.append("stash_spawn");
} else {
path.append("stash");
}
path.append(ext);
return path;
}
bool GetPermSaveNames(uint8_t dwIndex, char *szPerm)
{
const char *fmt;
if (dwIndex < giNumberOfLevels)
fmt = "perml%02d";
else if (dwIndex < giNumberOfLevels * 2) {
dwIndex -= giNumberOfLevels;
fmt = "perms%02d";
} else
return false;
sprintf(szPerm, fmt, dwIndex);
return true;
}
bool GetTempSaveNames(uint8_t dwIndex, char *szTemp)
{
const char *fmt;
if (dwIndex < giNumberOfLevels)
fmt = "templ%02d";
else if (dwIndex < giNumberOfLevels * 2) {
dwIndex -= giNumberOfLevels;
fmt = "temps%02d";
} else
return false;
sprintf(szTemp, fmt, dwIndex);
return true;
}
void RenameTempToPerm()
{
char szTemp[MAX_PATH];
char szPerm[MAX_PATH];
uint32_t dwIndex = 0;
while (GetTempSaveNames(dwIndex, szTemp)) {
[[maybe_unused]] bool result = GetPermSaveNames(dwIndex, szPerm); // DO NOT PUT DIRECTLY INTO ASSERT!
assert(result);
dwIndex++;
if (SaveWriter.HasFile(szTemp)) {
if (SaveWriter.HasFile(szPerm))
SaveWriter.RemoveHashEntry(szPerm);
SaveWriter.RenameFile(szTemp, szPerm);
}
}
assert(!GetPermSaveNames(dwIndex, szPerm));
}
bool ReadHero(MpqArchive &archive, PlayerPack *pPack)
{
size_t read;
auto buf = ReadArchive(archive, "hero", &read);
if (buf == nullptr)
return false;
bool ret = false;
if (read == sizeof(*pPack)) {
memcpy(pPack, buf.get(), sizeof(*pPack));
ret = true;
}
return ret;
}
void EncodeHero(const PlayerPack *pack)
{
size_t packedLen = codec_get_encoded_len(sizeof(*pack));
std::unique_ptr<byte[]> packed { new byte[packedLen] };
memcpy(packed.get(), pack, sizeof(*pack));
codec_encode(packed.get(), sizeof(*pack), packedLen, pfile_get_password());
SaveWriter.WriteFile("hero", packed.get(), packedLen);
}
bool OpenArchive(uint32_t saveNum)
{
return SaveWriter.Open(GetSavePath(saveNum).c_str());
}
void Game2UiPlayer(const Player &player, _uiheroinfo *heroinfo, bool bHasSaveFile)
{
CopyUtf8(heroinfo->name, player._pName, sizeof(heroinfo->name));
heroinfo->level = player._pLevel;
heroinfo->heroclass = player._pClass;
heroinfo->strength = player._pStrength;
heroinfo->magic = player._pMagic;
heroinfo->dexterity = player._pDexterity;
heroinfo->vitality = player._pVitality;
heroinfo->hassaved = bHasSaveFile;
heroinfo->herorank = player.pDiabloKillLevel;
heroinfo->spawned = gbIsSpawn;
}
bool GetFileName(uint8_t lvl, char *dst)
{
const char *fmt;
if (gbIsMultiplayer) {
if (lvl != 0)
return false;
fmt = "hero";
} else {
if (lvl < giNumberOfLevels)
fmt = "perml%02d";
else if (lvl < giNumberOfLevels * 2) {
lvl -= giNumberOfLevels;
fmt = "perms%02d";
} else if (lvl == giNumberOfLevels * 2)
fmt = "game";
else if (lvl == giNumberOfLevels * 2 + 1)
fmt = "hero";
else
return false;
}
sprintf(dst, fmt, lvl);
return true;
}
bool ArchiveContainsGame(MpqArchive &hsArchive)
{
if (gbIsMultiplayer)
return false;
auto gameData = ReadArchive(hsArchive, "game");
if (gameData == nullptr)
return false;
uint32_t hdr = LoadLE32(gameData.get());
return IsHeaderValid(hdr);
}
bool CompareSaves(const std::string &actualSavePath, const std::string &referenceSavePath)
{
std::vector<std::string> possibleFileNamesToCheck;
possibleFileNamesToCheck.emplace_back("hero");
possibleFileNamesToCheck.emplace_back("game");
possibleFileNamesToCheck.emplace_back("additionalMissiles");
char szPerm[MAX_PATH];
for (int i = 0; GetPermSaveNames(i, szPerm); i++) {
possibleFileNamesToCheck.emplace_back(szPerm);
}
std::int32_t error;
auto actualArchive = *MpqArchive::Open(actualSavePath.c_str(), error);
auto referenceArchive = *MpqArchive::Open(referenceSavePath.c_str(), error);
bool compareResult = true;
for (const auto &fileName : possibleFileNamesToCheck) {
size_t fileSizeActual;
auto fileDataActual = ReadArchive(actualArchive, fileName.c_str(), &fileSizeActual);
size_t fileSizeReference;
auto fileDataReference = ReadArchive(referenceArchive, fileName.c_str(), &fileSizeReference);
if (fileDataActual.get() == nullptr && fileDataReference.get() == nullptr) {
continue;
}
if (fileSizeActual != fileSizeReference) {
compareResult = false;
break;
}
if (memcmp(fileDataReference.get(), fileDataActual.get(), fileSizeActual) != 0) {
compareResult = false;
break;
}
}
return compareResult;
}
} // namespace
std::optional<MpqArchive> OpenSaveArchive(uint32_t saveNum)
{
std::int32_t error;
return MpqArchive::Open(GetSavePath(saveNum).c_str(), error);
}
std::optional<MpqArchive> OpenStashArchive()
{
std::int32_t error;
return MpqArchive::Open(GetStashSavePath().c_str(), error);
}
std::unique_ptr<byte[]> ReadArchive(MpqArchive &archive, const char *pszName, size_t *pdwLen)
{
int32_t error;
std::size_t length;
std::unique_ptr<byte[]> result = archive.ReadFile(pszName, length, error);
if (error != 0)
return nullptr;
std::size_t decodedLength = codec_decode(result.get(), length, pfile_get_password());
if (decodedLength == 0)
return nullptr;
if (pdwLen != nullptr)
*pdwLen = decodedLength;
return result;
}
const char *pfile_get_password()
{
if (gbIsSpawn)
return gbIsMultiplayer ? PASSWORD_SPAWN_MULTI : PASSWORD_SPAWN_SINGLE;
return gbIsMultiplayer ? PASSWORD_MULTI : PASSWORD_SINGLE;
}
PFileScopedArchiveWriter::PFileScopedArchiveWriter(bool clearTables)
: save_num_(gSaveNumber)
, clear_tables_(clearTables)
{
if (!OpenArchive(save_num_))
app_fatal(_("Failed to open player archive for writing."));
}
PFileScopedArchiveWriter::~PFileScopedArchiveWriter()
{
SaveWriter.Close(clear_tables_);
}
MpqWriter &CurrentSaveArchive()
{
return SaveWriter;
}
MpqWriter &StashArchive()
{
return StashWriter;
}
void pfile_write_hero(bool writeGameData, bool clearTables)
{
PFileScopedArchiveWriter scopedWriter(clearTables);
if (writeGameData) {
SaveGameData();
RenameTempToPerm();
}
PlayerPack pkplr;
Player &myPlayer = *MyPlayer;
PackPlayer(&pkplr, myPlayer, !gbIsMultiplayer, false);
EncodeHero(&pkplr);
if (!gbVanilla) {
SaveHotkeys();
SaveHeroItems(myPlayer);
}
}
void pfile_write_hero_demo(int demo)
{
savePrefix = fmt::format("demo_{}_reference_", demo);
pfile_write_hero(true, true);
savePrefix.clear();
}
HeroCompareResult pfile_compare_hero_demo(int demo)
{
savePrefix = fmt::format("demo_{}_reference_", demo);
std::string referenceSavePath = GetSavePath(gSaveNumber);
savePrefix.clear();
if (!FileExists(referenceSavePath.c_str()))
return HeroCompareResult::ReferenceNotFound;
savePrefix = fmt::format("demo_{}_actual_", demo);
pfile_write_hero(true, true);
std::string actualSavePath = GetSavePath(gSaveNumber);
savePrefix.clear();
bool compareResult = CompareSaves(actualSavePath, referenceSavePath);
return compareResult ? HeroCompareResult::Same : HeroCompareResult::Difference;
}
void sfile_write_stash()
{
if (!Stash.dirty)
return;
if (!StashWriter.Open(GetStashSavePath().c_str()))
app_fatal(_("Failed to open stash archive for writing."));
SaveStash();
StashWriter.Close();
Stash.dirty = false;
}
bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *))
{
memset(hero_names, 0, sizeof(hero_names));
for (uint32_t i = 0; i < MAX_CHARACTERS; i++) {
std::optional<MpqArchive> archive = OpenSaveArchive(i);
if (archive) {
PlayerPack pkplr;
if (ReadHero(*archive, &pkplr)) {
_uiheroinfo uihero;
uihero.saveNumber = i;
strcpy(hero_names[i], pkplr.pName);
bool hasSaveGame = ArchiveContainsGame(*archive);
if (hasSaveGame)
pkplr.bIsHellfire = gbIsHellfireSaveGame ? 1 : 0;
Player &player = Players[0];
player = {};
if (UnPackPlayer(&pkplr, player, false)) {
LoadHeroItems(player);
RemoveEmptyInventory(player);
CalcPlrInv(player, false);
Game2UiPlayer(player, &uihero, hasSaveGame);
uiAddHeroInfo(&uihero);
}
}
}
}
return true;
}
void pfile_ui_set_class_stats(unsigned int playerClass, _uidefaultstats *classStats)
{
classStats->strength = StrengthTbl[playerClass];
classStats->magic = MagicTbl[playerClass];
classStats->dexterity = DexterityTbl[playerClass];
classStats->vitality = VitalityTbl[playerClass];
}
uint32_t pfile_ui_get_first_unused_save_num()
{
uint32_t saveNum;
for (saveNum = 0; saveNum < MAX_CHARACTERS; saveNum++) {
if (hero_names[saveNum][0] == '\0')
break;
}
return saveNum;
}
bool pfile_ui_save_create(_uiheroinfo *heroinfo)
{
PlayerPack pkplr;
uint32_t saveNum = heroinfo->saveNumber;
if (saveNum >= MAX_CHARACTERS)
return false;
if (!OpenArchive(saveNum))
return false;
heroinfo->saveNumber = saveNum;
giNumberOfLevels = gbIsHellfire ? 25 : 17;
SaveWriter.RemoveHashEntries(GetFileName);
CopyUtf8(hero_names[saveNum], heroinfo->name, sizeof(hero_names[saveNum]));
Player &player = Players[0];
CreatePlayer(0, heroinfo->heroclass);
CopyUtf8(player._pName, heroinfo->name, PLR_NAME_LEN);
PackPlayer(&pkplr, player, true, false);
EncodeHero(&pkplr);
Game2UiPlayer(player, heroinfo, false);
if (!gbVanilla) {
SaveHotkeys();
SaveHeroItems(player);
}
SaveWriter.Close();
return true;
}
bool pfile_delete_save(_uiheroinfo *heroInfo)
{
uint32_t saveNum = heroInfo->saveNumber;
if (saveNum < MAX_CHARACTERS) {
hero_names[saveNum][0] = '\0';
RemoveFile(GetSavePath(saveNum));
}
return true;
}
void pfile_read_player_from_save(uint32_t saveNum, Player &player)
{
player = {};
PlayerPack pkplr;
{
std::optional<MpqArchive> archive = OpenSaveArchive(saveNum);
if (!archive)
app_fatal(_("Unable to open archive"));
if (!ReadHero(*archive, &pkplr))
app_fatal(_("Unable to load character"));
gbValidSaveFile = ArchiveContainsGame(*archive);
if (gbValidSaveFile)
pkplr.bIsHellfire = gbIsHellfireSaveGame ? 1 : 0;
}
if (!UnPackPlayer(&pkplr, player, false)) {
return;
}
LoadHeroItems(player);
RemoveEmptyInventory(player);
CalcPlrInv(player, false);
}
bool LevelFileExists()
{
char szName[MAX_PATH];
GetPermLevelNames(szName);
uint32_t saveNum = gSaveNumber;
if (!OpenArchive(saveNum))
app_fatal(_("Unable to read to save file archive"));
bool hasFile = SaveWriter.HasFile(szName);
SaveWriter.Close();
return hasFile;
}
void GetTempLevelNames(char *szTemp)
{
if (setlevel)
sprintf(szTemp, "temps%02d", setlvlnum);
else
sprintf(szTemp, "templ%02d", currlevel);
}
void GetPermLevelNames(char *szPerm)
{
uint32_t saveNum = gSaveNumber;
GetTempLevelNames(szPerm);
if (!OpenArchive(saveNum))
app_fatal(_("Unable to read to save file archive"));
bool hasFile = SaveWriter.HasFile(szPerm);
SaveWriter.Close();
if (!hasFile) {
if (setlevel)
sprintf(szPerm, "perms%02d", setlvlnum);
else
sprintf(szPerm, "perml%02d", currlevel);
}
}
void pfile_remove_temp_files()
{
if (gbIsMultiplayer)
return;
uint32_t saveNum = gSaveNumber;
if (!OpenArchive(saveNum))
app_fatal(_("Unable to write to save file archive"));
SaveWriter.RemoveHashEntries(GetTempSaveNames);
SaveWriter.Close();
}
void pfile_update(bool forceSave)
{
static Uint32 prevTick;
if (!gbIsMultiplayer)
return;
Uint32 tick = SDL_GetTicks();
if (!forceSave && tick - prevTick <= 60000)
return;
prevTick = tick;
pfile_write_hero();
sfile_write_stash();
}
} // namespace devilution