Browse Source

number of spell casting/display behavior fixes and prototype mentor option for group members

Start of work for /mentor and /unmentor issue #332 - set effective level based on targeted group member, right clicking them and mentoring, after you can stop mentoring.  The base stats and resistance stats revert to the lower level at this time.

spell fixes:

- secondary targets working for casting heal spells, recast is more or not working correctly now..
- overrides existing spell when there is a duration of time or trigger count to the spell
- doesnt prematurely show the icons available (eg. you cancel a spell then it pops up and you cant reuse or it starts a recast timer when it shouldnt)
- doesnt reset the recast timer (sometimes giving double the time to cast)
- unlocking a spell when all spells locked, no longer occurs
Image 3 years ago
parent
commit
261c2b75be

+ 5 - 2
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -4976,8 +4976,11 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 						}
 					break;
 				}
-
-
+		case COMMAND_UNMENTOR:
+		case COMMAND_MENTOR: {
+			client->GetPlayer()->MentorTarget();
+			break;
+		}
 		default: 
 		{
 			LogWrite(COMMAND__WARNING, 0, "Command", "Unhandled command: %s", command->command.data.c_str());

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

@@ -896,6 +896,9 @@ private:
 
 #define COMMAND_FROMBROKER           	527
 
+#define COMMAND_MENTOR		           	528
+#define COMMAND_UNMENTOR		        529
+
 
 #define GET_AA_XML						750
 #define ADD_AA							751

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

@@ -1666,14 +1666,6 @@ public:
 
 
 	bool SetInfoStructString(std::string field, std::string value);
-	bool SetInfoStructInt8(std::string field, int8 value);
-	bool SetInfoStructInt16(std::string field, int16 value);
-	bool SetInfoStructInt32(std::string field, int32 value);
-	bool SetInfoStructInt64(std::string field, int64 value);
-	bool SetInfoStructSInt8(std::string field, sint8 value);
-	bool SetInfoStructSInt16(std::string field, sint16 value);
-	bool SetInfoStructSInt32(std::string field, sint32 value);
-	bool SetInfoStructSInt64(std::string field, sint64 value);
 	bool SetInfoStructUInt(std::string field, int64 value);
 	bool SetInfoStructSInt(std::string field, sint64 value);
 	bool SetInfoStructFloat(std::string field, float value);

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

@@ -273,6 +273,8 @@ bool LuaInterface::LoadLuaSpell(const char* name) {
 		spell->MSpellTargets.SetName("LuaSpell.MSpellTargets");
 		spell->cancel_after_all_triggers = false;
 		spell->num_triggers = 0;
+		spell->num_calls = 0;
+		spell->is_recast_timer = false;
 		spell->had_triggers = false;
 		spell->had_dmg_remaining = false;
 		spell->slot_pos = 0;
@@ -1847,6 +1849,8 @@ LuaSpell* LuaInterface::GetSpell(const char* name)  {
 		new_spell->MSpellTargets.SetName("LuaSpell.MSpellTargets");
 		new_spell->cancel_after_all_triggers = false;
 		new_spell->num_triggers = 0;
+		new_spell->num_calls = 0;
+		new_spell->is_recast_timer = false;
 		new_spell->had_triggers = false;
 		new_spell->had_dmg_remaining = false;
 		new_spell->slot_pos = 0;

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

@@ -81,6 +81,7 @@ struct LuaSpell{
 	lua_State*		state;
 	string			file_name;
 	Timer			timer;
+	bool			is_recast_timer;
 	int16			num_calls;
 	int16           num_triggers;
 	int8            slot_pos;

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

@@ -120,6 +120,8 @@ Player::Player(){
 	m_playerSpawnHistoryRequired.SetName("Player::player_spawn_history_required");
 	gm_vision = false;
 	SetSaveSpellEffects(true);
+	reset_mentorship = false;
+	all_spells_locked = false;
 }
 Player::~Player(){
 	SetSaveSpellEffects(true);
@@ -2367,6 +2369,7 @@ void Player::AddSpellBookEntry(int32 spell_id, int8 tier, sint32 slot, int32 typ
 	spell->player = this;
 	spell->visible = true;
 	spell->in_use = false;
+	spell->in_remiss = false;
 	MSpellsBook.lock();
 	spells.push_back(spell);
 	MSpellsBook.unlock();
@@ -2675,6 +2678,8 @@ void Player::LockAllSpells() {
 			RemoveSpellStatus((*itr), SPELL_STATUS_LOCK, false);
 	}
 
+	all_spells_locked = true;
+
 	MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
 }
 
@@ -2685,13 +2690,24 @@ void Player::UnlockAllSpells(bool modify_recast, Spell* exception) {
 		exception_spell_id = exception->GetSpellID();
 	MSpellsBook.writelock(__FUNCTION__, __LINE__);
 	for (itr = spells.begin(); itr != spells.end(); itr++) {
+		MaintainedEffects* effect = 0;
+		if((effect = GetMaintainedSpell((*itr)->spell_id)) && effect->spell->spell->GetSpellData()->duration_until_cancel)
+			continue;
+
 		if ((*itr)->in_use == false && 
 			 (((*itr)->spell_id != exception_spell_id || 
 			 (*itr)->timer > 0 && (*itr)->timer != exception->GetSpellData()->linked_timer)
 		&& (*itr)->type != SPELL_BOOK_TYPE_TRADESKILL))
 			AddSpellStatus((*itr), SPELL_STATUS_LOCK, modify_recast);
+		else if((*itr)->in_remiss)
+		{
+			AddSpellStatus((*itr), SPELL_STATUS_LOCK);
+			(*itr)->in_remiss = false;
+		}
 	}
 
+	all_spells_locked = false;
+
 	MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
 }
 
@@ -2723,7 +2739,10 @@ void Player::UnlockSpell(Spell* spell) {
 		if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer))
 		{
 			spell2->in_use = false;
-			AddSpellStatus(spell2, SPELL_STATUS_LOCK);
+			if(all_spells_locked)
+				spell2->in_remiss = true;
+			else
+				AddSpellStatus(spell2, SPELL_STATUS_LOCK);
 		}
 	}
 	MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
@@ -6602,4 +6621,60 @@ void Player::SaveSpellEffects()
 	}
 	MMaintainedSpells.releasereadlock(__FUNCTION__, __LINE__);
 	MSpellEffects.releasereadlock(__FUNCTION__, __LINE__);
+}
+
+void Player::MentorTarget()
+{
+	if(client->GetPlayer()->GetGroupMemberInfo() && client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id)
+	{
+		client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id = 0;
+		reset_mentorship = true;
+		client->Message(CHANNEL_COMMAND_TEXT, "You stop mentoring, and return to level %u.", client->GetPlayer()->GetLevel());
+	}
+	else if(!reset_mentorship && client->GetPlayer()->GetTarget())
+	{
+		if(client->GetPlayer()->GetTarget()->IsPlayer())
+		{
+			Player* tmpPlayer = (Player*)client->GetPlayer()->GetTarget();
+			if(tmpPlayer->GetGroupMemberInfo() && tmpPlayer->GetGroupMemberInfo()->mentor_target_char_id)
+			{
+				client->Message(CHANNEL_COMMAND_TEXT, "You cannot mentor %s at this time.",tmpPlayer->GetName());
+				return;
+			}
+			if(client->GetPlayer()->group_id > 0 && client->GetPlayer()->GetTarget()->group_id == client->GetPlayer()->group_id)
+			{
+				if(client->GetPlayer()->GetGroupMemberInfo() && !client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id && 
+				client->GetPlayer()->GetZone() == client->GetPlayer()->GetTarget()->GetZone())
+				{
+					SetMentorStats(client->GetPlayer()->GetTarget()->GetLevel(), tmpPlayer->GetCharacterID());
+					client->Message(CHANNEL_COMMAND_TEXT, "You are now mentoring %s, reducing your effective level to %u.",client->GetPlayer()->GetTarget()->GetName(), client->GetPlayer()->GetTarget()->GetLevel());
+				}
+			}
+		}
+	}
+}
+
+void Player::SetMentorStats(int32 effective_level, int32 target_char_id)
+{
+	if(client->GetPlayer()->GetGroupMemberInfo())
+		client->GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id = target_char_id;
+	InfoStruct* info = GetInfoStruct();
+	info->set_effective_level(effective_level);
+	client->GetPlayer()->SetTotalHPBase(effective_level * effective_level * 2 + 40);
+	client->GetPlayer()->SetTotalPowerBase((sint32)(effective_level * effective_level * 2.1 + 45));
+	client->GetPlayer()->CalculateBonuses();
+	client->GetPlayer()->SetHP(GetTotalHP());
+	client->GetPlayer()->SetPower(GetTotalPower());
+	info->set_agi_base(effective_level * 2 + 15);
+	info->set_intel_base(effective_level * 2 + 15);
+	info->set_wis_base(effective_level * 2 + 15);
+	info->set_str_base(effective_level * 2 + 15);
+	info->set_sta_base(effective_level * 2 + 15);
+	info->set_cold_base((int16)(effective_level * 1.5 + 10));
+	info->set_heat_base((int16)(effective_level * 1.5 + 10));
+	info->set_disease_base((int16)(effective_level * 1.5 + 10));
+	info->set_mental_base((int16)(effective_level * 1.5 + 10));
+	info->set_magic_base((int16)(effective_level * 1.5 + 10));
+	info->set_divine_base((int16)(effective_level * 1.5 + 10));
+	info->set_poison_base((int16)(effective_level * 1.5 + 10));
 }

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

@@ -162,6 +162,7 @@ struct SpellBookEntry{
 	int32	timer;
 	bool	save_needed;
 	bool	in_use;
+	bool	in_remiss;
 	Player* player;
 	bool visible;
 };
@@ -976,7 +977,26 @@ public:
 	// bot index, spawn id
 	map<int32, int32> SpawnedBots;
 	bool StopSaveSpellEffects() { return stop_save_spell_effects; }
+
+	void MentorTarget();
+	void SetMentorStats(int32 effective_level, int32 target_char_id = 0);
+
+	bool ResetMentorship() { 
+		bool mentorship_status = reset_mentorship;
+		if(mentorship_status)
+		{
+			SetMentorStats(GetLevel());
+		}
+		reset_mentorship = false;
+		return mentorship_status;
+	}
+
+	void EnableResetMentorship()
+	{
+		reset_mentorship = true;
+	}
 private:
+	bool reset_mentorship;
 	bool range_attack;
 	int16 last_movement_activity;
 	bool returning_from_ld;
@@ -1101,6 +1121,8 @@ private:
 	map<int32, Spawn*>	player_spawn_id_map;
 	map<Spawn*, int32>	player_spawn_reverse_id_map;
 	map<Spawn*, int8>	player_removed_spawns;
+
+	bool all_spells_locked;
 };
 #pragma pack()
 #endif

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

