Browse Source

Spell stacking restrictions integrated using linked_timer_id and a new field, type_group_spell_id

Fix #267 - spell stacking restrictions in place

min_class_skill_req is now pulled from the database
sint32 type_group_spell_id added to spells, can use this to distinguish spells of different classes not competing (eg. wards of templar/inquisitor dont stack)
GetSpellData and SetSpellData support min_class_skill_req and type_group_spell_id
Image 3 years ago
parent
commit
4663fa36c9

+ 1 - 0
DB/updates/spells_update_feb24_2021.sql

@@ -0,0 +1 @@
+alter table spells add column type_group_spell_id int(10) signed not null default '0';

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

@@ -931,6 +931,61 @@ SpellEffects* Entity::GetSpellEffect(int32 id, Entity* caster) {
 	return ret;
 }
 
+SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer, sint32 type_group_spell_id, Entity* caster) {
+	SpellEffects* ret = 0;
+	InfoStruct* info = GetInfoStruct();
+	MSpellEffects.readlock(__FUNCTION__, __LINE__);
+	for(int i = 0; i < 45; i++) {
+		if(info->spell_effects[i].spell_id != 0xFFFFFFFF)
+		{
+			if(  (info->spell_effects[i].spell_id == id && linked_timer == 0 && type_group_spell_id == 0) ||
+				 (linked_timer > 0 && info->spell_effects[i].spell->spell->GetSpellData()->linked_timer == linked_timer) ||
+				(type_group_spell_id > 0 && info->spell_effects[i].spell->spell->GetSpellData()->type_group_spell_id == type_group_spell_id))
+			{
+				if (type_group_spell_id >= -1 && (!caster || info->spell_effects[i].caster == caster)){
+					ret = &info->spell_effects[i];
+					break;
+				}
+			}
+		}
+	}
+	MSpellEffects.releasereadlock(__FUNCTION__, __LINE__);
+	return ret;
+}
+
+LuaSpell* Entity::HasLinkedTimerID(LuaSpell* spell, Spawn* target, bool stackWithOtherPlayers) {
+	if(!spell->spell->GetSpellData()->linked_timer)
+		return nullptr;
+	LuaSpell* ret = nullptr;
+	InfoStruct* info = GetInfoStruct();
+	MSpellEffects.readlock(__FUNCTION__, __LINE__);
+	//this for loop primarily handles self checks and 'friendly' checks
+	for(int i = 0; i < NUM_MAINTAINED_EFFECTS; i++) {
+			if(info->maintained_effects[i].spell_id != 0xFFFFFFFF)
+			{
+				if( ((info->maintained_effects[i].spell_id == spell->spell->GetSpellID() && spell->spell->GetSpellData()->type_group_spell_id >= 0) ||
+					(info->maintained_effects[i].spell->spell->GetSpellData()->linked_timer > 0 && info->maintained_effects[i].spell->spell->GetSpellData()->linked_timer == spell->spell->GetSpellData()->linked_timer) || 
+					(spell->spell->GetSpellData()->type_group_spell_id > 0 && spell->spell->GetSpellData()->type_group_spell_id == info->maintained_effects[i].spell->spell->GetSpellData()->type_group_spell_id)) && 
+					((spell->spell->GetSpellData()->friendly_spell) || 
+					(!spell->spell->GetSpellData()->friendly_spell && spell->spell->GetSpellData()->type_group_spell_id >= -1 && spell->caster == info->maintained_effects[i].spell->caster) ) &&
+					(target == nullptr || info->maintained_effects[i].spell->initial_target == target->GetID())) {
+					ret = info->maintained_effects[i].spell;
+					break;
+				}
+			}
+	}
+	MSpellEffects.releasereadlock(__FUNCTION__, __LINE__);
+
+	if(!ret && !stackWithOtherPlayers && target && target->IsEntity())
+	{
+		SpellEffects* effect = ((Entity*)target)->GetSpellEffectWithLinkedTimer(spell->spell->GetSpellID(), spell->spell->GetSpellData()->linked_timer, spell->spell->GetSpellData()->type_group_spell_id, nullptr);
+		if(effect)
+			ret = effect->spell;
+	}
+	
+	return ret;
+}
+
 InfoStruct* Entity::GetInfoStruct(){ 
 	return &info_struct; 
 }

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

