Merge plus some work on fixing stacking for merchants.

This commit is contained in:
KimLS 2015-03-15 14:02:13 -07:00
commit 21ce5c6daa
17 changed files with 425 additions and 178 deletions

View File

@ -1,5 +1,15 @@
EQEMu Changelog (Started on Sept 24, 2003 15:50)
-------------------------------------------------------
== 03/04/2015 ==
Akkadius: Fix Spell Book Deletion
== 03/03/2015 ==
Uleat: Fix for 'Invalid Slot ID' messages. Bag slot count is now enforced during database saves to eliminate existing 'hidden' duplicated items..sorry MQ2 users...
Uleat: Fix for Item loss during corpse looting and possible item loss when purchasing items from LDoN or Adventure merchants. (cursor-related)
== 02/27/2015 ==
Uleat: Final 'tweak' for light sources until a valid issue arises. Wearable equipment now matches client behavior. Stackable light sources are bugged in the client..but, the current timer update implementation alleviates this condition.
== 02/26/2015 ==
Uleat: Updated light source criteria to (hopefully) match what the client uses (still needs tweaking)
Uleat: Changed 'general' light source checks to accept the last valid light source (client behavior)
@ -8,6 +18,13 @@ Notes:
- Wearable equipment types still register as valid light sources when in general slots (needs exemption criteria)
== 02/23/2015 ==
Noudess: Allow for a rule to set starting swimming && SenseHeading value.
I moved the swimming override to char create instead of setting it
every time a char enters a zone.
Also added rules to not ignore, but rather forrce sense heading packets to be
used to train it instead of maxing it out like before.
Uleat: Fix for RoF+ clients showing active 'Return Home' button when action is not available (swapped 'GoHome' and 'Enabled' fields in RoF-era CharacterSelectEntry structs)
== 02/21/2015 ==

View File

