Fix AdventureBot group travel and add explicit tram rescue routes

This commit is contained in:
Nico committed 2026-10-29 20:48:11 +01:00
1 parent 54d77d1e4d
commit 19075b6c75
32 files changed
+1211 -60

No files matched your search

@@ -10,6 +10,7 @@ code=r'''
#include <string>
#include <cmath>
#include <iostream>
#include <vector>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
struct Position{float x=0;};struct Guid{unsigned GetCounter(){return 26;}};
@@ -17,10 +18,14 @@ struct Player{float x=50;unsigned map=0;bool combat=false;unsigned stopped=0;Gui
float GetExactDist(Position const&p){return std::fabs(x-p.x);}void StopMoving(){++stopped;}};
bool Unsafe(Player*p){return p->combat;}
enum class ClientlessTravelState{Moving,Arrived,Rejected};
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Rejected;}status;unsigned cancels=0;auto GetStatus(unsigned){return &status;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}}travel;
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Rejected;}status;unsigned cancels=0;bool StartTravel(Player*,Position const&){status.State=ClientlessTravelState::Moving;return true;}auto GetStatus(unsigned){return &status;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}}travel;
auto sClientlessTravelMgr=&travel;
namespace ClientlessPositioning{float clearance=10;bool available=false;unsigned reserves=0;
float Clearance(Player*,Player const&){return clearance;}void Reserve(Player*,Player const&){++reserves;}
bool Nearby(Player*,Position const&,float,Position&spot,std::vector<Position>&){spot.x=3;return available;}}
struct AdventureBehaviorMgr{enum class Phase{Travel,OnSite};struct Record{
std::string Intent="groupformation",Micro="profession_tools",Detail;time_t Deadline=400,GatherRetryAt=0;
std::string Intent="groupformation",Micro="profession_tools",Detail;time_t Deadline=400,GatherRetryAt=0,NextSpacing=0;
bool Gathering=false,Dirty=false;Phase State=Phase::OnSite;unsigned AnchorMap=0,MicroTimer=20000;Position Anchor;
struct{bool Active=false,StopRequested=false;}Leisure;
struct{bool Active=true;}Social;struct{bool Active=false;}Recovery;
@@ -48,6 +53,12 @@ int main(){
assert(m.UpdateGathering(&p,r,112)&&p.stopped==1&&travel.cancels==1);
assert(r.Detail=="waiting at group meeting point before departure");
auto n=m.moves;assert(m.UpdateGathering(&p,r,399)&&m.moves==n); // hold, no new action
ClientlessPositioning::clearance=0;ClientlessPositioning::available=true;
assert(m.UpdateGathering(&p,r,399)&&r.NextSpacing==429&&travel.status.State==ClientlessTravelState::Moving);
auto cancels=travel.cancels;
assert(m.UpdateGathering(&p,r,399)&&travel.cancels==cancels); // spacing walk must finish
p.x=3;travel.status.State=ClientlessTravelState::Arrived;
assert(m.UpdateGathering(&p,r,399)&&travel.cancels==cancels);
assert(!m.UpdateGathering(&p,r,400)&&!r.Gathering&&r.GatherRetryAt==0); // deadline never waits for members
r={};r.State=AdventureBehaviorMgr::Phase::Travel;assert(!m.UpdateGathering(&p,r,100)&&!r.Gathering);
r.State=AdventureBehaviorMgr::Phase::OnSite;r.Dirty=true;assert(!m.UpdateGathering(&p,r,100));
@@ -46,7 +46,7 @@ struct Player:Position{
ObjectGuid guid;bool alive=true,combat=false,teleport=false,charmed=false,flying=false,falling=false,mounted=false,moving=false,taxi=false,transport=false,phase=true,visible=true;unsigned mapid=0;Group*g=&group;Session session;
Player(unsigned id):guid{id}{}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return g;}Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}unsigned GetPhaseShift(){return 169;}
Session*GetSession(){return &session;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool IsCharmed(){return charmed;}
bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;}
bool CanFly(){return mounted && flightCapable;}bool flightCapable=false;bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;}
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return visible && p->visible;}bool IsInAreaTriggerRadius(void*){return false;}
};
struct WorldPacket{WorldPacket(int,int){}template<class T>WorldPacket&operator<<(T){return *this;}};
@@ -71,7 +71,7 @@ enum class ClientlessTransportPhase{Waiting,Riding,Arrived,Failed};
struct TransportStatus{ClientlessTransportPhase Phase=ClientlessTransportPhase::Failed;};struct Stop{unsigned Map=0;struct Point{Position Pos;};std::vector<Point>Points{{{0,0,0}}};};
struct Transport{TransportStatus status;auto GetStatus(unsigned){return &status;}Stop*GetStop(std::string const&){return nullptr;}bool Start(Player*,std::string const&,std::string const&,std::string&){return false;}}transport;auto sClientlessTransportMgr=&transport;
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*){return false;}
bool Follow(Player*,Position const&,std::string&){++follows;status.State=ClientlessFlightState::Flying;status.FollowingPlayer=true;return true;}bool LandFollowing(Player*){++lands;status.FollowingPlayer=false;return true;}}flight;auto sClientlessFlightMgr=&flight;
struct Profile{float GroundMountMinDistance=60;};struct Lua{Profile profile;auto GetAdventureProfile(unsigned){return &profile;}void PrepareGroundMount(Player*p,float,char const*){p->mounted=true;}void DismountGroundMount(Player*p,char const*){p->mounted=false;}}lua;auto sClientlessLuaEngine=&lua;
struct AreaAccess{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access;
@@ -102,6 +102,20 @@ int main(){
leader.flying=true;leader.m_positionZ=80;starts=travel.starts;mgr.Follow(&a);assert(travel.starts==starts);leader.flying=false;leader.m_positionZ=0;
// A native-capable follower uses flight, and safely lands after a kick.
flight.enabled=true;leader.flying=true;mgr.Follow(&a);assert(flight.follows==1 && mgr.Airborne(&a));a.g=nullptr;mgr.Follow(&a);assert(flight.lands==1);a.g=&group;flight.status.State=ClientlessFlightState::Arrived;leader.flying=false;flight.enabled=false;mgr.Follow(&a);
// A stale owned ground catch-up route cannot suppress a visible-phase
// human leader taking off, even beyond normal visual detection distance.
leader.mounted=leader.flightCapable=leader.flying=true;leader.visible=false;
flight.enabled=true;flight.status.State=ClientlessFlightState::Failed;
mgr._records.at(26).RouteOwned=true;executor.status.State=ClientlessActionExecutionState::Moving;
auto cleared=executor.clears;auto flown=flight.follows;
mgr.Follow(&a);assert(executor.clears==cleared+1&&flight.follows==flown+1);
// Matching a grounded flying mount must never be immediately undone by
// the ground-follow dismount branch.
leader.flying=false;leader.visible=true;a.mounted=a.flightCapable=true;
flight.status.State=ClientlessFlightState::Arrived;leader.m_positionX=8;
mgr.Follow(&a);assert(a.mounted);
leader.mounted=leader.flightCapable=false;a.mounted=a.flightCapable=false;
flight.enabled=false;leader.m_positionX=20;
// Taxi journey and its arrival grace pause distance counting. Completion must
// not protect unrelated later ground separation.
auto& state=observed;state.Kind=AdventureGroupTravelMgr::Journey::Taxi;state.Generation=1;state.Began=GameTime::now;state.To="taxi_42";state.GoalMap=1;state.Goal={1000,0,0};leader.taxi=true;leader.m_positionX=900;
@@ -120,6 +134,20 @@ int main(){
// A blocked/unrecognized trip cannot grant unlimited separation protection.
mgr._records.at(26).Pending.clear();state.Kind=AdventureGroupTravelMgr::Journey::None;state.Generation=4;leader.mapid=0;leader.m_positionX=1000;a.mapid=0;a.m_positionX=0;executor.status.State=ClientlessActionExecutionState::Completed;
for(unsigned i=0;i<1200;++i){if(i%2==0)++GameTime::now;mgr.Observe(&a,500);}assert(mgr.Separated(&a));leader.m_positionX=10;mgr.Observe(&a,500);assert(!mgr.Separated(&a));
// Nearby targets belonging to adjacent rows are distinct: a 2yd change
// cannot reuse an old spline that ends at a different formation position.
leader.m_positionX=20;leader.m_positionZ=0;leader.moving=false;
travel.status.State=ClientlessTravelState::Arrived;mgr.Follow(&a);
auto oldTarget=travel.status.Target;leader.m_positionX+=2;auto startsBefore=travel.starts;
mgr.Follow(&a);assert(travel.starts==startsBefore+1&&travel.status.Target.GetExactDist(oldTarget)>1.9f);
// A short stop during an executing route still preserves row distance.
leader.m_positionX=6;leader.moving=false;executor.status.State=ClientlessActionExecutionState::Moving;
std::vector<Position>shortRoad{{0,0,0},{2,0,0},{4,0,0},{6,0,0},{8,0,0}};
assert(mgr.AdaptRoad(&a,shortRoad)&&shortRoad.back().GetPositionX()<6);
// Boarding is released once the leader actually starts the native taxi.
leader.taxi=true;shortRoad={{0,0,0},{2,0,0},{4,0,0},{6,0,0},{8,0,0}};
assert(mgr.AdaptRoad(&a,shortRoad)&&shortRoad.back().GetPositionX()==8);
leader.taxi=false;executor.status.State=ClientlessActionExecutionState::Completed;
// Recorded roads are bounded and validated, never a spline through a cliff.
std::vector<Position>road;for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);leader.moving=false;map.cliff=true;assert(!mgr.AdaptRoad(&a,road));map.cliff=false;road.clear();for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);assert(mgr.AdaptRoad(&a,road) && road.back().GetPositionX()<=60);
}
+2 -2
View File
@@ -9,7 +9,7 @@ a=s.index('bool ClientlessFlightMgr::ApproachDeparture(');b=s.index('\nvoid Clie
approach=s[a:b]
a=s.index(' bool OpenColumn(');b=s.index(' char const * StateName',a)
column=s[a:b]
a=s.index(' if(!airborne)\n');b=s.index(' record.Status.State = ClientlessFlightState::Mounting;',a)
a=s.index(' if(!existingMount)\n');b=s.index(' record.Status.State = ClientlessFlightState::Mounting;',a)
dismount=s[a:b]
a=s.index(' auto floor = sample(current.X, current.Y);');b=s.index(' player->StopMoving();',a)
touchdown=s[a:b]
@@ -73,7 +73,7 @@ struct ClientlessFlightMgr{
'''+touchdown+r'''
return true;
}
void KeepMount(Player*player,bool airborne){
void KeepMount(Player*player,bool airborne){uint32 existingMount=airborne?123:0;
'''+dismount+r'''
}
};
@@ -56,6 +56,9 @@ struct PathGenerator {static inline unsigned probes=0;static inline bool availab
std::string MakeLocalBridgeKey(std::string const&a,std::string const&b){return a+"->"+b;}
'''
cls='class ClientlessTravelGraphMgr { public: bool EnsureLoaded(){return true;}\n'+signature+'''
bool _rescuePlanning=false;
bool RescueNodeAllowed(ClientlessTravelNode const& n)const{return _rescuePlanning || n.Type!="rescue";}
bool RescueEdgeAllowed(ClientlessTravelEdge const& e)const{return RescueNodeAllowed(_nodes.at(e.FromNode))&&RescueNodeAllowed(_nodes.at(e.ToNode));}
ClientlessGraphProbeCache _positionProbes;std::unordered_map<std::string,std::vector<std::string>> _bridgeNeighborhoods;
std::unordered_map<std::string,ClientlessTravelNode> _nodes;
std::unordered_map<std::string,std::vector<ClientlessTravelEdge>> _edges;
+2 -2
View File
@@ -37,7 +37,7 @@ struct ClientlessFlightMgr{
Surface Sample(Player*,Record&,float,float){return {map.floor,ceiling,valid};}
bool Begin(Player*,Position const&,std::string&,bool following){assert(following);++beginnings;return true;}
void Fail(Player*,Record&r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;}
bool Follow(Player*,Position const&,std::string&);bool LandFollowing(Player*);bool AdvanceFollowing(Player*,Record&);
void HoldFollowing(Player*p,Record&r){if(r.Moving)p->StopMoving();r.Moving=false;}bool Follow(Player*,Position const&,std::string&);bool LandFollowing(Player*);bool AdvanceFollowing(Player*,Record&);
};
'''
for sig in ['bool ClientlessFlightMgr::Follow(', 'bool ClientlessFlightMgr::LandFollowing(', 'bool ClientlessFlightMgr::AdvanceFollowing(']:code+=method(sig)+'\n'
@@ -49,7 +49,7 @@ int main(){
assert(mgr.AdvanceFollowing(&p,r));assert(p.launches==1 && r.Segment.x>0 && r.Segment.x<=25 && r.Segment.z==20);
// Moving the leader updates the destination without inventing a ground landing.
target.Relocate(110,20,30);r.Elapsed=2000;assert(mgr.Follow(&p,target,error));assert(r.Status.Target.z==30 && r.TargetUpdated==2000);
clear=false;assert(!mgr.AdvanceFollowing(&p,r));assert(p.stops==1 && r.Status.State==ClientlessFlightState::Flying);
clear=false;r.Moving=true;assert(!mgr.AdvanceFollowing(&p,r));assert(p.stops==1 && r.Status.State==ClientlessFlightState::Flying);
clear=true;ceiling=25;assert(!mgr.Follow(&p,target,error));ceiling=300;
p.Relocate(110,20,30);r.Moving=true;assert(mgr.AdvanceFollowing(&p,r));assert(!r.Moving && p.stops==2); // holds in air, never dismounts at moving goal
assert(mgr.LandFollowing(&p));assert(!r.Status.FollowingPlayer && r.Status.Landing.z==0 && r.Status.Travelled==0);
@@ -0,0 +1,79 @@
"""Run native mount admission and stationary-flight publication adapters."""
from pathlib import Path
import subprocess
import tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
def method(sig):
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
while n:
n+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
code=r'''
#include <algorithm>
#include <cassert>
#include <vector>
using uint32=unsigned;using uint8=unsigned char;
constexpr int MAP_COMMON=0,UNIT_STATE_CASTING=1,MOUNT_CAPABILITY_FLAG_FLYING=1,SPELL_AURA_MOUNTED=1,SPELL_CAST_OK=0;
constexpr int MOVEMENTFLAG_FLYING=1,UNIT_FIELD_BYTES_1=1,UNIT_BYTES_1_OFFSET_ANIM_TIER=1,SMSG_MOVE_UPDATE=1;
enum class AnimTier:uint8{Ground=0,Fly=3};
struct Position{void Relocate(Position const&){}};
struct Map{struct Entry{int MapType=MAP_COMMON;}entry;Entry*GetEntry(){return &entry;}}map;
struct Guid{unsigned GetCounter(){return 49;}};
struct WorldPacket{explicit WorldPacket(int){}};
namespace GameTime{unsigned GetGameTimeMS(){return 100;}}
struct Player{
bool alive=true,combat=false,teleport=false,transport=false,taxi=false,falling=false,water=false,outdoors=true,mounted=true,fly=false,casting=false,learned=true;
unsigned flags=0,tier=0,stops=0,updates=0,animations=0,removals=0,casts=0;int castResult=SPELL_CAST_OK;
struct Capability{unsigned Flags=MOUNT_CAPABILITY_FLAG_FLYING;}capability;
struct{Guid guid;Position pos;unsigned time=0;}m_movementInfo;
bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return taxi;}
bool IsFalling(){return falling;}bool IsInWater(){return water;}bool IsOutdoors(){return outdoors;}Map*GetMap(){return &map;}bool IsMounted(){return mounted;}bool CanFly(){return fly;}
bool HasUnitState(int){return casting;}unsigned GetZoneId(){return 1519;}Guid GetGUID(){return {};}
bool HasSpell(unsigned){return learned;}Capability*GetMountCapability(unsigned){return &capability;}
void StopMoving(){++stops;}void RemoveAurasByType(int){++removals;mounted=false;}
int CastSpell(Player*,unsigned spell,bool triggered){assert(spell==123&&!triggered);++casts;return castResult;}
bool HasUnitMovementFlag(int flag){return flags&flag;}void AddUnitMovementFlag(int flag){flags|=flag;}
unsigned GetByteValue(int,int){return tier;}void SetAnimTier(AnimTier t){tier=unsigned(t);++animations;}
Position GetPosition(){return {};}void WriteMovementInfo(WorldPacket&){assert(flags&MOVEMENTFLAG_FLYING);}
void SendMessageToSet(WorldPacket*,bool){++updates;}
};
struct Battlefield{bool allowed=true;bool CanFlyIn(){return allowed;}}battle;
struct BattleMgr{Battlefield*GetBattlefieldToZoneId(Map*,unsigned){return &battle;}}battleMgr;auto sBattlefieldMgr=&battleMgr;
struct Profile{struct Mount{unsigned DisplayId,Weight;};std::vector<Mount>FlightMounts{{123,1}};}profile;
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
struct Travel{unsigned cancels=0;void CancelTravel(unsigned){++cancels;}}travel;auto sClientlessTravelMgr=&travel;
struct SpellMgr{void*GetSpellInfo(unsigned){return nullptr;}}spells;auto sSpellMgr=&spells;
unsigned FlightType(void*){return 1;}unsigned urand(unsigned low,unsigned){return low;}
struct ClientlessFlightMgr{struct Record{bool Moving=false;};bool enabled=true;
bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*);void HoldFollowing(Player*,Record&);
};
'''+method('bool ClientlessFlightMgr::PrepareFollowingMount(')+method('void ClientlessFlightMgr::HoldFollowing(')+r'''
int main(){
ClientlessFlightMgr m;Player p;
assert(m.PrepareFollowingMount(&p)&&p.casts==1&&p.removals==1&&travel.cancels==1);
p.mounted=p.fly=true;assert(!m.PrepareFollowingMount(&p)&&p.casts==1); // retain native flight mount
p.fly=false;p.casting=true;assert(m.PrepareFollowingMount(&p)&&p.casts==1);p.casting=false;
p.combat=true;assert(!m.PrepareFollowingMount(&p));p.combat=false;
p.outdoors=false;assert(!m.PrepareFollowingMount(&p));p.outdoors=true;
p.transport=true;assert(!m.PrepareFollowingMount(&p));p.transport=false;
p.taxi=true;assert(!m.PrepareFollowingMount(&p));p.taxi=false;
p.falling=true;assert(!m.PrepareFollowingMount(&p));p.falling=false;
p.water=true;assert(!m.PrepareFollowingMount(&p));p.water=false;
battle.allowed=false;assert(!m.PrepareFollowingMount(&p));battle.allowed=true;
p.learned=false;assert(!m.PrepareFollowingMount(&p));p.learned=true;
p.capability.Flags=0;assert(!m.PrepareFollowingMount(&p));p.capability.Flags=1;
profile.FlightMounts[0].Weight=0;assert(!m.PrepareFollowingMount(&p));profile.FlightMounts[0].Weight=1;
p.castResult=1;assert(!m.PrepareFollowingMount(&p));
ClientlessFlightMgr::Record r;r.Moving=true;
auto stops=p.stops;m.HoldFollowing(&p,r);
assert(!r.Moving&&p.stops==stops+1&&p.flags==MOVEMENTFLAG_FLYING&&p.tier==unsigned(AnimTier::Fly)&&p.updates==1);
m.HoldFollowing(&p,r);assert(p.updates==1&&p.stops==stops+1&&p.animations==1); // no idle packet flood
p.tier=0;m.HoldFollowing(&p,r);assert(p.updates==2&&p.animations==2); // restore interrupted animation
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Group flight mount: native admission, mount retention, casting wait, rejected capabilities, stationary flight animation and bounded publication passed')
+70
View File
@@ -0,0 +1,70 @@
"""Exercise the real rescue executor with explicit graph and native transfer doubles."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
a=s.index('bool ClientlessActionExecutorMgr::TryRescueTravel(');b=s.index('bool ClientlessActionExecutorMgr::RecoverTravel(',a)
code=r'''
#include <cassert>
#include <string>
#include <vector>
#include <unordered_set>
#include <utility>
using uint32=unsigned;
#define TC_LOG_WARN(...) ((void)0)
struct Node{unsigned MapId=369;std::string Type="rescue";float X=33,Y=40,Z=-4,O=0;}node;
struct Session{unsigned acks=0;void CompleteClientlessTeleport(){++acks;}}session;
struct Player{
unsigned map=369,transfers=0,stops=0;bool alive=true,combat=false,teleport=false,transport=false,taxi=false,flying=false,transferOK=true;
bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}
bool IsInFlight(){return taxi;}bool IsFlying(){return flying;}unsigned GetMapId(){return map;}
void StopMoving(){++stops;}Session*GetSession(){return &session;}
bool TeleportTo(unsigned m,float,float,float,float){assert(m==369);++transfers;return transferOK;}
};
struct ClientlessTravelRouteStep{std::string NodeKey;};
struct Graph{bool available=true,transfer=false,forceSeen=false;unsigned calls=0;
bool PlanRescueRoute(Player*,std::vector<ClientlessTravelRouteStep>&r,std::string&t,bool force,std::unordered_set<uint32>const*){
++calls;forceSeen=force;if(!available)return false;r={{"rescue"},{"exit"}};t=(transfer||force)?"rescue":"";return true;}
Node*FindNode(std::string const&){return &node;}
}graph;auto sClientlessTravelGraphMgr=&graph;
struct Areas{bool allowed=true;bool AllowsNode(Player*,Node const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
struct Core{bool managed=true;bool IsManaged(unsigned){return managed;}}core;auto sClientlessPlayerMgr=&core;
struct Travel{unsigned cancels=0;void CancelTravel(unsigned){++cancels;}}travel;auto sClientlessTravelMgr=&travel;
enum class ClientlessActionExecutionState{Moving,Failed};
struct ClientlessActionExecutorMgr{
struct Record{
bool RescueActive=false,RescueWalkFailed=false,StopAtNode=true,Active=false;
unsigned RescueAttempts=0;std::unordered_set<unsigned>FailedEdgeDatabaseIds;
std::vector<ClientlessTravelRouteStep>Route;unsigned RouteStep=99;std::vector<int>EdgePath;
bool DeferredStartActive=true,GroundExitActive=true,GroundResumeActive=true,TaxiActive=true,RecoveryWanderActive=true;
bool TransportActive=true,FlightActive=true,FlightApproachActive=true,PortalUseActive=true,FormationWaiting=true,GroundChunk=true,ReplanAtGroundEnd=true,GroundFlightExit=true;
bool FinalLocalLegActive=true,FinalLocalLegPending=true,RouteComplete=false;
std::string originalGoal="original-intent";
struct{ClientlessActionExecutionState State=ClientlessActionExecutionState::Failed;std::string Detail;}Status;
};unsigned starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;}
bool TryRescueTravel(Player*,unsigned,Record&);
};
'''+s[a:b]+r'''
int main(){
ClientlessActionExecutorMgr m;Player p;ClientlessActionExecutorMgr::Record r;
assert(m.TryRescueTravel(&p,58,r)&&r.RescueActive&&r.RescueAttempts==1&&p.transfers==0);
assert(r.Route.size()==2&&r.RouteStep==0&&!r.StopAtNode&&!r.FinalLocalLegPending&&!r.TransportActive&&!r.ReplanAtGroundEnd);
assert(r.originalGoal=="original-intent");assert(!m.TryRescueTravel(&p,58,r));
r.RescueWalkFailed=true;assert(m.TryRescueTravel(&p,58,r)&&graph.forceSeen&&p.transfers==1&&session.acks==1);
assert(!m.TryRescueTravel(&p,58,r)); // bounded attempts
r={};graph.transfer=true;p.map=0;assert(!m.TryRescueTravel(&p,58,r)&&p.transfers==1);p.map=369;
r={};areas.allowed=false;assert(!m.TryRescueTravel(&p,58,r));areas.allowed=true;
r={};node.Type="ground";assert(!m.TryRescueTravel(&p,58,r));node.Type="rescue";
r={};p.combat=true;auto calls=graph.calls;assert(!m.TryRescueTravel(&p,58,r)&&graph.calls==calls);p.combat=false;
p.transport=true;assert(!m.TryRescueTravel(&p,58,r));p.transport=false;
p.flying=true;assert(!m.TryRescueTravel(&p,58,r));p.flying=false;
p.alive=false;assert(!m.TryRescueTravel(&p,58,r));p.alive=true;
core.managed=false;assert(!m.TryRescueTravel(&p,58,r));core.managed=true;
graph.available=false;assert(!m.TryRescueTravel(&p,58,r));
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Rescue executor: walk first, tram-only validated transfer, native ACK, original goal retention, state reset, access and bounded retry guards passed')
+61
View File
@@ -0,0 +1,61 @@
"""Execute production adaptive planning and explicit recorded rescue search."""
from pathlib import Path
import runpy,subprocess,tempfile
root=Path(__file__).resolve().parents[2]
e=runpy.run_path(str(root/'tests/clientless/test_free_flight_planner.py'))
s=e['s']
def method(sig):
a=s.index(sig);b=s.index('{',a);n=1;c=b+1
while n:n+=(s[c]=='{')-(s[c]=='}');c+=1
return s[a:c]
preamble=e['preamble'].replace('uint32 GetMapId()const{return 0;}','uint32 map=369; uint32 GetMapId()const{return map;} float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}')
cls=e['cls'].replace('\n};','''
std::vector<ClientlessTravelEdgePoint>const*GetEdgePoints(uint32 id)const{auto i=_edgePoints.find(id);return i==_edgePoints.end()?nullptr:&i->second;}
bool PlanRescueRoute(Player*,std::vector<ClientlessTravelRouteStep>&,std::string&,bool=false,std::unordered_set<uint32>const* =nullptr);
bool FindNearestNodeToPosition(uint32,float,float,float,std::string&,float&)const;
};''')
code=preamble+e['structs']+cls+e['method']+method('bool ClientlessTravelGraphMgr::FindNearestNodeToPosition(')+method('bool ClientlessTravelGraphMgr::PlanRescueRoute(')+r'''
void node(ClientlessTravelGraphMgr&g,std::string k,float x,std::string type="ground"){
ClientlessTravelNode n;n.Key=k;n.Type=type;n.X=x;n.MapId=369;g._nodes[k]=n;
}
void edge(ClientlessTravelGraphMgr&g,std::string a,std::string b,unsigned id){
g._edges[a].push_back({id,a,b,"ground",10,false});
ClientlessTravelEdgePoint p;p.X=g._nodes.at(a).X;p.MapId=369;g._edgePoints[id].push_back(p);
p.X=g._nodes.at(b).X;g._edgePoints[id].push_back(p);
}
int main(){
ClientlessTravelGraphMgr g;Player p;p.x=5000;
node(g,"sw",0,"rescue");node(g,"if",2500,"rescue");node(g,"exit_sw",10);node(g,"exit_if",2490);
edge(g,"sw","exit_sw",1);edge(g,"if","exit_if",2);edge(g,"sw","if",3);
std::vector<ClientlessTravelRouteStep>r;std::string transfer,error,reached;bool complete=false;
// Rescue roots are excluded from ordinary nearest anchors and destinations.
float distance;assert(g.FindNearestNodeToPosition(369,0,0,0,reached,distance)&&reached=="exit_sw");
assert(!g.PlanAdaptiveRoute(&p,"sw",4000,200,r,complete,reached,error));
// A recorded rescue corridor cannot be borrowed as a normal shortcut,
// including virtual entries into the interior of its ground edges.
p.x=0;g.PlanAdaptiveRoute(&p,"exit_if",1,200,r,complete,reached,error);assert(!complete);p.x=5000;
// No traversable local start: only map369 may transfer to the nearest
// permitted authored rescue node with a recorded escape edge.
travel.complete=false;
assert(g.PlanRescueRoute(&p,r,transfer)&&transfer=="if"&&r.size()==2&&r.back().EdgeDatabaseId==2);
assert(!g._rescuePlanning); // scoped privilege cannot leak into later plans
p.map=0;assert(!g.PlanRescueRoute(&p,r,transfer));p.map=369;
// A reachable MMAP approach walks; it must not transfer a healthy bot.
travel.complete=true;p.x=2497;
assert(g.PlanRescueRoute(&p,r,transfer)&&transfer.empty()&&r.back().NodeKey=="exit_if");
assert(g.PlanRescueRoute(&p,r,transfer,true)&&transfer=="if");
// Blocked recording and faction/area denial select another legal escape;
// no bypass of a denied rescue root or native edge is allowed.
std::unordered_set<uint32>blocked{2};travel.complete=false;
assert(g.PlanRescueRoute(&p,r,transfer,true,&blocked)&&transfer=="sw");
access.blocked.insert("sw");assert(!g.PlanRescueRoute(&p,r,transfer,true,&blocked));access.blocked.clear();
blocked={1,2,3};assert(!g.PlanRescueRoute(&p,r,transfer,true,&blocked));
// A node without a recorded edge cannot become a teleport destination.
g._edgePoints.clear();assert(!g.PlanRescueRoute(&p,r,transfer,true));
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Rescue graph: normal exclusion, walking first, recorded-only escapes, bounded tram transfer, access/blocked-edge checks and scoped planning passed')