Add persistent instance visits and validate sleeping spaces
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-27 20:10:13 +01:00
1 parent 440c53acd7
commit e18a416bf4
22 files changed
+1631 -41

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
"""Execute production instance phases with native-transfer/persistence adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
start=s.index('bool AdventureBehaviorMgr::UpdateInstance(')
code=r'''
#include "AdventureInstancePolicy.h"
#include <cassert>
#include <string>
#include <vector>
#include <cstdint>
#include <cmath>
using uint32=uint32_t;using uint8=uint8_t;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_WARN(...) ((void)0)
constexpr int UNIT_STAND_STATE_STAND=0,UNIT_STAND_STATE_SIT=1,EMOTE_ONESHOT_KNEEL=16,CMSG_AREATRIGGER=1;
struct Position{float x=0,y=0,z=0,o=0;Position()=default;Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}};
struct Trigger{uint32 ContinentID=0;struct{float X=0,Y=0,Z=0;}Pos;};
struct Target{uint32 target_mapId=36;float target_X=0,target_Y=0,target_Z=0,target_Orientation=0;};
struct Triggers{Trigger entrance,exit;Triggers(){exit.ContinentID=36;exit.Pos.X=100;}Trigger* LookupEntry(uint32 id){return id==78?&entrance:id==121?&exit:nullptr;}}sAreaTriggerStore;
struct Targets{Target entrance,exit;Targets(){exit.target_mapId=0;exit.target_X=500;}Target* GetAreaTrigger(uint32 id){return id==78?&entrance:id==121?&exit:nullptr;}}targets;auto sObjectMgr=&targets;
struct MapEntry{bool IsRaid(){return false;}}mapEntry;
struct Maps{auto LookupEntry(uint32){return &mapEntry;}}sMapStore;
struct WorldPacket{uint32 trigger=0;WorldPacket(int,int){}WorldPacket& operator<<(uint32 id){trigger=id;return *this;}};
struct Save{uint32 GetInstanceId(){return 7;}unsigned GetPlayerCount(){return 1;}unsigned GetGroupCount(){return 0;}}save;
struct Bind{Save* save=nullptr;};
struct Session;
struct Player:Position{
uint32 map=0,instance=0,health=100,maximum=100;int stand=0,damages=0,entryCalls=0,exitCalls=0,teleports=0;
bool safe=true,group=false,deny=false;Bind bind;Session* session=nullptr;
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}bool IsInWorld(){return true;}
bool IsBeingTeleported(){return false;}bool GetGroup(){return group;}
Bind* GetBoundInstance(uint32,int){return bind.save?&bind:nullptr;}int GetDifficulty(bool){return 0;}
bool IsInAreaTriggerRadius(Trigger* t){return map==t->ContinentID&&std::fabs(x-t->Pos.X)<3;}
float GetAngle(float,float){return 0;}void SetFacingTo(float){}void HandleEmoteCommand(int){}
void SetStandState(int v){stand=v;}uint32 GetHealth(){return health;}uint32 GetMaxHealth(){return maximum;}
void SetHealth(uint32 h){health=h;++damages;}Session* GetSession(){return session;}
float GetExactDist(float a,float b,float c){return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
float GetExactDist(Position const& p){return GetExactDist(p.x,p.y,p.z);}
bool TeleportTo(uint32 m,float a,float b,float c,float o){++teleports;map=m;instance=m==36?7:0;x=a;y=b;z=c;this->o=o;return true;}
};
struct Session{Player* p;void CompleteClientlessTeleport(){}void HandleAreaTriggerOpcode(WorldPacket const& packet){
if(packet.trigger==78){++p->entryCalls;if(!p->deny){p->map=36;p->instance=7;p->x=0;}}
else{++p->exitCalls;if(!p->deny){p->map=0;p->instance=0;p->x=500;}}
}};
bool Unsafe(Player* p){return !p->safe;}
struct Database{bool shared=false;template<class... T>bool PQuery(char const*,T...){return shared;}}CharacterDatabase;
struct Manager{void ClearQueuedActions(uint32){}}manager;auto sClientlessPlayerMgr=&manager;
struct Executor{void Clear(uint32){}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Travel{void CancelTravel(uint32){}}travel;auto sClientlessTravelMgr=&travel;
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
struct AdventureBehaviorMgr{
enum class Phase{Idle,Travel,OnSite};
struct Record{AdventureInstancePolicy::Visit Instance;Phase State=Phase::OnSite;bool Dirty=false;std::string Detail;};
int saves=0,finishes=0;bool saveOK=true;
bool SaveInstance(Player*,Record&){++saves;return saveOK;}
void FinishInstance(Player* p,Record&r){++finishes;r.Instance={};p->SetStandState(0);}
bool UpdateInstance(Player*,Record&,time_t);
};
'''+s[start:]+r'''
using Stage=AdventureInstancePolicy::Phase;
AdventureBehaviorMgr::Record fresh(){AdventureBehaviorMgr::Record r;r.Instance.Active=true;r.Instance.Entry=78;r.Instance.Exit=121;r.Instance.Map=36;r.Instance.BlockEnd=3600;r.Instance.LossPercent=50;return r;}
int main(){
assert(AdventureInstancePolicy::ReturnAt(3600)==3300);
assert(AdventureInstancePolicy::MayEnter(3299,3600)&&!AdventureInstancePolicy::MayEnter(3300,3600));
assert(AdventureInstancePolicy::ReturnHealth(100,100,0)==100);
assert(AdventureInstancePolicy::ReturnHealth(100,100,50)==50);
assert(AdventureInstancePolicy::ReturnHealth(40,100,50)==40); // no healing
assert(AdventureInstancePolicy::ReturnHealth(UINT32_MAX,UINT32_MAX,50)==2147483647u);
AdventureBehaviorMgr mgr;auto r=fresh();Player p;Session session{&p};p.session=&session;
// 40 minutes' travel leaves 15 minutes inside and 5 outside.
assert(mgr.UpdateInstance(&p,r,2400)&&r.Instance.Stage==Stage::Preparing&&p.entryCalls==0);
assert(mgr.UpdateInstance(&p,r,2409)&&p.entryCalls==0);
assert(mgr.UpdateInstance(&p,r,2410)&&p.entryCalls==1&&p.map==36);
assert(mgr.UpdateInstance(&p,r,2411)&&r.Instance.Stage==Stage::Inside&&r.Instance.InstanceId==7);
int transfers=p.teleports;
assert(mgr.UpdateInstance(&p,r,3299)&&p.teleports==transfers&&p.exitCalls==0);
assert(mgr.UpdateInstance(&p,r,3300)&&r.Instance.Stage==Stage::Rest&&p.map==0&&p.x==500);
assert(p.exitCalls==1&&p.health==50&&p.damages==1&&r.Instance.InjuryApplied);
assert(mgr.UpdateInstance(&p,r,3400)&&p.damages==1);
assert(!mgr.UpdateInstance(&p,r,3600)&&mgr.finishes==1&&!r.Instance.Active);
// Persisted inside state on restart keeps the absolute return time.
r=fresh();r.Instance.Stage=Stage::Inside;r.Instance.InstanceId=7;p.map=36;p.instance=7;p.x=0;
assert(mgr.UpdateInstance(&p,r,3299)&&p.map==36);
assert(!mgr.UpdateInstance(&p,r,3700)&&p.map==0&&!r.Instance.Active);
// If native login ejected an expired copy, return to its associated exit.
r=fresh();r.Instance.Stage=Stage::Inside;r.Instance.InstanceId=7;p.map=0;p.x=999;
assert(mgr.UpdateInstance(&p,r,3400)&&p.x==500&&r.Instance.InjuryApplied);
// Replaying a saved return cannot compound damage.
r=fresh();r.Instance.Stage=Stage::Returning;r.Instance.InjuryApplied=true;p.x=500;
int damages=p.damages;assert(mgr.UpdateInstance(&p,r,3400)&&p.damages==damages);
// A failed durable entering save must not start a portal transfer.
r=fresh();r.Instance.Stage=Stage::Entering;p.map=0;p.x=0;mgr.saveOK=false;
int prevented=p.entryCalls;assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==prevented);mgr.saveOK=true;
// A denied entrance is not a visit; no exit or damage is invented.
r=fresh();r.Instance.Stage=Stage::Entering;p.map=0;p.x=0;p.deny=true;damages=p.damages;
assert(mgr.UpdateInstance(&p,r,100)&&r.Instance.Stage==Stage::Rest&&p.damages==damages);
p.deny=false;
// Late arrival never enters, nor applies a fake injury.
r=fresh();r.State=AdventureBehaviorMgr::Phase::Travel;int calls=p.entryCalls;
assert(mgr.UpdateInstance(&p,r,3350)&&r.Instance.Stage==Stage::Rest&&p.entryCalls==calls&&p.damages==damages);
// Shared or grouped copies are not accepted for this solo performance.
r=fresh();r.Instance.Stage=Stage::Entering;p.group=true;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);p.group=false;
r=fresh();r.Instance.Stage=Stage::Entering;p.bind.save=&save;CharacterDatabase.shared=true;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);CharacterDatabase.shared=false;p.bind.save=nullptr;
// A denied exit remains pending and cannot wound a player still inside.
r=fresh();r.Instance.Stage=Stage::Returning;p.map=36;p.instance=7;p.x=0;p.deny=true;
assert(mgr.UpdateInstance(&p,r,3400)&&r.Instance.Stage==Stage::Returning&&p.damages==damages);
p.deny=false;
// With no separate native exit, use the original entrance location.
r=fresh();r.Instance.Stage=Stage::Returning;r.Instance.Exit=0;p.map=36;p.instance=7;p.x=50;
assert(mgr.UpdateInstance(&p,r,3400)&&p.map==0&&p.x==0);
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'instance.cpp';exe=Path(tmp)/'instance';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 lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.')
@@ -0,0 +1,34 @@
"""Run the actual optional WorldScript startup recovery with stored visits."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/scripts/Custom/viano_bot_commands.cpp').read_text()
s=s[s.index('class ClientlessInstanceVisitResume :'):s.index('\nvoid AddSC_viano_bot_commands()')]
code=r'''
#include <cassert>
#include <cstdint>
#include <memory>
#include <vector>
using uint32=uint32_t;
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}};
struct Field{uint32 value;uint32 GetUInt32(){return value;}};
struct Rows{std::vector<std::vector<Field>> data;unsigned index=0;Field* Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
struct Database{bool schema=true;std::shared_ptr<Rows> rows=std::make_shared<Rows>();
std::shared_ptr<Rows> Query(char const* sql){if(sql[0]=='S'&&sql[1]=='H')return schema?std::make_shared<Rows>():nullptr;rows->index=0;return rows->data.empty()?nullptr:rows;}
}CharacterDatabase;
struct Manager{std::vector<std::pair<uint32,uint32>> started;void StartPlayer(uint32 account,uint32 guid,std::unique_ptr<int> controller){assert(controller&&*controller==42);started.emplace_back(account,guid);}}manager;
auto sClientlessPlayerMgr=&manager;
std::unique_ptr<int> CreateClientlessAdventureLuaController(){return std::make_unique<int>(42);}
'''+s+r'''
int main(){ClientlessInstanceVisitResume script;
CharacterDatabase.schema=false;script.OnStartup();assert(manager.started.empty());
CharacterDatabase.schema=true;script.OnStartup();assert(manager.started.empty());
CharacterDatabase.rows->data={{{6},{26}},{{7},{27}}};script.OnStartup();
assert((manager.started==std::vector<std::pair<uint32,uint32>>{{6,26},{7,27}}));
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'startup.cpp';exe=Path(tmp)/'startup';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Instance startup script: no schema/no visits stays idle; saved visitors resume with the Adventure controller and native accounts.')
@@ -0,0 +1,42 @@
"""Entrance visits use ordinary location travel, retaining the local MMAP leg."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
a=s.index('void AdventureBehaviorMgr::Begin(');b=s.index('\nvoid AdventureBehaviorMgr::Fallback(',a)
code=r'''
#include <cassert>
#include <string>
#include <unordered_map>
#include <unordered_set>
#define TC_LOG_INFO(...) ((void)0)
struct Player{struct Guid{int GetCounter(){return 26;}};Guid GetGUID(){return {};}};
struct ClientlessIntent{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
struct Location{bool IsArea=false;}location;
struct Store{bool exists=true;Location* Find(std::string const&){return exists?&location:nullptr;}}store;auto sAdventureLocationMgr=&store;
struct Manager{void ClearIntent(int){}void SetIntent(int,ClientlessIntent){}}manager;auto sClientlessPlayerMgr=&manager;
struct AdventureBehaviorMgr{
enum class Phase{Idle,Travel};
struct Record{bool Dirty=true,Enabled=true,Publishing=false,Queued=false;std::string Intent,Location,WantedIntent="instance",WantedLocation="instance_visit_at_101",Detail;Phase State=Phase::Idle;std::unordered_set<std::string>Tried;};
bool mapped=true,allowed=true;int fallback=0;std::string action,parameter,target;bool hard=false;
void Begin(Player*,Record&);
bool Allowed(Player*,Location const&,std::string const&){return allowed;}
void Fallback(Player*,Record&){++fallback;}
bool ResolveInstance(Player*,Record&){return mapped;}
std::string AreaNode(Player*,Location const&){return "reachable";}
void Queue(Player*,Record&,std::string const&a,std::string const&p,std::string const&t,bool h=false){action=a;parameter=p;target=t;hard=h;}
};
'''+s[a:b]+r'''
int main(){Player p;AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;
mgr.Begin(&p,r);assert(mgr.action=="travel_to_location"&&mgr.parameter=="id"&&mgr.target=="instance_visit_at_101"&&mgr.hard);
assert(r.State==AdventureBehaviorMgr::Phase::Travel&&!r.Dirty);
mgr.mapped=false;mgr.Begin(&p,r);assert(mgr.fallback==1);
mgr.mapped=true;r.WantedIntent="social";mgr.Begin(&p,r);assert(mgr.action=="travel_to_location"&&!mgr.hard);
location.IsArea=true;mgr.Begin(&p,r);assert(mgr.action=="travel_to_node"&&mgr.target=="reachable"&&mgr.hard);
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'begin.cpp';exe=Path(tmp)/'begin';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.')
@@ -0,0 +1,39 @@
"""Execute the entrance migration against native portal/graph fixtures."""
from pathlib import Path
import re,sqlite3
root=Path(__file__).resolve().parents[2]
db=sqlite3.connect(':memory:');db.create_function('CONCAT',-1,lambda *a:''.join(map(str,a)))
db.executescript('''
CREATE TABLE instance_template(map INTEGER PRIMARY KEY);
CREATE TABLE areatrigger_teleport(ID INTEGER PRIMARY KEY,Name TEXT,target_map INTEGER);
CREATE TABLE vcore_clientless_travel_node(node_key TEXT PRIMARY KEY,world_point_key TEXT,enabled INTEGER);
CREATE TABLE vcore_clientless_world_point(point_key TEXT PRIMARY KEY,category TEXT,map INTEGER,position_x REAL,position_y REAL,position_z REAL,orientation REAL,enabled INTEGER,comment TEXT);
CREATE TABLE vcore_adventure_location(point_key TEXT PRIMARY KEY,display_name TEXT,source_type TEXT,source_id INTEGER);
CREATE TABLE vcore_adventure_location_intent(category TEXT,intent TEXT,weight REAL,PRIMARY KEY(category,intent));
INSERT INTO instance_template VALUES(36);
INSERT INTO areatrigger_teleport VALUES(78,'Deadmines Entrance Target',36),(119,'Front exit',0),(121,'Rear exit',0),(527,'City portal',1),(999,'Disabled entrance',36);
INSERT INTO vcore_clientless_world_point VALUES
('s78','travel_portal',0,1,2,3,0,1,'native'),('s119','travel_portal',36,4,5,6,0,1,'native'),
('s121','travel_portal',36,7,8,9,0,1,'native'),('s527','travel_portal',0,10,11,12,0,1,'native'),
('s999','travel_portal',0,13,14,15,0,1,'native');
INSERT INTO vcore_clientless_travel_node VALUES
('portal_at_78_source','s78',1),('portal_at_119_source','s119',1),('portal_at_121_source','s121',1),
('portal_at_527_source','s527',1),('portal_at_999_source','s999',0);
''')
sql=(root/'sql/custom/world/2026_10_27_07_adventure_instances.sql').read_text()
sql=re.sub(r" COMMENT '[^']*'",'',sql)
sql=re.sub(r'UNIQUE KEY\(entry_trigger\)','UNIQUE(entry_trigger)',sql)
sql=re.sub(r'\) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',');',sql).replace('INSERT IGNORE','INSERT OR IGNORE')
db.executescript(sql)
assert db.execute('SELECT location_key,entry_trigger,exit_trigger FROM vcore_adventure_instance').fetchall()==[('instance_visit_at_78',78,0)]
assert db.execute("SELECT category,map FROM vcore_clientless_world_point WHERE point_key='instance_visit_at_78'").fetchone()==('instance_entrance',0)
assert db.execute("SELECT category,map FROM vcore_clientless_world_point WHERE point_key='s78'").fetchone()==('travel_portal',0)
db.executescript("UPDATE vcore_adventure_instance SET exit_trigger=121,enabled=0; UPDATE vcore_adventure_location_intent SET weight=2.5; UPDATE vcore_clientless_world_point SET position_x=77 WHERE point_key='instance_visit_at_78';")
before=list(db.iterdump());db.executescript(sql);assert list(db.iterdump())==before
characters=(root/'sql/custom/characters/2026_10_27_00_adventure_instance_visit.sql').read_text()
characters=re.sub(r'\) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',');',characters)
db.executescript(characters)
db.execute("INSERT INTO vcore_adventure_instance_visit VALUES(26,'instance_visit_at_78',78,121,36,123,3600,10,3,50,0,0)")
db.executescript(characters)
assert db.execute('SELECT block_end,instance_id,phase FROM vcore_adventure_instance_visit').fetchone()==(3600,123,3)
print('Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved.')
@@ -0,0 +1,91 @@
"""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.')
@@ -55,6 +55,7 @@ public:
time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
struct{bool Active=false;}Social;
struct{bool Active=false;}Recovery;
struct{bool Active=false;}Instance;
struct{bool Active=false,Flying=false,Progressive=false;std::vector<Position> Route;size_t Step=0;}Leisure;
AdventureBehaviorPolicy::Day Plan;Position Anchor;uint32 AnchorMap=0;int WorkObject=0;uint32 WorkEmote=0;bool WorkPending=false;
};
@@ -67,6 +68,7 @@ public:
void Fallback(Player*,Record&){++fallbacks;}
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();}
void Micro(Player*,Record&){++micros;}
bool UpdateInstance(Player*,Record&,time_t){return false;}
bool UpdateLeisure(Player*,Record&,uint32){++leisureUpdates;return true;}
bool UpdateSocial(Player*,Record&,uint32){return true;}
bool UpdateRecovery(Player*,Record&,uint32){return true;}
+4 -1
View File
@@ -9,6 +9,7 @@ def function(signature):
return source[a:e]
code=r'''
#include <algorithm>
#include "AdventureInstancePolicy.h"
#include <cassert>
#include <cmath>
#include <cstdint>
@@ -86,6 +87,7 @@ struct Executor{bool RequestStopAtNode(uint32){return false;}}executor;auto sCli
class AdventureBehaviorMgr{public:
enum class Phase{Idle,OnSite,Switching};
struct Record{
AdventureInstancePolicy::Visit Instance;
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000;std::mt19937 Random{26};
time_t Deadline=0;bool Dirty=false;std::unordered_set<std::string> Tried;Phase State=Phase::OnSite;
struct LeisureScene{bool Active=false,Flying=false,Moving=false,StopRequested=false,Returning=false,PlannedWalk=false,Progressive=false;std::string Kind;Position Origin;std::vector<Position> Route,WalkTrace;std::size_t Step=0,OutboundSteps=0;uint32 Elapsed=0,LegElapsed=0,PauseMs=0,CompanionWait=0,CompletedLegs=0;ClientlessCompanionScene Companion;}Leisure;
@@ -93,6 +95,7 @@ class AdventureBehaviorMgr{public:
int moves=0,resets=0;bool lastRiding=false,moveAllowed=true;Position requested;
bool LocalMove(Player*,Record&,Position const& target,float,bool riding=false){++moves;lastRiding=riding;requested=target;travel.status.State=ClientlessTravelState::Moving;return moveAllowed;}
void ResetMicro(Player* p,Record&r){++resets;ResetLeisure(p,r);}
void FinishInstance(Player*,Record&r){r.Instance={};}
bool workAvailable=false;bool ProfessionWorkAvailable(Player*,Record&){return workAvailable;}
bool FishingDirection(Player*,float&)const;
bool LeisureAvailable(Player*,Record const&,AdventureMicroAction const&)const;
@@ -215,6 +218,6 @@ int main(){
'''
with tempfile.TemporaryDirectory(prefix='vcore-leisure-') as folder:
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
subprocess.run(['c++','-std=c++17',str(cpp),'-o',str(binary)],check=True)
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('Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.')
+39 -4
View File
@@ -17,6 +17,7 @@ def function(signature):
code = r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <string>
@@ -25,13 +26,17 @@ using uint32=uint32_t;
#define TC_LOG_INFO(...) ((void)0)
constexpr int UNIT_STAND_STATE_SLEEP=3,UNIT_STAND_STATE_SIT=1;
struct Position {
float x=0,y=0,z=0;
float x=0,y=0,z=0,o=0;
Position()=default; Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}
float GetOrientation()const{return o;}
float GetExactDist(Position const& p)const{return GetExactDist(p.x,p.y,p.z);}
float GetPositionZ()const{return z;}
float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
};
struct Guid{uint32 GetCounter(){return 26;}};
struct Player:Position {
uint32 map=0;bool water=false,falling=false,phase=true;int stand=0,poses=0;
bool space=true;
std::vector<Player*> occupants;
uint32 GetMapId(){return map;}Guid GetGUID(){return {};}
bool IsInWater(){return water;}bool IsFalling(){return falling;}
@@ -40,6 +45,13 @@ struct Player:Position {
void SetFacingTo(float){}void SetStandState(int value){stand=value;++poses;}
int GetStandState(){return stand;}
};
bool SleepSpace(Player* p,Position const&){return p->space;}
std::vector<Position> candidates;
std::vector<Position> SleepCandidates(Player*,Position const&,bool){return candidates;}
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float FinalDistance=0;};
double Novelty(std::vector<std::string> const&,std::string const&){return 1;}
void Remember(std::vector<std::string>& h,std::string const& s){h.push_back(s);}
std::size_t Choose(std::vector<double> const& weights,int&){for(std::size_t i=0;i<weights.size();++i)if(weights[i]>0)return i;return weights.size();}
struct AdventureMicroAction{std::string Requirement;};
struct AdventureLocation {
struct {std::string Id="bed",Category="rest_sleep";uint32 MapId=0;float X=0,Y=0,Z=0,O=0;}Point;
@@ -54,14 +66,19 @@ auto sAdventureLocationMgr=&locations;
enum class ClientlessTravelState{Moving,Arrived,PathRejected};
struct Travel {
struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;}status;
float endpointError=0;bool suspicious=false;
bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics& d){d.FinalDistance=endpointError;d.Suspicious=suspicious;return true;}
bool present=true;Status const* GetStatus(uint32){return present?&status:nullptr;}
}travel;
auto sClientlessTravelMgr=&travel;
class AdventureBehaviorMgr {
public:
struct RecoveryScene{bool Active=true,Approaching=true;std::string Point="bed",Posture="sleep";uint32 Remaining=3000,ApproachElapsed=0;};
struct Record{Position Anchor;uint32 AnchorMap=0;RecoveryScene Recovery;std::string Detail;};
struct RecoveryScene{bool Active=true,Approaching=true;std::string Point="bed",Posture="sleep";uint32 Remaining=3000,ApproachElapsed=0;Position Destination;bool HasDestination=false;uint32 SpaceCheckMs=0;};
struct Record{Position Anchor;uint32 AnchorMap=0;RecoveryScene Recovery;std::string Detail;std::vector<std::string> TargetHistory;int Random=0;};
bool Allowed(Player*,AdventureLocation const& p,std::string const&)const{return p.allowed;}
std::vector<Position> moves;bool moveOK=true;
bool LocalMove(Player*,Record&,Position const& p,int){moves.push_back(p);return moveOK;}
bool StartRecovery(Player*,Record&,AdventureMicroAction const&,uint32);
bool RecoveryAvailable(Player*,Record const&,AdventureMicroAction const&)const;
bool UpdateRecovery(Player*,Record&,uint32);
};
@@ -69,6 +86,7 @@ public:
# Avoid a class/member name collision in the adapter.
code = code.replace('struct Locations {', 'struct LocationStore {')
code += function('bool AdventureBehaviorMgr::RecoveryAvailable(')
code += function('bool AdventureBehaviorMgr::StartRecovery(')
code += function('bool AdventureBehaviorMgr::UpdateRecovery(')
code += r'''
int main(){
@@ -78,8 +96,20 @@ int main(){
locations.points.push_back({});
assert(mgr.RecoveryAvailable(&p,r,sleep));
assert(!mgr.RecoveryAvailable(&p,r,ground)&&!mgr.RecoveryAvailable(&p,r,unknown));
// Reject endpoints beneath an elevated surface before starting movement.
candidates={Position(3,0,0)};travel.endpointError=0.7f;
assert(!mgr.StartRecovery(&p,r,sleep,7000)&&mgr.moves.empty());
travel.endpointError=0;assert(mgr.StartRecovery(&p,r,sleep,7000));
assert(r.Recovery.HasDestination && r.Recovery.Destination.x==3 && mgr.moves.size()==1);
assert(r.Recovery.Remaining==7000);
mgr.moves.clear();r=AdventureBehaviorMgr::Record{};
// Even if a tavern is first in the catalog, explicit sleep anchors have priority.
locations.points.insert(locations.points.begin(),AdventureLocation{});locations.points[0].Point.Id="inn";
locations.points[0].Point.Category="tavern";
assert(mgr.StartRecovery(&p,r,sleep,7000)&&r.Recovery.Point=="bed");
locations.points.erase(locations.points.begin());r=AdventureBehaviorMgr::Record{};
auto& point=locations.points[0];
point.Point.Category="tavern";assert(!mgr.RecoveryAvailable(&p,r,sleep)); // no arbitrary tavern floor
point.Point.Category="tavern";assert(mgr.RecoveryAvailable(&p,r,sleep)); // bounded indoor floor search
point.Point.Category="rest_sleep";point.Point.MapId=1;assert(!mgr.RecoveryAvailable(&p,r,sleep));
point.Point.MapId=0;point.allowed=false;assert(!mgr.RecoveryAvailable(&p,r,sleep));
point.allowed=true;point.Point.X=31;assert(!mgr.RecoveryAvailable(&p,r,sleep));
@@ -94,6 +124,11 @@ int main(){
assert(mgr.UpdateRecovery(&p,r,1000)&&p.stand==UNIT_STAND_STATE_SLEEP&&!r.Recovery.Approaching);
assert(mgr.UpdateRecovery(&p,r,1000));assert(mgr.UpdateRecovery(&p,r,1000));
assert(!mgr.UpdateRecovery(&p,r,1000)); // full duration starts after arrival
r.Recovery={};p.space=false;assert(!mgr.UpdateRecovery(&p,r,1000)&&r.Recovery.Approaching);p.space=true;
r.Recovery.Destination=Position(4,0,0);r.Recovery.HasDestination=true;
assert(!mgr.UpdateRecovery(&p,r,1000));p.x=4;
assert(mgr.UpdateRecovery(&p,r,1000)); // chosen offset, not the old obstructed marker
p.space=false;r.Recovery.SpaceCheckMs=0;assert(!mgr.UpdateRecovery(&p,r,1000));p.space=true;p.x=0;
r.Recovery={};travel.status.State=ClientlessTravelState::PathRejected;
assert(!mgr.UpdateRecovery(&p,r,1000));
travel.status.State=ClientlessTravelState::Moving;r.Recovery.ApproachElapsed=60000;
@@ -0,0 +1,51 @@
"""Run the production lie-down footprint against controlled collision geometry."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[2]
code = r'''
#include "AdventureRestSpace.h"
#include <cassert>
using namespace AdventureRestSpace;
int main()
{
Spot p{0,0,10,0};
auto flat=[](float,float,float){return 10.0f;};
auto open=[](float,float,float,float,float,float){return true;};
auto empty=[](float,float,float){return false;};
assert(Clear(p,2,flat,open,empty));
auto platform=[](float,float y,float){return std::fabs(y)<1?10.0f:9.3f;};
auto slope=[](float,float y,float){return 10.0f-std::fabs(y)*0.5f;};
assert(Raised(p,platform));assert(!Raised(p,slope));assert(!Raised(p,flat));
// A chair can be non-colliding: native chair occupancy must still veto it.
auto chair=[](float x,float y,float){return std::fabs(x-1.0f)<0.5f && std::fabs(y)<0.5f;};
assert(!Clear(p,2,flat,open,chair));
// Standing cylinder is clear; a wall at the sleeping body's foot is not.
auto wall=[](float,float,float,float x,float,float){return x<1.5f;};
assert(!Clear(p,2,flat,wall,empty));
auto underTable=[](float,float,float z,float,float,float c){return !(z<10.7f && c>10.7f);};
assert(!Clear(p,2,flat,underTable,empty));
auto ledge=[](float x,float,float){return x>1?8.0f:10.0f;};
assert(!Clear(p,2,ledge,open,empty));
auto stairs=[](float x,float,float){return x>0.5f?10.3f:10.0f;};
assert(!Clear(p,2,stairs,open,empty));
auto missing=[](float,float,float){return -100000.0f;};
assert(!Clear(p,2,missing,open,empty));
auto nan=[](float,float,float){return NAN;};
assert(!Clear(p,2,nan,open,empty));
// Rotate the whole body into a corridor; the unrotated pose hits its wall.
auto corridor=[](float,float,float,float x,float,float){return std::fabs(x)<0.8f;};
assert(!Clear(p,2,flat,corridor,empty));p.O=1.5707963f;
assert(Clear(p,2,flat,corridor,empty));
// A larger model requires proportionally more space.
assert(!Clear(p,4,flat,corridor,empty));
assert(BedName("Fancy Bed") && BedName("Hospital Bed") && BedName("Bedroll"));
assert(!BedName("Flowerbed") && !BedName("Wooden Chair") && !BedName("Bedside Table"));
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'space.cpp';src.write_text(code)
exe=Path(tmp)/'space'
subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-Werror','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed.')
@@ -0,0 +1,88 @@
"""Exercise native sleep-space and candidate adapters with map/object fixtures."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
def function(signature):
start=source.index(signature);body=source.index('{',start);end=body+1;depth=1
while depth:
depth+=(source[end]=='{')-(source[end]=='}');end+=1
return source[start:end]
code=r'''
#include "AdventureRestSpace.h"
#include <vector>
#include <cassert>
#include <cstdint>
using uint32=uint32_t;
#define TC_LOG_INFO(...) ((void)0)
constexpr float INVALID_HEIGHT=-100000;
constexpr int GAMEOBJECT_TYPE_CHAIR=7, LINEOFSIGHT_ALL_CHECKS=0;
namespace VMAP{enum class ModelIgnoreFlags{Nothing};}
struct Position{
float x=0,y=0,z=10,o=0;
Position()=default;Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}
float GetPositionX()const{return x;}float GetPositionY()const{return y;}
float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
float GetExactDist2d(float a,float b)const{return std::hypot(x-a,y-b);}
};
struct GameObject:Position{
std::string name="Wooden Chair";bool phase=true,spawned=true;int type=7;
bool isSpawned(){return spawned;}auto GetName(){return name;}int GetGoType(){return type;}
float GetObjectScale(){return 1;}
};
struct Creature:Position{bool phase=true;float GetBoundaryRadius(){return 0.5f;}};
struct PositionFullTerrainStatus{bool outdoors=false;};
struct Terrain{
bool outdoors=false;
void GetFullTerrainStatusForPosition(int,int,float,float,float,PositionFullTerrainStatus& t){t.outdoors=outdoors;}
};
struct Map{
Terrain terrain;bool water=false,bed=false;float level=10;
float GetHeight(int,float x,float y,float probe,bool,float){
float h=bed && std::fabs(x-4)<2.1f && std::fabs(y)<0.95f?10.7f:level;
return probe>=h?h:level;
}
float GetWaterLevel(int,float,float){return water?12:-100000;}
bool isInLineOfSight(int,float,float,float,float,float,float,int,VMAP::ModelIgnoreFlags){return true;}
Terrain* GetTerrain(){return &terrain;}
};
struct Player:Position{
bool phase=true;Map map;std::vector<GameObject*> objects;std::vector<Player*> people;std::vector<Creature*> creatures;
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
Map* GetMap(){return &map;}int GetPhaseShift(){return 0;}int GetMapId(){return 0;}
float GetCollisionHeight(){return 2;}
template<class T>bool IsInPhase(T* p){return p->phase;}
void GetGameObjectListWithEntryInGrid(std::vector<GameObject*>& v,int,float){v=objects;}
void GetPlayerListInGrid(std::vector<Player*>& v,float){v=people;}
void GetCreatureListWithEntryInGrid(std::vector<Creature*>& v,int,float){v=creatures;}
};
'''
code+=function(' bool SleepSpace(')+'\n'+function(' std::vector<Position> SleepCandidates(')
code+=r'''
int main(){
Player p;Position anchor(0,0,10);GameObject chair;p.objects={&chair};
assert(!SleepSpace(&p,anchor));
chair.phase=false;assert(SleepSpace(&p,anchor));chair.phase=true;
chair.spawned=false;assert(SleepSpace(&p,anchor));chair.spawned=true;
chair.z=3;assert(SleepSpace(&p,anchor));chair.z=10;
auto candidates=SleepCandidates(&p,anchor,true);
assert(!candidates.empty()); // shift off the chair, rather than merely reject the marker
for(auto const& c:candidates){assert(c.GetExactDist2d(0,0)>1);assert(std::fabs(c.z-10)<0.01f);}
p.map.terrain.outdoors=true;assert(SleepCandidates(&p,anchor,true).empty());p.map.terrain.outdoors=false;
p.map.water=true;assert(SleepCandidates(&p,anchor,true).empty());p.map.water=false;
p.map.level=3;assert(SleepCandidates(&p,anchor,true).empty());p.map.level=10; // not the storey below
p.objects.clear();Player visitor;visitor.x=1;p.people={&visitor};assert(!SleepSpace(&p,anchor));p.people.clear();
Creature npc;npc.x=1;p.creatures={&npc};assert(!SleepSpace(&p,anchor));p.creatures.clear();
GameObject bed;bed.name="Hospital Bed";bed.type=5;bed.x=4;p.objects={&bed};p.map.bed=true;
candidates=SleepCandidates(&p,anchor,true);assert(!candidates.empty());
assert(candidates.front().x==4 && std::fabs(candidates.front().z-10.7f)<0.01f); // real bed before floor
p.objects.clear();candidates=SleepCandidates(&p,anchor,true);assert(!candidates.empty());
assert(std::fabs(candidates.front().z-10.7f)<0.01f); // static raised surface before floor
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'targets.cpp';src.write_text(code);exe=Path(tmp)/'targets'
subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-Werror','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed.')