Add free cloak-style auto-hunt pull for 3 10 or 20 monster groups
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# 自动狩猎的披风式引怪
|
||||
|
||||
用户授权的玩法扩展:自动狩猎自带,不需要或消耗隐月披风;选项为关、3 组、10 组、20 组。
|
||||
客户端不移动怪物或伪造命中,服务端使用披风 `AggregateMonster` 中的 `BeginFight` 路径建立追击。
|
||||
原披风的道具消耗和随机拉怪行为保持不变;本功能按距离确定性选择最多 N 个队伍。
|
||||
|
||||
“组”是服务端实际的怪物 `CParty`,没有队伍的单只怪物算一组。同一队伍在范围内的成员一起拉取。
|
||||
不足所选组数时拉取实际可用的组数。范围使用自动狩猎的范围设置,上限 5000 cm。
|
||||
安全区、死亡/眩晕、低于 50% HP 或不能操作道具时不引怪;默认不拉首领。
|
||||
队伍在范围内存在首领或正在攻击其他玩家的成员时,整组跳过。
|
||||
|
||||
请求 `/autohunt_pull <3|10|20> <500..5000> <boss:0|1> <request-id>` 走现有 CG_CHAT。
|
||||
服务端每次发 `AutoHuntGather <id> mob <vid>`,最后发 `AutoHuntGather <id> done <groups>`,
|
||||
走现有 CHAT_TYPE_COMMAND。每个消息很短,不受一条聊天封包长度限制。
|
||||
客户端只接受当前请求的回包;停用、手动暂停后旧回包不生效。
|
||||
双方有 10 秒冷却,服务端冷却以角色任务标记保存,重新登录或开关狩猎不能绕过。
|
||||
客户端原地等待追击怪物进入近战距离,最多等待 8 秒;3 秒无回包则继续普通狩猎。
|
||||
|
||||
## 构建和回滚
|
||||
|
||||
`server.patch` 以 40250 的 `/usr/metin2/src` 为根,`patch -p1` 应用。
|
||||
`autohunt_pull.h` 和 `group_select.h` 是补丁中两份新增头文件的源文件。
|
||||
`../rebuild.sh` 已加入本补丁,沿用 build / deploy 流程。
|
||||
|
||||
2026-09-30 已在 192.168.21.203 编译并部署。原 `cmd.cpp`、`cmd_general.cpp`、旧 game
|
||||
备份在 `/root/autohunt-pull-20260930/`(旧程序为 `game.before`)。回滚时停 game、恢复旧程序后启动;
|
||||
不需要改数据库结构。原披风物品逻辑没有改动。
|
||||
|
||||
验证:`host.autohunt_pull` 直接编译本命令,检查 3/10/20 组、队伍成员数、排序、冷却、安全区、
|
||||
低血量和其他玩家目标过滤。`port.login_flow` 通过真实 Python 设置和聊天封包验证请求、回包及原地等待,
|
||||
桌面和移动模式均通过。真服临时角色实测回包为 **3 组、9 只怪物**,随后进入自动战斗。
|
||||
临时角色、账号和关联数据已在刷新对应 DB 缓存后清理;auth/game/db 监听已检查。
|
||||
macOS 原生包及 Android debug APK 已重新构建。
|
||||
@@ -0,0 +1,59 @@
|
||||
// PORT extension: free cloak-style auto-hunt pull, using the original BeginFight path.
|
||||
#ifdef MT_AUTOHUNT_SERVER_TEST
|
||||
#include "group_select.h"
|
||||
#else
|
||||
#include "sectree_manager.h"
|
||||
#include "autohunt_group_select.h"
|
||||
#endif
|
||||
|
||||
struct AutoHuntPullCollector {
|
||||
LPCHARACTER owner;
|
||||
int radius;
|
||||
bool bosses;
|
||||
std::vector<mt_autohunt::Candidate> candidates;
|
||||
AutoHuntPullCollector(LPCHARACTER ch, int range, bool boss) : owner(ch), radius(range), bosses(boss) {}
|
||||
void operator()(LPENTITY ent) {
|
||||
if (!ent->IsType(ENTITY_CHARACTER)) return;
|
||||
LPCHARACTER mob = (LPCHARACTER)ent;
|
||||
if (!mob->IsMonster() || mob->IsPC() || mob->IsDead() || mob->GetMapIndex() != owner->GetMapIndex()) return;
|
||||
const int distance = DISTANCE_APPROX(mob->GetX() - owner->GetX(), mob->GetY() - owner->GetY());
|
||||
if (distance > radius) return;
|
||||
const bool safe = !mob->GetSectree() ||
|
||||
(mob->GetSectree()->GetAttribute(mob->GetX(), mob->GetY()) & ATTR_BANPK);
|
||||
mt_autohunt::Candidate c;
|
||||
c.group = mob->GetParty() ? uint64_t(uintptr_t(mob->GetParty())) << 1 : (uint64_t((DWORD)mob->GetVID()) << 1) | 1;
|
||||
c.vid = (DWORD)mob->GetVID();
|
||||
c.distance = distance;
|
||||
c.eligible = !safe && (bosses || mob->GetMobRank() < MOB_RANK_BOSS) &&
|
||||
(!mob->GetVictim() || mob->GetVictim() == owner) && (mob->GetVictim() == owner || mob->CanBeginFight());
|
||||
candidates.push_back(c);
|
||||
}
|
||||
};
|
||||
|
||||
ACMD(do_autohunt_pull) {
|
||||
int groups = 0, radius = 0, bosses = 0;
|
||||
unsigned int request = 0;
|
||||
char extra = 0;
|
||||
if (sscanf(argument, "%d %d %d %u %c", &groups, &radius, &bosses, &request, &extra) != 4 ||
|
||||
(groups != 3 && groups != 10 && groups != 20) || radius < 500 || radius > 5000 ||
|
||||
(bosses != 0 && bosses != 1)) return;
|
||||
const int now = get_global_time();
|
||||
if (!ch->IsPC() || ch->IsDead() || ch->IsStun() || !ch->CanHandleItem() || !ch->GetSectree() ||
|
||||
(ch->GetSectree()->GetAttribute(ch->GetX(), ch->GetY()) & ATTR_BANPK) ||
|
||||
ch->GetHP() < ch->GetMaxHP() / 2 || now < ch->GetQuestFlag("autohunt.next_pull")) {
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u done 0", request);
|
||||
return;
|
||||
}
|
||||
// Server-side cooldown survives reconnects and switching auto hunt on/off.
|
||||
ch->SetQuestFlag("autohunt.next_pull", now + 10);
|
||||
AutoHuntPullCollector collector(ch, radius, bosses != 0);
|
||||
ch->GetSectree()->ForEachAround(collector);
|
||||
const mt_autohunt::Selection selected = mt_autohunt::SelectGroups(collector.candidates, groups);
|
||||
for (size_t i = 0; i < selected.mobs.size(); ++i) {
|
||||
LPCHARACTER mob = CHARACTER_MANAGER::instance().Find(selected.mobs[i]);
|
||||
if (!mob) continue;
|
||||
if (!mob->GetVictim()) mob->BeginFight(ch);
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u mob %u", request, selected.mobs[i]);
|
||||
}
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u done %d", request, selected.groups);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef MT_AUTOHUNT_GROUP_SELECT_H
|
||||
#define MT_AUTOHUNT_GROUP_SELECT_H
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mt_autohunt {
|
||||
struct Candidate {
|
||||
uint64_t group; // actual server party, or a unique key for an ungrouped monster
|
||||
uint32_t vid;
|
||||
int distance;
|
||||
bool eligible;
|
||||
};
|
||||
struct Selection {
|
||||
int groups;
|
||||
std::vector<uint32_t> mobs;
|
||||
Selection() : groups(0) {}
|
||||
};
|
||||
inline Selection SelectGroups(const std::vector<Candidate>& candidates, int count) {
|
||||
Selection out;
|
||||
if (count != 3 && count != 10 && count != 20) return out;
|
||||
struct Group {
|
||||
uint64_t key;
|
||||
int distance;
|
||||
bool eligible;
|
||||
std::vector<uint32_t> mobs;
|
||||
Group() : key(0), distance(2147483647), eligible(true) {}
|
||||
};
|
||||
std::map<uint64_t, Group> groups;
|
||||
for (size_t i = 0; i < candidates.size(); ++i) {
|
||||
const Candidate& c = candidates[i];
|
||||
Group& g = groups[c.group];
|
||||
g.key = c.group;
|
||||
g.distance = std::min(g.distance, c.distance);
|
||||
g.eligible = g.eligible && c.eligible;
|
||||
g.mobs.push_back(c.vid);
|
||||
}
|
||||
std::vector<std::pair<int, uint64_t> > ordered;
|
||||
for (std::map<uint64_t, Group>::const_iterator i = groups.begin(); i != groups.end(); ++i)
|
||||
if (i->second.eligible) ordered.push_back(std::make_pair(i->second.distance, i->first));
|
||||
std::sort(ordered.begin(), ordered.end());
|
||||
for (size_t i = 0; i < ordered.size() && out.groups < count; ++i) {
|
||||
const Group& g = groups[ordered[i].second];
|
||||
out.mobs.insert(out.mobs.end(), g.mobs.begin(), g.mobs.end());
|
||||
++out.groups;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,145 @@
|
||||
--- a/server/game/src/cmd.cpp
|
||||
+++ b/server/game/src/cmd.cpp
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "log.h"
|
||||
#include "desc.h"
|
||||
|
||||
+ACMD(do_autohunt_pull);
|
||||
ACMD(do_user_horse_ride);
|
||||
ACMD(do_user_horse_back);
|
||||
ACMD(do_user_horse_feed);
|
||||
@@ -279,6 +280,7 @@
|
||||
struct command_info cmd_info[] =
|
||||
{
|
||||
{ "!RESERVED!", NULL, 0, POS_DEAD, GM_IMPLEMENTOR }, /* 반드시 이 것이 처음이어야 한다. */
|
||||
+ { "autohunt_pull", do_autohunt_pull, 0, POS_STANDING, GM_PLAYER },
|
||||
{ "who", do_who, 0, POS_DEAD, GM_IMPLEMENTOR },
|
||||
{ "war", do_war, 0, POS_DEAD, GM_PLAYER },
|
||||
{ "warp", do_warp, 0, POS_DEAD, GM_LOW_WIZARD },
|
||||
--- a/server/game/src/cmd_general.cpp
|
||||
+++ b/server/game/src/cmd_general.cpp
|
||||
@@ -37,6 +37,8 @@
|
||||
#ifdef __AUCTION__
|
||||
#include "auction_manager.h"
|
||||
#endif
|
||||
+
|
||||
+#include "autohunt_pull.h"
|
||||
|
||||
extern int g_server_id;
|
||||
|
||||
--- /dev/null
|
||||
+++ b/server/game/src/autohunt_pull.h
|
||||
@@ -0,0 +1,59 @@
|
||||
+// PORT extension: free cloak-style auto-hunt pull, using the original BeginFight path.
|
||||
+#ifdef MT_AUTOHUNT_SERVER_TEST
|
||||
+#include "group_select.h"
|
||||
+#else
|
||||
+#include "sectree_manager.h"
|
||||
+#include "autohunt_group_select.h"
|
||||
+#endif
|
||||
+
|
||||
+struct AutoHuntPullCollector {
|
||||
+ LPCHARACTER owner;
|
||||
+ int radius;
|
||||
+ bool bosses;
|
||||
+ std::vector<mt_autohunt::Candidate> candidates;
|
||||
+ AutoHuntPullCollector(LPCHARACTER ch, int range, bool boss) : owner(ch), radius(range), bosses(boss) {}
|
||||
+ void operator()(LPENTITY ent) {
|
||||
+ if (!ent->IsType(ENTITY_CHARACTER)) return;
|
||||
+ LPCHARACTER mob = (LPCHARACTER)ent;
|
||||
+ if (!mob->IsMonster() || mob->IsPC() || mob->IsDead() || mob->GetMapIndex() != owner->GetMapIndex()) return;
|
||||
+ const int distance = DISTANCE_APPROX(mob->GetX() - owner->GetX(), mob->GetY() - owner->GetY());
|
||||
+ if (distance > radius) return;
|
||||
+ const bool safe = !mob->GetSectree() ||
|
||||
+ (mob->GetSectree()->GetAttribute(mob->GetX(), mob->GetY()) & ATTR_BANPK);
|
||||
+ mt_autohunt::Candidate c;
|
||||
+ c.group = mob->GetParty() ? uint64_t(uintptr_t(mob->GetParty())) << 1 : (uint64_t((DWORD)mob->GetVID()) << 1) | 1;
|
||||
+ c.vid = (DWORD)mob->GetVID();
|
||||
+ c.distance = distance;
|
||||
+ c.eligible = !safe && (bosses || mob->GetMobRank() < MOB_RANK_BOSS) &&
|
||||
+ (!mob->GetVictim() || mob->GetVictim() == owner) && (mob->GetVictim() == owner || mob->CanBeginFight());
|
||||
+ candidates.push_back(c);
|
||||
+ }
|
||||
+};
|
||||
+
|
||||
+ACMD(do_autohunt_pull) {
|
||||
+ int groups = 0, radius = 0, bosses = 0;
|
||||
+ unsigned int request = 0;
|
||||
+ char extra = 0;
|
||||
+ if (sscanf(argument, "%d %d %d %u %c", &groups, &radius, &bosses, &request, &extra) != 4 ||
|
||||
+ (groups != 3 && groups != 10 && groups != 20) || radius < 500 || radius > 5000 ||
|
||||
+ (bosses != 0 && bosses != 1)) return;
|
||||
+ const int now = get_global_time();
|
||||
+ if (!ch->IsPC() || ch->IsDead() || ch->IsStun() || !ch->CanHandleItem() || !ch->GetSectree() ||
|
||||
+ (ch->GetSectree()->GetAttribute(ch->GetX(), ch->GetY()) & ATTR_BANPK) ||
|
||||
+ ch->GetHP() < ch->GetMaxHP() / 2 || now < ch->GetQuestFlag("autohunt.next_pull")) {
|
||||
+ ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u done 0", request);
|
||||
+ return;
|
||||
+ }
|
||||
+ // Server-side cooldown survives reconnects and switching auto hunt on/off.
|
||||
+ ch->SetQuestFlag("autohunt.next_pull", now + 10);
|
||||
+ AutoHuntPullCollector collector(ch, radius, bosses != 0);
|
||||
+ ch->GetSectree()->ForEachAround(collector);
|
||||
+ const mt_autohunt::Selection selected = mt_autohunt::SelectGroups(collector.candidates, groups);
|
||||
+ for (size_t i = 0; i < selected.mobs.size(); ++i) {
|
||||
+ LPCHARACTER mob = CHARACTER_MANAGER::instance().Find(selected.mobs[i]);
|
||||
+ if (!mob) continue;
|
||||
+ if (!mob->GetVictim()) mob->BeginFight(ch);
|
||||
+ ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u mob %u", request, selected.mobs[i]);
|
||||
+ }
|
||||
+ ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u done %d", request, selected.groups);
|
||||
+}
|
||||
--- /dev/null
|
||||
+++ b/server/game/src/autohunt_group_select.h
|
||||
@@ -0,0 +1,51 @@
|
||||
+#ifndef MT_AUTOHUNT_GROUP_SELECT_H
|
||||
+#define MT_AUTOHUNT_GROUP_SELECT_H
|
||||
+#include <algorithm>
|
||||
+#include <vector>
|
||||
+#include <map>
|
||||
+#include <stdint.h>
|
||||
+
|
||||
+namespace mt_autohunt {
|
||||
+struct Candidate {
|
||||
+ uint64_t group; // actual server party, or a unique key for an ungrouped monster
|
||||
+ uint32_t vid;
|
||||
+ int distance;
|
||||
+ bool eligible;
|
||||
+};
|
||||
+struct Selection {
|
||||
+ int groups;
|
||||
+ std::vector<uint32_t> mobs;
|
||||
+ Selection() : groups(0) {}
|
||||
+};
|
||||
+inline Selection SelectGroups(const std::vector<Candidate>& candidates, int count) {
|
||||
+ Selection out;
|
||||
+ if (count != 3 && count != 10 && count != 20) return out;
|
||||
+ struct Group {
|
||||
+ uint64_t key;
|
||||
+ int distance;
|
||||
+ bool eligible;
|
||||
+ std::vector<uint32_t> mobs;
|
||||
+ Group() : key(0), distance(2147483647), eligible(true) {}
|
||||
+ };
|
||||
+ std::map<uint64_t, Group> groups;
|
||||
+ for (size_t i = 0; i < candidates.size(); ++i) {
|
||||
+ const Candidate& c = candidates[i];
|
||||
+ Group& g = groups[c.group];
|
||||
+ g.key = c.group;
|
||||
+ g.distance = std::min(g.distance, c.distance);
|
||||
+ g.eligible = g.eligible && c.eligible;
|
||||
+ g.mobs.push_back(c.vid);
|
||||
+ }
|
||||
+ std::vector<std::pair<int, uint64_t> > ordered;
|
||||
+ for (std::map<uint64_t, Group>::const_iterator i = groups.begin(); i != groups.end(); ++i)
|
||||
+ if (i->second.eligible) ordered.push_back(std::make_pair(i->second.distance, i->first));
|
||||
+ std::sort(ordered.begin(), ordered.end());
|
||||
+ for (size_t i = 0; i < ordered.size() && out.groups < count; ++i) {
|
||||
+ const Group& g = groups[ordered[i].second];
|
||||
+ out.mobs.insert(out.mobs.end(), g.mobs.begin(), g.mobs.end());
|
||||
+ ++out.groups;
|
||||
+ }
|
||||
+ return out;
|
||||
+}
|
||||
+}
|
||||
+#endif
|
||||
@@ -12,12 +12,15 @@
|
||||
# inventory-5-pages 5-page inventory; needs its DB migration once (inventory-5-pages/README.md)
|
||||
# hunt-set quest item.set_attribute, free items in extended shops
|
||||
# shop-item-time NPC shop items carry a new item's time sockets (tooltip shows the duration)
|
||||
# autohunt-pull free cloak-style pull for 3/10/20 actual monster groups
|
||||
# autohunt-pull free cloak-style pull for 3/10/20 actual monster groups
|
||||
# autohunt-pull free cloak-style pull for 3/10/20 actual monster groups
|
||||
set -eu
|
||||
|
||||
SRC=${SRC:-/usr/metin2/src}
|
||||
SERVER=${SERVER:-/usr/metin2/server}
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
PATCHES="inventory-5-pages hunt-set shop-item-time"
|
||||
PATCHES="inventory-5-pages hunt-set shop-item-time autohunt-pull"
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
patch_state() {
|
||||
|
||||
Reference in New Issue
Block a user