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.
783 lines
23 KiB
783 lines
23 KiB
/** |
|
* @file pfile.cpp |
|
* |
|
* Implementation of the save game encoding functionality. |
|
*/ |
|
#include "pfile.h" |
|
|
|
#include <cstdint> |
|
#include <string> |
|
#include <string_view> |
|
#include <unordered_map> |
|
|
|
#include <fmt/core.h> |
|
|
|
#include "codec.h" |
|
#include "engine.h" |
|
#include "engine/load_file.hpp" |
|
#include "init.h" |
|
#include "loadsave.h" |
|
#include "menu.h" |
|
#include "mpq/mpq_common.hpp" |
|
#include "pack.h" |
|
#include "playerdat.hpp" |
|
#include "qol/stash.h" |
|
#include "utils/endian.hpp" |
|
#include "utils/file_util.h" |
|
#include "utils/language.h" |
|
#include "utils/parse_int.hpp" |
|
#include "utils/paths.h" |
|
#include "utils/str_cat.hpp" |
|
#include "utils/str_split.hpp" |
|
#include "utils/utf8.hpp" |
|
|
|
#ifdef UNPACKED_SAVES |
|
#include "utils/file_util.h" |
|
#else |
|
#include "mpq/mpq_reader.hpp" |
|
#endif |
|
|
|
namespace devilution { |
|
|
|
#define PASSWORD_SPAWN_SINGLE "adslhfb1" |
|
#define PASSWORD_SPAWN_MULTI "lshbkfg1" |
|
#define PASSWORD_SINGLE "xrgyrkj1" |
|
#define PASSWORD_MULTI "szqnlsk1" |
|
|
|
bool gbValidSaveFile; |
|
|
|
namespace { |
|
|
|
/** List of character names for the character selection screen. */ |
|
char hero_names[MAX_CHARACTERS][PlayerNameLength]; |
|
|
|
std::string GetSavePath(uint32_t saveNum, std::string_view savePrefix = {}) |
|
{ |
|
return StrCat(paths::PrefPath(), savePrefix, |
|
gbIsSpawn |
|
? (gbIsMultiplayer ? "share_" : "spawn_") |
|
: (gbIsMultiplayer ? "multi_" : "single_"), |
|
saveNum, |
|
#ifdef UNPACKED_SAVES |
|
gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR |
|
#else |
|
gbIsHellfire ? ".hsv" : ".sv" |
|
#endif |
|
); |
|
} |
|
|
|
std::string GetStashSavePath() |
|
{ |
|
return StrCat(paths::PrefPath(), |
|
gbIsSpawn ? "stash_spawn" : "stash", |
|
#ifdef UNPACKED_SAVES |
|
gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR |
|
#else |
|
gbIsHellfire ? ".hsv" : ".sv" |
|
#endif |
|
); |
|
} |
|
|
|
bool GetSaveNames(uint8_t index, std::string_view prefix, char *out) |
|
{ |
|
char suf; |
|
if (index < giNumberOfLevels) |
|
suf = 'l'; |
|
else if (index < giNumberOfLevels * 2) { |
|
index -= giNumberOfLevels; |
|
suf = 's'; |
|
} else { |
|
return false; |
|
} |
|
|
|
*fmt::format_to(out, "{}{}{:02d}", prefix, suf, index) = '\0'; |
|
return true; |
|
} |
|
|
|
bool GetPermSaveNames(uint8_t dwIndex, char *szPerm) |
|
{ |
|
return GetSaveNames(dwIndex, "perm", szPerm); |
|
} |
|
|
|
bool GetTempSaveNames(uint8_t dwIndex, char *szTemp) |
|
{ |
|
return GetSaveNames(dwIndex, "temp", szTemp); |
|
} |
|
|
|
void RenameTempToPerm(SaveWriter &saveWriter) |
|
{ |
|
char szTemp[MaxMpqPathSize]; |
|
char szPerm[MaxMpqPathSize]; |
|
|
|
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(SaveReader &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(SaveWriter &saveWriter, const PlayerPack *pack) |
|
{ |
|
size_t packedLen = codec_get_encoded_len(sizeof(*pack)); |
|
std::unique_ptr<std::byte[]> packed { new std::byte[packedLen] }; |
|
|
|
memcpy(packed.get(), pack, sizeof(*pack)); |
|
codec_encode(packed.get(), sizeof(*pack), packedLen, pfile_get_password()); |
|
saveWriter.WriteFile("hero", packed.get(), packedLen); |
|
} |
|
|
|
SaveWriter GetSaveWriter(uint32_t saveNum) |
|
{ |
|
return SaveWriter(GetSavePath(saveNum)); |
|
} |
|
|
|
SaveWriter GetStashWriter() |
|
{ |
|
return SaveWriter(GetStashSavePath()); |
|
} |
|
|
|
#ifndef DISABLE_DEMOMODE |
|
void CopySaveFile(uint32_t saveNum, std::string targetPath) |
|
{ |
|
const std::string savePath = GetSavePath(saveNum); |
|
CopyFileOverwrite(savePath.c_str(), targetPath.c_str()); |
|
} |
|
#endif |
|
|
|
void Game2UiPlayer(const Player &player, _uiheroinfo *heroinfo, bool bHasSaveFile) |
|
{ |
|
CopyUtf8(heroinfo->name, player._pName, sizeof(heroinfo->name)); |
|
heroinfo->level = player.getCharacterLevel(); |
|
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) |
|
{ |
|
if (gbIsMultiplayer) { |
|
if (lvl != 0) |
|
return false; |
|
memcpy(dst, "hero", 5); |
|
return true; |
|
} |
|
if (GetPermSaveNames(lvl, dst)) { |
|
return true; |
|
} |
|
if (lvl == giNumberOfLevels * 2) { |
|
memcpy(dst, "game", 5); |
|
return true; |
|
} |
|
if (lvl == giNumberOfLevels * 2 + 1) { |
|
memcpy(dst, "hero", 5); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
bool ArchiveContainsGame(SaveReader &hsArchive) |
|
{ |
|
if (gbIsMultiplayer) |
|
return false; |
|
|
|
auto gameData = ReadArchive(hsArchive, "game"); |
|
if (gameData == nullptr) |
|
return false; |
|
|
|
uint32_t hdr = LoadLE32(gameData.get()); |
|
|
|
return IsHeaderValid(hdr); |
|
} |
|
|
|
std::optional<SaveReader> CreateSaveReader(std::string &&path) |
|
{ |
|
#ifdef UNPACKED_SAVES |
|
if (!FileExists(path)) |
|
return std::nullopt; |
|
return SaveReader(std::move(path)); |
|
#else |
|
std::int32_t error; |
|
return MpqArchive::Open(path.c_str(), error); |
|
#endif |
|
} |
|
|
|
#ifndef DISABLE_DEMOMODE |
|
struct CompareInfo { |
|
std::unique_ptr<std::byte[]> &data; |
|
size_t currentPosition; |
|
size_t size; |
|
bool isTownLevel; |
|
bool dataExists; |
|
}; |
|
|
|
struct CompareCounter { |
|
int reference; |
|
int actual; |
|
int max() |
|
{ |
|
return std::max(reference, actual); |
|
} |
|
void checkIfDataExists(int count, CompareInfo &compareInfoReference, CompareInfo &compareInfoActual) |
|
{ |
|
if (reference == count) |
|
compareInfoReference.dataExists = false; |
|
if (actual == count) |
|
compareInfoActual.dataExists = false; |
|
} |
|
}; |
|
|
|
inline bool string_ends_with(std::string_view value, std::string_view suffix) |
|
{ |
|
if (suffix.size() > value.size()) |
|
return false; |
|
return std::equal(suffix.rbegin(), suffix.rend(), value.rbegin()); |
|
} |
|
|
|
void CreateDetailDiffs(std::string_view prefix, std::string_view memoryMapFile, CompareInfo &compareInfoReference, CompareInfo &compareInfoActual, std::unordered_map<std::string, size_t> &foundDiffs) |
|
{ |
|
// Note: Detail diffs are currently only supported in unit tests |
|
std::string memoryMapFileAssetName = StrCat(paths::BasePath(), "/test/fixtures/memory_map/", memoryMapFile, ".txt"); |
|
|
|
SDL_RWops *handle = SDL_RWFromFile(memoryMapFileAssetName.c_str(), "r"); |
|
if (handle == nullptr) { |
|
app_fatal(StrCat("MemoryMapFile ", memoryMapFile, " is missing")); |
|
return; |
|
} |
|
|
|
size_t readBytes = SDL_RWsize(handle); |
|
std::unique_ptr<std::byte[]> memoryMapFileData { new std::byte[readBytes] }; |
|
SDL_RWread(handle, memoryMapFileData.get(), readBytes, 1); |
|
const std::string_view buffer(reinterpret_cast<const char *>(memoryMapFileData.get()), readBytes); |
|
|
|
std::unordered_map<std::string, CompareCounter> counter; |
|
|
|
auto getCounter = [&](const std::string &counterAsString) { |
|
auto it = counter.find(counterAsString); |
|
if (it != counter.end()) |
|
return it->second; |
|
const ParseIntResult<int> countFromMapFile = ParseInt<int>(counterAsString); |
|
if (!countFromMapFile.has_value()) |
|
app_fatal(StrCat("Failed to parse ", counterAsString, " as int")); |
|
return CompareCounter { countFromMapFile.value(), countFromMapFile.value() }; |
|
}; |
|
auto addDiff = [&](const std::string &diffKey) { |
|
auto it = foundDiffs.find(diffKey); |
|
if (it == foundDiffs.end()) { |
|
foundDiffs.insert_or_assign(diffKey, 1); |
|
} else { |
|
foundDiffs.insert_or_assign(diffKey, it->second + 1); |
|
} |
|
}; |
|
|
|
auto compareBytes = [&](size_t countBytes) { |
|
if (compareInfoReference.dataExists && compareInfoReference.currentPosition + countBytes > compareInfoReference.size) |
|
app_fatal(StrCat("Comparison failed. Not enough bytes in reference to compare. Location: ", prefix)); |
|
if (compareInfoActual.dataExists && compareInfoActual.currentPosition + countBytes > compareInfoActual.size) |
|
app_fatal(StrCat("Comparison failed. Not enough bytes in actual to compare. Location: ", prefix)); |
|
bool result = true; |
|
if (compareInfoReference.dataExists && compareInfoActual.dataExists) |
|
result = memcmp(compareInfoReference.data.get() + compareInfoReference.currentPosition, compareInfoActual.data.get() + compareInfoActual.currentPosition, countBytes) == 0; |
|
if (compareInfoReference.dataExists) |
|
compareInfoReference.currentPosition += countBytes; |
|
if (compareInfoActual.dataExists) |
|
compareInfoActual.currentPosition += countBytes; |
|
return result; |
|
}; |
|
|
|
auto read32BitInt = [&](CompareInfo &compareInfo, bool useLE) { |
|
int32_t value = 0; |
|
if (!compareInfo.dataExists) |
|
return value; |
|
if (compareInfo.currentPosition + sizeof(value) > compareInfo.size) |
|
app_fatal("read32BitInt failed. Too less bytes to read."); |
|
memcpy(&value, compareInfo.data.get() + compareInfo.currentPosition, sizeof(value)); |
|
if (useLE) |
|
value = SDL_SwapLE32(value); |
|
else |
|
value = SDL_SwapBE32(value); |
|
return value; |
|
}; |
|
|
|
for (std::string_view line : SplitByChar(buffer, '\n')) { |
|
if (!line.empty() && line.back() == '\r') |
|
line.remove_suffix(1); |
|
if (line.empty()) |
|
continue; |
|
const auto tokens = SplitByChar(line, ' '); |
|
auto it = tokens.begin(); |
|
const auto end = tokens.end(); |
|
if (it == end) |
|
continue; |
|
|
|
std::string_view command = *it; |
|
|
|
bool dataExistsReference = compareInfoReference.dataExists; |
|
bool dataExistsActual = compareInfoActual.dataExists; |
|
|
|
if (string_ends_with(command, "_HF")) { |
|
if (!gbIsHellfire) |
|
continue; |
|
command.remove_suffix(3); |
|
} |
|
if (string_ends_with(command, "_DA")) { |
|
if (gbIsHellfire) |
|
continue; |
|
command.remove_suffix(3); |
|
} |
|
if (string_ends_with(command, "_DL")) { |
|
if (compareInfoReference.isTownLevel && compareInfoActual.isTownLevel) |
|
continue; |
|
if (compareInfoReference.isTownLevel) |
|
compareInfoReference.dataExists = false; |
|
if (compareInfoActual.isTownLevel) |
|
compareInfoActual.dataExists = false; |
|
command.remove_suffix(3); |
|
} |
|
if (command == "R" || command == "LT" || command == "LC" || command == "LC_LE") { |
|
const auto bitsAsString = std::string(*++it); |
|
const auto comment = std::string(*++it); |
|
const ParseIntResult<size_t> parsedBytes = ParseInt<size_t>(bitsAsString); |
|
if (!parsedBytes.has_value()) |
|
app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); |
|
const size_t bytes = static_cast<size_t>(parsedBytes.value() / 8); |
|
|
|
if (command == "LT") { |
|
int32_t valueReference = read32BitInt(compareInfoReference, false); |
|
int32_t valueActual = read32BitInt(compareInfoActual, false); |
|
assert(sizeof(valueReference) == bytes); |
|
compareInfoReference.isTownLevel = valueReference == 0; |
|
compareInfoActual.isTownLevel = valueActual == 0; |
|
} |
|
if (command == "LC" || command == "LC_LE") { |
|
int32_t valueReference = read32BitInt(compareInfoReference, command == "LC_LE"); |
|
int32_t valueActual = read32BitInt(compareInfoActual, command == "LC_LE"); |
|
assert(sizeof(valueReference) == bytes); |
|
counter.insert_or_assign(std::string(comment), CompareCounter { valueReference, valueActual }); |
|
} |
|
|
|
if (!compareBytes(bytes)) { |
|
std::string diffKey = StrCat(prefix, ".", comment); |
|
addDiff(diffKey); |
|
} |
|
} else if (command == "M") { |
|
const auto countAsString = std::string(*++it); |
|
const auto bitsAsString = std::string(*++it); |
|
std::string_view comment = *++it; |
|
|
|
CompareCounter count = getCounter(countAsString); |
|
const ParseIntResult<size_t> parsedBytes = ParseInt<size_t>(bitsAsString); |
|
if (!parsedBytes.has_value()) |
|
app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); |
|
const size_t bytes = static_cast<size_t>(parsedBytes.value() / 8); |
|
for (int i = 0; i < count.max(); i++) { |
|
count.checkIfDataExists(i, compareInfoReference, compareInfoActual); |
|
if (!compareBytes(bytes)) { |
|
std::string diffKey = StrCat(prefix, ".", comment); |
|
addDiff(diffKey); |
|
} |
|
} |
|
} else if (command == "C") { |
|
const auto countAsString = std::string(*++it); |
|
auto subMemoryMapFile = std::string(*++it); |
|
const auto comment = std::string(*++it); |
|
|
|
CompareCounter count = getCounter(countAsString); |
|
subMemoryMapFile.erase(std::remove(subMemoryMapFile.begin(), subMemoryMapFile.end(), '\r'), subMemoryMapFile.end()); |
|
for (int i = 0; i < count.max(); i++) { |
|
count.checkIfDataExists(i, compareInfoReference, compareInfoActual); |
|
std::string subPrefix = StrCat(prefix, ".", comment); |
|
CreateDetailDiffs(subPrefix, subMemoryMapFile, compareInfoReference, compareInfoActual, foundDiffs); |
|
} |
|
} |
|
|
|
compareInfoReference.dataExists = dataExistsReference; |
|
compareInfoActual.dataExists = dataExistsActual; |
|
} |
|
} |
|
|
|
struct CompareTargets { |
|
std::string fileName; |
|
std::string memoryMapFileName; |
|
bool isTownLevel; |
|
}; |
|
|
|
HeroCompareResult CompareSaves(const std::string &actualSavePath, const std::string &referenceSavePath, bool logDetails) |
|
{ |
|
std::vector<CompareTargets> possibleFileToCheck; |
|
possibleFileToCheck.push_back({ "hero", "hero", false }); |
|
possibleFileToCheck.push_back({ "game", "game", false }); |
|
possibleFileToCheck.push_back({ "additionalMissiles", "additionalMissiles", false }); |
|
char szPerm[MaxMpqPathSize]; |
|
for (int i = 0; GetPermSaveNames(i, szPerm); i++) { |
|
possibleFileToCheck.push_back({ std::string(szPerm), "level", i == 0 }); |
|
} |
|
|
|
SaveReader actualArchive = *CreateSaveReader(std::string(actualSavePath)); |
|
SaveReader referenceArchive = *CreateSaveReader(std::string(referenceSavePath)); |
|
|
|
bool compareResult = true; |
|
std::string message; |
|
for (const auto &compareTarget : possibleFileToCheck) { |
|
size_t fileSizeActual = 0; |
|
auto fileDataActual = ReadArchive(actualArchive, compareTarget.fileName.c_str(), &fileSizeActual); |
|
size_t fileSizeReference = 0; |
|
auto fileDataReference = ReadArchive(referenceArchive, compareTarget.fileName.c_str(), &fileSizeReference); |
|
if (fileDataActual.get() == nullptr && fileDataReference.get() == nullptr) { |
|
continue; |
|
} |
|
if (fileSizeActual == fileSizeReference && memcmp(fileDataReference.get(), fileDataActual.get(), fileSizeActual) == 0) |
|
continue; |
|
compareResult = false; |
|
if (!message.empty()) |
|
message.append("\n"); |
|
if (fileSizeActual != fileSizeReference) |
|
StrAppend(message, "file \"", compareTarget.fileName, "\" is different size. Expected: ", fileSizeReference, " Actual: ", fileSizeActual); |
|
else |
|
StrAppend(message, "file \"", compareTarget.fileName, "\" has different content."); |
|
if (!logDetails) |
|
continue; |
|
std::unordered_map<std::string, size_t> foundDiffs; |
|
CompareInfo compareInfoReference = { fileDataReference, 0, fileSizeReference, compareTarget.isTownLevel, fileSizeReference != 0 }; |
|
CompareInfo compareInfoActual = { fileDataActual, 0, fileSizeActual, compareTarget.isTownLevel, fileSizeActual != 0 }; |
|
CreateDetailDiffs(compareTarget.fileName, compareTarget.memoryMapFileName, compareInfoReference, compareInfoActual, foundDiffs); |
|
if (compareInfoReference.currentPosition != fileSizeReference) |
|
app_fatal(StrCat("Comparsion failed. Uncompared bytes in reference. File: ", compareTarget.fileName)); |
|
if (compareInfoActual.currentPosition != fileSizeActual) |
|
app_fatal(StrCat("Comparsion failed. Uncompared bytes in actual. File: ", compareTarget.fileName)); |
|
for (auto entry : foundDiffs) { |
|
StrAppend(message, "\nDiff found in ", entry.first, " count: ", entry.second); |
|
} |
|
} |
|
return { compareResult ? HeroCompareResult::Same : HeroCompareResult::Difference, message }; |
|
} |
|
#endif // !DISABLE_DEMOMODE |
|
|
|
void pfile_write_hero(SaveWriter &saveWriter, bool writeGameData) |
|
{ |
|
if (writeGameData) { |
|
SaveGameData(saveWriter); |
|
RenameTempToPerm(saveWriter); |
|
} |
|
PlayerPack pkplr; |
|
Player &myPlayer = *MyPlayer; |
|
|
|
PackPlayer(pkplr, myPlayer); |
|
EncodeHero(saveWriter, &pkplr); |
|
if (!gbVanilla) { |
|
SaveHotkeys(saveWriter, myPlayer); |
|
SaveHeroItems(saveWriter, myPlayer); |
|
} |
|
} |
|
|
|
} // namespace |
|
|
|
#ifdef UNPACKED_SAVES |
|
std::unique_ptr<std::byte[]> SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) |
|
{ |
|
std::unique_ptr<std::byte[]> result; |
|
error = 0; |
|
const std::string path = dir_ + filename; |
|
uintmax_t size; |
|
if (!GetFileSize(path.c_str(), &size)) { |
|
error = 1; |
|
return nullptr; |
|
} |
|
fileSize = size; |
|
FILE *file = OpenFile(path.c_str(), "rb"); |
|
if (file == nullptr) { |
|
error = 1; |
|
return nullptr; |
|
} |
|
result.reset(new std::byte[size]); |
|
if (std::fread(result.get(), size, 1, file) != 1) { |
|
std::fclose(file); |
|
error = 1; |
|
return nullptr; |
|
} |
|
std::fclose(file); |
|
return result; |
|
} |
|
|
|
bool SaveWriter::WriteFile(const char *filename, const std::byte *data, size_t size) |
|
{ |
|
const std::string path = dir_ + filename; |
|
FILE *file = OpenFile(path.c_str(), "wb"); |
|
if (file == nullptr) { |
|
return false; |
|
} |
|
if (std::fwrite(data, size, 1, file) != 1) { |
|
std::fclose(file); |
|
return false; |
|
} |
|
std::fclose(file); |
|
return true; |
|
} |
|
|
|
void SaveWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) |
|
{ |
|
char pszFileName[MaxMpqPathSize]; |
|
|
|
for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { |
|
RemoveHashEntry(pszFileName); |
|
} |
|
} |
|
#endif |
|
|
|
std::optional<SaveReader> OpenSaveArchive(uint32_t saveNum) |
|
{ |
|
return CreateSaveReader(GetSavePath(saveNum)); |
|
} |
|
|
|
std::optional<SaveReader> OpenStashArchive() |
|
{ |
|
return CreateSaveReader(GetStashSavePath()); |
|
} |
|
|
|
std::unique_ptr<std::byte[]> ReadArchive(SaveReader &archive, const char *pszName, size_t *pdwLen) |
|
{ |
|
int32_t error; |
|
std::size_t length; |
|
|
|
std::unique_ptr<std::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; |
|
} |
|
|
|
void pfile_write_hero(bool writeGameData) |
|
{ |
|
SaveWriter saveWriter = GetSaveWriter(gSaveNumber); |
|
pfile_write_hero(saveWriter, writeGameData); |
|
} |
|
|
|
#ifndef DISABLE_DEMOMODE |
|
void pfile_write_hero_demo(int demo) |
|
{ |
|
std::string savePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); |
|
CopySaveFile(gSaveNumber, savePath); |
|
auto saveWriter = SaveWriter(savePath.c_str()); |
|
pfile_write_hero(saveWriter, true); |
|
} |
|
|
|
HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails) |
|
{ |
|
std::string referenceSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); |
|
|
|
if (!FileExists(referenceSavePath.c_str())) |
|
return { HeroCompareResult::ReferenceNotFound, {} }; |
|
|
|
std::string actualSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_actual_")); |
|
{ |
|
CopySaveFile(gSaveNumber, actualSavePath); |
|
SaveWriter saveWriter(actualSavePath.c_str()); |
|
pfile_write_hero(saveWriter, true); |
|
} |
|
|
|
return CompareSaves(actualSavePath, referenceSavePath, logDetails); |
|
} |
|
#endif |
|
|
|
void sfile_write_stash() |
|
{ |
|
if (!Stash.dirty) |
|
return; |
|
|
|
SaveWriter stashWriter = GetStashWriter(); |
|
|
|
SaveStash(stashWriter); |
|
|
|
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<SaveReader> 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]; |
|
|
|
UnPackPlayer(pkplr, player); |
|
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 = PlayersData[playerClass].baseStr; |
|
classStats->magic = PlayersData[playerClass].baseMag; |
|
classStats->dexterity = PlayersData[playerClass].baseDex; |
|
classStats->vitality = PlayersData[playerClass].baseVit; |
|
} |
|
|
|
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; |
|
heroinfo->saveNumber = saveNum; |
|
|
|
giNumberOfLevels = gbIsHellfire ? 25 : 17; |
|
|
|
SaveWriter saveWriter = GetSaveWriter(saveNum); |
|
saveWriter.RemoveHashEntries(GetFileName); |
|
CopyUtf8(hero_names[saveNum], heroinfo->name, sizeof(hero_names[saveNum])); |
|
|
|
Player &player = Players[0]; |
|
CreatePlayer(player, heroinfo->heroclass); |
|
CopyUtf8(player._pName, heroinfo->name, PlayerNameLength); |
|
PackPlayer(pkplr, player); |
|
EncodeHero(saveWriter, &pkplr); |
|
Game2UiPlayer(player, heroinfo, false); |
|
if (!gbVanilla) { |
|
SaveHotkeys(saveWriter, player); |
|
SaveHeroItems(saveWriter, player); |
|
} |
|
|
|
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).c_str()); |
|
} |
|
return true; |
|
} |
|
|
|
void pfile_read_player_from_save(uint32_t saveNum, Player &player) |
|
{ |
|
PlayerPack pkplr; |
|
{ |
|
std::optional<SaveReader> 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; |
|
} |
|
|
|
UnPackPlayer(pkplr, player); |
|
LoadHeroItems(player); |
|
RemoveEmptyInventory(player); |
|
CalcPlrInv(player, false); |
|
} |
|
|
|
void pfile_save_level() |
|
{ |
|
SaveWriter saveWriter = GetSaveWriter(gSaveNumber); |
|
SaveLevel(saveWriter); |
|
} |
|
|
|
void pfile_convert_levels() |
|
{ |
|
SaveWriter saveWriter = GetSaveWriter(gSaveNumber); |
|
ConvertLevels(saveWriter); |
|
} |
|
|
|
void pfile_remove_temp_files() |
|
{ |
|
if (gbIsMultiplayer) |
|
return; |
|
|
|
SaveWriter saveWriter = GetSaveWriter(gSaveNumber); |
|
saveWriter.RemoveHashEntries(GetTempSaveNames); |
|
} |
|
|
|
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
|
|
|