Files
Viano-Core/tests/clientless/test_portal_use.py
T
Nico 4474714c00
GCC PCH / build (push) Canceled after 0s
clang PCH / build (push) Canceled after 0s
Support interactive object portals and document travel route recording
Bind directed portal_use edges to native GameObject spawns and spell destinations. Execute normal clicks and verify completed clientless teleports, with regression coverage. Document live portal tests, mage tower calibration, Badlands portals and transport recording workflows.
2026-10-26 14:01:42 +01:00

219 lines
12 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()
a = source.index('bool ClientlessActionExecutorMgr::StartPortalUse(')
b = source.index('bool ClientlessActionExecutorMgr::StartRecordedEdgePath(', a)
methods = source[a:b]
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, 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;} 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;}
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 Graph {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;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++', '-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>
using uint8=uint8_t;using uint32=uint32_t;
constexpr uint32 MAX_SPELL_EFFECTS=3,GAMEOBJECT_TYPE_SPELLCASTER=22,SPELL_EFFECT_TELEPORT_UNITS=5,TARGET_DEST_DB=17;
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;}spellcaster;} 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;
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;auto GetSpellInfo(uint32){return hasSpell?&spell: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;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=10;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));
}
'''
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([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;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=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++', '-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.')