Browse Source

- Fix #314 Encounter states, locked, overmatched and broken now supported up to group level (raids and functionality to them will be done in a later milestone). This also entails all the functionality within, npcs go gray when in an encounter or yelled, you cant heal or damage a player or npc in an encounter.
- Fix #351 /yell now available, yell with self target breaks all active encounters, yell with direct target breaks that encounter, no target breaks first encounter
- WorldStructs.xml update required for /yell so others know you are yelling!
- /spawn details aggro (NPC only) added, view active encounter list (who receives credit, locked encounter) and hate list (actual damage/hate)
- InfoStruct now has a new UINT8 "engaged_encounter", set to 1 for being in an active encounter, 0 for out of encounter (allows regen/speed recovery)
- Returning from linkdead DB load reduced
- Returning from linkdead you won't see ghost versions of yourself
- Addressed returning from linkdead and spells missing, would despawn your pet or remove buffs for examples.
- Protections around private spawns aggroing/attacking players that cannot see them
- Additional spell crash protection when spell caster is not in zone

Emagi 1 year ago
parent
commit
7b023ec019

+ 36 - 16
EQ2/source/WorldServer/Combat.cpp

@@ -121,6 +121,17 @@ bool Entity::AttackAllowed(Entity* target, float distance, bool range_attack) {
 		LogWrite(COMBAT__DEBUG, 3, "AttackAllowed", "Failed to attack: players are not allowed to attack bots");
 		return false;
 	}
+	
+	if(rule_manager.GetGlobalRule(R_Combat, LockedEncounterNoAttack)->GetBool()) {
+		if(target->IsNPC() && (target->GetLockedNoLoot() == ENCOUNTER_STATE_LOCKED || target->GetLockedNoLoot() == ENCOUNTER_STATE_OVERMATCHED) && 
+			!attacker->IsEngagedBySpawnID(target->GetID())) {
+			return false;
+		}
+		else if(IsNPC() && (GetLockedNoLoot() == ENCOUNTER_STATE_LOCKED || GetLockedNoLoot() == ENCOUNTER_STATE_OVERMATCHED) && 
+			!target->IsEngagedBySpawnID(GetID())) {
+			return false;
+		}
+	}
 
 	if (attacker->IsPlayer() && target->IsPlayer())
 	{
@@ -177,6 +188,12 @@ bool Entity::AttackAllowed(Entity* target, float distance, bool range_attack) {
 			return false;
 		}
 	}
+	
+	if((target->IsPrivateSpawn() && !target->AllowedAccess(this)) || (IsPrivateSpawn() && AllowedAccess(target))) {
+			LogWrite(COMBAT__DEBUG, 3, "AttackAllowed", "Access to spawn disallowed.");
+		return false;
+	}
+	
 	return true;
 }
 
@@ -199,9 +216,13 @@ void Entity::MeleeAttack(Spawn* victim, float distance, bool primary, bool multi
 	}
 	if (IsStealthed() || IsInvis())
 		CancelAllStealth();
-		
-
+	
 	int8 hit_result = DetermineHit(victim, DAMAGE_PACKET_TYPE_SIMPLE_DAMAGE, damage_type, 0, false);
+	
+	if(victim->IsEntity()) {
+		CheckEncounterState((Entity*)victim);
+	}
+	
 	if(hit_result == DAMAGE_PACKET_RESULT_SUCCESSFUL){		
 		/*if(GetAdventureClass() == MONK){
 			max_damage*=3;
@@ -251,8 +272,7 @@ void Entity::MeleeAttack(Spawn* victim, float distance, bool primary, bool multi
 	//Apply attack speed mods
 	if(!multi_attack)
 		SetAttackDelay(primary);
-
-
+	
 	if(victim->IsNPC() && victim->EngagedInCombat() == false) {
 		((NPC*)victim)->AddHate(this, 50);
 	}
@@ -383,7 +403,11 @@ bool Entity::SpellAttack(Spawn* victim, float distance, LuaSpell* luaspell, int8
 		hit_result = DetermineHit(victim, DAMAGE_PACKET_TYPE_SPELL_DAMAGE, damage_type, 0, false);
 	else
 		hit_result = DetermineHit(victim, DAMAGE_PACKET_TYPE_SPELL_DAMAGE, damage_type, 0, true, luaspell);
-		
+	
+	if(victim->IsEntity()) {
+		CheckEncounterState((Entity*)victim);
+	}
+	
 	if(hit_result == DAMAGE_PACKET_RESULT_SUCCESSFUL) {
 		luaspell->last_spellattack_hit = true;
 		//If this spell is a tick and has already crit, force the tick to crit
@@ -547,7 +571,7 @@ bool Entity::ProcAttack(Spawn* victim, int8 damage_type, int32 low_damage, int32
 
 // this is used exclusively by LUA, heal_type is forced lower case via boost::lower(heal_type); in the LUA Functions used by this
 bool Entity::SpellHeal(Spawn* target, float distance, LuaSpell* luaspell, string heal_type, int32 low_heal, int32 high_heal, int8 crit_mod, bool no_calcs, string custom_spell_name){
-	 if(!target || !luaspell || !luaspell->spell)
+	 if(!target || !luaspell || !luaspell->spell || (target->IsPrivateSpawn() && !target->AllowedAccess(this)))
 		return false;
 
 	 if (!target->Alive())
@@ -664,6 +688,7 @@ bool Entity::SpellHeal(Spawn* target, float distance, LuaSpell* luaspell, string
 		for (itr = ((Entity*)target)->HatedBy.begin(); itr != ((Entity*)target)->HatedBy.end(); itr++) {
 			Spawn* spawn = GetZone()->GetSpawnByID(*itr);
 			if (spawn && spawn->IsEntity() && target != this) {
+				CheckEncounterState((Entity*)spawn);
 				((Entity*)spawn)->AddHate(this, hate_amt);
 			}
 		}
@@ -1175,12 +1200,15 @@ void Entity::AddHate(Entity* attacker, sint32 hate) {
 
 	hate = attacker->CalculateHateAmount(this, hate);
 	
-	if (IsNPC()) {
+	if (IsNPC() && ((NPC*)this)->Brain()) {
 		LogWrite(COMBAT__DEBUG, 3, "Combat", "Add NPC_AI Hate: Victim '%s', Attacker '%s', Hate: %i", GetName(), attacker->GetName(), hate);
 		((NPC*)this)->Brain()->AddHate(attacker, hate);
+		int8 loot_state = ((NPC*)this)->GetLockedNoLoot();
 		// if encounter size is 0 then add the attacker to the encounter
-		if (((NPC*)this)->Brain()->GetEncounterSize() == 0)
+		if ((loot_state != ENCOUNTER_STATE_AVAILABLE && loot_state != ENCOUNTER_STATE_BROKEN && ((NPC*)this)->Brain()->GetEncounterSize() == 0)) {
 			((NPC*)this)->Brain()->AddToEncounter(attacker);
+			AddTargetToEncounter(attacker);
+		}
 	}
 
 	if (attacker->GetThreatTransfer() && hate > 0) {
@@ -1297,10 +1325,6 @@ void Entity::KillSpawn(Spawn* dead, int8 type, int8 damage_type, int16 kill_blow
 	if (IsPlayer() && dead->IsEntity())
 		GetZone()->GetSpellProcess()->KillHOBySpawnID(dead->GetID());
 
-	//if (dead->IsEntity())								same code called in zone server
-		//((Entity*)dead)->InCombat(false);
-	
-
 	/* just for sake of not knowing if we are in a read lock, write lock, or no lock
 	**  say spawnlist is locked (DismissPet arg 3 true), which means RemoveSpawn will remove the id from the spawn_list outside of the potential lock
 	*/
@@ -1311,10 +1335,6 @@ void Entity::KillSpawn(Spawn* dead, int8 type, int8 damage_type, int16 kill_blow
 		((Entity*)dead)->DismissAllPets(false, true);
 	}
 
-	// If not in combat and no one in the encounter list add this killer to the list
-	if(dead->EngagedInCombat() == false && dead->IsNPC() && ((NPC*)dead)->Brain()->GetEncounterSize() == 0)
-		((NPC*)dead)->Brain()->AddToEncounter(this);
-
 	if (IsCasting())
 		GetZone()->Interrupted(this, dead, SPELL_ERROR_NOT_ALIVE);
 

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

@@ -2725,6 +2725,10 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				dead= cmdTarget;
 				if(dead && dead->IsPlayer() == false){
 					dead->SetHP(0);
+					if(dead->IsNPC()) {
+						client->GetPlayer()->CheckEncounterState((Entity*)dead);
+						((NPC*)dead)->AddHate(client->GetPlayer(), 1);
+					}
 					if(sep && sep->arg[0] && sep->IsNumber(0) && atoi(sep->arg[0]) == 1)
 						client->GetCurrentZone()->RemoveSpawn(dead, true, true, true, true, true);
 					else
@@ -4607,6 +4611,17 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 						client->Message(CHANNEL_COLOR_YELLOW, "%s is %s.", isFlanking ? "YOU are flanking" : "YOU are NOT flanking", cmdTarget->GetName());
 						break;
 					}
+					else if (ToLower(string(sep->arg[0])) == "aggro")
+					{
+						if(cmdTarget->IsNPC() && ((NPC*)cmdTarget)->Brain()) {
+							((NPC*)cmdTarget)->Brain()->SendEncounterList(client);
+							((NPC*)cmdTarget)->Brain()->SendHateList(client);
+						}
+						else {
+							client->SimpleMessage(CHANNEL_COLOR_RED, "/spawn details aggro is for NPCs only!");
+						}
+						break;
+					}
 				}
 				if (sep->IsNumber(0))
 				{
@@ -5600,6 +5615,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 		case COMMAND_CANCEL_EFFECT: { Command_CancelEffect(client, sep); break; }
 		case COMMAND_CUREPLAYER: { Command_CurePlayer(client, sep); break; }
 		case COMMAND_SHARE_QUEST: { Command_ShareQuest(client, sep); break; }
+		case COMMAND_YELL: { Command_Yell(client, sep); break; }
 		default: 
 		{
 			LogWrite(COMMAND__WARNING, 0, "Command", "Unhandled command: %s", command->command.data.c_str());
@@ -11657,7 +11673,7 @@ void Commands::Command_CancelEffect(Client* client, Seperator* sep)
 		
 		MaintainedEffects* meffect = effect->caster->GetMaintainedSpell(spell_id);
 		
-		if (!meffect || !meffect->spell || !meffect->spell->caster || 
+		if (!meffect || !meffect->spell || !meffect->spell->caster || !meffect->spell->caster->GetZone() ||
 		!meffect->spell->caster->GetZone()->GetSpellProcess()->DeleteCasterSpell(meffect->spell, "canceled", false, client->GetPlayer()))
 			client->Message(CHANNEL_COLOR_RED, "The spell effect could not be cancelled.");
 	}
@@ -11816,3 +11832,63 @@ void Commands::Command_ShareQuest(Client* client, Seperator* sep)
 		}
 	}
 }
