Add persistent instance visits and validate sleeping spaces
This commit is contained in:
1 parent
440c53acd7
commit
e18a416bf4
22 files changed
+1631
-41
No files matched your search
@@ -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;}
|
||||
|
||||
@@ -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.')
|
||||
@@ -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 ↦}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.')
|
||||
Reference in new issue
Block a user