257 lines
13 KiB
C++
257 lines
13 KiB
C++
#include "AdventureAreaMgr.h"
|
|
#include "ClientlessLuaEngine.h"
|
|
#include "ClientlessFlightMgr.h"
|
|
#include "ClientlessTravelGraphMgr.h"
|
|
#include "DatabaseEnv.h"
|
|
#include "DBCStores.h"
|
|
#include "Log.h"
|
|
#include "ObjectMgr.h"
|
|
#include "Player.h"
|
|
#include "PhaseShift.h"
|
|
#include "TerrainMgr.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
|
|
AdventureAreaMgr* AdventureAreaMgr::instance()
|
|
{
|
|
static AdventureAreaMgr mgr;
|
|
return &mgr;
|
|
}
|
|
|
|
bool AdventureAreaMgr::EnsureLoaded() { return _loaded || Reload(); }
|
|
bool AdventureAreaMgr::Applies(Player const* player) const
|
|
{
|
|
return player && sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
|
}
|
|
|
|
bool AdventureAreaMgr::Reload()
|
|
{
|
|
// Build a complete new snapshot before replacing the currently working one.
|
|
auto rows = WorldDatabase.Query("SELECT area_id,map_id,parent_area_id,allowed_factions,min_level,max_level,max_level_gap,prefer_flight FROM vcore_adventure_area_catalog");
|
|
if (!rows) return false;
|
|
std::unordered_map<unsigned, AdventureAreaPolicy::Area> areas;
|
|
do
|
|
{
|
|
auto f = rows->Fetch();
|
|
AdventureAreaPolicy::Area a;
|
|
a.Map=f[1].GetUInt32(); a.Parent=f[2].GetUInt32();
|
|
int* values[]{&a.Factions,&a.MinLevel,&a.MaxLevel,&a.LevelGap,&a.Flight};
|
|
for (unsigned i=0;i<5;++i) *values[i]=f[i+3].IsNull() ? -1 : f[i+3].GetUInt8();
|
|
if (a.Factions>3 || a.MinLevel>85 || a.MaxLevel>85 || a.LevelGap>85 || a.Flight>1 ||
|
|
(a.MinLevel>=0 && a.MaxLevel>=0 && a.MinLevel>a.MaxLevel)) return false;
|
|
areas.emplace(f[0].GetUInt32(),a);
|
|
} while(rows->NextRow());
|
|
std::unordered_map<uint32,uint8> maps;
|
|
auto mapRows=WorldDatabase.Query("SELECT map_id,map_type FROM vcore_adventure_map_catalog");
|
|
if (!mapRows) return false;
|
|
do { auto f=mapRows->Fetch(); maps.emplace(f[0].GetUInt32(),f[1].GetUInt8()); } while(mapRows->NextRow());
|
|
std::unordered_map<unsigned,AdventureAreaPolicy::Result> resolved;
|
|
for(auto const& pair:areas)
|
|
{
|
|
auto policy=AdventureAreaPolicy::Resolve(areas,pair.first);
|
|
if(!policy.Valid || !maps.count(pair.second.Map)) return false;
|
|
resolved.emplace(pair.first,policy);
|
|
}
|
|
|
|
struct Master { uint32 Spawn,Map,Faction; Position Pos; };
|
|
std::vector<Master> masters;
|
|
auto npcs=WorldDatabase.Query("SELECT c.guid,c.map,c.position_x,c.position_y,c.position_z,t.faction FROM creature c JOIN creature_template t ON t.entry=c.id WHERE (COALESCE(c.npcflag,t.npcflag) & 8192)<>0");
|
|
if(!npcs) return false;
|
|
do
|
|
{
|
|
auto f=npcs->Fetch(); Master m;
|
|
m.Spawn=f[0].GetUInt32();m.Map=f[1].GetUInt16();m.Pos.Relocate(f[2].GetFloat(),f[3].GetFloat(),f[4].GetFloat());m.Faction=f[5].GetUInt16();
|
|
masters.push_back(m);
|
|
} while(npcs->NextRow());
|
|
auto maskFor=[&](uint32 faction)
|
|
{
|
|
auto npc=sFactionTemplateStore.LookupEntry(faction);
|
|
auto alliance=sFactionTemplateStore.LookupEntry(1),horde=sFactionTemplateStore.LookupEntry(2);
|
|
if(!npc || !alliance || !horde) return 0u;
|
|
return unsigned(!npc->IsHostileTo(alliance) && !alliance->IsHostileTo(npc)) |
|
|
(unsigned(!npc->IsHostileTo(horde) && !horde->IsHostileTo(npc))<<1);
|
|
};
|
|
std::unordered_map<std::string,uint8> nodes;
|
|
auto nodeRows=WorldDatabase.Query("SELECT n.node_key,n.node_type,w.map,w.position_x,w.position_y,w.position_z,a.allowed_factions,a.flightmaster_spawn_id FROM vcore_clientless_travel_node n JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key LEFT JOIN vcore_clientless_node_access a ON a.node_key=n.node_key WHERE n.enabled=1 AND w.enabled=1");
|
|
if(!nodeRows) return false;
|
|
unsigned taxiKnown=0,taxiUnknown=0;
|
|
do
|
|
{
|
|
auto f=nodeRows->Fetch(); std::string key=f[0].GetString();
|
|
unsigned mask=f[6].IsNull()?3:f[6].GetUInt8();
|
|
if(mask>3) return false;
|
|
if(f[1].GetString()=="taxi")
|
|
{
|
|
uint32 map=f[2].GetUInt16(),binding=f[7].IsNull()?0:f[7].GetUInt32();
|
|
Position pos;pos.Relocate(f[3].GetFloat(),f[4].GetFloat(),f[5].GetFloat());
|
|
float nearest=std::numeric_limits<float>::max(); unsigned automatic=0;
|
|
for(auto const& m:masters)
|
|
{
|
|
if(m.Map!=map || (binding && binding!=m.Spawn)) continue;
|
|
float d=pos.GetExactDist(&m.Pos);
|
|
if(d>100.0f) continue;
|
|
if(d+0.5f<nearest) { nearest=d;automatic=maskFor(m.Faction); }
|
|
else if(std::fabs(d-nearest)<=0.5f) automatic|=maskFor(m.Faction);
|
|
}
|
|
mask &= automatic;
|
|
if(automatic) ++taxiKnown;else ++taxiUnknown;
|
|
}
|
|
nodes.emplace(key,uint8(mask));
|
|
} while(nodeRows->NextRow());
|
|
_areas.swap(areas);_resolved.swap(resolved);_mapTypes.swap(maps);_nodes.swap(nodes);
|
|
_positions.clear();_edgeAccess.clear();_loaded=true;++_revision;
|
|
TC_LOG_INFO("server.adventurebot", "AREA_CATALOG revision=%u areas=%u nodes=%u taxiResolved=%u taxiClosedUnknown=%u",_revision,uint32(_areas.size()),uint32(_nodes.size()),taxiKnown,taxiUnknown);
|
|
return true;
|
|
}
|
|
|
|
AdventureAreaPolicy::Result AdventureAreaMgr::Area(uint32 id) const
|
|
{
|
|
auto it=_resolved.find(id);return it==_resolved.end()?AdventureAreaPolicy::Result{}:it->second;
|
|
}
|
|
bool AdventureAreaMgr::AllowsArea(Player const* player,uint32 id) const
|
|
{
|
|
if(!Applies(player)) return true;
|
|
auto a=_areas.find(id);if(a==_areas.end()) return false;
|
|
auto map=_mapTypes.find(a->second.Map);
|
|
if(map==_mapTypes.end() || map->second==255) return false;
|
|
return Area(id).Allows(player->GetTeamId()==TEAM_ALLIANCE?1:2,player->getLevel(),map->second==0);
|
|
}
|
|
uint32 AdventureAreaMgr::AreaAt(Player const* player,uint32 map,Position const& pos)
|
|
{
|
|
// Exact coordinates and native terrain swaps; clear on catalog reload.
|
|
// Geography is shared across bots with the same native terrain swaps. A
|
|
// per-character key would repeat all catalog probes for every login.
|
|
float xyz[]{pos.GetPositionX(),pos.GetPositionY(),pos.GetPositionZ()};
|
|
std::string key(sizeof(map)+sizeof(xyz),'\0');
|
|
std::memcpy(key.data(),&map,sizeof(map));
|
|
std::memcpy(key.data()+sizeof(map),xyz,sizeof(xyz));
|
|
for(auto const& swap:player->GetPhaseShift().GetVisibleMapIds())key+=":"+std::to_string(swap.first);
|
|
auto it=_positions.find(key);if(it!=_positions.end())return it->second;
|
|
uint32 zone=0,area=0;
|
|
// LoadTerrain stores weak references. Retain the terrain while checking
|
|
// remote locations instead of repeatedly loading/unloading whole maps.
|
|
auto inserted=_terrains.emplace(map,nullptr);
|
|
if(inserted.second)inserted.first->second=sTerrainMgr.LoadTerrain(map);
|
|
if(auto const& terrain=inserted.first->second)terrain->GetZoneAndAreaId(player->GetPhaseShift(),map,zone,area,pos);
|
|
if(!area)area=zone;
|
|
if(_positions.size()>=100000)
|
|
for(unsigned i=0;i<2048 && !_positions.empty();++i)_positions.erase(_positions.begin());
|
|
_positions.emplace(std::move(key),area);return area;
|
|
}
|
|
bool AdventureAreaMgr::AllowsPosition(Player const* player,uint32 map,Position const& pos)
|
|
{
|
|
if(!Applies(player))return true;
|
|
if(!std::isfinite(pos.GetPositionX()) || !std::isfinite(pos.GetPositionY()) || !std::isfinite(pos.GetPositionZ()))return false;
|
|
if(!EnsureLoaded())return false;
|
|
auto area=AreaAt(player,map,pos);
|
|
// The editable catalog declares Outland/Cataclysm flight regions. Keep
|
|
// low-level blood-elf/draenei areas on map530 separate from Outland.
|
|
bool flightRequired=map==571 || Area(area).PreferFlight;
|
|
if(flightRequired)
|
|
{
|
|
if(player->getLevel()<60 || !sClientlessFlightMgr->Enabled(player))return false;
|
|
if(map==571 && (player->getLevel()<68 || !player->HasSpell(54197)))return false;
|
|
if(map!=530 && map!=571 && !player->HasSpell(90267))return false;
|
|
}
|
|
return AllowsArea(player,area);
|
|
}
|
|
bool AdventureAreaMgr::PreferredFlight(Player const* player,uint32 map,Position const& pos)
|
|
{
|
|
return Applies(player) && EnsureLoaded() && Area(AreaAt(player,map,pos)).PreferFlight;
|
|
}
|
|
bool AdventureAreaMgr::AllowsNode(Player const* player,ClientlessTravelNode const& node)
|
|
{
|
|
if(!Applies(player))return true;
|
|
if(!EnsureLoaded())return false;
|
|
auto mask=_nodes.find(node.Key);
|
|
if(mask==_nodes.end() || !(mask->second & (player->GetTeamId()==TEAM_ALLIANCE?1:2)))return false;
|
|
Position pos;pos.Relocate(node.X,node.Y,node.Z);
|
|
return AllowsPosition(player,node.MapId,pos);
|
|
}
|
|
bool AdventureAreaMgr::AllowsSegment(Player const* player,uint32 map,Position const& from,Position const& to)
|
|
{
|
|
if(!Applies(player))return true;
|
|
if(!EnsureLoaded())return false;
|
|
float distance=from.GetExactDist(&to);
|
|
if(!std::isfinite(distance) || distance>200000)return false;
|
|
unsigned count=std::max(1u,unsigned(std::ceil(distance/40.0f)));
|
|
if(count>5000)return false;
|
|
float xyz[]{from.GetPositionX(),from.GetPositionY(),from.GetPositionZ(),to.GetPositionX(),to.GetPositionY(),to.GetPositionZ()};
|
|
std::string key(sizeof(map)+sizeof(xyz),'\0');
|
|
std::memcpy(key.data(),&map,sizeof(map));std::memcpy(key.data()+sizeof(map),xyz,sizeof(xyz));
|
|
for(auto const& swap:player->GetPhaseShift().GetVisibleMapIds())key+=":"+std::to_string(swap.first);
|
|
if(_segments.size()>=131072 && !_segments.count(key))
|
|
for(unsigned i=0;i<2048 && !_segments.empty();++i)_segments.erase(_segments.begin());
|
|
auto& probe=_segments[key];
|
|
// A prior geometry probe may have stopped at the first area forbidden for
|
|
// another bot. Check the recorded areas, then lazily finish only as needed.
|
|
for(uint32 area:probe.Areas)if(!AllowsArea(player,area))return false;
|
|
while(probe.NextSample<=count)
|
|
{
|
|
unsigned i=probe.NextSample++;
|
|
float t=float(i)/count;Position p;
|
|
p.Relocate(from.GetPositionX()+(to.GetPositionX()-from.GetPositionX())*t,
|
|
from.GetPositionY()+(to.GetPositionY()-from.GetPositionY())*t,
|
|
from.GetPositionZ()+(to.GetPositionZ()-from.GetPositionZ())*t);
|
|
uint32 area=AreaAt(player,map,p);
|
|
if(std::find(probe.Areas.begin(),probe.Areas.end(),area)==probe.Areas.end())probe.Areas.push_back(area);
|
|
if(!AllowsArea(player,area))return false;
|
|
}
|
|
return true;
|
|
}
|
|
bool AdventureAreaMgr::AllowsEdge(Player const* player,ClientlessTravelEdge const& edge)
|
|
{
|
|
if(!Applies(player))return true;
|
|
auto from=sClientlessTravelGraphMgr->FindNode(edge.FromNode),to=sClientlessTravelGraphMgr->FindNode(edge.ToNode);
|
|
if(!from || !to || !AllowsNode(player,*from) || !AllowsNode(player,*to))return false;
|
|
// Items/quests/spells may change without a catalog reload; never cache
|
|
// actor prerequisites as geography or test them only after reaching a portal.
|
|
if(edge.Type=="portal_use" && !sClientlessTravelGraphMgr->PortalPrerequisites(player,edge.DatabaseId))return false;
|
|
std::string key=std::to_string(player->getLevel())+":"+std::to_string(player->GetTeam())+":"+std::to_string(edge.DatabaseId)+":"+edge.FromNode+":"+edge.ToNode;
|
|
for(auto const& swap:player->GetPhaseShift().GetVisibleMapIds())key+=":"+std::to_string(swap.first);
|
|
auto cached=_edgeAccess.find(key);if(cached!=_edgeAccess.end())return cached->second;
|
|
bool allowed=true;
|
|
if(edge.Type=="taxi")
|
|
{
|
|
uint32 path=0,cost=0;
|
|
// Both endpoints have already passed native flightmaster permissions.
|
|
if(from->Key.compare(0,5,"taxi_") || to->Key.compare(0,5,"taxi_"))return false;
|
|
char *sourceEnd=nullptr,*targetEnd=nullptr;
|
|
auto source=std::strtoul(from->Key.c_str()+5,&sourceEnd,10),target=std::strtoul(to->Key.c_str()+5,&targetEnd,10);
|
|
if(!source || !target || *sourceEnd || *targetEnd || source>UINT32_MAX || target>UINT32_MAX)return false;
|
|
sObjectMgr->GetTaxiPath(uint32(source),uint32(target),path,cost);
|
|
if(!path || path>=sTaxiPathNodesByPath.size())return false;
|
|
auto const& points=sTaxiPathNodesByPath[path];
|
|
for(std::size_t i=0;i<points.size() && allowed;++i)
|
|
{
|
|
auto p=points[i];if(!p){allowed=false;break;}
|
|
Position next;next.Relocate(p->Loc.X,p->Loc.Y,p->Loc.Z);
|
|
allowed=AllowsPosition(player,p->ContinentID,next);
|
|
if(i && points[i-1] && points[i-1]->ContinentID==p->ContinentID)
|
|
{
|
|
auto previous=points[i-1];Position start;start.Relocate(previous->Loc.X,previous->Loc.Y,previous->Loc.Z);
|
|
allowed=allowed && AllowsSegment(player,p->ContinentID,start,next);
|
|
}
|
|
}
|
|
}
|
|
// Native transports have no land path across the sea. Their two dock nodes
|
|
// and explicit node restrictions decide access; portal destinations likewise.
|
|
if(edge.Type=="ground")
|
|
{
|
|
Position previous;previous.Relocate(from->X,from->Y,from->Z);
|
|
if(auto points=sClientlessTravelGraphMgr->GetEdgePoints(edge.DatabaseId))
|
|
for(std::size_t i=0;i<points->size() && allowed;++i)
|
|
{
|
|
auto const& p=(*points)[edge.Reverse?points->size()-1-i:i];Position next;next.Relocate(p.X,p.Y,p.Z);
|
|
allowed=p.MapId==from->MapId && AllowsSegment(player,p.MapId,previous,next);previous=next;
|
|
}
|
|
Position end;end.Relocate(to->X,to->Y,to->Z);
|
|
allowed=allowed && from->MapId==to->MapId && AllowsSegment(player,to->MapId,previous,end);
|
|
}
|
|
if(_edgeAccess.size()>=100000)_edgeAccess.clear();
|
|
_edgeAccess.emplace(std::move(key),allowed);return allowed;
|
|
}
|