From 5481249d0f6025251e811f2e35daa79ee7c258aa Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Sun, 27 Aug 2017 22:37:39 +0100 Subject: Implement basic form of trading * Fixes #3450 * Addresses #1109 --- Server/Plugins/APIDump/APIDesc.lua | 2 +- src/ClientHandle.cpp | 16 +- src/ClientHandle.h | 2 + src/Entities/Entity.cpp | 2 +- src/Mobs/Monster.cpp | 6 +- src/Mobs/Villager.cpp | 318 +++++++++++++++++++++++++++++--- src/Mobs/Villager.h | 133 ++++++++++--- src/Protocol/Protocol.h | 2 + src/Protocol/ProtocolRecognizer.cpp | 10 + src/Protocol/ProtocolRecognizer.h | 1 + src/Protocol/Protocol_1_10.cpp | 2 +- src/Protocol/Protocol_1_11.cpp | 2 +- src/Protocol/Protocol_1_12.cpp | 2 +- src/Protocol/Protocol_1_8.cpp | 11 +- src/Protocol/Protocol_1_8.h | 1 + src/Protocol/Protocol_1_9.cpp | 36 +++- src/Protocol/Protocol_1_9.h | 1 + src/UI/CMakeLists.txt | 1 + src/UI/SlotArea.cpp | 223 +++++++++++++++++++++- src/UI/SlotArea.h | 50 ++++- src/UI/VillagerTradeWindow.h | 74 ++++++++ src/UI/Window.cpp | 2 +- src/UI/Window.h | 2 +- src/WorldStorage/NBTChunkSerializer.cpp | 5 +- src/WorldStorage/WSSAnvil.cpp | 8 +- 25 files changed, 838 insertions(+), 74 deletions(-) create mode 100644 src/UI/VillagerTradeWindow.h diff --git a/Server/Plugins/APIDump/APIDesc.lua b/Server/Plugins/APIDump/APIDesc.lua index 9a0b7a36e..c0569c5b1 100644 --- a/Server/Plugins/APIDump/APIDesc.lua +++ b/Server/Plugins/APIDump/APIDesc.lua @@ -12859,7 +12859,7 @@ end { Notes = "An inventory window", }, - wtNPCTrade = + wtVillagerTrade = { Notes = "A villager trade window", }, diff --git a/src/ClientHandle.cpp b/src/ClientHandle.cpp index 6923c9855..13c22de52 100644 --- a/src/ClientHandle.cpp +++ b/src/ClientHandle.cpp @@ -17,6 +17,7 @@ #include "UI/AnvilWindow.h" #include "UI/BeaconWindow.h" #include "UI/EnchantingWindow.h" +#include "UI/VillagerTradeWindow.h" #include "Item.h" #include "Mobs/Monster.h" #include "ChatColor.h" @@ -670,8 +671,14 @@ void cClientHandle::RemoveFromAllChunks() void cClientHandle::HandleNPCTrade(int a_SlotNum) { - // TODO - LOGWARNING("%s: Not implemented yet", __FUNCTION__); + auto CurrentWindow = GetPlayer()->GetWindow(); + if ((CurrentWindow == nullptr) || CurrentWindow->GetWindowType() != cWindow::WindowType::wtVillagerTrade) + { + Kick("Received trade selection when no trade in progress - hacked client?"); + return; + } + + static_cast(CurrentWindow)->PlayerChangedTradeOffer(*GetPlayer(), static_cast(a_SlotNum)); } @@ -3111,6 +3118,11 @@ void cClientHandle::SendUseBed(const cEntity & a_Entity, int a_BlockX, int a_Blo m_Protocol->SendUseBed(a_Entity, a_BlockX, a_BlockY, a_BlockZ); } +void cClientHandle::SendVillagerTradeList(const cWindow & TradeWindow, const std::vector & TradeOffers) +{ + m_Protocol->SendVillagerTradeList(TradeWindow, TradeOffers); +} + diff --git a/src/ClientHandle.h b/src/ClientHandle.h index 4a4898179..f790aa7f6 100644 --- a/src/ClientHandle.h +++ b/src/ClientHandle.h @@ -36,6 +36,7 @@ class cFallingBlock; class cCompositeChat; class cStatManager; class cMap; +struct VillagerTradeOffer; class cClientHandle; typedef std::shared_ptr cClientHandlePtr; @@ -221,6 +222,7 @@ public: // tolua_export void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity); void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4); void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ); + void SendVillagerTradeList (const cWindow &, const std::vector &); void SendWeather (eWeather a_Weather); void SendWholeInventory (const cWindow & a_Window); void SendWindowClose (const cWindow & a_Window); diff --git a/src/Entities/Entity.cpp b/src/Entities/Entity.cpp index 836e69961..13d8a8733 100644 --- a/src/Entities/Entity.cpp +++ b/src/Entities/Entity.cpp @@ -666,7 +666,7 @@ bool cEntity::ArmorCoversAgainst(eDamageType a_DamageType) int cEntity::GetEnchantmentCoverAgainst(const cEntity * a_Attacker, eDamageType a_DamageType, int a_Damage) { - int TotalEPF = 0.0; + int TotalEPF = 0; const cItem ArmorItems[] = { GetEquippedHelmet(), GetEquippedChestplate(), GetEquippedLeggings(), GetEquippedBoots() }; for (size_t i = 0; i < ARRAYCOUNT(ArmorItems); i++) diff --git a/src/Mobs/Monster.cpp b/src/Mobs/Monster.cpp index 0d433d861..86060583c 100644 --- a/src/Mobs/Monster.cpp +++ b/src/Mobs/Monster.cpp @@ -1135,14 +1135,14 @@ std::unique_ptr cMonster::NewMonsterFromType(eMonsterType a_MobType) } case mtVillager: { - int VillagerType = Random.RandInt(6); - if (VillagerType == 6) + int VillagerType = Random.RandInt(13); + if (VillagerType == 13) { // Give farmers a better chance of spawning VillagerType = 0; } - return cpp14::make_unique(static_cast(VillagerType)); + return cpp14::make_unique(static_cast(VillagerType), 1U); } case mtHorse: { diff --git a/src/Mobs/Villager.cpp b/src/Mobs/Villager.cpp index 26462ba31..36b3d75f9 100644 --- a/src/Mobs/Villager.cpp +++ b/src/Mobs/Villager.cpp @@ -2,21 +2,53 @@ #include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules #include "Villager.h" +#include "../UI/VillagerTradeWindow.h" +#include "../Entities/Player.h" #include "../World.h" #include "../BlockArea.h" #include "../Blocks/BlockHandler.h" #include "../BlockInServerPluginInterface.h" +#include "../ClientHandle.h" -cVillager::cVillager(eVillagerType VillagerType) : +cVillager::cVillager(VillagerCareer Career, unsigned TradeTier) : super("Villager", mtVillager, "entity.villager.hurt", "entity.villager.death", 0.6, 1.8), + cEntityWindowOwner(this), m_ActionCountDown(-1), - m_Type(VillagerType), + m_Career(Career), + m_TradeTier(TradeTier), m_VillagerAction(false) { + UpdateTradeTier(m_TradeTier); +} + + + + + +cVillager::VillagerProfession cVillager::VillagerCareerToProfession(VillagerCareer Career) +{ + switch (Career) + { + case VillagerCareer::Farmer: + case VillagerCareer::Fisherman: + case VillagerCareer::Shepherd: + case VillagerCareer::Fletcher: return cVillager::VillagerProfession::Farmer; + case VillagerCareer::Librarian: + case VillagerCareer::Cartographer: return cVillager::VillagerProfession::Librarian; + case VillagerCareer::Cleric: return cVillager::VillagerProfession::Priest; + case VillagerCareer::Armorer: + case VillagerCareer::WeaponSmith: + case VillagerCareer::ToolSmith: return cVillager::VillagerProfession::Blacksmith; + case VillagerCareer::Butcher: + case VillagerCareer::Leatherworker: return cVillager::VillagerProfession::Butcher; + case VillagerCareer::Nitwit: return cVillager::VillagerProfession::Nitwit; + } + + throw std::runtime_error("Unhandled career"); } @@ -32,10 +64,7 @@ bool cVillager::DoTakeDamage(TakeDamageInfo & a_TDI) if ((a_TDI.Attacker != nullptr) && a_TDI.Attacker->IsPlayer()) { - if (GetRandomProvider().RandBool(1.0 / 6.0)) - { - m_World->BroadcastEntityStatus(*this, esVillagerAngry); - } + m_World->BroadcastEntityStatus(*this, esVillagerAngry); } if (a_TDI.DamageType == dtLightning) @@ -63,27 +92,18 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) if (m_ActionCountDown > -1) { m_ActionCountDown--; - if (m_ActionCountDown == 0) + if ((m_ActionCountDown == 0) && (m_Career == VillagerCareer::Farmer)) { - switch (m_Type) - { - case vtFarmer: - { - HandleFarmerPlaceCrops(); - } - } + HandleFarmerPlaceCrops(); } return; } if (m_VillagerAction) { - switch (m_Type) + if (m_Career == VillagerCareer::Farmer) { - case vtFarmer: - { - HandleFarmerTryHarvestCrops(); - } + HandleFarmerTryHarvestCrops(); } m_VillagerAction = false; return; @@ -95,11 +115,169 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) return; } - switch (m_Type) + if (m_Career == VillagerCareer::Farmer) + { + HandleFarmerPrepareFarmCrops(); + } +} + + + + + +void cVillager::OnRightClicked(cPlayer & InteractingPlayer) +{ + if (m_TradeOffers.empty()) + { + // Client handles this ok, but the contract of GetTradeOffer says it must return something + return; + } + + // If the window is not created, open it anew: + if (GetWindow() == nullptr) + { + OpenWindow(new cVillagerTradeWindow(*this)); + } + + InteractingPlayer.OpenWindow(*GetWindow()); + InteractingPlayer.GetClientHandle()->SendVillagerTradeList(*GetWindow(), m_TradeOffers); + GetWorld()->BroadcastSoundEffect("entity.villager.trading", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +VillagerTradeOffer & cVillager::GetTradeOffer(unsigned PlayerOfferIndex, const cItem & PrimarySlot, const cItem & SecondarySlot) +{ + if ((PlayerOfferIndex != 0) || GetTransactionMultiplier(m_TradeOffers[PlayerOfferIndex], PrimarySlot, SecondarySlot) != 0) + { + return m_TradeOffers[PlayerOfferIndex]; + } + + for (auto & Offer : m_TradeOffers) + { + if (GetTransactionMultiplier(Offer, PrimarySlot, SecondarySlot) != 0) + { + return Offer; + } + } + + return m_TradeOffers[PlayerOfferIndex]; +} + + + + + +unsigned cVillager::GetTransactionMultiplier(const VillagerTradeOffer & Trade, const cItem & PrimarySlot, const cItem & SecondarySlot) +{ + if (Trade.IsTradeExhausted()) + { + return 0; + } + + if ( + PrimarySlot.IsEqual(Trade.PrimaryDesire) && + (PrimarySlot.m_ItemCount >= Trade.PrimaryDesire.m_ItemCount) && + SecondarySlot.IsEqual(Trade.SecondaryDesire) && + (SecondarySlot.m_ItemCount >= Trade.SecondaryDesire.m_ItemCount) + ) + { + // Slots matched normally: find common denominator of transaction multiplier + return std::min( + { + static_cast(PrimarySlot.m_ItemCount / Trade.PrimaryDesire.m_ItemCount), + Trade.SecondaryDesire.IsEmpty() ? std::numeric_limits::max() : static_cast(SecondarySlot.m_ItemCount / Trade.SecondaryDesire.m_ItemCount), + static_cast(Trade.Recompense.GetMaxStackSize() / Trade.Recompense.m_ItemCount) + } + ); + } + else if ( + Trade.SecondaryDesire.IsEmpty() && + PrimarySlot.IsEmpty() && + SecondarySlot.IsEqual(Trade.PrimaryDesire) && + (SecondarySlot.m_ItemCount >= Trade.PrimaryDesire.m_ItemCount) + ) + { + // Slots crossed - primary desire in secondary slot: swap comparison accordingly + return std::min( + static_cast(SecondarySlot.m_ItemCount / Trade.PrimaryDesire.m_ItemCount), + static_cast(Trade.Recompense.GetMaxStackSize() / Trade.Recompense.m_ItemCount) + ); + } + + return 0; +} + + + + + +void cVillager::HandleTradeAvailable() const +{ + GetWorld()->BroadcastSoundEffect("entity.villager.yes", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +void cVillager::HandleTradeInProgress() const +{ + GetWorld()->BroadcastSoundEffect("entity.villager.trading", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +void cVillager::HandleTradeUnavailable() const +{ + GetWorld()->BroadcastSoundEffect("entity.villager.no", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +void cVillager::HandleTransaction(VillagerTradeOffer & Trade, unsigned TransactionMultiplier) +{ + GetWorld()->BroadcastSoundEffect("entity.villager.yes", GetPosX(), GetPosY(), GetPosZ(), 1, 1); + Trade.Transactions += TransactionMultiplier; + + for (unsigned Try = 0; Try != TransactionMultiplier; Try++) { - case vtFarmer: + // One singular transaction has a 10% chance of upgrading the villager's trading tier + + if (GetRandomProvider().RandBool(0.1)) { - HandleFarmerPrepareFarmCrops(); + UpdateTradeTier(m_TradeTier + 1); + + class SendNewTradesCallback : public cItemCallback + { + public: + SendNewTradesCallback(const cWindow & VillagerTradeWindow, const std::vector & VillagerTradeOffers) : + TradeWindow(VillagerTradeWindow), + TradeOffers(VillagerTradeOffers) + { + } + + bool Item(cClientHandle * Handle) + { + Handle->SendVillagerTradeList(TradeWindow, TradeOffers); + return false; + } + + private: + const cWindow & TradeWindow; + const std::vector & TradeOffers; + + } SNTC(*GetWindow(), m_TradeOffers); + GetWindow()->ForEachClient(SNTC); + + m_World->BroadcastEntityStatus(*this, esVillagerHappy); + break; } } } @@ -107,6 +285,7 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) + //////////////////////////////////////////////////////////////////////////////// // Farmer functions: @@ -195,6 +374,101 @@ void cVillager::HandleFarmerPlaceCrops() +void cVillager::UpdateTradeTier(size_t DesiredTier) +{ + struct CareerHash + { + size_t operator()(const VillagerCareer & Career) const + { + return static_cast(Career); + } + }; + using Offer = VillagerTradeOffer; + + static const std::unordered_map< + VillagerCareer, + std::vector< + // Element index: trading tier (0-based) + std::vector // Trade offer + >, + CareerHash // TODO for C++14: unneeded + > TradeMatrix = + { + { + VillagerCareer::Farmer, + { + { // Tier 1 (equivalent) + Offer(cItem(E_ITEM_WHEAT, 18), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_POTATO, 15), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_CARROT, 15), cItem(E_ITEM_EMERALD)) + }, + { // Tier 2 + Offer(cItem(E_BLOCK_PUMPKIN, 8), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_PUMPKIN_PIE, 2)) + }, + { // Tier 3 + Offer(cItem(E_BLOCK_MELON, 7), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_RED_APPLE, 5)) + }, + { // Tier 4 + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_COOKIE, 6)), + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_CAKE)) + } + } + }, + { + VillagerCareer::Fisherman, {} + }, + { + VillagerCareer::Shepherd, {} + }, + { + VillagerCareer::Fletcher, {} + }, + { + VillagerCareer::Librarian, {} + }, + { + VillagerCareer::Cartographer, {} + }, + { + VillagerCareer::Cleric, {} + }, + { + VillagerCareer::Armorer, {} + }, + { + VillagerCareer::WeaponSmith, {} + }, + { + VillagerCareer::ToolSmith, {} + }, + { + VillagerCareer::Butcher, {} + }, + { + VillagerCareer::Leatherworker, {} + }, + { + VillagerCareer::Nitwit, {} + } + }; + + auto Tiers = TradeMatrix.find(m_Career)->second; + + m_TradeTier = std::min(Tiers.size(), DesiredTier); + m_TradeOffers.clear(); + + for (unsigned Tier = 0; Tier < m_TradeTier; Tier++) + { + m_TradeOffers.insert(m_TradeOffers.end(), Tiers[Tier].cbegin(), Tiers[Tier].cend()); + } +} + + + + + bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType) { switch (a_BlockType) diff --git a/src/Mobs/Villager.h b/src/Mobs/Villager.h index 6f3e7b4e8..0d8672c46 100644 --- a/src/Mobs/Villager.h +++ b/src/Mobs/Villager.h @@ -3,37 +3,123 @@ #include "PassiveMonster.h" #include "Blocks/ChunkInterface.h" +#include "../UI/WindowOwner.h" + + + + + +struct VillagerTradeOffer +{ + VillagerTradeOffer(const cItem & PrimaryTradeDesire, const cItem & TradeRecompense) : + PrimaryDesire(PrimaryTradeDesire), + Recompense(TradeRecompense), + Transactions(0), + MaximumTransactions(10) + { + } + + VillagerTradeOffer(const cItem & PrimaryTradeDesire, const cItem & SecondaryTradeDesire, const cItem & TradeRecompense) : + PrimaryDesire(PrimaryTradeDesire), + SecondaryDesire(SecondaryTradeDesire), + Recompense(TradeRecompense), + Transactions(0), + MaximumTransactions(10) + { + } + + cItem PrimaryDesire; + cItem SecondaryDesire; + cItem Recompense; + + bool IsTradeExhausted() const + { + return Transactions == MaximumTransactions; + } + + unsigned Transactions; + unsigned MaximumTransactions; +}; + class cVillager : - public cPassiveMonster + public cPassiveMonster, + public cEntityWindowOwner { typedef cPassiveMonster super; public: - enum eVillagerType + enum class VillagerProfession + { + Farmer = 0, + Librarian = 1, + Priest = 2, + Blacksmith = 3, + Butcher = 4, + Nitwit = 5 + }; + + enum class VillagerCareer { - vtFarmer = 0, - vtLibrarian = 1, - vtPriest = 2, - vtBlacksmith = 3, - vtButcher = 4, - vtGeneric = 5, - vtMax - } ; + Farmer, + Fisherman, + Shepherd, + Fletcher, + Librarian, + Cartographer, + Cleric, + Armorer, + WeaponSmith, + ToolSmith, + Butcher, + Leatherworker, + Nitwit + }; + + cVillager(VillagerCareer, unsigned); + CLASS_PROTODEF(cVillager) - cVillager(eVillagerType VillagerType); + /** Converts internal career to equivalent Vanilla profession. + Returns an enumeration with Vanilla-compatible values. */ + static VillagerProfession VillagerCareerToProfession(VillagerCareer); - CLASS_PROTODEF(cVillager) + /** Returns the career of the villager. */ + VillagerCareer GetCareer() const { return m_Career; } + + /** Returns a trading offer of the villager subject to criteria. + If the player has selected the first trade offer, the first available trade offer matching the input is returned, or the first trade offer if nothing was found. + For any other offer selection, the selected offer is returned. */ + VillagerTradeOffer & GetTradeOffer(unsigned, const cItem &, const cItem &); + + /** Returns number of transactions of a single trade can be completed, given input and output items. */ + static unsigned GetTransactionMultiplier(const VillagerTradeOffer &, const cItem &, const cItem &); + + /** Handles events where players interact with an available trade. + This includes adding a desired item to fulfil or exceed the offer, or otherwise interacting with a trade when it can be performed. */ + void HandleTradeAvailable() const; + + /** Handles events where players interact with the trading window. + This event does not include interactions with the currently presented trade itself. */ + void HandleTradeInProgress() const; + + /** Handles events where players interact with an unavailable trade. + This includes adding a desired item to fulfil or exceed the offer, or otherwise interacting with a trade when it can be performed. */ + void HandleTradeUnavailable() const; + + /** Convenience function for setting correct item counts and upgrading trade tiers if necessary. */ + void HandleTransaction(VillagerTradeOffer &, unsigned TransactionMultiplier); // cEntity overrides virtual bool DoTakeDamage(TakeDamageInfo & a_TDI) override; - virtual void Tick (std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; + virtual void Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; + virtual void OnRightClicked(cPlayer &) override; + +private: - // cVillager functions /** return true if the given blocktype are: crops, potatoes or carrots. */ bool IsBlockFarmable(BLOCKTYPE a_BlockType); @@ -48,19 +134,18 @@ public: void HandleFarmerPlaceCrops(); // Get and set functions. - int GetVilType(void) const { return m_Type; } - Vector3i GetCropsPos(void) const { return m_CropsPos; } - bool DoesHaveActionActivated(void) const { return m_VillagerAction; } + Vector3i GetCropsPos(void) const { return m_CropsPos; } + bool DoesHaveActionActivated(void) const { return m_VillagerAction; } -private: + /** Levels-up the villager to the given tier, if not already at maximum for their career. + Levelling-up unlocks additional trades available at that level, and re-enables all previously exhausted trade offers. */ + void UpdateTradeTier(size_t); int m_ActionCountDown; - int m_Type; + VillagerCareer m_Career; + size_t m_TradeTier; + std::vector m_TradeOffers; bool m_VillagerAction; Vector3i m_CropsPos; -} ; - - - - +}; diff --git a/src/Protocol/Protocol.h b/src/Protocol/Protocol.h index 54c5b7223..ad6e00c04 100644 --- a/src/Protocol/Protocol.h +++ b/src/Protocol/Protocol.h @@ -33,6 +33,7 @@ class cFallingBlock; class cCompositeChat; class cStatManager; class cPacketizer; +struct VillagerTradeOffer; @@ -140,6 +141,7 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) = 0; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) = 0; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) = 0; + virtual void SendVillagerTradeList (const cWindow &, const std::vector &) = 0; virtual void SendWeather (eWeather a_Weather) = 0; virtual void SendWholeInventory (const cWindow & a_Window) = 0; virtual void SendWindowClose (const cWindow & a_Window) = 0; diff --git a/src/Protocol/ProtocolRecognizer.cpp b/src/Protocol/ProtocolRecognizer.cpp index cb2a4fc9d..e28b8ae5c 100644 --- a/src/Protocol/ProtocolRecognizer.cpp +++ b/src/Protocol/ProtocolRecognizer.cpp @@ -957,6 +957,16 @@ void cProtocolRecognizer::SendUseBed(const cEntity & a_Entity, int a_BlockX, int +void cProtocolRecognizer::SendVillagerTradeList(const cWindow & TradeWindow, const std::vector & TradeOffers) +{ + ASSERT(m_Protocol != nullptr); + m_Protocol->SendVillagerTradeList(TradeWindow, TradeOffers); +} + + + + + void cProtocolRecognizer::SendWeather(eWeather a_Weather) { ASSERT(m_Protocol != nullptr); diff --git a/src/Protocol/ProtocolRecognizer.h b/src/Protocol/ProtocolRecognizer.h index 295c6db16..17347d4a9 100644 --- a/src/Protocol/ProtocolRecognizer.h +++ b/src/Protocol/ProtocolRecognizer.h @@ -136,6 +136,7 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) override; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) override; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) override; + virtual void SendVillagerTradeList (const cWindow &, const std::vector &) override; virtual void SendWeather (eWeather a_Weather) override; virtual void SendWholeInventory (const cWindow & a_Window) override; virtual void SendWindowClose (const cWindow & a_Window) override; diff --git a/src/Protocol/Protocol_1_10.cpp b/src/Protocol/Protocol_1_10.cpp index 7f86d4bdc..2cb9d415b 100644 --- a/src/Protocol/Protocol_1_10.cpp +++ b/src/Protocol/Protocol_1_10.cpp @@ -916,7 +916,7 @@ void cProtocol_1_10_0::WriteMobMetadata(cPacketizer & a_Pkt, const cMonster & a_ a_Pkt.WriteBEUInt8(VILLAGER_PROFESSION); a_Pkt.WriteBEUInt8(METADATA_TYPE_VARINT); - a_Pkt.WriteVarInt32(static_cast(Villager.GetVilType())); + a_Pkt.WriteVarInt32(static_cast(cVillager::VillagerCareerToProfession(Villager.GetCareer()))); break; } // case mtVillager diff --git a/src/Protocol/Protocol_1_11.cpp b/src/Protocol/Protocol_1_11.cpp index b9b6e9ac3..e1d512d0b 100644 --- a/src/Protocol/Protocol_1_11.cpp +++ b/src/Protocol/Protocol_1_11.cpp @@ -1076,7 +1076,7 @@ void cProtocol_1_11_0::WriteMobMetadata(cPacketizer & a_Pkt, const cMonster & a_ a_Pkt.WriteBEUInt8(VILLAGER_PROFESSION); a_Pkt.WriteBEUInt8(METADATA_TYPE_VARINT); - a_Pkt.WriteVarInt32(static_cast(Villager.GetVilType())); + a_Pkt.WriteVarInt32(static_cast(cVillager::VillagerCareerToProfession(Villager.GetCareer()))); break; } // case mtVillager diff --git a/src/Protocol/Protocol_1_12.cpp b/src/Protocol/Protocol_1_12.cpp index a8e38a4e0..6e2ef5daa 100644 --- a/src/Protocol/Protocol_1_12.cpp +++ b/src/Protocol/Protocol_1_12.cpp @@ -889,7 +889,7 @@ void cProtocol_1_12::WriteMobMetadata(cPacketizer & a_Pkt, const cMonster & a_Mo a_Pkt.WriteBEUInt8(VILLAGER_PROFESSION); a_Pkt.WriteBEUInt8(METADATA_TYPE_VARINT); - a_Pkt.WriteVarInt32(static_cast(Villager.GetVilType())); + a_Pkt.WriteVarInt32(static_cast(cVillager::VillagerCareerToProfession(Villager.GetCareer()))); break; } // case mtVillager diff --git a/src/Protocol/Protocol_1_8.cpp b/src/Protocol/Protocol_1_8.cpp index b6e5b5a38..0a0073b21 100644 --- a/src/Protocol/Protocol_1_8.cpp +++ b/src/Protocol/Protocol_1_8.cpp @@ -1597,6 +1597,15 @@ void cProtocol_1_8_0::SendUseBed(const cEntity & a_Entity, int a_BlockX, int a_B +void cProtocol_1_8_0::SendVillagerTradeList(const cWindow &, const std::vector &) +{ + // Unimplemented +} + + + + + void cProtocol_1_8_0::SendWeather(eWeather a_Weather) { ASSERT(m_State == 3); // In game mode? @@ -3577,7 +3586,7 @@ void cProtocol_1_8_0::WriteMobMetadata(cPacketizer & a_Pkt, const cMonster & a_M { auto & Villager = reinterpret_cast(a_Mob); a_Pkt.WriteBEUInt8(0x50); - a_Pkt.WriteBEInt32(Villager.GetVilType()); + a_Pkt.WriteBEInt32(static_cast(cVillager::VillagerCareerToProfession(Villager.GetCareer()))); a_Pkt.WriteBEUInt8(0x0c); a_Pkt.WriteBEInt8(Villager.IsBaby() ? -1 : 0); break; diff --git a/src/Protocol/Protocol_1_8.h b/src/Protocol/Protocol_1_8.h index d3d0daf0a..47a7549d3 100644 --- a/src/Protocol/Protocol_1_8.h +++ b/src/Protocol/Protocol_1_8.h @@ -129,6 +129,7 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) override; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) override; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) override; + virtual void SendVillagerTradeList (const cWindow &, const std::vector &) override; virtual void SendWeather (eWeather a_Weather) override; virtual void SendWholeInventory (const cWindow & a_Window) override; virtual void SendWindowClose (const cWindow & a_Window) override; diff --git a/src/Protocol/Protocol_1_9.cpp b/src/Protocol/Protocol_1_9.cpp index 475047417..08f883102 100644 --- a/src/Protocol/Protocol_1_9.cpp +++ b/src/Protocol/Protocol_1_9.cpp @@ -1642,6 +1642,40 @@ void cProtocol_1_9_0::SendUseBed(const cEntity & a_Entity, int a_BlockX, int a_B +void cProtocol_1_9_0::SendVillagerTradeList(const cWindow & TradeWindow, const std::vector & TradeOffers) +{ + ASSERT(m_State == 3); // In game mode? + + cPacketizer Pkt(*this, 0x18); // Plugin message + Pkt.WriteString("MC|TrList"); + Pkt.WriteBEInt32(static_cast(TradeWindow.GetWindowID())); + Pkt.WriteBEUInt8(static_cast(TradeOffers.size())); + + for (const auto & Trade : TradeOffers) + { + WriteItem(Pkt, Trade.PrimaryDesire); + WriteItem(Pkt, Trade.Recompense); + + if (Trade.SecondaryDesire.IsEmpty()) + { + Pkt.WriteBool(false); + } + else + { + Pkt.WriteBool(true); + WriteItem(Pkt, Trade.SecondaryDesire); + } + + Pkt.WriteBool(Trade.IsTradeExhausted()); + Pkt.WriteBEInt32(static_cast(Trade.Transactions)); + Pkt.WriteBEInt32(static_cast(Trade.MaximumTransactions)); + } +} + + + + + void cProtocol_1_9_0::SendWeather(eWeather a_Weather) { ASSERT(m_State == 3); // In game mode? @@ -4044,7 +4078,7 @@ void cProtocol_1_9_0::WriteMobMetadata(cPacketizer & a_Pkt, const cMonster & a_M a_Pkt.WriteBEUInt8(12); // Index 12: Type a_Pkt.WriteBEUInt8(METADATA_TYPE_VARINT); - a_Pkt.WriteVarInt32(static_cast(Villager.GetVilType())); + a_Pkt.WriteVarInt32(static_cast(cVillager::VillagerCareerToProfession(Villager.GetCareer()))); break; } // case mtVillager diff --git a/src/Protocol/Protocol_1_9.h b/src/Protocol/Protocol_1_9.h index 3fbbe86da..aae77fec8 100644 --- a/src/Protocol/Protocol_1_9.h +++ b/src/Protocol/Protocol_1_9.h @@ -135,6 +135,7 @@ public: virtual void SendUpdateBlockEntity (cBlockEntity & a_BlockEntity) override; virtual void SendUpdateSign (int a_BlockX, int a_BlockY, int a_BlockZ, const AString & a_Line1, const AString & a_Line2, const AString & a_Line3, const AString & a_Line4) override; virtual void SendUseBed (const cEntity & a_Entity, int a_BlockX, int a_BlockY, int a_BlockZ) override; + virtual void SendVillagerTradeList (const cWindow &, const std::vector &) override; virtual void SendWeather (eWeather a_Weather) override; virtual void SendWholeInventory (const cWindow & a_Window) override; virtual void SendWindowClose (const cWindow & a_Window) override; diff --git a/src/UI/CMakeLists.txt b/src/UI/CMakeLists.txt index d71e08ade..8d7848756 100644 --- a/src/UI/CMakeLists.txt +++ b/src/UI/CMakeLists.txt @@ -32,6 +32,7 @@ SET (HDRS HopperWindow.h InventoryWindow.h MinecartWithChestWindow.h + VillagerTradeWindow.h WindowOwner.h) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") diff --git a/src/UI/SlotArea.cpp b/src/UI/SlotArea.cpp index 94fd958d5..94b059fb9 100644 --- a/src/UI/SlotArea.cpp +++ b/src/UI/SlotArea.cpp @@ -6,6 +6,7 @@ #include "Globals.h" #include "SlotArea.h" #include "../Entities/Player.h" +#include "../Mobs/Villager.h" #include "../BlockEntities/BeaconEntity.h" #include "../BlockEntities/BrewingstandEntity.h" #include "../BlockEntities/ChestEntity.h" @@ -15,6 +16,7 @@ #include "../Entities/Minecart.h" #include "../Items/ItemHandler.h" #include "AnvilWindow.h" +#include "VillagerTradeWindow.h" #include "../CraftingRecipes.h" #include "../Root.h" #include "../FastRandom.h" @@ -235,15 +237,10 @@ void cSlotArea::ShiftClicked(cPlayer & a_Player, int a_SlotNum, const cItem & a_ void cSlotArea::DblClicked(cPlayer & a_Player, int a_SlotNum) { + // Assume single-click was sent first + cItem & Dragging = a_Player.GetDraggingItem(); if (Dragging.IsEmpty()) - { - // Move the item in the dblclicked slot into hand: - Dragging = *GetSlot(a_SlotNum, a_Player); - cItem EmptyItem; - SetSlot(a_SlotNum, a_Player, EmptyItem); - } - if (Dragging.IsEmpty()) { LOGD("%s DblClicked with an empty hand over empty slot, ignoring", a_Player.GetName().c_str()); return; @@ -2216,6 +2213,216 @@ void cSlotAreaMinecartWithChest::SetSlot(int a_SlotNum, cPlayer & a_Player, cons +//////////////////////////////////////////////////////////////////////////////// +// cSlotAreaVillagerTrade: + +cSlotAreaVillagerTrade::cSlotAreaVillagerTrade(cVillager & Villager, cWindow & ParentWindow) : + cSlotAreaTemporary(3, ParentWindow), + m_Villager(Villager) +{ +} + + + + + +void cSlotAreaVillagerTrade::UpdateTrade(const cPlayer & TradingPlayer) +{ + const auto & Contents = GetPlayerSlots(TradingPlayer); + const auto & Trade = m_Villager.GetTradeOffer( + static_cast(&m_ParentWindow)->GetPlayerTradeOfferIndex(TradingPlayer), + Contents[SlotIndices::PrimaryDesire], + Contents[SlotIndices::SecondaryDesire] + ); + const auto TransactionMultiplier = cVillager::GetTransactionMultiplier(Trade, Contents[SlotIndices::PrimaryDesire], Contents[SlotIndices::SecondaryDesire]); + + if (TransactionMultiplier != 0) + { + cItem Recompense(Trade.Recompense); + Recompense.m_ItemCount *= TransactionMultiplier; + Contents[SlotIndices::Recompense] = Recompense; + } + else + { + Contents[SlotIndices::Recompense].Empty(); + } +} + + + + + +void cSlotAreaVillagerTrade::SetSlot(int ActionIndex, cPlayer & TradingPlayer, const cItem & Item) +{ + const auto & Contents = GetPlayerSlots(TradingPlayer); + const auto & RecompenseSlot = Contents[SlotIndices::Recompense]; + + if ((ActionIndex == SlotIndices::Recompense) && !RecompenseSlot.IsEmpty()) + { + // Player clicked recompense slot. Slot was populated, meaning the trade can go ahead. + // Note that parameter Item holds the new remaining count of compensation items (if any) + + auto & Trade = m_Villager.GetTradeOffer( + static_cast(&m_ParentWindow)->GetPlayerTradeOfferIndex(TradingPlayer), + Contents[SlotIndices::PrimaryDesire], + Contents[SlotIndices::SecondaryDesire] + ); + const auto ActualTransactionMultiplier = static_cast((RecompenseSlot.m_ItemCount - Item.m_ItemCount) / Trade.Recompense.m_ItemCount); + + if (ActualTransactionMultiplier == 0) + { + // Not strictly needed in terms of preserving item count integrity but nothing will change so exit early + // Prevents associated villager from responding to a "trade" with no items exchanged + return; + } + + if (Contents[SlotIndices::PrimaryDesire].IsEmpty()) + { + // (Given that a trade can be made), player must have placed primary desire into secondary slot + + ASSERT(Trade.SecondaryDesire.IsEmpty()); + Contents[SlotIndices::SecondaryDesire].m_ItemCount -= Trade.PrimaryDesire.m_ItemCount * static_cast(ActualTransactionMultiplier); + } + else + { + // Primary\secondary desire\offer slots match up respectively + + Contents[SlotIndices::PrimaryDesire].m_ItemCount -= Trade.PrimaryDesire.m_ItemCount * static_cast(ActualTransactionMultiplier); + Contents[SlotIndices::SecondaryDesire].m_ItemCount -= Trade.SecondaryDesire.m_ItemCount * static_cast(ActualTransactionMultiplier); + } + + m_Villager.HandleTransaction(Trade, ActualTransactionMultiplier); + } + + super::SetSlot(ActionIndex, TradingPlayer, Item); +} + + + + + +void cSlotAreaVillagerTrade::OnPlayerRemoved(cPlayer & Player) +{ + TossItems(Player, SlotIndices::PrimaryDesire, SlotIndices::SecondaryDesire); + super::OnPlayerRemoved(Player); +} + + + + + +void cSlotAreaVillagerTrade::Clicked(cPlayer & Player, int SlotNumber, eClickAction ClickAction, const cItem & ClickedItem) +{ + auto & DraggingItem = Player.GetDraggingItem(); + if ( + (SlotNumber == SlotIndices::Recompense) && + ((ClickAction == eClickAction::caLeftClick) || (ClickAction == eClickAction::caRightClick)) + ) + { + cItem RecompenseSlot = *GetSlot(SlotIndices::Recompense, Player); + if (RecompenseSlot.IsEmpty()) + { + // No trade possible right now + return; + } + + // Single clicking on recompense slot should transfer one transaction's worth of items to the hand + // As long as there is no dragging item or the dragging item is of the same type as the compensation + + const auto MoveCount = m_Villager.GetTradeOffer( + static_cast(&m_ParentWindow)->GetPlayerTradeOfferIndex(Player), + *GetSlot(SlotIndices::PrimaryDesire, Player), + *GetSlot(SlotIndices::SecondaryDesire, Player) + ).Recompense.m_ItemCount; + if (DraggingItem.IsEmpty()) + { + DraggingItem = RecompenseSlot; + DraggingItem.m_ItemCount = MoveCount; + RecompenseSlot.m_ItemCount -= MoveCount; + SetSlot(SlotIndices::Recompense, Player, RecompenseSlot); // Perform transaction + } + else if (DraggingItem.IsEqual(RecompenseSlot)) + { + if (DraggingItem.GetMaxStackSize() < (DraggingItem.m_ItemCount + MoveCount)) + { + return; + } + + DraggingItem.m_ItemCount += MoveCount; + RecompenseSlot.m_ItemCount -= MoveCount; + SetSlot(SlotIndices::Recompense, Player, RecompenseSlot); // Perform transaction + } + + return; + } + + super::Clicked(Player, SlotNumber, ClickAction, ClickedItem); + UpdateTrade(Player); + + if (GetSlot(SlotIndices::Recompense, Player)->IsEmpty()) + { + m_Villager.HandleTradeUnavailable(); + } + else + { + m_Villager.HandleTradeAvailable(); + } +} + + + + + +void cSlotAreaVillagerTrade::NumberClicked(cPlayer & Player, int SlotNumber, eClickAction ClickAction) +{ + if (SlotNumber != SlotIndices::Recompense) + { + super::NumberClicked(Player, SlotNumber, ClickAction); + return; + } + + int HotbarSlotIndex = static_cast(ClickAction - caNumber1); + cItem HotbarSlot(Player.GetInventory().GetHotbarSlot(HotbarSlotIndex)); + cItem RecompenseSlot(*GetSlot(SlotNumber, Player)); + + if (!HotbarSlot.IsEmpty()) + { + // Prevent items being inserted into trade compensation slot + return; + } + + const auto MoveCount = m_Villager.GetTradeOffer( + static_cast(&m_ParentWindow)->GetPlayerTradeOfferIndex(Player), + *GetSlot(SlotIndices::PrimaryDesire, Player), + *GetSlot(SlotIndices::SecondaryDesire, Player) + ).Recompense.m_ItemCount; + + HotbarSlot = RecompenseSlot; + HotbarSlot.m_ItemCount = MoveCount; + RecompenseSlot.m_ItemCount -= MoveCount; + + Player.GetInventory().SetHotbarSlot(HotbarSlotIndex, HotbarSlot); + SetSlot(SlotNumber, Player, RecompenseSlot); +} + + + + + +void cSlotAreaVillagerTrade::DblClicked(cPlayer & Player, int SlotNumber) +{ + if (SlotNumber == SlotIndices::Recompense) + { + return; + } + + super::DblClicked(Player, SlotNumber); +} + + + + + //////////////////////////////////////////////////////////////////////////////// // cSlotAreaInventoryBase: @@ -2579,7 +2786,7 @@ void cSlotAreaTemporary::TossItems(cPlayer & a_Player, int a_Begin, int a_End) -cItem * cSlotAreaTemporary::GetPlayerSlots(cPlayer & a_Player) +cItem * cSlotAreaTemporary::GetPlayerSlots(const cPlayer & a_Player) { cItemMap::iterator itr = m_Items.find(a_Player.GetUniqueID()); if (itr == m_Items.end()) diff --git a/src/UI/SlotArea.h b/src/UI/SlotArea.h index be21cdada..99075aad7 100644 --- a/src/UI/SlotArea.h +++ b/src/UI/SlotArea.h @@ -22,6 +22,7 @@ class cChestEntity; class cEnderChestEntity; class cFurnaceEntity; class cMinecartWithChest; +class cVillager; class cCraftingRecipe; class cWorld; @@ -238,7 +239,7 @@ protected: cItemMap m_Items; /** Returns the pointer to the slot array for the player specified. */ - cItem * GetPlayerSlots(cPlayer & a_Player); + cItem * GetPlayerSlots(const cPlayer & a_Player); } ; @@ -498,7 +499,7 @@ protected: /** Called after an item has been brewed to handle statistics etc. */ void HandleBrewedItem(cPlayer & a_Player, const cItem & a_ClickedItem); -} ; +}; @@ -520,3 +521,48 @@ protected: + +class cSlotAreaVillagerTrade : + public cSlotAreaTemporary +{ + typedef cSlotAreaTemporary super; + +public: + cSlotAreaVillagerTrade(cVillager & Villager, cWindow & a_ParentWindow); + + /** Handles player interaction with a given trade. + Sets appropriate item counts based on input counts and output types. */ + void UpdateTrade(const cPlayer &); + +protected: + + enum SlotIndices : unsigned + { + PrimaryDesire = 0, + SecondaryDesire = 1, + Recompense = 2 + }; + + /** Override to respond to attempted trades. + Updates primary and secondary desire counts, assuming that they are correctly set for the given trade. + Informs the associated villager of the transaction. */ + virtual void SetSlot(int, cPlayer &, const cItem &) override; + + /** Override to toss all items in the trading window on player exit. */ + virtual void OnPlayerRemoved(cPlayer &) override; + + /** Override to update the trade items and inform the associated villager of the trading status. */ + virtual void Clicked(cPlayer &, int, eClickAction, const cItem &) override; + + /** Override to disable transferral of the compensation item to an already populated hotbar slot. + Default behaviour when destination hotbar slot is populated is to swap the source and destination, but this is not possible since the compensation slot cannot be backfilled. */ + virtual void NumberClicked(cPlayer &, int, eClickAction) override; + + /** Override to disable double-click behaviour for the compensation slot. */ + virtual void DblClicked(cPlayer &, int) override; + + /** Override to prevent distribution of items into the trading window. */ + virtual void DistributeStack(cItem &, cPlayer &, bool, bool, bool) override {} + + cVillager & m_Villager; +}; diff --git a/src/UI/VillagerTradeWindow.h b/src/UI/VillagerTradeWindow.h new file mode 100644 index 000000000..ec5ea26e7 --- /dev/null +++ b/src/UI/VillagerTradeWindow.h @@ -0,0 +1,74 @@ + +#pragma once + +#include "Window.h" +#include "SlotArea.h" +#include "../Mobs/Villager.h" + + + + + +class cVillagerTradeWindow : + public cWindow +{ + typedef cWindow super; + +public: + cVillagerTradeWindow(cVillager & Villager) : + cWindow(wtVillagerTrade, Printf("Villager № %i", Villager.GetUniqueID())), + m_Villager(Villager) + { + m_SlotAreas.push_back(new cSlotAreaVillagerTrade(m_Villager, *this)); + m_SlotAreas.push_back(new cSlotAreaInventory(*this)); + m_SlotAreas.push_back(new cSlotAreaHotBar(*this)); + } + + virtual void DistributeStack(cItem & a_ItemStack, int a_Slot, cPlayer & a_Player, cSlotArea * a_ClickedArea, bool a_ShouldApply) override + { + cSlotAreas AreasInOrder; + + if (a_ClickedArea == m_SlotAreas[0]) + { + // Trading Area + AreasInOrder.push_back(m_SlotAreas[1]); /* Inventory */ + AreasInOrder.push_back(m_SlotAreas[2]); /* Hotbar */ + super::DistributeStackToAreas(a_ItemStack, a_Player, AreasInOrder, a_ShouldApply, true); + } + else if (a_ClickedArea == m_SlotAreas[1]) + { + // Inventory + AreasInOrder.push_back(m_SlotAreas[2]); /* Hotbar */ + super::DistributeStackToAreas(a_ItemStack, a_Player, AreasInOrder, a_ShouldApply, true); + } + else + { + // Hotbar + AreasInOrder.push_back(m_SlotAreas[1]); /* Inventory */ + super::DistributeStackToAreas(a_ItemStack, a_Player, AreasInOrder, a_ShouldApply, true); + } + } + + virtual void OpenedByPlayer(cPlayer & Player) override + { + super::OpenedByPlayer(Player); + m_TradeIndexOpenedByPlayer[&Player] = 0; + m_Villager.HandleTradeInProgress(); + } + + void PlayerChangedTradeOffer(const cPlayer & Player, unsigned NewIndex) + { + m_TradeIndexOpenedByPlayer[&Player] = NewIndex; + static_cast(m_SlotAreas[0])->UpdateTrade(Player); + m_Villager.HandleTradeInProgress(); + } + + unsigned GetPlayerTradeOfferIndex(const cPlayer & Player) + { + return m_TradeIndexOpenedByPlayer[&Player]; + } + +private: + cVillager & m_Villager; + std::unordered_map m_TradeIndexOpenedByPlayer; +}; diff --git a/src/UI/Window.cpp b/src/UI/Window.cpp index 8bbc4f482..52364b287 100644 --- a/src/UI/Window.cpp +++ b/src/UI/Window.cpp @@ -71,7 +71,7 @@ const AString cWindow::GetWindowTypeName(void) const case wtDropSpenser: return "minecraft:dispenser"; case wtEnchantment: return "minecraft:enchanting_table"; case wtBrewery: return "minecraft:brewing_stand"; - case wtNPCTrade: return "minecraft:villager"; + case wtVillagerTrade: return "minecraft:villager"; case wtBeacon: return "minecraft:beacon"; case wtAnvil: return "minecraft:anvil"; case wtHopper: return "minecraft:hopper"; diff --git a/src/UI/Window.h b/src/UI/Window.h index d7a29dc47..c834eb0ed 100644 --- a/src/UI/Window.h +++ b/src/UI/Window.h @@ -60,7 +60,7 @@ public: wtDropSpenser = 3, // Dropper or Dispenser wtEnchantment = 4, wtBrewery = 5, - wtNPCTrade = 6, + wtVillagerTrade = 6, wtBeacon = 7, wtAnvil = 8, wtHopper = 9, diff --git a/src/WorldStorage/NBTChunkSerializer.cpp b/src/WorldStorage/NBTChunkSerializer.cpp index 1e8543648..5011075f1 100644 --- a/src/WorldStorage/NBTChunkSerializer.cpp +++ b/src/WorldStorage/NBTChunkSerializer.cpp @@ -664,8 +664,9 @@ void cNBTChunkSerializer::AddMonsterEntity(cMonster * a_Monster) case mtVillager: { const cVillager *Villager = reinterpret_cast(a_Monster); - m_Writer.AddInt("Profession", Villager->GetVilType()); - m_Writer.AddInt("Age", Villager->GetAge()); + m_Writer.AddInt("Profession", static_cast(cVillager::VillagerCareerToProfession(Villager->GetCareer()))); + m_Writer.AddInt("Career", static_cast(Villager->GetCareer())); + m_Writer.AddInt("Age", Villager->GetAge()); break; } case mtWither: diff --git a/src/WorldStorage/WSSAnvil.cpp b/src/WorldStorage/WSSAnvil.cpp index a3251481f..774699b8f 100755 --- a/src/WorldStorage/WSSAnvil.cpp +++ b/src/WorldStorage/WSSAnvil.cpp @@ -2837,9 +2837,13 @@ void cWSSAnvil::LoadVillagerFromNBT(cEntityList & a_Entities, const cParsedNBT & return; } - int Type = a_NBT.GetInt(TypeIdx); + int CareerIdx = a_NBT.FindChildByName(a_TagIdx, "Career"); + if (CareerIdx < 0) + { + return; + } - std::unique_ptr Monster = cpp14::make_unique(cVillager::eVillagerType(Type)); + std::unique_ptr Monster = cpp14::make_unique(cVillager::VillagerCareer(a_NBT.GetInt(TypeIdx)), 1U); if (!LoadEntityBaseFromNBT(*Monster.get(), a_NBT, a_TagIdx)) { return; -- cgit v1.2.3