devilutionX/Source/loadsave.cpp
ephphatha 9a2002e200 Get rid of RemoveEmptyLevelItems
This was a cleanup function to handle converting saves between Hellfire <-> Diablo, mainly when a hellfire specific item was dropped and should be removed from a Diablo game. This had an off by one with the way it iterated backwards over the active items list which meant it always called DeleteItem at least once (since it uses an invalid item past the end of the active items range). DeleteItem then always decremented the ActiveItems count. When saving the game after converting the items we then end up with a dropped items array containing an index to an item that is no longer considered active.

However the code in LoadDroppedItems already checks that the conversion was successful before considering it active, so this function doesn't need to be called. Instead we can just save if we converted game mode.
2022-02-08 12:10:42 +01:00

2323 lines
76 KiB
C++

/**
* @file loadsave.cpp
*
* Implementation of save game functionality.
*/
#include "loadsave.h"
#include <climits>
#include <cstring>
#include <numeric>
#include <unordered_map>
#include <SDL.h>
#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 "missiles.h"
#include "mpq/mpq_writer.hpp"
#include "pfile.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 <class T>
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 <class T>
T SwapBE(T in)
{
switch (sizeof(T)) {
case 2:
return SDL_SwapBE16(in);
case 4:
return SDL_SwapBE32(in);
case 8:
return SDL_SwapBE64(in);
default:
return in;
}
}
class LoadHelper {
std::unique_ptr<byte[]> m_buffer_;
size_t m_cur_ = 0;
size_t m_size_;
template <class T>
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(const char *szFileName)
{
m_buffer_ = pfile_read(szFileName, &m_size_);
}
bool IsValid(size_t size = 1)
{
return m_buffer_ != nullptr
&& m_size_ >= (m_cur_ + size);
}
template <typename T>
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 <class T>
T NextLE()
{
return SwapLE(Next<T>());
}
template <class T>
T NextBE()
{
return SwapBE(Next<T>());
}
bool NextBool8()
{
return Next<uint8_t>() != 0;
}
bool NextBool32()
{
return Next<uint32_t>() != 0;
}
};
class SaveHelper {
const char *m_szFileName_;
std::unique_ptr<byte[]> m_buffer_;
size_t m_cur_ = 0;
size_t m_capacity_;
public:
SaveHelper(const char *szFileName, size_t bufferLen)
: 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 <typename T>
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 <class T>
void WriteLE(T value)
{
value = SwapLE(value);
WriteBytes(&value, sizeof(value));
}
template <class T>
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);
CurrentSaveArchive().WriteFile(m_szFileName_, m_buffer_.get(), encodedLen);
}
};
void LoadItemData(LoadHelper &file, Item &item)
{
item._iSeed = file.NextLE<int32_t>();
item._iCreateInfo = file.NextLE<uint16_t>();
file.Skip(2); // Alignment
item._itype = static_cast<ItemType>(file.NextLE<uint32_t>());
item.position.x = file.NextLE<int32_t>();
item.position.y = file.NextLE<int32_t>();
item._iAnimFlag = file.NextBool32();
file.Skip(4); // Skip pointer _iAnimData
item.AnimInfo = {};
item.AnimInfo.NumberOfFrames = file.NextLE<int32_t>();
item.AnimInfo.CurrentFrame = file.NextLE<int32_t>();
file.Skip(8); // Skip _iAnimWidth and _iAnimWidth2
file.Skip(4); // Unused since 1.02
item._iSelFlag = file.NextLE<uint8_t>();
file.Skip(3); // Alignment
item._iPostDraw = file.NextBool32();
item._iIdentified = file.NextBool32();
item._iMagical = static_cast<item_quality>(file.NextLE<int8_t>());
file.NextBytes(item._iName, 64);
file.NextBytes(item._iIName, 64);
item._iLoc = static_cast<item_equip_type>(file.NextLE<int8_t>());
item._iClass = static_cast<item_class>(file.NextLE<uint8_t>());
file.Skip(1); // Alignment
item._iCurs = file.NextLE<int32_t>();
item._ivalue = file.NextLE<int32_t>();
item._iIvalue = file.NextLE<int32_t>();
item._iMinDam = file.NextLE<int32_t>();
item._iMaxDam = file.NextLE<int32_t>();
item._iAC = file.NextLE<int32_t>();
item._iFlags = file.NextLE<uint32_t>();
item._iMiscId = static_cast<item_misc_id>(file.NextLE<int32_t>());
item._iSpell = static_cast<spell_id>(file.NextLE<int32_t>());
item._iCharges = file.NextLE<int32_t>();
item._iMaxCharges = file.NextLE<int32_t>();
item._iDurability = file.NextLE<int32_t>();
item._iMaxDur = file.NextLE<int32_t>();
item._iPLDam = file.NextLE<int32_t>();
item._iPLToHit = file.NextLE<int32_t>();
item._iPLAC = file.NextLE<int32_t>();
item._iPLStr = file.NextLE<int32_t>();
item._iPLMag = file.NextLE<int32_t>();
item._iPLDex = file.NextLE<int32_t>();
item._iPLVit = file.NextLE<int32_t>();
item._iPLFR = file.NextLE<int32_t>();
item._iPLLR = file.NextLE<int32_t>();
item._iPLMR = file.NextLE<int32_t>();
item._iPLMana = file.NextLE<int32_t>();
item._iPLHP = file.NextLE<int32_t>();
item._iPLDamMod = file.NextLE<int32_t>();
item._iPLGetHit = file.NextLE<int32_t>();
item._iPLLight = file.NextLE<int32_t>();
item._iSplLvlAdd = file.NextLE<int8_t>();
item._iRequest = file.NextBool8();
file.Skip(2); // Alignment
item._iUid = file.NextLE<int32_t>();
item._iFMinDam = file.NextLE<int32_t>();
item._iFMaxDam = file.NextLE<int32_t>();
item._iLMinDam = file.NextLE<int32_t>();
item._iLMaxDam = file.NextLE<int32_t>();
item._iPLEnAc = file.NextLE<int32_t>();
item._iPrePower = static_cast<item_effect_type>(file.NextLE<int8_t>());
item._iSufPower = static_cast<item_effect_type>(file.NextLE<int8_t>());
file.Skip(2); // Alignment
item._iVAdd1 = file.NextLE<int32_t>();
item._iVMult1 = file.NextLE<int32_t>();
item._iVAdd2 = file.NextLE<int32_t>();
item._iVMult2 = file.NextLE<int32_t>();
item._iMinStr = file.NextLE<int8_t>();
item._iMinMag = file.NextLE<uint8_t>();
item._iMinDex = file.NextLE<int8_t>();
file.Skip(1); // Alignment
item._iStatFlag = file.NextBool32();
item.IDidx = static_cast<_item_indexes>(file.NextLE<int32_t>());
if (gbIsSpawn) {
item.IDidx = RemapItemIdxFromSpawn(item.IDidx);
}
if (!gbIsHellfireSaveGame) {
item.IDidx = RemapItemIdxFromDiablo(item.IDidx);
}
item.dwBuff = file.NextLE<uint32_t>();
if (gbIsHellfireSaveGame)
item._iDamAcFlags = file.NextLE<uint32_t>();
else
item._iDamAcFlags = 0;
RemoveInvalidItem(item);
}
void LoadPlayer(LoadHelper &file, Player &player)
{
player._pmode = static_cast<PLR_MODE>(file.NextLE<int32_t>());
for (int8_t &step : player.walkpath) {
step = file.NextLE<int8_t>();
}
player.plractive = file.NextBool8();
file.Skip(2); // Alignment
player.destAction = static_cast<action_id>(file.NextLE<int32_t>());
player.destParam1 = file.NextLE<int32_t>();
player.destParam2 = file.NextLE<int32_t>();
player.destParam3 = file.NextLE<int32_t>();
player.destParam4 = file.NextLE<int32_t>();
player.plrlevel = file.NextLE<uint32_t>();
player.position.tile.x = file.NextLE<int32_t>();
player.position.tile.y = file.NextLE<int32_t>();
player.position.future.x = file.NextLE<int32_t>();
player.position.future.y = file.NextLE<int32_t>();
file.Skip(8); // Skip _ptargx and _ptargy
player.position.last.x = file.NextLE<int32_t>();
player.position.last.y = file.NextLE<int32_t>();
player.position.old.x = file.NextLE<int32_t>();
player.position.old.y = file.NextLE<int32_t>();
player.position.offset.deltaX = file.NextLE<int32_t>();
player.position.offset.deltaY = file.NextLE<int32_t>();
player.position.velocity.deltaX = file.NextLE<int32_t>();
player.position.velocity.deltaY = file.NextLE<int32_t>();
player._pdir = static_cast<Direction>(file.NextLE<int32_t>());
file.Skip(4); // Unused
player._pgfxnum = file.NextLE<int32_t>();
file.Skip(4); // Skip pointer pData
player.AnimInfo = {};
player.AnimInfo.TicksPerFrame = file.NextLE<int32_t>() + 1;
player.AnimInfo.TickCounterOfCurrentFrame = file.NextLE<int32_t>();
player.AnimInfo.NumberOfFrames = file.NextLE<int32_t>();
player.AnimInfo.CurrentFrame = file.NextLE<int32_t>();
file.Skip(4); // Skip _pAnimWidth
file.Skip(4); // Skip _pAnimWidth2
file.Skip(4); // Skip _peflag
player._plid = file.NextLE<int32_t>();
player._pvid = file.NextLE<int32_t>();
player._pSpell = static_cast<spell_id>(file.NextLE<int32_t>());
player._pSplType = static_cast<spell_type>(file.NextLE<int8_t>());
player._pSplFrom = file.NextLE<int8_t>();
file.Skip(2); // Alignment
player._pTSpell = static_cast<spell_id>(file.NextLE<int32_t>());
file.Skip<int8_t>(); // Skip _pTSplType
file.Skip(3); // Alignment
player._pRSpell = static_cast<spell_id>(file.NextLE<int32_t>());
player._pRSplType = static_cast<spell_type>(file.NextLE<int8_t>());
file.Skip(3); // Alignment
player._pSBkSpell = static_cast<spell_id>(file.NextLE<int32_t>());
file.Skip<int8_t>(); // Skip _pSBkSplType
for (int8_t &spellLevel : player._pSplLvl)
spellLevel = file.NextLE<int8_t>();
file.Skip(7); // Alignment
player._pMemSpells = file.NextLE<uint64_t>();
player._pAblSpells = file.NextLE<uint64_t>();
player._pScrlSpells = file.NextLE<uint64_t>();
player._pSpellFlags = file.NextLE<uint8_t>();
file.Skip(3); // Alignment
for (auto &spell : player._pSplHotKey)
spell = static_cast<spell_id>(file.NextLE<int32_t>());
for (auto &spellType : player._pSplTHotKey)
spellType = static_cast<spell_type>(file.NextLE<int8_t>());
file.Skip<int32_t>(); // Skip _pwtype
player._pBlockFlag = file.NextBool8();
player._pInvincible = file.NextBool8();
player._pLightRad = file.NextLE<int8_t>();
player._pLvlChanging = file.NextBool8();
file.NextBytes(player._pName, PLR_NAME_LEN);
player._pClass = static_cast<HeroClass>(file.NextLE<int8_t>());
file.Skip(3); // Alignment
player._pStrength = file.NextLE<int32_t>();
player._pBaseStr = file.NextLE<int32_t>();
player._pMagic = file.NextLE<int32_t>();
player._pBaseMag = file.NextLE<int32_t>();
player._pDexterity = file.NextLE<int32_t>();
player._pBaseDex = file.NextLE<int32_t>();
player._pVitality = file.NextLE<int32_t>();
player._pBaseVit = file.NextLE<int32_t>();
player._pStatPts = file.NextLE<int32_t>();
player._pDamageMod = file.NextLE<int32_t>();
player._pBaseToBlk = file.NextLE<int32_t>();
if (player._pBaseToBlk == 0)
player._pBaseToBlk = BlockBonuses[static_cast<std::size_t>(player._pClass)];
player._pHPBase = file.NextLE<int32_t>();
player._pMaxHPBase = file.NextLE<int32_t>();
player._pHitPoints = file.NextLE<int32_t>();
player._pMaxHP = file.NextLE<int32_t>();
file.Skip(sizeof(int32_t)); // Skip _pHPPer - always derived from hp and maxHP.
player._pManaBase = file.NextLE<int32_t>();
player._pMaxManaBase = file.NextLE<int32_t>();
player._pMana = file.NextLE<int32_t>();
player._pMaxMana = file.NextLE<int32_t>();
file.Skip(sizeof(int32_t)); // Skip _pManaPer - always derived from mana and maxMana
player._pLevel = file.NextLE<int8_t>();
player._pMaxLvl = file.NextLE<int8_t>();
file.Skip(2); // Alignment
player._pExperience = file.NextLE<uint32_t>();
file.Skip<uint32_t>(); // Skip _pMaxExp - unused
player._pNextExper = file.NextLE<uint32_t>(); // This can be calculated based on pLevel (which in turn could be calculated based on pExperience)
player._pArmorClass = file.NextLE<int8_t>();
player._pMagResist = file.NextLE<int8_t>();
player._pFireResist = file.NextLE<int8_t>();
player._pLghtResist = file.NextLE<int8_t>();
player._pGold = file.NextLE<int32_t>();
player._pInfraFlag = file.NextBool32();
player.position.temp.x = file.NextLE<int32_t>();
player.position.temp.y = file.NextLE<int32_t>();
player.tempDirection = static_cast<Direction>(file.NextLE<int32_t>());
player.spellLevel = file.NextLE<int32_t>();
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<int32_t>();
player.position.offset2.deltaY = file.NextLE<int32_t>();
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<int32_t>();
file.Skip(4); // skip _pNWidth
file.Skip(4 * 8); // Skip pointers _pWAnim
player._pWFrames = file.NextLE<int32_t>();
file.Skip(4); // skip _pWWidth
file.Skip(4 * 8); // Skip pointers _pAAnim
player._pAFrames = file.NextLE<int32_t>();
file.Skip(4); // skip _pAWidth
player._pAFNum = file.NextLE<int32_t>();
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<int32_t>();
file.Skip(4); // skip _pSWidth
player._pSFNum = file.NextLE<int32_t>();
file.Skip(4 * 8); // Skip pointers _pHAnim
player._pHFrames = file.NextLE<int32_t>();
file.Skip(4); // skip _pHWidth
file.Skip(4 * 8); // Skip pointers _pDAnim
player._pDFrames = file.NextLE<int32_t>();
file.Skip(4); // skip _pDWidth
file.Skip(4 * 8); // Skip pointers _pBAnim
player._pBFrames = file.NextLE<int32_t>();
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<int32_t>();
for (int8_t &cell : player.InvGrid)
cell = file.NextLE<int8_t>();
for (Item &item : player.SpdList)
LoadItemData(file, item);
LoadItemData(file, player.HoldItem);
player._pIMinDam = file.NextLE<int32_t>();
player._pIMaxDam = file.NextLE<int32_t>();
player._pIAC = file.NextLE<int32_t>();
player._pIBonusDam = file.NextLE<int32_t>();
player._pIBonusToHit = file.NextLE<int32_t>();
player._pIBonusAC = file.NextLE<int32_t>();
player._pIBonusDamMod = file.NextLE<int32_t>();
file.Skip(4); // Alignment
player._pISpells = file.NextLE<uint64_t>();
player._pIFlags = file.NextLE<int32_t>();
player._pIGetHit = file.NextLE<int32_t>();
player._pISplLvlAdd = file.NextLE<int8_t>();
file.Skip(1); // Unused
file.Skip(2); // Alignment
player._pISplDur = file.NextLE<int32_t>();
player._pIEnAc = file.NextLE<int32_t>();
player._pIFMinDam = file.NextLE<int32_t>();
player._pIFMaxDam = file.NextLE<int32_t>();
player._pILMinDam = file.NextLE<int32_t>();
player._pILMaxDam = file.NextLE<int32_t>();
player._pOilType = static_cast<item_misc_id>(file.NextLE<int32_t>());
player.pTownWarps = file.NextLE<uint8_t>();
player.pDungMsgs = file.NextLE<uint8_t>();
player.pLvlLoad = file.NextLE<uint8_t>();
if (gbIsHellfireSaveGame) {
player.pDungMsgs2 = file.NextLE<uint8_t>();
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<uint16_t>();
file.Skip(14); // Available bytes
player.pDiabloKillLevel = file.NextLE<uint32_t>();
player.pDifficulty = static_cast<_difficulty>(file.NextLE<uint32_t>());
player.pDamAcFlags = file.NextLE<uint32_t>();
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<int32_t>();
monster._mmode = static_cast<MonsterMode>(file->NextLE<int32_t>());
monster._mgoal = static_cast<monster_goal>(file->NextLE<uint8_t>());
file->Skip(3); // Alignment
monster._mgoalvar1 = file->NextLE<int32_t>();
monster._mgoalvar2 = file->NextLE<int32_t>();
monster._mgoalvar3 = file->NextLE<int32_t>();
file->Skip(4); // Unused
monster._pathcount = file->NextLE<uint8_t>();
file->Skip(3); // Alignment
monster.position.tile.x = file->NextLE<int32_t>();
monster.position.tile.y = file->NextLE<int32_t>();
monster.position.future.x = file->NextLE<int32_t>();
monster.position.future.y = file->NextLE<int32_t>();
monster.position.old.x = file->NextLE<int32_t>();
monster.position.old.y = file->NextLE<int32_t>();
monster.position.offset.deltaX = file->NextLE<int32_t>();
monster.position.offset.deltaY = file->NextLE<int32_t>();
monster.position.velocity.deltaX = file->NextLE<int32_t>();
monster.position.velocity.deltaY = file->NextLE<int32_t>();
monster._mdir = static_cast<Direction>(file->NextLE<int32_t>());
monster._menemy = file->NextLE<int32_t>();
monster.enemyPosition.x = file->NextLE<uint8_t>();
monster.enemyPosition.y = file->NextLE<uint8_t>();
file->Skip(2); // Unused
file->Skip(4); // Skip pointer _mAnimData
monster.AnimInfo = {};
monster.AnimInfo.TicksPerFrame = file->NextLE<int32_t>();
monster.AnimInfo.TickCounterOfCurrentFrame = file->NextLE<int32_t>();
monster.AnimInfo.NumberOfFrames = file->NextLE<int32_t>();
monster.AnimInfo.CurrentFrame = file->NextLE<int32_t>();
file->Skip(4); // Skip _meflag
monster._mDelFlag = file->NextBool32();
monster._mVar1 = file->NextLE<int32_t>();
monster._mVar2 = file->NextLE<int32_t>();
monster._mVar3 = file->NextLE<int32_t>();
monster.position.temp.x = file->NextLE<int32_t>();
monster.position.temp.y = file->NextLE<int32_t>();
monster.position.offset2.deltaX = file->NextLE<int32_t>();
monster.position.offset2.deltaY = file->NextLE<int32_t>();
file->Skip(4); // Skip actionFrame
monster._mmaxhp = file->NextLE<int32_t>();
monster._mhitpoints = file->NextLE<int32_t>();
monster._mAi = static_cast<_mai_id>(file->NextLE<uint8_t>());
monster._mint = file->NextLE<uint8_t>();
file->Skip(2); // Alignment
monster._mFlags = file->NextLE<uint32_t>();
monster._msquelch = file->NextLE<uint8_t>();
file->Skip(3); // Alignment
file->Skip(4); // Unused
monster.position.last.x = file->NextLE<int32_t>();
monster.position.last.y = file->NextLE<int32_t>();
monster._mRndSeed = file->NextLE<uint32_t>();
monster._mAISeed = file->NextLE<uint32_t>();
file->Skip(4); // Unused
monster._uniqtype = file->NextLE<uint8_t>();
monster._uniqtrans = file->NextLE<uint8_t>();
monster._udeadval = file->NextLE<int8_t>();
monster.mWhoHit = file->NextLE<int8_t>();
monster.mLevel = file->NextLE<int8_t>();
file->Skip(1); // Alignment
monster.mExp = file->NextLE<uint16_t>();
if ((monster._mFlags & MFLAG_GOLEM) != 0) // Don't skip for golems
monster.mHit = file->NextLE<uint8_t>();
else
file->Skip(1); // Skip mHit as it's already initialized
monster.mMinDamage = file->NextLE<uint8_t>();
monster.mMaxDamage = file->NextLE<uint8_t>();
file->Skip(1); // Skip mHit2 as it's already initialized
monster.mMinDamage2 = file->NextLE<uint8_t>();
monster.mMaxDamage2 = file->NextLE<uint8_t>();
monster.mArmorClass = file->NextLE<uint8_t>();
file->Skip(1); // Alignment
monster.mMagicRes = file->NextLE<uint16_t>();
file->Skip(2); // Alignment
monster.mtalkmsg = static_cast<_speech_id>(file->NextLE<int32_t>());
if (monster.mtalkmsg == TEXT_KING1) // Fix original bad mapping of NONE for monsters
monster.mtalkmsg = TEXT_NONE;
monster.leader = file->NextLE<uint8_t>();
monster.leaderRelation = static_cast<LeaderRelation>(file->NextLE<uint8_t>());
monster.packsize = file->NextLE<uint8_t>();
monster.mlid = file->NextLE<int8_t>();
if (monster.mlid == 0)
monster.mlid = NO_LIGHT; // Correct incorect values in old saves
if ((monster._mFlags & MFLAG_BERSERK) != 0) {
int lightRadius = (currlevel < 17 || currlevel > 20) ? 3 : 9;
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<missile_id>(file->NextLE<int32_t>());
missile.position.tile.x = file->NextLE<int32_t>();
missile.position.tile.y = file->NextLE<int32_t>();
missile.position.offset.deltaX = file->NextLE<int32_t>();
missile.position.offset.deltaY = file->NextLE<int32_t>();
missile.position.velocity.deltaX = file->NextLE<int32_t>();
missile.position.velocity.deltaY = file->NextLE<int32_t>();
missile.position.start.x = file->NextLE<int32_t>();
missile.position.start.y = file->NextLE<int32_t>();
missile.position.traveled.deltaX = file->NextLE<int32_t>();
missile.position.traveled.deltaY = file->NextLE<int32_t>();
missile._mimfnum = file->NextLE<int32_t>();
missile._mispllvl = file->NextLE<int32_t>();
missile._miDelFlag = file->NextBool32();
missile._miAnimType = file->NextLE<uint8_t>();
file->Skip(3); // Alignment
missile._miAnimFlags = static_cast<MissileDataFlags>(file->NextLE<int32_t>());
file->Skip(4); // Skip pointer _miAnimData
missile._miAnimDelay = file->NextLE<int32_t>();
missile._miAnimLen = file->NextLE<int32_t>();
missile._miAnimWidth = file->NextLE<int32_t>();
missile._miAnimWidth2 = file->NextLE<int32_t>();
missile._miAnimCnt = file->NextLE<int32_t>();
missile._miAnimAdd = file->NextLE<int32_t>();
missile._miAnimFrame = file->NextLE<int32_t>();
missile._miDrawFlag = file->NextBool32();
missile._miLightFlag = file->NextBool32();
missile._miPreFlag = file->NextBool32();
missile._miUniqTrans = file->NextLE<uint32_t>();
missile._mirange = file->NextLE<int32_t>();
missile._misource = file->NextLE<int32_t>();
missile._micaster = static_cast<mienemy_type>(file->NextLE<int32_t>());
missile._midam = file->NextLE<int32_t>();
missile._miHitFlag = file->NextBool32();
missile._midist = file->NextLE<int32_t>();
missile._mlid = file->NextLE<int32_t>();
missile._mirnd = file->NextLE<int32_t>();
missile.var1 = file->NextLE<int32_t>();
missile.var2 = file->NextLE<int32_t>();
missile.var3 = file->NextLE<int32_t>();
missile.var4 = file->NextLE<int32_t>();
missile.var5 = file->NextLE<int32_t>();
missile.var6 = file->NextLE<int32_t>();
missile.var7 = file->NextLE<int32_t>();
missile.limitReached = file->NextBool32();
missile.lastCollisionTargetHash = 0;
if (Missiles.size() < Missiles.max_size()) {
Missiles.push_back(missile);
}
}
void LoadObject(LoadHelper &file, Object &object)
{
object._otype = static_cast<_object_id>(file.NextLE<int32_t>());
object.position.x = file.NextLE<int32_t>();
object.position.y = file.NextLE<int32_t>();
object._oLight = file.NextBool32();
object._oAnimFlag = file.NextLE<uint32_t>();
file.Skip(4); // Skip pointer _oAnimData
object._oAnimDelay = file.NextLE<int32_t>();
object._oAnimCnt = file.NextLE<int32_t>();
object._oAnimLen = file.NextLE<uint32_t>();
object._oAnimFrame = file.NextLE<uint32_t>();
object._oAnimWidth = file.NextLE<int32_t>();
file.Skip(4); // Skip _oAnimWidth2
object._oDelFlag = file.NextBool32();
object._oBreak = file.NextLE<int8_t>();
file.Skip(3); // Alignment
object._oSolidFlag = file.NextBool32();
object._oMissFlag = file.NextBool32();
object._oSelFlag = file.NextLE<int8_t>();
file.Skip(3); // Alignment
object._oPreFlag = file.NextBool32();
object._oTrapFlag = file.NextBool32();
object._oDoorFlag = file.NextBool32();
object._olid = file.NextLE<int32_t>();
object._oRndSeed = file.NextLE<uint32_t>();
object._oVar1 = file.NextLE<int32_t>();
object._oVar2 = file.NextLE<int32_t>();
object._oVar3 = file.NextLE<int32_t>();
object._oVar4 = file.NextLE<int32_t>();
object._oVar5 = file.NextLE<int32_t>();
object._oVar6 = file.NextLE<uint32_t>();
object.bookMessage = static_cast<_speech_id>(file.NextLE<int32_t>());
object._oVar8 = file.NextLE<int32_t>();
}
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<uint8_t>();
file->Skip<uint8_t>(); // _qtype, identical to _qidx
quest._qactive = static_cast<quest_state>(file->NextLE<uint8_t>());
quest._qlvltype = static_cast<dungeon_type>(file->NextLE<uint8_t>());
quest.position.x = file->NextLE<int32_t>();
quest.position.y = file->NextLE<int32_t>();
quest._qslvl = static_cast<_setlevels>(file->NextLE<uint8_t>());
quest._qidx = static_cast<quest_id>(file->NextLE<uint8_t>());
if (gbIsHellfireSaveGame) {
file->Skip(2); // Alignment
quest._qmsg = static_cast<_speech_id>(file->NextLE<int32_t>());
} else {
quest._qmsg = static_cast<_speech_id>(file->NextLE<uint8_t>());
}
quest._qvar1 = file->NextLE<uint8_t>();
quest._qvar2 = file->NextLE<uint8_t>();
file->Skip(2); // Alignment
if (!gbIsHellfireSaveGame)
file->Skip(1); // Alignment
quest._qlog = file->NextBool32();
ReturnLvlPosition.x = file->NextBE<int32_t>();
ReturnLvlPosition.y = file->NextBE<int32_t>();
ReturnLevel = file->NextBE<int32_t>();
ReturnLevelType = static_cast<dungeon_type>(file->NextBE<int32_t>());
file->Skip(sizeof(int32_t)); // Skip DoomQuestState
}
void LoadLighting(LoadHelper *file, Light *pLight)
{
pLight->position.tile.x = file->NextLE<int32_t>();
pLight->position.tile.y = file->NextLE<int32_t>();
pLight->_lradius = file->NextLE<int32_t>();
pLight->_lid = file->NextLE<int32_t>();
pLight->_ldel = file->NextBool32();
pLight->_lunflag = file->NextBool32();
file->Skip(4); // Unused
pLight->position.old.x = file->NextLE<int32_t>();
pLight->position.old.y = file->NextLE<int32_t>();
pLight->oldRadius = file->NextLE<int32_t>();
pLight->position.offset.x = file->NextLE<int32_t>();
pLight->position.offset.y = file->NextLE<int32_t>();
pLight->_lflags = file->NextBool32();
}
void LoadPortal(LoadHelper *file, int i)
{
Portal *pPortal = &Portals[i];
pPortal->open = file->NextBool32();
pPortal->position.x = file->NextLE<int32_t>();
pPortal->position.y = file->NextLE<int32_t>();
pPortal->level = file->NextLE<int32_t>();
pPortal->ltype = static_cast<dungeon_type>(file->NextLE<int32_t>());
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 = gnLevelTypeTbl[i];
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;
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<uint8_t>(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;
}
}
}
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<int32_t>(item._iSeed);
file.WriteLE<int16_t>(item._iCreateInfo);
file.Skip(2); // Alignment
file.WriteLE<int32_t>(static_cast<int32_t>(iType));
file.WriteLE<int32_t>(item.position.x);
file.WriteLE<int32_t>(item.position.y);
file.WriteLE<uint32_t>(item._iAnimFlag ? 1 : 0);
file.Skip(4); // Skip pointer _iAnimData
file.WriteLE<int32_t>(item.AnimInfo.NumberOfFrames);
file.WriteLE<int32_t>(item.AnimInfo.CurrentFrame);
// write _iAnimWidth for vanilla compatibility
file.WriteLE<int32_t>(ItemAnimWidth);
// write _iAnimWidth2 for vanilla compatibility
file.WriteLE<int32_t>(CalculateWidth2(ItemAnimWidth));
file.Skip<uint32_t>(); // _delFlag, unused since 1.02
file.WriteLE<uint8_t>(item._iSelFlag);
file.Skip(3); // Alignment
file.WriteLE<uint32_t>(item._iPostDraw ? 1 : 0);
file.WriteLE<uint32_t>(item._iIdentified ? 1 : 0);
file.WriteLE<int8_t>(item._iMagical);
file.WriteBytes(item._iName, 64);
file.WriteBytes(item._iIName, 64);
file.WriteLE<int8_t>(item._iLoc);
file.WriteLE<uint8_t>(item._iClass);
file.Skip(1); // Alignment
file.WriteLE<int32_t>(item._iCurs);
file.WriteLE<int32_t>(item._ivalue);
file.WriteLE<int32_t>(item._iIvalue);
file.WriteLE<int32_t>(item._iMinDam);
file.WriteLE<int32_t>(item._iMaxDam);
file.WriteLE<int32_t>(item._iAC);
file.WriteLE<uint32_t>(item._iFlags);
file.WriteLE<int32_t>(item._iMiscId);
file.WriteLE<int32_t>(item._iSpell);
file.WriteLE<int32_t>(item._iCharges);
file.WriteLE<int32_t>(item._iMaxCharges);
file.WriteLE<int32_t>(item._iDurability);
file.WriteLE<int32_t>(item._iMaxDur);
file.WriteLE<int32_t>(item._iPLDam);
file.WriteLE<int32_t>(item._iPLToHit);
file.WriteLE<int32_t>(item._iPLAC);
file.WriteLE<int32_t>(item._iPLStr);
file.WriteLE<int32_t>(item._iPLMag);
file.WriteLE<int32_t>(item._iPLDex);
file.WriteLE<int32_t>(item._iPLVit);
file.WriteLE<int32_t>(item._iPLFR);
file.WriteLE<int32_t>(item._iPLLR);
file.WriteLE<int32_t>(item._iPLMR);
file.WriteLE<int32_t>(item._iPLMana);
file.WriteLE<int32_t>(item._iPLHP);
file.WriteLE<int32_t>(item._iPLDamMod);
file.WriteLE<int32_t>(item._iPLGetHit);
file.WriteLE<int32_t>(item._iPLLight);
file.WriteLE<int8_t>(item._iSplLvlAdd);
file.WriteLE<int8_t>(item._iRequest ? 1 : 0);
file.Skip(2); // Alignment
file.WriteLE<int32_t>(item._iUid);
file.WriteLE<int32_t>(item._iFMinDam);
file.WriteLE<int32_t>(item._iFMaxDam);
file.WriteLE<int32_t>(item._iLMinDam);
file.WriteLE<int32_t>(item._iLMaxDam);
file.WriteLE<int32_t>(item._iPLEnAc);
file.WriteLE<int8_t>(item._iPrePower);
file.WriteLE<int8_t>(item._iSufPower);
file.Skip(2); // Alignment
file.WriteLE<int32_t>(item._iVAdd1);
file.WriteLE<int32_t>(item._iVMult1);
file.WriteLE<int32_t>(item._iVAdd2);
file.WriteLE<int32_t>(item._iVMult2);
file.WriteLE<int8_t>(item._iMinStr);
file.WriteLE<uint8_t>(item._iMinMag);
file.WriteLE<int8_t>(item._iMinDex);
file.Skip(1); // Alignment
file.WriteLE<uint32_t>(item._iStatFlag ? 1 : 0);
file.WriteLE<int32_t>(idx);
file.WriteLE<uint32_t>(item.dwBuff);
if (gbIsHellfire)
file.WriteLE<uint32_t>(item._iDamAcFlags);
}
void SavePlayer(SaveHelper &file, const Player &player)
{
file.WriteLE<int32_t>(player._pmode);
for (int8_t step : player.walkpath)
file.WriteLE<int8_t>(step);
file.WriteLE<uint8_t>(player.plractive ? 1 : 0);
file.Skip(2); // Alignment
file.WriteLE<int32_t>(player.destAction);
file.WriteLE<int32_t>(player.destParam1);
file.WriteLE<int32_t>(player.destParam2);
file.WriteLE<int32_t>(static_cast<int32_t>(player.destParam3));
file.WriteLE<int32_t>(player.destParam4);
file.WriteLE<uint32_t>(player.plrlevel);
file.WriteLE<int32_t>(player.position.tile.x);
file.WriteLE<int32_t>(player.position.tile.y);
file.WriteLE<int32_t>(player.position.future.x);
file.WriteLE<int32_t>(player.position.future.y);
// For backwards compatibility
const Point target = player.GetTargetPosition();
file.WriteLE<int32_t>(target.x);
file.WriteLE<int32_t>(target.y);
file.WriteLE<int32_t>(player.position.last.x);
file.WriteLE<int32_t>(player.position.last.y);
file.WriteLE<int32_t>(player.position.old.x);
file.WriteLE<int32_t>(player.position.old.y);
file.WriteLE<int32_t>(player.position.offset.deltaX);
file.WriteLE<int32_t>(player.position.offset.deltaY);
file.WriteLE<int32_t>(player.position.velocity.deltaX);
file.WriteLE<int32_t>(player.position.velocity.deltaY);
file.WriteLE<int32_t>(static_cast<int32_t>(player._pdir));
file.Skip(4); // Unused
file.WriteLE<int32_t>(player._pgfxnum);
file.Skip(4); // Skip pointer _pAnimData
file.WriteLE<int32_t>(std::max(0, player.AnimInfo.TicksPerFrame - 1));
file.WriteLE<int32_t>(player.AnimInfo.TickCounterOfCurrentFrame);
file.WriteLE<int32_t>(player.AnimInfo.NumberOfFrames);
file.WriteLE<int32_t>(player.AnimInfo.CurrentFrame);
// write _pAnimWidth for vanilla compatibility
int animWidth = player.AnimInfo.pCelSprite == nullptr ? 96 : player.AnimInfo.pCelSprite->Width();
file.WriteLE<int32_t>(animWidth);
// write _pAnimWidth2 for vanilla compatibility
file.WriteLE<int32_t>(CalculateWidth2(animWidth));
file.Skip<uint32_t>(); // Skip _peflag
file.WriteLE<int32_t>(player._plid);
file.WriteLE<int32_t>(player._pvid);
file.WriteLE<int32_t>(player._pSpell);
file.WriteLE<int8_t>(player._pSplType);
file.WriteLE<int8_t>(player._pSplFrom);
file.Skip(2); // Alignment
file.WriteLE<int32_t>(player._pTSpell);
file.Skip<int8_t>(); // Skip _pTSplType
file.Skip(3); // Alignment
file.WriteLE<int32_t>(player._pRSpell);
file.WriteLE<int8_t>(player._pRSplType);
file.Skip(3); // Alignment
file.WriteLE<int32_t>(player._pSBkSpell);
file.Skip<int8_t>(); // Skip _pSBkSplType
for (int8_t spellLevel : player._pSplLvl)
file.WriteLE<int8_t>(spellLevel);
file.Skip(7); // Alignment
file.WriteLE<uint64_t>(player._pMemSpells);
file.WriteLE<uint64_t>(player._pAblSpells);
file.WriteLE<uint64_t>(player._pScrlSpells);
file.WriteLE<uint8_t>(player._pSpellFlags);
file.Skip(3); // Alignment
for (auto &spellId : player._pSplHotKey)
file.WriteLE<int32_t>(spellId);
for (auto &spellType : player._pSplTHotKey)
file.WriteLE<int8_t>(spellType);
file.WriteLE<int32_t>(player.UsesRangedWeapon() ? 1 : 0);
file.WriteLE<uint8_t>(player._pBlockFlag ? 1 : 0);
file.WriteLE<uint8_t>(player._pInvincible ? 1 : 0);
file.WriteLE<int8_t>(player._pLightRad);
file.WriteLE<uint8_t>(player._pLvlChanging ? 1 : 0);
file.WriteBytes(player._pName, PLR_NAME_LEN);
file.WriteLE<int8_t>(static_cast<int8_t>(player._pClass));
file.Skip(3); // Alignment
file.WriteLE<int32_t>(player._pStrength);
file.WriteLE<int32_t>(player._pBaseStr);
file.WriteLE<int32_t>(player._pMagic);
file.WriteLE<int32_t>(player._pBaseMag);
file.WriteLE<int32_t>(player._pDexterity);
file.WriteLE<int32_t>(player._pBaseDex);
file.WriteLE<int32_t>(player._pVitality);
file.WriteLE<int32_t>(player._pBaseVit);
file.WriteLE<int32_t>(player._pStatPts);
file.WriteLE<int32_t>(player._pDamageMod);
file.WriteLE<int32_t>(player._pBaseToBlk);
file.WriteLE<int32_t>(player._pHPBase);
file.WriteLE<int32_t>(player._pMaxHPBase);
file.WriteLE<int32_t>(player._pHitPoints);
file.WriteLE<int32_t>(player._pMaxHP);
file.Skip<int32_t>(); // Skip _pHPPer
file.WriteLE<int32_t>(player._pManaBase);
file.WriteLE<int32_t>(player._pMaxManaBase);
file.WriteLE<int32_t>(player._pMana);
file.WriteLE<int32_t>(player._pMaxMana);
file.Skip<int32_t>(); // Skip _pManaPer
file.WriteLE<int8_t>(player._pLevel);
file.WriteLE<int8_t>(player._pMaxLvl);
file.Skip(2); // Alignment
file.WriteLE<uint32_t>(player._pExperience);
file.Skip<uint32_t>(); // Skip _pMaxExp
file.WriteLE<uint32_t>(player._pNextExper);
file.WriteLE<int8_t>(player._pArmorClass);
file.WriteLE<int8_t>(player._pMagResist);
file.WriteLE<int8_t>(player._pFireResist);
file.WriteLE<int8_t>(player._pLghtResist);
file.WriteLE<int32_t>(player._pGold);
file.WriteLE<uint32_t>(player._pInfraFlag ? 1 : 0);
file.WriteLE<int32_t>(player.position.temp.x);
file.WriteLE<int32_t>(player.position.temp.y);
file.WriteLE<int32_t>(static_cast<int32_t>(player.tempDirection));
file.WriteLE<int32_t>(player.spellLevel);
file.Skip<int32_t>(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking
file.WriteLE<int32_t>(player.position.offset2.deltaX);
file.WriteLE<int32_t>(player.position.offset2.deltaY);
file.Skip<int32_t>(); // Skip _pVar8
for (uint8_t i = 0; i < giNumberOfLevels; i++)
file.WriteLE<uint8_t>(player._pLvlVisited[i] ? 1 : 0);
for (uint8_t i = 0; i < giNumberOfLevels; i++)
file.WriteLE<uint8_t>(player._pSLvlVisited[i] ? 1 : 0); // only 10 used
file.Skip(2); // Alignment
file.Skip<int32_t>(); // Skip _pGFXLoad
file.Skip(4 * 8); // Skip pointers _pNAnim
file.WriteLE<int32_t>(player._pNFrames);
file.Skip(4); // Skip _pNWidth
file.Skip(4 * 8); // Skip pointers _pWAnim
file.WriteLE<int32_t>(player._pWFrames);
file.Skip(4); // Skip _pWWidth
file.Skip(4 * 8); // Skip pointers _pAAnim
file.WriteLE<int32_t>(player._pAFrames);
file.Skip(4); // Skip _pAWidth
file.WriteLE<int32_t>(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<int32_t>(player._pSFrames);
file.Skip(4); // Skip _pSWidth
file.WriteLE<int32_t>(player._pSFNum);
file.Skip(4 * 8); // Skip pointers _pHAnim
file.WriteLE<int32_t>(player._pHFrames);
file.Skip(4); // Skip _pHWidth
file.Skip(4 * 8); // Skip pointers _pDAnim
file.WriteLE<int32_t>(player._pDFrames);
file.Skip(4); // Skip _pDWidth
file.Skip(4 * 8); // Skip pointers _pBAnim
file.WriteLE<int32_t>(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<int32_t>(player._pNumInv);
for (int8_t cell : player.InvGrid)
file.WriteLE<int8_t>(cell);
for (const Item &item : player.SpdList)
SaveItem(file, item);
SaveItem(file, player.HoldItem);
file.WriteLE<int32_t>(player._pIMinDam);
file.WriteLE<int32_t>(player._pIMaxDam);
file.WriteLE<int32_t>(player._pIAC);
file.WriteLE<int32_t>(player._pIBonusDam);
file.WriteLE<int32_t>(player._pIBonusToHit);
file.WriteLE<int32_t>(player._pIBonusAC);
file.WriteLE<int32_t>(player._pIBonusDamMod);
file.Skip(4); // Alignment
file.WriteLE<uint64_t>(player._pISpells);
file.WriteLE<int32_t>(player._pIFlags);
file.WriteLE<int32_t>(player._pIGetHit);
file.WriteLE<int8_t>(player._pISplLvlAdd);
file.Skip<uint8_t>(); // Skip _pISplCost
file.Skip(2); // Alignment
file.WriteLE<int32_t>(player._pISplDur);
file.WriteLE<int32_t>(player._pIEnAc);
file.WriteLE<int32_t>(player._pIFMinDam);
file.WriteLE<int32_t>(player._pIFMaxDam);
file.WriteLE<int32_t>(player._pILMinDam);
file.WriteLE<int32_t>(player._pILMaxDam);
file.WriteLE<int32_t>(player._pOilType);
file.WriteLE<uint8_t>(player.pTownWarps);
file.WriteLE<uint8_t>(player.pDungMsgs);
file.WriteLE<uint8_t>(player.pLvlLoad);
if (gbIsHellfire)
file.WriteLE<uint8_t>(player.pDungMsgs2);
else
file.WriteLE<uint8_t>(player.pBattleNet ? 1 : 0);
file.WriteLE<uint8_t>(player.pManaShield ? 1 : 0);
file.WriteLE<uint8_t>(player.pOriginalCathedral ? 1 : 0);
file.Skip(2); // Available bytes
file.WriteLE<uint16_t>(player.wReflections);
file.Skip(14); // Available bytes
file.WriteLE<uint32_t>(player.pDiabloKillLevel);
file.WriteLE<uint32_t>(player.pDifficulty);
file.WriteLE<uint32_t>(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<int32_t>(monster._mMTidx);
file->WriteLE<int32_t>(static_cast<int>(monster._mmode));
file->WriteLE<uint8_t>(monster._mgoal);
file->Skip(3); // Alignment
file->WriteLE<int32_t>(monster._mgoalvar1);
file->WriteLE<int32_t>(monster._mgoalvar2);
file->WriteLE<int32_t>(monster._mgoalvar3);
file->Skip(4); // Unused
file->WriteLE<uint8_t>(monster._pathcount);
file->Skip(3); // Alignment
file->WriteLE<int32_t>(monster.position.tile.x);
file->WriteLE<int32_t>(monster.position.tile.y);
file->WriteLE<int32_t>(monster.position.future.x);
file->WriteLE<int32_t>(monster.position.future.y);
file->WriteLE<int32_t>(monster.position.old.x);
file->WriteLE<int32_t>(monster.position.old.y);
file->WriteLE<int32_t>(monster.position.offset.deltaX);
file->WriteLE<int32_t>(monster.position.offset.deltaY);
file->WriteLE<int32_t>(monster.position.velocity.deltaX);
file->WriteLE<int32_t>(monster.position.velocity.deltaY);
file->WriteLE<int32_t>(static_cast<int32_t>(monster._mdir));
file->WriteLE<int32_t>(monster._menemy);
file->WriteLE<uint8_t>(monster.enemyPosition.x);
file->WriteLE<uint8_t>(monster.enemyPosition.y);
file->Skip(2); // Unused
file->Skip(4); // Skip pointer _mAnimData
file->WriteLE<int32_t>(monster.AnimInfo.TicksPerFrame);
file->WriteLE<int32_t>(monster.AnimInfo.TickCounterOfCurrentFrame);
file->WriteLE<int32_t>(monster.AnimInfo.NumberOfFrames);
file->WriteLE<int32_t>(monster.AnimInfo.CurrentFrame);
file->Skip<uint32_t>(); // Skip _meflag
file->WriteLE<uint32_t>(monster._mDelFlag ? 1 : 0);
file->WriteLE<int32_t>(monster._mVar1);
file->WriteLE<int32_t>(monster._mVar2);
file->WriteLE<int32_t>(monster._mVar3);
file->WriteLE<int32_t>(monster.position.temp.x);
file->WriteLE<int32_t>(monster.position.temp.y);
file->WriteLE<int32_t>(monster.position.offset2.deltaX);
file->WriteLE<int32_t>(monster.position.offset2.deltaY);
file->Skip<int32_t>(); // Skip _mVar8
file->WriteLE<int32_t>(monster._mmaxhp);
file->WriteLE<int32_t>(monster._mhitpoints);
file->WriteLE<uint8_t>(monster._mAi);
file->WriteLE<uint8_t>(monster._mint);
file->Skip(2); // Alignment
file->WriteLE<uint32_t>(monster._mFlags);
file->WriteLE<uint8_t>(monster._msquelch);
file->Skip(3); // Alignment
file->Skip(4); // Unused
file->WriteLE<int32_t>(monster.position.last.x);
file->WriteLE<int32_t>(monster.position.last.y);
file->WriteLE<uint32_t>(monster._mRndSeed);
file->WriteLE<uint32_t>(monster._mAISeed);
file->Skip(4); // Unused
file->WriteLE<uint8_t>(monster._uniqtype);
file->WriteLE<uint8_t>(monster._uniqtrans);
file->WriteLE<int8_t>(monster._udeadval);
file->WriteLE<int8_t>(monster.mWhoHit);
file->WriteLE<int8_t>(monster.mLevel);
file->Skip(1); // Alignment
file->WriteLE<uint16_t>(monster.mExp);
file->WriteLE<uint8_t>(std::min<uint16_t>(monster.mHit, std::numeric_limits<uint8_t>::max())); // For backwards compatibility
file->WriteLE<uint8_t>(monster.mMinDamage);
file->WriteLE<uint8_t>(monster.mMaxDamage);
file->WriteLE<uint8_t>(std::min<uint16_t>(monster.mHit2, std::numeric_limits<uint8_t>::max())); // For backwards compatibility
file->WriteLE<uint8_t>(monster.mMinDamage2);
file->WriteLE<uint8_t>(monster.mMaxDamage2);
file->WriteLE<uint8_t>(monster.mArmorClass);
file->Skip(1); // Alignment
file->WriteLE<uint16_t>(monster.mMagicRes);
file->Skip(2); // Alignment
file->WriteLE<int32_t>(monster.mtalkmsg == TEXT_NONE ? 0 : monster.mtalkmsg); // Replicate original bad mapping of none for monsters
file->WriteLE<uint8_t>(monster.leader);
file->WriteLE<uint8_t>(static_cast<std::uint8_t>(monster.leaderRelation));
file->WriteLE<uint8_t>(monster.packsize);
// vanilla compatibility
if (monster.mlid == NO_LIGHT)
file->WriteLE<int8_t>(0);
else
file->WriteLE<int8_t>(monster.mlid);
// Omit pointer mName;
// Omit pointer MType;
// Omit pointer MData;
}
void SaveMissile(SaveHelper *file, const Missile &missile)
{
file->WriteLE<int32_t>(missile._mitype);
file->WriteLE<int32_t>(missile.position.tile.x);
file->WriteLE<int32_t>(missile.position.tile.y);
file->WriteLE<int32_t>(missile.position.offset.deltaX);
file->WriteLE<int32_t>(missile.position.offset.deltaY);
file->WriteLE<int32_t>(missile.position.velocity.deltaX);
file->WriteLE<int32_t>(missile.position.velocity.deltaY);
file->WriteLE<int32_t>(missile.position.start.x);
file->WriteLE<int32_t>(missile.position.start.y);
file->WriteLE<int32_t>(missile.position.traveled.deltaX);
file->WriteLE<int32_t>(missile.position.traveled.deltaY);
file->WriteLE<int32_t>(missile._mimfnum);
file->WriteLE<int32_t>(missile._mispllvl);
file->WriteLE<uint32_t>(missile._miDelFlag ? 1 : 0);
file->WriteLE<uint8_t>(missile._miAnimType);
file->Skip(3); // Alignment
file->WriteLE<int32_t>(static_cast<int32_t>(missile._miAnimFlags));
file->Skip(4); // Skip pointer _miAnimData
file->WriteLE<int32_t>(missile._miAnimDelay);
file->WriteLE<int32_t>(missile._miAnimLen);
file->WriteLE<int32_t>(missile._miAnimWidth);
file->WriteLE<int32_t>(missile._miAnimWidth2);
file->WriteLE<int32_t>(missile._miAnimCnt);
file->WriteLE<int32_t>(missile._miAnimAdd);
file->WriteLE<int32_t>(missile._miAnimFrame);
file->WriteLE<uint32_t>(missile._miDrawFlag ? 1 : 0);
file->WriteLE<uint32_t>(missile._miLightFlag ? 1 : 0);
file->WriteLE<uint32_t>(missile._miPreFlag ? 1 : 0);
file->WriteLE<uint32_t>(missile._miUniqTrans);
file->WriteLE<int32_t>(missile._mirange);
file->WriteLE<int32_t>(missile._misource);
file->WriteLE<int32_t>(missile._micaster);
file->WriteLE<int32_t>(missile._midam);
file->WriteLE<uint32_t>(missile._miHitFlag ? 1 : 0);
file->WriteLE<int32_t>(missile._midist);
file->WriteLE<int32_t>(missile._mlid);
file->WriteLE<int32_t>(missile._mirnd);
file->WriteLE<int32_t>(missile.var1);
file->WriteLE<int32_t>(missile.var2);
file->WriteLE<int32_t>(missile.var3);
file->WriteLE<int32_t>(missile.var4);
file->WriteLE<int32_t>(missile.var5);
file->WriteLE<int32_t>(missile.var6);
file->WriteLE<int32_t>(missile.var7);
file->WriteLE<uint32_t>(missile.limitReached ? 1 : 0);
}
void SaveObject(SaveHelper &file, const Object &object)
{
file.WriteLE<int32_t>(object._otype);
file.WriteLE<int32_t>(object.position.x);
file.WriteLE<int32_t>(object.position.y);
file.WriteLE<uint32_t>(object._oLight ? 1 : 0);
file.WriteLE<uint32_t>(object._oAnimFlag);
file.Skip(4); // Skip pointer _oAnimData
file.WriteLE<int32_t>(object._oAnimDelay);
file.WriteLE<int32_t>(object._oAnimCnt);
file.WriteLE<uint32_t>(object._oAnimLen);
file.WriteLE<uint32_t>(object._oAnimFrame);
file.WriteLE<int32_t>(object._oAnimWidth);
file.WriteLE<int32_t>(CalculateWidth2(object._oAnimWidth)); // Write _oAnimWidth2 for vanilla compatibility
file.WriteLE<uint32_t>(object._oDelFlag ? 1 : 0);
file.WriteLE<int8_t>(object._oBreak);
file.Skip(3); // Alignment
file.WriteLE<uint32_t>(object._oSolidFlag ? 1 : 0);
file.WriteLE<uint32_t>(object._oMissFlag ? 1 : 0);
file.WriteLE<int8_t>(object._oSelFlag);
file.Skip(3); // Alignment
file.WriteLE<uint32_t>(object._oPreFlag ? 1 : 0);
file.WriteLE<uint32_t>(object._oTrapFlag ? 1 : 0);
file.WriteLE<uint32_t>(object._oDoorFlag ? 1 : 0);
file.WriteLE<int32_t>(object._olid);
file.WriteLE<uint32_t>(object._oRndSeed);
file.WriteLE<int32_t>(object._oVar1);
file.WriteLE<int32_t>(object._oVar2);
file.WriteLE<int32_t>(object._oVar3);
file.WriteLE<int32_t>(object._oVar4);
file.WriteLE<int32_t>(object._oVar5);
file.WriteLE<uint32_t>(object._oVar6);
file.WriteLE<int32_t>(object.bookMessage);
file.WriteLE<int32_t>(object._oVar8);
}
void SaveQuest(SaveHelper *file, int i)
{
auto &quest = Quests[i];
file->WriteLE<uint8_t>(quest._qlevel);
file->WriteLE<uint8_t>(quest._qidx); // _qtype for compatability, used in DRLG_CheckQuests
file->WriteLE<uint8_t>(quest._qactive);
file->WriteLE<uint8_t>(quest._qlvltype);
file->WriteLE<int32_t>(quest.position.x);
file->WriteLE<int32_t>(quest.position.y);
file->WriteLE<uint8_t>(quest._qslvl);
file->WriteLE<uint8_t>(quest._qidx);
if (gbIsHellfire) {
file->Skip(2); // Alignment
file->WriteLE<int32_t>(quest._qmsg);
} else {
file->WriteLE<uint8_t>(quest._qmsg);
}
file->WriteLE<uint8_t>(quest._qvar1);
file->WriteLE<uint8_t>(quest._qvar2);
file->Skip(2); // Alignment
if (!gbIsHellfire)
file->Skip(1); // Alignment
file->WriteLE<uint32_t>(quest._qlog ? 1 : 0);
file->WriteBE<int32_t>(ReturnLvlPosition.x);
file->WriteBE<int32_t>(ReturnLvlPosition.y);
file->WriteBE<int32_t>(ReturnLevel);
file->WriteBE<int32_t>(ReturnLevelType);
file->Skip(sizeof(int32_t)); // Skip DoomQuestState
}
void SaveLighting(SaveHelper *file, Light *pLight)
{
file->WriteLE<int32_t>(pLight->position.tile.x);
file->WriteLE<int32_t>(pLight->position.tile.y);
file->WriteLE<int32_t>(pLight->_lradius);
file->WriteLE<int32_t>(pLight->_lid);
file->WriteLE<uint32_t>(pLight->_ldel ? 1 : 0);
file->WriteLE<uint32_t>(pLight->_lunflag ? 1 : 0);
file->Skip(4); // Unused
file->WriteLE<int32_t>(pLight->position.old.x);
file->WriteLE<int32_t>(pLight->position.old.y);
file->WriteLE<int32_t>(pLight->oldRadius);
file->WriteLE<int32_t>(pLight->position.offset.x);
file->WriteLE<int32_t>(pLight->position.offset.y);
file->WriteLE<uint32_t>(pLight->_lflags ? 1 : 0);
}
void SavePortal(SaveHelper *file, int i)
{
Portal *pPortal = &Portals[i];
file->WriteLE<uint32_t>(pPortal->open ? 1 : 0);
file->WriteLE<int32_t>(pPortal->position.x);
file->WriteLE<int32_t>(pPortal->position.y);
file->WriteLE<int32_t>(pPortal->level);
file->WriteLE<int32_t>(pPortal->ltype);
file->WriteLE<uint32_t>(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<uint8_t, uint8_t> 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<uint8_t>(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<uint8_t>((i + ActiveItemCount) % MAXITEMS);
std::unordered_map<uint8_t, uint8_t> 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<uint8_t, uint8_t> &itemIndexes)
{
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(itemIndexes.at(dItem[i][j]));
}
}
constexpr uint32_t VersionAdditionalMissiles = 0;
void SaveAdditionalMissiles()
{
constexpr size_t BytesWrittenBySaveMissile = 180;
size_t missileCountAdditional = (Missiles.size() > MaxMissilesForSaveGame) ? Missiles.size() - MaxMissilesForSaveGame : 0;
SaveHelper file("additionalMissiles", sizeof(uint32_t) + sizeof(uint32_t) + (missileCountAdditional * BytesWrittenBySaveMissile));
file.WriteLE<uint32_t>(VersionAdditionalMissiles);
file.WriteLE<uint32_t>(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("additionalMissiles");
if (!file.IsValid()) {
// no addtional Missiles saved
return;
}
auto loadedVersion = file.NextLE<uint32_t>();
if (loadedVersion > VersionAdditionalMissiles) {
// unknown version
return;
}
auto missileCountAdditional = file.NextLE<uint32_t>();
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 != 0;
isInvalid = isInvalid || item._iPrePower > IPL_LASTDIABLO;
isInvalid = isInvalid || item._iSufPower > IPL_LASTDIABLO;
}
if (isInvalid) {
item._itype = ItemType::None;
}
}
_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;
}
void LoadHotkeys()
{
LoadHelper file("hotkeys");
if (!file.IsValid())
return;
auto &myPlayer = Players[MyPlayerId];
for (auto &spellId : myPlayer._pSplHotKey) {
spellId = static_cast<spell_id>(file.NextLE<int32_t>());
}
for (auto &spellType : myPlayer._pSplTHotKey) {
spellType = static_cast<spell_type>(file.NextLE<int8_t>());
}
myPlayer._pRSpell = static_cast<spell_id>(file.NextLE<int32_t>());
myPlayer._pRSplType = static_cast<spell_type>(file.NextLE<int8_t>());
}
void SaveHotkeys()
{
auto &myPlayer = Players[MyPlayerId];
const size_t nHotkeyTypes = sizeof(myPlayer._pSplHotKey) / sizeof(myPlayer._pSplHotKey[0]);
const size_t nHotkeySpells = sizeof(myPlayer._pSplTHotKey) / sizeof(myPlayer._pSplTHotKey[0]);
SaveHelper file("hotkeys", (nHotkeyTypes * 4) + nHotkeySpells + 4 + 1);
for (auto &spellId : myPlayer._pSplHotKey) {
file.WriteLE<int32_t>(spellId);
}
for (auto &spellType : myPlayer._pSplTHotKey) {
file.WriteLE<uint8_t>(spellType);
}
file.WriteLE<int32_t>(myPlayer._pRSpell);
file.WriteLE<uint8_t>(myPlayer._pRSplType);
}
void LoadHeroItems(Player &player)
{
LoadHelper file("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;
}
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("game");
if (!file.IsValid())
app_fatal("%s", _("Unable to open save file archive"));
if (!IsHeaderValid(file.NextLE<uint32_t>()))
app_fatal("%s", _("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<uint32_t>());
currlevel = file.NextBE<uint32_t>();
leveltype = static_cast<dungeon_type>(file.NextBE<uint32_t>());
if (!setlevel)
leveltype = gnLevelTypeTbl[currlevel];
int viewX = file.NextBE<int32_t>();
int viewY = file.NextBE<int32_t>();
invflag = file.NextBool8();
chrflag = file.NextBool8();
int tmpNummonsters = file.NextBE<int32_t>();
auto savedItemCount = file.NextBE<uint32_t>();
int tmpNummissiles = file.NextBE<int32_t>();
int tmpNobjects = file.NextBE<int32_t>();
if (!gbIsHellfire && currlevel > 17)
app_fatal("%s", _("Player is on a Hellfire only level"));
for (uint8_t i = 0; i < giNumberOfLevels; i++) {
glSeedTbl[i] = file.NextBE<uint32_t>();
file.Skip(4); // Skip loading gnLevelTypeTbl
}
auto &myPlayer = Players[MyPlayerId];
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<int32_t>();
if (leveltype != DTYPE_TOWN) {
for (int &monsterId : ActiveMonsters)
monsterId = file.NextBE<int32_t>();
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<int8_t>(MaxMissilesForSaveGame);
// Skip AvailableMissiles
file.Skip<int8_t>(MaxMissilesForSaveGame);
for (int i = 0; i < tmpNummissiles; i++)
LoadMissile(&file);
for (int &objectId : ActiveObjects)
objectId = file.NextLE<int8_t>();
for (int &objectId : AvailableObjects)
objectId = file.NextLE<int8_t>();
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<int32_t>();
for (uint8_t &lightId : ActiveLights)
lightId = file.NextLE<uint8_t>();
for (int i = 0; i < ActiveLightCount; i++)
LoadLighting(&file, &Lights[ActiveLights[i]]);
VisionId = file.NextBE<int32_t>();
VisionCount = file.NextBE<int32_t>();
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<int8_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dFlags[i][j] = static_cast<DungeonFlag>(file.NextLE<uint8_t>()) & 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<int8_t>();
}
// skip dItem indexes, this gets populated in LoadDroppedItems
file.Skip<uint8_t>(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<int32_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dCorpse[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dObject[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dLight[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dPreLight[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert)
AutomapView[i][j] = file.NextLE<uint8_t>();
}
file.Skip(MAXDUNX * MAXDUNY); // dMissile
}
numpremium = file.NextBE<int32_t>();
premiumlevel = file.NextBE<int32_t>();
for (int i = 0; i < giNumberOfSmithPremiumItems; i++)
LoadPremium(file, i);
if (gbIsHellfire && !gbIsHellfireSaveGame)
SpawnPremium(MyPlayerId);
AutomapActive = file.NextBool8();
AutoMapScale = file.NextBE<int32_t>();
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("heroitems", itemCount * (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize) + sizeof(uint8_t));
file.WriteLE<uint8_t>(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 SaveGameData()
{
SaveHelper file("game", 320 * 1024);
if (gbIsSpawn && !gbIsHellfire)
file.WriteLE<uint32_t>(LoadLE32("SHAR"));
else if (gbIsSpawn && gbIsHellfire)
file.WriteLE<uint32_t>(LoadLE32("SHLF"));
else if (!gbIsSpawn && gbIsHellfire)
file.WriteLE<uint32_t>(LoadLE32("HELF"));
else if (!gbIsSpawn && !gbIsHellfire)
file.WriteLE<uint32_t>(LoadLE32("RETL"));
else
app_fatal("%s", _("Invalid game state"));
if (gbIsHellfire) {
giNumberOfLevels = 25;
giNumberQuests = 24;
giNumberOfSmithPremiumItems = 15;
} else {
giNumberOfLevels = 17;
giNumberQuests = 16;
giNumberOfSmithPremiumItems = 6;
}
file.WriteLE<uint8_t>(setlevel ? 1 : 0);
file.WriteBE<uint32_t>(setlvlnum);
file.WriteBE<uint32_t>(currlevel);
file.WriteBE<uint32_t>(leveltype);
file.WriteBE<int32_t>(ViewPosition.x);
file.WriteBE<int32_t>(ViewPosition.y);
file.WriteLE<uint8_t>(invflag ? 1 : 0);
file.WriteLE<uint8_t>(chrflag ? 1 : 0);
file.WriteBE<int32_t>(ActiveMonsterCount);
file.WriteBE<int32_t>(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<uint32_t>(static_cast<uint32_t>(std::min(Missiles.size(), MaxMissilesForSaveGame)));
file.WriteBE<int32_t>(ActiveObjectCount);
for (uint8_t i = 0; i < giNumberOfLevels; i++) {
file.WriteBE<uint32_t>(glSeedTbl[i]);
file.WriteBE<int32_t>(gnLevelTypeTbl[i]);
}
auto &myPlayer = Players[MyPlayerId];
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<int32_t>(monstkill);
if (leveltype != DTYPE_TOWN) {
for (int monsterId : ActiveMonsters)
file.WriteBE<int32_t>(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<uint8_t>(activeMissile);
// Write AvailableMissiles
for (size_t availableMissiles = Missiles.size(); availableMissiles < MaxMissilesForSaveGame; availableMissiles++)
file.WriteLE(static_cast<uint8_t>(availableMissiles));
const size_t savedMissiles = std::min(Missiles.size(), MaxMissilesForSaveGame);
file.Skip<uint8_t>(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<int8_t>(objectId));
for (int objectId : AvailableObjects)
file.WriteLE(static_cast<int8_t>(objectId));
for (int i = 0; i < ActiveObjectCount; i++)
SaveObject(file, Objects[ActiveObjects[i]]);
file.WriteBE<int32_t>(ActiveLightCount);
for (uint8_t lightId : ActiveLights)
file.WriteLE<uint8_t>(lightId);
for (int i = 0; i < ActiveLightCount; i++)
SaveLighting(&file, &Lights[ActiveLights[i]]);
file.WriteBE<int32_t>(VisionId);
file.WriteBE<int32_t>(VisionCount);
for (int i = 0; i < VisionCount; i++)
SaveLighting(&file, &VisionList[i]);
}
auto itemIndexes = SaveDroppedItems(file);
for (bool uniqueItemFlag : UniqueItemFlags)
file.WriteLE<uint8_t>(uniqueItemFlag ? 1 : 0);
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dLight[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(static_cast<uint8_t>(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<int8_t>(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<int32_t>(dMonster[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dCorpse[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dObject[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dLight[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dPreLight[i][j]);
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(AutomapView[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(TileContainsMissile({ i, j }) ? -1 : 0); // For backwards compatability
}
}
file.WriteBE<int32_t>(numpremium);
file.WriteBE<int32_t>(premiumlevel);
for (int i = 0; i < giNumberOfSmithPremiumItems; i++)
SaveItem(file, premiumitems[i]);
file.WriteLE<uint8_t>(AutomapActive ? 1 : 0);
file.WriteBE<int32_t>(AutoMapScale);
SaveAdditionalMissiles();
}
void SaveGame()
{
gbValidSaveFile = true;
pfile_write_hero(/*writeGameData=*/true);
}
void SaveLevel()
{
PFileScopedArchiveWriter scopedWriter;
auto &myPlayer = Players[MyPlayerId];
DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level
if (currlevel == 0)
glSeedTbl[0] = AdvanceRndSeed();
char szName[MAX_PATH];
GetTempLevelNames(szName);
SaveHelper file(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<int8_t>(dCorpse[i][j]);
}
}
file.WriteBE<int32_t>(ActiveMonsterCount);
file.WriteBE<int32_t>(ActiveItemCount);
file.WriteBE<int32_t>(ActiveObjectCount);
if (leveltype != DTYPE_TOWN) {
for (int monsterId : ActiveMonsters)
file.WriteBE<int32_t>(monsterId);
for (int i = 0; i < ActiveMonsterCount; i++)
SaveMonster(&file, Monsters[ActiveMonsters[i]]);
for (int objectId : ActiveObjects)
file.WriteLE<int8_t>(objectId);
for (int objectId : AvailableObjects)
file.WriteLE<int8_t>(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<uint8_t>(static_cast<uint8_t>(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<int32_t>(dMonster[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dObject[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dLight[i][j]);
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<int8_t>(dPreLight[i][j]);
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert)
file.WriteLE<uint8_t>(AutomapView[i][j]);
}
}
if (!setlevel)
myPlayer._pLvlVisited[currlevel] = true;
else
myPlayer._pSLvlVisited[setlvlnum] = true;
}
void LoadLevel()
{
char szName[MAX_PATH];
GetPermLevelNames(szName);
LoadHelper file(szName);
if (!file.IsValid())
app_fatal("%s", _("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<int8_t>();
}
SyncUniqDead();
}
ActiveMonsterCount = file.NextBE<int32_t>();
auto savedItemCount = file.NextBE<uint32_t>();
ActiveObjectCount = file.NextBE<int32_t>();
if (leveltype != DTYPE_TOWN) {
for (int &monsterId : ActiveMonsters)
monsterId = file.NextBE<int32_t>();
for (int i = 0; i < ActiveMonsterCount; i++)
LoadMonster(&file, Monsters[ActiveMonsters[i]]);
for (int &objectId : ActiveObjects)
objectId = file.NextLE<int8_t>();
for (int &objectId : AvailableObjects)
objectId = file.NextLE<int8_t>();
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<DungeonFlag>(file.NextLE<uint8_t>()) & DungeonFlag::LoadedFlags;
}
// skip dItem indexes, this gets populated in LoadDroppedItems
file.Skip<uint8_t>(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<int32_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dObject[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dLight[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < MAXDUNY; j++) {
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
dPreLight[i][j] = file.NextLE<int8_t>();
}
for (int j = 0; j < DMAXY; j++) {
for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert)
const auto automapView = static_cast<MapExplorationType>(file.NextLE<uint8_t>());
AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView;
}
}
}
if (!gbSkipSync) {
AutomapZoomReset();
ResyncQuests();
RedoMissileFlags();
UpdateLighting = true;
}
for (auto &player : Players) {
if (player.plractive && currlevel == player.plrlevel)
Lights[player._plid]._lunflag = true;
}
}
} // namespace devilution