Files
Viano-Core/tests/clientless/test_portal_use.py
T
Nico 3c361e435a
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Improve AdventureBot instance and flight navigation
2026-11-01 17:00:59 +01:00

261 lines
16 KiB
Python

"""Compile the production object-portal start/update methods against Core 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()
def production_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]
methods = '\n'.join(production_method(signature) for signature in (
'bool ClientlessActionExecutorMgr::StartPortalUse(',
'void ClientlessActionExecutorMgr::UpdatePortalUse(',
))
harness = r'''
#include <algorithm>
#include <cassert>
#include <cmath>
#include <map>
#include <string>
#include <vector>
#include <cstdint>
using uint32=uint32_t; using uint64=uint64_t;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_WARN(...) ((void)0)
constexpr int GAMEOBJECT_FLAGS=0, GO_FLAG_NOT_SELECTABLE=16, GO_FLAG_LOCKED=2;
constexpr int GAMEOBJECT_TYPE_SPELLCASTER=22, GAMEOBJECT_TYPE_GOOBER=10, CMSG_GAMEOBJ_USE=1;
struct WorldPacket { int guid=0; WorldPacket(int,int){} void operator<<(int v){guid=v;} };
struct GameObject {
int guid=83,entry=183325,flags=0,type=22;
bool spawned=true;
struct {struct {int spell=17334;} spellcaster;struct{int spell=17334;}goober;} info;
int GetEntry(){return entry;} int GetGUID(){return guid;}
bool isSpawned(){return spawned;} bool HasFlag(int,int f){return flags&f;}
int GetGoType(){return type;} auto GetGOInfo(){return &info;}
};
struct Map {
std::multimap<uint32,GameObject*> objects;
auto const& GetGameObjectBySpawnIdStore(){return objects;}
};
struct Player;
struct Session {
Player* player;
bool clientless=true,accept=true,ackWorks=true,far=true,wrongTarget=false;
int clicks=0,acks=0; uint64 sequence=0;
Player* GetPlayer(){return player;}
bool IsClientless(){return clientless;}
uint64 GetClientlessTeleportSequence(){return sequence;}
void HandleGameObjectUseOpcode(WorldPacket&);
void CompleteClientlessTeleport();
};
using WorldSession=Session;
struct Player {
int mapId=530;float x=-1894,y=5387,z=-12;
bool alive=true,pending=false,world=true,phase=true,visible=true,inRange=true,aboard=false;
Map map;Session session{this};GameObject go;
Player(){map.objects.emplace(83,&go);}
int GetMapId(){return mapId;}bool IsAlive(){return alive;}
bool IsBeingTeleported(){return pending;}bool IsInWorld(){return world;}
void* GetTransport(){return aboard?this:nullptr;}
Session* GetSession(){return &session;}Map* GetMap(){return &map;}
bool IsInPhase(GameObject*){return phase;}bool CanSeeOrDetect(GameObject*){return visible;}
GameObject* GetGameObjectIfCanInteractWith(int guid){return inRange&&guid==go.guid?&go:nullptr;}
float GetPositionX(){return x;}float GetPositionY(){return y;}float GetPositionZ(){return z;}
unsigned getLevel(){return 85;}
bool TeleportTo(uint32 map,float tx,float ty,float tz,float){pending=true;world=false;mapId=map;x=tx;y=ty;z=tz;return true;}
std::string GetName(){return "Arthoran";}void StopMoving(){}
};
void Session::HandleGameObjectUseOpcode(WorldPacket& packet) {
++clicks;assert(packet.guid==83);
if(accept){player->pending=true;if(far)player->world=false;}
}
void Session::CompleteClientlessTeleport(){
++acks;if(!ackWorks||!player->pending)return;
player->pending=false;player->world=true;++sequence;
player->mapId=wrongTarget?1:0;player->x=-8998;player->y=861;player->z=29;
}
struct Node {std::string Key;uint32 MapId=530;};
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;struct{uint32 MapId=0;float X=-8998,Y=861,Z=29,O=0;}Target;};
struct Graph {bool nativePrerequisites=true,adventureUnlock=false;
bool PortalNativePrerequisites(Player*,uint32){return nativePrerequisites;}
bool AdventurePortalUnlockAllowed(Player*,uint32){return adventureUnlock;}
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;
enum class ClientlessActionExecutionState {Moving,Failed,Completed};
struct Step {uint32 EdgeDatabaseId=9,MapId=0;std::string NodeKey="portal_go_83_target";bool EdgeReverse=false;float X=-8998,Y=861,Z=29;};
struct Record {
std::vector<Step> Route{{},{}};size_t RouteStep=1;
bool PortalUseActive=false,Active=true;uint64 PortalUseTeleportSequence=0;
uint32 PortalUseElapsedMs=0,PortalUseSpawnId=0,RecoveryCount=0;
std::vector<int> EdgePath;
struct {ClientlessActionExecutionState State=ClientlessActionExecutionState::Moving;std::string TargetName,Detail;} Status;
};
struct ClientlessActionExecutorMgr {
int continued=0;
bool StartPortalUse(Player*,uint32,Record&);
void UpdatePortalUse(Player*,uint32,Record&);
bool StartRouteStep(Player*,uint32,Record& r){++continued;r.Active=false;r.Status.State=ClientlessActionExecutionState::Completed;return true;}
};
'''
harness += methods
harness += r'''
int main(){
ClientlessActionExecutorMgr mgr;
{Player p;Record r;graph.nativePrerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.nativePrerequisites=true;}
{Player p;Record r;graph.nativePrerequisites=false;graph.adventureUnlock=true;
assert(mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0&&p.pending);
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2&&p.session.sequence==1);
graph.nativePrerequisites=true;graph.adventureUnlock=false;}
{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.
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));p.session.CompleteClientlessTeleport();
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2&&p.session.acks==1);}
{Player p;p.session.far=false;Record r;assert(mgr.StartPortalUse(&p,26,r));
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2);}
// Denied native click/cast, even when already standing at the expected target.
{Player p;p.session.accept=false;p.mapId=530;p.x=-8998;p.y=861;p.z=29;
Record r;r.Route[1].MapId=530;assert(mgr.StartPortalUse(&p,26,r));
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==1&&r.Active);
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&r.RouteStep==1);}
{Player p;p.session.ackWorks=false;Record r;assert(mgr.StartPortalUse(&p,26,r));
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&p.pending&&r.RouteStep==1);}
{Player p;p.session.wrongTarget=true;Record r;assert(mgr.StartPortalUse(&p,26,r));
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&r.RouteStep==1);}
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));p.session.CompleteClientlessTeleport();p.x+=6;
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&r.RouteStep==1);}
for(int kind=0;kind<12;++kind){
Player p;Record r;graph.available=true;
switch(kind){case 0:graph.available=false;break;case 1:r.RouteStep=0;break;
case 2:r.Route[1].EdgeReverse=true;break;case 3:p.mapId=1;break;
case 4:p.alive=false;break;case 5:p.pending=true;break;case 6:p.aboard=true;break;
case 7:p.session.clientless=false;break;case 8:p.map.objects.clear();break;
case 9:p.phase=false;break;case 10:p.inRange=false;break;case 11:p.go.flags=GO_FLAG_NOT_SELECTABLE;break;}
assert(!mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==0&&!r.Active);
}
graph.available=true;
{Player p;p.go.info.spellcaster.spell=1;Record r;assert(!mgr.StartPortalUse(&p,26,r));assert(!p.session.clicks);}
}
'''
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++','-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.')
# Exercise native destination discovery rather than duplicating the importer.
graph_source = (ROOT / 'src/server/game/World/ClientlessTravelGraphMgr.cpp').read_text()
a = graph_source.index('bool ClientlessTravelGraphMgr::ResolvePortalUse(')
b = graph_source.index('ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse', a)
definition_method = graph_source[a:b]
definition_harness = r'''
#include <cassert>
#include <string>
#include <cstdint>
#include <vector>
#include <map>
#include <unordered_set>
using uint8=uint8_t;using uint32=uint32_t;
constexpr uint32 MAX_SPELL_EFFECTS=3,GAMEOBJECT_TYPE_SPELLCASTER=22,GAMEOBJECT_TYPE_GOOBER=10,SPELL_EFFECT_TELEPORT_UNITS=5,SPELL_EFFECT_SCRIPT_EFFECT=77,TARGET_DEST_DB=17;
constexpr int SCRIPT_COMMAND_CAST_SPELL=15,SCRIPT_COMMAND_TELEPORT_TO=6,SF_CASTSPELL_TARGET_TO_TARGET=2;
struct ScriptInfo{int command=15;struct{unsigned SpellID=25649,Flags=2;int CreatureEntry=0;}CastSpell;struct{unsigned MapID=571,Flags=0;float DestX=1,DestY=2,DestZ=3,Orientation=0;}TeleportTo;};
std::map<unsigned,std::multimap<unsigned,ScriptInfo>> sSpellScripts;
using SpellEffIndex=uint8;
struct Position {float GetPositionX()const{return -1894;}float GetPositionY()const{return 5387;}float GetPositionZ()const{return -12;}float GetOrientation()const{return 0;}};
struct Spawn {uint32 id=183325,mapId=530;Position spawnPoint;} spawn;
struct Info {uint32 type=22;struct{uint32 spell=17334,playerCast=1;}spellcaster;struct{uint32 spell=35376,playerCast=0,open=0,questID=0,eventID=0,pageID=0,gossipID=0,linkedTrap=0,consumable=0;}goober;} info;
struct ObjectMgr {bool hasSpawn=true;auto GetGameObjectData(uint32){return hasSpawn?&spawn:nullptr;}auto GetGameObjectTemplate(uint32){return &info;}} objects;
struct Target{uint32 id=0;uint32 GetTarget()const{return id;}};
struct Effect{uint32 Effect=0;Target TargetA,TargetB;};
struct SpellInfo{uint32 Id=17334;Effect Effects[3];} spell,child;
struct SpellTargetPosition {uint32 target_mapId=0;float target_X=-8998,target_Y=861,target_Z=29,target_Orientation=2;} destination;
struct SpellMgr {bool hasSpell=true,hasTarget=true;SpellInfo* GetSpellInfo(uint32 id){return hasSpell?(id==spell.Id?&spell:id==child.Id?&child:nullptr):nullptr;}auto GetSpellTargetPosition(uint32,SpellEffIndex){return hasTarget?&destination:nullptr;}} spells;
auto sObjectMgr=&objects;auto sSpellMgr=&spells;
struct Node {std::string Key,Type,WorldPointKey;uint32 MapId=0;float X=0,Y=0,Z=0,O=0;};
struct ClientlessPortalUse {uint32 SpawnId=0,Entry=0,SpellId=0;std::vector<std::pair<uint32,uint8>> Requirements;Node Source,Target;};
struct ClientlessTravelGraphMgr{static bool ResolvePortalUse(uint32,ClientlessPortalUse&,std::string&);};
'''
definition_harness += definition_method
definition_harness += r'''
int main(){
spell.Effects[0].Effect=5;spell.Effects[0].TargetB.id=17;
ClientlessPortalUse p;std::string error;
assert(ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
assert(p.SpawnId==83&&p.Entry==183325&&p.SpellId==17334&&p.Source.MapId==530&&p.Target.MapId==0);
assert(p.Source.Key=="portal_go_83_source"&&p.Target.Key=="portal_go_83_target"&&p.Target.X==-8998);
objects.hasSpawn=false;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));objects.hasSpawn=true;
info.type=7;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));info.type=22;
spells.hasSpell=false;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spells.hasSpell=true;
spells.hasTarget=false;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spells.hasTarget=true;
spell.Effects[0].TargetB.id=0;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spell.Effects[0].TargetB.id=17;
spell.Effects[1].Effect=5;spell.Effects[1].TargetB.id=17;
assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spell.Effects[1]={};
spell.Effects[0].Effect=77;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
spell.Effects[1]={};spell.Id=35376;spell.Effects[0]={};spell.Effects[0].Effect=77;
info.type=10;child.Id=25649;child.Effects[0].Effect=5;child.Effects[0].TargetB.id=17;
sSpellScripts[35376].emplace(0,ScriptInfo{});
assert(ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
assert(p.SpellId==35376&&p.Requirements.size()==2&&p.Requirements[0].second==2&&p.Requirements[1].second==3);
info.goober.questID=1;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));info.goober.questID=0;
sSpellScripts[35376].begin()->second.CastSpell.Flags=0;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
sSpellScripts[35376].begin()->second.CastSpell.Flags=2;
sSpellScripts[35376].emplace(0,ScriptInfo{});assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
sSpellScripts[35376].clear();ScriptInfo direct;direct.command=6;sSpellScripts[35376].emplace(1,direct);
assert(ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error)&&p.Target.MapId==571&&p.Target.Z==3);
sSpellScripts[35376].clear();sSpellScripts[35376].emplace(4,direct);assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
sSpellScripts[35376].clear();ScriptInfo cycle;cycle.CastSpell.SpellID=35376;sSpellScripts[35376].emplace(0,cycle);
assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
}
'''
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++','-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.')
movement_source = (ROOT / 'src/server/game/Handlers/MovementHandler.cpp').read_text()
a = movement_source.index('void WorldSession::CompleteClientlessTeleport()')
b = movement_source.index('void WorldSession::CompleteNearTeleport(', a)
ack_harness = r'''
#include <cassert>
#include <cstdint>
struct Player {bool pending=false,far=false,world=true,delayed=false;bool IsHasDelayedTeleport(){return delayed;}bool IsBeingTeleported(){return pending;}bool IsBeingTeleportedFar(){return pending&&far;}bool IsBeingTeleportedNear(){return pending&&!far;}bool IsInWorld(){return world;}};
struct WorldSession {
Player* player;bool clientless=true,works=true;int near=0,far=0;uint64_t _clientlessTeleportSequence=0;
bool IsClientless(){return clientless;}Player* GetPlayer(){return player;}
void HandleMoveWorldportAck(){++far;if(works){player->pending=false;player->world=true;}}
void CompleteNearTeleport(Player* p){++near;if(works)p->pending=false;}
void CompleteClientlessTeleport();
};
'''
ack_harness += movement_source[a:b]
ack_harness += r'''
int main(){
Player p;WorldSession s{&p};s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==0);
p.pending=p.delayed=true;s.CompleteClientlessTeleport();assert(s.near==0&&s._clientlessTeleportSequence==0);p.delayed=false;
p.pending=true;s.CompleteClientlessTeleport();assert(s.near==1&&s._clientlessTeleportSequence==1);
s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==1);
p.pending=p.far=true;p.world=false;s.CompleteClientlessTeleport();assert(s.far==1&&s._clientlessTeleportSequence==2);
p.pending=true;s.works=false;s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==2&&p.pending);
s.clientless=false;s.works=true;s.CompleteClientlessTeleport();assert(p.pending&&s._clientlessTeleportSequence==2);
}
'''
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++','-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.')