summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Server/Plugins/APIDump/APIDesc.lua2
-rw-r--r--src/ClientHandle.cpp16
-rw-r--r--src/ClientHandle.h2
-rw-r--r--src/Entities/Entity.cpp2
-rw-r--r--src/Mobs/Monster.cpp6
-rw-r--r--src/Mobs/Villager.cpp318
-rw-r--r--src/Mobs/Villager.h133
-rw-r--r--src/Protocol/Protocol.h2
-rw-r--r--src/Protocol/ProtocolRecognizer.cpp10
-rw-r--r--src/Protocol/ProtocolRecognizer.h1
-rw-r--r--src/Protocol/Protocol_1_10.cpp2
-rw-r--r--src/Protocol/Protocol_1_11.cpp2
-rw-r--r--src/Protocol/Protocol_1_12.cpp2
-rw-r--r--src/Protocol/Protocol_1_8.cpp11
-rw-r--r--src/Protocol/Protocol_1_8.h1
-rw-r--r--src/Protocol/Protocol_1_9.cpp36
-rw-r--r--src/Protocol/Protocol_1_9.h1
-rw-r--r--src/UI/CMakeLists.txt1
-rw-r--r--src/UI/SlotArea.cpp223
-rw-r--r--src/UI/SlotArea.h50
-rw-r--r--src/UI/VillagerTradeWindow.h74
-rw-r--r--src/UI/Window.cpp2
-rw-r--r--src/UI/Window.h2
-rw-r--r--src/WorldStorage/NBTChunkSerializer.cpp5
-rwxr-xr-xsrc/WorldStorage/WSSAnvil.cpp8
25 files changed, 838 insertions, 74 deletions
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<cVillagerTradeWindow *>(CurrentWindow)->PlayerChangedTradeOffer(*GetPlayer(), static_cast<unsigned>(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<VillagerTradeOffer> & 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<cClientHandle> 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<VillagerTradeOffer> &);
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> 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<cVillager>(static_cast<cVillager::eVillagerType>(VillagerType));
+ return cpp14::make_unique<cVillager>(static_cast<cVillager::VillagerCareer>(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<unsigned>(PrimarySlot.m_ItemCount / Trade.PrimaryDesire.m_ItemCount),
+ Trade.SecondaryDesire.IsEmpty() ? std::numeric_limits<unsigned>::max() : static_cast<unsigned>(SecondarySlot.m_ItemCount / Trade.SecondaryDesire.m_ItemCount),
+ static_cast<unsigned>(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<unsigned>(SecondarySlot.m_ItemCount / Trade.PrimaryDesire.m_ItemCount),
+ static_cast<unsigned>(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<cClientHandle>
+ {
+ public:
+ SendNewTradesCallback(const cWindow & VillagerTradeWindow, const std::vector<VillagerTradeOffer> & VillagerTradeOffers) :
+ TradeWindow(VillagerTradeWindow),
+ TradeOffers(VillagerTradeOffers)
+ {
+ }
+
+ bool Item(cClientHandle * Handle)
+ {
+ Handle->SendVillagerTradeList(TradeWindow, TradeOffers);
+ return false;
+ }
+
+ private:
+ const cWindow & TradeWindow;
+ const std::vector<VillagerTradeOffer> & 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<size_t>(Career);
+ }
+ };
+ using Offer = VillagerTradeOffer;
+
+ static const std::unordered_map<
+ VillagerCareer,
+ std::vector<
+ // Element index: trading tier (0-based)
+ std::vector<Offer> // 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<VillagerTradeOffer> 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<VillagerTradeOffer> &) = 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<VillagerTradeOffer> & 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<VillagerTradeOffer> &) 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<UInt32>(Villager.GetVilType()));
+ a_Pkt.WriteVarInt32(static_cast<UInt32>(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<UInt32>(Villager.GetVilType()));
+ a_Pkt.WriteVarInt32(static_cast<UInt32>(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<UInt32>(Villager.GetVilType()));
+ a_Pkt.WriteVarInt32(static_cast<UInt32>(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<VillagerTradeOffer> &)
+{
+ // 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<const cVillager &>(a_Mob);
a_Pkt.WriteBEUInt8(0x50);
- a_Pkt.WriteBEInt32(Villager.GetVilType());
+ a_Pkt.WriteBEInt32(static_cast<Int32>(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<VillagerTradeOffer> &) 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<VillagerTradeOffer> & TradeOffers)
+{
+ ASSERT(m_State == 3); // In game mode?
+
+ cPacketizer Pkt(*this, 0x18); // Plugin message
+ Pkt.WriteString("MC|TrList");
+ Pkt.WriteBEInt32(static_cast<Int32>(TradeWindow.GetWindowID()));
+ Pkt.WriteBEUInt8(static_cast<UInt8>(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<Int32>(Trade.Transactions));
+ Pkt.WriteBEInt32(static_cast<Int32>(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<UInt32>(Villager.GetVilType()));
+ a_Pkt.WriteVarInt32(static_cast<UInt32>(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<VillagerTradeOffer> &) 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,16 +237,11 @@ 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;
}
@@ -2217,6 +2214,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<cVillagerTradeWindow *>(&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<cVillagerTradeWindow *>(&m_ParentWindow)->GetPlayerTradeOfferIndex(TradingPlayer),
+ Contents[SlotIndices::PrimaryDesire],
+ Contents[SlotIndices::SecondaryDesire]
+ );
+ const auto ActualTransactionMultiplier = static_cast<unsigned>((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<char>(ActualTransactionMultiplier);
+ }
+ else
+ {
+ // Primary\secondary desire\offer slots match up respectively
+
+ Contents[SlotIndices::PrimaryDesire].m_ItemCount -= Trade.PrimaryDesire.m_ItemCount * static_cast<char>(ActualTransactionMultiplier);
+ Contents[SlotIndices::SecondaryDesire].m_ItemCount -= Trade.SecondaryDesire.m_ItemCount * static_cast<char>(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<cVillagerTradeWindow *>(&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<int>(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<cVillagerTradeWindow *>(&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:
cSlotAreaInventoryBase::cSlotAreaInventoryBase(int a_NumSlots, int a_SlotOffset, cWindow & a_ParentWindow) :
@@ -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<cSlotAreaVillagerTrade *>(m_SlotAreas[0])->UpdateTrade(Player);
+ m_Villager.HandleTradeInProgress();
+ }
+
+ unsigned GetPlayerTradeOfferIndex(const cPlayer & Player)
+ {
+ return m_TradeIndexOpenedByPlayer[&Player];
+ }
+
+private:
+ cVillager & m_Villager;
+ std::unordered_map<const cPlayer *, unsigned> 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<const cVillager *>(a_Monster);
- m_Writer.AddInt("Profession", Villager->GetVilType());
- m_Writer.AddInt("Age", Villager->GetAge());
+ m_Writer.AddInt("Profession", static_cast<Int32>(cVillager::VillagerCareerToProfession(Villager->GetCareer())));
+ m_Writer.AddInt("Career", static_cast<Int32>(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<cVillager> Monster = cpp14::make_unique<cVillager>(cVillager::eVillagerType(Type));
+ std::unique_ptr<cVillager> Monster = cpp14::make_unique<cVillager>(cVillager::VillagerCareer(a_NBT.GetInt(TypeIdx)), 1U);
if (!LoadEntityBaseFromNBT(*Monster.get(), a_NBT, a_TagIdx))
{
return;