Files
Viano-Core/tests/clientless/test_adventure_social.py
T
Nico 54d77d1e4d
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Fix AdventureBot group preparation, public portals and flight recovery
Add preparation outings and equipment checks with a five-minute return to the meeting point. Correct static portal visitor casting and level conditions, with native interaction diagnostics. Recover suspended flying mounts, walk to verified takeoff sites, and validate landing and return volumes. Include SQL migrations, targeted regression tests and live validation checkpoints.
2026-10-29 18:21:43 +01:00

251 lines
19 KiB
Python

"""Run production social cues with native-adapter doubles and the real sequence clock."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureSocialActions.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);depth=1;c=b+1
while depth:
depth+=(source[c]=='{')-(source[c]=='}');c+=1
return source[a:c]
code=r'''
#include "AdventureSocialFlow.h"
#include "AdventureEconomyPolicy.h"
#include "AdventureExplorationPolicy.h"
#include <functional>
#include <deque>
#include <algorithm>
#include "AdventureProfessionPolicy.h"
#include <cassert>
#include <string>
#include <vector>
#include <cstdint>
using uint32=uint32_t;
using namespace AdventureSocialFlow;
#define TC_LOG_INFO(...) ((void)0)
constexpr float INVALID_HEIGHT=-100000;
constexpr int EMOTE_STATE_USE_STANDING=69,EMOTE_STATE_WORK_MINING=233,EMOTE_STATE_WORK_CHOPWOOD=234,EMOTE_STATE_KNEEL=68;
constexpr int CookingFire=818,UNIT_NPC_EMOTESTATE=0,UNIT_STAND_STATE_SIT=1,UNIT_STAND_STATE_SIT_LOW_CHAIR=4,CMSG_GAMEOBJ_USE=1;
constexpr int EMOTE_ONESHOT_QUESTION=6,EMOTE_ONESHOT_POINT=25,EMOTE_ONESHOT_USE_STANDING=432;
constexpr int EMOTE_ONESHOT_WAVE=3,EMOTE_ONESHOT_TALK=1,EMOTE_ONESHOT_YES=273,EMOTE_ONESHOT_CHEER=4,EMOTE_STATE_DANCE=10;
constexpr int SPELL_VISUAL_KIT_DRINK=438,SPELL_VISUAL_KIT_FOOD=406,GAMEOBJECT_TYPE_CHAIR=7,SPELL_CAST_OK=0;
struct ObjectGuid{int id=0;uint32 GetCounter(){return id;}bool operator==(ObjectGuid const& other)const{return id==other.id;}bool IsEmpty()const{return !id;}std::string ToString()const{return std::to_string(id);}};
struct Unit;struct GameObject;struct Player;
struct WorldObject{
int kind=0;ObjectGuid guid{1};virtual ~WorldObject()=default;
float x=0,y=0,z=0,heading=0;float GetPositionX(){return x;}float GetOrientation(){return heading;}float GetPositionY(){return y;}float GetPositionZ(){return z;}
float GetAngle(WorldObject*p){return std::atan2(p->y-y,p->x-x);}
Unit* ToUnit();GameObject* ToGameObject();ObjectGuid GetGUID(){return guid;}
};
struct Unit:WorldObject{bool service=false;Unit(){kind=1;}};
struct GameObject:WorldObject{
struct Info{float size=1;struct{int triggeredEvent=0;uint32 chairslots=1,chairheight=1;}chair;}info;
GameObject(){kind=2;guid.id=2;}
bool spawned=true;bool isSpawned(){return spawned;}
int GetGoType(){return GAMEOBJECT_TYPE_CHAIR;}Info* GetGOInfo(){return &info;}
};
Unit* WorldObject::ToUnit(){return kind==1?static_cast<Unit*>(this):nullptr;}
GameObject* WorldObject::ToGameObject(){return kind==2?static_cast<GameObject*>(this):nullptr;}
struct WorldPacket{WorldPacket(int,int){}WorldPacket& operator<<(ObjectGuid){return *this;}};
struct Session{int uses=0;void HandleGameObjectUseOpcode(WorldPacket&){++uses;}};
struct Position{float m_positionX=0,m_positionY=0,m_positionZ=0;void Relocate(Player*);float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}};
struct NativeMap{float GetHeight(int,float,float,float,bool,float){return 0;}}nativeMap;
struct Player:Unit{
Player(){x=2;}
int repairs=0;void DurabilityRepairAll(bool cost,float,bool guild){assert(!cost&&!guild);++repairs;}
std::vector<int> emotes;int faces=0,casts=0;bool casting=false,sit=false,los=true;float distance=2;Session session;
ObjectGuid selection;
bool visible=true;bool CanSeeOrDetect(GameObject*){return visible;}
NativeMap* GetMap(){return &nativeMap;}int GetPhaseShift(){return 0;}
bool IsInWater(){return false;}bool IsFalling(){return false;}bool IsBeingTeleported(){return false;}bool IsSitState(){return sit;}bool IsNonMeleeSpellCast(bool){return casting;}
float GetExactDist(Position const&){return distance;}float GetExactDist(WorldObject*){return distance;}void SetFacingToObject(WorldObject*){++faces;}
std::vector<float> headings;void SetFacingTo(float f){heading=f;headings.push_back(f);}int chairStand=5;uint32 GetStandState(){return sit?chairStand:0;}
std::vector<int> states;void SetSelection(ObjectGuid g){selection=g;}void SetUInt32Value(int,int state){states.push_back(state);}
void HandleEmoteCommand(int id){emotes.push_back(id);}void SendPlaySpellVisualKit(int,int,int){}
void SetStandState(int){sit=true;}int CastSpell(Player*,int,bool){++casts;casting=true;return SPELL_CAST_OK;}
bool IsWithinLOSInMap(WorldObject*){return los;}bool IsWithinLOS(float,float,float){return los;}bool GetGameObjectIfCanInteractWith(ObjectGuid){return true;}Session* GetSession(){return &session;}
};
void Position::Relocate(Player*p){m_positionX=p->x;m_positionY=p->y;m_positionZ=p->z;}
struct AdventureSocialContext{bool AllowCreateFire=true;std::string Intent="social";AdventureProfessionPolicy::Professions PrimaryProfessions;Position Anchor;std::deque<std::string> TargetHistory;int departures=0;bool reachable=true;bool Move(Position const&,float){++departures;return reachable;}};
struct AdventureSocialScene{
bool RepairDone=false;
bool Active=true,Approaching=false,Casting=false,WaitingChair=false,Seated=false,Departing=false;
std::string Action="social_npc",Kind="npc",TargetKey;ObjectGuid Target{1},PreviousSelection;Position Destination,Origin;float Facing=0;
uint32 Elapsed=0,CastElapsed=0,CheckTimer=0,Repaths=0,ServiceVisits=0,ServiceDuration=30000;Sequence Sequence;
};
WorldObject* target=nullptr;GameObject* fire=nullptr;bool friendly=true,approach=true;
WorldObject* Resolve(Player*,ObjectGuid g){return g.id?target:nullptr;}
bool InBounds(Player*,WorldObject* t,AdventureSocialContext&){return t!=nullptr;}
bool Friendly(Player*,Unit*,AdventureSocialContext&){return friendly;}
bool ServiceMatches(Unit* unit,std::string const&){return unit->service;}
bool TargetVisible(Player* p,WorldObject* target,std::string const&){return p->IsWithinLOSInMap(target);}
std::vector<Unit*> services;
std::vector<Unit*> People(Player*,std::string const&,AdventureSocialContext&){return services;}
template<class T>T* Pick(Player*,std::vector<T*> const& choices,AdventureSocialContext&){return choices.empty()?nullptr:choices[0];}
void Remember(std::deque<std::string>& history,std::string key){history.push_back(key);}
bool Approach(Player*,AdventureSocialScene& s,WorldObject*,AdventureSocialContext&){s.Approaching=true;return approach;}
GameObject* Fire(Player*,AdventureSocialContext&){return fire;}
bool CanMakeFire(Player*){return true;}
void Say(Player*,std::string const&,AdventureSocialContext&){}
uint32 Reaction(AdventureSocialContext&){return 11;}
enum class ClientlessTravelState{Moving,Arrived,Rejected};
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel;
struct AdventureProfessionTargets{static std::vector<Unit*> Find(Player*,std::string const&,AdventureSocialContext const&){return {};}static bool Valid(Player*,WorldObject*,std::string const&,AdventureSocialContext const&){return true;}};
struct AdventureMicroAction{std::string Requirement;};
class AdventureSocialActions{public:static bool Available(Player*,AdventureMicroAction const&,AdventureSocialContext&);static bool Update(Player*,AdventureSocialScene&,uint32,AdventureSocialContext&);};
'''.replace('Sequence Sequence;', 'AdventureSocialFlow::Sequence Sequence;')
# Compile the actual profession eligibility branch; unrelated object selectors are outside this fixture.
a=source.index('bool AdventureSocialActions::Available(')
b=source.index(' if (AdventureExplorationPolicy::Inspection(kind))',a)
code+=source[a:b]+' return false;\n}\n'
code+=function('bool OnChair(')+'\n'+function('bool AtFront(')+'\n'+function('std::vector < Step > Steps(')+'\n'+function('bool AdventureSocialActions::Update(')
code+=r'''
int main(){
for(float heading:{0.0f,1.5707963f,3.1415926f,4.7123890f,-3.1415926f})
{
for(float offset:{0.0f,0.28f,-0.28f,0.55f,-0.55f})
assert(ConversationFront(10,20,heading,10+2.2f*std::cos(heading+offset),20+2.2f*std::sin(heading+offset)));
assert(!ConversationFront(10,20,heading,10-2.2f*std::cos(heading),20-2.2f*std::sin(heading)));
assert(!ConversationFront(10,20,heading,10+2.2f*std::cos(heading+1.5707963f),20+2.2f*std::sin(heading+1.5707963f)));
}
assert(!ConversationFront(0,0,0,0.2f,0)&&!ConversationFront(0,0,0,8,0));
AdventureSocialContext c;Unit npc;GameObject chair;Player p;AdventureSocialScene s;target=&npc;
s.Sequence.Start(Steps("npc",30000));s.Approaching=true;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.empty());
travel.status.State=ClientlessTravelState::Arrived;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.size()==1&&p.emotes[0]==EMOTE_ONESHOT_WAVE);
bool active=true;for(unsigned i=0;i<60&&active;++i)active=AdventureSocialActions::Update(&p,s,1000,c);
assert(!active);int talks=0,waves=0;for(auto id:p.emotes){talks+=id==1;waves+=id==3;}assert(talks==2&&waves==2);
// Being close behind a partner must reposition, not start talking there.
s={};s.Sequence.Start(Steps("npc",30000));p.x=-2;auto before=p.emotes.size();
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.size()==before&&s.Repaths>0);p.x=2;
// Missing or newly hostile partners terminate the scene without more emotes.
s={};s.Sequence.Start(Steps("npc",30000));target=nullptr;auto count=p.emotes.size();
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.size()==count);
target=&npc;friendly=false;assert(!AdventureSocialActions::Update(&p,s,1000,c));friendly=true;
// Chairs go through the native use handler once and must confirm sitting.
s={};s.Target.id=2;s.Sequence.Start(Steps("seat",3000));target=&chair;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.session.uses==1&&s.WaitingChair);
assert(!AdventureSocialActions::Update(&p,s,1000,c)); // no fabricated successful seat
s={};s.Target.id=2;s.Sequence.Start(Steps("seat",3000));p.sit=true;p.x=0;
assert(AdventureSocialActions::Update(&p,s,1000,c));assert(AdventureSocialActions::Update(&p,s,1000,c)&&s.Seated);
// Native chair animation beside the actual slot is not successful seating.
float seatError=0;
assert(OnChair(&p,&chair,seatError));p.x=2;assert(!OnChair(&p,&chair,seatError));p.x=0;
p.chairStand=UNIT_STAND_STATE_SIT;assert(!OnChair(&p,&chair,seatError));p.chairStand=5;
p.z=1;assert(!OnChair(&p,&chair,seatError));p.z=0;
chair.heading=1.5707963f;assert(!OnChair(&p,&chair,seatError));p.heading=chair.heading;assert(OnChair(&p,&chair,seatError));chair.heading=p.heading=0;
chair.info.chair.chairslots=3;p.y=1;assert(OnChair(&p,&chair,seatError));p.y=0.5f;assert(!OnChair(&p,&chair,seatError));p.y=0;chair.info.chair.chairslots=1;
// Furniture with event triggers must never be clicked by these scenes.
s={};s.Target.id=2;s.Sequence.Start(Steps("seat",3000));chair.info.chair.triggeredEvent=1;int uses=p.session.uses;
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.session.uses==uses);
// Native interaction range alone must not allow seating through a wall.
chair.info.chair.triggeredEvent=0;p.los=false;s={};s.Target.id=2;s.Sequence.Start(Steps("seat",3000));uses=p.session.uses;
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.session.uses==uses);p.los=true;
// Fire cast uses a normal native spell once; absence of an object times out.
s={};s.Target.id=0;s.Sequence.Start(Steps("fire",30000));target=nullptr;fire=nullptr;p.casting=false;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.casts==1&&s.Casting);
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.casts==1);
p.casting=false;active=true;for(unsigned i=0;i<12&&active;++i)active=AdventureSocialActions::Update(&p,s,1000,c);
assert(!active&&p.casts==1);
// A disappeared fire must not be recreated in a rest context that forbids creation.
c.AllowCreateFire=false;s={};s.Target.id=0;s.Sequence.Start(Steps("fire",30000));
auto castCount=p.casts;assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.casts==castCount);
c.AllowCreateFire=true;
// Conversely, a real newly spawned object is confirmed and approached.
s={};s.Target.id=0;s.Sequence.Start(Steps("fire",30000));
assert(AdventureSocialActions::Update(&p,s,1000,c));p.casting=false;fire=&chair;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&s.Target.id==2&&s.Approaching&&!s.Casting);
// Service roles must still match while the scene is running.
target=&npc;fire=nullptr;npc.service=false;p.sit=false;p.x=2;p.distance=2;
s={};s.Kind="banking";s.Sequence.Start(Steps(s.Kind,15000));
assert(!AdventureSocialActions::Update(&p,s,1000,c));npc.service=true;
// The visit stays active until actual departure arrival, without more cues.
s={};s.Kind="banking";s.Sequence.Start(Steps(s.Kind,15000));
for(int i=0;i<60&&!s.Departing;++i) assert(AdventureSocialActions::Update(&p,s,1000,c));
assert(s.Departing&&c.departures==1);auto gestures=p.emotes.size();
travel.status.State=ClientlessTravelState::Moving;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.size()==gestures);
travel.status.State=ClientlessTravelState::Rejected;assert(!AdventureSocialActions::Update(&p,s,1000,c));
// Comparing explicitly changes to a different vendor, before departing.
Unit other;other.guid.id=3;other.service=true;services={&npc,&other};
s={};s.Kind="vendor_compare";s.Sequence.Start(Steps(s.Kind,12000));travel.status.State=ClientlessTravelState::Arrived;
for(int i=0;i<60&&!s.ServiceVisits;++i) assert(AdventureSocialActions::Update(&p,s,1000,c));
assert(s.ServiceVisits==1&&s.Target.id==3&&s.Approaching&&!s.Departing);
target=&other;
for(int i=0;i<60&&!s.Departing;++i) assert(AdventureSocialActions::Update(&p,s,1000,c));
assert(s.Departing);assert(!AdventureSocialActions::Update(&p,s,1000,c));
// No second vendor: never pretend a successful comparison or departure.
target=&npc;services={&npc};s={};s.Kind="vendor_compare";s.Sequence.Start({{Cue::Listen,1}});
assert(AdventureSocialActions::Update(&p,s,1000,c));
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&!s.ServiceVisits&&!s.Departing);
// Investigation waits for genuine arrival, then only gestures at the object.
s={};s.Kind="inspect_detail";s.Target.id=2;s.Sequence.Start(Steps(s.Kind,15000));
target=&chair;p.sit=false;p.x=2;p.distance=2;p.visible=p.los=true;chair.spawned=true;
s.Approaching=true;travel.status.State=ClientlessTravelState::Moving;
auto oldUses=p.session.uses,oldCasts=p.casts;auto oldEmotes=p.emotes.size();
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.size()==oldEmotes);
travel.status.State=ClientlessTravelState::Arrived;
active=true;for(int i=0;i<60&&active;++i) active=AdventureSocialActions::Update(&p,s,1000,c);
assert(!active&&p.session.uses==oldUses&&p.casts==oldCasts);
assert(std::find(p.emotes.begin()+oldEmotes,p.emotes.end(),EMOTE_ONESHOT_POINT)!=p.emotes.end());
s={};s.Kind="inspect";s.Target.id=2;s.Sequence.Start(Steps(s.Kind,15000));chair.spawned=false;
oldEmotes=p.emotes.size();assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.emotes.size()==oldEmotes);chair.spawned=true;
p.visible=false;assert(!AdventureSocialActions::Update(&p,s,1000,c));p.visible=true;
// Looking around uses a fixed initial direction rather than spinning each tick.
s={};s.Kind="scout";s.Target.id=0;s.Facing=1.2f;s.Sequence.Start(Steps(s.Kind,15000));p.headings.clear();
active=true;for(int i=0;i<60&&active;++i) active=AdventureSocialActions::Update(&p,s,1000,c);
assert(!active&&p.headings.size()==2&&std::fabs(p.headings[0]-0.45f)<0.001&&std::fabs(p.headings[1]-1.95f)<0.001);
// Group preparation reuses equipment checks but cannot perform profession work.
c.Intent="groupformation";c.PrimaryProfessions={"mining"};
assert(AdventureSocialActions::Available(&p,{"profession_tools"},c));
assert(!AdventureSocialActions::Available(&p,{"work_mining"},c));
s={};s.Kind="profession_tools";s.Target={};s.Approaching=false;s.Sequence.Start(Steps(s.Kind,20000));p.emotes.clear();
assert(AdventureSocialActions::Update(&p,s,1000,c));
assert(std::find(p.emotes.begin(),p.emotes.end(),EMOTE_ONESHOT_USE_STANDING)!=p.emotes.end());
// Profession gestures wait for arrival and never invoke use/cast handlers.
c.Intent="profession";c.PrimaryProfessions={"mining","blacksmithing"};
target=&chair;p.distance=2;p.visible=p.los=true;p.states.clear();
s={};s.Kind="work_mining";s.TargetKey="2";s.Target.id=2;s.Approaching=true;
s.Sequence.Start(Steps(s.Kind,20000));travel.status.State=ClientlessTravelState::Moving;
oldUses=p.session.uses;oldCasts=p.casts;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.states.empty());
travel.status.State=ClientlessTravelState::Arrived;
active=true;for(int i=0;i<60&&active;++i) active=AdventureSocialActions::Update(&p,s,1000,c);
assert(!active&&p.session.uses==oldUses&&p.casts==oldCasts);
assert(std::find(p.states.begin(),p.states.end(),EMOTE_STATE_WORK_MINING)!=p.states.end());
assert(std::find(c.TargetHistory.begin(),c.TargetHistory.end(),"work:2")!=c.TargetHistory.end());
// Losing profile authorization stops a scene before the next cue.
s={};s.Kind="work_mining";s.Target.id=2;s.Sequence.Start(Steps(s.Kind,20000));
p.states.clear();c.PrimaryProfessions.clear();
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.states.empty());
c.PrimaryProfessions={"mining"};
// Displacement while working must reapproach without advancing the cue.
p.distance=8;s.Approaching=false;travel.status.State=ClientlessTravelState::Moving;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&s.Approaching&&p.states.empty());
// Repair requires arrival and a completed valid service scene. No packets/casts.
Unit repairer;repairer.service=true;target=&repairer;c.Intent="economy";
p.distance=2;p.los=true;friendly=true;approach=true;
s={};s.Kind="repair";s.Action="economy_repair";s.Target.id=1;s.Approaching=true;
s.Sequence.Start(Steps(s.Kind,15000));travel.status.State=ClientlessTravelState::Moving;
int beforeRepairs=p.repairs;oldCasts=p.casts;oldUses=p.session.uses;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.repairs==beforeRepairs);
travel.status.State=ClientlessTravelState::Arrived;
active=true;for(int i=0;i<80&&active;++i)active=AdventureSocialActions::Update(&p,s,1000,c);
assert(p.repairs==beforeRepairs+1&&s.RepairDone&&p.casts==oldCasts&&p.session.uses==oldUses);
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.repairs==beforeRepairs+1);
// Lost repair permission/target must never repair in the background.
s={};s.Kind="repair";s.Target.id=1;s.Sequence.Start(Steps(s.Kind,15000));repairer.service=false;
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.repairs==beforeRepairs+1);
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-social-test-') 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([str(binary)],check=True)
print('Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed.')