#ifdef _WINDOWS #if (!defined(_MSC_VER) || (defined(_MSC_VER) && _MSC_VER < 1900)) #define snprintf _snprintf #define vsnprintf _vsnprintf #endif #define strncasecmp _strnicmp #define strcasecmp _stricmp #endif #include "../common/data_verification.h" #include "../common/global_define.h" #include "../common/eqemu_logsys.h" #include "../common/rulesys.h" #include "../common/strings.h" #include "../common/say_link.h" #include "corpse.h" #include "entity.h" #include "expedition.h" #include "groups.h" #include "mob.h" #include "raids.h" #include "bot.h" #include "quest_parser_collection.h" #include "string_ids.h" #include "worldserver.h" #include "../common/events/player_event_logs.h" #include "../common/repositories/character_corpses_repository.h" #include "../common/repositories/character_corpse_items_repository.h" #include extern EntityList entity_list; extern Zone *zone; extern WorldServer worldserver; extern npcDecayTimes_Struct npcCorpseDecayTimes[100]; void Corpse::SendEndLootErrorPacket(Client *client) { auto outapp = new EQApplicationPacket(OP_LootComplete, 0); client->QueuePacket(outapp); safe_delete(outapp); } void Corpse::SendLootReqErrorPacket(Client *client, LootResponse response) { auto outapp = new EQApplicationPacket(OP_MoneyOnCorpse, sizeof(moneyOnCorpseStruct)); auto *d = (moneyOnCorpseStruct *) outapp->pBuffer; d->response = static_cast(response); d->unknown1 = 0x5a; d->unknown2 = 0x40; client->QueuePacket(outapp); safe_delete(outapp); } Corpse::Corpse( NPC *npc, LootItems *item_list, uint32 npc_type_id, const NPCType **npc_type_data, uint32 decay_time ) : Mob( "Unnamed_Corpse", // in_name "", // in_lastname 0, // in_cur_hp 0, // in_max_hp npc->GetGender(), // in_gender npc->GetRace(), // in_race npc->GetClass(), // in_class BodyType::Humanoid, // in_bodytype npc->GetDeity(), // in_deity npc->GetLevel(), // in_level npc->GetNPCTypeID(), // in_npctype_id npc->GetSize(), // in_size 0.0f, // in_runspeed npc->GetPosition(), // position npc->GetInnateLightType(), // in_light npc->GetTexture(), // in_texture npc->GetHelmTexture(), // in_helmtexture 0, // in_ac 0, // in_atk 0, // in_str 0, // in_sta 0, // in_dex 0, // in_agi 0, // in_int 0, // in_wis 0, // in_cha 0, // in_haircolor 0, // in_beardcolor 0, // in_eyecolor1 0, // in_eyecolor2 0, // in_hairstyle 0, // in_luclinface 0, // in_beard 0, // in_drakkin_heritage 0, // in_drakkin_tattoo 0, // in_drakkin_details EQ::TintProfile(), // in_armor_tint 0xFF, // in_aa_title 0, // in_see_invis 0, // in_see_invis_undead 0, // in_see_hide 0, // in_see_improved_hide 0, // in_hp_regen 0, // in_mana_regen 0, // in_qglobal 0, // in_maxlevel 0, // in_scalerate 0, // in_armtexture 0, // in_bracertexture 0, // in_handtexture 0, // in_legtexture 0, // in_feettexture (*npc_type_data)->use_model, // in_usemodel false, // in_always_aggros_foes 0, // in_heroic_strikethrough false // in_keeps_sold_items ), m_corpse_decay_timer(decay_time), m_corpse_rezzable_timer(0), m_corpse_delay_timer(RuleI(NPC, CorpseUnlockTimer)), m_corpse_graveyard_timer(0), m_loot_cooldown_timer(10) { m_corpse_graveyard_timer.Disable(); m_is_corpse_changed = false; m_is_player_corpse = false; m_is_locked = false; m_being_looted_by_entity_id = 0xFFFFFFFF; if (item_list) { m_item_list = *item_list; item_list->clear(); } SetCash(npc->GetCopper(), npc->GetSilver(), npc->GetGold(), npc->GetPlatinum()); npctype_id = npc_type_id; SetPlayerKillItemID(0); m_character_id = 0; m_corpse_db_id = 0; m_player_corpse_depop = false; strcpy(corpse_name, npc->GetName()); strcpy(name, npc->GetName()); for (auto &npcCorpseDecayTime: npcCorpseDecayTimes) { if ( EQ::ValueWithin( level, npcCorpseDecayTime.minlvl, npcCorpseDecayTime.maxlvl ) ) { m_corpse_decay_timer.SetTimer(npcCorpseDecayTime.seconds * 1000); break; } } if (IsEmpty()) { m_corpse_decay_timer.SetTimer(RuleI(NPC, EmptyNPCCorpseDecayTime) + 1000); } if (npc->HasPrivateCorpse()) { m_corpse_delay_timer.SetTimer(m_corpse_decay_timer.GetRemainingTime() + 1000); } for (int &allowed_looter: m_allowed_looters) { allowed_looter = 0; } m_rezzed_experience = 0; UpdateEquipmentLight(); UpdateActiveLight(); m_loot_request_type = LootRequestType::Forbidden; } Corpse::Corpse(Client *c, int32 rez_exp, KilledByTypes in_killed_by) : Mob( "Unnamed_Corpse", // in_name "", // in_lastname 0, // in_cur_hp 0, // in_max_hp c->GetGender(), // in_gender c->GetRace(), // in_race c->GetClass(), // in_class BodyType::Humanoid, // in_bodytype c->GetDeity(), // in_deity c->GetLevel(), // in_level 0, // in_npctype_id c->GetSize(), // in_size 0, // in_runspeed c->GetPosition(), // position c->GetInnateLightType(), // in_light c->GetTexture(), // in_texture c->GetHelmTexture(), // in_helmtexture 0, // in_ac 0, // in_atk 0, // in_str 0, // in_sta 0, // in_dex 0, // in_agi 0, // in_int 0, // in_wis 0, // in_cha c->GetPP().haircolor, // in_haircolor c->GetPP().beardcolor, // in_beardcolor c->GetPP().eyecolor1, // in_eyecolor1 c->GetPP().eyecolor2, // in_eyecolor2 c->GetPP().hairstyle, // in_hairstyle c->GetPP().face, // in_luclinface c->GetPP().beard, // in_beard c->GetPP().drakkin_heritage, // in_drakkin_heritage c->GetPP().drakkin_tattoo, // in_drakkin_tattoo c->GetPP().drakkin_details, // in_drakkin_details EQ::TintProfile(), // in_armor_tint 0xff, // in_aa_title 0, // in_see_invis 0, // in_see_invis_undead 0, // in_see_hide 0, // in_see_improved_hide 0, // in_hp_regen 0, // in_mana_regen 0, // in_qglobal 0, // in_maxlevel 0, // in_scalerate 0, // in_armtexture 0, // in_bracertexture 0, // in_handtexture 0, // in_legtexture 0, // in_feettexture 0, // in_usemodel false, // in_always_aggro 0, // in_heroic_strikethrough false // in_keeps_sold_items ) { PlayerProfile_Struct *pp = &c->GetPP(); EQ::ItemInstance *item = nullptr; if (!zone->HasGraveyard()) { m_corpse_graveyard_timer.Disable(); } for (int &allowed_looter: m_allowed_looters) { allowed_looter = 0; } if (c->AutoConsentGroupEnabled()) { auto *g = c->GetGroup(); m_consented_group_id = g ? g->GetID() : 0; } if (c->AutoConsentRaidEnabled()) { auto *r = c->GetRaid(); m_consented_raid_id = r ? r->GetID() : 0; } m_consented_guild_id = c->AutoConsentGuildEnabled() ? c->GuildID() : 0; m_is_corpse_changed = true; m_rezzed_experience = rez_exp; m_is_player_corpse = true; m_is_locked = false; m_being_looted_by_entity_id = 0xFFFFFFFF; m_character_id = c->CharacterID(); m_corpse_db_id = 0; m_player_corpse_depop = false; m_copper = 0; m_silver = 0; m_gold = 0; m_platinum = 0; m_killed_by_type = (uint8) in_killed_by; m_is_rezzable = true; m_remaining_rez_time = 0; m_is_owner_online = false; m_account_id = c->AccountID(); // timers m_corpse_decay_timer.SetTimer(RuleI(Character, CorpseDecayTime)); m_corpse_rezzable_timer.SetTimer(RuleI(Character, CorpseResTime)); m_corpse_delay_timer.SetTimer(RuleI(NPC, CorpseUnlockTimer)); m_corpse_graveyard_timer.SetTimer(RuleI(Zone, GraveyardTimeMS)); m_loot_cooldown_timer.SetTimer(10); m_check_rezzable_timer.SetTimer(1000); m_check_owner_online_timer.SetTimer(RuleI(Character, CorpseOwnerOnlineTime)); m_corpse_rezzable_timer.Disable(); SetRezTimer(true); strcpy(corpse_name, pp->name); strcpy(name, pp->name); /* become_npc was not being initialized which led to some pretty funky things with newly created corpses */ m_become_npc = false; SetPlayerKillItemID(0); /* Check Rule to see if we can leave corpses */ if ( (!RuleB(Character, LeaveNakedCorpses) || RuleB(Character, LeaveCorpses)) && GetLevel() >= RuleI(Character, DeathItemLossLevel) ) { // cash // Let's not move the cash when 'RespawnFromHover = true' && 'client->GetClientVersion() < EQClientSoF' since the client doesn't. // (change to first client that supports 'death hover' mode, if not SoF.) if (!RuleB(Character, RespawnFromHover) || c->ClientVersion() < EQ::versions::ClientVersion::SoF) { auto corpse_copper = pp->copper; auto corpse_silver = pp->silver; auto corpse_gold = pp->gold; auto corpse_platinum = pp->platinum; pp->copper = 0; pp->silver = 0; pp->gold = 0; pp->platinum = 0; if (RuleB(Character, LeaveCursorMoneyOnCorpse)) { corpse_copper += pp->copper_cursor; corpse_silver += pp->silver_cursor; corpse_gold += pp->gold_cursor; corpse_platinum += pp->platinum_cursor; pp->copper_cursor = 0; pp->silver_cursor = 0; pp->gold_cursor = 0; pp->platinum_cursor = 0; } SetCash(corpse_copper, corpse_silver, corpse_gold, corpse_platinum); } // get their tints memcpy(&m_item_tint.Slot, &c->GetPP().item_tint, sizeof(m_item_tint)); // TODO soulbound items need not be added to corpse, but they need // to go into the regular slots on the player, out of bags std::list removed_list; // ideally, we would start at invslot::slotGeneral1 and progress to invslot::slotCursor.. // ..then regress and process invslot::EQUIPMENT_BEGIN through invslot::EQUIPMENT_END... // without additional work to database loading of player corpses, this order is not // currently preserved and a re-work of this processing loop is not warranted. for (int i = EQ::invslot::POSSESSIONS_BEGIN; i <= EQ::invslot::POSSESSIONS_END; ++i) { item = c->GetInv().GetItem(i); if (!item) { continue; } if (!c->IsBecomeNPC() || (c->IsBecomeNPC() && !item->GetItem()->NoRent)) { MoveItemToCorpse(c, item, i, removed_list); } } database.TransactionBegin(); // this should not be modified to include the entire range of invtype::TYPE_POSSESSIONS slots by default.. // ..due to the possibility of 'hidden' items from client version bias..or, possibly, soul-bound items (WoW?) if (!removed_list.empty()) { auto iter = removed_list.begin(); if (iter != removed_list.end()) { std::stringstream ss(""); ss << "DELETE FROM `inventory` WHERE `charid` = " << c->CharacterID(); ss << " AND `slotid` IN (" << (*iter); ++iter; while (iter != removed_list.end()) { ss << ", " << (*iter); ++iter; } ss << ")"; database.QueryDatabase(ss.str().c_str()); } } auto start = c->GetInv().cursor_cbegin(); auto finish = c->GetInv().cursor_cend(); database.SaveCursor(c->CharacterID(), start, finish); c->CalcBonuses(); c->Save(); IsRezzed(false); Save(); database.TransactionCommit(); UpdateEquipmentLight(); UpdateActiveLight(); return; } UpdateEquipmentLight(); UpdateActiveLight(); m_loot_request_type = LootRequestType::Forbidden; IsRezzed(false); Save(); } void Corpse::MoveItemToCorpse(Client *client, EQ::ItemInstance *inst, int16 equipSlot, std::list &removedList) { AddItem( inst->GetItem()->ID, inst->GetCharges(), equipSlot, inst->GetAugmentItemID(0), inst->GetAugmentItemID(1), inst->GetAugmentItemID(2), inst->GetAugmentItemID(3), inst->GetAugmentItemID(4), inst->GetAugmentItemID(5), inst->IsAttuned(), inst->GetCustomDataString(), inst->GetOrnamentationIcon(), inst->GetOrnamentationIDFile(), inst->GetOrnamentHeroModel() ); removedList.push_back(equipSlot); while (true) { if (!inst->IsClassBag()) { break; } if (equipSlot < EQ::invslot::GENERAL_BEGIN || equipSlot > EQ::invslot::slotCursor) { break; } for (int16 sub_index = EQ::invbag::SLOT_BEGIN; sub_index <= EQ::invbag::SLOT_END; ++sub_index) { int16 real_bag_slot = EQ::InventoryProfile::CalcSlotId(equipSlot, sub_index); auto bag_inst = client->GetInv().GetItem(real_bag_slot); if (bag_inst == nullptr) { continue; } AddItem( bag_inst->GetItem()->ID, bag_inst->GetCharges(), real_bag_slot, bag_inst->GetAugmentItemID(0), bag_inst->GetAugmentItemID(1), bag_inst->GetAugmentItemID(2), bag_inst->GetAugmentItemID(3), bag_inst->GetAugmentItemID(4), bag_inst->GetAugmentItemID(5), bag_inst->IsAttuned(), bag_inst->GetCustomDataString(), bag_inst->GetOrnamentationIcon(), bag_inst->GetOrnamentationIDFile(), bag_inst->GetOrnamentHeroModel() ); removedList.push_back(real_bag_slot); client->DeleteItemInInventory(real_bag_slot, 0, true, false); } break; } client->DeleteItemInInventory(equipSlot, 0, true, false); } // To be called from LoadFromDBData Corpse::Corpse( uint32 corpse_id, uint32 character_id, const char *character_name, LootItems *item_list, uint32 copper, uint32 silver, uint32 gold, uint32 platinum, const glm::vec4 &position, float size, uint8 gender, uint16 race, uint8 class_, uint8 deity, uint8 level, uint8 texture, uint8 helm_texture, uint32 rez_exp, uint32 gm_rez_exp, KilledByTypes killed_by, bool is_rezzable, uint32 rez_remaining_time, bool was_at_graveyard ) : Mob( "Unnamed_Corpse", // in_name "", // in_lastname 0, // in_cur_hp 0, // in_max_hp gender, // in_gender race, // in_race class_, // in_class BodyType::Humanoid, // in_bodytype deity, // in_deity level, // in_level 0, // in_npctype_id size, // in_size 0.0f, // in_runspeed position, // position 0, // in_light texture, // in_texture helm_texture, // in_helmtexture 0, // in_ac 0, // in_atk 0, // in_str 0, // in_sta 0, // in_dex 0, // in_agi 0, // in_int 0, // in_wis 0, // in_cha 0, // in_haircolor 0, // in_beardcolor 0, // in_eyecolor1 0, // in_eyecolor2 0, // in_hairstyle 0, // in_luclinface 0, // in_beard 0, // in_drakkin_heritage 0, // in_drakkin_tattoo 0, // in_drakkin_details EQ::TintProfile(), // in_armor_tint 0xFF, // in_aa_title 0, // in_see_invis 0, // in_see_invis_undead 0, // in_see_hide 0, // in_see_improved_hide 0, // in_hp_regen 0, // in_mana_regen 0, // in_qglobal 0, // in_maxlevel 0, // in_scalerate 0, // in_armtexture 0, // in_bracertexture 0, // in_handtexture 0, // in_legtexture 0, // in_feettexture 0, // in_usemodel false, // in_always_aggros_foes 0, // in_heroic_strikethrough false // in_keeps_sold_items ) { LoadPlayerCorpseDecayTime(corpse_id); if (!zone->HasGraveyard() || was_at_graveyard) { m_corpse_graveyard_timer.Disable(); } strcpy(corpse_name, character_name); strcpy(name, character_name); m_copper = copper; m_silver = silver; m_gold = gold; m_platinum = platinum; m_rezzed_experience = rez_exp; m_gm_rezzed_experience = gm_rez_exp; m_killed_by_type = (uint8) killed_by; m_is_rezzable = is_rezzable; m_remaining_rez_time = rez_remaining_time; m_is_owner_online = false; m_is_corpse_changed = false; m_is_player_corpse = true; m_is_locked = false; m_being_looted_by_entity_id = 0xFFFFFFFF; m_corpse_db_id = corpse_id; m_player_corpse_depop = false; m_character_id = character_id; m_item_list = *item_list; item_list->clear(); // timers m_corpse_decay_timer.SetTimer(RuleI(Character, CorpseDecayTime)); m_corpse_rezzable_timer.SetTimer(RuleI(Character, CorpseResTime)); m_corpse_delay_timer.SetTimer(RuleI(NPC, CorpseUnlockTimer)); m_corpse_graveyard_timer.SetTimer(RuleI(Zone, GraveyardTimeMS)); m_loot_cooldown_timer.SetTimer(10); m_check_owner_online_timer.SetTimer(RuleI(Character, CorpseOwnerOnlineTime)); m_check_rezzable_timer.SetTimer(1000); m_corpse_rezzable_timer.Disable(); SetRezTimer(); for (int &allowed_looter: m_allowed_looters) { allowed_looter = 0; } SetPlayerKillItemID(0); UpdateEquipmentLight(); UpdateActiveLight(); m_loot_request_type = LootRequestType::Forbidden; } Corpse::~Corpse() { if (m_is_player_corpse && !(m_player_corpse_depop && m_corpse_db_id == 0)) { Save(); } LootItems::iterator cur, end; cur = m_item_list.begin(); end = m_item_list.end(); for (; cur != end; ++cur) { LootItem *item = *cur; safe_delete(item); } m_item_list.clear(); } /* this needs to be called AFTER the entity_id is set the client does this too, so it's unchangable */ void Corpse::CalcCorpseName() { EntityList::RemoveNumbers(name); char tmp[64]; if (m_is_player_corpse) { snprintf(tmp, sizeof(tmp), "'s corpse%d", GetID()); } else { snprintf(tmp, sizeof(tmp), "`s_corpse%d", GetID()); } name[(sizeof(name) - 1) - strlen(tmp)] = 0; strcat(name, tmp); } bool Corpse::Save() { if (!m_is_player_corpse) { return true; } if (!m_is_corpse_changed) { return true; } CharacterCorpseEntry ce; ce.size = size; ce.locked = m_is_locked; ce.copper = m_copper; ce.silver = m_silver; ce.gold = m_gold; ce.plat = m_platinum; ce.race = race; ce.class_ = class_; ce.gender = gender; ce.deity = deity; ce.level = level; ce.texture = texture; ce.helmtexture = helmtexture; ce.exp = m_rezzed_experience; ce.gm_exp = m_gm_exp; ce.killed_by = m_killed_by_type; ce.rezzable = m_is_rezzable; ce.rez_time = m_remaining_rez_time; ce.item_tint = m_item_tint; ce.haircolor = haircolor; ce.beardcolor = beardcolor; ce.eyecolor2 = eyecolor1; ce.hairstyle = hairstyle; ce.face = luclinface; ce.beard = beard; ce.drakkin_heritage = drakkin_heritage; ce.drakkin_tattoo = drakkin_tattoo; ce.drakkin_details = drakkin_details; for (auto &item: m_item_list) { CharacterCorpseItemEntry e; e.item_id = item->item_id; e.equip_slot = item->equip_slot; e.charges = item->charges; e.lootslot = item->lootslot; e.aug_1 = item->aug_1; e.aug_2 = item->aug_2; e.aug_3 = item->aug_3; e.aug_4 = item->aug_4; e.aug_5 = item->aug_5; e.aug_6 = item->aug_6; e.attuned = item->attuned; e.custom_data = item->custom_data; e.ornamenticon = item->ornamenticon; e.ornamentidfile = item->ornamentidfile; e.ornament_hero_model = item->ornament_hero_model; ce.items.emplace_back(std::move(e)); } if (m_corpse_db_id == 0) { m_corpse_db_id = database.SaveCharacterCorpse( m_character_id, corpse_name, zone->GetZoneID(), zone->GetInstanceID(), ce, m_Position, m_consented_guild_id ); } else { m_corpse_db_id = database.UpdateCharacterCorpse( m_corpse_db_id, m_character_id, corpse_name, zone->GetZoneID(), zone->GetInstanceID(), ce, m_Position, m_consented_guild_id, IsRezzed() ); } return true; } void Corpse::Delete() { if (IsPlayerCorpse() && m_corpse_db_id != 0) { database.DeleteCharacterCorpse(m_corpse_db_id); } m_corpse_db_id = 0; m_player_corpse_depop = true; } void Corpse::Bury() { if (IsPlayerCorpse() && m_corpse_db_id != 0) { database.BuryCharacterCorpse(m_corpse_db_id); } m_corpse_db_id = 0; m_player_corpse_depop = true; } void Corpse::DepopNPCCorpse() { if (IsNPCCorpse()) { m_player_corpse_depop = true; } } void Corpse::DepopPlayerCorpse() { m_player_corpse_depop = true; } void Corpse::AddConsentName(const std::string &consent_player_name) { for (const auto &consented_player_name: m_consented_player_names) { if (strcasecmp(consented_player_name.c_str(), consent_player_name.c_str()) == 0) { return; } } m_consented_player_names.emplace_back(consent_player_name); } void Corpse::RemoveConsentName(const std::string &consent_player_name) { m_consented_player_names.erase( std::remove_if( m_consented_player_names.begin(), m_consented_player_names.end(), [consent_player_name](const std::string &consented_player_name) { return strcasecmp(consented_player_name.c_str(), consent_player_name.c_str()) == 0; } ), m_consented_player_names.end() ); } uint32 Corpse::CountItems() { return m_item_list.size(); } void Corpse::AddItem( uint32 itemnum, uint16 charges, int16 slot, uint32 aug1, uint32 aug2, uint32 aug3, uint32 aug4, uint32 aug5, uint32 aug6, bool attuned, const std::string &custom_data, uint32 ornamenticon, uint32 ornamentidfile, uint32 ornament_hero_model ) { if (!database.GetItem(itemnum)) { return; } m_is_corpse_changed = true; auto i = new LootItem; i->item_id = itemnum; i->charges = charges; i->equip_slot = slot; i->aug_1 = aug1; i->aug_2 = aug2; i->aug_3 = aug3; i->aug_4 = aug4; i->aug_5 = aug5; i->aug_6 = aug6; i->attuned = attuned; i->custom_data = custom_data; i->ornamenticon = ornamenticon; i->ornamentidfile = ornamentidfile; i->ornament_hero_model = ornament_hero_model; m_item_list.push_back(i); UpdateEquipmentLight(); } LootItem *Corpse::GetItem(uint16 lootslot, LootItem **bag_item_data) { LootItem *sitem = nullptr; LootItem *sitem2 = nullptr; for (const auto &item: m_item_list) { if (item->lootslot == lootslot) { sitem = item; break; } } if (sitem && bag_item_data && EQ::InventoryProfile::SupportsContainers(sitem->equip_slot)) { int16 bagstart = EQ::InventoryProfile::CalcSlotId(sitem->equip_slot, EQ::invbag::SLOT_BEGIN); // convert above code to for loop for (const auto &item: m_item_list) { if (item->equip_slot >= bagstart && item->equip_slot < bagstart + EQ::invbag::SLOT_COUNT) { bag_item_data[item->equip_slot - bagstart] = item; } } } return sitem; } uint32 Corpse::GetWornItem(int16 equip_slot) const { for (const auto &item: m_item_list) { if (item->equip_slot == equip_slot) { return item->item_id; } } return 0; } void Corpse::RemoveItem(uint16 lootslot) { if (lootslot == 0xFFFF) { return; } for (const auto &item: m_item_list) { if (item->lootslot == lootslot) { RemoveItem(item); return; } } } void Corpse::RemoveItem(LootItem *item_data) { for (auto iter = m_item_list.begin(); iter != m_item_list.end(); ++iter) { auto sitem = *iter; if (sitem != item_data) { continue; } m_is_corpse_changed = true; m_item_list.erase(iter); uint8 material = EQ::InventoryProfile::CalcMaterialFromSlot(sitem->equip_slot); // autos to unsigned char if (material != EQ::textures::materialInvalid) { SendWearChange(material); } UpdateEquipmentLight(); if (UpdateActiveLight()) { SendAppearancePacket(AppearanceType::Light, GetActiveLightType()); } safe_delete(sitem); return; } } void Corpse::RemoveItemByID(uint32 item_id, int quantity) { if (!database.GetItem(item_id)) { return; } if (!HasItem(item_id)) { return; } int removed_count = 0; for (auto current_item = m_item_list.begin(); current_item != m_item_list.end(); ++current_item) { LootItem *sitem = *current_item; if (removed_count == quantity) { break; } if (sitem && sitem->item_id == item_id) { int stack_size = sitem->charges > 1 ? sitem->charges : 1; if ((removed_count + stack_size) <= quantity) { removed_count += stack_size; m_is_corpse_changed = true; m_item_list.erase(current_item); } else { int amount_left = (quantity - removed_count); if (amount_left > 0) { if (stack_size > amount_left) { removed_count += amount_left; sitem->charges -= amount_left; m_is_corpse_changed = true; } else if (stack_size == amount_left) { removed_count += amount_left; m_item_list.erase(current_item); } } } } } } void Corpse::SetCash(uint32 in_copper, uint32 in_silver, uint32 in_gold, uint32 in_platinum) { m_copper = in_copper; m_silver = in_silver; m_gold = in_gold; m_platinum = in_platinum; m_is_corpse_changed = true; } void Corpse::RemoveCash() { m_copper = 0; m_silver = 0; m_gold = 0; m_platinum = 0; m_is_corpse_changed = true; } bool Corpse::IsEmpty() const { if (m_copper != 0 || m_silver != 0 || m_gold != 0 || m_platinum != 0) { return false; } return m_item_list.empty(); } bool Corpse::Process() { if (m_player_corpse_depop) { return false; } if (m_check_owner_online_timer.Check() && m_is_rezzable) { CheckIsOwnerOnline(); } if (m_corpse_delay_timer.Check()) { for (int &allowed_looter: m_allowed_looters) { allowed_looter = 0; } m_corpse_delay_timer.Disable(); return true; } if (m_corpse_graveyard_timer.Check()) { if (MovePlayerCorpseToGraveyard()) { m_corpse_graveyard_timer.Disable(); return false; } return true; // If the corpse was not moved, continue the corpse in the process rather than put in limbo } // Player is offline. If rez timer is enabled, disable it and save corpse. if (m_is_rezzable && m_check_rezzable_timer.Check()) { if (!m_is_owner_online) { if (m_corpse_rezzable_timer.Enabled()) { m_remaining_rez_time = m_corpse_rezzable_timer.GetRemainingTime(); m_corpse_rezzable_timer.Disable(); m_is_corpse_changed = true; Save(); } } else { //Player is online. If rez timer is disabled, enable it. if (m_corpse_rezzable_timer.Enabled()) { m_remaining_rez_time = m_corpse_rezzable_timer.GetRemainingTime(); } else { SetRezTimer(); } } } if (m_corpse_rezzable_timer.Check()) { CompleteResurrection(true); } /* This is when a corpse hits decay timer and does checks*/ if (m_corpse_decay_timer.Check()) { /* NPC */ if (IsNPCCorpse()) { m_corpse_decay_timer.Disable(); return false; } /* Client */ if (!RuleB(Zone, EnableShadowrest)) { Delete(); } else { if (database.BuryCharacterCorpse(m_corpse_db_id)) { Save(); m_player_corpse_depop = true; m_corpse_db_id = 0; LogCorpses("Tagged [{}] player corpse has buried", GetName()); } else { LogError("Unable to bury [{}] player corpse", GetName()); return true; } } m_corpse_decay_timer.Disable(); return false; } return true; } void Corpse::ResetDecayTimer() { int decay_ms = level > 54 ? RuleI(NPC, MajorNPCCorpseDecayTime) : RuleI(NPC, MinorNPCCorpseDecayTime); if (IsPlayerCorpse()) { decay_ms = RuleI(Character, CorpseDecayTime); } else if (IsEmpty()) { decay_ms = RuleI(NPC, EmptyNPCCorpseDecayTime) + 1000; } else { for (const npcDecayTimes_Struct &decay_time: npcCorpseDecayTimes) { if (level >= decay_time.minlvl && level <= decay_time.maxlvl) { decay_ms = decay_time.seconds * 1000; break; } } } m_corpse_decay_timer.SetTimer(decay_ms); } void Corpse::SetDecayTimer(uint32 decay_time) { if (decay_time == 0) { m_corpse_decay_timer.Trigger(); } else { m_corpse_decay_timer.Start(decay_time); } } bool Corpse::CanPlayerLoot(int character_id) { uint8 looters = 0; for (int allowed_looter: m_allowed_looters) { if (allowed_looter != 0) { looters++; } if (allowed_looter == character_id) { return true; } } return looters == 0; } void Corpse::AllowPlayerLoot(Mob *them, uint8 slot) { if (slot >= MAX_LOOTERS) { return; } if (them == nullptr || !them->IsClient()) { return; } m_allowed_looters[slot] = them->CastToClient()->CharacterID(); } void Corpse::MakeLootRequestPackets(Client *c, const EQApplicationPacket *app) { if (!c) { return; } // Added 12/08. Started compressing loot struct on live. if (m_player_corpse_depop) { SendLootReqErrorPacket(c, LootResponse::SomeoneElse); return; } if (IsPlayerCorpse() && !m_corpse_db_id) { // really should try to resave in this case // SendLootReqErrorPacket(client, 0); c->Message(Chat::Red, "Warning: Corpse's dbid = 0! Corpse will not survive zone shutdown!"); std::cout << "Error: PlayerCorpse::MakeLootRequestPackets: dbid = 0!" << std::endl; // return; } if (m_is_locked && c->Admin() < AccountStatus::GMAdmin) { SendLootReqErrorPacket(c, LootResponse::SomeoneElse); c->Message(Chat::Red, "Error: Corpse locked by GM."); return; } if (!m_being_looted_by_entity_id || (m_being_looted_by_entity_id != 0xFFFFFFFF && !entity_list.GetID(m_being_looted_by_entity_id))) { m_being_looted_by_entity_id = 0xFFFFFFFF; } if (DistanceSquaredNoZ(c->GetPosition(), m_Position) > 625) { SendLootReqErrorPacket(c, LootResponse::TooFar); return; } if (m_being_looted_by_entity_id != 0xFFFFFFFF && m_being_looted_by_entity_id != c->GetID()) { SendLootReqErrorPacket(c, LootResponse::SomeoneElse); return; } // all loot session disqualifiers should occur before this point as not to interfere with any current looter m_loot_request_type = LootRequestType::Forbidden; // loot_request_type is scoped to class Corpse and reset on a per-loot session basis if (c->GetGM()) { if (c->Admin() >= AccountStatus::GMAdmin) { m_loot_request_type = LootRequestType::GMAllowed; c->Message(Chat::White, "Your GM Status allows you to loot any items on this corpse."); } else { m_loot_request_type = LootRequestType::GMPeek; c->Message(Chat::White, "Your GM flag allows you to look at the items on this corpse."); } } else { if (IsPlayerCorpse()) { if (m_character_id == c->CharacterID()) { m_loot_request_type = LootRequestType::Self; } else if (CanPlayerLoot(c->CharacterID())) { if (GetPlayerKillItem() == -1) { m_loot_request_type = LootRequestType::AllowedPVPAll; } else if (GetPlayerKillItem() == 1) { m_loot_request_type = LootRequestType::AllowedPVPSingle; } else if (GetPlayerKillItem() > 1) { m_loot_request_type = LootRequestType::AllowedPVPDefined; } } } else if ((IsNPCCorpse() || m_become_npc) && CanPlayerLoot(c->CharacterID())) { m_loot_request_type = LootRequestType::AllowedPVE; } } LogInventory( "m_loot_request_type [{}] for [{}]", (int) m_loot_request_type, c->GetName() ); if (m_loot_request_type == LootRequestType::Forbidden) { SendLootReqErrorPacket(c, LootResponse::NotAtThisTime); return; } m_being_looted_by_entity_id = c->GetID(); c->CommonBreakInvisible(); // we should be "all good" so lets break invis now instead of earlier before all error checking is done // process coin bool loot_coin = false; std::string tmp; if (database.GetVariable("LootCoin", tmp)) { loot_coin = (tmp[0] == 1 && tmp[1] == '\0'); } if (m_loot_request_type == LootRequestType::GMPeek || m_loot_request_type == LootRequestType::GMAllowed) { if (GetPlatinum() || GetGold() || GetSilver() || GetCopper()) { c->Message( Chat::Yellow, fmt::format( "This corpse contains {}.", Strings::Money( GetPlatinum(), GetGold(), GetSilver(), GetCopper() ) ).c_str() ); } else { c->Message(Chat::Yellow, "This corpse contains no money."); } auto outapp = new EQApplicationPacket(OP_MoneyOnCorpse, sizeof(moneyOnCorpseStruct)); auto *d = (moneyOnCorpseStruct *) outapp->pBuffer; d->response = static_cast(LootResponse::Normal); d->unknown1 = 0x42; d->unknown2 = 0xef; d->copper = 0; d->silver = 0; d->gold = 0; d->platinum = 0; outapp->priority = 6; c->QueuePacket(outapp); safe_delete(outapp); } else { auto outapp = new EQApplicationPacket(OP_MoneyOnCorpse, sizeof(moneyOnCorpseStruct)); auto *d = (moneyOnCorpseStruct *) outapp->pBuffer; d->response = static_cast(LootResponse::Normal); d->unknown1 = 0x42; d->unknown2 = 0xef; Group *cgroup = c->GetGroup(); // this can be reworked into a switch and/or massaged to include specialized pve loot rules based on 'LootRequestType' if (!IsPlayerCorpse() && c->IsGrouped() && c->AutoSplitEnabled() && cgroup) { d->copper = 0; d->silver = 0; d->gold = 0; d->platinum = 0; cgroup->SplitMoney(GetCopper(), GetSilver(), GetGold(), GetPlatinum(), c); } else { d->copper = GetCopper(); d->silver = GetSilver(); d->gold = GetGold(); d->platinum = GetPlatinum(); c->AddMoneyToPP(GetCopper(), GetSilver(), GetGold(), GetPlatinum()); } RemoveCash(); Save(); outapp->priority = 6; c->QueuePacket(outapp); safe_delete(outapp); } // process items auto timestamps = database.GetItemRecastTimestamps(c->CharacterID()); if (m_loot_request_type == LootRequestType::AllowedPVPDefined) { auto pkitemid = GetPlayerKillItem(); auto pkitem = database.GetItem(pkitemid); auto pkinst = database.CreateItem(pkitem, pkitem->MaxCharges); if (pkinst) { if (pkitem->RecastDelay) { if (pkitem->RecastType != RECAST_TYPE_UNLINKED_ITEM) { pkinst->SetRecastTimestamp( timestamps.count(pkitem->RecastType) ? timestamps.at(pkitem->RecastType) : 0 ); } else { pkinst->SetRecastTimestamp(timestamps.count(pkitem->ID) ? timestamps.at(pkitem->ID) : 0); } } LogInventory("MakeLootRequestPackets() Slot [{}], Item [{}]", EQ::invslot::CORPSE_BEGIN, pkitem->Name); c->SendItemPacket(EQ::invslot::CORPSE_BEGIN, pkinst, ItemPacketLoot); safe_delete(pkinst); } else { LogInventory("MakeLootRequestPackets() PlayerKillItem [{}] not found", pkitemid); c->Message(Chat::Red, "PlayerKillItem (id: %i) could not be found!", pkitemid); } c->QueuePacket(app); return; } auto loot_slot = EQ::invslot::CORPSE_BEGIN; auto corpse_mask = c->GetInv().GetLookup()->CorpseBitmask; for (auto i: m_item_list) { // every loot session must either set all items' lootslots to 'invslot::SLOT_INVALID' // or to a valid enumerated client-versioned corpse slot (lootslot is not equip_slot) i->lootslot = 0xFFFF; // align server and client corpse slot mappings so translators can function properly while (loot_slot <= EQ::invslot::CORPSE_END && (((uint64) 1 << loot_slot) & corpse_mask) == 0) ++loot_slot; if (loot_slot > EQ::invslot::CORPSE_END) { continue; } if (IsPlayerCorpse()) { if (m_loot_request_type == LootRequestType::AllowedPVPSingle && loot_slot != EQ::invslot::CORPSE_BEGIN) { continue; } if (i->equip_slot < EQ::invslot::POSSESSIONS_BEGIN || i->equip_slot > EQ::invslot::POSSESSIONS_END) { continue; } } const auto *item = database.GetItem(i->item_id); auto inst = database.CreateItem( item, i->charges, i->aug_1, i->aug_2, i->aug_3, i->aug_4, i->aug_5, i->aug_6, i->attuned, i->custom_data, i->ornamenticon, i->ornamentidfile, i->ornament_hero_model ); if (!inst) { continue; } if (item->RecastDelay) { if (item->RecastType != RECAST_TYPE_UNLINKED_ITEM) { inst->SetRecastTimestamp(timestamps.count(item->RecastType) ? timestamps.at(item->RecastType) : 0); } else { inst->SetRecastTimestamp(timestamps.count(item->ID) ? timestamps.at(item->ID) : 0); } } LogInventory("MakeLootRequestPackets() Slot [{}], Item [{}]", loot_slot, item->Name); c->SendItemPacket(loot_slot, inst, ItemPacketLoot); safe_delete(inst); i->lootslot = loot_slot++; } // Disgrace: Client seems to require that we send the packet back... c->QueuePacket(app); // This is required for the 'Loot All' feature to work for SoD clients. I expect it is to tell the client that the // server has now sent all the items on the corpse. if (c->ClientVersion() >= EQ::versions::ClientVersion::SoD) { SendLootReqErrorPacket(c, LootResponse::LootAll); } } void Corpse::LootCorpseItem(Client *c, const EQApplicationPacket *app) { if (!c) { return; } auto lootitem = (LootingItem_Struct *) app->pBuffer; LogInventory( "m_loot_request_type [{}] slot_id [{}] for [{}]", (int) m_loot_request_type, lootitem->slot_id, c->GetName() ); if (m_loot_request_type < LootRequestType::GMAllowed) { // LootRequestType::Forbidden and LootRequestType::GMPeek c->QueuePacket(app); SendEndLootErrorPacket(c); // unlock corpse for others if (IsBeingLootedBy(c)) { ResetLooter(); } return; } if (!m_loot_cooldown_timer.Check()) { c->QueuePacket(app); SendEndLootErrorPacket(c); // unlock corpse for others if (IsBeingLootedBy(c)) { ResetLooter(); } return; } /* To prevent item loss for a player using 'Loot All' who doesn't have inventory space for all their items. */ if (RuleB(Character, CheckCursorEmptyWhenLooting) && !c->GetInv().CursorEmpty()) { c->Message(Chat::Red, "You may not loot an item while you have an item on your cursor."); c->QueuePacket(app); SendEndLootErrorPacket(c); /* Unlock corpse for others */ if (IsBeingLootedBy(c)) { ResetLooter(); } return; } if (!IsBeingLootedBy(c)) { c->QueuePacket(app); SendEndLootErrorPacket(c); return; } if (IsPlayerCorpse() && !CanPlayerLoot(c->CharacterID()) && !m_become_npc && (m_character_id != c->CharacterID() && c->Admin() < AccountStatus::GMLeadAdmin)) { c->Message(Chat::Red, "Error: This is a player corpse and you dont own it."); c->QueuePacket(app); SendEndLootErrorPacket(c); return; } if (m_is_locked && c->Admin() < AccountStatus::GMAdmin) { c->QueuePacket(app); SendLootReqErrorPacket(c, LootResponse::SomeoneElse); c->Message(Chat::Red, "Error: Corpse locked by GM."); return; } if (IsPlayerCorpse() && (m_character_id != c->CharacterID()) && CanPlayerLoot(c->CharacterID()) && GetPlayerKillItem() == 0) { c->Message(Chat::Red, "Error: You cannot loot any more items from this corpse."); c->QueuePacket(app); SendEndLootErrorPacket(c); ResetLooter(); return; } const EQ::ItemData *item = nullptr; EQ::ItemInstance *inst = nullptr; LootItem *item_data = nullptr, *bag_item_data[EQ::invbag::SLOT_COUNT] = {}; memset(bag_item_data, 0, sizeof(bag_item_data)); if (GetPlayerKillItem() > 1) { item = database.GetItem(GetPlayerKillItem()); } else if (GetPlayerKillItem() == -1 || GetPlayerKillItem() == 1) { item_data = GetItem(lootitem->slot_id); // dont allow them to loot entire bags of items as pvp reward } else { item_data = GetItem(lootitem->slot_id, bag_item_data); } if (GetPlayerKillItem() <= 1 && item_data != 0) { item = database.GetItem(item_data->item_id); } if (item) { if (item_data) { inst = database.CreateItem( item, item_data ? item_data->charges : 0, item_data->aug_1, item_data->aug_2, item_data->aug_3, item_data->aug_4, item_data->aug_5, item_data->aug_6, item_data->attuned, item_data->custom_data, item_data->ornamenticon, item_data->ornamentidfile, item_data->ornament_hero_model ); } else { inst = database.CreateItem(item); } } if (c && inst) { if (c->CheckLoreConflict(item)) { c->MessageString(Chat::White, LOOT_LORE_ERROR); c->QueuePacket(app); SendEndLootErrorPacket(c); ResetLooter(); delete inst; return; } if (inst->IsAugmented()) { for (int i = EQ::invaug::SOCKET_BEGIN; i <= EQ::invaug::SOCKET_END; i++) { EQ::ItemInstance *itm = inst->GetAugment(i); if (itm) { if (c->CheckLoreConflict(itm->GetItem())) { c->MessageString(Chat::White, LOOT_LORE_ERROR); c->QueuePacket(app); SendEndLootErrorPacket(c); ResetLooter(); delete inst; return; } } } } auto prevent_loot = false; if (RuleB(Zone, UseZoneController)) { auto controller = entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID); if (controller) { if (parse->HasQuestSub(ZONE_CONTROLLER_NPC_ID, EVENT_LOOT_ZONE)) { const auto &export_string = fmt::format( "{} {} {} {}", inst->GetItem()->ID, inst->GetCharges(), EntityList::RemoveNumbers(corpse_name), GetID() ); std::vector args = {inst, this}; if (parse->EventNPC(EVENT_LOOT_ZONE, controller, c, export_string, 0, &args) != 0) { prevent_loot = true; } } } } if (parse->PlayerHasQuestSub(EVENT_LOOT)) { const auto &export_string = fmt::format( "{} {} {} {}", inst->GetItem()->ID, inst->GetCharges(), EntityList::RemoveNumbers(corpse_name), GetID() ); std::vector args = {inst, this}; if (parse->EventPlayer(EVENT_LOOT, c, export_string, 0, &args) != 0) { prevent_loot = true; } } if (player_event_logs.IsEventEnabled(PlayerEvent::LOOT_ITEM) && !IsPlayerCorpse()) { auto e = PlayerEvent::LootItemEvent{ .item_id = inst->GetItem()->ID, .item_name = inst->GetItem()->Name, .charges = inst->GetCharges(), .npc_id = GetNPCTypeID(), .corpse_name = EntityList::RemoveNumbers(corpse_name) }; RecordPlayerEventLogWithClient(c, PlayerEvent::LOOT_ITEM, e); } if (!IsPlayerCorpse()) { // dynamic zones may prevent looting by non-members or based on lockouts auto dz = zone->GetDynamicZone(); if (dz && !dz->CanClientLootCorpse(c, GetNPCTypeID(), GetID())) { prevent_loot = true; // note on live this message is only sent once on the first loot attempt of an open corpse c->MessageString(Chat::Loot, LOOT_NOT_ALLOWED, inst->GetItem()->Name); } } if (parse->ItemHasQuestSub(inst, EVENT_LOOT)) { const auto &export_string = fmt::format( "{} {} {} {}", inst->GetItem()->ID, inst->GetCharges(), EntityList::RemoveNumbers(corpse_name), GetID() ); std::vector args = {inst, this}; if (parse->EventItem(EVENT_LOOT, c, inst, this, export_string, 0, &args) != 0) { prevent_loot = true; } } if (prevent_loot) { lootitem->auto_loot = -1; c->QueuePacket(app); safe_delete(inst); return; } // safe to ACK now c->QueuePacket(app); if (!IsPlayerCorpse()) { if (RuleB(Character, EnableDiscoveredItems) && c && !c->IsDiscovered(inst->GetItem()->ID)) { if (!c->GetGM()) { c->DiscoverItem(inst->GetItem()->ID); } else { const std::string& item_link = database.CreateItemLink(inst->GetItem()->ID); c->Message( Chat::White, fmt::format( "Your GM flag prevents {} from being added to discovered items.", item_link ).c_str() ); } } } if (zone->adv_data) { auto *ad = (ServerZoneAdventureDataReply_Struct *) zone->adv_data; if (ad->type == Adventure_Collect && !IsPlayerCorpse()) { if (ad->data_id == inst->GetItem()->ID) { zone->DoAdventureCountIncrease(); } } } // get count for task update before it's mutated by AutoPutLootInInventory int count = inst->IsStackable() ? inst->GetCharges() : 1; //Set recast on item when looting it! auto timestamps = database.GetItemRecastTimestamps(c->CharacterID()); const auto *d = inst->GetItem(); if (d->RecastDelay) { if (d->RecastType != RECAST_TYPE_UNLINKED_ITEM) { inst->SetRecastTimestamp(timestamps.count(d->RecastType) ? timestamps.at(d->RecastType) : 0); } else { inst->SetRecastTimestamp(timestamps.count(d->ID) ? timestamps.at(d->ID) : 0); } } /* First add it to the looter - this will do the bag contents too */ if (lootitem->auto_loot > 0) { if (!c->AutoPutLootInInventory(*inst, true, true, bag_item_data)) { c->PutLootInInventory(EQ::invslot::slotCursor, *inst, bag_item_data); } } else { c->PutLootInInventory(EQ::invslot::slotCursor, *inst, bag_item_data); } /* Update any tasks that have an activity to loot this item */ if (RuleB(TaskSystem, EnableTaskSystem) && IsNPCCorpse()) { c->UpdateTasksOnLoot(this, item->ID, count); } /* Remove it from Corpse */ if (item_data) { /* Delete needs to be before RemoveItem because its deletes the pointer for * item_data/bag_item_data */ database.DeleteItemOffCharacterCorpse( m_corpse_db_id, item_data->equip_slot, item_data->item_id ); /* Delete Item Instance */ RemoveItem(item_data->lootslot); } /* Remove Bag Contents */ if (item->IsClassBag() && (GetPlayerKillItem() != -1 || GetPlayerKillItem() != 1)) { for (int i = EQ::invbag::SLOT_BEGIN; i <= EQ::invbag::SLOT_END; i++) { if (bag_item_data[i]) { /* Delete needs to be before RemoveItem because its deletes the pointer for * item_data/bag_item_data */ database.DeleteItemOffCharacterCorpse( m_corpse_db_id, bag_item_data[i]->equip_slot, bag_item_data[i]->item_id ); /* Delete Item Instance */ RemoveItem(bag_item_data[i]); } } } if (GetPlayerKillItem() != -1) { SetPlayerKillItemID(0); } /* Send message with item link to groups and such */ EQ::SayLinkEngine linker; linker.SetLinkType(EQ::saylink::SayLinkItemInst); linker.SetItemInst(inst); linker.GenerateLink(); c->MessageString(Chat::Loot, LOOTED_MESSAGE, linker.Link().c_str()); if (!IsPlayerCorpse()) { Group *g = c->GetGroup(); if (g != nullptr) { g->GroupMessageString( c, Chat::Loot, OTHER_LOOTED_MESSAGE, c->GetName(), linker.Link().c_str() ); } else { Raid *r = c->GetRaid(); if (r != nullptr) { r->RaidMessageString( c, Chat::Loot, OTHER_LOOTED_MESSAGE, c->GetName(), linker.Link().c_str() ); } } } } else { SendEndLootErrorPacket(c); safe_delete(inst); return; } if (IsPlayerCorpse()) { c->SendItemLink(inst); } else { c->SendItemLink(inst, true); } safe_delete(inst); } void Corpse::EndLoot(Client *c, const EQApplicationPacket *app) { auto outapp = new EQApplicationPacket; outapp->SetOpcode(OP_LootComplete); outapp->size = 0; c->QueuePacket(outapp); safe_delete(outapp); m_being_looted_by_entity_id = 0xFFFFFFFF; if (IsEmpty()) { Delete(); } else { Save(); } } void Corpse::FillSpawnStruct(NewSpawn_Struct *ns, Mob *ForWho) { Mob::FillSpawnStruct(ns, ForWho); ns->spawn.max_hp = 120; ns->spawn.NPC = 2; UpdateActiveLight(); ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive]; } void Corpse::QueryLoot(Client *to) { if (!m_item_list.empty()) { int player_corpse_limit = to->GetInv().GetLookup()->InventoryTypeSize.Corpse; to->Message( Chat::White, fmt::format( "Loot | Name: {} ID: {}", GetName(), GetNPCTypeID() ).c_str() ); int item_count = 0; for (auto current_item: m_item_list) { int item_number = (item_count + 1); if (!current_item) { LogError("Corpse::QueryLoot() - ItemList error, null item."); continue; } if (!current_item->item_id || !database.GetItem(current_item->item_id)) { LogError("Corpse::QueryLoot() - Database error, invalid item."); continue; } EQ::SayLinkEngine linker; linker.SetLinkType(EQ::saylink::SayLinkLootItem); linker.SetLootData(current_item); to->Message( Chat::White, fmt::format( "Item {} | Name: {} ({}){}", item_number, linker.GenerateLink().c_str(), current_item->item_id, ( current_item->charges > 1 ? fmt::format( " Amount: {}", current_item->charges ) : "" ) ).c_str() ); item_count++; } } if (m_platinum || m_gold || m_silver || m_copper) { to->Message( Chat::White, fmt::format( "Money | {}", Strings::Money( m_platinum, m_gold, m_silver, m_copper ) ).c_str() ); } } bool Corpse::HasItem(uint32 item_id) { if (!database.GetItem(item_id)) { return false; } for (auto i: m_item_list) { if (!i) { LogError("Corpse::HasItem() - ItemList error, null item"); continue; } if (!i->item_id || !database.GetItem(i->item_id)) { LogError("Corpse::HasItem() - Database error, invalid item"); continue; } if (i->item_id == item_id) { return true; } } return false; } uint32 Corpse::CountItem(uint32 item_id) { uint32 item_count = 0; if (!database.GetItem(item_id)) { return item_count; } for (auto i: m_item_list) { if (!i) { LogError("Corpse::CountItem() - ItemList error, null item"); continue; } if (!i->item_id || !database.GetItem(i->item_id)) { LogError("Corpse::CountItem() - Database error, invalid item"); continue; } if (i->item_id == item_id) { item_count += i->charges > 0 ? i->charges : 1; } } return item_count; } uint32 Corpse::GetItemIDBySlot(uint16 loot_slot) { for (auto i: m_item_list) { if (i->lootslot == loot_slot) { return i->item_id; } } return 0; } uint16 Corpse::GetFirstLootSlotByItemID(uint32 item_id) { for (auto i: m_item_list) { if (i->item_id == item_id) { return i->lootslot; } } return 0; } bool Corpse::Summon(Client *c, bool spell, bool CheckDistance) { uint32 dist2 = 10000; // pow(100, 2); if (!spell) { if (GetCharID() == c->CharacterID()) { if (IsLocked() && c->Admin() < AccountStatus::GMAdmin) { c->Message(Chat::Red, "That corpse is locked by a GM."); return false; } if (!CheckDistance || (DistanceSquaredNoZ(m_Position, c->GetPosition()) <= dist2)) { GMMove(c->GetX(), c->GetY(), c->GetZ()); m_is_corpse_changed = true; } else { c->MessageString(Chat::Red, CORPSE_TOO_FAR); return false; } } else { bool consented = false; for (const auto &consented_player_name: m_consented_player_names) { if (strcasecmp(c->GetName(), consented_player_name.c_str()) == 0) { consented = true; break; } } if (!consented && m_consented_guild_id && m_consented_guild_id != GUILD_NONE) { if (c->GuildID() == m_consented_guild_id) { consented = true; } } if (!consented && m_consented_group_id) { Group *grp = c->GetGroup(); if (grp && grp->GetID() == m_consented_group_id) { consented = true; } } if (!consented && m_consented_raid_id) { Raid *raid = c->GetRaid(); if (raid && raid->GetID() == m_consented_raid_id) { consented = true; } } if (consented) { if (!CheckDistance || (DistanceSquaredNoZ(m_Position, c->GetPosition()) <= dist2)) { GMMove(c->GetX(), c->GetY(), c->GetZ()); m_is_corpse_changed = true; } else { c->MessageString(Chat::Red, CORPSE_TOO_FAR); return false; } } else { c->MessageString(Chat::Red, CONSENT_DENIED); return false; } } } else { GMMove(c->GetX(), c->GetY(), c->GetZ()); m_is_corpse_changed = true; } Save(); return true; } void Corpse::CompleteResurrection(bool timer_expired) { LogCorpses("Corpse [{}] has been rezzed.", GetName()); m_remaining_rez_time = m_corpse_rezzable_timer.GetRemainingTime(); if (timer_expired) { m_remaining_rez_time = 0; m_is_rezzable = false; // Players can no longer rez this corpse. m_corpse_rezzable_timer.Disable(); } IsRezzed(true); // Players can rez this corpse for no XP (corpse gate) provided rezzable is true. m_rezzed_experience = 0; m_is_corpse_changed = true; Save(); } void Corpse::Spawn() { auto app = new EQApplicationPacket; CreateSpawnPacket(app, this); entity_list.QueueClients(this, app); safe_delete(app); } uint32 Corpse::GetEquippedItemFromTextureSlot(uint8 material_slot) const { int16 invslot; if (material_slot > EQ::textures::LastTexture) { return 0; } invslot = EQ::InventoryProfile::CalcSlotFromMaterial(material_slot); if (invslot == INVALID_INDEX) { // GetWornItem() should be returning a 0 for any invalid index... return 0; } return GetWornItem(invslot); } uint32 Corpse::GetEquipmentColor(uint8 material_slot) const { const EQ::ItemData *item = nullptr; if (material_slot > EQ::textures::LastTexture) { return 0; } item = database.GetItem(GetEquippedItemFromTextureSlot(material_slot)); if (item) { return (m_item_tint.Slot[material_slot].UseTint ? m_item_tint.Slot[material_slot].Color : item->Color); } return 0; } void Corpse::UpdateEquipmentLight() { m_Light.Type[EQ::lightsource::LightEquipment] = 0; m_Light.Level[EQ::lightsource::LightEquipment] = 0; for (auto &i: m_item_list) { if (i->equip_slot < EQ::invslot::EQUIPMENT_BEGIN || i->equip_slot > EQ::invslot::EQUIPMENT_END) { continue; } if (i->equip_slot == EQ::invslot::slotAmmo) { continue; } auto item = database.GetItem(i->item_id); if (item == nullptr) { continue; } if (EQ::lightsource::IsLevelGreater(item->Light, m_Light.Type[EQ::lightsource::LightEquipment])) { m_Light.Type[EQ::lightsource::LightEquipment] = item->Light; } } uint8 general_light_type = 0; for (auto &i: m_item_list) { if (i->equip_slot < EQ::invslot::GENERAL_BEGIN || i->equip_slot > EQ::invslot::GENERAL_END) { continue; } auto item = database.GetItem(i->item_id); if (item == nullptr) { continue; } if (!item->IsClassCommon()) { continue; } if (item->Light < 9 || item->Light > 13) { continue; } if (EQ::lightsource::TypeToLevel(item->Light)) { general_light_type = item->Light; } } if (EQ::lightsource::IsLevelGreater(general_light_type, m_Light.Type[EQ::lightsource::LightEquipment])) { m_Light.Type[EQ::lightsource::LightEquipment] = general_light_type; } m_Light.Level[EQ::lightsource::LightEquipment] = EQ::lightsource::TypeToLevel(m_Light.Type[EQ::lightsource::LightEquipment]); } void Corpse::AddLooter(Mob *who) { for (int &looter: m_allowed_looters) { if (looter == 0) { looter = who->CastToClient()->CharacterID(); break; } } } void Corpse::LoadPlayerCorpseDecayTime(uint32 corpse_db_id) { if (!corpse_db_id) { return; } uint32 active_corpse_decay_timer = database.GetCharacterCorpseDecayTimer(corpse_db_id); if (active_corpse_decay_timer > 0 && RuleI(Character, CorpseDecayTime) > (active_corpse_decay_timer * 1000)) { m_corpse_decay_timer.SetTimer(RuleI(Character, CorpseDecayTime) - (active_corpse_decay_timer * 1000)); } else { m_corpse_decay_timer.SetTimer(2000); } if (active_corpse_decay_timer > 0 && RuleI(Zone, GraveyardTimeMS) > (active_corpse_decay_timer * 1000)) { m_corpse_graveyard_timer.SetTimer(RuleI(Zone, GraveyardTimeMS) - (active_corpse_decay_timer * 1000)); } else { m_corpse_graveyard_timer.SetTimer(3000); } } void Corpse::SendWorldSpawnPlayerCorpseInZone(uint32_t zone_id) { auto pack = std::make_unique( ServerOP_SpawnPlayerCorpse, sizeof(SpawnPlayerCorpse_Struct) ); auto *spc = reinterpret_cast(pack->pBuffer); spc->player_corpse_id = m_corpse_db_id; spc->zone_id = zone_id; worldserver.SendPacket(pack.get()); } bool Corpse::MovePlayerCorpseToGraveyard() { if (IsPlayerCorpse() && zone && zone->HasGraveyard()) { Save(); glm::vec4 graveyard_point = zone->GetGraveyardPoint(); uint16_t instance_id = (zone->GetZoneID() == zone->graveyard_zoneid()) ? zone->GetInstanceID() : 0; database.SendCharacterCorpseToGraveyard(m_corpse_db_id, zone->graveyard_zoneid(), instance_id, graveyard_point); SendWorldSpawnPlayerCorpseInZone(zone->graveyard_zoneid()); m_corpse_db_id = 0; m_player_corpse_depop = true; m_corpse_graveyard_timer.Disable(); LogCorpses( "Moved [{}] player corpse to the designated graveyard in zone [{}]", GetName(), ZoneName(zone->graveyard_zoneid()) ); return true; } return false; } bool Corpse::MovePlayerCorpseToNonInstance() { if (IsPlayerCorpse() && zone && zone->GetInstanceID() != 0) { Save(); database.SendCharacterCorpseToNonInstance(m_corpse_db_id); SendWorldSpawnPlayerCorpseInZone(zone->GetZoneID()); m_corpse_db_id = 0; m_player_corpse_depop = true; m_corpse_graveyard_timer.Disable(); LogCorpses( "Moved [{}] player corpse to non-instance version of zone [{}]", GetName(), ZoneName(zone->GetZoneID()) ); return true; } return false; } std::vector Corpse::GetLootList() { std::vector corpse_items; for (auto i: m_item_list) { if (!i) { LogError("Corpse::GetLootList() - ItemList error, null item"); continue; } if (std::find(corpse_items.begin(), corpse_items.end(), i->item_id) != corpse_items.end()) { continue; } corpse_items.push_back(i->item_id); } return corpse_items; } void Corpse::SetRezTimer(bool initial_timer) { LogCorpsesDetail("Checking for rezzable corpse [{}]", GetName()); if (!m_is_rezzable) { if (m_corpse_rezzable_timer.Enabled()) { m_corpse_rezzable_timer.Disable(); } return; } CheckIsOwnerOnline(); if (!m_is_owner_online && !initial_timer) { if (m_corpse_rezzable_timer.Enabled()) { m_corpse_rezzable_timer.Disable(); } return; } if (m_corpse_rezzable_timer.Enabled() && !initial_timer) { return; } if (initial_timer) { uint32 timer = RuleI(Character, CorpseResTime); if (static_cast(m_killed_by_type) == KilledByTypes::Killed_DUEL) { timer = RuleI(Character, DuelCorpseResTime); } m_remaining_rez_time = timer; } if (m_remaining_rez_time < 1) { // Corpse is no longer rezzable CompleteResurrection(true); return; } m_corpse_rezzable_timer.SetTimer(m_remaining_rez_time); } void Corpse::CheckIsOwnerOnline() { LogCorpsesDetail("Checking if owner is online for corpse [{}]", GetOwnerName()); Client *c = entity_list.GetClientByCharID(GetCharID()); if (!c) { // Client is not in the corpse's zone, send a packet to world to have it check. auto pack = new ServerPacket(ServerOP_IsOwnerOnline, sizeof(ServerIsOwnerOnline_Struct)); auto *o = (ServerIsOwnerOnline_Struct *) pack->pBuffer; strncpy(o->name, GetOwnerName(), sizeof(o->name)); o->corpse_id = GetID(); o->zone_id = zone->GetZoneID(); o->online = 0; o->account_id = m_account_id; worldserver.SendPacket(pack); safe_delete(pack); LogCorpsesDetail("Sent IsOwnerOnline packet to world for [{}]", GetName()); } else { SetOwnerOnline(true); } } void Corpse::CastRezz(uint16 spell_id, Mob *caster) { LogSpells( "spell_id [{}] IsRezzed() [{}], rez_experience [{}], rez_timer enabled [{}]", spell_id, IsRezzed(), m_rezzed_experience, m_corpse_rezzable_timer.Enabled() ); // refresh rezzed state from database const auto &e = CharacterCorpsesRepository::FindOne(database, m_corpse_db_id); if (!e.id) { caster->MessageString(Chat::White, REZZ_ALREADY_PENDING); return; } m_rezzed_experience = e.exp; m_gm_rezzed_experience = e.gm_exp; m_is_rezzable = e.rezzable; IsRezzed(e.is_rezzed); // Rez timer has expired, only GMs can rez at this point. (uses rezzable) if (!IsRezzable()) { if (caster && caster->IsClient()) { if (!caster->CastToClient()->GetGM()) { caster->MessageString(Chat::White, REZZ_ALREADY_PENDING); caster->MessageString(Chat::White, CORPSE_TOO_OLD); return; } caster->Message(Chat::White, "Your GM flag allows you to resurrect this corpse."); } } // Corpse has been rezzed, but timer is still active. Players can corpse gate, GMs can rez for XP. (uses is_rezzed) if (IsRezzed()) { auto c = caster && caster->IsClient() ? caster->CastToClient() : nullptr; if (c) { m_rezzed_experience = 0; if (c->GetGM()) { m_rezzed_experience = m_gm_rezzed_experience; m_gm_rezzed_experience = 0; c->Message(Chat::White, "Your GM flag allows you to resurrect this corpse and return experience."); } } } auto outapp = new EQApplicationPacket(OP_RezzRequest, sizeof(Resurrect_Struct)); auto *r = (Resurrect_Struct *) outapp->pBuffer; strn0cpy(r->your_name, corpse_name, 64); strn0cpy(r->corpse_name, name, 64); strn0cpy(r->rezzer_name, caster->GetName(), 64); r->zone_id = zone->GetZoneID(); r->instance_id = zone->GetInstanceID(); r->spellid = spell_id; r->x = m_Position.x; r->y = m_Position.y; r->z = GetFixedZ(m_Position); r->unknown000 = 0; r->unknown020 = 0; r->unknown088 = 0; // We send this to world, because it needs to go to the player who may not be in this zone. worldserver.RezzPlayer(outapp, m_rezzed_experience, m_corpse_db_id, OP_RezzRequest); safe_delete(outapp); } Corpse *Corpse::LoadCharacterCorpse( const CharacterCorpsesRepository::CharacterCorpses &cc, const glm::vec4 &position ) { if (!cc.id) { LogCorpses("Unable to create a corpse entity for character corpse_id [{}]", cc.id); return nullptr; } const auto &items = CharacterCorpseItemsRepository::GetWhere( database, fmt::format( "`corpse_id` = {}", cc.id ) ); LootItems item_list; for (auto &i: items) { item_list.push_back( new LootItem{ .item_id = i.item_id, .equip_slot = static_cast(i.equip_slot), .charges = static_cast(i.charges), .lootslot = 0, .aug_1 = i.aug_1, .aug_2 = i.aug_2, .aug_3 = i.aug_3, .aug_4 = i.aug_4, .aug_5 = i.aug_5, .aug_6 = static_cast(i.aug_6), .attuned = static_cast(i.attuned) } ); } auto c = new Corpse( cc.id, cc.charid, cc.charname.c_str(), &item_list, cc.copper, cc.silver, cc.gold, cc.platinum, position, cc.size, cc.gender, cc.race, cc.class_, cc.deity, cc.level, cc.texture, cc.helm_texture, cc.exp, cc.gm_exp, static_cast(cc.killed_by), cc.rezzable, cc.rez_time, RuleB(Zone, EnableShadowrest) ? false : cc.was_at_graveyard ); if (cc.is_locked) { c->Lock(); } // item tints and appearance c->m_item_tint.Head.Color = cc.wc_1; c->m_item_tint.Chest.Color = cc.wc_2; c->m_item_tint.Arms.Color = cc.wc_3; c->m_item_tint.Wrist.Color = cc.wc_4; c->m_item_tint.Hands.Color = cc.wc_5; c->m_item_tint.Legs.Color = cc.wc_6; c->m_item_tint.Feet.Color = cc.wc_7; c->m_item_tint.Primary.Color = cc.wc_8; c->m_item_tint.Secondary.Color = cc.wc_9; c->haircolor = cc.hair_color; c->beardcolor = cc.beard_color; c->eyecolor1 = cc.eye_color_1; c->eyecolor2 = cc.eye_color_2; c->hairstyle = cc.hair_style; c->luclinface = cc.face; c->beard = cc.beard; c->drakkin_heritage = cc.drakkin_heritage; c->drakkin_tattoo = cc.drakkin_tattoo; c->drakkin_details = cc.drakkin_details; c->m_become_npc = false; c->m_consented_guild_id = cc.guild_consent_id; c->IsRezzed(cc.is_rezzed); c->UpdateEquipmentLight(); return c; }