165 lines
7.0 KiB
C++
165 lines
7.0 KiB
C++
// Compile and execute the shipped server command against a small server API fixture.
|
||
#ifdef NDEBUG
|
||
#undef NDEBUG
|
||
#endif
|
||
#include <cassert>
|
||
#include <cstdarg>
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <map>
|
||
#include <string>
|
||
#include <vector>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
using DWORD = uint32_t;
|
||
constexpr int ENTITY_CHARACTER=1, ATTR_BANPK=4, MOB_RANK_BOSS=4, CHAT_TYPE_COMMAND=5, SECTREE_SIZE=6400;
|
||
struct Character;
|
||
using LPCHARACTER=Character*;
|
||
struct Entity { virtual ~Entity() {} bool IsType(int) { return true; } };
|
||
using LPENTITY=Entity*;
|
||
struct Party {};
|
||
struct Sectree {
|
||
bool safe=false;
|
||
std::vector<Entity*> entities;
|
||
DWORD GetAttribute(long,long) { return safe ? ATTR_BANPK : 0; }
|
||
template<class F> void for_each_entity(F& f) { for(auto* e:entities) f(e); }
|
||
template<class F> void ForEachAround(F& f) { for(auto* e:entities) f(e); }
|
||
};
|
||
struct Character: Entity {
|
||
DWORD vid=0;
|
||
int x=0,y=0,rank=0,hp=100,maxhp=100;
|
||
bool pc=false,dead=false,stun=false,canHandle=true;
|
||
Sectree* sec=nullptr;
|
||
Party* party=nullptr;
|
||
Character* victim=nullptr;
|
||
int next=0, beginCount=0;
|
||
std::vector<std::string> replies;
|
||
bool IsMonster() { return !pc; }
|
||
bool IsPC() { return pc; }
|
||
bool IsDead() { return dead; }
|
||
bool IsStun() { return stun; }
|
||
bool CanHandleItem() { return canHandle; }
|
||
bool CanBeginFight() { return !victim && !dead && !stun; }
|
||
int GetHP() { return hp; }
|
||
int GetMaxHP() { return maxhp; }
|
||
int GetX() { return x; }
|
||
int GetY() { return y; }
|
||
int GetMapIndex() { return 1; }
|
||
DWORD GetPlayerID() { return vid; }
|
||
int GetMobRank() { return rank; }
|
||
Sectree* GetSectree() { return sec; }
|
||
Party* GetParty() { return party; }
|
||
DWORD GetVID() { return vid; }
|
||
Character* GetVictim() { return victim; }
|
||
int GetQuestFlag(const char*) { return next; }
|
||
void SetQuestFlag(const char*,int value) { next=value; }
|
||
void BeginFight(Character* owner) { victim=owner; ++beginCount; }
|
||
void ChatPacket(int,const char* format,...) {
|
||
char buf[256]; va_list ap; va_start(ap,format); vsnprintf(buf,sizeof(buf),format,ap); va_end(ap);
|
||
replies.emplace_back(buf);
|
||
}
|
||
};
|
||
struct CHARACTER_MANAGER {
|
||
std::map<DWORD,Character*> mobs;
|
||
static CHARACTER_MANAGER& instance() { static CHARACTER_MANAGER value; return value; }
|
||
Character* Find(DWORD vid) { return mobs[vid]; }
|
||
};
|
||
using LPSECTREE=Sectree*;
|
||
struct SectreeMap {
|
||
std::map<std::pair<int,int>, Sectree*> cells;
|
||
template<class F> void for_each_in_rect(int x0,int y0,int x1,int y1,F& f) {
|
||
for (const auto& cell:cells)
|
||
if (cell.first.first>=x0/SECTREE_SIZE && cell.first.first<=x1/SECTREE_SIZE &&
|
||
cell.first.second>=y0/SECTREE_SIZE && cell.first.second<=y1/SECTREE_SIZE)
|
||
cell.second->for_each_entity(f);
|
||
}
|
||
Sectree* Find(int x,int y) {
|
||
auto it=cells.find({x/SECTREE_SIZE,y/SECTREE_SIZE});
|
||
return it==cells.end() ? nullptr : it->second;
|
||
}
|
||
};
|
||
using LPSECTREE_MAP=SectreeMap*;
|
||
struct SECTREE_MANAGER {
|
||
SectreeMap map;
|
||
static SECTREE_MANAGER& instance() { static SECTREE_MANAGER m; return m; }
|
||
SectreeMap* GetMap(int) { return ↦ }
|
||
};
|
||
int now=100;
|
||
int get_global_time() { return now; }
|
||
#define DISTANCE_APPROX(x,y) int(std::hypot(x,y))
|
||
#define ACMD(name) void name(LPCHARACTER ch,const char* argument)
|
||
#define MT_AUTOHUNT_SERVER_TEST
|
||
#include "../../tools/server/autohunt-pull/autohunt_pull.h"
|
||
|
||
int main() {
|
||
Sectree sec;
|
||
Character owner; owner.pc=true; owner.sec=&sec; owner.vid=1;
|
||
SECTREE_MANAGER::instance().map.cells[{0,0}]=&sec;
|
||
Character other; other.pc=true;
|
||
std::vector<Party> parties(25);
|
||
std::vector<Character> mobs(50);
|
||
for(size_t i=0;i<mobs.size();++i) {
|
||
auto& mob=mobs[i]; mob.vid=100+i; mob.x=100+int(i/2)*100; mob.sec=&sec; mob.party=&parties[i/2];
|
||
sec.entities.push_back(&mob); CHARACTER_MANAGER::instance().mobs[mob.vid]=&mob;
|
||
}
|
||
// A group owned by another player and a boss group are both excluded as whole groups.
|
||
mobs[0].victim=&other;
|
||
mobs[2].rank=MOB_RANK_BOSS;
|
||
do_autohunt_pull(&owner,"3 5000 0 7");
|
||
assert(owner.replies.back()=="AutoHuntGather 7 done 3");
|
||
assert(owner.replies.size()==7); // 3 actual parties × 2 members, then completion
|
||
for(int i=4;i<10;++i) assert(mobs[i].victim==&owner && mobs[i].beginCount==1);
|
||
assert(!mobs[1].victim && !mobs[3].victim && !mobs[10].victim);
|
||
owner.replies.clear();
|
||
do_autohunt_pull(&owner,"20 5000 1 8");
|
||
assert(owner.replies.size()==1 && owner.replies.back()=="AutoHuntGather 8 done 0"); // cooldown
|
||
now+=10;
|
||
owner.replies.clear();
|
||
do_autohunt_pull(&owner,"20 5000 1 9");
|
||
assert(owner.replies.back()=="AutoHuntGather 9 done 20");
|
||
assert(owner.replies.size()==41);
|
||
assert(mobs[4].beginCount==1); // already on this player; no redundant restart
|
||
now+=10; owner.replies.clear(); owner.hp=49;
|
||
do_autohunt_pull(&owner,"10 5000 1 10");
|
||
assert(owner.replies.back()=="AutoHuntGather 10 done 0");
|
||
owner.hp=100; sec.safe=true; owner.replies.clear();
|
||
do_autohunt_pull(&owner,"10 5000 1 11");
|
||
assert(owner.replies.back()=="AutoHuntGather 11 done 0");
|
||
sec.safe=false; owner.replies.clear();
|
||
do_autohunt_pull(&owner,"21 5000 0 12");
|
||
do_autohunt_pull(&owner,"3 15001 0 12");
|
||
do_autohunt_pull(&owner,"3 5000 0 12 extra");
|
||
assert(owner.replies.empty());
|
||
// Large ranges visit distant sectors and extend only the selected owner's pursuit.
|
||
Sectree distant;
|
||
Character far; far.vid=1000; far.x=14000; far.sec=&distant;
|
||
distant.entities.push_back(&far);
|
||
SECTREE_MANAGER::instance().map.cells[{2,0}]=&distant;
|
||
CHARACTER_MANAGER::instance().mobs[far.vid]=&far;
|
||
sec.entities.clear(); now+=10;
|
||
do_autohunt_pull(&owner,"20 10000 0 13");
|
||
assert(owner.replies.back()=="AutoHuntGather 13 done 0" && !far.victim);
|
||
now+=10;
|
||
do_autohunt_pull(&owner,"20 15000 0 14");
|
||
assert(owner.replies.back()=="AutoHuntGather 14 done 1" && far.victim==&owner);
|
||
assert(AutoHuntPullLeash(&far,&owner)==15001);
|
||
assert(AutoHuntPullLeash(&mobs[49],&owner)==4000);
|
||
do_autohunt_pull(&owner,"0 15000 0 15");
|
||
assert(AutoHuntPullLeash(&far,&owner)==4000);
|
||
now+=10; do_autohunt_pull(&owner,"3 15000 0 16");
|
||
assert(AutoHuntPullLeash(&far,&other)==4000);
|
||
now+=10; do_autohunt_pull(&owner,"3 15000 0 17");
|
||
now+=180;
|
||
assert(AutoHuntPullLeash(&far,&owner)==4000);
|
||
// Unequal group sizes, exact 10-group cap and nearest ordering independent of input order.
|
||
std::vector<mt_autohunt::Candidate> candidates;
|
||
for(int g=24;g>=0;--g)
|
||
for(int m=0;m<g%4+1;++m) candidates.push_back({uint64_t(g),uint32_t(g*10+m),g*100,true});
|
||
auto ten=mt_autohunt::SelectGroups(candidates,10);
|
||
assert(ten.groups==10 && ten.mobs.front()==0);
|
||
for(auto vid:ten.mobs) assert(vid/10 < 10);
|
||
auto sparse=mt_autohunt::SelectGroups({{1,1,100,true},{1,2,110,true},{2,3,50,true}},20);
|
||
assert(sparse.groups==2 && sparse.mobs.size()==3 && sparse.mobs[0]==3);
|
||
puts("autohunt_pull_test: PASS");
|
||
}
|