Add free cloak-style auto-hunt pull for 3 10 or 20 monster groups
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// 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;
|
||||
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 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; }
|
||||
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]; }
|
||||
};
|
||||
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;
|
||||
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 5001 0 12");
|
||||
do_autohunt_pull(&owner,"3 5000 0 12 extra");
|
||||
assert(owner.replies.empty());
|
||||
// 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");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// PORT extension test: real Python settings → CG_CHAT → GC_CHAT → player state.
|
||||
if (server && CPythonCharacterManager::Instance().GetMainInstancePtr()) {
|
||||
auto* main_instance = CPythonCharacterManager::Instance().GetMainInstancePtr();
|
||||
CPythonPlayer& player = CPythonPlayer::Instance();
|
||||
const auto savedConfig = player.AutoHunt_GetConfig();
|
||||
const int hp = player.GetStatus(POINT_HP), maxHP = player.GetStatus(POINT_MAX_HP);
|
||||
player.SetStatus(POINT_HP, 1000);
|
||||
player.SetStatus(POINT_MAX_HP, 1000);
|
||||
CHECK(py("import player; player.AutoHuntSetConfig(5000, 0, 0, 0, 0, 0, 0, 20)"));
|
||||
CHECK(player.AutoHunt_GetConfig().iGatherCount == 20);
|
||||
TPixelPosition savedPosition = {};
|
||||
main_instance->NEW_GetPixelPosition(&savedPosition);
|
||||
auto* pullMob = CPythonCharacterManager::Instance().GetInstancePtr(FakeLoginServer::kMobVID);
|
||||
TPixelPosition pullPosition = {};
|
||||
pullMob->NEW_GetPixelPosition(&pullPosition);
|
||||
pullPosition.x += 1000;
|
||||
main_instance->NEW_Stop();
|
||||
main_instance->NEW_SetPixelPosition(pullPosition);
|
||||
CHECK(!main_instance->IsInSafe());
|
||||
CHECK(py("player.AutoHuntSetConfig(5000, 0, 0, 0, 0, 0, 0, 3); player.AutoHuntSetEnabled(1)"));
|
||||
TPixelPosition origin = {};
|
||||
main_instance->NEW_GetPixelPosition(&origin);
|
||||
CHECK(pump_until(2, [&] {
|
||||
return server->Has("game2:chat 0 /autohunt_pull 3 5000 0 1") &&
|
||||
player.AutoHunt_GetGatheredCount() == 1 && !player.AutoHunt_IsGathering();
|
||||
}));
|
||||
TPixelPosition current = {};
|
||||
main_instance->NEW_GetPixelPosition(¤t);
|
||||
CHECK(std::hypot(current.x-origin.x, current.y-origin.y) < 5.0f);
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
CHECK(player.AutoHunt_HandleServerCommand("AutoHuntGather 1 mob 123"));
|
||||
CHECK(player.AutoHunt_GetGatheredCount() == 0);
|
||||
// Old replies must not complete a new request; cooldown remains across toggles.
|
||||
player.AutoHunt_SetEnabled(true);
|
||||
CHECK(player.AutoHunt_HandleServerCommand("AutoHuntGather 1 done 20"));
|
||||
CHECK(player.AutoHunt_GetGatheredCount() == 0);
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
player.AutoHunt_SetConfig(savedConfig);
|
||||
player.SetStatus(POINT_HP, hp);
|
||||
player.SetStatus(POINT_MAX_HP, maxHP);
|
||||
main_instance->NEW_Stop();
|
||||
main_instance->NEW_SetPixelPosition(savedPosition);
|
||||
pump_frames(30); // restore the camera/picking matrices before the existing combat fixture
|
||||
}
|
||||
@@ -864,6 +864,17 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
return Fail(name + ": send GC_SAFEBOX_SET");
|
||||
}
|
||||
}
|
||||
else if (text.rfind("/autohunt_pull ", 0) == 0) {
|
||||
int groups = 0, radius = 0, bosses = 0;
|
||||
unsigned request = 0;
|
||||
if (sscanf(text.c_str() + 15, "%d %d %d %u", &groups, &radius, &bosses, &request) != 4)
|
||||
return Fail(name + ": malformed autohunt pull");
|
||||
for (const std::string& cmd : {"AutoHuntGather " + std::to_string(request) + " mob " + std::to_string(kMobVID),
|
||||
"AutoHuntGather " + std::to_string(request) + " done 1"}) {
|
||||
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + cmd.size() + 1), 5, 0, 1};
|
||||
if (!c.Send(chat) || !c.Send(cmd.c_str(), cmd.size() + 1)) return Fail(name + ": autohunt pull reply");
|
||||
}
|
||||
}
|
||||
else if (text == "/safebox_close")
|
||||
{
|
||||
Event(name + ":safebox_close");
|
||||
|
||||
@@ -744,6 +744,7 @@ int main(int argc, char** argv)
|
||||
std::printf("port_login_flow_test: %s\n", e.c_str());
|
||||
}
|
||||
}
|
||||
#include "autohunt_pull_checks.inc"
|
||||
// 9. (批次 4-b) Combat: a left click on the stray dog reaches CPythonPlayer::__OnClickActor, which targets it
|
||||
// (CG_TARGET → GC_TARGET, uitarget shows the HP board) and walks into range to attack. Each attack frame's
|
||||
// hit (CPythonPlayerEventHandler::OnHit) sends CG_ATTACK; the fake server answers GC_DAMAGE_INFO + GC_TARGET,
|
||||
|
||||
Reference in New Issue
Block a user