diff --git a/changelog.txt b/changelog.txt index 8fbdecd1f..09cd52d88 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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 == diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index a88403139..6f1d52505 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -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 diff --git a/common/inventory.cpp b/common/inventory.cpp index a0a14f18b..b3fc99a0f 100644 --- a/common/inventory.cpp +++ b/common/inventory.cpp @@ -366,6 +366,43 @@ bool EQEmu::Inventory::Swap(const InventorySlot &src, const InventorySlot &dest, return true; } +bool EQEmu::Inventory::TryStacking(std::shared_ptr 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 inst) { if(!inst) return false; diff --git a/common/inventory.h b/common/inventory.h index 48b1614bf..a85e647bb 100644 --- a/common/inventory.h +++ b/common/inventory.h @@ -135,9 +135,12 @@ namespace EQEmu std::shared_ptr Get(const InventorySlot &slot); bool Put(const InventorySlot &slot, std::shared_ptr inst); bool Swap(const InventorySlot &src, const InventorySlot &dest, int charges); + bool TryStacking(std::shared_ptr inst, const InventorySlot &slot); bool Summon(const InventorySlot &slot, std::shared_ptr inst); bool PushToCursorBuffer(std::shared_ptr inst); bool PopFromCursorBuffer(); + bool PutStackInInventory(std::shared_ptr inst, bool try_worn, bool try_cursor); + InventorySlot PutItemInInventory(std::shared_ptr inst, bool try_worn, bool try_cursor); //utility static int CalcMaterialFromSlot(const InventorySlot &slot); diff --git a/common/patches/rof.cpp b/common/patches/rof.cpp index 7f4e2e120..50f4eb6de 100644 --- a/common/patches/rof.cpp +++ b/common/patches/rof.cpp @@ -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(); } diff --git a/common/ruletypes.h b/common/ruletypes.h index 12a8a0737..7894c5ebd 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -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 ) diff --git a/common/shareddb.cpp b/common/shareddb.cpp index 3b5170d75..ea3f1a55a 100644 --- a/common/shareddb.cpp +++ b/common/shareddb.cpp @@ -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)); } diff --git a/world/client.cpp b/world/client.cpp index 19e949b42..7878c94fb 100644 --- a/world/client.cpp +++ b/world/client.cpp @@ -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()); diff --git a/zone/bonuses.cpp b/zone/bonuses.cpp index 389323cdc..f650e11ce 100644 --- a/zone/bonuses.cpp +++ b/zone/bonuses.cpp @@ -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); diff --git a/zone/client.h b/zone/client.h index 860ac1dc7..a7e0cf296 100644 --- a/zone/client.h +++ b/zone/client.h @@ -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 item, uint8 type = ItemPacketTrade, bool try_worn = true, bool try_cursor = true); + bool TryStacking(std::shared_ptr 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(); diff --git a/zone/client_mods.cpp b/zone/client_mods.cpp index 688b792f4..42f8b474b 100644 --- a/zone/client_mods.cpp +++ b/zone/client_mods.cpp @@ -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; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index d36207c9e..c68b9984b 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -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) { diff --git a/zone/command.cpp b/zone/command.cpp index 5aabf6563..22600ede1 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -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. */ diff --git a/zone/corpse.cpp b/zone/corpse.cpp index f407b5fbd..b77d4add9 100644 --- a/zone/corpse.cpp +++ b/zone/corpse.cpp @@ -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(); diff --git a/zone/entity.cpp b/zone/entity.cpp index b53469fbc..8fd1cb14e 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -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; diff --git a/zone/inventory.cpp b/zone/inventory.cpp index de89acd94..e3db7047b 100644 --- a/zone/inventory.cpp +++ b/zone/inventory.cpp @@ -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 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 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 diff --git a/zone/perl_npc.cpp b/zone/perl_npc.cpp index a2943d035..febee6941 100644 --- a/zone/perl_npc.cpp +++ b/zone/perl_npc.cpp @@ -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;