Files
Viano-Core/tests/clientless/test_adventure_instance.py
T
Nico 649915e575
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Waiting to run
Fix AdventureBot travel recovery, area eligibility and safe build logout
2026-10-30 12:43:22 +01:00

126 lines
9.3 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 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);
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.')