Files
Viano-Core/tests/clientless/test_adventure_instance.py
T
Nico 3aa780662e Complete AdventureBot navigation, instance travel and shutdown fixes
Finish capital waiting travel and adaptive flight entry up to 10,000 yards.
Preserve checked vertical climbs, recover blocked ground departures and find
validated dry landings. Retain group routes and Undercity arrival constraints.

Restore defense from native combat references, coordinate instance rendezvous
and browser reentry, finalize clientless sessions before map destruction and
transfer Halaa capture-point ownership safely.

Include the associated regression tests, investigation reports and reusable
diagnostic scripts. Keep generated runtime evidence and server binaries local
through scoped ignore rules. Validation/build/live evidence is recorded in the
session checkpoint and individual reports.
2026-11-05 10:35:34 +01:00

129 lines
9.6 KiB
Python

"""Execute production instance phases with native-transfer/persistence adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
start=s.index('bool AdventureBehaviorMgr::UpdateInstance(')
code=r'''
#include "AdventureInstancePolicy.h"
#include <cassert>
#include <string>
#include <vector>
#include <cstdint>
#include <cmath>
using uint32=uint32_t;using uint8=uint8_t;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_WARN(...) ((void)0)
constexpr int UNIT_STAND_STATE_STAND=0,UNIT_STAND_STATE_SIT=1,EMOTE_ONESHOT_KNEEL=16,CMSG_AREATRIGGER=1;
struct Position{float x=0,y=0,z=0,o=0;Position()=default;Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}};
struct Trigger{uint32 ContinentID=0;struct{float X=0,Y=0,Z=0;}Pos;};
struct Target{uint32 target_mapId=36;float target_X=0,target_Y=0,target_Z=0,target_Orientation=0;};
struct Triggers{Trigger entrance,exit;Triggers(){exit.ContinentID=36;exit.Pos.X=100;}Trigger* LookupEntry(uint32 id){return id==78?&entrance:id==121?&exit:nullptr;}}sAreaTriggerStore;
struct Targets{Target entrance,exit;Targets(){exit.target_mapId=0;exit.target_X=500;}Target* GetAreaTrigger(uint32 id){return id==78?&entrance:id==121?&exit:nullptr;}}targets;auto sObjectMgr=&targets;
struct MapEntry{bool IsRaid(){return false;}}mapEntry;
struct Maps{auto LookupEntry(uint32){return &mapEntry;}}sMapStore;
struct WorldPacket{uint32 trigger=0;WorldPacket(int,int){}WorldPacket& operator<<(uint32 id){trigger=id;return *this;}};
struct Save{uint32 GetInstanceId(){return 7;}unsigned GetPlayerCount(){return 1;}unsigned GetGroupCount(){return 0;}}save;
struct Bind{Save* save=nullptr;};
using Difficulty=unsigned;constexpr int MSG_SET_RAID_DIFFICULTY=2,MSG_SET_DUNGEON_DIFFICULTY=3;
struct Group{unsigned GetLeaderGUID(){return 0;}Difficulty GetDifficulty(bool){return 0;}}group;
struct Session;
struct Player:Position{
uint32 map=0,instance=0,health=100,maximum=100;int stand=0,damages=0,entryCalls=0,exitCalls=0,teleports=0;
bool safe=true,group=false,deny=false;Bind bind;Session* session=nullptr;
struct Guid{uint32 GetCounter(){return 26;}operator unsigned()const{return 26;}};Guid GetGUID(){return {};}
uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}bool IsInWorld(){return true;}
bool IsBeingTeleported(){return false;}Group* GetGroup(){return group?&::group:nullptr;}
Bind* GetBoundInstance(uint32,int){return bind.save?&bind:nullptr;}int GetDifficulty(bool){return 0;}
bool IsInAreaTriggerRadius(Trigger* t){return map==t->ContinentID&&std::fabs(x-t->Pos.X)<3;}
float GetAngle(float,float){return 0;}void SetFacingTo(float){}void HandleEmoteCommand(int){}
void SetStandState(int v){stand=v;}uint32 GetHealth(){return health;}uint32 GetMaxHealth(){return maximum;}
void SetHealth(uint32 h){health=h;++damages;}Session* GetSession(){return session;}
float GetExactDist(float a,float b,float c){return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
float GetExactDist(Position const& p){return GetExactDist(p.x,p.y,p.z);}
bool TeleportTo(uint32 m,float a,float b,float c,float o){++teleports;map=m;instance=m==36?7:0;x=a;y=b;z=c;this->o=o;return true;}
};
namespace ObjectAccessor{Player*FindPlayer(unsigned){return nullptr;}}
struct Session{bool IsClientless(){return true;}void HandleSetRaidDifficultyOpcode(WorldPacket&){}void HandleSetDungeonDifficultyOpcode(WorldPacket&){}Player* p;void CompleteClientlessTeleport(){}void HandleAreaTriggerOpcode(WorldPacket const& packet){
if(packet.trigger==78){++p->entryCalls;if(!p->deny){p->map=36;p->instance=7;p->x=0;}}
else{++p->exitCalls;if(!p->deny){p->map=0;p->instance=0;p->x=500;}}
}};
bool Unsafe(Player* p){return !p->safe;}
struct Database{bool shared=false;template<class... T>bool PQuery(char const*,T...){return shared;}}CharacterDatabase;
struct Manager{void ClearQueuedActions(uint32){}}manager;auto sClientlessPlayerMgr=&manager;
struct Executor{void Clear(uint32){}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Travel{void CancelTravel(uint32){}}travel;auto sClientlessTravelMgr=&travel;
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
struct GroupTravel{unsigned follows=0;bool Follow(Player*){++follows;return true;}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
struct AdventureBehaviorMgr{
enum class Phase{Idle,Travel,OnSite};
struct Record{AdventureInstancePolicy::Visit Instance;Phase State=Phase::OnSite;bool Dirty=false;std::string Detail;};
int saves=0,finishes=0;bool saveOK=true;
bool SaveInstance(Player*,Record&){++saves;return saveOK;}
void FinishInstance(Player* p,Record&r){++finishes;r.Instance={};p->SetStandState(0);}
bool UpdateInstance(Player*,Record&,time_t);
};
'''+s[start:s.index("void AdventureBehaviorMgr::InterruptForCombat(")]+r'''
using Stage=AdventureInstancePolicy::Phase;
AdventureBehaviorMgr::Record fresh(){AdventureBehaviorMgr::Record r;r.Instance.Active=true;r.Instance.Entry=78;r.Instance.Exit=121;r.Instance.Map=36;r.Instance.BlockEnd=3600;r.Instance.LossPercent=50;return r;}
int main(){
assert(AdventureInstancePolicy::ReturnAt(3600)==3300);
assert(AdventureInstancePolicy::MayEnter(3299,3600)&&!AdventureInstancePolicy::MayEnter(3300,3600));
assert(AdventureInstancePolicy::ReturnHealth(100,100,0)==100);
assert(AdventureInstancePolicy::ReturnHealth(100,100,50)==50);
assert(AdventureInstancePolicy::ReturnHealth(40,100,50)==40); // no healing
assert(AdventureInstancePolicy::ReturnHealth(UINT32_MAX,UINT32_MAX,50)==2147483647u);
AdventureBehaviorMgr mgr;auto r=fresh();Player p;Session session{&p};p.session=&session;
// 40 minutes' travel leaves 15 minutes inside and 5 outside.
assert(mgr.UpdateInstance(&p,r,2400)&&r.Instance.Stage==Stage::Preparing&&p.entryCalls==0);
assert(mgr.UpdateInstance(&p,r,2409)&&p.entryCalls==0);
assert(mgr.UpdateInstance(&p,r,2410)&&p.entryCalls==1&&p.map==36);
assert(mgr.UpdateInstance(&p,r,2411)&&r.Instance.Stage==Stage::Inside&&r.Instance.InstanceId==7);
// A bot-led instance visitor uses local group follow once inside.
p.group=true;assert(mgr.UpdateInstance(&p,r,2412)&&groupTravel.follows==1&&p.exitCalls==0);p.group=false;
int transfers=p.teleports;
assert(mgr.UpdateInstance(&p,r,3299)&&p.teleports==transfers&&p.exitCalls==0);
assert(mgr.UpdateInstance(&p,r,3300)&&r.Instance.Stage==Stage::Rest&&p.map==0&&p.x==500);
assert(p.exitCalls==1&&p.health==50&&p.damages==1&&r.Instance.InjuryApplied);
assert(mgr.UpdateInstance(&p,r,3400)&&p.damages==1);
assert(!mgr.UpdateInstance(&p,r,3600)&&mgr.finishes==1&&!r.Instance.Active);
// Persisted inside state on restart keeps the absolute return time.
r=fresh();r.Instance.Stage=Stage::Inside;r.Instance.InstanceId=7;p.map=36;p.instance=7;p.x=0;
assert(mgr.UpdateInstance(&p,r,3299)&&p.map==36);
assert(!mgr.UpdateInstance(&p,r,3700)&&p.map==0&&!r.Instance.Active);
// If native login ejected an expired copy, return to its associated exit.
r=fresh();r.Instance.Stage=Stage::Inside;r.Instance.InstanceId=7;p.map=0;p.x=999;
assert(mgr.UpdateInstance(&p,r,3400)&&p.x==500&&r.Instance.InjuryApplied);
// Replaying a saved return cannot compound damage.
r=fresh();r.Instance.Stage=Stage::Returning;r.Instance.InjuryApplied=true;p.x=500;
int damages=p.damages;assert(mgr.UpdateInstance(&p,r,3400)&&p.damages==damages);
// A failed durable entering save must not start a portal transfer.
r=fresh();r.Instance.Stage=Stage::Entering;p.map=0;p.x=0;mgr.saveOK=false;
int prevented=p.entryCalls;assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==prevented);mgr.saveOK=true;
// A denied entrance is not a visit; no exit or damage is invented.
r=fresh();r.Instance.Stage=Stage::Entering;p.map=0;p.x=0;p.deny=true;damages=p.damages;
assert(mgr.UpdateInstance(&p,r,100)&&r.Instance.Stage==Stage::Rest&&p.damages==damages);
p.deny=false;
// Late arrival never enters, nor applies a fake injury.
r=fresh();r.State=AdventureBehaviorMgr::Phase::Travel;int calls=p.entryCalls;
assert(mgr.UpdateInstance(&p,r,3350)&&r.Instance.Stage==Stage::Rest&&p.entryCalls==calls&&p.damages==damages);
// Native groups may enter their group copy; unrelated solo shared binds remain rejected.
r=fresh();r.Instance.Stage=Stage::Entering;p.group=true;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls+1);++calls;p.group=false;p.map=0;p.instance=0;p.x=0;
r=fresh();r.Instance.Stage=Stage::Entering;p.bind.save=&save;CharacterDatabase.shared=true;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);CharacterDatabase.shared=false;p.bind.save=nullptr;
// A denied exit remains pending and cannot wound a player still inside.
r=fresh();r.Instance.Stage=Stage::Returning;p.map=36;p.instance=7;p.x=0;p.deny=true;
assert(mgr.UpdateInstance(&p,r,3400)&&r.Instance.Stage==Stage::Returning&&p.damages==damages);
p.deny=false;
// With no separate native exit, use the original entrance location.
r=fresh();r.Instance.Stage=Stage::Returning;r.Instance.Exit=0;p.map=36;p.instance=7;p.x=50;
assert(mgr.UpdateInstance(&p,r,3400)&&p.map==0&&p.x==0);
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'instance.cpp';exe=Path(tmp)/'instance';src.write_text(code)
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.')