diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 30941a1bf..114238d83 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -4389,6 +4389,19 @@ namespace RoF2 delete in; } + ENCODE(OP_CrystalCountUpdate) + { + ENCODE_LENGTH_EXACT(CrystalCountUpdate_Struct); + SETUP_DIRECT_ENCODE(CrystalCountUpdate_Struct, structs::CrystalCountUpdate_Struct); + + OUT(CurrentRadiantCrystals); + OUT(CareerRadiantCrystals); + OUT(CurrentEbonCrystals); + OUT(CareerEbonCrystals); + + FINISH_ENCODE(); + } + // DECODE methods DECODE(OP_AdventureMerchantSell) diff --git a/common/patches/rof2_ops.h b/common/patches/rof2_ops.h index b49784029..1ca7063f4 100644 --- a/common/patches/rof2_ops.h +++ b/common/patches/rof2_ops.h @@ -141,6 +141,7 @@ E(OP_ZoneEntry) E(OP_ZonePlayerToBind) E(OP_ZoneServerInfo) E(OP_ZoneSpawns) +E(OP_CrystalCountUpdate) // Begin RoF Decodes D(OP_AdventureMerchantSell) D(OP_AltCurrencySell) diff --git a/common/patches/rof2_structs.h b/common/patches/rof2_structs.h index ed34d44b8..0b638660e 100644 --- a/common/patches/rof2_structs.h +++ b/common/patches/rof2_structs.h @@ -5081,6 +5081,15 @@ struct MercenaryMerchantResponse_Struct { /*0004*/ }; +// Sent by Server to update character crystals. +struct CrystalCountUpdate_Struct +{ + /*000*/ uint32 CurrentRadiantCrystals; + /*004*/ uint32 CareerRadiantCrystals; + /*008*/ uint32 CurrentEbonCrystals; + /*012*/ uint32 CareerEbonCrystals; +}; + }; /*structs*/ }; /*RoF2*/ diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index e0de70add..7f77a2eb9 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -5004,38 +5004,49 @@ void Client::Handle_OP_CrystalCreate(const EQApplicationPacket *app) VERIFY_PACKET_LENGTH(OP_CrystalCreate, app, CrystalReclaim_Struct); CrystalReclaim_Struct *cr = (CrystalReclaim_Struct*)app->pBuffer; - if (cr->type == 5) { - if (cr->amount > GetEbonCrystals()) { - SummonItem(RuleI(Zone, EbonCrystalItemID), GetEbonCrystals()); - m_pp.currentEbonCrystals = 0; - m_pp.careerEbonCrystals = 0; - SaveCurrency(); - SendCrystalCounts(); - } - else { - SummonItem(RuleI(Zone, EbonCrystalItemID), cr->amount); - m_pp.currentEbonCrystals -= cr->amount; - m_pp.careerEbonCrystals -= cr->amount; - SaveCurrency(); - SendCrystalCounts(); - } + const uint32 requestQty = cr->amount; + const bool isRadiant = cr->type == 4; + const bool isEbon = cr->type == 5; + + // Check: Valid type requested. + if (!isRadiant && !isEbon) { + return; } - else if (cr->type == 4) { - if (cr->amount > GetRadiantCrystals()) { - SummonItem(RuleI(Zone, RadiantCrystalItemID), GetRadiantCrystals()); - m_pp.currentRadCrystals = 0; - m_pp.careerRadCrystals = 0; - SaveCurrency(); - SendCrystalCounts(); - } - else { - SummonItem(RuleI(Zone, RadiantCrystalItemID), cr->amount); - m_pp.currentRadCrystals -= cr->amount; - m_pp.careerRadCrystals -= cr->amount; - SaveCurrency(); - SendCrystalCounts(); - } + // Check: Valid quantity requested. + if (requestQty < 1) { + return; } + + // Check: Valid client state to make request. + // In this situation the client is either desynced or attempting an exploit. + const uint32 currentQty = isRadiant ? GetRadiantCrystals() : GetEbonCrystals(); + if (currentQty == 0) { + return; + } + + // Prevent the client from creating more than they have. + const uint32 amount = EQEmu::ClampUpper(requestQty, currentQty); + const uint32 itemID = isRadiant ? RuleI(Zone, RadiantCrystalItemID) : RuleI(Zone, EbonCrystalItemID); + + // Summon crystals for player. + const bool success = SummonItem(itemID, amount); + + if (!success) { + return; + } + + // Deduct crystals from client and update them. + if (isRadiant) { + m_pp.currentRadCrystals -= amount; + m_pp.careerRadCrystals -= amount; + } + else if (isEbon) { + m_pp.currentEbonCrystals -= amount; + m_pp.careerEbonCrystals -= amount; + } + + SaveCurrency(); + SendCrystalCounts(); } void Client::Handle_OP_CrystalReclaim(const EQApplicationPacket *app)