Files
Viano-Core/tests/clientless/test_adventure_social.py
T
Nico 440c53acd7
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Validate profession scenes and fix native workstation and area targeting
2026-10-27 18:01:03 +01:00

222 lines
16 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;}
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 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 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 bool Valid(Player*,WorldObject*,std::string const&,AdventureSocialContext const&){return true;}};
class AdventureSocialActions{public:static bool Update(Player*,AdventureSocialScene&,uint32,AdventureSocialContext&);};
'''.replace('Sequence Sequence;', 'AdventureSocialFlow::Sequence Sequence;')
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);
// 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());
}
'''
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.')