@@ -1157,6 +1157,8 @@ public:
 	MaintainedEffects* GetFreeMaintainedSpellSlot();
 	SpellEffects* GetFreeSpellEffectSlot();
 	SpellEffects* GetSpellEffect(int32 id, Entity* caster = 0);
+	SpellEffects* GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer = 0, sint32 type_group_spell_id = 0, Entity* caster = 0);
+	LuaSpell* HasLinkedTimerID(LuaSpell* spell, Spawn* target = nullptr,  bool stackWithOtherPlayers = true);
 
 	//flags
 	int32 GetFlags() { return info_struct.get_flags(); }

+ 15 - 3
EQ2/source/WorldServer/Player.cpp

@@ -2282,6 +2282,7 @@ void Player::AddSpellBookEntry(int32 spell_id, int8 tier, sint32 slot, int32 typ
 	spell->recast_available = 0;
 	spell->player = this;
 	spell->visible = true;
+	spell->in_use = false;
 	MSpellsBook.lock();
 	spells.push_back(spell);
 	MSpellsBook.unlock();
@@ -2600,7 +2601,10 @@ void Player::UnlockAllSpells(bool modify_recast, Spell* exception) {
 		exception_spell_id = exception->GetSpellID();
 	MSpellsBook.writelock(__FUNCTION__, __LINE__);
 	for (itr = spells.begin(); itr != spells.end(); itr++) {
-		if ((*itr)->spell_id != exception_spell_id && (*itr)->type != SPELL_BOOK_TYPE_TRADESKILL)
+		if ((*itr)->in_use == false && 
+			 (((*itr)->spell_id != exception_spell_id || 
+			 (*itr)->timer > 0 && (*itr)->timer != exception->GetSpellData()->linked_timer)
+		&& (*itr)->type != SPELL_BOOK_TYPE_TRADESKILL))
 			AddSpellStatus((*itr), SPELL_STATUS_LOCK, modify_recast);
 	}
 
@@ -2613,8 +2617,13 @@ void Player::LockSpell(Spell* spell, int16 recast) {
 	MSpellsBook.writelock(__FUNCTION__, __LINE__);
 	for (itr = spells.begin(); itr != spells.end(); itr++) {
 		spell2 = *itr;
-		if (spell2->spell_id == spell->GetSpellID() /*|| (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer)*/)
+		if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer))
+		{
+			spell2->in_use = true;
 			RemoveSpellStatus(spell2, SPELL_STATUS_LOCK, true, recast);
+		}
+		else if(spell2->in_use)
+			RemoveSpellStatus(spell2, SPELL_STATUS_LOCK, false, 0);
 	}
 	MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
 }
@@ -2627,8 +2636,11 @@ void Player::UnlockSpell(Spell* spell) {
 	MSpellsBook.writelock(__FUNCTION__, __LINE__);
 	for (itr = spells.begin(); itr != spells.end(); itr++) {
 		spell2 = *itr;
-		if (spell2->spell_id == spell->GetSpellID() /*|| (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer)*/)
+		if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer))
+		{
+			spell2->in_use = false;
 			AddSpellStatus(spell2, SPELL_STATUS_LOCK);
+		}
 	}
 	MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
 }

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

@@ -161,6 +161,7 @@ struct SpellBookEntry{
 	int16	recast;
 	int32	timer;
 	bool	save_needed;
+	bool	in_use;
 	Player* player;
 	bool visible;
 };

+ 75 - 1
EQ2/source/WorldServer/SpellProcess.cpp

@@ -336,6 +336,11 @@ void SpellProcess::CheckInterrupt(InterruptStruct* interrupt){
 	entity->GetZone()->SendInterruptPacket(entity, interrupt->spell);
 	if(interrupt->error_code > 0)
 		entity->GetZone()->SendSpellFailedPacket(client, interrupt->error_code);
+	if(entity->IsPlayer())
+	{
+		((Player*)entity)->UnlockSpell(interrupt->spell->spell);
+		SendSpellBookUpdate(((Player*)entity)->GetClient());
+	}
 }
 
 bool SpellProcess::DeleteCasterSpell(Spawn* caster, Spell* spell, string reason){
@@ -377,11 +382,13 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason){
 							spell->caster->GetZone()->TriggerCharSheetTimer();
 					}
 				}
