/**
 * @file loadsave.cpp
 *
 * Implementation of save game functionality.
 */
#include "loadsave.h"

#include <climits>
#include <cstdint>
#include <cstring>
#include <numeric>
#include <unordered_map>

#include <SDL.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 "plrmsg.h"
#include "qol/stash.h"
#include "stores.h"
#include "utils/endian_read.hpp"
#include "utils/language.h"
#include "utils/stdcompat/algorithm.hpp"

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<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);
	}

	size_t Size()
	{
		return m_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>(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<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 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 toHit;
	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._iSelFlag = file.NextLE<uint8_t>();
	file.Skip(3); // Alignment
	item._iPostDraw = file.NextBool32();
	item._iIdentified = file.NextBool32();
	item._iMagical = static_cast<item_quality>(file.NextLE<int8_t>());
	file.NextBytes(item._iName, 64);
	file.NextBytes(item._iIName, 64);
	item._iLoc = static_cast<item_equip_type>(file.NextLE<int8_t>());
	item._iClass = static_cast<item_class>(file.NextLE<uint8_t>());
	file.Skip(1); // Alignment
	item._iCurs = file.NextLE<int32_t>();
	item._ivalue = file.NextLE<int32_t>();
	item._iIvalue = file.NextLE<int32_t>();
	item._iMinDam = file.NextLE<int32_t>();
	item._iMaxDam = file.NextLE<int32_t>();
	item._iAC = file.NextLE<int32_t>();
	item._iFlags = 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
	for (uint8_t &spellLevel : player._pSplLvl)
		spellLevel = file.NextLE<uint8_t>();
	file.Skip(7); // Alignment
	player._pMemSpells = file.NextLE<uint64_t>();
	player._pAblSpells = file.NextLE<uint64_t>();
	player._pScrlSpells = file.NextLE<uint64_t>();
	player._pSpellFlags = 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>();
	player._pBaseToBlk = file.NextLE<int32_t>();
	if (player._pBaseToBlk == 0)
		player._pBaseToBlk = PlayersData[static_cast<std::size_t>(player._pClass)].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._pLevel = file.NextLE<int8_t>();
	player._pMaxLvl = file.NextLE<int8_t>();
	file.Skip(2); // Alignment
	player._pExperience = file.NextLE<uint32_t>();
	file.Skip<uint32_t>();                        // Skip _pMaxExp - unused
	player._pNextExper = file.NextLE<uint32_t>(); // This can be calculated based on pLevel (which in turn could be calculated based on pExperience)
	player._pArmorClass = file.NextLE<int8_t>();
	player._pMagResist = file.NextLE<int8_t>();
	player._pFireResist = file.NextLE<int8_t>();
	player._pLghtResist = file.NextLE<int8_t>();
	player._pGold = file.NextLE<int32_t>();
	player._pInfraFlag = file.NextBool32();

	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
	CalcPlrInv(player, false);

	player.executedSpell = player.queuedSpell; // Ensures backwards compatibility

	// Omit pointer _pNData
	// Omit pointer _pWData
	// Omit pointer _pAData
	// Omit pointer _pLData
	// Omit pointer _pFData
	// Omit pointer  _pTData
	// Omit pointer _pHData
	// Omit pointer _pDData
	// Omit pointer _pBData
	// Omit pointer pReserved

	// Ensure plrIsOnSetLevel and plrlevel is correctly initialized, cause in vanilla sometimes plrlevel is not updated to setlvlnum
	if (setlevel)
		player.setLevel(setlvlnum);
	else
		player.setLevel(currlevel);
}

bool gbSkipSync = false;

void LoadMonster(LoadHelper *file, Monster &monster, 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 (monsterConversionData != nullptr)
		monsterConversionData->toHit = file->NextLE<uint8_t>();
	else if (monster.isPlayerMinion()) // Don't skip for golems
		monster.golemToHit = file->NextLE<uint8_t>();
	else
		file->Skip(1); // Skip toHit - now calculated on the fly
	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 - now calculated on the fly
	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 incorect values in old saves

	// Omit pointer name;

	if (monster.mode == MonsterMode::Petrified)
		monster.animInfo.isPetrified = true;
}

/**
 * @brief Recalculate the pack size of monster group that may have underflown
 */
void SyncPackSize(Monster &leader)
{
	if (!leader.isUnique())
		return;
	if (leader.ai != MonsterAIID::Scavenger)
		return;

	leader.packSize = 0;

	for (size_t i = 0; i < ActiveMonsterCount; i++) {
		auto &minion = Monsters[ActiveMonsters[i]];
		if (minion.leaderRelation == LeaderRelation::Leashed && minion.getLeader() == &leader)
			leader.packSize++;
	}
}

void LoadMissile(LoadHelper *file)
{
	Missile missile = {};
	missile._mitype = static_cast<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._oSelFlag = file.NextLE<int8_t>();
	file.Skip(3); // Alignment
	object._oPreFlag = file.NextBool32();
	object._oTrapFlag = file.NextBool32();
	object._oDoorFlag = file.NextBool32();
	object._olid = file.NextLE<int32_t>();
	object._oRndSeed = file.NextLE<uint32_t>();
	object._oVar1 = file.NextLE<int32_t>();
	object._oVar2 = file.NextLE<int32_t>();
	object._oVar3 = file.NextLE<int32_t>();
	object._oVar4 = file.NextLE<int32_t>();
	object._oVar5 = file.NextLE<int32_t>();
	object._oVar6 = file.NextLE<uint32_t>();
	object.bookMessage = static_cast<_speech_id>(file.NextLE<int32_t>());
	object._oVar8 = file.NextLE<int32_t>();
}

void LoadItem(LoadHelper &file, Item &item)
{
	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(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);
}

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 = clamp<int>(heroItem._iMaxCharges, 0, unpackedItem._iMaxCharges);
				unpackedItem._iCharges = clamp<int>(heroItem._iCharges, 0, unpackedItem._iMaxCharges);
			}
			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, 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>(item._iSelFlag);
	file.Skip(3); // Alignment
	file.WriteLE<uint32_t>(item._iPostDraw ? 1 : 0);
	file.WriteLE<uint32_t>(item._iIdentified ? 1 : 0);
	file.WriteLE<int8_t>(item._iMagical);
	file.WriteBytes(item._iName, 64);
	file.WriteBytes(item._iIName, 64);
	file.WriteLE<int8_t>(item._iLoc);
	file.WriteLE<uint8_t>(item._iClass);
	file.Skip(1); // Alignment
	file.WriteLE<int32_t>(item._iCurs);
	file.WriteLE<int32_t>(item._ivalue);
	file.WriteLE<int32_t>(item._iIvalue);
	file.WriteLE<int32_t>(item._iMinDam);
	file.WriteLE<int32_t>(item._iMaxDam);
	file.WriteLE<int32_t>(item._iAC);
	file.WriteLE<uint32_t>(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._pBaseToBlk);
	file.WriteLE<int32_t>(player._pHPBase);
	file.WriteLE<int32_t>(player._pMaxHPBase);
	file.WriteLE<int32_t>(player._pHitPoints);
	file.WriteLE<int32_t>(player._pMaxHP);
	file.Skip<int32_t>(); // Skip _pHPPer
	file.WriteLE<int32_t>(player._pManaBase);
	file.WriteLE<int32_t>(player._pMaxManaBase);
	file.WriteLE<int32_t>(player._pMana);
	file.WriteLE<int32_t>(player._pMaxMana);
	file.Skip<int32_t>(); // Skip _pManaPer
	file.WriteLE<int8_t>(player._pLevel);
	file.WriteLE<int8_t>(player._pMaxLvl);
	file.Skip(2); // Alignment
	file.WriteLE<uint32_t>(player._pExperience);
	file.Skip<uint32_t>(); // Skip _pMaxExp
	file.WriteLE<uint32_t>(player._pNextExper);
	file.WriteLE<int8_t>(player._pArmorClass);
	file.WriteLE<int8_t>(player._pMagResist);
	file.WriteLE<int8_t>(player._pFireResist);
	file.WriteLE<int8_t>(player._pLghtResist);
	file.WriteLE<int32_t>(player._pGold);
	file.WriteLE<uint32_t>(player._pInfraFlag ? 1 : 0);

	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))));

	if (monsterConversionData != nullptr)
		file->WriteLE<uint8_t>(monsterConversionData->toHit);
	else
		file->WriteLE<uint8_t>(static_cast<uint8_t>(std::min<uint16_t>(monster.toHit(sgGameInitInfo.nDifficulty), 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>(object._oSelFlag);
	file.Skip(3); // Alignment
	file.WriteLE<uint32_t>(object._oPreFlag ? 1 : 0);
	file.WriteLE<uint32_t>(object._oTrapFlag ? 1 : 0);
	file.WriteLE<uint32_t>(object._oDoorFlag ? 1 : 0);
	file.WriteLE<int32_t>(object._olid);
	file.WriteLE<uint32_t>(object._oRndSeed);

	/* 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 compatability, used in DRLG_CheckQuests
	file->WriteLE<uint8_t>(quest._qactive);
	file->WriteLE<uint8_t>(quest._qlvltype);
	file->WriteLE<int32_t>(quest.position.x);
	file->WriteLE<int32_t>(quest.position.y);
	file->WriteLE<uint8_t>(quest._qslvl);
	file->WriteLE<uint8_t>(quest._qidx);
	if (gbIsHellfire) {
		file->Skip(2); // Alignment
		file->WriteLE<int32_t>(quest._qmsg);
	} else {
		file->WriteLE<uint8_t>(quest._qmsg);
	}
	file->WriteLE<uint8_t>(quest._qvar1);
	file->WriteLE<uint8_t>(quest._qvar2);
	file->Skip(2); // Alignment
	if (!gbIsHellfire)
		file->Skip(1); // Alignment
	file->WriteLE<uint32_t>(quest._qlog ? 1 : 0);

	file->WriteBE<int32_t>(ReturnLvlPosition.x);
	file->WriteBE<int32_t>(ReturnLvlPosition.y);
	file->WriteBE<int32_t>(ReturnLevel);
	file->WriteBE<int32_t>(ReturnLevelType);
	file->Skip(sizeof(int32_t)); // Skip DoomQuestState
}

void SaveLighting(SaveHelper *file, Light *pLight, 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
 */
std::unordered_map<uint8_t, uint8_t> SaveDroppedItems(SaveHelper &file)
{
	// Vanilla Diablo/Hellfire initialise the ActiveItems and AvailableItems arrays based on saved data, so write valid values for compatibility
	for (uint8_t i = 0; i < MAXITEMS; i++)
		file.WriteLE<uint8_t>(i); // Strictly speaking everything from ActiveItemCount onwards is unused but no harm writing non-zero values here.
	for (uint8_t i = 0; i < MAXITEMS; i++)
		file.WriteLE<uint8_t>((i + ActiveItemCount) % MAXITEMS);

	std::unordered_map<uint8_t, uint8_t> itemIndexes = { { 0, 0 } };
	for (uint8_t i = 0; i < ActiveItemCount; i++) {
		itemIndexes[ActiveItems[i] + 1] = i + 1;
		SaveItem(file, Items[ActiveItems[i]]);
	}
	return itemIndexes;
}

/**
 * @brief Saves the position of dropped items (in dItem)
 * @param file interface to the save file
 * @param itemIndexes a map converting from runtime item indexes to the relative position in the save file
 */
void SaveDroppedItemLocations(SaveHelper &file, const std::unordered_map<uint8_t, uint8_t> &itemIndexes)
{
	for (int j = 0; j < MAXDUNY; j++) {
		for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
			file.WriteLE<uint8_t>(itemIndexes.at(dItem[i][j]));
	}
}

constexpr uint32_t VersionAdditionalMissiles = 0;

void SaveAdditionalMissiles(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 addtional Missiles saved
		return;
	}

	auto loadedVersion = file.NextLE<uint32_t>();
	if (loadedVersion > VersionAdditionalMissiles) {
		// unknown version
		return;
	}
	auto missileCountAdditional = file.NextLE<uint32_t>();
	for (uint32_t i = 0U; i < missileCountAdditional; i++) {
		LoadMissile(&file);
	}
}

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)
		glSeedTbl[0] = AdvanceRndSeed();

	char szName[MaxMpqPathSize];
	GetTempLevelNames(szName);
	SaveHelper file(saveWriter, szName, 256 * 1024);

	if (leveltype != DTYPE_TOWN) {
		for (int j = 0; j < MAXDUNY; j++) {
			for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert)
				file.WriteLE<int8_t>(dCorpse[i][j]);
		}
	}

	file.WriteBE<int32_t>(ActiveMonsterCount);
	file.WriteBE<int32_t>(ActiveItemCount);
	file.WriteBE<int32_t>(ActiveObjectCount);

	if (leveltype != DTYPE_TOWN) {
		for (int monsterId : ActiveMonsters)
			file.WriteBE<int32_t>(monsterId);
		for (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 (int &monsterId : ActiveMonsters)
			monsterId = file.NextBE<int32_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;

bool IsStashSizeValid(size_t stashSize, uint32_t pages, uint32_t itemCount)
{
	const size_t itemSize = (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize);

	const size_t expectedSize = sizeof(uint8_t)
	    + sizeof(uint32_t)
	    + sizeof(uint32_t)
	    + (sizeof(uint32_t) + 10 * 10 * sizeof(uint16_t)) * pages
	    + sizeof(uint32_t)
	    + itemSize * itemCount
	    + sizeof(uint32_t);

	return stashSize == expectedSize;
}

} // 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) {
		EventPlrMsg(_("Stash version invalid. If you attempt to access your stash, data will be overwritten!!"), UiFlags::ColorRed);
		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>();
	if (!IsStashSizeValid(file.Size(), pages, itemCount)) {
		Stash = {};
		EventPlrMsg(_("Stash size invalid. If you attempt to access your stash, data will be overwritten!!"), UiFlags::ColorRed);
		return;
	}
	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();
	chrflag = file.NextBool8();
	int tmpNummonsters = file.NextBE<int32_t>();
	auto savedItemCount = file.NextBE<uint32_t>();
	int tmpNummissiles = file.NextBE<int32_t>();
	int tmpNobjects = file.NextBE<int32_t>();

	if (!gbIsHellfire && IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT))
		app_fatal(_("Player is on a Hellfire only level"));

	for (uint8_t i = 0; i < giNumberOfLevels; i++) {
		glSeedTbl[i] = file.NextBE<uint32_t>();
		file.Skip(4); // Skip loading gnLevelTypeTbl
	}

	Player &myPlayer = *MyPlayer;

	LoadPlayer(file, myPlayer);

	if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL)
		sgGameInitInfo.nDifficulty = DIFF_NORMAL;

	for (int i = 0; i < giNumberQuests; i++)
		LoadQuest(&file, i);
	for (int i = 0; i < MAXPORTAL; i++)
		LoadPortal(&file, i);

	if (gbIsHellfireSaveGame != gbIsHellfire) {
		pfile_convert_levels();
		RemoveEmptyInventory(myPlayer);
	}

	LoadGameLevel(firstflag, ENTRY_LOAD);
	SetPlrAnims(myPlayer);
	SyncPlrAnim(myPlayer);

	ViewPosition = { viewX, viewY };
	ActiveMonsterCount = tmpNummonsters;
	ActiveObjectCount = tmpNobjects;

	for (int &monstkill : MonsterKillCounts)
		monstkill = file.NextBE<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 (int &monsterId : ActiveMonsters)
			monsterId = file.NextBE<int32_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;
		}
	}

	missiles_process_charge();
	RedoMissileFlags();
	NewCursor(CURSOR_HAND);
	gbProcessPlayers = IsDiabloAlive(!firstflag);

	if (gbIsHellfireSaveGame != gbIsHellfire) {
		SaveGame();
	}

	gbIsHellfireSaveGame = gbIsHellfire;
}

