/** * @file loadsave.cpp * * Implementation of save game functionality. */ #include "loadsave.h" #include #include #include #include #include #include #include #include "automap.h" #include "codec.h" #include "control.h" #include "cursor.h" #include "dead.h" #include "doom.h" #include "engine.h" #include "engine/point.hpp" #include "engine/random.hpp" #include "init.h" #include "inv.h" #include "lighting.h" #include "menu.h" #include "missiles.h" #include "monster.h" #include "mpq/mpq_common.hpp" #include "pfile.h" #include "playerdat.hpp" #include "qol/stash.h" #include "stores.h" #include "utils/endian.hpp" #include "utils/language.h" namespace devilution { bool gbIsHellfireSaveGame; uint8_t giNumberOfLevels; namespace { constexpr size_t MaxMissilesForSaveGame = 125; uint8_t giNumberQuests; uint8_t giNumberOfSmithPremiumItems; template T SwapLE(T in) { switch (sizeof(T)) { case 2: return SDL_SwapLE16(in); case 4: return SDL_SwapLE32(in); case 8: return SDL_SwapLE64(in); default: return in; } } template T SwapBE(T in) { switch (sizeof(T)) { case 2: return SDL_SwapBE16(in); case 4: return SDL_SwapBE32(in); case 8: return static_cast(SDL_SwapBE64(in)); default: return in; } } class LoadHelper { std::unique_ptr m_buffer_; size_t m_cur_ = 0; size_t m_size_; template T Next() { const auto size = sizeof(T); if (!IsValid(size)) return 0; T value; memcpy(&value, &m_buffer_[m_cur_], size); m_cur_ += size; return value; } public: LoadHelper(std::optional archive, const char *szFileName) { if (archive) m_buffer_ = ReadArchive(*archive, szFileName, &m_size_); else m_buffer_ = nullptr; } bool IsValid(size_t size = 1) { return m_buffer_ != nullptr && m_size_ >= (m_cur_ + size); } template constexpr void Skip(size_t count = 1) { Skip(sizeof(T) * count); } void Skip(size_t size) { m_cur_ += size; } void NextBytes(void *bytes, size_t size) { if (!IsValid(size)) return; memcpy(bytes, &m_buffer_[m_cur_], size); m_cur_ += size; } template T NextLE() { return SwapLE(Next()); } template T NextBE() { return SwapBE(Next()); } template TDesired NextLENarrow(TSource modifier = 0) { static_assert(sizeof(TSource) > sizeof(TDesired), "Can only narrow to a smaller type"); TSource value = SwapLE(Next()) + modifier; return static_cast(clamp(value, std::numeric_limits::min(), std::numeric_limits::max())); } bool NextBool8() { return Next() != 0; } bool NextBool32() { return Next() != 0; } }; class SaveHelper { SaveWriter &m_mpqWriter; const char *m_szFileName_; std::unique_ptr m_buffer_; size_t m_cur_ = 0; size_t m_capacity_; public: SaveHelper(SaveWriter &mpqWriter, const char *szFileName, size_t bufferLen) : m_mpqWriter(mpqWriter) , m_szFileName_(szFileName) , m_buffer_(new byte[codec_get_encoded_len(bufferLen)]) , m_capacity_(bufferLen) { } bool IsValid(size_t len = 1) { return m_buffer_ != nullptr && m_capacity_ >= (m_cur_ + len); } template constexpr void Skip(size_t count = 1) { Skip(sizeof(T) * count); } void Skip(size_t len) { std::memset(&m_buffer_[m_cur_], 0, len); m_cur_ += len; } void WriteBytes(const void *bytes, size_t len) { if (!IsValid(len)) return; memcpy(&m_buffer_[m_cur_], bytes, len); m_cur_ += len; } template void WriteLE(T value) { value = SwapLE(value); WriteBytes(&value, sizeof(value)); } template void WriteBE(T value) { value = SwapBE(value); WriteBytes(&value, sizeof(value)); } ~SaveHelper() { const auto encodedLen = codec_get_encoded_len(m_cur_); const char *const password = pfile_get_password(); codec_encode(m_buffer_.get(), m_cur_, encodedLen, password); m_mpqWriter.WriteFile(m_szFileName_, m_buffer_.get(), encodedLen); } }; void LoadItemData(LoadHelper &file, Item &item) { item._iSeed = file.NextLE(); item._iCreateInfo = file.NextLE(); file.Skip(2); // Alignment item._itype = static_cast(file.NextLE()); item.position.x = file.NextLE(); item.position.y = file.NextLE(); item._iAnimFlag = file.NextBool32(); file.Skip(4); // Skip pointer _iAnimData item.AnimInfo = {}; item.AnimInfo.numberOfFrames = file.NextLENarrow(); item.AnimInfo.currentFrame = file.NextLENarrow(-1); file.Skip(8); // Skip _iAnimWidth and _iAnimWidth2 file.Skip(4); // Unused since 1.02 item._iSelFlag = file.NextLE(); file.Skip(3); // Alignment item._iPostDraw = file.NextBool32(); item._iIdentified = file.NextBool32(); item._iMagical = static_cast(file.NextLE()); file.NextBytes(item._iName, 64); file.NextBytes(item._iIName, 64); item._iLoc = static_cast(file.NextLE()); item._iClass = static_cast(file.NextLE()); file.Skip(1); // Alignment item._iCurs = file.NextLE(); item._ivalue = file.NextLE(); item._iIvalue = file.NextLE(); item._iMinDam = file.NextLE(); item._iMaxDam = file.NextLE(); item._iAC = file.NextLE(); item._iFlags = static_cast(file.NextLE()); item._iMiscId = static_cast(file.NextLE()); item._iSpell = static_cast(file.NextLE()); item._iCharges = file.NextLE(); item._iMaxCharges = file.NextLE(); item._iDurability = file.NextLE(); item._iMaxDur = file.NextLE(); item._iPLDam = file.NextLE(); item._iPLToHit = file.NextLE(); item._iPLAC = file.NextLE(); item._iPLStr = file.NextLE(); item._iPLMag = file.NextLE(); item._iPLDex = file.NextLE(); item._iPLVit = file.NextLE(); item._iPLFR = file.NextLE(); item._iPLLR = file.NextLE(); item._iPLMR = file.NextLE(); item._iPLMana = file.NextLE(); item._iPLHP = file.NextLE(); item._iPLDamMod = file.NextLE(); item._iPLGetHit = file.NextLE(); item._iPLLight = file.NextLE(); item._iSplLvlAdd = file.NextLE(); item._iRequest = file.NextBool8(); file.Skip(2); // Alignment item._iUid = file.NextLE(); item._iFMinDam = file.NextLE(); item._iFMaxDam = file.NextLE(); item._iLMinDam = file.NextLE(); item._iLMaxDam = file.NextLE(); item._iPLEnAc = file.NextLE(); item._iPrePower = static_cast(file.NextLE()); item._iSufPower = static_cast(file.NextLE()); file.Skip(2); // Alignment item._iVAdd1 = file.NextLE(); item._iVMult1 = file.NextLE(); item._iVAdd2 = file.NextLE(); item._iVMult2 = file.NextLE(); item._iMinStr = file.NextLE(); item._iMinMag = file.NextLE(); item._iMinDex = file.NextLE(); file.Skip(1); // Alignment item._iStatFlag = file.NextBool32(); item.IDidx = static_cast<_item_indexes>(file.NextLE()); if (gbIsSpawn) { item.IDidx = RemapItemIdxFromSpawn(item.IDidx); } if (!gbIsHellfireSaveGame) { item.IDidx = RemapItemIdxFromDiablo(item.IDidx); } item.dwBuff = file.NextLE(); if (gbIsHellfireSaveGame) item._iDamAcFlags = static_cast(file.NextLE()); else item._iDamAcFlags = ItemSpecialEffectHf::None; UpdateHellfireFlag(item, item._iIName); RemoveInvalidItem(item); } void LoadPlayer(LoadHelper &file, Player &player) { player._pmode = static_cast(file.NextLE()); for (int8_t &step : player.walkpath) { step = file.NextLE(); } player.plractive = file.NextBool8(); file.Skip(2); // Alignment player.destAction = static_cast(file.NextLE()); player.destParam1 = file.NextLE(); player.destParam2 = file.NextLE(); player.destParam3 = file.NextLE(); player.destParam4 = file.NextLE(); player.setLevel(file.NextLE()); player.position.tile.x = file.NextLE(); player.position.tile.y = file.NextLE(); player.position.future.x = file.NextLE(); player.position.future.y = file.NextLE(); file.Skip(2); // Skip _ptargx and _ptargy player.position.last.x = file.NextLE(); player.position.last.y = file.NextLE(); player.position.old.x = file.NextLE(); player.position.old.y = file.NextLE(); file.Skip(4); // Skip offset and velocity player._pdir = static_cast(file.NextLE()); file.Skip(4); // Unused player._pgfxnum = file.NextLENarrow(); file.Skip(); // Skip pointer pData player.AnimInfo = {}; player.AnimInfo.ticksPerFrame = file.NextLENarrow(1); player.AnimInfo.tickCounterOfCurrentFrame = file.NextLENarrow(); player.AnimInfo.numberOfFrames = file.NextLENarrow(); player.AnimInfo.currentFrame = file.NextLENarrow(-1); file.Skip(3); // Skip _pAnimWidth, _pAnimWidth2, _peflag player.lightId = file.NextLE(); file.Skip(); // _pvid player.queuedSpell.spellId = static_cast(file.NextLE()); player.queuedSpell.spellType = static_cast(file.NextLE()); auto spellFrom = file.NextLE(); if (!IsValidSpellFrom(spellFrom)) spellFrom = 0; player.spellFrom = spellFrom; player.queuedSpell.spellFrom = spellFrom; file.Skip(2); // Alignment player.inventorySpell = static_cast(file.NextLE()); file.Skip(); // Skip _pTSplType file.Skip(3); // Alignment player._pRSpell = static_cast(file.NextLE()); player._pRSplType = static_cast(file.NextLE()); file.Skip(3); // Alignment player._pSBkSpell = static_cast(file.NextLE()); file.Skip(); // Skip _pSBkSplType for (uint8_t &spellLevel : player._pSplLvl) spellLevel = file.NextLE(); file.Skip(7); // Alignment player._pMemSpells = file.NextLE(); player._pAblSpells = file.NextLE(); player._pScrlSpells = file.NextLE(); player._pSpellFlags = static_cast(file.NextLE()); file.Skip(3); // Alignment // Extra hotkeys: to keep single player save compatibility, read only 4 hotkeys here, rely on LoadHotkeys for the rest for (size_t i = 0; i < 4; i++) { player._pSplHotKey[i] = static_cast(file.NextLE()); } for (size_t i = 0; i < 4; i++) { player._pSplTHotKey[i] = static_cast(file.NextLE()); } file.Skip(); // Skip _pwtype player._pBlockFlag = file.NextBool8(); player._pInvincible = file.NextBool8(); player._pLightRad = file.NextLE(); player._pLvlChanging = file.NextBool8(); file.NextBytes(player._pName, PlayerNameLength); player._pClass = static_cast(file.NextLE()); file.Skip(3); // Alignment player._pStrength = file.NextLE(); player._pBaseStr = file.NextLE(); player._pMagic = file.NextLE(); player._pBaseMag = file.NextLE(); player._pDexterity = file.NextLE(); player._pBaseDex = file.NextLE(); player._pVitality = file.NextLE(); player._pBaseVit = file.NextLE(); player._pStatPts = file.NextLE(); player._pDamageMod = file.NextLE(); player._pBaseToBlk = file.NextLE(); if (player._pBaseToBlk == 0) player._pBaseToBlk = PlayersData[static_cast(player._pClass)].blockBonus; player._pHPBase = file.NextLE(); player._pMaxHPBase = file.NextLE(); player._pHitPoints = file.NextLE(); player._pMaxHP = file.NextLE(); file.Skip(); // Skip _pHPPer - always derived from hp and maxHP. player._pManaBase = file.NextLE(); player._pMaxManaBase = file.NextLE(); player._pMana = file.NextLE(); player._pMaxMana = file.NextLE(); file.Skip(); // Skip _pManaPer - always derived from mana and maxMana player._pLevel = file.NextLE(); player._pMaxLvl = file.NextLE(); file.Skip(2); // Alignment player._pExperience = file.NextLE(); file.Skip(); // Skip _pMaxExp - unused player._pNextExper = file.NextLE(); // This can be calculated based on pLevel (which in turn could be calculated based on pExperience) player._pArmorClass = file.NextLE(); player._pMagResist = file.NextLE(); player._pFireResist = file.NextLE(); player._pLghtResist = file.NextLE(); player._pGold = file.NextLE(); player._pInfraFlag = file.NextBool32(); int32_t tempPositionX = file.NextLE(); int32_t tempPositionY = file.NextLE(); if (player._pmode == PM_WALK_NORTHWARDS) { // These values are saved as offsets to remain consistent with old savefiles tempPositionX += player.position.tile.x; tempPositionY += player.position.tile.y; } player.position.temp.x = static_cast(tempPositionX); player.position.temp.y = static_cast(tempPositionY); player.tempDirection = static_cast(file.NextLE()); player.queuedSpell.spellLevel = file.NextLE(); file.Skip(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking file.Skip(2); // skip offset2; file.Skip(); // Skip actionFrame for (uint8_t i = 0; i < giNumberOfLevels; i++) player._pLvlVisited[i] = file.NextBool8(); for (uint8_t i = 0; i < giNumberOfLevels; i++) player._pSLvlVisited[i] = file.NextBool8(); file.Skip(2); // Alignment file.Skip(); // skip _pGFXLoad file.Skip(8); // Skip pointers _pNAnim player._pNFrames = file.NextLENarrow(); file.Skip(); // skip _pNWidth file.Skip(8); // Skip pointers _pWAnim player._pWFrames = file.NextLENarrow(); file.Skip(); // skip _pWWidth file.Skip(8); // Skip pointers _pAAnim player._pAFrames = file.NextLENarrow(); file.Skip(); // skip _pAWidth player._pAFNum = file.NextLENarrow(); file.Skip(8); // Skip pointers _pLAnim file.Skip(8); // Skip pointers _pFAnim file.Skip(8); // Skip pointers _pTAnim player._pSFrames = file.NextLENarrow(); file.Skip(); // skip _pSWidth player._pSFNum = file.NextLENarrow(); file.Skip(8); // Skip pointers _pHAnim player._pHFrames = file.NextLENarrow(); file.Skip(); // skip _pHWidth file.Skip(8); // Skip pointers _pDAnim player._pDFrames = file.NextLENarrow(); file.Skip(); // skip _pDWidth file.Skip(8); // Skip pointers _pBAnim player._pBFrames = file.NextLENarrow(); file.Skip(); // skip _pBWidth for (Item &item : player.InvBody) LoadItemData(file, item); for (Item &item : player.InvList) LoadItemData(file, item); player._pNumInv = file.NextLE(); for (int8_t &cell : player.InvGrid) cell = file.NextLE(); for (Item &item : player.SpdList) LoadItemData(file, item); LoadItemData(file, player.HoldItem); player._pIMinDam = file.NextLE(); player._pIMaxDam = file.NextLE(); player._pIAC = file.NextLE(); player._pIBonusDam = file.NextLE(); player._pIBonusToHit = file.NextLE(); player._pIBonusAC = file.NextLE(); player._pIBonusDamMod = file.NextLE(); file.Skip(4); // Alignment player._pISpells = file.NextLE(); player._pIFlags = static_cast(file.NextLE()); player._pIGetHit = file.NextLE(); player._pISplLvlAdd = file.NextLE(); file.Skip(1); // Unused file.Skip(2); // Alignment file.Skip(); // _pISplDur player._pIEnAc = file.NextLE(); player._pIFMinDam = file.NextLE(); player._pIFMaxDam = file.NextLE(); player._pILMinDam = file.NextLE(); player._pILMaxDam = file.NextLE(); player._pOilType = static_cast(file.NextLE()); player.pTownWarps = file.NextLE(); player.pDungMsgs = file.NextLE(); player.pLvlLoad = file.NextLE(); if (gbIsHellfireSaveGame) { player.pDungMsgs2 = file.NextLE(); } else { player.pDungMsgs2 = 0; file.Skip(1); // pBattleNet } player.pManaShield = file.NextBool8(); if (gbIsHellfireSaveGame) { player.pOriginalCathedral = file.NextBool8(); } else { file.Skip(1); player.pOriginalCathedral = true; } file.Skip(2); // Available bytes player.wReflections = file.NextLE(); file.Skip(14); // Available bytes player.pDiabloKillLevel = file.NextLE(); sgGameInitInfo.nDifficulty = static_cast<_difficulty>(file.NextLE()); player.pDamAcFlags = static_cast(file.NextLE()); file.Skip(20); // Available bytes CalcPlrItemVals(player, false); player.executedSpell = player.queuedSpell; // Ensures backwards compatibility // Omit pointer _pNData // Omit pointer _pWData // Omit pointer _pAData // Omit pointer _pLData // Omit pointer _pFData // Omit pointer _pTData // Omit pointer _pHData // Omit pointer _pDData // Omit pointer _pBData // Omit pointer pReserved // Ensure plrIsOnSetLevel and plrlevel is correctly initialized, cause in vanilla sometimes plrlevel is not updated to setlvlnum if (setlevel) player.setLevel(setlvlnum); else player.setLevel(currlevel); } bool gbSkipSync = false; void LoadMonster(LoadHelper *file, Monster &monster) { monster.levelType = file->NextLE(); monster.mode = static_cast(file->NextLE()); monster.goal = static_cast(file->NextLE()); file->Skip(3); // Alignment monster.goalVar1 = file->NextLENarrow(); monster.goalVar2 = file->NextLENarrow(); monster.goalVar3 = file->NextLENarrow(); file->Skip(4); // Unused monster.pathCount = file->NextLE(); file->Skip(3); // Alignment monster.position.tile.x = file->NextLE(); monster.position.tile.y = file->NextLE(); monster.position.future.x = file->NextLE(); monster.position.future.y = file->NextLE(); monster.position.old.x = file->NextLE(); monster.position.old.y = file->NextLE(); file->Skip(4); // Skip offset and velocity monster.direction = static_cast(file->NextLE()); monster.enemy = file->NextLE(); monster.enemyPosition.x = file->NextLE(); monster.enemyPosition.y = file->NextLE(); file->Skip(2); // Unused file->Skip(4); // Skip pointer _mAnimData monster.animInfo = {}; monster.animInfo.ticksPerFrame = file->NextLENarrow(); // Ensure that we can increase the tickCounterOfCurrentFrame at least once without overflow (needed for backwards compatibility for sitting gargoyles) monster.animInfo.tickCounterOfCurrentFrame = file->NextLENarrow(1) - 1; monster.animInfo.numberOfFrames = file->NextLENarrow(); monster.animInfo.currentFrame = file->NextLENarrow(-1); file->Skip(4); // Skip _meflag monster.isInvalid = file->NextBool32(); monster.var1 = file->NextLENarrow(); monster.var2 = file->NextLENarrow(); monster.var3 = file->NextLENarrow(); monster.position.temp.x = file->NextLENarrow(); monster.position.temp.y = file->NextLENarrow(); file->Skip(2); // skip offset2; file->Skip(4); // Skip actionFrame monster.maxHitPoints = file->NextLE(); monster.hitPoints = file->NextLE(); monster.ai = static_cast(file->NextLE()); monster.intelligence = file->NextLE(); file->Skip(2); // Alignment monster.flags = file->NextLE(); monster.activeForTicks = file->NextLE(); file->Skip(3); // Alignment file->Skip(4); // Unused monster.position.last.x = file->NextLE(); monster.position.last.y = file->NextLE(); monster.rndItemSeed = file->NextLE(); monster.aiSeed = file->NextLE(); file->Skip(4); // Unused monster.uniqueType = static_cast(file->NextLE() - 1); monster.uniqTrans = file->NextLE(); monster.corpseId = file->NextLE(); monster.whoHit = file->NextLE(); file->Skip(1); // Skip level - now calculated on the fly file->Skip(1); // Alignment file->Skip(2); // Skip exp - now calculated from monstdat when the monster dies if (monster.isPlayerMinion()) // Don't skip for golems monster.toHit = file->NextLE(); else file->Skip(1); // Skip hit as it's already initialized monster.minDamage = file->NextLE(); monster.maxDamage = file->NextLE(); file->Skip(1); // Skip toHitSpecial as it's already initialized monster.minDamageSpecial = file->NextLE(); monster.maxDamageSpecial = file->NextLE(); monster.armorClass = file->NextLE(); file->Skip(1); // Alignment monster.resistance = file->NextLE(); file->Skip(2); // Alignment monster.talkMsg = static_cast<_speech_id>(file->NextLE()); if (monster.talkMsg == TEXT_KING1) // Fix original bad mapping of NONE for monsters monster.talkMsg = TEXT_NONE; monster.leader = file->NextLE(); if (monster.leader == 0) monster.leader = Monster::NoLeader; // Golems shouldn't be leaders of other monsters monster.leaderRelation = static_cast(file->NextLE()); monster.packSize = file->NextLE(); monster.lightId = file->NextLE(); if (monster.lightId == 0) monster.lightId = NO_LIGHT; // Correct incorect values in old saves // Omit pointer name; if (monster.mode == MonsterMode::Petrified) monster.animInfo.isPetrified = true; } /** * @brief Recalculate the pack size of monster group that may have underflown */ void SyncPackSize(Monster &leader) { if (!leader.isUnique()) return; if (leader.ai != MonsterAIID::Scavenger) return; leader.packSize = 0; for (size_t i = 0; i < ActiveMonsterCount; i++) { auto &minion = Monsters[ActiveMonsters[i]]; if (minion.leaderRelation == LeaderRelation::Leashed && minion.getLeader() == &leader) leader.packSize++; } } void LoadMissile(LoadHelper *file) { Missile missile = {}; missile._mitype = static_cast(file->NextLE()); missile.position.tile.x = file->NextLE(); missile.position.tile.y = file->NextLE(); missile.position.offset.deltaX = file->NextLE(); missile.position.offset.deltaY = file->NextLE(); missile.position.velocity.deltaX = file->NextLE(); missile.position.velocity.deltaY = file->NextLE(); missile.position.start.x = file->NextLE(); missile.position.start.y = file->NextLE(); missile.position.traveled.deltaX = file->NextLE(); missile.position.traveled.deltaY = file->NextLE(); missile._mimfnum = file->NextLE(); missile._mispllvl = file->NextLE(); missile._miDelFlag = file->NextBool32(); missile._miAnimType = static_cast(file->NextLE()); file->Skip(3); // Alignment missile._miAnimFlags = static_cast(file->NextLE()); file->Skip(4); // Skip pointer _miAnimData missile._miAnimDelay = file->NextLE(); missile._miAnimLen = file->NextLE(); missile._miAnimWidth = file->NextLE(); missile._miAnimWidth2 = file->NextLE(); missile._miAnimCnt = file->NextLE(); missile._miAnimAdd = file->NextLE(); missile._miAnimFrame = file->NextLE(); missile._miDrawFlag = file->NextBool32(); missile._miLightFlag = file->NextBool32(); missile._miPreFlag = file->NextBool32(); missile._miUniqTrans = file->NextLE(); missile._mirange = file->NextLE(); missile._misource = file->NextLE(); missile._micaster = static_cast(file->NextLE()); missile._midam = file->NextLE(); missile._miHitFlag = file->NextBool32(); missile._midist = file->NextLE(); missile._mlid = file->NextLE(); missile._mirnd = file->NextLE(); missile.var1 = file->NextLE(); missile.var2 = file->NextLE(); missile.var3 = file->NextLE(); missile.var4 = file->NextLE(); missile.var5 = file->NextLE(); missile.var6 = file->NextLE(); missile.var7 = file->NextLE(); missile.limitReached = file->NextBool32(); missile.lastCollisionTargetHash = 0; if (Missiles.size() < Missiles.max_size()) { Missiles.push_back(missile); } } _object_id ConvertFromHellfireObject(_object_id type) { if (leveltype == DTYPE_NEST) { switch (type) { case OBJ_BARREL: return OBJ_POD; case OBJ_BARRELEX: return OBJ_PODEX; default: break; } } if (leveltype == DTYPE_CRYPT) { switch (type) { case OBJ_BARREL: return OBJ_URN; case OBJ_BARRELEX: return OBJ_URNEX; case OBJ_STORYBOOK: return OBJ_L5BOOKS; case OBJ_STORYCANDLE: return OBJ_L5CANDLE; case OBJ_L1LDOOR: return OBJ_L5LDOOR; case OBJ_L1RDOOR: return OBJ_L5RDOOR; case OBJ_LEVER: return OBJ_L5LEVER; case OBJ_SARC: return OBJ_L5SARC; default: break; } } return type; } void LoadObject(LoadHelper &file, Object &object) { object._otype = ConvertFromHellfireObject(static_cast<_object_id>(file.NextLE())); object.position.x = file.NextLE(); object.position.y = file.NextLE(); object.applyLighting = file.NextBool32(); object._oAnimFlag = file.NextBool32(); file.Skip(4); // Skip pointer _oAnimData object._oAnimDelay = file.NextLE(); object._oAnimCnt = file.NextLE(); object._oAnimLen = file.NextLE(); object._oAnimFrame = file.NextLE(); object._oAnimWidth = static_cast(file.NextLE()); file.Skip(4); // Skip _oAnimWidth2 object._oDelFlag = file.NextBool32(); object._oBreak = file.NextLE(); file.Skip(3); // Alignment object._oSolidFlag = file.NextBool32(); object._oMissFlag = file.NextBool32(); object._oSelFlag = file.NextLE(); file.Skip(3); // Alignment object._oPreFlag = file.NextBool32(); object._oTrapFlag = file.NextBool32(); object._oDoorFlag = file.NextBool32(); object._olid = file.NextLE(); object._oRndSeed = file.NextLE(); object._oVar1 = file.NextLE(); object._oVar2 = file.NextLE(); object._oVar3 = file.NextLE(); object._oVar4 = file.NextLE(); object._oVar5 = file.NextLE(); object._oVar6 = file.NextLE(); object.bookMessage = static_cast<_speech_id>(file.NextLE()); object._oVar8 = file.NextLE(); } void LoadItem(LoadHelper &file, Item &item) { LoadItemData(file, item); GetItemFrm(item); } void LoadPremium(LoadHelper &file, int i) { LoadItemData(file, premiumitems[i]); } void LoadQuest(LoadHelper *file, int i) { auto &quest = Quests[i]; quest._qlevel = file->NextLE(); file->Skip(); // _qtype, identical to _qidx quest._qactive = static_cast(file->NextLE()); quest._qlvltype = static_cast(file->NextLE()); quest.position.x = file->NextLE(); quest.position.y = file->NextLE(); quest._qslvl = static_cast<_setlevels>(file->NextLE()); quest._qidx = static_cast(file->NextLE()); if (gbIsHellfireSaveGame) { file->Skip(2); // Alignment quest._qmsg = static_cast<_speech_id>(file->NextLE()); } else { quest._qmsg = static_cast<_speech_id>(file->NextLE()); } quest._qvar1 = file->NextLE(); quest._qvar2 = file->NextLE(); file->Skip(2); // Alignment if (!gbIsHellfireSaveGame) file->Skip(1); // Alignment quest._qlog = file->NextBool32(); ReturnLvlPosition.x = file->NextBE(); ReturnLvlPosition.y = file->NextBE(); ReturnLevel = file->NextBE(); ReturnLevelType = static_cast(file->NextBE()); file->Skip(sizeof(int32_t)); // Skip DoomQuestState } void LoadLighting(LoadHelper *file, Light *pLight) { pLight->position.tile.x = file->NextLE(); pLight->position.tile.y = file->NextLE(); pLight->radius = file->NextLE(); file->Skip(); // _lid pLight->isInvalid = file->NextBool32(); pLight->hasChanged = file->NextBool32(); file->Skip(4); // Unused pLight->position.old.x = file->NextLE(); pLight->position.old.y = file->NextLE(); pLight->oldRadius = file->NextLE(); pLight->position.offset.deltaX = file->NextLE(); pLight->position.offset.deltaY = file->NextLE(); file->Skip(); // _lflags } void LoadPortal(LoadHelper *file, int i) { Portal *pPortal = &Portals[i]; pPortal->open = file->NextBool32(); pPortal->position.x = file->NextLE(); pPortal->position.y = file->NextLE(); pPortal->level = file->NextLE(); pPortal->ltype = static_cast(file->NextLE()); pPortal->setlvl = file->NextBool32(); if (!pPortal->setlvl) pPortal->ltype = GetLevelType(pPortal->level); } void GetLevelNames(string_view prefix, char *out) { char suf; uint8_t num; if (setlevel) { suf = 's'; num = static_cast(setlvlnum); } else { suf = 'l'; num = currlevel; } *fmt::format_to(out, "{}{}{:02d}", prefix, suf, num) = '\0'; } void GetTempLevelNames(char *szTemp) { return GetLevelNames("temp", szTemp); } void GetPermLevelNames(char *szPerm) { return GetLevelNames("perm", szPerm); } bool LevelFileExists(SaveWriter &archive) { char szName[MaxMpqPathSize]; GetTempLevelNames(szName); if (archive.HasFile(szName)) return true; GetPermLevelNames(szName); return archive.HasFile(szName); } bool IsShopPriceValid(const Item &item) { const int boyPriceLimit = 90000; if (!gbIsHellfire && (item._iCreateInfo & CF_BOY) != 0 && item._iIvalue > boyPriceLimit) return false; const int premiumPriceLimit = 140000; if (!gbIsHellfire && (item._iCreateInfo & CF_SMITHPREMIUM) != 0 && item._iIvalue > premiumPriceLimit) return false; const uint16_t smithOrWitch = CF_SMITH | CF_WITCH; const int smithAndWitchPriceLimit = gbIsHellfire ? 200000 : 140000; if ((item._iCreateInfo & smithOrWitch) != 0 && item._iIvalue > smithAndWitchPriceLimit) return false; return true; } void LoadMatchingItems(LoadHelper &file, const Player &player, const int n, Item *pItem) { Item heroItem; for (int i = 0; i < n; i++) { Item &unpackedItem = pItem[i]; LoadItemData(file, heroItem); if (unpackedItem.isEmpty() || heroItem.isEmpty()) continue; if (unpackedItem._iSeed != heroItem._iSeed) continue; if (heroItem.IDidx == IDI_EAR) continue; if (gbIsMultiplayer) { // Ensure that the unpacked item was regenerated using the appropriate // game's item generation logic before attempting to use it for validation if ((heroItem.dwBuff & CF_HELLFIRE) != (unpackedItem.dwBuff & CF_HELLFIRE)) { unpackedItem = {}; RecreateItem(player, unpackedItem, heroItem.IDidx, heroItem._iCreateInfo, heroItem._iSeed, heroItem._ivalue, (heroItem.dwBuff & CF_HELLFIRE) != 0); unpackedItem._iIdentified = heroItem._iIdentified; unpackedItem._iMaxDur = heroItem._iMaxDur; unpackedItem._iDurability = ClampDurability(unpackedItem, heroItem._iDurability); unpackedItem._iMaxCharges = std::clamp(heroItem._iMaxCharges, 0, unpackedItem._iMaxCharges); unpackedItem._iCharges = std::clamp(heroItem._iCharges, 0, unpackedItem._iMaxCharges); } if (!IsShopPriceValid(unpackedItem)) { unpackedItem.clear(); continue; } if (gbIsHellfire) { unpackedItem._iPLToHit = ClampToHit(unpackedItem, heroItem._iPLToHit); // Oil of Accuracy unpackedItem._iMaxDam = ClampMaxDam(unpackedItem, heroItem._iMaxDam); // Oil of Sharpness } } else { unpackedItem = heroItem; } } } /** * @brief Loads items on the current dungeon floor * @param file interface to the save file * @param savedItemCount how many items to read from the save file */ void LoadDroppedItems(LoadHelper &file, size_t savedItemCount) { // Skip loading ActiveItems and AvailableItems, the indices are initialised below based on the number of valid items file.Skip(MAXITEMS * 2); // Reset ActiveItems, the Items array will be populated from the start std::iota(ActiveItems, ActiveItems + MAXITEMS, 0); ActiveItemCount = 0; // Clear dItem so we can populate valid drop locations memset(dItem, 0, sizeof(dItem)); for (size_t i = 0; i < savedItemCount; i++) { Item &item = Items[ActiveItemCount]; LoadItem(file, item); if (!item.isEmpty()) { // Loaded a valid item ActiveItemCount++; // populate its location in the lookup table with the offset in the Items array + 1 (so 0 can be used for "no item") dItem[item.position.x][item.position.y] = ActiveItemCount; } } } int getHellfireLevelType(int type) { if (type == DTYPE_CRYPT) return DTYPE_CATHEDRAL; if (type == DTYPE_NEST) return DTYPE_CAVES; return type; } void SaveItem(SaveHelper &file, const Item &item) { auto idx = item.IDidx; if (!gbIsHellfire) idx = RemapItemIdxToDiablo(idx); if (gbIsSpawn) idx = RemapItemIdxToSpawn(idx); ItemType iType = item._itype; if (idx == -1) { idx = _item_indexes::IDI_GOLD; iType = ItemType::None; } file.WriteLE(item._iSeed); file.WriteLE(item._iCreateInfo); file.Skip(2); // Alignment file.WriteLE(static_cast(iType)); file.WriteLE(item.position.x); file.WriteLE(item.position.y); file.WriteLE(item._iAnimFlag ? 1 : 0); file.Skip(4); // Skip pointer _iAnimData file.WriteLE(item.AnimInfo.numberOfFrames); file.WriteLE(item.AnimInfo.currentFrame + 1); // write _iAnimWidth for vanilla compatibility file.WriteLE(ItemAnimWidth); // write _iAnimWidth2 for vanilla compatibility file.WriteLE(CalculateWidth2(ItemAnimWidth)); file.Skip(); // _delFlag, unused since 1.02 file.WriteLE(item._iSelFlag); file.Skip(3); // Alignment file.WriteLE(item._iPostDraw ? 1 : 0); file.WriteLE(item._iIdentified ? 1 : 0); file.WriteLE(item._iMagical); file.WriteBytes(item._iName, 64); file.WriteBytes(item._iIName, 64); file.WriteLE(item._iLoc); file.WriteLE(item._iClass); file.Skip(1); // Alignment file.WriteLE(item._iCurs); file.WriteLE(item._ivalue); file.WriteLE(item._iIvalue); file.WriteLE(item._iMinDam); file.WriteLE(item._iMaxDam); file.WriteLE(item._iAC); file.WriteLE(static_cast(item._iFlags)); file.WriteLE(item._iMiscId); file.WriteLE(static_cast(item._iSpell)); file.WriteLE(item._iCharges); file.WriteLE(item._iMaxCharges); file.WriteLE(item._iDurability); file.WriteLE(item._iMaxDur); file.WriteLE(item._iPLDam); file.WriteLE(item._iPLToHit); file.WriteLE(item._iPLAC); file.WriteLE(item._iPLStr); file.WriteLE(item._iPLMag); file.WriteLE(item._iPLDex); file.WriteLE(item._iPLVit); file.WriteLE(item._iPLFR); file.WriteLE(item._iPLLR); file.WriteLE(item._iPLMR); file.WriteLE(item._iPLMana); file.WriteLE(item._iPLHP); file.WriteLE(item._iPLDamMod); file.WriteLE(item._iPLGetHit); file.WriteLE(item._iPLLight); file.WriteLE(item._iSplLvlAdd); file.WriteLE(item._iRequest ? 1 : 0); file.Skip(2); // Alignment file.WriteLE(item._iUid); file.WriteLE(item._iFMinDam); file.WriteLE(item._iFMaxDam); file.WriteLE(item._iLMinDam); file.WriteLE(item._iLMaxDam); file.WriteLE(item._iPLEnAc); file.WriteLE(item._iPrePower); file.WriteLE(item._iSufPower); file.Skip(2); // Alignment file.WriteLE(item._iVAdd1); file.WriteLE(item._iVMult1); file.WriteLE(item._iVAdd2); file.WriteLE(item._iVMult2); file.WriteLE(item._iMinStr); file.WriteLE(item._iMinMag); file.WriteLE(item._iMinDex); file.Skip(1); // Alignment file.WriteLE(item._iStatFlag ? 1 : 0); file.WriteLE(idx); file.WriteLE(item.dwBuff); if (gbIsHellfire) file.WriteLE(static_cast(item._iDamAcFlags)); } void SavePlayer(SaveHelper &file, const Player &player) { file.WriteLE(player._pmode); for (int8_t step : player.walkpath) file.WriteLE(step); file.WriteLE(player.plractive ? 1 : 0); file.Skip(2); // Alignment file.WriteLE(player.destAction); file.WriteLE(player.destParam1); file.WriteLE(player.destParam2); file.WriteLE(static_cast(player.destParam3)); file.WriteLE(player.destParam4); file.WriteLE(player.plrlevel); file.WriteLE(player.position.tile.x); file.WriteLE(player.position.tile.y); file.WriteLE(player.position.future.x); file.WriteLE(player.position.future.y); // For backwards compatibility const Point target = player.GetTargetPosition(); file.WriteLE(target.x); file.WriteLE(target.y); file.WriteLE(player.position.last.x); file.WriteLE(player.position.last.y); file.WriteLE(player.position.old.x); file.WriteLE(player.position.old.y); DisplacementOf offset = {}; DisplacementOf offset2 = {}; DisplacementOf velocity = {}; if (player.isWalking()) { offset = player.position.CalculateWalkingOffset(player._pdir, player.AnimInfo); offset2 = player.position.CalculateWalkingOffsetShifted8(player._pdir, player.AnimInfo); velocity = player.position.GetWalkingVelocityShifted8(player._pdir, player.AnimInfo); } file.WriteLE(offset.deltaX); file.WriteLE(offset.deltaY); file.WriteLE(velocity.deltaX); file.WriteLE(velocity.deltaY); file.WriteLE(static_cast(player._pdir)); file.Skip(4); // Unused file.WriteLE(player._pgfxnum); file.Skip(4); // Skip pointer _pAnimData file.WriteLE(std::max(0, player.AnimInfo.ticksPerFrame - 1)); file.WriteLE(player.AnimInfo.tickCounterOfCurrentFrame); file.WriteLE(player.AnimInfo.numberOfFrames); file.WriteLE(player.AnimInfo.currentFrame + 1); // write _pAnimWidth for vanilla compatibility const int animWidth = player.getSpriteWidth(); file.WriteLE(animWidth); // write _pAnimWidth2 for vanilla compatibility file.WriteLE(CalculateWidth2(animWidth)); file.Skip(); // Skip _peflag file.WriteLE(player.lightId); file.WriteLE(1); // _pvid file.WriteLE(static_cast(player.queuedSpell.spellId)); file.WriteLE(static_cast(player.queuedSpell.spellType)); file.WriteLE(player.queuedSpell.spellFrom); file.Skip(2); // Alignment file.WriteLE(static_cast(player.inventorySpell)); file.Skip(); // Skip _pTSplType file.Skip(3); // Alignment file.WriteLE(static_cast(player._pRSpell)); file.WriteLE(static_cast(player._pRSplType)); file.Skip(3); // Alignment file.WriteLE(static_cast(player._pSBkSpell)); file.Skip(); // Skip _pSBkSplType for (uint8_t spellLevel : player._pSplLvl) file.WriteLE(spellLevel); file.Skip(7); // Alignment file.WriteLE(player._pMemSpells); file.WriteLE(player._pAblSpells); file.WriteLE(player._pScrlSpells); file.WriteLE(static_cast(player._pSpellFlags)); file.Skip(3); // Alignment // Extra hotkeys: to keep single player save compatibility, write only 4 hotkeys here, rely on SaveHotkeys for the rest for (size_t i = 0; i < 4; i++) { file.WriteLE(static_cast(player._pSplHotKey[i])); } for (size_t i = 0; i < 4; i++) { file.WriteLE(static_cast(player._pSplTHotKey[i])); } file.WriteLE(player.UsesRangedWeapon() ? 1 : 0); file.WriteLE(player._pBlockFlag ? 1 : 0); file.WriteLE(player._pInvincible ? 1 : 0); file.WriteLE(player._pLightRad); file.WriteLE(player._pLvlChanging ? 1 : 0); file.WriteBytes(player._pName, PlayerNameLength); file.WriteLE(static_cast(player._pClass)); file.Skip(3); // Alignment file.WriteLE(player._pStrength); file.WriteLE(player._pBaseStr); file.WriteLE(player._pMagic); file.WriteLE(player._pBaseMag); file.WriteLE(player._pDexterity); file.WriteLE(player._pBaseDex); file.WriteLE(player._pVitality); file.WriteLE(player._pBaseVit); file.WriteLE(player._pStatPts); file.WriteLE(player._pDamageMod); file.WriteLE(player._pBaseToBlk); file.WriteLE(player._pHPBase); file.WriteLE(player._pMaxHPBase); file.WriteLE(player._pHitPoints); file.WriteLE(player._pMaxHP); file.Skip(); // Skip _pHPPer file.WriteLE(player._pManaBase); file.WriteLE(player._pMaxManaBase); file.WriteLE(player._pMana); file.WriteLE(player._pMaxMana); file.Skip(); // Skip _pManaPer file.WriteLE(player._pLevel); file.WriteLE(player._pMaxLvl); file.Skip(2); // Alignment file.WriteLE(player._pExperience); file.Skip(); // Skip _pMaxExp file.WriteLE(player._pNextExper); file.WriteLE(player._pArmorClass); file.WriteLE(player._pMagResist); file.WriteLE(player._pFireResist); file.WriteLE(player._pLghtResist); file.WriteLE(player._pGold); file.WriteLE(player._pInfraFlag ? 1 : 0); int32_t tempPositionX = player.position.temp.x; int32_t tempPositionY = player.position.temp.y; if (player._pmode == PM_WALK_NORTHWARDS) { // For backwards compatibility, save this as an offset tempPositionX -= player.position.tile.x; tempPositionY -= player.position.tile.y; } file.WriteLE(tempPositionX); file.WriteLE(tempPositionY); file.WriteLE(static_cast(player.tempDirection)); file.WriteLE(player.queuedSpell.spellLevel); file.Skip(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking file.WriteLE(offset2.deltaX); file.WriteLE(offset2.deltaY); file.Skip(); // Skip _pVar8 for (uint8_t i = 0; i < giNumberOfLevels; i++) file.WriteLE(player._pLvlVisited[i] ? 1 : 0); for (uint8_t i = 0; i < giNumberOfLevels; i++) file.WriteLE(player._pSLvlVisited[i] ? 1 : 0); // only 10 used file.Skip(2); // Alignment file.Skip(); // Skip _pGFXLoad file.Skip(8); // Skip pointers _pNAnim file.WriteLE(player._pNFrames); file.Skip(); // Skip _pNWidth file.Skip(8); // Skip pointers _pWAnim file.WriteLE(player._pWFrames); file.Skip(); // Skip _pWWidth file.Skip(8); // Skip pointers _pAAnim file.WriteLE(player._pAFrames); file.Skip(); // Skip _pAWidth file.WriteLE(player._pAFNum); file.Skip(8); // Skip pointers _pLAnim file.Skip(8); // Skip pointers _pFAnim file.Skip(8); // Skip pointers _pTAnim file.WriteLE(player._pSFrames); file.Skip(); // Skip _pSWidth file.WriteLE(player._pSFNum); file.Skip(8); // Skip pointers _pHAnim file.WriteLE(player._pHFrames); file.Skip(); // Skip _pHWidth file.Skip(8); // Skip pointers _pDAnim file.WriteLE(player._pDFrames); file.Skip(); // Skip _pDWidth file.Skip(8); // Skip pointers _pBAnim file.WriteLE(player._pBFrames); file.Skip(); // Skip _pBWidth for (const Item &item : player.InvBody) SaveItem(file, item); for (const Item &item : player.InvList) SaveItem(file, item); file.WriteLE(player._pNumInv); for (int8_t cell : player.InvGrid) file.WriteLE(cell); for (const Item &item : player.SpdList) SaveItem(file, item); SaveItem(file, player.HoldItem); file.WriteLE(player._pIMinDam); file.WriteLE(player._pIMaxDam); file.WriteLE(player._pIAC); file.WriteLE(player._pIBonusDam); file.WriteLE(player._pIBonusToHit); file.WriteLE(player._pIBonusAC); file.WriteLE(player._pIBonusDamMod); file.Skip(4); // Alignment file.WriteLE(player._pISpells); file.WriteLE(static_cast(player._pIFlags)); file.WriteLE(player._pIGetHit); file.WriteLE(player._pISplLvlAdd); file.Skip(); // Skip _pISplCost file.Skip(2); // Alignment file.Skip(); // _pISplDur file.WriteLE(player._pIEnAc); file.WriteLE(player._pIFMinDam); file.WriteLE(player._pIFMaxDam); file.WriteLE(player._pILMinDam); file.WriteLE(player._pILMaxDam); file.WriteLE(player._pOilType); file.WriteLE(player.pTownWarps); file.WriteLE(player.pDungMsgs); file.WriteLE(player.pLvlLoad); if (gbIsHellfire) file.WriteLE(player.pDungMsgs2); else file.WriteLE(0); file.WriteLE(player.pManaShield ? 1 : 0); file.WriteLE(player.pOriginalCathedral ? 1 : 0); file.Skip(2); // Available bytes file.WriteLE(player.wReflections); file.Skip(14); // Available bytes file.WriteLE(player.pDiabloKillLevel); file.WriteLE(sgGameInitInfo.nDifficulty); file.WriteLE(static_cast(player.pDamAcFlags)); file.Skip(20); // Available bytes // Omit pointer _pNData // Omit pointer _pWData // Omit pointer _pAData // Omit pointer _pLData // Omit pointer _pFData // Omit pointer _pTData // Omit pointer _pHData // Omit pointer _pDData // Omit pointer _pBData // Omit pointer pReserved } void SaveMonster(SaveHelper *file, Monster &monster) { file->WriteLE(monster.levelType); file->WriteLE(static_cast(monster.mode)); file->WriteLE(static_cast(monster.goal)); file->Skip(3); // Alignment file->WriteLE(monster.goalVar1); file->WriteLE(monster.goalVar2); file->WriteLE(monster.goalVar3); file->Skip(4); // Unused file->WriteLE(monster.pathCount); file->Skip(3); // Alignment file->WriteLE(monster.position.tile.x); file->WriteLE(monster.position.tile.y); file->WriteLE(monster.position.future.x); file->WriteLE(monster.position.future.y); file->WriteLE(monster.position.old.x); file->WriteLE(monster.position.old.y); DisplacementOf offset = {}; DisplacementOf offset2 = {}; DisplacementOf velocity = {}; if (monster.isWalking()) { offset = monster.position.CalculateWalkingOffset(monster.direction, monster.animInfo); offset2 = monster.position.CalculateWalkingOffsetShifted4(monster.direction, monster.animInfo); velocity = monster.position.GetWalkingVelocityShifted4(monster.direction, monster.animInfo); } file->WriteLE(offset.deltaX); file->WriteLE(offset.deltaY); file->WriteLE(velocity.deltaX); file->WriteLE(velocity.deltaY); file->WriteLE(static_cast(monster.direction)); file->WriteLE(monster.enemy); file->WriteLE(monster.enemyPosition.x); file->WriteLE(monster.enemyPosition.y); file->Skip(2); // Unused file->Skip(4); // Skip pointer _mAnimData file->WriteLE(monster.animInfo.ticksPerFrame); file->WriteLE(monster.animInfo.tickCounterOfCurrentFrame); file->WriteLE(monster.animInfo.numberOfFrames); file->WriteLE(monster.animInfo.currentFrame + 1); file->Skip(); // Skip _meflag file->WriteLE(monster.isInvalid ? 1 : 0); file->WriteLE(monster.var1); file->WriteLE(monster.var2); file->WriteLE(monster.var3); file->WriteLE(monster.position.temp.x); file->WriteLE(monster.position.temp.y); file->WriteLE(offset2.deltaX); file->WriteLE(offset2.deltaY); file->Skip(); // Skip _mVar8 file->WriteLE(monster.maxHitPoints); file->WriteLE(monster.hitPoints); file->WriteLE(static_cast(monster.ai)); file->WriteLE(monster.intelligence); file->Skip(2); // Alignment file->WriteLE(monster.flags); file->WriteLE(monster.activeForTicks); file->Skip(3); // Alignment file->Skip(4); // Unused file->WriteLE(monster.position.last.x); file->WriteLE(monster.position.last.y); file->WriteLE(monster.rndItemSeed); file->WriteLE(monster.aiSeed); file->Skip(4); // Unused file->WriteLE(static_cast(monster.uniqueType) + 1); file->WriteLE(monster.uniqTrans); file->WriteLE(monster.corpseId); file->WriteLE(monster.whoHit); file->WriteLE(static_cast(monster.level(sgGameInitInfo.nDifficulty))); file->Skip(1); // Alignment file->WriteLE(static_cast(std::min(std::numeric_limits::max(), monster.exp(sgGameInitInfo.nDifficulty)))); file->WriteLE(static_cast(std::min(monster.toHit, std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.minDamage); file->WriteLE(monster.maxDamage); file->WriteLE(static_cast(std::min(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.minDamageSpecial); file->WriteLE(monster.maxDamageSpecial); file->WriteLE(monster.armorClass); file->Skip(1); // Alignment file->WriteLE(monster.resistance); file->Skip(2); // Alignment file->WriteLE(monster.talkMsg == TEXT_NONE ? 0 : monster.talkMsg); // Replicate original bad mapping of none for monsters file->WriteLE(monster.leader == Monster::NoLeader ? 0 : monster.leader); // Vanilla uses 0 as the default leader which corresponds to player 0s golem file->WriteLE(static_cast(monster.leaderRelation)); file->WriteLE(monster.packSize); // vanilla compatibility if (monster.lightId == NO_LIGHT) file->WriteLE(0); else file->WriteLE(monster.lightId); // Omit pointer name; } void SaveMissile(SaveHelper *file, const Missile &missile) { file->WriteLE(static_cast(missile._mitype)); file->WriteLE(missile.position.tile.x); file->WriteLE(missile.position.tile.y); file->WriteLE(missile.position.offset.deltaX); file->WriteLE(missile.position.offset.deltaY); file->WriteLE(missile.position.velocity.deltaX); file->WriteLE(missile.position.velocity.deltaY); file->WriteLE(missile.position.start.x); file->WriteLE(missile.position.start.y); file->WriteLE(missile.position.traveled.deltaX); file->WriteLE(missile.position.traveled.deltaY); file->WriteLE(missile._mimfnum); file->WriteLE(missile._mispllvl); file->WriteLE(missile._miDelFlag ? 1 : 0); file->WriteLE(static_cast(missile._miAnimType)); file->Skip(3); // Alignment file->WriteLE(static_cast(missile._miAnimFlags)); file->Skip(4); // Skip pointer _miAnimData file->WriteLE(missile._miAnimDelay); file->WriteLE(missile._miAnimLen); file->WriteLE(missile._miAnimWidth); file->WriteLE(missile._miAnimWidth2); file->WriteLE(missile._miAnimCnt); file->WriteLE(missile._miAnimAdd); file->WriteLE(missile._miAnimFrame); file->WriteLE(missile._miDrawFlag ? 1 : 0); file->WriteLE(missile._miLightFlag ? 1 : 0); file->WriteLE(missile._miPreFlag ? 1 : 0); file->WriteLE(missile._miUniqTrans); file->WriteLE(missile._mirange); file->WriteLE(missile._misource); file->WriteLE(missile._micaster); file->WriteLE(missile._midam); file->WriteLE(missile._miHitFlag ? 1 : 0); file->WriteLE(missile._midist); file->WriteLE(missile._mlid); file->WriteLE(missile._mirnd); file->WriteLE(missile.var1); file->WriteLE(missile.var2); file->WriteLE(missile.var3); file->WriteLE(missile.var4); file->WriteLE(missile.var5); file->WriteLE(missile.var6); file->WriteLE(missile.var7); file->WriteLE(missile.limitReached ? 1 : 0); } _object_id ConvertToHellfireObject(_object_id type) { if (leveltype == DTYPE_NEST) { switch (type) { case OBJ_POD: return OBJ_BARREL; case OBJ_PODEX: return OBJ_BARRELEX; default: break; } } if (leveltype == DTYPE_CRYPT) { switch (type) { case OBJ_URN: return OBJ_BARREL; case OBJ_URNEX: return OBJ_BARRELEX; case OBJ_L5BOOKS: return OBJ_STORYBOOK; case OBJ_L5CANDLE: return OBJ_STORYCANDLE; case OBJ_L5LDOOR: return OBJ_L1LDOOR; case OBJ_L5RDOOR: return OBJ_L1RDOOR; case OBJ_L5LEVER: return OBJ_LEVER; case OBJ_L5SARC: return OBJ_SARC; default: break; } } return type; } void SaveObject(SaveHelper &file, const Object &object) { file.WriteLE(ConvertToHellfireObject(object._otype)); file.WriteLE(object.position.x); file.WriteLE(object.position.y); file.WriteLE(object.applyLighting ? 1 : 0); file.WriteLE(object._oAnimFlag ? 1 : 0); file.Skip(4); // Skip pointer _oAnimData file.WriteLE(object._oAnimDelay); file.WriteLE(object._oAnimCnt); file.WriteLE(object._oAnimLen); file.WriteLE(object._oAnimFrame); file.WriteLE(object._oAnimWidth); file.WriteLE(CalculateWidth2(static_cast(object._oAnimWidth))); // Write _oAnimWidth2 for vanilla compatibility file.WriteLE(object._oDelFlag ? 1 : 0); file.WriteLE(object._oBreak); file.Skip(3); // Alignment file.WriteLE(object._oSolidFlag ? 1 : 0); file.WriteLE(object._oMissFlag ? 1 : 0); file.WriteLE(object._oSelFlag); file.Skip(3); // Alignment file.WriteLE(object._oPreFlag ? 1 : 0); file.WriteLE(object._oTrapFlag ? 1 : 0); file.WriteLE(object._oDoorFlag ? 1 : 0); file.WriteLE(object._olid); file.WriteLE(object._oRndSeed); /* Make dynamic light sources unseen when saving level data for level change */ int32_t var1 = object._oVar1; switch (object._otype) { case OBJ_L1LIGHT: case OBJ_SKFIRE: case OBJ_CANDLE1: case OBJ_CANDLE2: case OBJ_BOOKCANDLE: case OBJ_STORYCANDLE: case OBJ_L5CANDLE: case OBJ_TORCHL: case OBJ_TORCHR: case OBJ_TORCHL2: case OBJ_TORCHR2: case OBJ_BCROSS: case OBJ_TBCROSS: if (var1 != -1) var1 = 0; break; default: break; } file.WriteLE(var1); file.WriteLE(object._oVar2); file.WriteLE(object._oVar3); file.WriteLE(object._oVar4); file.WriteLE(object._oVar5); file.WriteLE(object._oVar6); file.WriteLE(object.bookMessage); file.WriteLE(object._oVar8); } void SaveQuest(SaveHelper *file, int i) { auto &quest = Quests[i]; file->WriteLE(quest._qlevel); file->WriteLE(quest._qidx); // _qtype for compatability, used in DRLG_CheckQuests file->WriteLE(quest._qactive); file->WriteLE(quest._qlvltype); file->WriteLE(quest.position.x); file->WriteLE(quest.position.y); file->WriteLE(quest._qslvl); file->WriteLE(quest._qidx); if (gbIsHellfire) { file->Skip(2); // Alignment file->WriteLE(quest._qmsg); } else { file->WriteLE(quest._qmsg); } file->WriteLE(quest._qvar1); file->WriteLE(quest._qvar2); file->Skip(2); // Alignment if (!gbIsHellfire) file->Skip(1); // Alignment file->WriteLE(quest._qlog ? 1 : 0); file->WriteBE(ReturnLvlPosition.x); file->WriteBE(ReturnLvlPosition.y); file->WriteBE(ReturnLevel); file->WriteBE(ReturnLevelType); file->Skip(sizeof(int32_t)); // Skip DoomQuestState } void SaveLighting(SaveHelper *file, Light *pLight, bool vision = false) { file->WriteLE(pLight->position.tile.x); file->WriteLE(pLight->position.tile.y); file->WriteLE(pLight->radius); file->WriteLE(vision ? 1 : 0); // _lid file->WriteLE(pLight->isInvalid ? 1 : 0); file->WriteLE(pLight->hasChanged ? 1 : 0); file->Skip(4); // Unused file->WriteLE(pLight->position.old.x); file->WriteLE(pLight->position.old.y); file->WriteLE(pLight->oldRadius); file->WriteLE(pLight->position.offset.deltaX); file->WriteLE(pLight->position.offset.deltaY); file->WriteLE(vision ? 1 : 0); } void SavePortal(SaveHelper *file, int i) { Portal *pPortal = &Portals[i]; file->WriteLE(pPortal->open ? 1 : 0); file->WriteLE(pPortal->position.x); file->WriteLE(pPortal->position.y); file->WriteLE(pPortal->level); file->WriteLE(pPortal->setlvl ? pPortal->ltype : getHellfireLevelType(pPortal->ltype)); file->WriteLE(pPortal->setlvl ? 1 : 0); } /** * @brief Saves items on the current dungeon floor * @param file interface to the save file * @return a map converting from runtime item indexes to the relative position in the save file, used by SaveDroppedItemLocations * @see SaveDroppedItemLocations */ std::unordered_map SaveDroppedItems(SaveHelper &file) { // Vanilla Diablo/Hellfire initialise the ActiveItems and AvailableItems arrays based on saved data, so write valid values for compatibility for (uint8_t i = 0; i < MAXITEMS; i++) file.WriteLE(i); // Strictly speaking everything from ActiveItemCount onwards is unused but no harm writing non-zero values here. for (uint8_t i = 0; i < MAXITEMS; i++) file.WriteLE((i + ActiveItemCount) % MAXITEMS); std::unordered_map itemIndexes = { { 0, 0 } }; for (uint8_t i = 0; i < ActiveItemCount; i++) { itemIndexes[ActiveItems[i] + 1] = i + 1; SaveItem(file, Items[ActiveItems[i]]); } return itemIndexes; } /** * @brief Saves the position of dropped items (in dItem) * @param file interface to the save file * @param itemIndexes a map converting from runtime item indexes to the relative position in the save file */ void SaveDroppedItemLocations(SaveHelper &file, const std::unordered_map &itemIndexes) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(itemIndexes.at(dItem[i][j])); } } constexpr uint32_t VersionAdditionalMissiles = 0; void SaveAdditionalMissiles(SaveWriter &saveWriter) { constexpr size_t BytesWrittenBySaveMissile = 180; uint32_t missileCountAdditional = (Missiles.size() > MaxMissilesForSaveGame) ? static_cast(Missiles.size() - MaxMissilesForSaveGame) : 0; SaveHelper file(saveWriter, "additionalMissiles", sizeof(uint32_t) + sizeof(uint32_t) + (missileCountAdditional * BytesWrittenBySaveMissile)); file.WriteLE(VersionAdditionalMissiles); file.WriteLE(missileCountAdditional); if (missileCountAdditional > 0) { auto it = Missiles.cbegin(); // std::list::const_iterator doesn't provide operator+() :/ using std::advance to get past the missiles we've already saved std::advance(it, MaxMissilesForSaveGame); for (; it != Missiles.cend(); it++) { SaveMissile(&file, *it); } } } void LoadAdditionalMissiles() { LoadHelper file(OpenSaveArchive(gSaveNumber), "additionalMissiles"); if (!file.IsValid()) { // no addtional Missiles saved return; } auto loadedVersion = file.NextLE(); if (loadedVersion > VersionAdditionalMissiles) { // unknown version return; } auto missileCountAdditional = file.NextLE(); for (uint32_t i = 0U; i < missileCountAdditional; i++) { LoadMissile(&file); } } const int DiabloItemSaveSize = 368; const int HellfireItemSaveSize = 372; } // namespace void ConvertLevels(SaveWriter &saveWriter) { // Backup current level state bool tmpSetlevel = setlevel; _setlevels tmpSetlvlnum = setlvlnum; int tmpCurrlevel = currlevel; dungeon_type tmpLeveltype = leveltype; gbSkipSync = true; setlevel = false; // Convert regular levels for (int i = 0; i < giNumberOfLevels; i++) { currlevel = i; if (!LevelFileExists(saveWriter)) continue; leveltype = GetLevelType(currlevel); LoadLevel(); SaveLevel(saveWriter); } setlevel = true; // Convert quest levels for (auto &quest : Quests) { if (quest._qactive == QUEST_NOTAVAIL) { continue; } leveltype = quest._qlvltype; if (leveltype == DTYPE_NONE) { continue; } setlvlnum = quest._qslvl; if (!LevelFileExists(saveWriter)) continue; LoadLevel(); SaveLevel(saveWriter); } gbSkipSync = false; // Restore current level state setlevel = tmpSetlevel; setlvlnum = tmpSetlvlnum; currlevel = tmpCurrlevel; leveltype = tmpLeveltype; } void RemoveInvalidItem(Item &item) { bool isInvalid = !IsItemAvailable(item.IDidx) || !IsUniqueAvailable(item._iUid); if (!gbIsHellfire) { isInvalid = isInvalid || (item._itype == ItemType::Staff && GetSpellStaffLevel(item._iSpell) == -1); isInvalid = isInvalid || (item._iMiscId == IMISC_BOOK && GetSpellBookLevel(item._iSpell) == -1); isInvalid = isInvalid || item._iDamAcFlags != ItemSpecialEffectHf::None; isInvalid = isInvalid || item._iPrePower > IPL_LASTDIABLO; isInvalid = isInvalid || item._iSufPower > IPL_LASTDIABLO; } if (isInvalid) { item.clear(); } } _item_indexes RemapItemIdxFromDiablo(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) -> int { if (i == IDI_SORCERER) { return IDI_SORCERER_DIABLO; } if (i >= 156) { i += 5; // Hellfire exclusive items } if (i >= 88) { i += 1; // Scroll of Search } if (i >= 83) { i += 4; // Oils } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } _item_indexes RemapItemIdxToDiablo(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) -> int { if (i == IDI_SORCERER_DIABLO) { return IDI_SORCERER; } if ((i >= 83 && i <= 86) || i == 92 || i >= 161) { return -1; // Hellfire exclusive items } if (i >= 93) { i -= 1; // Scroll of Search } if (i >= 87) { i -= 4; // Oils } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } _item_indexes RemapItemIdxFromSpawn(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) { if (i >= 62) { i += 9; // Medium and heavy armors } if (i >= 96) { i += 1; // Scroll of Stone Curse } if (i >= 98) { i += 1; // Scroll of Guardian } if (i >= 99) { i += 1; // Scroll of ... } if (i >= 101) { i += 1; // Scroll of Golem } if (i >= 102) { i += 1; // Scroll of None } if (i >= 104) { i += 1; // Scroll of Apocalypse } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } _item_indexes RemapItemIdxToSpawn(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) { if (i >= 104) { i -= 1; // Scroll of Apocalypse } if (i >= 102) { i -= 1; // Scroll of None } if (i >= 101) { i -= 1; // Scroll of Golem } if (i >= 99) { i -= 1; // Scroll of ... } if (i >= 98) { i -= 1; // Scroll of Guardian } if (i >= 96) { i -= 1; // Scroll of Stone Curse } if (i >= 71) { i -= 9; // Medium and heavy armors } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } bool IsHeaderValid(uint32_t magicNumber) { gbIsHellfireSaveGame = false; if (magicNumber == LoadLE32("SHAR")) { return true; } if (magicNumber == LoadLE32("SHLF")) { gbIsHellfireSaveGame = true; return true; } if (!gbIsSpawn && magicNumber == LoadLE32("RETL")) { return true; } if (!gbIsSpawn && magicNumber == LoadLE32("HELF")) { gbIsHellfireSaveGame = true; return true; } return false; } // Returns the size of the hotkeys file with the number of hotkeys passed and if a header with the number of hotkeys is present in the file size_t HotkeysSize(size_t nHotkeys = NumHotkeys) { // header spells spell types active spell active spell type return sizeof(uint8_t) + (nHotkeys * sizeof(int32_t)) + (nHotkeys * sizeof(uint8_t)) + sizeof(int32_t) + sizeof(uint8_t); } void LoadHotkeys() { LoadHelper file(OpenSaveArchive(gSaveNumber), "hotkeys"); if (!file.IsValid()) return; Player &myPlayer = *MyPlayer; size_t nHotkeys = 4; // Defaults to old save format number // Refill the spell arrays with no selection std::fill(myPlayer._pSplHotKey, myPlayer._pSplHotKey + NumHotkeys, SpellID::Invalid); std::fill(myPlayer._pSplTHotKey, myPlayer._pSplTHotKey + NumHotkeys, SpellType::Invalid); // Checking if the save file has the old format with only 4 hotkeys and no header if (file.IsValid(HotkeysSize(nHotkeys))) { // The file contains a header byte and at least 4 entries, so we can assume it's a new format save nHotkeys = file.NextLE(); } // Read all hotkeys in the file for (size_t i = 0; i < nHotkeys; i++) { // Do not load hotkeys past the size of the spell types array, discard the rest if (i < NumHotkeys) { myPlayer._pSplHotKey[i] = static_cast(file.NextLE()); } else { file.Skip(); } } for (size_t i = 0; i < nHotkeys; i++) { // Do not load hotkeys past the size of the spells array, discard the rest if (i < NumHotkeys) { myPlayer._pSplTHotKey[i] = static_cast(file.NextLE()); } else { file.Skip(); } } // Load the selected spell last myPlayer._pRSpell = static_cast(file.NextLE()); myPlayer._pRSplType = static_cast(file.NextLE()); } void SaveHotkeys(SaveWriter &saveWriter, const Player &player) { SaveHelper file(saveWriter, "hotkeys", HotkeysSize()); // Write the number of spell hotkeys file.WriteLE(static_cast(NumHotkeys)); // Write the spell hotkeys for (auto &spellId : player._pSplHotKey) { file.WriteLE(static_cast(spellId)); } for (auto &spellType : player._pSplTHotKey) { file.WriteLE(static_cast(spellType)); } // Write the selected spell last file.WriteLE(static_cast(player._pRSpell)); file.WriteLE(static_cast(player._pRSplType)); } void LoadHeroItems(Player &player) { LoadHelper file(OpenSaveArchive(gSaveNumber), "heroitems"); if (!file.IsValid()) return; gbIsHellfireSaveGame = file.NextBool8(); LoadMatchingItems(file, player, NUM_INVLOC, player.InvBody); LoadMatchingItems(file, player, InventoryGridCells, player.InvList); LoadMatchingItems(file, player, MaxBeltItems, player.SpdList); gbIsHellfireSaveGame = gbIsHellfire; } constexpr uint8_t StashVersion = 0; void LoadStash() { const char *filename; if (!gbIsMultiplayer) filename = "spstashitems"; else filename = "mpstashitems"; Stash = {}; LoadHelper file(OpenStashArchive(), filename); if (!file.IsValid()) return; auto version = file.NextLE(); if (version > StashVersion) return; Stash.gold = file.NextLE(); auto pages = file.NextLE(); for (unsigned i = 0; i < pages; i++) { auto page = file.NextLE(); for (auto &row : Stash.stashGrids[page]) { for (uint16_t &cell : row) { cell = file.NextLE(); } } } auto itemCount = file.NextLE(); Stash.stashList.resize(itemCount); for (unsigned i = 0; i < itemCount; i++) { LoadItemData(file, Stash.stashList[i]); } Stash.SetPage(file.NextLE()); } void RemoveEmptyInventory(Player &player) { for (int i = InventoryGridCells; i > 0; i--) { int8_t idx = player.InvGrid[i - 1]; if (idx > 0 && player.InvList[idx - 1].isEmpty()) { player.RemoveInvItem(idx - 1); } } } void LoadGame(bool firstflag) { FreeGameMem(); LoadHelper file(OpenSaveArchive(gSaveNumber), "game"); if (!file.IsValid()) app_fatal(_("Unable to open save file archive")); if (!IsHeaderValid(file.NextLE())) app_fatal(_("Invalid save file")); if (gbIsHellfireSaveGame) { giNumberOfLevels = 25; giNumberQuests = 24; giNumberOfSmithPremiumItems = 15; } else { // Todo initialize additional levels and quests if we are running Hellfire giNumberOfLevels = 17; giNumberQuests = 16; giNumberOfSmithPremiumItems = 6; } pfile_remove_temp_files(); setlevel = file.NextBool8(); setlvlnum = static_cast<_setlevels>(file.NextBE()); currlevel = file.NextBE(); leveltype = static_cast(file.NextBE()); if (!setlevel) leveltype = GetLevelType(currlevel); int viewX = file.NextBE(); int viewY = file.NextBE(); invflag = file.NextBool8(); chrflag = file.NextBool8(); int tmpNummonsters = file.NextBE(); auto savedItemCount = file.NextBE(); int tmpNummissiles = file.NextBE(); int tmpNobjects = file.NextBE(); if (!gbIsHellfire && IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) app_fatal(_("Player is on a Hellfire only level")); for (uint8_t i = 0; i < giNumberOfLevels; i++) { glSeedTbl[i] = file.NextBE(); file.Skip(4); // Skip loading gnLevelTypeTbl } Player &myPlayer = *MyPlayer; LoadPlayer(file, myPlayer); if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL) sgGameInitInfo.nDifficulty = DIFF_NORMAL; for (int i = 0; i < giNumberQuests; i++) LoadQuest(&file, i); for (int i = 0; i < MAXPORTAL; i++) LoadPortal(&file, i); if (gbIsHellfireSaveGame != gbIsHellfire) { pfile_convert_levels(); RemoveEmptyInventory(myPlayer); } LoadGameLevel(firstflag, ENTRY_LOAD); SetPlrAnims(myPlayer); SyncPlrAnim(myPlayer); ViewPosition = { viewX, viewY }; ActiveMonsterCount = tmpNummonsters; ActiveObjectCount = tmpNobjects; for (int &monstkill : MonsterKillCounts) monstkill = file.NextBE(); // skip ahead for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES] file.Skip(4 * (MaxMonsters - NUM_MTYPES)); if (leveltype != DTYPE_TOWN) { for (int &monsterId : ActiveMonsters) monsterId = file.NextBE(); for (size_t i = 0; i < ActiveMonsterCount; i++) LoadMonster(&file, Monsters[ActiveMonsters[i]]); for (size_t i = 0; i < ActiveMonsterCount; i++) SyncPackSize(Monsters[ActiveMonsters[i]]); // Skip ActiveMissiles file.Skip(MaxMissilesForSaveGame); // Skip AvailableMissiles file.Skip(MaxMissilesForSaveGame); for (int i = 0; i < tmpNummissiles; i++) LoadMissile(&file); // For petrified monsters, the data in missile.var1 must be used to // load the appropriate animation data for the monster in missile.var2 for (size_t i = 0; i < ActiveMonsterCount; i++) SyncMonsterAnim(Monsters[ActiveMonsters[i]]); for (int &objectId : ActiveObjects) objectId = file.NextLE(); for (int &objectId : AvailableObjects) objectId = file.NextLE(); for (int i = 0; i < ActiveObjectCount; i++) LoadObject(file, Objects[ActiveObjects[i]]); for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); ActiveLightCount = file.NextBE(); for (uint8_t &lightId : ActiveLights) lightId = file.NextLE(); for (int i = 0; i < ActiveLightCount; i++) LoadLighting(&file, &Lights[ActiveLights[i]]); file.Skip(); // VisionId int visionCount = file.NextBE(); for (int i = 0; i < visionCount; i++) { LoadLighting(&file, &VisionList[i]); VisionActive[i] = true; } } LoadDroppedItems(file, savedItemCount); LoadAdditionalMissiles(); for (bool &uniqueItemFlag : UniqueItemFlags) uniqueItemFlag = file.NextBool8(); file.Skip(MAXDUNY * MAXDUNX); // dLight for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dFlags[i][j] = static_cast(file.NextLE()) & DungeonFlag::LoadedFlags; } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dPlayer[i][j] = file.NextLE(); } // skip dItem indexes, this gets populated in LoadDroppedItems file.Skip(MAXDUNX * MAXDUNY); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dMonster[i][j] = file.NextBE(); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dCorpse[i][j] = file.NextLE(); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dObject[i][j] = file.NextLE(); } file.Skip(MAXDUNY * MAXDUNX); // dLight for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dPreLight[i][j] = file.NextLE(); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) const auto automapView = static_cast(file.NextLE()); AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView; } } file.Skip(MAXDUNX * MAXDUNY); // dMissile // No need to load dLight, we can recreate it accurately from LightList memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light ChangeLightXY(myPlayer.lightId, myPlayer.position.tile); // forces player light refresh } else { memset(dLight, 0, sizeof(dLight)); } numpremium = file.NextBE(); premiumlevel = file.NextBE(); for (int i = 0; i < giNumberOfSmithPremiumItems; i++) LoadPremium(file, i); if (gbIsHellfire && !gbIsHellfireSaveGame) SpawnPremium(myPlayer); AutomapActive = file.NextBool8(); AutoMapScale = file.NextBE(); AutomapZoomReset(); ResyncQuests(); if (leveltype != DTYPE_TOWN) { RedoPlayerVision(); ProcessVisionList(); ProcessLightList(); } // convert stray manashield missiles into pManaShield flag for (auto &missile : Missiles) { if (missile._mitype == MissileID::ManaShield && !missile._miDelFlag) { Players[missile._misource].pManaShield = true; missile._miDelFlag = true; } } missiles_process_charge(); RedoMissileFlags(); NewCursor(CURSOR_HAND); gbProcessPlayers = IsDiabloAlive(!firstflag); if (gbIsHellfireSaveGame != gbIsHellfire) { SaveGame(); } gbIsHellfireSaveGame = gbIsHellfire; } void SaveHeroItems(SaveWriter &saveWriter, Player &player) { size_t itemCount = static_cast(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; SaveHelper file(saveWriter, "heroitems", itemCount * (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize) + sizeof(uint8_t)); file.WriteLE(gbIsHellfire ? 1 : 0); for (const Item &item : player.InvBody) SaveItem(file, item); for (const Item &item : player.InvList) SaveItem(file, item); for (const Item &item : player.SpdList) SaveItem(file, item); } void SaveStash(SaveWriter &stashWriter) { const char *filename; if (!gbIsMultiplayer) filename = "spstashitems"; else filename = "mpstashitems"; const int itemSize = (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize); SaveHelper file( stashWriter, filename, sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + (sizeof(uint32_t) + 10 * 10 * sizeof(uint16_t)) * Stash.stashGrids.size() + sizeof(uint32_t) + itemSize * Stash.stashList.size() + sizeof(uint32_t)); file.WriteLE(StashVersion); file.WriteLE(Stash.gold); std::vector pagesToSave; for (const auto &stashPage : Stash.stashGrids) { if (std::any_of(stashPage.second.cbegin(), stashPage.second.cend(), [](const auto &row) { return std::any_of(row.cbegin(), row.cend(), [](auto cell) { return cell > 0; }); })) { // found a page that contains at least one item pagesToSave.push_back(stashPage.first); } }; // 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) { file.WriteLE(page); for (const auto &row : Stash.stashGrids[page]) { for (uint16_t cell : row) { file.WriteLE(cell); } } } // 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) { SaveItem(file, item); } file.WriteLE(static_cast(Stash.GetPage())); } void SaveGameData(SaveWriter &saveWriter) { SaveHelper file(saveWriter, "game", 320 * 1024); if (gbIsSpawn && !gbIsHellfire) file.WriteLE(LoadLE32("SHAR")); else if (gbIsSpawn && gbIsHellfire) file.WriteLE(LoadLE32("SHLF")); else if (!gbIsSpawn && gbIsHellfire) file.WriteLE(LoadLE32("HELF")); else if (!gbIsSpawn && !gbIsHellfire) file.WriteLE(LoadLE32("RETL")); else app_fatal(_("Invalid game state")); if (gbIsHellfire) { giNumberOfLevels = 25; giNumberQuests = 24; giNumberOfSmithPremiumItems = 15; } else { giNumberOfLevels = 17; giNumberQuests = 16; giNumberOfSmithPremiumItems = 6; } file.WriteLE(setlevel ? 1 : 0); file.WriteBE(setlvlnum); file.WriteBE(currlevel); file.WriteBE(getHellfireLevelType(leveltype)); file.WriteBE(ViewPosition.x); file.WriteBE(ViewPosition.y); file.WriteLE(invflag ? 1 : 0); file.WriteLE(chrflag ? 1 : 0); file.WriteBE(ActiveMonsterCount); file.WriteBE(ActiveItemCount); // ActiveMissileCount will be a value from 0-125 (for vanilla compatibility). Writing an unsigned value here to avoid // warnings about casting from unsigned to signed, but there's no sign extension issues when reading this as a signed // value later so it doesn't have to match in LoadGameData(). file.WriteBE(static_cast(std::min(Missiles.size(), MaxMissilesForSaveGame))); file.WriteBE(ActiveObjectCount); for (uint8_t i = 0; i < giNumberOfLevels; i++) { file.WriteBE(glSeedTbl[i]); file.WriteBE(getHellfireLevelType(GetLevelType(i))); } Player &myPlayer = *MyPlayer; SavePlayer(file, myPlayer); for (int i = 0; i < giNumberQuests; i++) SaveQuest(&file, i); for (int i = 0; i < MAXPORTAL; i++) SavePortal(&file, i); for (int monstkill : MonsterKillCounts) file.WriteBE(monstkill); // add padding for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES] file.Skip(4 * (MaxMonsters - NUM_MTYPES)); if (leveltype != DTYPE_TOWN) { for (int monsterId : ActiveMonsters) file.WriteBE(monsterId); for (size_t i = 0; i < ActiveMonsterCount; i++) SaveMonster(&file, Monsters[ActiveMonsters[i]]); // Write ActiveMissiles for (uint8_t activeMissile = 0; activeMissile < MaxMissilesForSaveGame; activeMissile++) file.WriteLE(activeMissile); // Write AvailableMissiles for (size_t availableMissiles = Missiles.size(); availableMissiles < MaxMissilesForSaveGame; availableMissiles++) file.WriteLE(static_cast(availableMissiles)); const size_t savedMissiles = std::min(Missiles.size(), MaxMissilesForSaveGame); file.Skip(savedMissiles); // Write Missile Data { auto missilesEnd = Missiles.cbegin(); std::advance(missilesEnd, savedMissiles); for (auto it = Missiles.cbegin(); it != missilesEnd; it++) { SaveMissile(&file, *it); } } for (int objectId : ActiveObjects) file.WriteLE(static_cast(objectId)); for (int objectId : AvailableObjects) file.WriteLE(static_cast(objectId)); for (int i = 0; i < ActiveObjectCount; i++) SaveObject(file, Objects[ActiveObjects[i]]); file.WriteBE(ActiveLightCount); for (uint8_t lightId : ActiveLights) file.WriteLE(lightId); for (int i = 0; i < ActiveLightCount; i++) SaveLighting(&file, &Lights[ActiveLights[i]]); int visionCount = Players.size(); file.WriteBE(visionCount + 1); // VisionId file.WriteBE(visionCount); for (const Player &player : Players) SaveLighting(&file, &VisionList[player.getId()], true); } auto itemIndexes = SaveDroppedItems(file); for (bool uniqueItemFlag : UniqueItemFlags) file.WriteLE(uniqueItemFlag ? 1 : 0); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dLight[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(static_cast(dFlags[i][j] & DungeonFlag::SavedFlags)); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dPlayer[i][j]); } SaveDroppedItemLocations(file, itemIndexes); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteBE(dMonster[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dCorpse[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dObject[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dLight[i][j]); // BUGFIX: dLight got saved already } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dPreLight[i][j]); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(AutomapView[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(TileContainsMissile({ i, j }) ? -1 : 0); // For backwards compatability } } file.WriteBE(numpremium); file.WriteBE(premiumlevel); for (int i = 0; i < giNumberOfSmithPremiumItems; i++) SaveItem(file, premiumitems[i]); file.WriteLE(AutomapActive ? 1 : 0); file.WriteBE(AutoMapScale); SaveAdditionalMissiles(saveWriter); } void SaveGame() { gbValidSaveFile = true; pfile_write_hero(/*writeGameData=*/true); sfile_write_stash(); } void SaveLevel(SaveWriter &saveWriter) { Player &myPlayer = *MyPlayer; DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level if (leveltype == DTYPE_TOWN) glSeedTbl[0] = AdvanceRndSeed(); char szName[MaxMpqPathSize]; GetTempLevelNames(szName); SaveHelper file(saveWriter, szName, 256 * 1024); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dCorpse[i][j]); } } file.WriteBE(ActiveMonsterCount); file.WriteBE(ActiveItemCount); file.WriteBE(ActiveObjectCount); if (leveltype != DTYPE_TOWN) { for (int monsterId : ActiveMonsters) file.WriteBE(monsterId); for (size_t i = 0; i < ActiveMonsterCount; i++) SaveMonster(&file, Monsters[ActiveMonsters[i]]); for (int objectId : ActiveObjects) file.WriteLE(objectId); for (int objectId : AvailableObjects) file.WriteLE(objectId); for (int i = 0; i < ActiveObjectCount; i++) { SaveObject(file, Objects[ActiveObjects[i]]); } } auto itemIndexes = SaveDroppedItems(file); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(static_cast(dFlags[i][j] & DungeonFlag::SavedFlags)); } SaveDroppedItemLocations(file, itemIndexes); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteBE(dMonster[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dObject[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dLight[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dPreLight[i][j]); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(AutomapView[i][j]); } } if (!setlevel) myPlayer._pLvlVisited[currlevel] = true; else myPlayer._pSLvlVisited[setlvlnum] = true; } void LoadLevel() { char szName[MaxMpqPathSize]; std::optional archive = OpenSaveArchive(gSaveNumber); GetTempLevelNames(szName); if (!archive || !archive->HasFile(szName)) GetPermLevelNames(szName); LoadHelper file(std::move(archive), szName); if (!file.IsValid()) app_fatal(_("Unable to open save file archive")); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dCorpse[i][j] = file.NextLE(); } MoveLightsToCorpses(); } ActiveMonsterCount = file.NextBE(); auto savedItemCount = file.NextBE(); ActiveObjectCount = file.NextBE(); if (leveltype != DTYPE_TOWN) { for (int &monsterId : ActiveMonsters) monsterId = file.NextBE(); for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; LoadMonster(&file, monster); if (monster.isUnique() && monster.lightId != NO_LIGHT) Lights[monster.lightId].isInvalid = false; } if (!gbSkipSync) { for (size_t i = 0; i < ActiveMonsterCount; i++) SyncMonsterAnim(Monsters[ActiveMonsters[i]]); } for (int &objectId : ActiveObjects) objectId = file.NextLE(); for (int &objectId : AvailableObjects) objectId = file.NextLE(); for (int i = 0; i < ActiveObjectCount; i++) LoadObject(file, Objects[ActiveObjects[i]]); if (!gbSkipSync) { for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); } } LoadDroppedItems(file, savedItemCount); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dFlags[i][j] = static_cast(file.NextLE()) & DungeonFlag::LoadedFlags; } // skip dItem indexes, this gets populated in LoadDroppedItems file.Skip(MAXDUNX * MAXDUNY); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dMonster[i][j] = file.NextBE(); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dObject[i][j] = file.NextLE(); } file.Skip(MAXDUNY * MAXDUNX); // dLight for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dPreLight[i][j] = file.NextLE(); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) const auto automapView = static_cast(file.NextLE()); AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView; } } // No need to load dLight, we can recreate it accurately from LightList memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh } else { memset(dLight, 0, sizeof(dLight)); } if (!gbSkipSync) { AutomapZoomReset(); ResyncQuests(); RedoMissileFlags(); UpdateLighting = true; } for (Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) Lights[player.lightId].hasChanged = true; } } } // namespace devilution