Files
Viano-Core/tests/clientless/test_adventure_group_departure.py
T

114 lines
8.8 KiB
Python

"""Execute persisted production handoffs with an actual clock and transactional DB double."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
def function(sig):
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
header=(root/'src/server/game/World/AdventureGroupMgr.h').read_text()
a=header.index('struct AdventureGroupPlan');b=header.index('class TC_GAME_API',a)
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureInstancePolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <unordered_map>
using uint32=unsigned;using uint64=unsigned long long;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_ERROR(...) ((void)0)
struct Guid{unsigned id=0;unsigned GetCounter(){return id;}};struct Player{Guid guid;Guid GetGUID(){return guid;}};
namespace GameTime{time_t now;time_t GetGameTime(){return now;}}
std::tm Local(time_t now){std::tm tm{};localtime_r(&now,&tm);return tm;}unsigned DateOf(std::tm tm){return (1900+tm.tm_year)*10000+(tm.tm_mon+1)*100+tm.tm_mday;}
time_t Clock(int h,int m=0){std::tm tm{};tm.tm_year=126;tm.tm_mon=9;tm.tm_mday=29;tm.tm_hour=h;tm.tm_min=m;return mktime(&tm);}
struct Field{std::string value;uint64 GetUInt64(){return std::stoull(value);}std::string GetString(){return value;}};
struct Row{Field fields[5];Field*Fetch(){return fields;}};
using Result=std::shared_ptr<Row>;
struct Transaction{std::string sql;template<class...Args>void PAppend(char const*fmt,Args...args){char b[2000];snprintf(b,sizeof(b),fmt,args...);sql=b;}};
using Tx=std::shared_ptr<Transaction>;
struct Database{
bool present=true,fail=false;std::unordered_map<unsigned,Row>rows;
Tx BeginTransaction(){return std::make_shared<Transaction>();}void EscapeString(std::string&){}
struct Future{bool ok;bool get(){return ok;}};struct Commit{Future m_future;};
Commit AsyncCommitTransaction(Tx tx){if(fail)return {{false}};unsigned id;uint64 at,end,activity;char intent[64],location[128];
int count=sscanf(tx->sql.c_str(),"REPLACE INTO vcore_adventure_group_handoff (character_guid,depart_at,formation_end,activity_end,intent,location_key) VALUES (%u,%llu,%llu,%llu,'%63[^']','%127[^']')",&id,&at,&end,&activity,intent,location);
assert(count==6);rows[id]={{{std::to_string(at)},{std::to_string(end)},{std::to_string(activity)},{intent},{location}}};return {{true}};}
Result Query(char const*){return present?std::make_shared<Row>():nullptr;}
Result PQuery(char const*,unsigned id){return rows.count(id)?std::make_shared<Row>(rows.at(id)):nullptr;}
void PExecute(char const*,unsigned id){rows.erase(id);}
}WorldDatabase;
struct Location{unsigned InstanceCapacity=5,InstanceDifficulty=1;}location;
struct Locations{Location*Find(std::string const&key){return key.empty()?nullptr:&location;}}locations;auto sAdventureLocationMgr=&locations;
'''+header[a:b]+r'''
struct Encounters{bool Active(Player*){return false;}}encounters;auto sAdventureEncounterMgr=&encounters;
struct AdventureBehaviorMgr{
struct Record{
bool Enabled=true,Dirty=false,Schedule=true,GroupOverride=false;int Hour=14;unsigned Date=20261029;
std::string Intent="groupformation",Location="capital",WantedIntent="groupformation",WantedLocation="capital",PersonalIntent,PersonalLocation,ManualNextIntent,ManualNextLocation;
time_t Deadline=Clock(15),PersonalDeadline=0;AdventureBehaviorPolicy::Day Plan;
AdventureGroupPolicy::Departure Departure;AdventureInstancePolicy::Visit Instance;
};
std::unordered_map<unsigned,Record>_records;unsigned changes=0;
void Generate(Player*,Record&r,unsigned date){r.Date=date;}
bool Allowed(Player*,Location const&,std::string const&){return true;}
void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;r.Dirty=true;}
void RefreshDay(Player*);void RestoreDeparture(Player*,Record&);bool UpdateDeparture(Player*,Record&,time_t);void ArmGroupDeparture(Player*,AdventureGroupPlan const&);
AdventureGroupPlan GroupPlan(Player*);void ResumePersonalPlan(Player*);bool AdoptGroup(Player*,Player*);std::string InteractionSchedule(Player*);
};
'''
for sig in ('void AdventureBehaviorMgr::RefreshDay(', 'void AdventureBehaviorMgr::RestoreDeparture(', 'void AdventureBehaviorMgr::ArmGroupDeparture(', 'bool AdventureBehaviorMgr::UpdateDeparture(', 'AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(', 'void AdventureBehaviorMgr::ResumePersonalPlan(', 'bool AdventureBehaviorMgr::AdoptGroup(', 'std::string AdventureBehaviorMgr::InteractionSchedule('):code+=function(sig)+'\n'
code+=r'''
void Prepare(AdventureBehaviorMgr::Record&r,std::string const&goal){for(auto&slot:r.Plan)slot={"rest","inn"};r.Plan[14]={"groupformation","capital"};r.Plan[15]={"instance",goal};}
int main(){setenv("TZ","UTC",1);tzset();GameTime::now=Clock(14,20);AdventureBehaviorMgr mgr;Player leader{{1}},member{{2}};
auto&a=mgr._records[1];auto&b=mgr._records[2];Prepare(a,"deadmines_d1");Prepare(b,"shadowfang_d1");
auto source=mgr.GroupPlan(&leader);assert(source.Capacity==5&&source.Mode==1&&source.NextDeadline==Clock(16));
mgr.ArmGroupDeparture(&leader,source);mgr.ArmGroupDeparture(&member,source);
assert(a.Departure.At==Clock(14,25)&&b.Departure.At==Clock(14,25));
assert(b.Departure.Location=="deadmines_d1"&&b.Plan[15].Location=="shadowfang_d1");
assert(WorldDatabase.rows.size()==2);auto saved=WorldDatabase.rows[2];
GameTime::now=Clock(14,23);mgr.ArmGroupDeparture(&member,source);assert(b.Departure.At==Clock(14,25)); // no extending/rearming
assert(!mgr.UpdateDeparture(&member,b,GameTime::now)&&b.Deadline==Clock(14,25));
assert(mgr.GroupPlan(&member).Forming);
GameTime::now=Clock(14,25);assert(mgr.UpdateDeparture(&member,b,GameTime::now));
assert(b.WantedLocation=="deadmines_d1"&&b.WantedIntent=="instance"&&b.Deadline==Clock(16));
auto plan=mgr.GroupPlan(&member);assert(!plan.Forming&&plan.EarlyDeparture&&plan.Location=="deadmines_d1");
// Kick/incomplete group cannot roll the copied current slot back.
b.GroupOverride=true;mgr.ResumePersonalPlan(&member);assert(!b.GroupOverride&&b.WantedLocation=="deadmines_d1");
assert(mgr.InteractionSchedule(&member)=="instance:deadmines_d1");
// A failed journey selects a normal fallback. Repeated maintenance must not
// repin it, followers inherit it and a kick retains it in the same intent.
b.Intent="instance";b.Location=b.WantedLocation="fallback_dungeon";unsigned changed=mgr.changes;
assert(mgr.UpdateDeparture(&member,b,GameTime::now));assert(mgr.changes==changed&&b.WantedLocation=="fallback_dungeon");
assert(mgr.GroupPlan(&member).Location=="fallback_dungeon");
b.GroupOverride=true;mgr.ResumePersonalPlan(&member);assert(b.WantedLocation=="fallback_dungeon"&&mgr.changes==changed);
a.Intent="instance";a.WantedIntent="instance";a.Location=a.WantedLocation="leader_fallback";a.Deadline=Clock(16);
mgr.AdoptGroup(&member,&leader);assert(b.WantedLocation=="leader_fallback");
assert(b.Plan[15].Location=="shadowfang_d1");
// Restore independently of any surviving group and continue the same current slot.
AdventureBehaviorMgr restored;auto&c=restored._records[2];Prepare(c,"shadowfang_d1");
restored.RestoreDeparture(&member,c);assert(c.Departure.At==Clock(14,25));
restored.RefreshDay(&member);assert(c.WantedLocation=="deadmines_d1"); // human follow can skip the movement behavior tick
assert(c.Plan[15].Location=="shadowfang_d1");
// At the original boundary the next, untouched personal slot applies.
GameTime::now=Clock(15);restored.UpdateDeparture(&member,c,GameTime::now);
assert(c.WantedLocation=="shadowfang_d1"&&!c.Departure.At&&!WorldDatabase.rows.count(2));
// Late completion never extends the fixed last-five-minute window.
GameTime::now=Clock(14,57);auto&late=restored._records[3];Player lateBot{{3}};Prepare(late,"deadmines_d1");
restored.ArmGroupDeparture(&lateBot,restored.GroupPlan(&lateBot));assert(late.Departure.At==Clock(15));
assert(AdventureGroupPolicy::Gathering("groupformation",late.Deadline,GameTime::now));
GameTime::now=Clock(15);assert(!restored.UpdateDeparture(&lateBot,late,GameTime::now));assert(late.WantedIntent=="instance");
// No armed in-memory transition when persistence fails.
GameTime::now=Clock(14,20);auto&failed=restored._records[4];Player failedBot{{4}};Prepare(failed,"deadmines_d1");
WorldDatabase.fail=true;restored.ArmGroupDeparture(&failedBot,restored.GroupPlan(&failedBot));assert(!failed.Departure.At&&!WorldDatabase.rows.count(4));
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production handoffs: full-group deadline, late clamp, unchanged next slot, kick, restart persistence, expiry and failed save passed.')