218 lines
17 KiB
Python
218 lines
17 KiB
Python
"""Execute production reaction/death state machine with native-world adapters."""
|
|
from pathlib import Path
|
|
import subprocess,tempfile
|
|
root=Path(__file__).resolve().parents[2]
|
|
source=(root/'src/server/game/World/AdventureCombatMgr.cpp').read_text()
|
|
def function(signature):
|
|
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
|
|
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
|
|
return source[a:e]
|
|
header=(root/'src/server/game/World/AdventureCombatMgr.h').read_text()
|
|
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
|
|
code=r'''
|
|
#include "AdventureCombatPolicy.h"
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <deque>
|
|
#include <vector>
|
|
#include <memory>
|
|
#include <unordered_map>
|
|
#include <algorithm>
|
|
#include <ctime>
|
|
#include <set>
|
|
#include <list>
|
|
constexpr int UNIT_STATE_CASTING=1;
|
|
using uint32=unsigned;using uint8=unsigned char;
|
|
#define TC_GAME_API
|
|
#define TC_LOG_INFO(...) ((void)0)
|
|
#define TC_LOG_WARN(...) ((void)0)
|
|
constexpr int PLAYER_FLAGS=0,PLAYER_FLAGS_GHOST=1,CMSG_REPOP_REQUEST=2,CMSG_RECLAIM_CORPSE=3,CORPSE_RESURRECTABLE_PVP=1;
|
|
constexpr float CORPSE_RECLAIM_RADIUS=39,INVALID_HEIGHT=-100000;
|
|
struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}std::string ToString()const{return std::to_string(id);}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}};
|
|
struct Position{
|
|
float x=0,y=0,z=0;void Relocate(float a,float b,float c){x=a;y=b;z=c;}
|
|
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}
|
|
float GetExactDist(Position const& b)const{return std::sqrt((x-b.x)*(x-b.x)+(y-b.y)*(y-b.y)+(z-b.z)*(z-b.z));}
|
|
float GetExactDist(Position const* b)const{return GetExactDist(*b);}
|
|
};
|
|
struct Map{bool IsDungeon(){return false;}float GetHeight(int,float,float,float,bool,float){return 0;}}map;
|
|
struct Creature;struct Player;
|
|
struct Unit:Position{
|
|
ObjectGuid guid;bool valid=true,alive=true,phase=true,visible=true,inWorld=true;
|
|
virtual unsigned getLevel(){return 60;}
|
|
virtual Creature* ToCreature(){return nullptr;}virtual Player* ToPlayer(){return nullptr;}
|
|
virtual bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}virtual ObjectGuid GetGUID(){return guid;}
|
|
float GetOrientation(){return 0;}Unit*GetVictim(){return nullptr;}Position const& GetPosition()const{return *this;}
|
|
void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}float GetAngle(Player*){return 0;}
|
|
};
|
|
struct Creature:Unit{bool controlled=false,evading=false,boss=false,elite=false;bool isElite(){return elite;}unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
|
|
struct Player;
|
|
struct Corpse:Position{
|
|
time_t released=100;int saves=0;ObjectGuid GetGUID(){return {700};}time_t GetGhostTime(){return released;}int GetType(){return 0;}
|
|
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}Map* GetMap(){return ↦}bool IsWithinDistInMap(Player*,float,bool);void SaveToDB(){++saves;}
|
|
}corpse;
|
|
struct WorldPacket{int opcode;WorldPacket(int o,int):opcode(o){}template<class T>WorldPacket&operator<<(T){return *this;}};
|
|
struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopRequestOpcode(WorldPacket&);void HandleReclaimCorpseOpcode(WorldPacket&);};
|
|
std::vector<Creature*> escapeCreatures;
|
|
struct Group{ObjectGuid leader{26};ObjectGuid GetLeaderGUID(){return leader;}}group;
|
|
struct Player:Unit{
|
|
bool IsResurrectRequested(){return false;}void ResurrectUsingRequestData(){}void ClearResurrectRequestData(){}
|
|
bool IsWithinLOSInMap(Unit*){return los;}bool IsFlying(){return false;}bool IsFalling(){return false;}bool HasUnitState(int){return false;}void GetPartyMembers(std::list<Unit*>&){}bool IsInMap(Unit*){return true;}
|
|
bool grouped=false;float health=100;std::set<Unit*> attackers;
|
|
bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75;
|
|
Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}Group* GetGroup(){return grouped?&group:nullptr;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
|
|
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;}
|
|
uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return ↦}int GetPhaseShift(){return 0;}
|
|
bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}unsigned getClass(){return 1;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
|
|
void SaveToDB(){++saves;}void RepopAtGraveyard(){++graveyards;teleport=true;Relocate(-100,0,0);}void ResurrectPlayer(float hp,bool sick){assert(hp==0.5f);sickness=sick;alive=true;ghost=false;++revives;}void SpawnCorpseBones(){++bones;}
|
|
bool IsHostileTo(Unit*){return true;}float GetHealthPct(){return health;}
|
|
auto const& getAttackers(){return attackers;}
|
|
void GetCreatureListWithEntryInGrid(std::vector<Creature*>&v,int,float){v=escapeCreatures;}
|
|
void GetPlayerListInGrid(std::vector<Player*>&,float){}
|
|
void AttackStop(){}void CastStop(){}void SetStandState(int){}
|
|
struct Motion{void Clear(){}}motion;Motion*GetMotionMaster(){return &motion;}
|
|
void StopMoving(){}
|
|
};
|
|
bool Corpse::IsWithinDistInMap(Player*p,float r,bool){return GetExactDist(p)<=r;}
|
|
void Session::HandleRepopRequestOpcode(WorldPacket&){++releases;owner->ghost=true;owner->Relocate(-100,0,0);}
|
|
void Session::HandleReclaimCorpseOpcode(WorldPacket&){++reclaims;assert(corpse.IsWithinDistInMap(owner,39,true));owner->alive=true;owner->ghost=false;}
|
|
// Update's local combat approach adapter.
|
|
void nearPoint(Creature*,Player*,float&,float&,float&,float,float){}
|
|
struct FakeCreature:Creature{using Creature::Creature;void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=this->x;y=this->y;z=this->z;}float GetAngle(Player*){return 0;}};
|
|
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
|
namespace ObjectAccessor{std::unordered_map<unsigned,Unit*> objects;Unit* GetUnit(Player&,ObjectGuid g){auto i=objects.find(g.id);return i==objects.end()?nullptr:i->second;}}
|
|
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=100;};
|
|
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
|
|
struct TravelStatus{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;};
|
|
struct Travel{
|
|
TravelStatus status;bool present=false,path=true,nearOnly=false;int starts=0,analyses=0;
|
|
auto GetStatus(uint32)->TravelStatus*{return present?&status:nullptr;}
|
|
void CancelTravel(uint32){present=false;}
|
|
bool AnalyzePath(Player*p,Position const&end,ClientlessPathDiagnostics& d){++analyses;d.PathLength=p->GetExactDist(end);return path && (!nearOnly || end.x<-15);}
|
|
bool StartTravel(Player*,Position const&end){++starts;present=true;status.State=ClientlessTravelState::Moving;status.Target=end;return true;}
|
|
}travel;auto sClientlessTravelMgr=&travel;
|
|
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
|
'''
|
|
code+=r'''
|
|
struct Profile{unsigned Class=0,Specialization=0,SoloSpecialization=0;std::string SourcePath;};
|
|
struct Lua{Profile*GetAdventureProfile(unsigned){return nullptr;}void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
|
|
'''
|
|
# production Update uses direct Position members and creature near point.
|
|
code=code.replace('float x=0,y=0,z=0;','union{float x=0;float m_positionX;};union{float y=0;float m_positionY;};union{float z=0;float m_positionZ;};',1)
|
|
code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};', 'bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}float GetAngle(Player*){return 0;}void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}};')
|
|
# Forward declaration needed in Creature's adapter.
|
|
code=code.replace('struct Creature;','struct Creature;struct Player;',1)
|
|
code+=r'''
|
|
struct Encounter{bool Death(Player*){return false;}bool Update(Player*,unsigned){return false;}}encounter;auto sAdventureEncounterMgr=&encounter;
|
|
struct Generator{bool PrepareCombat(Player*,std::string&){return true;}}generator;auto sAdventureGeneratorMgr=&generator;
|
|
namespace AdventureRotationPolicy{unsigned SoloSpec(unsigned,unsigned){return 0;}}
|
|
bool AdventureTank(Unit*){return false;}
|
|
struct PlayerAI{static unsigned GetPlayerSpec(Player*){return 0;}};
|
|
'''
|
|
code+=header+r'''
|
|
class AdventureCombatMgr::Fighter{public:bool Ranged(){return false;}int ticks=0;void Tick(Unit*,uint32){++ticks;}};
|
|
|
|
bool AdventureCombatMgr::GroupHeal(Player*,Record&){return false;}
|
|
bool AdventureCombatMgr::GroupTarget(Player*,Record&){return false;}
|
|
bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;}
|
|
Unit* AdventureCombatMgr::PvPTarget(Player*)const{return nullptr;}
|
|
Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;}
|
|
bool AdventureCombatMgr::Start(Player*,Record&r,Unit*c,bool train,bool defense,time_t until){r.AI=std::make_shared<Fighter>();r.Target=c->GetGUID();r.Training=train;r.Defending=defense;r.Until=until;return true;}
|
|
void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;}
|
|
'''
|
|
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Flee(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
|
|
code+=r'''
|
|
int main(){
|
|
using namespace AdventureCombatPolicy;
|
|
assert(SafeFirstAttack(60,62,60,false,false,1,false,0));
|
|
assert(!SafeFirstAttack(60,63,100,false,false,1,false,0));
|
|
assert(!SafeFirstAttack(60,60,59,false,false,1,false,0));
|
|
assert(!SafeFirstAttack(60,60,100,true,false,1,false,0));
|
|
assert(!SafeFirstAttack(60,60,100,false,true,1,false,0));
|
|
assert(!SafeFirstAttack(60,60,100,false,false,2,false,0));
|
|
assert(!SafeFirstAttack(60,60,100,false,false,1,true,29));
|
|
assert(!CemeteryFallback(399,100,true));assert(CemeteryFallback(400,100,true));
|
|
assert(!CemeteryFallback(5000,100,false));assert(!CemeteryFallback(99,100,true));
|
|
assert(SegmentDistance(5,3,0,0,0,0,10,0,0)==3);
|
|
AdventureCombatMgr mgr;Player p;Creature a(1),b(2),c(3);ObjectAccessor::objects={{1,&a},{2,&b},{3,&c}};
|
|
|
|
Player enemy;enemy.guid={99};ObjectAccessor::objects[99]=&enemy;
|
|
assert(mgr.Valid(&p,&enemy,false));assert(!mgr.Valid(&p,&enemy,true));
|
|
enemy.valid=false;assert(!mgr.Valid(&p,&enemy,false));enemy.valid=true;
|
|
assert(mgr.Valid(&p,&a,false));assert(!mgr.Valid(&p,&a,true));
|
|
Creature dummy(4);dummy.script="npc_training_dummy";assert(mgr.Valid(&p,&dummy,true));assert(!mgr.Valid(&p,&dummy,false));
|
|
dummy.level=63;assert(!mgr.Valid(&p,&dummy,true));dummy.level=60;
|
|
dummy.boss=true;assert(!mgr.Valid(&p,&dummy,true));dummy.boss=false;
|
|
dummy.phase=false;assert(!mgr.Valid(&p,&dummy,true));dummy.phase=true;
|
|
dummy.visible=false;assert(!mgr.Valid(&p,&dummy,true));dummy.visible=true;
|
|
dummy.controlled=true;assert(!mgr.Valid(&p,&dummy,true));dummy.controlled=false;
|
|
dummy.valid=false;assert(!mgr.Valid(&p,&dummy,true));
|
|
auto&r=mgr._records[26];
|
|
mgr.DamageTaken(&p,&enemy,1);assert(r.Damagers.front().id==99);r.Damagers.clear();
|
|
mgr.DamageTaken(&p,&a,0);assert(r.Damagers.empty());
|
|
mgr.DamageTaken(&p,&a,5);mgr.DamageTaken(&p,&b,5);mgr.Update(&p,500);
|
|
assert(r.Target.id==1 && r.Defending); // first actual damager
|
|
mgr.DamageTaken(&p,&c,5);mgr.Update(&p,500);assert(r.Target.id==1);
|
|
a.alive=false;mgr.Update(&p,500);assert(!r.AI);mgr.Update(&p,500);assert(r.Target.id==2);
|
|
mgr.Finish(&p,r);r.Damagers.clear();mgr.Start(&p,r,&c,true,false,500);
|
|
mgr.DamageTaken(&p,&b,1);mgr.Update(&p,500);assert(r.Target.id==2 && r.Defending && !r.Training);
|
|
// Actual player attackers use the same first-damager focus and native PvP permission.
|
|
mgr.Finish(&p,r);r.Damagers.clear();mgr.DamageTaken(&p,&enemy,1);mgr.Update(&p,500);
|
|
assert(r.Target.id==99 && r.Defending);mgr.DamageTaken(&p,&c,1);mgr.Update(&p,500);assert(r.Target.id==99);
|
|
enemy.valid=false;mgr.Update(&p,500);assert(!r.AI);r.Damagers.clear();
|
|
// Release through native handler and save ghost plus corpse timestamp.
|
|
mgr.Finish(&p,r);p.alive=false;p.ghost=false;r.Dead=false;mgr.Death(&p,r);
|
|
assert(p.ghost && p.session.releases==1 && p.saves==1 && corpse.saves==1);
|
|
// Native reclaim radius and native delay both apply.
|
|
p.Relocate(38,0,0);GameTime::now=129;mgr.Death(&p,r);assert(p.session.reclaims==0);
|
|
GameTime::now=130;mgr.Death(&p,r);assert(p.alive && p.session.reclaims==1);
|
|
// Exact corpse point inaccessible, near-side point in legal radius reachable.
|
|
p.alive=false;p.ghost=true;p.Relocate(-100,0,0);travel={};travel.nearOnly=true;r.NextCorpseSearch=0;
|
|
GameTime::now=500;mgr.Death(&p,r);assert(travel.starts==1 && travel.status.Target.x<-15);
|
|
assert(p.graveyards==0 && !p.alive); // time alone never revives reachable ghost
|
|
GameTime::now=900;mgr.Death(&p,r);assert(p.graveyards==0);
|
|
// No path: wait all five minutes from original native persisted ghost time.
|
|
travel={};travel.path=false;r.NextCorpseSearch=0;GameTime::now=399;mgr.Death(&p,r);assert(!p.alive && p.graveyards==0);
|
|
// Simulate process restart: volatile record gone; corpse timestamp survives.
|
|
AdventureCombatMgr::Record resumed;resumed.Dead=true;GameTime::now=400;mgr.Death(&p,resumed);
|
|
assert(p.graveyards==1 && p.teleport && !p.alive && resumed.CemeteryPending);
|
|
p.teleport=false;mgr.Death(&p,resumed);assert(p.alive && !p.sickness && p.revives==1 && p.bones==1 && p.durability==75);
|
|
// Flight boundaries, actual threats, group override and blocked fallback.
|
|
assert(!ShouldFlee(false,20,60,65,false,false));
|
|
assert(ShouldFlee(false,19,60,60,false,false));
|
|
assert(ShouldFlee(false,100,60,66,false,false));
|
|
assert(ShouldFlee(false,100,60,60,true,false));
|
|
assert(!ShouldFlee(false,100,60,59,true,false));
|
|
assert(!ShouldFlee(true,1,60,90,true,true));
|
|
p.alive=true;p.ghost=false;p.Relocate(0,0,0);p.combat=true;p.health=100;
|
|
r={};a.alive=true;a.valid=true;a.level=66;a.Relocate(1,0,0);p.attackers={&a};travel={};GameTime::now=1000;
|
|
mgr.Start(&p,r,&a,false,true,0);
|
|
assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==1);
|
|
assert(travel.status.Target.GetExactDist(a)>p.GetExactDist(a)+3);
|
|
// Reject the direct escape corridor if an additional hostile blocks it.
|
|
Creature obstacle(100);obstacle.Relocate(-15,0,0);escapeCreatures={&obstacle};
|
|
GameTime::now=1002;mgr.Flee(&p,r);
|
|
assert(SegmentDistance(-15,0,0,0,0,0,travel.status.Target.x,travel.status.Target.y,travel.status.Target.z)>=12);
|
|
escapeCreatures.clear();
|
|
// Group join aborts immediately, even while critically injured.
|
|
p.grouped=true;p.health=1;assert(!mgr.Flee(&p,r)&&!r.Fleeing&&!travel.present);
|
|
assert(!mgr.Flee(&p,r));p.grouped=false;p.health=100;
|
|
// Blocked paths preserve native self-defence; no teleport/resurrection.
|
|
travel={};travel.path=false;GameTime::now=1004;
|
|
int ticks=r.AI->ticks;assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==0&&r.AI->ticks>ticks);
|
|
int oldRevives=p.revives,oldGraves=p.graveyards;
|
|
// Ending native combat alone is insufficient while still close to danger.
|
|
p.combat=false;p.attackers.clear();r.Defending=false;
|
|
GameTime::now=1006;assert(mgr.Flee(&p,r)&&r.Fleeing);
|
|
p.Relocate(-60,0,0);GameTime::now=1008;assert(mgr.Flee(&p,r)&&!r.Fleeing&&!r.AI);
|
|
assert(p.revives==oldRevives&&p.graveyards==oldGraves);
|
|
// No aggression means no flight solely from low health.
|
|
r={};p.health=1;assert(!mgr.Flee(&p,r));
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p=Path(d);(p/'test.cpp').write_text(code)
|
|
subprocess.run(['g++','-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 combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.')
|