/** * @file loadsave.cpp * * Implementation of save game functionality. */ #include "loadsave.h" #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 "mpq/mpq_writer.hpp" #include "pfile.h" #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()); } bool NextBool8() { return Next() != 0; } bool NextBool32() { return Next() != 0; } }; class SaveHelper { MpqWriter &m_mpqWriter; const char *m_szFileName_; std::unique_ptr m_buffer_; size_t m_cur_ = 0; size_t m_capacity_; public: SaveHelper(MpqWriter &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.NextLE(); item.AnimInfo.CurrentFrame = file.NextLE() - 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; 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.plrlevel = 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(8); // 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(); player.position.offset.deltaX = file.NextLE(); player.position.offset.deltaY = file.NextLE(); player.position.velocity.deltaX = file.NextLE(); player.position.velocity.deltaY = file.NextLE(); player._pdir = static_cast(file.NextLE()); file.Skip(4); // Unused player._pgfxnum = file.NextLE(); file.Skip(4); // Skip pointer pData player.AnimInfo = {}; player.AnimInfo.TicksPerFrame = file.NextLE() + 1; player.AnimInfo.TickCounterOfCurrentFrame = file.NextLE(); player.AnimInfo.NumberOfFrames = file.NextLE(); player.AnimInfo.CurrentFrame = file.NextLE() - 1; file.Skip(4); // Skip _pAnimWidth file.Skip(4); // Skip _pAnimWidth2 file.Skip(4); // Skip _peflag player._plid = file.NextLE(); player._pvid = file.NextLE(); player._pSpell = static_cast(file.NextLE()); player._pSplType = static_cast(file.NextLE()); player._pSplFrom = file.NextLE(); file.Skip(2); // Alignment player._pTSpell = 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 (int8_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, PLR_NAME_LEN); 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 = BlockBonuses[static_cast(player._pClass)]; player._pHPBase = file.NextLE(); player._pMaxHPBase = file.NextLE(); player._pHitPoints = file.NextLE(); player._pMaxHP = file.NextLE(); file.Skip(sizeof(int32_t)); // 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(sizeof(int32_t)); // 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(); player.position.temp.x = file.NextLE(); player.position.temp.y = file.NextLE(); player.tempDirection = static_cast(file.NextLE()); player.spellLevel = file.NextLE(); file.Skip(4); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking player.position.offset2.deltaX = file.NextLE(); player.position.offset2.deltaY = file.NextLE(); file.Skip(4); // 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(4); // skip _pGFXLoad file.Skip(4 * 8); // Skip pointers _pNAnim player._pNFrames = file.NextLE(); file.Skip(4); // skip _pNWidth file.Skip(4 * 8); // Skip pointers _pWAnim player._pWFrames = file.NextLE(); file.Skip(4); // skip _pWWidth file.Skip(4 * 8); // Skip pointers _pAAnim player._pAFrames = file.NextLE(); file.Skip(4); // skip _pAWidth player._pAFNum = file.NextLE(); file.Skip(4 * 8); // Skip pointers _pLAnim file.Skip(4 * 8); // Skip pointers _pFAnim file.Skip(4 * 8); // Skip pointers _pTAnim player._pSFrames = file.NextLE(); file.Skip(4); // skip _pSWidth player._pSFNum = file.NextLE(); file.Skip(4 * 8); // Skip pointers _pHAnim player._pHFrames = file.NextLE(); file.Skip(4); // skip _pHWidth file.Skip(4 * 8); // Skip pointers _pDAnim player._pDFrames = file.NextLE(); file.Skip(4); // skip _pDWidth file.Skip(4 * 8); // Skip pointers _pBAnim player._pBFrames = file.NextLE(); file.Skip(4); // 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 player._pISplDur = file.NextLE(); 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(); player.pBattleNet = false; } else { player.pDungMsgs2 = 0; player.pBattleNet = file.NextBool8(); } 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(); player.pDifficulty = static_cast<_difficulty>(file.NextLE()); player.pDamAcFlags = static_cast(file.NextLE()); file.Skip(20); // Available bytes CalcPlrItemVals(player, false); // 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 } bool gbSkipSync = false; void LoadMonster(LoadHelper *file, Monster &monster) { monster._mMTidx = file->NextLE(); monster._mmode = static_cast(file->NextLE()); monster._mgoal = static_cast(file->NextLE()); file->Skip(3); // Alignment monster._mgoalvar1 = file->NextLE(); monster._mgoalvar2 = file->NextLE(); monster._mgoalvar3 = file->NextLE(); 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(); monster.position.offset.deltaX = file->NextLE(); monster.position.offset.deltaY = file->NextLE(); monster.position.velocity.deltaX = file->NextLE(); monster.position.velocity.deltaY = file->NextLE(); monster._mdir = static_cast(file->NextLE()); monster._menemy = 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->NextLE(); monster.AnimInfo.TickCounterOfCurrentFrame = file->NextLE(); monster.AnimInfo.NumberOfFrames = file->NextLE(); monster.AnimInfo.CurrentFrame = file->NextLE() - 1; file->Skip(4); // Skip _meflag monster._mDelFlag = file->NextBool32(); monster._mVar1 = file->NextLE(); monster._mVar2 = file->NextLE(); monster._mVar3 = file->NextLE(); monster.position.temp.x = file->NextLE(); monster.position.temp.y = file->NextLE(); monster.position.offset2.deltaX = file->NextLE(); monster.position.offset2.deltaY = file->NextLE(); file->Skip(4); // Skip actionFrame monster._mmaxhp = file->NextLE(); monster._mhitpoints = file->NextLE(); monster._mAi = static_cast<_mai_id>(file->NextLE()); monster._mint = file->NextLE(); file->Skip(2); // Alignment monster._mFlags = file->NextLE(); monster._msquelch = file->NextLE(); file->Skip(3); // Alignment file->Skip(4); // Unused monster.position.last.x = file->NextLE(); monster.position.last.y = file->NextLE(); monster._mRndSeed = file->NextLE(); monster._mAISeed = file->NextLE(); file->Skip(4); // Unused monster._uniqtype = file->NextLE(); monster._uniqtrans = file->NextLE(); monster._udeadval = file->NextLE(); monster.mWhoHit = file->NextLE(); monster.mLevel = file->NextLE(); file->Skip(1); // Alignment monster.mExp = file->NextLE(); if ((monster._mFlags & MFLAG_GOLEM) != 0) // Don't skip for golems monster.mHit = file->NextLE(); else file->Skip(1); // Skip mHit as it's already initialized monster.mMinDamage = file->NextLE(); monster.mMaxDamage = file->NextLE(); file->Skip(1); // Skip mHit2 as it's already initialized monster.mMinDamage2 = file->NextLE(); monster.mMaxDamage2 = file->NextLE(); monster.mArmorClass = file->NextLE(); file->Skip(1); // Alignment monster.mMagicRes = file->NextLE(); file->Skip(2); // Alignment monster.mtalkmsg = static_cast<_speech_id>(file->NextLE()); if (monster.mtalkmsg == TEXT_KING1) // Fix original bad mapping of NONE for monsters monster.mtalkmsg = TEXT_NONE; monster.leader = file->NextLE(); monster.leaderRelation = static_cast(file->NextLE()); monster.packsize = file->NextLE(); monster.mlid = file->NextLE(); if (monster.mlid == 0) monster.mlid = NO_LIGHT; // Correct incorect values in old saves if ((monster._mFlags & MFLAG_BERSERK) != 0) { int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; monster.mlid = AddLight(monster.position.tile, lightRadius); } // Omit pointer mName; // Omit pointer MType; // Omit pointer MData; if (gbSkipSync) return; SyncMonsterAnim(monster); } /** * @brief Recalculate the pack size of monster group that may have underflown */ void SyncPackSize(Monster &leader) { if (leader._uniqtype == 0) return; if (leader._mAi != AI_SCAV) return; leader.packsize = 0; for (int i = 0; i < ActiveMonsterCount; i++) { auto &minion = Monsters[ActiveMonsters[i]]; if (minion.leaderRelation == LeaderRelation::Leashed && &Monsters[minion.leader] == &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 = 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._oLight = file.NextBool32(); object._oAnimFlag = file.NextLE(); 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->_lradius = file->NextLE(); pLight->_lid = file->NextLE(); pLight->_ldel = file->NextBool32(); pLight->_lunflag = 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(); pLight->_lflags = file->NextBool32(); } 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(); } void ConvertLevels() { // 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()) continue; leveltype = GetLevelType(currlevel); LoadLevel(); SaveLevel(); } 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()) continue; LoadLevel(); SaveLevel(); } gbSkipSync = false; // Restor current level state setlevel = tmpSetlevel; setlvlnum = tmpSetlvlnum; currlevel = tmpCurrlevel; leveltype = tmpLeveltype; } void LoadMatchingItems(LoadHelper &file, const int n, Item *pItem) { Item tempItem; for (int i = 0; i < n; i++) { LoadItemData(file, tempItem); if (pItem[i].isEmpty() || tempItem.isEmpty()) continue; if (pItem[i]._iSeed != tempItem._iSeed) continue; if (tempItem.IDidx == IDI_EAR) continue; pItem[i] = tempItem; } } /** * @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(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); file.WriteLE(player.position.offset.deltaX); file.WriteLE(player.position.offset.deltaY); file.WriteLE(player.position.velocity.deltaX); file.WriteLE(player.position.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 int animWidth = player.AnimInfo.celSprite ? player.AnimInfo.celSprite->Width() : 96; file.WriteLE(animWidth); // write _pAnimWidth2 for vanilla compatibility file.WriteLE(CalculateWidth2(animWidth)); file.Skip(); // Skip _peflag file.WriteLE(player._plid); file.WriteLE(player._pvid); file.WriteLE(player._pSpell); file.WriteLE(player._pSplType); file.WriteLE(player._pSplFrom); file.Skip(2); // Alignment file.WriteLE(player._pTSpell); file.Skip(); // Skip _pTSplType file.Skip(3); // Alignment file.WriteLE(player._pRSpell); file.WriteLE(player._pRSplType); file.Skip(3); // Alignment file.WriteLE(player._pSBkSpell); file.Skip(); // Skip _pSBkSplType for (int8_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(player._pSplHotKey[i]); } for (size_t i = 0; i < 4; i++) { file.WriteLE(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, PLR_NAME_LEN); 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); file.WriteLE(player.position.temp.x); file.WriteLE(player.position.temp.y); file.WriteLE(static_cast(player.tempDirection)); file.WriteLE(player.spellLevel); file.Skip(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking file.WriteLE(player.position.offset2.deltaX); file.WriteLE(player.position.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(4 * 8); // Skip pointers _pNAnim file.WriteLE(player._pNFrames); file.Skip(4); // Skip _pNWidth file.Skip(4 * 8); // Skip pointers _pWAnim file.WriteLE(player._pWFrames); file.Skip(4); // Skip _pWWidth file.Skip(4 * 8); // Skip pointers _pAAnim file.WriteLE(player._pAFrames); file.Skip(4); // Skip _pAWidth file.WriteLE(player._pAFNum); file.Skip(4 * 8); // Skip pointers _pLAnim file.Skip(4 * 8); // Skip pointers _pFAnim file.Skip(4 * 8); // Skip pointers _pTAnim file.WriteLE(player._pSFrames); file.Skip(4); // Skip _pSWidth file.WriteLE(player._pSFNum); file.Skip(4 * 8); // Skip pointers _pHAnim file.WriteLE(player._pHFrames); file.Skip(4); // Skip _pHWidth file.Skip(4 * 8); // Skip pointers _pDAnim file.WriteLE(player._pDFrames); file.Skip(4); // Skip _pDWidth file.Skip(4 * 8); // Skip pointers _pBAnim file.WriteLE(player._pBFrames); file.Skip(4); // 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.WriteLE(player._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(player.pBattleNet ? 1 : 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(player.pDifficulty); 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._mMTidx); file->WriteLE(static_cast(monster._mmode)); file->WriteLE(monster._mgoal); file->Skip(3); // Alignment file->WriteLE(monster._mgoalvar1); file->WriteLE(monster._mgoalvar2); file->WriteLE(monster._mgoalvar3); 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); file->WriteLE(monster.position.offset.deltaX); file->WriteLE(monster.position.offset.deltaY); file->WriteLE(monster.position.velocity.deltaX); file->WriteLE(monster.position.velocity.deltaY); file->WriteLE(static_cast(monster._mdir)); file->WriteLE(monster._menemy); 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._mDelFlag ? 1 : 0); file->WriteLE(monster._mVar1); file->WriteLE(monster._mVar2); file->WriteLE(monster._mVar3); file->WriteLE(monster.position.temp.x); file->WriteLE(monster.position.temp.y); file->WriteLE(monster.position.offset2.deltaX); file->WriteLE(monster.position.offset2.deltaY); file->Skip(); // Skip _mVar8 file->WriteLE(monster._mmaxhp); file->WriteLE(monster._mhitpoints); file->WriteLE(monster._mAi); file->WriteLE(monster._mint); file->Skip(2); // Alignment file->WriteLE(monster._mFlags); file->WriteLE(monster._msquelch); file->Skip(3); // Alignment file->Skip(4); // Unused file->WriteLE(monster.position.last.x); file->WriteLE(monster.position.last.y); file->WriteLE(monster._mRndSeed); file->WriteLE(monster._mAISeed); file->Skip(4); // Unused file->WriteLE(monster._uniqtype); file->WriteLE(monster._uniqtrans); file->WriteLE(monster._udeadval); file->WriteLE(monster.mWhoHit); file->WriteLE(monster.mLevel); file->Skip(1); // Alignment file->WriteLE(monster.mExp); file->WriteLE(static_cast(std::min(monster.mHit, std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.mMinDamage); file->WriteLE(monster.mMaxDamage); file->WriteLE(static_cast(std::min(monster.mHit2, std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.mMinDamage2); file->WriteLE(monster.mMaxDamage2); file->WriteLE(monster.mArmorClass); file->Skip(1); // Alignment file->WriteLE(monster.mMagicRes); file->Skip(2); // Alignment file->WriteLE(monster.mtalkmsg == TEXT_NONE ? 0 : monster.mtalkmsg); // Replicate original bad mapping of none for monsters file->WriteLE(monster.leader); file->WriteLE(static_cast(monster.leaderRelation)); file->WriteLE(monster.packsize); // vanilla compatibility if (monster.mlid == NO_LIGHT) file->WriteLE(0); else file->WriteLE(monster.mlid); // Omit pointer mName; // Omit pointer MType; // Omit pointer MData; } void SaveMissile(SaveHelper *file, const Missile &missile) { file->WriteLE(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(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._oLight ? 1 : 0); file.WriteLE(object._oAnimFlag); 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); file.WriteLE(object._oVar1); 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) { file->WriteLE(pLight->position.tile.x); file->WriteLE(pLight->position.tile.y); file->WriteLE(pLight->_lradius); file->WriteLE(pLight->_lid); file->WriteLE(pLight->_ldel ? 1 : 0); file->WriteLE(pLight->_lunflag ? 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(pLight->_lflags ? 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->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 indexMap 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() { constexpr size_t BytesWrittenBySaveMissile = 180; uint32_t missileCountAdditional = (Missiles.size() > MaxMissilesForSaveGame) ? static_cast(Missiles.size() - MaxMissilesForSaveGame) : 0; SaveHelper file(CurrentSaveArchive(), "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 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, SPL_INVALID); std::fill(myPlayer._pSplTHotKey, myPlayer._pSplTHotKey + NumHotkeys, RSPLTYPE_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() { Player &myPlayer = *MyPlayer; SaveHelper file(CurrentSaveArchive(), "hotkeys", HotkeysSize()); // Write the number of spell hotkeys file.WriteLE(static_cast(NumHotkeys)); // Write the spell hotkeys for (auto &spellId : myPlayer._pSplHotKey) { file.WriteLE(spellId); } for (auto &spellType : myPlayer._pSplTHotKey) { file.WriteLE(spellType); } // Write the selected spell last file.WriteLE(myPlayer._pRSpell); file.WriteLE(myPlayer._pRSplType); } void LoadHeroItems(Player &player) { LoadHelper file(OpenSaveArchive(gSaveNumber), "heroitems"); if (!file.IsValid()) return; gbIsHellfireSaveGame = file.NextBool8(); LoadMatchingItems(file, NUM_INVLOC, player.InvBody); LoadMatchingItems(file, NUM_INV_GRID_ELEM, player.InvList); LoadMatchingItems(file, 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 = NUM_INV_GRID_ELEM; 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("%s", _("Unable to open save file archive").c_str()); if (!IsHeaderValid(file.NextLE())) app_fatal("%s", _("Invalid save file").c_str()); 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("%s", _("Player is on a Hellfire only level").c_str()); for (uint8_t i = 0; i < giNumberOfLevels; i++) { glSeedTbl[i] = file.NextBE(); file.Skip(4); // Skip loading gnLevelTypeTbl } Player &myPlayer = *MyPlayer; LoadPlayer(file, myPlayer); sgGameInitInfo.nDifficulty = myPlayer.pDifficulty; 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) { ConvertLevels(); RemoveEmptyInventory(myPlayer); } LoadGameLevel(firstflag, ENTRY_LOAD); SyncInitPlr(MyPlayerId); SyncPlrAnim(MyPlayerId); ViewPosition = { viewX, viewY }; ActiveMonsterCount = tmpNummonsters; ActiveObjectCount = tmpNobjects; for (int &monstkill : MonsterKillCounts) monstkill = file.NextBE(); if (leveltype != DTYPE_TOWN) { for (int &monsterId : ActiveMonsters) monsterId = file.NextBE(); for (int i = 0; i < ActiveMonsterCount; i++) LoadMonster(&file, Monsters[ActiveMonsters[i]]); for (int 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 (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]]); VisionId = file.NextBE(); VisionCount = file.NextBE(); for (int i = 0; i < VisionCount; i++) LoadLighting(&file, &VisionList[i]); } LoadDroppedItems(file, savedItemCount); LoadAdditionalMissiles(); for (bool &uniqueItemFlag : UniqueItemFlags) uniqueItemFlag = file.NextBool8(); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dLight[i][j] = file.NextLE(); } 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(); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dLight[i][j] = file.NextLE(); } 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) AutomapView[i][j] = file.NextLE(); } file.Skip(MAXDUNX * MAXDUNY); // dMissile } numpremium = file.NextBE(); premiumlevel = file.NextBE(); for (int i = 0; i < giNumberOfSmithPremiumItems; i++) LoadPremium(file, i); if (gbIsHellfire && !gbIsHellfireSaveGame) SpawnPremium(MyPlayerId); AutomapActive = file.NextBool8(); AutoMapScale = file.NextBE(); AutomapZoomReset(); ResyncQuests(); if (leveltype != DTYPE_TOWN) ProcessLightList(); RedoPlayerVision(); ProcessVisionList(); // convert stray manashield missiles into pManaShield flag for (auto &missile : Missiles) { if (missile._mitype == MIS_MANASHIELD && !missile._miDelFlag) { Players[missile._misource].pManaShield = true; missile._miDelFlag = true; } } missiles_process_charge(); RedoMissileFlags(); NewCursor(CURSOR_HAND); gbProcessPlayers = true; if (gbIsHellfireSaveGame != gbIsHellfire) { SaveGame(); } gbIsHellfireSaveGame = gbIsHellfire; } void SaveHeroItems(Player &player) { size_t itemCount = NUM_INVLOC + NUM_INV_GRID_ELEM + MAXBELTITEMS; SaveHelper file(CurrentSaveArchive(), "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() { const char *filename; if (!gbIsMultiplayer) filename = "spstashitems"; else filename = "mpstashitems"; const int itemSize = (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize); SaveHelper file( StashArchive(), 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() { SaveHelper file(CurrentSaveArchive(), "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("%s", _("Invalid game state").c_str()); 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); leveltype = GetLevelType(currlevel); for (uint8_t i = 0; i < giNumberOfLevels; i++) { file.WriteBE(glSeedTbl[i]); file.WriteBE(getHellfireLevelType(GetLevelType(i))); } Player &myPlayer = *MyPlayer; myPlayer.pDifficulty = sgGameInitInfo.nDifficulty; 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); if (leveltype != DTYPE_TOWN) { for (int monsterId : ActiveMonsters) file.WriteBE(monsterId); for (int 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]]); file.WriteBE(VisionId); file.WriteBE(VisionCount); for (int i = 0; i < VisionCount; i++) SaveLighting(&file, &VisionList[i]); } 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]); } 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(); } void SaveGame() { gbValidSaveFile = true; pfile_write_hero(/*writeGameData=*/true); sfile_write_stash(); } void SaveLevel() { PFileScopedArchiveWriter scopedWriter; 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[MAX_PATH]; GetTempLevelNames(szName); SaveHelper file(CurrentSaveArchive(), 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 (int 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[MAX_PATH]; GetPermLevelNames(szName); LoadHelper file(OpenSaveArchive(gSaveNumber), szName); if (!file.IsValid()) app_fatal("%s", _("Unable to open save file archive").c_str()); 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(); } SyncUniqDead(); } ActiveMonsterCount = file.NextBE(); auto savedItemCount = file.NextBE(); ActiveObjectCount = file.NextBE(); if (leveltype != DTYPE_TOWN) { for (int &monsterId : ActiveMonsters) monsterId = file.NextBE(); for (int i = 0; i < ActiveMonsterCount; i++) LoadMonster(&file, 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(); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dLight[i][j] = file.NextLE(); } 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; } } } if (!gbSkipSync) { AutomapZoomReset(); ResyncQuests(); RedoMissileFlags(); UpdateLighting = true; } for (Player &player : Players) { if (player.plractive && currlevel == player.plrlevel) Lights[player._plid]._lunflag = true; } } } // namespace devilution