Files
Viano-Core/tests/clientless/test_adventure_social.py
T
Nico 119d63ed1c
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Implement autonomous AdventureBot intents and social micro actions
Add profile-driven daily schedules, semantic location import and editable catalogs, flexible intent execution and safe graph handovers. Implement coherent social scenes, native chair use and campfires, frontal conversation approaches and walking. Fix clientless near-teleport observer identity and native flight area queries. Include migrations, regression tests and implementation documentation.
2026-10-26 22:45:38 +01:00

133 lines
9.2 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 <cassert>
#include <string>
#include <vector>
#include <cstdint>
using uint32=uint32_t;
using namespace AdventureSocialFlow;
#define TC_LOG_INFO(...) ((void)0)
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_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 IsEmpty()const{return !id;}std::string ToString()const{return std::to_string(id);}};
struct Unit;struct GameObject;
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;}
Unit* ToUnit();GameObject* ToGameObject();ObjectGuid GetGUID(){return guid;}
};
struct Unit:WorldObject{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;}
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 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 IsBeingTeleported(){return false;}bool IsSitState(){return sit;}bool IsNonMeleeSpellCast(bool){return casting;}
float GetExactDist(WorldObject*){return distance;}void SetFacingToObject(WorldObject*){++faces;}
void SetFacingTo(float){}int chairStand=5;uint32 GetStandState(){return sit?chairStand:0;}
void SetSelection(ObjectGuid g){selection=g;}void SetUInt32Value(int,int){}
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;}
};
struct Position{};
struct AdventureSocialContext{};
struct AdventureSocialScene{
bool Active=true,Approaching=false,Casting=false,WaitingChair=false,Seated=false;
std::string Action="social_npc",Kind="npc",TargetKey;ObjectGuid Target{1},PreviousSelection;Position Destination;
uint32 Elapsed=0,CastElapsed=0,CheckTimer=0,Repaths=0;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 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};
struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel;
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);
// 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);
}
'''
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.')