void SaveHeroItems(SaveWriter &saveWriter, Player &player)
{
	size_t itemCount = static_cast<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 &stashPage : Stash.stashGrids) {
		if (std::any_of(stashPage.second.cbegin(), stashPage.second.cend(), [](const auto &row) {
			    return std::any_of(row.cbegin(), row.cend(), [](auto cell) {
				    return cell > 0;
			    });
		    })) {
			// found a page that contains at least one item
			pagesToSave.push_back(stashPage.first);
		}
	};

	// Current stash size is 100 pages. Will definitely fit in a 32 bit value.
	file.WriteLE<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>(chrflag ? 1 : 0);
	file.WriteBE<int32_t>(ActiveMonsterCount);
	file.WriteBE<int32_t>(ActiveItemCount);
	// ActiveMissileCount will be a value from 0-125 (for vanilla compatibility). Writing an unsigned value here to avoid
	// warnings about casting from unsigned to signed, but there's no sign extension issues when reading this as a signed
	// value later so it doesn't have to match in LoadGameData().
	file.WriteBE<uint32_t>(static_cast<uint32_t>(std::min(Missiles.size(), MaxMissilesForSaveGame)));
	file.WriteBE<int32_t>(ActiveObjectCount);

	for (uint8_t i = 0; i < giNumberOfLevels; i++) {
		file.WriteBE<uint32_t>(glSeedTbl[i]);
		file.WriteBE<int32_t>(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 (int monsterId : ActiveMonsters)
			file.WriteBE<int32_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]]);

		int visionCount = 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 compatability
		}
	}

	file.WriteBE<int32_t>(numpremium);
	file.WriteBE<int32_t>(premiumlevel);

	for (int i = 0; i < giNumberOfSmithPremiumItems; i++)
		SaveItem(file, premiumitems[i]);

	file.WriteLE<uint8_t>(AutomapActive ? 1 : 0);
	file.WriteBE<int32_t>(AutoMapScale);

	SaveAdditionalMissiles(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
