195 lines
11 KiB
Python
195 lines
11 KiB
Python
"""Execute production optional-exit methods with deterministic movement doubles."""
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
|
|
root = Path(__file__).resolve().parents[2]
|
|
source = (root / 'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
|
header = (root / 'src/server/game/World/ClientlessActionExecutorMgr.h').read_text()
|
|
graph = (root / 'src/server/game/World/ClientlessTravelGraphMgr.h').read_text()
|
|
|
|
def method(signature):
|
|
start = source.index(signature)
|
|
opening = source.index('{', start)
|
|
depth, end = 1, opening + 1
|
|
while depth:
|
|
depth += (source[end] == '{') - (source[end] == '}')
|
|
end += 1
|
|
return source[start:end]
|
|
|
|
preamble = r'''
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
using uint8=uint8_t; using uint32=uint32_t; using uint64=uint64_t; using int32=int32_t;
|
|
#define TC_GAME_API
|
|
#define TC_LOG_INFO(...) ((void)0)
|
|
#define TC_LOG_WARN(...) ((void)0)
|
|
#define TC_LOG_DEBUG(...) ((void)0)
|
|
struct Player;
|
|
struct ClientlessAction;
|
|
struct Position {
|
|
float x=0,y=0,z=0,o=0;
|
|
void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}
|
|
void Relocate(Player const*);
|
|
float GetPositionX()const{return x;} float GetPositionY()const{return y;}
|
|
float GetPositionZ()const{return z;} float GetOrientation()const{return o;}
|
|
};
|
|
struct Motion {
|
|
int launches=0;std::vector<Position> path;
|
|
void MoveSmoothPath(int,Position const* p,std::size_t count,bool){++launches;path.assign(p,p+count);}
|
|
};
|
|
struct Player {
|
|
float x=0; bool spline=true, flying=false;Motion motion;
|
|
bool IsFlying()const{return flying;}bool IsSplineEnabled()const{return spline;}
|
|
bool IsInWorld()const{return true;}uint32 GetMapId()const{return 0;}
|
|
float GetPositionX()const{return x;}float GetPositionY()const{return 0;}float GetPositionZ()const{return 0;}
|
|
float GetOrientation()const{return 0;}
|
|
float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+b*b+c*c);}
|
|
std::string GetName()const{return "Arthoran";}
|
|
Motion* GetMotionMaster(){return &motion;}
|
|
};
|
|
void Position::Relocate(Player const* p){x=p->x;y=z=o=0;}
|
|
enum class ClientlessTravelState {Moving,Arrived,PathRejected};
|
|
struct ClientlessPathDiagnostics {bool Complete=false;float FinalDistance=0,PathLength=0;};
|
|
struct TravelStatus {ClientlessTravelState State=ClientlessTravelState::Moving;};
|
|
struct Travel {
|
|
bool reachable=false, starts=true,rejectOnStart=false;int probes=0, launches=0,cancels=0;float targetX=0;TravelStatus status;
|
|
bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics& nav){++probes;nav.Complete=reachable;nav.PathLength=40;return true;}
|
|
bool StartTravel(Player* p,Position const& pos,bool){++launches;targetX=pos.x;p->spline=false;status.State=starts&&!rejectOnStart?ClientlessTravelState::Moving:ClientlessTravelState::PathRejected;return starts;}
|
|
void CancelTravel(uint32){++cancels;}
|
|
TravelStatus const* GetStatus(uint32){return &status;}
|
|
} travel;
|
|
auto sClientlessTravelMgr=&travel;
|
|
enum class ClientlessFlightState {Mounting,Flying,Arrived,Failed};
|
|
struct FlightStatus {ClientlessFlightState State=ClientlessFlightState::Mounting;};
|
|
struct Flight {
|
|
bool enabled=false,starts=false,recovery=false;int launches=0;FlightStatus status;
|
|
void BeginPlanning(){}
|
|
float Estimate(Player*,uint32,Position const&,Position const&,bool fallback=false){recovery=fallback;return enabled?100:std::numeric_limits<float>::infinity();}
|
|
bool Start(Player*,Position const&,std::string&){++launches;return starts;}
|
|
FlightStatus const* GetStatus(uint32){return &status;}
|
|
} flight;
|
|
auto sClientlessFlightMgr=&flight;
|
|
struct Lua {void PrepareGroundMount(Player*,float,char const*){}} lua;
|
|
auto sClientlessLuaEngine=&lua;
|
|
'''
|
|
structs = graph[graph.index('struct TC_GAME_API ClientlessTravelNode'):graph.index('class TC_GAME_API ClientlessTravelGraphMgr')]
|
|
cls = header[header.index('enum class ClientlessActionExecutionState'):header.index('#define sClientlessActionExecutorMgr')].replace('private:', 'public:')
|
|
doubles = r'''
|
|
struct Graph {
|
|
ClientlessTravelNode node;
|
|
std::vector<ClientlessTravelEdgePoint> points;
|
|
auto GetEdgePoints(uint32){return &points;}
|
|
ClientlessTravelNode const* FindNode(std::string const& key){return key==node.Key?&node:nullptr;}
|
|
} graph;
|
|
auto sClientlessTravelGraphMgr=&graph;
|
|
static int recoveries=0;
|
|
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32,std::string const&){++recoveries;return false;}
|
|
'''
|
|
methods = '\n'.join(method(sig) for sig in (
|
|
'bool StartGroundConnection(',
|
|
'bool ClientlessActionExecutorMgr::PrepareRecordedGroundEdge(',
|
|
'bool ClientlessActionExecutorMgr::GetTravelGoal(',
|
|
'bool ClientlessActionExecutorMgr::TryFallbackFlight(',
|
|
'void ClientlessActionExecutorMgr::CheckGroundExit(',
|
|
'bool ClientlessActionExecutorMgr::ResumeRecordedEdge(',
|
|
'void ClientlessActionExecutorMgr::UpdateGroundExit(',
|
|
'bool ClientlessActionExecutorMgr::StartRecordedEdgePath(',
|
|
))
|
|
tests = r'''
|
|
int main(){
|
|
using Record=ClientlessActionExecutorMgr::Record;
|
|
ClientlessActionExecutorMgr mgr;Player p;Record r;
|
|
r.Active=true;r.Status.ActionName="travel_to_location";r.FinalX=100;r.FinalLocationId="inn";
|
|
r.Route.resize(2);r.Route[0].NodeKey="entry";r.Route[1].NodeKey="gate";
|
|
r.Route[1].EdgeType="ground";r.Route[1].EdgeDatabaseId=42;r.RouteStep=1;
|
|
for(float x:{0.f,20.f,40.f,60.f,80.f,100.f,120.f}){
|
|
ClientlessTravelEdgePoint point;point.X=x;r.EdgePath.push_back(point);
|
|
}
|
|
r.EdgeNextPoint=2;r.RecoveryCount=4;r.ReplanAtGroundEnd=true;
|
|
// A virtual plan keeps the entire road after its proposed exit, to the real node.
|
|
graph.node.Key="gate";graph.node.X=120;graph.points=r.EdgePath;
|
|
Record virtualPlan=r;virtualPlan.Route[1].NodeKey="virtual_ground_exit";
|
|
virtualPlan.Route[1].GroundContinuationNode="gate";
|
|
virtualPlan.Route[1].GroundStartIndex=1;virtualPlan.Route[1].GroundEndIndex=3;
|
|
virtualPlan.Route.push_back({});virtualPlan.Route.back().EdgeType="free_flight";
|
|
assert(mgr.PrepareRecordedGroundEdge(virtualPlan));
|
|
assert(virtualPlan.Route[1].NodeKey=="gate" && virtualPlan.Route[1].GroundEndIndex==-1);
|
|
assert(virtualPlan.Route.size()==3 && virtualPlan.ReplanAtGroundEnd && virtualPlan.GroundFlightExit);
|
|
assert(virtualPlan.EdgePath.front().X==20 && virtualPlan.EdgePath.back().X==120);
|
|
// Reverse execution retains the same forward-only continuation in execution order.
|
|
virtualPlan=r;virtualPlan.Route[1].NodeKey="virtual_ground_exit";
|
|
virtualPlan.Route[1].GroundContinuationNode="gate";virtualPlan.Route[1].EdgeReverse=true;
|
|
virtualPlan.Route[1].GroundStartIndex=1;virtualPlan.Route[1].GroundEndIndex=3;
|
|
assert(mgr.PrepareRecordedGroundEdge(virtualPlan));
|
|
assert(virtualPlan.EdgePath.front().X==100 && virtualPlan.EdgePath.back().X==0);
|
|
// Twenty failed probes, not five: same route, edge, direction and active spline.
|
|
for(int i=0;i<20;++i){
|
|
r.GroundExitCheckMs=0;mgr.CheckGroundExit(&p,26,r);
|
|
assert(r.Active && p.spline && !r.GroundExitActive && r.EdgePath.size()==7);
|
|
assert(r.RouteStep==1 && r.Route.size()==2 && r.Route[1].NodeKey=="gate");
|
|
assert(r.RecoveryCount==4 && recoveries==0 && travel.cancels==0 && travel.launches==0);
|
|
assert(r.FailedBridgeKeys.empty() && r.FailedEdgeDatabaseIds.empty());
|
|
mgr.CheckGroundExit(&p,26,r); // clock has not elapsed: do not probe again
|
|
}
|
|
assert(travel.probes==20 && r.GroundExitCheckMs==6000);
|
|
// A later valid probe selects the exit without pre-emptive route replacement.
|
|
travel.reachable=true;r.GroundExitCheckMs=0;mgr.CheckGroundExit(&p,26,r);
|
|
assert(r.GroundExitActive && !p.spline && travel.launches==1 && r.EdgePath.size()==7);
|
|
// If the accepted exit subsequently fails, reconnect only to the NEXT road point.
|
|
p.x=35;travel.status.State=ClientlessTravelState::PathRejected;
|
|
mgr.UpdateGroundExit(&p,26,r);
|
|
assert(r.GroundResumeActive && !r.GroundExitActive && travel.targetX==40 && recoveries==0);
|
|
p.x=40;travel.status.State=ClientlessTravelState::Arrived;mgr.UpdateGroundExit(&p,26,r);
|
|
assert(!r.GroundResumeActive && p.motion.launches==1 && p.motion.path.front().x==40);
|
|
assert(r.EdgeNextPoint==3 && r.EdgeSplinePoints.front()==3);
|
|
for(auto const& point:p.motion.path)assert(point.x>=40); // no replay towards entry
|
|
// An execution-query rejection after a successful probe resumes road immediately.
|
|
travel.starts=false;r.GroundExitCheckMs=0;p.spline=true;
|
|
mgr.CheckGroundExit(&p,26,r);assert(!r.GroundExitActive && p.motion.launches==2 && recoveries==0);
|
|
travel.starts=true;travel.rejectOnStart=true;r.GroundExitCheckMs=0;p.spline=true;
|
|
mgr.CheckGroundExit(&p,26,r);assert(!r.GroundExitActive && p.motion.launches==3 && recoveries==0);
|
|
travel.rejectOnStart=false;
|
|
// A successful local exit completes the original destination.
|
|
r.GroundExitCheckMs=0;mgr.CheckGroundExit(&p,26,r);
|
|
p.x=100;travel.status.State=ClientlessTravelState::Arrived;mgr.UpdateGroundExit(&p,26,r);
|
|
assert(!r.Active && r.Status.State==ClientlessActionExecutionState::Completed);
|
|
assert(r.Status.TargetName=="location:inn" && r.EdgePath.empty() && recoveries==0);
|
|
// A normal graph route with no speculative virtual end can also take a valid exit.
|
|
r.Active=true;r.ReplanAtGroundEnd=false;r.EdgePath.resize(1);r.EdgeNextPoint=0;r.GroundExitCheckMs=0;
|
|
mgr.CheckGroundExit(&p,26,r);assert(r.GroundExitActive);
|
|
// Missing regular travel triggers the separate recovery flight policy.
|
|
Record fallback;fallback.Status.ActionName="travel_to_location";fallback.FinalX=260;
|
|
flight.enabled=true;flight.starts=true;
|
|
assert(mgr.TryFallbackFlight(&p,26,fallback));
|
|
assert(flight.recovery && fallback.FlightActive && fallback.RouteStep==1);
|
|
assert(fallback.Route.back().X==260 && fallback.FinalLocalLegPending);
|
|
assert(!mgr.TryFallbackFlight(&p,26,fallback)); // no repeated accepted fallback
|
|
// A failed flight departure from an optional exit also preserves forward road.
|
|
Record air;air=r;air.GroundExitActive=false;air.GroundFlightExit=true;air.GroundExitCheckMs=0;
|
|
air.EdgeNextPoint=0;air.EdgePath.resize(2);air.EdgePath[0].X=120;air.EdgePath[1].X=140;
|
|
travel.reachable=false;flight.starts=false;p.spline=false;
|
|
auto before=p.motion.launches;mgr.CheckGroundExit(&p,26,air);
|
|
assert(!flight.recovery && p.motion.launches==before+1 && !air.GroundExitActive);
|
|
// End-point rejoin does not launch an empty spline and fail the journey.
|
|
air.GroundResumeActive=true;air.GroundExitActive=false;p.x=140;travel.status.State=ClientlessTravelState::Arrived;
|
|
before=p.motion.launches;mgr.UpdateGroundExit(&p,26,air);
|
|
assert(!air.GroundResumeActive && p.motion.launches==before && air.Active);
|
|
std::cout<<"optional exits: repeated probes, retained route, delayed success, forward resume, flight fallback passed\n";
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix='vcore-ground-exit-') as folder:
|
|
cpp = Path(folder) / 'test.cpp'
|
|
binary = Path(folder) / 'test'
|
|
cpp.write_text(preamble + structs + cls + doubles + methods + tests)
|
|
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
|
|
subprocess.run([str(binary)], check=True)
|