@ -126,6 +126,14 @@ struct LDoNTrapTemplate
// All clients translate the character select information to some degree
struct Inventory_Slot_Struct
{
int16 type;
int16 slot;
int16 bag;
int16 aug;
};
struct Color_Struct
{
union {
@ -1969,13 +1977,11 @@ Unknowns:
struct Merchant_Sell_Struct {
/*000*/ uint32 npcid; // Merchant NPC's entity id
/*004*/ uint32 playerid; // Player's entity id
/*008*/ uint32 itemslot;
uint32 unknown12;
/*016*/ uint8 quantity; // Already sold
/*017*/ uint8 Unknown016[3];
/*020*/ uint32 price;
uint32 npcid;
uint32 playerid;
uint32 itemslot;
int32 quantity;
uint32 price;
};
struct Merchant_Purchase_Struct {
/*000*/ uint32 npcid; // Merchant NPC's entity id

View File

@ -366,6 +366,43 @@ bool EQEmu::Inventory::Swap(const InventorySlot &src, const InventorySlot &dest,
return true;
}
bool EQEmu::Inventory::TryStacking(std::shared_ptr<EQEmu::ItemInstance> inst, const InventorySlot &slot) {
auto target_inst = Get(slot);
if(!inst || !target_inst ||
!inst->IsStackable() || !target_inst->IsStackable())
{
return false;
}
if(inst->GetBaseItem()->ID != target_inst->GetBaseItem()->ID) {
return false;
}
int stack_avail = target_inst->GetBaseItem()->StackSize - target_inst->GetCharges();
if(stack_avail <= 0) {
return false;
}
impl_->data_model_->Begin();
if(inst->GetCharges() <= stack_avail) {
inst->SetCharges(0);
target_inst->SetCharges(target_inst->GetCharges() + inst->GetCharges());
impl_->data_model_->Delete(slot);
impl_->data_model_->Insert(slot, target_inst);
} else {
inst->SetCharges(inst->GetCharges() - stack_avail);
target_inst->SetCharges(target_inst->GetCharges() + stack_avail);
impl_->data_model_->Delete(slot);
impl_->data_model_->Insert(slot, target_inst);
}
impl_->data_model_->Commit();
return true;
}
bool EQEmu::Inventory::Summon(const InventorySlot &slot, std::shared_ptr<ItemInstance> inst) {
if(!inst)
return false;

View File

@ -135,9 +135,12 @@ namespace EQEmu
std::shared_ptr<ItemInstance> Get(const InventorySlot &slot);
bool Put(const InventorySlot &slot, std::shared_ptr<ItemInstance> inst);
bool Swap(const InventorySlot &src, const InventorySlot &dest, int charges);
bool TryStacking(std::shared_ptr<EQEmu::ItemInstance> inst, const InventorySlot &slot);
bool Summon(const InventorySlot &slot, std::shared_ptr<ItemInstance> inst);
bool PushToCursorBuffer(std::shared_ptr<ItemInstance> inst);
bool PopFromCursorBuffer();
bool PutStackInInventory(std::shared_ptr<ItemInstance> inst, bool try_worn, bool try_cursor);
InventorySlot PutItemInInventory(std::shared_ptr<ItemInstance> inst, bool try_worn, bool try_cursor);
//utility
static int CalcMaterialFromSlot(const InventorySlot &slot);

View File

@ -3062,13 +3062,13 @@ namespace RoF
{
ENCODE_LENGTH_EXACT(Merchant_Sell_Struct);
SETUP_DIRECT_ENCODE(Merchant_Sell_Struct, structs::Merchant_Sell_Struct);
OUT(npcid);
OUT(playerid);
OUT(itemslot);
OUT(quantity);
OUT(price);
FINISH_ENCODE();
}
@ -4821,13 +4821,13 @@ namespace RoF
{
DECODE_LENGTH_EXACT(structs::Merchant_Sell_Struct);
SETUP_DIRECT_DECODE(Merchant_Sell_Struct, structs::Merchant_Sell_Struct);
IN(npcid);
IN(playerid);
IN(itemslot);
IN(quantity);
IN(price);
FINISH_DIRECT_DECODE();
}

View File

@ -133,6 +133,9 @@ RULE_INT ( Skills, MaxTrainTradeskills, 21 )
RULE_BOOL ( Skills, UseLimitTradeskillSearchSkillDiff, true )
RULE_INT ( Skills, MaxTradeskillSearchSkillDiff, 50 )
RULE_INT ( Skills, MaxTrainSpecializations, 50 ) // Max level a GM trainer will train casting specializations
RULE_INT ( Skills, SwimmingStartValue, 100 )
RULE_BOOL ( Skills, TrainSenseHeading, false )
RULE_INT ( Skills, SenseHeadingStartValue, 200 )
RULE_CATEGORY_END()
RULE_CATEGORY( Pets )

View File

@ -256,7 +256,9 @@ bool SharedDatabase::UpdateSharedBankSlot(uint32 char_id, const ItemInst* inst,
// Save bag contents, if slot supports bag contents
if (inst->IsType(ItemClassContainer) && InventoryOld::SupportsContainers(slot_id)) {
for (uint8 idx = SUB_BEGIN; idx < EmuConstants::ITEM_CONTAINER_SIZE; idx++) {
// Limiting to bag slot count will get rid of 'hidden' duplicated items and 'Invalid Slot ID'
// messages through attrition (and the modded code in SaveInventory)
for (uint8 idx = SUB_BEGIN; idx < inst->GetItem()->BagSlots && idx < EmuConstants::ITEM_CONTAINER_SIZE; idx++) {
const ItemInst* baginst = inst->GetItem(idx);
SaveInventory(char_id, baginst, InventoryOld::CalcSlotId(slot_id, idx));
}

View File

@ -1430,7 +1430,10 @@ bool Client::OPCharCreate(char *name, CharCreate_Struct *cc)
SetRaceStartingSkills(&pp);
SetClassStartingSkills(&pp);
SetClassLanguages(&pp);
pp.skills[SkillSenseHeading] = 200;
pp.skills[SkillSwimming] = RuleI(Skills, SwimmingStartValue);
pp.skills[SkillSenseHeading] = RuleI(Skills, SenseHeadingStartValue);
// strcpy(pp.servername, WorldConfig::get()->ShortName.c_str());

View File

@ -81,6 +81,8 @@ void Client::CalcBonuses()
CalcAABonuses(&aabonuses); //we're not quite ready for this
Log.Out(Logs::Detail, Logs::AA, "Finished calculating AA Bonuses for %s.", this->GetCleanName());
ProcessItemCaps(); // caps that depend on spell/aa bonuses
RecalcWeight();
CalcAC();
@ -183,16 +185,24 @@ void Client::CalcItemBonuses(StatBonuses* newbon) {
AdditiveWornBonuses(inst, newbon);
}
}
}
// Caps
if(newbon->HPRegen > CalcHPRegenCap())
newbon->HPRegen = CalcHPRegenCap();
// These item stat caps depend on spells/AAs so we process them after those are processed
void Client::ProcessItemCaps()
{
itembonuses.HPRegen = std::min(itembonuses.HPRegen, CalcHPRegenCap());
itembonuses.ManaRegen = std::min(itembonuses.ManaRegen, CalcManaRegenCap());
itembonuses.EnduranceRegen = std::min(itembonuses.EnduranceRegen, CalcEnduranceRegenCap());
if(newbon->ManaRegen > CalcManaRegenCap())
newbon->ManaRegen = CalcManaRegenCap();
// The Sleeper Tomb Avatar proc counts towards item ATK
// The client uses a 100 here, so using a 100 here the client and server will agree
// For example, if you set the effect to be 200 it will get 100 item ATK and 100 spell ATK
if (IsValidSpell(2434) && FindBuff(2434)) {
itembonuses.ATK += 100;
spellbonuses.ATK -= 100;
}
if(newbon->EnduranceRegen > CalcEnduranceRegenCap())
newbon->EnduranceRegen = CalcEnduranceRegenCap();
itembonuses.ATK = std::min(itembonuses.ATK, CalcItemATKCap());
}
void Client::AddItemBonuses(const ItemInst *inst, StatBonuses* newbon, bool isAug, bool isTribute) {
@ -225,6 +235,7 @@ void Client::AddItemBonuses(const ItemInst *inst, StatBonuses* newbon, bool isAu
newbon->HP += item->HP;
newbon->Mana += item->Mana;
newbon->Endurance += item->Endur;
newbon->ATK += item->Attack;
newbon->STR += (item->AStr + item->HeroicStr);
newbon->STA += (item->ASta + item->HeroicSta);
newbon->DEX += (item->ADex + item->HeroicDex);
@ -278,6 +289,7 @@ void Client::AddItemBonuses(const ItemInst *inst, StatBonuses* newbon, bool isAu
newbon->HP += CalcRecommendedLevelBonus( lvl, reclvl, item->HP );
newbon->Mana += CalcRecommendedLevelBonus( lvl, reclvl, item->Mana );
newbon->Endurance += CalcRecommendedLevelBonus( lvl, reclvl, item->Endur );
newbon->ATK += CalcRecommendedLevelBonus( lvl, reclvl, item->Attack );
newbon->STR += CalcRecommendedLevelBonus( lvl, reclvl, (item->AStr + item->HeroicStr) );
newbon->STA += CalcRecommendedLevelBonus( lvl, reclvl, (item->ASta + item->HeroicSta) );
newbon->DEX += CalcRecommendedLevelBonus( lvl, reclvl, (item->ADex + item->HeroicDex) );
@ -335,16 +347,6 @@ void Client::AddItemBonuses(const ItemInst *inst, StatBonuses* newbon, bool isAu
if(item->EnduranceRegen > 0)
newbon->EnduranceRegen += item->EnduranceRegen;
if(item->Attack > 0) {
int cap = RuleI(Character, ItemATKCap);
cap += itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap;
if((newbon->ATK + item->Attack) > cap)
newbon->ATK = RuleI(Character, ItemATKCap);
else
newbon->ATK += item->Attack;
}
if(item->DamageShield > 0) {
if((newbon->DamageShield + item->DamageShield) > RuleI(Character, ItemDamageShieldCap))
newbon->DamageShield = RuleI(Character, ItemDamageShieldCap);

View File

@ -245,6 +245,8 @@ public:
virtual bool IsClient() const { return true; }
void CompleteConnect();
bool TryStacking(ItemInst* item, uint8 type = ItemPacketTrade, bool try_worn = true, bool try_cursor = true);
bool TryStacking(std::shared_ptr<EQEmu::ItemInstance> item, uint8 type = ItemPacketTrade, bool try_worn = true, bool try_cursor = true);
bool TryStacking(std::shared_ptr<EQEmu::ItemInstance> item, const EQEmu::InventorySlot &slot, uint8 type = ItemPacketTrade);
void SendTraderPacket(Client* trader, uint32 Unknown72 = 51);
void SendBuyerPacket(Client* Buyer);
GetItems_Struct* GetTraderItems();
@ -1284,6 +1286,7 @@ protected:
void CalcEdibleBonuses(StatBonuses* newbon);
void CalcAABonuses(StatBonuses* newbon);
void ApplyAABonuses(uint32 aaid, uint32 slots, StatBonuses* newbon);
void ProcessItemCaps();
void MakeBuffFadePacket(uint16 spell_id, int slot_id, bool send_message = true);
bool client_data_loaded;
@ -1337,6 +1340,7 @@ private:
int32 GetACMit();
int32 GetACAvoid();
int32 CalcATK();
int32 CalcItemATKCap();
int32 CalcHaste();
int32 CalcAlcoholPhysicalEffect();

View File

@ -2148,6 +2148,12 @@ int32 Client::CalcEnduranceRegenCap()
return (cap * RuleI(Character, EnduranceRegenMultiplier) / 100);
}
int32 Client::CalcItemATKCap()
{
int cap = RuleI(Character, ItemATKCap) + itembonuses.ItemATKCap + spellbonuses.ItemATKCap + aabonuses.ItemATKCap;
return cap;
}
int Client::GetRawACNoShield(int &shield_ac) const
{
int ac = itembonuses.AC + spellbonuses.AC + aabonuses.AC;

View File

@ -333,7 +333,13 @@ void MapOpcodes()
ConnectedOpcodes[OP_Save] = &Client::Handle_OP_Save;
ConnectedOpcodes[OP_SaveOnZoneReq] = &Client::Handle_OP_SaveOnZoneReq;
ConnectedOpcodes[OP_SelectTribute] = &Client::Handle_OP_SelectTribute;
ConnectedOpcodes[OP_SenseHeading] = &Client::Handle_OP_Ignore;
// Use or Ignore sense heading based on rule.
bool train=RuleB(Skills, TrainSenseHeading);
ConnectedOpcodes[OP_SenseHeading] =
(train) ? &Client::Handle_OP_SenseHeading : &Client::Handle_OP_Ignore;
ConnectedOpcodes[OP_SenseTraps] = &Client::Handle_OP_SenseTraps;
ConnectedOpcodes[OP_SetGuildMOTD] = &Client::Handle_OP_SetGuildMOTD;
ConnectedOpcodes[OP_SetRunMode] = &Client::Handle_OP_SetRunMode;
@ -1437,10 +1443,6 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
if (m_pp.ldon_points_tak < 0 || m_pp.ldon_points_tak > 2000000000){ m_pp.ldon_points_tak = 0; }
if (m_pp.ldon_points_available < 0 || m_pp.ldon_points_available > 2000000000){ m_pp.ldon_points_available = 0; }
/* Set Swimming Skill 100 by default if under 100 */
if (GetSkill(SkillSwimming) < 100)
SetSkill(SkillSwimming, 100);
/* Initialize AA's : Move to function eventually */
for (uint32 a = 0; a < MAX_PP_AA_ARRAY; a++){ aa[a] = &m_pp.aa_array[a]; }
query = StringFormat(
@ -5081,6 +5083,7 @@ void Client::Handle_OP_DeleteSpell(const EQApplicationPacket *app)
if (m_pp.spell_book[dss->spell_slot] != SPELLBOOK_UNKNOWN) {
m_pp.spell_book[dss->spell_slot] = SPELLBOOK_UNKNOWN;
database.DeleteCharacterSpell(this->CharacterID(), m_pp.spell_book[dss->spell_slot], dss->spell_slot);
dss->success = 1;
}
else
@ -11605,6 +11608,27 @@ void Client::Handle_OP_SelectTribute(const EQApplicationPacket *app)
return;
}
void Client::Handle_OP_SenseHeading(const EQApplicationPacket *app)
{
if (!HasSkill(SkillSenseHeading))
return;
int chancemod=0;
// The client seems to limit sense heading packets based on skill
// level. So if we're really low, we don't hit this routine very often.
// I think it's the GUI deciding when to skill you up.
// So, I'm adding a mod here which is larger at lower levels so
// very low levels get a much better chance to skill up when the GUI
// eventually sends a message.
if (GetLevel() <= 8)
chancemod += (9-level) * 10;
CheckIncreaseSkill(SkillSenseHeading, nullptr, chancemod);
return;
}
void Client::Handle_OP_SenseTraps(const EQApplicationPacket *app)
{
if (!HasSkill(SkillSenseTraps))
@ -11914,19 +11938,13 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
sizeof(Merchant_Sell_Struct), app->size);
return;
}
RDTSC_Timer t1;
t1.start();
Merchant_Sell_Struct* mp = (Merchant_Sell_Struct*)app->pBuffer;
#if EQDEBUG >= 5
Log.Out(Logs::General, Logs::None, "%s, purchase item..", GetName());
DumpPacket(app);
#endif
int merchantid;
bool tmpmer_used = false;
Mob* tmp = entity_list.GetMob(mp->npcid);
if (tmp == 0 || !tmp->IsNPC() || tmp->GetClass() != MERCHANT)
if (!tmp || !tmp->IsNPC() || tmp->GetClass() != MERCHANT)
return;
if (mp->quantity < 1) return;
@ -11986,7 +12004,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
safe_delete(delitempacket);
return;
}
if (CheckLoreConflict(item))
if (m_inventory.CheckLoreConflict(item))
{
Message(15, "You can only have one of a lore item.");
return;
@ -12017,7 +12035,7 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
else
charges = item->MaxCharges;
ItemInst* inst = database.CreateItemOld(item, charges);
auto inst = database.CreateItem(item->ID, charges);
int SinglePrice = 0;
if (RuleB(Merchant, UsePriceMod))
@ -12033,7 +12051,6 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
if (mpo->price < 0)
{
safe_delete(outapp);
safe_delete(inst);
return;
}
@ -12049,126 +12066,119 @@ void Client::Handle_OP_ShopPlayerBuy(const EQApplicationPacket *app)
database.SetMQDetectionFlag(AccountName(), GetName(), hacker_str, zone->GetShortName());
safe_delete_array(hacker_str);
safe_delete(outapp);
safe_delete(inst);
return;
}
bool stacked = TryStacking(inst);
if (!stacked)
freeslotid = m_inv.FindFreeSlot(false, true, item->Size);
// shouldn't we be reimbursing if these two fail?
//make sure we are not completely full...
if (freeslotid == MainCursor) {
if (m_inv.GetItem(MainCursor) != nullptr) {
Message(13, "You do not have room for any more items.");
safe_delete(outapp);
safe_delete(inst);
return;
}
}
if (!stacked && freeslotid == INVALID_INDEX)
{
Message(13, "You do not have room for any more items.");
safe_delete(outapp);
safe_delete(inst);
return;
}
std::string packet;
if (!stacked && inst) {
PutItemInInventory(freeslotid, *inst);
SendItemPacket(freeslotid, inst, ItemPacketTrade);
}
else if (!stacked){
Log.Out(Logs::General, Logs::Error, "OP_ShopPlayerBuy: item->ItemClass Unknown! Type: %i", item->ItemClass);
}
QueuePacket(outapp);
if (inst && tmpmer_used){
int32 new_charges = prevcharges - mp->quantity;
zone->SaveTempItem(merchantid, tmp->GetNPCTypeID(), item_id, new_charges);
if (new_charges <= 0){
EQApplicationPacket* delitempacket = new EQApplicationPacket(OP_ShopDelItem, sizeof(Merchant_DelItem_Struct));
Merchant_DelItem_Struct* delitem = (Merchant_DelItem_Struct*)delitempacket->pBuffer;
delitem->itemslot = mp->itemslot;
delitem->npcid = mp->npcid;
delitem->playerid = mp->playerid;
delitempacket->priority = 6;
entity_list.QueueClients(tmp, delitempacket); //que for anyone that could be using the merchant so they see the update
safe_delete(delitempacket);
}
else {
// Update the charges/quantity in the merchant window
inst->SetCharges(new_charges);
inst->SetPrice(SinglePrice);
inst->SetMerchantSlot(mp->itemslot);
inst->SetMerchantCount(new_charges);
SendItemPacket(mp->itemslot, inst, ItemPacketMerchant);
}
}
safe_delete(inst);
safe_delete(outapp);
// start QS code
// stacking purchases not supported at this time - entire process will need some work to catch them properly
if (RuleB(QueryServ, PlayerLogMerchantTransactions)) {
ServerPacket* qspack = new ServerPacket(ServerOP_QSPlayerLogMerchantTransactions, sizeof(QSMerchantLogTransaction_Struct)+sizeof(QSTransactionItems_Struct));
QSMerchantLogTransaction_Struct* qsaudit = (QSMerchantLogTransaction_Struct*)qspack->pBuffer;
qsaudit->zone_id = zone->GetZoneID();
qsaudit->merchant_id = tmp->CastToNPC()->MerchantType;
qsaudit->merchant_money.platinum = 0;
qsaudit->merchant_money.gold = 0;
qsaudit->merchant_money.silver = 0;
qsaudit->merchant_money.copper = 0;
qsaudit->merchant_count = 1;
qsaudit->char_id = character_id;
qsaudit->char_money.platinum = (mpo->price / 1000);
qsaudit->char_money.gold = (mpo->price / 100) % 10;
qsaudit->char_money.silver = (mpo->price / 10) % 10;
qsaudit->char_money.copper = mpo->price % 10;
qsaudit->char_count = 0;
qsaudit->items[0].char_slot = freeslotid == INVALID_INDEX ? 0 : freeslotid;
qsaudit->items[0].item_id = item->ID;
qsaudit->items[0].charges = mpo->quantity;
if (freeslotid == INVALID_INDEX) {
qsaudit->items[0].aug_1 = 0;
qsaudit->items[0].aug_2 = 0;
qsaudit->items[0].aug_3 = 0;
qsaudit->items[0].aug_4 = 0;
qsaudit->items[0].aug_5 = 0;
}
else {
qsaudit->items[0].aug_1 = m_inv[freeslotid]->GetAugmentItemID(0);
qsaudit->items[0].aug_2 = m_inv[freeslotid]->GetAugmentItemID(1);
qsaudit->items[0].aug_3 = m_inv[freeslotid]->GetAugmentItemID(2);
qsaudit->items[0].aug_4 = m_inv[freeslotid]->GetAugmentItemID(3);
qsaudit->items[0].aug_5 = m_inv[freeslotid]->GetAugmentItemID(4);
}
qspack->Deflate();
if (worldserver.Connected()) { worldserver.SendPacket(qspack); }
safe_delete(qspack);
}
// end QS code
if (RuleB(EventLog, RecordBuyFromMerchant))
LogMerchant(this, tmp, mpo->quantity, mpo->price, item, true);
if ((RuleB(Character, EnableDiscoveredItems)))
{
if (!GetGM() && !IsDiscovered(item_id))
DiscoverItem(item_id);
}
t1.stop();
std::cout << "At 1: " << t1.getDuration() << std::endl;
return;
//if (!stacked)
// freeslotid = m_inv.FindFreeSlot(false, true, item->Size);
//
//// shouldn't we be reimbursing if these two fail?
//
////make sure we are not completely full...
//if (freeslotid == MainCursor) {
// if (m_inv.GetItem(MainCursor) != nullptr) {
// Message(13, "You do not have room for any more items.");
// safe_delete(outapp);
// return;
// }
//}
//
//if (!stacked && freeslotid == INVALID_INDEX)
//{
// Message(13, "You do not have room for any more items.");
// safe_delete(outapp);
// return;
//}
//
//std::string packet;
//if (!stacked && inst) {
// PutItemInInventory(freeslotid, *inst);
// SendItemPacket(freeslotid, inst, ItemPacketTrade);
//}
//else if (!stacked){
// Log.Out(Logs::General, Logs::Error, "OP_ShopPlayerBuy: item->ItemClass Unknown! Type: %i", item->ItemClass);
//}
//QueuePacket(outapp);
//if (inst && tmpmer_used){
// int32 new_charges = prevcharges - mp->quantity;
// zone->SaveTempItem(merchantid, tmp->GetNPCTypeID(), item_id, new_charges);
// if (new_charges <= 0){
// EQApplicationPacket* delitempacket = new EQApplicationPacket(OP_ShopDelItem, sizeof(Merchant_DelItem_Struct));
// Merchant_DelItem_Struct* delitem = (Merchant_DelItem_Struct*)delitempacket->pBuffer;
// delitem->itemslot = mp->itemslot;
// delitem->npcid = mp->npcid;
// delitem->playerid = mp->playerid;
// delitempacket->priority = 6;
// entity_list.QueueClients(tmp, delitempacket); //que for anyone that could be using the merchant so they see the update
// safe_delete(delitempacket);
// }
// else {
// // Update the charges/quantity in the merchant window
// inst->SetCharges(new_charges);
// inst->SetPrice(SinglePrice);
// inst->SetMerchantSlot(mp->itemslot);
// inst->SetMerchantCount(new_charges);
//
// SendItemPacket(mp->itemslot, inst, ItemPacketMerchant);
// }
//}
//safe_delete(inst);
//safe_delete(outapp);
//
//// start QS code
//// stacking purchases not supported at this time - entire process will need some work to catch them properly
//if (RuleB(QueryServ, PlayerLogMerchantTransactions)) {
// ServerPacket* qspack = new ServerPacket(ServerOP_QSPlayerLogMerchantTransactions, sizeof(QSMerchantLogTransaction_Struct)+sizeof(QSTransactionItems_Struct));
// QSMerchantLogTransaction_Struct* qsaudit = (QSMerchantLogTransaction_Struct*)qspack->pBuffer;
//
// qsaudit->zone_id = zone->GetZoneID();
// qsaudit->merchant_id = tmp->CastToNPC()->MerchantType;
// qsaudit->merchant_money.platinum = 0;
// qsaudit->merchant_money.gold = 0;
// qsaudit->merchant_money.silver = 0;
// qsaudit->merchant_money.copper = 0;
// qsaudit->merchant_count = 1;
// qsaudit->char_id = character_id;
// qsaudit->char_money.platinum = (mpo->price / 1000);
// qsaudit->char_money.gold = (mpo->price / 100) % 10;
// qsaudit->char_money.silver = (mpo->price / 10) % 10;
// qsaudit->char_money.copper = mpo->price % 10;
// qsaudit->char_count = 0;
//
// qsaudit->items[0].char_slot = freeslotid == INVALID_INDEX ? 0 : freeslotid;
// qsaudit->items[0].item_id = item->ID;
// qsaudit->items[0].charges = mpo->quantity;
//
// if (freeslotid == INVALID_INDEX) {
// qsaudit->items[0].aug_1 = 0;
// qsaudit->items[0].aug_2 = 0;
// qsaudit->items[0].aug_3 = 0;
// qsaudit->items[0].aug_4 = 0;
// qsaudit->items[0].aug_5 = 0;
// }
// else {
// qsaudit->items[0].aug_1 = m_inv[freeslotid]->GetAugmentItemID(0);
// qsaudit->items[0].aug_2 = m_inv[freeslotid]->GetAugmentItemID(1);
// qsaudit->items[0].aug_3 = m_inv[freeslotid]->GetAugmentItemID(2);
// qsaudit->items[0].aug_4 = m_inv[freeslotid]->GetAugmentItemID(3);
// qsaudit->items[0].aug_5 = m_inv[freeslotid]->GetAugmentItemID(4);
// }
//
// qspack->Deflate();
// if (worldserver.Connected()) { worldserver.SendPacket(qspack); }
// safe_delete(qspack);
//}
//// end QS code
//
//if (RuleB(EventLog, RecordBuyFromMerchant))
// LogMerchant(this, tmp, mpo->quantity, mpo->price, item, true);
//
//if ((RuleB(Character, EnableDiscoveredItems)))
//{
// if (!GetGM() && !IsDiscovered(item_id))
// DiscoverItem(item_id);
//}
}
void Client::Handle_OP_ShopPlayerSell(const EQApplicationPacket *app)
{

View File

@ -28,7 +28,7 @@
set to nullptr and 0 respectively since they aren't used when adding
an alias. The function pointers being equal is makes it an alias.
The access level you set with command_add is only a default if
the command isn't listed in the addon.ini file.
the command isn't listed in the commands db table.
*/

View File

@ -1217,9 +1217,9 @@ void Corpse::LootItem(Client* client, const EQApplicationPacket* app) {
linker.SetLinkType(linker.linkItemInst);
linker.SetItemInst(inst);
auto item_link = linker.GenerateLink();
auto item_link = linker.GenerateLink();
client->Message_StringID(MT_LootMessages, LOOTED_MESSAGE, item_link.c_str());
client->Message_StringID(MT_LootMessages, LOOTED_MESSAGE, item_link.c_str());
if (!IsPlayerCorpse()) {
Group *g = client->GetGroup();

View File

@ -2737,7 +2737,7 @@ void EntityList::WriteEntityIDs()
BulkZoneSpawnPacket::BulkZoneSpawnPacket(Client *iSendTo, uint32 iMaxSpawnsPerPacket)
{
data = 0;
data = nullptr;
pSendTo = iSendTo;
pMaxSpawnsPerPacket = iMaxSpawnsPerPacket;
}
@ -2745,7 +2745,7 @@ BulkZoneSpawnPacket::BulkZoneSpawnPacket(Client *iSendTo, uint32 iMaxSpawnsPerPa
BulkZoneSpawnPacket::~BulkZoneSpawnPacket()
{
SendBuffer();
safe_delete_array(data)
safe_delete_array(data);
}
bool BulkZoneSpawnPacket::AddSpawn(NewSpawn_Struct *ns)
@ -2764,7 +2764,8 @@ bool BulkZoneSpawnPacket::AddSpawn(NewSpawn_Struct *ns)
return false;
}
void BulkZoneSpawnPacket::SendBuffer() {
void BulkZoneSpawnPacket::SendBuffer()
{
if (!data)
return;

View File

@ -882,34 +882,71 @@ bool Client::PutItemInInventory(int16 slot_id, const ItemInst& inst, bool client
void Client::PutLootInInventory(int16 slot_id, const ItemInst &inst, ServerLootItem_Struct** bag_item_data)
{
Log.Out(Logs::Detail, Logs::Inventory, "Putting loot item %s (%d) into slot %d", inst.GetItem()->Name, inst.GetItem()->ID, slot_id);
m_inv.PutItem(slot_id, inst);
SendLootItemInPacket(&inst, slot_id);
bool cursor_empty = m_inv.CursorEmpty();
if (slot_id == MainCursor) {
m_inv.PushCursor(inst);
auto s = m_inv.cursor_cbegin(), e = m_inv.cursor_cend();
database.SaveCursor(this->CharacterID(), s, e);
}
else {
m_inv.PutItem(slot_id, inst);
database.SaveInventory(this->CharacterID(), &inst, slot_id);
}
if(bag_item_data) { // bag contents
int16 interior_slot;
// our bag went into slot_id, now let's pack the contents in
for(int i = SUB_BEGIN; i < EmuConstants::ITEM_CONTAINER_SIZE; i++) {
if(bag_item_data[i] == nullptr)
// Subordinate items in cursor buffer must be sent via ItemPacketSummonItem or we just overwrite the visible cursor and desync the client
if (slot_id == MainCursor && !cursor_empty) {
// RoF+ currently has a specialized cursor handler
if (GetClientVersion() < ClientVersion::RoF)
SendItemPacket(slot_id, &inst, ItemPacketSummonItem);
}
else {
SendLootItemInPacket(&inst, slot_id);
}
if (bag_item_data) {
for (int index = 0; index < EmuConstants::ITEM_CONTAINER_SIZE; ++index) {
if (bag_item_data[index] == nullptr)
continue;
const ItemInst *bagitem = database.CreateItemOld(bag_item_data[i]->item_id, bag_item_data[i]->charges, bag_item_data[i]->aug_1, bag_item_data[i]->aug_2, bag_item_data[i]->aug_3, bag_item_data[i]->aug_4, bag_item_data[i]->aug_5, bag_item_data[i]->aug_6, bag_item_data[i]->attuned);
interior_slot = InventoryOld::CalcSlotId(slot_id, i);
Log.Out(Logs::Detail, Logs::Inventory, "Putting bag loot item %s (%d) into slot %d (bag slot %d)", inst.GetItem()->Name, inst.GetItem()->ID, interior_slot, i);
PutLootInInventory(interior_slot, *bagitem);
const ItemInst *bagitem = database.CreateItemOld(
bag_item_data[index]->item_id,
bag_item_data[index]->charges,
bag_item_data[index]->aug_1,
bag_item_data[index]->aug_2,
bag_item_data[index]->aug_3,
bag_item_data[index]->aug_4,
bag_item_data[index]->aug_5,
bag_item_data[index]->aug_6,
bag_item_data[index]->attuned
);
// Dump bag contents to cursor in the event that owning bag is not the first cursor item
// (This assumes that the data passed is correctly associated..no safety checks are implemented)
if (slot_id == MainCursor && !cursor_empty) {
Log.Out(Logs::Detail, Logs::Inventory,
"Putting bag loot item %s (%d) into slot %d (non-empty cursor override)",
inst.GetItem()->Name, inst.GetItem()->ID, MainCursor);
PutLootInInventory(MainCursor, *bagitem);
}
else {
auto bag_slot = InventoryOld::CalcSlotId(slot_id, index);
Log.Out(Logs::Detail, Logs::Inventory,
"Putting bag loot item %s (%d) into slot %d (bag slot %d)",
inst.GetItem()->Name, inst.GetItem()->ID, bag_slot, index);
PutLootInInventory(bag_slot, *bagitem);
}
safe_delete(bagitem);
}
}
CalcBonuses();
}
bool Client::TryStacking(ItemInst* item, uint8 type, bool try_worn, bool try_cursor){
if(!item || !item->IsStackable() || item->GetCharges()>=item->GetItem()->StackSize)
return false;
@ -944,6 +981,47 @@ bool Client::TryStacking(ItemInst* item, uint8 type, bool try_worn, bool try_cur
return false;
}
bool Client::TryStacking(std::shared_ptr<EQEmu::ItemInstance> item, uint8 type, bool try_worn, bool try_cursor) {
if(!item || !item->IsStackable() || item->GetCharges() >= item->GetItem()->StackSize)
return false;
if(try_worn) {
for(int i = EQEmu::PersonalSlotCharm; i <= EQEmu::PersonalSlotAmmo; ++i) {
if(TryStacking(item, EQEmu::InventorySlot(EQEmu::InvTypePersonal, i), type)) {
return true;
}
}
}
for(int i = EQEmu::PersonalSlotGeneral1; i <= EQEmu::PersonalSlotGeneral10; ++i) {
if(TryStacking(item, EQEmu::InventorySlot(EQEmu::InvTypePersonal, i), type)) {
return true;
}
}
if(try_cursor) {
if(TryStacking(item, EQEmu::InventorySlot(EQEmu::InvTypePersonal, EQEmu::PersonalSlotCursor), type)) {
return true;
}
}
return false;
}
bool Client::TryStacking(std::shared_ptr<EQEmu::ItemInstance> item, const EQEmu::InventorySlot &slot, uint8 type) {
//make this a function
uint32 item_id = item->GetItem()->ID;
auto tmp_inst = m_inventory.Get(slot);
if(tmp_inst && tmp_inst->GetItem()->ID == item_id && tmp_inst->GetCharges() < tmp_inst->GetItem()->StackSize) {
bool v = m_inventory.TryStacking(item, slot);
if(v) {
SendItemPacket(slot, item, ItemPacketTrade);
}
}
return item->GetCharges() == 0;
}
// Locate an available space in inventory to place an item
// and then put the item there
// The change will be saved to the database

View File

@ -2409,6 +2409,78 @@ XS(XS_NPC_AddDefensiveProc) {
XSRETURN_EMPTY;
}
XS(XS_NPC_RemoveMeleeProc);
XS(XS_NPC_RemoveMeleeProc) {
dXSARGS;
if (items != 2)
Perl_croak(aTHX_ "Usage: NPC::RemoveMeleeProc(THIS,spellid)");
{
NPC * THIS;
int spell_id = (int)SvIV(ST(1));
dXSTARG;
if (sv_derived_from(ST(0), "NPC")) {
IV tmp = SvIV((SV*)SvRV(ST(0)));
THIS = INT2PTR(NPC *,tmp);
}
else
Perl_croak(aTHX_ "THIS is not of type NPC");
if(THIS == NULL)
Perl_croak(aTHX_ "THIS is NULL, avoiding crash.");
THIS->RemoveProcFromWeapon(spell_id, false);
}
XSRETURN_EMPTY;
}
XS(XS_NPC_RemoveRangedProc);
XS(XS_NPC_RemoveRangedProc) {
dXSARGS;
if (items != 2)
Perl_croak(aTHX_ "Usage: NPC::RemoveRangedProc(THIS,spellid)");
{
NPC * THIS;
int spell_id = (int)SvIV(ST(1));
dXSTARG;
if (sv_derived_from(ST(0), "NPC")) {
IV tmp = SvIV((SV*)SvRV(ST(0)));
THIS = INT2PTR(NPC *,tmp);
}
else
Perl_croak(aTHX_ "THIS is not of type NPC");
if(THIS == NULL)
Perl_croak(aTHX_ "THIS is NULL, avoiding crash.");
THIS->RemoveRangedProc(spell_id, false);
}
XSRETURN_EMPTY;
}
XS(XS_NPC_RemoveDefensiveProc);
XS(XS_NPC_RemoveDefensiveProc) {
dXSARGS;
if (items != 2)
Perl_croak(aTHX_ "Usage: NPC::RemoveDefensiveProc(THIS,spellid)");
{
NPC * THIS;
int spell_id = (int)SvIV(ST(1));
dXSTARG;
if (sv_derived_from(ST(0), "NPC")) {
IV tmp = SvIV((SV*)SvRV(ST(0)));
THIS = INT2PTR(NPC *,tmp);
}
else
Perl_croak(aTHX_ "THIS is not of type NPC");
if(THIS == NULL)
Perl_croak(aTHX_ "THIS is NULL, avoiding crash.");
THIS->RemoveDefensiveProc(spell_id, false);
}
XSRETURN_EMPTY;
}
XS(XS_NPC_ChangeLastName); /* prototype to pass -Wmissing-prototypes */
XS(XS_NPC_ChangeLastName)
{
@ -2566,6 +2638,9 @@ XS(boot_NPC)
newXSproto(strcpy(buf, "AddMeleeProc"), XS_NPC_AddMeleeProc, file, "$$$");
newXSproto(strcpy(buf, "AddRangedProc"), XS_NPC_AddRangedProc, file, "$$$");
newXSproto(strcpy(buf, "AddDefensiveProc"), XS_NPC_AddDefensiveProc, file, "$$$");
newXSproto(strcpy(buf, "RemoveMeleeProc"), XS_NPC_RemoveMeleeProc, file, "$$");
newXSproto(strcpy(buf, "RemoveRangedProc"), XS_NPC_RemoveRangedProc, file, "$$");
newXSproto(strcpy(buf, "RemoveDefensiveProc"), XS_NPC_RemoveDefensiveProc, file, "$$");
newXSproto(strcpy(buf, "ChangeLastName"), XS_NPC_ChangeLastName, file, "$:$");
newXSproto(strcpy(buf, "ClearLastName"), XS_NPC_ClearLastName, file, "$");
XSRETURN_YES;