Files
Viano-Core/tests/clientless/test_adventure_instance_persistence.py
T
Nico e18a416bf4
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s
Add persistent instance visits and validate sleeping spaces
2026-10-27 20:10:13 +01:00

92 lines
5.5 KiB
Python

"""Run production save/restore with a transactional character-DB adapter."""
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(signature):
a=s.index(signature);b=s.index('{',a);n=b+1;depth=1
while depth:
depth+=(s[n]=='{')-(s[n]=='}');n+=1
return s[a:n]
code=r'''
#include "AdventureInstancePolicy.h"
#include <cassert>
#include <memory>
#include <string>
#include <vector>
#include <cstdint>
using uint32=uint32_t;using uint64=uint64_t;using uint8=uint8_t;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_ERROR(...) ((void)0)
constexpr int UNIT_STAND_STATE_STAND=0;
std::string str(char const* s){return s;}template<class T>std::string str(T n){return std::to_string(n);}
struct Field{std::string value;std::string GetString(){return value;}uint8 GetUInt8(){return std::stoul(value);}uint32 GetUInt32(){return std::stoul(value);}uint64 GetUInt64(){return std::stoull(value);}};
struct Row{std::vector<Field> fields;Field* Fetch(){return fields.data();}};
using Result=std::shared_ptr<Row>;
struct Operation{std::string sql;std::vector<std::string> args;};
struct Transaction{std::vector<Operation> ops;template<class... A>void PAppend(char const* sql,A... args){ops.push_back({sql,{str(args)...}});}};
using Tx=std::shared_ptr<Transaction>;
struct DB{
bool fail=false,table=true;std::vector<std::string> row;std::vector<std::string> character;std::vector<Operation> last;
Tx BeginTransaction(){return std::make_shared<Transaction>();}void EscapeString(std::string&){}
struct Future{bool success;bool get(){return success;}};
struct Commit{Future m_future;};
Commit AsyncCommitTransaction(Tx& tx){last=tx->ops;if(fail)return {{false}};for(auto const& op:tx->ops){
if(op.sql=="CHARACTER_SAVE")character=op.args;
else if(op.sql.find("REPLACE INTO")==0)row=op.args;
else if(op.sql.find("DELETE FROM")==0)row.clear();
}return {{true}};}
Result Query(char const*){return table?std::make_shared<Row>():nullptr;}
Result PQuery(char const* sql,uint32){if(row.empty())return nullptr;auto r=std::make_shared<Row>();
if(std::string(sql).find("SELECT phase,")==0)for(auto i:{8,6,10})r->fields.push_back({row.at(i)});
else for(unsigned i=1;i<row.size();++i)r->fields.push_back({row[i]});
return r;
}
}CharacterDatabase;
struct Player{
uint32 health=100,map=36,instance=7;bool teleport=false;int stand=0;
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
bool IsBeingTeleported(){return teleport;}uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}
void SaveToDB(Tx& tx){tx->PAppend("CHARACTER_SAVE",health,map,instance);}
void SetStandState(int v){stand=v;}
};
struct AdventureBehaviorMgr{
enum class Phase{Idle,OnSite};
struct Record{AdventureInstancePolicy::Visit Instance;bool Schedule=false,Dirty=false;Phase State=Phase::Idle;
std::string Intent,WantedIntent,Location,WantedLocation,Detail;time_t Deadline=0;};
bool SaveInstance(Player*,Record&);bool RestoreInstance(Player*,Record&);void FinishInstance(Player*,Record&);
};
'''
for signature in ('bool AdventureBehaviorMgr::SaveInstance(', 'bool AdventureBehaviorMgr::RestoreInstance(', 'void AdventureBehaviorMgr::FinishInstance('):code+=function(signature)+'\n'
code+=r'''
int main(){
AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;Player p;
assert(!mgr.RestoreInstance(&p,r));
auto& v=r.Instance;v.Active=true;v.Location="instance_visit_at_78";v.Entry=78;v.Exit=121;v.Map=36;v.InstanceId=7;
v.BlockEnd=3600;v.PrepareUntil=2410;v.Stage=AdventureInstancePolicy::Phase::Inside;v.LossPercent=37;r.Schedule=true;
assert(mgr.SaveInstance(&p,r));assert(CharacterDatabase.last.size()==2);
assert(CharacterDatabase.character[0]=="100"&&CharacterDatabase.character[2]=="7");
// Restart creates an empty behavior record, then restores the actual visit.
AdventureBehaviorMgr::Record restored;assert(mgr.RestoreInstance(&p,restored));
assert(restored.Intent=="instance"&&restored.Location==v.Location&&restored.Deadline==3600&&restored.Schedule);
assert(restored.Instance.Stage==AdventureInstancePolicy::Phase::Inside&&restored.Instance.InstanceId==7);
assert(restored.Instance.LossPercent==37&&!restored.Instance.InjuryApplied);
// Character health and the once-only marker commit together, never separately.
v.Stage=AdventureInstancePolicy::Phase::Rest;v.InjuryApplied=true;p.health=63;p.map=0;p.instance=0;
CharacterDatabase.fail=true;assert(!mgr.SaveInstance(&p,r));
assert(CharacterDatabase.character[0]=="100");assert(mgr.RestoreInstance(&p,restored)&&!restored.Instance.InjuryApplied);
CharacterDatabase.fail=false;assert(mgr.SaveInstance(&p,r));
assert(CharacterDatabase.character[0]=="63"&&CharacterDatabase.character[1]=="0");
assert(mgr.RestoreInstance(&p,restored)&&restored.Instance.InjuryApplied&&restored.Deadline==3600);
// Clear the visit atomically with final character state, and release a queued intent.
r.Dirty=true;r.State=AdventureBehaviorMgr::Phase::OnSite;mgr.FinishInstance(&p,r);
assert(!r.Instance.Active&&r.State==AdventureBehaviorMgr::Phase::Idle&&CharacterDatabase.row.empty());
assert(!mgr.RestoreInstance(&p,restored));
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'save.cpp';exe=Path(tmp)/'save';src.write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed.')