@@ -63,6 +63,7 @@ bool PlayerGroup::AddMember(Entity* member) {
 		gmi->client = member->GetZone()->GetClientBySpawn(member);
 	else
 		gmi->client = 0;
+	gmi->mentor_target_char_id = 0;
 
 	member->SetGroupMemberInfo(gmi);
 	member->group_id = gmi->group_id;
@@ -82,7 +83,7 @@ bool PlayerGroup::RemoveMember(Entity* member) {
 	}
 
 	bool ret = false;
-
+	
 	member->SetGroupMemberInfo(0);
 	MGroupMembers.writelock();
 	deque<GroupMemberInfo*>::iterator erase_itr = m_members.end();
@@ -90,6 +91,13 @@ bool PlayerGroup::RemoveMember(Entity* member) {
 	for (itr = m_members.begin(); itr != m_members.end(); itr++) {
 		if (gmi == *itr)
 			erase_itr = itr;
+		
+		if(member->IsPlayer() && (*itr)->mentor_target_char_id == ((Player*)member)->GetCharacterID() && (*itr)->client)
+		{
+			(*itr)->mentor_target_char_id = 0;
+			(*itr)->client->GetPlayer()->EnableResetMentorship();
+		}
+
 		if ((*itr)->client)
 			(*itr)->client->GetPlayer()->SetCharSheetChanged(true);
 	}
@@ -115,6 +123,12 @@ void PlayerGroup::Disband() {
 			if ((*itr)->member->IsBot())
 				((Bot*)(*itr)->member)->Camp();
 		}
+		if((*itr)->mentor_target_char_id && (*itr)->client)
+		{
+			(*itr)->mentor_target_char_id = 0;
+			(*itr)->client->GetPlayer()->EnableResetMentorship();
+		}
+
 		if ((*itr)->client)
 			(*itr)->client->GetPlayer()->SetCharSheetChanged(true);
 
@@ -217,6 +231,15 @@ bool PlayerGroupManager::RemoveGroupMember(int32 group_id, Entity* member) {
 	bool ret = false;
 	bool remove = false;
 	Client* client = 0;
+	if(member->GetGroupMemberInfo()->mentor_target_char_id)
+	{
+		if(member->IsPlayer())
+		{
+			Player* tmpPlayer = (Player*)member;
+			member->GetGroupMemberInfo()->mentor_target_char_id = 0;
+			tmpPlayer->EnableResetMentorship();
+		}
+	}
 	MGroups.writelock(__FUNCTION__, __LINE__);
 
 	if (m_groups.count(group_id) > 0) {
@@ -760,6 +783,28 @@ bool PlayerGroupManager::IsInGroup(int32 group_id, Entity* member) {
 	return ret;
 }
 
+Entity* PlayerGroupManager::IsPlayerInGroup(int32 group_id, int32 character_id) {
+	Entity* ret = nullptr;
+
+	MGroups.readlock(__FUNCTION__, __LINE__);
+
+	if (m_groups.count(group_id) > 0) {
+		m_groups[group_id]->MGroupMembers.readlock(__FUNCTION__, __LINE__);
+		deque<GroupMemberInfo*>* members = m_groups[group_id]->GetMembers();
+		for (int8 i = 0; i < members->size(); i++) {
+			if (members->at(i)->member && members->at(i)->member->IsPlayer() && character_id == ((Player*)members->at(i)->member)->GetCharacterID()) {
+				ret = members->at(i)->member;
+				break;
+			}
+		}
+		m_groups[group_id]->MGroupMembers.releasereadlock(__FUNCTION__, __LINE__);
+	}
+
+	MGroups.releasereadlock(__FUNCTION__, __LINE__);
+
+	return ret;
+}
+
 void PlayerGroup::RemoveClientReference(Client* remove) {
 	deque<GroupMemberInfo*>::iterator itr;
 	MGroupMembers.writelock();

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

@@ -45,6 +45,7 @@ struct GroupMemberInfo {
 	bool	leader;
 	Client*	client;
 	Entity*	member;
+	int32	mentor_target_char_id;
 };
 
 /// <summary>Represents a players group in game</summary>
@@ -172,6 +173,7 @@ public:
 	void UpdateGroupBuffs();
 
 	bool IsInGroup(int32 group_id, Entity* member);
+	Entity* IsPlayerInGroup(int32 group_id, int32 char_id);
 	// TODO: Any function below this comment
 	bool IsSpawnInGroup(int32 group_id, string name); // used in follow
 	Player* GetGroupLeader(int32 group_id);

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

@@ -210,7 +210,9 @@ void SpellProcess::Process(){
 		while(itr.Next()){
 			recast_timer = itr->value;
 			if(recast_timer->timer->Check(false)){
-				UnlockSpell(recast_timer->client, recast_timer->spell);
+				MaintainedEffects* effect = 0;
+				if(recast_timer->caster && (!(effect = recast_timer->caster->GetMaintainedSpell(recast_timer->spell_id)) || !effect->spell->spell->GetSpellData()->duration_until_cancel))
+					UnlockSpell(recast_timer->client, recast_timer->spell);
 				safe_delete(recast_timer->timer);
 				recast_timers.Remove(recast_timer, true);
 			}
@@ -287,7 +289,8 @@ bool SpellProcess::IsReady(Spell* spell, Entity* caster){
 	MutexList<RecastTimer*>::iterator itr = recast_timers.begin();
 	while(itr.Next()){
 		recast_timer = itr->value;
-		if(recast_timer->spell == spell && recast_timer->caster == caster){
+		if((recast_timer->spell == spell || recast_timer->spell_id == spell->GetSpellID()) &&
+			recast_timer->caster == caster){
 			ret = false;
 			break;
 		}
@@ -307,6 +310,11 @@ void SpellProcess::CheckRecast(Spell* spell, Entity* caster, float timer_overrid
 			timer->timer = new Timer((int32)(spell->GetSpellData()->recast*1000));
 		else
 			timer->timer = new Timer((int32)(timer_override*1000));
+		
+		timer->type_group_spell_id = spell->GetSpellData()->type_group_spell_id;
+		timer->linked_timer = spell->GetSpellData()->linked_timer;
+		timer->spell_id = spell->GetSpellID();
+
 		recast_timers.Add(timer);
 		if(caster->IsPlayer()){
 			if(timer_override == 0)
@@ -366,6 +374,7 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removi
 	bool ret = false;
 	Spawn* target = 0;
 	if(spell) {
+		bool hasTimer = !IsReady(spell->spell, spell->caster);
 		if (active_spells.count(spell) > 0)
 			active_spells.Remove(spell);
 		if (spell->caster) {
@@ -383,11 +392,14 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removi
 							spell->caster->GetZone()->TriggerCharSheetTimer();
 					}
 				}
-				CheckRecast(spell->spell, spell->caster);
-				if (spell->caster && spell->caster->IsPlayer())
-					SendSpellBookUpdate(spell->caster->GetZone()->GetClientBySpawn(spell->caster));
+				if(!spell->spell->GetSpellData()->duration_until_cancel)
+				{
+					CheckRecast(spell->spell, spell->caster);
+					if (spell->caster && spell->caster->IsPlayer())
+						SendSpellBookUpdate(spell->caster->GetZone()->GetClientBySpawn(spell->caster));
+				}
 			}
-			if(spell->caster->IsPlayer())
+			if(IsReady(spell->spell, spell->caster) && spell->caster->IsPlayer())
 				((Player*)spell->caster)->UnlockSpell(spell->spell);
 			
 			spell->caster->RemoveProc(0, spell);
@@ -599,7 +611,7 @@ void SpellProcess::SendFinishedCast(LuaSpell* spell, Client* client){
 		
 		if(spell->resisted && spell->spell->GetSpellData()->recast > 0)
 			CheckRecast(spell->spell, client->GetPlayer(), 0.5); // half sec recast on resisted spells
-		else if (!spell->interrupted && spell->spell->GetSpellData()->cast_type != SPELL_CAST_TYPE_TOGGLE)
+		else if (!spell->interrupted)
 			CheckRecast(spell->spell, client->GetPlayer());
 		else if(spell->caster && spell->caster->IsPlayer())
 		{
@@ -1057,25 +1069,28 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
 			{
 				if(conflictSpell->spell->GetSpellData()->min_class_skill_req <= lua_spell->spell->GetSpellData()->min_class_skill_req)
 				{
-					if(spell->GetSpellData()->friendly_spell)
+					if(lua_spell->spell->GetSpellData()->duration_until_cancel && !lua_spell->num_triggers)
 					{
-						ZoneServer* zone = caster->GetZone();
-						Spawn* tmpTarget = zone->GetSpawnByID(conflictSpell->initial_target);
-						if(tmpTarget && tmpTarget->IsEntity())
+						if(spell->GetSpellData()->friendly_spell)
+						{
+							ZoneServer* zone = caster->GetZone();
+							Spawn* tmpTarget = zone->GetSpawnByID(conflictSpell->initial_target);
+							if(tmpTarget && tmpTarget->IsEntity())
+							{
+								zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
+								CheckRemoveTargetFromSpell(conflictSpell);
+								((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
+								if(client && IsReady(conflictSpell->spell, client->GetPlayer()))
+									UnlockSpell(client, conflictSpell->spell);
+							}
+							DeleteSpell(lua_spell);
+							return;
+						}
+						else if(lua_spell->spell->GetSpellData()->spell_type == SPELL_TYPE_DEBUFF)
 						{
-							zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
-							CheckRemoveTargetFromSpell(conflictSpell);
-							((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
-							if(client && IsReady(conflictSpell->spell, client->GetPlayer()))
-								UnlockSpell(client, conflictSpell->spell);
+							SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell);
+							return;
 						}
-						DeleteSpell(lua_spell);
-						return;
-					}
-					else if(lua_spell->spell->GetSpellData()->spell_type == SPELL_TYPE_DEBUFF)
-					{
-						SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell);
-						return;
 					}
 				}
 				else
@@ -1963,6 +1978,7 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell)
 						if (target->IsNPC() && !target->IsBot()) {
 							if (!target->IsPet() || (target->IsPet() && ((NPC*)target)->GetOwner()->IsNPC())) {
 								if (secondary_target && secondary_target->IsPlayer()) {
+									target = secondary_target;
 									luaspell->initial_target = target->GetID();
 									luaspell->initial_target_char_id = (target && target->IsPlayer()) ? ((Player*)target)->GetCharacterID() : 0;
 									luaspell->targets.push_back(target->GetID());

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

@@ -144,6 +144,9 @@ struct RecastTimer{
 	Client*			client;
 	Spell*			spell;
 	Timer*			timer;
+	int32			spell_id;
+	int32			linked_timer;
+	int32			type_group_spell_id;
 };
 
 /// <summary> Handles all spell casts for a zone, only 1 SpellProcess per zone </summary>
@@ -268,7 +271,7 @@ public:
 	/// <summary>Removes the timers for the given spawn</summary>
 	/// <param name='spawn'>Spawn to remove the timers for</param>
 	/// <param name='remove_all'>Remove all timers (cast, recast, active, queue, interrupted)? If false only cast timers are removed</param>
-	void RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all = false, bool delete_recast = true, bool call_expire_function = true);
+	void RemoveSpellTimersFromSpawn(Spawn* spawn, bool remove_all = false, bool delete_recast = false, bool call_expire_function = true);
 
 	/// <summary>Sets the recast timer for the spell </summary>
 	/// <param name='spell'>The spell to set the recast for</param>

+ 19 - 2
EQ2/source/WorldServer/client.cpp

@@ -814,9 +814,22 @@ void Client::SendCharInfo() {
 	if (version > 546)
 		ClientPacketFunctions::SendHousingList(this);
 	
+	bool groupMentor = false;
 	GetPlayer()->group_id = rejoin_group_id;
 	if(!world.RejoinGroup(this, rejoin_group_id))
 		GetPlayer()->group_id = 0;
+	else
+	{
+		Entity* ent = world.GetGroupManager()->IsPlayerInGroup(rejoin_group_id, GetPlayer()->GetGroupMemberInfo()->mentor_target_char_id);
+		if(ent && ent->IsPlayer())
+		{
+			GetPlayer()->SetMentorStats(ent->GetLevel(), ent->GetID());
+			groupMentor = true;
+		}
+	}
+
+	if(!groupMentor)
+		GetPlayer()->SetMentorStats(GetPlayer()->GetLevel(), 0);
 
 	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_MAINTAINEDEFFECTS);
 	database.LoadCharacterSpellEffects(GetCharacterID(), this, DB_TYPE_SPELLEFFECTS);
@@ -2969,6 +2982,8 @@ bool Client::Process(bool zone_process) {
 	if (pos_update.Check())
 	{
 		ProcessStateCommands();
+		
+		GetPlayer()->ResetMentorship(); // check if we need to asynchronously reset mentorship
 
 		if(GetPlayer()->GetRegionMap())
 			GetPlayer()->GetRegionMap()->TicRegionsNearSpawn(this->GetPlayer(), regionDebugMessaging ? this : nullptr);
@@ -3981,8 +3996,6 @@ void Client::Zone(ZoneServer* new_zone, bool set_coords) {
 	Save();
 
 	char* new_zone_ip = 0;
-	struct in_addr in;
-	in.s_addr = this->GetIP();
 	if (IsPrivateAddress(this->GetIP()) && strlen(net.GetInternalWorldAddress()) > 0)
 		new_zone_ip = net.GetInternalWorldAddress();
 	else
@@ -4186,6 +4199,10 @@ void Client::HandleVerbRequest(EQApplicationPacket* app) {
 						delete_commands.push_back(player->CreateEntityCommand("kick from group", 10000, "kickfromgroup", "", 0, 0));
 						delete_commands.push_back(player->CreateEntityCommand("make group leader", 10000, "makeleader", "", 0, 0));
 					}
+					if(spawn->IsPlayer() && !player->GetGroupMemberInfo()->mentor_target_char_id)
+						delete_commands.push_back(player->CreateEntityCommand("Mentor", 10000, "mentor", "", 0, 0));
+					else if(spawn->IsPlayer() && player->GetGroupMemberInfo()->mentor_target_char_id == ((Player*)spawn)->GetCharacterID())
+						delete_commands.push_back(player->CreateEntityCommand("Stop Mentoring", 10000, "unmentor", "", 0, 0));
 				}
 				else if (!player->GetGroupMemberInfo() || (player->GetGroupMemberInfo()->leader && world.GetGroupManager()->GetGroupSize(player->GetGroupMemberInfo()->group_id) < 6))
 					delete_commands.push_back(player->CreateEntityCommand("invite to group", 10000, "invite", "", 0, 0));