222 lines
16 KiB
Python
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.')
|