Fix AdventureBot travel recovery, area eligibility and safe build logout
This commit is contained in:
1 parent
60cab950bd
commit
649915e575
46 files changed
+1500
-77
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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 ↦}
|
||||
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 ↦}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.')
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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);
|
||||
|
||||
@@ -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.')
|
||||
Reference in new issue
Block a user