Fix AdventureBot travel recovery, area eligibility and safe build logout
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Waiting to run

This commit is contained in:
Nico committed 2026-10-30 12:43:22 +01:00
1 parent 60cab950bd
commit 649915e575
46 files changed
+1500 -77

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
// Travel algorithms are extracted into small world doubles in these tests.
// Logging is independently exercised by test_travel_telemetry.py.
#ifndef CLIENTLESS_TELEMETRY_TEST_STUB_H
#define CLIENTLESS_TELEMETRY_TEST_STUB_H
#include <string>
namespace ClientlessTravelTelemetry {
template<class... T> void Note(T const&...) {}
template<class... T> void Begin(T const&...) {}
template<class... T> void Tick(T const&...) {}
template<class... T> void Clear(T const&...) {}
template<class... T> std::string State(T const&...) { return {}; }
}
#endif
@@ -37,6 +37,14 @@ int main()
assert(!Resolve(areas,999).Valid);
areas[8]={0,9};assert(!Resolve(areas,8).Valid); // orphan
areas[9]={0,8};assert(!Resolve(areas,8).Valid); // cycle
// Independent native city roots inherit physical surrounding regions.
areas[2817]={571,0,-1,77,80,-1,1};
areas[4395]={571,2817,-1,68,85,-1,-1};
auto dalaran=Resolve(areas,4395);
assert(dalaran.PreferFlight&&dalaran.MinLevel==77&&!dalaran.Allows(1,18,true));
areas[3519]={530,0,-1,62,65,-1,1};areas[3703]={530,3519};
auto shattrath=Resolve(areas,3703);
assert(shattrath.PreferFlight&&shattrath.MinLevel==62&&!shattrath.Allows(2,18,true));
// A reloaded parent restriction changes the exact same child without edits.
areas[1].Factions=0;
assert(!Resolve(areas,2).Allows(2,85,true));
@@ -24,6 +24,7 @@ std::tm Local(time_t){std::tm tm{};tm.tm_hour=5;return tm;}unsigned DateOf(std::
struct Location{unsigned InstanceCapacity=5,InstanceDifficulty=1;};struct Locations{Location l;Location*Find(std::string const&){return &l;}}locations;auto sAdventureLocationMgr=&locations;
struct AdventureBehaviorMgr{
struct Record{bool Enabled=true,Schedule=true,GroupOverride=false,Dirty=false;int Hour=-1;unsigned Date=1;std::string Intent,Location,WantedIntent,WantedLocation,ManualNextIntent,ManualNextLocation;std::string PersonalIntent,PersonalLocation;time_t PersonalDeadline=0;time_t Deadline=3600;AdventureBehaviorPolicy::Day Plan;AdventureInstancePolicy::Visit Instance;AdventureGroupPolicy::Departure Departure;};
bool allowed=true;bool Allowed(Player*,::Location const&,std::string const&){return allowed;}
std::unordered_map<unsigned,Record>_records;int changes=0;
void Generate(Player*,Record&r,unsigned date){r.Date=date;}
void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;}
@@ -34,6 +35,7 @@ int main(){
AdventureBehaviorMgr mgr;Player leader{{29}},member{{31}};auto& a=mgr._records[29];auto& b=mgr._records[31];
a.Intent="groupformation";a.Location="leader-capital";a.Plan[5]={"groupformation","leader-capital"};a.Plan[6]={"instance","deadmines"};
b.Intent="groupformation";b.Location="other-capital";b.Plan[5]={"groupformation","other-capital"};b.Plan[6]={"instance","shadowfang"};
mgr.allowed=false;assert(!mgr.GroupPlan(&member).Enabled);mgr.allowed=true;
auto plan=mgr.GroupPlan(&member);assert(plan.Forming&&plan.NextIntent=="instance"&&plan.NextLocation=="shadowfang");
assert(mgr.AdoptGroup(&member,&leader));assert(b.WantedLocation=="leader-capital"&&b.Deadline==a.Deadline&&b.Schedule);
assert(b.Plan[6].Location=="shadowfang"); // own future plan remains available after departure
+1 -1
View File
@@ -120,6 +120,6 @@ int main(){
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'instance.cpp';exe=Path(tmp)/'instance';src.write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run(['g++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.')
@@ -14,6 +14,7 @@ code=r'''
#include <string>
using uint32=uint32_t;
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}virtual void OnUpdate(uint32){}};
namespace ClientlessSafeLogout{void Update(){}}
namespace ClientlessCombatPreparation{int ticks=0;void Update(uint32){++ticks;}}
struct Field{uint32 value;uint32 GetUInt32(){return value;}};
struct Rows{std::vector<std::vector<Field>> data;unsigned index=0;Field* Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
@@ -17,7 +17,7 @@ struct Store{bool exists=true;Location* Find(std::string const&){return exists?&
struct Manager{void ClearIntent(int){}void SetIntent(int,ClientlessIntent){}}manager;auto sClientlessPlayerMgr=&manager;
struct AdventureBehaviorMgr{
enum class Phase{Idle,Travel};
struct Record{bool Dirty=true,Enabled=true,Publishing=false,Queued=false;std::string Intent,Location,WantedIntent="instance",WantedLocation="instance_visit_at_101",Detail;Phase State=Phase::Idle;std::unordered_set<std::string>Tried;};
struct Record{long Deadline=0;bool Dirty=true,Enabled=true,Publishing=false,Queued=false;std::string Intent,Location,WantedIntent="instance",WantedLocation="instance_visit_at_101",Detail;Phase State=Phase::Idle;std::unordered_set<std::string>Tried;};
bool mapped=true,allowed=true;int fallback=0;std::string action,parameter,target;bool hard=false;
void Begin(Player*,Record&);
bool Allowed(Player*,Location const&,std::string const&){return allowed;}
@@ -38,6 +38,6 @@ int main(){Player p;AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'begin.cpp';exe=Path(tmp)/'begin';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run(['g++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.')
@@ -30,6 +30,7 @@ enum class ClientlessPlayerState{Offline,Online};
struct World{std::unordered_map<unsigned,ClientlessPlayerState>states;auto GetClientlessPlayerState(unsigned id){return states[id];}}world;auto sWorld=&world;
struct Core{std::vector<unsigned>starts;void StartPlayer(unsigned,unsigned guid,int){starts.push_back(guid);world.states[guid]=ClientlessPlayerState::Online;}}core;auto sClientlessPlayerMgr=&core;
int CreateClientlessAdventureLuaController(){return 0;}
namespace ClientlessSafeLogout{void Update(){}}
namespace ClientlessCombatPreparation{unsigned updates=0;void Update(unsigned){++updates;}}
'''+s[a:b]+r'''
int main(){
@@ -132,6 +132,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-adventure-state-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
print('Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.')
@@ -0,0 +1,34 @@
"""Native gossip binding resolves POI maps independently of overlapping continent XY."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
code=r'''
#include "AdventurePoiPolicy.h"
#include <cassert>
using namespace AdventurePoiPolicy;
int main(){
Menus menus{{100,{{101,0}}},{101,{{100,140},{0,141}}}};
std::vector<Root> roots{{100,{571,5800,600,650}}};
auto bindings=Bind(menus,roots);Anchor chosen;
assert(Select(bindings,140,5831,508,chosen)&&chosen.Map==571&&chosen.Z==650);
assert(!Select(bindings,999,5831,508,chosen));
// A much closer unrelated teleport anchor on map1 is not part of gossip ownership.
roots.push_back({999,{1,5831,508,3}});bindings=Bind(menus,roots);
assert(Select(bindings,140,5831,508,chosen)&&chosen.Map==571);
// Native association with several maps is ambiguous; do not guess by XY.
roots.push_back({100,{0,5831,508,3}});bindings=Bind(menus,roots);
assert(!Select(bindings,140,5831,508,chosen));
// A floating platform must not become the terrain hundreds of yards below.
assert(Height(640,-100000,[](float from){return from<658?184.f:658.f;})==658);
assert(Height(650,-100000,[](float from){return from<675?650.f:675.f;})==650);
assert(Height(640,-100000,[](float){return -100000.f;})==-100000);
// Choose a nearby guard's height only after its map has been established.
roots.pop_back();roots.push_back({100,{571,5832,508,658}});bindings=Bind(menus,roots);
assert(Select(bindings,140,5831,508,chosen)&&chosen.Map==571&&chosen.Z==658);
}
'''
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('POI native gossip graph: recursive menus/cycles, unique map, ambiguity rejection, local height anchor.')
+30
View File
@@ -0,0 +1,30 @@
"""The existing reveal/hide command must change the selected player's full bitfield."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/scripts/Commands/cs_cheat.cpp').read_text()
a=s.index(' static bool HandleExploreCheatCommand(');b=s.index('\n};',a)
code=r'''
#include <cstdlib>
#include <string>
#include <cassert>
using uint8=unsigned char;
constexpr int PLAYER_EXPLORED_ZONES_SIZE=156,PLAYER_EXPLORED_ZONES_1=0;
constexpr int LANG_NO_CHAR_SELECTED=0,LANG_YOU_SET_EXPLORE_ALL=1,LANG_YOURS_EXPLORE_SET_ALL=2,LANG_YOU_SET_EXPLORE_NOTHING=3,LANG_YOURS_EXPLORE_SET_NOTHING=4;
struct Session{};
struct Player{unsigned zones[156]{};Session session;Session*GetSession(){return &session;}void SetUInt32Value(int i,unsigned v){zones[i]=v;}};
struct ChatHandler{Player*target=nullptr;explicit ChatHandler(Session*){}ChatHandler()=default;
Player*getSelectedPlayer(){return target;}void SendSysMessage(int){}void SetSentErrorMessage(bool){}
bool needReportToTarget(Player*){return false;}std::string GetNameLink(Player* =nullptr){return "Tester";}
template<class...T>void PSendSysMessage(T const&...){} };
'''+s[a:b]+r'''
int main(){Player selected,caller;ChatHandler handler;handler.target=&selected;
assert(HandleExploreCheatCommand(&handler,"1"));for(auto bit:selected.zones)assert(bit==0xFFFFFFFFu);for(auto bit:caller.zones)assert(bit==0);
assert(HandleExploreCheatCommand(&handler,"0"));for(auto bit:selected.zones)assert(bit==0);
assert(!HandleExploreCheatCommand(&handler,""));handler.target=nullptr;assert(!HandleExploreCheatCommand(&handler,"1"));
}
'''
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('Native cheat explore reveals/clears every zone field on selected player; caller stays unchanged.')
@@ -0,0 +1,34 @@
"""Execute the city-parent migration on fixtures, preserving operator overrides."""
from pathlib import Path
import sqlite3,re,importlib.util,struct,tempfile,sys
sys.dont_write_bytecode=True
root=Path(__file__).resolve().parents[2]
s=(root/'sql/custom/world/2026_10_30_00_adventure_city_parents.sql').read_text()
s='\n'.join(l for l in s.splitlines() if not l.startswith('--'))
# Only adapt UPDATE JOIN syntax; execute the production CASE/filters unchanged.
m=re.search(r'UPDATE (\w+) city\s+JOIN \w+ region ON (.*?)\s+SET city.parent_area_id=region.area_id\s+WHERE (.*);',s,re.S)
assert m
sql=f'UPDATE {m[1]} AS city SET parent_area_id=region.area_id FROM {m[1]} AS region WHERE ({m[2]}) AND ({m[3]})'
db=sqlite3.connect(':memory:');db.execute('CREATE TABLE vcore_adventure_area_catalog(area_id INTEGER PRIMARY KEY,map_id INTEGER,parent_area_id INTEGER,min_level INTEGER,prefer_flight INTEGER)')
rows=[(2817,571,0,77,1),(4395,571,0,68,None),(3519,530,0,62,1),(3703,530,0,None,None),(12,0,0,1,0),(1519,0,999,None,None),(1,0,0,1,0),(1537,1,0,None,None),(999,0,0,1,0)]
db.executemany('INSERT INTO vcore_adventure_area_catalog VALUES(?,?,?,?,?)',rows);db.execute(sql)
after=db.execute('SELECT * FROM vcore_adventure_area_catalog ORDER BY area_id').fetchall()
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=4395').fetchone()==(2817,)
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=3703').fetchone()==(3519,)
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=1519').fetchone()==(999,)
assert db.execute('SELECT parent_area_id FROM vcore_adventure_area_catalog WHERE area_id=1537').fetchone()==(0,)
db.execute(sql);assert db.execute('SELECT * FROM vcore_adventure_area_catalog ORDER BY area_id').fetchall()==after
# New DBC exports use the same semantic parents, without needing a C++ change.
spec=importlib.util.spec_from_file_location('catalog',root/'contrib/clientless/export_area_catalog.py');mod=importlib.util.module_from_spec(spec);spec.loader.exec_module(mod)
def dbc(path,fields,rows):
data=b''.join(struct.pack('<'+'I'*fields,*r) for r in rows);strings=b'\0Test\0'
path.write_bytes(struct.pack('<4s4I',b'WDBC',len(rows),fields,fields*4,len(strings))+data+strings)
with tempfile.TemporaryDirectory()as directory:
p=Path(directory);maps=[];areas=[]
for mid in (530,571):
r=[0]*20;r[0]=mid;r[6]=1;maps.append(r)
for aid,mid in [(2817,571),(4395,571),(3519,530),(3703,530)]:
r=[0]*26;r[0]=aid;r[1]=mid;r[11]=1;areas.append(r)
dbc(p/'Map.dbc',20,maps);dbc(p/'AreaTable.dbc',26,areas);exported=mod.export(p)
assert 'SELECT 4395,571,2817,' in exported and 'SELECT 3703,530,3519,' in exported
print('Editable city catalog parents: Dalaran/CrystalSong, Shattrath/Terokkar, operator preservation, map guard, idempotence and new DBC exports.')
+18 -4
View File
@@ -9,6 +9,8 @@ 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(' float support=',s.index('bool ClientlessFlightMgr::Begin('));b=s.index(' auto const * profile =',a)
preparation=s[a:b]
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)
@@ -31,10 +33,11 @@ struct Position{float x=0,y=0,z=0,o=0;
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}};
struct Guid{unsigned GetCounter()const{return 84;}};
struct Map{float feet=0;float GetHeight(int,float,float,float,bool,float){return feet;}}map;
struct PositionFullTerrainStatus{bool outdoors=true;};
struct Map{bool outdoors=true;void GetFullTerrainStatusForPosition(int,float,float,float,PositionFullTerrainStatus&t){t.outdoors=outdoors;}float feet=0;float GetHeight(int,float,float,float,bool,float){return feet;}}map;
struct Player:Position{bool mounted=false,fly=false,aura=false;int removals=0;
Guid GetGUID(){return {};}unsigned GetMapId(){return 530;}int GetPhaseShift(){return 0;}Map* GetMap(){return &map;}
bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool HasAura(unsigned){return aura;}
bool outdoor=true;bool IsOutdoors(){return outdoor;}bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool HasAura(unsigned){return aura;}
void RemoveAurasByType(unsigned){++removals;mounted=fly=false;}};
Point PointOf(Position const&p){return {p.x,p.y,p.z};}
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
@@ -60,6 +63,9 @@ struct ClientlessFlightMgr{
std::unordered_map<unsigned,Record>_records;Surface surface{0,300,true};int approached=0;bool seek=false;
Surface Sample(Player*,Record&,float,float){return surface;}
bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&);
bool Prepare(Player*player,Record&record,std::string&error){auto origin=surface;bool following=false;
'''+preparation+r'''
return false;}
bool Admit(Player* player,Record&record,std::string&error,bool following=false){
auto origin=surface;std::vector<std::pair<unsigned,unsigned>>candidates{{123,1}};
'''+admission+r'''
@@ -80,6 +86,13 @@ struct ClientlessFlightMgr{
'''+find_return+approach+r'''
int main(){
ClientlessFlightMgr mgr;Player p;Record r;std::string error;r.Status.Target={100,0,0};
// An indoor actor may walk to a verified outdoor spot before mounting.
p.outdoor=false;assert(mgr.Prepare(&p,r,error)&&mgr._records.at(84).ApproachingDeparture);
mgr._records.clear();travel.starts=0;map.outdoors=false;
assert(!mgr.Prepare(&p,r,error)&&travel.starts==0);map.outdoors=true;p.outdoor=true;
// Open ground beyond the old 40-yard ring remains usable, with a bounded MMAP leg.
travel.nav.PathLength=200;assert(mgr.ApproachDeparture(&p,r,false));
travel.nav.PathLength=15;travel.starts=0;mgr._records.clear();
// A normal supported open-air departure remains valid.
assert(mgr.Admit(&p,r,error));
// Brynli: a native flying mount already suspended ten yards above terrain.
@@ -108,9 +121,10 @@ int main(){
areas.allowed=false;assert(!mgr.Admit(&p,r,error));areas.allowed=true;
travel.nav.Complete=false;assert(!mgr.Admit(&p,r,error));travel.nav.Complete=true;
travel.nav.MaxZDelta=20;assert(!mgr.Admit(&p,r,error));travel.nav.MaxZDelta=1;
travel.nav.PathLength=200;assert(!mgr.Admit(&p,r,error));travel.nav.PathLength=15;
travel.nav.PathLength=601;assert(!mgr.Admit(&p,r,error));travel.nav.PathLength=15;
travel.start=false;assert(!mgr.Admit(&p,r,error));travel.start=true;
map.feet=INVALID_HEIGHT;assert(!mgr.Admit(&p,r,error));
mgr.surface.Ground=0;map.feet=NAN;assert(!mgr.Admit(&p,r,error));map.feet=0;mgr.surface.Ground=12;
// A touchdown may use the actual lower floor, never a roof above it.
map.feet=0;p.z=0;mgr.surface.Ground=12;
assert(OpenColumn(&p,0,0,{0,300,true}));narrow=true;assert(!OpenColumn(&p,0,0,{0,300,true}));narrow=false;
@@ -124,6 +138,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-flight-departure-') 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(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-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('Production flight admission: airborne mount retained, lower-floor MMAP departure, blocked/cliff/missing support rejected.')
@@ -0,0 +1,51 @@
"""Exercise the production landing search, including distant and layered alternatives."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
a=s.index(' bool foundLanding = false;');b=s.index(' sClientlessTravelMgr->CancelTravel(',a)
code=r'''
#include <cmath>
#include <cassert>
#include <string>
#include <algorithm>
#include <array>
using uint32=unsigned;
constexpr float MAX_HEIGHT=100000;constexpr int NAV_GROUND=1;
struct Position{float x=0,y=0,z=0,o=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}};
struct Surface{float Ground=0,Ceiling=500;bool Valid=true;};
struct Map{float GetHeight(int,float,float,float,bool,float){return 20;}}map;
struct Player{int GetPhaseShift(){return 0;}Map*GetMap(){return &map;}unsigned GetMapId(){return 530;}unsigned GetInstanceId(){return 0;}};
struct Record{struct{Position Landing;bool FollowingPlayer=false;}Status;struct{float Clearance=60;}Settings;};
struct Areas{bool allowed=true;bool AllowsSegment(Player*,unsigned,Position const&,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
struct PathGenerator{struct Path{bool complete=true;float FinalDistance=0,PathLength=180;bool IsComplete(){return complete;}};
static inline Path result;static inline bool roofReachable=false;static inline unsigned probes=0;
static Path AnalyzePositions(unsigned,unsigned,unsigned,Position const&a,Position const&,int){++probes;auto p=result;if(a.z==20&&!roofReachable)p.complete=false;return p;}};
bool column=true;float openAt=50;bool water=false;
bool OpenColumn(Player*,float x,float y,Surface const&,float){return column&&std::hypot(x,y)>=openAt;}
Surface Sample(Player*,Record&,float x,float y){return {water?30.f:std::hypot(x,y)<50?20.f:0.f,500,true};}
bool Find(Player*player,Record&record,Position const&target,std::string&error){bool following=false;
'''+s[a:b]+r'''
return true;
}
int main(){Player p;Record record;Position target;std::string error;
// Nearby roof candidates are unusable. A safe outdoor landing 80yd away works.
assert(Find(&p,record,target,error));assert(std::hypot(record.Status.Landing.x,record.Status.Landing.y)>=79&&record.Status.Landing.z==0);
// >5yd level difference is fine only when the native MMAP approach is complete.
openAt=0;PathGenerator::roofReachable=true;assert(Find(&p,record,target,error)&&record.Status.Landing.z==20);
PathGenerator::roofReachable=false;assert(Find(&p,record,target,error)&&record.Status.Landing.z==0);
PathGenerator::result.complete=false;assert(!Find(&p,record,target,error));PathGenerator::result.complete=true;
PathGenerator::result.FinalDistance=2;assert(!Find(&p,record,target,error));PathGenerator::result.FinalDistance=0;
PathGenerator::result.PathLength=601;assert(!Find(&p,record,target,error));PathGenerator::result.PathLength=180;
column=false;assert(!Find(&p,record,target,error));column=true;
areas.allowed=false;assert(!Find(&p,record,target,error));areas.allowed=true;
water=true;assert(!Find(&p,record,target,error));
}
'''
# Define the static aggregate out-of-line for older standalone compilers.
code=code.replace('static inline Path result;', 'static Path result;').replace('bool column=true;', 'PathGenerator::Path PathGenerator::result;\nbool column=true;')
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production landing search: distant/layered alternatives, native final ground approach, water/collision/area rejection.')
+1 -1
View File
@@ -129,6 +129,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-flight-policy-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')),'-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
print('Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.')
@@ -49,8 +49,8 @@ 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;
struct Player:Position {
bool spline=true, flying=false;Motion motion;
void StopMoving(){spline=false;}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;}
@@ -94,6 +94,7 @@ struct Graph {
ClientlessTravelNode const* FindNode(std::string const& key){return key==node.Key?&node:nullptr;}
} graph;
auto sClientlessTravelGraphMgr=&graph;
struct Areas{bool AllowsNode(Player*,ClientlessTravelNode const&){return true;}bool AllowsSegment(Player*,uint32,Position const&,Position const&){return true;}}areas;auto sAdventureAreaMgr=&areas;
static int recoveries=0;
bool ClientlessActionExecutorMgr::RecoverTravel(Player*,uint32,Record&,uint32,std::string const&){++recoveries;return false;}
'''
@@ -202,5 +203,5 @@ 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(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
@@ -0,0 +1,30 @@
"""Run the production member restore for guildless, guilded and deleted characters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/game/Groups/Group.cpp').read_text()
a=s.index('void Group::LoadMemberFromDB(');b=s.index('\nvoid Group::ConvertToLFG',a)
code=r'''
#include <cassert>
#include <string>
#include <vector>
using uint8=unsigned char;
enum class HighGuid{Player};struct ObjectGuid{using LowType=unsigned;unsigned id;ObjectGuid()=default;ObjectGuid(HighGuid,unsigned i):id(i){}};
struct Cache{bool exists=true;unsigned guild=0;bool GetCharacterNameByGuid(ObjectGuid,std::string&n){n="Bot";return exists;}
bool GetPlayerGuildIdByGUID(ObjectGuid,unsigned&g){if(!guild)return false;g=guild;return true;}}cache;auto sCharacterCache=&cache;
constexpr int CHAR_DEL_GROUP_MEMBER=1;struct CharacterDatabasePreparedStatement{void setUInt32(int,unsigned){}};
struct DB{int deletes=0;CharacterDatabasePreparedStatement stmt;auto GetPreparedStatement(int){return &stmt;}void Execute(CharacterDatabasePreparedStatement*){++deletes;}}CharacterDatabase;
struct LFG{unsigned loaded=0;void SetupGroupMember(ObjectGuid,unsigned){++loaded;}}lfg;auto sLFGMgr=&lfg;
struct Group{struct MemberSlot{ObjectGuid guid;std::string name;unsigned guildId;uint8 group,flags,roles;};std::vector<MemberSlot>m_memberSlots;unsigned subgroups=0;
unsigned GetGUID(){return 10;}void SubGroupCounterIncrease(unsigned){++subgroups;}void LoadMemberFromDB(unsigned,uint8,uint8,uint8);};
'''+s[a:b]+r'''
int main(){Group group;group.LoadMemberFromDB(26,2,1,4);assert(CharacterDatabase.deletes==0&&group.m_memberSlots.size()==1);
auto member=group.m_memberSlots[0];assert(member.guildId==0&&member.guid.id==26&&member.roles==4&&member.flags==2&&member.group==1);
cache.guild=123;group.LoadMemberFromDB(27,0,0,1);assert(group.m_memberSlots[1].guildId==123&&lfg.loaded==2&&group.subgroups==2);
cache.exists=false;group.LoadMemberFromDB(28,0,0,1);assert(CharacterDatabase.deletes==1&&group.m_memberSlots.size()==2&&lfg.loaded==2);
}
'''
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('Production group restore retains guildless members and their roles; only missing characters are deleted.')
+5 -4
View File
@@ -81,7 +81,7 @@ void Session::CompleteClientlessTeleport(){
}
struct Node {std::string Key;uint32 MapId=530;};
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;};
struct Graph {bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
struct Graph {bool prerequisites=true;bool PortalPrerequisites(Player*,uint32){return prerequisites;}bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
struct Travel {void CancelTravel(uint32){}} travel;
struct Lua {void DismountGroundMount(Player*,const char*){}} lua;
auto sClientlessTravelGraphMgr=&graph;auto sClientlessTravelMgr=&travel;auto sClientlessLuaEngine=&lua;
@@ -105,6 +105,7 @@ harness += methods
harness += r'''
int main(){
ClientlessActionExecutorMgr mgr;
{Player p;Record r;graph.prerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.prerequisites=true;}
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==1&&!p.world);
mgr.UpdatePortalUse(&p,26,r);assert(!r.PortalUseActive&&r.RouteStep==2&&p.mapId==0&&p.session.sequence==1);}
// The global lifecycle may have ACKed a delayed cast before our next update.
@@ -139,7 +140,7 @@ int main(){
with tempfile.TemporaryDirectory(prefix='vcore-portal-use-test-') as folder:
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
cpp.write_text(harness)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print('Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.')
@@ -190,7 +191,7 @@ int main(){
with tempfile.TemporaryDirectory(prefix='vcore-portal-definition-test-') as folder:
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
cpp.write_text(definition_harness)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print('Native portal destination discovery and unsupported/ambiguous spell rejection passed.')
@@ -224,6 +225,6 @@ int main(){
with tempfile.TemporaryDirectory(prefix='vcore-portal-ack-test-') as folder:
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
cpp.write_text(ack_harness)
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run(['c++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', str(cpp), '-o', str(binary)], check=True)
subprocess.run([str(binary)], check=True)
print('Production clientless ACK completion sequence increments only after completed near/far transfers.')
@@ -0,0 +1,48 @@
"""Run production remote prerequisite evaluation with native condition-shaped doubles."""
from pathlib import Path
import subprocess,tempfile,re
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/Conditions/ConditionMgr.cpp').read_text()
a=s.index('bool ConditionMgr::IsPlayerMeetingPrerequisiteConditions(');b=s.index('\nbool ConditionMgr::IsObjectMeetingNotGroupedConditions(',a)
method=s[a:b]
names=sorted(set(re.findall(r'\bCONDITION_[A-Z_]+',method)) - {'CONDITION_SOURCE_TYPE_NONE','CONDITION_SOURCE_TYPE_MAX'})
code=r'''
#include <map>
#include <vector>
#include <cassert>
using uint32=unsigned;using uint8=unsigned char;
enum ConditionSourceType{CONDITION_SOURCE_TYPE_NONE, SPELL, CONDITION_SOURCE_TYPE_MAX};
'''+ 'enum Kind{'+','.join(names)+r''',SPATIAL};
struct Player{unsigned level=52;bool quest=false;};
struct ConditionSourceInfo{Player* targets[3];ConditionSourceInfo(Player*a,Player*b,Player*c):targets{a,b,c}{}};
struct Condition{unsigned ElseGroup=0,ReferenceId=0,ScriptId=0,ConditionTarget=1;Kind ConditionType=CONDITION_LEVEL;bool negative=false;
bool isLoaded()const{return true;}
bool Meets(ConditionSourceInfo&i)const{bool v=ConditionType==CONDITION_LEVEL?i.targets[ConditionTarget]->level>=58:i.targets[ConditionTarget]->quest;return negative?!v:v;}};
using ConditionContainer=std::vector<Condition*>;
struct ConditionMgr{std::map<unsigned,ConditionContainer>ConditionStore[CONDITION_SOURCE_TYPE_MAX],ConditionReferenceStore;
bool IsPlayerMeetingPrerequisiteConditions(ConditionSourceType,unsigned,Player const*,uint8)const;};
'''+method+r'''
int main(){ConditionMgr mgr;Player p;Condition level,quest;quest.ConditionType=CONDITION_QUESTREWARDED;
auto check=[&](unsigned mask=2){return mgr.IsPlayerMeetingPrerequisiteConditions(SPELL,65729,&p,mask);};
assert(check());mgr.ConditionStore[SPELL][65729]={&level};assert(!check());p.level=58;assert(check());p.level=52;
// Visiting player is target1 for GO casts. Do not apply player tests to a GO caster.
level.ConditionTarget=0;assert(check());assert(!check(3));level.ConditionTarget=1;
mgr.ConditionStore[SPELL][65729]={&level,&quest};assert(!check());p.quest=true;assert(!check());
quest.ElseGroup=1;assert(check());p.quest=false;assert(!check());
// Remote spatial/script requirements are unknown, not a reason to reject an OR branch.
quest.ConditionType=SPATIAL;assert(check());quest.ElseGroup=0;assert(!check());
quest.ElseGroup=1;quest.ConditionType=CONDITION_QUESTREWARDED;quest.ScriptId=12;assert(check());quest.ScriptId=0;
Condition ref;ref.ReferenceId=42;mgr.ConditionReferenceStore[42]={&level};mgr.ConditionStore[SPELL][65729]={&ref};assert(!check());
p.level=58;assert(check());level.negative=true;assert(!check());p.level=52;assert(check());
// Recursive/broken reference must defer to native use rather than recurse forever.
mgr.ConditionReferenceStore[42]={&ref};assert(check());
}
'''
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)
# Prerequisites must run before the cached static edge decision.
a=(root/'src/server/game/World/AdventureAreaMgr.cpp').read_text();a=a[a.index('bool AdventureAreaMgr::AllowsEdge('):]
assert a.index('PortalPrerequisites')<a.index('_edgeAccess.find')
print('Production portal prerequisites: level/quest, target roles, AND/OR, negative and reference conditions; no stale permission cache.')
+26
View File
@@ -0,0 +1,26 @@
"""Run the actual logout eligibility gate, without a live server."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2];s=(root/'src/server/scripts/Custom/viano_bot_commands.cpp').read_text()
a=s.index('bool Ready(Player const* player)',s.index('std::unordered_map<uint32,bool> Pending;'));b=s.index('\nbool Command(',a)
code=r'''
#include <cassert>
#include <initializer_list>
constexpr int UNIT_STATE_CASTING=1;
struct Player{bool world=true,alive=true,combat=false,transport=false,taxi=false,flying=false,falling=false,teleport=false,casting=false,spline=false;
bool IsInWorld()const{return world;}bool IsAlive()const{return alive;}bool IsInCombat()const{return combat;}bool GetTransport()const{return transport;}
bool IsInFlight()const{return taxi;}bool IsFlying()const{return flying;}bool IsFalling()const{return falling;}bool IsBeingTeleported()const{return teleport;}
bool HasUnitState(int)const{return casting;}bool IsSplineEnabled()const{return spline;}};
'''+s[a:b]+r'''
int main(){Player p;assert(Ready(&p)&&!Ready(nullptr));
for(auto field:{&Player::combat,&Player::transport,&Player::taxi,&Player::flying,&Player::falling,&Player::teleport,&Player::casting,&Player::spline}){
p.*field=true;assert(!Ready(&p));p.*field=false;assert(Ready(&p));
}
p.world=false;assert(!Ready(&p));p.world=true;p.alive=false;assert(!Ready(&p));
}
'''
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('Native safe logout gate waits for combat, casting, movement, transport, taxi, flight, falling, teleport and resurrection.')
+13 -3
View File
@@ -128,7 +128,7 @@ bool Safe(std::string const& key){return !key.empty();}
namespace Trinity {bool IsValidMapCoord(float,float,float,float=0){return true;}}
'''
helpers = '\n'.join(function(source, marker) for marker in [
' float Distance(', ' bool SameService(', ' bool LoadNode(', ' Position WorldPoint(', ' float PathLength('
' char const* PhaseName(', ' float Distance(', ' bool SameService(', ' bool LoadNode(', ' Position WorldPoint(', ' float PathLength('
])
methods = '\n'.join(function(source, marker) for marker in [
'bool ClientlessTransportMgr::ResumePassenger(', 'bool ClientlessTransportMgr::SelectResumedDock(',
@@ -190,7 +190,7 @@ int main(){
go.x=50;p.x=51;mgr.Cancel(26);run(mgr,p,abort);assert(abort.Status.Phase==Phase::Riding&&p.transport);
go.x=5;p.x=6;run(mgr,p,abort);assert(abort.Status.Phase==Phase::Failed&&abort.Status.Cancelled&&!p.transport&&p.x==0);
// A timeout aboard never removes the passenger over sea/in midair.
assert(mgr.Start(&p,"A","B",error));auto&timeout=mgr._rides.at(26);run(mgr,p,timeout);assert(p.transport);timeout.Elapsed=1800001;run(mgr,p,timeout,1);assert(timeout.Status.Phase==Phase::Failed&&p.transport);go.base.RemovePassenger(&p);
assert(mgr.Start(&p,"A","B",error));auto&timeout=mgr._rides.at(26);run(mgr,p,timeout);assert(p.transport);timeout.Elapsed=1800001;run(mgr,p,timeout,1);assert(timeout.Status.Phase==Phase::Riding&&timeout.Abort&&timeout.RetryDelay==3000&&p.transport);go.base.RemovePassenger(&p);
// Departure during a boarding transition keeps the Passenger until safe recovery.
p.x=0;go.x=5;go.state.Stopped=true;assert(mgr.Start(&p,"A","B",error));auto&late=mgr._rides.at(26);
for(int i=0;i<10&&!p.transport;i++)mgr.UpdateRide(&p,late,100);
@@ -280,6 +280,16 @@ int main(){
copy1.base.RemovePassenger(&orphan);orphan.map=0;orphan.object=&vessel;vessel.x=100;
vessel.base.AddPassenger(&orphan);orphan.x=101;orphan.m_movementInfo.transport.pos.x=1;
run(resumed,orphan,resumed._rides.at(26));assert(!orphan.transport&&orphan.map==0&&orphan.x==104);
// Lost attachment during both boarding and disembarkation uses recorded departure rescue.
travel.path=false;travel.present=false;
for(auto phase:{Phase::Boarding,Phase::Leaving}){
Player fallen;fallen.object=&vessel;fallen.x=50;fallen.falling=true;
auto ride=adopted;ride.From=stop("A",5,0);ride.To=stop("B",100,104);
ride.Status.Phase=phase;ride.Status.Recovering=false;ride.Abort=false;ride.Elapsed=0;ride.RetryDelay=0;
ride.Point=phase==Phase::Boarding?3:2;ride.Moving=false;
resumed.UpdateRide(&fallen,ride,100);
assert(ride.Status.Recovering&&fallen.teleports==1&&fallen.x==0);
}
std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n";
}
'''
@@ -287,5 +297,5 @@ with tempfile.TemporaryDirectory(prefix='vcore-transport-test-') as directory:
path = Path(directory)
cpp = path / 'transport.cpp'
cpp.write_text(preamble + position + '\n' + offset + world + header + helpers + stubs + methods + animated + tests)
subprocess.run(['g++', '-std=c++17', '-O0', '-g', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(path / 'test')], check=True)
subprocess.run(['g++','-include',str(Path(__file__).with_name('telemetry_test_stub.h')), '-std=c++17', '-O0', '-g', '-Wall', '-Wextra', '-Wno-unused-parameter', str(cpp), '-o', str(path / 'test')], check=True)
subprocess.run([str(path / 'test')], check=True)
@@ -74,7 +74,16 @@ int main(){Player p;Position anchor;
assert(launches==2&&Distance(first,Passengers[2].Target)>=1.5f);
// Near departure, a running walk is stopped on the deck.
object.dock.RemainingPause=1000;assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,false,5000));assert(!p.spline);
object.entry=999;assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
// Ships without a mesh still recover interrupted exits at a real stop.
object.entry=190536;p.current={9,0,0};auto unsupportedBefore=fallback;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(fallback==unsupportedBefore);
object.dock.RemainingPause=60000;object.dock.Stopped=false;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));assert(fallback==unsupportedBefore);
object.dock.Stopped=true;assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
assert(fallback==unsupportedBefore+1&&p.current[0]==0&&p.transport==&object);
assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000)&&fallback==unsupportedBefore+1);
object.entry=186238;p.current={9,0,0};assert(ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000));
assert(fallback==unsupportedBefore+2);
Passengers.clear();object.entry=176310;object.dock.RemainingPause=60000;
p.current={4,0,0};p.spline=false;launchOK=false;auto before=fallback;
assert(!ClientlessTransportDeckMgr::Update(&p,&object,anchor,true,5000)&&Passengers[1].ReturnFailed);
+48
View File
@@ -0,0 +1,48 @@
"""Run the real temporary trace lifecycle and its per-bot log budget."""
from pathlib import Path
import subprocess,tempfile,re
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessTravelTelemetry.h').read_text()
assert 'Remove before development completion' in s
s=re.sub(r'#include "[^\n]+"\n','',s)
code=r'''
#include <string>
#include <cstdint>
#include <ctime>
using uint64=uint64_t;using uint32=unsigned;
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
unsigned logs=0;
#define TC_LOG_INFO(...) (++logs)
constexpr int SKILL_RIDING=1;
struct Guid{unsigned id;unsigned GetCounter()const{return id;}};
struct Group{Guid GetGUID()const{return {1};}Guid GetLeaderGUID()const{return {26};}};
struct Player{unsigned guid=26;Guid GetGUID()const{return {guid};}Group*GetGroup()const{return nullptr;}
unsigned GetMapId()const{return 0;}float GetPositionX()const{return 1;}float GetPositionY()const{return 2;}float GetPositionZ()const{return 3;}
unsigned getLevel()const{return 60;}unsigned GetAreaId()const{return 12;}unsigned GetZoneId()const{return 12;}
bool IsAlive()const{return true;}bool IsOutdoors()const{return true;}bool IsInWater()const{return false;}bool IsFalling()const{return false;}
bool IsInCombat()const{return false;}bool IsInFlight()const{return false;}void*GetTransport()const{return nullptr;}
bool IsBeingTeleported()const{return false;}bool IsMounted()const{return false;}bool CanFly()const{return false;}
unsigned GetSkillValue(int)const{return 225;}bool HasSpell(int)const{return true;}};
'''+s+r'''
#include <cassert>
int main(){using namespace ClientlessTravelTelemetry;Player p;
Note(&p,"route","no journey");assert(logs==0&&Traces().empty());
Begin(&p,"adventure","Elwynn",200);assert(logs==0&&Traces().empty());
Begin(&p,"instance","Deadmines",200);assert(logs==1);
auto id=Traces().at(26).Id;Begin(&p,"instance","Deadmines",200);assert(logs==1&&Traces().at(26).Id==id);
Note(&p,"route","same");Note(&p,"route","same");assert(logs==2);
for(unsigned i=0;i<100;++i)Note(&p,"route",std::to_string(i));assert(logs==40);
for(unsigned i=0;i<100;++i)Begin(&p,"raid",std::to_string(i),200);assert(logs==40);
GameTime::now+=60;Note(&p,"route","new minute");assert(logs==41);
Tick(&p);assert(logs==42);Tick(&p);assert(logs==42);
GameTime::now+=20;Tick(&p); // unchanged full state is deduplicated, too
assert(logs==42);
Begin(&p,"rest","inn",300);assert(Traces().empty());
Begin(&p,"raid","Karazhan",400);assert(!Traces().empty());Clear(&p);assert(Traces().empty());
}
'''
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('Temporary travel traces: instance/raid scope, stable journey ID, deduplication, 40/min budget across goal changes and logout cleanup.')