+
+
+
+/* 
+	Function: Command_Yell()
+	Purpose	: Yell to break an encounter
+	Example	: /yell
+	* Uses self target, encounter target or no target
+*/ 
+void Commands::Command_Yell(Client* client, Seperator* sep) {
+	Entity* player = (Entity*)client->GetPlayer();
+	Spawn* res = nullptr;
+	Spawn* prev = nullptr;
+	bool cycleAll = false;
+	if(player->GetTarget() == player) {
+		cycleAll = true; // self target breaks all encounters
+	}
+	else if(player->GetTarget()) {
+		res = player->GetTarget(); // selected target other than self only dis-engages that encounter
+	}
+	
+	if(res && !client->GetPlayer()->IsEngagedBySpawnID(res->GetID()))
+		return;
+	
+	bool alreadyYelled = false;
+	do {
+	if(!res && player->IsEngagedInEncounter(&res)) { // no target is set, dis-engage top of hated by list
+
+	}
+	if(!res || prev == res) {
+		return;
+	}
+
+	if(res->IsNPC() && ((NPC*)res)->Brain()) {
+		if(!alreadyYelled) {
+			client->SimpleMessage(CHANNEL_COLOR_YELLOW, "You yell for help!");
+			string yellMsg = std::string(client->GetPlayer()->GetName()) + " yelled for help!";
+			client->GetPlayer()->GetZone()->SimpleMessage(CHANNEL_COLOR_RED, yellMsg.c_str(), client->GetPlayer(), 35.0f, false);
+			client->GetPlayer()->GetZone()->SendYellPacket(client->GetPlayer());
+		}
+		alreadyYelled = true;
+		
+		NPC* npc = (NPC*)res;
+		npc->Brain()->ClearEncounter();
+		npc->SetLockedNoLoot(ENCOUNTER_STATE_BROKEN);
+		npc->UpdateEncounterState(ENCOUNTER_STATE_BROKEN);
+	}
+	prev = res;
+	res = nullptr;
+	}while(cycleAll);
+	
+	if(!player->IsEngagedInEncounter()) {		
+		if(player->GetInfoStruct()->get_engaged_encounter()) {
+			player->GetInfoStruct()->set_engaged_encounter(0);
+			player->SetRegenValues((player->GetInfoStruct()->get_effective_level() > 0) ? player->GetInfoStruct()->get_effective_level() : player->GetLevel());
+			client->GetPlayer()->SetCharSheetChanged(true);
+			player->info_changed = true;
+		}
+	}
+}

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

@@ -448,6 +448,7 @@ public:
 	void Command_CancelEffect(Client* client, Seperator* sep);
 	void Command_CurePlayer(Client* client, Seperator* sep);
 	void Command_ShareQuest(Client* client, Seperator* sep);
+	void Command_Yell(Client* client, Seperator* sep);
 
 	// AA Commands
 	void Get_AA_Xml(Client* client, Seperator* sep);

+ 70 - 9
EQ2/source/WorldServer/Entity.cpp

@@ -338,6 +338,8 @@ void Entity::MapInfoStruct()
 	
 	get_int8_funcs["friendly_target_npc"] = l::bind(&InfoStruct::get_friendly_target_npc, &info_struct);
 	get_int32_funcs["last_claim_time"] = l::bind(&InfoStruct::get_last_claim_time, &info_struct);
+	
+	get_int8_funcs["engaged_encounter"] = l::bind(&InfoStruct::get_engaged_encounter, &info_struct);
 
 /** SETS **/
 	set_string_funcs["name"] = l::bind(&InfoStruct::set_name, &info_struct, l::_1);
@@ -521,6 +523,8 @@ void Entity::MapInfoStruct()
 	
 	set_int8_funcs["friendly_target_npc"] = l::bind(&InfoStruct::set_friendly_target_npc, &info_struct, l::_1);
 	set_int32_funcs["last_claim_time"] = l::bind(&InfoStruct::set_last_claim_time, &info_struct, l::_1);
+	
+	set_int8_funcs["engaged_encounter"] = l::bind(&InfoStruct::set_engaged_encounter, &info_struct, l::_1);
 
 }
 
