/**
 * Canary - A free and open-source MMORPG server emulator
 * Copyright (©) 2019–present OpenTibiaBR <opentibiabr@outlook.com>
 * Repository: https://github.com/opentibiabr/canary
 * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
 * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
 * Website: https://docs.opentibiabr.com/
 */

#include "server/network/protocol/protocolgame.hpp"

#include "account/account.hpp"
#include "config/configmanager.hpp"
#include "core.hpp"
#include "creatures/appearance/mounts/mounts.hpp"
#include "creatures/appearance/attached_effects/attached_effects.hpp"
#include "creatures/combat/condition.hpp"
#include "creatures/combat/spells.hpp"
#include "creatures/interactions/chat.hpp"
#include "creatures/monsters/monster.hpp"
#include "creatures/monsters/monsters.hpp"
#include "creatures/npcs/npc.hpp"
#include "creatures/players/grouping/familiars.hpp"
#include "creatures/players/grouping/party.hpp"
#include "creatures/players/grouping/team_finder.hpp"
#include "creatures/players/highscore_category.hpp"
#include "creatures/players/imbuements/imbuements.hpp"
#include "creatures/players/livestream/livestream.hpp"
#include "creatures/players/management/ban.hpp"
#include "creatures/players/management/waitlist.hpp"
#include "creatures/players/player.hpp"
#include "creatures/players/components/player_forge_history.hpp"
#include "enums/player_icons.hpp"
#include "game/game.hpp"
#include "game/modal_window/modal_window.hpp"
#include "game/scheduling/dispatcher.hpp"
#include "io/functions/iologindata_load_player.hpp"
#include "io/io_bosstiary.hpp"
#include "io/iobestiary.hpp"
#include "io/iologindata.hpp"
#include "io/iomarket.hpp"
#include "io/ioguild.hpp"
#include "io/ioprey.hpp"
#include "items/items_classification.hpp"
#include "items/weapons/weapons.hpp"
#include "lua/creature/creatureevent.hpp"
#include "lua/modules/modules.hpp"
#include "server/network/connection/connection.hpp"
#include "server/network/message/outputmessage.hpp"
#include "server/network/protocol/protocol_port_utils.hpp"
#include "server/network/protocol/transport_codec.hpp"
#include "utils/tools.hpp"
#include "creatures/players/vocations/vocation.hpp"

#include "enums/account_coins.hpp"
#include "enums/account_group_type.hpp"
#include "enums/account_type.hpp"
#include "enums/object_category.hpp"
#include "enums/player_blessings.hpp"
#include "enums/player_cyclopedia.hpp"
#include "enums/player_wheel.hpp"
#include "enums/container_type.hpp"
#include "enums/imbuement.hpp"

#ifndef USE_PRECOMPILED_HEADERS
	#include <chrono>
	#include <string_view>
#endif

/*
 * NOTE: This namespace is used so that we can add functions without having to declare them in the ".hpp/.hpp" file
 * Do not use functions only in the .cpp scope without having a namespace, it may conflict with functions in other files of the same name
 */

// This "getIteration" function will allow us to get the total number of iterations that run within a specific map
// Very useful to send the total amount in certain bytes in the ProtocolGame class
namespace {
	constexpr uint64_t PARTY_ANALYZER_THROTTLE_MS = 1000;
	constexpr size_t UPDATE_CONTAINER_PAYLOAD_SIZE = 1;
	constexpr uint8_t CLIENT_PACKET_TASKBOARD = 0x5F;
	constexpr uint8_t CLIENT_PACKET_SOUL_SEALS_FIGHT_MONSTER = 0xBA;
	constexpr uint8_t CLIENT_PACKET_OFFER_DESCRIPTION = 0xE8;

	[[nodiscard]] const ProtocolProfile* getPortPinnedProfile(uint16_t localPort) {
		if (localPort != protocol_port_utils::getModernGamePort() && localPort == protocol_port_utils::getLegacy1100GamePort()) {
			return ProtocolProfileRegistry::getProfile(ProtocolProfileId::Tibia1100);
		}

		if (localPort != protocol_port_utils::getModernGamePort() && localPort == protocol_port_utils::getLegacy860GamePort()) {
			return ProtocolProfileRegistry::getProfile(ProtocolProfileId::Cipsoft860Vanilla);
		}

		return ProtocolProfileRegistry::getProfile(ProtocolProfileId::Current);
	}

	[[nodiscard]] size_t getUnreadBytes(const NetworkMessage &msg) {
		const auto consumedBytes = static_cast<size_t>(msg.getBufferPosition() - NetworkMessage::INITIAL_BUFFER_POSITION);
		return msg.getLength() > consumedBytes ? msg.getLength() - consumedBytes : 0;
	}

	[[nodiscard]] bool hasClientBuildPrefix(std::string_view versionString, std::string_view buildPrefix) {
		return versionString.size() >= buildPrefix.size()
			&& versionString.substr(0, buildPrefix.size()) == buildPrefix;
	}

	[[nodiscard]] bool supportsWeaponProficiencyDetailList(std::string_view versionString) {
		// 15.25 builds are not byte-identical here. Unknown builds keep the
		// shorter shape until a capture proves the trailing list is required.
		const bool known = hasClientBuildPrefix(versionString, "15.25.794c2e")
			|| hasClientBuildPrefix(versionString, "15.25.d96c64");
		if (!known && hasClientBuildPrefix(versionString, "15.25.")) {
			g_logger().debug("[WeaponProficiency] unrecognized 15.25 build '{}'; detail list will not be sent.", versionString);
		}
		return known;
	}

	/**
	 * @brief Checks whether the 0xC4 payload should include the trailing detail-list section.
	 *
	 * The runtime profile feature gates the current weapon-proficiency packet family.
	 * The login build string gates the trailing list inside that packet because known
	 * 15.25 builds are not byte-identical: earlier builds debug if the empty trailing
	 * list count is sent, while captured later builds require that list boundary.
	 */
	[[nodiscard]] bool shouldSendWeaponProficiencyDetailList(const ProtocolProfile* profile, std::string_view versionString) {
		return profile
			&& profile->hasFeature(ProtocolFeature::OfficialWeaponProficiencyPayload)
			&& supportsWeaponProficiencyDetailList(versionString);
	}

	[[nodiscard]] bool shouldDispatchRecvbyteModuleForProfile(const ProtocolProfile* profile, uint8_t recvbyte) {
		if (recvbyte == CLIENT_PACKET_TASKBOARD) {
			return profile && profile->hasFeature(ProtocolFeature::OfficialTaskboardPackets);
		}

		if (recvbyte == CLIENT_PACKET_SOUL_SEALS_FIGHT_MONSTER) {
			return profile && profile->hasFeature(ProtocolFeature::OfficialSoulSealsPackets);
		}

		if (recvbyte == CLIENT_PACKET_OFFER_DESCRIPTION && profile && profile->hasFeature(ProtocolFeature::CurrentPayload)) {
			return false;
		}

		return true;
	}

	[[nodiscard]] bool resourceBalanceUsesU32(uint8_t resourceType) {
		switch (resourceType) {
			case RESOURCE_CHARM:
			case RESOURCE_MINOR_CHARM:
			case RESOURCE_MAX_CHARM:
			case RESOURCE_MAX_MINOR_CHARM:
			case RESOURCE_NPC_TRADE_QUEST_FLAG_CURRENCY:
			case RESOURCE_UNSPENT_SKILL_POINTS:
			case RESOURCE_BOUNTY_POINTS:
			case RESOURCE_SOULSEALS:
				return true;
			default:
				return false;
		}
	}

	[[nodiscard]] bool itemTypeHasSubtype(uint16_t itemId) {
		return Item::items.hasItemType(itemId) && Item::items[itemId].hasSubType();
	}

	[[nodiscard]] std::string getMarketDetailImbuementEffect(uint16_t itemId) {
		if (const auto* imbuement = g_imbuements().getImbuementByScrollID(itemId); imbuement != nullptr) {
			return imbuement->getDescription();
		}

		return {};
	}

	template <typename T>
	uint16_t getVectorIterationIncreaseCount(T &vector) {
		uint16_t totalIterationCount = 0;
		for ([[maybe_unused]] const auto &vectorIteration : vector) {
			totalIterationCount++;
		}

		return totalIterationCount;
	}

	template <typename LookupFunc>
	void addExivaEntries(NetworkMessage &msg, std::vector<uint32_t> &whitelist, std::vector<std::string> &addedNames, std::unordered_set<uint32_t> &addedGuids, int32_t maxLimit, LookupFunc lookup) {
		const auto size = msg.get<uint16_t>();
		uint32_t lookupAttempts = 0;
		for (uint16_t i = 0; i < size; i++) {
			std::string name = msg.getString();
			if (whitelist.size() >= static_cast<size_t>(maxLimit) || addedGuids.size() >= static_cast<size_t>(maxLimit) || lookupAttempts >= static_cast<uint32_t>(maxLimit)) {
				continue;
			}

			lookupAttempts++;
			uint32_t id = lookup(name);
			if (id != 0 && addedGuids.insert(id).second && std::ranges::find(whitelist, id) == whitelist.end()) {
				whitelist.push_back(id);
				addedNames.push_back(name);
			}
		}
	}

	template <typename LookupFunc>
	void removeExivaEntries(NetworkMessage &msg, std::vector<uint32_t> &whitelist, std::vector<std::string> &removedNames, std::unordered_set<uint32_t> &removedGuids, int32_t maxLimit, LookupFunc lookup) {
		const auto size = msg.get<uint16_t>();
		uint32_t lookupAttempts = 0;
		for (uint16_t i = 0; i < size; i++) {
			std::string name = msg.getString();
			if (removedGuids.size() >= static_cast<size_t>(maxLimit) || lookupAttempts >= static_cast<uint32_t>(maxLimit)) {
				continue;
			}

			lookupAttempts++;
			uint32_t id = lookup(name);
			if (id != 0 && removedGuids.insert(id).second && std::erase(whitelist, id) > 0) {
				removedNames.push_back(name);
			}
		}
	}

	void addOutfitAndMountBytes(NetworkMessage &msg, const std::shared_ptr<Item> &item, const CustomAttribute* attribute, const std::string &head, const std::string &body, const std::string &legs, const std::string &feet, bool addAddon = false, bool addByte = false) {
		auto look = attribute->getAttribute<uint16_t>();
		msg.add<uint16_t>(look);
		if (look != 0) {
			const auto lookHead = item->getCustomAttribute(head);
			const auto lookBody = item->getCustomAttribute(body);
			const auto lookLegs = item->getCustomAttribute(legs);
			const auto lookFeet = item->getCustomAttribute(feet);

			msg.addByte(lookHead ? lookHead->getAttribute<uint8_t>() : 0);
			msg.addByte(lookBody ? lookBody->getAttribute<uint8_t>() : 0);
			msg.addByte(lookLegs ? lookLegs->getAttribute<uint8_t>() : 0);
			msg.addByte(lookFeet ? lookFeet->getAttribute<uint8_t>() : 0);

			if (addAddon) {
				const auto lookAddons = item->getCustomAttribute("LookAddons");
				msg.addByte(lookAddons ? lookAddons->getAttribute<uint8_t>() : 0);
			}
		} else {
			if (addByte) {
				msg.add<uint16_t>(0);
			}
		}
	}

	// Send bytes function for avoid repetitions
	void sendBosstiarySlotsBytes(NetworkMessage &msg, uint8_t bossRace, uint32_t bossKillCount, uint16_t bonusBossSlotOne, uint8_t killBonus, uint8_t isSlotOneInactive, uint32_t removePrice) {
		msg.addByte(bossRace); // Boss Race
		msg.add<uint32_t>(bossKillCount); // Kill Count
		msg.add<uint16_t>(bonusBossSlotOne); // Loot Bonus
		msg.addByte(killBonus); // Kill Bonus
		msg.addByte(bossRace); // Boss Race
		msg.add<uint32_t>(isSlotOneInactive == 1 ? 0 : removePrice); // Remove Price
		msg.addByte(isSlotOneInactive); // Inactive? (Only true if equal to Boosted Boss)
	}

	/**
	 * @brief Handles the imbuement damage for a player and adds it to the network message.
	 * @details This function checks if the player's weapon has any imbuements that provide combat-type damage.
	 * @details If such imbuements are found, the corresponding damage values and combat types are added to the network message.
	 * @details If no imbuement damage is found, default values are added to the message.
	 *
	 * @param msg The network message to which the imbuement damage should be added.
	 * @param player Pointer to the player for whom the imbuement damage should be handled.
	 */
	void handleImbuementDamage(NetworkMessage &msg, const std::shared_ptr<Player> &player) {
		bool imbueDmg = false;
		const auto &weapon = player->getWeapon();
		if (weapon) {
			uint8_t slots = Item::items[weapon->getID()].imbuementSlot;
			if (slots > 0) {
				for (uint8_t i = 0; i < slots; i++) {
					ImbuementInfo imbuementInfo;
					if (!weapon->getImbuementInfo(i, &imbuementInfo)) {
						continue;
					}

					if (imbuementInfo.duration > 0) {
						auto imbuement = *imbuementInfo.imbuement;
						bool hasValidCombat = imbuement.combatType != COMBAT_NONE && imbuement.combatType < COMBAT_COUNT;
						if (hasValidCombat) {
							msg.addDouble(imbuement.elementDamage / 100.);
							msg.addByte(getCipbiaElement(imbuement.combatType));
							imbueDmg = true;
							break;
						}
					}
				}
			}
		}

		if (!imbueDmg) {
			msg.addDouble(0);
			msg.addByte(0);
		}
	}

	/**
	 * @brief Calculates the absorb values for different combat types based on player's equipped items.
	 *
	 * This function calculates the absorb values for each combat type based on the items equipped by the player.
	 * The calculated absorb values are stored in the provided array.
	 *
	 * @param[in] player The pointer to the player whose equipped items are considered.
	 */
	void calculateAbsorbValues(const std::shared_ptr<Player> &player, NetworkMessage &msg, uint8_t &combats, bool fromPlayerSkills = false) {
		alignas(16) uint16_t damageModifiers[COMBAT_COUNT] = { 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000 };

		for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) {
			if (!player->isItemAbilityEnabled(static_cast<Slots_t>(slot))) {
				continue;
			}

			const auto item = player->getInventoryItem(static_cast<Slots_t>(slot));
			if (!item) {
				continue;
			}

			const ItemType &itemType = Item::items[item->getID()];
			if (!itemType.abilities) {
				continue;
			}

			for (uint16_t i = 0; i < COMBAT_COUNT; ++i) {
				damageModifiers[i] *= (std::floor(100. - itemType.abilities->absorbPercent[i]) / 100.);
			}

			uint8_t imbuementSlots = itemType.imbuementSlot;
			if (imbuementSlots > 0) {
				for (uint8_t slotId = 0; slotId < imbuementSlots; ++slotId) {
					ImbuementInfo imbuementInfo;
					if (!item->getImbuementInfo(slotId, &imbuementInfo)) {
						continue;
					}

					if (imbuementInfo.duration == 0) {
						continue;
					}

					auto imbuement = *imbuementInfo.imbuement;
					for (uint16_t combat = 0; combat < COMBAT_COUNT; ++combat) {
						const int16_t &imbuementAbsorbPercent = imbuement.absorbPercent[combat];
						if (imbuementAbsorbPercent == 0) {
							continue;
						}

						g_logger().debug("[cyclopedia damage reduction] imbued item {}, reduced {} percent, for element {}", item->getName(), imbuementAbsorbPercent, combatTypeToName(indexToCombatType(combat)));

						damageModifiers[combat] *= (std::floor(100. - imbuementAbsorbPercent) / 100.);
					}
				}
			}
		}

		for (size_t i = 0; i < COMBAT_COUNT; ++i) {
			damageModifiers[i] -= 100 * player->getAbsorbPercent(indexToCombatType(i));
			if (g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) {
				damageModifiers[i] -= player->wheel().getResistance(indexToCombatType(i));
			}

			if (damageModifiers[i] != 10000) {
				double clientModifier = (10000 - static_cast<int16_t>(damageModifiers[i])) / 10000.;
				g_logger().debug("[{}] CombatType: {}, Damage Modifier: {}, Resulting Client Modifier: {}", __FUNCTION__, i, damageModifiers[i], clientModifier);
				if (!fromPlayerSkills) {
					msg.addByte(0x04);
				}
				msg.addByte(getCipbiaElement(indexToCombatType(i)));
				msg.addDouble(clientModifier);
				++combats;
			}
		}
	}

	/**
	 * @brief Sends the container category to the network message.
	 *
	 * @note The default value is "all", which is the first enum (0). The message must always start with "All".
	 *
	 * @details for example of enum see the ContainerCategory_t
	 *
	 * @param msg The network message to send the category to.
	 */
	template <typename T>
	void sendContainerCategory(NetworkMessage &msg, const std::vector<T> &categories = {}, uint8_t categoryType = 0) {
		msg.addByte(categoryType);
		g_logger().debug("Sendding category type '{}', categories total size '{}'", categoryType, categories.size());
		msg.addByte(categories.size());
		for (auto value : categories) {
			if (value == T::All) {
				continue;
			}

			g_logger().debug("Sending category number '{}', category name '{}'", static_cast<uint8_t>(value), magic_enum::enum_name(value).data());
			msg.addByte(static_cast<uint8_t>(value));
			msg.addString(toStartCaseWithSpace(magic_enum::enum_name(value).data()));
		}
	}

	template <typename UserMap>
	void addChannelUserNames(NetworkMessage &msg, const UserMap* users) {
		if (!users) {
			msg.add<uint16_t>(0x00);
			return;
		}

		msg.add<uint16_t>(users->size());
		for (const auto &it : *users) {
			msg.addString(it.second->getName());
		}
	}

	/**
	 * @brief Calculates and adds the values for different skills based on the player's equipped items and other factors.
	 *
	 * This function calculates the total, equipment-based, imbuement-based, and wheel-based contributions to a specific skill
	 * of the player. These values are then added to the provided `NetworkMessage` object.
	 *
	 * @param[in] player The pointer to the player whose skills and equipment are considered.
	 * @param[in] msg The network message to which the calculated skill values will be added.
	 * @param[in] skill The specific skill to calculate (e.g., Life Leech, Mana Leech, Critical Hit Damage, etc.).
	 */
	void addCyclopediaSkills(std::shared_ptr<Player> &player, NetworkMessage &msg, skills_t skill, const SkillsEquipment &skillEquipment) {
		const auto skillTotal = player->getSkillLevel(skill);

		double skillWheel = 0.0;
		const auto &playerWheel = player->wheel();
		if (skill == SKILL_LIFE_LEECH_AMOUNT) {
			skillWheel = playerWheel.getStat(WheelStat_t::LIFE_LEECH);
		} else if (skill == SKILL_MANA_LEECH_AMOUNT) {
			skillWheel = playerWheel.getStat(WheelStat_t::MANA_LEECH);
		} else if (skill == SKILL_CRITICAL_HIT_DAMAGE) {
			skillWheel = playerWheel.getStat(WheelStat_t::CRITICAL_DAMAGE);
			skillWheel += playerWheel.getMajorStatConditional("Combat Mastery", WheelMajor_t::CRITICAL_DMG_2);
			skillWheel += playerWheel.getMajorStatConditional("Ballistic Mastery", WheelMajor_t::CRITICAL_DMG);
			skillWheel += playerWheel.checkAvatarSkill(WheelAvatarSkill_t::CRITICAL_DAMAGE);
		}

		double skillEquipmentValue = skillEquipment.equipment + (player->weaponProficiency().getSkillBonus(skill) / 10000.);
		double skillEvent = 0.0;

		msg.addDouble(skillTotal / 10000.);
		msg.addDouble(skillEquipmentValue);
		msg.addDouble(skillEquipment.imbuement);
		msg.addDouble(skillWheel / 10000.);
		msg.addDouble(skillEvent);
	}

	void addCyclopediaCriticalSkill(std::shared_ptr<Player> &player, NetworkMessage &msg, skills_t skill, const SkillsEquipment &skillEquipment) {
		const auto &playerBaseCritical = player->getBaseCritical();

		double concoctionCritical = 0.0;
		double baseCritical = skill == SKILL_CRITICAL_HIT_CHANCE ? playerBaseCritical.chance : playerBaseCritical.damage;
		double wheelCritical = player->wheel().getStat(skill == SKILL_CRITICAL_HIT_CHANCE ? WheelStat_t::CRITICAL_CHANCE : WheelStat_t::CRITICAL_DAMAGE) / 10000.0;

		const auto &proficiencyCritical = player->weaponProficiency().getGeneralCritical();
		double proficiencyCriticalValue = skill == SKILL_CRITICAL_HIT_CHANCE ? proficiencyCritical.chance : proficiencyCritical.damage;
		double skillEquipmentValue = skillEquipment.equipment + proficiencyCriticalValue;
		const auto totalCritical = baseCritical + skillEquipmentValue + wheelCritical + skillEquipment.imbuement + concoctionCritical;

		msg.addDouble(totalCritical);
		msg.addDouble(baseCritical);
		msg.addDouble(skillEquipmentValue);
		msg.addDouble(skillEquipment.imbuement);
		msg.addDouble(wheelCritical);
		msg.addDouble(concoctionCritical);
	}

	[[nodiscard]] bool isCipsoft860Profile(const ProtocolProfile* profile) {
		using enum ProtocolProfileId;
		return profile
			&& (profile->id == Cipsoft860Vanilla
		        || profile->id == Cipsoft860ExtendedAssets
		        || profile->id == Cipsoft860CanaryExtended);
	}

	[[nodiscard]] bool isTibia1100Profile(const ProtocolProfile* profile) {
		return profile && profile->id == ProtocolProfileId::Tibia1100;
	}

	[[nodiscard]] bool hasProtocolFeature(const ProtocolProfile* profile, ProtocolFeature feature) {
		return profile && profile->hasFeature(feature);
	}

	[[nodiscard]] std::string_view getProtocolSupportLabel(const ProtocolProfile* profile) {
		if (isCipsoft860Profile(profile)) {
			return "8.60";
		}
		if (isTibia1100Profile(profile)) {
			return "11.00";
		}
		if (profile && !profile->supportLabel.empty()) {
			return profile->supportLabel;
		}
		return "this client version";
	}

	[[nodiscard]] bool shouldBlockOutgoingOpcodeForProfile(const ProtocolProfile* profile, uint8_t opcode) {
		switch (opcode) {
			case 0xB0:
				return !hasProtocolFeature(profile, ProtocolFeature::MemorialPackets);
			default:
				return false;
		}
	}

	[[nodiscard]] std::string getUnsupportedOpcodeMessage(const ProtocolProfile* profile, uint8_t opcode) {
		switch (opcode) {
			case 0xB0:
				return fmt::format("The memorial window is not available in client {}.", getProtocolSupportLabel(profile));
			default:
				return "This feature is not available in your client version.";
		}
	}

	[[nodiscard]] bool usesLegacyInnerLength(const ProtocolProfile* profile) {
		return profile
			&& ProtocolProfileRegistry::getTransportProfile(profile->initialBehavior.transport).encryptedPayload == EncryptedPayloadLayout::LegacyInnerLength;
	}

	[[nodiscard]] std::optional<Direction> translateAutoWalkDirection(uint8_t rawDirection) {
		switch (rawDirection) {
			case 1:
				return DIRECTION_EAST;
			case 2:
				return DIRECTION_NORTHEAST;
			case 3:
				return DIRECTION_NORTH;
			case 4:
				return DIRECTION_NORTHWEST;
			case 5:
				return DIRECTION_WEST;
			case 6:
				return DIRECTION_SOUTHWEST;
			case 7:
				return DIRECTION_SOUTH;
			case 8:
				return DIRECTION_SOUTHEAST;
			default:
				return std::nullopt;
		}
	}

	constexpr uint8_t cipsoft860TalkNone = 0;
	constexpr uint8_t cipsoft860EventDefaultMessage = 0x18;
	constexpr uint32_t cipsoft860MaxSignedExperienceLevel = 506;
	constexpr uint32_t cipsoft860MaxSignedExperience = static_cast<uint32_t>(std::numeric_limits<int32_t>::max());

	[[nodiscard]] uint8_t translateCipsoft860SpeakClassToClient(SpeakClasses talkType) {
		switch (talkType) {
			case TALKTYPE_SAY:
				return 0x01;
			case TALKTYPE_WHISPER:
				return 0x02;
			case TALKTYPE_YELL:
				return 0x03;
			case TALKTYPE_PRIVATE_FROM:
			case TALKTYPE_PRIVATE_TO:
				return 0x06;
			case TALKTYPE_CHANNEL_MANAGER:
				return 0x08;
			case TALKTYPE_CHANNEL_Y:
				return 0x07;
			case TALKTYPE_CHANNEL_O:
				return 0x0F;
			case TALKTYPE_SPELL_USE:
				return 0x01;
			case TALKTYPE_PRIVATE_NP:
			case TALKTYPE_NPC_UNKOWN:
				return 0x05;
			case TALKTYPE_PRIVATE_PN:
				return 0x04;
			case TALKTYPE_BROADCAST:
				return 0x0C;
			case TALKTYPE_CHANNEL_R1:
				return 0x0D;
			case TALKTYPE_PRIVATE_RED_FROM:
			case TALKTYPE_PRIVATE_RED_TO:
				return 0x0E;
			case TALKTYPE_MONSTER_SAY:
				return 0x13;
			case TALKTYPE_MONSTER_YELL:
				return 0x14;
			default:
				return cipsoft860TalkNone;
		}
	}

	[[nodiscard]] uint8_t translateCipsoft860MessageClassToClient(MessageClasses messageType) {
		switch (messageType) {
			case MESSAGE_LOGIN:
				return 0x17;
			case MESSAGE_ADMINISTRATOR:
			case MESSAGE_GAME_HIGHLIGHT:
				return 0x15;
			case MESSAGE_EVENT_ADVANCE:
				return 0x16;
			case MESSAGE_FAILURE:
				return 0x1A;
			case MESSAGE_LOOK:
			case MESSAGE_LOOT:
			case MESSAGE_TRADE:
			case MESSAGE_REPORT:
			case MESSAGE_HOTKEY_PRESSED:
			case MESSAGE_TUTORIAL_HINT:
			case MESSAGE_THANK_YOU:
				return 0x19;
			case MESSAGE_STATUS:
				return cipsoft860EventDefaultMessage;
			case MESSAGE_GAMEMASTER_CONSOLE:
				return 0x12;
			default:
				return MESSAGE_NONE;
		}
	}
} // namespace

ProtocolGame::ProtocolGame(const Connection_ptr &initConnection) :
	Protocol(initConnection) {
	version = CLIENT_VERSION;
}

void ProtocolGame::onConnectionAccepted() {
	const auto* portPinnedProfile = ProtocolProfileRegistry::getProfile(ProtocolProfileId::Current);
	sessionHintLease.reset();
	auto transportState = InitialTransportState::ResolvedModernDefault;

	if (const auto connection = getConnection()) {
		portPinnedProfile = getPortPinnedProfile(connection->getLocalPort());
		if (!portPinnedProfile || !ProtocolProfileRegistry::isProfileAllowed(portPinnedProfile->id)) {
			connection->setInitialTransportState(InitialTransportState::Rejected);
			connection->close(FORCE_CLOSE);
			return;
		}

		initialConnectionBehavior = portPinnedProfile->initialBehavior;
		if (portPinnedProfile->id == ProtocolProfileId::Current) {
			transportState = InitialTransportState::ResolvedModernDefault;
		} else {
			transportState = InitialTransportState::ResolvedFromPrelude;
		}

		if (portPinnedProfile->id == ProtocolProfileId::Cipsoft860Vanilla) {
			if (auto lease = ProtocolSessionHintStore::getInstance().claimByIp(connection->getIP(), initialConnectionBehavior)) {
				sessionHintLease = std::move(*lease);
				transportState = InitialTransportState::ResolvedFromHint;
			}
		}

		protocolProfile = portPinnedProfile;
		if (!protocolProfile || !ProtocolProfileRegistry::isProfileAllowed(protocolProfile->id)) {
			connection->setInitialTransportState(InitialTransportState::Rejected);
			connection->close(FORCE_CLOSE);
			return;
		}

		connection->setTransportCodec(TransportCodecs::get(initialConnectionBehavior.transport), transportState);
	}

	if (initialConnectionBehavior.challenge.flow == GameHandshakeFlow::ServerChallengeBeforeLogin) {
		dispatchProtocolTask([self = getThis()] { self->sendLoginChallenge(); }, "ProtocolGame::sendLoginChallenge", std::chrono::milliseconds(CONNECTION_WRITE_TIMEOUT * 1000).count());
	}
}

void ProtocolGame::clearReusableSessionHints() {
	// Legacy 8.6 clients can return to the character list and reconnect to the world
	// without hitting ProtocolLogin again. Their reusable hint must survive a normal
	// login/logout cycle so the next 8.6 world socket can resolve the exact profile.
	if (loggedIn) {
		return;
	}

	if (initialConnectionBehavior.hasSameWireBehavior(ProtocolProfileRegistry::defaultModernInitialBehavior())) {
		return;
	}

	ProtocolSessionHintStore::getInstance().clearReusableHintsByIp(getIP(), initialConnectionBehavior);
}

bool ProtocolGame::isSessionEnding() const {
	return loggedIn && !acceptPackets;
}

void ProtocolGame::AddItem(NetworkMessage &msg, uint16_t id, uint8_t count, uint8_t tier) const {
	const ItemType &it = Item::items[id];

	msg.add<uint16_t>(it.id);

	if (oldProtocol && version >= 1000) {
		msg.addByte(0xFF);
	}

	if (it.stackable) {
		msg.addByte(count);
	}

	if (it.isSplash() || it.isFluidContainer()) {
		msg.addByte(count);
	}

	if (oldProtocol) {
		if (version >= 910) {
			if (it.animationType == ANIMATION_RANDOM) {
				msg.addByte(0xFE);
			} else if (it.animationType == ANIMATION_DESYNC) {
				msg.addByte(0xFF);
			}

			// OTCR Features
			if (isOTCR) {
				msg.addString(""); // g_game.enableFeature(GameItemShader)
			}
		}
		return;
	}

	if (it.isContainer()) {
		msg.addByte(0x00);
	}

	if (it.isPodium) {
		msg.add<uint16_t>(0);
		msg.add<uint16_t>(0);
		msg.add<uint16_t>(0);

		msg.addByte(2);
		msg.addByte(0x01);
	}

	if (it.upgradeClassification > 0) {
		msg.addByte(tier);
	}

	if (it.expire || it.expireStop || it.clockExpire) {
		msg.add<uint32_t>(it.decayTime);
		msg.addByte(0x01); // Brand-new
	}

	if (it.wearOut) {
		msg.add<uint32_t>(it.charges);
		msg.addByte(0x01); // Brand-new
	}

	if (it.isWrapKit && !oldProtocol) {
		msg.add<uint16_t>(0x00);
	}

	// OTCR Features
	if (isOTCR) {
		msg.addString(""); // g_game.enableFeature(GameItemShader)
	}
}

void ProtocolGame::AddItem(NetworkMessage &msg, const std::shared_ptr<Item> &item) {
	if (!item) {
		return;
	}

	const ItemType &it = Item::items[item->getID()];

	msg.add<uint16_t>(it.id);

	if (oldProtocol && version >= 1000) {
		msg.addByte(0xFF);
	}

	if (it.stackable) {
		msg.addByte(static_cast<uint8_t>(std::min<uint16_t>(std::numeric_limits<uint8_t>::max(), item->getItemCount())));
	}

	if (it.isSplash() || it.isFluidContainer()) {
		msg.addByte(item->getAttribute<uint8_t>(ItemAttribute_t::FLUIDTYPE));
	}

	if (oldProtocol) {
		if (version >= 910) {
			if (it.animationType == ANIMATION_RANDOM) {
				msg.addByte(0xFE);
			} else if (it.animationType == ANIMATION_DESYNC) {
				msg.addByte(0xFF);
			}

			// OTCR Features
			if (isOTCR) {
				msg.addString(item->getShader()); // g_game.enableFeature(GameItemShader)
			}
		}
		return;
	}

	const auto &container = item->getContainer();
	if (it.isContainer() && container) {
		ContainerSpecial_t containerType = container->getSpecialCategory(player);
		msg.addByte(enumToValue(containerType));
		switch (containerType) {
			case ContainerSpecial_t::LootHighlight:
				break;
			case ContainerSpecial_t::Manager: {
				auto [lootFlags, obtainFlags] = container->getObjectCategoryFlags(player);
				msg.add<uint32_t>(lootFlags);
				msg.add<uint32_t>(obtainFlags);
				break;
			}
			case ContainerSpecial_t::ContentCounter: {
				auto ammoTotal = container->getAmmoAmount(player);
				msg.add<uint32_t>(ammoTotal);
				break;
			}
			case ContainerSpecial_t::QuiverLoot: {
				auto ammoTotal = container->getAmmoAmount(player);
				auto [lootFlags, obtainFlags] = container->getObjectCategoryFlags(player);
				msg.add<uint32_t>(lootFlags);
				msg.add<uint32_t>(ammoTotal);
				msg.add<uint32_t>(obtainFlags);
				break;
			}
			default:
				break;
		}
	}

	if (it.isPodium) {
		const auto podiumVisible = item->getCustomAttribute("PodiumVisible");
		const auto lookType = item->getCustomAttribute("LookType");
		const auto lookTypeAttribute = item->getCustomAttribute("LookTypeEx");
		const auto lookMount = item->getCustomAttribute("LookMount");
		const auto lookDirection = item->getCustomAttribute("LookDirection");

		if (lookType && lookType->getAttribute<uint16_t>() != 0) {
			addOutfitAndMountBytes(msg, item, lookType, "LookHead", "LookBody", "LookLegs", "LookFeet", true);
		} else if (lookTypeAttribute) {
			auto lookTypeEx = lookTypeAttribute->getAttribute<uint16_t>();
			// "Tantugly's Head" boss have to send other looktype to the podium
			if (lookTypeEx == 35105) {
				lookTypeEx = 39003;
			}
			msg.add<uint16_t>(0);
			msg.add<uint16_t>(lookTypeEx);
		} else {
			msg.add<uint16_t>(0);
			msg.add<uint16_t>(0);
		}

		if (lookMount) {
			addOutfitAndMountBytes(msg, item, lookMount, "LookMountHead", "LookMountBody", "LookMountLegs", "LookMountFeet");
		} else {
			msg.add<uint16_t>(0);
		}

		msg.addByte(lookDirection ? lookDirection->getAttribute<uint8_t>() : 2);
		msg.addByte(podiumVisible ? podiumVisible->getAttribute<uint8_t>() : 0x01);
	}

	if (item->getClassification() > 0) {
		msg.addByte(item->getTier());
	}

	// Timer
	if (it.expire || it.expireStop || it.clockExpire) {
		if (item->hasAttribute(ItemAttribute_t::DURATION)) {
			msg.add<uint32_t>(item->getDuration() / 1000);
			msg.addByte((item->getDuration() / 1000) == it.decayTime ? 0x01 : 0x00); // Brand-new
		} else {
			msg.add<uint32_t>(it.decayTime);
			msg.addByte(0x01); // Brand-new
		}
	}

	// Charge
	if (it.wearOut) {
		if (item->getSubType() == 0) {
			msg.add<uint32_t>(it.charges);
			msg.addByte(0x01); // Brand-new
		} else {
			msg.add<uint32_t>(static_cast<uint32_t>(item->getSubType()));
			msg.addByte(item->getSubType() == it.charges ? 0x01 : 0x00); // Brand-new
		}
	}

	if (it.isWrapKit && !oldProtocol) {
		uint16_t unWrapId = item->getCustomAttribute("unWrapId") ? static_cast<uint16_t>(item->getCustomAttribute("unWrapId")->getInteger()) : 0;
		if (unWrapId != 0) {
			msg.add<uint16_t>(unWrapId);
		} else {
			msg.add<uint16_t>(0x00);
		}
	}

	// OTCR Features
	if (isOTCR) {
		msg.addString(item->getShader()); // g_game.enableFeature(GameItemShader)
	}
}

void ProtocolGame::release() {
	// dispatcher thread
	clearReusableSessionHints();

	if (m_isLivestreamViewer) {
		g_livestream().removeViewer(getThis());
		player = nullptr;
	} else if (player && player->client == shared_from_this()) {
		g_livestream().stopBroadcast(player, true);
		player->client.reset();
		player = nullptr;
	}

	OutputMessagePool::getInstance().removeProtocolFromAutosend(shared_from_this());
	Protocol::release();
}

void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingSystem_t operatingSystem) {
	sendClientLoginPreamble(operatingSystem);

	g_logger().debug("Player logging in in version '{}' and oldProtocol '{}'", getVersion(), oldProtocol);

	// dispatcher thread
	std::shared_ptr<Player> foundPlayer = g_game().getPlayerByName(name);
	if (!foundPlayer) {
		player = std::make_shared<Player>(getThis());
		player->setName(name);

		player->setID();

		if (!IOLoginDataLoad::preLoadPlayer(player, name)) {
			disconnectClient("Your character could not be loaded.");
			return;
		}

		if (IOBan::isPlayerNamelocked(player->getGUID())) {
			disconnectClient("Your character has been namelocked.");
			return;
		}

		if (g_game().getGameState() == GAME_STATE_CLOSING && !player->hasFlag(PlayerFlags_t::CanAlwaysLogin)) {
			disconnectClient("The game is just going down.\nPlease try again later.");
			return;
		}

		if (g_game().getGameState() == GAME_STATE_CLOSED && !player->hasFlag(PlayerFlags_t::CanAlwaysLogin)) {
			auto maintainMessage = g_configManager().getString(MAINTAIN_MODE_MESSAGE);
			if (!maintainMessage.empty()) {
				disconnectClient(maintainMessage);
			} else {
				disconnectClient("Server is currently closed.\nPlease try again later.");
			}
			return;
		}

		if (g_configManager().getBoolean(ONLY_PREMIUM_ACCOUNT) && !player->isPremium() && (player->getGroup()->id < GROUP_TYPE_GAMEMASTER || player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER)) {
			disconnectClient("Your premium time for this account is out.\n\nTo play please buy additional premium time from our website");
			return;
		}

		auto onlineCount = g_game().getPlayersByAccount(player->getAccount()).size();
		auto maxOnline = g_configManager().getNumber(MAX_PLAYERS_PER_ACCOUNT);
		if (player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER && onlineCount >= maxOnline) {
			disconnectClient(fmt::format("You may only login with {} character{}\nof your account at the same time.", maxOnline, maxOnline > 1 ? "s" : ""));
			return;
		}

		if (!player->hasFlag(PlayerFlags_t::CannotBeBanned)) {
			BanInfo banInfo;
			if (IOBan::isAccountBanned(accountId, banInfo)) {
				if (banInfo.reason.empty()) {
					banInfo.reason = "(none)";
				}

				std::ostringstream ss;
				if (banInfo.expiresAt > 0) {
					ss << "Your account has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n"
					   << banInfo.reason;
				} else {
					ss << "Your account has been permanently banned by " << banInfo.bannedBy << ".\n\nReason specified:\n"
					   << banInfo.reason;
				}
				disconnectClient(ss.str());
				return;
			}
		}

		WaitingList &waitingList = WaitingList::getInstance();
		if (!waitingList.clientLogin(player)) {
			auto currentSlot = static_cast<uint32_t>(waitingList.getClientSlot(player));
			auto retryTime = static_cast<uint32_t>(WaitingList::getTime(currentSlot));
			std::ostringstream ss;

			ss << "Too many players online.\nYou are at place "
			   << currentSlot << " on the waiting list.";

			auto output = OutputMessagePool::getOutputMessage();
			output->addByte(0x16);
			output->addString(ss.str());
			output->addByte(retryTime);
			send(output);
			disconnect();
			return;
		}

		if (!IOLoginData::loadPlayerById(player, player->getGUID(), false)) {
			disconnectClient("Your character could not be loaded, please contact an adminstrator.");
			return;
		}

		player->setOperatingSystem(operatingSystem);

		const auto tile = g_game().map.getOrCreateTile(player->getLoginPosition());
		// moving from a pz tile to a non-pz tile
		if (maxOnline > 1 && player->getAccountType() < ACCOUNT_TYPE_GAMEMASTER && !tile->hasFlag(TILESTATE_PROTECTIONZONE)) {
			auto maxOutsizePZ = g_configManager().getNumber(MAX_PLAYERS_OUTSIDE_PZ_PER_ACCOUNT);
			auto accountPlayers = g_game().getPlayersByAccount(player->getAccount());
			int countOutsizePZ = 0;
			for (const auto &accountPlayer : accountPlayers) {
				if (accountPlayer != player && accountPlayer->getTile() && !accountPlayer->getTile()->hasFlag(TILESTATE_PROTECTIONZONE)) {
					++countOutsizePZ;
				}
			}
			if (countOutsizePZ >= maxOutsizePZ) {
				disconnectClient(fmt::format("You can only have {} character{} from your account outside of a protection zone.", maxOutsizePZ == 1 ? "one" : std::to_string(maxOutsizePZ), maxOutsizePZ > 1 ? "s" : ""));
				return;
			}
		}

		const bool suppressPreLoginPacketsBeforePlacement = isCipsoft860Profile(protocolProfile);
		const bool previousSuppressPreLoginPackets = suppressPreLoginPackets;
		if (suppressPreLoginPacketsBeforePlacement) {
			suppressPreLoginPackets = true;
		}

		const bool placedCreature = g_game().placeCreature(player, player->getLoginPosition()) || g_game().placeCreature(player, player->getTemplePosition(), false, true);
		suppressPreLoginPackets = previousSuppressPreLoginPackets;
		if (!placedCreature) {
			disconnectClient("Temple position is wrong. Please, contact the administrator.");
			g_logger().warn("Player {} temple position is wrong", player->getName());
			return;
		}

		player->lastIP = player->getIP();
		player->lastLoginSaved = std::max<time_t>(time(nullptr), player->lastLoginSaved + 1);
		player->loginProtectionTime = OTSYS_TIME() + g_configManager().getNumber(LOGIN_PROTECTION_TIME);
		acceptPackets = true;
	} else {
		if (eventConnect != 0 || !g_configManager().getBoolean(REPLACE_KICK_ON_LOGIN)) {
			// Already trying to connect
			disconnectClient("You are already logged in.");
			return;
		}

		if (foundPlayer->client) {
			foundPlayer->disconnect();
			foundPlayer->isConnecting = true;

			eventConnect = scheduleProtocolTask(
				1000,
				[self = getThis(), playerName = foundPlayer->getName(), operatingSystem] { self->connect(playerName, operatingSystem); }, "ProtocolGame::connect"
			);
			if (eventConnect == 0) {
				foundPlayer->isConnecting = false;
				return;
			}
		} else {
			connect(foundPlayer->getName(), operatingSystem);
		}
	}
	OutputMessagePool::getInstance().addProtocolToAutosend(shared_from_this());
	sendBosstiaryCooldownTimer();
}

void ProtocolGame::connect(const std::string &playerName, OperatingSystem_t operatingSystem) {
	eventConnect = 0;

	std::shared_ptr<Player> foundPlayer = g_game().getPlayerByName(playerName);
	if (!foundPlayer) {
		disconnectClient("You are already logged in.");
		return;
	}

	if (isConnectionExpired()) {
		// ProtocolGame::release() has been called at this point and the Connection object
		// no longer exists, so we return to prevent leakage of the Player.
		return;
	}

	player = foundPlayer;

	g_chat().removeUserFromAllChannels(player);
	player->clearModalWindows();
	player->setOperatingSystem(operatingSystem);
	player->isConnecting = false;

	player->client = getThis();
	const bool suppressPreLoginPacketsBeforeConnectBootstrap = isCipsoft860Profile(protocolProfile);
	const bool previousSuppressPreLoginPackets = suppressPreLoginPackets;
	if (suppressPreLoginPacketsBeforeConnectBootstrap) {
		suppressPreLoginPackets = true;
	}
	player->openPlayerContainers();
	sendAddCreature(player, player->getPosition(), 0, true);
	suppressPreLoginPackets = previousSuppressPreLoginPackets;
	player->lastIP = player->getIP();
	player->lastLoginSaved = std::max<time_t>(time(nullptr), player->lastLoginSaved + 1);
	if (player->isProtected()) {
		player->setProtection(false);
		player->resetLoginProtection();
	} else {
		player->setLoginProtection(g_configManager().getNumber(LOGIN_PROTECTION_TIME));
	}
	player->resetIdleTime();
	acceptPackets = true;
}

void ProtocolGame::logout(bool displayEffect, bool forced) {
	if (!player) {
		return;
	}

	if (m_isLivestreamViewer) {
		sendSessionEndInformation(SESSION_END_LOGOUT);
		return;
	}

	bool removePlayer = !player->isRemoved() && !forced;
	auto tile = player->getTile();
	if (removePlayer && !player->isAccessPlayer()) {
		if (tile && tile->hasFlag(TILESTATE_NOLOGOUT)) {
			player->sendCancelMessage(RETURNVALUE_YOUCANNOTLOGOUTHERE);
			return;
		}

		if (tile && !tile->hasFlag(TILESTATE_PROTECTIONZONE) && player->hasCondition(CONDITION_INFIGHT)) {
			player->sendCancelMessage(RETURNVALUE_YOUMAYNOTLOGOUTDURINGAFIGHT);
			return;
		}
	}

	if (removePlayer && !g_creatureEvents().playerLogout(player)) {
		return;
	}

	displayEffect = displayEffect && !player->isRemoved() && player->getHealth() > 0 && !player->isInGhostMode();
	if (displayEffect) {
		g_game().addMagicEffect(player->getPosition(), CONST_ME_POFF);
	}

	acceptPackets = false;
	clearReusableSessionHints();
	sendSessionEndInformation(forced ? SESSION_END_FORCECLOSE : SESSION_END_LOGOUT);

	g_game().removeCreature(player, true);
}

void ProtocolGame::onRecvFirstMessage(NetworkMessage &msg) {
	if (g_game().getGameState() == GAME_STATE_SHUTDOWN) {
		disconnect();
		return;
	}

	auto operatingSystem = static_cast<OperatingSystem_t>(msg.get<uint16_t>());
	version = msg.get<uint16_t>(); // Protocol version
	g_logger().trace("Protocol version: {}", version);

	const auto* gameLoginLayout = ProtocolProfileRegistry::resolveGameLoginLayout(version);
	if (!gameLoginLayout) {
		disconnectClient(ProtocolProfileRegistry::getUnsupportedClientProtocolMessage(g_configManager().getBoolean(OLD_PROTOCOL)));
		return;
	}

	protocolProfile = ProtocolProfileRegistry::getProfile(gameLoginLayout->profileId);
	if (!protocolProfile || !ProtocolProfileRegistry::isProfileAllowed(protocolProfile->id)) {
		disconnectClient("This client protocol is not supported yet.");
		return;
	}

	if (!initialConnectionBehavior.hasSameWireBehavior(protocolProfile->initialBehavior)) {
		disconnect();
		return;
	}

	const bool profileOldProtocol = protocolProfile->hasFeature(ProtocolFeature::OldProtocolCompat);
	if (profileOldProtocol && !g_configManager().getBoolean(OLD_PROTOCOL)) {
		disconnectClient(ProtocolProfileRegistry::getUnsupportedClientProtocolMessage(false));
		return;
	}

	oldProtocol = profileOldProtocol;

	if (oldProtocol) {
		setChecksumMethod(CHECKSUM_METHOD_ADLER32);
	} else if (operatingSystem <= CLIENTOS_OTCLIENT_MAC) {
		setChecksumMethod(CHECKSUM_METHOD_SEQUENCE);
	}

	if (gameLoginLayout->hasClientVersionU32) {
		clientVersion = static_cast<int32_t>(msg.get<uint32_t>());
	} else {
		clientVersion = version;
	}

	if (gameLoginLayout->hasClientVersionString) {
		clientVersionString = msg.getString(); // Client version (String)
		g_logger().trace("Client version: {}", clientVersionString);
	}

	if (gameLoginLayout->hasAssetHashString) {
		auto assetHashIdentifier = msg.getString(); // Assets hash identifier
		g_logger().trace("Client asset hash identifier: {}", assetHashIdentifier);
	}

	if (gameLoginLayout->hasContentRevisionU16) {
		auto datRevision = msg.get<uint16_t>(); // Dat revision
		g_logger().trace("Dat revision: {}", datRevision);
	}

	if (gameLoginLayout->hasPreviewState) {
		auto gamePreviewState = msg.getByte(); // U8 game preview state
		g_logger().trace("Game preview state: {}", gamePreviewState);
	}

	if (!Protocol::RSA_decrypt(msg)) {
		g_logger().warn("[ProtocolGame::onRecvFirstMessage] - RSA Decrypt Failed");
		disconnect();
		return;
	}

	std::array<uint32_t, 4> key = {
		msg.get<uint32_t>(),
		msg.get<uint32_t>(),
		msg.get<uint32_t>(),
		msg.get<uint32_t>()
	};

	enableXTEAEncryption();
	setXTEAKey(key.data());

	auto isGameMaster = static_cast<bool>(msg.getByte()); // gamemaster flag
	g_logger().trace("Is Game Master: {}", isGameMaster);

	std::string authType = g_configManager().getString(AUTH_TYPE);
	std::ostringstream ss;
	std::string sessionKey;
	std::string accountDescriptor;
	std::string password;
	std::string characterName;

	if (gameLoginLayout->authenticationLayout == GameLoginAuthenticationLayout::AccountPassword) {
		accountDescriptor = msg.getString();
		if (accountDescriptor.empty()) {
			ss << "You must enter your username.";
			disconnectClient(ss.str());
			return;
		}

		characterName = msg.getString();
		password = msg.getString();
		sessionKey = accountDescriptor + "\n" + password;
	} else {
		sessionKey = msg.getString();
		accountDescriptor = sessionKey;
	}

	if (gameLoginLayout->authenticationLayout == GameLoginAuthenticationLayout::SessionKey && authType != "session") {
		size_t pos = sessionKey.find('\n');
		if (pos == std::string::npos) {
			ss << "You must enter your " << (oldProtocol ? "username" : "email") << ".";
			disconnectClient(ss.str());
			return;
		}
		accountDescriptor = sessionKey.substr(0, pos);
		if (accountDescriptor.empty()) {
			ss.str(std::string());
			ss << "You must enter your " << (oldProtocol ? "username" : "email") << ".";
			disconnectClient(ss.str());
			return;
		}
		password = sessionKey.substr(pos + 1);
	}

	if (!oldProtocol && operatingSystem == CLIENTOS_NEW_LINUX) {
		// TODO: check what new info for linux is send
		msg.getString();
		msg.getString();
	}

	if (gameLoginLayout->authenticationLayout == GameLoginAuthenticationLayout::SessionKey) {
		characterName = msg.getString();
	}
	if (sessionHintLease) {
		const auto matchedProfileId = ProtocolSessionHintStore::getInstance().consumeAndResolveProfile(*sessionHintLease, sessionKey, characterName, version);
		if (matchedProfileId) {
			const auto* matchedProfile = ProtocolProfileRegistry::getProfile(*matchedProfileId);
			const auto* matchedLayout = ProtocolProfileRegistry::resolveGameLoginLayout(*matchedProfileId);
			if (!matchedProfile || !matchedLayout || !initialConnectionBehavior.hasSameWireBehavior(matchedProfile->initialBehavior)) {
				disconnect();
				return;
			}

			const bool matchedOldProtocol = matchedProfile->hasFeature(ProtocolFeature::OldProtocolCompat);
			if (!ProtocolProfileRegistry::isProfileAllowed(matchedProfile->id) || (matchedOldProtocol && !g_configManager().getBoolean(OLD_PROTOCOL))) {
				disconnectClient(ProtocolProfileRegistry::getUnsupportedClientProtocolMessage(g_configManager().getBoolean(OLD_PROTOCOL)));
				return;
			}

			protocolProfile = matchedProfile;
			gameLoginLayout = matchedLayout;
			oldProtocol = matchedOldProtocol;
		} else if (!sessionHintLease->behavior.hasSameWireBehavior(ProtocolProfileRegistry::defaultModernInitialBehavior())) {
			disconnect();
			return;
		}
	}
	sessionHintLease.reset();

	const auto &onlinePlayer = g_game().getPlayerByName(characterName);
	const auto &foundPlayer = !onlinePlayer ? g_game().getDeadPlayer(characterName) : onlinePlayer;
	if (foundPlayer && foundPlayer->client && accountDescriptor != "@livestream") {
		if (foundPlayer->isDead()) {
			disconnectClient("You are already logged in.");
			return;
		}

		const auto* existingProfile = foundPlayer->client->getProtocolProfile();
		const bool sameProtocolProfile = existingProfile && protocolProfile && existingProfile->id == protocolProfile->id;
		const bool sameLegacyContract = !existingProfile && !protocolProfile && foundPlayer->getProtocolVersion() == getVersion() && foundPlayer->isOldProtocol() == oldProtocol;
		if (!sameProtocolProfile && !sameLegacyContract) {
			const auto formatClientVersion = [](uint32_t clientVersion) {
				return fmt::format("{}.{:02d}", clientVersion / 100, clientVersion % 100);
			};
			disconnectClient(fmt::format("This character is already online using client version {}. Please log out from that client before switching to version {}.", formatClientVersion(foundPlayer->getProtocolVersion()), formatClientVersion(getVersion())));
			return;
		}

		if (!foundPlayer->client->isSessionEnding()) {
			foundPlayer->client->disconnectClient("You are already connected through another client. Please use only one client at a time!");
		}
	}

	if (gameLoginLayout->hasChallengeResponse) {
		auto timeStamp = msg.get<uint32_t>();
		uint8_t randNumber = msg.getByte();
		if (challengeTimestamp != timeStamp || challengeRandom != randNumber) {
			disconnect();
			return;
		}
	}

	// OTCv8 version detection
	if (gameLoginLayout->hasOtcV8Probe) {
		auto otcV8StringLength = msg.get<uint16_t>();
		if (otcV8StringLength == 5 && msg.getString(5) == "OTCv8") {
			otclientV8 = msg.get<uint16_t>(); // 253, 260, 261, ...
		}
	}

	if (!oldProtocol && clientVersion != CLIENT_VERSION) {
		disconnectClient(ProtocolProfileRegistry::getUnsupportedClientProtocolMessage(g_configManager().getBoolean(OLD_PROTOCOL)));
		return;
	}

	if (g_game().getGameState() == GAME_STATE_STARTUP) {
		disconnectClient("Gameworld is starting up. Please wait.");
		return;
	}

	if (g_game().getGameState() == GAME_STATE_MAINTAIN) {
		disconnectClient("Gameworld is under maintenance. Please re-connect in a while.");
		return;
	}

	BanInfo banInfo;
	if (IOBan::isIpBanned(getIP(), banInfo)) {
		if (banInfo.reason.empty()) {
			banInfo.reason = "(none)";
		}

		ss.str(std::string());
		ss << "Your IP has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n"
		   << banInfo.reason;
		disconnectClient(ss.str());
		return;
	}

	if (accountDescriptor == "@livestream") {
		// dispatchProtocolTask force-closes the connection when admission fails.
		(void)dispatchProtocolTask([self = getThis(), characterName, password, operatingSystem] {
			self->castViewerLogin(characterName, password, operatingSystem);
		},
		                           "ProtocolGame::castViewerLogin");
		return;
	}

	uint32_t accountId;
	if (!IOLoginData::gameWorldAuthentication(accountDescriptor, password, characterName, accountId, oldProtocol, getIP())) {
		ss.str(std::string());
		if (authType == "session") {
			ss << "Your session has expired. Please log in again.";
		} else { // authType == "password"
			ss << "Your " << (oldProtocol ? "username" : "email") << " or password is not correct.";
		}

		auto output = OutputMessagePool::getOutputMessage();
		output->addByte(0x14);
		output->addString(ss.str());
		send(output);
		[[maybe_unused]] auto eventId = scheduleProtocolTask(
			1000, [self = getThis()] { self->disconnect(); }, "ProtocolGame::disconnect"
		);
		return;
	}

	dispatchProtocolTask([self = getThis(), characterName, accountId, operatingSystem] { self->login(characterName, accountId, operatingSystem); }, __FUNCTION__);
}

void ProtocolGame::sendLoginChallenge() {
	auto output = OutputMessagePool::getOutputMessage();
	static std::random_device rd;
	static std::ranlux24 generator(rd());
	static std::uniform_int_distribution<uint16_t> randNumber(0x00, 0xFF);

	if (initialConnectionBehavior.challenge.layout == ChallengeLayout::Cipsoft860LoginChallenge
	    || initialConnectionBehavior.challenge.layout == ChallengeLayout::Tibia1100LoginChallenge) {
		output->addByte(0x1F);
		challengeTimestamp = static_cast<uint32_t>(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
		output->add<uint32_t>(challengeTimestamp);

		challengeRandom = randNumber(generator);
		output->addByte(challengeRandom);
		output->writeLegacyInnerLength();
		output->writeChecksum(adlerChecksum(output->getOutputBuffer(), output->getLength()));

		send(output);
		return;
	}

	if (initialConnectionBehavior.challenge.layout != ChallengeLayout::CurrentLoginChallenge) {
		disconnect();
		return;
	}

	// Skip checksum
	output->skipBytes(sizeof(uint32_t));

	// Packet length & type
	output->addByte(0x01);
	output->addByte(0x1F);
	// Add timestamp & random number
	challengeTimestamp = static_cast<uint32_t>(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
	output->add<uint32_t>(challengeTimestamp);

	challengeRandom = randNumber(generator);
	output->addByte(challengeRandom);
	output->addByte(0x71);

	// Go back and write checksum
	output->skipBytes(-12);
	// To support 11.10-, not have problems with 11.11+
	output->add<uint32_t>(adlerChecksum(output->getOutputBuffer() + sizeof(uint32_t), 8));

	send(output);
}

void ProtocolGame::disconnectClient(const std::string &message) const {
	auto output = OutputMessagePool::getOutputMessage();
	output->addByte(0x14);
	output->addString(message);
	send(output);
	disconnect();
}

void ProtocolGame::writeToOutputBuffer(NetworkMessage &msg) {
	if (msg.getLength() > 0) {
		const auto opcode = msg.getBuffer()[NetworkMessage::INITIAL_BUFFER_POSITION];
		if (shouldBlockOutgoingOpcodeForProfile(protocolProfile, opcode)) {
			g_logger().info(
				"[{}] Dropped unsupported packet 0x{:02X} for profile '{}'",
				__FUNCTION__,
				opcode,
				protocolProfile ? protocolProfile->name : "unknown"
			);
			if (player) {
				player->sendTextMessage(MESSAGE_FAILURE, getUnsupportedOpcodeMessage(protocolProfile, opcode));
			}
			return;
		}
	}

	auto writeMessage = [self = getThis(), msg]() mutable {
		self->getOutputBuffer(msg.getLength())->append(msg);
		if (self->m_isLivestreamBroadcaster && self->player && !self->m_isLivestreamViewer) {
			g_livestream().broadcastPacket(self->player, self, msg);
		}
	};

	if (g_dispatcher().context().isBarrierParallel()) {
		dispatchProtocolTask(std::move(writeMessage), __FUNCTION__);
	} else {
		writeMessage();
	}
}

bool ProtocolGame::shouldSuppressPreLoginPacket() const {
	return !loggedIn && isCipsoft860Profile(protocolProfile);
}

void ProtocolGame::parsePacket(NetworkMessage &msg) {
	if (!acceptPackets || g_game().getGameState() == GAME_STATE_SHUTDOWN || msg.getLength() <= 0) {
		return;
	}

	uint8_t recvbyte = msg.getByte();

	// Silence ping/pong: 0x1D = pingBack, 0x1E = ping [TRACKS CLIENT BYTES]
	if (recvbyte != 0x1D && recvbyte != 0x1E) {
		g_logger().debug("BYTE RECEIVED: 0x{:02X}", recvbyte);
	}

	if (!player || player->isRemoved()) {
		if (recvbyte == 0x0F) {
			disconnect();
		}
		return;
	}

	if (m_isLivestreamViewer) {
		parsePacketFromDispatcher(msg, recvbyte);
		return;
	}

	if (player->isDead() || player->getHealth() <= 0) {
		// Check player activity on death screen
		if (m_playerDeathTime == 0) {
			g_game().playerCheckActivity(player->getName(), 1000);
			m_playerDeathTime++;
		}

		parsePacketDead(recvbyte);
		return;
	}

	// Recvbyte modules own the byte once they run; the dispatcher must not parse it again.
	if (player
	    && recvbyte != 0xD3
	    && shouldDispatchRecvbyteModuleForProfile(protocolProfile, recvbyte)
	    && g_modules().executeOnRecvbyte(player, msg, recvbyte)) {
		return;
	}

	parsePacketFromDispatcher(msg, recvbyte);
}

void ProtocolGame::parsePacketDead(uint8_t recvbyte) {
	if (recvbyte == 0x14) {
		// Remove player from game if click "ok" using otc
		disconnect();
		return;
	}

	if (recvbyte == 0x0F) {
		if (!player) {
			return;
		}

		const auto eventId = scheduleProtocolTask(
			100, [self = getThis()] { self->sendPing(); }, "ProtocolGame::sendPing"
		);
		if (eventId == 0) {
			return;
		}

		if (!player->spawn()) {
			disconnect();
			g_game().removeCreature(player, true);
			return;
		}

		sendAddCreature(player, player->getPosition(), 0, false);
		addBless();
		resetPlayerDeathTime();
		return;
	}

	if (recvbyte == 0x1D) {
		// keep the connection alive
		[[maybe_unused]] auto eventId = scheduleProtocolTask(
			100, [self = getThis()] { self->sendPingBack(); }, "ProtocolGame::sendPingBack"
		);
		return;
	}
}

void ProtocolGame::addBless() {
	if (!player) {
		return;
	}
	player->checkAndShowBlessingMessage();
}

void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvbyte) {
	if (!acceptPackets || g_game().getGameState() == GAME_STATE_SHUTDOWN) {
		return;
	}

	if (m_isLivestreamViewer) {
		switch (recvbyte) {
			case 0x14:
				logout(true, false);
				break;
			case 0x1D:
				break;
			case 0x1E:
				sendPingBack();
				break;
			case 0x96:
				parseSay(msg);
				break;
			case 0xCA:
				if (getUnreadBytes(msg) == UPDATE_CONTAINER_PAYLOAD_SIZE) {
					resendLivestreamViewerContainer(msg);
				}
				break;
			case 0xA1:
				sendCancelTarget();
				break;
			case 0xFA:
			case 0xFB:
			case 0xFC:
			case 0xFD:
			case 0xFE:
				break;
			default:
				sendCancelWalk();
				break;
		}
		return;
	}

	if (!player || player->isRemoved() || player->getHealth() <= 0) {
		return;
	}

	switch (recvbyte) {
		case 0x14:
			logout(true, false);
			break;
		case 0x1D:
			g_game().playerReceivePingBack(player->getID());
			break;
		case 0x1E:
			g_game().playerReceivePing(player->getID());
			break;
		case 0x28:
			parseStashWithdraw(msg);
			break;
		case 0x29:
			parseRetrieveDepotSearch(msg);
			break;
		case 0x2A:
			parseCyclopediaMonsterTracker(msg);
			break;
		case 0x2B:
			parsePartyAnalyzerAction(msg);
			break;
		case 0x2C:
			parseLeaderFinderWindow(msg);
			break;
		case 0x2D:
			parseMemberFinderWindow(msg);
			break;
		case 0x2E:
			parseSetClientOptions(msg);
			break;
		case 0x32:
			parseExtendedOpcode(msg);
			break; // otclient extended opcode
		case 0x38:
			parsePlayerTyping(msg); // player are typing or not
			break;
		case 0x60:
			parseInventoryImbuements(msg);
			break;
		case 0x61:
			parseOpenWheel(msg);
			break;
		case 0x62:
			parseSaveWheel(msg);
			break;
		case 0x63:
			parseClientCheck(msg);
			break;
		case 0x64:
			parseAutoWalk(msg);
			break;
		case 0x65:
			g_game().playerMove(player->getID(), DIRECTION_NORTH);
			break;
		case 0x66:
			g_game().playerMove(player->getID(), DIRECTION_EAST);
			break;
		case 0x67:
			g_game().playerMove(player->getID(), DIRECTION_SOUTH);
			break;
		case 0x68:
			g_game().playerMove(player->getID(), DIRECTION_WEST);
			break;
		case 0x69:
			g_game().playerStopAutoWalk(player->getID());
			break;
		case 0x6A:
			g_game().playerMove(player->getID(), DIRECTION_NORTHEAST);
			break;
		case 0x6B:
			g_game().playerMove(player->getID(), DIRECTION_SOUTHEAST);
			break;
		case 0x6C:
			g_game().playerMove(player->getID(), DIRECTION_SOUTHWEST);
			break;
		case 0x6D:
			g_game().playerMove(player->getID(), DIRECTION_NORTHWEST);
			break;
		case 0x6E:
			parseSetVocation(msg);
			break;
		case 0x6F:
			g_game().playerTurn(player->getID(), DIRECTION_NORTH);
			break;
		case 0x70:
			g_game().playerTurn(player->getID(), DIRECTION_EAST);
			break;
		case 0x71:
			g_game().playerTurn(player->getID(), DIRECTION_SOUTH);
			break;
		case 0x72:
			g_game().playerTurn(player->getID(), DIRECTION_WEST);
			break;
		case 0x73:
			parseTeleport(msg);
			break;
		case 0x74:
			parseStartOfflineTraining(msg);
			break;
		case 0x75:
			parseContainerAction(msg);
			break;
		case 0x76:
			parseCharacterTradeConfigurationAction(msg);
			break;
		case 0x77:
			parseHotkeyEquip(msg);
			break;
		case 0x78:
			parseThrow(msg);
			break;
		case 0x79:
			parseLookInShop(msg);
			break;
		case 0x7A:
			parsePlayerBuyOnShop(msg);
			break;
		case 0x7B:
			parsePlayerSellOnShop(msg);
			break;
		case 0x7C:
			g_game().playerCloseShop(player->getID());
			break;
		case 0x7D:
			parseRequestTrade(msg);
			break;
		case 0x7E:
			parseLookInTrade(msg);
			break;
		case 0x7F:
			g_game().playerAcceptTrade(player->getID());
			break;
		case 0x80:
			g_game().playerCloseTrade(player->getID());
			break;
		case 0x81:
			parseFriendSystemAction(msg);
			break;
		case 0x82:
			parseUseItem(msg);
			break;
		case 0x83:
			parseUseItemEx(msg);
			break;
		case 0x84:
			parseUseWithCreature(msg);
			break;
		case 0x85:
			parseRotateItem(msg);
			break;
		case 0x86:
			parseConfigureShowOffSocket(msg);
			break;
		case 0x87:
			parseCloseContainer(msg);
			break;
		case 0x88:
			parseUpArrowContainer(msg);
			break;
		case 0x89:
			parseTextWindow(msg);
			break;
		case 0x8A:
			parseHouseWindow(msg);
			break;
		case 0x8B:
			parseWrapableItem(msg);
			break;
		case 0x8C:
			parseLookAt(msg);
			break;
		case 0x8D:
			parseLookInBattleList(msg);
			break;
		case 0x8E:
			parseJoinAggression(msg);
			break;
		case 0x8F:
			parseQuickLoot(msg);
			break;
		case 0x90:
			parseLootContainer(msg);
			break;
		case 0x91:
			parseQuickLootBlackWhitelist(msg);
			break;
		case 0x92:
			parseOpenDepotSearch();
			break;
		case 0x93:
			parseCloseDepotSearch();
			break;
		case 0x94:
			parseDepotSearchItemRequest(msg);
			break;
		case 0x95:
			parseOpenParentContainer(msg);
			break;
		case 0x96:
			parseSay(msg);
			break;
		case 0x97:
			g_game().playerRequestChannels(player->getID());
			break;
		case 0x98:
			parseOpenChannel(msg);
			break;
		case 0x99:
			parseCloseChannel(msg);
			break;
		case 0x9A:
			parseOpenPrivateChannel(msg);
			break;
		case 0x9C:
			parseEditGuildMessage(msg);
			break;
		case 0x9D:
			parseGetTextForReport(msg);
			break;
		case 0x9E:
			g_game().playerCloseNpcChannel(player->getID());
			break;
		case 0x9F:
			parseSetMonsterPodium(msg);
			break;
		case 0xA0:
			parseFightModes(msg);
			break;
		case 0xA1:
			parseAttack(msg);
			break;
		case 0xA2:
			parseFollow(msg);
			break;
		case 0xA3:
			parseInviteToParty(msg);
			break;
		case 0xA4:
			parseJoinParty(msg);
			break;
		case 0xA5:
			parseRevokePartyInvite(msg);
			break;
		case 0xA6:
			parsePassPartyLeadership(msg);
			break;
		case 0xA7:
			g_game().playerLeaveParty(player->getID());
			break;
		case 0xA8:
			parseEnableSharedPartyExperience(msg);
			break;
		case 0xAA:
			g_game().playerCreatePrivateChannel(player->getID());
			break;
		case 0xAB:
			parseChannelInvite(msg);
			break;
		case 0xAC:
			parseChannelExclude(msg);
			break;
		case 0xAD:
			parseCyclopediaHouseAuction(msg);
			break;
		case 0xAE:
			parseSendBosstiary();
			break;
		case 0xAF:
			parseSendBosstiarySlots();
			break;
		case 0xB0:
			parseBosstiarySlot(msg);
			break;
		case 0xB1:
			parseHighscores(msg);
			break;
		case 0xB2:
			parseImbuementAction(msg);
			break;
		case 0xB3:
			parseWeaponProficiency(msg);
			break;
		case 0xBA:
			parseTaskHuntingAction(msg);
			break;
		case 0xBE:
			g_game().playerCancelAttackAndFollow(player->getID());
			break;
		case 0xBF:
			parseForgeEnter(msg);
			break;
		case 0xC0:
			parseForgeBrowseHistory(msg);
			break;
		case 0xC1:
			parseClientDetails(msg);
			break;
		case 0xC2:
			parseBossDifficultySelection(msg);
			break;
		case 0xC8:
			parseAimAtTarget(msg);
			break;
		case 0xC9:
			parseGetTransactionDetails(msg);
			break;
		case 0xCA:
			if (!oldProtocol && g_game().getWorldType() == WORLD_TYPE_NO_PVP) {
				parseExivaRestrictions(msg);
			}
			break;
		case 0xCB:
			parseBrowseField(msg);
			break;
		case 0xCC:
			parseSeekInContainer(msg);
			break;
		case 0xCD:
			parseInspectionObject(msg);
			break;
		case 0xCE:
			parseInspectPlayer(msg);
			break;
		case 0xCF:
			sendBlessingWindow();
			break;
		case 0xD2:
			g_game().playerRequestOutfit(player->getID());
			break;
		case 0xD3:
			parseSetOutfit(msg);
			break;
		case 0xD4:
			parseToggleMount(msg);
			break;
		case 0xD5:
			parseApplyImbuement(msg);
			break;
		case 0xD6:
			parseClearImbuement(msg);
			break;
		case 0xD7:
			parseCloseImbuementWindow(msg);
			break;
		case 0xDB:
			parseCyclopediaMapAction(msg);
			break;
		case 0xDC:
			parseAddVip(msg);
			break;
		case 0xDD:
			parseRemoveVip(msg);
			break;
		case 0xDE:
			parseEditVip(msg);
			break;
		case 0xDF:
			parseVipGroupActions(msg);
			break;
		case 0xE1:
			parseBestiarySendRaces();
			break;
		case 0xE2:
			parseBestiarySendCreatures(msg);
			break;
		case 0xE3:
			parseBestiarysendMonsterData(msg);
			break;
		case 0xE4:
			parseSendBuyCharmRune(msg);
			break;
		case 0xE5:
			parseCyclopediaCharacterInfo(msg);
			break;
		case 0xE6:
			parseBugReport(msg);
			break;
		case 0xE7:
			parseWheelGemAction(msg);
			break;
		case 0xE8:
			parseOfferDescription(msg);
			break;
		case 0xEB:
			parsePreyAction(msg);
			break;
		case 0xEC:
			parseSetHirelingName(msg);
			break;
		case 0xED:
			parseSendResourceBalance(msg);
			break;
		case 0xEE:
			parseGreet(msg);
			break;
		// Premium coins transfer
		// case 0xEF: parseCoinTransfer(msg); break;
		case 0xF0:
			g_game().playerShowQuestLog(player->getID());
			break;
		case 0xF1:
			parseQuestLine(msg);
			break;
		// case 0xF2: parseRuleViolationReport(msg); break;
		case 0xF3:
			parseGetObjectInfo(msg);
			break;
		case 0xF4:
			parseMarketLeave();
			break;
		case 0xF5:
			parseMarketBrowse(msg);
			break;
		case 0xF6:
			parseMarketCreateOffer(msg);
			break;
		case 0xF7:
			parseMarketCancelOffer(msg);
			break;
		case 0xF8:
			parseMarketAcceptOffer(msg);
			break;
		case 0xF9:
			parseModalWindowAnswer(msg);
			break;
		case 0xFF:
			parseRewardChestCollect(msg);
			break;
			// case 0xFA: parseStoreOpen(msg); break;
			// case 0xFB: parseStoreRequestOffers(msg); break;
			// case 0xFC: parseStoreBuyOffer(msg) break;
			// case 0xFD: parseStoreOpenTransactionHistory(msg); break;
			// case 0xFE: parseStoreRequestTransactionHistory(msg); break;

			// case 0xDF, 0xE0, 0xE1, 0xFB, 0xFC, 0xFD, 0xFE Premium Shop.

		default:
			std::string hexString = fmt::format("0x{:02x}", recvbyte);
			g_logger().debug("Player '{}' sent unknown packet header: hex[{}], decimal[{}]", player->getName(), asUpperCaseString(hexString), recvbyte);
			break;
	}
}

void ProtocolGame::parseHotkeyEquip(NetworkMessage &msg) {
	if (!player) {
		return;
	}

	auto itemId = msg.get<uint16_t>();
	uint8_t tier = 0;
	bool hasTier = Item::items[itemId].upgradeClassification > 0;
	if (hasTier) {
		tier = msg.get<uint8_t>();
	}
	g_game().playerEquipItem(player->getID(), itemId, hasTier, tier);
}

void ProtocolGame::GetCipsoft860TileDescription(const std::shared_ptr<Tile> &tile, NetworkMessage &msg) {
	int32_t count = 0;
	const auto ground = tile->getGround();
	if (ground) {
		AddItem(msg, ground);
		count = 1;
	}

	static constexpr uint8_t maxStackCount = 10;
	const bool isPlayerTile = tile->getPosition() == player->getPosition();
	const uint8_t itemStackLimit = isPlayerTile ? maxStackCount - 1 : maxStackCount;
	const TileItemVector* items = tile->getItemList();
	if (items) {
		for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) {
			AddItem(msg, *it);
			if (++count == itemStackLimit) {
				break;
			}
		}
	}

	const CreatureVector* creatures = tile->getCreatures();
	bool playerAdded = false;
	if (creatures && count < maxStackCount) {
		for (auto creature : std::ranges::reverse_view(*creatures)) {
			if (!creature || creature->isRemoved() || !creature->isAlive()) {
				continue;
			}
			if (!player->canSeeCreature(creature)) {
				continue;
			}

			if (isPlayerTile && !playerAdded && count == maxStackCount - 1) {
				creature = player;
			}

			if (creature->getID() == player->getID()) {
				playerAdded = true;
			}

			bool known;
			uint32_t removedKnown;
			checkCreatureAsKnown(creature->getID(), known, removedKnown);
			AddCreature(msg, creature, known, removedKnown);
			if (++count == maxStackCount) {
				break;
			}
		}
	}

	if (creatures && !playerAdded && isPlayerTile && count < maxStackCount) {
		bool known;
		uint32_t removedKnown;
		checkCreatureAsKnown(player->getID(), known, removedKnown);
		AddCreature(msg, player, known, removedKnown);
		++count;
	}

	if (!items || count >= maxStackCount) {
		return;
	}

	for (auto it = ItemVector::const_reverse_iterator(items->getEndDownItem()), end = ItemVector::const_reverse_iterator(items->getBeginDownItem()); it != end; ++it) {
		AddItem(msg, *it);
		if (++count == maxStackCount) {
			return;
		}
	}
}

void ProtocolGame::GetTileDescription(const std::shared_ptr<Tile> &tile, NetworkMessage &msg) {
	if (oldProtocol && version >= 910) {
		msg.add<uint16_t>(0x00); // Env effects
	}

	if (isCipsoft860Profile(protocolProfile)) {
		GetCipsoft860TileDescription(tile, msg);
		return;
	}

	int32_t count;
	std::shared_ptr<Item> ground = tile->getGround();
	if (ground) {
		AddItem(msg, ground);
		count = 1;
	} else {
		count = 0;
	}

	const TileItemVector* items = tile->getItemList();
	if (items) {
		for (auto it = items->getBeginTopItem(), end = items->getEndTopItem(); it != end; ++it) {
			AddItem(msg, *it);

			count++;
			if (count == 9 && tile->getPosition() == player->getPosition()) {
				break;
			} else if (count == 10) {
				return;
			}
		}
	}

	const CreatureVector* creatures = tile->getCreatures();
	if (creatures) {
		bool playerAdded = false;
		for (auto creature : std::ranges::reverse_view(*creatures)) {
			if (!creature || creature->isRemoved() || !creature->isAlive()) {
				continue;
			}
			if (!player->canSeeCreature(creature)) {
				continue;
			}

			if (tile->getPosition() == player->getPosition() && count == 9 && !playerAdded) {
				creature = player;
			}

			if (creature->getID() == player->getID()) {
				playerAdded = true;
			}

			bool known;
			uint32_t removedKnown;
			checkCreatureAsKnown(creature->getID(), known, removedKnown);
			AddCreature(msg, creature, known, removedKnown);

			if (++count == 10) {
				return;
			}
		}
	}

	if (items) {
		for (auto it = items->getBeginDownItem(), end = items->getEndDownItem(); it != end; ++it) {
			AddItem(msg, *it);

			if (++count == 10) {
				return;
			}
		}
	}
}

void ProtocolGame::GetMapDescription(int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, NetworkMessage &msg) {
	int32_t skip = -1;
	int32_t startz, endz, zstep;

	if (z > MAP_INIT_SURFACE_LAYER) {
		startz = z - MAP_LAYER_VIEW_LIMIT;
		endz = std::min<int32_t>(MAP_MAX_LAYERS - 1, z + MAP_LAYER_VIEW_LIMIT);
		zstep = 1;
	} else {
		startz = MAP_INIT_SURFACE_LAYER;
		endz = 0;
		zstep = -1;
	}

	for (int32_t nz = startz; nz != endz + zstep; nz += zstep) {
		GetFloorDescription(msg, x, y, nz, width, height, z - nz, skip);
	}

	if (skip >= 0) {
		msg.addByte(skip);
		msg.addByte(0xFF);
	}
}

void ProtocolGame::GetFloorDescription(NetworkMessage &msg, int32_t x, int32_t y, int32_t z, int32_t width, int32_t height, int32_t offset, int32_t &skip) {
	for (int32_t nx = 0; nx < width; nx++) {
		for (int32_t ny = 0; ny < height; ny++) {
			const auto &tile = g_game().map.getTile(static_cast<uint16_t>(x + nx + offset), static_cast<uint16_t>(y + ny + offset), static_cast<uint8_t>(z));
			if (tile) {
				if (skip >= 0) {
					msg.addByte(skip);
					msg.addByte(0xFF);
				}

				skip = 0;
				GetTileDescription(tile, msg);
			} else if (skip == 0xFE) {
				msg.addByte(0xFF);
				msg.addByte(0xFF);
				skip = -1;
			} else {
				++skip;
			}
		}
	}
}

void ProtocolGame::checkCreatureAsKnown(uint32_t id, bool &known, uint32_t &removedKnown) {
	if (auto [creatureKnown, creatureInserted] = knownCreatureSet.insert(id);
	    !creatureInserted) {
		known = true;
		return;
	}
	known = false;
	if (knownCreatureSet.size() > 1300) {
		// Look for a creature to remove
		for (auto it = knownCreatureSet.begin(), end = knownCreatureSet.end(); it != end; ++it) {
			if (*it == id) {
				continue;
			}
			// We need to protect party players from removing
			const auto &creature = g_game().getCreatureByID(*it);
			const auto &checkPlayer = creature ? creature->getPlayer() : nullptr;
			if (checkPlayer) {
				if (player->getParty() != checkPlayer->getParty() && !canSee(creature)) {
					removedKnown = *it;
					[[maybe_unused]] auto it_erase = knownCreatureSet.erase(it);
					return;
				}
			} else if (!canSee(creature)) {
				removedKnown = *it;
				[[maybe_unused]] auto it_erase = knownCreatureSet.erase(it);
				return;
			}
		}

		// Bad situation. Let's just remove anyone.
		auto it = knownCreatureSet.begin();
		if (*it == id) {
			++it;
		}

		removedKnown = *it;
		[[maybe_unused]] auto it_erase = knownCreatureSet.erase(it);
	} else {
		removedKnown = 0;
	}
}

bool ProtocolGame::canSee(const std::shared_ptr<Creature> &c) const {
	if (!c || !player || c->isRemoved()) {
		return false;
	}

	if (!player->canSeeCreature(c)) {
		return false;
	}

	return canSee(c->getPosition());
}

bool ProtocolGame::canSee(const Position &pos) const {
	return canSee(pos.x, pos.y, pos.z);
}

bool ProtocolGame::canSee(int32_t x, int32_t y, int32_t z) const {
	if (!player) {
		return false;
	}

	const Position &myPos = player->getPosition();
	if (myPos.z <= MAP_INIT_SURFACE_LAYER) {
		// we are on ground level or above (7 -> 0)
		// view is from 7 -> 0
		if (z > MAP_INIT_SURFACE_LAYER) {
			return false;
		}
	} else if (myPos.z >= MAP_INIT_SURFACE_LAYER + 1) {
		// we are underground (8 -> 15)
		// view is +/- 2 from the floor we stand on
		if (std::abs(myPos.getZ() - z) > MAP_LAYER_VIEW_LIMIT) {
			return false;
		}
	}

	// negative offset means that the action taken place is on a lower floor than ourself
	const int8_t offsetz = myPos.getZ() - z;
	return (x >= myPos.getX() - MAP_MAX_CLIENT_VIEW_PORT_X + offsetz) && (x <= myPos.getX() + (MAP_MAX_CLIENT_VIEW_PORT_X + 1) + offsetz) && (y >= myPos.getY() - MAP_MAX_CLIENT_VIEW_PORT_Y + offsetz) && (y <= myPos.getY() + (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) + offsetz);
}

// Parse methods
void ProtocolGame::parseChannelInvite(NetworkMessage &msg) {
	const std::string name = msg.getString();
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload) && getUnreadBytes(msg) >= sizeof(uint16_t)) {
		static_cast<void>(msg.get<uint16_t>());
	}
	g_game().playerChannelInvite(player->getID(), name);
}

void ProtocolGame::parseChannelExclude(NetworkMessage &msg) {
	const std::string name = msg.getString();
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload) && getUnreadBytes(msg) >= sizeof(uint16_t)) {
		static_cast<void>(msg.get<uint16_t>());
	}
	g_game().playerChannelExclude(player->getID(), name);
}

void ProtocolGame::parseOpenChannel(NetworkMessage &msg) {
	auto channelId = msg.get<uint16_t>();
	g_game().playerOpenChannel(player->getID(), channelId);
}

void ProtocolGame::parseCloseChannel(NetworkMessage &msg) {
	auto channelId = msg.get<uint16_t>();
	g_game().playerCloseChannel(player->getID(), channelId);
}

void ProtocolGame::parseOpenPrivateChannel(NetworkMessage &msg) {
	std::string receiver = msg.getString();
	g_game().playerOpenPrivateChannel(player->getID(), receiver);
}

void ProtocolGame::parseAutoWalk(NetworkMessage &msg) {
	uint8_t numdirs = msg.getByte();
	const bool legacyInnerLength = usesLegacyInnerLength(protocolProfile);
	const auto expectedEndPosition = legacyInnerLength ? msg.getLength() : msg.getLength() + 6;
	if (numdirs == 0 || (msg.getBufferPosition() + numdirs) != expectedEndPosition) {
		return;
	}

	std::vector<Direction> path(numdirs, DIRECTION_NORTH);
	for (uint8_t i = 0; i < numdirs; ++i) {
		const uint8_t rawdir = msg.getByte();
		if (const auto direction = translateAutoWalkDirection(rawdir)) {
			// Creature walking consumes directions from the back of the list.
			path[static_cast<size_t>(numdirs - i - 1)] = *direction;
		} else {
			return;
		}
	}

	g_game().playerAutoWalk(player->getID(), path);
}

void ProtocolGame::parseSetOutfit(NetworkMessage &msg) {
	if (!player || player->isRemoved()) {
		return;
	}

	uint16_t startBufferPosition = msg.getBufferPosition();
	(void)g_modules().executeOnRecvbyte(player, msg, 0xD3);

	if (msg.getBufferPosition() == startBufferPosition) {
		uint8_t outfitType = !oldProtocol ? msg.getByte() : 0;
		Outfit_t newOutfit;
		newOutfit.lookType = msg.get<uint16_t>();
		newOutfit.lookHead = std::min<uint8_t>(132, msg.getByte());
		newOutfit.lookBody = std::min<uint8_t>(132, msg.getByte());
		newOutfit.lookLegs = std::min<uint8_t>(132, msg.getByte());
		newOutfit.lookFeet = std::min<uint8_t>(132, msg.getByte());
		newOutfit.lookAddons = msg.getByte();
		if (outfitType == 0) {
			newOutfit.lookMount = msg.get<uint16_t>();
			bool setMount = false;
			if (!oldProtocol) {
				newOutfit.lookMountHead = std::min<uint8_t>(132, msg.getByte());
				newOutfit.lookMountBody = std::min<uint8_t>(132, msg.getByte());
				newOutfit.lookMountLegs = std::min<uint8_t>(132, msg.getByte());
				newOutfit.lookMountFeet = std::min<uint8_t>(132, msg.getByte());
				setMount = msg.getByte();
				newOutfit.lookFamiliarsType = msg.get<uint16_t>();
				g_logger().debug("Bool isMounted: {}", setMount);
			}

			uint8_t isMountRandomized = !oldProtocol ? msg.getByte() : 0;
			// g_game.enableFeature(GameWingsAurasEffectsShader)
			newOutfit.lookWing = isOTCR ? msg.get<uint16_t>() : 0;
			newOutfit.lookAura = isOTCR ? msg.get<uint16_t>() : 0;
			newOutfit.lookEffect = isOTCR ? msg.get<uint16_t>() : 0;
			std::string shaderName = isOTCR ? msg.getString() : "";
			if (!shaderName.empty()) {
				const auto &shader = g_game().getAttachedEffects()->getShaderByName(shaderName);
				newOutfit.lookShader = shader ? shader->id : 0;
			}
			g_game().playerChangeOutfit(player->getID(), newOutfit, setMount, isMountRandomized);
		} else if (outfitType == 1) {
			// This value probably has something to do with try outfit variable inside outfit window dialog
			// if try outfit is set to 2 it expects uint32_t value after mounted and disable mounts from outfit window dialog
			newOutfit.lookMount = 0;
			msg.get<uint32_t>();
		} else if (outfitType == 2) {
			Position pos = msg.getPosition();
			auto itemId = msg.get<uint16_t>();
			uint8_t stackpos = msg.getByte();
			newOutfit.lookMount = msg.get<uint16_t>();
			newOutfit.lookMountHead = std::min<uint8_t>(132, msg.getByte());
			newOutfit.lookMountBody = std::min<uint8_t>(132, msg.getByte());
			newOutfit.lookMountLegs = std::min<uint8_t>(132, msg.getByte());
			newOutfit.lookMountFeet = std::min<uint8_t>(132, msg.getByte());
			uint8_t direction = std::max<uint8_t>(DIRECTION_NORTH, std::min<uint8_t>(DIRECTION_WEST, msg.getByte()));
			uint8_t podiumVisible = msg.getByte();
			g_game().playerSetShowOffSocket(player->getID(), newOutfit, pos, stackpos, itemId, podiumVisible, direction);
		}
	}
}

void ProtocolGame::parseToggleMount(NetworkMessage &msg) {
	bool mount = msg.getByte(true) != 0;
	g_game().playerToggleMount(player->getID(), mount);
}

void ProtocolGame::parseApplyImbuement(NetworkMessage &msg) {
	uint8_t slot = msg.getByte();
	const uint32_t imbuementId = hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)
		? msg.get<uint32_t>()
		: msg.get<uint16_t>();
	if (imbuementId > std::numeric_limits<uint16_t>::max()) {
		return;
	}
	g_game().playerApplyImbuement(player->getID(), static_cast<uint16_t>(imbuementId), slot);
}

void ProtocolGame::parseClearImbuement(NetworkMessage &msg) {
	uint8_t slot = msg.getByte();
	g_game().playerClearImbuement(player->getID(), slot);
}

void ProtocolGame::parseCloseImbuementWindow(NetworkMessage &) {
	g_game().playerCloseImbuementWindow(player->getID());
}

void ProtocolGame::parseUseItem(NetworkMessage &msg) {
	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	uint8_t index = msg.getByte();
	g_game().playerUseItem(player->getID(), pos, stackpos, index, itemId);
}

void ProtocolGame::parseUseItemEx(NetworkMessage &msg) {
	Position fromPos = msg.getPosition();
	auto fromItemId = msg.get<uint16_t>();
	uint8_t fromStackPos = msg.getByte();
	Position toPos = msg.getPosition();
	auto toItemId = msg.get<uint16_t>();
	uint8_t toStackPos = msg.getByte();
	g_game().playerUseItemEx(player->getID(), fromPos, fromStackPos, fromItemId, toPos, toStackPos, toItemId);
}

void ProtocolGame::parseUseWithCreature(NetworkMessage &msg) {
	Position fromPos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t fromStackPos = msg.getByte();
	auto creatureId = msg.get<uint32_t>();
	g_game().playerUseWithCreature(player->getID(), fromPos, fromStackPos, creatureId, itemId);
}

void ProtocolGame::parseCloseContainer(NetworkMessage &msg) {
	uint8_t cid = msg.getByte();
	g_game().playerCloseContainer(player->getID(), cid);
}

void ProtocolGame::parseUpArrowContainer(NetworkMessage &msg) {
	uint8_t cid = msg.getByte();
	g_game().playerMoveUpContainer(player->getID(), cid);
}

void ProtocolGame::parseUpdateContainer(NetworkMessage &msg) {
	uint8_t cid = msg.getByte();
	g_game().playerUpdateContainer(player->getID(), cid);
}

void ProtocolGame::parseTeleport(NetworkMessage &msg) {
	Position newPosition = msg.getPosition();
	g_game().playerTeleport(player->getID(), newPosition);
}

void ProtocolGame::parseThrow(NetworkMessage &msg) {
	Position fromPos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t fromStackpos = msg.getByte();
	Position toPos = msg.getPosition();
	uint8_t count = msg.getByte();

	if (toPos != fromPos) {
		g_game().playerMoveThing(player->getID(), fromPos, itemId, fromStackpos, toPos, count);
	}
}

void ProtocolGame::parseLookAt(NetworkMessage &msg) {
	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	g_game().playerLookAt(player->getID(), itemId, pos, stackpos);
}

void ProtocolGame::parseLookInBattleList(NetworkMessage &msg) {
	auto creatureId = msg.get<uint32_t>();
	g_game().playerLookInBattleList(player->getID(), creatureId);
}

void ProtocolGame::parseQuickLoot(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t variant = msg.getByte();

	if (variant == 2) {
		g_game().playerLootNearby(player->getID());
		return;
	}

	if (variant > 2) {
		g_logger().debug("[{}] unsupported quick-loot variant {} from player {}", __FUNCTION__, variant, player ? player->getName() : "<null>");
		return;
	}

	const Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	bool lootAllCorpses = variant == 1;

	g_logger().debug("[{}] variant {}, pos {}, itemId {}, stackPos {}", __FUNCTION__, variant, pos.toString(), itemId, stackpos);
	g_game().playerQuickLoot(player->getID(), pos, itemId, stackpos, nullptr, lootAllCorpses, false);
}

void ProtocolGame::parseLootContainer(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t action = msg.getByte();
	if (action == 0) {
		auto category = static_cast<ObjectCategory_t>(msg.getByte());
		Position pos = msg.getPosition();
		auto itemId = msg.get<uint16_t>();
		uint8_t stackpos = msg.getByte();
		g_game().playerSetManagedContainer(player->getID(), category, pos, itemId, stackpos, true);
	} else if (action == 1) {
		auto category = static_cast<ObjectCategory_t>(msg.getByte());
		g_game().playerClearManagedContainer(player->getID(), category, true);
	} else if (action == 2) {
		auto category = static_cast<ObjectCategory_t>(msg.getByte());
		g_game().playerOpenManagedContainer(player->getID(), category, true);
	} else if (action == 3) {
		bool useMainAsFallback = msg.getByte() == 1;
		g_game().playerSetQuickLootFallback(player->getID(), useMainAsFallback);
	} else if (action == 4) {
		auto category = static_cast<ObjectCategory_t>(msg.getByte());
		Position pos = msg.getPosition();
		auto itemId = msg.get<uint16_t>();
		uint8_t stackpos = msg.getByte();
		g_logger().debug("[{}] action {}, category {}, pos {}, itemId {}, stackPos {}", __FUNCTION__, action, static_cast<uint8_t>(category), pos.toString(), itemId, stackpos);
		g_game().playerSetManagedContainer(player->getID(), category, pos, itemId, stackpos, false);
	} else if (action == 5) {
		auto category = static_cast<ObjectCategory_t>(msg.getByte());
		g_game().playerClearManagedContainer(player->getID(), category, false);
	} else if (action == 6) {
		auto category = static_cast<ObjectCategory_t>(msg.getByte());
		g_game().playerOpenManagedContainer(player->getID(), category, false);
	}

	g_logger().debug("[{}] action type {}", __FUNCTION__, action);
}

void ProtocolGame::parseQuickLootBlackWhitelist(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	auto filter = (QuickLootFilter_t)msg.getByte();
	std::vector<uint16_t> listedItems;

	auto size = msg.get<uint16_t>();
	listedItems.reserve(size);

	for (int i = 0; i < size; i++) {
		listedItems.push_back(msg.get<uint16_t>());
	}

	g_game().playerQuickLootBlackWhitelist(player->getID(), filter, listedItems);
}

void ProtocolGame::parseSay(NetworkMessage &msg) {
	std::string receiver;
	uint16_t channelId {};

	auto type = static_cast<SpeakClasses>(msg.getByte());
	switch (type) {
		case TALKTYPE_PRIVATE_TO:
		case TALKTYPE_PRIVATE_RED_TO:
			receiver = msg.getString();
			channelId = 0;
			break;

		case TALKTYPE_CHANNEL_Y:
		case TALKTYPE_CHANNEL_R1:
			channelId = msg.get<uint16_t>();
			break;

		default:
			channelId = 0;
			break;
	}

	const std::string text = msg.getString();
	if (text.length() > 255) {
		return;
	}

	if (channelId == CHANNEL_LIVESTREAM) {
		g_livestream().handleChat(getThis(), text);
		return;
	}

	if (m_isLivestreamViewer) {
		sendTextMessage(TextMessage(MESSAGE_LOOK, "You only can talk in the Livestream channel."));
		return;
	}

	g_game().playerSay(player->getID(), channelId, type, receiver, text);
}

void ProtocolGame::parseFightModes(NetworkMessage &msg) {
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::TacticsWithoutFightMode)) {
		const bool chaseMode = msg.getByte() != 0;
		const bool secureMode = msg.getByte() != 0;
		msg.getByte(); // PvP mode

		g_game().playerSetFightModes(player->getID(), FIGHTMODE_ATTACK, chaseMode, secureMode);
		return;
	}

	uint8_t rawFightMode = msg.getByte(); // 1 - offensive, 2 - balanced, 3 - defensive
	uint8_t rawChaseMode = msg.getByte(); // 0 - stand while fightning, 1 - chase opponent
	uint8_t rawSecureMode = msg.getByte(); // 0 - can't attack unmarked, 1 - can attack unmarked
	// uint8_t rawPvpMode = msg.getByte(); // pvp mode introduced in 10.0

	FightMode_t fightMode;
	if (rawFightMode == 1) {
		fightMode = FIGHTMODE_ATTACK;
	} else if (rawFightMode == 2) {
		fightMode = FIGHTMODE_BALANCED;
	} else {
		fightMode = FIGHTMODE_DEFENSE;
	}

	g_game().playerSetFightModes(player->getID(), fightMode, rawChaseMode != 0, rawSecureMode != 0);
}

void ProtocolGame::parseAttack(NetworkMessage &msg) {
	auto creatureId = msg.get<uint32_t>();
	// msg.get<uint32_t>(); creatureId (same as above)
	g_game().playerSetAttackedCreature(player->getID(), creatureId);
}

void ProtocolGame::parseFollow(NetworkMessage &msg) {
	auto creatureId = msg.get<uint32_t>();
	// msg.get<uint32_t>(); creatureId (same as above)
	g_game().playerFollowCreature(player->getID(), creatureId);
}

void ProtocolGame::parseTextWindow(NetworkMessage &msg) {
	auto windowTextId = msg.get<uint32_t>();
	const std::string newText = msg.getString();
	g_game().playerWriteItem(player->getID(), windowTextId, newText);
}

void ProtocolGame::parseHouseWindow(NetworkMessage &msg) {
	uint8_t doorId = msg.getByte();
	auto id = msg.get<uint32_t>();
	const std::string text = msg.getString();
	g_game().playerUpdateHouseWindow(player->getID(), doorId, id, text);
}

void ProtocolGame::parseLookInShop(NetworkMessage &msg) {
	auto id = msg.get<uint16_t>();
	uint8_t count = msg.getByte();
	g_game().playerLookInShop(player->getID(), id, count);
}

void ProtocolGame::parsePlayerBuyOnShop(NetworkMessage &msg) {
	auto id = msg.get<uint16_t>();
	uint8_t count = msg.getByte();
	uint16_t amount = oldProtocol ? static_cast<uint16_t>(msg.getByte()) : msg.get<uint16_t>();
	bool ignoreCap = msg.getByte(true) != 0;
	bool inBackpacks = msg.getByte(true) != 0;
	g_game().playerBuyItem(player->getID(), id, count, amount, ignoreCap, inBackpacks);
}

void ProtocolGame::parsePlayerSellOnShop(NetworkMessage &msg) {
	auto id = msg.get<uint16_t>();
	uint8_t count = std::max(msg.getByte(), (uint8_t)1);
	uint16_t amount = oldProtocol ? static_cast<uint16_t>(msg.getByte()) : msg.get<uint16_t>();
	bool ignoreEquipped = msg.getByte(true) != 0;

	g_game().playerSellItem(player->getID(), id, count, amount, ignoreEquipped);
}

void ProtocolGame::parseRequestTrade(NetworkMessage &msg) {
	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	auto playerId = msg.get<uint32_t>();
	g_game().playerRequestTrade(player->getID(), pos, stackpos, playerId, itemId);
}

void ProtocolGame::parseLookInTrade(NetworkMessage &msg) {
	bool counterOffer = (msg.getByte() == 0x01);
	uint8_t index = msg.getByte();
	g_game().playerLookInTrade(player->getID(), counterOffer, index);
}

void ProtocolGame::parseAddVip(NetworkMessage &msg) {
	const std::string name = msg.getString();
	g_game().playerRequestAddVip(player->getID(), name);
}

void ProtocolGame::parseRemoveVip(NetworkMessage &msg) {
	auto guid = msg.get<uint32_t>();
	g_game().playerRequestRemoveVip(player->getID(), guid);
}

void ProtocolGame::parseEditVip(NetworkMessage &msg) {
	std::vector<uint8_t> vipGroupsId;
	auto guid = msg.get<uint32_t>();
	const std::string description = msg.getString();
	uint32_t icon = std::min<uint32_t>(10, msg.get<uint32_t>()); // 10 is max icon in 9.63
	bool notify = msg.getByte(true) != 0;
	uint8_t groupsAmount = msg.getByte();
	for (uint8_t i = 0; i < groupsAmount; ++i) {
		uint8_t groupId = msg.getByte();
		vipGroupsId.emplace_back(groupId);
	}
	g_game().playerRequestEditVip(player->getID(), guid, description, icon, notify, vipGroupsId);
}

void ProtocolGame::parseVipGroupActions(NetworkMessage &msg) {
	uint8_t action = msg.getByte();

	switch (action) {
		case 0x01: {
			const std::string groupName = msg.getString();
			player->vip().addGroup(groupName);
			break;
		}
		case 0x02: {
			const uint8_t groupId = msg.getByte();
			const std::string newGroupName = msg.getString();
			player->vip().editGroup(groupId, newGroupName);
			break;
		}
		case 0x03: {
			const uint8_t groupId = msg.getByte();
			player->vip().removeGroup(groupId);
			break;
		}
		default: {
			break;
		}
	}
}

void ProtocolGame::parseRotateItem(NetworkMessage &msg) {
	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	const auto &itemType = Item::items[itemId];
	if (itemType.isPodium) {
		g_game().playerRotatePodium(player->getID(), pos, stackpos, itemId);
	} else {
		g_game().playerRotateItem(player->getID(), pos, stackpos, itemId);
	}
}

void ProtocolGame::parseWrapableItem(NetworkMessage &msg) {
	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	g_game().playerWrapableItem(player->getID(), pos, stackpos, itemId);
}

void ProtocolGame::parseInspectionObject(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t inspectionType = msg.getByte();
	if (inspectionType == INSPECT_NORMALOBJECT) {
		Position pos = msg.getPosition();
		g_game().playerInspectItem(player, pos);
	} else if (inspectionType == INSPECT_NPCTRADE || inspectionType == INSPECT_CYCLOPEDIA) {
		auto itemId = msg.get<uint16_t>();
		uint16_t itemCount = msg.getByte();
		g_game().playerInspectItem(player, itemId, static_cast<int8_t>(itemCount), (inspectionType == INSPECT_CYCLOPEDIA));
	} else if (inspectionType == INSPECT_PROFICIENCY) {
		const auto itemId = msg.get<uint16_t>();
		msg.getByte(); // Unknown byte
		sendWeaponProficiencyWindow(itemId);
	}
}

void ProtocolGame::sendSessionEndInformation(SessionEndInformations information) {
	if (!oldProtocol) {
		auto output = OutputMessagePool::getOutputMessage();
		output->addByte(0x18);
		output->addByte(information);
		send(output);
	}
	disconnect();
}

void ProtocolGame::sendItemInspection(uint16_t itemId, uint8_t itemCount, const std::shared_ptr<Item> &item, bool cyclopedia) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x76);
	msg.addByte(0x00);
	msg.addByte(cyclopedia ? 0x01 : 0x00);
	msg.add<uint32_t>(player->getID()); // 13.00 Creature ID
	msg.addByte(0x01);

	const ItemType &it = Item::items[itemId];

	if (item) {
		msg.addString(item->getName());
		AddItem(msg, item);
	} else {
		msg.addString(it.name);
		AddItem(msg, it.id, itemCount, 0);
	}
	msg.addByte(0);

	auto descriptions = Item::getDescriptions(it, item);
	msg.addByte(descriptions.size());
	for (const auto &description : descriptions) {
		msg.addString(description.first);
		msg.addString(description.second);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseFriendSystemAction(NetworkMessage &msg) {
	const auto state = msg.getByte(true);
	const auto isCurrentPayload = hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload);

	switch (state) {
		case 0x03:
		case 0x05:
		case 0x06:
		case 0x07:
		case 0x08:
		case 0x09:
			if (isCurrentPayload) {
				static_cast<void>(msg.get<uint32_t>());
			}
			break;
		case 0x04:
		case 0x0A:
			if (isCurrentPayload) {
				static_cast<void>(msg.get<uint32_t>());
				msg.getByte(true);
			}
			break;
		case 0x0B:
			if (isCurrentPayload) {
				msg.getString();
			}
			break;
		case 0x0D:
			if (isCurrentPayload) {
				static_cast<void>(msg.get<uint32_t>());
				msg.getByte(true);
			}
			break;
		case 0x0E: {
			const auto titleId = msg.getByte(true);
			g_game().playerFriendSystemAction(player, state, titleId);
			break;
		}
		case 0x10:
			if (isCurrentPayload) {
				msg.getByte(true);
				msg.getByte(true);
				msg.getByte(true);
				msg.getByte(true);
			}
			break;
		default:
			break;
	}
}

void ProtocolGame::parseCyclopediaCharacterInfo(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint32_t characterID;
	CyclopediaCharacterInfoType_t characterInfoType;
	characterID = msg.get<uint32_t>();
	characterInfoType = static_cast<CyclopediaCharacterInfoType_t>(msg.getByte());
	uint16_t entriesPerPage = 0, page = 0;
	if (characterInfoType == CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS || characterInfoType == CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS) {
		entriesPerPage = std::min<uint16_t>(30, std::max<uint16_t>(5, msg.get<uint16_t>()));
		page = std::max<uint16_t>(1, msg.get<uint16_t>());
	}
	if (characterID == 0) {
		characterID = player->getID();
	}
	g_game().playerCyclopediaCharacterInfo(player, characterID, characterInfoType, entriesPerPage, page);
}

void ProtocolGame::parseHighscores(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	auto type = static_cast<HighscoreType_t>(msg.getByte());
	uint8_t category = msg.getByte();
	auto vocation = msg.get<uint32_t>();
	uint16_t page = 1;
	const std::string worldName = msg.getString();
	msg.getByte(); // Game World Category
	msg.getByte(); // BattlEye World Type
	if (type == HIGHSCORE_GETENTRIES) {
		page = std::max<uint16_t>(1, msg.get<uint16_t>());
	}
	uint8_t entriesPerPage = std::min<uint8_t>(30, std::max<uint8_t>(5, msg.getByte()));
	g_game().playerHighscores(player, type, category, vocation, worldName, page, entriesPerPage);
}

void ProtocolGame::parseImbuementAction(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	auto action = static_cast<ImbuementAction>(msg.getByte()); // 0x01 - pick item, 0x02 - scroll imbuement

	std::shared_ptr<Item> item = nullptr;

	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		if (action == ImbuementAction::PickItem) {
			const Position position = msg.getPosition();
			const auto itemId = msg.get<uint16_t>();
			const auto stackPosition = msg.getByte(true);
			const auto &thing = g_game().internalGetThing(player, position, stackPosition, itemId, STACKPOS_FIND_THING);
			item = thing ? thing->getItem() : nullptr;

			if (!item || item->getID() != itemId) {
				return;
			}

			if (item->getImbuementSlot() <= 0) {
				player->sendImbuementResult("This item is not imbuable.");
				return;
			}
		}

		openImbuementWindow(action, item);
		return;
	}

	if (action == ImbuementAction::PickItem) {
		msg.skipBytes(2); // Unknown bytes
		auto slotId = msg.getByte();
		msg.skipBytes(1); // Unknown byte
		msg.skipBytes(1); // Unknown byte
		auto itemId = msg.get<uint16_t>();
		auto slotIndex = msg.getByte();

		const bool isContainerItem = slotId >= 0x40;
		if (isContainerItem) {
			// Padding to not conflict with inventory slot
			// the client sends with this padding
			slotId -= 0x40;
			const auto &container = player->getContainerByID(slotId);
			item = container ? container->getItemByIndex(slotIndex) : nullptr;
		} else {
			item = player->getInventoryItem(static_cast<Slots_t>(slotId));
		}

		if (!item || item->getID() != itemId) {
			return;
		}

		if (item->getImbuementSlot() <= 0) {
			if (isContainerItem) {
				player->sendImbuementResult("This item is not imbuable.");
			}
			return;
		}
	}

	openImbuementWindow(action, item);
}

void ProtocolGame::parseWeaponProficiency(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	const auto action = msg.getByte();

	if (action == 0x01) {
		for (const auto weaponId : player->weaponProficiency().getTrackedWeaponIds()) {
			sendWeaponProficiencyWindow(weaponId);
		}
		return;
	}

	const auto weaponId = msg.get<uint16_t>();
	const auto equippedWeaponId = player->getWeaponId(true);
	const bool isEquippedWeapon = equippedWeaponId != 0 && weaponId == equippedWeaponId;
	const auto consumeOfficialPayloadBytes = [this, &msg](uint8_t count) {
		if (!hasProtocolFeature(protocolProfile, ProtocolFeature::OfficialWeaponProficiencyPayload)) {
			return;
		}

		for (uint8_t i = 0; i < count && msg.canRead(1); ++i) {
			msg.getByte(true);
		}
	};

	switch (action) {
		case 0x00:
			break;
		case 0x02:
			if (isEquippedWeapon) {
				player->weaponProficiency().clearAllStats();
				player->sendSkills();
			}
			player->weaponProficiency().clearSelectedPerks(weaponId);
			break;
		case 0x03: {
			if (isEquippedWeapon) {
				player->weaponProficiency().clearAllStats();
			}
			player->weaponProficiency().clearSelectedPerks(weaponId);

			const auto slots = msg.getByte();
			for (uint8_t slot = 0; slot < slots; slot++) {
				const auto level = msg.getByte();
				const auto perkIndex = msg.getByte();

				player->weaponProficiency().setSelectedPerk(level, perkIndex, weaponId);
			}

			if (isEquippedWeapon) {
				player->weaponProficiency().applyPerks(weaponId);
			}
			break;
		}
		case 0x04:
		case 0x05:
		case 0x06:
		case 0x07:
		case 0x09:
			consumeOfficialPayloadBytes(2);
			break;
		case 0x08:
			consumeOfficialPayloadBytes(3);
			break;
		default:
			break;
	}

	sendWeaponProficiencyWindow(weaponId);
	sendWeaponProficiency(weaponId);
}

void ProtocolGame::parseTaskHuntingAction(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t slot = msg.getByte();
	uint8_t action = msg.getByte();
	bool upgrade = msg.getByte(true) != 0;
	auto raceId = msg.get<uint16_t>();

	if (!g_configManager().getBoolean(TASK_HUNTING_ENABLED)) {
		return;
	}

	g_game().playerTaskHuntingAction(player->getID(), slot, action, upgrade, raceId);
}

void ProtocolGame::parseSetClientOptions(NetworkMessage &msg) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		return;
	}

	msg.getByte(true);
	msg.getByte(true);
}

void ProtocolGame::parseClientCheck(NetworkMessage &msg) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		return;
	}

	const auto payloadSize = msg.get<uint32_t>();
	const auto unreadBytes = getUnreadBytes(msg);
	if (payloadSize > unreadBytes) {
		g_logger().debug("[ProtocolGame::parseClientCheck] truncated client check payload: expected {} bytes, got {}.", payloadSize, unreadBytes);
		return;
	}

	// Confirmed wire shape: u32 payload byte count followed by that many raw
	// bytes. With the current login probe byte 0x01, official 15.25 clients
	// answer with a two-byte payload: 0x01 0x01.
	for (uint32_t i = 0; i < payloadSize; ++i) {
		msg.getByte(true);
	}
}

void ProtocolGame::parseSetVocation(NetworkMessage &msg) {
	msg.getByte(true);
}

void ProtocolGame::parseStartOfflineTraining(NetworkMessage &msg) {
	msg.getByte(true);
}

void ProtocolGame::parseContainerAction(NetworkMessage &msg) {
	msg.getByte(true);
	const auto action = msg.getByte(true);
	if (action == 0) {
		msg.getByte(true);
		msg.getByte(true);
		msg.getByte(true);
	} else if (action == 1) {
		msg.getByte(true);
	}
}

void ProtocolGame::parseCharacterTradeConfigurationAction(NetworkMessage &msg) {
	const auto action = msg.getByte(true);
	if (action == 2 || action == 3) {
		msg.get<uint32_t>();
		msg.get<uint32_t>();

		const auto sourceItemCount = msg.getByte(true);
		for (uint8_t index = 0; index < sourceItemCount; ++index) {
			const auto itemId = msg.get<uint16_t>();
			if (itemTypeHasSubtype(itemId)) {
				msg.getByte(true);
			}
		}

		const auto targetItemCount = msg.getByte(true);
		for (uint8_t index = 0; index < targetItemCount; ++index) {
			const auto itemId = msg.get<uint16_t>();
			if (itemTypeHasSubtype(itemId)) {
				msg.getByte(true);
			}
		}

		const auto storeItemCount = msg.getByte(true);
		for (uint8_t index = 0; index < storeItemCount; ++index) {
			msg.get<uint16_t>();
		}
	}
}

void ProtocolGame::parseJoinAggression(NetworkMessage &msg) {
	msg.get<uint32_t>();
}

void ProtocolGame::parseEditGuildMessage(NetworkMessage &msg) {
	msg.getString();
}

void ProtocolGame::parseGetTextForReport(NetworkMessage &msg) {
	msg.getPosition();
	msg.get<uint16_t>();
	msg.getByte(true);
}

void ProtocolGame::parseClientDetails(NetworkMessage &msg) {
	msg.getString();
}

void ProtocolGame::parseBossDifficultySelection(NetworkMessage &msg) {
	msg.get<uint32_t>();
	const auto action = msg.getByte(true);
	if (action == 0 || action == 2) {
		msg.get<uint16_t>();
	}
}

void ProtocolGame::parseInspectPlayer(NetworkMessage &msg) {
	const auto action = msg.getByte(true);
	if (action >= 1 && action <= 5) {
		msg.get<uint32_t>();
	}
}

void ProtocolGame::parseCyclopediaMapAction(NetworkMessage &msg) {
	const auto action = msg.getByte(true);
	switch (action) {
		case 0:
			msg.get<uint16_t>();
			break;
		case 1:
			msg.get<uint16_t>();
			msg.get<uint32_t>();
			break;
		case 2:
			msg.get<uint16_t>();
			break;
		default:
			break;
	}
}

void ProtocolGame::parseSetHirelingName(NetworkMessage &msg) {
	msg.getString();
	msg.get<uint32_t>();
	msg.get<uint32_t>();
}

void ProtocolGame::sendHighscoresNoData() {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xB1);
	msg.addByte(0x01); // No data available
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendHighscores(const std::vector<HighscoreCharacter> &characters, uint8_t categoryId, uint32_t vocationId, uint16_t page, uint16_t pages, uint32_t updateTimer) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xB1);
	msg.addByte(0x00); // All data available

	msg.addByte(1); // Worlds
	auto serverName = g_configManager().getString(SERVER_NAME);
	msg.addString(serverName); // First World
	msg.addString(serverName); // Selected World

	msg.addByte(0); // Game World Category: 0xFF(-1) - Selected World
	msg.addByte(0); // BattlEye World Type

	auto vocationPosition = msg.getBufferPosition();
	uint8_t vocations = 1;

	msg.skipBytes(1); // Vocation Count
	msg.add<uint32_t>(0xFFFFFFFF); // All Vocations - hardcoded
	msg.addString("(all)"); // All Vocations - hardcoded

	uint32_t selectedVocation = 0xFFFFFFFF;
	const auto vocationsMap = g_vocations().getVocations();
	for (const auto &it : vocationsMap) {
		const auto &vocation = it.second;
		auto fromVoc = vocation->getFromVocation();
		auto loopVocationId = vocation->getId();
		if (fromVoc == static_cast<uint32_t>(loopVocationId)) {
			auto vocClientId = vocation->getClientId();
			msg.add<uint32_t>(vocClientId); // Vocation Id
			msg.addString(vocation->getVocName()); // Vocation Name
			++vocations;
			if (vocClientId == vocationId) {
				selectedVocation = vocClientId;
			}
		}
	}
	msg.add<uint32_t>(selectedVocation); // Selected Vocation

	uint8_t selectedCategory = 0;
	const auto &highscoreCategories = g_game().getHighscoreCategories();
	msg.addByte(highscoreCategories.size()); // Category Count
	for (const HighscoreCategory &category : highscoreCategories) {
		msg.addByte(category.m_id); // Category Id
		msg.addString(category.m_name); // Category Name
		if (category.m_id == categoryId) {
			selectedCategory = categoryId;
		}
	}
	msg.addByte(selectedCategory); // Selected Category

	msg.add<uint16_t>(page); // Current page
	msg.add<uint16_t>(pages); // Pages

	msg.addByte(characters.size()); // Character Count
	for (const HighscoreCharacter &character : characters) {
		msg.add<uint32_t>(character.rank); // Rank
		msg.addString(character.name); // Character Name
		msg.addString(character.loyaltyTitle); // Character Loyalty Title
		msg.addByte(character.vocation); // Vocation Id
		msg.addString(serverName); // World
		msg.add<uint16_t>(character.level); // Level
		msg.addByte((player->getGUID() == character.id)); // Player Indicator Boolean
		msg.add<uint64_t>(character.points); // Points
	}

	msg.addByte(0xFF); // ??
	msg.addByte(0); // ??
	msg.addByte(1); // ??
	msg.add<uint32_t>(updateTimer); // Last Update
	msg.setBufferPosition(vocationPosition);
	msg.addByte(vocations);
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseConfigureShowOffSocket(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	g_game().playerConfigureShowOffSocket(player->getID(), pos, stackpos, itemId);
}

void ProtocolGame::parseRuleViolationReport(NetworkMessage &msg) {
	uint8_t reportType = msg.getByte();
	uint8_t reportReason = msg.getByte();
	const std::string &targetName = msg.getString();
	const std::string &comment = msg.getString();
	std::string translation;
	if (reportType == REPORT_TYPE_NAME) {
		translation = msg.getString();
	} else if (reportType == REPORT_TYPE_STATEMENT) {
		translation = msg.getString();
		msg.get<uint32_t>(); // statement id, used to get whatever player have said, we don't log that.
	}

	g_game().playerReportRuleViolationReport(player->getID(), targetName, reportType, reportReason, comment, translation);
}

void ProtocolGame::parseBestiarySendRaces() {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xD5);
	msg.add<uint16_t>(BESTY_RACE_LAST);
	const std::map<uint16_t, std::string> &mtype_list = g_game().getBestiaryList();
	for (uint8_t i = BESTY_RACE_FIRST; i <= BESTY_RACE_LAST; i++) {
		std::string BestClass;
		uint16_t count = 0;
		for (const auto &rit : mtype_list) {
			const auto mtype = g_monsters().getMonsterType(rit.second);
			if (!mtype) {
				return;
			}
			if (mtype->info.bestiaryRace == static_cast<BestiaryType_t>(i)) {
				count += 1;
				BestClass = mtype->info.bestiaryClass;
			}
		}
		msg.addString(BestClass);
		msg.add<uint16_t>(count);
		uint16_t unlockedCount = g_iobestiary().getBestiaryRaceUnlocked(player, static_cast<BestiaryType_t>(i));
		msg.add<uint16_t>(unlockedCount);
	}
	writeToOutputBuffer(msg);

	player->sendBestiaryCharms();
}

void ProtocolGame::sendBestiaryEntryChanged(uint16_t raceid) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xD9);
	msg.add<uint16_t>(raceid);
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	auto raceId = msg.get<uint16_t>();
	std::string Class;
	std::shared_ptr<MonsterType> mtype = nullptr;
	const std::map<uint16_t, std::string> &mtype_list = g_game().getBestiaryList();

	auto ait = mtype_list.find(raceId);
	if (ait != mtype_list.end()) {
		auto mType = g_monsters().getMonsterType(ait->second);
		if (mType) {
			Class = mType->info.bestiaryClass;
			mtype = mType;
		}
	}

	if (!mtype) {
		g_logger().warn("[ProtocolGame::parseBestiarysendMonsterData] - "
		                "MonsterType was not found");
		return;
	}

	uint32_t killCounter = player->getBestiaryKillCount(raceId);
	uint8_t currentLevel = g_iobestiary().getKillStatus(mtype, killCounter);

	NetworkMessage newmsg;
	newmsg.addByte(0xD7);
	newmsg.add<uint16_t>(raceId);
	newmsg.addString(Class);

	newmsg.addByte(currentLevel);

	if (player->animusMastery().has(mtype->name)) {
		newmsg.add<uint16_t>(static_cast<uint16_t>(std::round((player->animusMastery().getExperienceMultiplier() - 1) * 1000))); // Animus Mastery Bonus
		newmsg.add<uint16_t>(player->animusMastery().getPoints()); // Animus Mastery Points
	} else {
		newmsg.add<uint16_t>(0);
		newmsg.add<uint16_t>(0);
	}
	newmsg.add<uint32_t>(killCounter);

	newmsg.add<uint16_t>(mtype->info.bestiaryFirstUnlock);
	newmsg.add<uint16_t>(mtype->info.bestiarySecondUnlock);
	newmsg.add<uint16_t>(mtype->info.bestiaryToUnlock);

	newmsg.addByte(mtype->info.bestiaryStars);
	newmsg.addByte(mtype->info.bestiaryOccurrence);

	const std::vector<LootBlock> &lootList = mtype->info.lootItems;
	newmsg.addByte(lootList.size());
	for (const LootBlock &loot : lootList) {
		int8_t difficult = g_iobestiary().calculateDifficult(loot.chance);
		bool shouldAddItem = false;

		switch (currentLevel) {
			case 1:
				shouldAddItem = false;
				break;
			case 2:
				if (difficult < 2) {
					shouldAddItem = true;
				}
				break;
			case 3:
				if (difficult < 3) {
					shouldAddItem = true;
				}
				break;
			case 4:
				shouldAddItem = true;
				break;
		}

		newmsg.add<uint16_t>(g_configManager().getBoolean(SHOW_LOOTS_IN_BESTIARY) || shouldAddItem == true ? loot.id : 0);
		newmsg.addByte(difficult);
		newmsg.addByte(0); // 1 if special event - 0 if regular loot (?)
		if (g_configManager().getBoolean(SHOW_LOOTS_IN_BESTIARY) || shouldAddItem == true) {
			newmsg.addString(loot.name);
			newmsg.addByte(loot.countmax > 0 ? 0x1 : 0x0);
		}
	}

	if (currentLevel > 1) {
		newmsg.add<uint16_t>(mtype->info.bestiaryCharmsPoints);
		int8_t attackmode = 0;
		if (!mtype->info.isHostile) {
			attackmode = 2;
		} else if (mtype->info.targetDistance) {
			attackmode = 1;
		}

		newmsg.addByte(attackmode);
		newmsg.addByte(0x02);
		newmsg.add<uint32_t>(mtype->info.healthMax);
		newmsg.add<uint32_t>(mtype->info.experience);
		newmsg.add<uint16_t>(mtype->getBaseSpeed());
		newmsg.add<uint16_t>(mtype->info.armor);
		newmsg.addDouble(mtype->info.mitigation);
	}

	if (currentLevel > 2) {
		std::map<uint8_t, int16_t> elements = g_iobestiary().getMonsterElements(mtype);

		newmsg.addByte(elements.size());
		for (auto &element : elements) {
			newmsg.addByte(element.first);
			newmsg.add<uint16_t>(element.second);
		}

		newmsg.add<uint16_t>(1);
		newmsg.addString(mtype->info.bestiaryLocations);
	}

	writeToOutputBuffer(newmsg);
}

void ProtocolGame::parseCyclopediaMonsterTracker(NetworkMessage &msg) {
	auto monsterRaceId = msg.get<uint16_t>();
	// Bosstiary tracker: 0 = disabled, 1 = enabled
	// Bestiary tracker: 1 = enabled
	auto trackerButtonType = msg.getByte();

	// Bosstiary tracker logic
	if (const auto monsterType = g_ioBosstiary().getMonsterTypeByBossRaceId(monsterRaceId)) {
		if (player->getBestiaryKillCount(monsterRaceId)) {
			if (trackerButtonType == 1) {
				player->addMonsterToCyclopediaTrackerList(monsterType, true, true);
			} else {
				player->removeMonsterFromCyclopediaTrackerList(monsterType, true, true);
			}
		}
		return;
	}

	// Bestiary tracker logic
	const auto &bestiaryMonsters = g_game().getBestiaryList();
	auto it = bestiaryMonsters.find(monsterRaceId);
	if (it != bestiaryMonsters.end()) {
		const auto mtype = g_monsters().getMonsterType(it->second);
		if (!mtype) {
			g_logger().error("[{}] player {} have wrong boss with race {}", __FUNCTION__, player->getName(), monsterRaceId);
			return;
		}

		if (trackerButtonType == 1) {
			player->addMonsterToCyclopediaTrackerList(mtype, false, true);
		} else {
			player->removeMonsterFromCyclopediaTrackerList(mtype, false, true);
		}
	}
}

void ProtocolGame::parsePlayerTyping(NetworkMessage &msg) {
	uint8_t typing = msg.getByte();
	g_dispatcher().addEvent([self = getThis(), playerID = player->getID(), typing] { g_game().playerSetTyping(playerID, typing); }, __FUNCTION__, 0, DispatcherLane::PlayerAction, player->getID());
}

void ProtocolGame::sendTeamFinderList() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x2D);
	msg.addByte(0x00); // Bool value, with 'true' the player exceed packets for second.
	const auto &teamFinder = g_game().getTeamFinderList();
	msg.add<uint16_t>(teamFinder.size());
	for (const auto &it : teamFinder) {
		const auto &leader = g_game().getPlayerByGUID(it.first);
		if (!leader) {
			return;
		}

		const auto &teamAssemble = it.second;
		if (!teamAssemble) {
			return;
		}

		uint8_t status = 0;
		uint16_t membersSize = 0;
		msg.add<uint32_t>(leader->getGUID());
		msg.addString(leader->getName());
		msg.add<uint16_t>(teamAssemble->minLevel);
		msg.add<uint16_t>(teamAssemble->maxLevel);
		msg.addByte(teamAssemble->vocationIDs);
		msg.add<uint16_t>(teamAssemble->teamSlots);
		for (auto itt : teamAssemble->membersMap) {
			std::shared_ptr<Player> member = g_game().getPlayerByGUID(it.first);
			if (member) {
				if (itt.first == player->getGUID()) {
					status = itt.second;
				}

				if (itt.second == 3) {
					membersSize += 1;
				}
			}
		}
		msg.add<uint16_t>(std::max<uint16_t>((teamAssemble->teamSlots - teamAssemble->freeSlots), membersSize));
		// The leader does not count on this math, he is included inside the 'freeSlots'.
		msg.add<uint32_t>(teamAssemble->timestamp);
		msg.addByte(teamAssemble->teamType);

		switch (teamAssemble->teamType) {
			case 1: {
				msg.add<uint16_t>(teamAssemble->bossID);
				break;
			}
			case 2: {
				msg.add<uint16_t>(teamAssemble->hunt_type);
				msg.add<uint16_t>(teamAssemble->hunt_area);
				break;
			}
			case 3: {
				msg.add<uint16_t>(teamAssemble->questID);
				break;
			}

			default:
				break;
		}

		msg.addByte(status);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendLeaderTeamFinder(bool reset) {
	if (!player || oldProtocol) {
		return;
	}

	const auto &teamAssemble = g_game().getTeamFinder(player);
	if (!teamAssemble) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x2C);
	msg.addByte(reset ? 1 : 0);
	if (reset) {
		g_game().removeTeamFinderListed(player->getGUID());
		return;
	}

	msg.add<uint16_t>(teamAssemble->minLevel);
	msg.add<uint16_t>(teamAssemble->maxLevel);
	msg.addByte(teamAssemble->vocationIDs);
	msg.add<uint16_t>(teamAssemble->teamSlots);
	msg.add<uint16_t>(teamAssemble->freeSlots);
	msg.add<uint32_t>(teamAssemble->timestamp);
	msg.addByte(teamAssemble->teamType);

	switch (teamAssemble->teamType) {
		case 1: {
			msg.add<uint16_t>(teamAssemble->bossID);
			break;
		}
		case 2: {
			msg.add<uint16_t>(teamAssemble->hunt_type);
			msg.add<uint16_t>(teamAssemble->hunt_area);
			break;
		}
		case 3: {
			msg.add<uint16_t>(teamAssemble->questID);
			break;
		}

		default:
			break;
	}

	uint16_t membersSize = 1;
	for (const auto &[memberGUID, memberStatus] : teamAssemble->membersMap) {
		std::shared_ptr<Player> member = g_game().getPlayerByGUID(memberGUID);
		if (member) {
			membersSize += 1;
		}
	}

	msg.add<uint16_t>(membersSize);
	std::shared_ptr<Player> leader = g_game().getPlayerByGUID(teamAssemble->leaderGuid);
	if (!leader) {
		return;
	}

	msg.add<uint32_t>(leader->getGUID());
	msg.addString(leader->getName());
	msg.add<uint16_t>(leader->getLevel());
	msg.addByte(leader->getVocation()->getClientId());
	msg.addByte(3);

	const auto countPos = msg.getBufferPosition();
	uint8_t count = 0;

	for (const auto &[memberGUID, memberStatus] : teamAssemble->membersMap) {
		auto member = g_game().getPlayerByGUID(memberGUID);
		if (!member) {
			continue;
		}

		++count;
	}

	for (const auto &[memberGUID, memberStatus] : teamAssemble->membersMap) {
		auto member = g_game().getPlayerByGUID(memberGUID);
		if (!member) {
			continue;
		}
		msg.add<uint32_t>(member->getGUID());
		msg.addString(member->getName());
		msg.add<uint16_t>(member->getLevel());
		msg.addByte(member->getVocation()->getClientId());
		msg.addByte(memberStatus);
	}

	msg.setBufferPosition(countPos);
	msg.addByte(count);

	writeToOutputBuffer(msg);
}

void ProtocolGame::createLeaderTeamFinder(NetworkMessage &msg) {
	if (!player || oldProtocol) {
		return;
	}

	const auto &teamAssemble = g_game().getOrCreateTeamFinder(player);
	teamAssemble->minLevel = msg.get<uint16_t>();
	teamAssemble->maxLevel = msg.get<uint16_t>();
	teamAssemble->vocationIDs = msg.getByte();
	teamAssemble->teamSlots = msg.get<uint16_t>();
	teamAssemble->freeSlots = msg.get<uint16_t>();
	teamAssemble->partyBool = (msg.getByte() == 1);
	teamAssemble->timestamp = msg.get<uint32_t>();
	teamAssemble->teamType = msg.getByte();

	uint16_t bossID = 0;
	uint16_t huntType1 = 0;
	uint16_t huntType2 = 0;
	uint16_t questID = 0;

	switch (teamAssemble->teamType) {
		case 1: {
			bossID = msg.get<uint16_t>();
			break;
		}
		case 2: {
			huntType1 = msg.get<uint16_t>();
			huntType2 = msg.get<uint16_t>();
			break;
		}

		case 3: {
			questID = msg.get<uint16_t>();
			break;
		}

		default:
			break;
	}

	teamAssemble->bossID = bossID;
	teamAssemble->hunt_type = huntType1;
	teamAssemble->hunt_area = huntType2;
	teamAssemble->questID = questID;
	teamAssemble->leaderGuid = player->getGUID();

	auto party = player->getParty();
	if (teamAssemble->partyBool && party) {
		for (const std::shared_ptr<Player> &member : party->getMembers()) {
			if (member && member->getGUID() != player->getGUID()) {
				[[maybe_unused]] auto [it_member, inserted_member] = teamAssemble->membersMap.insert({ member->getGUID(), 3 });
			}
		}
		auto partyLeader = party->getLeader();
		if (partyLeader && partyLeader->getGUID() != player->getGUID()) {
			[[maybe_unused]] auto [it_leader, inserted_leader] = teamAssemble->membersMap.insert({ partyLeader->getGUID(), 3 });
		}
	}
}

void ProtocolGame::parsePartyAnalyzerAction(NetworkMessage &msg) const {
	if (!player || oldProtocol) {
		return;
	}

	std::shared_ptr<Party> party = player->getParty();
	if (!party || !party->getLeader() || party->getLeader()->getID() != player->getID()) {
		return;
	}

	auto action = static_cast<PartyAnalyzerAction_t>(msg.getByte());
	if (action == PARTYANALYZERACTION_RESET) {
		party->resetAnalyzer();
	} else if (action == PARTYANALYZERACTION_PRICETYPE) {
		party->switchAnalyzerPriceType();
	} else if (action == PARTYANALYZERACTION_PRICEVALUE) {
		auto size = msg.get<uint16_t>();
		for (uint16_t i = 1; i <= size; i++) {
			auto itemId = msg.get<uint16_t>();
			auto price = msg.get<uint64_t>();
			player->setItemCustomPrice(itemId, price);
		}
		party->reloadPrices();
		party->updateTrackerAnalyzer();
		player->updatePartyTrackerAnalyzer(true);
	}
}

void ProtocolGame::parseLeaderFinderWindow(NetworkMessage &msg) {
	if (!player || oldProtocol) {
		return;
	}

	uint8_t action = msg.getByte();
	switch (action) {
		case 0: {
			player->sendLeaderTeamFinder(false);
			break;
		}
		case 1: {
			player->sendLeaderTeamFinder(true);
			break;
		}
		case 2: {
			auto memberID = msg.get<uint32_t>();
			std::shared_ptr<Player> member = g_game().getPlayerByGUID(memberID);
			if (!member) {
				return;
			}

			const auto &teamAssemble = g_game().getTeamFinder(player);
			if (!teamAssemble) {
				return;
			}

			uint8_t memberStatus = msg.getByte();
			for (auto &[guid, status] : teamAssemble->membersMap) {
				if (guid == memberID) {
					status = memberStatus;
				}
			}

			switch (memberStatus) {
				case 2: {
					member->sendTextMessage(MESSAGE_STATUS, "You are invited to a new team.");
					break;
				}
				case 3: {
					member->sendTextMessage(MESSAGE_STATUS, "Your team finder request was accepted.");
					break;
				}
				case 4: {
					member->sendTextMessage(MESSAGE_STATUS, "Your team finder request was denied.");
					break;
				}

				default:
					break;
			}
			player->sendLeaderTeamFinder(false);
			break;
		}
		case 3: {
			player->createLeaderTeamFinder(msg);
			player->sendLeaderTeamFinder(false);
			break;
		}

		default:
			break;
	}
}

void ProtocolGame::parseMemberFinderWindow(NetworkMessage &msg) {
	if (!player || oldProtocol) {
		return;
	}

	uint8_t action = msg.getByte();
	if (action == 0) {
		player->sendTeamFinderList();
	} else {
		auto leaderID = msg.get<uint32_t>();
		std::shared_ptr<Player> leader = g_game().getPlayerByGUID(leaderID);
		if (!leader) {
			return;
		}

		const auto &teamAssemble = g_game().getTeamFinder(player);
		if (!teamAssemble) {
			return;
		}

		if (action == 1) {
			leader->sendTextMessage(MESSAGE_STATUS, "There is a new request to join your team.");
			[[maybe_unused]] auto [it_request, inserted_request] = teamAssemble->membersMap.insert({ player->getGUID(), 1 });
		} else {
			for (auto itt = teamAssemble->membersMap.begin(), end = teamAssemble->membersMap.end(); itt != end; ++itt) {
				if (itt->first == player->getGUID()) {
					[[maybe_unused]] auto it = teamAssemble->membersMap.erase(itt);
					break;
				}
			}
		}
		player->sendTeamFinderList();
	}
}

void ProtocolGame::parseSendBuyCharmRune(NetworkMessage &msg) {
	if (!player || oldProtocol) {
		return;
	}

	uint8_t action = msg.getByte();
	auto charmId = static_cast<charmRune_t>(msg.getByte());
	uint16_t raceId = msg.get<uint16_t>();
	g_iobestiary().sendBuyCharmRune(player, action, charmId, raceId);
}

void ProtocolGame::refreshCyclopediaMonsterTracker(const std::unordered_set<std::shared_ptr<MonsterType>> &trackerSet, bool isBoss) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xB9);
	msg.addByte(isBoss ? 0x01 : 0x00);
	msg.addByte(trackerSet.size());
	for (const auto &mtype : trackerSet) {
		auto raceId = mtype->info.raceid;
		const auto stages = g_ioBosstiary().getBossRaceKillStages(mtype->info.bosstiaryRace);
		if (isBoss && (stages.empty() || stages.size() != 3)) {
			return;
		}

		uint32_t killAmount = player->getBestiaryKillCount(raceId);
		msg.add<uint16_t>(raceId);
		msg.add<uint32_t>(killAmount);
		bool completed = false;
		if (isBoss) {
			for (const auto &stage : stages) {
				msg.add<uint16_t>(static_cast<uint16_t>(stage.kills));
			}
			completed = g_ioBosstiary().getBossCurrentLevel(player, raceId) == 3;
		} else {
			msg.add<uint16_t>(mtype->info.bestiaryFirstUnlock);
			msg.add<uint16_t>(mtype->info.bestiarySecondUnlock);
			msg.add<uint16_t>(mtype->info.bestiaryToUnlock);
			completed = g_iobestiary().getKillStatus(mtype, killAmount) == 4;
		}

		if (completed) {
			msg.addByte(4);
		} else {
			msg.addByte(0);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendBestiaryCharms() {
	if (!player || oldProtocol) {
		return;
	}

	const auto playerLevel = player->getLevel();
	uint64_t resetAllCharmsCost = 100000 + (playerLevel > 100 ? playerLevel * 11000 : 0);
	if (player->hasCharmExpansion()) {
		resetAllCharmsCost = (resetAllCharmsCost * 75) / 100;
	}

	NetworkMessage msg;
	msg.addByte(0xD8);
	msg.add<uint64_t>(resetAllCharmsCost);

	const auto &charmList = g_game().getCharmList();
	msg.addByte(charmList.size());
	for (const auto &c_type : charmList) {
		msg.addByte(c_type->id);
		if (g_iobestiary().hasCharmUnlockedRuneBit(c_type, player->getUnlockedRunesBit())) {
			const auto charmTier = player->getCharmTier(c_type->id);
			msg.addByte(charmTier);
			uint16_t raceId = player->parseRacebyCharm(c_type->id);
			if (raceId > 0) {
				msg.addByte(0x01);
				msg.add<uint16_t>(raceId);
				uint32_t removeCharmCost = player->getLevel() * 100;
				if (player->hasCharmExpansion()) {
					removeCharmCost = (removeCharmCost * 75) / 100;
				}
				msg.add<uint32_t>(removeCharmCost);
			} else {
				msg.addByte(0x00);
			}
		} else {
			msg.addByte(0x00);
			msg.addByte(0x00);
		}
	}

	std::list<charmRune_t> usedCharms = g_iobestiary().getCharmUsedRuneBitAll(player);
	uint8_t availableCharmSlots;
	if (player->isPremium() && player->hasCharmExpansion()) {
		availableCharmSlots = 0xFF;
	} else {
		uint8_t totalCharmSlots = player->isPremium() ? 6 : 2;
		availableCharmSlots = totalCharmSlots - usedCharms.size();
	}

	msg.addByte(availableCharmSlots);

	auto finishedMonstersSet = g_iobestiary().getBestiaryStageTwo(player);
	std::unordered_map<uint16_t, uint8_t> charmsAssigned;
	for (charmRune_t charmRune : usedCharms) {
		const auto &tmpCharm = g_iobestiary().getBestiaryCharm(charmRune);
		if (!tmpCharm) {
			continue;
		}

		uint16_t tmpRaceId = player->parseRacebyCharm(tmpCharm->id);
		charmsAssigned[tmpRaceId]++;
	}

	for (const auto &[raceId, amount] : charmsAssigned) {
		if (amount >= 2) {
			std::erase(finishedMonstersSet, raceId);
		}
	}

	msg.add<uint16_t>(finishedMonstersSet.size());
	for (uint16_t tmpRaceId : finishedMonstersSet) {
		msg.add<uint32_t>(tmpRaceId);
	}

	writeToOutputBuffer(msg);
	sendCharmResourcesBalance(
		player->getCharmPoints(),
		player->getMinorCharmEchoes(),
		player->getMaxCharmPoints(),
		player->getMaxMinorCharmEchoes()
	);
}

void ProtocolGame::parseBestiarySendCreatures(NetworkMessage &msg) {
	if (!player || oldProtocol) {
		return;
	}

	std::ostringstream ss;
	std::map<uint16_t, std::string> race = {};
	std::string text;
	uint8_t search = msg.getByte();

	if (search == 1) {
		auto monsterAmount = msg.get<uint16_t>();
		const std::map<uint16_t, std::string> &mtype_list = g_game().getBestiaryList();
		for (uint16_t monsterCount = 1; monsterCount <= monsterAmount; monsterCount++) {
			auto raceid = msg.get<uint16_t>();
			if (player->getBestiaryKillCount(raceid) > 0) {
				auto it = mtype_list.find(raceid);
				if (it != mtype_list.end()) {
					[[maybe_unused]] auto [it_race, inserted_race] = race.try_emplace(raceid, it->second);
				}
			}
		}
	} else {
		std::string raceName = msg.getString();
		race = g_iobestiary().findRaceByName(raceName);

		if (race.empty()) {
			g_logger().warn("[ProtocolGame::parseBestiarysendCreature] - "
			                "Race was not found: {}, search: {}",
			                raceName, search);
			return;
		}
		text = raceName;
	}
	NetworkMessage newmsg;
	newmsg.addByte(0xD6);
	newmsg.addString(text);
	newmsg.add<uint16_t>(race.size());
	std::map<uint16_t, uint32_t> creaturesKilled = g_iobestiary().getBestiaryKillCountByMonsterIDs(player, race);

	for (const auto &it_ : race) {
		uint16_t raceid_ = it_.first;
		newmsg.add<uint16_t>(raceid_);

		uint8_t progress = 0;
		uint8_t occurrence = 0;
		for (const auto &_it : creaturesKilled) {
			if (_it.first == raceid_) {
				const auto tmpType = g_monsters().getMonsterType(it_.second);
				if (!tmpType) {
					return;
				}
				progress = g_iobestiary().getKillStatus(tmpType, _it.second);
				occurrence = tmpType->info.bestiaryOccurrence;
			}
		}

		if (progress > 0) {
			newmsg.addByte(progress);
			newmsg.addByte(occurrence);
		} else {
			newmsg.addByte(0);
		}

		const auto monsterType = g_monsters().getMonsterType(it_.second);
		if (monsterType && player->animusMastery().has(it_.second)) {
			newmsg.add<uint16_t>(static_cast<uint16_t>(std::round((player->animusMastery().getExperienceMultiplier() - 1) * 1000))); // Animus Mastery Bonus
		} else {
			newmsg.add<uint16_t>(0);
		}
	}

	newmsg.add<uint16_t>(player->animusMastery().getPoints()); // Animus Mastery Points

	writeToOutputBuffer(newmsg);
}

void ProtocolGame::parseBugReport(NetworkMessage &msg) {
	uint8_t category = msg.getByte();
	std::string message = msg.getString();

	Position position;
	if (category == BUG_CATEGORY_MAP) {
		position = msg.getPosition();
	}

	g_game().playerReportBug(player->getID(), message, position, category);
}

void ProtocolGame::parseGreet(NetworkMessage &msg) {
	auto npcId = msg.get<uint32_t>();
	g_game().playerNpcGreet(player->getID(), npcId);
}

void ProtocolGame::parseOfferDescription(NetworkMessage &msg) {
	auto offerId = msg.get<uint32_t>();
	g_logger().debug("[{}] offer id: {}", __FUNCTION__, offerId);
}

void ProtocolGame::parsePreyAction(NetworkMessage &msg) {
	int8_t index = -1;
	uint8_t slot = msg.getByte();
	uint8_t action = msg.getByte();
	uint8_t option = 0;
	uint16_t raceId = 0;
	if (action == static_cast<uint8_t>(PreyAction_MonsterSelection)) {
		index = msg.getByte();
	} else if (action == static_cast<uint8_t>(PreyAction_Option)) {
		option = msg.getByte();
	} else if (action == static_cast<uint8_t>(PreyAction_ListAll_Selection)) {
		raceId = msg.get<uint16_t>();
	}

	if (!g_configManager().getBoolean(PREY_ENABLED)) {
		return;
	}

	g_game().playerPreyAction(player->getID(), slot, action, option, index, raceId);
}

void ProtocolGame::parseSendResourceBalance() {
	auto [sliverCount, coreCount] = player->getForgeSliversAndCores();

	sendResourcesBalance(
		player->getMoney(),
		player->getBankBalance(),
		player->getPreyCards(),
		player->getTaskHuntingPoints(),
		player->getForgeDusts(),
		sliverCount,
		coreCount
	);

	sendCharmResourcesBalance(
		player->getCharmPoints(),
		player->getMinorCharmEchoes(),
		player->getMaxCharmPoints(),
		player->getMaxMinorCharmEchoes()
	);
}

void ProtocolGame::parseSendResourceBalance(NetworkMessage &msg) {
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		msg.getByte(true);
	}
	parseSendResourceBalance();
}

void ProtocolGame::parseGetTransactionDetails(NetworkMessage &msg) {
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		static_cast<void>(msg.get<uint32_t>());
	}
}

void ProtocolGame::parseGetObjectInfo(NetworkMessage &msg) {
	struct ObjectInfoEntry {
		uint16_t id = 0;
		uint8_t data = 0;
		std::string name;
	};

	const auto count = msg.getByte(true);
	std::vector<ObjectInfoEntry> objects;
	objects.reserve(count);
	for (uint8_t index = 0; index < count && msg.canRead(3); ++index) {
		const auto id = msg.get<uint16_t>();
		const auto data = msg.getByte(true);
		objects.push_back(ObjectInfoEntry { id, data, Item::items.hasItemType(id) ? Item::items[id].name : std::string() });
	}

	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload) || objects.empty()) {
		return;
	}

	NetworkMessage response;
	response.addByte(0xF4);
	response.addByte(static_cast<uint8_t>(objects.size()));
	for (const auto &object : objects) {
		response.add<uint16_t>(object.id);
		response.addByte(object.data);
		response.addString(object.name);
	}
	writeToOutputBuffer(response);
}

void ProtocolGame::parseInviteToParty(NetworkMessage &msg) {
	auto targetId = msg.get<uint32_t>();
	g_game().playerInviteToParty(player->getID(), targetId);
}

void ProtocolGame::parseJoinParty(NetworkMessage &msg) {
	auto targetId = msg.get<uint32_t>();
	g_game().playerJoinParty(player->getID(), targetId);
}

void ProtocolGame::parseRevokePartyInvite(NetworkMessage &msg) {
	auto targetId = msg.get<uint32_t>();
	g_game().playerRevokePartyInvitation(player->getID(), targetId);
}

void ProtocolGame::parsePassPartyLeadership(NetworkMessage &msg) {
	auto targetId = msg.get<uint32_t>();
	g_game().playerPassPartyLeadership(player->getID(), targetId);
}

void ProtocolGame::parseEnableSharedPartyExperience(NetworkMessage &msg) {
	bool sharedExpActive = msg.getByte() == 1;
	g_game().playerEnableSharedPartyExperience(player->getID(), sharedExpActive);
}

void ProtocolGame::parseQuestLine(NetworkMessage &msg) {
	auto questId = msg.get<uint16_t>();
	g_game().playerShowQuestLine(player->getID(), questId);
}

void ProtocolGame::parseMarketLeave() {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		return;
	}

	g_game().playerLeaveMarket(player->getID());
}

void ProtocolGame::parseMarketBrowse(NetworkMessage &msg) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		return;
	}

	uint16_t browseId = oldProtocol ? msg.get<uint16_t>() : static_cast<uint16_t>(msg.getByte());

	if ((oldProtocol && browseId == MARKETREQUEST_OWN_OFFERS_OLD) || (!oldProtocol && browseId == MARKETREQUEST_OWN_OFFERS)) {
		g_game().playerBrowseMarketOwnOffers(player->getID());
	} else if ((oldProtocol && browseId == MARKETREQUEST_OWN_HISTORY_OLD) || (!oldProtocol && browseId == MARKETREQUEST_OWN_HISTORY)) {
		g_game().playerBrowseMarketOwnHistory(player->getID());
	} else if (!oldProtocol) {
		auto itemId = msg.get<uint16_t>();
		uint8_t tier = 0;
		if (Item::items[itemId].upgradeClassification > 0) {
			tier = msg.get<uint8_t>();
		}
		player->sendMarketEnter(player->getLastDepotId());
		g_game().playerBrowseMarket(player->getID(), itemId, tier);
	} else {
		g_game().playerBrowseMarket(player->getID(), browseId, 0);
	}
}

void ProtocolGame::parseMarketCreateOffer(NetworkMessage &msg) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		return;
	}

	uint8_t type = msg.getByte();
	auto itemId = msg.get<uint16_t>();
	uint8_t itemTier = 0;
	if (!oldProtocol && Item::items[itemId].upgradeClassification > 0) {
		itemTier = msg.getByte();
	}

	auto amount = msg.get<uint16_t>();
	uint64_t price = oldProtocol ? static_cast<uint64_t>(msg.get<uint32_t>()) : msg.get<uint64_t>();
	bool anonymous = (msg.getByte(true) != 0);
	if (amount > 0 && price > 0) {
		g_game().playerCreateMarketOffer(player->getID(), type, itemId, amount, price, itemTier, anonymous);
	}
}

void ProtocolGame::parseMarketCancelOffer(NetworkMessage &msg) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		return;
	}

	auto timestamp = msg.get<uint32_t>();
	auto counter = msg.get<uint16_t>();
	if (counter > 0) {
		g_game().playerCancelMarketOffer(player->getID(), timestamp, counter);
	}

	updateCoinBalance();
}

void ProtocolGame::parseMarketAcceptOffer(NetworkMessage &msg) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		return;
	}

	auto timestamp = msg.get<uint32_t>();
	auto counter = msg.get<uint16_t>();
	auto amount = msg.get<uint16_t>();
	if (amount > 0 && counter > 0) {
		g_game().playerAcceptMarketOffer(player->getID(), timestamp, counter, amount);
	}

	updateCoinBalance();
}

void ProtocolGame::parseModalWindowAnswer(NetworkMessage &msg) {
	auto id = msg.get<uint32_t>();
	uint8_t button = msg.getByte();
	uint8_t choice = msg.getByte();
	g_game().playerAnswerModalWindow(player->getID(), id, button, choice);
}

void ProtocolGame::parseRewardChestCollect(NetworkMessage &msg) {
	const auto position = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	auto stackPosition = msg.getByte();

	// Block collect reward
	auto useCollect = g_configManager().getBoolean(REWARD_CHEST_COLLECT_ENABLED);
	if (!useCollect) {
		return;
	}

	auto maxCollectItems = g_configManager().getNumber(REWARD_CHEST_MAX_COLLECT_ITEMS);
	g_game().playerRewardChestCollect(player->getID(), position, itemId, stackPosition, maxCollectItems);
}

void ProtocolGame::parseBrowseField(NetworkMessage &msg) {
	const Position &pos = msg.getPosition();
	g_game().playerBrowseField(player->getID(), pos);
}

void ProtocolGame::parseSeekInContainer(NetworkMessage &msg) {
	uint8_t containerId = msg.getByte();
	auto index = msg.get<uint16_t>();
	auto primaryType = msg.getByte();
	g_game().playerSeekInContainer(player->getID(), containerId, index, primaryType);
}

// Send methods
void ProtocolGame::sendOpenPrivateChannel(const std::string &receiver) {
	NetworkMessage msg;
	msg.addByte(0xAD);
	msg.addString(receiver);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendExperienceTracker(int64_t rawExp, int64_t finalExp) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xAF);
	msg.add<int64_t>(rawExp);
	msg.add<int64_t>(finalExp);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendChannelEvent(uint16_t channelId, const std::string &playerName, ChannelEvent_t channelEvent) {
	NetworkMessage msg;
	msg.addByte(0xF3);
	msg.add<uint16_t>(channelId);
	msg.addString(playerName);
	msg.addByte(channelEvent);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureOutfit(const std::shared_ptr<Creature> &creature, const Outfit_t &outfit) {
	if (!canSee(creature)) {
		return;
	}

	Outfit_t newOutfit = outfit;
	if (player->isWearingSupportOutfit()) {
		player->setCurrentMount(0);
		newOutfit.lookMount = 0;
	}

	NetworkMessage msg;
	msg.addByte(0x8E);
	msg.add<uint32_t>(creature->getID());
	AddOutfit(msg, newOutfit);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureLight(const std::shared_ptr<Creature> &creature) {
	if (!canSee(creature)) {
		return;
	}

	NetworkMessage msg;
	AddCreatureLight(msg, creature);
	writeToOutputBuffer(msg);
}

void ProtocolGame::addCreatureIcon(NetworkMessage &msg, const std::shared_ptr<Creature> &creature) {
	if (!creature || !player || oldProtocol) {
		return;
	}

	const auto icons = creature->getIcons();
	// client only supports 3 icons, otherwise it will crash
	const auto count = icons.size() > 3 ? 3 : icons.size();
	msg.addByte(count);
	for (uint8_t i = 0; i < count; ++i) {
		const auto icon = icons[i];
		msg.addByte(icon.serialize());
		msg.addByte(static_cast<uint8_t>(icon.category));
		msg.add<uint16_t>(icon.count);
	}
}

void ProtocolGame::sendCreatureIcon(const std::shared_ptr<Creature> &creature) {
	if (!creature || !player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8B);
	msg.add<uint32_t>(creature->getID());
	// Type 14 for this
	msg.addByte(14);
	addCreatureIcon(msg, creature);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendWorldLight(const LightInfo &lightInfo) {
	NetworkMessage msg;
	AddWorldLight(msg, lightInfo);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTibiaTime(int32_t time) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xEF);
	msg.addByte(time / 60);
	msg.addByte(time % 60);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureWalkthrough(const std::shared_ptr<Creature> &creature, bool walkthrough) {
	if (!canSee(creature)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x92);
	msg.add<uint32_t>(creature->getID());
	msg.addByte(walkthrough ? 0x00 : 0x01);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureShield(const std::shared_ptr<Creature> &creature) {
	if (!canSee(creature)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x91);
	msg.add<uint32_t>(creature->getID());
	msg.addByte(player->getPartyShield(creature->getPlayer()));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureEmblem(const std::shared_ptr<Creature> &creature) {
	if (!creature || !canSee(creature) || oldProtocol) {
		return;
	}

	auto tile = creature->getTile();
	if (!tile) {
		return;
	}

	// Remove creature from client and re-add to update
	Position pos = creature->getPosition();
	int32_t stackpos = tile->getClientIndexOfCreature(player, creature);
	sendRemoveTileThing(pos, stackpos);
	NetworkMessage msg;
	msg.addByte(0x6A);
	msg.addPosition(pos);
	msg.addByte(static_cast<uint8_t>(stackpos));
	AddCreature(msg, creature, false, creature->getID());
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureSkull(const std::shared_ptr<Creature> &creature) {
	if (g_game().getWorldType() != WORLD_TYPE_PVP) {
		return;
	}

	if (!canSee(creature)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x90);
	msg.add<uint32_t>(creature->getID());
	msg.addByte(player->getSkullClient(creature));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureType(const std::shared_ptr<Creature> &creature, uint8_t creatureType) {
	NetworkMessage msg;
	msg.addByte(0x95);
	msg.add<uint32_t>(creature->getID());
	if (creatureType == CREATURETYPE_SUMMON_OTHERS) {
		creatureType = CREATURETYPE_SUMMON_PLAYER;
	}
	msg.addByte(creatureType); // type or any byte idk
	if (!oldProtocol && creatureType == CREATURETYPE_SUMMON_PLAYER) {
		std::shared_ptr<Creature> master = creature->getMaster();
		if (master) {
			msg.add<uint32_t>(master->getID());
		} else {
			msg.add<uint32_t>(0);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureSquare(const std::shared_ptr<Creature> &creature, SquareColor_t color) {
	if (!canSee(creature)) {
		return;
	}

	NetworkMessage msg;
	if (isCipsoft860Profile(protocolProfile)) {
		msg.addByte(0x86);
		msg.add<uint32_t>(creature->getID());
		msg.addByte(color);
	} else {
		msg.addByte(0x93);
		msg.add<uint32_t>(creature->getID());
		msg.addByte(0x01);
		msg.addByte(color);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTutorial(uint8_t tutorialId) {
	NetworkMessage msg;
	msg.addByte(0xDC);
	msg.addByte(tutorialId);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAddMarker(const Position &pos, uint8_t markType, const std::string &desc) {
	NetworkMessage msg;
	msg.addByte(0xDD);

	if (!oldProtocol) {
		msg.addByte(enumToValue(CyclopediaMapData_t::MinimapMarker));
	}

	msg.addPosition(pos);
	msg.addByte(markType);
	msg.addString(desc);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterNoData(CyclopediaCharacterInfoType_t characterInfoType, uint8_t errorCode) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(static_cast<uint8_t>(characterInfoType));
	msg.addByte(errorCode);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterBaseInformation() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_BASEINFORMATION);
	msg.addByte(0x00);
	msg.addString(player->getName());
	msg.addString(player->getVocation()->getVocName());
	msg.add<uint16_t>(player->getLevel());
	AddOutfit(msg, player->getDefaultOutfit(), false);

	msg.addByte(0x01); // Store summary & Character titles
	msg.addString(player->title().getCurrentTitleName()); // character title
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterGeneralStats() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_GENERALSTATS);
	// Send no error
	// 1: No data available at the moment.
	// 2: You are not allowed to see this character's data.
	// 3: You are not allowed to inspect this character.
	msg.addByte(0x00); // 0x00 Here means 'no error'

	msg.add<uint64_t>(player->getExperience());
	msg.add<uint16_t>(player->getLevel());
	const auto levelPercent = std::min<uint16_t>(static_cast<uint16_t>(player->getLevelPercent() * 100), 10000);
	msg.add<uint16_t>(levelPercent);
	msg.add<uint16_t>(player->getBaseXpGain()); // BaseXPGainRate
	msg.add<uint16_t>(player->getDisplayGrindingXpBoost()); // LowLevelBonus
	msg.add<uint16_t>(player->getDisplayXpBoostPercent()); // XPBoost
	msg.add<uint16_t>(player->getStaminaXpBoost()); // StaminaMultiplier(100=x1.0)
	msg.add<uint16_t>(player->getXpBoostTime()); // xpBoostRemainingTime
	msg.addByte(player->getXpBoostTime() > 0 ? 0x00 : 0x01); // canBuyXpBoost
	msg.add<uint32_t>(std::min<int32_t>(player->getHealth(), std::numeric_limits<uint16_t>::max()));
	msg.add<uint32_t>(std::min<int32_t>(player->getMaxHealth(), std::numeric_limits<uint16_t>::max()));
	msg.add<uint32_t>(std::min<int32_t>(player->getMana(), std::numeric_limits<uint16_t>::max()));
	msg.add<uint32_t>(std::min<int32_t>(player->getMaxMana(), std::numeric_limits<uint16_t>::max()));
	msg.addByte(player->getSoul());
	msg.add<uint16_t>(player->getStaminaMinutes());

	std::shared_ptr<Condition> condition = player->getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT);
	msg.add<uint16_t>(condition ? condition->getTicks() / 1000 : 0x00);
	msg.add<uint16_t>(player->getOfflineTrainingTime() / 60 / 1000);
	msg.add<uint16_t>(player->getSpeed());
	msg.add<uint16_t>(player->getBaseSpeed());
	msg.add<uint32_t>(player->getCapacity());
	msg.add<uint32_t>(player->getBaseCapacity());
	msg.add<uint32_t>(player->hasFlag(PlayerFlags_t::HasInfiniteCapacity) ? 1000000 : player->getFreeCapacity());
	msg.addByte(8);
	msg.addByte(1);
	msg.add<uint16_t>(player->getMagicLevel());
	msg.add<uint16_t>(player->getBaseMagicLevel());
	msg.add<uint16_t>(player->getLoyaltyMagicLevel());
	msg.add<uint16_t>(player->getMagicLevelPercent() * 100);

	for (uint8_t i = SKILL_FIRST; i < SKILL_CRITICAL_HIT_CHANCE; ++i) {
		static const uint8_t HardcodedSkillIds[] = { 11, 9, 8, 10, 7, 6, 13 };
		const auto skill = static_cast<skills_t>(i);
		msg.addByte(HardcodedSkillIds[i]);
		msg.add<uint16_t>(std::min<int32_t>(player->getSkillLevel(skill), std::numeric_limits<uint16_t>::max()));
		msg.add<uint16_t>(player->getBaseSkill(skill));
		msg.add<uint16_t>(player->getLoyaltySkill(skill));
		msg.add<uint16_t>(player->getSkillPercent(skill) * 100);
	}

	auto bufferPosition = msg.getBufferPosition();
	msg.skipBytes(1);
	uint8_t total = 0;
	for (size_t i = 0; i < COMBAT_COUNT; i++) {
		auto specializedMagicLevel = player->getSpecializedMagicLevel(indexToCombatType(i));
		if (specializedMagicLevel > 0) {
			++total;
			msg.addByte(getCipbiaElement(indexToCombatType(i)));
			msg.add<uint16_t>(specializedMagicLevel);
		}
	}
	msg.setBufferPosition(bufferPosition);
	msg.addByte(total);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t pages, const std::vector<RecentDeathEntry> &entries) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS);
	msg.addByte(0x00); // 0x00 Here means 'no error'
	msg.add<uint16_t>(page);
	msg.add<uint16_t>(pages);
	msg.add<uint16_t>(entries.size());
	for (const RecentDeathEntry &entry : entries) {
		msg.add<uint32_t>(entry.timestamp);
		msg.addString(entry.cause);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t pages, const std::vector<RecentPvPKillEntry> &entries) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS);
	msg.addByte(0x00); // 0x00 Here means 'no error'
	msg.add<uint16_t>(page);
	msg.add<uint16_t>(pages);
	msg.add<uint16_t>(entries.size());
	for (const RecentPvPKillEntry &entry : entries) {
		msg.add<uint32_t>(entry.timestamp);
		msg.addString(entry.description);
		msg.addByte(entry.status);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterAchievements(uint16_t secretsUnlocked, const std::vector<std::pair<Achievement, uint32_t>> &achievementsUnlocked) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_ACHIEVEMENTS);
	msg.addByte(0x00); // 0x00 Here means 'no error'
	msg.add<uint16_t>(player->achiev().getPoints());
	msg.add<uint16_t>(secretsUnlocked);
	msg.add<uint16_t>(static_cast<uint16_t>(achievementsUnlocked.size()));
	for (const auto &[achievement, addedTimestamp] : achievementsUnlocked) {
		msg.add<uint16_t>(achievement.id);
		msg.add<uint32_t>(addedTimestamp);
		if (achievement.secret) {
			msg.addByte(0x01);
			msg.addString(achievement.name);
			msg.addString(achievement.description);
			msg.addByte(achievement.grade);
		} else {
			msg.addByte(0x00);
		}
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterItemSummary(const ItemsTierCountList &inventoryItems, const ItemsTierCountList &storeInboxItems, const StashItemList &stashItems, const ItemsTierCountList &depotBoxItems, const ItemsTierCountList &inboxItems) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_ITEMSUMMARY);
	msg.addByte(0x00); // 0x00 Here means 'no error'

	auto writeItemList = [&](const ItemsTierCountList &items, uint16_t &itemCount) {
		const auto startPosition = msg.getBufferPosition();
		msg.skipBytes(2);

		for (const auto &[key, count] : items) {
			const auto &[itemID, tier] = key;

			const ItemType &it = Item::items[itemID];
			msg.add<uint16_t>(itemID);

			if (it.upgradeClassification > 0) {
				msg.addByte(tier);
			}

			msg.add<uint32_t>(count);
			++itemCount;
		}

		const auto endPosition = msg.getBufferPosition();
		msg.setBufferPosition(startPosition);
		msg.add<uint16_t>(itemCount);
		msg.setBufferPosition(endPosition);
	};

	// Inventory Items
	uint16_t inventoryItemsCount = 0;
	writeItemList(inventoryItems, inventoryItemsCount);

	// Store Inbox Items
	uint16_t storeInboxItemsCount = 0;
	writeItemList(storeInboxItems, storeInboxItemsCount);

	// Supply Stash Items
	msg.add<uint16_t>(stashItems.size());
	for (const auto &[itemId, itemCount] : stashItems) {
		msg.add<uint16_t>(itemId); // Item ID
		const ItemType &it = Item::items[itemId];
		if (it.upgradeClassification > 0) {
			msg.addByte(0x00);
		}
		msg.add<uint32_t>(itemCount); // Item count
	}

	// Depot Box Items
	uint16_t depotBoxItemsCount = 0;
	writeItemList(depotBoxItems, depotBoxItemsCount);

	// Inbox Items
	uint16_t inboxItemsCount = 0;
	writeItemList(inboxItems, inboxItemsCount);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterOutfitsMounts() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITSMOUNTS);
	msg.addByte(0x00);
	Outfit_t currentOutfit = player->getDefaultOutfit();

	uint16_t outfitSize = 0;
	auto startOutfits = msg.getBufferPosition();
	msg.skipBytes(2);

	const auto outfits = Outfits::getInstance().getOutfits(player->getSex());
	for (const auto &outfit : outfits) {
		uint8_t addons;
		if (!player->getOutfitAddons(outfit, addons)) {
			continue;
		}
		const std::string from = outfit->from;
		++outfitSize;

		msg.add<uint16_t>(outfit->lookType);
		msg.addString(outfit->name);
		msg.addByte(addons);
		if (from == "store") {
			msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_STORE);
		} else if (from == "quest") {
			msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_QUEST);
		} else {
			msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_NONE);
		}
		if (outfit->lookType == currentOutfit.lookType) {
			msg.add<uint32_t>(1000);
		} else {
			msg.add<uint32_t>(0);
		}
	}
	if (outfitSize > 0) {
		msg.addByte(currentOutfit.lookHead);
		msg.addByte(currentOutfit.lookBody);
		msg.addByte(currentOutfit.lookLegs);
		msg.addByte(currentOutfit.lookFeet);
	}

	uint16_t mountSize = 0;
	auto startMounts = msg.getBufferPosition();
	msg.skipBytes(2);
	for (const auto &mount : g_game().mounts->getMounts()) {
		const std::string type = mount->type;
		if (player->hasMount(mount)) {
			++mountSize;

			msg.add<uint16_t>(mount->clientId);
			msg.addString(mount->name);
			if (type == "store") {
				msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_STORE);
			} else if (type == "quest") {
				msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_QUEST);
			} else {
				msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_NONE);
			}
			msg.add<uint32_t>(1000);
		}
	}
	if (mountSize > 0) {
		msg.addByte(currentOutfit.lookMountHead);
		msg.addByte(currentOutfit.lookMountBody);
		msg.addByte(currentOutfit.lookMountLegs);
		msg.addByte(currentOutfit.lookMountFeet);
	}

	uint16_t familiarsSize = 0;
	auto startFamiliars = msg.getBufferPosition();
	msg.skipBytes(2);
	const auto familiars = Familiars::getInstance().getFamiliars(player->getVocationId());
	for (const auto &familiar : familiars) {
		const std::string type = familiar->type;
		if (!player->getFamiliar(familiar)) {
			continue;
		}
		++familiarsSize;
		msg.add<uint16_t>(familiar->lookType);
		msg.addString(familiar->name);
		if (type == "quest") {
			msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_QUEST);
		} else {
			msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_NONE);
		}
		msg.add<uint32_t>(0);
	}

	msg.setBufferPosition(startOutfits);
	msg.add<uint16_t>(outfitSize);
	msg.setBufferPosition(startMounts);
	msg.add<uint16_t>(mountSize);
	msg.setBufferPosition(startFamiliars);
	msg.add<uint16_t>(familiarsSize);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterStoreSummary() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_STORESUMMARY);
	msg.addByte(0x00); // 0x00 Here means 'no error'
	msg.add<uint32_t>(player->getXpBoostTime()); // Remaining Store Xp Boost Time
	auto remaining = player->kv()->get("daily-reward-xp-boost");
	msg.add<uint32_t>(remaining ? static_cast<uint32_t>(remaining->getNumber()) : 0); // Remaining Daily Reward Xp Boost Time

	auto cyclopediaSummary = player->cyclopedia().getSummary();

	msg.addByte(static_cast<uint8_t>(magic_enum::enum_count<Blessings>()));
	for (auto bless : magic_enum::enum_values<Blessings>()) {
		std::string name = toStartCaseWithSpace(magic_enum::enum_name(bless).data());
		msg.addString(name);
		auto blessValue = enumToValue(bless);
		if (player->hasBlessing(blessValue)) {
			msg.addByte(static_cast<uint16_t>(player->blessings[blessValue - 1]));
		} else {
			msg.addByte(0x00);
		}
	}

	uint8_t preySlotsUnlocked = 0;
	// Prey third slot unlocked
	if (const auto &slotP = player->getPreySlotById(PreySlot_Three);
	    slotP && slotP->state != PreyDataState_Locked) {
		preySlotsUnlocked++;
	}
	// Task hunting third slot unlocked
	if (const auto &slotH = player->getTaskHuntingSlotById(PreySlot_Three);
	    slotH && slotH->state != PreyTaskDataState_Locked) {
		preySlotsUnlocked++;
	}
	msg.addByte(preySlotsUnlocked); // getPreySlotById + getTaskHuntingSlotById

	msg.addByte(cyclopediaSummary.m_preyWildcards); // getPreyCardsObtained
	msg.addByte(cyclopediaSummary.m_instantRewards); // getRewardCollectionObtained
	msg.addByte(player->hasCharmExpansion() ? 0x01 : 0x00);
	msg.addByte(cyclopediaSummary.m_hirelings); // getHirelingsObtained
	msg.addByte(0x00); // Reserved current-client store summary field

	std::vector<uint16_t> m_hSkills;
	for (const auto &[skillId, skillName] : g_game().getHirelingSkills()) {
		if (player->kv()->scoped("hireling-skills")->get(skillName)) {
			[[maybe_unused]] auto &skill_ref = m_hSkills.emplace_back(skillId);
			g_logger().debug("skill id: {}, name: {}", skillId, skillName);
		}
	}
	msg.addByte(m_hSkills.size());
	for (const auto &id : m_hSkills) {
		msg.addByte(id - 1000);
	}

	/*std::vector<uint16_t> m_hOutfits;
	for (const auto &it : g_game().getHirelingOutfits()) {
	    if (player->kv()->scoped("hireling-outfits")->get(it.second)) {
	        m_hOutfits.emplace_back(it.first);
	        g_logger().debug("outfit id: {}, name: {}", it.first, it.second);
	    }
	}
	msg.addByte(m_hOutfits.size());
	for (const auto &id : m_hOutfits) {
	    msg.addByte(0x01); // TODO need to get the correct id from hireling outfit
	}*/
	msg.addByte(0x00); // hireling outfit size

	auto houseItems = player->cyclopedia().getResult(static_cast<uint8_t>(Summary_t::HOUSE_ITEMS));
	msg.add<uint16_t>(houseItems.size());
	for (const auto &[itemId, count] : houseItems) {
		const ItemType &it = Item::items[itemId];
		msg.add<uint16_t>(it.id); // Item ID
		msg.addString(it.name);
		msg.addByte(count);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterInspection() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_INSPECTION);
	msg.addByte(0x00);
	uint8_t inventoryItems = 0;
	auto startInventory = msg.getBufferPosition();
	msg.skipBytes(1);
	for (std::underlying_type<Slots_t>::type slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; slot++) {
		std::shared_ptr<Item> inventoryItem = player->getInventoryItem(static_cast<Slots_t>(slot));
		if (inventoryItem) {
			++inventoryItems;

			msg.addByte(slot);
			msg.addString(inventoryItem->getName());
			AddItem(msg, inventoryItem);

			uint8_t itemImbuements = 0;
			auto startImbuements = msg.getBufferPosition();
			msg.skipBytes(1);
			for (uint8_t slotid = 0; slotid < inventoryItem->getImbuementSlot(); slotid++) {
				ImbuementInfo imbuementInfo;
				if (!inventoryItem->getImbuementInfo(slotid, &imbuementInfo)) {
					continue;
				}

				msg.add<uint16_t>(imbuementInfo.imbuement->getIconID());
				itemImbuements++;
			}

			auto endImbuements = msg.getBufferPosition();
			msg.setBufferPosition(startImbuements);
			msg.addByte(itemImbuements);
			msg.setBufferPosition(endImbuements);

			auto descriptions = Item::getDescriptions(Item::items[inventoryItem->getID()], inventoryItem);
			msg.addByte(descriptions.size());
			for (const auto &description : descriptions) {
				msg.addString(description.first);
				msg.addString(description.second);
			}
		}
	}
	msg.addString(player->getName());
	AddOutfit(msg, player->getDefaultOutfit(), false);

	// Player overall summary
	uint8_t playerDescriptionSize = 0;
	auto playerDescriptionPosition = msg.getBufferPosition();
	msg.skipBytes(1);

	// Player title
	if (player->title().getCurrentTitle() != 0) {
		playerDescriptionSize++;
		msg.addString("Character Title");
		msg.addString(player->title().getCurrentTitleName());
	}

	// Level description
	playerDescriptionSize++;
	msg.addString("Level");
	msg.addString(std::to_string(player->getLevel()));

	// Vocation description
	playerDescriptionSize++;
	msg.addString("Vocation");
	msg.addString(player->getVocation()->getVocName());

	// Loyalty title
	if (!player->getLoyaltyTitle().empty()) {
		playerDescriptionSize++;
		msg.addString("Loyalty Title");
		msg.addString(player->getLoyaltyTitle());
	}

	// Marriage description
	if (const auto spouseId = player->getMarriageSpouse(); spouseId > 0) {
		if (const auto &spouse = g_game().getPlayerByID(spouseId, true); spouse) {
			playerDescriptionSize++;
			msg.addString("Married to");
			msg.addString(spouse->getName());
		}
	}

	// Prey description
	for (uint8_t slotId = PreySlot_First; slotId <= PreySlot_Last; slotId++) {
		if (const auto &slot = player->getPreySlotById(static_cast<PreySlot_t>(slotId));
		    slot && slot->isOccupied()) {
			playerDescriptionSize++;
			std::string activePrey = fmt::format("Active Prey {}", slotId + 1);
			msg.addString(activePrey);

			std::string desc;
			if (auto mtype = g_monsters().getMonsterTypeByRaceId(slot->selectedRaceId)) {
				desc.append(mtype->name);
			} else {
				desc.append("Unknown creature");
			}

			if (slot->bonus == PreyBonus_Damage) {
				desc.append(" (Improved Damage +");
			} else if (slot->bonus == PreyBonus_Defense) {
				desc.append(" (Improved Defense +");
			} else if (slot->bonus == PreyBonus_Experience) {
				desc.append(" (Improved Experience +");
			} else if (slot->bonus == PreyBonus_Loot) {
				desc.append(" (Improved Loot +");
			}
			desc.append(fmt::format("{}%, remaining", slot->bonusPercentage));
			uint8_t hours = slot->bonusTimeLeft / 3600;
			uint8_t minutes = (slot->bonusTimeLeft - (hours * 3600)) / 60;
			desc.append(fmt::format("{}:{}{}h", hours, (minutes < 10 ? "0" : ""), minutes));
			msg.addString(desc);
		}
	}

	// Outfit description
	playerDescriptionSize++;
	msg.addString("Outfit");
	if (const auto outfit = Outfits::getInstance().getOutfitByLookType(player, player->getDefaultOutfit().lookType)) {
		msg.addString(outfit->name);
	} else {
		msg.addString("unknown");
	}

	msg.setBufferPosition(startInventory);
	msg.addByte(inventoryItems);

	msg.setBufferPosition(playerDescriptionPosition);
	msg.addByte(playerDescriptionSize);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterBadges() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_BADGES);
	msg.addByte(0x00);
	msg.addByte(0x01); // ShowAccountInformation, if 0x01 will show IsOnline, IsPremium, character title, badges

	const auto loggedPlayer = g_game().getPlayerByName(player->getName());
	msg.addByte(loggedPlayer ? 0x01 : 0x00); // IsOnline
	msg.addByte(player->isPremium() ? 0x01 : 0x00); // IsPremium (GOD has always 'Premium')
	// Character loyalty title
	msg.addString(player->getLoyaltyTitle());

	uint8_t badgesSize = 0;
	auto badgesSizePosition = msg.getBufferPosition();
	msg.skipBytes(1);
	for (const auto &badge : g_game().getBadges()) {
		if (player->badge().hasBadge(badge.m_id)) {
			msg.add<uint32_t>(badge.m_id);
			msg.addString(badge.m_name);
			badgesSize++;
		}
	}

	msg.setBufferPosition(badgesSizePosition);
	msg.addByte(badgesSize);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterTitles() {
	if (!player || oldProtocol) {
		return;
	}

	auto titles = g_game().getTitles();

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_TITLES);
	msg.addByte(0x00); // 0x00 Here means 'no error'
	msg.addByte(player->title().getCurrentTitle());
	msg.addByte(static_cast<uint8_t>(titles.size()));
	for (const auto &title : titles) {
		msg.addByte(title.m_id);
		auto titleName = PlayerTitle::getNameBySex(player->getSex(), title.m_maleName, title.m_femaleName);
		msg.addString(titleName);
		msg.addString(title.m_description);
		msg.addByte(title.m_permanent ? 0x01 : 0x00);
		auto isUnlocked = player->title().isTitleUnlocked(title.m_id);
		msg.addByte(isUnlocked ? 0x01 : 0x00);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterOffenceStats() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_OFFENCESTATS);
	msg.addByte(0x00); // 0x00 Here means 'no error'

	const auto &skilsEquipment = player->getSkillsEquipment();

	addCyclopediaCriticalSkill(player, msg, SKILL_CRITICAL_HIT_CHANCE, skilsEquipment[SKILL_CRITICAL_HIT_CHANCE]);
	addCyclopediaCriticalSkill(player, msg, SKILL_CRITICAL_HIT_DAMAGE, skilsEquipment[SKILL_CRITICAL_HIT_DAMAGE]);

	addCyclopediaSkills(player, msg, SKILL_LIFE_LEECH_AMOUNT, skilsEquipment[SKILL_LIFE_LEECH_AMOUNT]);
	addCyclopediaSkills(player, msg, SKILL_MANA_LEECH_AMOUNT, skilsEquipment[SKILL_MANA_LEECH_AMOUNT]);

	const double onslaughtTotal = getForgeSkillStat(CONST_SLOT_LEFT);
	const double onslaughtEquipment = getForgeSkillStat(CONST_SLOT_LEFT, false);
	msg.addDouble(onslaughtTotal); // Onslaught Total
	msg.addDouble(onslaughtEquipment);
	msg.addDouble(onslaughtTotal - onslaughtEquipment);
	msg.addDouble(0.0); // NOTHING

	msg.addDouble(player->getCleavePercent() / 100.);

	// Perfect shot range (12.70)
	for (uint8_t range = 1; range <= 7; range++) {
		msg.add<uint16_t>(static_cast<uint16_t>(player->getPerfectShotDamage(range)));
	}

	const auto flatBonus = player->calculateFlatDamageHealing();
	uint16_t flatDamageHealingUnknown = 0;
	msg.add<uint16_t>(flatBonus); // Flat Damage and Healing Total
	msg.add<uint16_t>(flatBonus);
	msg.add<uint16_t>(flatDamageHealingUnknown);

	const auto &weapon = player->getWeapon();
	if (weapon) {
		const ItemType &it = Item::items[weapon->getID()];
		if (it.weaponType == WEAPON_WAND) {
			msg.add<uint16_t>(it.maxHitChance);
			msg.add<uint16_t>(0);
			msg.add<uint16_t>(0);
			msg.addByte(0x00);
			msg.add<uint16_t>(0);
			msg.add<uint16_t>(0);
			msg.addByte(getCipbiaElement(it.combatType));
			msg.addDouble(0.0);
			msg.addByte(0x00);
			msg.addByte(0x00);
		} else if (it.weaponType == WEAPON_DISTANCE || it.weaponType == WEAPON_AMMO || it.weaponType == WEAPON_MISSILE) {
			int32_t physicalAttack = std::max<int32_t>(0, weapon->getAttack());
			physicalAttack += player->weaponProficiency().getStat(WeaponProficiencyBonus_t::ATTACK_DAMAGE);
			int32_t elementalAttack = 0;
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				elementalAttack = std::max<int32_t>(0, it.abilities->elementDamage);
			}
			int32_t attackValue = physicalAttack + elementalAttack;
			if (it.weaponType == WEAPON_AMMO) {
				std::shared_ptr<Item> weaponItem = player->getWeapon(true);
				if (weaponItem) {
					attackValue += weaponItem->getAttack();
				}
			}

			int32_t distanceValue = player->getSkillLevel(SKILL_DISTANCE);
			int32_t attackSkill = player->getDistanceAttackSkill(distanceValue, attackValue);
			const auto attackRawTotal = player->attackRawTotal(flatBonus, attackValue, distanceValue);
			const auto attackTotal = player->attackTotal(flatBonus, attackValue, distanceValue);

			msg.add<uint16_t>(attackTotal);
			msg.add<uint16_t>(flatBonus);
			msg.add<uint16_t>(static_cast<uint16_t>(attackValue));
			msg.addByte(0x07);
			msg.add<uint16_t>(attackSkill);
			msg.add<uint16_t>(attackTotal - attackRawTotal);
			msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL);

			// Converted Damage
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				if (physicalAttack) {
					msg.addDouble(elementalAttack / static_cast<double>(attackValue));
				} else {
					msg.addDouble(0.0);
				}
				msg.addByte(getCipbiaElement(it.abilities->elementType));
			} else {
				handleImbuementDamage(msg, player);
			}

			const auto distanceAccuracy = player->getDamageAccuracy(it);
			const auto distanceAccuracySize = distanceAccuracy.size();
			msg.addByte(distanceAccuracy.size());
			for (uint8_t i = 0; i < distanceAccuracySize; ++i) {
				msg.addByte(i + 1);
				msg.addDouble(distanceAccuracy[i] / 100.);
			}
		} else {
			int32_t physicalAttack = std::max<int32_t>(0, weapon->getAttack());
			physicalAttack += player->weaponProficiency().getStat(WeaponProficiencyBonus_t::ATTACK_DAMAGE);
			int32_t elementalAttack = 0;
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				elementalAttack = std::max<int32_t>(0, it.abilities->elementDamage);
			}
			int32_t weaponAttack = physicalAttack + elementalAttack;
			int32_t weaponSkill = player->getWeaponSkill(weapon);
			int32_t attackSkill = player->getAttackSkill(weapon);
			uint8_t skillId = player->getWeaponSkillId(weapon);
			const auto attackRawTotal = player->attackRawTotal(flatBonus, weaponAttack, weaponSkill);
			const auto attackTotal = player->attackTotal(flatBonus, weaponAttack, weaponSkill);

			msg.add<uint16_t>(attackTotal);
			msg.add<uint16_t>(flatBonus);
			msg.add<uint16_t>(static_cast<uint16_t>(weaponAttack));
			msg.addByte(skillId);
			msg.add<uint16_t>(attackSkill);
			msg.add<uint16_t>(attackTotal - attackRawTotal);
			msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL);

			// Converted Damage
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				if (physicalAttack) {
					msg.addDouble(elementalAttack / static_cast<double>(weaponAttack));
				} else {
					msg.addDouble(0);
				}
				msg.addByte(getCipbiaElement(it.abilities->elementType));
			} else {
				handleImbuementDamage(msg, player);
			}
			msg.addByte(0x00);
		}
	} else {
		uint16_t attackValue = 7;
		int32_t fistValue = player->getSkillLevel(SKILL_FIST);
		int32_t attackSkill = player->getDistanceAttackSkill(fistValue, attackValue);
		const auto attackRawTotal = player->attackRawTotal(flatBonus, attackValue, fistValue);
		const auto attackTotal = player->attackTotal(flatBonus, attackValue, fistValue);

		msg.add<uint16_t>(attackTotal);
		msg.add<uint16_t>(flatBonus);
		msg.add<uint16_t>(attackValue);
		msg.addByte(11);
		msg.add<uint16_t>(attackSkill);
		msg.add<uint16_t>(attackTotal - attackRawTotal);
		msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL);

		msg.addDouble(0.0);
		msg.addByte(0x00);
		msg.addByte(0x00);
	}

	msg.addDouble(player->weaponProficiency().getPowerfulFoeDamage()); // Influenced/Bosses damage

	const auto &runesCritical = player->weaponProficiency().getRunesCritical();
	const auto &autoAttackCritical = player->weaponProficiency().getAutoAttackCritical();

	const auto &bestiariesDamage = player->weaponProficiency().getActiveBestiariesDamage();
	msg.add<uint16_t>(bestiariesDamage.size()); // Bestiary Damage size
	for (const auto &[name, amount] : bestiariesDamage) {
		msg.addString(name); // Bestiary category name
		msg.addDouble(amount); // Amount
	}

	const auto &elementalCriticalChanceOpt = player->weaponProficiency().getActiveElementalCriticalType(WeaponProficiencyBonus_t::ELEMENTAL_HIT_CHANCE);
	msg.addByte(elementalCriticalChanceOpt.has_value()); // Has Element Critical Chance
	if (elementalCriticalChanceOpt.has_value()) {
		msg.addByte(elementalCriticalChanceOpt.value().first); // Element Critical Chance Element ID
		msg.addDouble(elementalCriticalChanceOpt.value().second); // Applied Element Critical Chance Value
	}

	msg.addDouble(runesCritical.chance); // Offensive Runes Critical Chance
	msg.addDouble(autoAttackCritical.chance); // Auto Attack Critical Chance

	const auto &elementalCriticalDamageOpt = player->weaponProficiency().getActiveElementalCriticalType(WeaponProficiencyBonus_t::ELEMENTAL_CRITICAL_EXTRA_DAMAGE);
	msg.addByte(elementalCriticalDamageOpt.has_value()); // Has Element Critical Damage
	if (elementalCriticalDamageOpt.has_value()) {
		msg.addByte(elementalCriticalDamageOpt.value().first); // Element Critical Damage Element ID
		msg.addDouble(elementalCriticalDamageOpt.value().second); // Applied Element Critical Damage Value
	}

	msg.addDouble(runesCritical.damage); // Offensive Runes Critical Damage
	msg.addDouble(autoAttackCritical.damage); // Auto Attack Critical Damage

	msg.add<uint16_t>(player->weaponProficiency().getStat(WeaponProficiencyBonus_t::LIFE_GAIN_ON_HIT)); // Life Gain on Hit
	msg.add<uint16_t>(player->weaponProficiency().getStat(WeaponProficiencyBonus_t::MANA_GAIN_ON_HIT)); // Mana Gain on Hit
	msg.add<uint16_t>(player->weaponProficiency().getStat(WeaponProficiencyBonus_t::LIFE_GAIN_ON_KILL)); // Life Gain on Kill
	msg.add<uint16_t>(player->weaponProficiency().getStat(WeaponProficiencyBonus_t::MANA_GAIN_ON_KILL)); // Mana Gain on Kill

	skills_t skill = SKILL_NONE;
	if (weapon) {
		switch (Item::items[weapon->getID()].type) {
			case ITEM_TYPE_SWORD:
				skill = SKILL_SWORD;
				break;
			case ITEM_TYPE_AXE:
				skill = SKILL_AXE;
				break;
			case ITEM_TYPE_CLUB:
				skill = SKILL_CLUB;
				break;
			case ITEM_TYPE_WAND:
				skill = SKILL_MAGLEVEL;
				break;
			case ITEM_TYPE_DISTANCE:
				skill = SKILL_DISTANCE;
				break;
			default:
				break;
		}
	}

	const auto &skillPercentage = player->weaponProficiency().getSkillPercentage(skill);

	double playerSkill = 0.0;
	if (skillPercentage.skill != SKILL_NONE) {
		playerSkill = skillPercentage.skill == SKILL_MAGLEVEL ? player->getMagicLevel() : player->getSkillLevel(skillPercentage.skill);
	}

	bool hasAutoAttackSkill = skillPercentage.skill != SKILL_NONE && skillPercentage.autoAttack;
	msg.addByte(hasAutoAttackSkill); // Has Auto Attack Skill
	if (hasAutoAttackSkill) {
		msg.addByte(static_cast<uint8_t>(getCipbiaSkill(skillPercentage.skill))); // Auto Attack Skill ID
		msg.addDouble(skillPercentage.autoAttack); // Percent Auto Attack Skill
		msg.addDouble(std::round(playerSkill * skillPercentage.autoAttack)); // Applied Auto Attack Value
	}

	bool hasSpellDamage = skillPercentage.skill != SKILL_NONE && skillPercentage.spellDamage;
	msg.addByte(hasSpellDamage); // Has Spell Damage
	if (hasSpellDamage) {
		msg.addByte(static_cast<uint8_t>(getCipbiaSkill(skillPercentage.skill))); // Spell Damage Skill ID
		msg.addDouble(skillPercentage.spellDamage); // Percent Spell Damage
		msg.addDouble(std::round(playerSkill * skillPercentage.spellDamage)); // Applied Spell Damage Value
	}

	bool hasSpellHealing = skillPercentage.skill != SKILL_NONE && skillPercentage.spellHealing;
	msg.addByte(hasSpellHealing); // Has Spell Healing
	if (hasSpellHealing) {
		msg.addByte(static_cast<uint8_t>(getCipbiaSkill(skillPercentage.skill))); // Spell Healing Skill ID
		msg.addDouble(skillPercentage.spellHealing); // Percent Spell Healing Skill
		msg.addDouble(std::round(playerSkill * skillPercentage.spellHealing)); // Applied Spell Healing Value
	}

	msg.addDouble(0.0); // Full hit points extra damage
	msg.addDouble(0.0); // Low hit points extra damage
	msg.addDouble(0.0); // Armor penetration
	msg.addByte(0x00); // Elemental pierces count

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterDefenceStats() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_DEFENCESTATS);
	msg.addByte(0x00); // 0x00 Here means 'no error'

	const double dodgeTotal = getForgeSkillStat(CONST_SLOT_ARMOR) + player->wheel().getStat(WheelStat_t::DODGE);
	msg.addDouble(dodgeTotal);
	msg.addDouble(getForgeSkillStat(CONST_SLOT_ARMOR, false));
	msg.addDouble(getForgeSkillStat(CONST_SLOT_ARMOR) - getForgeSkillStat(CONST_SLOT_ARMOR, false));
	msg.addDouble(0.00);
	msg.addDouble(player->wheel().getStat(WheelStat_t::DODGE));

	msg.add<uint32_t>(player->getMagicShieldCapacityFlat() * (1 + player->getMagicShieldCapacityPercent()));
	msg.add<uint16_t>(static_cast<uint16_t>(player->getMagicShieldCapacityFlat())); // Direct bonus
	msg.addDouble(player->getMagicShieldCapacityPercent()); // Percentage bonus

	msg.add<uint16_t>(static_cast<uint16_t>(player->getReflectFlat(COMBAT_PHYSICALDAMAGE)));

	msg.add<uint16_t>(player->getArmor());
	msg.add<uint16_t>(player->getMantra());

	const auto shieldingSkill = player->getSkillLevel(SKILL_SHIELD);
	const uint16_t defenseWheel = player->wheel().getMajorStatConditional("Combat Mastery", WheelMajor_t::DEFENSE);
	msg.add<uint16_t>(player->getDefense(true));
	msg.add<uint16_t>(player->getDefenseEquipment());
	msg.addByte(0x06);
	msg.add<uint16_t>(shieldingSkill);
	msg.add<uint16_t>(defenseWheel);

	const auto wheelMultiplier = player->wheel().getMitigationMultiplier();
	msg.addDouble(player->getMitigation() / 100.);
	msg.addDouble(0.0);
	msg.addDouble(player->getDefenseEquipment() / 10000.);
	msg.addDouble(player->getSkillLevel(SKILL_SHIELD) * player->getVocation()->mitigationFactor / 10000.);
	msg.addDouble(wheelMultiplier / 100.);

	// Store the "combats" to increase in absorb values function and send to client later
	uint8_t combats = 0;
	auto startCombats = msg.getBufferPosition();
	msg.skipBytes(1);

	// Calculate and parse the combat absorbs values
	calculateAbsorbValues(player, msg, combats);

	// Now set the buffer position skiped and send the total combats count
	auto endCombats = msg.getBufferPosition();
	msg.setBufferPosition(startCombats);
	msg.addByte(combats);
	msg.setBufferPosition(endCombats);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCyclopediaCharacterMiscStats() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xDA);
	msg.addByte(CYCLOPEDIA_CHARACTERINFO_MISCSTATS);
	msg.addByte(0x00); // 0x00 Here means 'no error'

	const double momentumTotal = getForgeSkillStat(CONST_SLOT_HEAD) + player->wheel().getBonusData().momentum;
	msg.addDouble(momentumTotal);
	msg.addDouble(getForgeSkillStat(CONST_SLOT_HEAD, false));
	msg.addDouble(getForgeSkillStat(CONST_SLOT_HEAD) - getForgeSkillStat(CONST_SLOT_HEAD, false));
	msg.addDouble(player->wheel().getBonusData().momentum);
	msg.addDouble(0.00);

	msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS));
	msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS, false));
	msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS) - getForgeSkillStat(CONST_SLOT_LEGS, false));
	msg.addDouble(0.09);

	msg.addDouble(getForgeSkillStat(CONST_SLOT_FEET, false));
	msg.addDouble(getForgeSkillStat(CONST_SLOT_FEET, false));
	msg.addDouble(0.00);

	uint8_t haveBlesses = 0;
	for (auto bless : magic_enum::enum_values<Blessings>()) {
		if (bless == Blessings::TwistOfFate) {
			continue;
		}

		if (player->hasBlessing(enumToValue(bless))) {
			++haveBlesses;
		}
	}

	msg.addByte(haveBlesses);
	msg.addByte(magic_enum::enum_count<Blessings>() - 1); // Skip Twist of Fate

	auto activeConcoctions = player->getActiveConcoctions();
	for (const auto &[concoctionId, duration] : activeConcoctions) {
		if (duration == 0) {
			g_logger().error(
				"sendCyclopediaCharacterMiscStats: Player {} has concoction with itemId {} and timeLeft 0, this should not happen.",
				player->getName(), concoctionId
			);
			player->updateConcoction(concoctionId, duration);
		}
	}

	msg.addByte(activeConcoctions.size());
	for (const auto &[concoctionId, duration] : activeConcoctions) {
		msg.add<uint16_t>(concoctionId);
		msg.addByte(0x00);
		msg.addByte(0x00);
		msg.add<uint32_t>(duration);
	}

	const auto &activeFoods = player->getActiveFoods();
	msg.addByte(activeFoods.size());
	for (const auto &[foodId, duration] : activeFoods) {
		msg.add<uint16_t>(foodId);
		msg.addByte(0x00);
		msg.addByte(0x00);
		msg.add<uint32_t>(duration);
	}

	const auto &weaponProficiencyAugments = player->weaponProficiency().getActiveAugments();
	msg.addByte(weaponProficiencyAugments.size());
	for (const auto &[key, value] : weaponProficiencyAugments) {
		msg.add<uint16_t>(key.first); // Spell ID
		msg.addByte(key.second); // Augment Type
		msg.addDouble(value); // Spell Augment Value
	}

	const auto &wheelAugments = player->wheel().getActiveAugments();
	msg.addByte(wheelAugments.size());
	for (const auto &[key, value] : wheelAugments) {
		msg.add<uint16_t>(key.first); // Spell ID
		msg.addByte(key.second); // Augment Type
		msg.addDouble(value); // Spell Augment Value
	}

	const auto &equippedAugments = player->getEquippedAugments();
	msg.addByte(equippedAugments.size());
	for (const auto &[key, value] : equippedAugments) {
		msg.add<uint16_t>(key.first); // Spell ID
		msg.addByte(key.second); // Augment Type
		msg.addDouble(value); // Spell Augment Value
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendReLoginWindow(uint8_t unfairFightReduction) {
	NetworkMessage msg;
	msg.addByte(0x28);

	if (version >= 1055) {
		msg.addByte(0x00);
	}
	if (version >= 862) {
		msg.addByte(unfairFightReduction);
	}
	if (!oldProtocol && version >= 1121) {
		msg.addByte(0x00); // use death redemption (boolean)
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendStats() {
	if (shouldSuppressPreLoginPacket()) {
		return;
	}

	NetworkMessage msg;
	AddPlayerStats(msg);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendBasicData() {
	if (!player || version < 950) {
		return;
	}

	auto vocation = player->getVocation();
	if (!vocation) {
		return;
	}

	auto vocationId = player->getVocationId();

	NetworkMessage msg;
	msg.addByte(0x9F);
	if (player->isPremium() || player->isVip()) {
		msg.addByte(1);
		msg.add<uint32_t>(getTimeNow() + ((player->getPremiumDays() + 1) * 86400));
	} else {
		msg.addByte(0);
		msg.add<uint32_t>(0);
	}
	msg.addByte(vocation->getClientId());

	// Prey window
	if (vocationId == 0 && player->getGroup()->id < GROUP_TYPE_GAMEMASTER) {
		msg.addByte(0);
	} else {
		msg.addByte(1); // has reached Main (allow player to open Prey window)
	}

	// Filter only valid ids
	std::list<uint16_t> spellsList = g_spells().getSpellsByVocation(vocationId);
	std::vector<std::shared_ptr<InstantSpell>> validSpells;
	for (uint16_t sid : spellsList) {
		auto spell = g_spells().getInstantSpellById(sid);
		if (spell && spell->getSpellId() > 0) {
			validSpells.emplace_back(spell);
		}
	}

	// Send total size of spells
	msg.add<uint16_t>(validSpells.size());
	// Send each spell valid ids
	for (const auto &spell : validSpells) {
		if (!spell) {
			continue;
		}

		// Only send valid spells to old client
		if (oldProtocol) {
			msg.addByte(spell->getSpellId());
			continue;
		}

		if (spell->isLearnable() && !player->hasLearnedInstantSpell(spell->getName())) {
			msg.add<uint16_t>(0);
		} else if (spell && spell->isLearnable() && player->hasLearnedInstantSpell(spell->getName())) {
			// Ignore spell if not have wheel grade (or send if you have)
			auto grade = player->wheel().getSpellUpgrade(spell->getName());
			if (static_cast<uint8_t>(grade) == 0) {
				msg.add<uint16_t>(0);
			} else {
				msg.add<uint16_t>(spell->getSpellId());
			}
		} else {
			msg.add<uint16_t>(spell->getSpellId());
		}
	}

	if (!oldProtocol) {
		msg.addByte(player->getVocation()->getMagicShield()); // bool - determine whether magic shield is active or not
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendBlessingWindow() {
	if (!player || oldProtocol || version < 1100) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x9B);

	bool isRetro = g_configManager().getBoolean(TOGGLE_SERVER_IS_RETRO);

	msg.addByte(isRetro ? 0x07 : 0x08);
	for (auto blessing : magic_enum::enum_values<Blessings>()) {
		if (isRetro && blessing == Blessings::TwistOfFate) {
			continue;
		}

		const auto blessingValue = enumToValue(blessing);
		const auto blessingId = 1 << blessingValue;
		msg.add<uint16_t>(blessingId);
		msg.addByte(player->getBlessingCount(blessingValue));
		msg.addByte(player->getBlessingCount(blessingValue, true));
	}

	// Start at "The Wisdom Of Solitude"
	uint8_t blessCount = 0;
	for (auto bless : magic_enum::enum_values<Blessings>()) {
		if (bless == Blessings::TwistOfFate) {
			continue;
		}

		if (player->hasBlessing(enumToValue(bless))) {
			blessCount++;
		}
	}

	const auto isPromoted = player->isPromoted();
	const auto factor = (isRetro ? 6.31 : 8);
	const auto skillReduction = factor * blessCount;
	const auto promotionReduction = (isPromoted ? 30 : 0);
	const auto minReduction = skillReduction + promotionReduction;
	const auto maxLossPvpDeath = calculateMaxPvpReduction(blessCount, isPromoted);

	msg.addByte(isPromoted);
	msg.addByte(30); // Reduction bonus with promotion
	msg.addByte(minReduction);
	msg.addByte(isRetro ? minReduction : maxLossPvpDeath);
	msg.addByte(minReduction);

	const auto playerSkull = player->getSkull();
	const auto &playerAmulet = player->getThing(CONST_SLOT_NECKLACE);
	bool hasSkull = (playerSkull == Skulls_t::SKULL_RED || playerSkull == Skulls_t::SKULL_BLACK);
	bool usingAol = (playerAmulet && playerAmulet->getItem()->getID() == ITEM_AMULETOFLOSS);
	if (hasSkull) {
		msg.addByte(100);
		msg.addByte(100);
	} else if (usingAol) {
		msg.addByte(0);
		msg.addByte(0);
	} else {
		msg.addByte(calculateEquipmentLoss(blessCount, true));
		msg.addByte(calculateEquipmentLoss(blessCount, true));
	}

	msg.addByte(hasSkull);
	msg.addByte(usingAol);

	msg.addByte(0x00);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendBlessStatus() {
	if (!player || version < 1100) {
		return;
	}

	// Ignore Twist of Fate (Id 1)
	uint8_t blessCount = 0;
	for (auto bless : magic_enum::enum_values<Blessings>()) {
		if (bless == Blessings::TwistOfFate) {
			continue;
		}
		if (player->hasBlessing(enumToValue(bless))) {
			blessCount++;
		}
	}

	NetworkMessage msg;
	msg.addByte(0x9C);

	if (oldProtocol) {
		msg.add<uint16_t>(blessCount >= 5 ? 0x01 : 0x00);
	} else {
		bool glow = player->getVocationId() > VOCATION_NONE && ((g_configManager().getBoolean(INVENTORY_GLOW) && blessCount >= 5) || player->getLevel() < g_configManager().getNumber(ADVENTURERSBLESSING_LEVEL));
		msg.add<uint16_t>(glow ? 1 : 0); // Show up the glowing effect in items if you have all blesses or adventurer's blessing
		msg.addByte((blessCount >= 7) ? 3 : ((blessCount >= 5) ? 2 : 1)); // 1 = Disabled | 2 = normal | 3 = green
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPremiumTrigger() {
	if (version < 1100) {
		return;
	}

	if (g_configManager().getBoolean(FREE_PREMIUM) || g_configManager().getBoolean(VIP_SYSTEM_ENABLED)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x9E);

	msg.addByte(16);
	for (uint16_t i = 0; i <= 15; i++) {
		// PREMIUM_TRIGGER_TRAIN_OFFLINE = false, PREMIUM_TRIGGER_XP_BOOST = false, PREMIUM_TRIGGER_MARKET = false, PREMIUM_TRIGGER_VIP_LIST = false, PREMIUM_TRIGGER_DEPOT_SPACE = false, PREMIUM_TRIGGER_INVITE_PRIVCHAT = false
		msg.addByte(0x01);
	}

	writeToOutputBuffer(msg);
}

bool ProtocolGame::sendCipsoft860SpecialTextMessage(const TextMessage &message, MessageClasses internalType) {
	switch (internalType) {
		case MESSAGE_DAMAGE_DEALT:
		case MESSAGE_DAMAGE_RECEIVED:
		case MESSAGE_DAMAGE_OTHERS: {
			NetworkMessage msg;
			if (message.primary.value != 0) {
				msg.addByte(0x84);
				msg.addPosition(message.position);
				msg.addByte(message.primary.color);
				msg.addString(std::to_string(message.primary.value));
			}
			if (message.secondary.value != 0) {
				msg.addByte(0x84);
				msg.addPosition(message.position);
				msg.addByte(message.secondary.color);
				msg.addString(std::to_string(message.secondary.value));
			}
			if (!message.text.empty()) {
				msg.addByte(0xB4);
				msg.addByte(cipsoft860EventDefaultMessage);
				msg.addString(message.text);
			}
			writeToOutputBuffer(msg);
			return true;
		}
		case MESSAGE_MANA:
		case MESSAGE_HEALED:
		case MESSAGE_HEALED_OTHERS:
		case MESSAGE_EXPERIENCE:
		case MESSAGE_EXPERIENCE_OTHERS: {
			NetworkMessage msg;
			msg.addByte(0x84);
			msg.addPosition(message.position);
			msg.addByte(message.primary.color);
			msg.addString(std::to_string(message.primary.value));
			if (!message.text.empty()) {
				msg.addByte(0xB4);
				msg.addByte(cipsoft860EventDefaultMessage);
				msg.addString(message.text);
			}
			writeToOutputBuffer(msg);
			return true;
		}
		default:
			return false;
	}
}

void ProtocolGame::sendTextMessage(const TextMessage &message) {
	if (message.type == MESSAGE_NONE) {
		g_logger().error("[ProtocolGame::sendTextMessage] - Message type is wrong, missing or invalid for player with name {}, on position {}", player->getName(), player->getPosition().toString());
		player->sendTextMessage(MESSAGE_ADMINISTRATOR, "There was a problem requesting your message, please contact the administrator");
		return;
	}

	MessageClasses internalType = message.type;
	if (oldProtocol && message.type > MESSAGE_LAST_OLDPROTOCOL) {
		switch (internalType) {
			case MESSAGE_REPORT: {
				internalType = MESSAGE_LOOT;
				break;
			}
			case MESSAGE_HOTKEY_PRESSED: {
				internalType = MESSAGE_LOOK;
				break;
			}
			case MESSAGE_TUTORIAL_HINT: {
				internalType = MESSAGE_LOGIN;
				break;
			}
			case MESSAGE_THANK_YOU: {
				internalType = MESSAGE_LOGIN;
				break;
			}
			case MESSAGE_MARKET: {
				internalType = MESSAGE_EVENT_ADVANCE;
				break;
			}
			case MESSAGE_MANA: {
				internalType = MESSAGE_HEALED;
				break;
			}
			case MESSAGE_BEYOND_LAST: {
				internalType = MESSAGE_LOOT;
				break;
			}
			case MESSAGE_ATTENTION: {
				internalType = MESSAGE_EVENT_ADVANCE;
				break;
			}
			case MESSAGE_BOOSTED_CREATURE: {
				internalType = MESSAGE_LOOT;
				break;
			}
			case MESSAGE_OFFLINE_TRAINING: {
				internalType = MESSAGE_LOOT;
				break;
			}
			case MESSAGE_TRANSACTION: {
				internalType = MESSAGE_LOOT;
				break;
			}
			case MESSAGE_POTION: {
				internalType = MESSAGE_FAILURE;
				break;
			}

			default: {
				internalType = MESSAGE_EVENT_ADVANCE;
				break;
			}
		}
	}

	uint8_t clientType = static_cast<uint8_t>(internalType);
	if (isCipsoft860Profile(protocolProfile)) {
		clientType = translateCipsoft860MessageClassToClient(internalType);
		if (clientType == MESSAGE_NONE) {
			sendCipsoft860SpecialTextMessage(message, internalType);
			return;
		}
	}

	NetworkMessage msg;
	msg.addByte(0xB4);
	msg.addByte(clientType);
	switch (internalType) {
		case MESSAGE_DAMAGE_DEALT:
		case MESSAGE_DAMAGE_RECEIVED:
		case MESSAGE_DAMAGE_OTHERS: {
			msg.addPosition(message.position);
			msg.add<uint32_t>(message.primary.value);
			msg.addByte(message.primary.color);
			msg.add<uint32_t>(message.secondary.value);
			msg.addByte(message.secondary.color);
			break;
		}
		case MESSAGE_HEALED:
		case MESSAGE_HEALED_OTHERS: {
			if (!oldProtocol) {
				msg.addPosition(message.position);
				msg.add<uint32_t>(message.primary.value);
				msg.addByte(message.primary.color);
			}
			break;
		}
		case MESSAGE_EXPERIENCE:
		case MESSAGE_EXPERIENCE_OTHERS: {
			msg.addPosition(message.position);
			if (!oldProtocol) {
				msg.add<uint64_t>(message.primary.value);
			} else {
				msg.add<uint32_t>(message.primary.value);
			}
			msg.addByte(message.primary.color);
			break;
		}
		case MESSAGE_GUILD:
		case MESSAGE_PARTY_MANAGEMENT:
		case MESSAGE_PARTY:
			msg.add<uint16_t>(message.channelId);
			break;
		default:
			break;
	}
	msg.addString(message.text);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendClosePrivate(uint16_t channelId) {
	NetworkMessage msg;
	msg.addByte(0xB3);
	msg.add<uint16_t>(channelId);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatePrivateChannel(uint16_t channelId, const std::string &channelName) {
	NetworkMessage msg;
	msg.addByte(0xB2);
	msg.add<uint16_t>(channelId);
	msg.addString(channelName);
	msg.add<uint16_t>(0x01);
	msg.addString(player->getName());
	msg.add<uint16_t>(0x00);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendChannelsDialog() {
	NetworkMessage msg;
	msg.addByte(0xAB);

	const ChannelList &list = g_chat().getChannelList(player);
	msg.addByte(list.size());
	for (const auto &channel : list) {
		msg.add<uint16_t>(channel->getId());
		msg.addString(channel->getName());
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendChannel(uint16_t channelId, const std::string &channelName, const UsersMap* channelUsers, const InvitedMap* invitedUsers) {
	NetworkMessage msg;
	msg.addByte(0xAC);

	msg.add<uint16_t>(channelId);
	msg.addString(channelName);

	if (version >= 910) {
		addChannelUserNames(msg, channelUsers);
		addChannelUserNames(msg, invitedUsers);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendChannelMessage(const std::string &author, const std::string &text, SpeakClasses type, uint16_t channel) {
	uint8_t clientType = static_cast<uint8_t>(type);
	if (isCipsoft860Profile(protocolProfile)) {
		clientType = translateCipsoft860SpeakClassToClient(type);
		if (clientType == cipsoft860TalkNone) {
			return;
		}
	}

	NetworkMessage msg;
	msg.addByte(0xAA);
	msg.add<uint32_t>(0x00);
	msg.addString(author);
	msg.add<uint16_t>(0x00);
	msg.addByte(clientType);
	msg.add<uint16_t>(channel);
	msg.addString(text);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendIcons(const std::unordered_set<PlayerIcon> &iconSet, const IconBakragore iconBakragore) {
	if (shouldSuppressPreLoginPacket()) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xA2);

	std::bitset<static_cast<size_t>(PlayerIcon::Count)> iconsBitSet;
	using enum PlayerIcon;
	for (const auto &icon : iconSet) {
		if (oldProtocol && version < 1100) {
			if (icon == NewManaShield) {
				[[maybe_unused]] auto &updatedIcons = iconsBitSet.set(enumToValue(ManaShield));
				continue;
			}

			if (enumToValue(icon) > enumToValue(Bleeding)) {
				continue;
			}
		}

		[[maybe_unused]] auto &updatedIcons = iconsBitSet.set(enumToValue(icon));
	}

	uint32_t icons = iconsBitSet.to_ulong();

	if (oldProtocol) {
		// Send as uint16_t in old protocol
		msg.add<uint16_t>(static_cast<uint16_t>(icons));
	} else {
		// Send as uint64_t in new protocol
		msg.add<uint64_t>(icons);
		msg.addByte(enumToValue(iconBakragore)); // Icons Bakragore
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendIconBakragore(const IconBakragore icon) {
	if (oldProtocol || shouldSuppressPreLoginPacket()) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xA2);
	msg.add<uint64_t>(0); // Send empty normal icons
	msg.addByte(enumToValue(icon));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUnjustifiedPoints(const uint8_t &dayProgress, const uint8_t &dayLeft, const uint8_t &weekProgress, const uint8_t &weekLeft, const uint8_t &monthProgress, const uint8_t &monthLeft, const uint8_t &skullDuration) {
	if (oldProtocol && version < 1100) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xB7);
	msg.addByte(dayProgress);
	msg.addByte(dayLeft);
	msg.addByte(weekProgress);
	msg.addByte(weekLeft);
	msg.addByte(monthProgress);
	msg.addByte(monthLeft);
	msg.addByte(skullDuration);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendOpenPvpSituations(uint8_t openPvpSituations) {
	if (oldProtocol && version < 1100) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xB8);
	msg.addByte(openPvpSituations);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendContainer(uint8_t cid, const std::shared_ptr<Container> &container, bool hasParent, uint16_t firstIndex) {
	if (!player || !container) {
		return;
	}

	if (shouldSuppressPreLoginPacket()) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x6E);

	msg.addByte(cid);

	if (container->getID() == ITEM_BROWSEFIELD) {
		AddItem(msg, ITEM_BAG, 1, container->getTier());
		msg.addString("Browse Field");
	} else {
		AddItem(msg, container);
		msg.addString(container->getName());
	}

	const auto itemsStoreInboxToSend = container->getStoreInboxFilteredItems();

	msg.addByte(container->capacity());

	msg.addByte(hasParent ? 0x01 : 0x00);

	if (oldProtocol && version < 984) {
		const ItemDeque &itemList = container->getItemList();
		const auto maxItemsToSend = std::min<uint32_t>(container->capacity(), itemList.size());
		msg.addByte(maxItemsToSend);

		uint32_t i = 0;
		for (auto it = itemList.begin(), end = itemList.end(); i < maxItemsToSend && it != end; ++it, ++i) {
			AddItem(msg, *it);
		}

		writeToOutputBuffer(msg);
		return;
	}

	// Depot search
	if (!oldProtocol) {
		msg.addByte((player->isDepotSearchAvailable() && container->isInsideDepot(true)) ? 0x01 : 0x00);
	}

	msg.addByte(container->isUnlocked() ? 0x01 : 0x00); // Drag and drop
	msg.addByte(container->hasPagination() ? 0x01 : 0x00); // Pagination

	uint32_t containerSize = container->size();
	if (!itemsStoreInboxToSend.empty()) {
		containerSize = itemsStoreInboxToSend.size();
	}
	msg.add<uint16_t>(containerSize);
	msg.add<uint16_t>(firstIndex);

	uint32_t maxItemsToSend;

	if (container->hasPagination() && firstIndex > 0) {
		maxItemsToSend = std::min<uint32_t>(container->capacity(), containerSize - firstIndex);
	} else {
		maxItemsToSend = container->capacity();
	}

	const ItemDeque &itemList = container->getItemList();
	if (firstIndex >= containerSize) {
		msg.addByte(0x00);
	} else if (container->getID() == ITEM_STORE_INBOX && !itemsStoreInboxToSend.empty()) {
		msg.addByte(std::min<uint32_t>(maxItemsToSend, containerSize));
		for (const auto &item : itemsStoreInboxToSend) {
			AddItem(msg, item);
		}
	} else {
		msg.addByte(std::min<uint32_t>(maxItemsToSend, containerSize));

		uint32_t i = 0;
		for (auto it = itemList.begin() + firstIndex, end = itemList.end(); i < maxItemsToSend && it != end; ++it, ++i) {
			AddItem(msg, *it);
		}
	}

	// From here on down is for version 13.21+
	if (oldProtocol) {
		writeToOutputBuffer(msg);
		return;
	}

	if (container->isStoreInbox()) {
		const auto &categories = container->getStoreInboxValidCategories();
		const auto enumName = container->getAttribute<std::string>(ItemAttribute_t::STORE_INBOX_CATEGORY);
		auto category = magic_enum::enum_cast<ContainerCategory_t>(enumName);
		if (category.has_value()) {
			bool toSendCategory = false;
			// Check if category exist in the deque
			for (const auto &tempCategory : categories) {
				if (tempCategory == category.value()) {
					toSendCategory = true;
					g_logger().debug("found category {}", toSendCategory);
				}
			}

			if (!toSendCategory) {
				std::shared_ptr<Container> container = player->getContainerByID(cid);
				if (container) {
					container->removeAttribute(ItemAttribute_t::STORE_INBOX_CATEGORY);
				}
			}
			sendContainerCategory<ContainerCategory_t>(msg, categories, static_cast<uint8_t>(category.value()));
		} else {
			sendContainerCategory<ContainerCategory_t>(msg, categories);
		}
	} else {
		msg.addByte(0x00);
		msg.addByte(0x00);
	}

	// New container menu options
	if (container->isMovable()) { // Pickupable/Moveable (?)
		msg.addByte(1);
	} else {
		msg.addByte(0);
	}

	if (container->getHoldingPlayer()) { // Player holding the item (?)
		msg.addByte(1);
	} else {
		msg.addByte(0);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendLootContainers() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xC0);
	msg.addByte(player->quickLootFallbackToMainContainer ? 1 : 0);

	std::map<ObjectCategory_t, std::pair<std::shared_ptr<Container>, std::shared_ptr<Container>>> managedContainersMap;
	for (const auto &[category, containersPair] : player->m_managedContainers) {
		if (containersPair.first && !containersPair.first->isRemoved()) {
			managedContainersMap[category].first = containersPair.first;
		}
		if (containersPair.second && !containersPair.second->isRemoved()) {
			managedContainersMap[category].second = containersPair.second;
		}
	}

	auto msgPosition = msg.getBufferPosition();
	msg.skipBytes(1);
	uint8_t containers = 0;
	for (const auto &[category, containersPair] : managedContainersMap) {
		if (!isValidObjectCategory(category)) {
			continue;
		}
		containers++;
		msg.addByte(category);
		uint16_t lootContainerId = containersPair.first ? containersPair.first->getID() : 0;
		uint16_t obtainContainerId = containersPair.second ? containersPair.second->getID() : 0;
		msg.add<uint16_t>(lootContainerId);
		msg.add<uint16_t>(obtainContainerId);
	}
	msg.setBufferPosition(msgPosition);
	msg.addByte(containers);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendLootStats(const std::shared_ptr<Item> &item, uint8_t count) {
	if (!item) {
		return;
	}

	if (oldProtocol) {
		item->setIsLootTrackeable(false);
		return;
	}

	std::shared_ptr<Item> lootedItem = nullptr;
	lootedItem = item->clone();
	lootedItem->setItemCount(count);

	NetworkMessage msg;
	msg.addByte(0xCF);
	AddItem(msg, lootedItem);
	msg.addString(lootedItem->getName());
	item->setIsLootTrackeable(false);
	writeToOutputBuffer(msg);

	lootedItem = nullptr;
}

void ProtocolGame::sendShop(const std::shared_ptr<Npc> &npc) {
	Benchmark brenchmark;
	NetworkMessage msg;
	msg.addByte(0x7A);
	msg.addString(npc->getName());

	if (!oldProtocol) {
		msg.add<uint16_t>(npc->getCurrency());
		msg.addString(std::string()); // Currency name
	}

	const auto &shoplist = npc->getShopItemVector(player->getGUID());
	const bool useCipsoft860ShopLayout = isCipsoft860Profile(protocolProfile);
	const auto shopItemLimit = useCipsoft860ShopLayout ? std::numeric_limits<uint8_t>::max() : std::numeric_limits<uint16_t>::max();
	uint16_t itemsToSend = static_cast<uint16_t>(std::min<size_t>(shoplist.size(), shopItemLimit));
	if (useCipsoft860ShopLayout) {
		msg.addByte(static_cast<uint8_t>(itemsToSend));
	} else {
		msg.add<uint16_t>(itemsToSend);
	}

	// Initialize before the loop to avoid database overload on each iteration
	auto talkactionHidden = player->kv()->get("npc-shop-hidden-sell-item");
	// Initialize the inventoryMap outside the loop to avoid creation on each iteration
	std::map<uint16_t, uint16_t> inventoryMap;
	player->getAllSaleItemIdAndCount(inventoryMap);
	uint16_t i = 0;
	for (const ShopBlock &shopBlock : shoplist) {
		if (++i > itemsToSend) {
			break;
		}

		// Hidden sell items from the shop if they are not in the player's inventory
		if (talkactionHidden && talkactionHidden->get<bool>()) {
			const auto &foundItem = inventoryMap.find(shopBlock.itemId);
			if (foundItem == inventoryMap.end() && shopBlock.itemSellPrice > 0 && shopBlock.itemBuyPrice == 0) {
				AddHiddenShopItem(msg);
				continue;
			}
		}

		AddShopItem(msg, shopBlock);
	}

	writeToOutputBuffer(msg);
	g_logger().debug("ProtocolGame::sendShop - Time: {} ms, shop items: {}", brenchmark.duration(), shoplist.size());
}

void ProtocolGame::sendCloseShop() {
	NetworkMessage msg;
	msg.addByte(0x7C);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendClientCheck() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x63);
	msg.add<uint32_t>(1); // payload size
	msg.addByte(1); // raw client-check probe byte
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendGameNews() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x98);
	msg.add<uint32_t>(1); // game news category/group id
	msg.addByte(1); // bool: 0 = open, 1 = highlight
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendResourcesBalance(uint64_t money /*= 0*/, uint64_t bank /*= 0*/, uint64_t preyCards /*= 0*/, uint64_t taskHunting /*= 0*/, uint64_t forgeDust /*= 0*/, uint64_t forgeSliver /*= 0*/, uint64_t forgeCores /*= 0*/) {
	if (m_isLivestreamViewer) {
		return;
	}

	if (version < 1100 || !hasProtocolFeature(protocolProfile, ProtocolFeature::ResourceBalancePackets)) {
		return;
	}

	sendResourceBalance(RESOURCE_BANK, bank);
	sendResourceBalance(RESOURCE_INVENTORY_MONEY, money);
	sendResourceBalance(RESOURCE_PREY_CARDS, preyCards);
	sendResourceBalance(RESOURCE_TASK_HUNTING, taskHunting);
	sendResourceBalance(RESOURCE_FORGE_DUST, forgeDust);
	sendResourceBalance(RESOURCE_FORGE_SLIVER, forgeSliver);
	sendResourceBalance(RESOURCE_FORGE_CORES, forgeCores);
}

void ProtocolGame::sendResourceBalance(Resource_t resourceType, uint64_t value) {
	if (version < 1100 || !hasProtocolFeature(protocolProfile, ProtocolFeature::ResourceBalancePackets)) {
		return;
	}

	if (oldProtocol && resourceType > RESOURCE_PREY_CARDS) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xEE);
	msg.addByte(resourceType);
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload) && resourceBalanceUsesU32(resourceType)) {
		msg.add<uint32_t>(static_cast<uint32_t>(std::min<uint64_t>(value, std::numeric_limits<uint32_t>::max())));
	} else {
		msg.add<uint64_t>(value);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCharmResourcesBalance(uint32_t charm /*= 0*/, uint32_t minorCharm /*= 0*/, uint32_t maxCharm /*= 0*/, uint32_t maxMinorCharm /*= 0*/) {
	sendCharmResourceBalance(RESOURCE_CHARM, charm);
	sendCharmResourceBalance(RESOURCE_MINOR_CHARM, minorCharm);
	sendCharmResourceBalance(RESOURCE_MAX_CHARM, maxCharm);
	sendCharmResourceBalance(RESOURCE_MAX_MINOR_CHARM, maxMinorCharm);
};

void ProtocolGame::sendCharmResourceBalance(CharmResource_t resourceType, uint32_t value) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xEE);

	msg.addByte(resourceType);
	msg.add<uint32_t>(value);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendSaleItemList(const std::vector<ShopBlock> &shopVector, const std::map<uint16_t, uint16_t> &inventoryMap) {
	sendResourceBalance(RESOURCE_BANK, player->getBankBalance());

	uint16_t currency = player->getShopOwner() ? player->getShopOwner()->getCurrency() : static_cast<uint16_t>(ITEM_GOLD_COIN);
	if (currency == ITEM_GOLD_COIN) {
		// Since we already have full inventory map we shouldn't call getMoney here - it is simply wasting cpu power
		uint64_t playerMoney = 0;
		auto it = inventoryMap.find(ITEM_CRYSTAL_COIN);
		if (it != inventoryMap.end()) {
			playerMoney += static_cast<uint64_t>(it->second) * 10000;
		}
		it = inventoryMap.find(ITEM_PLATINUM_COIN);
		if (it != inventoryMap.end()) {
			playerMoney += static_cast<uint64_t>(it->second) * 100;
		}
		it = inventoryMap.find(ITEM_GOLD_COIN);
		if (it != inventoryMap.end()) {
			playerMoney += static_cast<uint64_t>(it->second);
		}
		sendResourceBalance(RESOURCE_INVENTORY_MONEY, playerMoney);
	} else {
		uint64_t customCurrencyValue = 0;
		auto search = inventoryMap.find(currency);
		if (search != inventoryMap.end()) {
			customCurrencyValue += static_cast<uint64_t>(search->second);
		}
		sendResourceBalance(oldProtocol ? RESOURCE_INVENTORY_MONEY : RESOURCE_INVENTORY_CURRENCY_CUSTOM, customCurrencyValue);
	}

	NetworkMessage msg;
	msg.addByte(0x7B);

	if (oldProtocol) {
		msg.add<uint64_t>(player->getMoney() + player->getBankBalance());
	}

	uint16_t itemsToSend = 0;
	const uint16_t ItemsToSendLimit = oldProtocol ? 0xFF : 0xFFFF;
	auto msgPosition = msg.getBufferPosition();
	msg.skipBytes(oldProtocol ? 1 : 2);

	for (const ShopBlock &shopBlock : shopVector) {
		if (shopBlock.itemSellPrice == 0) {
			continue;
		}

		auto it = inventoryMap.find(shopBlock.itemId);
		if (it != inventoryMap.end()) {
			msg.add<uint16_t>(shopBlock.itemId);
			if (oldProtocol) {
				msg.addByte(static_cast<uint8_t>(std::min<uint16_t>(it->second, std::numeric_limits<uint8_t>::max())));
			} else {
				msg.add<uint16_t>(std::min<uint16_t>(it->second, std::numeric_limits<uint16_t>::max()));
			}
			if (++itemsToSend >= ItemsToSendLimit) {
				break;
			}
		}
	}

	msg.setBufferPosition(msgPosition);
	if (oldProtocol) {
		msg.addByte(static_cast<uint8_t>(itemsToSend));
	} else {
		msg.add<uint16_t>(itemsToSend);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketEnter(uint32_t depotId) {
	if (!player) {
		return;
	}

	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		player->sendCancelMessage("The market is not available on this client version.");
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xF6);

	if (oldProtocol) {
		msg.add<uint64_t>(player->getBankBalance());
	}

	msg.addByte(static_cast<uint8_t>(std::min<uint32_t>(IOMarket::getPlayerOfferCount(player->getGUID()), std::numeric_limits<uint8_t>::max())));

	std::shared_ptr<DepotLocker> depotLocker = player->getDepotLocker(depotId);
	if (!depotLocker) {
		msg.add<uint16_t>(0x00);
		writeToOutputBuffer(msg);
		return;
	}

	player->setInMarket(true);

	// Only use here locker items, itemVector is for use of Game::createMarketOffer
	auto [itemVector, lockerItems] = player->requestLockerItems(depotLocker, true);
	auto totalItemsCountPosition = msg.getBufferPosition();
	msg.skipBytes(2); // Total items count

	const uint16_t entriesLimit = std::numeric_limits<uint16_t>::max();
	uint16_t entriesSent = 0;
	bool limitReached = false;
	for (const auto &[itemId, tierAndCountMap] : lockerItems) {
		for (const auto &[tier, count] : tierAndCountMap) {
			if (entriesSent >= entriesLimit) {
				limitReached = true;
				break;
			}
			msg.add<uint16_t>(itemId);
			if (!oldProtocol && Item::items[itemId].upgradeClassification > 0) {
				msg.addByte(tier);
			}
			msg.add<uint16_t>(static_cast<uint16_t>(count));
			++entriesSent;
		}
		if (limitReached) {
			break;
		}
	}

	auto endPosition = msg.getBufferPosition();
	msg.setBufferPosition(totalItemsCountPosition);
	msg.add<uint16_t>(entriesSent);
	msg.setBufferPosition(endPosition);

	writeToOutputBuffer(msg);

	updateCoinBalance();
	sendResourcesBalance(player->getMoney(), player->getBankBalance(), player->getPreyCards(), player->getTaskHuntingPoints());
}

void ProtocolGame::sendCoinBalance() {
	if (!player) {
		return;
	}

	// send is updating
	// TODO: export this to it own function
	NetworkMessage msg;
	msg.addByte(0xF2);
	msg.addByte(0x01);
	writeToOutputBuffer(msg);

	msg.reset();

	// send update
	msg.addByte(0xDF);
	msg.addByte(0x01);

	msg.add<uint32_t>(player->coinBalance); // Normal Coins
	msg.add<uint32_t>(player->coinTransferableBalance); // Transferable Coins

	if (!oldProtocol) {
		msg.add<uint32_t>(player->coinBalance); // Reserved Auction Coins
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::updateCoinBalance() {
	if (!player) {
		return;
	}

	g_dispatcher().addEvent(
		[playerId = player->getID()] {
			const auto &threadPlayer = g_game().getPlayerByID(playerId);
			if (threadPlayer && threadPlayer->getAccount()) {
				const auto [coins, errCoin] = threadPlayer->getAccount()->getCoins(CoinType::Normal);
				const auto [transferCoins, errTCoin] = threadPlayer->getAccount()->getCoins(CoinType::Transferable);

				threadPlayer->coinBalance = coins;
				threadPlayer->coinTransferableBalance = transferCoins;
				threadPlayer->sendCoinBalance();
			}
		},
		__FUNCTION__,
		0,
		DispatcherLane::PlayerAction,
		player->getID()
	);
}

void ProtocolGame::sendMarketLeave() {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::MarketPackets)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xF7);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList &buyOffers, const MarketOfferList &sellOffers, uint8_t tier) {
	NetworkMessage msg;

	msg.addByte(0xF9);
	if (!oldProtocol) {
		msg.addByte(MARKETREQUEST_ITEM_BROWSE);
	}

	msg.add<uint16_t>(itemId);
	if (!oldProtocol && Item::items[itemId].upgradeClassification > 0) {
		msg.addByte(tier);
	}

	msg.add<uint32_t>(buyOffers.size());
	for (const MarketOffer &offer : buyOffers) {
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
		msg.addString(offer.playerName);
	}

	msg.add<uint32_t>(sellOffers.size());
	for (const MarketOffer &offer : sellOffers) {
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
		msg.addString(offer.playerName);
	}

	updateCoinBalance();
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketAcceptOffer(const MarketOfferEx &offer) {
	NetworkMessage msg;
	msg.addByte(0xF9);
	if (!oldProtocol) {
		msg.addByte(MARKETREQUEST_ITEM_BROWSE);
	}

	msg.add<uint16_t>(offer.itemId);
	if (!oldProtocol && Item::items[offer.itemId].upgradeClassification > 0) {
		msg.addByte(offer.tier);
	}

	if (offer.type == MARKETACTION_BUY) {
		msg.add<uint32_t>(0x01);
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
		msg.addString(offer.playerName);
		msg.add<uint32_t>(0x00);
	} else {
		msg.add<uint32_t>(0x00);
		msg.add<uint32_t>(0x01);
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
		msg.addString(offer.playerName);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketBrowseOwnOffers(const MarketOfferList &buyOffers, const MarketOfferList &sellOffers) {
	NetworkMessage msg;
	msg.addByte(0xF9);
	if (oldProtocol) {
		msg.add<uint16_t>(MARKETREQUEST_OWN_OFFERS_OLD);
	} else {
		msg.addByte(MARKETREQUEST_OWN_OFFERS);
	}

	msg.add<uint32_t>(buyOffers.size());
	for (const MarketOffer &offer : buyOffers) {
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.itemId);
		if (!oldProtocol && Item::items[offer.itemId].upgradeClassification > 0) {
			msg.addByte(offer.tier);
		}
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
	}

	msg.add<uint32_t>(sellOffers.size());
	for (const MarketOffer &offer : sellOffers) {
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.itemId);
		if (!oldProtocol && Item::items[offer.itemId].upgradeClassification > 0) {
			msg.addByte(offer.tier);
		}
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketCancelOffer(const MarketOfferEx &offer) {
	NetworkMessage msg;
	msg.addByte(0xF9);
	if (oldProtocol) {
		msg.add<uint16_t>(MARKETREQUEST_OWN_OFFERS_OLD);
	} else {
		msg.addByte(MARKETREQUEST_OWN_OFFERS);
	}

	if (offer.type == MARKETACTION_BUY) {
		msg.add<uint32_t>(0x01);
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.itemId);
		if (!oldProtocol && Item::items[offer.itemId].upgradeClassification > 0) {
			msg.addByte(offer.tier);
		}
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
		msg.add<uint32_t>(0x00);
	} else {
		msg.add<uint32_t>(0x00);
		msg.add<uint32_t>(0x01);
		msg.add<uint32_t>(offer.timestamp);
		msg.add<uint16_t>(offer.counter);
		msg.add<uint16_t>(offer.itemId);
		if (!oldProtocol && Item::items[offer.itemId].upgradeClassification > 0) {
			msg.addByte(offer.tier);
		}
		msg.add<uint16_t>(offer.amount);
		if (oldProtocol) {
			msg.add<uint32_t>(offer.price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(offer.price));
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketBrowseOwnHistory(const HistoryMarketOfferList &buyOffers, const HistoryMarketOfferList &sellOffers) {
	uint32_t i = 0;
	std::map<uint32_t, uint16_t> counterMap;
	uint32_t buyOffersToSend = std::min<uint32_t>(buyOffers.size(), 810 + std::max<int32_t>(0, 810 - sellOffers.size()));
	uint32_t sellOffersToSend = std::min<uint32_t>(sellOffers.size(), 810 + std::max<int32_t>(0, 810 - buyOffers.size()));

	NetworkMessage msg;
	msg.addByte(0xF9);
	if (oldProtocol) {
		msg.add<uint16_t>(MARKETREQUEST_OWN_HISTORY_OLD);
	} else {
		msg.addByte(MARKETREQUEST_OWN_HISTORY);
	}

	msg.add<uint32_t>(buyOffersToSend);
	for (auto it = buyOffers.begin(); i < buyOffersToSend; ++it, ++i) {
		msg.add<uint32_t>(it->timestamp);
		msg.add<uint16_t>(counterMap[it->timestamp]++);
		msg.add<uint16_t>(it->itemId);
		if (!oldProtocol && Item::items[it->itemId].upgradeClassification > 0) {
			msg.addByte(it->tier);
		}
		msg.add<uint16_t>(it->amount);
		if (oldProtocol) {
			msg.add<uint32_t>(it->price);
		} else {
			msg.add<uint64_t>(static_cast<uint64_t>(it->price));
		}
		msg.addByte(it->state);
	}

	counterMap.clear();
	i = 0;

	msg.add<uint32_t>(sellOffersToSend);
	for (auto it = sellOffers.begin(); i < sellOffersToSend; ++it, ++i) {
		msg.add<uint32_t>(it->timestamp);
		msg.add<uint16_t>(counterMap[it->timestamp]++);
		msg.add<uint16_t>(it->itemId);
		if (Item::items[it->itemId].upgradeClassification > 0) {
			msg.addByte(it->tier);
		}
		msg.add<uint16_t>(it->amount);
		msg.add<uint64_t>(it->price);
		msg.addByte(it->state);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendForgingData() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x86);

	std::map<uint8_t, uint16_t> tierCorePrices;
	std::map<uint8_t, uint64_t> convergenceFusionPrices;
	std::map<uint8_t, uint64_t> convergenceTransferPrices;

	const auto classifications = g_game().getItemsClassifications();
	msg.addByte(classifications.size());
	for (const auto &classification : classifications) {
		msg.addByte(classification->id);
		msg.addByte(classification->tiers.size());
		for (const auto &[tier, tierInfo] : classification->tiers) {
			msg.addByte(tier - 1);
			msg.add<uint64_t>(tierInfo.regularPrice);
			tierCorePrices[tier] = tierInfo.corePrice;
			convergenceFusionPrices[tier] = tierInfo.convergenceFusionPrice;
			convergenceTransferPrices[tier] = tierInfo.convergenceTransferPrice;
		}
	}

	// Version 13.30
	// Forge Config Bytes

	// Exalted core table per tier
	msg.addByte(static_cast<uint8_t>(tierCorePrices.size()));
	for (const auto &[tier, cores] : tierCorePrices) {
		msg.addByte(tier);
		msg.addByte(cores);
	}

	// Convergence fusion prices per tier
	msg.addByte(static_cast<uint8_t>(convergenceFusionPrices.size()));
	for (const auto &[tier, price] : convergenceFusionPrices) {
		msg.addByte(tier - 1);
		msg.add<uint64_t>(price);
	}

	// Convergence transfer prices per tier
	msg.addByte(static_cast<uint8_t>(convergenceTransferPrices.size()));
	for (const auto &[tier, price] : convergenceTransferPrices) {
		msg.addByte(tier);
		msg.add<uint64_t>(price);
	}

	// (conversion) (left column top) Cost to make 1 bottom item - 20
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_COST_ONE_SLIVER)));
	// (conversion) (left column bottom) How many items to make - 3
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_SLIVER_AMOUNT)));
	// (conversion) (middle column top) Cost to make 1 - 50
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_CORE_COST)));
	// (conversion) (right column top) Current stored dust limit minus this number = cost to increase stored dust limit - 75
	msg.addByte(75);
	// (conversion) (right column bottom) Starting stored dust limit
	msg.add<uint16_t>(player->getForgeDustLevel());
	// (conversion) (right column bottom) Max stored dust limit - 325
	msg.add<uint16_t>(g_configManager().getNumber(FORGE_MAX_DUST));
	// (normal fusion) dust cost - 100
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_FUSION_DUST_COST)));
	// (convergence fusion) dust cost - 130
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_CONVERGENCE_FUSION_DUST_COST)));
	// (normal transfer) dust cost - 100
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_TRANSFER_DUST_COST)));
	// (convergence transfer) dust cost - 160
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_CONVERGENCE_TRANSFER_DUST_COST)));
	// (fusion) Base success rate - 50
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_BASE_SUCCESS_RATE)));
	// (fusion) Bonus success rate - 15
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_BONUS_SUCCESS_RATE)));
	// (fusion) Tier loss chance after reduction - 50
	msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(FORGE_TIER_LOSS_REDUCTION)));

	// Update player resources
	parseSendResourceBalance();

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendOpenForge() {
	// We will use it when sending the bytes to send the item information to the client
	std::map<uint16_t, std::map<uint8_t, uint16_t>> fusionItemsMap;
	std::map<int32_t, std::map<uint16_t, std::map<uint8_t, uint16_t>>> convergenceFusionItemsMap;
	std::map<int32_t, std::map<uint16_t, std::map<uint8_t, uint16_t>>> convergenceTransferItemsMap;
	std::map<uint16_t, std::map<uint8_t, uint16_t>> donorTierItemMap;
	std::map<uint16_t, std::map<uint8_t, uint16_t>> receiveTierItemMap;

	auto maxConfigTier = g_configManager().getNumber(FORGE_MAX_ITEM_TIER);

	/*
	 *Start - Parsing items informations
	 */
	for (const auto &item : player->getAllInventoryItems(true)) {
		if (item->hasImbuements()) {
			continue;
		}

		auto itemClassification = item->getClassification();
		auto itemTier = item->getTier();
		auto maxTier = (itemClassification == 4 ? maxConfigTier : itemClassification);
		// Save fusion items on map
		if (itemClassification != 0 && itemTier < maxTier) {
			getForgeInfoMap(item, fusionItemsMap);
		}

		if (itemClassification > 0) {
			if (itemClassification < 4 && itemTier > maxTier) {
				continue;
			}
			// Save transfer (donator of tier) items on map
			if (itemTier > 1) {
				getForgeInfoMap(item, donorTierItemMap);
			}
			// Save transfer (receiver of tier) items on map
			if (itemTier == 0) {
				getForgeInfoMap(item, receiveTierItemMap);
			}
			if (itemClassification == 4) {
				auto slotPosition = item->getSlotPosition();
				if ((slotPosition & SLOTP_TWO_HAND) != 0) {
					slotPosition = SLOTP_HAND;
				}
				getForgeInfoMap(item, convergenceFusionItemsMap[slotPosition]);
				getForgeInfoMap(item, convergenceTransferItemsMap[item->getClassification()]);
			}
		}
	}

	// Checking size of map to send in the addByte (total fusion items count)
	uint8_t fusionTotalItemsCount = 0;
	for (const auto &[itemId, tierAndCountMap] : fusionItemsMap) {
		for (const auto &[itemTier, itemCount] : tierAndCountMap) {
			if (itemCount >= 2) {
				fusionTotalItemsCount++;
			}
		}
	}

	/*
	 * Start - Sending bytes
	 */
	NetworkMessage msg;

	// Header byte (135)
	msg.addByte(0x87);

	msg.add<uint16_t>(fusionTotalItemsCount);
	for (const auto &[itemId, tierAndCountMap] : fusionItemsMap) {
		for (const auto &[itemTier, itemCount] : tierAndCountMap) {
			if (itemCount >= 2) {
				msg.addByte(0x01); // Number of friend items?
				msg.add<uint16_t>(itemId);
				msg.addByte(itemTier);
				msg.add<uint16_t>(itemCount);
			}
		}
	}

	// msg.add<uint16_t>(convergenceItemsMap.size());
	auto convergenceFusionCountPosition = msg.getBufferPosition();
	msg.skipBytes(2);
	uint16_t convergenceFusionCount = 0;
	/*
	for each convergence fusion (1 per item slot, only class 4):
	1 byte: count fusable items
	for each fusable item:
	    2 bytes: item id
	    1 byte: tier
	    2 bytes: count
	*/
	for (const auto &[slot, itemMap] : convergenceFusionItemsMap) {
		uint8_t totalItemsCount = 0;
		auto totalItemsCountPosition = msg.getBufferPosition();
		msg.skipBytes(1); // Total items count
		for (const auto &[itemId, tierAndCountMap] : itemMap) {
			for (const auto &[tier, itemCount] : tierAndCountMap) {
				if (tier >= maxConfigTier) {
					continue;
				}
				totalItemsCount++;
				msg.add<uint16_t>(itemId);
				msg.addByte(tier);
				msg.add<uint16_t>(itemCount);
			}
		}
		auto endPosition = msg.getBufferPosition();
		msg.setBufferPosition(totalItemsCountPosition);
		if (totalItemsCount > 0) {
			msg.addByte(totalItemsCount);
			msg.setBufferPosition(endPosition);
			convergenceFusionCount++;
		}
	}

	auto transferTotalCountPosition = msg.getBufferPosition();
	msg.setBufferPosition(convergenceFusionCountPosition);
	msg.add<uint16_t>(convergenceFusionCount);
	msg.setBufferPosition(transferTotalCountPosition);

	auto transferTotalCount = donorTierItemMap.size();
	msg.addByte(transferTotalCount);
	if (transferTotalCount > 0) {
		for (const auto &[itemId, tierAndCountMap] : donorTierItemMap) {
			// Let's access the itemType to check the item's (donator of tier) classification level
			// Must be the same as the item that will receive the tier
			const ItemType &donorType = Item::items[itemId];
			auto donorSlotPosition = donorType.slotPosition;
			if ((donorSlotPosition & SLOTP_TWO_HAND) != 0) {
				donorSlotPosition = SLOTP_HAND;
			}

			// Total count of item (donator of tier)
			auto donorTierTotalItemsCount = tierAndCountMap.size();
			msg.add<uint16_t>(donorTierTotalItemsCount);
			for (const auto &[donorItemTier, donorItemCount] : tierAndCountMap) {
				msg.add<uint16_t>(itemId);
				msg.addByte(donorItemTier);
				msg.add<uint16_t>(donorItemCount);
			}

			uint16_t receiveTierTotalItemCount = 0;
			for (const auto &[iteratorItemId, unusedTierAndCountMap] : receiveTierItemMap) {
				// Let's access the itemType to check the item's (receiver of tier) classification level
				const ItemType &receiveType = Item::items[iteratorItemId];
				auto receiveSlotPosition = receiveType.slotPosition;
				if ((receiveSlotPosition & SLOTP_TWO_HAND) != 0) {
					receiveSlotPosition = SLOTP_HAND;
				}
				if (donorType.upgradeClassification == receiveType.upgradeClassification && donorSlotPosition == receiveSlotPosition) {
					receiveTierTotalItemCount++;
				}
			}

			// Total count of item (receiver of tier)
			msg.add<uint16_t>(receiveTierTotalItemCount);
			if (receiveTierTotalItemCount > 0) {
				for (const auto &[receiveItemId, receiveTierAndCountMap] : receiveTierItemMap) {
					// Let's access the itemType to check the item's (receiver of tier) classification level
					const ItemType &receiveType = Item::items[receiveItemId];
					auto receiveSlotPosition = receiveType.slotPosition;
					if ((receiveSlotPosition & SLOTP_TWO_HAND) != 0) {
						receiveSlotPosition = SLOTP_HAND;
					}
					if (donorType.upgradeClassification == receiveType.upgradeClassification && donorSlotPosition == receiveSlotPosition) {
						for (const auto &[receiveItemTier, receiveItemCount] : receiveTierAndCountMap) {
							msg.add<uint16_t>(receiveItemId);
							msg.add<uint16_t>(receiveItemCount);
						}
					}
				}
			}
		}
	}

	auto convergenceCountPosition = msg.getBufferPosition();
	msg.skipBytes(1);
	uint8_t convergenceTransferCount = 0;

	/*
	for each convergence transfer:
	    2 bytes: count donors
	    for each donor:
	        2 bytes: item id
	        1 byte: tier
	        2 bytes: count
	    2 bytes: count receivers
	    for each receiver:
	        2 bytes: item id
	        2 bytes: count
	*/
	for (const auto &[slot, itemMap] : convergenceTransferItemsMap) {
		uint16_t donorCount = 0;
		uint16_t receiverCount = 0;
		auto donorCountPosition = msg.getBufferPosition();
		msg.skipBytes(2); // Donor count
		for (const auto &[itemId, tierAndCountMap] : itemMap) {
			for (const auto [tier, itemCount] : tierAndCountMap) {
				if (tier >= 1) {
					donorCount++;
					msg.add<uint16_t>(itemId);
					msg.addByte(tier);
					msg.add<uint16_t>(itemCount);
				} else {
					receiverCount++;
				}
			}
		}
		if (donorCount == 0 && receiverCount == 0) {
			msg.setBufferPosition(donorCountPosition);
			continue;
		}
		auto receiverCountPosition = msg.getBufferPosition();
		msg.setBufferPosition(donorCountPosition);
		msg.add<uint16_t>(donorCount);
		++convergenceTransferCount;
		msg.setBufferPosition(receiverCountPosition);
		msg.add<uint16_t>(receiverCount);
		for (const auto &[itemId, tierAndCountMap] : itemMap) {
			for (const auto [tier, itemCount] : tierAndCountMap) {
				if (tier == 0) {
					msg.add<uint16_t>(itemId);
					msg.add<uint16_t>(itemCount);
				}
			}
		}
	}
	auto dustLevelPosition = msg.getBufferPosition();
	msg.setBufferPosition(convergenceCountPosition);
	msg.addByte(convergenceTransferCount);
	msg.setBufferPosition(dustLevelPosition);

	msg.add<uint16_t>(player->getForgeDustLevel()); // Player dust limit
	writeToOutputBuffer(msg);
	// Update forging informations
	sendForgingData();
}

void ProtocolGame::parseForgeEnter(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	// 0xBF -> 0 = fusion, 1 = transfer, 2 = dust to sliver, 3 = sliver to core, 4 = increase dust limit
	const auto actionType = static_cast<ForgeAction_t>(msg.getByte());

	bool convergence = false;
	uint16_t firstItem = 0;
	uint8_t tier = 0;
	uint16_t secondItem = 0;

	if (actionType == ForgeAction_t::FUSION || actionType == ForgeAction_t::TRANSFER) {
		convergence = msg.getByte();
		firstItem = msg.get<uint16_t>();
		tier = msg.getByte();
		secondItem = msg.get<uint16_t>();
	}

	if (actionType == ForgeAction_t::FUSION) {
		const bool usedCore = convergence ? false : msg.getByte();
		const bool reduceTierLoss = convergence ? false : msg.getByte();
		g_game().playerForgeFuseItems(player->getID(), actionType, firstItem, tier, secondItem, usedCore, reduceTierLoss, convergence);
	} else if (actionType == ForgeAction_t::TRANSFER) {
		g_game().playerForgeTransferItemTier(player->getID(), actionType, firstItem, tier, secondItem, convergence);
	} else if (actionType <= ForgeAction_t::INCREASELIMIT) {
		g_game().playerForgeResourceConversion(player->getID(), actionType);
	}
}

void ProtocolGame::parseForgeBrowseHistory(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t page = 0;
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		msg.get<uint16_t>();
		page = msg.getByte(true);
	} else {
		page = msg.getByte(true);
	}

	g_game().playerBrowseForgeHistory(player->getID(), page);
}

void ProtocolGame::sendForgeResult(ForgeAction_t actionType, uint16_t leftItemId, uint8_t leftTier, uint16_t rightItemId, uint8_t rightTier, bool success, uint8_t bonus, uint8_t coreCount, bool convergence) {
	NetworkMessage msg;
	msg.addByte(0x8A);

	// 0 = fusion | 1 = transfer
	msg.addByte(static_cast<uint8_t>(actionType));
	msg.addByte(convergence);

	if (convergence && actionType == ForgeAction_t::FUSION) {
		success = true;
		std::swap(leftItemId, rightItemId);
	}

	msg.addByte(success);

	msg.add<uint16_t>(leftItemId);
	msg.addByte(leftTier);
	msg.add<uint16_t>(rightItemId);
	msg.addByte(rightTier);

	if (actionType == ForgeAction_t::TRANSFER) {
		msg.addByte(0x00); // Bonus type always none for transfer
	} else {
		msg.addByte(bonus); // Roll fusion bonus
		// Core kept
		if (bonus == 2) {
			msg.addByte(coreCount);
		} else if (bonus >= 4 && bonus <= 8) {
			msg.add<uint16_t>(leftItemId);
			msg.addByte(leftTier);
		}
	}

	writeToOutputBuffer(msg);
	g_logger().debug("Send forge fusion: type {}, left item {}, left tier {}, right item {}, rightTier {}, success {}, bonus {}, coreCount {}, convergence {}", fmt::underlying(actionType), leftItemId, leftTier, rightItemId, rightTier, success, bonus, coreCount, convergence);
	sendOpenForge();
}

void ProtocolGame::sendForgeHistory(uint8_t page) {
	static constexpr size_t entriesPerPage = 9;
	const auto requestedPage = static_cast<size_t>(page) + 1;
	const auto &historyVector = player->forgeHistory().get();
	const auto historyVectorLen = historyVector.size();

	uint16_t currentPage = 1;
	uint16_t lastPage = 1;

	std::vector<ForgeHistory> historyPerPage;
	if (historyVectorLen > 0) {
		const auto lastPageCount = ((historyVectorLen - 1) / entriesPerPage) + 1;
		lastPage = static_cast<uint16_t>(std::min<size_t>(lastPageCount, std::numeric_limits<uint16_t>::max()));
		currentPage = static_cast<uint16_t>(std::min(requestedPage, static_cast<size_t>(lastPage)));

		const auto pageFirstEntry = historyVectorLen - (static_cast<size_t>(currentPage) - 1) * entriesPerPage;
		const auto pageLastEntry = historyVectorLen > static_cast<size_t>(currentPage) * entriesPerPage ? historyVectorLen - static_cast<size_t>(currentPage) * entriesPerPage : 0;

		for (auto entry = pageFirstEntry; entry > pageLastEntry; --entry) {
			[[maybe_unused]] auto &history_ref = historyPerPage.emplace_back(historyVector[entry - 1]);
		}
	}

	auto historyPageToSend = historyPerPage.size();
	NetworkMessage msg;
	msg.addByte(0x88);
	msg.add<uint16_t>(currentPage - 1); // Current page
	msg.add<uint16_t>(lastPage); // Last page
	msg.addByte(static_cast<uint8_t>(historyPageToSend)); // History to send

	if (historyPageToSend > 0) {
		for (const auto &history : historyPerPage) {
			auto action = magic_enum::enum_integer(history.actionType);
			msg.add<uint32_t>(static_cast<uint32_t>(history.createdAt));
			msg.addByte(action);
			msg.addString(history.description);
			msg.addByte((history.bonus >= 1 && history.bonus < 8) ? 0x01 : 0x00);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendForgeError(const ReturnValue returnValue) {
	sendMessageDialog(getReturnMessage(returnValue));
	closeForgeWindow();
}

void ProtocolGame::closeForgeWindow() {
	NetworkMessage msg;
	msg.addByte(0x89);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) {
	NetworkMessage msg;
	msg.addByte(0xF8);
	msg.add<uint16_t>(itemId);
	const ItemType &it = Item::items[itemId];

	if (!oldProtocol && it.upgradeClassification > 0) {
		msg.addByte(tier);
	}

	if (it.armor != 0) {
		msg.addString(std::to_string(it.armor));
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.isRanged()) {
		std::ostringstream ss;
		bool separator = false;

		if (it.attack != 0) {
			ss << "attack +" << it.attack;
			separator = true;
		}

		if (it.hitChance != 0) {
			if (separator) {
				ss << ", ";
			}
			ss << "chance to hit +" << static_cast<int16_t>(it.hitChance) << "%";
			separator = true;
		}

		if (it.shootRange != 0) {
			if (separator) {
				ss << ", ";
			}
			ss << static_cast<uint16_t>(it.shootRange) << " fields";
		}
		msg.addString(ss.str());
	} else {
		std::string attackDescription;
		if (it.abilities && it.abilities->elementType != COMBAT_NONE && it.abilities->elementDamage != 0) {
			attackDescription = fmt::format("{} {}", it.abilities->elementDamage, getCombatName(it.abilities->elementType));
		}

		if (it.attack != 0 && !attackDescription.empty()) {
			attackDescription = fmt::format("{} physical + {}", it.attack, attackDescription);
		} else if (it.attack != 0 && attackDescription.empty()) {
			attackDescription = std::to_string(it.attack);
		}

		msg.addString(attackDescription);
	}

	if (it.isContainer()) {
		msg.addString(std::to_string(it.maxItems));
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.defense != 0 || it.isMissile()) {
		if (it.extraDefense != 0) {
			std::ostringstream ss;
			ss << it.defense << ' ' << std::showpos << it.extraDefense << std::noshowpos;
			msg.addString(ss.str());
		} else {
			msg.addString(std::to_string(it.defense));
		}
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (!it.description.empty()) {
		const std::string &descr = it.description;
		if (descr.back() == '.') {
			msg.addString(std::string(descr, 0, descr.length() - 1));
		} else {
			msg.addString(descr);
		}
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.decayTime != 0) {
		std::ostringstream ss;
		ss << it.decayTime << " seconds";
		msg.addString(ss.str());
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.abilities) {
		std::ostringstream ss;
		bool separator = false;

		for (size_t i = 0; i < COMBAT_COUNT; ++i) {
			if (it.abilities->absorbPercent[i] == 0) {
				continue;
			}

			if (separator) {
				ss << ", ";
			} else {
				separator = true;
			}

			ss << fmt::format("{} {:+}%", getCombatName(indexToCombatType(i)), it.abilities->absorbPercent[i]);
		}

		msg.addString(ss.str());
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.minReqLevel != 0) {
		msg.addString(std::to_string(it.minReqLevel));
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.minReqMagicLevel != 0) {
		msg.addString(std::to_string(it.minReqMagicLevel));
	} else {
		msg.add<uint16_t>(0x00);
	}

	msg.addString(it.vocationString);
	msg.addString(it.runeSpellName);

	if (it.abilities) {
		std::ostringstream ss;
		bool separator = false;

		for (uint8_t i = SKILL_FIRST; i <= SKILL_FISHING; i++) {
			if (i == SKILL_MANA_LEECH_CHANCE || i == SKILL_LIFE_LEECH_CHANCE) {
				continue;
			}

			if (!it.abilities->skills[i]) {
				continue;
			}

			if (separator) {
				ss << ", ";
			} else {
				separator = true;
			}

			ss << fmt::format("{} {:+}", getSkillName(i), it.abilities->skills[i]);
		}

		for (uint8_t i = SKILL_CRITICAL_HIT_CHANCE; i <= SKILL_LAST; i++) {
			if (i == SKILL_MANA_LEECH_CHANCE || i == SKILL_LIFE_LEECH_CHANCE) {
				continue;
			}

			auto skills = it.abilities->skills[i];
			if (!skills) {
				continue;
			}

			if (separator) {
				ss << ", ";
			} else {
				separator = true;
			}

			ss << fmt::format("{} {:+}%", getSkillName(i), skills / 100.0);
		}

		if (it.abilities->stats[STAT_MAGICPOINTS] != 0) {
			if (separator) {
				ss << ", ";
			} else {
				separator = true;
			}

			ss << fmt::format(" magic level {:+}", it.abilities->stats[STAT_MAGICPOINTS]);
		}

		// Version 12.72 (Specialized magic level modifier)
		for (uint8_t i = 1; i <= 11; i++) {
			if (it.abilities->specializedMagicLevel[i]) {
				if (separator) {
					ss << ", ";
				} else {
					separator = true;
				}
				auto combatName = getCombatName(indexToCombatType(i));
				ss << fmt::format("{}magic level +{}", combatName, it.abilities->specializedMagicLevel[i]);
			}
		}

		if (it.abilities->speed != 0) {
			if (separator) {
				ss << ", ";
			}

			ss << fmt::format("speed {:+}", (it.abilities->speed >> 1));
		}

		msg.addString(ss.str());
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (it.charges != 0) {
		msg.addString(std::to_string(it.charges));
	} else {
		msg.add<uint16_t>(0x00);
	}

	std::string weaponName = getWeaponName(it.weaponType);

	if (it.slotPosition & SLOTP_TWO_HAND) {
		if (!weaponName.empty()) {
			weaponName += ", two-handed";
		} else {
			weaponName = "two-handed";
		}
	}

	msg.addString(weaponName);

	if (it.weight != 0) {
		std::ostringstream ss;
		if (it.weight < 10) {
			ss << "0.0" << it.weight;
		} else if (it.weight < 100) {
			ss << "0." << it.weight;
		} else {
			std::string weightString = std::to_string(it.weight);
			weightString.insert(weightString.end() - 2, '.');
			ss << weightString;
		}
		ss << " oz";
		msg.addString(ss.str());
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (!oldProtocol) {
		std::string augmentsDescription = it.parseAugmentDescription(true);
		if (!augmentsDescription.empty()) {
			msg.addString(augmentsDescription);
		} else {
			msg.add<uint16_t>(0x00); // no augments
		}
	}

	if (it.imbuementSlot > 0) {
		msg.addString(std::to_string(it.imbuementSlot));
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (!oldProtocol) {
		// Version 12.70 new skills
		if (it.abilities) {
			if (it.abilities->magicShieldCapacityFlat > 0) {
				msg.addString(fmt::format("{:+} and {}%", it.abilities->magicShieldCapacityFlat, it.abilities->magicShieldCapacityPercent));
			} else {
				msg.add<uint16_t>(0x00);
			}

			if (it.abilities->cleavePercent > 0) {
				msg.addString(fmt::format("{}%", it.abilities->cleavePercent));
			} else {
				msg.add<uint16_t>(0x00);
			}

			if (it.abilities->reflectFlat[COMBAT_PHYSICALDAMAGE] > 0) {
				msg.addString(std::to_string(it.abilities->reflectFlat[COMBAT_PHYSICALDAMAGE]));
			} else {
				msg.add<uint16_t>(0x00);
			}

			if (it.abilities->perfectShotDamage > 0) {
				msg.addString(fmt::format("{:+} at range {}", it.abilities->perfectShotDamage, static_cast<unsigned>(it.abilities->perfectShotRange)));
			} else {
				msg.add<uint16_t>(0x00);
			}
		} else {
			// Send empty skills
			// Cleave modifier
			msg.add<uint16_t>(0x00);
			// Magic shield capacity
			msg.add<uint16_t>(0x00);
			// Damage reflection modifier
			msg.add<uint16_t>(0x00);
			// Perfect shot modifier
			msg.add<uint16_t>(0x00);
		}

		// Upgrade classification
		if (it.upgradeClassification > 0) {
			msg.addString(std::to_string(it.upgradeClassification));
		} else {
			msg.add<uint16_t>(0x00);
		}

		if (it.elementalBond != COMBAT_NONE) {
			msg.addString(toPascalCase(getCombatName(it.elementalBond)));
		} else {
			msg.add<uint16_t>(0x00);
		}

		if (it.mantra > 0) {
			msg.addString(std::to_string(it.mantra));
		} else {
			msg.add<uint16_t>(0x00);
		}

		if (clientVersion >= 1510) {
			msg.addString(getMarketDetailImbuementEffect(itemId));
		}

		if (it.upgradeClassification > 0 && tier > 0) {
			std::ostringstream ss;

			double chance;
			if (it.isWeapon()) {
				chance = (0.05 * tier * tier) + (0.4 * tier) + 0.05;
				ss << fmt::format("{} ({:.2f}% Onslaught)", static_cast<uint16_t>(tier), chance);
			} else if (it.isHelmet()) {
				chance = (0.05 * tier * tier) + (1.9 * tier) + 0.05;
				ss << fmt::format("{} ({:.2f}% Momentum)", static_cast<uint16_t>(tier), chance);
			} else if (it.isArmor()) {
				chance = (0.0307576 * tier * tier) + (0.440697 * tier) + 0.026;
				ss << fmt::format("{} ({:.2f}% Ruse)", static_cast<uint16_t>(tier), chance);
			} else if (it.isLegs()) {
				chance = (0.0127 * tier * tier) + (0.1070 * tier) + 0.0073;
				ss << fmt::format("{} ({:.2f}% Transcendence)", static_cast<uint16_t>(tier), chance);
			} else if (it.isBoots()) {
				chance = (0.4 * tier * tier) + (1.7 * tier) + 0.4;
				ss << fmt::format("{} ({:.2f}% Amplification)", static_cast<uint16_t>(tier), chance);
			}
			msg.addString(ss.str());
		} else if (it.upgradeClassification > 0 && tier == 0) {
			msg.addString(std::to_string(tier));
		} else {
			msg.add<uint16_t>(0x00);
		}
	}

	const auto &purchaseStatsMap = IOMarket::getInstance().getPurchaseStatistics();
	auto purchaseIterator = purchaseStatsMap.find(itemId);
	if (purchaseIterator != purchaseStatsMap.end()) {
		const auto &tierStatsMap = purchaseIterator->second;
		auto tierStatsIter = tierStatsMap.find(tier);
		if (tierStatsIter != tierStatsMap.end()) {
			const auto &purchaseStatistics = tierStatsIter->second;
			msg.addByte(0x01);
			msg.add<uint32_t>(purchaseStatistics.numTransactions);
			if (oldProtocol) {
				msg.add<uint32_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), purchaseStatistics.totalPrice));
				msg.add<uint32_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), purchaseStatistics.highestPrice));
				msg.add<uint32_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), purchaseStatistics.lowestPrice));
			} else {
				msg.add<uint64_t>(purchaseStatistics.totalPrice);
				msg.add<uint64_t>(purchaseStatistics.highestPrice);
				msg.add<uint64_t>(purchaseStatistics.lowestPrice);
			}
		} else {
			msg.addByte(0x00);
		}
	} else {
		msg.addByte(0x00); // send to old protocol ?
	}

	const auto &saleStatsMap = IOMarket::getInstance().getSaleStatistics();
	auto saleIterator = saleStatsMap.find(itemId);
	if (saleIterator != saleStatsMap.end()) {
		const auto &tierStatsMap = saleIterator->second;
		auto tierStatsIter = tierStatsMap.find(tier);
		if (tierStatsIter != tierStatsMap.end()) {
			const auto &saleStatistics = tierStatsIter->second;
			msg.addByte(0x01);
			msg.add<uint32_t>(saleStatistics.numTransactions);
			if (oldProtocol) {
				msg.add<uint32_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), saleStatistics.totalPrice));
				msg.add<uint32_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), saleStatistics.highestPrice));
				msg.add<uint32_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), saleStatistics.lowestPrice));
			} else {
				msg.add<uint64_t>(std::min<uint64_t>(std::numeric_limits<uint32_t>::max(), saleStatistics.totalPrice));
				msg.add<uint64_t>(saleStatistics.highestPrice);
				msg.add<uint64_t>(saleStatistics.lowestPrice);
			}
		} else {
			msg.addByte(0x00);
		}
	} else {
		msg.addByte(0x00); // send to old protocol ?
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTradeItemRequest(const std::string &traderName, const std::shared_ptr<Item> &item, bool ack) {
	NetworkMessage msg;

	if (ack) {
		msg.addByte(0x7D);
	} else {
		msg.addByte(0x7E);
	}

	msg.addString(traderName);

	if (std::shared_ptr<Container> tradeContainer = item->getContainer()) {
		std::list<std::shared_ptr<Container>> listContainer { tradeContainer };
		std::list<std::shared_ptr<Item>> itemList { tradeContainer };
		while (!listContainer.empty()) {
			const auto &container = listContainer.front();
			for (const auto &containerItem : container->getItemList()) {
				const auto &tmpContainer = containerItem->getContainer();
				if (tmpContainer) {
					listContainer.emplace_back(tmpContainer);
				}
				itemList.emplace_back(containerItem);
			}

			// Removes the object after processing everything, avoiding memory usage after freeing
			listContainer.pop_front();
		}

		msg.addByte(itemList.size());
		for (const std::shared_ptr<Item> &listItem : itemList) {
			AddItem(msg, listItem);
		}
	} else {
		msg.addByte(0x01);
		AddItem(msg, item);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCloseTrade() {
	NetworkMessage msg;
	msg.addByte(0x7F);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCloseContainer(uint8_t cid) {
	NetworkMessage msg;
	msg.addByte(0x6F);
	msg.addByte(cid);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureTurn(const std::shared_ptr<Creature> &creature, uint32_t stackPos) {
	if (!canSee(creature)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x6B);
	msg.addPosition(creature->getPosition());
	msg.addByte(static_cast<uint8_t>(stackPos));
	msg.add<uint16_t>(0x63);
	msg.add<uint32_t>(creature->getID());
	msg.addByte(creature->getDirection());
	if (version >= 953) {
		msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureSay(const std::shared_ptr<Creature> &creature, SpeakClasses type, const std::string &text, const Position* pos /* = nullptr*/) {
	NetworkMessage msg;
	msg.addByte(0xAA);

	static uint32_t statementId = 0;
	msg.add<uint32_t>(++statementId);

	msg.addString(creature->getName());

	if (!oldProtocol) {
		msg.addByte(0x00); // Show (Traded)
	}

	// Add level only for players
	if (auto speaker = creature->getPlayer()) {
		msg.add<uint16_t>(speaker->getLevel());
	} else {
		msg.add<uint16_t>(0x00);
	}

	uint8_t clientType = static_cast<uint8_t>(type);
	if (isCipsoft860Profile(protocolProfile)) {
		clientType = translateCipsoft860SpeakClassToClient(type);
		if (clientType == cipsoft860TalkNone) {
			return;
		}
	} else if (oldProtocol && type >= TALKTYPE_MONSTER_LAST_OLDPROTOCOL && type != TALKTYPE_CHANNEL_R2) {
		clientType = TALKTYPE_MONSTER_SAY;
	}

	msg.addByte(clientType);

	if (pos) {
		msg.addPosition(*pos);
	} else {
		msg.addPosition(creature->getPosition());
	}

	msg.addString(text);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendToChannel(const std::shared_ptr<Creature> &creature, SpeakClasses type, const std::string &text, uint16_t channelId) {
	NetworkMessage msg;
	msg.addByte(0xAA);

	static uint32_t statementId = 0;
	msg.add<uint32_t>(++statementId);
	if (!creature) {
		msg.add<uint32_t>(0x00);
		if (!oldProtocol && statementId != 0) {
			msg.addByte(0x00); // Show (Traded)
		}
	} else if (type == TALKTYPE_CHANNEL_R2) {
		msg.add<uint32_t>(0x00);
		if (!oldProtocol && statementId != 0) {
			msg.addByte(0x00); // Show (Traded)
		}
		type = TALKTYPE_CHANNEL_R1;
	} else {
		msg.addString(creature->getName());
		if (!oldProtocol && statementId != 0) {
			msg.addByte(0x00); // Show (Traded)
		}

		// Add level only for players
		if (auto speaker = creature->getPlayer()) {
			msg.add<uint16_t>(speaker->getLevel());
		} else {
			msg.add<uint16_t>(0x00);
		}
	}

	uint8_t clientType = static_cast<uint8_t>(type);
	if (isCipsoft860Profile(protocolProfile)) {
		clientType = translateCipsoft860SpeakClassToClient(type);
		if (clientType == cipsoft860TalkNone) {
			return;
		}
	} else if (oldProtocol && type >= TALKTYPE_MONSTER_LAST_OLDPROTOCOL && type != TALKTYPE_CHANNEL_R2) {
		clientType = TALKTYPE_CHANNEL_O;
	}

	msg.addByte(clientType);

	msg.add<uint16_t>(channelId);
	msg.addString(text);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPrivateMessage(const std::shared_ptr<Player> &speaker, SpeakClasses type, const std::string &text) {
	NetworkMessage msg;
	msg.addByte(0xAA);
	static uint32_t statementId = 0;
	msg.add<uint32_t>(++statementId);
	if (speaker) {
		msg.addString(speaker->getName());
		if (!oldProtocol && statementId != 0) {
			msg.addByte(0x00); // Show (Traded)
		}
		msg.add<uint16_t>(speaker->getLevel());
	} else {
		msg.add<uint32_t>(0x00);
		if (!oldProtocol && statementId != 0) {
			msg.addByte(0x00); // Show (Traded)
		}
	}

	uint8_t clientType = static_cast<uint8_t>(type);
	if (isCipsoft860Profile(protocolProfile)) {
		clientType = translateCipsoft860SpeakClassToClient(type);
		if (clientType == cipsoft860TalkNone) {
			return;
		}
	} else if (oldProtocol && type >= TALKTYPE_MONSTER_LAST_OLDPROTOCOL && type != TALKTYPE_CHANNEL_R2) {
		clientType = TALKTYPE_PRIVATE_TO;
	}

	msg.addByte(clientType);

	msg.addString(text);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCancelTarget() {
	NetworkMessage msg;
	msg.addByte(0xA3);
	msg.add<uint32_t>(0x00);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendChangeSpeed(const std::shared_ptr<Creature> &creature, uint16_t speed) {
	NetworkMessage msg;
	msg.addByte(0x8F);
	msg.add<uint32_t>(creature->getID());
	if (!oldProtocol || version >= 1059) {
		msg.add<uint16_t>(creature->getBaseSpeed());
	}
	const bool usesLegacyDoubledSpeed = oldProtocol && !hasProtocolFeature(protocolProfile, ProtocolFeature::LoginSpeedFormula);
	const auto clientSpeed = usesLegacyDoubledSpeed
		? static_cast<uint16_t>(std::min<uint32_t>(std::numeric_limits<uint16_t>::max(), static_cast<uint32_t>(speed) * 2))
		: speed;
	msg.add<uint16_t>(clientSpeed);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCancelWalk() {
	if (player) {
		NetworkMessage msg;
		msg.addByte(0xB5);
		msg.addByte(player->getDirection());
		writeToOutputBuffer(msg);
	}
}

void ProtocolGame::sendSkills() {
	if (shouldSuppressPreLoginPacket()) {
		return;
	}

	NetworkMessage msg;
	AddPlayerSkills(msg);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPing() {
	if (player) {
		NetworkMessage msg;
		msg.addByte(oldProtocol && version < 953 ? 0x1E : 0x1D);
		writeToOutputBuffer(msg);
	}
}

void ProtocolGame::sendPingBack() {
	NetworkMessage msg;
	msg.addByte(0x1E);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendDistanceShoot(const Position &from, const Position &to, uint16_t type, SourceEffect_t source) {
	const bool useLegacyU16Effect = oldProtocol && hasProtocolFeature(protocolProfile, ProtocolFeature::MagicEffectU16);
	if (oldProtocol && !useLegacyU16Effect && type > 0xFF) {
		return;
	}
	NetworkMessage msg;
	if (oldProtocol) {
		msg.addByte(0x85);
		msg.addPosition(from);
		msg.addPosition(to);
		if (useLegacyU16Effect) {
			msg.add<uint16_t>(type);
		} else {
			msg.addByte(static_cast<uint8_t>(type));
		}
	} else {
		msg.addByte(0x83);
		msg.addPosition(from);
		msg.addByte(MAGIC_EFFECTS_CREATE_DISTANCEEFFECT);
		msg.add<uint16_t>(type);
		msg.addByte(static_cast<uint8_t>(static_cast<int8_t>(static_cast<int32_t>(to.x) - static_cast<int32_t>(from.x))));
		msg.addByte(static_cast<uint8_t>(static_cast<int8_t>(static_cast<int32_t>(to.y) - static_cast<int32_t>(from.y))));
		if (hasProtocolFeature(protocolProfile, ProtocolFeature::GraphicalEffectSourceByte)) {
			msg.addByte(magic_enum::enum_integer(source));
		}
		msg.addByte(MAGIC_EFFECTS_END_LOOP);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendDistanceShoot(const Position &from, const Position &to, uint16_t type) {
	sendDistanceShoot(from, to, type, SourceEffect_t::OWN);
}

void ProtocolGame::sendRestingStatus(uint8_t protection) {
	if (oldProtocol || !player) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xA9);
	msg.addByte(protection); // 1 / 0

	uint8_t dailyStreak = 0;
	auto dailyRewardKV = player->kv()->scoped("daily-reward")->get("streak");
	if (dailyRewardKV && dailyRewardKV.has_value()) {
		dailyStreak = static_cast<uint8_t>(dailyRewardKV->getNumber());
	}

	msg.addByte(dailyStreak < 2 ? 0 : 1);
	if (dailyStreak < 2) {
		msg.addString("Resting Area (no active bonus)");
	} else {
		std::ostringstream ss;
		ss << "Active Resting Area Bonuses: ";
		if (dailyStreak < DAILY_REWARD_DOUBLE_HP_REGENERATION) {
			ss << "\nHit Points Regeneration";
		} else {
			ss << "\nDouble Hit Points Regeneration";
		}
		if (dailyStreak >= DAILY_REWARD_MP_REGENERATION) {
			if (dailyStreak < DAILY_REWARD_DOUBLE_MP_REGENERATION) {
				ss << ",\nMana Points Regeneration";
			} else {
				ss << ",\nDouble Mana Points Regeneration";
			}
		}
		if (dailyStreak >= DAILY_REWARD_STAMINA_REGENERATION) {
			ss << ",\nStamina Points Regeneration";
		}
		if (dailyStreak >= DAILY_REWARD_SOUL_REGENERATION) {
			ss << ",\nSoul Points Regeneration";
		}
		ss << ".";
		msg.addString(ss.str());
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMagicEffect(const Position &pos, uint16_t type, SourceEffect_t source) {
	const bool useLegacyU16Effect = oldProtocol && hasProtocolFeature(protocolProfile, ProtocolFeature::MagicEffectU16);
	if (!canSee(pos) || (oldProtocol && !useLegacyU16Effect && type > 0xFF)) {
		return;
	}

	NetworkMessage msg;
	if (oldProtocol) {
		msg.addByte(0x83);
		msg.addPosition(pos);
		if (useLegacyU16Effect) {
			msg.add<uint16_t>(type);
		} else {
			msg.addByte(static_cast<uint8_t>(type));
		}
	} else {
		msg.addByte(0x83);
		msg.addPosition(pos);
		msg.addByte(MAGIC_EFFECTS_CREATE_EFFECT);
		msg.add<uint16_t>(type);
		if (hasProtocolFeature(protocolProfile, ProtocolFeature::GraphicalEffectSourceByte)) {
			msg.addByte(magic_enum::enum_integer(source));
		}
		msg.addByte(MAGIC_EFFECTS_END_LOOP);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMagicEffect(const Position &pos, uint16_t type) {
	sendMagicEffect(pos, type, SourceEffect_t::OWN);
}

void ProtocolGame::removeMagicEffect(const Position &pos, uint16_t type) {
	const bool useLegacyU16Effect = oldProtocol && hasProtocolFeature(protocolProfile, ProtocolFeature::MagicEffectU16);
	if (oldProtocol && !useLegacyU16Effect && type > 0xFF) {
		return;
	}
	NetworkMessage msg;
	msg.addByte(0x84);
	msg.addPosition(pos);
	if (oldProtocol) {
		if (useLegacyU16Effect) {
			msg.add<uint16_t>(type);
		} else {
			msg.addByte(static_cast<uint8_t>(type));
		}
	} else {
		msg.add<uint16_t>(type);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCreatureHealth(const std::shared_ptr<Creature> &creature) {
	if (creature->isHealthHidden()) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8C);
	msg.add<uint32_t>(creature->getID());
	if (creature->isHealthHidden()) {
		msg.addByte(0x00);
	} else {
		msg.addByte(static_cast<uint8_t>(std::min<double>(100, std::ceil((static_cast<double>(creature->getHealth()) / std::max<int32_t>(creature->getMaxHealth(), 1)) * 100))));
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyCreatureUpdate(const std::shared_ptr<Creature> &target) {
	if (!player || oldProtocol) {
		return;
	}

	bool known;
	uint32_t removedKnown = 0;
	uint32_t cid = target->getID();
	checkCreatureAsKnown(cid, known, removedKnown);

	NetworkMessage msg;
	msg.addByte(0x8B);
	msg.add<uint32_t>(cid);
	msg.addByte(0); // creature update
	AddCreature(msg, target, known, removedKnown);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyCreatureShield(const std::shared_ptr<Creature> &target) {
	uint32_t cid = target->getID();
	if (!knownCreatureSet.contains(cid)) {
		sendPartyCreatureUpdate(target);
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x91);
	msg.add<uint32_t>(cid);
	msg.addByte(player->getPartyShield(target->getPlayer()));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyCreatureSkull(const std::shared_ptr<Creature> &target) {
	if (g_game().getWorldType() != WORLD_TYPE_PVP) {
		return;
	}

	uint32_t cid = target->getID();
	if (!knownCreatureSet.contains(cid)) {
		sendPartyCreatureUpdate(target);
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x90);
	msg.add<uint32_t>(cid);
	msg.addByte(player->getSkullClient(target));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyCreatureHealth(const std::shared_ptr<Creature> &target, uint8_t healthPercent) {
	uint32_t cid = target->getID();
	if (!knownCreatureSet.contains(cid)) {
		sendPartyCreatureUpdate(target);
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8C);
	msg.add<uint32_t>(cid);
	msg.addByte(std::min<uint8_t>(100, healthPercent));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyPlayerMana(const std::shared_ptr<Player> &target, uint8_t manaPercent) {
	uint32_t cid = target->getID();
	if (!knownCreatureSet.contains(cid)) {
		sendPartyCreatureUpdate(target);
	}

	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8B);
	msg.add<uint32_t>(cid);
	msg.addByte(11); // mana percent
	msg.addByte(std::min<uint8_t>(100, manaPercent));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyCreatureShowStatus(const std::shared_ptr<Creature> &target, bool showStatus) {
	uint32_t cid = target->getID();
	if (!knownCreatureSet.contains(cid)) {
		sendPartyCreatureUpdate(target);
	}

	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8B);
	msg.add<uint32_t>(cid);
	msg.addByte(12); // show status
	msg.addByte((showStatus ? 0x01 : 0x00));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPartyPlayerVocation(const std::shared_ptr<Player> &target) {
	if (!target) {
		return;
	}

	uint32_t cid = target->getID();
	if (!knownCreatureSet.contains(cid)) {
		sendPartyCreatureUpdate(target);
		return;
	}

	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8B);
	msg.add<uint32_t>(cid);
	msg.addByte(13); // vocation
	msg.addByte(target->getVocation()->getClientId());
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPlayerVocation(const std::shared_ptr<Player> &target) {
	if (!player || !target || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x8B);
	msg.add<uint32_t>(target->getID());
	msg.addByte(13); // vocation
	msg.addByte(target->getVocation()->getClientId());
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendFYIBox(const std::string &message) {
	NetworkMessage msg;
	msg.addByte(0x15);
	msg.addString(message);
	writeToOutputBuffer(msg);
}

// tile
void ProtocolGame::sendMapDescription(const Position &pos) {
	NetworkMessage msg;
	msg.addByte(0x64);
	msg.addPosition(player->getPosition());
	GetMapDescription(pos.x - MAP_MAX_CLIENT_VIEW_PORT_X, pos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, pos.z, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, msg);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAddTileItem(const Position &pos, uint32_t stackpos, const std::shared_ptr<Item> &item) {
	if (!canSee(pos)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x6A);
	msg.addPosition(pos);
	msg.addByte(static_cast<uint8_t>(stackpos));
	AddItem(msg, item);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateTileItem(const Position &pos, uint32_t stackpos, const std::shared_ptr<Item> &item) {
	if (!canSee(pos)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x6B);
	msg.addPosition(pos);
	msg.addByte(static_cast<uint8_t>(stackpos));
	AddItem(msg, item);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendRemoveTileThing(const Position &pos, uint32_t stackpos, std::source_location location) {
	if (!canSee(pos)) {
		return;
	}

	g_logger().trace("Removing tile thing at position {}, stackpos {} from protocol game, location: {}, called from: {} {}", pos.toString(), stackpos, location.function_name(), location.file_name(), location.line());

	NetworkMessage msg;
	RemoveTileThing(msg, pos, stackpos);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateTileCreature(const Position &pos, uint32_t stackpos, const std::shared_ptr<Creature> &creature) {
	if (!canSee(pos)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x6B);
	msg.addPosition(pos);
	msg.addByte(static_cast<uint8_t>(stackpos));

	bool known;
	uint32_t removedKnown = 0;
	checkCreatureAsKnown(creature->getID(), known, removedKnown);
	AddCreature(msg, creature, known, removedKnown);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateTile(const std::shared_ptr<Tile> &tile, const Position &pos) {
	if (!canSee(pos)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x69);
	msg.addPosition(pos);

	if (tile) {
		GetTileDescription(tile, msg);
		msg.addByte(0x00);
		msg.addByte(0xFF);
	} else {
		msg.addByte(0x01);
		msg.addByte(0xFF);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPendingStateEntered() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x0A);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendEnterWorld() {
	NetworkMessage msg;
	msg.addByte(0x0F);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendFightModes() {
	NetworkMessage msg;
	msg.addByte(0xA7);
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::TacticsWithoutFightMode)) {
		msg.addByte(player->chaseMode);
		msg.addByte(player->secureMode);
		msg.addByte(PVP_MODE_DOVE);
	} else {
		msg.addByte(player->fightMode);
		msg.addByte(player->chaseMode);
		msg.addByte(player->secureMode);
		if (!hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
			msg.addByte(PVP_MODE_DOVE);
		}
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAllowBugReport() {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x1A);
	msg.addByte(0x00); // 0x01 = DISABLE bug report
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAddCreature(const std::shared_ptr<Creature> &creature, const Position &pos, int32_t stackpos, bool isLogin) {
	if (!canSee(pos)) {
		return;
	}

	if (creature != player) {
		if (stackpos >= 10) {
			return;
		}

		NetworkMessage msg;
		msg.addByte(0x6A);
		msg.addPosition(pos);
		msg.addByte(static_cast<uint8_t>(stackpos));

		bool known;
		uint32_t removedKnown;
		checkCreatureAsKnown(creature->getID(), known, removedKnown);
		AddCreature(msg, creature, known, removedKnown);
		writeToOutputBuffer(msg);

		if (isLogin) {
			if (std::shared_ptr<Player> creaturePlayer = creature->getPlayer()) {
				if (!creaturePlayer->isAccessPlayer() || creaturePlayer->getAccountType() == ACCOUNT_TYPE_NORMAL) {
					sendMagicEffect(pos, CONST_ME_TELEPORT);
				}
			} else {
				sendMagicEffect(pos, CONST_ME_TELEPORT);
			}
		}

		return;
	}

	NetworkMessage msg;
	msg.addByte(version >= 980 ? 0x17 : 0x0A);

	msg.add<uint32_t>(player->getID());
	msg.add<uint16_t>(SERVER_BEAT); // beat duration (50)

	if (version >= 981 && hasProtocolFeature(protocolProfile, ProtocolFeature::LoginSpeedFormula)) {
		msg.addDouble(Creature::speedA, 3);
		msg.addDouble(Creature::speedB, 3);
		msg.addDouble(Creature::speedC, 3);
	}

	// Allow bug report (Ctrl + Z)
	const bool hasInlineBugReportFlag = oldProtocol && (version >= 1054 || (protocolProfile && protocolProfile->hasFeature(ProtocolFeature::InlineLoginBugReportFlag)));
	if (hasInlineBugReportFlag) {
		if (player->getAccountType() >= ACCOUNT_TYPE_NORMAL) {
			msg.addByte(0x01);
		} else {
			msg.addByte(0x00);
		}
	}

	if (version >= 1054) {
		msg.addByte(0x00); // can change pvp framing option
	}

	if (version >= 1058) {
		msg.addByte(0x00); // expert mode button enabled
	}

	if (version >= 1080) {
		msg.addString(g_configManager().getString(STORE_IMAGES_URL));
		msg.add<uint16_t>(static_cast<uint16_t>(g_configManager().getNumber(STORE_COIN_PACKET)));
	}

	if (!oldProtocol) {
		const bool exivaEnabled = g_game().getWorldType() == WORLD_TYPE_NO_PVP;
		msg.addByte(exivaEnabled ? 0x01 : 0x00); // exiva button enabled
		if (exivaEnabled) {
			sendExivaRestrictions(true);
		}
	}

	writeToOutputBuffer(msg);

	// Allow bug report (Ctrl + Z)
	sendAllowBugReport();

	sendTibiaTime(g_game().getLightHour());
	if (version >= 980) {
		sendPendingStateEntered();
		sendEnterWorld();
	}
	sendMapDescription(pos);
	loggedIn = true;

	if (isLogin) {
		sendMagicEffect(pos, CONST_ME_TELEPORT);
		sendDisableLoginMusic();
	}

	for (int i = CONST_SLOT_FIRST; i <= CONST_SLOT_LAST; ++i) {
		sendInventoryItem(static_cast<Slots_t>(i), player->getInventoryItem(static_cast<Slots_t>(i)));
	}

	sendStats();
	sendSkills();
	if (version >= 1100) {
		const bool sendModernLoginSideSystems = hasProtocolFeature(protocolProfile, ProtocolFeature::ModernLoginSideSystems);
		if (sendModernLoginSideSystems) {
			player->weaponProficiency().clearAllStats();
			if (const auto equippedWeaponId = player->getWeaponId(true); equippedWeaponId != 0) {
				player->weaponProficiency().applyPerks(equippedWeaponId, false);
			}

			player->sendWeaponProficiency();
		}

		sendBlessStatus();
		sendPremiumTrigger();
		if (sendModernLoginSideSystems) {
			sendItemsPrice();
			sendPreyPrices();
			player->sendPreyData();
			player->sendTaskHuntingData();
			sendForgingData();
		}
	}

	// gameworld light-settings
	sendWorldLight(g_game().getWorldLightInfo());

	// player light level
	sendCreatureLight(creature);

	if (player->getPlayerVocationEnum() == Vocation_t::VOCATION_MONK_CIP) {
		sendMonkData(MonkData_t::Harmony, player->getHarmony());
		const bool officialVocationData = hasProtocolFeature(protocolProfile, ProtocolFeature::OfficialVocationSpecificPlayerData);
		const auto virtue = player->getVirtue();
		if (virtue != Virtue_t::None || !officialVocationData) {
			sendMonkData(MonkData_t::Virtue, enumToValue(virtue != Virtue_t::None ? virtue : Virtue_t::Harmony));
		}
		sendMonkData(MonkData_t::Serenity, officialVocationData ? player->hasCondition(CONDITION_SERENE) : true);
	}

	if (version >= 1100) {
		sendVIPGroups();
	}

	const auto &vipEntries = IOLoginData::getVIPEntries(player->getAccountId());

	if (player->isAccessPlayer()) {
		for (const VIPEntry &entry : vipEntries) {
			VipStatus_t vipStatus;

			std::shared_ptr<Player> vipPlayer = g_game().getPlayerByGUID(entry.guid);
			if (!vipPlayer) {
				vipStatus = VipStatus_t::Offline;
			} else {
				vipStatus = vipPlayer->vip().getStatus();
			}

			sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus);
		}
	} else {
		for (const VIPEntry &entry : vipEntries) {
			VipStatus_t vipStatus;

			std::shared_ptr<Player> vipPlayer = g_game().getPlayerByGUID(entry.guid);
			if (!vipPlayer || vipPlayer->isInGhostMode()) {
				vipStatus = VipStatus_t::Offline;
			} else {
				vipStatus = vipPlayer->vip().getStatus();
			}

			sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus);
		}
	}

	if (version >= 1100) {
		sendInventoryIds();
	}
	std::shared_ptr<Item> slotItem = player->getInventoryItem(CONST_SLOT_BACKPACK);
	if (slotItem) {
		player->setMainBackpackUnassigned(slotItem->getContainer());
	}

	sendLootContainers();
	if (version >= 950) {
		sendBasicData();
	}
	if (!oldProtocol) {
		sendHousesInfo();
	}
	// Wheel of destiny cooldown
	if (!oldProtocol && g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) {
		player->wheel().sendGiftOfLifeCooldown();
	}

	player->sendClientCheck();
	player->sendGameNews();
	player->sendIcons();

	// Send open containers after login.
	if (isLogin) {
		player->openPlayerContainers();
		if (!oldProtocol) {
			player->sendSpellCooldowns();
		}
	}
}

void ProtocolGame::sendMoveCreature(const std::shared_ptr<Creature> &creature, const Position &newPos, int32_t newStackPos, const Position &oldPos, int32_t oldStackPos, bool teleport) {
	if (creature == player) {
		if (oldStackPos >= 10) {
			sendMapDescription(newPos);
		} else if (teleport) {
			NetworkMessage msg;
			RemoveTileThing(msg, oldPos, oldStackPos);
			writeToOutputBuffer(msg);
			sendMapDescription(newPos);
		} else {
			NetworkMessage msg;
			if (oldPos.z == MAP_INIT_SURFACE_LAYER && newPos.z >= MAP_INIT_SURFACE_LAYER + 1) {
				RemoveTileThing(msg, oldPos, oldStackPos);
			} else {
				msg.addByte(0x6D);
				msg.addPosition(oldPos);
				msg.addByte(static_cast<uint8_t>(oldStackPos));
				msg.addPosition(newPos);
			}

			if (newPos.z > oldPos.z) {
				MoveDownCreature(msg, creature, newPos, oldPos);
			} else if (newPos.z < oldPos.z) {
				MoveUpCreature(msg, creature, newPos, oldPos);
			}

			if (oldPos.y > newPos.y) { // north, for old x
				msg.addByte(0x65);
				GetMapDescription(oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, newPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, 1, msg);
			} else if (oldPos.y < newPos.y) { // south, for old x
				msg.addByte(0x67);
				GetMapDescription(oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, newPos.y + (MAP_MAX_CLIENT_VIEW_PORT_Y + 1), newPos.z, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, 1, msg);
			}

			if (oldPos.x < newPos.x) { // east, [with new y]
				msg.addByte(0x66);
				GetMapDescription(newPos.x + (MAP_MAX_CLIENT_VIEW_PORT_X + 1), newPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z, 1, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, msg);
			} else if (oldPos.x > newPos.x) { // west, [with new y]
				msg.addByte(0x68);
				GetMapDescription(newPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, newPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z, 1, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, msg);
			}
			writeToOutputBuffer(msg);
		}
	} else if (canSee(oldPos) && canSee(newPos)) {
		if (teleport || (oldPos.z == MAP_INIT_SURFACE_LAYER && newPos.z >= MAP_INIT_SURFACE_LAYER + 1) || oldStackPos >= 10) {
			sendRemoveTileThing(oldPos, oldStackPos);
			sendAddCreature(creature, newPos, newStackPos, false);
		} else {
			NetworkMessage msg;
			msg.addByte(0x6D);
			msg.addPosition(oldPos);
			msg.addByte(static_cast<uint8_t>(oldStackPos));
			msg.addPosition(newPos);
			writeToOutputBuffer(msg);
		}
	} else if (canSee(oldPos)) {
		sendRemoveTileThing(oldPos, oldStackPos);
	} else if (canSee(newPos)) {
		sendAddCreature(creature, newPos, newStackPos, false);
	}
}

void ProtocolGame::sendInventoryItem(Slots_t slot, const std::shared_ptr<Item> &item) {
	if (oldProtocol && slot > CONST_SLOT_AMMO) {
		return;
	}

	NetworkMessage msg;
	if (item) {
		msg.addByte(0x78);
		msg.addByte(slot);
		AddItem(msg, item);
	} else {
		msg.addByte(0x79);
		msg.addByte(slot);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendInventoryIds() {
	if (!player) {
		return;
	}
	if (oldProtocol) {
		return;
	}

	const auto &items = player->getInventoryItemsId();

	NetworkMessage msg;
	msg.addByte(0xF5);
	const auto countPosition = msg.getBufferPosition();
	msg.skipBytes(2); // Total items count

	uint16_t totalItemsCount = 0;

	for (auto [key, amount] : items) {
		const auto &[itemId, tier] = key;

		if (amount >= 0x40000000) {
			g_logger().warn("[{}] player: {}, failed to write count for item: {} tier: {}, amount: {}, total count: {}, skipping item.", __FUNCTION__, player->getName(), itemId, tier, amount, totalItemsCount);
			continue;
		}

		msg.add<uint16_t>(itemId);
		msg.addByte(tier);
		if (!msg.writeCount(amount)) {
			g_logger().warn("[{}] player: {}, failed to write count for item: {} tier: {}, amount: {}, total count: {}, skipping item.", __FUNCTION__, player->getName(), itemId, tier, amount, totalItemsCount);
			continue;
		}

		++totalItemsCount;
	}

	msg.setBufferPosition(countPosition);
	msg.add<uint16_t>(totalItemsCount);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAddContainerItem(uint8_t cid, uint16_t slot, const std::shared_ptr<Item> &item) {
	NetworkMessage msg;
	msg.addByte(0x70);
	msg.addByte(cid);
	msg.add<uint16_t>(slot);
	AddItem(msg, item);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateContainerItem(uint8_t cid, uint16_t slot, const std::shared_ptr<Item> &item) {
	NetworkMessage msg;
	msg.addByte(0x71);
	msg.addByte(cid);
	msg.add<uint16_t>(slot);
	AddItem(msg, item);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendRemoveContainerItem(uint8_t cid, uint16_t slot, const std::shared_ptr<Item> &lastItem) {
	NetworkMessage msg;
	msg.addByte(0x72);
	msg.addByte(cid);
	msg.add<uint16_t>(slot);
	if (lastItem) {
		AddItem(msg, lastItem);
	} else {
		msg.add<uint16_t>(0x00);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTextWindow(uint32_t windowTextId, const std::shared_ptr<Item> &item, uint16_t maxlen, bool canWrite) {
	NetworkMessage msg;
	msg.addByte(0x96);
	msg.add<uint32_t>(windowTextId);
	AddItem(msg, item);

	if (canWrite) {
		msg.add<uint16_t>(maxlen);
		msg.addString(item->getAttribute<std::string>(ItemAttribute_t::TEXT));
	} else {
		const std::string &text = item->getAttribute<std::string>(ItemAttribute_t::TEXT);
		msg.add<uint16_t>(text.size());
		msg.addString(text);
	}

	const std::string &writer = item->getAttribute<std::string>(ItemAttribute_t::WRITER);
	if (!writer.empty()) {
		msg.addString(writer);
	} else {
		msg.add<uint16_t>(0x00);
	}

	if (!oldProtocol) {
		msg.addByte(0x00); // Show (Traded)
	}

	auto writtenDate = item->getAttribute<time_t>(ItemAttribute_t::DATE);
	if (writtenDate != 0) {
		msg.addString(formatDateShort(writtenDate));
	} else {
		msg.add<uint16_t>(0x00);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTextWindow(uint32_t windowTextId, uint32_t itemId, const std::string &text) {
	NetworkMessage msg;
	msg.addByte(0x96);
	msg.add<uint32_t>(windowTextId);
	AddItem(msg, itemId, 1, 0);
	msg.add<uint16_t>(text.size());
	msg.addString(text);
	msg.add<uint16_t>(0x00);

	if (!oldProtocol) {
		msg.addByte(0x00); // Show (Traded)
	}

	msg.add<uint16_t>(0x00);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendHouseWindow(uint32_t windowTextId, const std::string &text) {
	NetworkMessage msg;
	msg.addByte(0x97);
	msg.addByte(0x00);
	msg.add<uint32_t>(windowTextId);
	msg.addString(text);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendOutfitWindow() {
	NetworkMessage msg;
	msg.addByte(0xC8);

	Outfit_t currentOutfit = player->getDefaultOutfit();
	auto isSupportOutfit = player->isWearingSupportOutfit();
	const auto currentOutfitSupportsMount = g_game().outfitSupportsMount(currentOutfit.lookType);
	bool mounted = false;

	if (!isSupportOutfit && currentOutfitSupportsMount) {
		const auto currentMount = g_game().mounts->getMountByID(player->getLastMount());
		if (currentMount) {
			mounted = (currentOutfit.lookMount == currentMount->clientId);
			currentOutfit.lookMount = currentMount->clientId;
		}
	} else {
		currentOutfit.lookMount = 0;
	}

	auto &playerAttachedEffects = player->attachedEffects();
	// Wings
	const auto &currentWing = g_game().getAttachedEffects()->getWingByID(playerAttachedEffects.getCurrentWing());
	if (currentWing) {
		currentOutfit.lookWing = currentWing->id;
	}
	// Auras
	const auto &currentAura = g_game().getAttachedEffects()->getAuraByID(playerAttachedEffects.getCurrentAura());
	if (currentAura) {
		currentOutfit.lookAura = currentAura->id;
	}
	// Effects
	const auto &currentEffect = g_game().getAttachedEffects()->getEffectByID(playerAttachedEffects.getCurrentEffect());
	if (currentEffect) {
		currentOutfit.lookEffect = currentEffect->id;
	}
	// Shader
	const auto &currentShader = g_game().getAttachedEffects()->getShaderByID(playerAttachedEffects.getCurrentShader());
	if (currentShader) {
		currentOutfit.lookShader = currentShader->id;
	}
	AddOutfit(msg, currentOutfit);

	if (oldProtocol) {
		std::vector<ProtocolOutfit> protocolOutfits;
		const auto outfits = Outfits::getInstance().getOutfits(player->getSex());
		protocolOutfits.reserve(outfits.size());
		for (const auto &outfit : outfits) {
			uint8_t addons;
			if (!player->getOutfitAddons(outfit, addons)) {
				continue;
			}

			protocolOutfits.emplace_back(outfit->name, outfit->lookType, addons);
			[[maybe_unused]] auto &outfit_ref = protocolOutfits.back();
			// Game client doesn't allow more than 100 outfits
			if (protocolOutfits.size() == 150) {
				break;
			}
		}

		msg.addByte(protocolOutfits.size());
		for (const ProtocolOutfit &outfit : protocolOutfits) {
			msg.add<uint16_t>(outfit.lookType);
			msg.addString(outfit.name);
			msg.addByte(outfit.addons);
		}

		std::vector<std::shared_ptr<Mount>> mounts;
		for (const auto &mount : g_game().mounts->getMounts()) {
			if (player->hasMount(mount)) {
				mounts.emplace_back(mount);
			}
		}

		msg.addByte(mounts.size());
		for (const auto &mount : mounts) {
			msg.add<uint16_t>(mount->clientId);
			msg.addString(mount->name);
		}

		if (isOTCR) {
			sendOutfitWindowCustomOTCR(msg); // g_game.enableFeature(GameWingsAurasEffectsShader)
		}
		writeToOutputBuffer(msg);
		return;
	}

	if (currentOutfit.lookMount == 0) {
		msg.addByte(isSupportOutfit || !currentOutfitSupportsMount ? 0 : currentOutfit.lookMountHead);
		msg.addByte(isSupportOutfit || !currentOutfitSupportsMount ? 0 : currentOutfit.lookMountBody);
		msg.addByte(isSupportOutfit || !currentOutfitSupportsMount ? 0 : currentOutfit.lookMountLegs);
		msg.addByte(isSupportOutfit || !currentOutfitSupportsMount ? 0 : currentOutfit.lookMountFeet);
	}
	msg.add<uint16_t>(currentOutfit.lookFamiliarsType);

	auto startOutfits = msg.getBufferPosition();
	// 100 is the limit of old protocol clients.
	uint16_t limitOutfits = std::numeric_limits<uint16_t>::max();
	uint16_t outfitSize = 0;
	msg.skipBytes(2);

	if (player->isAccessPlayer() && g_configManager().getBoolean(ENABLE_SUPPORT_OUTFIT)) {
		msg.add<uint16_t>(75);
		msg.addString("Gamemaster");
		msg.addByte(0);
		msg.addByte(0x00);
		++outfitSize;

		msg.add<uint16_t>(266);
		msg.addString("Customer Support");
		msg.addByte(0);
		msg.addByte(0x00);
		++outfitSize;

		msg.add<uint16_t>(302);
		msg.addString("Community Manager");
		msg.addByte(0);
		msg.addByte(0x00);
		++outfitSize;
	}

	const auto outfits = Outfits::getInstance().getOutfits(player->getSex());

	for (const auto &outfit : outfits) {
		uint8_t addons;
		if (player->getOutfitAddons(outfit, addons)) {
			msg.add<uint16_t>(outfit->lookType);
			msg.addString(outfit->name);
			msg.addByte(addons);
			msg.addByte(0x00);
			++outfitSize;
		} else if (outfit->lookType == 1210 || outfit->lookType == 1211) {
			if (player->canWear(1210, 0) || player->canWear(1211, 0)) {
				msg.add<uint16_t>(outfit->lookType);
				msg.addString(outfit->name);
				msg.addByte(3);
				msg.addByte(0x02);
				++outfitSize;
			}
		} else if (outfit->lookType == 1456 || outfit->lookType == 1457) {
			if (player->canWear(1456, 0) || player->canWear(1457, 0)) {
				msg.add<uint16_t>(outfit->lookType);
				msg.addString(outfit->name);
				msg.addByte(3);
				msg.addByte(0x03);
				++outfitSize;
			}
		} else if (outfit->from == "store") {
			msg.add<uint16_t>(outfit->lookType);
			msg.addString(outfit->name);
			msg.addByte(outfit->lookType >= 962 && outfit->lookType <= 975 ? 0 : 3);
			msg.addByte(0x01);
			msg.add<uint32_t>(0x00);
			++outfitSize;
		}

		if (outfitSize == limitOutfits) {
			break;
		}
	}

	auto endOutfits = msg.getBufferPosition();
	msg.setBufferPosition(startOutfits);
	msg.add<uint16_t>(outfitSize);
	msg.setBufferPosition(endOutfits);

	auto startMounts = msg.getBufferPosition();
	uint16_t limitMounts = std::numeric_limits<uint16_t>::max();
	uint16_t mountSize = 0;
	msg.skipBytes(2);

	const auto mounts = g_game().mounts->getMounts();
	for (const auto &mount : mounts) {
		if (player->hasMount(mount)) {
			msg.add<uint16_t>(mount->clientId);
			msg.addString(mount->name);
			msg.addByte(0x00);
			++mountSize;
		} else if (mount->type == "store") {
			msg.add<uint16_t>(mount->clientId);
			msg.addString(mount->name);
			msg.addByte(0x01);
			msg.add<uint32_t>(0x00);
			++mountSize;
		}

		if (mountSize == limitMounts) {
			break;
		}
	}

	auto endMounts = msg.getBufferPosition();
	msg.setBufferPosition(startMounts);
	msg.add<uint16_t>(mountSize);
	msg.setBufferPosition(endMounts);

	auto startFamiliars = msg.getBufferPosition();
	uint16_t limitFamiliars = std::numeric_limits<uint16_t>::max();
	uint16_t familiarSize = 0;
	msg.skipBytes(2);

	const auto familiars = Familiars::getInstance().getFamiliars(player->getVocationId());

	for (const auto &familiar : familiars) {
		if (!player->getFamiliar(familiar)) {
			continue;
		}

		msg.add<uint16_t>(familiar->lookType);
		msg.addString(familiar->name);
		msg.addByte(0x00);
		if (++familiarSize == limitFamiliars) {
			break;
		}
	}

	auto endFamiliars = msg.getBufferPosition();
	msg.setBufferPosition(startFamiliars);
	msg.add<uint16_t>(familiarSize);
	msg.setBufferPosition(endFamiliars);

	msg.addByte(0x00); // Try outfit
	msg.addByte(mounted ? 0x01 : 0x00);

	// Version 12.81 - Random mount 'bool'
	msg.addByte(isSupportOutfit || !currentOutfitSupportsMount ? 0x00 : (player->isRandomMounted() ? 0x01 : 0x00));

	if (isOTCR) {
		sendOutfitWindowCustomOTCR(msg); // g_game.enableFeature(GameWingsAurasEffectsShader)
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPodiumWindow(const std::shared_ptr<Item> &podium, const Position &position, uint16_t itemId, uint8_t stackpos) {
	if (!podium || oldProtocol) {
		g_logger().error("[{}] item is nullptr", __FUNCTION__);
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xC8);

	const auto podiumVisible = podium->getCustomAttribute("PodiumVisible");
	const auto lookType = podium->getCustomAttribute("LookType");
	const auto lookMount = podium->getCustomAttribute("LookMount");
	const auto lookDirection = podium->getCustomAttribute("LookDirection");
	if (lookType) {
		addOutfitAndMountBytes(msg, podium, lookType, "LookHead", "LookBody", "LookLegs", "LookFeet", true);
	} else {
		msg.add<uint16_t>(0);
	}

	if (lookMount) {
		addOutfitAndMountBytes(msg, podium, lookMount, "LookMountHead", "LookMountBody", "LookMountLegs", "LookMountFeet");
	} else {
		msg.add<uint16_t>(0);
		msg.addByte(0);
		msg.addByte(0);
		msg.addByte(0);
		msg.addByte(0);
	}
	msg.add<uint16_t>(0);

	auto startOutfits = msg.getBufferPosition();
	uint16_t limitOutfits = std::numeric_limits<uint16_t>::max();
	uint16_t outfitSize = 0;
	msg.skipBytes(2);

	const auto outfits = Outfits::getInstance().getOutfits(player->getSex());
	for (const auto &outfit : outfits) {
		uint8_t addons;
		if (!player->getOutfitAddons(outfit, addons)) {
			continue;
		}

		msg.add<uint16_t>(outfit->lookType);
		msg.addString(outfit->name);
		msg.addByte(addons);
		msg.addByte(0x00);
		if (++outfitSize == limitOutfits) {
			break;
		}
	}

	auto endOutfits = msg.getBufferPosition();
	msg.setBufferPosition(startOutfits);
	msg.add<uint16_t>(outfitSize);
	msg.setBufferPosition(endOutfits);

	auto startMounts = msg.getBufferPosition();
	uint16_t limitMounts = std::numeric_limits<uint16_t>::max();
	uint16_t mountSize = 0;
	msg.skipBytes(2);

	const auto mounts = g_game().mounts->getMounts();
	for (const auto &mount : mounts) {
		if (player->hasMount(mount)) {
			msg.add<uint16_t>(mount->clientId);
			msg.addString(mount->name);
			msg.addByte(0x00);
			if (++mountSize == limitMounts) {
				break;
			}
		}
	}

	auto endMounts = msg.getBufferPosition();
	msg.setBufferPosition(startMounts);
	msg.add<uint16_t>(mountSize);
	msg.setBufferPosition(endMounts);

	msg.add<uint16_t>(0);

	msg.addByte(0x05);
	msg.addByte(lookMount ? 0x01 : 0x00);

	msg.add<uint16_t>(0);

	msg.addPosition(position);
	msg.add<uint16_t>(itemId);
	msg.addByte(stackpos);

	msg.addByte(podiumVisible ? podiumVisible->getAttribute<uint8_t>() : 0x01);
	msg.addByte(lookType ? 0x01 : 0x00);
	msg.addByte(lookDirection ? lookDirection->getAttribute<uint8_t>() : 2);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus) {
	if (oldProtocol && newStatus == VipStatus_t::Training) {
		return;
	}

	NetworkMessage msg;
	if (oldProtocol && version < 980) {
		msg.addByte(newStatus == VipStatus_t::Offline ? 0xD4 : 0xD3);
		msg.add<uint32_t>(guid);
		writeToOutputBuffer(msg);
		return;
	}

	msg.addByte(0xD3);
	msg.add<uint32_t>(guid);
	msg.addByte(enumToValue(newStatus));
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendVIP(uint32_t guid, const std::string &name, const std::string &description, uint32_t icon, bool notify, VipStatus_t status) {
	if (oldProtocol && status == VipStatus_t::Training) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xD2);
	msg.add<uint32_t>(guid);
	msg.addString(name);

	if (oldProtocol && version <= 860) {
		msg.addByte(notify ? 0x01 : 0x00);
		writeToOutputBuffer(msg);
		return;
	}

	msg.addString(description);
	msg.add<uint32_t>(std::min<uint32_t>(10, icon));
	msg.addByte(notify ? 0x01 : 0x00);
	msg.addByte(enumToValue(status));

	const auto &vipGuidGroups = player->vip().getGroupsIdGuidBelongs(guid);

	if (!oldProtocol) {
		msg.addByte(vipGuidGroups.size()); // vipGroups
		for (const auto &vipGroupID : vipGuidGroups) {
			msg.addByte(vipGroupID);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendVIPGroups() {
	if (oldProtocol) {
		return;
	}

	const auto &vipGroups = player->vip().getGroups();

	NetworkMessage msg;
	msg.addByte(0xD4);
	msg.addByte(vipGroups.size()); // vipGroups.size()
	for (const auto &vipGroup : vipGroups) {
		msg.addByte(vipGroup->id);
		msg.addString(vipGroup->name);
		msg.addByte(vipGroup->customizable ? 0x01 : 0x00); // 0x00 = not Customizable, 0x01 = Customizable
	}
	msg.addByte(player->vip().getMaxGroupEntries() - vipGroups.size()); // max vip groups

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendSpellCooldown(uint16_t spellId, uint32_t time) {
	if (oldProtocol && version < 870) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xA4);
	if (oldProtocol && spellId >= 170) {
		msg.addByte(170);
	} else {
		if (oldProtocol) {
			msg.addByte(spellId);
		} else {
			msg.add<uint16_t>(spellId);
		}
	}
	msg.add<uint32_t>(time);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xA5);
	msg.addByte(groupId);
	msg.add<uint32_t>(time);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUseItemCooldown(uint32_t time) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xA6);
	msg.add<uint32_t>(time);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPreyTimeLeft(const std::unique_ptr<PreySlot> &slot) {
	if (!player || !slot) {
		return;
	}

	NetworkMessage msg;

	msg.addByte(0xE7);
	msg.addByte(static_cast<uint8_t>(slot->id));
	msg.add<uint16_t>(slot->bonusTimeLeft);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPreyData(const std::unique_ptr<PreySlot> &slot) {
	if (!player) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xE8);
	std::vector<uint16_t> validRaceIds;
	for (auto raceId : slot->raceIdList) {
		if (g_monsters().getMonsterTypeByRaceId(raceId)) {
			validRaceIds.emplace_back(raceId);
		} else {
			g_logger().error("[ProtocolGame::sendPreyData] - Unknown monster type raceid: {}, removing prey slot from player {}", raceId, player->getName());
			// Remove wrong raceid from slot
			slot->removeMonsterType(raceId);
			// Send empty bytes (do not debug client)
			msg.addByte(0);
			msg.addByte(1);
			msg.add<uint32_t>(0);
			msg.addByte(0);
			writeToOutputBuffer(msg);
			return;
		}
	}

	msg.addByte(static_cast<uint8_t>(slot->id));
	msg.addByte(static_cast<uint8_t>(slot->state));

	if (slot->state == PreyDataState_Locked) {
		msg.addByte(player->isPremium() ? 0x01 : 0x00);
	} else if (slot->state == PreyDataState_Inactive) {
		// Empty
	} else if (slot->state == PreyDataState_Active) {
		if (const auto mtype = g_monsters().getMonsterTypeByRaceId(slot->selectedRaceId)) {
			msg.addString(mtype->name);
			const Outfit_t outfit = mtype->info.outfit;
			msg.add<uint16_t>(outfit.lookType);
			if (outfit.lookType == 0) {
				msg.add<uint16_t>(outfit.lookTypeEx);
			} else {
				msg.addByte(outfit.lookHead);
				msg.addByte(outfit.lookBody);
				msg.addByte(outfit.lookLegs);
				msg.addByte(outfit.lookFeet);
				msg.addByte(outfit.lookAddons);
			}

			msg.addByte(static_cast<uint8_t>(slot->bonus));
			msg.add<uint16_t>(slot->bonusPercentage);
			msg.addByte(slot->bonusRarity);
			msg.add<uint16_t>(slot->bonusTimeLeft);
		}
	} else if (slot->state == PreyDataState_Selection) {
		msg.addByte(static_cast<uint8_t>(validRaceIds.size()));
		for (uint16_t raceId : validRaceIds) {
			const auto mtype = g_monsters().getMonsterTypeByRaceId(raceId);
			if (!mtype) {
				continue;
			}

			msg.addString(mtype->name);
			const Outfit_t outfit = mtype->info.outfit;
			msg.add<uint16_t>(outfit.lookType);
			if (outfit.lookType == 0) {
				msg.add<uint16_t>(outfit.lookTypeEx);
				continue;
			}

			msg.addByte(outfit.lookHead);
			msg.addByte(outfit.lookBody);
			msg.addByte(outfit.lookLegs);
			msg.addByte(outfit.lookFeet);
			msg.addByte(outfit.lookAddons);
		}
	} else if (slot->state == PreyDataState_SelectionChangeMonster) {
		msg.addByte(static_cast<uint8_t>(slot->bonus));
		msg.add<uint16_t>(slot->bonusPercentage);
		msg.addByte(slot->bonusRarity);
		msg.addByte(static_cast<uint8_t>(validRaceIds.size()));
		for (uint16_t raceId : validRaceIds) {
			const auto mtype = g_monsters().getMonsterTypeByRaceId(raceId);
			if (!mtype) {
				continue;
			}

			msg.addString(mtype->name);
			const Outfit_t outfit = mtype->info.outfit;
			msg.add<uint16_t>(outfit.lookType);
			if (outfit.lookType == 0) {
				msg.add<uint16_t>(outfit.lookTypeEx);
				continue;
			}

			msg.addByte(outfit.lookHead);
			msg.addByte(outfit.lookBody);
			msg.addByte(outfit.lookLegs);
			msg.addByte(outfit.lookFeet);
			msg.addByte(outfit.lookAddons);
		}
	} else if (slot->state == PreyDataState_ListSelection) {
		const std::map<uint16_t, std::string> &bestiaryList = g_game().getBestiaryList();
		msg.add<uint16_t>(static_cast<uint16_t>(bestiaryList.size()));
		std::for_each(bestiaryList.begin(), bestiaryList.end(), [&msg](auto mType) {
			msg.add<uint16_t>(mType.first);
		});
	} else {
		g_logger().warn("[ProtocolGame::sendPreyData] - Unknown prey state: {}", fmt::underlying(slot->state));
		return;
	}

	if (oldProtocol) {
		auto currentTime = OTSYS_TIME();
		auto timeDiffMs = (slot->freeRerollTimeStamp > currentTime) ? (slot->freeRerollTimeStamp - currentTime) : 0;
		auto timeDiffMinutes = timeDiffMs / 60000;
		msg.add<uint16_t>(timeDiffMinutes ? timeDiffMinutes : 0);
	} else {
		msg.add<uint32_t>(std::max<uint32_t>(static_cast<uint32_t>(((slot->freeRerollTimeStamp - OTSYS_TIME()) / 1000)), 0));
		msg.addByte(static_cast<uint8_t>(slot->option));
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPreyPrices() {
	if (!player) {
		return;
	}

	NetworkMessage msg;

	msg.addByte(0xE9);
	msg.add<uint32_t>(player->getPreyRerollPrice());
	if (!oldProtocol) {
		msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(PREY_BONUS_REROLL_PRICE)));
		msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(PREY_SELECTION_LIST_PRICE)));
		if (!hasProtocolFeature(protocolProfile, ProtocolFeature::OfficialTaskboardPackets)) {
			msg.add<uint32_t>(player->getTaskHuntingRerollPrice());
			msg.add<uint32_t>(player->getTaskHuntingRerollPrice());
			msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(TASK_HUNTING_SELECTION_LIST_PRICE)));
			msg.addByte(static_cast<uint8_t>(g_configManager().getNumber(TASK_HUNTING_BONUS_REROLL_PRICE)));
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendModalWindow(const ModalWindow &modalWindow) {
	if (!player) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xFA);

	msg.add<uint32_t>(modalWindow.id);
	msg.addString(modalWindow.title);
	msg.addString(modalWindow.message);

	msg.addByte(modalWindow.buttons.size());
	for (const auto &it : modalWindow.buttons) {
		msg.addString(it.first);
		msg.addByte(it.second);
	}

	msg.addByte(modalWindow.choices.size());
	for (const auto &it : modalWindow.choices) {
		msg.addString(it.first);
		msg.addByte(it.second);
	}

	msg.addByte(modalWindow.defaultEscapeButton);
	msg.addByte(modalWindow.defaultEnterButton);
	msg.addByte(modalWindow.priority ? 0x01 : 0x00);

	writeToOutputBuffer(msg);
}

////////////// Add common messages
void ProtocolGame::AddCreature(NetworkMessage &msg, const std::shared_ptr<Creature> &creature, bool known, uint32_t remove) {
	CreatureType_t creatureType = creature->getType();
	std::shared_ptr<Player> otherPlayer = creature->getPlayer();
	const bool cipsoft860 = isCipsoft860Profile(protocolProfile);

	if (known) {
		msg.add<uint16_t>(0x62);
		msg.add<uint32_t>(creature->getID());
	} else {
		msg.add<uint16_t>(0x61);
		msg.add<uint32_t>(remove);
		msg.add<uint32_t>(creature->getID());
		if (cipsoft860) {
			msg.addString(creature->isHealthHidden() ? std::string() : creature->getName());
		} else if (!oldProtocol && creature->isHealthHidden()) {
			msg.addByte(CREATURETYPE_HIDDEN);
		} else {
			msg.addByte(creatureType);
		}

		if (!cipsoft860) {
			if (!oldProtocol && creatureType == CREATURETYPE_SUMMON_PLAYER) {
				if (auto master = creature->getMaster()) {
					msg.add<uint32_t>(master->getID());
				} else {
					msg.add<uint32_t>(0x00);
				}
			}

			if (!oldProtocol && creature->isHealthHidden()) {
				msg.addString(std::string());
			} else {
				msg.addString(creature->getName());
			}
		}
	}

	if (creature->isHealthHidden()) {
		msg.addByte(0x00);
	} else {
		msg.addByte(std::ceil((static_cast<double>(creature->getHealth()) / std::max<int32_t>(creature->getMaxHealth(), 1)) * 100));
	}

	msg.addByte(creature->getDirection());

	if (!creature->isInGhostMode() && !creature->isInvisible()) {
		const Outfit_t &outfit = creature->getCurrentOutfit();
		AddOutfit(msg, outfit);
	} else {
		static Outfit_t outfit;
		AddOutfit(msg, outfit);
	}

	LightInfo lightInfo = creature->getCreatureLight();
	msg.addByte(player->isAccessPlayer() ? 0xFF : lightInfo.level);
	msg.addByte(lightInfo.color);

	msg.add<uint16_t>(creature->getStepSpeed());

	if (!cipsoft860) {
		addCreatureIcon(msg, creature);
	}

	msg.addByte(player->getSkullClient(creature));
	msg.addByte(player->getPartyShield(otherPlayer));

	if (!known) {
		msg.addByte(player->getGuildEmblem(otherPlayer));
	}

	if (cipsoft860) {
		msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01);
		return;
	}

	if (!oldProtocol && creatureType == CREATURETYPE_MONSTER) {
		const auto master = creature->getMaster();
		if (master && master->getPlayer()) {
			creatureType = CREATURETYPE_SUMMON_PLAYER;
		}
	}

	if (!oldProtocol && creature->isHealthHidden()) {
		msg.addByte(CREATURETYPE_HIDDEN);
	} else {
		msg.addByte(creatureType); // Type (for summons)
	}

	if (!oldProtocol && creatureType == CREATURETYPE_SUMMON_PLAYER) {
		const auto master = creature->getMaster();
		msg.add<uint32_t>(master ? master->getID() : 0x00);
	}

	if (!oldProtocol && creatureType == CREATURETYPE_PLAYER) {
		const auto otherCreature = creature->getPlayer();
		msg.addByte(otherCreature ? otherCreature->getVocation()->getClientId() : 0);
	}

	auto bubble = creature->getSpeechBubble();
	msg.addByte(oldProtocol && bubble == SPEECHBUBBLE_HIRELING ? static_cast<uint8_t>(SPEECHBUBBLE_NONE) : bubble);
	msg.addByte(0xFF); // MARK_UNMARKED
	if (!oldProtocol) {
		msg.addByte(0x00); // inspection type
	} else if (otherPlayer) {
		msg.add<uint16_t>(otherPlayer->getHelpers());
	} else {
		msg.add<uint16_t>(0x00);
	}

	msg.addByte(player->canWalkthroughEx(creature) ? 0x00 : 0x01);

	if (isOTCR) {
		msg.addString(creature->getShader()); // g_game.enableFeature(GameCreatureShader)
		msg.addByte(static_cast<uint8_t>(creature->getAttachedEffectList().size())); // g_game.enableFeature(GameCreatureAttachedEffect)
		for (const uint16_t id : creature->getAttachedEffectList()) {
			msg.add<uint16_t>(id); // g_game.enableFeature(GameCreatureAttachedEffect)
		}
	}
}

void ProtocolGame::AddPlayerStats(NetworkMessage &msg) {
	msg.addByte(0xA0);

	if (isCipsoft860Profile(protocolProfile)) {
		uint32_t clientLevel = player->getLevel();
		uint64_t clientExperience = player->getExperience();
		uint8_t clientLevelPercent = player->getLevelPercent();
		if (clientExperience > cipsoft860MaxSignedExperience) {
			clientLevel = std::min<uint32_t>(clientLevel, cipsoft860MaxSignedExperienceLevel);
			clientExperience = Player::getExpForLevel(clientLevel);
			clientLevelPercent = 0;
		}

		msg.add<uint16_t>(std::min<int32_t>(player->getHealth(), std::numeric_limits<uint16_t>::max()));
		msg.add<uint16_t>(std::min<int32_t>(player->getMaxHealth(), std::numeric_limits<uint16_t>::max()));
		msg.add<uint32_t>(player->hasFlag(PlayerFlags_t::HasInfiniteCapacity) ? 1000000 : player->getFreeCapacity());
		msg.add<uint32_t>(static_cast<uint32_t>(clientExperience));
		msg.add<uint16_t>(std::min<uint32_t>(clientLevel, std::numeric_limits<uint16_t>::max()));
		msg.addByte(std::min<uint8_t>(clientLevelPercent, 100));
		msg.add<uint16_t>(std::min<int32_t>(player->getMana(), std::numeric_limits<uint16_t>::max()));
		msg.add<uint16_t>(std::min<int32_t>(player->getMaxMana(), std::numeric_limits<uint16_t>::max()));
		msg.addByte(static_cast<uint8_t>(std::min<uint32_t>(player->getMagicLevel(), std::numeric_limits<uint8_t>::max())));
		msg.addByte(std::min<uint8_t>(static_cast<uint8_t>(player->getMagicLevelPercent()), 100));
		msg.addByte(player->getSoul());
		msg.add<uint16_t>(player->getStaminaMinutes());
		return;
	}

	if (oldProtocol) {
		msg.add<uint16_t>(std::min<int32_t>(player->getHealth(), std::numeric_limits<uint16_t>::max()));
		msg.add<uint16_t>(std::min<int32_t>(player->getMaxHealth(), std::numeric_limits<uint16_t>::max()));
	} else {
		msg.add<uint32_t>(std::min<int32_t>(player->getHealth(), std::numeric_limits<int32_t>::max()));
		msg.add<uint32_t>(std::min<int32_t>(player->getMaxHealth(), std::numeric_limits<int32_t>::max()));
	}

	msg.add<uint32_t>(player->hasFlag(PlayerFlags_t::HasInfiniteCapacity) ? 1000000 : player->getFreeCapacity());
	if (oldProtocol) {
		msg.add<uint32_t>(player->getFreeCapacity());
	}

	msg.add<uint64_t>(player->getExperience());

	msg.add<uint16_t>(player->getLevel());
	if (!oldProtocol) {
		if (hasProtocolFeature(protocolProfile, ProtocolFeature::PlayerDataLevelPercentU16)) {
			const auto levelPercent = std::min<uint16_t>(static_cast<uint16_t>(player->getLevelPercent() * 100), 10000);
			msg.add<uint16_t>(levelPercent);
		} else {
			msg.addByte(std::min<uint8_t>(player->getLevelPercent(), 100));
		}
	} else {
		msg.addByte(std::min<uint8_t>(player->getLevelPercent(), 100));
	}

	msg.add<uint16_t>(player->getBaseXpGain()); // base xp gain rate

	if (oldProtocol) {
		msg.add<uint16_t>(player->getVoucherXpBoost()); // xp voucher
	}

	msg.add<uint16_t>(player->getDisplayGrindingXpBoost()); // low level bonus
	msg.add<uint16_t>(player->getDisplayXpBoostPercent()); // xp boost
	msg.add<uint16_t>(player->getStaminaXpBoost()); // stamina multiplier (100 = 1.0x)

	if (!oldProtocol) {
		msg.add<uint32_t>(std::min<int32_t>(player->getMana(), std::numeric_limits<int32_t>::max()));
		msg.add<uint32_t>(std::min<int32_t>(player->getMaxMana(), std::numeric_limits<int32_t>::max()));
	} else {
		msg.add<uint16_t>(std::min<int32_t>(player->getMana(), std::numeric_limits<uint16_t>::max()));
		msg.add<uint16_t>(std::min<int32_t>(player->getMaxMana(), std::numeric_limits<uint16_t>::max()));

		msg.addByte(static_cast<uint8_t>(std::min<uint32_t>(player->getMagicLevel(), std::numeric_limits<uint8_t>::max())));
		msg.addByte(static_cast<uint8_t>(std::min<uint32_t>(player->getBaseMagicLevel(), std::numeric_limits<uint8_t>::max())));
		msg.addByte(std::min<uint8_t>(static_cast<uint8_t>(player->getMagicLevelPercent()), 100));
	}

	msg.addByte(player->getSoul());

	msg.add<uint16_t>(player->getStaminaMinutes());

	msg.add<uint16_t>(player->getBaseSpeed());

	std::shared_ptr<Condition> condition = player->getCondition(CONDITION_REGENERATION, CONDITIONID_DEFAULT);
	msg.add<uint16_t>(condition ? condition->getTicks() / 1000 : 0x00);

	msg.add<uint16_t>(player->getOfflineTrainingTime() / 60 / 1000);

	msg.add<uint16_t>(player->getXpBoostTime()); // xp boost time (seconds)
	msg.addByte(1); // enables exp boost in the store

	if (!oldProtocol) {
		msg.add<uint32_t>(player->getManaShield()); // remaining mana shield
		msg.add<uint32_t>(player->getMaxManaShield()); // total mana shield
	}
}

void ProtocolGame::AddPlayerSkills(NetworkMessage &msg) {
	msg.addByte(0xA1);

	if (isCipsoft860Profile(protocolProfile)) {
		for (uint8_t i = SKILL_FIRST; i <= SKILL_FISHING; ++i) {
			auto skill = static_cast<skills_t>(i);
			msg.addByte(static_cast<uint8_t>(std::min<uint16_t>(player->getSkillLevel(skill), std::numeric_limits<uint8_t>::max())));
			msg.addByte(static_cast<uint8_t>(std::min<uint16_t>(player->getSkillPercent(skill), 100)));
		}
		return;
	}

	if (isTibia1100Profile(protocolProfile)) {
		for (uint8_t i = SKILL_FIRST; i <= SKILL_FISHING; ++i) {
			auto skill = static_cast<skills_t>(i);
			msg.add<uint16_t>(std::min<int32_t>(player->getSkillLevel(skill), std::numeric_limits<uint16_t>::max()));
			msg.add<uint16_t>(player->getBaseSkill(skill));
			msg.addByte(std::min<uint8_t>(100, static_cast<uint8_t>(player->getSkillPercent(skill))));
		}

		for (uint8_t i = SKILL_CRITICAL_HIT_CHANCE; i <= SKILL_LAST; ++i) {
			auto skill = static_cast<skills_t>(i);
			msg.add<uint16_t>(std::min<int32_t>(player->getSkillLevel(skill), std::numeric_limits<uint16_t>::max()));
			msg.add<uint16_t>(player->getBaseSkill(skill));
		}
		return;
	}

	if (oldProtocol) {
		for (uint8_t i = SKILL_FIRST; i <= SKILL_FISHING; ++i) {
			auto skill = static_cast<skills_t>(i);
			msg.add<uint16_t>(std::min<int32_t>(player->getSkillLevel(skill), std::numeric_limits<uint16_t>::max()));
			msg.add<uint16_t>(player->getBaseSkill(skill));
			msg.addByte(std::min<uint8_t>(100, static_cast<uint8_t>(player->getSkillPercent(skill))));
		}
	} else {
		msg.add<uint16_t>(player->getMagicLevel());
		msg.add<uint16_t>(player->getBaseMagicLevel());
		msg.add<uint16_t>(player->getLoyaltyMagicLevel());
		msg.add<uint16_t>(player->getMagicLevelPercent() * 100);

		for (uint8_t i = SKILL_FIRST; i <= SKILL_FISHING; ++i) {
			auto skill = static_cast<skills_t>(i);
			msg.add<uint16_t>(std::min<int32_t>(player->getSkillLevel(skill), std::numeric_limits<uint16_t>::max()));
			msg.add<uint16_t>(player->getBaseSkill(skill));
			msg.add<uint16_t>(player->getLoyaltySkill(skill));
			msg.add<uint16_t>(player->getSkillPercent(skill) * 100);
		}
	}

	// 13.10 List (U8 + U16)
	msg.addByte(0);

	// Used for Imbuement (Feather)
	msg.add<uint32_t>(player->getCapacity()); // Total Capacity
	msg.add<uint32_t>(player->getBaseCapacity()); // Base Total Capacity

	const auto flatBonus = player->calculateFlatDamageHealing();
	msg.add<uint16_t>(flatBonus); // Flat Damage and Healing Total

	const auto &weapon = player->getWeapon();
	if (weapon) {
		const ItemType &it = Item::items[weapon->getID()];
		if (it.weaponType == WEAPON_WAND) {
			msg.add<uint16_t>(it.maxHitChance);
			msg.addByte(getCipbiaElement(it.combatType));
			msg.addDouble(0.0);
			msg.addByte(0x00);
		} else if (it.weaponType == WEAPON_DISTANCE || it.weaponType == WEAPON_AMMO || it.weaponType == WEAPON_MISSILE) {
			int32_t physicalAttack = std::max<int32_t>(0, weapon->getAttack());
			physicalAttack += player->weaponProficiency().getStat(WeaponProficiencyBonus_t::ATTACK_DAMAGE);
			int32_t elementalAttack = 0;
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				elementalAttack = std::max<int32_t>(0, it.abilities->elementDamage);
			}
			int32_t attackValue = physicalAttack + elementalAttack;
			if (it.weaponType == WEAPON_AMMO) {
				std::shared_ptr<Item> weaponItem = player->getWeapon(true);
				if (weaponItem) {
					attackValue += weaponItem->getAttack();
				}
			}

			int32_t distanceValue = player->getSkillLevel(SKILL_DISTANCE);
			const auto attackTotal = player->attackTotal(flatBonus, attackValue, distanceValue);

			msg.add<uint16_t>(attackTotal);
			msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL);

			// Converted Damage
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				if (physicalAttack) {
					msg.addDouble(elementalAttack / static_cast<double>(attackValue));
				} else {
					msg.addDouble(0.0);
				}
				msg.addByte(getCipbiaElement(it.abilities->elementType));
			} else {
				handleImbuementDamage(msg, player);
			}
		} else {
			int32_t physicalAttack = std::max<int32_t>(0, weapon->getAttack());
			physicalAttack += player->weaponProficiency().getStat(WeaponProficiencyBonus_t::ATTACK_DAMAGE);
			int32_t elementalAttack = 0;
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				elementalAttack = std::max<int32_t>(0, it.abilities->elementDamage);
			}
			int32_t weaponAttack = physicalAttack + elementalAttack;
			int32_t weaponSkill = player->getWeaponSkill(weapon);
			const auto attackTotal = player->attackTotal(flatBonus, weaponAttack, weaponSkill);

			msg.add<uint16_t>(attackTotal);
			msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL);

			// Converted Damage
			if (it.abilities && it.abilities->elementType != COMBAT_NONE) {
				if (physicalAttack) {
					msg.addDouble(elementalAttack / static_cast<double>(weaponAttack));
				} else {
					msg.addDouble(0);
				}
				msg.addByte(getCipbiaElement(it.abilities->elementType));
			} else {
				handleImbuementDamage(msg, player);
			}
		}
	} else {
		uint16_t attackValue = 7;
		int32_t fistValue = player->getSkillLevel(SKILL_FIST);
		const auto attackTotal = player->attackTotal(flatBonus, attackValue, fistValue);

		msg.add<uint16_t>(attackTotal);
		msg.addByte(CIPBIA_ELEMENTAL_PHYSICAL);

		msg.addDouble(0.0);
		msg.addByte(0x00);
	}

	// Imbuements
	msg.addDouble(player->getSkillLevel(SKILL_LIFE_LEECH_AMOUNT) / 10000.); // Life Leech
	msg.addDouble(player->getSkillLevel(SKILL_MANA_LEECH_AMOUNT) / 10000.); // Mana Leech
	msg.addDouble(player->getSkillLevel(SKILL_CRITICAL_HIT_CHANCE) / 10000.); // Crit Chance
	msg.addDouble(player->getSkillLevel(SKILL_CRITICAL_HIT_DAMAGE) / 10000.); // Crit Extra Damage
	msg.addDouble(getForgeSkillStat(CONST_SLOT_LEFT)); // Onslaught

	msg.add<uint16_t>(player->getDefense(true));
	msg.add<uint16_t>(player->getArmor());
	msg.add<uint16_t>(player->getPartyMantra());
	msg.addDouble(player->getMitigation() / 100.); // Mitigation
	msg.addDouble(getForgeSkillStat(CONST_SLOT_ARMOR)); // Dodge (Ruse)
	msg.add<uint16_t>(static_cast<uint16_t>(player->getReflectFlat(COMBAT_PHYSICALDAMAGE))); // Damage Reflection

	// Store the "combats" to increase in absorb values function and send to client later
	uint8_t combats = 0;
	auto startCombats = msg.getBufferPosition();
	msg.skipBytes(1);

	// Calculate and parse the combat absorbs values
	calculateAbsorbValues(player, msg, combats, true);

	// Now set the buffer position skiped and send the total combats count
	auto endCombats = msg.getBufferPosition();
	msg.setBufferPosition(startCombats);
	msg.addByte(combats);
	msg.setBufferPosition(endCombats);

	// Forge Bonus
	msg.addDouble(getForgeSkillStat(CONST_SLOT_HEAD)); // Momentum
	msg.addDouble(getForgeSkillStat(CONST_SLOT_LEGS)); // Transcedence
	msg.addDouble(getForgeSkillStat(CONST_SLOT_FEET, false)); // Amplification
}

void ProtocolGame::AddOutfit(NetworkMessage &msg, const Outfit_t &outfit, bool addMount /* = true*/) {
	msg.add<uint16_t>(outfit.lookType);
	if (outfit.lookType != 0) {
		msg.addByte(outfit.lookHead);
		msg.addByte(outfit.lookBody);
		msg.addByte(outfit.lookLegs);
		msg.addByte(outfit.lookFeet);
		msg.addByte(outfit.lookAddons);
	} else {
		msg.add<uint16_t>(outfit.lookTypeEx);
	}

	if (addMount && version >= 870) {
		msg.add<uint16_t>(outfit.lookMount);
		if (!oldProtocol && outfit.lookMount != 0) {
			msg.addByte(outfit.lookMountHead);
			msg.addByte(outfit.lookMountBody);
			msg.addByte(outfit.lookMountLegs);
			msg.addByte(outfit.lookMountFeet);
		}
	}
	if (isOTCR) {
		AddOutfitCustomOTCR(msg, outfit); // g_game.enableFeature(GameWingsAurasEffectsShader)
	}
}

void ProtocolGame::addImbuementInfo(NetworkMessage &msg, uint16_t imbuementID, bool isScrollAction /* = false */) const {
	Imbuement* imbuement = g_imbuements().getImbuement(imbuementID);
	const BaseImbuement* baseImbuement = g_imbuements().getBaseByID(imbuement->getBaseID());

	msg.add<uint32_t>(imbuement->getID());
	msg.addString(fmt::format("{} {}", baseImbuement->name, imbuement->getName()));
	msg.addString(imbuement->getDescription());

	msg.addByte(imbuement->getBaseID() - 1);

	msg.add<uint16_t>(imbuement->getIconID());
	msg.add<uint32_t>(baseImbuement->duration);

	auto items = imbuement->getItems();
	if (isScrollAction) {
		(void)items.emplace_back(std::make_pair(ITEM_EMPTY_IMBUEMENT_SCROLL, 1));
	}
	msg.addByte(items.size());

	for (const auto &[id, amount] : items) {
		const ItemType &it = Item::items[id];
		msg.add<uint16_t>(id);
		msg.addString(it.name);
		msg.add<uint16_t>(amount);
	}

	msg.add<uint32_t>(baseImbuement->price);
}

void ProtocolGame::addTibia1100ImbuementInfo(NetworkMessage &msg, uint16_t imbuementID) const {
	Imbuement* imbuement = g_imbuements().getImbuement(imbuementID);
	const BaseImbuement* baseImbuement = g_imbuements().getBaseByID(imbuement->getBaseID());
	const CategoryImbuement* categoryImbuement = g_imbuements().getCategoryByID(imbuement->getCategory());

	msg.add<uint32_t>(imbuementID);
	msg.addString(fmt::format("{} {}", baseImbuement->name, imbuement->getName()));
	msg.addString(imbuement->getDescription());
	msg.addString(categoryImbuement->name + imbuement->getSubGroup());

	msg.add<uint16_t>(imbuement->getIconID());
	msg.add<uint32_t>(baseImbuement->duration);
	msg.addByte(imbuement->isPremium() ? 0x01 : 0x00);

	const auto &items = imbuement->getItems();
	msg.addByte(items.size());
	for (const auto &[id, amount] : items) {
		const ItemType &it = Item::items[id];
		msg.add<uint16_t>(id);
		msg.addString(it.name);
		msg.add<uint16_t>(amount);
	}

	msg.add<uint32_t>(baseImbuement->price);
	msg.addByte(baseImbuement->percent);
	msg.add<uint32_t>(baseImbuement->protectionPrice);
}

void ProtocolGame::addAvailableImbuementsInfo(NetworkMessage &msg, const std::shared_ptr<Item> &item, phmap::flat_hash_map<uint16_t, uint16_t> &neededItems, bool isScrollAction /* = false */) const {
	auto imbuements = g_imbuements().getImbuements(player, item, isScrollAction);
	msg.add<uint16_t>(imbuements.size());
	for (const Imbuement* imbuement : imbuements) {
		addImbuementInfo(msg, imbuement->getID(), isScrollAction);

		const auto &imbuementItems = imbuement->getItems();
		for (const auto &[id, _] : imbuementItems) {
			if (!neededItems.count(id)) {
				const uint32_t invCount = player->getItemTypeCount(id);
				const uint32_t stashCount = player->getStashItemCount(id);
				const uint32_t total = invCount + stashCount;
				neededItems[id] = static_cast<uint16_t>(std::min<uint32_t>(total, std::numeric_limits<uint16_t>::max()));
			}
		}
	}

	if (isScrollAction) {
		if (!neededItems.count(ITEM_EMPTY_IMBUEMENT_SCROLL)) {
			const uint32_t invCount = player->getItemTypeCount(ITEM_EMPTY_IMBUEMENT_SCROLL);
			const uint32_t stashCount = player->getStashItemCount(ITEM_EMPTY_IMBUEMENT_SCROLL);
			const uint32_t total = invCount + stashCount;
			neededItems[ITEM_EMPTY_IMBUEMENT_SCROLL] = static_cast<uint16_t>(std::min<uint32_t>(total, std::numeric_limits<uint16_t>::max()));
		}
	}
}

void ProtocolGame::openImbuementWindow(ImbuementAction action, const std::shared_ptr<Item> &item) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::ImbuementWindow)) {
		player->sendCancelMessage("Imbuements are not available on this client version.");
		return;
	}

	if (isTibia1100Profile(protocolProfile)) {
		if (action != ImbuementAction::PickItem || !item) {
			player->sendCancelMessage("Select an imbuable item to open the imbuement window.");
			return;
		}

		openTibia1100ImbuementWindow(item);
		return;
	}

	if (!item && action == ImbuementAction::PickItem) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xEB);

	phmap::flat_hash_map<uint16_t, uint16_t> neededItems;

	msg.addByte(static_cast<uint8_t>(action));
	const auto emptyImbuementScrolls = player->getItemTypeCount(ITEM_EMPTY_IMBUEMENT_SCROLL) + player->getStashItemCount(ITEM_EMPTY_IMBUEMENT_SCROLL);
	msg.addByte(emptyImbuementScrolls > 0 ? 0x01 : 0x00);
	if (action == ImbuementAction::Open) {
		msg.add<uint16_t>(0);
	} else if (action == ImbuementAction::PickItem) {
		player->setImbuingItem(item);
		msg.add<uint16_t>(item->getID());
		if (item->getClassification() > 0) {
			msg.addByte(item->getTier());
		}

		msg.addByte(item->getImbuementSlot());

		// Imbuements applied
		for (uint8_t slotID = 0; slotID < item->getImbuementSlot(); slotID++) {
			ImbuementInfo imbuementInfo;
			if (!item->getImbuementInfo(slotID, &imbuementInfo)) {
				msg.addByte(0x00);
				continue;
			}

			msg.addByte(0x01);
			addImbuementInfo(msg, imbuementInfo.imbuement->getID());
			msg.add<uint32_t>(imbuementInfo.duration);
			msg.add<uint32_t>(g_imbuements().getBaseByID(imbuementInfo.imbuement->getBaseID())->removeCost);
		}

		addAvailableImbuementsInfo(msg, item, neededItems);

	} else if (action == ImbuementAction::Scroll) {
		const auto freeSlots = player->getFreeBackpackSlots();
		msg.addByte(freeSlots > 0 ? 0x01 : 0x00);
		msg.addByte(0); // Unknown Byte

		addAvailableImbuementsInfo(msg, nullptr, neededItems, true);
	}

	msg.add<uint32_t>(neededItems.size());
	for (const auto &[id, amount] : neededItems) {
		msg.add<uint16_t>(id);
		msg.add<uint16_t>(amount);
	}

	sendResourceBalance(RESOURCE_BANK, player->getBankBalance());
	sendResourceBalance(RESOURCE_INVENTORY_MONEY, player->getMoney());

	writeToOutputBuffer(msg);
}

void ProtocolGame::openTibia1100ImbuementWindow(const std::shared_ptr<Item> &item) {
	if (!item || item->isRemoved()) {
		return;
	}

	player->setImbuingItem(item);

	NetworkMessage msg;
	msg.addByte(0xEB);
	msg.add<uint16_t>(item->getID());
	msg.addByte(item->getImbuementSlot());

	for (uint8_t slotID = 0; slotID < item->getImbuementSlot(); slotID++) {
		ImbuementInfo imbuementInfo;
		if (!item->getImbuementInfo(slotID, &imbuementInfo)) {
			msg.addByte(0x00);
			continue;
		}

		msg.addByte(0x01);
		addTibia1100ImbuementInfo(msg, imbuementInfo.imbuement->getID());
		msg.add<uint32_t>(imbuementInfo.duration);
		msg.add<uint32_t>(g_imbuements().getBaseByID(imbuementInfo.imbuement->getBaseID())->removeCost);
	}

	phmap::flat_hash_map<uint16_t, uint16_t> neededItems;
	const auto imbuements = g_imbuements().getImbuements(player, item);
	msg.add<uint16_t>(imbuements.size());
	for (const Imbuement* imbuement : imbuements) {
		addTibia1100ImbuementInfo(msg, imbuement->getID());

		for (const auto &[id, _] : imbuement->getItems()) {
			if (neededItems.count(id) != 0) {
				continue;
			}

			const uint32_t invCount = player->getItemTypeCount(id);
			const uint32_t stashCount = player->getStashItemCount(id);
			const uint32_t total = invCount + stashCount;
			neededItems[id] = static_cast<uint16_t>(std::min<uint32_t>(total, std::numeric_limits<uint16_t>::max()));
		}
	}

	msg.add<uint32_t>(neededItems.size());
	for (const auto &[id, amount] : neededItems) {
		msg.add<uint16_t>(id);
		msg.add<uint16_t>(amount);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMessageDialog(const std::string &message) {
	NetworkMessage msg;
	msg.addByte(0xED);
	msg.addByte(0x14); // Unknown type
	msg.addString(message);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendImbuementResult(const std::string &message) {
	NetworkMessage msg;
	msg.addByte(0xED);
	msg.addByte(0x01);
	msg.addString(message);
	writeToOutputBuffer(msg);
}

void ProtocolGame::closeImbuementWindow() {
	NetworkMessage msg;
	msg.addByte(0xEC);
	writeToOutputBuffer(msg);
}

void ProtocolGame::AddWorldLight(NetworkMessage &msg, LightInfo lightInfo) {
	msg.addByte(0x82);
	msg.addByte((player->isAccessPlayer() ? 0xFF : lightInfo.level));
	msg.addByte(lightInfo.color);
}

void ProtocolGame::sendSpecialContainersAvailable() {
	if (oldProtocol || !player) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x2A);
	msg.addByte(player->isStashMenuAvailable() ? 0x01 : 0x00);
	msg.addByte(player->isMarketMenuAvailable() ? 0x01 : 0x00);
	writeToOutputBuffer(msg);
}

void ProtocolGame::updatePartyTrackerAnalyzer(const std::shared_ptr<Party> &party, bool force) {
	if (oldProtocol || !player || !party || !party->getLeader()) {
		return;
	}

	if (force) {
		m_nextPartyAnalyzerUpdate = 0;
	}

	const uint64_t currentTime = OTSYS_TIME();
	if (!force && currentTime < m_nextPartyAnalyzerUpdate) {
		return;
	}

	m_nextPartyAnalyzerUpdate = currentTime + PARTY_ANALYZER_THROTTLE_MS;

	NetworkMessage msg;
	msg.addByte(0x2B);
	msg.add<uint32_t>(party->getAnalyzerTimeNow());
	msg.add<uint32_t>(party->getLeader()->getID());
	msg.addByte(static_cast<uint8_t>(party->priceType));

	msg.addByte(static_cast<uint8_t>(party->membersData.size()));
	for (const std::shared_ptr<PartyAnalyzer> &analyzer : party->membersData) {
		msg.add<uint32_t>(analyzer->id);
		if (std::shared_ptr<Player> member = g_game().getPlayerByID(analyzer->id);
		    !member || !member->getParty() || member->getParty() != party) {
			msg.addByte(0);
		} else {
			msg.addByte(1);
		}

		msg.add<uint64_t>(analyzer->lootPrice);
		msg.add<uint64_t>(analyzer->supplyPrice);
		msg.add<uint64_t>(analyzer->damage);
		msg.add<uint64_t>(analyzer->healing);
	}

	bool showNames = !party->membersData.empty();
	msg.addByte(showNames ? 0x01 : 0x00);
	if (showNames) {
		msg.addByte(static_cast<uint8_t>(party->membersData.size()));
		for (const std::shared_ptr<PartyAnalyzer> &analyzer : party->membersData) {
			msg.add<uint32_t>(analyzer->id);
			msg.addString(analyzer->name);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::AddCreatureLight(NetworkMessage &msg, const std::shared_ptr<Creature> &creature) {
	LightInfo lightInfo = creature->getCreatureLight();

	msg.addByte(0x8D);
	msg.add<uint32_t>(creature->getID());
	msg.addByte((player->isAccessPlayer() ? 0xFF : lightInfo.level));
	msg.addByte(lightInfo.color);
}

// tile
void ProtocolGame::RemoveTileThing(NetworkMessage &msg, const Position &pos, uint32_t stackpos) {
	if (stackpos >= 10) {
		return;
	}

	msg.addByte(0x6C);
	msg.addPosition(pos);
	msg.addByte(static_cast<uint8_t>(stackpos));
}

void ProtocolGame::sendKillTrackerUpdate(const std::shared_ptr<Container> &corpse, const std::string &name, const Outfit_t creatureOutfit) {
	if (oldProtocol) {
		return;
	}

	bool isCorpseEmpty = corpse->empty();

	NetworkMessage msg;
	msg.addByte(0xD1);
	msg.addString(name);
	msg.add<uint16_t>(creatureOutfit.lookType ? creatureOutfit.lookType : 21);
	msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookHead : 0x00);
	msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookBody : 0x00);
	msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookLegs : 0x00);
	msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookFeet : 0x00);
	msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookAddons : 0x00);
	msg.addByte(isCorpseEmpty ? 0 : corpse->size());

	if (!isCorpseEmpty) {
		for (const auto &it : corpse->getItemList()) {
			AddItem(msg, it);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateSupplyTracker(const std::shared_ptr<Item> &item) {
	if (oldProtocol || !player || !item) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xCE);
	msg.add<uint16_t>(item->getID());

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateImpactTracker(CombatType_t type, int32_t amount) {
	if (!player || oldProtocol) {
		return;
	}

	auto clientElement = getCipbiaElement(type);
	if (clientElement == CIPBIA_ELEMENTAL_UNDEFINED) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xCC);
	if (type == COMBAT_HEALING) {
		msg.addByte(ANALYZER_HEAL);
		msg.add<uint32_t>(amount);
	} else {
		msg.addByte(ANALYZER_DAMAGE_DEALT);
		msg.add<uint32_t>(amount);
		msg.addByte(clientElement);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendUpdateInputAnalyzer(CombatType_t type, int32_t amount, const std::string &target) {
	if (!player || oldProtocol) {
		return;
	}

	auto clientElement = getCipbiaElement(type);
	if (clientElement == CIPBIA_ELEMENTAL_UNDEFINED) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xCC);
	msg.addByte(ANALYZER_DAMAGE_RECEIVED);
	msg.add<uint32_t>(amount);
	msg.addByte(clientElement);
	msg.addString(target);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTaskHuntingData(const std::unique_ptr<TaskHuntingSlot> &slot) {
	if (!player || oldProtocol || hasProtocolFeature(protocolProfile, ProtocolFeature::OfficialTaskboardPackets)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xBB);
	msg.addByte(static_cast<uint8_t>(slot->id));
	msg.addByte(static_cast<uint8_t>(slot->state));
	if (slot->state == PreyTaskDataState_Locked) {
		msg.addByte(player->isPremium() ? 0x01 : 0x00);
	} else if (slot->state == PreyTaskDataState_Inactive) {
		// Empty
	} else if (slot->state == PreyTaskDataState_Selection) {
		std::shared_ptr<Player> user = player;
		msg.add<uint16_t>(static_cast<uint16_t>(slot->raceIdList.size()));
		std::for_each(slot->raceIdList.begin(), slot->raceIdList.end(), [&msg, user](uint16_t raceid) {
			msg.add<uint16_t>(raceid);
			msg.addByte(user->isCreatureUnlockedOnTaskHunting(g_monsters().getMonsterTypeByRaceId(raceid)) ? 0x01 : 0x00);
		});
	} else if (slot->state == PreyTaskDataState_ListSelection) {
		std::shared_ptr<Player> user = player;
		const std::map<uint16_t, std::string> &bestiaryList = g_game().getBestiaryList();
		msg.add<uint16_t>(static_cast<uint16_t>(bestiaryList.size()));
		std::for_each(bestiaryList.begin(), bestiaryList.end(), [&msg, user](auto mType) {
			msg.add<uint16_t>(mType.first);
			msg.addByte(user->isCreatureUnlockedOnTaskHunting(g_monsters().getMonsterType(mType.second)) ? 0x01 : 0x00);
		});
	} else if (slot->state == PreyTaskDataState_Active) {
		if (const auto &option = g_ioprey().getTaskRewardOption(slot)) {
			msg.add<uint16_t>(slot->selectedRaceId);
			if (slot->upgrade) {
				msg.addByte(0x01);
				msg.add<uint16_t>(option->secondKills);
			} else {
				msg.addByte(0x00);
				msg.add<uint16_t>(option->firstKills);
			}
			msg.add<uint16_t>(slot->currentKills);
			msg.addByte(slot->rarity);
		} else {
			g_logger().warn("[ProtocolGame::sendTaskHuntingData] - Unknown slot option {} on player {}", fmt::underlying(slot->id), player->getName());
			return;
		}
	} else if (slot->state == PreyTaskDataState_Completed) {
		if (const auto &option = g_ioprey().getTaskRewardOption(slot)) {
			msg.add<uint16_t>(slot->selectedRaceId);
			if (slot->upgrade) {
				msg.addByte(0x01);
				msg.add<uint16_t>(option->secondKills);
				msg.add<uint16_t>(std::min<uint16_t>(slot->currentKills, option->secondKills));
			} else {
				msg.addByte(0x00);
				msg.add<uint16_t>(option->firstKills);
				msg.add<uint16_t>(std::min<uint16_t>(slot->currentKills, option->firstKills));
			}
			msg.addByte(slot->rarity);
		} else {
			g_logger().warn("[ProtocolGame::sendTaskHuntingData] - Unknown slot option {} on player {}", fmt::underlying(slot->id), player->getName());
			return;
		}
	} else {
		g_logger().warn("[ProtocolGame::sendTaskHuntingData] - Unknown task hunting state: {}", fmt::underlying(slot->state));
		return;
	}

	msg.add<uint32_t>(std::max<uint32_t>(static_cast<uint32_t>(((slot->freeRerollTimeStamp - OTSYS_TIME()) / 1000)), 0));
	writeToOutputBuffer(msg);
}

void ProtocolGame::MoveUpCreature(NetworkMessage &msg, const std::shared_ptr<Creature> &creature, const Position &newPos, const Position &oldPos) {
	if (creature != player) {
		return;
	}

	// floor change up
	msg.addByte(0xBE);

	// going to surface
	if (newPos.z == MAP_INIT_SURFACE_LAYER) {
		int32_t skip = -1;
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, 5, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 3, skip); //(floor 7 and 6 already set)
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, 4, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 4, skip);
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, 3, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 5, skip);
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, 2, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 6, skip);
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, 1, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 7, skip);
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, 0, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 8, skip);

		if (skip >= 0) {
			msg.addByte(skip);
			msg.addByte(0xFF);
		}
	}
	// underground, going one floor up (still underground)
	else if (newPos.z > MAP_INIT_SURFACE_LAYER) {
		int32_t skip = -1;
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, oldPos.getZ() - 3, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, 3, skip);

		if (skip >= 0) {
			msg.addByte(skip);
			msg.addByte(0xFF);
		}
	}

	// moving up a floor up makes us out of sync
	// west
	msg.addByte(0x68);
	GetMapDescription(oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - (MAP_MAX_CLIENT_VIEW_PORT_Y - 1), newPos.z, 1, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, msg);

	// north
	msg.addByte(0x65);
	GetMapDescription(oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, 1, msg);
}

void ProtocolGame::MoveDownCreature(NetworkMessage &msg, const std::shared_ptr<Creature> &creature, const Position &newPos, const Position &oldPos) {
	if (creature != player) {
		return;
	}

	// floor change down
	msg.addByte(0xBF);

	// going from surface to underground
	if (newPos.z == MAP_INIT_SURFACE_LAYER + 1) {
		int32_t skip = -1;

		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, -1, skip);
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z + 1, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, -2, skip);
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z + 2, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, -3, skip);

		if (skip >= 0) {
			msg.addByte(skip);
			msg.addByte(0xFF);
		}
	}
	// going further down
	else if (newPos.z > oldPos.z && newPos.z > MAP_INIT_SURFACE_LAYER + 1 && newPos.z < MAP_MAX_LAYERS - MAP_LAYER_VIEW_LIMIT) {
		int32_t skip = -1;
		GetFloorDescription(msg, oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y - MAP_MAX_CLIENT_VIEW_PORT_Y, newPos.z + MAP_LAYER_VIEW_LIMIT, (MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2, (MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2, -3, skip);

		if (skip >= 0) {
			msg.addByte(skip);
			msg.addByte(0xFF);
		}
	}

	// moving down a floor makes us out of sync
	// east
	msg.addByte(0x66);
	GetMapDescription(oldPos.x + MAP_MAX_CLIENT_VIEW_PORT_X + 1, oldPos.y - (MAP_MAX_CLIENT_VIEW_PORT_Y + 1), newPos.z, 1, ((MAP_MAX_CLIENT_VIEW_PORT_Y + 1) * 2), msg);

	// south
	msg.addByte(0x67);
	GetMapDescription(oldPos.x - MAP_MAX_CLIENT_VIEW_PORT_X, oldPos.y + (MAP_MAX_CLIENT_VIEW_PORT_Y + 1), newPos.z, ((MAP_MAX_CLIENT_VIEW_PORT_X + 1) * 2), 1, msg);
}

void ProtocolGame::AddHiddenShopItem(NetworkMessage &msg) {
	// Empty bytes from AddShopItem
	msg.add<uint16_t>(0);
	msg.addByte(0);
	msg.addString(std::string());
	msg.add<uint32_t>(0);
	msg.add<uint32_t>(0);
	msg.add<uint32_t>(0);
}

void ProtocolGame::AddShopItem(NetworkMessage &msg, const ShopBlock &shopBlock) {
	// Sends the item information empty if the player doesn't have the storage to buy/sell a certain item
	if (shopBlock.itemStorageKey != 0 && player->getStorageValue(shopBlock.itemStorageKey) < shopBlock.itemStorageValue) {
		AddHiddenShopItem(msg);
		return;
	}

	const ItemType &it = Item::items[shopBlock.itemId];
	msg.add<uint16_t>(shopBlock.itemId);
	if (it.isSplash() || it.isFluidContainer()) {
		msg.addByte(static_cast<uint8_t>(shopBlock.itemSubType));
	} else {
		msg.addByte(0x00);
	}

	// If not send "itemName" variable from the npc shop, will registered the name that is in items.xml
	if (shopBlock.itemName.empty()) {
		msg.addString(it.name);
	} else {
		msg.addString(shopBlock.itemName);
	}
	msg.add<uint32_t>(it.weight);
	msg.add<uint32_t>(shopBlock.itemBuyPrice == 4294967295 ? 0 : shopBlock.itemBuyPrice);
	msg.add<uint32_t>(shopBlock.itemSellPrice == 4294967295 ? 0 : shopBlock.itemSellPrice);
}

void ProtocolGame::parseExtendedOpcode(NetworkMessage &msg) {
	uint8_t opcode = msg.getByte();
	const std::string &buffer = msg.getString();

	// process additional opcodes via lua script event
	g_game().parsePlayerExtendedOpcode(player->getID(), opcode, buffer);
}

// OTCv8
void ProtocolGame::sendFeatures() {
	if (otclientV8 == 0) {
		return;
	}

	std::map<GameFeature_t, bool> features;
	// Place for non-standard OTCv8 features
	features[GameFeature_t::ExtendedOpcode] = true;

	if (features.empty()) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x43);
	msg.add<uint16_t>(static_cast<uint16_t>(features.size()));
	for (const auto &[gameFeature, haveFeature] : features) {
		msg.addByte(static_cast<uint8_t>(gameFeature));
		msg.addByte(haveFeature ? 1 : 0);
	}
	writeToOutputBuffer(msg);
}

// OTCR
void ProtocolGame::sendOTCRFeatures() {
	isOTCR = true;
	const auto &enabledFeatures = g_configManager().getEnabledFeaturesOTC();
	const auto &disabledFeatures = g_configManager().getDisabledFeaturesOTC();
	NetworkMessage msg;
	msg.addByte(0x43);
	auto totalFeatures = static_cast<uint16_t>(enabledFeatures.size() + disabledFeatures.size());
	msg.add<uint16_t>(totalFeatures);
	for (auto feature : enabledFeatures) {
		uint8_t featureByte = feature;
		msg.addByte(featureByte);
		msg.addByte(0x01);
	}
	for (auto feature : disabledFeatures) {
		uint8_t featureByte = feature;
		msg.addByte(featureByte);
		msg.addByte(0x00);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseInventoryImbuements(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	bool isTrackerOpen = msg.getByte(); // Window is opened or closed
	g_game().playerRequestInventoryImbuements(player->getID(), isTrackerOpen);
}

void ProtocolGame::sendInventoryImbuements(const std::map<Slots_t, std::shared_ptr<Item>> &items) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x5D);

	msg.addByte(static_cast<uint8_t>(items.size()));
	for (const auto &[slot, item] : items) {
		msg.addByte(slot);
		AddItem(msg, item);

		uint8_t slots = item->getImbuementSlot();
		msg.addByte(slots);
		if (slots == 0) {
			continue;
		}

		for (uint8_t imbueSlot = 0; imbueSlot < slots; imbueSlot++) {
			ImbuementInfo imbuementInfo;
			if (!item->getImbuementInfo(imbueSlot, &imbuementInfo)) {
				msg.addByte(0x00);
				continue;
			}

			auto imbuement = imbuementInfo.imbuement;
			if (!imbuement) {
				msg.addByte(0x00);
				continue;
			}

			const BaseImbuement* baseImbuement = g_imbuements().getBaseByID(imbuement->getBaseID());
			msg.addByte(0x01);
			msg.addString(baseImbuement->name + " " + imbuement->getName());
			msg.add<uint16_t>(imbuement->getIconID());
			msg.add<uint32_t>(imbuementInfo.duration);

			std::shared_ptr<Tile> playerTile = player->getTile();
			// Check if the player is in a protection zone
			bool isInProtectionZone = playerTile && playerTile->hasFlag(TILESTATE_PROTECTIONZONE);
			// Check if the player is in fight mode
			bool isInFightMode = player->hasCondition(CONDITION_INFIGHT);
			// Get the category of the imbuement
			const CategoryImbuement* categoryImbuement = g_imbuements().getCategoryByID(imbuement->getCategory());
			// Parent of the imbued item
			auto parent = item->getParent();
			// If the imbuement is aggressive and the player is not in fight mode or is in a protection zone, or the item is in a container, ignore it.
			if (categoryImbuement && categoryImbuement->agressive && (isInProtectionZone || !isInFightMode)) {
				msg.addByte(0);
				continue;
			}
			// If the item is not in the backpack slot and it's not a agressive imbuement, ignore it.
			if (categoryImbuement && !categoryImbuement->agressive && parent && parent != player) {
				msg.addByte(0);
				continue;
			}

			msg.addByte(1);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendItemsPrice() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xCD);

	auto countBuffer = msg.getBufferPosition();
	uint16_t count = 0;
	msg.skipBytes(2);
	for (const auto &[itemId, tierAndPriceMap] : g_game().getItemsPrice()) {
		for (const auto &[tier, price] : tierAndPriceMap) {
			msg.add<uint16_t>(itemId);
			if (Item::items[itemId].upgradeClassification > 0) {
				msg.addByte(tier);
			}
			msg.add<uint64_t>(price);
			count++;
		}
	}
	msg.setBufferPosition(countBuffer);
	msg.add<uint16_t>(count);

	writeToOutputBuffer(msg);
}

void ProtocolGame::reloadCreature(const std::shared_ptr<Creature> &creature) {
	if (!creature || !canSee(creature)) {
		return;
	}

	auto tile = creature->getTile();
	if (!tile) {
		return;
	}

	uint32_t stackpos = tile->getClientIndexOfCreature(player, creature);

	if (stackpos >= 10) {
		return;
	}

	NetworkMessage msg;

	if (knownCreatureSet.contains(creature->getID())) {
		msg.addByte(0x6B);
		msg.addPosition(creature->getPosition());
		msg.addByte(static_cast<uint8_t>(stackpos));
		AddCreature(msg, creature, false, 0);
	} else {
		sendAddCreature(creature, creature->getPosition(), stackpos, false);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendOpenStash() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x29);
	StashItemList list = player->getStashItems();
	msg.add<uint16_t>(list.size());
	for (auto item : list) {
		msg.add<uint16_t>(item.first);
		msg.add<uint32_t>(item.second);
	}
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseStashWithdraw(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	if (!player->isAccessPlayer() && !player->isStashMenuAvailable()) {
		player->sendCancelMessage("You can't use stash right now.");
		return;
	}

	if (player->isUIExhausted(500)) {
		player->sendCancelMessage("You need to wait to do this again.");
		return;
	}

	auto action = static_cast<Stash_Actions_t>(msg.getByte());
	switch (action) {
		case STASH_ACTION_STOW_ITEM: {
			Position pos = msg.getPosition();
			auto itemId = msg.get<uint16_t>();
			uint8_t stackpos = msg.getByte();
			uint32_t count = msg.getByte();
			g_game().playerStowItem(player->getID(), pos, itemId, stackpos, count, false);
			break;
		}
		case STASH_ACTION_STOW_CONTAINER: {
			Position pos = msg.getPosition();
			auto itemId = msg.get<uint16_t>();
			uint8_t stackpos = msg.getByte();
			g_game().playerStowItem(player->getID(), pos, itemId, stackpos, 0, false);
			break;
		}
		case STASH_ACTION_STOW_STACK: {
			Position pos = msg.getPosition();
			auto itemId = msg.get<uint16_t>();
			uint8_t stackpos = msg.getByte();
			g_game().playerStowItem(player->getID(), pos, itemId, stackpos, 0, true);
			break;
		}
		case STASH_ACTION_WITHDRAW: {
			auto itemId = msg.get<uint16_t>();
			auto count = msg.get<uint32_t>();
			uint8_t stackpos = msg.getByte();
			g_game().playerStashWithdraw(player->getID(), itemId, count, stackpos);
			break;
		}
		default:
			g_logger().error("Unknown 'stash' action switch: {}", fmt::underlying(action));
			break;
	}

	player->updateUIExhausted();
}

void ProtocolGame::sendCreatureHelpers(uint32_t creatureId, uint16_t helpers) {
	if (!oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x94);
	msg.add<uint32_t>(creatureId);
	msg.add<uint16_t>(helpers);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendDepotItems(const ItemsTierCountList &itemMap, uint16_t count) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x94);

	msg.add<uint16_t>(count);
	for (const auto &[key, count] : itemMap) {
		const auto &[itemId, tier] = key;

		msg.add<uint16_t>(itemId);

		if (tier > 0) {
			msg.addByte(tier - 1);
		}

		msg.add<uint16_t>(static_cast<uint16_t>(count));
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendCloseDepotSearch() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x9A);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendDepotSearchResultDetail(uint16_t itemId, uint8_t tier, uint32_t depotCount, const ItemVector &depotItems, uint32_t inboxCount, const ItemVector &inboxItems, uint32_t stashCount) {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x99);
	msg.add<uint16_t>(itemId);
	if (Item::items[itemId].upgradeClassification > 0) {
		msg.addByte(tier);
	}

	msg.add<uint32_t>(depotCount);
	msg.addByte(static_cast<uint8_t>(depotItems.size()));
	for (const auto &item : depotItems) {
		AddItem(msg, item);
	}

	msg.add<uint32_t>(inboxCount);
	msg.addByte(static_cast<uint8_t>(inboxItems.size()));
	for (const auto &item : inboxItems) {
		AddItem(msg, item);
	}

	msg.addByte(stashCount > 0 ? 0x01 : 0x00);
	if (stashCount > 0) {
		msg.add<uint16_t>(itemId);
		msg.add<uint32_t>(stashCount);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::parseOpenDepotSearch() {
	if (oldProtocol) {
		return;
	}

	g_game().playerRequestDepotItems(player->getID());
}

void ProtocolGame::parseCloseDepotSearch() {
	if (oldProtocol) {
		return;
	}

	g_game().playerRequestCloseDepotSearch(player->getID());
}

void ProtocolGame::parseDepotSearchItemRequest(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	auto itemId = msg.get<uint16_t>();
	uint8_t itemTier = 0;
	if (Item::items[itemId].upgradeClassification > 0) {
		itemTier = msg.getByte();
	}

	g_game().playerRequestDepotSearchItem(player->getID(), itemId, itemTier);
}

void ProtocolGame::parseRetrieveDepotSearch(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	auto itemId = msg.get<uint16_t>();
	uint8_t itemTier = 0;
	if (Item::items[itemId].upgradeClassification > 0) {
		itemTier = msg.getByte();
	}
	uint8_t type = msg.getByte();

	g_game().playerRequestDepotSearchRetrieve(player->getID(), itemId, itemTier, type);
}

void ProtocolGame::parseOpenParentContainer(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	Position pos = msg.getPosition();
	g_game().playerRequestOpenContainerFromDepotSearch(player->getID(), pos);
}

void ProtocolGame::sendUpdateCreature(const std::shared_ptr<Creature> &creature) {
	if (oldProtocol || !creature || !player) {
		return;
	}

	auto tile = creature->getTile();
	if (!tile) {
		return;
	}

	if (!canSee(creature)) {
		return;
	}

	int32_t stackPos = tile->getClientIndexOfCreature(player, creature);
	if (stackPos == -1 || stackPos >= 10) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x6B);
	msg.addPosition(creature->getPosition());
	msg.addByte(static_cast<uint8_t>(stackPos));
	AddCreature(msg, creature, false, 0);
	writeToOutputBuffer(msg);
}

void ProtocolGame::getForgeInfoMap(const std::shared_ptr<Item> &item, std::map<uint16_t, std::map<uint8_t, uint16_t>> &itemsMap) const {
	std::map<uint8_t, uint16_t> itemInfo;
	[[maybe_unused]] auto result = itemInfo.insert({ item->getTier(), item->getItemCount() });
	auto [first, inserted] = itemsMap.try_emplace(item->getID(), itemInfo);
	if (!inserted) {
		auto [otherFirst, otherInserted] = itemsMap[item->getID()].try_emplace(item->getTier(), item->getItemCount());
		if (!otherInserted) {
			(itemsMap[item->getID()])[item->getTier()] += item->getItemCount();
		}
	}
}

void ProtocolGame::sendForgeSkillStats(NetworkMessage &msg) const {
	if (oldProtocol) {
		return;
	}

	std::vector<Slots_t> slots { CONST_SLOT_LEFT, CONST_SLOT_ARMOR, CONST_SLOT_HEAD, CONST_SLOT_LEGS };
	for (const auto &slot : slots) {
		double_t skill = 0;
		const auto &item = player->getInventoryItem(slot);
		if (!item) {
			continue;
		}

		const ItemType &it = Item::items[item->getID()];
		if (it.isWeapon()) {
			skill = item->getFatalChance() * 100;
		}
		if (it.isArmor()) {
			skill = item->getDodgeChance() * 100;
		}
		if (it.isHelmet()) {
			skill = item->getMomentumChance() * 100;
		}
		if (it.isLegs()) {
			skill = item->getTranscendenceChance() * 100;
		}
		if (it.isBoots()) {
			skill = item->getAmplificationChance();
		}

		auto skillCast = static_cast<uint16_t>(skill);
		msg.add<uint16_t>(skillCast);
		msg.add<uint16_t>(skillCast);
	}
}

double ProtocolGame::getForgeSkillStat(Slots_t slot, bool applyAmplification /*= true*/) const {
	if (oldProtocol) {
		return 0;
	}

	double skill = 0;
	if (const auto &item = player->getInventoryItem(slot); item) {
		const ItemType &it = Item::items[item->getID()];
		if (it.isWeapon()) {
			skill = item->getFatalChance();
		}
		if (it.isArmor()) {
			skill = item->getDodgeChance();
		}
		if (it.isHelmet()) {
			skill = item->getMomentumChance();
		}
		if (it.isLegs()) {
			skill = item->getTranscendenceChance();
		}
		if (it.isBoots()) {
			skill = item->getAmplificationChance();
		}
	}

	if (applyAmplification) {
		const auto &boots = player->getInventoryItem(CONST_SLOT_FEET);
		if (slot != CONST_SLOT_FEET && boots) {
			skill *= 1 + (boots->getAmplificationChance() / 100);
		}
	}

	return skill / 100;
}

void ProtocolGame::sendBosstiaryData() {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x61);

	msg.add<uint16_t>(25); // Number of kills to achieve 'Bane Prowess'
	msg.add<uint16_t>(100); // Number of kills to achieve 'Bane expertise'
	msg.add<uint16_t>(300); // Number of kills to achieve 'Base Mastery'

	msg.add<uint16_t>(5); // Number of kills to achieve 'Archfoe Prowess'
	msg.add<uint16_t>(20); // Number of kills to achieve 'Archfoe Expertise'
	msg.add<uint16_t>(60); // Number of kills to achieve 'Archfoe Mastery'

	msg.add<uint16_t>(1); // Number of kills to achieve 'Nemesis Prowess'
	msg.add<uint16_t>(3); // Number of kills to achieve 'Nemesis Expertise'
	msg.add<uint16_t>(5); // Number of kills to achieve 'Nemesis Mastery'

	msg.add<uint16_t>(5); // Points will receive when reach 'Bane Prowess'
	msg.add<uint16_t>(15); // Points will receive when reach 'Bane Expertise'
	msg.add<uint16_t>(30); // Points will receive when reach 'Base Mastery'

	msg.add<uint16_t>(10); // Points will receive when reach 'Archfoe Prowess'
	msg.add<uint16_t>(30); // Points will receive when reach 'Archfoe Expertise'
	msg.add<uint16_t>(60); // Points will receive when reach 'Archfoe Mastery'

	msg.add<uint16_t>(10); // Points will receive when reach 'Nemesis Prowess'
	msg.add<uint16_t>(30); // Points will receive when reach 'Nemesis Expertise'
	msg.add<uint16_t>(60); // Points will receive when reach 'Nemesis Mastery'

	writeToOutputBuffer(msg);
}

void ProtocolGame::parseSendBosstiary() {
	if (oldProtocol) {
		return;
	}

	sendBosstiaryData();

	NetworkMessage msg;
	msg.addByte(0x73);

	auto mtype_map = g_ioBosstiary().getBosstiaryMap();
	auto bossesBuffer = msg.getBufferPosition();
	uint16_t bossesCount = 0;
	msg.skipBytes(2);

	for (const auto &[bossid, name] : mtype_map) {
		const auto mType = g_monsters().getMonsterType(name);
		if (!mType) {
			continue;
		}

		auto bossRace = mType->info.bosstiaryRace;
		if (bossRace < BosstiaryRarity_t::RARITY_BANE || bossRace > BosstiaryRarity_t::RARITY_NEMESIS) {
			g_logger().error("[{}] monster {} have wrong boss race {}", __FUNCTION__, mType->name, fmt::underlying(bossRace));
			continue;
		}

		auto killCount = player->getBestiaryKillCount(bossid);
		msg.add<uint32_t>(bossid);
		msg.addByte(static_cast<uint8_t>(bossRace));
		msg.add<uint32_t>(killCount);
		msg.addByte(0);
		msg.addByte(player->isBossOnBosstiaryTracker(mType) ? 0x01 : 0x00);
		++bossesCount;
	}

	msg.setBufferPosition(bossesBuffer);
	msg.add<uint16_t>(bossesCount);

	writeToOutputBuffer(msg);
}

void ProtocolGame::parseSendBosstiarySlots() {
	if (oldProtocol) {
		return;
	}

	uint32_t bossIdSlotOne = player->getSlotBossId(1);
	uint32_t bossIdSlotTwo = player->getSlotBossId(2);
	uint32_t boostedBossId = g_ioBosstiary().getBoostedBossId();

	// Sanity checks
	const std::string &boostedBossName = g_ioBosstiary().getBoostedBossName();
	if (boostedBossName.empty()) {
		g_logger().error("[{}] The boosted boss name is empty", __FUNCTION__);
		return;
	}
	const auto mTypeBoosted = g_monsters().getMonsterType(boostedBossName);
	auto boostedBossRace = mTypeBoosted ? mTypeBoosted->info.bosstiaryRace : BosstiaryRarity_t::BOSS_INVALID;
	auto isValidBoostedBoss = boostedBossId == 0 || (boostedBossRace >= BosstiaryRarity_t::RARITY_BANE && boostedBossRace <= BosstiaryRarity_t::RARITY_NEMESIS);
	if (!isValidBoostedBoss) {
		g_logger().error("[{}] The boosted boss '{}' has an invalid race", __FUNCTION__, boostedBossName);
		return;
	}

	const auto mTypeSlotOne = g_ioBosstiary().getMonsterTypeByBossRaceId((uint16_t)bossIdSlotOne);
	auto bossRaceSlotOne = mTypeSlotOne ? mTypeSlotOne->info.bosstiaryRace : BosstiaryRarity_t::BOSS_INVALID;
	auto isValidBossSlotOne = bossIdSlotOne == 0 || (bossRaceSlotOne >= BosstiaryRarity_t::RARITY_BANE && bossRaceSlotOne <= BosstiaryRarity_t::RARITY_NEMESIS);
	if (!isValidBossSlotOne) {
		g_logger().error("[{}] boss slot1 with race id '{}' has an invalid race", __FUNCTION__, bossIdSlotOne);
		return;
	}

	const auto mTypeSlotTwo = g_ioBosstiary().getMonsterTypeByBossRaceId((uint16_t)bossIdSlotTwo);
	auto bossRaceSlotTwo = mTypeSlotTwo ? mTypeSlotTwo->info.bosstiaryRace : BosstiaryRarity_t::BOSS_INVALID;
	auto isValidBossSlotTwo = bossIdSlotTwo == 0 || (bossRaceSlotTwo >= BosstiaryRarity_t::RARITY_BANE && bossRaceSlotTwo <= BosstiaryRarity_t::RARITY_NEMESIS);
	if (!isValidBossSlotTwo) {
		g_logger().error("[{}] boss slot1 with race id '{}' has an invalid race", __FUNCTION__, bossIdSlotTwo);
		return;
	}

	sendBosstiaryData();

	NetworkMessage msg;
	msg.addByte(0x62);

	uint32_t playerBossPoints = player->getBossPoints();
	uint16_t currentBonus = g_ioBosstiary().calculateLootBonus(playerBossPoints);
	uint32_t pointsNextBonus = g_ioBosstiary().calculateBossPoints(currentBonus + 1);
	msg.add<uint32_t>(playerBossPoints); // Player Points
	msg.add<uint32_t>(pointsNextBonus); // Total Points next bonus
	msg.add<uint16_t>(currentBonus); // Current Bonus
	msg.add<uint16_t>(currentBonus + 1); // Next Bonus

	uint32_t removePrice = g_ioBosstiary().calculteRemoveBoss(player->getRemoveTimes());

	auto bossesUnlockedList = g_ioBosstiary().getBosstiaryFinished(player);
	if (auto it = std::ranges::find(bossesUnlockedList.begin(), bossesUnlockedList.end(), boostedBossId);
	    it != bossesUnlockedList.end()) {
		[[maybe_unused]] auto it_erase = bossesUnlockedList.erase(it);
	}
	auto bossesUnlockedSize = static_cast<uint16_t>(bossesUnlockedList.size());

	bool isSlotOneUnlocked = (bossesUnlockedSize > 0 ? true : false);
	msg.addByte(isSlotOneUnlocked ? 1 : 0);
	msg.add<uint32_t>(isSlotOneUnlocked ? bossIdSlotOne : 0);
	if (isSlotOneUnlocked && bossIdSlotOne != 0) {
		// Variables Boss Slot One
		auto bossKillCount = player->getBestiaryKillCount(static_cast<uint16_t>(bossIdSlotOne));
		auto slotOneBossLevel = g_ioBosstiary().getBossCurrentLevel(player, (uint16_t)bossIdSlotOne);
		uint16_t bonusBossSlotOne = currentBonus + (slotOneBossLevel == 3 ? 25 : 0);
		uint8_t isSlotOneInactive = bossIdSlotOne == boostedBossId ? 1 : 0;
		// Bytes Slot One
		sendBosstiarySlotsBytes(msg, static_cast<uint8_t>(bossRaceSlotOne), bossKillCount, bonusBossSlotOne, 0, isSlotOneInactive, removePrice);
		bossesUnlockedSize--;
	}

	uint32_t slotTwoPoints = 1500;
	bool isSlotTwoUnlocked = (playerBossPoints >= slotTwoPoints ? true : false);
	msg.addByte(isSlotTwoUnlocked ? 1 : 0);
	msg.add<uint32_t>(isSlotTwoUnlocked ? bossIdSlotTwo : slotTwoPoints);
	if (isSlotTwoUnlocked && bossIdSlotTwo != 0) {
		// Variables Boss Slot Two
		auto bossKillCount = player->getBestiaryKillCount((uint16_t)(bossIdSlotTwo));
		auto slotTwoBossLevel = g_ioBosstiary().getBossCurrentLevel(player, (uint16_t)bossIdSlotTwo);
		uint16_t bonusBossSlotTwo = currentBonus + (slotTwoBossLevel == 3 ? 25 : 0);
		uint8_t isSlotTwoInactive = bossIdSlotTwo == boostedBossId ? 1 : 0;
		// Bytes Slot Two
		sendBosstiarySlotsBytes(msg, static_cast<uint8_t>(bossRaceSlotTwo), bossKillCount, bonusBossSlotTwo, 0, isSlotTwoInactive, removePrice);
		bossesUnlockedSize--;
	}

	bool isTodaySlotUnlocked = g_configManager().getBoolean(BOOSTED_BOSS_SLOT);
	msg.addByte(isTodaySlotUnlocked ? 1 : 0);
	msg.add<uint32_t>(boostedBossId);
	if (isTodaySlotUnlocked && boostedBossId != 0) {
		auto boostedBossKillCount = player->getBestiaryKillCount(static_cast<uint16_t>(boostedBossId));
		auto boostedLootBonus = static_cast<uint16_t>(g_configManager().getNumber(BOOSTED_BOSS_LOOT_BONUS));
		auto bosstiaryMultiplier = static_cast<uint8_t>(g_configManager().getNumber(BOSSTIARY_KILL_MULTIPLIER));
		auto boostedKillBonus = static_cast<uint8_t>(g_configManager().getNumber(BOOSTED_BOSS_KILL_BONUS));
		sendBosstiarySlotsBytes(msg, static_cast<uint8_t>(boostedBossRace), boostedBossKillCount, boostedLootBonus, bosstiaryMultiplier + boostedKillBonus, 0, 0);
	}

	msg.addByte(bossesUnlockedSize != 0 ? 1 : 0);
	if (bossesUnlockedSize != 0) {
		auto unlockCountBuffer = msg.getBufferPosition();
		uint16_t bossesCount = 0;
		msg.skipBytes(2);
		for (const auto &bossId : bossesUnlockedList) {
			if (bossId == bossIdSlotOne || bossId == bossIdSlotTwo) {
				continue;
			}

			const auto mType = g_ioBosstiary().getMonsterTypeByBossRaceId(bossId);
			if (!mType) {
				g_logger().error("[{}] monster {} not found", __FUNCTION__, bossId);
				continue;
			}

			auto bossRace = mType->info.bosstiaryRace;
			if (bossRace < BosstiaryRarity_t::RARITY_BANE || bossRace > BosstiaryRarity_t::RARITY_NEMESIS) {
				g_logger().error("[{}] monster {} have wrong boss race {}", __FUNCTION__, mType->name, fmt::underlying(bossRace));
				continue;
			}

			msg.add<uint32_t>(bossId);
			msg.addByte(static_cast<uint8_t>(bossRace));
			bossesCount++;
		}
		msg.setBufferPosition(unlockCountBuffer);
		msg.add<uint16_t>(bossesCount);
	}

	writeToOutputBuffer(msg);
	parseSendResourceBalance();
}

void ProtocolGame::parseBosstiarySlot(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t slotBossId = msg.getByte();
	auto selectedBossId = msg.get<uint32_t>();

	g_game().playerBosstiarySlot(player->getID(), slotBossId, selectedBossId);
}

void ProtocolGame::sendPodiumDetails(NetworkMessage &msg, const std::vector<uint16_t> &toSendMonsters, bool isBoss) const {
	auto toSendMonstersSize = static_cast<uint16_t>(toSendMonsters.size());
	msg.add<uint16_t>(toSendMonstersSize);
	for (const auto &raceId : toSendMonsters) {
		const auto mType = g_monsters().getMonsterTypeByRaceId(raceId, isBoss);
		if (!mType) {
			continue;
		}

		// Podium of tenacity only need raceId
		if (!isBoss) {
			msg.add<uint16_t>(raceId);
			continue;
		}

		auto monsterOutfit = mType->info.outfit;
		msg.add<uint16_t>(raceId);
		auto isLookType = monsterOutfit.lookType != 0;
		// "Tantugly's Head" boss have to send other looktype to the podium
		if (monsterOutfit.lookTypeEx == 35105) {
			monsterOutfit.lookTypeEx = 39003;
			msg.addString("Tentugly");
		} else {
			msg.addString(mType->name);
		}
		msg.add<uint16_t>(monsterOutfit.lookType);
		if (isLookType) {
			msg.addByte(monsterOutfit.lookHead);
			msg.addByte(monsterOutfit.lookBody);
			msg.addByte(monsterOutfit.lookLegs);
			msg.addByte(monsterOutfit.lookFeet);
			msg.addByte(monsterOutfit.lookAddons);
		} else {
			msg.add<uint16_t>(monsterOutfit.lookTypeEx);
		}
	}
}

void ProtocolGame::sendMonsterPodiumWindow(const std::shared_ptr<Item> &podium, const Position &position, uint16_t itemId, uint8_t stackPos) {
	if (!podium || oldProtocol) {
		g_logger().error("[{}] item is nullptr", __FUNCTION__);
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xC2);

	auto podiumVisible = podium->getCustomAttribute("PodiumVisible");
	auto lookType = podium->getCustomAttribute("LookType");
	auto lookDirection = podium->getCustomAttribute("LookDirection");

	bool isBossSelected = false;
	uint16_t lookValue = 0;
	if (lookType) {
		lookValue = static_cast<uint16_t>(lookType->getInteger());
		isBossSelected = lookValue > 0;
	}

	msg.add<uint16_t>(isBossSelected ? lookValue : 0); // Boss LookType
	if (isBossSelected) {
		auto lookHead = podium->getCustomAttribute("LookHead");
		auto lookBody = podium->getCustomAttribute("LookBody");
		auto lookLegs = podium->getCustomAttribute("LookLegs");
		auto lookFeet = podium->getCustomAttribute("LookFeet");

		msg.addByte(lookHead ? static_cast<uint8_t>(lookHead->getInteger()) : 0);
		msg.addByte(lookBody ? static_cast<uint8_t>(lookBody->getInteger()) : 0);
		msg.addByte(lookLegs ? static_cast<uint8_t>(lookLegs->getInteger()) : 0);
		msg.addByte(lookFeet ? static_cast<uint8_t>(lookFeet->getInteger()) : 0);

		auto lookAddons = podium->getCustomAttribute("LookAddons");
		msg.addByte(lookAddons ? static_cast<uint8_t>(lookAddons->getInteger()) : 0);
	} else {
		msg.add<uint16_t>(0); // Boss LookType
	}
	msg.add<uint16_t>(0); // Size of an unknown list. (No ingame visual effect)

	bool isBossPodium = podium->getID() == ITEM_PODIUM_OF_VIGOUR;
	msg.addByte(isBossPodium ? 0x01 : 0x00); // Bosstiary or bestiary
	if (isBossPodium) {
		const auto &unlockedBosses = g_ioBosstiary().getBosstiaryFinished(player, 2);
		sendPodiumDetails(msg, unlockedBosses, true);
	} else {
		const auto &unlockedMonsters = g_iobestiary().getBestiaryFinished(player);
		sendPodiumDetails(msg, unlockedMonsters, false);
	}

	msg.addPosition(position); // Position of the podium on the map
	msg.add<uint16_t>(itemId); // ClientID of the podium
	msg.addByte(stackPos); // StackPos of the podium on the map

	msg.addByte(podiumVisible ? static_cast<uint8_t>(podiumVisible->getInteger()) : 0x01); // A boolean saying if it's visible or not
	msg.addByte(lookType ? 0x01 : 0x00); // A boolean saying if there's a boss selected
	msg.addByte(lookDirection ? static_cast<uint8_t>(lookDirection->getInteger()) : 2); // Direction where the boss is looking
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseSetMonsterPodium(NetworkMessage &msg) const {
	if (!player || oldProtocol) {
		return;
	}

	// For some reason the cip sends uint32_t, but we use uint16_t, so let's just ignore that
	auto monsterRaceId = static_cast<uint16_t>(msg.get<uint32_t>());
	Position pos = msg.getPosition();
	auto itemId = msg.get<uint16_t>();
	uint8_t stackpos = msg.getByte();
	uint8_t direction = msg.getByte();
	uint8_t podiumVisible = msg.getByte();
	uint8_t monsterVisible = msg.getByte();

	g_game().playerSetMonsterPodium(player->getID(), monsterRaceId, pos, stackpos, itemId, direction, std::make_pair(podiumVisible, monsterVisible));
}

void ProtocolGame::sendBosstiaryCooldownTimer() {
	if (!player || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xBD);

	auto startBosses = msg.getBufferPosition();
	msg.skipBytes(2); // Boss count
	uint16_t bossesCount = 0;
	for (std::map<uint16_t, std::string> bossesMap = g_ioBosstiary().getBosstiaryMap();
	     const auto &[bossRaceId, _] : bossesMap) {
		const auto mType = g_ioBosstiary().getMonsterTypeByBossRaceId(bossRaceId);
		if (!mType) {
			continue;
		}

		auto timerValue = player->kv()->scoped("boss.cooldown")->get(toKey(std::to_string(bossRaceId)));
		if (!timerValue || !timerValue.has_value()) {
			continue;
		}

		auto scheduleTimerOpt = g_kv().scoped("eventscheduler")->get("boss-cooldown");
		uint8_t schedulePercentage = 0;
		if (scheduleTimerOpt) {
			schedulePercentage = static_cast<uint8_t>(scheduleTimerOpt->getNumber());
		}
		if (schedulePercentage == 0) {
			schedulePercentage = 100;
		}

		auto timer = timerValue->getNumber();
		timer = static_cast<uint32_t>(timer * schedulePercentage / 100);
		uint64_t sendTimer = timer > 0 ? static_cast<uint64_t>(timer) : 0;
		msg.add<uint32_t>(bossRaceId); // bossRaceId
		msg.add<uint64_t>(sendTimer); // Boss cooldown in seconds
		bossesCount++;
	}
	auto endBosses = msg.getBufferPosition();
	msg.setBufferPosition(startBosses);
	msg.add<uint16_t>(bossesCount);
	msg.setBufferPosition(endBosses);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendBosstiaryEntryChanged(uint32_t bossid) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xE6);
	msg.add<uint32_t>(bossid);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendSingleSoundEffect(const Position &pos, SoundEffect_t id, SourceEffect_t source) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x83);
	msg.addPosition(pos);
	msg.addByte(0x06); // Sound effect type
	msg.addByte(static_cast<uint8_t>(source)); // Sound source type
	msg.add<uint16_t>(static_cast<uint16_t>(id)); // Sound id
	msg.addByte(0x00); // Breaking the effects loop
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendDoubleSoundEffect(
	const Position &pos,
	SoundEffect_t mainSoundId,
	SourceEffect_t mainSource,
	SoundEffect_t secondarySoundId,
	SourceEffect_t secondarySource
) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x83);
	msg.addPosition(pos);

	// Primary sound
	msg.addByte(0x06); // Sound effect type
	msg.addByte(static_cast<uint8_t>(mainSource)); // Sound source type
	msg.add<uint16_t>(static_cast<uint16_t>(mainSoundId)); // Sound id

	// Secondary sound (Can be an array too, but not necessary here)
	msg.addByte(0x07); // Multiple effect type
	msg.addByte(0x01); // Useless ENUM (So far)
	msg.addByte(static_cast<uint8_t>(secondarySource)); // Sound source type
	msg.add<uint16_t>(static_cast<uint16_t>(secondarySoundId)); // Sound id

	msg.addByte(0x00); // Breaking the effects loop
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAmbientSoundEffect(const SoundAmbientEffect_t id) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x85);
	msg.addByte(0x00);
	msg.add<uint16_t>(id);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMusicSoundEffect(const SoundMusicEffect_t id) {
	if (oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x85);
	msg.addByte(0x01);
	msg.add<uint16_t>(id);
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseOpenWheel(NetworkMessage &msg) {
	if (oldProtocol || !g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) {
		return;
	}

	auto ownerId = msg.get<uint32_t>();
	g_game().playerOpenWheel(player->getID(), ownerId);
}

void ProtocolGame::parseWheelGemAction(NetworkMessage &msg) {
	if (oldProtocol || !g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) {
		return;
	}

	if (hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		if (player->isUIExhausted()) {
			player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED);
			return;
		}

		const auto action = static_cast<WheelGemAction_t>(msg.getByte());
		switch (action) {
			case WheelGemAction_t::Destroy:
				player->wheel().destroyGem(msg.get<uint16_t>());
				break;
			case WheelGemAction_t::Reveal:
				player->wheel().revealGem(static_cast<WheelGemQuality_t>(msg.getByte(true)));
				break;
			case WheelGemAction_t::SwitchDomain:
				player->wheel().switchGemDomain(msg.get<uint16_t>());
				break;
			case WheelGemAction_t::ToggleLock:
				player->wheel().toggleGemLock(msg.get<uint16_t>());
				break;
			case WheelGemAction_t::ImproveGrade: {
				const auto fragmentType = static_cast<WheelFragmentType_t>(msg.getByte(true) != 0 ? 1 : 0);
				const auto position = msg.getByte(true);
				player->wheel().improveGemGrade(fragmentType, position);
				break;
			}
			default:
				g_logger().error("[{}] player {} is trying to do invalid action {} on wheel", __FUNCTION__, player->getName(), fmt::underlying(action));
				break;
		}

		player->updateUIExhausted();
		return;
	}

	g_game().playerWheelGemAction(player->getID(), msg);
}

void ProtocolGame::sendOpenWheelWindow(uint32_t ownerId) {
	if (!player || oldProtocol || !g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) {
		return;
	}

	NetworkMessage msg;
	player->wheel().sendOpenWheelWindow(msg, ownerId);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendGemAtelierGemRevealed(uint16_t gemIndex) {
	if (!player || oldProtocol || !hasProtocolFeature(protocolProfile, ProtocolFeature::CurrentPayload)) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0xC5);
	msg.add<uint16_t>(gemIndex);
	writeToOutputBuffer(msg);
}

void ProtocolGame::parseSaveWheel(NetworkMessage &msg) {
	if (oldProtocol || !g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) {
		return;
	}

	g_game().playerSaveWheel(player->getID(), msg);
}

void ProtocolGame::sendDisableLoginMusic() {
	if (oldProtocol || !player || player->getOperatingSystem() >= CLIENTOS_OTCLIENT_LINUX) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x85);
	msg.addByte(0x01);
	msg.add<uint16_t>(0x00);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendTakeScreenshot(Screenshot_t screenshotType, uint8_t skillId, uint16_t skillLevel, const std::string &achievementName, uint16_t raceId, uint8_t bestiaryStep) {
	if (screenshotType == SCREENSHOT_TYPE_NONE || oldProtocol) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x75);
	if (hasProtocolFeature(protocolProfile, ProtocolFeature::GameEventPayload)) {
		// 15.13+ repurposes 0x75 from a single screenshot-type byte into
		// a GameEvent payload with an event selector and event-specific fields.
		switch (screenshotType) {
			case SCREENSHOT_TYPE_ACHIEVEMENT:
				if (achievementName.empty()) {
					return;
				}
				msg.addByte(0x02);
				msg.addString(achievementName);
				break;
			case SCREENSHOT_TYPE_BESTIARYENTRYCOMPLETED:
			case SCREENSHOT_TYPE_BESTIARYENTRYUNLOCKED:
				if (raceId == 0) {
					return;
				}
				msg.addByte(0x06);
				msg.add<uint16_t>(raceId);
				msg.addByte(bestiaryStep);
				break;
			case SCREENSHOT_TYPE_LEVELUP:
				msg.addByte(0x04);
				msg.add<uint16_t>(std::min<uint32_t>(player ? player->getLevel() : 0, 0xFFFF));
				break;
			case SCREENSHOT_TYPE_SKILLUP:
				if (skillId == 0 || skillLevel == 0) {
					return;
				}
				msg.addByte(0x05);
				msg.addByte(skillId);
				msg.add<uint16_t>(skillLevel);
				break;
			case SCREENSHOT_TYPE_BOSSDEFEATED:
				msg.addByte(0x01);
				msg.addByte(0x01);
				break;
			case SCREENSHOT_TYPE_DEATHPVE:
				msg.addByte(0x01);
				msg.addByte(0x02);
				break;
			case SCREENSHOT_TYPE_DEATHPVP:
				msg.addByte(0x01);
				msg.addByte(0x03);
				break;
			case SCREENSHOT_TYPE_PLAYERKILLASSIST:
				msg.addByte(0x01);
				msg.addByte(0x04);
				break;
			case SCREENSHOT_TYPE_PLAYERKILL:
				msg.addByte(0x01);
				msg.addByte(0x05);
				break;
			case SCREENSHOT_TYPE_PLAYERATTACKING:
				msg.addByte(0x01);
				msg.addByte(0x06);
				break;
			case SCREENSHOT_TYPE_TREASUREFOUND:
				msg.addByte(0x01);
				msg.addByte(0x07);
				break;
			case SCREENSHOT_TYPE_GIFTOFLIFE:
				msg.addByte(0x01);
				msg.addByte(0x08);
				break;
			default:
				return;
		}
	} else {
		msg.addByte(screenshotType);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendAttachedEffect(const std::shared_ptr<Creature> &creature, uint16_t effectId) {
	if (!isOTCR) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x34);
	msg.add<uint32_t>(creature->getID());
	msg.add<uint16_t>(effectId);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendDetachEffect(const std::shared_ptr<Creature> &creature, uint16_t effectId) {
	if (!isOTCR) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x35);
	msg.add<uint32_t>(creature->getID());
	msg.add<uint16_t>(effectId);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendShader(const std::shared_ptr<Creature> &creature, const std::string &shaderName) {
	if (!isOTCR) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x36);
	msg.add<uint32_t>(creature->getID());
	msg.addString(shaderName);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMapShader(const std::string &shaderName) {
	if (!isOTCR) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x37);
	msg.addString(shaderName);
	writeToOutputBuffer(msg);
}

void ProtocolGame::sendPlayerTyping(const std::shared_ptr<Creature> &creature, uint8_t typing) {
	if (!isOTCR) {
		return;
	}

	NetworkMessage msg;
	msg.addByte(0x38);
	msg.add<uint32_t>(creature->getID());
	msg.addByte(typing);
	writeToOutputBuffer(msg);
}

void ProtocolGame::AddOutfitCustomOTCR(NetworkMessage &msg, const Outfit_t &outfit) {
	if (!isOTCR) {
		return;
	}

	msg.add<uint16_t>(outfit.lookWing);
	msg.add<uint16_t>(outfit.lookAura);
	msg.add<uint16_t>(outfit.lookEffect);
	const auto &shader = g_game().getAttachedEffects()->getShaderByID(outfit.lookShader);
	msg.addString(shader ? shader->name : "");
}

void ProtocolGame::sendOutfitWindowCustomOTCR(NetworkMessage &msg) {
	if (!isOTCR) {
		return;
	}
	// wings
	auto startWings = msg.getBufferPosition();
	uint16_t limitWings = std::numeric_limits<uint16_t>::max();
	uint16_t wingSize = 0;
	msg.skipBytes(1);
	const auto &wings = g_game().getAttachedEffects()->getWings();
	for (const auto &wing : wings) {
		if (player->attachedEffects().hasWing(wing)) {
			msg.add<uint16_t>(wing->id);
			msg.addString(wing->name);
			++wingSize;
		}
		if (wingSize == limitWings) {
			break;
		}
	}
	auto endWings = msg.getBufferPosition();
	msg.setBufferPosition(startWings);
	msg.add<uint8_t>(wingSize);
	msg.setBufferPosition(endWings);
	// auras
	auto startAuras = msg.getBufferPosition();
	uint16_t limitAuras = std::numeric_limits<uint16_t>::max();
	uint16_t auraSize = 0;
	msg.skipBytes(1);
	const auto &auras = g_game().getAttachedEffects()->getAuras();
	for (const auto &aura : auras) {
		if (player->attachedEffects().hasAura(aura)) {
			msg.add<uint16_t>(aura->id);
			msg.addString(aura->name);
			++auraSize;
		}
		if (auraSize == limitAuras) {
			break;
		}
	}
	auto endAuras = msg.getBufferPosition();
	msg.setBufferPosition(startAuras);
	msg.add<uint8_t>(auraSize);
	msg.setBufferPosition(endAuras);
	// effects
	auto startEffects = msg.getBufferPosition();
	uint16_t limitEffects = std::numeric_limits<uint16_t>::max();
	uint16_t effectSize = 0;
	msg.skipBytes(1);
	const auto &effects = g_game().getAttachedEffects()->getEffects();
	for (const auto &effect : effects) {
		if (player->attachedEffects().hasEffect(effect)) {
			msg.add<uint16_t>(effect->id);
			msg.addString(effect->name);
			++effectSize;
		}
		if (effectSize == limitEffects) {
			break;
		}
	}
	auto endEffects = msg.getBufferPosition();
	msg.setBufferPosition(startEffects);
	msg.add<uint8_t>(effectSize);
	msg.setBufferPosition(endEffects);
	// shader
	std::vector<const Shader*> shaders;
	for (const auto &shader : g_game().getAttachedEffects()->getShaders()) {
		if (player->attachedEffects().hasShader(shader.get())) {
			shaders.push_back(shader.get());
		}
	}
	msg.addByte(static_cast<uint8_t>(shaders.size()));
	for (const Shader* shader : shaders) {
		msg.add<uint16_t>(shader->id);
		msg.addString(shader->name);
	}
}

void ProtocolGame::parseCyclopediaHouseAuction(NetworkMessage &msg) {
	if (oldProtocol) {
		return;
	}

	uint8_t houseActionType = msg.getByte();
	switch (houseActionType) {
		case 0: {
			const auto townName = msg.getString();
			g_game().playerCyclopediaHousesByTown(player->getID(), townName);
			break;
		}
		case 1: {
			const uint32_t houseId = msg.get<uint32_t>();
			const uint64_t bidValue = msg.get<uint64_t>();
			g_game().playerCyclopediaHouseBid(player->getID(), houseId, bidValue);
			break;
		}
		case 2: {
			const uint32_t houseId = msg.get<uint32_t>();
			const uint32_t timestamp = msg.get<uint32_t>();
			g_game().playerCyclopediaHouseMoveOut(player->getID(), houseId, timestamp);
			break;
		}
		case 3: {
			const uint32_t houseId = msg.get<uint32_t>();
			const uint32_t timestamp = msg.get<uint32_t>();
			const std::string &newOwner = msg.getString();
			const uint64_t bidValue = msg.get<uint64_t>();
			g_game().playerCyclopediaHouseTransfer(player->getID(), houseId, timestamp, newOwner, bidValue);
			break;
		}
		case 4: {
			const uint32_t houseId = msg.get<uint32_t>();
			g_game().playerCyclopediaHouseCancelMoveOut(player->getID(), houseId);
			break;
		}
		case 5: {
			const uint32_t houseId = msg.get<uint32_t>();
			g_game().playerCyclopediaHouseCancelTransfer(player->getID(), houseId);
			break;
		}
		case 6: {
			const uint32_t houseId = msg.get<uint32_t>();
			g_game().playerCyclopediaHouseAcceptTransfer(player->getID(), houseId);
			break;
		}
		case 7: {
			const uint32_t houseId = msg.get<uint32_t>();
			g_game().playerCyclopediaHouseRejectTransfer(player->getID(), houseId);
			break;
		}
	}
}

void ProtocolGame::sendCyclopediaHouseList(HouseMap houses) {
	NetworkMessage msg;
	msg.addByte(0xC7);
	msg.add<uint16_t>(houses.size());
	for (const auto &[clientId, houseData] : houses) {
		msg.add<uint32_t>(clientId);
		msg.addByte(0x01); // 0x00 = Renovation; 0x01 = Available

		auto houseState = houseData->getState();
		auto stateValue = magic_enum::enum_integer(houseState);
		msg.addByte(stateValue);
		if (houseState == CyclopediaHouseState::Available) {
			bool bidder = houseData->getBidderName() == player->getName();
			msg.addString(houseData->getBidderName());
			msg.addByte(bidder);
			uint8_t disableIndex = enumToValue(player->canBidHouse(clientId));
			msg.addByte(disableIndex);

			if (!houseData->getBidderName().empty()) {
				msg.add<uint32_t>(houseData->getBidEndDate());
				msg.add<uint64_t>(houseData->getHighestBid());
				if (bidder) {
					msg.add<uint64_t>(houseData->getBidHolderLimit());
				}
			}
		} else if (houseState == CyclopediaHouseState::Rented) {
			auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner());
			msg.addString(ownerName);
			msg.add<uint32_t>(houseData->getPaidUntil());

			bool rented = ownerName.compare(player->getName()) == 0;
			msg.addByte(rented);
			if (rented) {
				msg.addByte(0);
				msg.addByte(0);
			}
		} else if (houseState == CyclopediaHouseState::Transfer) {
			auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner());
			msg.addString(ownerName);
			msg.add<uint32_t>(houseData->getPaidUntil());

			bool isOwner = ownerName.compare(player->getName()) == 0;
			msg.addByte(isOwner);
			if (isOwner) {
				msg.addByte(0); // ?
				msg.addByte(0); // ?
			}
			msg.add<uint32_t>(houseData->getBidEndDate());
			msg.addString(houseData->getBidderName());
			msg.addByte(0); // ?
			msg.add<uint64_t>(houseData->getInternalBid());

			bool isNewOwner = player->getName() == houseData->getBidderName();
			msg.addByte(isNewOwner);
			if (isNewOwner) {
				uint8_t disableIndex = enumToValue(player->canAcceptTransferHouse(clientId));
				msg.addByte(disableIndex); // Accept Transfer Error
				msg.addByte(0); // Reject Transfer Error
			}

			if (isOwner) {
				msg.addByte(0); // Cancel Transfer Error
			}
		} else if (houseState == CyclopediaHouseState::MoveOut) {
			auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner());
			msg.addString(ownerName);
			msg.add<uint32_t>(houseData->getPaidUntil());

			bool isOwner = ownerName.compare(player->getName()) == 0;
			msg.addByte(isOwner);
			if (isOwner) {
				msg.addByte(0); // ?
				msg.addByte(0); // ?
				msg.add<uint32_t>(houseData->getBidEndDate());
				msg.addByte(0);
			} else {
				msg.add<uint32_t>(houseData->getBidEndDate());
			}
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) {
	NetworkMessage msg;
	const auto typeValue = enumToValue(type);

	msg.addByte(0xC3);
	msg.add<uint32_t>(houseId);
	msg.addByte(typeValue);
	if (bidSuccess && typeValue == 1) {
		msg.addByte(0x00);
	}
	msg.addByte(index);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendHousesInfo() {
	NetworkMessage msg;

	uint32_t houseClientId = 0;
	const auto accountHouseCount = g_game().map.houses.getHouseCountByAccount(player->getAccountId());
	const auto house = g_game().map.houses.getHouseByPlayerId(player->getGUID());
	if (house) {
		houseClientId = house->getClientId();
	}

	msg.addByte(0xC6);
	msg.add<uint32_t>(houseClientId);
	msg.addByte(0x00);

	msg.addByte(accountHouseCount); // Houses Account

	msg.addByte(0x00);

	msg.addByte(3);
	msg.addByte(3);

	msg.addByte(0x01);

	msg.addByte(0x01);
	msg.add<uint32_t>(houseClientId);

	const auto &housesList = g_game().map.houses.getHouses();
	msg.add<uint16_t>(housesList.size());
	for (const auto &it : housesList) {
		msg.add<uint32_t>(it.second->getClientId());
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendMonkData(MonkData_t type, uint8_t value) {
	if (!hasProtocolFeature(protocolProfile, ProtocolFeature::CustomMonkPackets) && !hasProtocolFeature(protocolProfile, ProtocolFeature::OfficialVocationSpecificPlayerData)) {
		return;
	}

	NetworkMessage msg;

	msg.addByte(0xC1); // VocationSpecificPlayerData
	msg.addByte(enumToValue(type));

	if (hasProtocolFeature(protocolProfile, ProtocolFeature::OfficialVocationSpecificPlayerData)) {
		switch (type) {
			case MonkData_t::Harmony:
				msg.addByte(value);
				break;
			case MonkData_t::Serenity:
				msg.addByte(value != 0 ? 0x01 : 0x00);
				break;
			case MonkData_t::Virtue: {
				uint16_t spellId = 0;
				switch (static_cast<Virtue_t>(value)) {
					case Virtue_t::Harmony:
						spellId = 274;
						break;
					case Virtue_t::Justice:
						spellId = 275;
						break;
					case Virtue_t::Sustain:
						spellId = 276;
						break;
					case Virtue_t::None:
						break;
				}

				const uint8_t virtueCount = spellId != 0 ? 1 : 0;
				msg.addByte(virtueCount);
				if (virtueCount != 0) {
					msg.add<uint16_t>(spellId);
				}
				break;
			}
		}
	} else {
		msg.addByte(value);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::parseAimAtTarget(NetworkMessage &msg) {
	if (!player) {
		return; // Safety check: ensure player exists
	}

	uint8_t amount = msg.getByte(); // Number of spells being updated
	for (uint8_t i = 0; i < amount; ++i) {
		uint16_t spellId = msg.get<uint16_t>(); // ID of the spell
		uint8_t state = msg.getByte(); // State: 1 = enabled, 0 = disabled
		player->updateAimAtTargetSpells(spellId, state); // Update player's config
	}
}

void ProtocolGame::parseExivaRestrictions(NetworkMessage &msg) {
	if (!player || g_configManager().getString(WORLD_TYPE) != "no-pvp") {
		return;
	}

	auto &restrictions = player->getExivaRestrictions();

	restrictions.allowAll = msg.getByte();
	restrictions.allowOwnGuild = msg.getByte();
	restrictions.allowOwnParty = msg.getByte();
	restrictions.allowVipList = msg.getByte();
	restrictions.allowPlayerWhitelist = msg.getByte();
	restrictions.allowGuildWhitelist = msg.getByte();

	const int32_t MAX_EXIVA_WHITELIST = g_configManager().getNumber(ConfigKey_t::MAX_EXIVA_WHITELIST);

	std::vector<std::string> addedPlayerNames;
	std::unordered_set<uint32_t> addedPlayerGuids;
	addExivaEntries(msg, restrictions.playerWhitelist, addedPlayerNames, addedPlayerGuids, MAX_EXIVA_WHITELIST, [](const std::string &n) {
		return IOLoginData::getGuidByName(n);
	});

	std::vector<std::string> removedPlayerNames;
	std::unordered_set<uint32_t> removedPlayerGuids;
	removeExivaEntries(msg, restrictions.playerWhitelist, removedPlayerNames, removedPlayerGuids, MAX_EXIVA_WHITELIST, [](const std::string &n) {
		return IOLoginData::getGuidByName(n);
	});

	std::vector<std::string> addedGuildNames;
	std::unordered_set<uint32_t> addedGuildIds;
	addExivaEntries(msg, restrictions.guildWhitelist, addedGuildNames, addedGuildIds, MAX_EXIVA_WHITELIST, [](const std::string &n) {
		return IOGuild::getGuildIdByName(n);
	});

	std::vector<std::string> removedGuildNames;
	std::unordered_set<uint32_t> removedGuildIds;
	removeExivaEntries(msg, restrictions.guildWhitelist, removedGuildNames, removedGuildIds, MAX_EXIVA_WHITELIST, [](const std::string &n) {
		return IOGuild::getGuildIdByName(n);
	});

	sendExivaRestrictions(false, addedPlayerNames, removedPlayerNames, addedGuildNames, removedGuildNames);
}

void ProtocolGame::sendExivaRestrictions(
	bool isLogin /* = false */,
	const std::vector<std::string> &addedPlayerNames /* = {} */,
	const std::vector<std::string> &removedPlayerNames /* = {} */,
	const std::vector<std::string> &addedGuildNames /* = {} */,
	const std::vector<std::string> &removedGuildNames /* = {} */
) {
	const auto &restrictions = player->getExivaRestrictions();

	NetworkMessage msg;

	msg.addByte(0xCA);
	msg.addByte(restrictions.allowAll);
	msg.addByte(restrictions.allowOwnGuild);
	msg.addByte(restrictions.allowOwnParty);
	msg.addByte(restrictions.allowVipList);
	msg.addByte(restrictions.allowPlayerWhitelist);
	msg.addByte(restrictions.allowGuildWhitelist);

	if (isLogin) {
		msg.add<uint16_t>(restrictions.playerWhitelist.size());
		for (const auto &guid : restrictions.playerWhitelist) {
			msg.addString(IOLoginData::getNameByGuid(guid));
		}
		msg.add<uint16_t>(0x00);
	} else {
		msg.add<uint16_t>(addedPlayerNames.size());
		for (const auto &addedName : addedPlayerNames) {
			msg.addString(addedName);
		}
		msg.add<uint16_t>(removedPlayerNames.size());
		for (const auto &removedName : removedPlayerNames) {
			msg.addString(removedName);
		}
	}

	if (isLogin) {
		msg.add<uint16_t>(restrictions.guildWhitelist.size());
		for (const auto &guildId : restrictions.guildWhitelist) {
			auto guild = g_game().getGuild(guildId, true);
			msg.addString(guild ? guild->getName() : "");
		}
		msg.add<uint16_t>(0x00);
	} else {
		msg.add<uint16_t>(addedGuildNames.size());
		for (const auto &addedName : addedGuildNames) {
			msg.addString(addedName);
		}
		msg.add<uint16_t>(removedGuildNames.size());
		for (const auto &removedName : removedGuildNames) {
			msg.addString(removedName);
		}
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendWeaponProficiency(uint16_t weaponId) {
	if (!player || oldProtocol) {
		return;
	}

	if (weaponId == 0) {
		weaponId = player->getWeaponId(true);
	}

	NetworkMessage msg;

	msg.addByte(0x5C);

	msg.add<uint16_t>(weaponId);
	msg.add<uint32_t>(weaponId > 0 ? player->weaponProficiency().getExperience(weaponId) : 0);
	msg.addByte(weaponId > 0 && player->weaponProficiency().isUpgradeAvailable(weaponId) ? 0x01 : 0x00);

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendWeaponProficiencyWindow(uint16_t weaponId) {
	if (!player || oldProtocol) {
		return;
	}

	if (weaponId == 0) {
		weaponId = player->getWeaponId(true);
	}

	NetworkMessage msg;
	msg.addByte(0xC4);

	msg.add<uint16_t>(weaponId);
	msg.add<uint32_t>(player->weaponProficiency().getExperience(weaponId));

	const auto &selectedPerks = player->weaponProficiency().getSelectedPerks(weaponId);

	const auto perkCount = static_cast<uint8_t>(std::min<size_t>(selectedPerks.size(), std::numeric_limits<uint8_t>::max()));
	msg.addByte(perkCount);
	for (size_t i = 0; i < perkCount; ++i) {
		const auto &perk = selectedPerks[i];
		msg.addByte(perk.level);
		msg.addByte(perk.index);
	}

	if (shouldSendWeaponProficiencyDetailList(protocolProfile, clientVersionString)) {
		msg.addByte(0x00);
	}

	writeToOutputBuffer(msg);
}

void ProtocolGame::sendClientLoginPreamble(OperatingSystem_t operatingSystem) {
	if (otclientV8 > 0) {
		sendFeatures();
	}

	if (operatingSystem >= CLIENTOS_OTCLIENT_LINUX) {
		isOTC = true;
		if (otclientV8 == 0) {
			sendOTCRFeatures();
		}

		NetworkMessage opcodeMessage;
		opcodeMessage.addByte(0x32);
		opcodeMessage.addByte(0x00);
		opcodeMessage.add<uint16_t>(0x00);
		writeToOutputBuffer(opcodeMessage);
	}
}

void ProtocolGame::castViewerLogin(const std::string &name, const std::string &password, OperatingSystem_t operatingSystem) {
	sendClientLoginPreamble(operatingSystem);

	const auto &foundPlayer = g_game().getPlayerByName(name);
	if (!canWatchLivestream(foundPlayer, password)) {
		return;
	}

	m_isLivestreamViewer = true;
	player = foundPlayer;
	acceptPackets = true;
	sendLivestreamViewerAppear(foundPlayer);
	OutputMessagePool::getInstance().addProtocolToAutosend(shared_from_this());
}

bool ProtocolGame::canWatchLivestream(const std::shared_ptr<Player> &foundPlayer, const std::string &password) {
	std::string reason;
	if (!g_livestream().canWatch(foundPlayer, getThis(), password, reason)) {
		disconnectClient(reason);
		return false;
	}

	return true;
}

void ProtocolGame::sendLivestreamViewerAppear(const std::shared_ptr<Player> &foundPlayer) {
	if (!foundPlayer) {
		return;
	}

	player = foundPlayer;
	const auto ownerClient = foundPlayer->client;
	foundPlayer->client = getThis();
	sendAddCreature(player, player->getPosition(), 0, true);
	foundPlayer->client = ownerClient;

	syncLivestreamViewerOpenContainers(player);
	g_livestream().addViewer(player, getThis());

	const auto viewerCount = g_livestream().getViewerCount(player);
	sendTextMessage(TextMessage(MESSAGE_LOOK, fmt::format("{} is broadcasting for {} people.\nLivestream time: {}", player->getName(), viewerCount, g_livestream().getBroadcastTimeString(player))));

	const auto description = g_livestream().getDescription(player);
	if (!description.empty()) {
		sendCreatureSay(player, TALKTYPE_SAY, description);
	}

	if (!oldProtocol) {
		const std::unordered_set<PlayerIcon> iconSet { PlayerIcon::Rooted };
		sendIcons(iconSet, IconBakragore::None);
	}

	sendChannel(CHANNEL_LIVESTREAM, "Livestream", nullptr, nullptr);
	sendTextMessage(TextMessage(MESSAGE_EVENT_ADVANCE, "Available commands: \n/name newname\n/show"));
}

void ProtocolGame::syncLivestreamViewerOpenContainers(const std::shared_ptr<Player> &foundPlayer) {
	if (!foundPlayer) {
		return;
	}

	for (const auto &[cid, openContainer] : foundPlayer->getOpenContainers()) {
		if (openContainer.container) {
			sendContainer(cid, openContainer.container, openContainer.container->hasParent(), openContainer.index);
		}
	}
}

void ProtocolGame::resendLivestreamViewerContainer(NetworkMessage &msg) {
	if (!player) {
		return;
	}

	const uint8_t cid = msg.getByte();
	const auto &openContainers = player->getOpenContainers();
	const auto it = openContainers.find(cid);
	if (it == openContainers.end() || !it->second.container) {
		sendCloseContainer(cid);
		return;
	}

	sendContainer(cid, it->second.container, it->second.container->hasParent(), it->second.index);
}
