114 lines
8.8 KiB
Python
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.')
|