Implement basic form of trading

* Fixes #3450
* Addresses #1109
This commit is contained in:
Tiger Wang 2017-08-27 22:37:39 +01:00
parent da0778dfaa
commit 5481249d0f
25 changed files with 837 additions and 73 deletions

View File

@ -12859,7 +12859,7 @@ end
{
Notes = "An inventory window",
},
wtNPCTrade =
wtVillagerTrade =
{
Notes = "A villager trade window",
},

View File

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

View File

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

View File

@ -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++)

View File

@ -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:
{

View File

@ -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)
{
case vtFarmer:
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)
{
HandleFarmerPrepareFarmCrops();
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++)
{
// One singular transaction has a 10% chance of upgrading the villager's trading tier
if (GetRandomProvider().RandBool(0.1))
{
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)

View File

@ -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
{
vtFarmer = 0,
vtLibrarian = 1,
vtPriest = 2,
vtBlacksmith = 3,
vtButcher = 4,
vtGeneric = 5,
vtMax
} ;
Farmer = 0,
Librarian = 1,
Priest = 2,
Blacksmith = 3,
Butcher = 4,
Nitwit = 5
};
cVillager(eVillagerType VillagerType);
enum class VillagerCareer
{
Farmer,
Fisherman,
Shepherd,
Fletcher,
Librarian,
Cartographer,
Cleric,
Armorer,
WeaponSmith,
ToolSmith,
Butcher,
Leatherworker,
Nitwit
};
cVillager(VillagerCareer, unsigned);
CLASS_PROTODEF(cVillager)
/** Converts internal career to equivalent Vanilla profession.
Returns an enumeration with Vanilla-compatible values. */
static VillagerProfession VillagerCareerToProfession(VillagerCareer);
/** 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;
} ;
};

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -32,6 +32,7 @@ SET (HDRS
HopperWindow.h
InventoryWindow.h
MinecartWithChestWindow.h
VillagerTradeWindow.h
WindowOwner.h)
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")

View File

@ -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<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:
@ -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())

View File

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

View File

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

View File

@ -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";

View File

@ -60,7 +60,7 @@ public:
wtDropSpenser = 3, // Dropper or Dispenser
wtEnchantment = 4,
wtBrewery = 5,
wtNPCTrade = 6,
wtVillagerTrade = 6,
wtBeacon = 7,
wtAnvil = 8,
wtHopper = 9,

View File

@ -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:

View File

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