Browse Source

Cygni Updates

- Deadlock fix on spawn removal and deleting spawns
- Fix #358 lootdrop will be checked against completed quest identified by ID
alter table lootdrop add column no_drop_quest_completed int(10) unsigned not null default 0;
- zone instance type now cached into a map after querying zone id (so we don't continuously query)
- mentor now removes spells on the person mentoring
- Fix #354 - GetZoneLockoutTimer(Player, ZoneID, displayClient=false) added, returns string of lockout time (includes a space at the beginning).  DisplayClient = true will send the message to the client.
- Can't hail dead spawns
- Crash fix in TempRemoveGroup when group id no longer exists
Image 2 years ago
parent
commit
1af9491eb4

+ 1 - 1
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -2035,7 +2035,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				if (spawn->IsNPC())
 					show_bubble = false;
 				client->GetCurrentZone()->HandleChatMessage(client->GetPlayer(), 0, CHANNEL_SAY, tmp, HEAR_SPAWN_DISTANCE, 0, show_bubble);
-				if(spawn->IsPlayer() == false && spawn->GetDistance(client->GetPlayer()) < rule_manager.GetGlobalRule(R_Spawn, HailDistance)->GetInt32()){
+				if(spawn->IsPlayer() == false && spawn->Alive() && spawn->GetDistance(client->GetPlayer()) < rule_manager.GetGlobalRule(R_Spawn, HailDistance)->GetInt32()){
 					if(spawn->IsNPC() && ((NPC*)spawn)->EngagedInCombat())
 						spawn->GetZone()->CallSpawnScript(spawn, SPAWN_SCRIPT_HAILED_BUSY, client->GetPlayer());
 					else

+ 17 - 0
EQ2/source/WorldServer/Entity.cpp

@@ -138,6 +138,23 @@ void Entity::DeleteSpellEffects(bool removeClient)
 	}
 }
 
+void Entity::RemoveSpells()
+{
+	
+	for(int i=0;i<45;i++){
+		if(i<30){
+			if(GetInfoStruct()->maintained_effects[i].spell_id != 0xFFFFFFFF)
+			{
+				GetZone()->GetSpellProcess()->AddSpellCancel(GetInfoStruct()->maintained_effects[i].spell);
+			}
+		}
+		if(GetInfoStruct()->spell_effects[i].spell_id != 0xFFFFFFFF)
+		{
+			RemoveSpellEffect(GetInfoStruct()->spell_effects[i].spell);
+		}
+	}
+}
+
 void Entity::MapInfoStruct()
 {
 /** GETS **/

+ 1 - 0
EQ2/source/WorldServer/Entity.h

@@ -1097,6 +1097,7 @@ public:
 	virtual ~Entity();
 
 	void DeleteSpellEffects(bool removeClient = false);
+	void RemoveSpells();
 	void MapInfoStruct();
 	virtual float GetDodgeChance();
 	virtual void AddMaintainedSpell(LuaSpell* spell);

+ 2 - 1
EQ2/source/WorldServer/Items/LootDB.cpp

@@ -56,7 +56,7 @@ void WorldDatabase::LoadLoot(ZoneServer* zone)
 	}
 	
 	// Now, load Loot Drops for configured loot tables
-	if (database_new.Select(&result, "SELECT loot_table_id, item_id, item_charges, equip_item, probability FROM lootdrop")) {
+	if (database_new.Select(&result, "SELECT loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed FROM lootdrop")) {
 		count = 0;
 		LogWrite(LOOT__DEBUG, 0, "Loot", "--Loading LootDrops...");
 		LootDrop* drop = 0;
@@ -68,6 +68,7 @@ void WorldDatabase::LoadLoot(ZoneServer* zone)
 			drop->item_charges = result.GetInt16Str("item_charges");
 			drop->equip_item = (result.GetInt8Str("equip_item") == 1);
 			drop->probability = result.GetFloatStr("probability");
+			drop->no_drop_quest_completed_id = result.GetInt32Str("no_drop_quest_completed");
 			zone->AddLootDrop(id, drop);
 
 			LogWrite(LOOT__DEBUG, 5, "Loot", "---Loading LootDrop item_id %u (tableID: %u", drop->item_id, id);

+ 27 - 0
EQ2/source/WorldServer/LuaFunctions.cpp

@@ -12240,6 +12240,7 @@ int EQ2Emu_lua_GetArrowColor(lua_State* state) {
 	Player* player = (Player*)lua_interface->GetSpawn(state);
 	int8 level = lua_interface->GetInt8Value(state, 2);
 	lua_interface->ResetFunctionStack(state);
+
 	if (player && player->IsPlayer() && level > 0) {
 		lua_interface->SetInt32Value(state, player->GetArrowColor(level));
 		return 1;
@@ -12250,6 +12251,7 @@ int EQ2Emu_lua_GetTSArrowColor(lua_State* state) {
 	Player* player = (Player*)lua_interface->GetSpawn(state);
 	int8 level = lua_interface->GetInt8Value(state, 2);
 	lua_interface->ResetFunctionStack(state);
+
 	if (player && player->IsPlayer() && level > 0) {
 		lua_interface->SetInt32Value(state, player->GetTSArrowColor(level));
 		return 1;
@@ -12260,6 +12262,7 @@ int EQ2Emu_lua_GetTSArrowColor(lua_State* state) {
 int EQ2Emu_lua_GetSpawnByRailID(lua_State* state) {
 	ZoneServer* zone = lua_interface->GetZone(state);
 	sint64 rail_id = lua_interface->GetSInt64Value(state, 2);
+	lua_interface->ResetFunctionStack(state);
 
 	if (zone) {
 		Spawn* spawn = zone->GetTransportByRailID(rail_id);
@@ -12298,6 +12301,7 @@ int EQ2Emu_lua_SetRailID(lua_State* state) {
 
 int EQ2Emu_lua_IsZoneLoading(lua_State* state) {
 	ZoneServer* zone = lua_interface->GetZone(state);
+	lua_interface->ResetFunctionStack(state);
 
 	if (zone) {
 		lua_interface->SetBooleanValue(state, zone->IsLoading());
@@ -12307,10 +12311,33 @@ int EQ2Emu_lua_IsZoneLoading(lua_State* state) {
 }
 int EQ2Emu_lua_IsRunning(lua_State* state) {
 	Spawn* spawn = lua_interface->GetSpawn(state);
+	lua_interface->ResetFunctionStack(state);
 
 	if (spawn) {
 		lua_interface->SetBooleanValue(state, spawn->IsRunning());
 		return 1;
 	}
 	return 0;
+}
+
+int EQ2Emu_lua_GetZoneLockoutTimer(lua_State* state) {
+	if (!lua_interface)
+		return 0;
+	Spawn* player = lua_interface->GetSpawn(state);
+	int32 zoneID = lua_interface->GetInt32Value(state, 2);
+	bool displayClient = lua_interface->GetInt32Value(state, 3);
+	lua_interface->ResetFunctionStack(state);
+
+	if (!player || !player->IsPlayer() || !player->GetClient()) {
+		lua_interface->LogError("%s: LUA GetZoneLockoutTimer command error: player is not valid, does not exist", lua_interface->GetScriptName(state));
+	}
+	else if(!zoneID) {
+		lua_interface->LogError("%s: LUA GetZoneLockoutTimer command error: zoneID is not set.");
+	}
+	else
+	{
+		lua_interface->SetStringValue(state, player->GetClient()->IdentifyInstanceLockout(zoneID, displayClient).c_str());
+		return 1;
+	}
+	return 0;
 }

+ 2 - 0
EQ2/source/WorldServer/LuaFunctions.h

@@ -593,4 +593,6 @@ int EQ2Emu_lua_GetSpawnByRailID(lua_State* state);
 int EQ2Emu_lua_SetRailID(lua_State* state);
 int EQ2Emu_lua_IsZoneLoading(lua_State* state);
 int EQ2Emu_lua_IsRunning(lua_State* state);
+
+int EQ2Emu_lua_GetZoneLockoutTimer(lua_State* state);
 #endif

+ 2 - 0
EQ2/source/WorldServer/LuaInterface.cpp

@@ -1422,6 +1422,8 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	lua_register(state, "SetRailID", EQ2Emu_lua_SetRailID);
 	lua_register(state, "IsZoneLoading", EQ2Emu_lua_IsZoneLoading);
 	lua_register(state, "IsRunning", EQ2Emu_lua_IsRunning);
+	
+	lua_register(state, "GetZoneLockoutTimer", EQ2Emu_lua_GetZoneLockoutTimer);
 }
 
 void LuaInterface::LogError(const char* error, ...)  {

+ 1 - 0
EQ2/source/WorldServer/Player.cpp

@@ -6685,6 +6685,7 @@ void Player::MentorTarget()
 
 void Player::SetMentorStats(int32 effective_level, int32 target_char_id)
 {
+	RemoveSpells();
 	if(client->GetPlayer()->GetGroupMemberInfo())
 		client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id = target_char_id;
 	InfoStruct* info = GetInfoStruct();

+ 28 - 2
EQ2/source/WorldServer/PlayerGroups.cpp

@@ -83,9 +83,10 @@ bool PlayerGroup::RemoveMember(Entity* member) {
 	}
 
 	bool ret = false;
-	
-	member->SetGroupMemberInfo(0);
+
 	MGroupMembers.writelock();
+	member->SetGroupMemberInfo(0);
+
 	deque<GroupMemberInfo*>::iterator erase_itr = m_members.end();
 	deque<GroupMemberInfo*>::iterator itr;
 	for (itr = m_members.begin(); itr != m_members.end(); itr++) {
@@ -538,6 +539,31 @@ void PlayerGroupManager::SendGroupQuests(int32 group_id, Client* client) {
 	MGroups.releasereadlock(__FUNCTION__, __LINE__);
 }
 
+bool PlayerGroupManager::HasGroupCompletedQuest(int32 group_id, int32 quest_id) {
+	bool questComplete = true;
+	GroupMemberInfo* info = 0;
+	MGroups.readlock(__FUNCTION__, __LINE__);
+	if (m_groups.count(group_id) > 0) {
+		m_groups[group_id]->MGroupMembers.readlock(__FUNCTION__, __LINE__);
+		deque<GroupMemberInfo*>* members = m_groups[group_id]->GetMembers();
+		deque<GroupMemberInfo*>::iterator itr;
+		for (itr = members->begin(); itr != members->end(); itr++) {
+			info = *itr;
+			if (info->client) {
+				bool isComplete = info->client->GetPlayer()->GetCompletedQuest(quest_id);
+				if(!isComplete)
+				{
+					questComplete = isComplete;
+					break;
+				}
+			}
+		}
+		m_groups[group_id]->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__);
+	}
+	MGroups.releasereadlock(__FUNCTION__, __LINE__);
+	return questComplete;
+}
+
 void PlayerGroupManager::SimpleGroupMessage(int32 group_id, const char* message) {
 	MGroups.readlock(__FUNCTION__, __LINE__);
 

+ 1 - 0
EQ2/source/WorldServer/PlayerGroups.h

@@ -165,6 +165,7 @@ public:
 	int32 GetGroupSize(int32 group_id);
 
 	void SendGroupQuests(int32 group_id, Client* client);
+	bool HasGroupCompletedQuest(int32 group_id, int32 quest_id);
 
 	void SimpleGroupMessage(int32 group_id, const char* message);
 	void GroupMessage(int32 group_id, const char* message, ...);

+ 1 - 1
EQ2/source/WorldServer/Spawn.cpp

@@ -2016,7 +2016,7 @@ void Spawn::InitializePosPacketData(Player* player, PacketStruct* packet, bool b
 	int16 version = packet->GetVersion();
 
 	int32 new_grid_id = 0;
-	if(player->GetMap() != nullptr)
+	if(player->GetMap() != nullptr && player->GetMap()->IsMapLoaded())
 	{
 		std::map<int32,TimedGridData>::iterator itr = established_grid_id.find(version);
 		if ( itr == established_grid_id.end() || itr->second.timestamp <= (Timer::GetCurrentTime2()))

+ 1 - 0
EQ2/source/WorldServer/World.h

@@ -77,6 +77,7 @@ struct LootDrop{
 	int16	item_charges;
 	bool	equip_item;
 	float	probability;
+	int32	no_drop_quest_completed_id;
 };
 
 struct GroundSpawnEntry {

+ 6 - 0
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -3902,6 +3902,11 @@ bool WorldDatabase::VerifyZone(const char* name){
 
 int8 WorldDatabase::GetInstanceTypeByZoneID(int32 zoneID)
 {
+
+	std::map<int32, int8>::iterator itr = zone_instance_types.find(zoneID);
+	if(itr != zone_instance_types.end())
+		return itr->second;
+	
 	DatabaseResult result;
 	int8 ret = 0;
 
@@ -3917,6 +3922,7 @@ int8 WorldDatabase::GetInstanceTypeByZoneID(int32 zoneID)
 	{
 		result.Next();
 		ret = (result.GetInt8Str("instance_type+0") == 0) ? 0 : result.GetInt8Str("instance_type+0") - 1;
+		zone_instance_types.insert(make_pair(zoneID, ret));
 		LogWrite(INSTANCE__DEBUG, 0, "Instance", "Found instance type %i for zone_id %u", ret, zoneID);
 	}
 	else

+ 2 - 1
EQ2/source/WorldServer/WorldDatabase.h

@@ -612,10 +612,11 @@ public:
 	void				LoadCharacterSpellEffects(int32 char_id, Client *client, int8 db_spell_type);
 private:
 	DatabaseNew			database_new;
-	map<int32, string>	zone_names;
+	std::map<int32, string>	zone_names;
 	string				skills;
 	int32				max_zonename;
 	char**				zonename_array;
+	std::map<int32, int8>	zone_instance_types;
 };
 #endif
 

+ 62 - 24
EQ2/source/WorldServer/client.cpp

@@ -3641,19 +3641,13 @@ bool Client::Summon(const char* search_name) {
 		return true;
 }
 
-bool Client::TryZoneInstance(int32 zoneID, bool zone_coords_valid) {
-	ZoneServer* instance_zone = NULL;
-	int8 instanceType = 0;
-
-	// determine if this is a group instanced zone that already exists
-	instance_zone = GetPlayer()->GetGroupMemberInZone(zoneID);
-
-	if (instance_zone != NULL)
-		Zone(instance_zone->GetInstanceID(), zone_coords_valid, true);
-	else if ((instanceType = database.GetInstanceTypeByZoneID(zoneID)) > 0)
-	{
-		// best to check if we already have our own instance!
-		InstanceData* data = GetPlayer()->GetCharacterInstances()->FindInstanceByZoneID(zoneID);
+std::string Client::IdentifyInstanceLockout(int32 zoneID, bool displayClient) {
+	int8 instanceType = database.GetInstanceTypeByZoneID(zoneID);
+	if(instanceType < 1)
+		return std::string("");
+	
+	ZoneServer* instance_zone = nullptr;
+	InstanceData* data = GetPlayer()->GetCharacterInstances()->FindInstanceByZoneID(zoneID);
 		if (data) {
 			// If lockout instances check to see if we are locked out
 			if (instanceType == SOLO_LOCKOUT_INSTANCE || instanceType == GROUP_LOCKOUT_INSTANCE || instanceType == RAID_LOCKOUT_INSTANCE) {
@@ -3690,14 +3684,14 @@ bool Client::TryZoneInstance(int32 zoneID, bool zone_coords_valid) {
 
 					if (hour > 0) {
 						char temp[10];
-						sprintf(temp, " %i", hour);
+						snprintf(temp, 9," %i", hour);
 						time_msg.append(temp);
 						time_msg.append(" hour");
 						time_msg.append((hour > 1) ? "s" : "");
 					}
 					if (min > 0) {
 						char temp[5];
-						sprintf(temp, " %i", min);
+						snprintf(temp, 4," %i", min);
 						time_msg.append(temp);
 						time_msg.append(" minute");
 						time_msg.append((min > 1) ? "s" : "");
@@ -3705,16 +3699,34 @@ bool Client::TryZoneInstance(int32 zoneID, bool zone_coords_valid) {
 					// Only add seconds if minutes and hours are 0
 					if (hour == 0 && min == 0 && sec > 0) {
 						char temp[5];
-						sprintf(temp, " %i", sec);
+						snprintf(temp, 4," %i", sec);
 						time_msg.append(temp);
 						time_msg.append(" second");
 						time_msg.append((sec > 1) ? "s" : "");
 					}
 
-					Message(CHANNEL_COLOR_YELLOW, "You may not enter again for%s.", time_msg.c_str());
-					return true;
+					if(displayClient)
+						Message(CHANNEL_COLOR_YELLOW, "You may not enter again for%s.", time_msg.c_str());
+					return time_msg;
 				}
 			}
+		}
+	return std::string("");
+}
+
+ZoneServer* Client::IdentifyInstance(int32 zoneID) {
+	int8 instanceType = database.GetInstanceTypeByZoneID(zoneID);
+	if(instanceType < 1)
+		return nullptr;
+
+	ZoneServer* instance_zone = nullptr;
+	InstanceData* data = GetPlayer()->GetCharacterInstances()->FindInstanceByZoneID(zoneID);
+		if (data) {
+			std::string lockoutTime = IdentifyInstanceLockout(zoneID);
+			// If lockout instances check to see if we are locked out
+			if (lockoutTime.length() > 0) {
+					return nullptr;
+			}
 
 			// Need to update `character_instances` table with new timestamps (for persistent) and instance id's
 			instance_zone = zone_list.GetByInstanceID(data->instance_id, zoneID);
@@ -3731,7 +3743,23 @@ bool Client::TryZoneInstance(int32 zoneID, bool zone_coords_valid) {
 				data->instance_id = instance_zone->GetInstanceID();
 			}
 		}
-		else {
+	return instance_zone;
+}
+
+bool Client::TryZoneInstance(int32 zoneID, bool zone_coords_valid) {
+	ZoneServer* instance_zone = NULL;
+	int8 instanceType = 0;
+
+	// determine if this is a group instanced zone that already exists
+	instance_zone = GetPlayer()->GetGroupMemberInZone(zoneID);
+
+	if (instance_zone != NULL)
+		Zone(instance_zone->GetInstanceID(), zone_coords_valid, true);
+	else if ((instanceType = database.GetInstanceTypeByZoneID(zoneID)) > 0)
+	{
+		// best to check if we already have our own instance!
+		if((instance_zone = IdentifyInstance(zoneID)) == nullptr)
+		{
 			switch (instanceType)
 			{
 			case SOLO_LOCKOUT_INSTANCE:
@@ -10313,12 +10341,22 @@ void Client::TempRemoveGroup()
 	{
 		world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__);
 		PlayerGroup* group = world.GetGroupManager()->GetGroup(this->GetPlayer()->GetGroupMemberInfo()->group_id);
-		group->MGroupMembers.writelock();
-		this->GetPlayer()->GetGroupMemberInfo()->client = 0;
-		this->GetPlayer()->GetGroupMemberInfo()->member = 0;
-		group->MGroupMembers.releasewritelock();
+		if(group)
+		{
+			group->MGroupMembers.writelock();
+			this->GetPlayer()->GetGroupMemberInfo()->client = 0;
+			this->GetPlayer()->GetGroupMemberInfo()->member = 0;
+			this->GetPlayer()->SetGroupMemberInfo(0);
+			group->MGroupMembers.releasewritelock();
 
-		group->RemoveClientReference(this);
+			group->RemoveClientReference(this);
+		}
+		else
+		{
+			this->GetPlayer()->GetGroupMemberInfo()->client = 0;
+			this->GetPlayer()->GetGroupMemberInfo()->member = 0;
+			this->GetPlayer()->SetGroupMemberInfo(0);
+		}
 
 		world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);
 	}

+ 2 - 0
EQ2/source/WorldServer/client.h

@@ -186,6 +186,8 @@ public:
 	void	ChangeLevel(int16 old_level, int16 new_level);
 	void	ChangeTSLevel(int16 old_level, int16 new_level);
 	bool	Summon(const char* search_name);
+	std::string	IdentifyInstanceLockout(int32 zoneID, bool displayClient = true);
+	ZoneServer*	IdentifyInstance(int32 zoneID);
 	bool	TryZoneInstance(int32 zoneID, bool zone_coords_valid=false);
 	bool	GotoSpawn(const char* search_name, bool forceTarget=false);
 	void	DisplayDeadWindow();

+ 46 - 5
EQ2/source/WorldServer/zoneserver.cpp

@@ -1209,6 +1209,7 @@ void ZoneServer::RemovePendingDelete(Spawn* spawn) {
 
 void ZoneServer::DeleteSpawns(bool delete_all) {
 	MSpawnDeleteList.writelock(__FUNCTION__, __LINE__);
+	MPendingSpawnRemoval.readlock(__FUNCTION__, __LINE__);
 	if(spawn_delete_list.size() > 0){
 		map<Spawn*, int32>::iterator itr;
 		map<Spawn*, int32>::iterator erase_itr;
@@ -1216,6 +1217,10 @@ void ZoneServer::DeleteSpawns(bool delete_all) {
 		Spawn* spawn = 0;
 		for (itr = spawn_delete_list.begin(); itr != spawn_delete_list.end(); ) {
 			if (delete_all || current_time >= itr->second){
+				// we haven't removed it from the spawn list yet..
+				if(!delete_all && m_pendingSpawnRemove.count(itr->first->GetID()))
+					continue;
+				
 				spawn = itr->first;
 				if (spawn && movementMgr != nullptr) {
 					movementMgr->RemoveMob((Entity*)spawn);
@@ -1228,6 +1233,7 @@ void ZoneServer::DeleteSpawns(bool delete_all) {
 				itr++;
 		}
 	}
+	MPendingSpawnRemoval.releasereadlock(__FUNCTION__, __LINE__);
 	MSpawnDeleteList.releasewritelock(__FUNCTION__, __LINE__);
 }
 
@@ -2328,7 +2334,7 @@ void ZoneServer::ProcessSpawnLocations()
 	LogWrite(SPAWN__TRACE, 0, "Spawn", "Exit %s", __FUNCTION__);
 }
 
-void ZoneServer::AddLoot(NPC* npc){
+void ZoneServer::AddLoot(NPC* npc, Spawn* killer){
 	vector<int32> loot_tables = GetSpawnLootList(npc->GetDatabaseID(), GetZoneID(), npc->GetLevel(), race_types_list.GetRaceType(npc->GetModelType()), npc);
 	if(loot_tables.size() > 0){
 		vector<LootDrop*>* loot_drops = 0;
@@ -2343,7 +2349,8 @@ void ZoneServer::AddLoot(NPC* npc){
 		// the following loop,loops through each table
 		for(loot_list_itr = loot_tables.begin(); loot_list_itr != loot_tables.end(); loot_list_itr++){
 			table = GetLootTable(*loot_list_itr);
-			if(table && table->maxcoin > 0){
+			// if killer is assigned this is on-death, we already assigned coin
+			if(!killer && table && table->maxcoin > 0){
 				chancecoin = rand()%100;
 				if(table->coin_probability >= chancecoin){
 					if(table->maxcoin > table->mincoin)
@@ -2386,6 +2393,36 @@ void ZoneServer::AddLoot(NPC* npc){
 							for (loot_drop_itr = loot_drops->begin(); loot_drop_itr != loot_drops->end(); loot_drop_itr++) {
 								drop = *loot_drop_itr;
 
+								// if no killer is provided, then we are instantiating the spawn loot, quest related loot should be added on death of the spawn to check against spawn/group members
+								if(drop->no_drop_quest_completed_id && killer == nullptr)
+									continue;
+								else if(!drop->no_drop_quest_completed_id && killer) // skip since this doesn't have a quest id attached and we are doing after-math of death loot additions
+									continue;
+								else if(killer && drop->no_drop_quest_completed_id) // check if the player already completed quest related to item
+								{
+									Player* player = nullptr;
+									if(killer->IsPlayer())
+									{
+										player = (Player*)killer;
+										// player has already completed the quest
+										if(player->GetCompletedQuest(drop->no_drop_quest_completed_id) && !player->GetGroupMemberInfo())
+										{
+											LogWrite(PLAYER__DEBUG, 0, "Player", "%s: Player has completed quest %u, skipping loot item %u", npc->GetName(), drop->no_drop_quest_completed_id, drop->item_id);
+											continue;
+										}
+										else if(player->GetGroupMemberInfo() && world.GetGroupManager()->HasGroupCompletedQuest(player->GetGroupMemberInfo()->group_id, drop->no_drop_quest_completed_id))
+										{
+											LogWrite(PLAYER__DEBUG, 0, "Player", "%s: Group %u has completed quest %u, skipping loot item %u", npc->GetName(), player->GetGroupMemberInfo()->group_id, drop->no_drop_quest_completed_id, drop->item_id);
+											continue;
+										}
+									}
+									else
+									{
+										LogWrite(PLAYER__DEBUG, 0, "Player", "%s: Killer is not a player, skipping loot item %u", npc->GetName(), drop->item_id);
+										continue;
+									}
+								}
+
 								if (npc->HasLootItemID(drop->item_id))
 									continue;
 
@@ -4227,6 +4264,10 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 
 	if(dead->IsEntity())
 	{
+		// add any special quest related loot (no_drop_quest_completed)
+		if(dead->IsNPC() && killer && killer != dead)
+			AddLoot((NPC*)dead, killer);
+		
 		((Entity*)dead)->InCombat(false);
 		dead->SetInitialState(16512, false); // This will make aerial npc's fall after death
 		dead->SetHP(0);
@@ -7917,7 +7958,7 @@ Spawn* ZoneServer::GetSpawnFromUniqueItemID(int32 unique_id)
 void ZoneServer::AddPendingSpawnRemove(int32 id)
 {
 		MPendingSpawnRemoval.writelock(__FUNCTION__, __LINE__);
-		m_pendingSpawnRemove.push_back(id);
+		m_pendingSpawnRemove.insert(make_pair(id,true));
 		MPendingSpawnRemoval.releasewritelock(__FUNCTION__, __LINE__);
 }
 
@@ -7926,9 +7967,9 @@ void ZoneServer::ProcessSpawnRemovals()
 	MSpawnList.writelock(__FUNCTION__, __LINE__);
 	MPendingSpawnRemoval.writelock(__FUNCTION__, __LINE__);
 	if (m_pendingSpawnRemove.size() > 0) {
-		vector<int32>::iterator itr2;
+		map<int32,bool>::iterator itr2;
 		for (itr2 = m_pendingSpawnRemove.begin(); itr2 != m_pendingSpawnRemove.end(); itr2++) 
-			spawn_list.erase(*itr2);
+			spawn_list.erase(itr2->first);
 
 		m_pendingSpawnRemove.clear();
 	}

+ 2 - 2
EQ2/source/WorldServer/zoneserver.h

@@ -290,7 +290,7 @@ public:
 	void	ApplySetSpawnCommand(Client* client, Spawn* target, int8 type, const char* value);
 	void	SetSpawnCommand(Spawn* spawn, int8 type, char* value, Client* client = 0);
 	void	SetSpawnCommand(int32 spawn_id, int8 type, char* value, Client* client = 0);
-	void	AddLoot(NPC* npc);
+	void	AddLoot(NPC* npc, Spawn* killer = nullptr);
 	
 	NPC*	AddNPCSpawn(SpawnLocation* spawnlocation, SpawnEntry* spawnentry);
 	Object*	AddObjectSpawn(SpawnLocation* spawnlocation, SpawnEntry* spawnentry);
@@ -955,7 +955,7 @@ private:
 	
 	int32 watchdogTimestamp;
 
-	vector<int32> m_pendingSpawnRemove;
+	std::map<int32, bool> m_pendingSpawnRemove;
 	Mutex MPendingSpawnRemoval;
 
 	std::map<int32, int32> lua_queued_state_commands;