Browse Source

Complete persisting of buff spells cross zone

Complete Fix #305

- Other players pets (grouped) will persist buffs, non-grouped direct target also persist
- Save spell state added to persist beyond the self player since targets/spells can evolve rapidly with other players/npc/pets in the mix, resulted in two new rules:
	RULE_INIT(R_Spells, PlayerSpellSaveStateWaitInterval, "100"); // time in milliseconds we wait before performing a save when the spell save trigger is activated, allows additional actions to take place until the cap is hit
	RULE_INIT(R_Spells, PlayerSpellSaveStateCap, "1000"); // sets a maximum wait time before we queue a spell state save to the DB, given a lot can go on in a short period with players especially in combat, maybe good to have this at a higher interval.
Image 3 years ago
parent
commit
22ef3e0557

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

@@ -851,6 +851,9 @@ void Entity::AddSpellEffect(LuaSpell* luaspell, int32 override_expire_time){
 		changed = true;
 		info_changed = true;
 		AddChangedZoneSpawn();
+
+		if(luaspell->caster && luaspell->caster->IsPlayer() && luaspell->caster != this)
+			((Player*)luaspell->caster)->GetClient()->TriggerSpellSave();
 	}
 }
 

+ 21 - 6
EQ2/source/WorldServer/Player.cpp

@@ -3172,6 +3172,12 @@ void Player::AddSpellEffect(LuaSpell* luaspell, int32 override_expire_time){
 		effect->tier = spell->GetSpellTier();
 		GetSpellEffectMutex()->releasewritelock(__FUNCTION__, __LINE__);
 		charsheet_changed = true;
+
+		if(luaspell->caster && luaspell->caster->IsPlayer() && luaspell->caster != this)
+		{
+			GetClient()->TriggerSpellSave();
+			((Player*)luaspell->caster)->GetClient()->TriggerSpellSave();
+		}
 	}	
 }
 
@@ -6433,7 +6439,7 @@ void Player::SaveSpellEffects()
 			info->maintained_effects[i].spell->MSpellTargets.readlock(__FUNCTION__, __LINE__);
 			std::string insertTargets = string("insert into character_spell_effect_targets (caster_char_id, target_char_id, target_type, db_effect_type, spell_id, effect_slot, slot_pos) values ");
 			bool firstTarget = true;
