diff --git a/CMake/platforms/dreamcast.cmake b/CMake/platforms/dreamcast.cmake index 1b82a79f583..f673c22bcc1 100644 --- a/CMake/platforms/dreamcast.cmake +++ b/CMake/platforms/dreamcast.cmake @@ -15,7 +15,7 @@ set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(NOSOUND ON) set(DEVILUTIONX_STATIC_ZLIB ON) set(UNPACKED_MPQS ON) -set(UNPACKED_SAVES OFF) +set(UNPACKED_SAVES ON) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_STATIC_LUA ON) #set(DCMAKE_BUILD_TYPE Debug) diff --git a/Source/codec.cpp b/Source/codec.cpp index a58de0c6ce9..2e5ce331efb 100644 --- a/Source/codec.cpp +++ b/Source/codec.cpp @@ -90,6 +90,7 @@ void XorBlock(const uint32_t *shaResult, uint32_t *out) std::size_t codec_decode(std::byte *pbSrcDst, std::size_t size, const char *pszPassword) { + Log("codec_decode(pbSrcDst, {}, \"{}\")", size, pszPassword); uint32_t buf[BlockSize]; uint32_t dst[SHA1HashSize]; @@ -112,17 +113,19 @@ std::size_t codec_decode(std::byte *pbSrcDst, std::size_t size, const char *pszP memset(buf, 0, sizeof(buf)); const CodecSignature sig = GetCodecSignature(pbSrcDst); if (sig.error > 0) { + Log("sig.error = {}", sig.error); return 0; } SHA1Result(context, dst); if (sig.checksum != dst[0]) { - LogError("Checksum mismatch signature={} vs calculated={}", sig.checksum, dst[0]); + Log("Checksum mismatch signature={} vs calculated={}", sig.checksum, dst[0]); memset(dst, 0, sizeof(dst)); return 0; } size += sig.lastChunkSize - BlockSizeBytes; + Log("codec_decode OK"); return size; } @@ -135,6 +138,7 @@ std::size_t codec_get_encoded_len(std::size_t dwSrcBytes) void codec_encode(std::byte *pbSrcDst, std::size_t size, std::size_t size64, const char *pszPassword) { + Log("codec_encode(pbSrcDst, {}, {}, \"{}\")", size, size64, pszPassword); uint32_t buf[BlockSize]; uint32_t tmp[SHA1HashSize]; uint32_t dst[SHA1HashSize]; diff --git a/Source/diablo.cpp b/Source/diablo.cpp index f4a3657947d..92baaa92159 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -2888,6 +2888,7 @@ void DisableInputEventHandler(const SDL_Event &event, uint16_t modState) void LoadGameLevel(bool firstflag, lvl_entry lvldir) { + Log("LoadGameLevel(firstflag = {}, lvldir = {})", firstflag, static_cast(lvldir)); _music_id neededTrack = GetLevelMusic(leveltype); ClearFloatingNumbers(); diff --git a/Source/engine/assets.cpp b/Source/engine/assets.cpp index 9a65c4702e0..d795d86fd8a 100644 --- a/Source/engine/assets.cpp +++ b/Source/engine/assets.cpp @@ -29,9 +29,9 @@ char *FindUnpackedMpqFile(char *relativePath) return false; path = relativePath - unpackedDir->size(); std::memcpy(path, unpackedDir->data(), unpackedDir->size()); - Log("\t\tpath = {}", path); + //Log("\t\tpath = {}", path); if (FileExists(path)) { - Log("\t\tFOUND!"); + //Log("\t\tFOUND!"); return true; } path = nullptr; @@ -78,7 +78,7 @@ bool FindMpqFile(std::string_view filename, MpqArchive **archive, uint32_t *file #ifdef UNPACKED_MPQS AssetRef FindAsset(std::string_view filename) { - Log("UNPACKED_MPQS FindAsset {}", filename); + //Log("UNPACKED_MPQS FindAsset {}", filename); AssetRef result; if (filename.empty() || filename.back() == '\\') return result; @@ -93,7 +93,7 @@ AssetRef FindAsset(std::string_view filename) std::replace(relativePath, pathEnd, '\\', '/'); #endif // Absolute path: - Log("relativePath = {}", relativePath); + //Log("relativePath = {}", relativePath); if (relativePath[0] == '/') { if (FileExists(relativePath)) { *BufCopy(result.path, std::string_view(relativePath, filename.size())) = '\0'; @@ -102,7 +102,7 @@ AssetRef FindAsset(std::string_view filename) } // Unpacked MPQ file: - Log("Calling FindUnpackedMpqFile(\"{}\")", relativePath); + //Log("Calling FindUnpackedMpqFile(\"{}\")", relativePath); char *const unpackedMpqPath = FindUnpackedMpqFile(relativePath); if (unpackedMpqPath != nullptr) { *BufCopy(result.path, std::string_view(unpackedMpqPath, pathEnd - unpackedMpqPath)) = '\0'; diff --git a/Source/init.cpp b/Source/init.cpp index a285ba6ac73..a6b10eb88cc 100644 --- a/Source/init.cpp +++ b/Source/init.cpp @@ -87,9 +87,9 @@ std::optional FindUnpackedMpqData(const std::vector &p targetPath.clear(); targetPath.reserve(path.size() + mpqName.size() + 1); targetPath.append(path).append(mpqName) += DirectorySeparator; - Log("Testing FindUnpackedMpqData {}", targetPath); + //Log("Testing FindUnpackedMpqData {}", targetPath); if (FileExists(targetPath)) { - Log(" Found unpacked MPQ directory: {}", targetPath); + //Log(" Found unpacked MPQ directory: {}", targetPath); return targetPath; } } diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 601473e5d2b..51ed9b1f7b7 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -73,6 +73,7 @@ Cutscenes GetCutSceneFromLevelType(dungeon_type type) Cutscenes PickCutscene(interface_mode uMsg) { + Log("MyPlayer->plrlevel = {}", MyPlayer->plrlevel); switch (uMsg) { case WM_DIABLOADGAME: case WM_DIABNEWGAME: @@ -351,6 +352,7 @@ void ShowProgress(interface_mode uMsg) case WM_DIABNEXTLVL: IncProgress(); if (!gbIsMultiplayer) { + Log("pfile_save_level()"); pfile_save_level(); } else { DeltaSaveLevel(); @@ -361,12 +363,14 @@ void ShowProgress(interface_mode uMsg) currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); + Log("LoadGameLevel(false, ENTRY_MAIN)"); LoadGameLevel(false, ENTRY_MAIN); IncProgress(); break; case WM_DIABPREVLVL: IncProgress(); if (!gbIsMultiplayer) { + Log("pfile_save_level()"); pfile_save_level(); } else { DeltaSaveLevel(); @@ -377,6 +381,7 @@ void ShowProgress(interface_mode uMsg) leveltype = GetLevelType(currlevel); assert(myPlayer.isOnActiveLevel()); IncProgress(); + Log("LoadGameLevel(false, ENTRY_PREV)"); LoadGameLevel(false, ENTRY_PREV); IncProgress(); break; diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 0c0dd1533ad..3c4db181917 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -179,6 +179,8 @@ class SaveHelper { , m_buffer_(new std::byte[codec_get_encoded_len(bufferLen)]) , m_capacity_(bufferLen) { + Log("SaveHelper instantiated: this(mpqWriter, \"{}\", {})", szFileName, bufferLen); + Log("SaveHelper allocated byte array of {} bytes", codec_get_encoded_len(bufferLen)); } bool IsValid(size_t len = 1) @@ -224,9 +226,12 @@ class SaveHelper { ~SaveHelper() { + //const auto encodedLen = m_cur_; const auto encodedLen = codec_get_encoded_len(m_cur_); const char *const password = pfile_get_password(); + Log("codec_encode(m_buffer_.get(), {}, {}, \"{}\")", m_cur_, encodedLen, password); codec_encode(m_buffer_.get(), m_cur_, encodedLen, password); + Log("~SaveHelper WriteFile(\"{}\", m_buffer_.get(), {})", m_szFileName_, encodedLen); m_mpqWriter.WriteFile(m_szFileName_, m_buffer_.get(), encodedLen); } }; @@ -1832,6 +1837,7 @@ void LoadLevelSeeds() void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) { + Log("SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData)"); Player &myPlayer = *MyPlayer; DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level @@ -1841,6 +1847,7 @@ void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) char szName[MaxMpqPathSize]; GetTempLevelNames(szName); + Log("SaveHelper file(saveWriter, \"{}\", 256 * 1024)", szName); SaveHelper file(saveWriter, szName, 256 * 1024); if (leveltype != DTYPE_TOWN) { @@ -1911,11 +1918,18 @@ void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) void LoadLevel(LevelConversionData *levelConversionData) { + Log("LoadLevel"); char szName[MaxMpqPathSize]; std::optional archive = OpenSaveArchive(gSaveNumber); + Log("OpenSaveArchive({})", gSaveNumber); GetTempLevelNames(szName); - if (!archive || !archive->HasFile(szName)) + Log("szName = '{}'", szName); + Log("!archive = {}", !archive); + Log("!archive->HasFile(\"{}\") = {}", szName, !archive->HasFile(szName)); + if (!archive || !archive->HasFile(szName)) { + Log("{} not found in archive, calling GetPermLevelNames(\"{}\")", szName, szName); GetPermLevelNames(szName); + } LoadHelper file(std::move(archive), szName); if (!file.IsValid()) app_fatal(_("Unable to open save file archive")); @@ -2349,6 +2363,7 @@ void RemoveEmptyInventory(Player &player) void LoadGame(bool firstflag) { + Log("LoadGame(firstflag = {})", firstflag); FreeGameMem(); LoadHelper file(OpenSaveArchive(gSaveNumber), "game"); @@ -2369,6 +2384,7 @@ void LoadGame(bool firstflag) giNumberOfSmithPremiumItems = 6; } + Log("pfile_remove_temp_files"); pfile_remove_temp_files(); setlevel = file.NextBool8(); @@ -2398,13 +2414,16 @@ void LoadGame(bool firstflag) Player &myPlayer = *MyPlayer; + Log("LoadPlayer(file, myPlayer);"); LoadPlayer(file, myPlayer); if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL) sgGameInitInfo.nDifficulty = DIFF_NORMAL; + Log("LoadQuests {}", giNumberQuests); for (int i = 0; i < giNumberQuests; i++) LoadQuest(&file, i); + Log("LoadPortals", MAXPORTAL); for (int i = 0; i < MAXPORTAL; i++) LoadPortal(&file, i); @@ -2413,6 +2432,7 @@ void LoadGame(bool firstflag) RemoveEmptyInventory(myPlayer); } + Log("LoadGameLevel"); LoadGameLevel(firstflag, ENTRY_LOAD); SetPlrAnims(myPlayer); SyncPlrAnim(myPlayer); @@ -2429,6 +2449,7 @@ void LoadGame(bool firstflag) if (leveltype != DTYPE_TOWN) { for (unsigned &monsterId : ActiveMonsters) monsterId = file.NextBE(); + Log("LoadMonsters {}", ActiveMonsterCount); for (size_t i = 0; i < ActiveMonsterCount; i++) LoadMonster(&file, Monsters[ActiveMonsters[i]]); for (size_t i = 0; i < ActiveMonsterCount; i++) @@ -2437,6 +2458,7 @@ void LoadGame(bool firstflag) file.Skip(MaxMissilesForSaveGame); // Skip AvailableMissiles file.Skip(MaxMissilesForSaveGame); + Log("LoadMissiles {}", tmpNummissiles); for (int i = 0; i < tmpNummissiles; i++) LoadMissile(&file); // For petrified monsters, the data in missile.var1 must be used to @@ -2612,6 +2634,7 @@ void SaveStash(SaveWriter &stashWriter) } }; + Log("Saving {} pages of stash", pagesToSave.size()); // Current stash size is 100 pages. Will definitely fit in a 32 bit value. file.WriteLE(static_cast(pagesToSave.size())); for (const auto &page : pagesToSave) { @@ -2619,6 +2642,7 @@ void SaveStash(SaveWriter &stashWriter) for (const auto &row : Stash.stashGrids[page]) { for (uint16_t cell : row) { file.WriteLE(cell); + Log("\t\tSaving stash item {}", cell); } } } @@ -2626,6 +2650,7 @@ void SaveStash(SaveWriter &stashWriter) // 100 pages of 100 items is still only 10 000, as with the page count will definitely fit in 32 bits even in the worst case. file.WriteLE(static_cast(Stash.stashList.size())); for (const Item &item : Stash.stashList) { + Log("SaveItem(file, item)"); SaveItem(file, item); } @@ -2634,6 +2659,7 @@ void SaveStash(SaveWriter &stashWriter) void SaveGameData(SaveWriter &saveWriter) { + Log("SaveHelper file(saveWriter, \"game\", 320 * 1024)"); SaveHelper file(saveWriter, "game", 320 * 1024); if (gbIsSpawn && !gbIsHellfire) @@ -2799,6 +2825,7 @@ void SaveGameData(SaveWriter &saveWriter) void SaveGame() { + Log("SaveGame()"); gbValidSaveFile = true; pfile_write_hero(/*writeGameData=*/true); sfile_write_stash(); diff --git a/Source/mpq/mpq_reader.cpp b/Source/mpq/mpq_reader.cpp index 9bc3d9b4b63..c9952fadae5 100644 --- a/Source/mpq/mpq_reader.cpp +++ b/Source/mpq/mpq_reader.cpp @@ -1,149 +1,171 @@ -#include "mpq/mpq_reader.hpp" -#include "utils/log.hpp" - -#include -#include -#include - -#include - -namespace devilution { - -std::optional MpqArchive::Open(const char *path, int32_t &error) -{ - mpq_archive_s *archive; - error = libmpq__archive_open(&archive, path, -1); - Log("libmpq__archive_open {} result = {}", path, error); - if (error != 0) { - if (error == LIBMPQ_ERROR_EXIST) - error = 0; - return std::nullopt; - } - return MpqArchive { std::string(path), archive }; -} - -std::optional MpqArchive::Clone(int32_t &error) -{ - mpq_archive_s *copy; - error = libmpq__archive_dup(archive_, path_.c_str(), ©); - if (error != 0) - return std::nullopt; - return MpqArchive { path_, copy }; -} - -const char *MpqArchive::ErrorMessage(int32_t errorCode) -{ - return libmpq__strerror(errorCode); -} - -MpqArchive &MpqArchive::operator=(MpqArchive &&other) noexcept -{ - path_ = std::move(other.path_); - if (archive_ != nullptr) - libmpq__archive_close(archive_); - archive_ = other.archive_; - tmp_buf_ = std::move(other.tmp_buf_); - return *this; -} - -MpqArchive::~MpqArchive() -{ - if (archive_ != nullptr) - libmpq__archive_close(archive_); -} - -bool MpqArchive::GetFileNumber(MpqFileHash fileHash, uint32_t &fileNumber) -{ - return libmpq__file_number_from_hash(archive_, fileHash[0], fileHash[1], fileHash[2], &fileNumber) == 0; -} - -std::unique_ptr MpqArchive::ReadFile(std::string_view filename, std::size_t &fileSize, int32_t &error) -{ - std::unique_ptr result; - std::uint32_t fileNumber; - error = libmpq__file_number_s(archive_, filename.data(), filename.size(), &fileNumber); - if (error != 0) - return result; - - libmpq__off_t unpackedSize; - error = libmpq__file_size_unpacked(archive_, fileNumber, &unpackedSize); - if (error != 0) - return result; - - error = OpenBlockOffsetTable(fileNumber, filename); - if (error != 0) - return result; - - result = std::make_unique(static_cast(unpackedSize)); - - const std::size_t blockSize = GetBlockSize(fileNumber, 0, error); - if (error != 0) - return result; - - std::vector &tmp = GetTemporaryBuffer(blockSize); - if (error != 0) - return result; - - error = libmpq__file_read_with_filename_and_temporary_buffer_s( - archive_, fileNumber, filename.data(), filename.size(), reinterpret_cast(result.get()), unpackedSize, - tmp.data(), static_cast(blockSize), nullptr); - if (error != 0) { - result = nullptr; - CloseBlockOffsetTable(fileNumber); - return result; - } - CloseBlockOffsetTable(fileNumber); - - fileSize = static_cast(unpackedSize); - return result; -} - -int32_t MpqArchive::ReadBlock(uint32_t fileNumber, uint32_t blockNumber, uint8_t *out, size_t outSize) -{ - std::vector &tmpBuf = GetTemporaryBuffer(outSize); - return libmpq__block_read_with_temporary_buffer( - archive_, fileNumber, blockNumber, out, static_cast(outSize), - tmpBuf.data(), outSize, - /*transferred=*/nullptr); -} - -std::size_t MpqArchive::GetUnpackedFileSize(uint32_t fileNumber, int32_t &error) -{ - libmpq__off_t unpackedSize; - error = libmpq__file_size_unpacked(archive_, fileNumber, &unpackedSize); - return static_cast(unpackedSize); -} - -uint32_t MpqArchive::GetNumBlocks(uint32_t fileNumber, int32_t &error) -{ - uint32_t numBlocks; - error = libmpq__file_blocks(archive_, fileNumber, &numBlocks); - return numBlocks; -} - -int32_t MpqArchive::OpenBlockOffsetTable(uint32_t fileNumber, std::string_view filename) -{ - return libmpq__block_open_offset_with_filename_s(archive_, fileNumber, filename.data(), filename.size()); -} - -int32_t MpqArchive::CloseBlockOffsetTable(uint32_t fileNumber) -{ - return libmpq__block_close_offset(archive_, fileNumber); -} - -// Requires the block offset table to be open -std::size_t MpqArchive::GetBlockSize(uint32_t fileNumber, uint32_t blockNumber, int32_t &error) -{ - libmpq__off_t blockSize; - error = libmpq__block_size_unpacked(archive_, fileNumber, blockNumber, &blockSize); - return static_cast(blockSize); -} - -bool MpqArchive::HasFile(std::string_view filename) const -{ - std::uint32_t fileNumber; - int32_t error = libmpq__file_number_s(archive_, filename.data(), filename.size(), &fileNumber); - return error == 0; -} - -} // namespace devilution +#include "mpq/mpq_reader.hpp" +#include "utils/file_util.h" +#include "utils/log.hpp" + +#include +#include +#include + +#include + +namespace devilution { + +std::optional MpqArchive::Open(const char *path, int32_t &error) +{ + mpq_archive_s *archive; + error = libmpq__archive_open(&archive, path, -1); + Log("libmpq__archive_open {} result = {}", path, error); + if (error != 0) { + if (error == LIBMPQ_ERROR_EXIST) + error = 0; + return std::nullopt; + } + return MpqArchive { std::string(path), archive }; +} + +std::optional MpqArchive::Clone(int32_t &error) +{ + mpq_archive_s *copy; + error = libmpq__archive_dup(archive_, path_.c_str(), ©); + if (error != 0) + return std::nullopt; + return MpqArchive { path_, copy }; +} + +const char *MpqArchive::ErrorMessage(int32_t errorCode) +{ + return libmpq__strerror(errorCode); +} + +MpqArchive &MpqArchive::operator=(MpqArchive &&other) noexcept +{ + path_ = std::move(other.path_); + if (archive_ != nullptr) + libmpq__archive_close(archive_); + archive_ = other.archive_; + tmp_buf_ = std::move(other.tmp_buf_); + return *this; +} + +MpqArchive::~MpqArchive() +{ + if (archive_ != nullptr) + libmpq__archive_close(archive_); +} + +bool MpqArchive::GetFileNumber(MpqFileHash fileHash, uint32_t &fileNumber) +{ + return libmpq__file_number_from_hash(archive_, fileHash[0], fileHash[1], fileHash[2], &fileNumber) == 0; +} + +std::unique_ptr MpqArchive::ReadFile(std::string_view filename, std::size_t &fileSize, int32_t &error) +{ + std::unique_ptr result; + std::uint32_t fileNumber; + error = libmpq__file_number_s(archive_, filename.data(), filename.size(), &fileNumber); + + Log("path = {}", path_); + + std::uintmax_t archiveFileSize = 1337; + GetFileSize(path_.c_str(), &archiveFileSize); + Log("archiveFileSize = {}", archiveFileSize); + + Log("libmpq__file_number_s(archive_, \"{}\", {}, {}) = {};", filename.data(), filename.size(), fileNumber, error); + if (error != 0) + return result; + + Log("ReadFile 1"); + libmpq__off_t unpackedSize; + error = libmpq__file_size_unpacked(archive_, fileNumber, &unpackedSize); + if (error != 0) + return result; + + Log("ReadFile 2 unpackedSize = {}", unpackedSize); + Log("ReadFile 2 OpenBlockOffsetTable({}, {}, {})", fileNumber, filename.data(), filename.size()); + error = OpenBlockOffsetTable(fileNumber, filename); + Log("ReadFile 2 OpenBlockOffsetTable({}, {}) = {}", fileNumber, filename.data(), error); + if (error != 0) + return result; + + Log("ReadFile 3"); + result = std::make_unique(static_cast(unpackedSize)); + + const std::size_t blockSize = GetBlockSize(fileNumber, 0, error); + if (error != 0) + return result; + + Log("ReadFile 4 blockSize = {}", blockSize); + std::vector &tmp = GetTemporaryBuffer(blockSize); + if (error != 0) + return result; + + Log("ReadFile 5"); + Log("ReadFile 5 {} > {} ? {}", static_cast(unpackedSize), static_cast(blockSize), static_cast(unpackedSize) > static_cast(blockSize)); + error = libmpq__file_read_with_filename_and_temporary_buffer_s( + archive_, fileNumber, filename.data(), filename.size(), reinterpret_cast(result.get()), unpackedSize, + tmp.data(), static_cast(blockSize), nullptr); + if (error != 0) { + result = nullptr; + CloseBlockOffsetTable(fileNumber); + return result; + } + CloseBlockOffsetTable(fileNumber); + + Log("ReadFile 6"); + fileSize = static_cast(unpackedSize); + Log("fileSize = {}", fileSize); + return result; +} + +int32_t MpqArchive::ReadBlock(uint32_t fileNumber, uint32_t blockNumber, uint8_t *out, size_t outSize) +{ + std::vector &tmpBuf = GetTemporaryBuffer(outSize); + return libmpq__block_read_with_temporary_buffer( + archive_, fileNumber, blockNumber, out, static_cast(outSize), + tmpBuf.data(), outSize, + /*transferred=*/nullptr); +} + +std::size_t MpqArchive::GetUnpackedFileSize(uint32_t fileNumber, int32_t &error) +{ + libmpq__off_t unpackedSize; + error = libmpq__file_size_unpacked(archive_, fileNumber, &unpackedSize); + return static_cast(unpackedSize); +} + +uint32_t MpqArchive::GetNumBlocks(uint32_t fileNumber, int32_t &error) +{ + uint32_t numBlocks; + error = libmpq__file_blocks(archive_, fileNumber, &numBlocks); + return numBlocks; +} + +int32_t MpqArchive::OpenBlockOffsetTable(uint32_t fileNumber, std::string_view filename) +{ + auto result = libmpq__block_open_offset_with_filename_s(archive_, fileNumber, filename.data(), filename.size()); + Log("libmpq__block_open_offset_with_filename_s(archive_, {}, {}, {}) = {}", fileNumber, filename.data(), filename.size(), result); + return result; +} + +int32_t MpqArchive::CloseBlockOffsetTable(uint32_t fileNumber) +{ + return libmpq__block_close_offset(archive_, fileNumber); +} + +// Requires the block offset table to be open +std::size_t MpqArchive::GetBlockSize(uint32_t fileNumber, uint32_t blockNumber, int32_t &error) +{ + libmpq__off_t blockSize; + error = libmpq__block_size_unpacked(archive_, fileNumber, blockNumber, &blockSize); + return static_cast(blockSize); +} + +bool MpqArchive::HasFile(std::string_view filename) const +{ + std::uint32_t fileNumber; + int32_t error = libmpq__file_number_s(archive_, filename.data(), filename.size(), &fileNumber); + Log("MpqArchive::HasFile(\"{}\") = {}", filename, error); + return error == 0; +} + +} // namespace devilution diff --git a/Source/mpq/mpq_writer.cpp b/Source/mpq/mpq_writer.cpp index c6020e05ce9..e23f9b09c53 100644 --- a/Source/mpq/mpq_writer.cpp +++ b/Source/mpq/mpq_writer.cpp @@ -1,534 +1,562 @@ -#include "mpq/mpq_writer.hpp" - -#include -#include -#include -#include -#include - -#include - -#include "appfat.h" -#include "encrypt.h" -#include "engine.h" -#include "utils/endian.hpp" -#include "utils/file_util.h" -#include "utils/language.h" -#include "utils/log.hpp" -#include "utils/str_cat.hpp" - -namespace devilution { - -namespace { - -// Validates that a Type is of a particular size and that its alignment is <= the size of the type. -// Done with templates so that error messages include actual size. -template -struct AssertEq : std::true_type { - static_assert(A == B, "A == B not satisfied"); -}; -template -struct AssertLte : std::true_type { - static_assert(A <= B, "A <= B not satisfied"); -}; -template -struct CheckSize : AssertEq, AssertLte { -}; - -// Check sizes and alignments of the structs that we decrypt and encrypt. -// The decryption algorithm treats them as a stream of 32-bit uints, so the -// sizes must be exact as there cannot be any padding. -static_assert(CheckSize(4 * 4)>::value, "sizeof(MpqHashEntry) == 4 * 4 && alignof(MpqHashEntry) <= 4 * 4 not satisfied"); -static_assert(CheckSize(4 * 4)>::value, "sizeof(MpqBlockEntry) == 4 * 4 && alignof(MpqBlockEntry) <= 4 * 4 not satisfied"); - -// We use fixed size block and hash entry tables. -constexpr uint32_t HashEntriesCount = 2048; -constexpr uint32_t BlockEntriesCount = 2048; -constexpr uint32_t BlockEntrySize = HashEntriesCount * sizeof(MpqBlockEntry); -constexpr uint32_t HashEntrySize = BlockEntriesCount * sizeof(MpqHashEntry); - -// We store the block and the hash entry tables immediately after the header. -// This is unlike most other MPQ archives, that store these at the end of the file. -constexpr long MpqBlockEntryOffset = sizeof(MpqFileHeader); -constexpr long MpqHashEntryOffset = MpqBlockEntryOffset + BlockEntrySize; - -// Special return value for `GetHashIndex` and `GetHandle`. -constexpr uint32_t HashEntryNotFound = -1; - -// We use 4096-byte blocks, generally. -constexpr uint16_t BlockSizeFactor = 3; -constexpr uint32_t BlockSize = 512 << BlockSizeFactor; // 4096 - -// Sometimes we can end up with smaller blocks. -constexpr uint32_t MinBlockSize = 1024; - -void ByteSwapHdr(MpqFileHeader *hdr) -{ - hdr->signature = SDL_SwapLE32(hdr->signature); - hdr->headerSize = SDL_SwapLE32(hdr->headerSize); - hdr->fileSize = SDL_SwapLE32(hdr->fileSize); - hdr->version = SDL_SwapLE16(hdr->version); - hdr->blockSizeFactor = SDL_SwapLE16(hdr->blockSizeFactor); - hdr->hashEntriesOffset = SDL_SwapLE32(hdr->hashEntriesOffset); - hdr->blockEntriesOffset = SDL_SwapLE32(hdr->blockEntriesOffset); - hdr->hashEntriesCount = SDL_SwapLE32(hdr->hashEntriesCount); - hdr->blockEntriesCount = SDL_SwapLE32(hdr->blockEntriesCount); -} - -bool IsAllocatedUnusedBlock(const MpqBlockEntry *block) -{ - return block->offset != 0 && block->flags == 0 && block->unpackedSize == 0; -} - -bool IsUnallocatedBlock(const MpqBlockEntry *block) -{ - return block->offset == 0 && block->packedSize == 0 && block->unpackedSize == 0 && block->flags == 0; -} - -} // namespace - -MpqWriter::MpqWriter(const char *path) -{ - const std::string dir = std::string(Dirname(path)); - RecursivelyCreateDir(dir.c_str()); - Log("Opening {}", path); - std::string error; - bool exists = FileExists(path); - const char *mode = "wb"; - if (exists) { - mode = "r+b"; - std::uintmax_t fileSize; - if (!GetFileSize(path, &fileSize)) { - error = R"(GetFileSize failed: "{}")"; - Log(error, path, std::strerror(errno)); - goto on_error; - } - size_ = static_cast(fileSize); - Log("GetFileSize(\"{}\") = {}", path, size_); - } else { - } - if (!stream_.Open(path, mode)) { - stream_.Close(); - error = "Failed to open file"; - goto on_error; - } - - name_ = path; - - if (blockTable_ == nullptr || hashTable_ == nullptr) { - MpqFileHeader fhdr; - if (!exists) { - InitDefaultMpqHeader(&fhdr); - } else if (!ReadMPQHeader(&fhdr)) { - error = "Failed to read MPQ header"; - goto on_error; - } - blockTable_ = std::make_unique(BlockEntriesCount); - std::memset(blockTable_.get(), 0, BlockEntriesCount * sizeof(MpqBlockEntry)); - if (fhdr.blockEntriesCount > 0) { - if (!stream_.Read(reinterpret_cast(blockTable_.get()), static_cast(fhdr.blockEntriesCount * sizeof(MpqBlockEntry)))) { - error = "Failed to read block table"; - goto on_error; - } - libmpq__decrypt_block(reinterpret_cast(blockTable_.get()), fhdr.blockEntriesCount * sizeof(MpqBlockEntry), LIBMPQ_BLOCK_TABLE_HASH_KEY); - } - hashTable_ = std::make_unique(HashEntriesCount); - - // We fill with 0xFF so that the `block` field defaults to -1 (a null block pointer). - std::memset(hashTable_.get(), 0xFF, HashEntriesCount * sizeof(MpqHashEntry)); - - if (fhdr.hashEntriesCount > 0) { - if (!stream_.Read(reinterpret_cast(hashTable_.get()), static_cast(fhdr.hashEntriesCount * sizeof(MpqHashEntry)))) { - error = "Failed to read hash entries"; - goto on_error; - } - libmpq__decrypt_block(reinterpret_cast(hashTable_.get()), fhdr.hashEntriesCount * sizeof(MpqHashEntry), LIBMPQ_HASH_TABLE_HASH_KEY); - } - -#ifndef CAN_SEEKP_BEYOND_EOF - if (!stream_.Seekp(0, SEEK_SET)) - goto on_error; - - // Memorize stream begin, we'll need it for calculations later. - if (!stream_.Tellp(&streamBegin_)) - goto on_error; - - // Write garbage header and tables because some platforms cannot `Seekp` beyond EOF. - // The data is incorrect at this point, it will be overwritten on Close. - if (!exists) - WriteHeaderAndTables(); -#endif - } - return; -on_error: - app_fatal(StrCat(_("Failed to open archive for writing."), "\n", path, "\n", error)); -} - -MpqWriter::~MpqWriter() -{ - if (!stream_.IsOpen()) - return; - Log("Closing {}", name_); - - bool result = true; - if (!(stream_.Seekp(0, SEEK_SET) && WriteHeaderAndTables())) - result = false; - stream_.Close(); - if (result && size_ != 0) { - Log("ResizeFile(\"{}\", {})", name_, size_); - //result = true; - result = ResizeFile(name_.c_str(), size_); - } - if (!result) - Log("Closing failed {}", name_); -} - -uint32_t MpqWriter::FetchHandle(std::string_view filename) const -{ - return GetHashIndex(CalculateMpqFileHash(filename)); -} - -void MpqWriter::InitDefaultMpqHeader(MpqFileHeader *hdr) -{ - std::memset(hdr, 0, sizeof(*hdr)); - hdr->signature = MpqFileHeader::DiabloSignature; - hdr->headerSize = MpqFileHeader::DiabloSize; - hdr->blockSizeFactor = BlockSizeFactor; - hdr->version = 0; - size_ = MpqHashEntryOffset + HashEntrySize; -} - -bool MpqWriter::IsValidMpqHeader(MpqFileHeader *hdr) const -{ - return hdr->signature == MpqFileHeader::DiabloSignature - && hdr->headerSize == MpqFileHeader::DiabloSize - && hdr->version <= 0 - && hdr->blockSizeFactor == BlockSizeFactor - && hdr->fileSize == size_ - && hdr->hashEntriesOffset == MpqHashEntryOffset - && hdr->blockEntriesOffset == sizeof(MpqFileHeader) - && hdr->hashEntriesCount == HashEntriesCount - && hdr->blockEntriesCount == BlockEntriesCount; -} - -bool MpqWriter::ReadMPQHeader(MpqFileHeader *hdr) -{ - const bool hasHdr = size_ >= sizeof(*hdr); - if (hasHdr) { - if (!stream_.Read(reinterpret_cast(hdr), sizeof(*hdr))) - return false; - ByteSwapHdr(hdr); - } - if (!hasHdr || !IsValidMpqHeader(hdr)) { - InitDefaultMpqHeader(hdr); - } - return true; -} - -MpqBlockEntry *MpqWriter::NewBlock(uint32_t *blockIndex) -{ - MpqBlockEntry *blockEntry = blockTable_.get(); - - for (unsigned i = 0; i < BlockEntriesCount; ++i, ++blockEntry) { - if (!IsUnallocatedBlock(blockEntry)) - continue; - - if (blockIndex != nullptr) - *blockIndex = i; - - return blockEntry; - } - - app_fatal("Out of free block entries"); -} - -void MpqWriter::AllocBlock(uint32_t blockOffset, uint32_t blockSize) -{ - MpqBlockEntry *block; - bool expand; - do { - block = blockTable_.get(); - expand = false; - for (unsigned i = BlockEntriesCount; i-- != 0; ++block) { - // Expand to adjacent blocks. - if (!IsAllocatedUnusedBlock(block)) - continue; - if (block->offset + block->packedSize == blockOffset) { - blockOffset = block->offset; - blockSize += block->packedSize; - memset(block, 0, sizeof(MpqBlockEntry)); - expand = true; - break; - } - if (blockOffset + blockSize == block->offset) { - blockSize += block->packedSize; - memset(block, 0, sizeof(MpqBlockEntry)); - expand = true; - break; - } - } - } while (expand); - if (blockOffset + blockSize > size_) { - // Expanded beyond EOF, this should never happen. - app_fatal("MPQ free list error"); - } - if (blockOffset + blockSize == size_) { - size_ = blockOffset; - } else { - block = NewBlock(); - block->offset = blockOffset; - block->packedSize = blockSize; - block->unpackedSize = 0; - block->flags = 0; - } -} - -uint32_t MpqWriter::FindFreeBlock(uint32_t size) -{ - uint32_t result; - - MpqBlockEntry *block = blockTable_.get(); - for (unsigned i = 0; i < BlockEntriesCount; ++i, ++block) { - // Find a block entry to use space from. - if (!IsAllocatedUnusedBlock(block) || block->packedSize < size) - continue; - - result = block->offset; - block->offset += size; - block->packedSize -= size; - - // Clear the block entry if we used its entire capacity. - if (block->packedSize == 0) - memset(block, 0, sizeof(*block)); - - return result; - } - - result = size_; - size_ += size; - return result; -} - -uint32_t MpqWriter::GetHashIndex(MpqFileHash fileHash) const // NOLINT(bugprone-easily-swappable-parameters) -{ - uint32_t i = HashEntriesCount; - for (unsigned idx = fileHash[0] & 0x7FF; hashTable_[idx].block != MpqHashEntry::NullBlock; idx = (idx + 1) & 0x7FF) { - if (i-- == 0) - break; - if (hashTable_[idx].hashA != fileHash[1]) - continue; - if (hashTable_[idx].hashB != fileHash[2]) - continue; - if (hashTable_[idx].block == MpqHashEntry::DeletedBlock) - continue; - - return idx; - } - - return HashEntryNotFound; -} - -bool MpqWriter::WriteHeaderAndTables() -{ - return WriteHeader() && WriteBlockTable() && WriteHashTable(); -} - -MpqBlockEntry *MpqWriter::AddFile(std::string_view filename, MpqBlockEntry *block, uint32_t blockIndex) -{ - const MpqFileHash fileHash = CalculateMpqFileHash(filename); - if (GetHashIndex(fileHash) != HashEntryNotFound) - app_fatal(StrCat("Hash collision between \"", filename, "\" and existing file\n")); - unsigned int hIdx = fileHash[0] & 0x7FF; - - bool hasSpace = false; - for (unsigned i = 0; i < HashEntriesCount; ++i) { - if (hashTable_[hIdx].block == MpqHashEntry::NullBlock || hashTable_[hIdx].block == MpqHashEntry::DeletedBlock) { - hasSpace = true; - break; - } - hIdx = (hIdx + 1) & 0x7FF; - } - if (!hasSpace) - app_fatal("Out of hash space"); - - if (block == nullptr) - block = NewBlock(&blockIndex); - - MpqHashEntry &entry = hashTable_[hIdx]; - entry.hashA = fileHash[1]; - entry.hashB = fileHash[2]; - entry.locale = 0; - entry.platform = 0; - entry.block = blockIndex; - - return block; -} - -bool MpqWriter::WriteFileContents(const std::byte *fileData, uint32_t fileSize, MpqBlockEntry *block) -{ - const uint32_t numSectors = (fileSize + (BlockSize - 1)) / BlockSize; - const uint32_t offsetTableByteSize = sizeof(uint32_t) * (numSectors + 1); - block->offset = FindFreeBlock(fileSize + offsetTableByteSize); - // `packedSize` is reduced at the end of the function if it turns out to be smaller. - block->packedSize = fileSize + offsetTableByteSize; - block->unpackedSize = fileSize; - block->flags = MpqBlockEntry::FlagExists | MpqBlockEntry::CompressPkZip; - - // We populate the table of sector offsets while we write the data. - // We can't pre-populate it because we don't know the compressed sector sizes yet. - // First offset is the start of the first sector, last offset is the end of the last sector. - std::unique_ptr offsetTable { new uint32_t[numSectors + 1] }; - -#ifdef CAN_SEEKP_BEYOND_EOF - if (!stream_.Seekp(block->offset + offsetTableByteSize, SEEK_SET)) - return false; -#else - // Ensure we do not Seekp beyond EOF by filling the missing space. - long stream_end; - if (!stream_.Seekp(0, SEEK_END) || !stream_.Tellp(&stream_end)) - return false; - const std::uintmax_t cur_size = stream_end - streamBegin_; - if (cur_size < block->offset + offsetTableByteSize) { - if (cur_size < block->offset) { - std::unique_ptr filler { new char[block->offset - cur_size] }; - if (!stream_.Write(filler.get(), block->offset - cur_size)) - return false; - } - if (!stream_.Write(reinterpret_cast(offsetTable.get()), offsetTableByteSize)) - return false; - } else { - if (!stream_.Seekp(block->offset + offsetTableByteSize, SEEK_SET)) - return false; - } -#endif - - uint32_t destSize = offsetTableByteSize; - std::byte mpqBuf[BlockSize]; - size_t curSector = 0; - while (true) { - uint32_t len = std::min(fileSize, BlockSize); - memcpy(mpqBuf, fileData, len); - fileData += len; - len = PkwareCompress(mpqBuf, len); - if (!stream_.Write(reinterpret_cast(&mpqBuf[0]), len)) - return false; - offsetTable[curSector++] = SDL_SwapLE32(destSize); - destSize += len; // compressed length - if (fileSize <= BlockSize) - break; - - fileSize -= BlockSize; - } - - offsetTable[numSectors] = SDL_SwapLE32(destSize); - if (!stream_.Seekp(block->offset, SEEK_SET)) - return false; - if (!stream_.Write(reinterpret_cast(offsetTable.get()), offsetTableByteSize)) - return false; - if (!stream_.Seekp(destSize - offsetTableByteSize, SEEK_CUR)) - return false; - - if (destSize < block->packedSize) { - const uint32_t remainingBlockSize = block->packedSize - destSize; - if (remainingBlockSize >= MinBlockSize) { - // Allocate another block if we didn't use all of this one. - block->packedSize = destSize; - AllocBlock(block->packedSize + block->offset, remainingBlockSize); - } - } - return true; -} - -bool MpqWriter::WriteHeader() -{ - MpqFileHeader fhdr; - - memset(&fhdr, 0, sizeof(fhdr)); - fhdr.signature = MpqFileHeader::DiabloSignature; - fhdr.headerSize = MpqFileHeader::DiabloSize; - fhdr.fileSize = size_; - fhdr.version = 0; - fhdr.blockSizeFactor = BlockSizeFactor; - fhdr.hashEntriesOffset = MpqHashEntryOffset; - fhdr.blockEntriesOffset = MpqBlockEntryOffset; - fhdr.hashEntriesCount = HashEntriesCount; - fhdr.blockEntriesCount = BlockEntriesCount; - ByteSwapHdr(&fhdr); - - return stream_.Write(reinterpret_cast(&fhdr), sizeof(fhdr)); -} - -bool MpqWriter::WriteBlockTable() -{ - libmpq__encrypt_block(reinterpret_cast(blockTable_.get()), BlockEntrySize, LIBMPQ_BLOCK_TABLE_HASH_KEY); - const bool success = stream_.Write(reinterpret_cast(blockTable_.get()), BlockEntrySize); - libmpq__decrypt_block(reinterpret_cast(blockTable_.get()), BlockEntrySize, LIBMPQ_BLOCK_TABLE_HASH_KEY); - return success; -} - -bool MpqWriter::WriteHashTable() -{ - libmpq__encrypt_block(reinterpret_cast(hashTable_.get()), HashEntrySize, LIBMPQ_HASH_TABLE_HASH_KEY); - const bool success = stream_.Write(reinterpret_cast(hashTable_.get()), HashEntrySize); - libmpq__decrypt_block(reinterpret_cast(hashTable_.get()), HashEntrySize, LIBMPQ_HASH_TABLE_HASH_KEY); - return success; -} - -void MpqWriter::RemoveHashEntry(std::string_view filename) -{ - const uint32_t hIdx = FetchHandle(filename); - if (hIdx == HashEntryNotFound) { - return; - } - - MpqHashEntry *hashEntry = &hashTable_[hIdx]; - MpqBlockEntry *block = &blockTable_[hashEntry->block]; - hashEntry->block = MpqHashEntry::DeletedBlock; - const uint32_t blockOffset = block->offset; - const uint32_t blockSize = block->packedSize; - memset(block, 0, sizeof(*block)); - AllocBlock(blockOffset, blockSize); -} - -void MpqWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) -{ - char pszFileName[MaxMpqPathSize]; - - for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { - RemoveHashEntry(pszFileName); - } -} - -bool MpqWriter::WriteFile(std::string_view filename, const std::byte *data, size_t size) -{ - MpqBlockEntry *blockEntry; - - RemoveHashEntry(filename); - blockEntry = AddFile(filename, nullptr, 0); - if (!WriteFileContents(data, static_cast(size), blockEntry)) { - RemoveHashEntry(filename); - return false; - } - return true; -} - -void MpqWriter::RenameFile(std::string_view name, std::string_view newName) // NOLINT(bugprone-easily-swappable-parameters) -{ - uint32_t index = FetchHandle(name); - if (index == HashEntryNotFound) { - return; - } - - MpqHashEntry *hashEntry = &hashTable_[index]; - uint32_t block = hashEntry->block; - MpqBlockEntry *blockEntry = &blockTable_[block]; - hashEntry->block = MpqHashEntry::DeletedBlock; - AddFile(newName, blockEntry, block); -} - -bool MpqWriter::HasFile(std::string_view name) const -{ - return FetchHandle(name) != HashEntryNotFound; -} - -} // namespace devilution +#include "mpq/mpq_writer.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "appfat.h" +#include "encrypt.h" +#include "engine.h" +#include "utils/endian.hpp" +#include "utils/file_util.h" +#include "utils/language.h" +#include "utils/log.hpp" +#include "utils/str_cat.hpp" + +#ifdef __DREAMCAST__ +#include +#endif + +namespace devilution { + +namespace { + +// Validates that a Type is of a particular size and that its alignment is <= the size of the type. +// Done with templates so that error messages include actual size. +template +struct AssertEq : std::true_type { + static_assert(A == B, "A == B not satisfied"); +}; +template +struct AssertLte : std::true_type { + static_assert(A <= B, "A <= B not satisfied"); +}; +template +struct CheckSize : AssertEq, AssertLte { +}; + +// Check sizes and alignments of the structs that we decrypt and encrypt. +// The decryption algorithm treats them as a stream of 32-bit uints, so the +// sizes must be exact as there cannot be any padding. +static_assert(CheckSize(4 * 4)>::value, "sizeof(MpqHashEntry) == 4 * 4 && alignof(MpqHashEntry) <= 4 * 4 not satisfied"); +static_assert(CheckSize(4 * 4)>::value, "sizeof(MpqBlockEntry) == 4 * 4 && alignof(MpqBlockEntry) <= 4 * 4 not satisfied"); + +// We use fixed size block and hash entry tables. +constexpr uint32_t HashEntriesCount = 2048; +constexpr uint32_t BlockEntriesCount = 2048; +constexpr uint32_t BlockEntrySize = HashEntriesCount * sizeof(MpqBlockEntry); +constexpr uint32_t HashEntrySize = BlockEntriesCount * sizeof(MpqHashEntry); + +// We store the block and the hash entry tables immediately after the header. +// This is unlike most other MPQ archives, that store these at the end of the file. +constexpr long MpqBlockEntryOffset = sizeof(MpqFileHeader); +constexpr long MpqHashEntryOffset = MpqBlockEntryOffset + BlockEntrySize; + +// Special return value for `GetHashIndex` and `GetHandle`. +constexpr uint32_t HashEntryNotFound = -1; + +// We use 4096-byte blocks, generally. +constexpr uint16_t BlockSizeFactor = 3; +constexpr uint32_t BlockSize = 512 << BlockSizeFactor; // 4096 + +// Sometimes we can end up with smaller blocks. +constexpr uint32_t MinBlockSize = 1024; + +void ByteSwapHdr(MpqFileHeader *hdr) +{ + hdr->signature = SDL_SwapLE32(hdr->signature); + hdr->headerSize = SDL_SwapLE32(hdr->headerSize); + hdr->fileSize = SDL_SwapLE32(hdr->fileSize); + hdr->version = SDL_SwapLE16(hdr->version); + hdr->blockSizeFactor = SDL_SwapLE16(hdr->blockSizeFactor); + hdr->hashEntriesOffset = SDL_SwapLE32(hdr->hashEntriesOffset); + hdr->blockEntriesOffset = SDL_SwapLE32(hdr->blockEntriesOffset); + hdr->hashEntriesCount = SDL_SwapLE32(hdr->hashEntriesCount); + hdr->blockEntriesCount = SDL_SwapLE32(hdr->blockEntriesCount); +} + +bool IsAllocatedUnusedBlock(const MpqBlockEntry *block) +{ + return block->offset != 0 && block->flags == 0 && block->unpackedSize == 0; +} + +bool IsUnallocatedBlock(const MpqBlockEntry *block) +{ + return block->offset == 0 && block->packedSize == 0 && block->unpackedSize == 0 && block->flags == 0; +} + +} // namespace + +MpqWriter::MpqWriter(const char *path) +{ + const std::string dir = std::string(Dirname(path)); + RecursivelyCreateDir(dir.c_str()); + Log("Opening {}", path); + std::string error; + bool exists = FileExists(path); + const char *mode = "wb"; + if (exists) { + Log("\tPath {} already exists, not recreating it", path); + mode = "r+b"; + std::uintmax_t fileSize; + if (!GetFileSize(path, &fileSize)) { + error = R"(GetFileSize failed: "{}")"; + Log(error, path, std::strerror(errno)); + goto on_error; + } + size_ = static_cast(fileSize); + Log("GetFileSize(\"{}\") = {}", path, size_); + } else { +#ifdef __DREAMCAST__ + auto baseName = std::string(path); + baseName.erase(0, dir.length() + 1); + Log("\tAllocating 1024 kB for path {} with basename {}", path, baseName); + int ramFileSize = 1.5 * 1024 * 1024; + void* buffer = calloc(ramFileSize, sizeof(std::byte)); + memset(buffer, 0xFF, ramFileSize); + Log("\tMallocation succeeded ? {}", buffer != NULL); + int attach_result = fs_ramdisk_attach(baseName.c_str(), buffer, ramFileSize); + Log("\tAttach result: {}", attach_result); +#endif + } + if (!stream_.Open(path, mode)) { + stream_.Close(); + error = "Failed to open file"; + goto on_error; + } + + name_ = path; + + if (blockTable_ == nullptr || hashTable_ == nullptr) { + MpqFileHeader fhdr; + if (!exists) { + InitDefaultMpqHeader(&fhdr); + } else if (!ReadMPQHeader(&fhdr)) { + error = "Failed to read MPQ header"; + goto on_error; + } + blockTable_ = std::make_unique(BlockEntriesCount); + std::memset(blockTable_.get(), 0, BlockEntriesCount * sizeof(MpqBlockEntry)); + if (fhdr.blockEntriesCount > 0) { + if (!stream_.Read(reinterpret_cast(blockTable_.get()), static_cast(fhdr.blockEntriesCount * sizeof(MpqBlockEntry)))) { + error = "Failed to read block table"; + goto on_error; + } + libmpq__decrypt_block(reinterpret_cast(blockTable_.get()), fhdr.blockEntriesCount * sizeof(MpqBlockEntry), LIBMPQ_BLOCK_TABLE_HASH_KEY); + } + hashTable_ = std::make_unique(HashEntriesCount); + + // We fill with 0xFF so that the `block` field defaults to -1 (a null block pointer). + std::memset(hashTable_.get(), 0xFF, HashEntriesCount * sizeof(MpqHashEntry)); + + if (fhdr.hashEntriesCount > 0) { + if (!stream_.Read(reinterpret_cast(hashTable_.get()), static_cast(fhdr.hashEntriesCount * sizeof(MpqHashEntry)))) { + error = "Failed to read hash entries"; + goto on_error; + } + libmpq__decrypt_block(reinterpret_cast(hashTable_.get()), fhdr.hashEntriesCount * sizeof(MpqHashEntry), LIBMPQ_HASH_TABLE_HASH_KEY); + } + +#ifndef CAN_SEEKP_BEYOND_EOF + if (!stream_.Seekp(0, SEEK_SET)) + goto on_error; + + // Memorize stream begin, we'll need it for calculations later. + if (!stream_.Tellp(&streamBegin_)) + goto on_error; + + // Write garbage header and tables because some platforms cannot `Seekp` beyond EOF. + // The data is incorrect at this point, it will be overwritten on Close. + if (!exists) + WriteHeaderAndTables(); +#endif + } + return; +on_error: + app_fatal(StrCat(_("Failed to open archive for writing."), "\n", path, "\n", error)); +} + +MpqWriter::~MpqWriter() +{ + malloc_stats(); + if (!stream_.IsOpen()) + return; + Log("Closing {}", name_); + + bool result = true; + if (!(stream_.Seekp(0, SEEK_SET) && WriteHeaderAndTables())) + result = false; + stream_.Close(); + if (result && size_ != 0) { + Log("ResizeFile(\"{}\", {})", name_, size_); + result = true; + //result = ResizeFile(name_.c_str(), size_); + } + if (!result) + Log("Closing failed {}", name_); +} + +uint32_t MpqWriter::FetchHandle(std::string_view filename) const +{ + return GetHashIndex(CalculateMpqFileHash(filename)); +} + +void MpqWriter::InitDefaultMpqHeader(MpqFileHeader *hdr) +{ + std::memset(hdr, 0, sizeof(*hdr)); + hdr->signature = MpqFileHeader::DiabloSignature; + hdr->headerSize = MpqFileHeader::DiabloSize; + hdr->blockSizeFactor = BlockSizeFactor; + hdr->version = 0; + size_ = MpqHashEntryOffset + HashEntrySize; +} + +bool MpqWriter::IsValidMpqHeader(MpqFileHeader *hdr) const +{ + return hdr->signature == MpqFileHeader::DiabloSignature + && hdr->headerSize == MpqFileHeader::DiabloSize + && hdr->version <= 0 + && hdr->blockSizeFactor == BlockSizeFactor + && hdr->fileSize == size_ + && hdr->hashEntriesOffset == MpqHashEntryOffset + && hdr->blockEntriesOffset == sizeof(MpqFileHeader) + && hdr->hashEntriesCount == HashEntriesCount + && hdr->blockEntriesCount == BlockEntriesCount; +} + +bool MpqWriter::ReadMPQHeader(MpqFileHeader *hdr) +{ + const bool hasHdr = size_ >= sizeof(*hdr); + if (hasHdr) { + if (!stream_.Read(reinterpret_cast(hdr), sizeof(*hdr))) + return false; + ByteSwapHdr(hdr); + } + if (!hasHdr || !IsValidMpqHeader(hdr)) { + InitDefaultMpqHeader(hdr); + } + return true; +} + +MpqBlockEntry *MpqWriter::NewBlock(uint32_t *blockIndex) +{ + MpqBlockEntry *blockEntry = blockTable_.get(); + + for (unsigned i = 0; i < BlockEntriesCount; ++i, ++blockEntry) { + if (!IsUnallocatedBlock(blockEntry)) + continue; + + if (blockIndex != nullptr) + *blockIndex = i; + + return blockEntry; + } + + app_fatal("Out of free block entries"); +} + +void MpqWriter::AllocBlock(uint32_t blockOffset, uint32_t blockSize) +{ + MpqBlockEntry *block; + bool expand; + do { + block = blockTable_.get(); + expand = false; + for (unsigned i = BlockEntriesCount; i-- != 0; ++block) { + // Expand to adjacent blocks. + if (!IsAllocatedUnusedBlock(block)) + continue; + if (block->offset + block->packedSize == blockOffset) { + blockOffset = block->offset; + blockSize += block->packedSize; + memset(block, 0, sizeof(MpqBlockEntry)); + expand = true; + break; + } + if (blockOffset + blockSize == block->offset) { + blockSize += block->packedSize; + memset(block, 0, sizeof(MpqBlockEntry)); + expand = true; + break; + } + } + } while (expand); + if (blockOffset + blockSize > size_) { + // Expanded beyond EOF, this should never happen. + app_fatal("MPQ free list error"); + } + if (blockOffset + blockSize == size_) { + size_ = blockOffset; + } else { + block = NewBlock(); + block->offset = blockOffset; + block->packedSize = blockSize; + block->unpackedSize = 0; + block->flags = 0; + } +} + +uint32_t MpqWriter::FindFreeBlock(uint32_t size) +{ + uint32_t result; + + MpqBlockEntry *block = blockTable_.get(); + for (unsigned i = 0; i < BlockEntriesCount; ++i, ++block) { + // Find a block entry to use space from. + if (!IsAllocatedUnusedBlock(block) || block->packedSize < size) + continue; + + result = block->offset; + block->offset += size; + block->packedSize -= size; + + // Clear the block entry if we used its entire capacity. + if (block->packedSize == 0) + memset(block, 0, sizeof(*block)); + + return result; + } + + result = size_; + size_ += size; + return result; +} + +uint32_t MpqWriter::GetHashIndex(MpqFileHash fileHash) const // NOLINT(bugprone-easily-swappable-parameters) +{ + uint32_t i = HashEntriesCount; + for (unsigned idx = fileHash[0] & 0x7FF; hashTable_[idx].block != MpqHashEntry::NullBlock; idx = (idx + 1) & 0x7FF) { + if (i-- == 0) + break; + if (hashTable_[idx].hashA != fileHash[1]) + continue; + if (hashTable_[idx].hashB != fileHash[2]) + continue; + if (hashTable_[idx].block == MpqHashEntry::DeletedBlock) + continue; + + return idx; + } + + return HashEntryNotFound; +} + +bool MpqWriter::WriteHeaderAndTables() +{ + return WriteHeader() && WriteBlockTable() && WriteHashTable(); +} + +MpqBlockEntry *MpqWriter::AddFile(std::string_view filename, MpqBlockEntry *block, uint32_t blockIndex) +{ + Log("AddFile(\"{}\", block, {})", filename, blockIndex); + const MpqFileHash fileHash = CalculateMpqFileHash(filename); + if (GetHashIndex(fileHash) != HashEntryNotFound) + app_fatal(StrCat("Hash collision between \"", filename, "\" and existing file\n")); + unsigned int hIdx = fileHash[0] & 0x7FF; + + bool hasSpace = false; + for (unsigned i = 0; i < HashEntriesCount; ++i) { + if (hashTable_[hIdx].block == MpqHashEntry::NullBlock || hashTable_[hIdx].block == MpqHashEntry::DeletedBlock) { + hasSpace = true; + break; + } + hIdx = (hIdx + 1) & 0x7FF; + } + if (!hasSpace) + app_fatal("Out of hash space"); + + if (block == nullptr) + block = NewBlock(&blockIndex); + + MpqHashEntry &entry = hashTable_[hIdx]; + entry.hashA = fileHash[1]; + entry.hashB = fileHash[2]; + entry.locale = 0; + entry.platform = 0; + entry.block = blockIndex; + + Log("AddFile(\"{}\", block, {}) OK", filename, blockIndex); + return block; +} + +bool MpqWriter::WriteFileContents(const std::byte *fileData, uint32_t fileSize, MpqBlockEntry *block) +{ + const uint32_t numSectors = (fileSize + (BlockSize - 1)) / BlockSize; + const uint32_t offsetTableByteSize = sizeof(uint32_t) * (numSectors + 1); + block->offset = FindFreeBlock(fileSize + offsetTableByteSize); + // `packedSize` is reduced at the end of the function if it turns out to be smaller. + block->packedSize = fileSize + offsetTableByteSize; + block->unpackedSize = fileSize; + block->flags = MpqBlockEntry::FlagExists | MpqBlockEntry::CompressPkZip; + + // We populate the table of sector offsets while we write the data. + // We can't pre-populate it because we don't know the compressed sector sizes yet. + // First offset is the start of the first sector, last offset is the end of the last sector. + std::unique_ptr offsetTable { new uint32_t[numSectors + 1] }; + +#ifdef CAN_SEEKP_BEYOND_EOF + if (!stream_.Seekp(block->offset + offsetTableByteSize, SEEK_SET)) + return false; +#else + // Ensure we do not Seekp beyond EOF by filling the missing space. + long stream_end; + if (!stream_.Seekp(0, SEEK_END) || !stream_.Tellp(&stream_end)) + return false; + const std::uintmax_t cur_size = stream_end - streamBegin_; + if (cur_size < block->offset + offsetTableByteSize) { + if (cur_size < block->offset) { + std::unique_ptr filler { new char[block->offset - cur_size] }; + if (!stream_.Write(filler.get(), block->offset - cur_size)) + return false; + } + if (!stream_.Write(reinterpret_cast(offsetTable.get()), offsetTableByteSize)) + return false; + } else { + if (!stream_.Seekp(block->offset + offsetTableByteSize, SEEK_SET)) + return false; + } +#endif + + uint32_t destSize = offsetTableByteSize; + std::byte mpqBuf[BlockSize]; + size_t curSector = 0; + while (true) { + uint32_t len = std::min(fileSize, BlockSize); + memcpy(mpqBuf, fileData, len); + fileData += len; + len = PkwareCompress(mpqBuf, len); + if (!stream_.Write(reinterpret_cast(&mpqBuf[0]), len)) + return false; + offsetTable[curSector++] = SDL_SwapLE32(destSize); + destSize += len; // compressed length + if (fileSize <= BlockSize) + break; + + fileSize -= BlockSize; + } + + offsetTable[numSectors] = SDL_SwapLE32(destSize); + if (!stream_.Seekp(block->offset, SEEK_SET)) + return false; + if (!stream_.Write(reinterpret_cast(offsetTable.get()), offsetTableByteSize)) + return false; + if (!stream_.Seekp(destSize - offsetTableByteSize, SEEK_CUR)) + return false; + + if (destSize < block->packedSize) { + const uint32_t remainingBlockSize = block->packedSize - destSize; + if (remainingBlockSize >= MinBlockSize) { + // Allocate another block if we didn't use all of this one. + block->packedSize = destSize; + AllocBlock(block->packedSize + block->offset, remainingBlockSize); + } + } + return true; +} + +bool MpqWriter::WriteHeader() +{ + MpqFileHeader fhdr; + + memset(&fhdr, 0, sizeof(fhdr)); + fhdr.signature = MpqFileHeader::DiabloSignature; + fhdr.headerSize = MpqFileHeader::DiabloSize; + fhdr.fileSize = size_; + fhdr.version = 0; + fhdr.blockSizeFactor = BlockSizeFactor; + fhdr.hashEntriesOffset = MpqHashEntryOffset; + fhdr.blockEntriesOffset = MpqBlockEntryOffset; + fhdr.hashEntriesCount = HashEntriesCount; + fhdr.blockEntriesCount = BlockEntriesCount; + ByteSwapHdr(&fhdr); + + return stream_.Write(reinterpret_cast(&fhdr), sizeof(fhdr)); +} + +bool MpqWriter::WriteBlockTable() +{ + libmpq__encrypt_block(reinterpret_cast(blockTable_.get()), BlockEntrySize, LIBMPQ_BLOCK_TABLE_HASH_KEY); + const bool success = stream_.Write(reinterpret_cast(blockTable_.get()), BlockEntrySize); + libmpq__decrypt_block(reinterpret_cast(blockTable_.get()), BlockEntrySize, LIBMPQ_BLOCK_TABLE_HASH_KEY); + return success; +} + +bool MpqWriter::WriteHashTable() +{ + libmpq__encrypt_block(reinterpret_cast(hashTable_.get()), HashEntrySize, LIBMPQ_HASH_TABLE_HASH_KEY); + const bool success = stream_.Write(reinterpret_cast(hashTable_.get()), HashEntrySize); + libmpq__decrypt_block(reinterpret_cast(hashTable_.get()), HashEntrySize, LIBMPQ_HASH_TABLE_HASH_KEY); + return success; +} + +void MpqWriter::RemoveHashEntry(std::string_view filename) +{ + const uint32_t hIdx = FetchHandle(filename); + if (hIdx == HashEntryNotFound) { + return; + } + + MpqHashEntry *hashEntry = &hashTable_[hIdx]; + MpqBlockEntry *block = &blockTable_[hashEntry->block]; + hashEntry->block = MpqHashEntry::DeletedBlock; + const uint32_t blockOffset = block->offset; + const uint32_t blockSize = block->packedSize; + memset(block, 0, sizeof(*block)); + AllocBlock(blockOffset, blockSize); +} + +void MpqWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) +{ + Log("RemoveHashEntries"); + char pszFileName[MaxMpqPathSize]; + + for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { + Log("RemoveHashEntriey(\"{}\")", pszFileName); + RemoveHashEntry(pszFileName); + } +} + +bool MpqWriter::WriteFile(std::string_view filename, const std::byte *data, size_t size) +{ + MpqBlockEntry *blockEntry; + + RemoveHashEntry(filename); + blockEntry = AddFile(filename, nullptr, 0); + if (!WriteFileContents(data, static_cast(size), blockEntry)) { + Log("WriteFileContents(data, {}, blockEntry) = false", static_cast(size)); + RemoveHashEntry(filename); + return false; + } + Log("WriteFileContents(data, {}, blockEntry) = true", static_cast(size)); + return true; +} + +void MpqWriter::RenameFile(std::string_view name, std::string_view newName) // NOLINT(bugprone-easily-swappable-parameters) +{ + Log("RenameFile(\"{}\", \"{}\")", name, newName); + uint32_t index = FetchHandle(name); + if (index == HashEntryNotFound) { + Log("FetchNandle(\"{}\") = HashEntryNotFound", name); + return; + } + + Log("FetchNandle(\"{}\") = {}", name, index); + MpqHashEntry *hashEntry = &hashTable_[index]; + uint32_t block = hashEntry->block; + MpqBlockEntry *blockEntry = &blockTable_[block]; + hashEntry->block = MpqHashEntry::DeletedBlock; + AddFile(newName, blockEntry, block); +} + +bool MpqWriter::HasFile(std::string_view name) const +{ + Log("MpqWriter::HasFile(\"{}\") = {}", name, FetchHandle(name)); + return FetchHandle(name) != HashEntryNotFound; +} + +} // namespace devilution diff --git a/Source/mpq/mpq_writer.hpp b/Source/mpq/mpq_writer.hpp index 9d72150ed7c..93ac6ec48ce 100644 --- a/Source/mpq/mpq_writer.hpp +++ b/Source/mpq/mpq_writer.hpp @@ -63,7 +63,7 @@ class MpqWriter { // Amiga cannot Seekp beyond EOF. // See https://github.com/bebbo/libnix/issues/30 -#ifndef __AMIGA__ +#if !defined(__AMIGA__) && !defined(__DREAMCAST__) #define CAN_SEEKP_BEYOND_EOF #endif diff --git a/Source/pfile.cpp b/Source/pfile.cpp index 131fa2f7bcf..8ba352e3a89 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -1,822 +1,1016 @@ -/** - * @file pfile.cpp - * - * Implementation of the save game encoding functionality. - */ -#include "pfile.h" - -#include -#include -#include -#include - -#include - -#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/stdcompat/filesystem.hpp" -#include "utils/str_cat.hpp" -#include "utils/str_split.hpp" -#include "utils/utf8.hpp" - -#ifdef __DREAMCAST__ -#include -#endif - -#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 = {}) -{ - auto result = StrCat(paths::PrefPath(), savePrefix, - gbIsSpawn - ? (gbIsMultiplayer ? "share_" : "sp_") - : (gbIsMultiplayer ? "multi_" : "single_"), - saveNum, -#ifdef UNPACKED_SAVES - gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR -#else - gbIsHellfire ? ".hsv" : ".sv" -#endif - ); - Log("save path = {}", result); - return result; -} - -std::string GetStashSavePath() -{ - return StrCat(paths::PrefPath(), - gbIsSpawn ? "stsp" : "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 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); -#if defined(UNPACKED_SAVES) -#ifdef DVL_NO_FILESYSTEM -#error "UNPACKED_SAVES requires either DISABLE_DEMOMODE or C++17 " -#endif - CreateDir(targetPath.c_str()); - for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(savePath)) { - CopyFileOverwrite(entry.path().string().c_str(), (targetPath + entry.path().filename().string()).c_str()); - } -#else - CopyFileOverwrite(savePath.c_str(), targetPath.c_str()); -#endif -} -#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 CreateSaveReader(std::string &&path) -{ -#ifdef UNPACKED_SAVES - Log("\tAttempting to load save file {}", path); - if (!FileExists(path)) { - Log("\tFailed ):"); - return std::nullopt; - } - Log("\tFound save path {} (:", path); - 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 &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 &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 = static_cast(SDL_RWsize(handle)); - std::unique_ptr memoryMapFileData { new std::byte[readBytes] }; -#if SDL_VERSION_ATLEAST(2, 0, 0) - SDL_RWread(handle, memoryMapFileData.get(), readBytes, 1); -#else - SDL_RWread(handle, memoryMapFileData.get(), static_cast(readBytes), 1); -#endif - SDL_RWclose(handle); - - const std::string_view buffer(reinterpret_cast(memoryMapFileData.get()), readBytes); - - std::unordered_map counter; - - auto getCounter = [&](const std::string &counterAsString) { - auto it = counter.find(counterAsString); - if (it != counter.end()) - return it->second; - const ParseIntResult countFromMapFile = ParseInt(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 parsedBytes = ParseInt(bitsAsString); - if (!parsedBytes.has_value()) - app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); - const size_t bytes = static_cast(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 parsedBytes = ParseInt(bitsAsString); - if (!parsedBytes.has_value()) - app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); - const size_t bytes = static_cast(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 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 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 (const auto &[location, count] : foundDiffs) { - StrAppend(message, "\nDiff found in ", location, " count: ", count); - } - } - 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); - } -} - -void RemoveAllInvalidItems(Player &player) -{ - for (int i = 0; i < NUM_INVLOC; i++) - RemoveInvalidItem(player.InvBody[i]); - for (int i = 0; i < player._pNumInv; i++) - RemoveInvalidItem(player.InvList[i]); - for (int i = 0; i < MaxBeltItems; i++) - RemoveInvalidItem(player.SpdList[i]); - RemoveEmptyInventory(player); -} - -} // namespace - -#ifdef UNPACKED_SAVES -std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) -{ - std::unique_ptr 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 OpenSaveArchive(uint32_t saveNum) -{ - return CreateSaveReader(GetSavePath(saveNum)); -} - -std::optional OpenStashArchive() -{ - return CreateSaveReader(GetStashSavePath()); -} - -std::unique_ptr ReadArchive(SaveReader &archive, const char *pszName, size_t *pdwLen) -{ - int32_t error; - std::size_t length; - - std::unique_ptr 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 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); - RemoveAllInvalidItems(player); - CalcPlrInv(player, false); - - Game2UiPlayer(player, &uihero, hasSaveGame); - uiAddHeroInfo(&uihero); - } - } - } - - return true; -} - -void pfile_ui_set_class_stats(HeroClass playerClass, _uidefaultstats *classStats) -{ - const ClassAttributes &classAttributes = GetClassAttributes(playerClass); - classStats->strength = classAttributes.baseStr; - classStats->magic = classAttributes.baseMag; - classStats->dexterity = classAttributes.baseDex; - classStats->vitality = classAttributes.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 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); - RemoveAllInvalidItems(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 +/** + * @file pfile.cpp + * + * Implementation of the save game encoding functionality. + */ +#include "pfile.h" + +#include +#include +#include +#include + +#include + +#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/stdcompat/filesystem.hpp" +#include "utils/str_cat.hpp" +#include "utils/str_split.hpp" +#include "utils/utf8.hpp" + +#ifdef __DREAMCAST__ +#include +#include +#include +#endif + +#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; + + +void listdir(const char *dir, int depth) { + file_t d = fs_open(dir, O_RDONLY | O_DIR); + dirent_t *entry; + printf("============ RAMDISK ============\n"); + while(NULL != (entry = fs_readdir(d))) { + char absolutePath[1024]; + strcpy(absolutePath, dir); + strcat(absolutePath, "/"); + strcat(absolutePath, entry->name); + uintmax_t size = 0; + if(!GetFileSize(absolutePath, &size)) { + size = 1337; + } + bool isDir = entry->size == -1; + printf("[%s]\t%.2f kB\t%.2f kB (GetFileSize)\t%s\n", isDir ? "DIR" : "FIL", entry->size / 1024.0, size / 1024.0, entry->name); + if(isDir) { + printf("absolutePath = %s, depth = %d\n", absolutePath, depth); + listdir(absolutePath, depth + 1); + } + } + fs_close(d); + printf("============ RAMDISK ============\n\n"); +} +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 = {}) +{ + auto result = StrCat(paths::PrefPath(), savePrefix, + gbIsSpawn + ? (gbIsMultiplayer ? "share_" : "sp_") + : (gbIsMultiplayer ? "multi_" : "single_"), + saveNum, +#ifdef UNPACKED_SAVES + gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv_" //DIRECTORY_SEPARATOR_STR +#else + gbIsHellfire ? ".hsv" : ".sv" +#endif + ); + Log("save path = {}", result); + return result; +} + +std::string GetStashSavePath() +{ + return StrCat(paths::PrefPath(), + gbIsSpawn ? "stsp" : "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) +{ + Log("RenameTempToPerm"); + 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++; + Log("GetPermSaveNames({}, \"{}\")", dwIndex, szTemp); + if (saveWriter.HasFile(szTemp)) { + Log("saveWriter.HasFile(\"{}\") = true", szTemp); + if (saveWriter.HasFile(szPerm)) { + Log("saveWriter.HasFile(\"{}\") = true => RemoveHashEntry", szPerm); + saveWriter.RemoveHashEntry(szPerm); + } + Log("saveWriter.RenameFile(\"{}\", {})", szTemp, 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) { + Log("ReadArchive(archive, \"hero\", {}) = false", read); + return false; + } + + bool ret = false; + if (read == sizeof(*pPack)) { + memcpy(pPack, buf.get(), sizeof(*pPack)); + ret = true; + } + Log("{} == sizeof(*pPack) ({}) = {}", read, sizeof(*pPack), read == sizeof(*pPack)); + Log("Read player {}", pPack->pName); + Log("\tpHPBase = {}", pPack->pHPBase); + + listdir("/ram", 0); + return ret; +} + +void EncodeHero(SaveWriter &saveWriter, const PlayerPack *pack) +{ + Log("EncodeHero"); + size_t packedLen = codec_get_encoded_len(sizeof(*pack)); + //size_t packedLen = sizeof(*pack); + std::unique_ptr packed { new std::byte[packedLen] }; + + Log("memcpy(packed.get(), pack, {})", sizeof(*pack)); + memcpy(packed.get(), pack, sizeof(*pack)); + codec_encode(packed.get(), sizeof(*pack), packedLen, pfile_get_password()); + Log("Saving player {}", pack->pName); + Log("\tpHPBase = {}", pack->pHPBase); + bool result = saveWriter.WriteFile("hero", packed.get(), packedLen /* sizeof(*pack) */); + Log("saveWriter.WriteFile(\"hero\", packed.get(), {}) = {}", packedLen, result); + + if(true) + { + void *packedCopy = malloc(packedLen); + memcpy(packedCopy, packed.get(), packedLen); + PlayerPack pPackCopy; + codec_decode(packedCopy, packedLen, pfile_get_password()); + memcpy(&pPackCopy, packedCopy, sizeof(pPackCopy)); + Log("Saved player {}", pPackCopy.pName); + Log("\tpHPBase = {}", pPackCopy.pHPBase); + free(packedCopy); + } +} + +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); +#if defined(UNPACKED_SAVES) +#ifdef DVL_NO_FILESYSTEM +#error "UNPACKED_SAVES requires either DISABLE_DEMOMODE or C++17 " +#endif + CreateDir(targetPath.c_str()); + for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(savePath)) { + CopyFileOverwrite(entry.path().string().c_str(), (targetPath + entry.path().filename().string()).c_str()); + } +#else + CopyFileOverwrite(savePath.c_str(), targetPath.c_str()); +#endif +} +#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 CreateSaveReader(std::string &&path) +{ +#ifdef UNPACKED_SAVES + Log("\tAttempting to load save file {}", path); + std::string heroFile = path + "hero"; + if (!FileExists(heroFile)) { + Log("\tFailed ):"); + return std::nullopt; + } + Log("\tFound save path {} (:", path); + 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 &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 &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 = static_cast(SDL_RWsize(handle)); + std::unique_ptr memoryMapFileData { new std::byte[readBytes] }; +#if SDL_VERSION_ATLEAST(2, 0, 0) + SDL_RWread(handle, memoryMapFileData.get(), readBytes, 1); +#else + SDL_RWread(handle, memoryMapFileData.get(), static_cast(readBytes), 1); +#endif + SDL_RWclose(handle); + + const std::string_view buffer(reinterpret_cast(memoryMapFileData.get()), readBytes); + + std::unordered_map counter; + + auto getCounter = [&](const std::string &counterAsString) { + auto it = counter.find(counterAsString); + if (it != counter.end()) + return it->second; + const ParseIntResult countFromMapFile = ParseInt(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 parsedBytes = ParseInt(bitsAsString); + if (!parsedBytes.has_value()) + app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); + const size_t bytes = static_cast(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 parsedBytes = ParseInt(bitsAsString); + if (!parsedBytes.has_value()) + app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); + const size_t bytes = static_cast(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 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 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 (const auto &[location, count] : foundDiffs) { + StrAppend(message, "\nDiff found in ", location, " count: ", count); + } + } + return { compareResult ? HeroCompareResult::Same : HeroCompareResult::Difference, message }; +} +#endif // !DISABLE_DEMOMODE + +void pfile_write_hero(SaveWriter &saveWriter, bool writeGameData) +{ + Log("pfile_write_hero with writeGameData = {}", writeGameData); + if (writeGameData) { + SaveGameData(saveWriter); + RenameTempToPerm(saveWriter); + Log("Game data saved"); + } + PlayerPack pkplr; + Player &myPlayer = *MyPlayer; + + PackPlayer(pkplr, myPlayer); + Log("Player data packed"); + EncodeHero(saveWriter, &pkplr); + Log("Player data saved"); + if (!gbVanilla) { + SaveHotkeys(saveWriter, myPlayer); + SaveHeroItems(saveWriter, myPlayer); + } +} + +void RemoveAllInvalidItems(Player &player) +{ + for (int i = 0; i < NUM_INVLOC; i++) + RemoveInvalidItem(player.InvBody[i]); + for (int i = 0; i < player._pNumInv; i++) + RemoveInvalidItem(player.InvList[i]); + for (int i = 0; i < MaxBeltItems; i++) + RemoveInvalidItem(player.SpdList[i]); + RemoveEmptyInventory(player); +} + +} // namespace + +#ifdef UNPACKED_SAVES +#ifdef __DREAMCAST__ +std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) +{ + Log("SaveReader::ReadFile(\"{}\", fileSize, error)", filename); + std::unique_ptr result; + error = 0; + const std::string path = dir_ + filename; + //uintmax_t size; + Log("path = \"{}\"", path); + //if (!GetFileSize(path.c_str(), &size)) { + // Log("GetFileSize(\"{}\", &size) = false", path); + // error = 1; + // return nullptr; + //} + std::byte* contents; + ssize_t size = fs_load(path.c_str(), &contents); + Log("size = {}", size); + if(size == -1) + { + Log("SaveReader::ReadFile KO"); + return nullptr; + } + fileSize = size; + result.reset(contents); + Log("SaveReader::ReadFile OK"); + if(false && strcmp(path.c_str(), "/ram/sp_0_sv_hero") == 0) + { + PlayerPack pPack; + memcpy(&pPack, result.get(), fileSize); + Log("SaveReader::ReadFile player (result) {}", pPack.pName); + Log("\tpHPBase = {}", pPack.pHPBase); + + memcpy(&pPack, contents, fileSize); + Log("SaveReader::ReadFile player (contents) {}", pPack.pName); + Log("\tpHPBase = {}", pPack.pHPBase); + } + return result; +} + +bool SaveWriter::WriteFile(const char *filename, const std::byte *data, size_t size) +{ + Log("SaveWriter::WriteFile(\"{}\", data[], {})", filename, size); + const std::string path = dir_ + filename; + Log("dir_ = {}", dir_); + Log("path = {}", path); + const char* baseName = basename(path.c_str()); + bool exists = FileExists(baseName); + if(exists) + { + Log("{} exists, removing it", path); + void *toFree; + size_t ignore; + int detach_result = fs_ramdisk_detach(baseName, &toFree, &ignore); + free(toFree); + Log("fs_ramdisk_detach result = {}", detach_result); + if(detach_result == -1) + { + return false; + } + } + Log("\tAllocating {} bytes for path {}", size, baseName); + void* buffer = malloc(size); + memcpy(buffer, data, size); + Log("\tMallocation succeeded ? {}", buffer != NULL); + int attach_result = fs_ramdisk_attach(baseName, buffer, size); + Log("\tAttach result: {}", attach_result); + Log("Current ramdisk contents:"); + listdir("/ram", 0); + if(true && strcmp(baseName, "sp_0_sv_hero") == 0) + { + void *tmp; + size_t tmpSize = fs_load(path.c_str(), &tmp); + Log("Reread {} from file we just stored ({}) into *tmp", tmpSize, path.c_str()); + size_t decodedSize = codec_decode(tmp, tmpSize, pfile_get_password()); + Log("decodedSize = {}", decodedSize); + PlayerPack pPack; + memcpy(&pPack, tmp, sizeof(pPack)); + Log("sizeOf(pPack) = {}", sizeof(pPack)); + + Log("SaveWriter::WriteFile player (after codec_decode and fs_load) {}", pPack.pName); + Log("\tpHPBase = {}", pPack.pHPBase); + free(tmp); + } + return attach_result != -1; +} +#else +std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) +{ + Log("SaveReader::ReadFile(\"{}\", fileSize, error)", filename); + std::unique_ptr result; + error = 0; + const std::string path = dir_ + filename; + uintmax_t size; + Log("path = \"{}\"", path); + if (!GetFileSize(path.c_str(), &size)) { + Log("GetFileSize(\"{}\", &size) = false", path); + error = 1; + return nullptr; + } + fileSize = size; + Log("size = {}", size); + FILE *file = OpenFile(path.c_str(), "rb"); + if (file == nullptr) { + Log("OpenFile(\"{}\", \"rb\") = nullptr", path); + error = 1; + return nullptr; + } + result.reset(new std::byte[size]); + Log("std::fread(result.get(), {}, 1, file)", size); + if (std::fread(result.get(), size, 1, file) != 1) { + Log("std::fread(result.get(), {}, 1, file) != 1",size); + std::fclose(file); + error = 1; + return nullptr; + } + std::fclose(file); + Log("SaveReader::ReadFile OK"); + if(false && strcmp(path.c_str(), "/ram/sp_0_sv_hero") == 0) + { + PlayerPack pPack; + memcpy(&pPack, result.get(), fileSize); + Log("SaveReader::ReadFile player {}", pPack.pName); + Log("\tpHPBase = {}", pPack.pHPBase); + } + return result; +} + +bool SaveWriter::WriteFile(const char *filename, const std::byte *data, size_t size) +{ + Log("SaveWriter::WriteFile(\"{}\", data[], {})", filename, size); + const std::string path = dir_ + filename; + Log("dir_ = {}", dir_); + Log("path = {}", path); + FILE *file = OpenFile(path.c_str(), "wb"); + if (file == nullptr) { + Log("OpenFile({}, wb)", path.c_str()); + return false; + } + if (std::fwrite(data, size, 1, file) != 1) { + Log("fwrite(data, {}, 1, file) != 1", size); + std::fclose(file); + return false; + } + std::fclose(file); + Log("SaveWriter::WriteFile OK"); + return true; +} +#endif //def __DREAMCAST__ +void SaveWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) +{ + char pszFileName[MaxMpqPathSize]; + + for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { + Log("RemoveHashEntry(\"{}\")", pszFileName); + RemoveHashEntry(pszFileName); + } +} +#endif //def UNPACKED_SAVES + +std::optional OpenSaveArchive(uint32_t saveNum) +{ + return CreateSaveReader(GetSavePath(saveNum)); +} + +std::optional OpenStashArchive() +{ + return CreateSaveReader(GetStashSavePath()); +} + +std::unique_ptr ReadArchive(SaveReader &archive, const char *pszName, size_t *pdwLen) +{ + int32_t error; + std::size_t length; + + Log("ReadArchive(archive, \"{}\", {})", pszName, *pdwLen); + Log("ReadArchive 0"); + std::unique_ptr result = archive.ReadFile(pszName, length, error); + if (error != 0) { + Log("ReadArchive 0 error = {}", error); + return nullptr; + } + + Log("ReadArchive 1, length = {}", length); + //std::size_t decodedLength = length; + std::size_t decodedLength = codec_decode(result.get(), length, pfile_get_password()); + if (decodedLength == 0) { + Log("ReadArchive nullptr"); + return nullptr; + } + if(strcmp(pszName, "hero") == 0) + { + PlayerPack pPack; + memcpy(&pPack, result.get(), decodedLength); + Log("ReadArchive player {}", pPack.pName); + Log("\tpHPBase = {}", pPack.pHPBase); + } + + Log("ReadArchive 2"); + if (pdwLen != nullptr) + *pdwLen = decodedLength; + + Log("ReadArchive 3 {}", 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) +{ + Log("pfile_write_hero_demo({})", 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 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); + RemoveAllInvalidItems(player); + CalcPlrInv(player, false); + + Game2UiPlayer(player, &uihero, hasSaveGame); + uiAddHeroInfo(&uihero); + } + } + } + + return true; +} + +void pfile_ui_set_class_stats(HeroClass playerClass, _uidefaultstats *classStats) +{ + const ClassAttributes &classAttributes = GetClassAttributes(playerClass); + classStats->strength = classAttributes.baseStr; + classStats->magic = classAttributes.baseMag; + classStats->dexterity = classAttributes.baseDex; + classStats->vitality = classAttributes.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) +{ + Log("pfile_ui_save_create"); + PlayerPack pkplr; + + uint32_t saveNum = heroinfo->saveNumber; + if (saveNum >= MAX_CHARACTERS) + return false; + heroinfo->saveNumber = saveNum; + + giNumberOfLevels = gbIsHellfire ? 25 : 17; + + Log("GetSaveWriter({})", saveNum); + 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 archive = OpenSaveArchive(saveNum); + if (!archive) { + listdir("/ram", 0); + app_fatal(_("Unable to open archive")); + } + if (!ReadHero(*archive, &pkplr)) { + listdir("/ram", 0); + app_fatal(_("Unable to load character")); + } + + gbValidSaveFile = ArchiveContainsGame(*archive); + if (gbValidSaveFile) + pkplr.bIsHellfire = gbIsHellfireSaveGame ? 1 : 0; + } + + UnPackPlayer(pkplr, player); + LoadHeroItems(player); + RemoveAllInvalidItems(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() +{ + Log("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 diff --git a/Source/pfile.h b/Source/pfile.h index c025f0c9349..e96205e7c43 100644 --- a/Source/pfile.h +++ b/Source/pfile.h @@ -19,7 +19,7 @@ namespace devilution { -#define MAX_CHARACTERS 99 +#define MAX_CHARACTERS 1 //todo restore me to 99 extern bool gbValidSaveFile; @@ -28,6 +28,7 @@ struct SaveReader { explicit SaveReader(std::string &&dir) : dir_(std::move(dir)) { + Log("new SaveReader(\"{}\");", dir); } const std::string &dir() const @@ -50,6 +51,7 @@ struct SaveWriter { explicit SaveWriter(std::string &&dir) : dir_(std::move(dir)) { + Log("new SaveWriter(\"{}\");", dir); } bool WriteFile(const char *filename, const std::byte *data, size_t size); diff --git a/Source/utils/file_util.cpp b/Source/utils/file_util.cpp index 731339f438b..00c710a83f2 100644 --- a/Source/utils/file_util.cpp +++ b/Source/utils/file_util.cpp @@ -109,18 +109,18 @@ bool FileExists(const char *path) #elif defined(__DREAMCAST__) int file = fs_open(path, O_RDONLY); if(file != -1) { - Log("FileExists O_RDONLY {} = true", path); + //Log("FileExists O_RDONLY {} = true", path); fs_close(file); return true; } - Log("FileExists O_RDONLY {} = false", path); + //Log("FileExists O_RDONLY {} = false", path); file = fs_open(path, O_RDONLY | O_DIR); if(file != -1) { - Log("FileExists O_RDONLY | O_DIR {} = true", path); + //Log("FileExists O_RDONLY | O_DIR {} = true", path); fs_close(file); return true; } - Log("FileExists O_RDONLY | O_DIR {} = false", path); + //Log("FileExists O_RDONLY | O_DIR {} = false", path); return false; #elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) return ::access(path, F_OK) == 0; @@ -242,24 +242,49 @@ bool GetFileSize(const char *path, std::uintmax_t *size) #endif #elif defined(__DREAMCAST__) - file_t fh = fs_open(path, O_RDONLY); - if(fh == -1) + if(strncmp(path, "/ram", strlen("/ram")) == 0) { - fs_close(fh); - LogVerbose("GetFileSize(\"{}\") = ERROR; fh = -1", path); + file_t ramDir = fs_open("/ram", O_RDONLY | O_DIR); + dirent_t *entry; + while(NULL != (entry = fs_readdir(ramDir))) { + char absolutePath[1024]; + strcpy(absolutePath, "/ram/"); + strcat(absolutePath, entry->name); + if(strcmp(absolutePath, path) == 0) + { + Log("strcmp({}, {}) = {} == 0", absolutePath, path, strcmp(absolutePath, path)); + *size = entry->size; + fs_close(ramDir); + return true; + } + } + fs_close(ramDir); return false; } - uint64 result = fs_total64(fh); - fs_close(fh); - *size = static_cast(result); - LogVerbose("GetFileSize(\"{}\") = {} (casted to {})", path, result, *size); - return true; + else + { + file_t fh = fs_open(path, O_RDONLY); + if(fh == -1) + { + fs_close(fh); + //Log("GetFileSize(\"{}\") = ERROR; fh = -1", path); + return false; + } + size_t result = fs_total(fh); + fs_close(fh); + *size = static_cast(result); + //Log("GetFileSize(\"{}\") = {} (casted to {})", path, result, *size); + return true; + } #else struct ::stat statResult; - if (::stat(path, &statResult) == -1) + if (::stat(path, &statResult) == -1) { + Log("::stat(\"{}\", &statResult) = -1", path); return false; + } *size = static_cast(statResult.st_size); return true; + #endif } @@ -336,31 +361,15 @@ bool TruncateFile(const char *path, off_t size) { Log("TruncateFile(\"{}\", {})", path, size); void *contents; - //todo only read up to size size_t read = fs_load(path, &contents); if(read == -1) { - Log("fs_load(\"{}\", &contents) = -1", path); - return false; + return false; } - if(-1 == fs_unlink(path)) - { - Log("fs_unlink(\"{}\") = -1", path); - } + fs_unlink(path); file_t fh = fs_open(path, O_WRONLY); - if(fh == -1) - { - Log("fs_open(\"{}\", O_WRONLY) = -1", path); - return false; - } int result = fs_write(fh, contents, size); - if(result == -1) - { - Log("fs_write(fh, contents, {}) = -1", size); - return false; - } - fs_close(fh); free(contents); return result != -1; } diff --git a/Source/utils/logged_fstream.hpp b/Source/utils/logged_fstream.hpp index 80d358b0dba..7ec69f48743 100644 --- a/Source/utils/logged_fstream.hpp +++ b/Source/utils/logged_fstream.hpp @@ -69,9 +69,9 @@ struct LoggedFStream { const char *errorMessage = std::strerror(errno); if (errorMessage == nullptr) errorMessage = ""; - LogError(LogCategory::System, fmtWithError.c_str(), args..., errorMessage); + Log(fmtWithError.c_str(), args..., errorMessage); } else { - LogVerbose(LogCategory::System, fmt, args...); + Log(fmt, args...); } return ok; } diff --git a/Source/utils/sdl2_to_1_2_backports.cpp b/Source/utils/sdl2_to_1_2_backports.cpp index 648278716ab..d55439557c6 100644 --- a/Source/utils/sdl2_to_1_2_backports.cpp +++ b/Source/utils/sdl2_to_1_2_backports.cpp @@ -882,7 +882,7 @@ char *SDL_GetPrefPath(const char *org, const char *app) retval = SDL_strdup("PROGDIR:"); return retval; #elif defined(__DREAMCAST__) - retval = SDL_strdup("/vmu/a1/"); + retval = SDL_strdup("/ram/"); return retval; #endif