@@ -908,9 +912,17 @@ void Entity::InCombat(bool val){
 		changeCombatState = true;
 
 	in_combat = val;
+	
+	bool update_regen = false;
+	if(GetInfoStruct()->get_engaged_encounter()) {
+		if(!IsAggroed() || !IsEngagedInEncounter()) {
+			GetInfoStruct()->set_engaged_encounter(0);
+			update_regen = true;
+		}
+	}
 
-	if(changeCombatState)
-		SetRegenValues(GetInfoStruct()->get_effective_level());
+	if(changeCombatState || update_regen)
+		SetRegenValues((GetInfoStruct()->get_effective_level() > 0) ? GetInfoStruct()->get_effective_level() : GetLevel());
 }
 
 void Entity::DoRegenUpdate(){
@@ -1513,13 +1525,16 @@ void Entity::CalculateBonuses(){
 void Entity::SetRegenValues(int16 effective_level)
 {
 	bool classicRegen = rule_manager.GetGlobalRule(R_Spawn, ClassicRegen)->GetBool();
+	bool override_ = (IsPlayer() && !GetInfoStruct()->get_engaged_encounter());
+	
 	if(!GetInfoStruct()->get_hp_regen_override())
 	{
 		sint16 regen_hp_rate = 0;
 		sint16 temp = 0;
 
 		MStats.lock();
-		if(!IsAggroed())
+		
+		if(!IsAggroed() || override_)
 		{
 			if(classicRegen)
 			{
@@ -1551,7 +1566,7 @@ void Entity::SetRegenValues(int16 effective_level)
 		sint16 temp = 0;
 
 		MStats.lock();
-		if(!IsAggroed())
+		if(!IsAggroed() || override_)
 		{
 			if(classicRegen)
 			{				
@@ -2141,7 +2156,7 @@ int32 Entity::CheckWards(Entity* attacker, int32 damage, int8 damage_type) {
 		ward->HitCount++; // increment hit count
 		ward->RoundTriggered = true;
 		
-		if (ward->MaxHitCount && spell->num_triggers)
+		if (ward->MaxHitCount && spell->num_triggers && spell->caster->GetZone())
 		{
 			spell->num_triggers--;
 			ClientPacketFunctions::SendMaintainedExamineUpdate(spell->caster->GetZone()->GetClientBySpawn(spell->caster), spell->slot_pos, spell->num_triggers, 0);
@@ -2185,7 +2200,7 @@ float Entity::GetSpeed() {
 		ret += stats[ITEM_STAT_STEALTHINVISSPEEDMOD];
 	}
 	
-	if (!EngagedInCombat()) {
+	if (!GetInfoStruct()->get_engaged_encounter()) {
 		if (stats.count(ITEM_STAT_SPEED) && stats.count(ITEM_STAT_MOUNTSPEED)) {
 			ret += max(stats[ITEM_STAT_SPEED], stats[ITEM_STAT_MOUNTSPEED]);
 		}
@@ -2196,8 +2211,7 @@ float Entity::GetSpeed() {
 			ret += stats[ITEM_STAT_MOUNTSPEED];
 		}
 	}
-
-	if (EngagedInCombat()) {
+	else if (GetInfoStruct()->get_engaged_encounter()) {
 		
 		if (GetMaxSpeed() > 0.0f)
 			ret = GetMaxSpeed();
@@ -2215,7 +2229,7 @@ float Entity::GetSpeed() {
 float Entity::GetAirSpeed() {
 	float ret = speed;
 
-	if (!EngagedInCombat())
+	if (!GetInfoStruct()->get_engaged_encounter())
 		ret += stats[ITEM_STAT_MOUNTAIRSPEED];
 
 	ret *= speed_multiplier;
@@ -3666,4 +3680,51 @@ Entity*	Entity::GetOwner() {
 		ent = (Entity*)spawn;
 
 	return ent;
+}
+
+bool Entity::IsEngagedInEncounter(Spawn** res) {
+	if(res) {
+		*res = nullptr;
+	}
+	bool ret = false;
+	set<int32>::iterator itr;
+	MHatedBy.lock();
+	if(IsPlayer()) {
+		for (itr = HatedBy.begin(); itr != HatedBy.end(); itr++) {
+			Spawn* spawn = GetZone()->GetSpawnByID(*itr);
+			if (spawn && spawn->IsNPC() && ((NPC*)spawn)->Brain() && (spawn->GetLockedNoLoot() == ENCOUNTER_STATE_LOCKED || spawn->GetLockedNoLoot() == ENCOUNTER_STATE_OVERMATCHED)) {
+				if((ret = ((NPC*)spawn)->Brain()->IsPlayerInEncounter(((Player*)this)->GetCharacterID()))) {
+					if(res)
+						*res = spawn;
+					break;
+				}
+			}
+		}
+	}
+	else {
+		for (itr = HatedBy.begin(); itr != HatedBy.end(); itr++) {
+			Spawn* spawn = GetZone()->GetSpawnByID(*itr);
+			if (spawn && spawn->IsNPC() && ((NPC*)spawn)->Brain() && (spawn->GetLockedNoLoot() == ENCOUNTER_STATE_LOCKED || spawn->GetLockedNoLoot() == ENCOUNTER_STATE_OVERMATCHED)) {
+				if((ret = ((NPC*)spawn)->Brain()->IsEntityInEncounter(GetID()))) {
+					if(res)
+						*res = spawn;
+					break;
+				}
+			}
+		}
+
+	}
+	MHatedBy.unlock();
+	
+	return ret;
+}
+
+bool Entity::IsEngagedBySpawnID(int32 id) {
+	bool ret = false;
+	Spawn* spawn = GetZone()->GetSpawnByID(id);
+	if (spawn && spawn->IsNPC() && ((NPC*)spawn)->Brain()) {
+		ret = ((NPC*)spawn)->Brain()->IsEntityInEncounter(GetID());
+	}
+	
+	return ret;
 }

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

@@ -273,6 +273,8 @@ struct InfoStruct{
 		
 		friendly_target_npc_ = 0;
 		last_claim_time_ = 0;
+		
+		engaged_encounter_ = 0;
 	}
 
 
@@ -457,6 +459,8 @@ struct InfoStruct{
 		override_ranged_weapon_ = oldStruct->get_override_ranged_weapon();
 		friendly_target_npc_ = oldStruct->get_friendly_target_npc();
 		last_claim_time_ = oldStruct->get_last_claim_time();
+		
+		engaged_encounter_ = oldStruct->get_engaged_encounter();
 
 	}
 	//mutable std::shared_mutex mutex_;
@@ -659,6 +663,7 @@ struct InfoStruct{
 	int8	get_friendly_target_npc() { std::lock_guard<std::mutex> lk(classMutex); return friendly_target_npc_; }
 	int32	get_last_claim_time() { std::lock_guard<std::mutex> lk(classMutex); return last_claim_time_; }
 	
+	int8	get_engaged_encounter() { std::lock_guard<std::mutex> lk(classMutex); return engaged_encounter_; }
 	
 	void	set_name(std::string value) { std::lock_guard<std::mutex> lk(classMutex); name_ = value; }
 	
@@ -943,6 +948,8 @@ struct InfoStruct{
 	void	set_override_ranged_weapon(int8 value) { std::lock_guard<std::mutex> lk(classMutex); override_ranged_weapon_ = value; }
 	void	set_friendly_target_npc(int8 value) { std::lock_guard<std::mutex> lk(classMutex); friendly_target_npc_ = value; }
 	void	set_last_claim_time(int32 value) { std::lock_guard<std::mutex> lk(classMutex); last_claim_time_ = value; }
+	
+	void	set_engaged_encounter(int8 value) { std::lock_guard<std::mutex> lk(classMutex); engaged_encounter_ = value; }
 
 	void	ResetEffects(Spawn* spawn)
 	{
@@ -1146,6 +1153,8 @@ private:
 	int8			friendly_target_npc_;
 	int32			last_claim_time_;
 	
+	int8			engaged_encounter_;
+	
 	// when PacketStruct is fixed for C++17 this should become a shared_mutex and handle read/write lock
 	std::mutex		classMutex;
 };
@@ -1847,6 +1856,9 @@ public:
 		MStats.unlock();
 		return item_chance_or_skill;
 	}
+	
+	bool IsEngagedInEncounter(Spawn** res = nullptr);
+	bool IsEngagedBySpawnID(int32 id);
 		
 	// when PacketStruct is fixed for C++17 this should become a shared_mutex and handle read/write lock
 	std::mutex		MEquipment;

+ 4 - 1
EQ2/source/WorldServer/LuaFunctions.cpp

@@ -1737,6 +1737,7 @@ int EQ2Emu_lua_AddHate(lua_State* state) {
 			for (int32 i = 0; i < luaspell->targets.size(); i++) {
 				Spawn* spawn = zone->GetSpawnByID(luaspell->targets.at(i));
 				if (spawn && spawn->IsNPC() && spawn->Alive() && spawn->GetZone()) {
+					entity->CheckEncounterState((Entity*)spawn);
 					((NPC*)spawn)->AddHate((Entity*)entity, amount);
 					if (send_packet)
 						entity->GetZone()->SendThreatPacket(entity, npc, amount, luaspell->spell->GetName());
@@ -1744,8 +1745,10 @@ int EQ2Emu_lua_AddHate(lua_State* state) {
 			}
 			luaspell->MSpellTargets.releasereadlock(__FUNCTION__, __LINE__);
 		}
-		else if (npc && npc->IsNPC() && npc->GetZone())
+		else if (npc && npc->IsNPC() && npc->GetZone()) {
+			entity->CheckEncounterState((Entity*)npc);
 			((NPC*)npc)->AddHate((Entity*)entity, amount);
+		}
 	}
 	lua_interface->ResetFunctionStack(state);
 	return 0;

+ 6 - 1
EQ2/source/WorldServer/NPC.cpp

@@ -28,6 +28,7 @@
 #include "Appearances.h"
 #include "SpellProcess.h"
 #include "Skills.h"
+#include "Rules/Rules.h"
 
 extern MasterSpellList master_spell_list;
 extern ConfigReader configReader;
@@ -36,6 +37,7 @@ extern World world;
 extern Races races;
 extern Appearance master_appearance_list;
 extern MasterSkillList master_skill_list;
+extern RuleManager rule_manager;
 
 NPC::NPC(){	
 	Initialize();
@@ -334,10 +336,13 @@ void NPC::InCombat(bool val){
 		}
 	}
 
+	int8 ruleAutoLockEncounter = rule_manager.GetGlobalRule(R_World, AutoLockEncounter)->GetInt8();
 	in_combat = val;
 	if(val){
 		LogWrite(NPC__DEBUG, 3, "NPC", "'%s' engaged in combat with '%s'", this->GetName(), ( GetTarget() ) ? GetTarget()->GetName() : "Unknown" );
-		SetLockedNoLoot(ENCOUNTER_STATE_LOCKED);
+		if(ruleAutoLockEncounter) {
+			SetLockedNoLoot(ENCOUNTER_STATE_LOCKED);
+		}
 		AddIconValue(64);
 		// In combat so lets set the NPC's speed to its max speed
 		if (GetMaxSpeed() > 0)

+ 93 - 6
EQ2/source/WorldServer/NPC_AI.cpp

@@ -235,7 +235,18 @@ void Brain::AddHate(Entity* entity, sint32 hate) {
 	entity->MHatedBy.unlock();
 
 	// Unlock the list
+	bool ownerExistsAddHate = false;
+	
+	if(entity->IsPet() && entity->GetOwner()) {
+		map<int32, sint32>::iterator itr = m_hatelist.find(entity->GetOwner()->GetID());
+		if(itr == m_hatelist.end()) {
+			ownerExistsAddHate = true;
+		}
+	}
 	MHateList.releasewritelock(__FUNCTION__, __LINE__);
+	if(ownerExistsAddHate) {
+		AddHate(entity->GetOwner(), 0);
+	}
 }
 
 void Brain::ClearHate() {
@@ -326,6 +337,29 @@ sint8 Brain::GetHatePercentage(Entity* entity) {
 	return (sint8)(percentage * 100);
 }
 
+void Brain::SendHateList(Client* client) {
+	MHateList.readlock(__FUNCTION__, __LINE__);
+
+	client->Message(CHANNEL_COLOR_YELLOW, "%s's HateList", m_body->GetName());
+	client->Message(CHANNEL_COLOR_YELLOW, "-------------------");
+	map<int32, sint32>::iterator itr;
+	if (m_hatelist.size() > 0) {
+		// Loop through the list looking for the entity that this NPC hates the most
+		for(itr = m_hatelist.begin(); itr != m_hatelist.end(); itr++) {
+			Entity* ent = (Entity*)GetBody()->GetZone()->GetSpawnByID(itr->first);
+			// Compare the hate value for the current iteration to our stored highest value
+			if(ent) {
+				client->Message(CHANNEL_COLOR_YELLOW, "%s : %i", ent->GetName(), itr->second);
+			}
+			else {
+				client->Message(CHANNEL_COLOR_YELLOW, "%u (cannot identity spawn id->entity) : %i", itr->first, itr->second);
+			}
+		}
+	}
+	client->Message(CHANNEL_COLOR_YELLOW, "-------------------");
+	MHateList.releasereadlock(__FUNCTION__, __LINE__);
+}
+
 vector<Entity*>* Brain::GetHateList() {
 	vector<Entity*>* ret = new vector<Entity*>;
 	map<int32, sint32>::iterator itr;
@@ -461,10 +495,14 @@ bool Brain::HasRecovered() {
 }
 
 void Brain::AddToEncounter(Entity* entity) {
-
 	// If player pet then set the entity to the pets owner
-	if (entity->IsPet() && ((NPC*)entity)->GetOwner() && ((NPC*)entity)->GetOwner()->IsPlayer())
-		entity = ((NPC*)entity)->GetOwner();
+	if (entity->IsPet() && entity->GetOwner() && !entity->IsBot()) {
+		m_encounter.push_back(entity->GetID());
+		entity = entity->GetOwner();
+	}
+	else if(entity->HasPet() && entity->GetPet()) {
+		m_encounter.push_back(entity->GetPet()->GetID());
+	}
 
 	// If player or bot then get the group
 	int32 group_id = 0;
@@ -487,10 +525,12 @@ void Brain::AddToEncounter(Entity* entity) {
 			group->MGroupMembers.readlock(__FUNCTION__, __LINE__);
 			deque<GroupMemberInfo*>* members = group->GetMembers();
 			for (itr = members->begin(); itr != members->end(); itr++) {
-				if ((*itr)->client)
+				if ((*itr)->member)
 				{
-					m_encounter.push_back((*itr)->client->GetPlayer()->GetID());
-					m_encounter_playerlist.insert(make_pair((*itr)->client->GetPlayer()->GetCharacterID(), (*itr)->client->GetPlayer()->GetID()));
+					m_encounter.push_back((*itr)->member->GetID());
+					if((*itr)->client) {
+						m_encounter_playerlist.insert(make_pair((*itr)->client->GetPlayer()->GetCharacterID(), (*itr)->client->GetPlayer()->GetID()));
+					}
 				}
 			}
 			group->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__);
@@ -577,6 +617,32 @@ vector<int32>* Brain::GetEncounter() {
 	return ret;
 }
 
+bool Brain::IsPlayerInEncounter(int32 char_id) {
+	bool ret = false;
+	MEncounter.readlock(__FUNCTION__, __LINE__);
+	std::map<int32,int32>::iterator itr = m_encounter_playerlist.find(char_id);
+	if(itr != m_encounter_playerlist.end()) {
+		ret = true;
+	}
+	MEncounter.releasereadlock(__FUNCTION__, __LINE__);
+	return ret;
+}
+
+bool Brain::IsEntityInEncounter(int32 id) {
+	bool ret = false;
+	MEncounter.readlock(__FUNCTION__, __LINE__);
+	vector<int32>::iterator itr;
+	for (itr = m_encounter.begin(); itr != m_encounter.end(); itr++) {
+		if ((*itr) == id) {
+			// found the entity in the encounter list, set return value to true and break the loop
+			ret = true;
+			break;
+		}
+	}
+	MEncounter.releasereadlock(__FUNCTION__, __LINE__);
+	return ret;
+}
+
 void Brain::ClearEncounter() {
 	MEncounter.writelock(__FUNCTION__, __LINE__);
 	m_encounter.clear();
@@ -585,6 +651,27 @@ void Brain::ClearEncounter() {
 	MEncounter.releasewritelock(__FUNCTION__, __LINE__);
 }
 
+void Brain::SendEncounterList(Client* client) {
+	client->Message(CHANNEL_COLOR_YELLOW, "%s's EncounterList", m_body->GetName());
+	client->Message(CHANNEL_COLOR_YELLOW, "-------------------");
+	vector<int32>::iterator itr;
+
+	// Check the encounter list to see if the given entity is in it, if so return true.
+	MEncounter.readlock(__FUNCTION__, __LINE__);
+
+	for (itr = m_encounter.begin(); itr != m_encounter.end(); itr++) {
+		Entity* ent = (Entity*)GetBody()->GetZone()->GetSpawnByID((*itr));
+			// Compare the hate value for the current iteration to our stored highest value
+			if(ent) {
+				client->Message(CHANNEL_COLOR_YELLOW, "%s", ent->GetName());
+			}
+			else {
+				client->Message(CHANNEL_COLOR_YELLOW, "%u (cannot identity spawn id->entity)", (*itr));
+			}
+	}
+	client->Message(CHANNEL_COLOR_YELLOW, "-------------------");
+	MEncounter.releasereadlock(__FUNCTION__, __LINE__);	
+}
 
 /* Example of how to extend the default AI */
 

+ 7 - 1
EQ2/source/WorldServer/NPC_AI.h

@@ -71,6 +71,8 @@ public:
 	/// <returns>Percentage of hate as a sint8</returns>
 	sint8 GetHatePercentage(Entity* entity);
 
+	void SendHateList(Client* client);
+	
 	///<summary>Gets a list of all the entities in the hate list</summary>
 	vector<Entity*>* GetHateList();
 
@@ -105,13 +107,17 @@ public:
 	int8 GetEncounterSize();
 	/// <summary>Clears the encounter list</summary>
 	void ClearEncounter();
+	
+	void SendEncounterList(Client* client);
+	
 	/// <summary>Gets a copy of the encounter list</summary>
 	/// <returns>A copy of the encounter list as a vector<Entity*>*</returns>
 	vector<int32>* GetEncounter();
 	/// <summary>Checks to see if a player is in the encounter</summary>
 	/// <returns>True if the encounter list contains a player</returns>
 	bool PlayerInEncounter() { return m_playerInEncounter; }
-
+	bool IsPlayerInEncounter(int32 char_id);
+	bool IsEntityInEncounter(int32 id);
 	/* Helper functions*/
 
 	/// <summary>Gets the NPC this brain controls</summary>

+ 16 - 31
EQ2/source/WorldServer/Player.cpp

@@ -3038,7 +3038,7 @@ void Player::AddMaintainedSpell(LuaSpell* luaspell){
 	int32 target_type = 0;
 	Spawn* spawn = 0;
 
-	if(effect){
+	if(effect && luaspell->caster && luaspell->caster->GetZone()){
 		GetMaintainedMutex()->writelock(__FUNCTION__, __LINE__);
 		strcpy(effect->name, spell->GetSpellData()->name.data.c_str());
 		effect->target = luaspell->initial_target;
@@ -3693,9 +3693,17 @@ void Player::InCombat(bool val, bool range) {
 		AddIconValue(64);
 	else
 		RemoveIconValue(64);
-
-	if(changeCombatState)
-		SetRegenValues(GetInfoStruct()->get_effective_level());
+	
+	bool update_regen = false;
+	if(GetInfoStruct()->get_engaged_encounter()) {
+		if(!IsAggroed() || !IsEngagedInEncounter()) {
+			GetInfoStruct()->set_engaged_encounter(0);
+			update_regen = true;
+		}
+	}
+	
+	if(changeCombatState || update_regen)
+		SetRegenValues((GetInfoStruct()->get_effective_level() > 0) ? GetInfoStruct()->get_effective_level() : GetLevel());
 
 	charsheet_changed = true;
 	info_changed = true;
@@ -4467,11 +4475,10 @@ PacketStruct* Player::GetQuestJournalPacket(Quest* quest, int16 version, int32 c
 
 	PacketStruct* packet = configReader.getStruct("WS_QuestJournalUpdate", version);
 	if (packet) {
-
 		packet->setArrayLengthByName("num_quests", 1);
 		packet->setArrayLengthByName("num_quest_zones", 1);
 		packet->setArrayDataByName("quest_zones_zone", quest->GetType());
-		packet->setArrayDataByName("quest_zones_zone_id", 0);
+		packet->setArrayDataByName("quest_zones_zone_id", 1);
 		
 		if(!quest->GetDeleted() && !quest->GetCompleted())
 			packet->setArrayDataByName("active", 1);
@@ -5209,30 +5216,6 @@ bool Player::ShouldSendSpawn(Spawn* spawn){
 	return false;
 }
 
-int8 Player::GetArrowColor(int8 spawn_level){
-	int8 color = 0;
-	sint16 diff = spawn_level - GetLevel();
-	if(GetLevel() < 10)
-		diff *= 3;
-	else if(GetLevel() <= 20)
-		diff *= 2;
-	if(diff >= 9)
-		color = ARROW_COLOR_RED;
-	else if(diff >= 5)
-		color = ARROW_COLOR_ORANGE;
-	else if(diff >= 1)
-		color = ARROW_COLOR_YELLOW;
-	else if(diff == 0)
-		color = ARROW_COLOR_WHITE;	
-	else if(diff <= -11)
-		color = ARROW_COLOR_GRAY;
-	else if(diff <= -6)
-		color = ARROW_COLOR_GREEN;
-	else //if(diff < 0)
-		color = ARROW_COLOR_BLUE;
-	return color;
-}
-
 int8 Player::GetTSArrowColor(int8 level){
 	int8 color = 0;
 	sint16 diff = level - GetTSLevel();
@@ -7022,7 +7005,9 @@ void Player::MentorTarget()
 
 void Player::SetMentorStats(int32 effective_level, int32 target_char_id, bool update_stats)
 {
-	RemoveSpells();
+	if(update_stats) {
+		RemoveSpells();
+	}
 	if(client->GetPlayer()->GetGroupMemberInfo())
 		client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id = target_char_id;
 	InfoStruct* info = GetInfoStruct();

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

@@ -634,7 +634,6 @@ public:
 	void	ClearEverything();
 	bool	IsResurrecting();
 	void	SetResurrecting(bool val);
-	int8	GetArrowColor(int8 spawn_level);
 	int8    GetTSArrowColor(int8 level);
 	Spawn*	GetSpawnByIndex(int16 index);
 	int16	GetIndexForSpawn(Spawn* spawn);

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

@@ -248,6 +248,7 @@ void RuleManager::Init()
 	RULE_INIT(R_Combat, StrengthNPC, "10"); // divider for strength NPC only str/x = additional dmg to low/high dmg
 	RULE_INIT(R_Combat, StrengthOther, "25"); // divider for strength other than NPC str/x = additional dmg to low/high dmg
 	RULE_INIT(R_Combat, MaxSkillBonusByLevel, "1.5"); // Level * 1.5 = max bonus skill allowed
+	RULE_INIT(R_Combat, LockedEncounterNoAttack, "1"); // when set to 1, players/group members not part of the encounter cannot attack until /yell
 
 	/* SPAWN */
 	RULE_INIT(R_Spawn, SpeedMultiplier, "300"); // note: this value was 1280 until 6/1/2009, then was 600 til Sep 2009, when it became 300...?
@@ -307,6 +308,8 @@ void RuleManager::Init()
 																	// 5+ - send to new and old starting zones as needed
 	RULE_INIT(R_World, EnforceRacialAlignment, "1");
 	RULE_INIT(R_World, MemoryCacheZoneMaps, "0");					// 0 disables caching the zone maps in memory, too many individual/unique zones entered may cause a lot of memory build up
+	RULE_INIT(R_World, AutoLockEncounter, "0");						// When set to 0 we require player to attack to lock the encounter, otherwise if 1 then npc can auto lock encounter
+	
 	//INSERT INTO `ruleset_details`(`id`, `ruleset_id`, `rule_category`, `rule_type`, `rule_value`, `description`) VALUES (NULL, '1', 'R_World', '', '', '')
 
 	/* ZONE */

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

@@ -106,6 +106,7 @@ enum RuleType {
 	StrengthNPC,
 	StrengthOther,
 	MaxSkillBonusByLevel,
+	LockedEncounterNoAttack,
 
 	/* SPAWN */
 	SpeedMultiplier,
@@ -160,6 +161,7 @@ enum RuleType {
 	StartingZoneRuleFlag,
 	EnforceRacialAlignment,
 	MemoryCacheZoneMaps,
+	AutoLockEncounter,
 
 	/* ZONE */
 	MinZoneLevelOverrideStatus,

+ 116 - 5
EQ2/source/WorldServer/Spawn.cpp

@@ -139,6 +139,7 @@ Spawn::Spawn(){
 	trigger_widget_id = 0;
 	scared_by_strong_players = false;
 	is_alive = true;
+	SetLockedNoLoot(ENCOUNTER_STATE_AVAILABLE);
 }
 
 Spawn::~Spawn(){
@@ -322,11 +323,21 @@ void Spawn::InitializeVisPacketData(Player* player, PacketStruct* vis_packet) {
 					}
 				}
 			}
-			vis_packet->setDataByName("arrow_color", arrow_color);
-			if (appearance.attackable == 0)
+			if(IsNPC() && (((Entity*)this)->GetLockedNoLoot() == ENCOUNTER_STATE_BROKEN || 
+					(((Entity*)this)->GetLockedNoLoot() == ENCOUNTER_STATE_OVERMATCHED) || 
+					((Entity*)this)->GetLockedNoLoot() == ENCOUNTER_STATE_LOCKED && !((NPC*)this)->Brain()->IsEntityInEncounter(player->GetID()))) {
+				vis_packet->setDataByName("arrow_color", ARROW_COLOR_GRAY);
+			}
+			else {
+				vis_packet->setDataByName("arrow_color", arrow_color);
+			}
+			if (appearance.attackable == 0 || IsPlayer() || IsBot() || (IsEntity() && ((Entity*)this)->GetOwner() &&
+				(((Entity*)this)->GetOwner()->IsPlayer() || ((Entity*)this)->GetOwner()->IsBot()))) {
 				vis_packet->setDataByName("locked_no_loot", 1);
-			else
+				}
+			else {
 				vis_packet->setDataByName("locked_no_loot", appearance.locked_no_loot);
+			}
 			if (player->GetArrowColor(GetLevel()) == ARROW_COLOR_GRAY)
 				if (npc_con == -4)
 					npc_con = -3;
@@ -2315,7 +2326,6 @@ void Spawn::InitializeInfoPacketData(Player* spawn, PacketStruct* packet) {
 		spawnHiddenFromClient = true;
 
 	if (!spawnHiddenFromClient && (appearance.targetable == 1 || appearance.show_level == 1 || appearance.display_name == 1)) {
-		appearance.locked_no_loot = 1; //for now
 		if (!IsObject() && !IsGroundSpawn() && !IsWidget() && !IsSign()) {
 			int8 percent = 0;
 			if (GetHP() > 0)
@@ -3643,6 +3653,83 @@ Spawn* Spawn::IsSpawnGroupMembersAlive(Spawn* ignore_spawn, bool npc_only) {
 	return ret;
 }
 
+void Spawn::UpdateEncounterState(int8 new_state) {
+	if (MSpawnGroup && HasSpawnGroup()) {
+		MSpawnGroup->readlock(__FUNCTION__, __LINE__);
+		vector<Spawn*>::iterator itr;
+		for (itr = spawn_group_list->begin(); itr != spawn_group_list->end(); itr++) {
+			if ((*itr)->Alive() && (*itr)->IsNPC()) {
+				NPC* npc = (NPC*)(*itr);
+				(*itr)->SetLockedNoLoot(new_state);
+				if(new_state == ENCOUNTER_STATE_BROKEN && npc->Brain()) {
+					npc->Brain()->ClearEncounter();
+				}
+			}
+		}
+		MSpawnGroup->releasereadlock(__FUNCTION__, __LINE__);
+	}
+}
+
+void Spawn::CheckEncounterState(Entity* victim) {
+	if(!IsEntity() || !victim->IsNPC())
+		return;
+	
+	Entity* ent = ((Entity*)this);
+	if(victim->GetLockedNoLoot() == ENCOUNTER_STATE_AVAILABLE) {
+		if(!ent->GetInfoStruct()->get_engaged_encounter()) {
+			ent->GetInfoStruct()->set_engaged_encounter(1);
+		}
+	
+		Entity* attacker = nullptr;
+		if(ent->GetOwner())
+			attacker = ent->GetOwner();
+		else
+			attacker = ent;
+		
+		if(!attacker->GetInfoStruct()->get_engaged_encounter()) {
+			attacker->GetInfoStruct()->set_engaged_encounter(1);
+		}
+		
+		int8 skip_loot_gray_mob_flag = rule_manager.GetGlobalRule(R_Loot, SkipLootGrayMob)->GetInt8();
+		
+		int8 difficulty = attacker->GetArrowColor(victim->GetLevel());
+		
+		int8 new_enc_state = ENCOUNTER_STATE_AVAILABLE;
+		if(skip_loot_gray_mob_flag && difficulty == ARROW_COLOR_GRAY) {
+			if(!attacker->IsPlayer() && !attacker->IsBot()) {
+				new_enc_state = ENCOUNTER_STATE_BROKEN;
+			}
+			else {
+				new_enc_state = ENCOUNTER_STATE_OVERMATCHED;
+			}
+		}
+		else {
+			if(attacker->IsPlayer() || attacker->IsBot()) {
+				new_enc_state = ENCOUNTER_STATE_LOCKED;
+			}
+			else {
+				new_enc_state = ENCOUNTER_STATE_BROKEN;
+			}
+		}
+		
+		victim->SetLockedNoLoot(new_enc_state);
+		victim->UpdateEncounterState(new_enc_state);
+	}
+}
+
+void Spawn::AddTargetToEncounter(Entity* entity) {
+	if (MSpawnGroup && HasSpawnGroup()) {
+		MSpawnGroup->readlock(__FUNCTION__, __LINE__);
+		vector<Spawn*>::iterator itr;
+		for (itr = spawn_group_list->begin(); itr != spawn_group_list->end(); itr++) {
+			if ((*itr) != this && (*itr)->Alive() && (*itr)->IsNPC()) {
+				((NPC*)(*itr))->Brain()->AddToEncounter(entity);
+			}
+		}
+		MSpawnGroup->releasereadlock(__FUNCTION__, __LINE__);
+	}
+}
+
 void Spawn::AddSpawnToGroup(Spawn* spawn){
 	if(!spawn)
 		return;
@@ -4519,4 +4606,28 @@ void Spawn::SetLocation(int32 id, bool setUpdateFlags)
 	else {
 		SetPos(&appearance.pos.grid_id, id, setUpdateFlags);
 	}
-}
+}
+
+int8 Spawn::GetArrowColor(int8 spawn_level){
+	int8 color = 0;
+	sint16 diff = spawn_level - GetLevel();
+	if(GetLevel() < 10)
+		diff *= 3;
+	else if(GetLevel() <= 20)
+		diff *= 2;
+	if(diff >= 9)
+		color = ARROW_COLOR_RED;
+	else if(diff >= 5)
+		color = ARROW_COLOR_ORANGE;
+	else if(diff >= 1)
+		color = ARROW_COLOR_YELLOW;
+	else if(diff == 0)
+		color = ARROW_COLOR_WHITE;	
+	else if(diff <= -11)
+		color = ARROW_COLOR_GRAY;
+	else if(diff <= -6)
+		color = ARROW_COLOR_GREEN;
+	else //if(diff < 0)
+		color = ARROW_COLOR_BLUE;
+	return color;
+}

+ 6 - 0
EQ2/source/WorldServer/Spawn.h

@@ -1007,6 +1007,10 @@ public:
 	bool	HasSpawnGroup();
 	bool	IsInSpawnGroup(Spawn* spawn);
 	Spawn*	IsSpawnGroupMembersAlive(Spawn* ignore_spawn=nullptr, bool npc_only = true);
+	void	UpdateEncounterState(int8 new_state);
+	void	CheckEncounterState(Entity* victim);
+	void	AddTargetToEncounter(Entity* entity);
+	
 	void	SendSpawnChanges(bool val){ send_spawn_changes = val; }
 	void	SetSpawnGroupID(int32 id);
 	int32	GetSpawnGroupID();
@@ -1295,6 +1299,8 @@ public:
 	int32 InsertRegionToSpawn(Region_Node* node, ZBSP_Node* bsp_root, WaterRegionType regionType, bool in_region = true);
 	bool HasRegionTracked(Region_Node* node, ZBSP_Node* bsp_root, bool in_region);
 	
+	int8 GetArrowColor(int8 spawn_level);
+	
 	EquipmentItemList equipment_list;
 	EquipmentItemList appearance_equipment_list;
 protected:

+ 39 - 27
EQ2/source/WorldServer/SpellProcess.cpp

@@ -416,7 +416,7 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removi
 					if(curConcentration >= actual_concentration)
 					{
 						spell->caster->GetInfoStruct()->set_cur_concentration(curConcentration - actual_concentration);
-						if (spell->caster->IsPlayer())
+						if (spell->caster->IsPlayer()&& spell->caster->GetZone())
 							spell->caster->GetZone()->TriggerCharSheetTimer();
 					}
 				}
@@ -481,7 +481,9 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removi
 								fade_message_others.replace(fade_message_others.find("%C"), 2, spell->caster->GetName());
 								send_to_sender = false;
 							}
-							spell->caster->GetZone()->SimpleMessage(CHANNEL_SPELLS_OTHER, fade_message_others.c_str(), target, 50, send_to_sender);
+							if(spell->caster->GetZone()) {
+								spell->caster->GetZone()->SimpleMessage(CHANNEL_SPELLS_OTHER, fade_message_others.c_str(), target, 50, send_to_sender);
+							}
 						}
 					}
 				}
@@ -507,14 +509,16 @@ bool SpellProcess::ProcessSpell(LuaSpell* spell, bool first_cast, const char* fu
 		{
 			for(int t=0;t<spell->targets.size();t++)
 			{
-				Spawn* altSpawn = spell->caster->GetZone()->GetSpawnByID(spell->targets[t]);
-				if(altSpawn)
-				{
-					std::string functionCall = ApplyLuaFunction(spell, first_cast, function, timer, altSpawn);
-					if(functionCall.length() < 1)
-						ret = false;
-					else
-						ret = lua_interface->CallSpellProcess(spell, 2 + spell->spell->GetLUAData()->size(), functionCall);
+				if(spell->caster->GetZone()) {
+					Spawn* altSpawn = spell->caster->GetZone()->GetSpawnByID(spell->targets[t]);
+					if(altSpawn)
+					{
+						std::string functionCall = ApplyLuaFunction(spell, first_cast, function, timer, altSpawn);
+						if(functionCall.length() < 1)
+							ret = false;
+						else
+							ret = lua_interface->CallSpellProcess(spell, 2 + spell->spell->GetLUAData()->size(), functionCall);
+					}
 				}
 			}
 			return true;
@@ -732,7 +736,7 @@ bool SpellProcess::TakePower(LuaSpell* spell, int32 custom_power_req){
 		
 		if(spell->caster->GetPower() >= req){
 			spell->caster->SetPower(spell->caster->GetPower() - req);
-			if(spell->caster->IsPlayer())
+			if(spell->caster->IsPlayer() && spell->caster->GetZone())
 				spell->caster->GetZone()->TriggerCharSheetTimer();
 			return true;
 		}
@@ -759,7 +763,7 @@ bool SpellProcess::TakeHP(LuaSpell* spell, int32 custom_hp_req) {
      		req = spell->spell->GetHPRequired(spell->caster); 
      if(spell->caster->GetHP() >= req){ 
         spell->caster->SetHP(spell->caster->GetHP() - req);
-		if(spell->caster->IsPlayer())
+		if(spell->caster->IsPlayer() && spell->caster->GetZone())
 			spell->caster->GetZone()->TriggerCharSheetTimer(); 
         return true; 
      } 
@@ -784,7 +788,7 @@ bool SpellProcess::AddConcentration(LuaSpell* spell) {
 		int8 current_avail = 5 - curConcentration;
 		if (current_avail >= req) {
 			spell->caster->GetInfoStruct()->set_cur_concentration(curConcentration + req);
-			if (spell->caster->IsPlayer())
+			if (spell->caster->IsPlayer() && spell->caster->GetZone())
 				spell->caster->GetZone()->TriggerCharSheetTimer();
 			LogWrite(SPELL__DEBUG, 0, "Spell", "Concentration is now %u on %s", spell->caster->GetInfoStruct()->get_cur_concentration(), spell->caster->GetName());
 			return true;
@@ -808,7 +812,7 @@ bool SpellProcess::TakeSavagery(LuaSpell* spell) {
      req = spell->spell->GetSavageryRequired(spell->caster); 
      if(spell->caster->GetSavagery() >= req){ 
         spell->caster->SetSavagery(spell->caster->GetSavagery() - req);
-		if(spell->caster->IsPlayer())
+		if(spell->caster->IsPlayer() && spell->caster->GetZone())
 			spell->caster->GetZone()->TriggerCharSheetTimer(); 
         return true; 
      } 
@@ -831,7 +835,7 @@ bool SpellProcess::AddDissonance(LuaSpell* spell) {
      req = spell->spell->GetDissonanceRequired(spell->caster); 
      if(spell->caster->GetDissonance() >= req){ 
         spell->caster->SetDissonance(spell->caster->GetDissonance() - req);
-		if(spell->caster->IsPlayer())
+		if(spell->caster->IsPlayer() && spell->caster->GetZone())
 			spell->caster->GetZone()->TriggerCharSheetTimer(); 
         return true; 
      } 
@@ -1277,7 +1281,18 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
 		{
 			LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Target Enemy (%s) and Max AE Targets = 0.", spell->GetName(), target->GetName());
 
-			if (spell->GetSpellData()->friendly_spell && target->IsPlayer() )
+		
+			if(spell->GetSpellData()->friendly_spell && (caster->IsPlayer() || caster->IsBot()) && (target->IsPlayer() || target->IsBot()) && 
+			   ((Entity*)target)->GetInfoStruct()->get_engaged_encounter() && 
+				((!((Entity*)caster)->GetGroupMemberInfo() || !((Entity*)target)->GetGroupMemberInfo()) || 
+				(((Entity*)caster)->GetGroupMemberInfo()->group_id != ((Entity*)target)->GetGroupMemberInfo()->group_id))) {
+				LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Target (%s) is engaged in combat and cannot be assisted.", spell->GetName(), target->GetName());
+				zone->SendSpellFailedPacket(client, SPELL_ERROR_NOT_A_FRIEND);
+				lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
+				DeleteSpell(lua_spell);
+				return;
+			}
+			else if (spell->GetSpellData()->friendly_spell && target->IsPlayer() )
 			{
 				LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Is Friendly, Target is Player (%s).", spell->GetName(), target->GetName());
 
@@ -1368,18 +1383,15 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
 					DeleteSpell(lua_spell);
 					return;
 				}
-
-				if ((target->IsPlayer() || target->IsBot()) && (caster->IsPlayer() || caster->IsBot())) 
+				
+				bool attackAllowed = (Entity*)caster->AttackAllowed((Entity*)target, 0);
+				if (!attackAllowed)
 				{
-					bool attackAllowed = (Entity*)caster->AttackAllowed((Entity*)target, 0);
-					if (!attackAllowed)
-					{
-						LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Target (%s) is player and not attackable.", spell->GetName(), target->GetName());
-						zone->SendSpellFailedPacket(client, SPELL_ERROR_NOT_AN_ENEMY);
-						lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
-						DeleteSpell(lua_spell);
-						return;
-					}
+					LogWrite(SPELL__DEBUG, 1, "Spell", "%s: Target (%s) is player and not attackable.", spell->GetName(), target->GetName());
+					zone->SendSpellFailedPacket(client, SPELL_ERROR_NOT_AN_ENEMY);
+					lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
+					DeleteSpell(lua_spell);
+					return;
 				}
 
 				if (target->IsPet() && ((NPC*)target)->GetOwner() && ((NPC*)target)->GetOwner() == caster) {

+ 39 - 5
EQ2/source/WorldServer/client.cpp

@@ -900,8 +900,25 @@ void Client::SendCharInfo() {
 	if(!groupMentor)
 		GetPlayer()->SetMentorStats(GetPlayer()->GetLevel(), 0, false);
 
-	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_MAINTAINEDEFFECTS);
-	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_SPELLEFFECTS);
+	if(!GetPlayer()->IsReturningFromLD()) {
+		database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_MAINTAINEDEFFECTS);
+		database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_SPELLEFFECTS);
+	}
+	else {
+		Spawn* pet_spawn = nullptr;
+		if(GetPlayer()->GetPet())
+			pet_spawn = GetPlayer()->GetPet();
+		else if(GetPlayer()->GetCharmedPet())
+			pet_spawn = GetPlayer()->GetCharmedPet();
+		else if(GetPlayer()->GetCosmeticPet())
+			pet_spawn = GetPlayer()->GetCosmeticPet();
+		else if(GetPlayer()->GetDeityPet())
+			pet_spawn = GetPlayer()->GetDeityPet();
+		
+		if(pet_spawn) {
+			GetPlayer()->GetInfoStruct()->set_pet_id(GetPlayer()->GetIDWithPlayerSpawn(pet_spawn));
+		}
+	}
 
 	GetPlayer()->SetSaveSpellEffects(false);
 	GetPlayer()->SetCharSheetChanged(true);
@@ -4452,7 +4469,7 @@ void Client::DetermineCharacterUpdates() {
 }
 
 void Client::Save() {
-	if (GetCharacterID() == 0)
+	if (GetCharacterID() == 0 || IsSaveDisabled())
 		return;
 
 	if (current_zone) {
@@ -10500,7 +10517,8 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code)
 		SetReadyForSpawns(false);
 		ready_for_updates = false;
 		LogWrite(ZONE__INFO, 0, "ZoneAuth", "Access Key: %u, Character Name: %s, Account ID: %u, Client Data Version: %u", zar->GetAccessKey(), zar->GetCharacterName(), zar->GetAccountID(), version);
-		if (database.loadCharacter(zar->GetCharacterName(), zar->GetAccountID(), this)) {
+		Client* client = zone_list.GetClientByCharName(zar->GetCharacterName());
+		if (client || database.loadCharacter(zar->GetCharacterName(), zar->GetAccountID(), this)) {
 			GetPlayer()->CalculateOfflineDebtRecovery(GetLastSavedTimeStamp());
 			GetPlayer()->vis_changed = false;
 			GetPlayer()->info_changed = false;
@@ -10509,10 +10527,11 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code)
 			if (pvp_allowed)
 				this->GetPlayer()->SetAttackable(1);
 			MDeletePlayer.writelock(__FUNCTION__, __LINE__);
-			Client* client = zone_list.GetClientByCharName(player->GetName());
 			if (client) {
 				if (client->getConnection())
 					client->getConnection()->SendDisconnect(true);
+				
+				bool restore_ld_success = false;
 				if (client->GetCurrentZone() && !client->IsZoning()) {
 					//swap players, allowing the client to resume his LD'd player (ONLY if same version of the client)
 					if (client->GetVersion() == version) {
@@ -10522,15 +10541,30 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code)
 						SetPlayer(client->GetPlayer());
 						GetPlayer()->SetClient(this);
 						GetPlayer()->SetReturningFromLD(true);
+										
+						SetCharacterID(client->GetCharacterID());
+						SetAccountID(client->GetAccountID());
+						SetAdminStatus(client->GetAdminStatus());
 						SetCurrentZone(GetPlayer()->GetZone());
 						GetPlayer()->GetZone()->UpdateClientSpawnMap(GetPlayer(), this);
 						client->SetPlayer(current_player);
 						GetPlayer()->GetZone()->UpdateClientSpawnMap(current_player, client);
 						GetPlayer()->ResetSavedSpawns();
+						restore_ld_success = true;
+						
+						char tmpldname[128];
+						snprintf(tmpldname, 128, "%s Linkdead",GetPlayer()->GetName());
+						client->GetPlayer()->SetName(tmpldname, false);
 					}
 					ZoneServer* tmpZone = client->GetCurrentZone();
 					tmpZone->RemoveClientImmediately(client);
 				}
+				if(!restore_ld_success && !database.loadCharacter(zar->GetCharacterName(), zar->GetAccountID(), this)) {
+					LogWrite(ZONE__ERROR, 0, "Zone", "Error reloading LD character and loading DB character: %s", player->GetName());
+					ClientPacketFunctions::SendLoginDenied(this);
+					Disconnect();
+					return false;
+				}
 			}
 			MDeletePlayer.releasewritelock(__FUNCTION__, __LINE__);
 			if (!GetCurrentZone()) {

+ 101 - 37
EQ2/source/WorldServer/zoneserver.cpp

@@ -894,6 +894,9 @@ void ZoneServer::RepopSpawns(Client* client, Spawn* in_spawn){
 bool ZoneServer::AggroVictim(NPC* npc, Spawn* victim, Client* client)
 {
 	bool isEntity = victim->IsEntity();
+	if(isEntity && !npc->AttackAllowed((Entity*)victim))
+		return false;
+	
 	if (npc->HasSpawnGroup()) {
 		vector<Spawn*>* groupVec = npc->GetSpawnGroup();
 		for (int32 i = 0; i < groupVec->size(); i++) {
@@ -973,7 +976,7 @@ bool ZoneServer::CheckEnemyList(NPC* npc) {
 				for (spawn_itr = spawns->begin(); spawn_itr != spawns->end(); spawn_itr++) {
 					Spawn* spawn = GetSpawnByID(*spawn_itr);
 					if (spawn) {
-						if ((distance = spawn->GetDistance(npc)) <= npc->GetAggroRadius() && npc->CheckLoS(spawn))
+						if ((!npc->IsPrivateSpawn() || npc->AllowedAccess(spawn)) && (distance = spawn->GetDistance(npc)) <= npc->GetAggroRadius() && npc->CheckLoS(spawn))
 							attack_spawns[distance] = spawn;
 					}
 				}
@@ -996,7 +999,7 @@ bool ZoneServer::CheckEnemyList(NPC* npc) {
 				for (spawn_itr = spawns->begin(); spawn_itr != spawns->end(); spawn_itr++) {
 					Spawn* spawn = GetSpawnByID(*spawn_itr);
 					if (spawn) {
-						if ((distance = spawn->GetDistance(npc)) <= npc->GetAggroRadius() && npc->CheckLoS(spawn))
+						if ((!npc->IsPrivateSpawn() || npc->AllowedAccess(spawn)) && (distance = spawn->GetDistance(npc)) <= npc->GetAggroRadius() && npc->CheckLoS(spawn))
 							reverse_attack_spawns[distance] = spawn;
 					}
 				}
@@ -1106,7 +1109,8 @@ void ZoneServer::CheckSpawnRange(Client* client, Spawn* spawn, bool initial_logi
 
 			spawn_range_map.Get(client)->Put(spawn->GetID(), curDist);
 
-			if(!initial_login && client && spawn->IsNPC() && curDist <= ((NPC*)spawn)->GetAggroRadius() && !client->GetPlayer()->GetInvulnerable())
+			if(!initial_login && client && spawn->IsNPC() && (!spawn->IsPrivateSpawn() || spawn->AllowedAccess(client->GetPlayer())) 
+					&& curDist <= ((NPC*)spawn)->GetAggroRadius() && !client->GetPlayer()->GetInvulnerable())
 				CheckNPCAttacks((NPC*)spawn, client->GetPlayer(), client);
 		} 
 
@@ -3272,9 +3276,6 @@ void ZoneServer::RemoveClient(Client* client)
 
 			chat.LeaveAllChannels(client);
 		}
-		
-		int32 LD_Timer = rule_manager.GetGlobalRule(R_World, LinkDeadTimer)->GetInt32();
-		int32 DisconnectClientTimer = rule_manager.GetGlobalRule(R_World, RemoveDisconnectedClientsTimer)->GetInt32();
 
 		if(!zoneShuttingDown && !client->IsZoning())
 		{
@@ -3298,7 +3299,6 @@ void ZoneServer::RemoveClient(Client* client)
 			if( (client->GetPlayer()->GetActivityStatus() & ACTIVITY_STATUS_LINKDEAD) > 0)
 			{
 				LogWrite(ZONE__DEBUG, 0, "Zone", "Removing client '%s' (%u) due to LD/Exit...", client->GetPlayer()->GetName(), client->GetPlayer()->GetCharacterID());
-				//connected_clients.Remove(client, true, LD_Timer + DisconnectClientTimer);
 			}
 			else
 			{
@@ -3341,6 +3341,8 @@ void ZoneServer::RemoveClient(Client* client)
 		database.ToggleCharacterOnline(client, 0);
 		
 		RemoveSpawn(client->GetPlayer(), false, true, true, true, true);
+		
+		int32 DisconnectClientTimer = rule_manager.GetGlobalRule(R_World, RemoveDisconnectedClientsTimer)->GetInt32();
 		connected_clients.Remove(client, true, DisconnectClientTimer); // changed from a hardcoded 30000 (30 sec) to the DisconnectClientTimer rule
 	}
 }
@@ -3350,6 +3352,15 @@ void ZoneServer::RemoveClientImmediately(Client* client) {
 
 	if(client) 
 	{
+		if(client->GetPlayer()) {
+			if((client->GetPlayer()->GetActivityStatus() & ACTIVITY_STATUS_LINKDEAD) > 0) {
+				client->GetPlayer()->SetActivityStatus(client->GetPlayer()->GetActivityStatus() - ACTIVITY_STATUS_LINKDEAD);
+			}
+			if ((client->GetPlayer()->GetActivityStatus() & ACTIVITY_STATUS_CAMPING) == 0) {
+				client->GetPlayer()->SetActivityStatus(client->GetPlayer()->GetActivityStatus() + ACTIVITY_STATUS_CAMPING);
+			}
+			client->Disconnect();
+		}
 		MClientList.writelock(__FUNCTION__, __LINE__);
 		std::vector<Client*>::iterator itr = find(clients.begin(), clients.end(), client);
 		if (itr != clients.end())
@@ -3439,19 +3450,23 @@ void ZoneServer::ClientProcess()
 		{
 			LogWrite(ZONE__ERROR, 0, "Zone", "Exception caught when in ZoneServer::ClientProcess() for zone '%s'!\n%s, %i", GetZoneName(), __FUNCTION__, __LINE__);
 			try{
+				bool isLinkdead = false;
 				if(!client->IsZoning())
 				{
 					if( (client->GetPlayer()->GetActivityStatus() & ACTIVITY_STATUS_CAMPING) == 0 )
 					{
 						client->GetPlayer()->SetActivityStatus(client->GetPlayer()->GetActivityStatus() + ACTIVITY_STATUS_LINKDEAD);
 						client->StartLinkdeadTimer();
+						isLinkdead = true;
 						if(client->GetPlayer()->GetGroupMemberInfo())
 							world.GetGroupManager()->GroupMessage(client->GetPlayer()->GetGroupMemberInfo()->group_id, "%s has gone Linkdead.", client->GetPlayer()->GetName());
 					}
 				}
 				
-				RemoveClient(client);
-				client->Disconnect();
+				if(!isLinkdead) {
+					RemoveClient(client);
+					client->Disconnect();
+				}
 			}
 			catch(...){
 				LogWrite(ZONE__ERROR, 0, "Zone", "Exception caught when in ZoneServer::ClientProcess(), second try\n%s, %i", __FUNCTION__, __LINE__);
@@ -4644,13 +4659,22 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 	Client* client = 0;
 	vector<int32>* encounter = 0;
 	bool killer_in_encounter = false;
-
+	int8 loot_state = dead->GetLockedNoLoot();
+		
 	if(dead->IsEntity())
 	{
 		// add any special quest related loot (no_drop_quest_completed)
-		if(dead->IsNPC() && killer && killer != dead)
-			AddLoot((NPC*)dead, killer);
-		
+		if(dead->IsNPC() && ((NPC*)dead)->Brain()) {
+			if(!((NPC*)dead)->Brain()->PlayerInEncounter() || (loot_state != ENCOUNTER_STATE_LOCKED && loot_state != ENCOUNTER_STATE_OVERMATCHED)) {
+				LogWrite(LOOT__DEBUG, 0, "Loot", "NPC %s bypassed loot drop due to no player in encounter, or encounter state not locked.", ((NPC*)dead)->GetName());
+			}
+			else {
+				Entity* hated = ((NPC*)dead)->Brain()->GetMostHated();
+				if(hated) {
+					AddLoot((NPC*)dead, hated);
+				}
+			}
+		}
 		((Entity*)dead)->InCombat(false);
 		dead->SetInitialState(16512, false); // This will make aerial npc's fall after death
 		dead->SetHP(0);
@@ -4700,30 +4724,30 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 	dead->SetActionState(0);
 	dead->SetTempActionState(0);
 
-		// Needs npc to have access to the encounter list for who is allowed to loot
-		NPC* chest = 0;
-		
-		if (dead->IsNPC() && !((NPC*)dead)->Brain()->PlayerInEncounter()) {
-			dead->SetLootCoins(0);
-			dead->ClearLoot();
-		}
+	// Needs npc to have access to the encounter list for who is allowed to loot
+	NPC* chest = 0;
+	
+	if (dead->IsNPC() && !((NPC*)dead)->Brain()->PlayerInEncounter()) {
+		dead->SetLootCoins(0);
+		dead->ClearLoot();
+	}
 
-		Spawn* groupMemberAlive = nullptr;
-		// If dead has loot attempt to drop a chest
-		if (dead->HasLoot()) {
-			if(!(groupMemberAlive = dead->IsSpawnGroupMembersAlive(dead))) {
-				chest = ((Entity*)dead)->DropChest();
-			}
-			else {
-				switch(dead->GetLootDropType()) {
-					case 0: 
-						// default drop all chest type as a group
-						dead->TransferLoot(groupMemberAlive);
-					break;
-					case 1:
-						// this is a primary mob it drops its own loot
-						chest = ((Entity*)dead)->DropChest();
-					break;
+	Spawn* groupMemberAlive = nullptr;
+	// If dead has loot attempt to drop a chest
+	if (dead->HasLoot()) {
+		if(!(groupMemberAlive = dead->IsSpawnGroupMembersAlive(dead))) {
+			chest = ((Entity*)dead)->DropChest();
+		}
+		else {
+			switch(dead->GetLootDropType()) {
+				case 0: 
+					// default drop all chest type as a group
+					dead->TransferLoot(groupMemberAlive);
+				break;
+				case 1:
+					// this is a primary mob it drops its own loot
+					chest = ((Entity*)dead)->DropChest();
+				break;
 			}
 		}
 	}
@@ -4754,7 +4778,9 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo
 				// valid client?
 				if (client) {
 					// Check for quest kill updates
-					client->CheckPlayerQuestsKillUpdate(dead);
+					if(!dead->IsNPC() || loot_state != ENCOUNTER_STATE_BROKEN) {
+						client->CheckPlayerQuestsKillUpdate(dead);
+					}
 					// If the dead mob is not a player and if it had a faction with an ID greater or equal to 10 the send faction changes
 					if (!dead->IsPlayer() && dead->GetFactionID() > 10)
 						ProcessFaction(dead, client);
@@ -5098,6 +5124,44 @@ void ZoneServer::SendThreatPacket(Spawn* caster, Spawn* target, int32 threat_amt
 	MClientList.releasereadlock(__FUNCTION__, __LINE__);
 }
 
+
+void ZoneServer::SendYellPacket(Spawn* yeller, float max_distance) {
+	Client* client = 0;
+
+					
+	string yellMsg = std::string(yeller->GetName()) + " yelled for help!";
+	vector<Client*>::iterator client_itr;
+	PacketStruct* packet = nullptr;
+	MClientList.readlock(__FUNCTION__, __LINE__);
+	for (client_itr = clients.begin(); client_itr != clients.end(); client_itr++) {
+		client = *client_itr;
+		if(!client || !client->GetPlayer() || client->GetPlayer()->WasSentSpawn(yeller->GetID()) == false)
+			continue;
+		if(client->GetPlayer()->GetDistance(yeller) > max_distance)
+			continue;
+		
+		if(packet && packet->GetVersion() == client->GetVersion()) {
+			client->QueuePacket(packet->serialize());
+		}
+		else {
+			safe_delete(packet);
+			packet = configReader.getStruct("WS_EncounterBroken", client->GetVersion());
+			if (packet) {
+				packet->setDataByName("message", yellMsg.c_str());
+				/* none of the other data seems necessary, keeping for reference for future disassembly
+				packet2->setDataByName("unknown2", 0x40);
+				packet2->setDataByName("unknown3", 0x40);
+				packet2->setDataByName("unknown4", 0xFF);
+				packet2->setDataByName("unknown5", 0xFF);
+				packet2->setDataByName("unknown6", 0xFF);*/
+				client->QueuePacket(packet->serialize());
+			}
+		}
+	}
+	safe_delete(packet);
+	MClientList.releasereadlock(__FUNCTION__, __LINE__);
+}
+
 void ZoneServer::SendSpellFailedPacket(Client* client, int16 error){
 	if(!client)
 		return;

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

@@ -468,6 +468,7 @@ public:
 	void			PlayFlavorID(Spawn* spawn, int8 type, int32 id, int16 index, int8 language);
 	void			PlayVoice(Spawn* spawn, const char* mp3, int32 key1, int32 key2);
 	void			SendThreatPacket(Spawn* caster, Spawn* target, int32 threat_amt, const char* spell_name);
+	void			SendYellPacket(Spawn* yeller, float max_distance=50.0f);
 	void			KillSpawnByDistance(Spawn* spawn, float max_distance, bool include_players = false, bool send_packet = false);
 	void			SpawnSetByDistance(Spawn* spawn, float max_distance, string field, string value);
 	void			AddSpawnScriptTimer(SpawnScriptTimer* timer);

+ 87 - 78
server/WorldStructs.xml

@@ -2005,84 +2005,84 @@ to zero and treated like placeholders." />
 <Data ElementName="bind_zone" Type="char" Size="61" /> <!-- 4837 -->
 </Struct>
 <Struct Name="WS_CharacterSheet" ClientVersion="60114" OpcodeName="OP_UpdateCharacterSheetMsg">
-<Data ElementName="character_name" Type="char" Size="40" />
-<Data ElementName="unknown_1_1_MJ" Type="int16" Size="1" />
-<Data ElementName="race" Type="int8" Size="1" />
-<Data ElementName="gender" Type="int8" Size="1" />
-<Data ElementName="exiled" Type="int8" Size="1" />
-<Data ElementName="class1" Type="int32" Size="1" />
-<Data ElementName="class2" Type="int32" Size="1" />
-<Data ElementName="class3" Type="int32" Size="1" />
-<Data ElementName="tradeskill_class1" Type="int32" Size="1" />
-<Data ElementName="tradeskill_class2" Type="int32" Size="1" />
-<Data ElementName="tradeskill_class3" Type="int32" Size="1" />
-<Data ElementName="level" Type="int16" Size="1" />
-<Data ElementName="effective_level" Type="int16" Size="1" />
-<Data ElementName="tradeskill_level" Type="int16" Size="1" />
-<Data ElementName="unknown_1_2_MJ" Type="int32" Size="1" />
-<Data ElementName="account_age_base" Type="int16" Size="1" />
-<Data ElementName="account_age_bonus" Type="int16" Size="1" />
-<Data ElementName="deity" Type="char" Size="32" />
-<Data ElementName="last_name" Type="char" Size="20" />
-<Data ElementName="unknown3" Type="int8" Size="1" />
-<Data ElementName="character_name2" Type="char" Size="40" />
-<Data ElementName="character_name2_unknown" Type="int16" Size="1" />
-<Data ElementName="character_name3" Type="char" Size="40" />
-<Data ElementName="character_name3_unknown" Type="int16" Size="1" />    
-<Data ElementName="current_hp" Type="sint64" Size="1" />
-<Data ElementName="max_hp" Type="int64" Size="1" />
-<Data ElementName="base_hp" Type="int32" Size="1" />
-<Data ElementName="base_hp2" Type="int32" Size="1" />
-<Data ElementName="current_power" Type="sint32" Size="1" />
-<Data ElementName="max_power" Type="sint32" Size="1" />
-<Data ElementName="base_power" Type="int32" Size="1" />
-<Data ElementName="conc_used" Type="int8" Size="1" />
-<Data ElementName="conc_max" Type="int8" Size="1" />
-<Data ElementName="savagery" Type="sint32" Size="1" />
-<Data ElementName="max_savagery" Type="sint32" Size="1" />
-<Data ElementName="unknown4b" Type="int32" Size="1" />
-<Data ElementName="savagery_level" Type="int32" Size="1" />
-<Data ElementName="max_savagery_level" Type="int32" Size="1" />
-<Data ElementName="unknown4c" Type="int8" Size="4" />
-<Data ElementName="dissonance" Type="sint32" Size="1" /> <!-- index 283 -->
-<Data ElementName="max_dissonance" Type="sint32" Size="1" />
-<Data ElementName="unknown5c" Type="int8" Size="4" />
-<Data ElementName="hp_regen" Type="int32" Size="1" />
-<Data ElementName="power_regen" Type="int32" Size="1" />
-<Data ElementName="unknown6" Type="int32" Size="2" />
-<Data ElementName="unknown7" Type="float" Size="2" />
-<Data ElementName="stat_bonus_health" Type="float" Size="1" />
-<Data ElementName="stat_bonus_power" Type="float" Size="1" />
-<Data ElementName="bonus_health" Type="int32" Size="1" />
-<Data ElementName="unknown8" Type="int32" Size="1" />
-<Data ElementName="bonus_power" Type="int32" Size="1" />
-<Data ElementName="stat_bonus_damage" Type="float" Size="1" />
-<Data ElementName="mitigation_pct_pve" Type="int16" Size="1" />
-<Data ElementName="mitigation_pct_pvp" Type="int16" Size="1" />
-<Data ElementName="toughness" Type="int16" Size="1" />
-<Data ElementName="toughness_resist_dmg_pvp" Type="float" Size="1" />
-<Data ElementName="lethality" Type="int16" Size="1" />
-<Data ElementName="lethality_pct" Type="float" Size="1" />
-<Data ElementName="avoidance_pct" Type="int16" Size="1" />
-<Data ElementName="avoidance_reduction" Type="int16" Size="1" />
-<Data ElementName="avoidance" Type="int16" Size="1" />
-<Data ElementName="unknown10" Type="int16" Size="1" />
-<Data ElementName="avoidance_base" Type="int16" Size="1" />
-<Data ElementName="unknown10a" Type="int16" Size="1" />
-<Data ElementName="parry" Type="int16" Size="1" />
-<Data ElementName="unknown11" Type="int16" Size="1" />
-<Data ElementName="block" Type="int16" Size="1" />
-<Data ElementName="unknown12" Type="int16" Size="1" />
-<Data ElementName="uncontested_block" Type="int16" Size="1" />
-<Data ElementName="unknown13" Type="int16" Size="1" />
-<Data ElementName="uncontested_riposte" Type="int16" Size="1" />
-<Data ElementName="uncontested_dodge" Type="int16" Size="1" />
-<Data ElementName="uncontested_parry" Type="int16" Size="1" />
-<Data ElementName="str" Type="int32" Size="1" />
-<Data ElementName="sta" Type="int32" Size="1" />
-<Data ElementName="agi" Type="int32" Size="1" />
-<Data ElementName="wis" Type="int32" Size="1" />
-<Data ElementName="int" Type="int32" Size="1" />
+<Data ElementName="character_name" Type="char" Size="40" /> <!-- 40 -->
+<Data ElementName="unknown_1_1_MJ" Type="int16" Size="1" /> <!-- 41 -->
+<Data ElementName="race" Type="int8" Size="1" /> <!-- 42 -->
+<Data ElementName="gender" Type="int8" Size="1" /> <!-- 43 -->
+<Data ElementName="exiled" Type="int8" Size="1" /> <!-- 44 -->
+<Data ElementName="class1" Type="int32" Size="1" /> <!-- 48 -->
+<Data ElementName="class2" Type="int32" Size="1" /> <!-- 52 -->
+<Data ElementName="class3" Type="int32" Size="1" /> <!-- 56 -->
+<Data ElementName="tradeskill_class1" Type="int32" Size="1" /> <!-- 60 -->
+<Data ElementName="tradeskill_class2" Type="int32" Size="1" /> <!-- 64 -->
+<Data ElementName="tradeskill_class3" Type="int32" Size="1" /> <!-- 68 -->
+<Data ElementName="level" Type="int16" Size="1" /> <!-- 70 -->
+<Data ElementName="effective_level" Type="int16" Size="1" /> <!-- 72 -->
+<Data ElementName="tradeskill_level" Type="int16" Size="1" /> <!-- 74 -->
+<Data ElementName="unknown_1_2_MJ" Type="int32" Size="1" /> <!-- 78 -->
+<Data ElementName="account_age_base" Type="int16" Size="1" /> <!-- 80 -->
+<Data ElementName="account_age_bonus" Type="int16" Size="1" /> <!-- 82 -->
+<Data ElementName="deity" Type="char" Size="32" /> <!-- 114 -->
+<Data ElementName="last_name" Type="char" Size="20" /> <!-- 124 -->
+<Data ElementName="unknown3" Type="int8" Size="1" /> <!-- 125 -->
+<Data ElementName="character_name2" Type="char" Size="40" /> <!-- 165 -->
+<Data ElementName="character_name2_unknown" Type="int16" Size="1" /> <!-- 167 -->
+<Data ElementName="character_name3" Type="char" Size="40" /> <!-- 207 -->
+<Data ElementName="character_name3_unknown" Type="int16" Size="1" />  <!-- 209 -->   
+<Data ElementName="current_hp" Type="sint64" Size="1" /> <!-- 217 -->
+<Data ElementName="max_hp" Type="int64" Size="1" /> <!-- 225 -->
+<Data ElementName="base_hp" Type="int32" Size="1" /> <!-- 229 -->
+<Data ElementName="base_hp2" Type="int32" Size="1" /> <!-- 233 -->
+<Data ElementName="current_power" Type="sint32" Size="1" /> <!-- 227 -->
+<Data ElementName="max_power" Type="sint32" Size="1" /> <!-- 231 -->
+<Data ElementName="base_power" Type="int32" Size="1" /> <!-- 235 -->
+<Data ElementName="conc_used" Type="int8" Size="1" /> <!-- 236 -->
+<Data ElementName="conc_max" Type="int8" Size="1" /> <!-- 237 -->
+<Data ElementName="savagery" Type="sint32" Size="1" /> <!-- 241 -->
+<Data ElementName="max_savagery" Type="sint32" Size="1" /> <!-- 245 -->
+<Data ElementName="unknown4b" Type="int32" Size="1" /> <!-- 249 -->
+<Data ElementName="savagery_level" Type="int32" Size="1" /> <!-- 253 -->
+<Data ElementName="max_savagery_level" Type="int32" Size="1" /> <!-- 257 -->
+<Data ElementName="unknown4c" Type="int8" Size="4" /> <!-- 261 -->
+<Data ElementName="dissonance" Type="sint32" Size="1" />  <!-- 265 -->
+<Data ElementName="max_dissonance" Type="sint32" Size="1" /> <!-- 269 -->
+<Data ElementName="unknown5c" Type="int8" Size="4" /> <!-- 273 -->
+<Data ElementName="hp_regen" Type="int32" Size="1" /> <!-- 277 -->
+<Data ElementName="power_regen" Type="int32" Size="1" /> <!-- 281 -->
+<Data ElementName="unknown6" Type="int32" Size="2" /> <!-- 289 -->
+<Data ElementName="unknown7" Type="float" Size="2" /> <!-- 297 -->
+<Data ElementName="stat_bonus_health" Type="float" Size="1" /> <!-- 301 -->
+<Data ElementName="stat_bonus_power" Type="float" Size="1" /> <!-- 305 -->
+<Data ElementName="bonus_health" Type="int32" Size="1" /> <!-- 309 -->
+<Data ElementName="unknown8" Type="int32" Size="1" /> <!-- 313 -->
+<Data ElementName="bonus_power" Type="int32" Size="1" /> <!-- 317 -->
+<Data ElementName="stat_bonus_damage" Type="float" Size="1" /> <!-- 321 -->
+<Data ElementName="mitigation_pct_pve" Type="int16" Size="1" /> <!-- 323 -->
+<Data ElementName="mitigation_pct_pvp" Type="int16" Size="1" /> <!-- 325 -->
+<Data ElementName="toughness" Type="int16" Size="1" /> <!-- 327 -->
+<Data ElementName="toughness_resist_dmg_pvp" Type="float" Size="1" /> <!-- 331 -->
+<Data ElementName="lethality" Type="int16" Size="1" /> <!-- 333 -->
+<Data ElementName="lethality_pct" Type="float" Size="1" /> <!-- 337 -->
+<Data ElementName="avoidance_pct" Type="int16" Size="1" /> <!-- 339 -->
+<Data ElementName="avoidance_reduction" Type="int16" Size="1" /> <!-- 341 -->
+<Data ElementName="avoidance" Type="int16" Size="1" /> <!-- 343 -->
+<Data ElementName="unknown10" Type="int16" Size="1" /> <!-- 345 -->
+<Data ElementName="avoidance_base" Type="int16" Size="1" /> <!-- 347 -->
+<Data ElementName="unknown10a" Type="int16" Size="1" /> <!-- 349 -->
+<Data ElementName="parry" Type="int16" Size="1" /> <!-- 351 -->
+<Data ElementName="unknown11" Type="int16" Size="1" /> <!-- 353 -->
+<Data ElementName="block" Type="int16" Size="1" /> <!-- 355 -->
+<Data ElementName="unknown12" Type="int16" Size="1" /> <!-- 357 -->
+<Data ElementName="uncontested_block" Type="int16" Size="1" /> <!-- 359 -->
+<Data ElementName="unknown13" Type="int16" Size="1" /> <!-- 361 -->
+<Data ElementName="uncontested_riposte" Type="int16" Size="1" /> <!-- 363 -->
+<Data ElementName="uncontested_dodge" Type="int16" Size="1" /> <!-- 365 -->
+<Data ElementName="uncontested_parry" Type="int16" Size="1" /> <!-- 367 -->
+<Data ElementName="str" Type="int32" Size="1" /> <!-- 369 -->
+<Data ElementName="sta" Type="int32" Size="1" /> <!-- 371 -->
+<Data ElementName="agi" Type="int32" Size="1" /> <!-- 373 -->
+<Data ElementName="wis" Type="int32" Size="1" /> <!-- 375 -->
+<Data ElementName="int" Type="int32" Size="1" /> <!-- 377 -->
 <Data ElementName="str_base" Type="int32" Size="1" />
 <Data ElementName="sta_base" Type="int32" Size="1" />
 <Data ElementName="agi_base" Type="int32" Size="1" />
@@ -19122,4 +19122,13 @@ to zero and treated like placeholders." />
 	<Data ElementName="item_id" Type="int32"/>
 </Data>
 </Struct>
+<Struct Name="WS_EncounterBroken" ClientVersion="1" OpcodeName="OP_EncounterBrokenMsg">
+   <Data ElementName="message" Type="EQ2_16Bit_String" />
+   <Data ElementName="unknown1" Type="int32"/>
+   <Data ElementName="unknown2" Type="int8"/>
+   <Data ElementName="unknown3" Type="int8"/>
+   <Data ElementName="unknown4" Type="int8"/>
+   <Data ElementName="unknown5" Type="int8"/>
+   <Data ElementName="unknown6" Type="int8"/>
+</Struct>
 </EQ2Emulator>