-			map<int32, bool> targetsInserted;
+			map<Spawn*, int8> targetsInserted;
 			for (int8 t = 0; t < info->maintained_effects[i].spell->targets.size(); t++) {
 				int32 spawn_id = info->maintained_effects[i].spell->targets.at(t);
 					Spawn* spawn = GetZone()->GetSpawnByID(spawn_id);
@@ -6441,6 +6447,10 @@ void Player::SaveSpellEffects()
 					if(spawn && (spawn->IsPlayer() || spawn->IsPet()))
 					{
 						int32 tmpCharID = 0;
+						int8 type = 0;
+
+						if(targetsInserted.find(spawn) != targetsInserted.end())
+							continue;
 						
 						if(spawn->IsPlayer())
 							tmpCharID = ((Player*)spawn)->GetCharacterID();
@@ -6448,17 +6458,22 @@ void Player::SaveSpellEffects()
 						{
 							tmpCharID = 0xFFFFFFFF;
 						}
-						if(targetsInserted.find(tmpCharID) != targetsInserted.end())
-							continue;
+						else if(spawn->IsPet() && ((Entity*)spawn)->GetOwner() && 
+									((Entity*)spawn)->GetOwner()->IsPlayer())
+						{
+							type = ((Entity*)spawn)->GetPetType();
+							Player* petOwner =  (Player*)((Entity*)spawn)->GetOwner();
+							tmpCharID = petOwner->GetCharacterID();
+						}
 
 						if(!firstTarget)
 							insertTargets.append(", ");
 						
-						targetsInserted.insert(make_pair(tmpCharID, true));
+						targetsInserted.insert(make_pair(spawn, true));
 
 
 						LogWrite(SPELL__DEBUG, 0, "Spell", "%s has target %s (%u) added to spell %s", GetName(), spawn ? spawn->GetName() : "NA", tmpCharID, info->maintained_effects[i].spell->spell->GetName());
-						insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(tmpCharID) + ", " + "0" + ", " + 
+						insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(tmpCharID) + ", " + std::to_string(type) + ", " + 
 						std::to_string(DB_TYPE_MAINTAINEDEFFECTS) + ", " + std::to_string(info->maintained_effects[i].spell_id) + ", " + std::to_string(i) + 
 						", " + std::to_string(info->maintained_effects[i].slot_pos) + ")");
 						firstTarget = false;
@@ -6471,7 +6486,7 @@ void Player::SaveSpellEffects()
 					insertTargets.append(", ");
 
 				LogWrite(SPELL__DEBUG, 0, "Spell", "%s has target %s (%u) added to spell %s", GetName(), spawn ? spawn->GetName() : "NA", entries->first, info->maintained_effects[i].spell->spell->GetName());
-				insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(entries->first) + ", " + "0" + ", " + 
+				insertTargets.append("(" + std::to_string(caster_char_id) + ", " + std::to_string(entries->first) + ", " + std::to_string(entries->second) + ", " + 
 				std::to_string(DB_TYPE_MAINTAINEDEFFECTS) + ", " + std::to_string(info->maintained_effects[i].spell_id) + ", " + std::to_string(i) + 
 				", " + std::to_string(info->maintained_effects[i].slot_pos) + ")");
 

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

@@ -667,8 +667,7 @@ void PlayerGroupManager::UpdateGroupBuffs() {
 
 						if(group_member->GetZone() != caster->GetZone())
 						{
-							if(group_member->IsPlayer())
-								luaspell->char_id_targets.insert(make_pair(((Player*)group_member)->GetCharacterID(), 0));			
+							SpellProcess::AddSelfAndPetToCharTargets(luaspell, group_member);		
 						}
 						else
 						{

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

@@ -330,6 +330,8 @@ void RuleManager::Init()
 	RULE_INIT(R_Spells, FizzleDefaultSkill, ".2"); // offset against MaxSkill to average out to 100%, default of .2f so we don't go over the threshold if no skill
 	RULE_INIT(R_Spells, EnableCrossZoneGroupBuffs, "0"); // enables/disables allowing cross zone group buffs
 	RULE_INIT(R_Spells, EnableCrossZoneTargetBuffs, "0"); // enables/disables allowing cross zone target buffs
+	RULE_INIT(R_Spells, PlayerSpellSaveStateWaitInterval, "100"); // time in milliseconds we wait before performing a save when the spell save trigger is activated, allows additional actions to take place until the cap is hit
+	RULE_INIT(R_Spells, PlayerSpellSaveStateCap, "1000"); // sets a maximum wait time before we queue a spell state save to the DB, given a lot can go on in a short period with players especially in combat, maybe good to have this at a higher interval.
 
 	RULE_INIT(R_Expansion, GlobalExpansionFlag, "0");
 	RULE_INIT(R_Expansion, GlobalHolidayFlag, "0");

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

@@ -179,6 +179,8 @@ enum RuleType {
 	FizzleDefaultSkill,
 	EnableCrossZoneGroupBuffs,
 	EnableCrossZoneTargetBuffs,
+	PlayerSpellSaveStateWaitInterval,
+	PlayerSpellSaveStateCap,
 
 	/* ZONE TIMERS */
 	RegenTimer,

+ 41 - 13
EQ2/source/WorldServer/SpellProcess.cpp

@@ -1931,23 +1931,30 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 			if (caster->IsPlayer() && target->HasTarget())
 			{
 				secondary_target = target->GetTarget();
-				if (secondary_target) {
 					// check if spell is friendly
 					if (data->friendly_spell) {
 						//if target is NPC (and not a bot) on friendly spell, check to see if target is friendly
 						if (target->IsNPC() && !target->IsBot()) {
 							if (!target->IsPet() || (target->IsPet() && ((NPC*)target)->GetOwner()->IsNPC())) {
-								if (secondary_target->IsPlayer()) {
-									implied = true;
+								if (secondary_target && secondary_target->IsPlayer()) {
+									luaspell->initial_target = target->GetID();
+									luaspell->initial_target_char_id = (target && target->IsPlayer()) ? ((Player*)target)->GetCharacterID() : 0;
+									luaspell->targets.push_back(target->GetID());
 								}
-								else if (secondary_target->IsPet() && ((NPC*)secondary_target)->GetOwner()->IsPlayer())
+								else if (secondary_target && secondary_target->IsPet() && ((NPC*)secondary_target)->GetOwner()->IsPlayer())
 									implied = true;
 							}
+							else if (target->IsPet() && ((Entity*)target)->GetOwner()->IsPlayer())
+							{
+								luaspell->initial_target = target->GetID();
+								luaspell->initial_target_char_id = (target && target->IsPlayer()) ? ((Player*)target)->GetCharacterID() : 0;
+								luaspell->targets.push_back(target->GetID());
+							}
 						}
 					}   // if spell is not friendly
 					else {   // check if there is an implied target for this non-friendly spell
 						if (target->IsPlayer() || (target->IsPet() && ((NPC*)target)->GetOwner()->IsPlayer())) {
-							if (secondary_target->IsNPC()) {
+							if (secondary_target && secondary_target->IsNPC()) {
 								if (!secondary_target->IsPet() || (secondary_target->IsPet() && ((NPC*)secondary_target)->GetOwner()->IsNPC())) {
 									implied = true;
 								}
@@ -1962,7 +1969,7 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 								GetPlayerGroupTargets((Player*)target, caster, luaspell);
 
 							}
-							else if (target->IsPlayer() && ((Entity*)caster)->AttackAllowed((Entity*)secondary_target))
+							else if (secondary_target && target->IsPlayer() && ((Entity*)caster)->AttackAllowed((Entity*)secondary_target))
 							{
 								implied = true;
 								luaspell->initial_target = secondary_target->GetID();
@@ -1972,7 +1979,6 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 							}
 						}
 					} // end friendly spell check
-				}
 			}
 			else if (caster->IsPlayer()) {
 				if (data->friendly_spell) {
@@ -2093,8 +2099,7 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 								{
 									if(group_member->GetZone() != caster->GetZone())
 									{
-										if(group_member->IsPlayer())
-											luaspell->char_id_targets.insert(make_pair(((Player*)group_member)->GetCharacterID(), 0));
+										SpellProcess::AddSelfAndPetToCharTargets(luaspell, group_member);		
 									}
 									else if (group_member->GetZone() == luaspell->caster->GetZone()) {
 										luaspell->targets.push_back(group_member->GetID());
@@ -2170,14 +2175,14 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 					if (data->can_effect_raid > 0 || data->affect_only_group_members > 0 || data->group_spell > 0) 
 					{
 						// if caster is in a group, and target is a player and targeted player is a group member
-						if (((Player*)caster)->GetGroupMemberInfo() && (target->IsPlayer() || target->IsBot()) && ((Player*)caster)->IsGroupMember((Entity*)target))
+						if (((Player*)caster)->GetGroupMemberInfo() && (target->IsPlayer() || target->IsBot() || target->IsPet()) && ((Player*)caster)->IsGroupMember((Entity*)target))
 							luaspell->targets.push_back(target->GetID()); // return the target
 						else
 							luaspell->targets.push_back(caster->GetID()); // else return the caster
 					}
 					else if (target->IsPlayer() || target->IsBot()) // else it is not raid, group only or group spell
 						luaspell->targets.push_back(target->GetID()); // return target for single spell
-					else
+					else if ((luaspell->targets.size() < 1) || (!target->IsPet() || (((Entity*)target)->GetOwner() && !((Entity*)target)->GetOwner()->IsPlayer()))) 
 						luaspell->targets.push_back(caster->GetID()); // and if no target, cast on self
 				}
 				else if (caster->IsNPC()) // caster is an NPC
@@ -2584,9 +2589,16 @@ void SpellProcess::CheckRemoveTargetFromSpell(LuaSpell* spell, bool allow_delete
 									targets->erase(target_itr);
 									if (remove_spawn->IsEntity())
 									{
-										if(!removing_all_spells && remove_spawn->IsPlayer())
+										if(!removing_all_spells)
 										{
-											spell->char_id_targets.insert(make_pair(((Player*)remove_spawn)->GetCharacterID(),0));
+											if(remove_spawn->IsPlayer())
+												spell->char_id_targets.insert(make_pair(((Player*)remove_spawn)->GetCharacterID(),0));
+											else if(remove_spawn->IsPet() && ((Entity*)remove_spawn)->GetOwner() && ((Entity*)remove_spawn)->GetOwner()->IsPlayer())
+											{
+												Entity* pet = (Entity*)remove_spawn;
+												Player* ownerPlayer = (Player*)pet->GetOwner();
+												spell->char_id_targets.insert(make_pair(ownerPlayer->GetCharacterID(),pet->GetPetType()));
+											}
 										}
 										((Entity*)remove_spawn)->RemoveEffectsFromLuaSpell(spell);
 									}
@@ -2754,4 +2766,20 @@ void SpellProcess::AddSelfAndPet(LuaSpell* spell, Spawn* caster, bool onlyPet)
 		spell->targets.push_back(((Entity*)caster)->GetCharmedPet()->GetID());
 	if(!onlyPet && caster->IsEntity() && ((Entity*)caster)->IsPet() && ((Entity*)caster)->GetOwner())
 		spell->targets.push_back(((Entity*)caster)->GetOwner()->GetID());
+}
+
+void SpellProcess::AddSelfAndPetToCharTargets(LuaSpell* spell, Spawn* caster, bool onlyPet)
+{
+	if(!caster->IsPlayer())
+		return;
+	
+	Player* player = ((Player*)caster);
+	int32 charID = player->GetCharacterID();
+	
+	if(player->HasPet() && player->GetPet())
+		spell->char_id_targets.insert(make_pair(charID, player->GetPet()->GetPetType()));
+	if(player->HasPet() && player->GetCharmedPet())
+		spell->char_id_targets.insert(make_pair(charID, player->GetPet()->GetPetType()));
+	if(!onlyPet)
+		spell->char_id_targets.insert(make_pair(charID, 0x00));
 }

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

@@ -389,6 +389,7 @@ public:
 
 	void AddActiveSpell(LuaSpell* spell);
 	static void AddSelfAndPet(LuaSpell* spell, Spawn* caster, bool onlyPet=false);
+	static void AddSelfAndPetToCharTargets(LuaSpell* spell, Spawn* caster, bool onlyPet=false);
 private:
 	Mutex MSpellProcess;
 	MutexMap<Entity*,Spell*> spell_que;

+ 5 - 3
EQ2/source/WorldServer/World.cpp

@@ -1437,9 +1437,9 @@ void World::SendGroupQuests(PlayerGroup* group, Client* client){
 	}
 }*/
 
-void World::RejoinGroup(Client* client, int32 group_id){
+bool World::RejoinGroup(Client* client, int32 group_id){
 	if (!group_id) // no need if no group id!
-		return;
+		return false;
 
 	world.GetGroupManager()->GroupLock(__FUNCTION__, __LINE__);
 	PlayerGroup* group = world.GetGroupManager()->GetGroup(group_id);
@@ -1457,7 +1457,7 @@ void World::RejoinGroup(Client* client, int32 group_id){
 			client->GetCharacterID());
 		LogWrite(PLAYER__ERROR, 0, "Player", "Group did not exist for player %s to group id %i, async query to group_id = 0.", name.c_str(), group_id);
 		world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);
-		return;
+		return false;
 	}
 	deque<GroupMemberInfo*>::iterator itr;
 	GroupMemberInfo* info = 0;
@@ -1490,6 +1490,8 @@ void World::RejoinGroup(Client* client, int32 group_id){
 		LogWrite(PLAYER__ERROR, 0, "Player", "Identified group match for player %s to group id %u, however the player name was not present in the group!  May be an old group id that has been re-used.", name.c_str(), group_id);
 	
 	world.GetGroupManager()->ReleaseGroupLock(__FUNCTION__, __LINE__);
+
+	return match;
 }
 
 

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

@@ -568,7 +568,7 @@ public:
 	//void GroupReadUnLock();
 	//void CheckRemoveGroupedPlayer();
 	//void SendGroupUpdate(PlayerGroup* group, Client* exclude = 0);
-	void RejoinGroup(Client* client, int32 group_id);
+	bool RejoinGroup(Client* client, int32 group_id);
 	//bool MakeLeader(Client* leader, string new_leader);
 	
 	void AddBonuses(ItemStatsValues* values, int16 type, sint32 value, Entity* entity);

+ 62 - 27
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -4133,11 +4133,6 @@ void WorldDatabase::LoadSpawnScriptData() {
 				else
 					LogWrite(LUA__ERROR, 0, "LUA", "Invalid Entry in spawn_scripts table.");
 			}
-			if( spawn_id || spawnentry_id || spawn_location_id ) {
-
-				LogWrite(LUA__DEBUG, 5, "LUA", "SpawnScript %s loaded.", spawn_script.c_str());
-
-			}
 
 			spawn_id = 0;
 			spawnentry_id = 0;
@@ -7610,7 +7605,7 @@ void WorldDatabase::LoadCharacterSpellEffects(int32 char_id, Client* client, int
 			if (database_new.Select(&targets, "SELECT target_char_id, target_type, db_effect_type, spell_id from character_spell_effect_targets where caster_char_id = %u and effect_slot = %u and slot_pos = %u", char_id, effect_slot, slot_pos)) {
 				while (targets.Next()) {
 					int32 target_char = targets.GetInt32Str("target_char_id");
-					int16 target_type = targets.GetInt32Str("target_type");
+					int8 maintained_target_type = targets.GetInt32Str("target_type");
 					int32 in_spell_id = targets.GetInt32Str("spell_id");
 					if(spell_id != in_spell_id)
 						continue;
@@ -7621,8 +7616,8 @@ void WorldDatabase::LoadCharacterSpellEffects(int32 char_id, Client* client, int
 					{
 						if( player->HasPet() )
 						{
-							idToAdd = player->GetPet()->GetID();
 							restoreSpells.insert(make_pair(lua_spell, player->GetPet()));
+							// target added via restoreSpells
 						}
 					}
 					else
@@ -7630,32 +7625,47 @@ void WorldDatabase::LoadCharacterSpellEffects(int32 char_id, Client* client, int
 						Client* client2 = zone_list.GetClientByCharID(target_char);
 						if(client2 && client2->GetPlayer() && client2->GetCurrentZone() == client->GetCurrentZone())
 						{
-							idToAdd = client2->GetPlayer()->GetID();
-							if(client != client2)
+							if(maintained_target_type > 0)
+							{
+								if(client != client2)
+								{
+									lua_spell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
+									
+									if(client2->GetPlayer()->GetPet() && maintained_target_type == PET_TYPE_COMBAT)
+									{
+										restoreSpells.insert(make_pair(lua_spell, client2->GetPlayer()->GetPet()));
+										// target added via restoreSpells
+									}
+									if(client2->GetPlayer()->GetCharmedPet() && maintained_target_type == PET_TYPE_CHARMED)
+									{
+										restoreSpells.insert(make_pair(lua_spell, client2->GetPlayer()->GetCharmedPet()));
+										// target added via restoreSpells
+									}
+									
+									lua_spell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
+								}
+							} // end of pet clause
+							else if(client != client2) // maintained type must be 0, so client
 								restoreSpells.insert(make_pair(lua_spell, client2->GetPlayer()));
 							
 							lua_spell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
 							multimap<int32,int8>::iterator entries;
-							while((entries = lua_spell->char_id_targets.find(target_char)) != lua_spell->char_id_targets.end())
+							for(entries = lua_spell->char_id_targets.begin(); entries != lua_spell->char_id_targets.end(); entries++)
 							{
-								lua_spell->char_id_targets.erase(entries);
+								int32 ent_char_id = entries->first;
+								int8 ent_target_type = entries->second;
+								if(ent_char_id == target_char && ent_target_type == maintained_target_type)
+									entries = lua_spell->char_id_targets.erase(entries);
 							}
 							lua_spell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
 						}
 						else
 						{
 							lua_spell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
-							lua_spell->char_id_targets.insert(make_pair(target_char,0));
+							lua_spell->char_id_targets.insert(make_pair(target_char,maintained_target_type));
 							lua_spell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
 						}
 					}
-					
-					if(!idToAdd)
-						continue;
-					
-					lua_spell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
-					lua_spell->targets.push_back(idToAdd);
-					lua_spell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
 				}
 			}
 			
@@ -7725,20 +7735,37 @@ void WorldDatabase::LoadCharacterSpellEffects(int32 char_id, Client* client, int
 		}
 	}
 
-	if(!rule_manager.GetGlobalRule(R_Spells,EnableCrossZoneTargetBuffs)->GetInt8() && db_spell_type == DB_TYPE_SPELLEFFECTS)
+	if(db_spell_type == DB_TYPE_SPELLEFFECTS)
 	{
+		// if the cross_zone_target_buff option is disabled then we check for all possible targets within the current zone
+		// if cross_zone_target_buff is enabled, we only check to restore pets, the players get restored by their own spell effects (we don't directly track the pets, indirectly we do through the player casting and their targets)
+		int8 cross_zone_target_buff = rule_manager.GetGlobalRule(R_Spells,EnableCrossZoneTargetBuffs)->GetInt8();
+
 		DatabaseResult targets;
-		if (database_new.Select(&targets, "SELECT caster_char_id, target_type, spell_id from character_spell_effect_targets where target_char_id = %u", player->GetCharacterID())) {
+		if (
+			(!cross_zone_target_buff && database_new.Select(&targets, "SELECT caster_char_id, target_type, spell_id from character_spell_effect_targets where target_char_id = %u", player->GetCharacterID())) ||
+			(database_new.Select(&targets, "SELECT caster_char_id, target_type, spell_id from character_spell_effect_targets where target_char_id = %u and target_type > 0", player->GetCharacterID())))
+		 {
 			while (targets.Next()) {
 				int32 caster_char_id = targets.GetInt32Str("caster_char_id");
-				int16 target_type = targets.GetInt32Str("target_type");
+				int8 prev_target_type = targets.GetInt32Str("target_type");
 				int32 in_spell_id = targets.GetInt32Str("spell_id");
 					Client* tmpCaster = nullptr;
 					MaintainedEffects* effect = nullptr;
-					if ( caster_char_id != player->GetCharacterID() && (tmpCaster = zone_list.GetClientByCharID(caster_char_id)) != nullptr 
-								&& tmpCaster->GetCurrentZone() == player->GetZone() && tmpCaster->GetPlayer() && (effect = tmpCaster->GetPlayer()->GetMaintainedSpell(in_spell_id)) != nullptr)
+					if (caster_char_id != player->GetCharacterID() && (tmpCaster = zone_list.GetClientByCharID(caster_char_id)) != nullptr && (cross_zone_target_buff || 
+								 tmpCaster->GetCurrentZone() == player->GetZone()) && tmpCaster->GetPlayer() && (effect = tmpCaster->GetPlayer()->GetMaintainedSpell(in_spell_id)) != nullptr)
 					{
-						if(!player->GetSpellEffect(effect->spell_id, tmpCaster->GetPlayer()))
+						if(prev_target_type > 0)
+						{
+							if(player->HasPet())
+							{
+								if(player->GetPet() && prev_target_type == PET_TYPE_COMBAT)
+									restoreSpells.insert(make_pair(effect->spell, player->GetPet()));
+								if(player->GetCharmedPet() && prev_target_type == PET_TYPE_CHARMED)
+									restoreSpells.insert(make_pair(effect->spell, player->GetCharmedPet()));
+							}
+						}
+						else if(!player->GetSpellEffect(effect->spell_id, tmpCaster->GetPlayer()))
 						{
 							if(effect->spell->initial_target_char_id == player->GetCharacterID())
 								effect->spell->initial_target = player->GetID();
@@ -7766,15 +7793,23 @@ void WorldDatabase::LoadCharacterSpellEffects(int32 char_id, Client* client, int
 		if(!caster)
 			caster = client->GetPlayer();
 		
+		int32 group_id_target = target->group_id;
+		if(target->IsPet() && target->GetOwner())
+			group_id_target = target->GetOwner()->group_id;
+			
 		if(caster != target && caster->GetPet() != target && 
-			tmpSpell->spell->GetSpellData()->group_spell && tmpSpell->spell->GetSpellData()->friendly_spell && (caster->group_id == 0 || target->group_id == 0 || caster->group_id != target->group_id))
+			tmpSpell->spell->GetSpellData()->group_spell && tmpSpell->spell->GetSpellData()->friendly_spell && (caster->group_id == 0 || group_id_target == 0 || caster->group_id != group_id_target))
 		{
-			LogWrite(LUA__WARNING, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: %s player no longer grouped with %s to reload bonuses for spell %s.", target->GetName(), caster ? caster->GetName() : "?", tmpSpell->spell->GetName());
+			LogWrite(LUA__WARNING, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: %s player no longer grouped with %s to reload bonuses for spell %s (target_groupid: %u, caster_groupid: %u).", target->GetName(), caster ? caster->GetName() : "?", tmpSpell->spell->GetName(), group_id_target, caster->group_id);
 			continue;
 		}
 
 		LogWrite(LUA__WARNING, 0, "LUA", "WorldDatabase::LoadCharacterSpellEffects: %s using caster %s to reload bonuses for spell %s.", player->GetName(), caster ? caster->GetName() : "?", tmpSpell->spell->GetName());
 		
+		tmpSpell->MSpellTargets.writelock(__FUNCTION__, __LINE__);
+		tmpSpell->targets.push_back(target->GetID());
+		tmpSpell->MSpellTargets.releasewritelock(__FUNCTION__, __LINE__);
+
 		target->AddSpellEffect(tmpSpell, tmpSpell->timer.GetRemainingTime() != 0 ? tmpSpell->timer.GetRemainingTime() : 0);
 		vector<BonusValues*>* sb_list = caster->GetAllSpellBonuses(tmpSpell);
 		for (int32 x = 0; x < sb_list->size(); x++) {

+ 56 - 3
EQ2/source/WorldServer/client.cpp

@@ -202,6 +202,8 @@ Client::Client(EQStream* ieqs) : pos_update(125), quest_pos_timer(2000), lua_deb
 	last_saved_timestamp = 0;
 	MQueueStateCmds.SetName("Client::MQueueStateCmds");
 	MConversation.SetName("Client::MConversation");
+	save_spell_state_timer.Disable();
+	save_spell_state_time_bucket = 0;
 }
 
 Client::~Client() {
@@ -806,6 +808,10 @@ void Client::SendCharInfo() {
 	if (version > 546)
 		ClientPacketFunctions::SendHousingList(this);
 	
+	GetPlayer()->group_id = rejoin_group_id;
+	if(!world.RejoinGroup(this, rejoin_group_id))
+		GetPlayer()->group_id = 0;
+
 	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_MAINTAINEDEFFECTS);
 	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_SPELLEFFECTS);
 	GetPlayer()->SetSaveSpellEffects(false);
@@ -1380,7 +1386,6 @@ bool Client::HandlePacket(EQApplicationPacket* app) {
 		if (!IsReadyForSpawns())
 			SetReadyForSpawns(true);
 		SendCharInfo();
-		world.RejoinGroup(this, rejoin_group_id);
 		pos_update.Start();
 		quest_pos_timer.Start();
 		break;
@@ -2924,6 +2929,15 @@ bool Client::Process(bool zone_process) {
 		LogWrite(CCLIENT__DEBUG, 1, "Client", "%s, CheckQuestQueue", __FUNCTION__, __LINE__);
 		CheckQuestQueue();
 	}
+
+	MSaveSpellStateMutex.lock();
+	if(save_spell_state_timer.Check())
+	{
+		save_spell_state_timer.Disable();
+		GetPlayer()->SaveSpellEffects();
+	}
+	MSaveSpellStateMutex.unlock();
+
 	if (pos_update.Check())
 	{
 		ProcessStateCommands();
@@ -3892,8 +3906,10 @@ void Client::Zone(ZoneServer* new_zone, bool set_coords) {
 
 	LogWrite(CCLIENT__DEBUG, 0, "Client", "%s: Removing player from fighting...", __FUNCTION__);
 	//GetCurrentZone()->GetCombat()->RemoveHate(player);
+	MSaveSpellStateMutex.lock();
 	player->SaveSpellEffects();
 	player->SetSaveSpellEffects(true);
+	MSaveSpellStateMutex.unlock();
 	// Remove players pet from zone if there is one
 	((Entity*)player)->DismissAllPets();
 
@@ -4065,7 +4081,9 @@ void Client::Save() {
 
 		GetPlayer()->SaveHistory();
 		GetPlayer()->SaveLUAHistory();
+		MSaveSpellStateMutex.lock();
 		GetPlayer()->SaveSpellEffects();
+		MSaveSpellStateMutex.unlock();
 	}
 
 }
@@ -9528,10 +9546,11 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code)
 				GetCurrentZone()->SetSpawnStructs(this);
 				connected_to_zone = true;
 				client_list.Remove(this); //remove from master client list
-				new_client_login = true;
 				GetCurrentZone()->AddClient(this); //add to zones client list
-
 				zone_list.AddClientToMap(player->GetName(), this);
+				// this initiates additional DB loading and client offloading within the Zone the player resides, need the client already added in zone and to the map above.
+				new_client_login = true;
+
 				const char* zone_script = world.GetZoneScript(GetCurrentZone()->GetZoneID());
 				if (zone_script && lua_interface)
 					lua_interface->RunZoneScript(zone_script, "new_client", GetCurrentZone(), GetPlayer());
@@ -10291,4 +10310,38 @@ void Client::AwardCoins(int64 total_coins, std::string reason)
 		int8 type = CHANNEL_LOOT;
 		SimpleMessage(type, message.c_str());
 		}
+}
+
+void Client::TriggerSpellSave()
+{
+	int32 interval = rule_manager.GetGlobalRule(R_Spells, PlayerSpellSaveStateWaitInterval)->GetInt32();
+	// default to not have some bogus value in the rule
+	if(interval < 1)
+		interval = 100;
+	
+	MSaveSpellStateMutex.lock();
+	if(!save_spell_state_timer.Enabled())
+	{
+		save_spell_state_time_bucket = 0;
+		save_spell_state_timer.Start(interval, true);
+	}
+	else
+	{
+		int32 elapsed_time = save_spell_state_timer.GetElapsedTime();
+		save_spell_state_time_bucket += elapsed_time;
+
+		int32 save_wait_cap = rule_manager.GetGlobalRule(R_Spells, PlayerSpellSaveStateCap)->GetInt32();
+		
+		// default to not have some bogus value in the rule
+		if(save_wait_cap < interval)
+			save_wait_cap = interval+1;
+		
+		if(save_spell_state_time_bucket >= save_wait_cap)
+		{
+			// save immediately and disable timer
+			GetPlayer()->SaveSpellEffects();
+			save_spell_state_timer.Disable();
+		}
+	}
+	MSaveSpellStateMutex.unlock();
 }

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

@@ -478,6 +478,8 @@ public:
 	void PurgeItem(Item* item);
 	void ConsumeFoodDrink(Item* item, int32 slot);
 	void AwardCoins(int64 total_coins, std::string reason = string(""));
+
+	void TriggerSpellSave();
 private:
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);
@@ -593,6 +595,9 @@ private:
 
 	map<int32, int32> queued_state_commands;
 	Mutex MQueueStateCmds;
+	Timer save_spell_state_timer; // will be the 're-trigger' to delay
+	int32 save_spell_state_time_bucket; // bucket as we collect over time when timer is reset by new spell effects being casted
+	std::mutex MSaveSpellStateMutex;
 };
 
 class ClientList {