Add AdventureBot groups, raid preparation and shared travel
Implement native bot recruitment and player admission, leader-owned destinations, intent-specific capacities, preparation scheduling and shared graph and flight travel. Support native group instance visits and separate raid entrances. Include pending personality-driven faction PvP and solo fleeing, preserving the absolute group fleeing exclusion. Add regression fixtures, migration, documentation and test-server evidence.
This commit is contained in:
1 parent
04aa87fcfc
commit
d6fea6ee68
46 files changed
+3031
-59
No files matched your search
@@ -21,6 +21,7 @@
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Group.h"
|
||||
#include "GroupMgr.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "Log.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
@@ -136,6 +137,7 @@ void WorldSession::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClien
|
||||
// player already in another group or invited
|
||||
if (group2 || invitedPlayer->GetGroupInvite())
|
||||
{
|
||||
if(group2)sClientlessPlayerMgr->GroupJoinRequest(invitedPlayer,invitingPlayer);
|
||||
SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_ALREADY_IN_GROUP_S);
|
||||
|
||||
if (group2)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include "Group.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureEconomyPolicy.h"
|
||||
#include "AdventureRestSpace.h"
|
||||
@@ -26,6 +28,7 @@
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "InstanceSaveMgr.h"
|
||||
#include "Opcodes.h"
|
||||
#include "WorldPacket.h"
|
||||
@@ -133,7 +136,7 @@ namespace
|
||||
|
||||
bool Known(std::string const & intent)
|
||||
{
|
||||
return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance";
|
||||
return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance" || intent == "raid" || intent == "groupformation";
|
||||
}
|
||||
bool Busy(uint32 guid)
|
||||
{
|
||||
@@ -272,19 +275,21 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
|
||||
{
|
||||
std::vector < double > weights
|
||||
{
|
||||
Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f)
|
||||
Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f), Affinity(profile->ActivityAffinity, "raid", 0.25f)
|
||||
};
|
||||
auto i = Choose(weights, record.Random);
|
||||
return std::array < std::string, 7 >
|
||||
return std::array < std::string, 8 >
|
||||
{
|
||||
"social", "adventure", "profession", "rest", "leisure", "economy", "instance"
|
||||
"social", "adventure", "profession", "rest", "leisure", "economy", "instance", "raid"
|
||||
}
|
||||
[std::min(i, std::size_t(6))];
|
||||
[std::min(i, std::size_t(7))];
|
||||
};
|
||||
record.Plan = AdventureBehaviorPolicy::Generate(record.Random, Affinity(profile->Personality, "wanderlust", 0.3f), pick,[&](std::string const & intent)
|
||||
{
|
||||
return PickLocation(player, record, intent);
|
||||
});
|
||||
AdventureGroupPolicy::Prepare(record.Plan,record.Random,Affinity(profile->Personality,"sociability",0.5f),
|
||||
[&](){return PickLocation(player,record,"groupformation");});
|
||||
auto tx = WorldDatabase.BeginTransaction();
|
||||
for (unsigned h = 0; h < 24; ++ h)
|
||||
{
|
||||
@@ -333,6 +338,8 @@ void AdventureBehaviorMgr::Queue(Player * player, Record & r, std::string const
|
||||
a.Name = action;
|
||||
a.Parameters[key] = value;
|
||||
a.Parameters["hard_destination"] = hard ? "true" : "false";
|
||||
if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID() && sAdventureGroupMgr->BotLed(player))
|
||||
a.Parameters["route_source"]=std::to_string(group->GetLeaderGUID().GetCounter());
|
||||
r.Queued = sClientlessPlayerMgr->QueueAction(player->GetGUID().GetCounter(), std::move(a));
|
||||
}
|
||||
|
||||
@@ -396,7 +403,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
|
||||
intent.Parameters["location"] = r.Location;
|
||||
sClientlessPlayerMgr->SetIntent(player->GetGUID().GetCounter(), std::move(intent));
|
||||
r.Publishing = false;
|
||||
if (r.Intent=="instance")
|
||||
if ((r.Intent=="instance" || r.Intent=="raid"))
|
||||
{
|
||||
if (!ResolveInstance(player,r)) {Fallback(player,r);return;}
|
||||
Queue(player,r,"travel_to_location","id",r.Location,true);
|
||||
@@ -1261,7 +1268,18 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
else r.Deadline = deadline;
|
||||
}
|
||||
}
|
||||
else if (r.Deadline && now >= r.Deadline) SetWanted(player, r, "", "", 0);
|
||||
else if (r.Deadline && now >= r.Deadline)
|
||||
{
|
||||
if(r.Intent=="groupformation" && !r.ManualNextIntent.empty())
|
||||
{
|
||||
auto intent=r.ManualNextIntent,location=r.ManualNextLocation;
|
||||
r.ManualNextIntent.clear();r.ManualNextLocation.clear();
|
||||
SetWanted(player,r,intent,location,now+3600);
|
||||
}
|
||||
else SetWanted(player,r,"","",0);
|
||||
}
|
||||
if(auto group=player->GetGroup())if(sAdventureGroupMgr->BotLed(player) && group->GetLeaderGUID()!=player->GetGUID())
|
||||
if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))AdoptGroup(player,leader);
|
||||
if (advanceLeisure()) return;
|
||||
if (Unsafe(player))
|
||||
{
|
||||
@@ -1329,7 +1347,9 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
}
|
||||
if (r.State == Phase::OnSite)
|
||||
{
|
||||
if (r.Intent=="instance") {UpdateInstance(player,r,now);return;}
|
||||
if ((r.Intent=="instance" || r.Intent=="raid")) {UpdateInstance(player,r,now);return;}
|
||||
if(r.Intent=="groupformation")return;
|
||||
if(sAdventureGroupMgr->FollowMember(player))return;
|
||||
if (r.Recovery.Active)
|
||||
{
|
||||
if (UpdateRecovery(player,r,1000)) return;
|
||||
@@ -1455,6 +1475,17 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
||||
handler->SendSysMessage("Schedule mode updated; changes use a safe graph handover.");
|
||||
return true;
|
||||
}
|
||||
if(op=="prepare")
|
||||
{
|
||||
std::string next,meeting;uint32 seconds=3600;
|
||||
in>>next>>meeting;if(in>>seconds)seconds=std::clamp(seconds,30u,3600u);
|
||||
auto location=sAdventureLocationMgr->Find(meeting);
|
||||
if(!AdventureGroupPolicy::Eligible(next) || !location || !Allowed(player,*location,"groupformation"))return false;
|
||||
auto destination=PickLocation(player,r,next);if(destination.empty())return false;
|
||||
r.Schedule=false;r.Enabled=true;r.ManualNextIntent=next;r.ManualNextLocation=destination;
|
||||
SetWanted(player,r,"groupformation",meeting,GameTime::GetGameTime()+seconds);
|
||||
handler->SendSysMessage("Manual preparation started; next activity begins at the deadline even if the group is incomplete.");return true;
|
||||
}
|
||||
if (op == "intent")
|
||||
{
|
||||
std::string intent, location;
|
||||
@@ -1464,23 +1495,23 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
||||
auto l = sAdventureLocationMgr->Find(location);
|
||||
if (! Known(intent) || ! l || ! Allowed(player, * l, intent))
|
||||
{
|
||||
handler->SendSysMessage("Use social/adventure/profession/rest/leisure and a matching enabled location.");
|
||||
handler->SendSysMessage("Use social/adventure/profession/rest/leisure/economy/instance/raid/groupformation and a matching enabled location.");
|
||||
return false;
|
||||
}
|
||||
r.Schedule = false;
|
||||
r.Enabled = true;
|
||||
r.Enabled = true;r.ManualNextIntent.clear();r.ManualNextLocation.clear();
|
||||
SetWanted(player, r, intent, location, GameTime::GetGameTime() + seconds);
|
||||
handler->SendSysMessage("Intent requested; travel and micro actions are autonomous.");
|
||||
return true;
|
||||
}
|
||||
if (op == "stop")
|
||||
{
|
||||
r.Schedule = false;
|
||||
r.Schedule = false;r.ManualNextIntent.clear();r.ManualNextLocation.clear();
|
||||
SetWanted(player, r, "", "", 0);
|
||||
handler->SendSysMessage("Behavior stop requested at a safe point.");
|
||||
return true;
|
||||
}
|
||||
handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status <guid>|plan <guid>|schedule <guid> on/off|intent <guid> <intent> <location> [seconds]|micro <guid> <scene-action>|stop <guid>");
|
||||
handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status <guid>|plan <guid>|schedule <guid> on/off|intent <guid> <intent> <location> [seconds]|prepare <guid> <next-intent> <capital-location> [seconds]|micro <guid> <scene-action>|stop <guid>");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1519,7 +1550,8 @@ bool AdventureBehaviorMgr::RestoreInstance(Player* player, Record& r)
|
||||
v.Map=f[3].GetUInt32();v.InstanceId=f[4].GetUInt32();v.BlockEnd=time_t(f[5].GetUInt64());
|
||||
v.PrepareUntil=time_t(f[6].GetUInt64());v.Stage=AdventureInstancePolicy::Phase(f[7].GetUInt8());
|
||||
v.LossPercent=f[8].GetUInt8();v.InjuryApplied=f[9].GetUInt8()!=0;r.Schedule=f[10].GetUInt8()!=0;
|
||||
r.Intent=r.WantedIntent="instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd;
|
||||
auto restoredMap=sMapStore.LookupEntry(v.Map);
|
||||
r.Intent=r.WantedIntent=restoredMap && restoredMap->IsRaid()?"raid":"instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd;
|
||||
r.State=v.Stage==AdventureInstancePolicy::Phase::Travel?Phase::Idle:Phase::OnSite;
|
||||
r.Dirty=v.Stage==AdventureInstancePolicy::Phase::Travel;
|
||||
r.Detail="resuming saved instance visit";
|
||||
@@ -1561,8 +1593,8 @@ bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r)
|
||||
sClientlessTravelGraphMgr->EnsureLoaded();
|
||||
if (!source || !map || !map->IsDungeon() || !outside || outside->MapType!=MAP_COMMON ||
|
||||
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_source") ||
|
||||
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target") || player->GetGroup())
|
||||
{r.Detail="solo instance entrance is not a valid native graph transition";return false;}
|
||||
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target"))
|
||||
{r.Detail="instance entrance is not a valid native graph transition";return false;}
|
||||
auto validExit=[&](uint32 id)
|
||||
{
|
||||
auto at=sAreaTriggerStore.LookupEntry(id);auto dest=sObjectMgr->GetAreaTrigger(id);
|
||||
@@ -1661,7 +1693,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
|
||||
bool(CharacterDatabase.PQuery("SELECT guid FROM character_instance WHERE instance=%u AND guid<>%u LIMIT 1",id,player->GetGUID().GetCounter())) ||
|
||||
bool(CharacterDatabase.PQuery("SELECT guid FROM group_instance WHERE instance=%u LIMIT 1",id));
|
||||
}
|
||||
if (player->GetGroup() || shared || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry))
|
||||
if ((!player->GetGroup() && shared) || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry))
|
||||
{
|
||||
r.Detail="native instance entrance not reached or private solo binding unavailable";
|
||||
v.Stage=Stage::Rest;SaveInstance(player,r);return true;
|
||||
@@ -1682,7 +1714,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
|
||||
}
|
||||
if (v.Stage==Stage::Inside)
|
||||
{
|
||||
if (!v.Abort && !player->GetGroup() && now<AdventureInstancePolicy::ReturnAt(v.BlockEnd) &&
|
||||
if (!v.Abort && now<AdventureInstancePolicy::ReturnAt(v.BlockEnd) &&
|
||||
player->GetMapId()==v.Map && player->GetInstanceId()==v.InstanceId)
|
||||
{r.Detail="waiting just inside private instance entrance";return true;}
|
||||
v.Stage=Stage::Returning;
|
||||
@@ -1776,7 +1808,13 @@ std::string AdventureBehaviorMgr::InteractionSchedule(Player* player)
|
||||
auto& r=it->second;
|
||||
auto local=Local(GameTime::GetGameTime());
|
||||
if(r.Schedule && r.Date!=DateOf(local)){Generate(player,r,DateOf(local));r.Hour=-1;}
|
||||
if(!r.Schedule)return (r.Deadline && GameTime::GetGameTime()>=r.Deadline?"expired:":"")+r.Intent+":"+r.Location;
|
||||
if(!r.Schedule)
|
||||
{
|
||||
auto deadline=r.GroupOverride?r.PersonalDeadline:r.Deadline;
|
||||
auto const& intent=r.GroupOverride?r.PersonalIntent:r.WantedIntent;
|
||||
auto const& location=r.GroupOverride?r.PersonalLocation:r.WantedLocation;
|
||||
return (deadline && GameTime::GetGameTime()>=deadline?"expired:":"")+intent+":"+location;
|
||||
}
|
||||
auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour];
|
||||
return slot.Intent+":"+slot.Location;
|
||||
}
|
||||
@@ -1787,3 +1825,53 @@ void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
if(it!=_records.end())it->second.Detail="social/group interaction; original deadline retained";
|
||||
}
|
||||
|
||||
AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(Player* player)
|
||||
{
|
||||
AdventureGroupPlan plan;
|
||||
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return plan;
|
||||
auto& r=it->second;auto now=GameTime::GetGameTime();auto local=Local(now);
|
||||
if(r.Schedule && r.Date!=DateOf(local))Generate(player,r,DateOf(local));
|
||||
plan.Enabled=r.Enabled && !(r.Dirty && r.WantedIntent.empty());plan.Intent=r.Intent;plan.Location=r.Location;plan.Deadline=r.Deadline;
|
||||
if(r.Schedule)
|
||||
{
|
||||
plan.Intent=r.Plan[local.tm_hour].Intent;
|
||||
plan.Location=r.Plan[local.tm_hour].Location;
|
||||
if(local.tm_hour<23){plan.NextIntent=r.Plan[local.tm_hour+1].Intent;plan.NextLocation=r.Plan[local.tm_hour+1].Location;}
|
||||
}
|
||||
if(!r.Schedule){plan.NextIntent=r.ManualNextIntent;plan.NextLocation=r.ManualNextLocation;}
|
||||
plan.Forming=plan.Enabled && plan.Intent=="groupformation";
|
||||
return plan;
|
||||
}
|
||||
bool AdventureBehaviorMgr::AdoptGroup(Player* player,Player* leader)
|
||||
{
|
||||
auto a=_records.find(player->GetGUID().GetCounter()),b=_records.find(leader->GetGUID().GetCounter());
|
||||
if(a==_records.end() || b==_records.end() || !b->second.Enabled || (b->second.Dirty && b->second.WantedIntent.empty()))return false;
|
||||
auto& r=a->second;auto const& source=b->second;
|
||||
if(!r.GroupOverride)
|
||||
{
|
||||
r.GroupOverride=true;r.PersonalIntent=r.WantedIntent;r.PersonalLocation=r.WantedLocation;r.PersonalDeadline=r.Deadline;
|
||||
}
|
||||
if(r.Instance.Active && r.Instance.Stage!=AdventureInstancePolicy::Phase::Travel)return true;
|
||||
if(r.WantedIntent!=source.Intent || r.WantedLocation!=source.Location)
|
||||
SetWanted(player,r,source.Intent,source.Location,source.Deadline);
|
||||
else r.Deadline=source.Deadline;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::ResumePersonalPlan(Player* player)
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return;
|
||||
auto& r=it->second;if(!r.GroupOverride)return;r.GroupOverride=false;
|
||||
auto now=GameTime::GetGameTime();
|
||||
if(r.Schedule)
|
||||
{
|
||||
auto local=Local(now);if(r.Date!=DateOf(local))Generate(player,r,DateOf(local));
|
||||
auto const& slot=r.Plan[local.tm_hour];
|
||||
std::tm end=local;end.tm_min=0;end.tm_sec=0;++end.tm_hour;end.tm_isdst=-1;
|
||||
r.Hour=local.tm_hour;SetWanted(player,r,slot.Intent,slot.Location,mktime(&end));
|
||||
}
|
||||
else if(!r.PersonalDeadline || now<r.PersonalDeadline)
|
||||
SetWanted(player,r,r.PersonalIntent,r.PersonalLocation,r.PersonalDeadline);
|
||||
else SetWanted(player,r,"","",0);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef VCORE_ADVENTURE_BEHAVIOR_MGR_H
|
||||
#define VCORE_ADVENTURE_BEHAVIOR_MGR_H
|
||||
#include "Common.h"
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "Position.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "AdventureBehaviorPolicy.h"
|
||||
@@ -31,6 +32,9 @@ public:
|
||||
bool Command(ChatHandler* handler, char const* args);
|
||||
void InterruptForCombat(Player* player);
|
||||
void InterruptForInteraction(Player* player);
|
||||
AdventureGroupPlan GroupPlan(Player* player);
|
||||
void ResumePersonalPlan(Player* player);
|
||||
bool AdoptGroup(Player* player,Player* leader);
|
||||
std::string CombatIntent(Player* player) const;
|
||||
std::string InteractionIntent(Player* player) const;
|
||||
std::string InteractionSchedule(Player* player);
|
||||
@@ -47,9 +51,11 @@ private:
|
||||
};
|
||||
struct Record
|
||||
{
|
||||
bool Enabled=false, Schedule=false, Publishing=false, Queued=false, Dirty=true;
|
||||
bool Enabled=false, Schedule=false, Publishing=false, Queued=false, Dirty=true, GroupOverride=false;
|
||||
Phase State=Phase::Idle;
|
||||
std::string Intent, Location, WantedIntent, WantedLocation, Detail, Micro;
|
||||
std::string Intent, Location, WantedIntent, WantedLocation, Detail, Micro, ManualNextIntent, ManualNextLocation;
|
||||
std::string PersonalIntent,PersonalLocation;
|
||||
time_t PersonalDeadline=0;
|
||||
time_t Deadline=0;
|
||||
uint32 Date=0, Timer=0, MicroTimer=0, GestureTimer=0, RetryTimer=0;
|
||||
int Hour=-1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "Group.h"
|
||||
#include "AdventureCombatPolicy.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
@@ -7,6 +8,8 @@
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "Creature.h"
|
||||
#include "DBCStructure.h"
|
||||
#include "Random.h"
|
||||
#include "Corpse.h"
|
||||
#include "Player.h"
|
||||
#include "PlayerAI.h"
|
||||
@@ -141,6 +144,7 @@ void AdventureCombatMgr::Finish(Player* p,Record& r)
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u)
|
||||
{auto c=u->ToCreature();return c && c->GetScriptName()=="npc_training_dummy";}))p->CombatStop(true); // Only the bounded dummy scene may end its own combat.
|
||||
r.Fleeing=false;r.ThreatPositions.clear();
|
||||
r.AI.reset();r.Target.Clear();r.Training=false;r.Defending=false;r.UnreachableMs=0;r.Cooldown=10000;
|
||||
}
|
||||
void AdventureCombatMgr::Stop(Player* p)
|
||||
@@ -148,6 +152,42 @@ void AdventureCombatMgr::Stop(Player* p)
|
||||
auto it=_records.find(p->GetGUID().GetCounter());if(it!=_records.end())Finish(p,it->second);
|
||||
}
|
||||
void AdventureCombatMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
|
||||
Unit* AdventureCombatMgr::PvPTarget(Player* p) const
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
if(!profile)return nullptr;
|
||||
auto trait=[&](char const* key)
|
||||
{auto it=profile->Personality.find(key);return it==profile->Personality.end()?0.0f:it->second;};
|
||||
auto chance=AdventureCombatPolicy::PvPWillingness(sAdventureBehaviorMgr->CombatIntent(p),
|
||||
trait("adventurousness"),trait("danger_tolerance"));
|
||||
if(chance<=0 || p->GetHealthPct()<AdventureCombatPolicy::MinimumHealth)return nullptr;
|
||||
if(p->GetPowerType()==POWER_MANA && p->GetMaxPower(POWER_MANA) &&
|
||||
100.0f*p->GetPower(POWER_MANA)/p->GetMaxPower(POWER_MANA)<AdventureCombatPolicy::MinimumMana)return nullptr;
|
||||
Unit* best=nullptr;
|
||||
auto consider=[&](Unit* target)
|
||||
{
|
||||
if(!Valid(p,target,false) || !p->IsHostileTo(target) ||
|
||||
!AdventureCombatPolicy::PvPLevel(p->getLevel(),target->getLevel()) ||
|
||||
p->GetExactDist(target)>AdventureCombatPolicy::PvPRadius ||
|
||||
(best && p->GetExactDist(target)>=p->GetExactDist(best)))return;
|
||||
if(auto player=target->ToPlayer())
|
||||
{if(player->GetTeam()==p->GetTeam())return;}
|
||||
else
|
||||
{
|
||||
auto creature=target->ToCreature();
|
||||
auto faction=target->GetFactionTemplateEntry();
|
||||
auto enemy=p->GetTeam()==ALLIANCE?FACTION_GROUP_MASK_HORDE:FACTION_GROUP_MASK_ALLIANCE;
|
||||
if(!creature || creature->GetControllingPlayer() || !faction || !(faction->FactionGroup & enemy) ||
|
||||
creature->isElite() || creature->isWorldBoss())return;
|
||||
}
|
||||
if(Reachable(p,target))best=target;
|
||||
};
|
||||
std::vector<Player*> players;p->GetPlayerListInGrid(players,AdventureCombatPolicy::PvPRadius);
|
||||
for(auto target:players)consider(target);
|
||||
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,AdventureCombatPolicy::PvPRadius);
|
||||
for(auto target:creatures)consider(target);
|
||||
return best && frand(0,1)<chance?best:nullptr;
|
||||
}
|
||||
Creature* AdventureCombatMgr::Proactive(Player* p) const
|
||||
{
|
||||
auto intent=sAdventureBehaviorMgr->CombatIntent(p);
|
||||
@@ -158,6 +198,8 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const
|
||||
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius);
|
||||
for(auto c:all)
|
||||
{
|
||||
auto faction=c->GetFactionTemplateEntry();
|
||||
if(faction && (faction->FactionGroup & (FACTION_GROUP_MASK_ALLIANCE|FACTION_GROUP_MASK_HORDE)))continue;
|
||||
if(best && p->GetExactDist(c)>=p->GetExactDist(best))continue;
|
||||
if(intent=="adventure")
|
||||
{
|
||||
@@ -174,11 +216,114 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const
|
||||
}
|
||||
return best;
|
||||
}
|
||||
bool AdventureCombatMgr::Flee(Player* p,Record& r)
|
||||
{
|
||||
if(p->GetGroup())
|
||||
{
|
||||
if(r.Fleeing)
|
||||
{
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving();
|
||||
r.Fleeing=false;r.ThreatPositions.clear();
|
||||
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_CANCEL guid=%u reason='group'",p->GetGUID().GetCounter());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::vector<Unit*> threats;
|
||||
auto add=[&](Unit* u)
|
||||
{
|
||||
if(u && u!=p && u->IsAlive() && u->IsInWorld() && p->IsInPhase(u) &&
|
||||
p->CanSeeOrDetect(u) && std::find(threats.begin(),threats.end(),u)==threats.end())threats.push_back(u);
|
||||
};
|
||||
for(auto u:p->getAttackers())add(u);
|
||||
for(auto id:r.Damagers)add(ObjectAccessor::GetUnit(*p,id));
|
||||
if(r.Defending || (p->IsInCombat() && !r.Training))add(ObjectAccessor::GetUnit(*p,r.Target));
|
||||
bool danger=false;
|
||||
for(auto u:threats)
|
||||
{
|
||||
auto c=u->ToCreature();
|
||||
if(AdventureCombatPolicy::ShouldFlee(false,p->GetHealthPct(),p->getLevel(),u->getLevel(),
|
||||
c && c->isElite(),c && c->isWorldBoss()))danger=true;
|
||||
}
|
||||
if(!r.Fleeing && (!danger || r.Training))return false;
|
||||
if(!r.Fleeing)
|
||||
{
|
||||
r.Fleeing=true;r.NextEscape=0;
|
||||
sAdventureBehaviorMgr->InterruptForCombat(p);
|
||||
sClientlessLuaEngine->DismountGroundMount(p,"combat-flee");
|
||||
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();
|
||||
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_START guid=%u",p->GetGUID().GetCounter());
|
||||
}
|
||||
// Keep last known threat positions when attackers vanish from visibility.
|
||||
if(!threats.empty())
|
||||
{
|
||||
r.ThreatPositions.clear();
|
||||
for(auto u:threats)r.ThreatPositions.push_back(u->GetPosition());
|
||||
}
|
||||
auto clearance=[&](Position const& point)
|
||||
{
|
||||
float distance=1000;
|
||||
for(auto const& threat:r.ThreatPositions)distance=std::min(distance,point.GetExactDist(threat));
|
||||
return distance;
|
||||
};
|
||||
if(!p->IsInCombat() && clearance(p->GetPosition())>=50)
|
||||
{
|
||||
r.Fleeing=false;r.ThreatPositions.clear();r.Damagers.clear();
|
||||
Finish(p,r);r.Cooldown=30000;
|
||||
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_END guid=%u",p->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
auto now=GameTime::GetGameTime();
|
||||
if(now<r.NextEscape)return true;
|
||||
r.NextEscape=now+2;
|
||||
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,80);
|
||||
std::vector<Player*> players;p->GetPlayerListInGrid(players,80);
|
||||
std::vector<Unit*> nearby;
|
||||
for(auto c:creatures)nearby.push_back(c);
|
||||
for(auto other:players)nearby.push_back(other);
|
||||
Position best;bool found=false;float score=-1e30f;
|
||||
float current=clearance(p->GetPosition());
|
||||
for(float radius:{18.0f,30.0f})for(unsigned i=0;i<12;++i)
|
||||
{
|
||||
float angle=i*6.2831853f/12;
|
||||
float x=p->GetPositionX()+radius*std::cos(angle),y=p->GetPositionY()+radius*std::sin(angle);
|
||||
float z=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,p->GetPositionZ()+5,true,15);
|
||||
if(z<=INVALID_HEIGHT)continue;
|
||||
Position end;end.Relocate(x,y,z);
|
||||
float distance=clearance(end);
|
||||
if(distance<current+3)continue;
|
||||
bool unsafe=false;
|
||||
for(auto u:nearby)
|
||||
{
|
||||
if(!u->IsAlive() || !p->IsInPhase(u) || !p->CanSeeOrDetect(u) || !p->IsHostileTo(u) ||
|
||||
std::find(threats.begin(),threats.end(),u)!=threats.end())continue;
|
||||
if(AdventureCombatPolicy::SegmentDistance(u->GetPositionX(),u->GetPositionY(),u->GetPositionZ(),
|
||||
p->GetPositionX(),p->GetPositionY(),p->GetPositionZ(),x,y,z)<12){unsafe=true;break;}
|
||||
}
|
||||
if(unsafe)continue;
|
||||
ClientlessPathDiagnostics path;
|
||||
if(!sClientlessTravelMgr->AnalyzePath(p,end,path) || !path.Complete || path.Suspicious || path.PathLength>60)continue;
|
||||
float value=distance-0.15f*path.PathLength;
|
||||
if(value>score){score=value;best=end;found=true;}
|
||||
}
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
p->AttackStop();p->CastStop();
|
||||
if(found && sClientlessTravelMgr->StartTravel(p,best))
|
||||
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_PATH guid=%u clearance=%.2f",p->GetGUID().GetCounter(),clearance(best));
|
||||
else
|
||||
{
|
||||
p->StopMoving();
|
||||
auto target=ObjectAccessor::GetUnit(*p,r.Target);
|
||||
if(r.AI && Valid(p,target,false) && p->IsWithinMeleeRange(target))r.AI->Tick(target,500);
|
||||
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_BLOCKED guid=%u",p->GetGUID().GetCounter());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool AdventureCombatMgr::Death(Player* p,Record& r)
|
||||
{
|
||||
if(!r.Dead)
|
||||
{
|
||||
if(r.AI)Finish(p,r);
|
||||
r.Fleeing=false;r.ThreatPositions.clear();
|
||||
sAdventureBehaviorMgr->InterruptForCombat(p);r.Damagers.clear();r.Dead=true;r.Suspended=true;
|
||||
TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter());
|
||||
}
|
||||
@@ -250,8 +395,8 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
if(!p->IsInWorld())return false;
|
||||
auto& r=_records[p->GetGUID().GetCounter()];
|
||||
r.Cooldown=r.Cooldown>diff?r.Cooldown-diff:0;
|
||||
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.AI || !p->IsAlive();
|
||||
if(r.Timer>diff){r.Timer-=diff;return r.AI || !p->IsAlive() || r.Dead;}
|
||||
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.Fleeing || r.AI || !p->IsAlive();
|
||||
if(r.Timer>diff){r.Timer-=diff;return r.Fleeing || r.AI || !p->IsAlive() || r.Dead;}
|
||||
r.Timer=500;
|
||||
if(!p->IsAlive())return Death(p,r);
|
||||
if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;}
|
||||
@@ -264,8 +409,21 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
if(r.AI)Finish(p,r);
|
||||
Start(p,r,c,false,true,0);break;
|
||||
}
|
||||
if(!r.AI && !p->GetGroup() && !p->IsInCombat() && !r.Cooldown)
|
||||
if(auto c=Proactive(p))Start(p,r,c,false,false,0);
|
||||
if(Flee(p,r))return true;
|
||||
if(!r.AI && (!p->GetGroup() || p->GetGroup()->GetLeaderGUID()==p->GetGUID()) && !p->IsInCombat() && !r.Cooldown)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();
|
||||
if(now>=r.NextPvP)
|
||||
{
|
||||
r.NextPvP=now+30;
|
||||
if(auto target=PvPTarget(p))
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot","COMBAT_PVP_INITIATE guid=%u target='%s'",p->GetGUID().GetCounter(),target->GetGUID().ToString().c_str());
|
||||
Start(p,r,target,false,false,0);
|
||||
}
|
||||
}
|
||||
if(!r.AI && !p->GetGroup())if(auto c=Proactive(p))Start(p,r,c,false,false,0);
|
||||
}
|
||||
if(!r.AI)return false;
|
||||
auto c=ObjectAccessor::GetUnit(*p,r.Target);
|
||||
if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80)
|
||||
@@ -295,7 +453,7 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
bool AdventureCombatMgr::Busy(Player* player) const
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
return it!=_records.end() && (it->second.AI || it->second.Dead || !it->second.Damagers.empty());
|
||||
return it!=_records.end() && (it->second.Fleeing || it->second.AI || it->second.Dead || !it->second.Damagers.empty());
|
||||
}
|
||||
|
||||
bool AdventureCombatMgr::Assist(Player* p,Unit* target)
|
||||
|
||||
@@ -32,9 +32,9 @@ private:
|
||||
std::shared_ptr<Fighter> AI;
|
||||
ObjectGuid Target,Helper;
|
||||
std::deque<ObjectGuid> Damagers;
|
||||
bool Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
|
||||
time_t Until=0,NextCorpseSearch=0;
|
||||
std::vector<Position> FailedCorpsePoints;
|
||||
bool Fleeing=false,Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
|
||||
time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0;
|
||||
std::vector<Position> FailedCorpsePoints,ThreatPositions;
|
||||
uint32 Timer=0,UnreachableMs=0,Cooldown=0;
|
||||
};
|
||||
bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until);
|
||||
@@ -42,7 +42,9 @@ private:
|
||||
bool Safe(Player* player,Creature* target) const;
|
||||
bool Reachable(Player* player,Unit* target) const;
|
||||
Creature* Dummy(Player* player) const;
|
||||
Unit* PvPTarget(Player* player) const;
|
||||
Creature* Proactive(Player* player) const;
|
||||
bool Flee(Player* player,Record& record);
|
||||
bool Death(Player* player,Record& record);
|
||||
void Finish(Player* player,Record& record);
|
||||
std::unordered_map<uint32,Record> _records;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#define VCORE_ADVENTURE_COMBAT_POLICY_H
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
namespace AdventureCombatPolicy
|
||||
{
|
||||
// Conservative initial tuning. Self-defence deliberately bypasses this filter.
|
||||
@@ -13,6 +15,21 @@ namespace AdventureCombatPolicy
|
||||
return enemy<=level+MaxLevelAdvantage && health>=MinimumHealth && !elite && !boss &&
|
||||
nearbyHostiles<=1 && (!manaUser || mana>=MinimumMana);
|
||||
}
|
||||
inline bool ShouldFlee(bool grouped,float health,unsigned level,unsigned enemy,bool elite,bool boss)
|
||||
{
|
||||
return !grouped && (health<20 || (enemy>level && enemy-level>=6) ||
|
||||
((elite || boss) && enemy>=level));
|
||||
}
|
||||
constexpr float PvPRadius=50;
|
||||
inline bool PvPLevel(unsigned level,unsigned enemy)
|
||||
{return level>enemy?level-enemy<=5:enemy-level<=5;}
|
||||
inline float PvPWillingness(std::string const& intent,float adventure,float risk)
|
||||
{
|
||||
float weight=intent=="adventure"?1.0f:intent=="leisure"?0.35f:
|
||||
intent=="social"?0.2f:intent=="profession"?0.15f:intent=="economy"?0.1f:0.0f;
|
||||
return weight*std::max(0.0f,std::min(1.0f,adventure))*
|
||||
std::max(0.0f,std::min(1.0f,risk));
|
||||
}
|
||||
inline bool CemeteryFallback(std::int64_t now,std::int64_t released,bool noPath)
|
||||
{return noPath && released>0 && now>=released && now-released>=300;}
|
||||
inline float SegmentDistance(float x,float y,float z,float ax,float ay,float az,float bx,float by,float bz)
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "Channel.h"
|
||||
#include "ChannelMgr.h"
|
||||
#include "Chat.h"
|
||||
#include "GameTime.h"
|
||||
#include "Group.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Opcodes.h"
|
||||
#include "PartyPackets.h"
|
||||
#include "Player.h"
|
||||
#include "Map.h"
|
||||
#include "Random.h"
|
||||
#include "WorldSession.h"
|
||||
#include "Log.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
float Sociability(Player* p)
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
if(profile){auto it=profile->Personality.find("sociability");if(it!=profile->Personality.end())return std::clamp(it->second,0.0f,1.0f);}
|
||||
return .5f;
|
||||
}
|
||||
bool Ready(Player* p)
|
||||
{return p && p->IsInWorld() && p->IsAlive() && !p->IsInCombat() && !sAdventureCombatMgr->Busy(p) &&
|
||||
!p->IsBeingTeleported() && !p->GetTransport() && !p->IsInFlight() && !p->IsFlying() && !p->IsCharmed() && !p->GetMap()->IsDungeon();}
|
||||
std::string Activity(AdventureGroupPlan const& plan){return plan.Forming?plan.NextIntent:plan.Intent;}
|
||||
}
|
||||
AdventureGroupMgr* AdventureGroupMgr::instance(){static AdventureGroupMgr mgr;return &mgr;}
|
||||
bool AdventureGroupMgr::BotLed(Player* p) const
|
||||
{
|
||||
auto group=p->GetGroup();if(!group)return false;
|
||||
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
|
||||
return leader && leader->GetSession()->IsClientless() && sClientlessLuaEngine->GetAdventureProfile(leader->GetGUID().GetCounter());
|
||||
}
|
||||
bool AdventureGroupMgr::Forming(Player* p) const
|
||||
{
|
||||
if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
|
||||
return sAdventureBehaviorMgr->GroupPlan(leader).Forming;
|
||||
return sAdventureBehaviorMgr->GroupPlan(p).Forming;
|
||||
}
|
||||
bool AdventureGroupMgr::PlannedTransition(Player* p)
|
||||
{
|
||||
if(!BotLed(p))return false;
|
||||
auto it=_records.find(p->GetGUID());
|
||||
if(it==_records.end() || it->second.FormationIntent.empty())return false;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
|
||||
if(plan.Intent!=it->second.FormationIntent)
|
||||
{
|
||||
if(plan.Forming)if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
|
||||
return sAdventureBehaviorMgr->GroupPlan(leader).Intent==it->second.FormationIntent;
|
||||
return false;
|
||||
}
|
||||
it->second.FormationIntent.clear();return true;
|
||||
}
|
||||
void AdventureGroupMgr::Speak(Player* p,std::string const& message)
|
||||
{
|
||||
if(!AdventureGroupPolicy::Capital(p->GetZoneId()))return;
|
||||
auto manager=ChannelMgr::forTeam(p->GetTeam());if(!manager)return;
|
||||
auto channel=manager->GetJoinChannel(0,"sucheNachGruppe");if(!channel)return;
|
||||
if(std::find(p->GetJoinedChannels().begin(),p->GetJoinedChannels().end(),channel)==p->GetJoinedChannels().end())channel->JoinChannel(p,"");
|
||||
channel->Say(p->GetGUID(),message,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH);
|
||||
}
|
||||
void AdventureGroupMgr::Offline(Player* p)
|
||||
{
|
||||
// Native player logout owns channel membership cleanup.
|
||||
_records.erase(p->GetGUID());
|
||||
}
|
||||
bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
|
||||
{
|
||||
if(!Ready(invited) || !Ready(inviter) || invited->GetGroup() || !inviter->GetSession()->IsClientless() ||
|
||||
invited->GetTeam()!=inviter->GetTeam())return false;
|
||||
auto it=_records.find(invited->GetGUID());
|
||||
if(it==_records.end() || it->second.Inviter!=inviter->GetGUID() || it->second.InviteUntil<GameTime::GetGameTime())return false;
|
||||
auto a=sAdventureBehaviorMgr->GroupPlan(inviter),b=sAdventureBehaviorMgr->GroupPlan(invited);
|
||||
auto intent=Activity(a);
|
||||
if(!a.Enabled || !b.Enabled || a.Forming!=b.Forming ||
|
||||
!AdventureGroupPolicy::Compatible(intent,Activity(b),inviter->getLevel(),invited->getLevel()))return false;
|
||||
auto group=inviter->GetGroup();
|
||||
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<AdventureGroupPolicy::Capacity(intent))) && frand(0,1)<Sociability(invited);
|
||||
}
|
||||
void AdventureGroupMgr::Update(Player* p)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
|
||||
if(PlayerLeadership(p))return;
|
||||
if(!Ready(p))return;
|
||||
if(EnforceCapacity(p))return;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(p);if(!plan.Enabled)return;
|
||||
auto intent=Activity(plan);if(!AdventureGroupPolicy::Eligible(intent))return;
|
||||
auto group=p->GetGroup();
|
||||
if(group && BotLed(p))if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
|
||||
{auto leaderPlan=sAdventureBehaviorMgr->GroupPlan(leader);if(leaderPlan.Forming)r.FormationIntent=Activity(leaderPlan);}
|
||||
if(group && group->GetLeaderGUID()!=p->GetGUID())return;
|
||||
if(intent=="raid" && p->GetRaidDifficulty()!=RAID_DIFFICULTY_25MAN_NORMAL)
|
||||
{
|
||||
WorldPacket mode(MSG_SET_RAID_DIFFICULTY,4);mode<<uint32(RAID_DIFFICULTY_25MAN_NORMAL);
|
||||
p->GetSession()->HandleSetRaidDifficultyOpcode(mode);
|
||||
}
|
||||
auto capacity=AdventureGroupPolicy::Capacity(intent);
|
||||
if(group && capacity>5 && !group->isRaidGroup())group->ConvertToRaid();
|
||||
if(group && capacity<=5 && group->isRaidGroup() && group->GetMembersCount()<=5)group->ConvertToGroup();
|
||||
unsigned pending=0;
|
||||
for(auto const& peer:_records)if(peer.second.Inviter==p->GetGUID() && peer.second.InviteUntil>now)
|
||||
{
|
||||
auto invited=ObjectAccessor::FindPlayer(peer.first);
|
||||
if(invited && invited->GetGroupInvite())++pending;
|
||||
}
|
||||
if((group?group->GetMembersCount():1)+pending>=capacity || p->GetGroupInvite() || now<r.SearchAt)return;
|
||||
r.SearchAt=now+urand(20,40);
|
||||
if(plan.Forming)
|
||||
{
|
||||
if(!AdventureGroupPolicy::Capital(p->GetZoneId()))return;
|
||||
if(now>=r.SpeakAt)
|
||||
{
|
||||
r.SpeakAt=now+urand(120,240);
|
||||
auto place=sAdventureLocationMgr->Find(plan.Location);
|
||||
std::string activity=intent=="instance"?"einen Instanzbesuch":intent=="raid"?"einen Schlachtzug":intent=="profession"?"gemeinsame Berufsarbeit":intent=="social"?"eine gesellige Runde":"ein Abenteuer";
|
||||
Speak(p,"Ich suche Gefaehrten fuer "+activity+" in der naechsten Stunde. Treffpunkt: "+(place?place->Name:std::string("hier in der Stadt"))+".");
|
||||
}
|
||||
}
|
||||
if(frand(0,1)>Sociability(p))return;
|
||||
// Snapshot GUIDs: callbacks/native invitations may update manager records.
|
||||
std::vector<ObjectGuid> candidates;for(auto const& entry:_records)candidates.push_back(entry.first);
|
||||
std::sort(candidates.begin(),candidates.end());
|
||||
for(auto guid:candidates)
|
||||
{
|
||||
if(guid==p->GetGUID())continue;
|
||||
auto other=ObjectAccessor::FindPlayer(guid);
|
||||
if(!Ready(other) || other->GetGroupInvite() || other->GetTeam()!=p->GetTeam())continue;
|
||||
auto peerPlan=sAdventureBehaviorMgr->GroupPlan(other);
|
||||
if(!peerPlan.Enabled || peerPlan.Forming!=plan.Forming ||
|
||||
!AdventureGroupPolicy::Compatible(intent,Activity(peerPlan),p->getLevel(),other->getLevel()))continue;
|
||||
if(intent=="profession")
|
||||
{
|
||||
auto a=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
auto b=sClientlessLuaEngine->GetAdventureProfile(other->GetGUID().GetCounter());
|
||||
bool compatible=false;
|
||||
if(a && b)for(auto const& profession:a->PrimaryProfessions)if(b->PrimaryProfessions.count(profession))compatible=true;
|
||||
if(!compatible)continue;
|
||||
}
|
||||
if(auto existing=other->GetGroup())
|
||||
{
|
||||
if(group || existing->GetLeaderGUID()!=other->GetGUID() ||
|
||||
existing->GetMembersCount()>=AdventureGroupPolicy::Capacity(intent))continue;
|
||||
if(plan.Forming)
|
||||
{if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;}
|
||||
else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) || p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue;
|
||||
if(r.Partners[guid]>now || _records[guid].Partners[p->GetGUID()]>now || r.InviteUntil>now)continue;
|
||||
// Structured join request: the existing native leader sends the
|
||||
// invitation, so requesting membership never transfers leadership.
|
||||
Invite(other,p,intent,plan.Forming);return;
|
||||
}
|
||||
auto& peer=_records[guid];
|
||||
if(peer.InviteUntil>now || r.Partners[guid]>now || peer.Partners[p->GetGUID()]>now)continue;
|
||||
if(plan.Forming)
|
||||
{if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;}
|
||||
else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) ||
|
||||
p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue;
|
||||
Invite(p,other,intent,plan.Forming);
|
||||
break;
|
||||
}
|
||||
}
|
||||
void AdventureGroupMgr::Report(ChatHandler* h,Player* p) const
|
||||
{
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
|
||||
h->PSendSysMessage("Bot group intent='%s' next='%s' forming=%u botLed=%u capacity=%u meeting='%s'",plan.Intent.c_str(),plan.NextIntent.c_str(),plan.Forming,BotLed(p),AdventureGroupPolicy::Capacity(Activity(plan)),plan.Location.c_str());
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::FollowMember(Player* p)
|
||||
{
|
||||
auto group=p->GetGroup();
|
||||
if(!group || !BotLed(p) || group->GetLeaderGUID()==p->GetGUID())return false;
|
||||
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return true;
|
||||
auto& r=_records[p->GetGUID()];auto now=GameTime::GetGameTime();
|
||||
if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100)
|
||||
{
|
||||
if(now>=r.SearchAt)
|
||||
{
|
||||
r.SearchAt=now+15;std::string node;float distance=0;
|
||||
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000)
|
||||
{ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true;
|
||||
if(p->GetExactDist(leader)>5)
|
||||
{
|
||||
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5)
|
||||
{
|
||||
Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f);
|
||||
ClientlessPathDiagnostics path;
|
||||
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150)sClientlessTravelMgr->StartTravel(p,end);
|
||||
}
|
||||
}
|
||||
else sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
|
||||
void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent,bool forming)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];auto& peer=_records[other->GetGUID()];
|
||||
unsigned pending=0;
|
||||
for(auto const& entry:_records)if(entry.second.Inviter==p->GetGUID() && entry.second.InviteUntil>now)
|
||||
if(auto target=ObjectAccessor::FindPlayer(entry.first))if(target->GetGroupInvite())++pending;
|
||||
auto group=p->GetGroup();
|
||||
if((group?group->GetMembersCount():1)+pending>=AdventureGroupPolicy::Capacity(intent))return;
|
||||
peer.Inviter=p->GetGUID();peer.InviteUntil=now+30;
|
||||
if(forming){peer.FormationIntent=intent;r.FormationIntent=intent;}
|
||||
peer.Partners[p->GetGUID()]=now+600;r.Partners[other->GetGUID()]=now+600;
|
||||
WorldPackets::Party::PartyInviteClient packet{WorldPacket(CMSG_PARTY_INVITE)};
|
||||
packet.TargetName=other->GetName();packet.TargetGUID=other->GetGUID();
|
||||
p->GetSession()->HandlePartyInviteOpcode(packet);
|
||||
if(forming && other->GetGroupInvite())Speak(other,"Ich wuerde mich euch anschliessen.");
|
||||
else if(other->GetGroupInvite()){p->SetFacingToObject(other);p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE);}
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_BOT_INVITE leader=%u target=%u intent='%s' forming=%u",p->GetGUID().GetCounter(),other->GetGUID().GetCounter(),intent.c_str(),forming);
|
||||
}
|
||||
|
||||
void AdventureGroupMgr::RequestPlayerAdmission(Player* member,Player* requester)
|
||||
{
|
||||
if(!requester || requester->GetSession()->IsClientless() || requester->GetGroup() || requester->GetGroupInvite() || !BotLed(member))return;
|
||||
auto group=member->GetGroup();auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
|
||||
if(!Ready(leader) || !Ready(requester) || leader->GetTeam()!=requester->GetTeam())return;
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[leader->GetGUID()];
|
||||
if(r.Partners[requester->GetGUID()]>now)return;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);
|
||||
if(group->GetMembersCount()>=AdventureGroupPolicy::Capacity(Activity(plan)) || frand(0,1)>Sociability(leader))return;
|
||||
Invite(leader,requester,Activity(plan),false);
|
||||
}
|
||||
bool AdventureGroupMgr::PlayerLeadership(Player* leader)
|
||||
{
|
||||
auto group=leader->GetGroup();if(!group || group->GetLeaderGUID()!=leader->GetGUID())return false;
|
||||
for(auto member=group->GetFirstMember();member;member=member->next())
|
||||
{
|
||||
auto player=member->GetSource();
|
||||
if(!player || player->GetSession()->IsClientless())continue;
|
||||
group->ChangeLeader(player->GetGUID());group->SendUpdate();
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_PLAYER_LEADER old=%u leader=%u",leader->GetGUID().GetCounter(),player->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::EnforceCapacity(Player* p)
|
||||
{
|
||||
auto group=p->GetGroup();if(!group || !BotLed(p) || group->GetLeaderGUID()==p->GetGUID())return false;
|
||||
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return false;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);auto intent=Activity(plan);
|
||||
if(!AdventureGroupPolicy::Eligible(intent))return false;
|
||||
auto capacity=AdventureGroupPolicy::Capacity(intent);if(group->GetMembersCount()<=capacity)return false;
|
||||
unsigned rank=1;
|
||||
for(auto const& member:group->GetMemberSlots())
|
||||
{
|
||||
if(member.guid==group->GetLeaderGUID())continue;
|
||||
++rank;
|
||||
if(member.guid!=p->GetGUID())continue;
|
||||
if(rank<=capacity)return false;
|
||||
p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE);
|
||||
group->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_CAPACITY_LEAVE guid=%u capacity=%u",p->GetGUID().GetCounter(),capacity);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef VCORE_ADVENTURE_GROUP_MGR_H
|
||||
#define VCORE_ADVENTURE_GROUP_MGR_H
|
||||
#include "Common.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
class Player;
|
||||
class ChatHandler;
|
||||
struct AdventureGroupPlan
|
||||
{
|
||||
bool Enabled=false,Forming=false;
|
||||
std::string Intent,Location,NextIntent,NextLocation;
|
||||
time_t Deadline=0;
|
||||
};
|
||||
class TC_GAME_API AdventureGroupMgr
|
||||
{
|
||||
public:
|
||||
static AdventureGroupMgr* instance();
|
||||
void Update(Player* player);
|
||||
void RequestPlayerAdmission(Player* member,Player* requester);
|
||||
bool EnforceCapacity(Player* player);
|
||||
bool PlayerLeadership(Player* leader);
|
||||
void Offline(Player* player);
|
||||
bool Accept(Player* invited,Player* inviter);
|
||||
bool FollowMember(Player* player);
|
||||
bool BotLed(Player* player) const;
|
||||
bool Forming(Player* player) const;
|
||||
bool PlannedTransition(Player* player);
|
||||
void Report(ChatHandler* handler,Player* player) const;
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
time_t SearchAt=0,SpeakAt=0;
|
||||
ObjectGuid Inviter;
|
||||
std::string FormationIntent;
|
||||
time_t InviteUntil=0;
|
||||
std::unordered_map<ObjectGuid,time_t> Partners;
|
||||
};
|
||||
void Invite(Player* leader,Player* member,std::string const& intent,bool forming);
|
||||
void Speak(Player* player,std::string const& message);
|
||||
std::unordered_map<ObjectGuid,Record> _records;
|
||||
};
|
||||
#define sAdventureGroupMgr AdventureGroupMgr::instance()
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef VCORE_ADVENTURE_GROUP_POLICY_H
|
||||
#define VCORE_ADVENTURE_GROUP_POLICY_H
|
||||
#include <string>
|
||||
#include <random>
|
||||
namespace AdventureGroupPolicy
|
||||
{
|
||||
inline bool Capital(unsigned zone)
|
||||
{switch(zone){case 1519:case 1537:case 1657:case 1637:case 1638:case 1497:case 3487:case 3557:case 3703:case 4395:return true;default:return false;}}
|
||||
inline bool Eligible(std::string const& intent)
|
||||
{return intent=="instance" || intent=="raid" || intent=="adventure" || intent=="profession" || intent=="social";}
|
||||
inline unsigned Capacity(std::string const& intent)
|
||||
{return intent=="raid"?20:intent=="instance"?5:intent=="social"?10:3;}
|
||||
inline bool Compatible(std::string const& a,std::string const& b,unsigned level,unsigned other)
|
||||
{return Eligible(a) && a==b && (level>other?level-other<=5:other-level<=5);}
|
||||
template<class Day,class Meeting>
|
||||
void Prepare(Day& plan,std::mt19937& random,float sociability,Meeting meeting)
|
||||
{
|
||||
// Work backwards: never replace the very activity a previous preparation
|
||||
// hour was already created for.
|
||||
for(unsigned h=23;h>0;--h)
|
||||
{
|
||||
auto const& next=plan[h];
|
||||
if(!Eligible(next.Intent) || next.Intent==plan[h-1].Intent)continue;
|
||||
if(next.Intent!="instance" && next.Intent!="raid" && std::uniform_real_distribution<float>(0,1)(random)>sociability)continue;
|
||||
auto location=meeting();if(!location.empty())plan[h-1]={"groupformation",location};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureInteractionPolicy.h"
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
@@ -73,6 +74,7 @@ void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
|
||||
TC_LOG_INFO("server.adventurebot","INTERACTION_GROUP_LEAVE guid=%u reason='%s'",p->GetGUID().GetCounter(),reason);
|
||||
p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
|
||||
sAdventureBehaviorMgr->InterruptForInteraction(p);
|
||||
sAdventureBehaviorMgr->ResumePersonalPlan(p);
|
||||
r.Grouped=false;r.Leader.Clear();r.Invite.Clear();r.Partner.Clear();r.SceneUntil=0;
|
||||
}
|
||||
void AdventureInteractionMgr::Emote(Player* p,Player* source,uint32 emote)
|
||||
@@ -98,15 +100,16 @@ void AdventureInteractionMgr::Reply(Player* p,Player* other,uint32 emote)
|
||||
}
|
||||
void AdventureInteractionMgr::CombatHelp(Player* p,ObjectGuid helper)
|
||||
{ _records[p->GetGUID().GetCounter()].Helper=helper; }
|
||||
void AdventureInteractionMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
|
||||
void AdventureInteractionMgr::Offline(Player* p){sAdventureGroupMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());}
|
||||
bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
{
|
||||
auto& r=_records[p->GetGUID().GetCounter()];auto now=GameTime::GetGameTime();
|
||||
if(r.Timer>diff){r.Timer-=diff;return r.Grouped || r.SceneUntil>now;}
|
||||
if(r.Timer>diff){r.Timer-=diff;return (r.Grouped && !sAdventureGroupMgr->BotLed(p)) || r.SceneUntil>now;}
|
||||
r.Timer=1000;
|
||||
bool unsafe=!p->IsAlive() || p->IsInCombat() || sAdventureCombatMgr->Busy(p) ||
|
||||
p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed();
|
||||
if(unsafe){r.Partner.Clear();r.SceneUntil=0;return p->GetGroup()!=nullptr;}
|
||||
sAdventureGroupMgr->Update(p);
|
||||
for(auto it=r.Encounters.begin();it!=r.Encounters.end();)if(now>=it->second)it=r.Encounters.erase(it);else ++it;
|
||||
if(r.Helper)
|
||||
{
|
||||
@@ -114,7 +117,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
if(Visible(p,helper,25) && p->IsFriendlyTo(helper))
|
||||
p->Say(German(p)?"Danke für die Hilfe!":"Thank you for the help!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,helper);
|
||||
}
|
||||
if(auto invite=p->GetGroupInvite())
|
||||
if(auto invite=p->GetGroupInvite();invite && invite->GetLeaderGUID()!=p->GetGUID())
|
||||
{
|
||||
auto leader=invite->GetLeaderGUID();
|
||||
if(r.Invite!=leader){r.Invite=leader;r.InviteAt=now+urand(2,4);}
|
||||
@@ -122,7 +125,8 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
{
|
||||
auto inviter=ObjectAccessor::FindPlayer(leader);
|
||||
bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() &&
|
||||
!inviter->GetSession()->IsClientless() && frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p));
|
||||
(inviter->GetSession()->IsClientless()?sAdventureGroupMgr->Accept(p,inviter):
|
||||
frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p)));
|
||||
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept;
|
||||
p->GetSession()->HandlePartyInviteResponseOpcode(response);r.Invite.Clear();
|
||||
TC_LOG_INFO("server.adventurebot","INTERACTION_INVITE guid=%u leader=%u accepted=%u",p->GetGUID().GetCounter(),leader.GetCounter(),accept);
|
||||
@@ -146,11 +150,13 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
{
|
||||
r.Grouped=true;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
|
||||
r.Partner.Clear();r.SceneUntil=0;
|
||||
sAdventureBehaviorMgr->InterruptForInteraction(p);
|
||||
if(!sAdventureGroupMgr->BotLed(p) || group->GetLeaderGUID()!=p->GetGUID())sAdventureBehaviorMgr->InterruptForInteraction(p);
|
||||
PartySay(p,"Ich begleite euch eine Weile.","I'll accompany you for a while.");
|
||||
}
|
||||
if(r.Leader!=group->GetLeaderGUID())
|
||||
{
|
||||
if(leader==p && sAdventureGroupMgr->BotLed(p) && !p->GetMap()->IsDungeon())sAdventureBehaviorMgr->ResumePersonalPlan(p);
|
||||
if(r.Leader && !sAdventureGroupMgr->BotLed(p))sAdventureBehaviorMgr->InterruptForInteraction(p);
|
||||
r.Leader=group->GetLeaderGUID();r.LastActivity=now;r.AfkSince=0;
|
||||
if(leader){r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();}
|
||||
}
|
||||
@@ -163,12 +169,14 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();
|
||||
if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0;
|
||||
}
|
||||
if(AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight()))
|
||||
bool forming=sAdventureGroupMgr->Forming(p);
|
||||
if(forming){r.LastActivity=now;r.AfkSince=0;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);}
|
||||
if(!forming && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying()))
|
||||
{
|
||||
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
|
||||
{
|
||||
r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
|
||||
if(frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
|
||||
if(!sAdventureGroupMgr->PlannedTransition(p) && frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
|
||||
}
|
||||
if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
|
||||
}
|
||||
@@ -181,6 +189,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
if(!target)for(auto attacker:ally->getAttackers())if(attacker->GetVictim()==ally){target=attacker;break;}
|
||||
if(target && sAdventureCombatMgr->Assist(p,target))return true;
|
||||
}
|
||||
if(sAdventureGroupMgr->BotLed(p))return false;
|
||||
if(!leader || leader==p || !leader->IsAlive())return true;
|
||||
auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true;
|
||||
@@ -214,7 +223,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);}
|
||||
if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);sAdventureBehaviorMgr->ResumePersonalPlan(p);}
|
||||
auto intent=sAdventureBehaviorMgr->InteractionIntent(p);
|
||||
if(now>=r.NextEncounter && now>=r.NextSocial && !p->GetMap()->IsDungeon() && (intent=="social" || intent=="leisure" || intent=="rest"))
|
||||
{
|
||||
@@ -242,6 +251,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
|
||||
void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
|
||||
{
|
||||
sAdventureGroupMgr->Report(handler,p);
|
||||
auto it=_records.find(p->GetGUID().GetCounter());
|
||||
if(it==_records.end()){handler->SendSysMessage("No interaction state yet.");return;}
|
||||
auto const& r=it->second;auto now=GameTime::GetGameTime();
|
||||
@@ -252,3 +262,12 @@ void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
|
||||
static_cast<unsigned long long>(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0),
|
||||
static_cast<unsigned long long>(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p));
|
||||
}
|
||||
|
||||
bool AdventureInteractionMgr::RequestLeave(Player* p)
|
||||
{
|
||||
if(!AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),
|
||||
p->GetTransport() || p->IsInFlight() || p->IsFlying()))return false;
|
||||
if(p->GetGroupInvite())p->UninviteFromGroup();
|
||||
if(p->GetGroup())Leave(p,_records[p->GetGUID().GetCounter()],"manual-test");
|
||||
return true;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class TC_GAME_API AdventureInteractionMgr
|
||||
{
|
||||
public:
|
||||
static AdventureInteractionMgr* instance();
|
||||
bool RequestLeave(Player* player);
|
||||
bool Update(Player* player,uint32 diff);
|
||||
void Emote(Player* player,Player* source,uint32 emote);
|
||||
void Offline(Player* player);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureBehaviorPolicy.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include "Chat.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DBCStores.h"
|
||||
@@ -53,6 +54,12 @@ bool AdventureLocationMgr::Reload()
|
||||
}
|
||||
while (rows->NextRow());
|
||||
}
|
||||
if (WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance'"))
|
||||
if(auto rows=WorldDatabase.Query("SELECT i.location_key,a.target_map FROM vcore_adventure_instance i JOIN areatrigger_teleport a ON a.ID=i.entry_trigger WHERE i.enabled=1")) do
|
||||
{
|
||||
auto f=rows->Fetch();
|
||||
for(auto& l:_locations)if(l.Point.Id==f[0].GetString())l.InstanceMap=f[1].GetUInt16();
|
||||
}while(rows->NextRow());
|
||||
if (auto rows = WorldDatabase.Query("SELECT category,intent,weight FROM vcore_adventure_location_intent WHERE weight>=0")) do
|
||||
{
|
||||
auto f = rows->Fetch();
|
||||
@@ -110,6 +117,18 @@ bool AdventureLocationMgr::IsWorkstation(uint32 entry,std::string const& role) c
|
||||
|
||||
float AdventureLocationMgr::Weight(AdventureLocation const & location, std::string const & intent) const
|
||||
{
|
||||
if(intent=="instance" || intent=="raid")
|
||||
{
|
||||
auto map=sMapStore.LookupEntry(location.InstanceMap);
|
||||
if(!map || !map->IsDungeon() || map->IsRaid()!=(intent=="raid"))return 0;
|
||||
if(intent=="raid")
|
||||
{
|
||||
Difficulty difficulty=RAID_DIFFICULTY_25MAN_NORMAL;
|
||||
auto data=sDBCManager.GetDownscaledMapDifficultyData(location.InstanceMap,difficulty);
|
||||
if(!data || data->maxPlayers<20)return 0;
|
||||
}
|
||||
}
|
||||
if(intent=="groupformation" && !AdventureGroupPolicy::Capital(location.ZoneId?location.ZoneId:location.AreaId))return 0;
|
||||
auto type = _types.find(location.Point.Category);
|
||||
if (type != _types.end())
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ struct AdventureLocation
|
||||
{
|
||||
ClientlessWorldPoint Point;
|
||||
std::string Name, Region;
|
||||
uint32 AreaId = 0, ZoneId = 0;
|
||||
uint32 AreaId = 0, ZoneId = 0, InstanceMap = 0;
|
||||
bool IsArea = false;
|
||||
uint8 Faction = 0;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "Group.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
@@ -256,6 +258,15 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c
|
||||
if (action.Name == "move_to_point")
|
||||
return StartMoveToPoint(player, action, record);
|
||||
|
||||
record.PendingShared.reset();record.SharedWaitMs=0;
|
||||
if ((action.Name=="travel_to_node" || action.Name=="travel_to_location") && action.Parameters.count("route_source"))
|
||||
{
|
||||
if(AdoptRoute(player,action,record))return true;
|
||||
record.PendingShared=std::make_shared<ClientlessAction>(action);record.Active=true;
|
||||
record.Status.State=ClientlessActionExecutionState::SelectingTarget;
|
||||
record.Status.Detail="waiting for group leader route";return true;
|
||||
}
|
||||
|
||||
if (action.Name == "travel_to_node")
|
||||
return StartTravelToNode(player, action, record);
|
||||
|
||||
@@ -2814,6 +2825,17 @@ void ClientlessActionExecutorMgr::Update(uint32 diff)
|
||||
if (!player->IsInWorld())
|
||||
continue;
|
||||
|
||||
if(record.Active && record.PendingShared)
|
||||
{
|
||||
record.SharedWaitMs+=std::min(diff,30000u);
|
||||
auto action=*record.PendingShared;
|
||||
if(AdoptRoute(player,action,record)){record.PendingShared.reset();continue;}
|
||||
if(record.SharedWaitMs<15000)continue;
|
||||
// A local leader leg has no macro route; late arrivals may also
|
||||
// need their own native catch-up route before joining the convoy.
|
||||
action.Parameters.erase("route_source");record.PendingShared.reset();
|
||||
StartAction(player,action,record);continue;
|
||||
}
|
||||
if (record.Active)
|
||||
{
|
||||
record.GroundExitCheckMs = diff >= record.GroundExitCheckMs ? 0 : record.GroundExitCheckMs - diff;
|
||||
@@ -2828,3 +2850,40 @@ void ClientlessActionExecutorMgr::Update(uint32 diff)
|
||||
StartAction(player, action, record);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction const& action,Record& record)
|
||||
{
|
||||
auto arg=action.Parameters.find("route_source");if(arg==action.Parameters.end())return false;
|
||||
uint32 sourceGuid=uint32(std::strtoul(arg->second.c_str(),nullptr,10));
|
||||
auto group=player->GetGroup();
|
||||
if(!group || group->GetLeaderGUID().GetCounter()!=sourceGuid || sourceGuid==player->GetGUID().GetCounter())return false;
|
||||
auto source=_records.find(sourceGuid);
|
||||
if(source==_records.end() || source->second.Route.empty())return false;
|
||||
// Copy values: inserting/updating executor records must never leave a pointer
|
||||
// into another character's unordered-map record alive across execution.
|
||||
auto route=source->second.Route;
|
||||
auto const snapshot=source->second;
|
||||
auto goal=action.Parameters.find(action.Name=="travel_to_location"?"id":"node");
|
||||
if(goal==action.Parameters.end() || goal->second!=(action.Name=="travel_to_location"?snapshot.FinalLocationId:snapshot.PlannedDestinationNode))return false;
|
||||
auto start=std::min(snapshot.RouteStep?snapshot.RouteStep-1:0,route.size()-1);
|
||||
auto anchor=route[start];
|
||||
std::vector<ClientlessTravelRouteStep> joined;
|
||||
Position point;point.Relocate(anchor.X,anchor.Y,anchor.Z);
|
||||
if(player->GetMapId()==anchor.MapId && player->GetExactDist(point)<100)
|
||||
{anchor.EdgeType="start";anchor.EdgeDatabaseId=0;joined.push_back(anchor);}
|
||||
else
|
||||
{
|
||||
bool complete=false;std::string reached,error;
|
||||
if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error) || !complete)return false;
|
||||
}
|
||||
joined.insert(joined.end(),route.begin()+start+1,route.end());
|
||||
record.Route=std::move(joined);record.RouteStep=0;
|
||||
record.FinalLocalLegPending=snapshot.FinalLocalLegPending || !snapshot.FinalLocationId.empty();
|
||||
record.FinalMapId=snapshot.FinalMapId;record.FinalX=snapshot.FinalX;record.FinalY=snapshot.FinalY;
|
||||
record.FinalZ=snapshot.FinalZ;record.FinalO=snapshot.FinalO;record.FinalLocationId=snapshot.FinalLocationId;
|
||||
record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode;
|
||||
record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode;
|
||||
record.HardDestination=snapshot.HardDestination;record.Active=true;
|
||||
TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()));
|
||||
return StartRouteStep(player,player->GetGUID().GetCounter(),record);
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Common.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -51,6 +52,8 @@ private:
|
||||
{
|
||||
ClientlessActionExecutionStatus Status;
|
||||
bool Active = false;
|
||||
std::shared_ptr<ClientlessAction> PendingShared;
|
||||
uint32 SharedWaitMs=0;
|
||||
bool StopAtNode = false;
|
||||
|
||||
std::vector<ClientlessTravelRouteStep> Route;
|
||||
@@ -110,6 +113,7 @@ private:
|
||||
std::string AdaptiveReachedNode;
|
||||
};
|
||||
|
||||
bool AdoptRoute(Player* player,ClientlessAction const& action,Record& record);
|
||||
bool StartAction(Player* player, ClientlessAction const& action, Record& record);
|
||||
bool StartApproach(Player* player, ClientlessAction const& action, Record& record);
|
||||
bool StartMoveToPoint(Player* player, ClientlessAction const& action, Record& record);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "MoveSpline.h"
|
||||
#include "PhasingHandler.h"
|
||||
#include "Player.h"
|
||||
#include "Group.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Random.h"
|
||||
#include "SpellInfo.h"
|
||||
@@ -139,6 +140,8 @@ ClientlessFlightStatus const * ClientlessFlightMgr::GetStatus(uint32 guid) const
|
||||
|
||||
void ClientlessFlightMgr::Clear(uint32 guid)
|
||||
{
|
||||
auto it=_records.find(guid);
|
||||
if(it!=_records.end() && it->second.Status.State!=ClientlessFlightState::Arrived)it->second.Published->Aborted=true;
|
||||
_records.erase(guid);
|
||||
}
|
||||
|
||||
@@ -270,6 +273,16 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s
|
||||
record.Status.State = ClientlessFlightState::Mounting;
|
||||
record.Status.MountSpell = spell;
|
||||
record.Status.Detail = "waiting for native flying mount";
|
||||
if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID())
|
||||
{
|
||||
auto source=_records.find(group->GetLeaderGUID().GetCounter());
|
||||
if(source!=_records.end() && source->second.Status.MapId==player->GetMapId() &&
|
||||
source->second.Status.Target.GetExactDist(target)<1 && !source->second.Published->Aborted)
|
||||
{
|
||||
record.Following=source->second.Published;record.Status.Landing=source->second.Status.Landing;
|
||||
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter());
|
||||
}
|
||||
}
|
||||
_records[player->GetGUID().GetCounter()] = std::move(record);
|
||||
if (player->CastSpell(player, spell, false) != SPELL_CAST_OK)
|
||||
{
|
||||
@@ -283,6 +296,7 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s
|
||||
|
||||
void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason)
|
||||
{
|
||||
record.Published->Aborted=true;
|
||||
player->StopMoving();
|
||||
record.Moving = false;
|
||||
if (record.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
|
||||
@@ -338,6 +352,13 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
back, Corridor(current, back, goal, record.Settings, sample, ray, true), true, false
|
||||
};
|
||||
}
|
||||
else if(record.Following)
|
||||
{
|
||||
auto shared=FollowTrail(current,goal,record.Settings,*record.Following,record.SharedIndex,sample,ray);
|
||||
if(shared.Waiting)return true;
|
||||
next=shared.Step;
|
||||
if(handoff && next.Valid && Distance(next.Next,PointOf(record.Segment))<1)return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const memory = record.Memory;
|
||||
@@ -368,6 +389,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
Fail(player, record, "flight spline rejected");
|
||||
return false;
|
||||
}
|
||||
record.Published->Points.push_back(next.Next);
|
||||
record.Moving = true;
|
||||
record.SegmentElapsed = 0;
|
||||
++ record.Status.Segments;
|
||||
|
||||
@@ -36,6 +36,9 @@ class TC_GAME_API ClientlessFlightMgr
|
||||
bool Command(ChatHandler * handler, char const * args);
|
||||
private : struct Record
|
||||
{
|
||||
std::shared_ptr<ClientlessFlightPath::SharedTrail> Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
|
||||
std::shared_ptr<ClientlessFlightPath::SharedTrail> Following;
|
||||
std::size_t SharedIndex=0;
|
||||
ClientlessFlightStatus Status;
|
||||
ClientlessFlightPath::Settings Settings;
|
||||
ClientlessFlightPath::Memory Memory;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
// Geometry-only receding-horizon navigation. Server adapters supply phased
|
||||
// terrain/ceiling and static+dynamic collision queries. No Player shortcuts.
|
||||
namespace ClientlessFlightPath
|
||||
@@ -209,6 +210,22 @@ namespace ClientlessFlightPath
|
||||
if (best.Valid) memory.Departed = true;
|
||||
return best;
|
||||
}
|
||||
struct SharedTrail
|
||||
{
|
||||
std::vector<Point> Points;
|
||||
bool Aborted=false;
|
||||
};
|
||||
struct SharedResult {Result Step;bool Waiting=false;};
|
||||
inline SharedResult FollowTrail(Point const& current,Point const& goal,Settings const& settings,
|
||||
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray)
|
||||
{
|
||||
if(trail.Aborted)return {};
|
||||
while(index<trail.Points.size() && Distance(current,trail.Points[index])<0.7f)++index;
|
||||
if(index==trail.Points.size())return {{},true};
|
||||
auto next=trail.Points[index];
|
||||
return {{next,Corridor(current,next,goal,settings,sample,ray,true),false,Distance(next,goal)<0.7f},false};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "AdventureSocialActions.h"
|
||||
@@ -230,6 +231,8 @@ public:
|
||||
}
|
||||
void OnSharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target) override
|
||||
{ sAdventureCombatMgr->SharedTargetDamage(player,helper,target); }
|
||||
void OnGroupJoinRequest(Player* player,Player* requester) override
|
||||
{sAdventureGroupMgr->RequestPlayerAdmission(player,requester);}
|
||||
void OnTextEmote(Player* player, Player* source, uint32 emote) override
|
||||
{ sAdventureInteractionMgr->Emote(player,source,emote); }
|
||||
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
|
||||
|
||||
@@ -157,7 +157,8 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
decltype(_damageNotices) damageNotices;
|
||||
decltype(_emoteNotices) emoteNotices;
|
||||
decltype(_helpNotices) helpNotices;
|
||||
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); }
|
||||
decltype(_groupJoinNotices) groupJoinNotices;
|
||||
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); groupJoinNotices.swap(_groupJoinNotices); }
|
||||
|
||||
// CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS
|
||||
sClientlessTravelMgr->Update(diff);
|
||||
@@ -208,6 +209,9 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
auto helpers=helpNotices.find(characterGuid);
|
||||
if(helpers!=helpNotices.end())for(auto const& notice:helpers->second)
|
||||
record.Controller->OnSharedTargetDamage(player,notice.Helper,notice.Target);
|
||||
auto joins=groupJoinNotices.find(characterGuid);
|
||||
if(joins!=groupJoinNotices.end())for(auto const& source:joins->second)
|
||||
record.Controller->OnGroupJoinRequest(player,ObjectAccessor::FindPlayer(source));
|
||||
record.Controller->Update(player, diff);
|
||||
}
|
||||
}
|
||||
@@ -268,3 +272,11 @@ void ClientlessPlayerMgr::SharedTargetDamage(Unit* victim,Unit* attacker,uint32
|
||||
if(queue.size()<8)queue.push_back({helper->GetGUID(),victim->GetGUID()});
|
||||
}
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::GroupJoinRequest(Player* target,Player* requester)
|
||||
{
|
||||
if(!target || !requester || target==requester || !target->GetSession()->IsClientless() || requester->GetSession()->IsClientless())return;
|
||||
std::lock_guard<std::mutex> lock(_damageMutex);
|
||||
auto& queue=_groupJoinNotices[target->GetGUID().GetCounter()];
|
||||
if(queue.size()<8)queue.push_back(requester->GetGUID());
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
virtual void OnPlayerLogout(Player*) { }
|
||||
// Notification after health was reduced, never the mutable pre-damage event.
|
||||
virtual void OnDamageTaken(Player*, Unit*, uint32) { }
|
||||
virtual void OnGroupJoinRequest(Player*,Player*) { }
|
||||
virtual void OnTextEmote(Player*, Player*, uint32) { }
|
||||
virtual void OnSharedTargetDamage(Player*, ObjectGuid, ObjectGuid) { }
|
||||
};
|
||||
@@ -54,6 +55,7 @@ public:
|
||||
|
||||
bool IsManaged(uint32 characterGuid) const;
|
||||
void DamageTaken(Player* player, Unit* attacker, uint32 damage);
|
||||
void GroupJoinRequest(Player* target,Player* requester);
|
||||
void TextEmote(Player* source, Player* target, uint32 emote);
|
||||
void SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage);
|
||||
void MarkActivity(Player* player);
|
||||
@@ -89,6 +91,7 @@ private:
|
||||
std::unordered_map<uint32,std::deque<HelpNotice>> _helpNotices;
|
||||
struct EmoteNotice { ObjectGuid Source; uint32 Emote; };
|
||||
std::unordered_map<uint32,std::deque<EmoteNotice>> _emoteNotices;
|
||||
std::unordered_map<uint32,std::deque<ObjectGuid>> _groupJoinNotices;
|
||||
std::unordered_map<ObjectGuid,time_t> _activity;
|
||||
std::mutex _damageMutex;
|
||||
std::unordered_map<uint32,std::deque<DamageNotice>> _damageNotices;
|
||||
|
||||
@@ -72,6 +72,9 @@ namespace
|
||||
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
|
||||
if(!player || !sClientlessPlayerMgr->IsManaged(guid))
|
||||
{handler->SendSysMessage("Usage: clientless interaction <online-managed-guid>");return false;}
|
||||
std::string op;input>>op;
|
||||
if(op=="leave")return sAdventureInteractionMgr->RequestLeave(player);
|
||||
if(!op.empty())return false;
|
||||
sAdventureInteractionMgr->Report(handler,player);return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user