You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2819 lines
95 KiB
2819 lines
95 KiB
/** |
|
* @file loadsave.cpp |
|
* |
|
* Implementation of save game functionality. |
|
*/ |
|
#include "loadsave.h" |
|
|
|
#include <climits> |
|
#include <cstdint> |
|
#include <cstring> |
|
#include <numeric> |
|
|
|
#include <SDL.h> |
|
#include <ankerl/unordered_dense.h> |
|
#include <fmt/core.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 "menu.h" |
|
#include "missiles.h" |
|
#include "monster.h" |
|
#include "mpq/mpq_common.hpp" |
|
#include "pfile.h" |
|
#include "playerdat.hpp" |
|
#include "qol/stash.h" |
|
#include "stores.h" |
|
#include "utils/algorithm/container.hpp" |
|
#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 static_cast<T>(SDL_SwapBE64(in)); |
|
default: |
|
return in; |
|
} |
|
} |
|
|
|
class LoadHelper { |
|
std::unique_ptr<std::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(std::optional<SaveReader> 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 <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>()); |
|
} |
|
|
|
template <class TSource, class TDesired> |
|
TDesired NextLENarrow(TSource modifier = 0) |
|
{ |
|
static_assert(sizeof(TSource) > sizeof(TDesired), "Can only narrow to a smaller type"); |
|
TSource value = SwapLE(Next<TSource>()) + modifier; |
|
return static_cast<TDesired>(std::clamp<TSource>(value, std::numeric_limits<TDesired>::min(), std::numeric_limits<TDesired>::max())); |
|
} |
|
|
|
bool NextBool8() |
|
{ |
|
return Next<uint8_t>() != 0; |
|
} |
|
|
|
bool NextBool32() |
|
{ |
|
return Next<uint32_t>() != 0; |
|
} |
|
}; |
|
|
|
class SaveHelper { |
|
SaveWriter &m_mpqWriter; |
|
const char *m_szFileName_; |
|
std::unique_ptr<std::byte[]> m_buffer_; |
|
size_t m_cur_ = 0; |
|
size_t m_capacity_; |
|
|
|
public: |
|
SaveHelper(SaveWriter &mpqWriter, const char *szFileName, size_t bufferLen) |
|
: m_mpqWriter(mpqWriter) |
|
, m_szFileName_(szFileName) |
|
, m_buffer_(new std::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); |
|
m_mpqWriter.WriteFile(m_szFileName_, m_buffer_.get(), encodedLen); |
|
} |
|
}; |
|
|
|
struct MonsterConversionData { |
|
int8_t monsterLevel; |
|
uint16_t experience; |
|
uint8_t toHitSpecial; |
|
}; |
|
|
|
struct LevelConversionData { |
|
MonsterConversionData monsterConversionData[MaxMonsters]; |
|
}; |
|
|
|
void LoadItemData(LoadHelper &file, Item &item) |
|
{ |
|
item._iSeed = file.NextLE<uint32_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.NextLENarrow<int32_t, int8_t>(); |
|
item.AnimInfo.currentFrame = file.NextLENarrow<int32_t, int8_t>(-1); |
|
file.Skip(8); // Skip _iAnimWidth and _iAnimWidth2 |
|
file.Skip(4); // Unused since 1.02 |
|
item.selectionRegion = static_cast<SelectionRegion>(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 = static_cast<ItemSpecialEffect>(file.NextLE<uint32_t>()); |
|
item._iMiscId = static_cast<item_misc_id>(file.NextLE<int32_t>()); |
|
item._iSpell = static_cast<SpellID>(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 = static_cast<ItemSpecialEffectHf>(file.NextLE<uint32_t>()); |
|
else |
|
item._iDamAcFlags = ItemSpecialEffectHf::None; |
|
UpdateHellfireFlag(item, item._iIName); |
|
} |
|
|
|
void LoadAndValidateItemData(LoadHelper &file, Item &item) |
|
{ |
|
LoadItemData(file, item); |
|
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.setLevel(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<uint32_t>(2); // 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>(); |
|
file.Skip<int32_t>(4); // Skip offset and velocity |
|
player._pdir = static_cast<Direction>(file.NextLE<int32_t>()); |
|
file.Skip(4); // Unused |
|
player._pgfxnum = file.NextLENarrow<uint32_t, uint8_t>(); |
|
file.Skip<uint32_t>(); // Skip pointer pData |
|
player.AnimInfo = {}; |
|
player.AnimInfo.ticksPerFrame = file.NextLENarrow<int32_t, int8_t>(1); |
|
player.AnimInfo.tickCounterOfCurrentFrame = file.NextLENarrow<int32_t, int8_t>(); |
|
player.AnimInfo.numberOfFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
player.AnimInfo.currentFrame = file.NextLENarrow<int32_t, int8_t>(-1); |
|
file.Skip<uint32_t>(3); // Skip _pAnimWidth, _pAnimWidth2, _peflag |
|
player.lightId = file.NextLE<int32_t>(); |
|
file.Skip<int32_t>(); // _pvid |
|
|
|
player.queuedSpell.spellId = static_cast<SpellID>(file.NextLE<int32_t>()); |
|
player.queuedSpell.spellType = static_cast<SpellType>(file.NextLE<int8_t>()); |
|
auto spellFrom = file.NextLE<int8_t>(); |
|
if (!IsValidSpellFrom(spellFrom)) |
|
spellFrom = 0; |
|
player.spellFrom = spellFrom; |
|
player.queuedSpell.spellFrom = spellFrom; |
|
file.Skip(2); // Alignment |
|
player.inventorySpell = static_cast<SpellID>(file.NextLE<int32_t>()); |
|
file.Skip<int8_t>(); // Skip _pTSplType |
|
file.Skip(3); // Alignment |
|
player._pRSpell = static_cast<SpellID>(file.NextLE<int32_t>()); |
|
player._pRSplType = static_cast<SpellType>(file.NextLE<int8_t>()); |
|
file.Skip(3); // Alignment |
|
player._pSBkSpell = static_cast<SpellID>(file.NextLE<int32_t>()); |
|
file.Skip<int8_t>(); // Skip _pSBkSplType |
|
|
|
// Only read spell levels for learnable spells |
|
for (int i = 0; i < static_cast<int>(SpellID::LAST); i++) { |
|
auto spl = static_cast<SpellID>(i); |
|
if (GetSpellData(spl).sBookLvl != -1) |
|
player._pSplLvl[i] = file.NextLE<uint8_t>(); |
|
else |
|
file.Skip<uint8_t>(); |
|
} |
|
// Skip indices that are unused |
|
for (int i = static_cast<int>(SpellID::LAST); i < 64; i++) |
|
file.Skip<uint8_t>(); |
|
// These spells are unavailable in Diablo as learnable spells |
|
if (!gbIsHellfire) { |
|
player._pSplLvl[static_cast<uint8_t>(SpellID::Apocalypse)] = 0; |
|
player._pSplLvl[static_cast<uint8_t>(SpellID::Nova)] = 0; |
|
} |
|
|
|
file.Skip(7); // Alignment |
|
player._pMemSpells = file.NextLE<uint64_t>(); |
|
player._pAblSpells = file.NextLE<uint64_t>(); |
|
player._pScrlSpells = file.NextLE<uint64_t>(); |
|
player._pSpellFlags = static_cast<SpellFlag>(file.NextLE<uint8_t>()); |
|
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<SpellID>(file.NextLE<int32_t>()); |
|
} |
|
for (size_t i = 0; i < 4; i++) { |
|
player._pSplTHotKey[i] = static_cast<SpellType>(file.NextLE<uint8_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, PlayerNameLength); |
|
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>(); |
|
file.Skip<int32_t>(); // Skip _pBaseToBlk - always a copy of PlayerData.blockBonus |
|
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<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<int32_t>(); // Skip _pManaPer - always derived from mana and maxMana |
|
player.setCharacterLevel(file.NextLE<uint8_t>()); |
|
file.Skip<uint8_t>(); // Skip _pMaxLevel - unused |
|
file.Skip(2); // Alignment |
|
player._pExperience = file.NextLE<uint32_t>(); |
|
file.Skip<uint32_t>(); // Skip _pMaxExp - unused |
|
file.Skip<uint32_t>(); // Skip _pNextExper, we retrieve it when needed based on _pLevel |
|
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(); |
|
|
|
int32_t tempPositionX = file.NextLE<int32_t>(); |
|
int32_t tempPositionY = file.NextLE<int32_t>(); |
|
if (player._pmode == PM_WALK_NORTHWARDS) { |
|
// These values are saved as offsets to remain consistent with old savefiles |
|
tempPositionX += player.position.tile.x; |
|
tempPositionY += player.position.tile.y; |
|
} |
|
player.position.temp.x = static_cast<WorldTileCoord>(tempPositionX); |
|
player.position.temp.y = static_cast<WorldTileCoord>(tempPositionY); |
|
|
|
player.tempDirection = static_cast<Direction>(file.NextLE<int32_t>()); |
|
player.queuedSpell.spellLevel = file.NextLE<int32_t>(); |
|
file.Skip<uint32_t>(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking |
|
file.Skip<int32_t>(2); // skip offset2; |
|
file.Skip<uint32_t>(); // 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<uint32_t>(); // skip _pGFXLoad |
|
file.Skip<uint32_t>(8); // Skip pointers _pNAnim |
|
player._pNFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pNWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pWAnim |
|
player._pWFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pWWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pAAnim |
|
player._pAFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pAWidth |
|
player._pAFNum = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(8); // Skip pointers _pLAnim |
|
file.Skip<uint32_t>(8); // Skip pointers _pFAnim |
|
file.Skip<uint32_t>(8); // Skip pointers _pTAnim |
|
player._pSFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pSWidth |
|
player._pSFNum = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(8); // Skip pointers _pHAnim |
|
player._pHFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pHWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pDAnim |
|
player._pDFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pDWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pBAnim |
|
player._pBFrames = file.NextLENarrow<int32_t, int8_t>(); |
|
file.Skip<uint32_t>(); // skip _pBWidth |
|
|
|
for (Item &item : player.InvBody) |
|
LoadAndValidateItemData(file, item); |
|
|
|
for (Item &item : player.InvList) |
|
LoadAndValidateItemData(file, item); |
|
|
|
player._pNumInv = file.NextLE<int32_t>(); |
|
|
|
for (int8_t &cell : player.InvGrid) |
|
cell = file.NextLE<int8_t>(); |
|
|
|
for (Item &item : player.SpdList) |
|
LoadAndValidateItemData(file, item); |
|
|
|
LoadAndValidateItemData(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 = static_cast<ItemSpecialEffect>(file.NextLE<int32_t>()); |
|
player._pIGetHit = file.NextLE<int32_t>(); |
|
player._pISplLvlAdd = file.NextLE<int8_t>(); |
|
file.Skip(1); // Unused |
|
file.Skip(2); // Alignment |
|
file.Skip<int32_t>(); // _pISplDur |
|
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>(); |
|
} else { |
|
player.pDungMsgs2 = 0; |
|
file.Skip(1); // pBattleNet |
|
} |
|
player.pManaShield = file.NextBool8(); |
|
if (gbIsHellfireSaveGame) { |
|
player.pOriginalCathedral = file.NextBool8(); |
|
} else { |
|
file.Skip(1); |
|
player.pOriginalCathedral = true; |
|
} |
|
file.Skip(2); // Available bytes |
|
player.wReflections = file.NextLE<uint16_t>(); |
|
file.Skip(14); // Available bytes |
|
|
|
player.pDiabloKillLevel = file.NextLE<uint32_t>(); |
|
sgGameInitInfo.nDifficulty = static_cast<_difficulty>(file.NextLE<uint32_t>()); |
|
player.pDamAcFlags = static_cast<ItemSpecialEffectHf>(file.NextLE<uint32_t>()); |
|
file.Skip(20); // Available bytes |
|
CalcPlrItemVals(player, false); |
|
|
|
player.executedSpell = player.queuedSpell; // Ensures backwards compatibility |
|
|
|
// Omit pointer _pNData |
|
// Omit pointer _pWData |
|
// Omit pointer _pAData |
|
// Omit pointer _pLData |
|
// Omit pointer _pFData |
|
// Omit pointer _pTData |
|
// Omit pointer _pHData |
|
// Omit pointer _pDData |
|
// Omit pointer _pBData |
|
// Omit pointer pReserved |
|
|
|
// Ensure plrIsOnSetLevel and plrlevel is correctly initialized, because in vanilla sometimes plrlevel is not updated to setlvlnum |
|
if (setlevel) |
|
player.setLevel(setlvlnum); |
|
else |
|
player.setLevel(currlevel); |
|
} |
|
|
|
bool gbSkipSync = false; |
|
|
|
void LoadMonster(LoadHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr) |
|
{ |
|
monster.levelType = file->NextLE<int32_t>(); |
|
monster.mode = static_cast<MonsterMode>(file->NextLE<int32_t>()); |
|
monster.goal = static_cast<MonsterGoal>(file->NextLE<uint8_t>()); |
|
file->Skip(3); // Alignment |
|
monster.goalVar1 = file->NextLENarrow<int32_t, int16_t>(); |
|
monster.goalVar2 = file->NextLENarrow<int32_t, int8_t>(); |
|
monster.goalVar3 = file->NextLENarrow<int32_t, int8_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>(); |
|
file->Skip<int32_t>(4); // Skip offset and velocity |
|
monster.direction = static_cast<Direction>(file->NextLE<int32_t>()); |
|
monster.enemy = 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->NextLENarrow<int32_t, int8_t>(); |
|
// Ensure that we can increase the tickCounterOfCurrentFrame at least once without overflow (needed for backwards compatibility for sitting gargoyles) |
|
monster.animInfo.tickCounterOfCurrentFrame = file->NextLENarrow<int32_t, int8_t>(1) - 1; |
|
monster.animInfo.numberOfFrames = file->NextLENarrow<int32_t, int8_t>(); |
|
monster.animInfo.currentFrame = file->NextLENarrow<int32_t, int8_t>(-1); |
|
file->Skip(4); // Skip _meflag |
|
monster.isInvalid = file->NextBool32(); |
|
monster.var1 = file->NextLENarrow<int32_t, int16_t>(); |
|
monster.var2 = file->NextLENarrow<int32_t, int16_t>(); |
|
monster.var3 = file->NextLENarrow<int32_t, int8_t>(); |
|
monster.position.temp.x = file->NextLENarrow<int32_t, WorldTileCoord>(); |
|
monster.position.temp.y = file->NextLENarrow<int32_t, WorldTileCoord>(); |
|
file->Skip<int32_t>(2); // skip offset2; |
|
file->Skip(4); // Skip actionFrame |
|
monster.maxHitPoints = file->NextLE<int32_t>(); |
|
monster.hitPoints = file->NextLE<int32_t>(); |
|
|
|
monster.ai = static_cast<MonsterAIID>(file->NextLE<uint8_t>()); |
|
monster.intelligence = file->NextLE<uint8_t>(); |
|
file->Skip(2); // Alignment |
|
monster.flags = file->NextLE<uint32_t>(); |
|
monster.activeForTicks = 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.rndItemSeed = file->NextLE<uint32_t>(); |
|
monster.aiSeed = file->NextLE<uint32_t>(); |
|
file->Skip(4); // Unused |
|
|
|
monster.uniqueType = static_cast<UniqueMonsterType>(file->NextLE<uint8_t>() - 1); |
|
monster.uniqTrans = file->NextLE<uint8_t>(); |
|
monster.corpseId = file->NextLE<int8_t>(); |
|
|
|
monster.whoHit = file->NextLE<int8_t>(); |
|
if (monsterConversionData != nullptr) |
|
monsterConversionData->monsterLevel = file->NextLE<int8_t>(); |
|
else |
|
file->Skip(1); // Skip level - now calculated on the fly |
|
file->Skip(1); // Alignment |
|
if (monsterConversionData != nullptr) |
|
monsterConversionData->experience = file->NextLE<uint16_t>(); |
|
else |
|
file->Skip(2); // Skip exp - now calculated from monstdat when the monster dies |
|
|
|
if (monster.isPlayerMinion()) // Don't skip for golems |
|
monster.toHit = file->NextLE<uint8_t>(); |
|
else |
|
file->Skip(1); // Skip hit as it's already initialized |
|
monster.minDamage = file->NextLE<uint8_t>(); |
|
monster.maxDamage = file->NextLE<uint8_t>(); |
|
if (monsterConversionData != nullptr) |
|
monsterConversionData->toHitSpecial = file->NextLE<uint8_t>(); |
|
else |
|
file->Skip(1); // Skip toHitSpecial as it's already initialized |
|
monster.minDamageSpecial = file->NextLE<uint8_t>(); |
|
monster.maxDamageSpecial = file->NextLE<uint8_t>(); |
|
monster.armorClass = file->NextLE<uint8_t>(); |
|
file->Skip(1); // Alignment |
|
monster.resistance = file->NextLE<uint16_t>(); |
|
file->Skip(2); // Alignment |
|
|
|
monster.talkMsg = static_cast<_speech_id>(file->NextLE<int32_t>()); |
|
if (monster.talkMsg == TEXT_KING1) // Fix original bad mapping of NONE for monsters |
|
monster.talkMsg = TEXT_NONE; |
|
monster.leader = file->NextLE<uint8_t>(); |
|
if (monster.leader == 0) |
|
monster.leader = Monster::NoLeader; // Golems shouldn't be leaders of other monsters |
|
monster.leaderRelation = static_cast<LeaderRelation>(file->NextLE<uint8_t>()); |
|
monster.packSize = file->NextLE<uint8_t>(); |
|
monster.lightId = file->NextLE<int8_t>(); |
|
if (monster.lightId == 0) |
|
monster.lightId = NO_LIGHT; // Correct incorrect values in old saves |
|
|
|
// Omit pointer name; |
|
|
|
if (monster.mode == MonsterMode::Petrified) |
|
monster.animInfo.isPetrified = true; |
|
} |
|
|
|
/** |
|
* @brief Recalculate the pack size of monster group that may have underflown |
|
*/ |
|
void SyncPackSize(Monster &leader) |
|
{ |
|
if (!leader.isUnique()) |
|
return; |
|
if (leader.ai != MonsterAIID::Scavenger) |
|
return; |
|
|
|
leader.packSize = 0; |
|
|
|
for (size_t i = 0; i < ActiveMonsterCount; i++) { |
|
Monster &minion = Monsters[ActiveMonsters[i]]; |
|
if (minion.leaderRelation == LeaderRelation::Leashed && minion.getLeader() == &leader) |
|
leader.packSize++; |
|
} |
|
} |
|
|
|
void LoadMissile(LoadHelper *file) |
|
{ |
|
Missile missile = {}; |
|
missile._mitype = static_cast<MissileID>(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 = static_cast<MissileGraphicID>(file->NextLE<uint8_t>()); |
|
file->Skip(3); // Alignment |
|
missile._miAnimFlags = static_cast<MissileGraphicsFlags>(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); |
|
} |
|
} |
|
|
|
_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<int32_t>())); |
|
object.position.x = file.NextLE<int32_t>(); |
|
object.position.y = file.NextLE<int32_t>(); |
|
object.applyLighting = file.NextBool32(); |
|
object._oAnimFlag = file.NextBool32(); |
|
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 = static_cast<uint16_t>(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.selectionRegion = static_cast<SelectionRegion>(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) |
|
{ |
|
LoadAndValidateItemData(file, item); |
|
GetItemFrm(item); |
|
} |
|
|
|
void LoadPremium(LoadHelper &file, int i) |
|
{ |
|
LoadAndValidateItemData(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->radius = file->NextLE<int32_t>(); |
|
file->Skip<int32_t>(); // _lid |
|
pLight->isInvalid = file->NextBool32(); |
|
pLight->hasChanged = 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.deltaX = file->NextLE<int32_t>(); |
|
pLight->position.offset.deltaY = file->NextLE<int32_t>(); |
|
file->Skip<uint32_t>(); // _lflags |
|
} |
|
|
|
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(); |
|
if (!pPortal->setlvl) |
|
pPortal->ltype = GetLevelType(pPortal->level); |
|
} |
|
|
|
void GetLevelNames(std::string_view prefix, char *out) |
|
{ |
|
char suf; |
|
uint8_t num; |
|
if (setlevel) { |
|
suf = 's'; |
|
num = static_cast<uint8_t>(setlvlnum); |
|
} else { |
|
suf = 'l'; |
|
num = currlevel; |
|
} |
|
*fmt::format_to(out, "{}{}{:02d}", prefix, suf, num) = '\0'; |
|
} |
|
|
|
void GetTempLevelNames(char *szTemp) |
|
{ |
|
return GetLevelNames("temp", szTemp); |
|
} |
|
|
|
void GetPermLevelNames(char *szPerm) |
|
{ |
|
return GetLevelNames("perm", szPerm); |
|
} |
|
|
|
bool LevelFileExists(SaveWriter &archive) |
|
{ |
|
char szName[MaxMpqPathSize]; |
|
|
|
GetTempLevelNames(szName); |
|
if (archive.HasFile(szName)) |
|
return true; |
|
|
|
GetPermLevelNames(szName); |
|
return archive.HasFile(szName); |
|
} |
|
|
|
bool IsShopPriceValid(const Item &item) |
|
{ |
|
const int boyPriceLimit = 90000; |
|
if (!gbIsHellfire && (item._iCreateInfo & CF_BOY) != 0 && item._iIvalue > boyPriceLimit) |
|
return false; |
|
|
|
const int premiumPriceLimit = 140000; |
|
if (!gbIsHellfire && (item._iCreateInfo & CF_SMITHPREMIUM) != 0 && item._iIvalue > premiumPriceLimit) |
|
return false; |
|
|
|
const uint16_t smithOrWitch = CF_SMITH | CF_WITCH; |
|
const int smithAndWitchPriceLimit = gbIsHellfire ? 200000 : 140000; |
|
if ((item._iCreateInfo & smithOrWitch) != 0 && item._iIvalue > smithAndWitchPriceLimit) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
void LoadMatchingItems(LoadHelper &file, const Player &player, const int n, Item *pItem) |
|
{ |
|
Item heroItem; |
|
|
|
for (int i = 0; i < n; i++) { |
|
Item &unpackedItem = pItem[i]; |
|
LoadItemData(file, heroItem); |
|
if (unpackedItem.isEmpty() || heroItem.isEmpty()) |
|
continue; |
|
if (unpackedItem._iSeed != heroItem._iSeed) |
|
continue; |
|
if (heroItem.IDidx == IDI_EAR) |
|
continue; |
|
if (gbIsMultiplayer) { |
|
// Ensure that the unpacked item was regenerated using the appropriate |
|
// game's item generation logic before attempting to use it for validation |
|
if ((heroItem.dwBuff & CF_HELLFIRE) != (unpackedItem.dwBuff & CF_HELLFIRE)) { |
|
unpackedItem = {}; |
|
RecreateItem(player, unpackedItem, heroItem.IDidx, heroItem._iCreateInfo, heroItem._iSeed, heroItem._ivalue, (heroItem.dwBuff & CF_HELLFIRE) != 0); |
|
unpackedItem._iIdentified = heroItem._iIdentified; |
|
unpackedItem._iMaxDur = heroItem._iMaxDur; |
|
unpackedItem._iDurability = ClampDurability(unpackedItem, heroItem._iDurability); |
|
unpackedItem._iMaxCharges = std::clamp<int>(heroItem._iMaxCharges, 0, unpackedItem._iMaxCharges); |
|
unpackedItem._iCharges = std::clamp<int>(heroItem._iCharges, 0, unpackedItem._iMaxCharges); |
|
} |
|
if (!IsShopPriceValid(unpackedItem)) { |
|
unpackedItem.clear(); |
|
continue; |
|
} |
|
if (gbIsHellfire) { |
|
unpackedItem._iPLToHit = ClampToHit(unpackedItem, heroItem._iPLToHit); // Oil of Accuracy |
|
unpackedItem._iMaxDam = ClampMaxDam(unpackedItem, heroItem._iMaxDam); // Oil of Sharpness |
|
} |
|
} else { |
|
unpackedItem = heroItem; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* @brief Loads items on the current dungeon floor |
|
* @param file interface to the save file |
|
* @param savedItemCount how many items to read from the save file |
|
*/ |
|
void LoadDroppedItems(LoadHelper &file, size_t savedItemCount) |
|
{ |
|
// Skip loading ActiveItems and AvailableItems, the indices are initialised below based on the number of valid items |
|
file.Skip<uint8_t>(MAXITEMS * 2); |
|
|
|
// Reset ActiveItems, the Items array will be populated from the start |
|
std::iota(ActiveItems, ActiveItems + MAXITEMS, uint8_t { 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<uint32_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 + 1); |
|
// 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>(static_cast<uint8_t>(item.selectionRegion)); |
|
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>(static_cast<uint32_t>(item._iFlags)); |
|
file.WriteLE<int32_t>(item._iMiscId); |
|
file.WriteLE<int32_t>(static_cast<int8_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>(static_cast<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); |
|
DisplacementOf<int16_t> offset = {}; |
|
DisplacementOf<int16_t> offset2 = {}; |
|
DisplacementOf<int16_t> velocity = {}; |
|
if (player.isWalking()) { |
|
offset = player.position.CalculateWalkingOffset(player._pdir, player.AnimInfo); |
|
offset2 = player.position.CalculateWalkingOffsetShifted8(player._pdir, player.AnimInfo); |
|
velocity = player.position.GetWalkingVelocityShifted8(player._pdir, player.AnimInfo); |
|
} |
|
file.WriteLE<int32_t>(offset.deltaX); |
|
file.WriteLE<int32_t>(offset.deltaY); |
|
file.WriteLE<int32_t>(velocity.deltaX); |
|
file.WriteLE<int32_t>(velocity.deltaY); |
|
file.WriteLE<int32_t>(static_cast<int32_t>(player._pdir)); |
|
file.Skip(4); // Unused |
|
file.WriteLE<uint32_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 + 1); |
|
// write _pAnimWidth for vanilla compatibility |
|
const int animWidth = player.getSpriteWidth(); |
|
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.lightId); |
|
file.WriteLE<int32_t>(1); // _pvid |
|
|
|
file.WriteLE<int32_t>(static_cast<int8_t>(player.queuedSpell.spellId)); |
|
file.WriteLE<int8_t>(static_cast<int8_t>(player.queuedSpell.spellType)); |
|
file.WriteLE<int8_t>(player.queuedSpell.spellFrom); |
|
file.Skip(2); // Alignment |
|
file.WriteLE<int32_t>(static_cast<int8_t>(player.inventorySpell)); |
|
file.Skip<int8_t>(); // Skip _pTSplType |
|
file.Skip(3); // Alignment |
|
file.WriteLE<int32_t>(static_cast<int8_t>(player._pRSpell)); |
|
file.WriteLE<int8_t>(static_cast<uint8_t>(player._pRSplType)); |
|
file.Skip(3); // Alignment |
|
file.WriteLE<int32_t>(static_cast<int8_t>(player._pSBkSpell)); |
|
file.Skip<int8_t>(); // Skip _pSBkSplType |
|
|
|
for (uint8_t spellLevel : player._pSplLvl) |
|
file.WriteLE<uint8_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>(static_cast<uint8_t>(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<int32_t>(static_cast<int8_t>(player._pSplHotKey[i])); |
|
} |
|
for (size_t i = 0; i < 4; i++) { |
|
file.WriteLE<uint8_t>(static_cast<uint8_t>(player._pSplTHotKey[i])); |
|
} |
|
|
|
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, PlayerNameLength); |
|
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.getBaseToBlock()); // set _pBaseToBlk for backwards compatibility |
|
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<uint8_t>(player.getCharacterLevel()); |
|
file.Skip<uint8_t>(); // skip _pMaxLevel, this value is uninitialised in most cases in Diablo/Hellfire so there's no point setting it. |
|
file.Skip(2); // Alignment |
|
file.WriteLE<uint32_t>(player._pExperience); |
|
file.Skip<uint32_t>(); // Skip _pMaxExp |
|
file.WriteLE<uint32_t>(player.getNextExperienceThreshold()); // set _pNextExper for backwards compatibility |
|
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); |
|
|
|
int32_t tempPositionX = player.position.temp.x; |
|
int32_t tempPositionY = player.position.temp.y; |
|
if (player._pmode == PM_WALK_NORTHWARDS) { |
|
// For backwards compatibility, save this as an offset |
|
tempPositionX -= player.position.tile.x; |
|
tempPositionY -= player.position.tile.y; |
|
} |
|
file.WriteLE<int32_t>(tempPositionX); |
|
file.WriteLE<int32_t>(tempPositionY); |
|
|
|
file.WriteLE<int32_t>(static_cast<int32_t>(player.tempDirection)); |
|
file.WriteLE<int32_t>(player.queuedSpell.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>(offset2.deltaX); |
|
file.WriteLE<int32_t>(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<uint32_t>(8); // Skip pointers _pNAnim |
|
file.WriteLE<int32_t>(player._pNFrames); |
|
file.Skip<uint32_t>(); // Skip _pNWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pWAnim |
|
file.WriteLE<int32_t>(player._pWFrames); |
|
file.Skip<uint32_t>(); // Skip _pWWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pAAnim |
|
file.WriteLE<int32_t>(player._pAFrames); |
|
file.Skip<uint32_t>(); // Skip _pAWidth |
|
file.WriteLE<int32_t>(player._pAFNum); |
|
file.Skip<uint32_t>(8); // Skip pointers _pLAnim |
|
file.Skip<uint32_t>(8); // Skip pointers _pFAnim |
|
file.Skip<uint32_t>(8); // Skip pointers _pTAnim |
|
file.WriteLE<int32_t>(player._pSFrames); |
|
file.Skip<uint32_t>(); // Skip _pSWidth |
|
file.WriteLE<int32_t>(player._pSFNum); |
|
file.Skip<uint32_t>(8); // Skip pointers _pHAnim |
|
file.WriteLE<int32_t>(player._pHFrames); |
|
file.Skip<uint32_t>(); // Skip _pHWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pDAnim |
|
file.WriteLE<int32_t>(player._pDFrames); |
|
file.Skip<uint32_t>(); // Skip _pDWidth |
|
file.Skip<uint32_t>(8); // Skip pointers _pBAnim |
|
file.WriteLE<int32_t>(player._pBFrames); |
|
file.Skip<uint32_t>(); // 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>(static_cast<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.Skip<int32_t>(); // _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>(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>(sgGameInitInfo.nDifficulty); |
|
file.WriteLE<uint32_t>(static_cast<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, MonsterConversionData *monsterConversionData = nullptr) |
|
{ |
|
file->WriteLE<int32_t>(monster.levelType); |
|
file->WriteLE<int32_t>(static_cast<int>(monster.mode)); |
|
file->WriteLE<uint8_t>(static_cast<uint8_t>(monster.goal)); |
|
file->Skip(3); // Alignment |
|
file->WriteLE<int32_t>(monster.goalVar1); |
|
file->WriteLE<int32_t>(monster.goalVar2); |
|
file->WriteLE<int32_t>(monster.goalVar3); |
|
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); |
|
DisplacementOf<int16_t> offset = {}; |
|
DisplacementOf<int16_t> offset2 = {}; |
|
DisplacementOf<int16_t> velocity = {}; |
|
if (monster.isWalking()) { |
|
offset = monster.position.CalculateWalkingOffset(monster.direction, monster.animInfo); |
|
offset2 = monster.position.CalculateWalkingOffsetShifted4(monster.direction, monster.animInfo); |
|
velocity = monster.position.GetWalkingVelocityShifted4(monster.direction, monster.animInfo); |
|
} |
|
file->WriteLE<int32_t>(offset.deltaX); |
|
file->WriteLE<int32_t>(offset.deltaY); |
|
file->WriteLE<int32_t>(velocity.deltaX); |
|
file->WriteLE<int32_t>(velocity.deltaY); |
|
file->WriteLE<int32_t>(static_cast<int32_t>(monster.direction)); |
|
file->WriteLE<int32_t>(monster.enemy); |
|
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 + 1); |
|
file->Skip<uint32_t>(); // Skip _meflag |
|
file->WriteLE<uint32_t>(monster.isInvalid ? 1 : 0); |
|
file->WriteLE<int32_t>(monster.var1); |
|
file->WriteLE<int32_t>(monster.var2); |
|
file->WriteLE<int32_t>(monster.var3); |
|
file->WriteLE<int32_t>(monster.position.temp.x); |
|
file->WriteLE<int32_t>(monster.position.temp.y); |
|
file->WriteLE<int32_t>(offset2.deltaX); |
|
file->WriteLE<int32_t>(offset2.deltaY); |
|
file->Skip<int32_t>(); // Skip _mVar8 |
|
file->WriteLE<int32_t>(monster.maxHitPoints); |
|
file->WriteLE<int32_t>(monster.hitPoints); |
|
|
|
file->WriteLE<uint8_t>(static_cast<int8_t>(monster.ai)); |
|
file->WriteLE<uint8_t>(monster.intelligence); |
|
file->Skip(2); // Alignment |
|
file->WriteLE<uint32_t>(monster.flags); |
|
file->WriteLE<uint8_t>(monster.activeForTicks); |
|
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.rndItemSeed); |
|
file->WriteLE<uint32_t>(monster.aiSeed); |
|
file->Skip(4); // Unused |
|
|
|
file->WriteLE<uint8_t>(static_cast<uint8_t>(monster.uniqueType) + 1); |
|
file->WriteLE<uint8_t>(monster.uniqTrans); |
|
file->WriteLE<int8_t>(monster.corpseId); |
|
|
|
file->WriteLE<int8_t>(monster.whoHit); |
|
if (monsterConversionData != nullptr) |
|
file->WriteLE<int8_t>(monsterConversionData->monsterLevel); |
|
else |
|
file->WriteLE<int8_t>(static_cast<int8_t>(monster.level(sgGameInitInfo.nDifficulty))); |
|
file->Skip(1); // Alignment |
|
if (monsterConversionData != nullptr) |
|
file->WriteLE<uint16_t>(monsterConversionData->experience); |
|
else |
|
file->WriteLE<uint16_t>(static_cast<uint16_t>(std::min<unsigned>(std::numeric_limits<uint16_t>::max(), monster.exp(sgGameInitInfo.nDifficulty)))); |
|
|
|
file->WriteLE<uint8_t>(static_cast<uint8_t>(std::min<uint16_t>(monster.toHit, std::numeric_limits<uint8_t>::max()))); // For backwards compatibility |
|
file->WriteLE<uint8_t>(monster.minDamage); |
|
file->WriteLE<uint8_t>(monster.maxDamage); |
|
if (monsterConversionData != nullptr) |
|
file->WriteLE<uint8_t>(monsterConversionData->toHitSpecial); |
|
else |
|
file->WriteLE<uint8_t>(static_cast<uint8_t>(std::min<uint16_t>(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits<uint8_t>::max()))); // For backwards compatibility |
|
file->WriteLE<uint8_t>(monster.minDamageSpecial); |
|
file->WriteLE<uint8_t>(monster.maxDamageSpecial); |
|
file->WriteLE<uint8_t>(monster.armorClass); |
|
file->Skip(1); // Alignment |
|
file->WriteLE<uint16_t>(monster.resistance); |
|
file->Skip(2); // Alignment |
|
|
|
file->WriteLE<int32_t>(monster.talkMsg == TEXT_NONE ? 0 : monster.talkMsg); // Replicate original bad mapping of none for monsters |
|
file->WriteLE<uint8_t>(monster.leader == Monster::NoLeader ? 0 : monster.leader); // Vanilla uses 0 as the default leader which corresponds to player 0s golem |
|
file->WriteLE<uint8_t>(static_cast<std::uint8_t>(monster.leaderRelation)); |
|
file->WriteLE<uint8_t>(monster.packSize); |
|
// vanilla compatibility |
|
if (monster.lightId == NO_LIGHT) |
|
file->WriteLE<int8_t>(0); |
|
else |
|
file->WriteLE<int8_t>(monster.lightId); |
|
|
|
// Omit pointer name; |
|
} |
|
|
|
void SaveMissile(SaveHelper *file, const Missile &missile) |
|
{ |
|
file->WriteLE<int32_t>(static_cast<int8_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>(static_cast<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); |
|
} |
|
|
|
_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<int32_t>(ConvertToHellfireObject(object._otype)); |
|
file.WriteLE<int32_t>(object.position.x); |
|
file.WriteLE<int32_t>(object.position.y); |
|
file.WriteLE<uint32_t>(object.applyLighting ? 1 : 0); |
|
file.WriteLE<uint32_t>(object._oAnimFlag ? 1 : 0); |
|
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(static_cast<int>(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>(static_cast<uint8_t>(object.selectionRegion)); |
|
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); |
|
|
|
/* Make dynamic light sources unseen when saving level data for level change */ |
|
int32_t var1 = object._oVar1; |
|
switch (object._otype) { |
|
case OBJ_L1LIGHT: |
|
case OBJ_SKFIRE: |
|
case OBJ_CANDLE1: |
|
case OBJ_CANDLE2: |
|
case OBJ_BOOKCANDLE: |
|
case OBJ_STORYCANDLE: |
|
case OBJ_L5CANDLE: |
|
case OBJ_TORCHL: |
|
case OBJ_TORCHR: |
|
case OBJ_TORCHL2: |
|
case OBJ_TORCHR2: |
|
case OBJ_BCROSS: |
|
case OBJ_TBCROSS: |
|
if (var1 != -1) |
|
var1 = 0; |
|
break; |
|
default: |
|
break; |
|
} |
|
file.WriteLE<int32_t>(var1); |
|
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 compatibility, 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, bool vision = false) |
|
{ |
|
file->WriteLE<int32_t>(pLight->position.tile.x); |
|
file->WriteLE<int32_t>(pLight->position.tile.y); |
|
file->WriteLE<int32_t>(pLight->radius); |
|
file->WriteLE<int32_t>(vision ? 1 : 0); // _lid |
|
file->WriteLE<uint32_t>(pLight->isInvalid ? 1 : 0); |
|
file->WriteLE<uint32_t>(pLight->hasChanged ? 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.deltaX); |
|
file->WriteLE<int32_t>(pLight->position.offset.deltaY); |
|
file->WriteLE<uint32_t>(vision ? 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->setlvl ? pPortal->ltype : getHellfireLevelType(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 |
|
*/ |
|
ankerl::unordered_dense::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); |
|
|
|
ankerl::unordered_dense::map<uint8_t, uint8_t> itemIndexes; |
|
itemIndexes.reserve(ActiveItemCount + 1); |
|
itemIndexes.emplace(0, 0); |
|
for (uint8_t i = 0; i < ActiveItemCount; i++) { |
|
itemIndexes[ActiveItems[i] + 1] = i + 1; |
|
SaveItem(file, Items[ActiveItems[i]]); |
|
} |
|
return itemIndexes; |
|
} |
|
|
|
/** |
|
* @brief Saves the position of dropped items (in dItem) |
|
* @param file interface to the save file |
|
* @param itemIndexes a map converting from runtime item indexes to the relative position in the save file |
|
*/ |
|
void SaveDroppedItemLocations(SaveHelper &file, const ankerl::unordered_dense::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(SaveWriter &saveWriter) |
|
{ |
|
constexpr size_t BytesWrittenBySaveMissile = 180; |
|
uint32_t missileCountAdditional = (Missiles.size() > MaxMissilesForSaveGame) ? static_cast<uint32_t>(Missiles.size() - MaxMissilesForSaveGame) : 0; |
|
SaveHelper file(saveWriter, "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(OpenSaveArchive(gSaveNumber), "additionalMissiles"); |
|
|
|
if (!file.IsValid()) { |
|
// no additional 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); |
|
} |
|
} |
|
|
|
void SaveLevelSeeds(SaveWriter &saveWriter) |
|
{ |
|
SaveHelper file(saveWriter, "levelseeds", giNumberOfLevels * (sizeof(uint8_t) + sizeof(uint32_t))); |
|
|
|
for (int i = 0; i < giNumberOfLevels; i++) { |
|
file.WriteLE<uint8_t>(LevelSeeds[i] ? 1 : 0); |
|
if (LevelSeeds[i]) { |
|
file.WriteLE<uint32_t>(*LevelSeeds[i]); |
|
} |
|
} |
|
} |
|
|
|
void LoadLevelSeeds() |
|
{ |
|
LoadHelper file(OpenSaveArchive(gSaveNumber), "levelseeds"); |
|
if (!file.IsValid()) |
|
return; |
|
|
|
for (int i = 0; i < giNumberOfLevels; i++) { |
|
if (file.NextLE<uint8_t>() != 0) { |
|
LevelSeeds[i] = file.NextLE<uint32_t>(); |
|
} else { |
|
LevelSeeds[i] = std::nullopt; |
|
} |
|
} |
|
} |
|
|
|
void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) |
|
{ |
|
Player &myPlayer = *MyPlayer; |
|
|
|
DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level |
|
|
|
if (leveltype == DTYPE_TOWN) |
|
DungeonSeeds[0] = GenerateSeed(); |
|
|
|
char szName[MaxMpqPathSize]; |
|
GetTempLevelNames(szName); |
|
SaveHelper file(saveWriter, szName, 256 * 1024); |
|
|
|
if (leveltype != DTYPE_TOWN) { |
|
for (int j = 0; j < MAXDUNY; j++) { |
|
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) |
|
file.WriteLE<int8_t>(dCorpse[i][j]); |
|
} |
|
} |
|
|
|
file.WriteBE(static_cast<int32_t>(ActiveMonsterCount)); |
|
file.WriteBE<int32_t>(ActiveItemCount); |
|
file.WriteBE<int32_t>(ActiveObjectCount); |
|
|
|
if (leveltype != DTYPE_TOWN) { |
|
for (unsigned monsterId : ActiveMonsters) |
|
file.WriteBE<uint32_t>(monsterId); |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) { |
|
MonsterConversionData *monsterConversionData = nullptr; |
|
if (levelConversionData != nullptr) |
|
monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]]; |
|
SaveMonster(&file, Monsters[ActiveMonsters[i]], monsterConversionData); |
|
} |
|
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<uint8_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>(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(LevelConversionData *levelConversionData) |
|
{ |
|
char szName[MaxMpqPathSize]; |
|
std::optional<SaveReader> archive = OpenSaveArchive(gSaveNumber); |
|
GetTempLevelNames(szName); |
|
if (!archive || !archive->HasFile(szName)) |
|
GetPermLevelNames(szName); |
|
LoadHelper file(std::move(archive), szName); |
|
if (!file.IsValid()) |
|
app_fatal(_("Unable to open save file archive")); |
|
|
|
if (leveltype != DTYPE_TOWN) { |
|
for (int j = 0; j < MAXDUNY; j++) { |
|
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) |
|
dCorpse[i][j] = file.NextLE<int8_t>(); |
|
} |
|
MoveLightsToCorpses(); |
|
} |
|
|
|
ActiveMonsterCount = file.NextBE<int32_t>(); |
|
auto savedItemCount = file.NextBE<uint32_t>(); |
|
ActiveObjectCount = file.NextBE<int32_t>(); |
|
|
|
if (leveltype != DTYPE_TOWN) { |
|
for (unsigned &monsterId : ActiveMonsters) |
|
monsterId = file.NextBE<uint32_t>(); |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) { |
|
Monster &monster = Monsters[ActiveMonsters[i]]; |
|
MonsterConversionData *monsterConversionData = nullptr; |
|
if (levelConversionData != nullptr) |
|
monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]]; |
|
LoadMonster(&file, monster, monsterConversionData); |
|
if (monster.isUnique() && monster.lightId != NO_LIGHT) |
|
Lights[monster.lightId].isInvalid = false; |
|
} |
|
if (!gbSkipSync) { |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) |
|
SyncMonsterAnim(Monsters[ActiveMonsters[i]]); |
|
} |
|
for (int &objectId : ActiveObjects) |
|
objectId = file.NextLE<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>(); |
|
} |
|
file.Skip<uint8_t>(MAXDUNY * MAXDUNX); // dLight |
|
for (int j = 0; j < MAXDUNY; j++) { |
|
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) |
|
dPreLight[i][j] = file.NextLE<uint8_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; |
|
} |
|
} |
|
|
|
// No need to load dLight, we can recreate it accurately from LightList |
|
memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light |
|
ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh |
|
} else { |
|
memset(dLight, 0, sizeof(dLight)); |
|
} |
|
|
|
if (!gbSkipSync) { |
|
AutomapZoomReset(); |
|
ResyncQuests(); |
|
RedoMissileFlags(); |
|
UpdateLighting = true; |
|
} |
|
|
|
for (Player &player : Players) { |
|
if (player.plractive && player.isOnActiveLevel()) |
|
Lights[player.lightId].hasChanged = true; |
|
} |
|
} |
|
|
|
const int DiabloItemSaveSize = 368; |
|
const int HellfireItemSaveSize = 372; |
|
|
|
} // namespace |
|
|
|
void ConvertLevels(SaveWriter &saveWriter) |
|
{ |
|
// Backup current level state |
|
bool tmpSetlevel = setlevel; |
|
_setlevels tmpSetlvlnum = setlvlnum; |
|
int tmpCurrlevel = currlevel; |
|
dungeon_type tmpLeveltype = leveltype; |
|
|
|
gbSkipSync = true; |
|
|
|
setlevel = false; // Convert regular levels |
|
for (int i = 0; i < giNumberOfLevels; i++) { |
|
currlevel = i; |
|
if (!LevelFileExists(saveWriter)) |
|
continue; |
|
|
|
leveltype = GetLevelType(currlevel); |
|
|
|
LevelConversionData levelConversionData; |
|
LoadLevel(&levelConversionData); |
|
SaveLevel(saveWriter, &levelConversionData); |
|
} |
|
|
|
setlevel = true; // Convert quest levels |
|
for (auto &quest : Quests) { |
|
if (quest._qactive == QUEST_NOTAVAIL) { |
|
continue; |
|
} |
|
|
|
leveltype = quest._qlvltype; |
|
if (leveltype == DTYPE_NONE) { |
|
continue; |
|
} |
|
|
|
setlvlnum = quest._qslvl; |
|
if (!LevelFileExists(saveWriter)) |
|
continue; |
|
|
|
LevelConversionData levelConversionData; |
|
LoadLevel(&levelConversionData); |
|
SaveLevel(saveWriter, &levelConversionData); |
|
} |
|
|
|
gbSkipSync = false; |
|
|
|
// Restore current level state |
|
setlevel = tmpSetlevel; |
|
setlvlnum = tmpSetlvlnum; |
|
currlevel = tmpCurrlevel; |
|
leveltype = tmpLeveltype; |
|
} |
|
|
|
void RemoveInvalidItem(Item &item) |
|
{ |
|
bool isInvalid = !IsItemAvailable(item.IDidx) || !IsUniqueAvailable(item._iUid); |
|
|
|
if (!gbIsHellfire) { |
|
isInvalid = isInvalid || (item._itype == ItemType::Staff && GetSpellStaffLevel(item._iSpell) == -1); |
|
isInvalid = isInvalid || (item._iMiscId == IMISC_BOOK && GetSpellBookLevel(item._iSpell) == -1); |
|
isInvalid = isInvalid || item._iDamAcFlags != ItemSpecialEffectHf::None; |
|
isInvalid = isInvalid || item._iPrePower > IPL_LASTDIABLO; |
|
isInvalid = isInvalid || item._iSufPower > IPL_LASTDIABLO; |
|
} |
|
|
|
if (isInvalid) { |
|
item.clear(); |
|
} |
|
} |
|
|
|
_item_indexes RemapItemIdxFromDiablo(_item_indexes i) |
|
{ |
|
constexpr auto GetItemIdValue = [](int i) -> int { |
|
if (i == IDI_SORCERER) { |
|
return IDI_SORCERER_DIABLO; |
|
} |
|
if (i >= 156) { |
|
i += 5; // Hellfire exclusive items |
|
} |
|
if (i >= 88) { |
|
i += 1; // Scroll of Search |
|
} |
|
if (i >= 83) { |
|
i += 4; // Oils |
|
} |
|
|
|
return i; |
|
}; |
|
|
|
return static_cast<_item_indexes>(GetItemIdValue(i)); |
|
} |
|
|
|
_item_indexes RemapItemIdxToDiablo(_item_indexes i) |
|
{ |
|
constexpr auto GetItemIdValue = [](int i) -> int { |
|
if (i == IDI_SORCERER_DIABLO) { |
|
return IDI_SORCERER; |
|
} |
|
if ((i >= 83 && i <= 86) || i == 92 || i >= 161) { |
|
return -1; // Hellfire exclusive items |
|
} |
|
if (i >= 93) { |
|
i -= 1; // Scroll of Search |
|
} |
|
if (i >= 87) { |
|
i -= 4; // Oils |
|
} |
|
|
|
return i; |
|
}; |
|
|
|
return static_cast<_item_indexes>(GetItemIdValue(i)); |
|
} |
|
|
|
_item_indexes RemapItemIdxFromSpawn(_item_indexes i) |
|
{ |
|
constexpr auto GetItemIdValue = [](int i) { |
|
if (i >= 62) { |
|
i += 9; // Medium and heavy armors |
|
} |
|
if (i >= 96) { |
|
i += 1; // Scroll of Stone Curse |
|
} |
|
if (i >= 98) { |
|
i += 1; // Scroll of Guardian |
|
} |
|
if (i >= 99) { |
|
i += 1; // Scroll of ... |
|
} |
|
if (i >= 101) { |
|
i += 1; // Scroll of Golem |
|
} |
|
if (i >= 102) { |
|
i += 1; // Scroll of None |
|
} |
|
if (i >= 104) { |
|
i += 1; // Scroll of Apocalypse |
|
} |
|
|
|
return i; |
|
}; |
|
|
|
return static_cast<_item_indexes>(GetItemIdValue(i)); |
|
} |
|
|
|
_item_indexes RemapItemIdxToSpawn(_item_indexes i) |
|
{ |
|
constexpr auto GetItemIdValue = [](int i) { |
|
if (i >= 104) { |
|
i -= 1; // Scroll of Apocalypse |
|
} |
|
if (i >= 102) { |
|
i -= 1; // Scroll of None |
|
} |
|
if (i >= 101) { |
|
i -= 1; // Scroll of Golem |
|
} |
|
if (i >= 99) { |
|
i -= 1; // Scroll of ... |
|
} |
|
if (i >= 98) { |
|
i -= 1; // Scroll of Guardian |
|
} |
|
if (i >= 96) { |
|
i -= 1; // Scroll of Stone Curse |
|
} |
|
if (i >= 71) { |
|
i -= 9; // Medium and heavy armors |
|
} |
|
|
|
return i; |
|
}; |
|
|
|
return static_cast<_item_indexes>(GetItemIdValue(i)); |
|
} |
|
|
|
bool IsHeaderValid(uint32_t magicNumber) |
|
{ |
|
gbIsHellfireSaveGame = false; |
|
if (magicNumber == LoadLE32("SHAR")) { |
|
return true; |
|
} |
|
if (magicNumber == LoadLE32("SHLF")) { |
|
gbIsHellfireSaveGame = true; |
|
return true; |
|
} |
|
if (!gbIsSpawn && magicNumber == LoadLE32("RETL")) { |
|
return true; |
|
} |
|
if (!gbIsSpawn && magicNumber == LoadLE32("HELF")) { |
|
gbIsHellfireSaveGame = true; |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// Returns the size of the hotkeys file with the number of hotkeys passed and if a header with the number of hotkeys is present in the file |
|
size_t HotkeysSize(size_t nHotkeys = NumHotkeys) |
|
{ |
|
// header spells spell types active spell active spell type |
|
return sizeof(uint8_t) + (nHotkeys * sizeof(int32_t)) + (nHotkeys * sizeof(uint8_t)) + sizeof(int32_t) + sizeof(uint8_t); |
|
} |
|
|
|
void LoadHotkeys() |
|
{ |
|
LoadHelper file(OpenSaveArchive(gSaveNumber), "hotkeys"); |
|
if (!file.IsValid()) |
|
return; |
|
|
|
Player &myPlayer = *MyPlayer; |
|
size_t nHotkeys = 4; // Defaults to old save format number |
|
|
|
// Refill the spell arrays with no selection |
|
std::fill(myPlayer._pSplHotKey, myPlayer._pSplHotKey + NumHotkeys, SpellID::Invalid); |
|
std::fill(myPlayer._pSplTHotKey, myPlayer._pSplTHotKey + NumHotkeys, SpellType::Invalid); |
|
|
|
// Checking if the save file has the old format with only 4 hotkeys and no header |
|
if (file.IsValid(HotkeysSize(nHotkeys))) { |
|
// The file contains a header byte and at least 4 entries, so we can assume it's a new format save |
|
nHotkeys = file.NextLE<uint8_t>(); |
|
} |
|
|
|
// 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<SpellID>(file.NextLE<int32_t>()); |
|
} else { |
|
file.Skip<int32_t>(); |
|
} |
|
} |
|
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<SpellType>(file.NextLE<uint8_t>()); |
|
} else { |
|
file.Skip<uint8_t>(); |
|
} |
|
} |
|
|
|
// Load the selected spell last |
|
myPlayer._pRSpell = static_cast<SpellID>(file.NextLE<int32_t>()); |
|
myPlayer._pRSplType = static_cast<SpellType>(file.NextLE<uint8_t>()); |
|
} |
|
|
|
void SaveHotkeys(SaveWriter &saveWriter, const Player &player) |
|
{ |
|
SaveHelper file(saveWriter, "hotkeys", HotkeysSize()); |
|
|
|
// Write the number of spell hotkeys |
|
file.WriteLE<uint8_t>(static_cast<uint8_t>(NumHotkeys)); |
|
|
|
// Write the spell hotkeys |
|
for (auto &spellId : player._pSplHotKey) { |
|
file.WriteLE<int32_t>(static_cast<int8_t>(spellId)); |
|
} |
|
for (auto &spellType : player._pSplTHotKey) { |
|
file.WriteLE<uint8_t>(static_cast<uint8_t>(spellType)); |
|
} |
|
|
|
// Write the selected spell last |
|
file.WriteLE<int32_t>(static_cast<int8_t>(player._pRSpell)); |
|
file.WriteLE<uint8_t>(static_cast<uint8_t>(player._pRSplType)); |
|
} |
|
|
|
void LoadHeroItems(Player &player) |
|
{ |
|
LoadHelper file(OpenSaveArchive(gSaveNumber), "heroitems"); |
|
if (!file.IsValid()) |
|
return; |
|
|
|
gbIsHellfireSaveGame = file.NextBool8(); |
|
|
|
LoadMatchingItems(file, player, NUM_INVLOC, player.InvBody); |
|
LoadMatchingItems(file, player, InventoryGridCells, player.InvList); |
|
LoadMatchingItems(file, player, MaxBeltItems, player.SpdList); |
|
|
|
gbIsHellfireSaveGame = gbIsHellfire; |
|
} |
|
|
|
constexpr uint8_t StashVersion = 0; |
|
|
|
void LoadStash() |
|
{ |
|
const char *filename; |
|
if (!gbIsMultiplayer) |
|
filename = "spstashitems"; |
|
else |
|
filename = "mpstashitems"; |
|
|
|
Stash = {}; |
|
|
|
LoadHelper file(OpenStashArchive(), filename); |
|
if (!file.IsValid()) |
|
return; |
|
|
|
auto version = file.NextLE<uint8_t>(); |
|
if (version > StashVersion) |
|
return; |
|
|
|
Stash.gold = file.NextLE<uint32_t>(); |
|
|
|
auto pages = file.NextLE<uint32_t>(); |
|
for (unsigned i = 0; i < pages; i++) { |
|
auto page = file.NextLE<uint32_t>(); |
|
for (auto &row : Stash.stashGrids[page]) { |
|
for (uint16_t &cell : row) { |
|
cell = file.NextLE<uint16_t>(); |
|
} |
|
} |
|
} |
|
|
|
auto itemCount = file.NextLE<uint32_t>(); |
|
Stash.stashList.resize(itemCount); |
|
for (unsigned i = 0; i < itemCount; i++) { |
|
LoadAndValidateItemData(file, Stash.stashList[i]); |
|
} |
|
|
|
Stash.SetPage(file.NextLE<uint32_t>()); |
|
} |
|
|
|
void RemoveEmptyInventory(Player &player) |
|
{ |
|
for (int i = InventoryGridCells; i > 0; i--) { |
|
int8_t idx = player.InvGrid[i - 1]; |
|
if (idx > 0 && player.InvList[idx - 1].isEmpty()) { |
|
player.RemoveInvItem(idx - 1); |
|
} |
|
} |
|
} |
|
|
|
void LoadGame(bool firstflag) |
|
{ |
|
FreeGameMem(); |
|
|
|
LoadHelper file(OpenSaveArchive(gSaveNumber), "game"); |
|
if (!file.IsValid()) |
|
app_fatal(_("Unable to open save file archive")); |
|
|
|
if (!IsHeaderValid(file.NextLE<uint32_t>())) |
|
app_fatal(_("Invalid save file")); |
|
|
|
if (gbIsHellfireSaveGame) { |
|
giNumberOfLevels = 25; |
|
giNumberQuests = 24; |
|
giNumberOfSmithPremiumItems = 15; |
|
} else { |
|
// Todo initialize additional levels and quests if we are running Hellfire |
|
giNumberOfLevels = 17; |
|
giNumberQuests = 16; |
|
giNumberOfSmithPremiumItems = 6; |
|
} |
|
|
|
pfile_remove_temp_files(); |
|
|
|
setlevel = file.NextBool8(); |
|
setlvlnum = static_cast<_setlevels>(file.NextBE<uint32_t>()); |
|
currlevel = file.NextBE<uint32_t>(); |
|
leveltype = static_cast<dungeon_type>(file.NextBE<uint32_t>()); |
|
if (!setlevel) |
|
leveltype = GetLevelType(currlevel); |
|
int viewX = file.NextBE<int32_t>(); |
|
int viewY = file.NextBE<int32_t>(); |
|
invflag = file.NextBool8(); |
|
CharFlag = 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 && IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) |
|
app_fatal(_("Player is on a Hellfire only level")); |
|
|
|
for (uint8_t i = 0; i < giNumberOfLevels; i++) { |
|
DungeonSeeds[i] = file.NextBE<uint32_t>(); |
|
LevelSeeds[i] = std::nullopt; |
|
file.Skip(4); // Skip loading gnLevelTypeTbl |
|
} |
|
LoadLevelSeeds(); |
|
|
|
Player &myPlayer = *MyPlayer; |
|
|
|
LoadPlayer(file, myPlayer); |
|
|
|
if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL) |
|
sgGameInitInfo.nDifficulty = DIFF_NORMAL; |
|
|
|
for (int i = 0; i < giNumberQuests; i++) |
|
LoadQuest(&file, i); |
|
for (int i = 0; i < MAXPORTAL; i++) |
|
LoadPortal(&file, i); |
|
|
|
if (gbIsHellfireSaveGame != gbIsHellfire) { |
|
pfile_convert_levels(); |
|
RemoveEmptyInventory(myPlayer); |
|
} |
|
|
|
LoadGameLevel(firstflag, ENTRY_LOAD); |
|
SetPlrAnims(myPlayer); |
|
SyncPlrAnim(myPlayer); |
|
|
|
ViewPosition = { viewX, viewY }; |
|
ActiveMonsterCount = tmpNummonsters; |
|
ActiveObjectCount = tmpNobjects; |
|
|
|
for (int &monstkill : MonsterKillCounts) |
|
monstkill = file.NextBE<int32_t>(); |
|
|
|
// skip ahead for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES] |
|
file.Skip(4 * (MaxMonsters - NUM_MTYPES)); |
|
if (leveltype != DTYPE_TOWN) { |
|
for (unsigned &monsterId : ActiveMonsters) |
|
monsterId = file.NextBE<uint32_t>(); |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) |
|
LoadMonster(&file, Monsters[ActiveMonsters[i]]); |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) |
|
SyncPackSize(Monsters[ActiveMonsters[i]]); |
|
// Skip ActiveMissiles |
|
file.Skip<int8_t>(MaxMissilesForSaveGame); |
|
// Skip AvailableMissiles |
|
file.Skip<int8_t>(MaxMissilesForSaveGame); |
|
for (int i = 0; i < tmpNummissiles; i++) |
|
LoadMissile(&file); |
|
// For petrified monsters, the data in missile.var1 must be used to |
|
// load the appropriate animation data for the monster in missile.var2 |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) |
|
SyncMonsterAnim(Monsters[ActiveMonsters[i]]); |
|
for (int &objectId : ActiveObjects) |
|
objectId = file.NextLE<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]]); |
|
|
|
file.Skip<int32_t>(); // VisionId |
|
int visionCount = file.NextBE<int32_t>(); |
|
|
|
for (int i = 0; i < visionCount; i++) { |
|
LoadLighting(&file, &VisionList[i]); |
|
VisionActive[i] = true; |
|
} |
|
} |
|
|
|
LoadDroppedItems(file, savedItemCount); |
|
|
|
LoadAdditionalMissiles(); |
|
|
|
for (bool &uniqueItemFlag : UniqueItemFlags) |
|
uniqueItemFlag = file.NextBool8(); |
|
|
|
file.Skip<uint8_t>(MAXDUNY * MAXDUNX); // dLight |
|
for (int j = 0; j < MAXDUNY; j++) { |
|
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) |
|
dFlags[i][j] = static_cast<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>(); |
|
} |
|
file.Skip<uint8_t>(MAXDUNY * MAXDUNX); // dLight |
|
for (int j = 0; j < MAXDUNY; j++) { |
|
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) |
|
dPreLight[i][j] = file.NextLE<uint8_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; |
|
} |
|
} |
|
file.Skip(MAXDUNX * MAXDUNY); // dMissile |
|
|
|
// No need to load dLight, we can recreate it accurately from LightList |
|
memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light |
|
ChangeLightXY(myPlayer.lightId, myPlayer.position.tile); // forces player light refresh |
|
} else { |
|
memset(dLight, 0, sizeof(dLight)); |
|
} |
|
|
|
numpremium = file.NextBE<int32_t>(); |
|
premiumlevel = file.NextBE<int32_t>(); |
|
|
|
for (int i = 0; i < giNumberOfSmithPremiumItems; i++) |
|
LoadPremium(file, i); |
|
if (gbIsHellfire && !gbIsHellfireSaveGame) |
|
SpawnPremium(myPlayer); |
|
|
|
AutomapActive = file.NextBool8(); |
|
AutoMapScale = file.NextBE<int32_t>(); |
|
AutomapZoomReset(); |
|
ResyncQuests(); |
|
|
|
if (leveltype != DTYPE_TOWN) { |
|
RedoPlayerVision(); |
|
ProcessVisionList(); |
|
ProcessLightList(); |
|
} |
|
|
|
// convert stray manashield missiles into pManaShield flag |
|
for (auto &missile : Missiles) { |
|
if (missile._mitype == MissileID::ManaShield && !missile._miDelFlag) { |
|
Players[missile._misource].pManaShield = true; |
|
missile._miDelFlag = true; |
|
} |
|
} |
|
|
|
SetUpMissileAnimationData(); |
|
RedoMissileFlags(); |
|
NewCursor(CURSOR_HAND); |
|
gbProcessPlayers = IsDiabloAlive(!firstflag); |
|
|
|
if (gbIsHellfireSaveGame != gbIsHellfire) { |
|
SaveGame(); |
|
} |
|
|
|
gbIsHellfireSaveGame = gbIsHellfire; |
|
} |
|
|
|
void SaveHeroItems(SaveWriter &saveWriter, Player &player) |
|
{ |
|
size_t itemCount = static_cast<size_t>(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; |
|
SaveHelper file(saveWriter, "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 SaveStash(SaveWriter &stashWriter) |
|
{ |
|
const char *filename; |
|
if (!gbIsMultiplayer) |
|
filename = "spstashitems"; |
|
else |
|
filename = "mpstashitems"; |
|
|
|
const int itemSize = (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize); |
|
|
|
SaveHelper file( |
|
stashWriter, |
|
filename, |
|
sizeof(uint8_t) |
|
+ sizeof(uint32_t) |
|
+ sizeof(uint32_t) |
|
+ (sizeof(uint32_t) + 10 * 10 * sizeof(uint16_t)) * Stash.stashGrids.size() |
|
+ sizeof(uint32_t) |
|
+ itemSize * Stash.stashList.size() |
|
+ sizeof(uint32_t)); |
|
|
|
file.WriteLE<uint8_t>(StashVersion); |
|
|
|
file.WriteLE<uint32_t>(Stash.gold); |
|
|
|
std::vector<unsigned> pagesToSave; |
|
for (const auto &[page, grid] : Stash.stashGrids) { |
|
if (c_any_of(grid, [](const auto &row) { |
|
return c_any_of(row, [](StashStruct::StashCell cell) { |
|
return cell > 0; |
|
}); |
|
})) { |
|
// found a page that contains at least one item |
|
pagesToSave.push_back(page); |
|
} |
|
}; |
|
|
|
// Current stash size is 100 pages. Will definitely fit in a 32 bit value. |
|
file.WriteLE<uint32_t>(static_cast<uint32_t>(pagesToSave.size())); |
|
for (const auto &page : pagesToSave) { |
|
file.WriteLE<uint32_t>(page); |
|
for (const auto &row : Stash.stashGrids[page]) { |
|
for (uint16_t cell : row) { |
|
file.WriteLE<uint16_t>(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<uint32_t>(static_cast<uint32_t>(Stash.stashList.size())); |
|
for (const Item &item : Stash.stashList) { |
|
SaveItem(file, item); |
|
} |
|
|
|
file.WriteLE<uint32_t>(static_cast<uint32_t>(Stash.GetPage())); |
|
} |
|
|
|
void SaveGameData(SaveWriter &saveWriter) |
|
{ |
|
SaveHelper file(saveWriter, "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(_("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>(getHellfireLevelType(leveltype)); |
|
file.WriteBE<int32_t>(ViewPosition.x); |
|
file.WriteBE<int32_t>(ViewPosition.y); |
|
file.WriteLE<uint8_t>(invflag ? 1 : 0); |
|
file.WriteLE<uint8_t>(CharFlag ? 1 : 0); |
|
file.WriteBE(static_cast<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>(DungeonSeeds[i]); |
|
file.WriteBE<int32_t>(getHellfireLevelType(GetLevelType(i))); |
|
} |
|
|
|
Player &myPlayer = *MyPlayer; |
|
SavePlayer(file, myPlayer); |
|
|
|
for (int i = 0; i < giNumberQuests; i++) |
|
SaveQuest(&file, i); |
|
for (int i = 0; i < MAXPORTAL; i++) |
|
SavePortal(&file, i); |
|
for (int monstkill : MonsterKillCounts) |
|
file.WriteBE<int32_t>(monstkill); |
|
// add padding for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES] |
|
file.Skip(4 * (MaxMonsters - NUM_MTYPES)); |
|
|
|
if (leveltype != DTYPE_TOWN) { |
|
for (unsigned monsterId : ActiveMonsters) |
|
file.WriteBE<uint32_t>(monsterId); |
|
for (size_t i = 0; i < ActiveMonsterCount; i++) |
|
SaveMonster(&file, Monsters[ActiveMonsters[i]]); |
|
// Write ActiveMissiles |
|
for (uint8_t activeMissile = 0; activeMissile < MaxMissilesForSaveGame; activeMissile++) |
|
file.WriteLE<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]]); |
|
|
|
const auto visionCount = static_cast<int32_t>(Players.size()); |
|
file.WriteBE<int32_t>(visionCount + 1); // VisionId |
|
file.WriteBE<int32_t>(visionCount); |
|
|
|
for (const Player &player : Players) |
|
SaveLighting(&file, &VisionList[player.getId()], true); |
|
} |
|
|
|
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<uint8_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<uint8_t>(dLight[i][j]); // BUGFIX: dLight got saved already |
|
} |
|
for (int j = 0; j < MAXDUNY; j++) { |
|
for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) |
|
file.WriteLE<uint8_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 compatibility |
|
} |
|
} |
|
|
|
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(saveWriter); |
|
SaveLevelSeeds(saveWriter); |
|
} |
|
|
|
void SaveGame() |
|
{ |
|
gbValidSaveFile = true; |
|
pfile_write_hero(/*writeGameData=*/true); |
|
sfile_write_stash(); |
|
} |
|
|
|
void SaveLevel(SaveWriter &saveWriter) |
|
{ |
|
SaveLevel(saveWriter, nullptr); |
|
} |
|
|
|
void LoadLevel() |
|
{ |
|
LoadLevel(nullptr); |
|
} |
|
|
|
} // namespace devilution
|
|
|