-				
 				CheckRecast(spell->spell, spell->caster);
 				if (spell->caster && spell->caster->IsPlayer())
 					SendSpellBookUpdate(spell->caster->GetZone()->GetClientBySpawn(spell->caster));
 			}
+			if(spell->caster->IsPlayer())
+				((Player*)spell->caster)->UnlockSpell(spell->spell);
+			
 			spell->caster->RemoveProc(0, spell);
 			spell->caster->RemoveMaintainedSpell(spell);
 			CheckRemoveTargetFromSpell(spell, false);
@@ -561,10 +568,15 @@ void SpellProcess::SendFinishedCast(LuaSpell* spell, Client* client){
 			UnlockAllSpells(client, spell->spell);
 		else
 			UnlockAllSpells(client);
+		
 		if(spell->resisted && spell->spell->GetSpellData()->recast > 0)
 			CheckRecast(spell->spell, client->GetPlayer(), 0.5); // half sec recast on resisted spells
 		else if (!spell->interrupted && spell->spell->GetSpellData()->cast_type != SPELL_CAST_TYPE_TOGGLE)
 			CheckRecast(spell->spell, client->GetPlayer());
+		else if(spell->caster && spell->caster->IsPlayer())
+		{
+			((Player*)spell->caster)->LockSpell(spell->spell, (int16)(spell->spell->GetSpellData()->recast * 10));
+		}
 		PacketStruct* packet = configReader.getStruct("WS_FinishCastSpell", client->GetVersion());
 		if(packet){
 			packet->setMediumStringByName("spell_name", spell->spell->GetSpellData()->name.data.c_str());			
@@ -994,6 +1006,41 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
 			DeleteSpell(lua_spell);
 			return;
 		}
+		int8 spell_type = lua_spell->spell->GetSpellData()->spell_type;
+
+		LuaSpell* conflictSpell = caster->HasLinkedTimerID(lua_spell, target, (spell_type == SPELL_TYPE_DD || spell_type == SPELL_TYPE_DOT));
+		if(conflictSpell)
+		{
+			if(conflictSpell->spell->GetSpellData()->min_class_skill_req > 0 && lua_spell->spell->GetSpellData()->min_class_skill_req > 0)
+			{
+				if(conflictSpell->spell->GetSpellData()->min_class_skill_req <= lua_spell->spell->GetSpellData()->min_class_skill_req)
+				{
+					if(spell->GetSpellData()->friendly_spell)
+					{
+						ZoneServer* zone = caster->GetZone();
+						Spawn* tmpTarget = zone->GetSpawnByID(conflictSpell->initial_target);
+						if(tmpTarget && tmpTarget->IsEntity())
+						{
+							zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
+							((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
+							if(client)
+								UnlockSpell(client, conflictSpell->spell);
+						}
+						return;
+					}
+					else if(lua_spell->spell->GetSpellData()->spell_type == SPELL_TYPE_DEBUFF)
+					{
+						SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell);
+						return;
+					}
+				}
+				else
+				{
+					SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell);
+					return;
+				}	
+			}
+		}
 
 		if ((caster->IsMezzed() && !spell->CastWhileMezzed()) || (caster->IsStunned() && !spell->CastWhileStunned())) 
 		{
@@ -1407,10 +1454,29 @@ bool SpellProcess::CastProcessedSpell(LuaSpell* spell, bool passive, bool in_her
 	if (spell->spell->GetSpellData()->max_aoe_targets > 0 && spell->targets.size() == 0) {
 		GetSpellTargetsTrueAOE(spell);
 		if (spell->targets.size() == 0) {
+			if(client)
+			{
+				client->GetPlayer()->UnlockAllSpells(true);
+				SendSpellBookUpdate(client);
+			}
 			spell->caster->GetZone()->SendSpellFailedPacket(client, SPELL_ERROR_NO_TARGETS_IN_RANGE);
 			return false;
 		}
 	}
+	
+	if(!spell->spell->GetSpellData()->friendly_spell)
+	{
+		ZoneServer* zone = client->GetCurrentZone();
+		Spawn* tmpTarget = zone->GetSpawnByID(spell->initial_target);
+		int8 spell_type = spell->spell->GetSpellData()->spell_type;
+		LuaSpell* conflictSpell = spell->caster->HasLinkedTimerID(spell, tmpTarget, (spell_type == SPELL_TYPE_DD || spell_type == SPELL_TYPE_DOT));
+		if(conflictSpell && tmpTarget && tmpTarget->IsEntity())
+		{
+			((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
+			zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
+		}
+	}
+	
 	MutexList<LuaSpell*>::iterator itr = active_spells.begin();
 	bool processedSpell = false;
 	LuaSpell* replace_spell = 0;
@@ -2552,3 +2618,11 @@ void SpellProcess::DeleteSpell(LuaSpell* spell)
 
 	safe_delete(spell);
 }
+
+void SpellProcess::SpellCannotStack(ZoneServer* zone, Client* client, Entity* caster, LuaSpell* lua_spell, LuaSpell* conflictSpell)
+{
+	LogWrite(SPELL__DEBUG, 1, "Spell", "%s cannot stack spell %s, conflicts with %s.", caster->GetName(), lua_spell->spell->GetName(), conflictSpell->spell->GetName());
+	zone->SendSpellFailedPacket(client, SPELL_ERROR_TAKE_EFFECT_MOREPOWERFUL);
+	lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
+	DeleteSpell(lua_spell);
+}

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

@@ -381,6 +381,8 @@ public:
 	void AddSpellCancel(LuaSpell* spell);
 
 	void DeleteSpell(LuaSpell* spell);
+
+	void SpellCannotStack(ZoneServer* zone, Client* client, Entity* caster, LuaSpell* lua_spell, LuaSpell* conflictSpell);
 private:
 	/// <summary>Sends the spell data to the lua script</summary>
 	/// <param name='spell'>LuaSpell to call the lua script for</param>

+ 25 - 0
EQ2/source/WorldServer/Spells.cpp

@@ -69,6 +69,7 @@ Spell::Spell(Spell* host_spell)
 		spell->cast_type = host_spell->GetSpellData()->cast_type;
 		spell->cast_while_moving = host_spell->GetSpellData()->cast_while_moving;
 		spell->class_skill = host_spell->GetSpellData()->class_skill;
+		spell->min_class_skill_req = host_spell->GetSpellData()->min_class_skill_req;
 		spell->control_effect_type = host_spell->GetSpellData()->control_effect_type;
 		spell->description = EQ2_16BitString(host_spell->GetSpellData()->description);
 		spell->det_type = host_spell->GetSpellData()->det_type;
@@ -139,6 +140,7 @@ Spell::Spell(Spell* host_spell)
 		spell->tier = host_spell->GetSpellData()->tier;
 		spell->ts_loc_index = host_spell->GetSpellData()->ts_loc_index;
 		spell->type = host_spell->GetSpellData()->type;
+		spell->type_group_spell_id = host_spell->GetSpellData()->type_group_spell_id;
 	}
 
 	heal_spell = host_spell->IsHealSpell();
@@ -726,6 +728,7 @@ void Spell::SetPacketInformation(PacketStruct* packet, Client* client, bool disp
 	packet->setSubstructDataByName("spell_info", "type", spell->type);
 	packet->setSubstructDataByName("spell_info", "unknown_MJ1d", 1); //63119 test
 	packet->setSubstructDataByName("spell_info", "class_skill", spell->class_skill);
+	packet->setSubstructDataByName("spell_info", "min_class_skill_req", spell->min_class_skill_req);
 	packet->setSubstructDataByName("spell_info", "mastery_skill", spell->mastery_skill);
 	packet->setSubstructDataByName("spell_info", "duration_flag", spell->duration_until_cancel);
 	if (client && spell->type != 2) {
@@ -1331,6 +1334,11 @@ bool Spell::GetSpellData(lua_State* state, std::string field)
 		lua_interface->SetInt32Value(state, GetSpellData()->class_skill);
 		valSet = true;
 	}
+	else if (field == "min_class_skill_req")
+	{
+		lua_interface->SetInt32Value(state, GetSpellData()->min_class_skill_req);
+		valSet = true;
+	}
 	else if (field == "mastery_skill")
 	{
 		lua_interface->SetInt32Value(state, GetSpellData()->mastery_skill);
@@ -1636,6 +1644,11 @@ bool Spell::GetSpellData(lua_State* state, std::string field)
 		lua_interface->SetSInt32Value(state, GetSpellData()->spell_name_crc);
 		valSet = true;
 	}
+	else if (field == "type_group_spell_id")
+	{
+		lua_interface->SetSInt32Value(state, GetSpellData()->type_group_spell_id);
+		valSet = true;
+	}
 
 	return valSet;
 }
@@ -1683,6 +1696,12 @@ bool Spell::SetSpellData(lua_State* state, std::string field, int8 fieldArg)
 		GetSpellData()->class_skill = class_skill;
 		valSet = true;
 	}
+	else if (field == "min_class_skill_req")
+	{
+		int16 min_class_skill_req = lua_interface->GetInt16Value(state, fieldArg);
+		GetSpellData()->min_class_skill_req = min_class_skill_req;
+		valSet = true;
+	}
 	else if (field == "mastery_skill")
 	{
 		int32 mastery_skill = lua_interface->GetInt32Value(state, fieldArg);
@@ -2031,6 +2050,12 @@ bool Spell::SetSpellData(lua_State* state, std::string field, int8 fieldArg)
 		GetSpellData()->spell_type = spell_type;
 		valSet = true;
 	}
+	else if (field == "type_group_spell_id")
+	{
+		sint32 type_group_spell_id = lua_interface->GetSInt32Value(state, fieldArg);
+		GetSpellData()->type_group_spell_id = type_group_spell_id;
+		valSet = true;
+	}
 
 	return valSet;
 }

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

@@ -224,6 +224,7 @@ struct SpellData{
 	int16	icon_backdrop;
 	int16	type;
 	int32	class_skill;
+	int16	min_class_skill_req;
 	int32	mastery_skill;
 	int8    ts_loc_index;
 	int8	num_levels;
@@ -286,6 +287,7 @@ struct SpellData{
 	int32	soe_spell_crc;
 	int8	spell_type;
 	int32	spell_name_crc;
+	sint32	type_group_spell_id;
 };
 class Spell{
 public:

+ 3 - 2
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -4555,7 +4555,7 @@ void WorldDatabase::LoadSpells()
 	int32 total = 0;
 	map<int32, vector<LevelArray*> >* level_data = LoadSpellClasses();
 
-	if( !database_new.Select(&result, "SELECT s.`id`, ts.spell_id, ts.index, `name`, `description`, `type`, `class_skill`, `mastery_skill`, `tier`, `is_aa`,`hp_req`, `power_req`,`power_by_level`, `cast_time`, `recast`, `radius`, `max_aoe_targets`, `req_concentration`, `range`, `duration1`, `duration2`, `resistibility`, `hp_upkeep`, `power_upkeep`, `duration_until_cancel`, `target_type`, `recovery`, `power_req_percent`, `hp_req_percent`, `icon`, `icon_heroic_op`, `icon_backdrop`, `success_message`, `fade_message`, `fade_message_others`, `cast_type`, `lua_script`, `call_frequency`, `interruptable`, `spell_visual`, `effect_message`, `min_range`, `can_effect_raid`, `affect_only_group_members`, `hit_bonus`, `display_spell_tier`, `friendly_spell`, `group_spell`, `spell_book_type`, spell_type+0, s.is_active, savagery_req, savagery_req_percent, savagery_upkeep, dissonance_req, dissonance_req_percent, dissonance_upkeep, linked_timer_id, det_type, incurable, control_effect_type, cast_while_moving, casting_flags, persist_through_death, not_maintained, savage_bar, savage_bar_slot, soe_spell_crc, 0xffffffff-CRC32(s.`name`) as 'spell_name_crc' "
+	if( !database_new.Select(&result, "SELECT s.`id`, ts.spell_id, ts.index, `name`, `description`, `type`, `class_skill`, `min_class_skill_req`, `mastery_skill`, `tier`, `is_aa`,`hp_req`, `power_req`,`power_by_level`, `cast_time`, `recast`, `radius`, `max_aoe_targets`, `req_concentration`, `range`, `duration1`, `duration2`, `resistibility`, `hp_upkeep`, `power_upkeep`, `duration_until_cancel`, `target_type`, `recovery`, `power_req_percent`, `hp_req_percent`, `icon`, `icon_heroic_op`, `icon_backdrop`, `success_message`, `fade_message`, `fade_message_others`, `cast_type`, `lua_script`, `call_frequency`, `interruptable`, `spell_visual`, `effect_message`, `min_range`, `can_effect_raid`, `affect_only_group_members`, `hit_bonus`, `display_spell_tier`, `friendly_spell`, `group_spell`, `spell_book_type`, spell_type+0, s.is_active, savagery_req, savagery_req_percent, savagery_upkeep, dissonance_req, dissonance_req_percent, dissonance_upkeep, linked_timer_id, det_type, incurable, control_effect_type, cast_while_moving, casting_flags, persist_through_death, not_maintained, savage_bar, savage_bar_slot, soe_spell_crc, 0xffffffff-CRC32(s.`name`) as 'spell_name_crc', type_group_spell_id "
 									"FROM (spells s, spell_tiers st) "
 									"LEFT JOIN spell_ts_ability_index ts "
 									"ON s.`id` = ts.spell_id "
@@ -4614,8 +4614,8 @@ void WorldDatabase::LoadSpells()
 
 			/* Skill Requirements */
 			data->class_skill				= result.GetInt32Str("class_skill");
+			data->min_class_skill_req		= result.GetInt16Str("min_class_skill_req");
 			data->mastery_skill				= result.GetInt32Str("mastery_skill");
-			// no min_class_skill_req?
 
 			/* Cost  */
 			data->req_concentration			= result.GetInt16Str("req_concentration");
@@ -4653,6 +4653,7 @@ void WorldDatabase::LoadSpells()
 			data->resistibility				= result.GetFloatStr("resistibility");
 			data->linked_timer				= result.GetInt32Str("linked_timer_id");
 			data->spell_name_crc			= result.GetInt32Str("spell_name_crc");
+			data->type_group_spell_id		= result.GetSInt32Str("type_group_spell_id");
 
 			/* Cast Messaging */
 			string message					= result.GetStringStr("success_message");

+ 21 - 0
EQ2/source/WorldServer/zoneserver.cpp

@@ -7814,4 +7814,25 @@ void ZoneServer::ProcessSpawnRemovals()
 		MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
 	}
 	MPendingSpawnRemoval.releasewritelock(__FUNCTION__, __LINE__);
+}
+
+void ZoneServer::AddSpawnToGroup(Spawn* spawn, int32 group_id)
+{
+	if( spawn->GetSpawnGroupID() > 0 )
+		spawn->RemoveSpawnFromGroup();
+	MutexList<int32>* groupList = &spawn_group_map.Get(group_id);
+	MutexList<int32>::iterator itr2 = groupList->begin();
+	
+	while(itr2.Next())
+	{
+		Spawn* groupSpawn = GetSpawnByID(itr2.value);
+		if(groupSpawn)
+		{
+			// found existing group member to add it in
+			spawn->AddSpawnToGroup(groupSpawn);
+			break;
+		}
+	}
+	groupList->Add(spawn->GetID());
+	spawn->SetSpawnGroupID(group_id);
 }

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

@@ -666,6 +666,8 @@ public:
 	void ProcessSpawnRemovals();
 
 	bool	SendRemoveSpawn(Client* client, Spawn* spawn, PacketStruct* packet = 0, bool delete_spawn = false);
+
+	void	AddSpawnToGroup(Spawn* spawn, int32 group_id);
 private:
 #ifndef WIN32
 	pthread_t ZoneThread;