Add free cloak-style auto-hunt pull for 3 10 or 20 monster groups
This commit is contained in:
@@ -646,3 +646,4 @@
|
||||
{"date": "2026-09-30", "unit": "EffectLib/ParticleSystemInstance.cpp", "action": "fix-platform-adapter", "evidence": "XRGB destination alpha semantics fixed in Vulkan pipeline; focused mapping + GPU pixel regression; original particle logic unchanged; desktop/mobile login regressions and Android build pass."}
|
||||
{"date": "2026-09-30", "unit": "EterLib/GrpFontTexture.cpp", "action": "improve-platform-rendering", "evidence": "Retina UI clarity: dynamic font atlas rebuild preserving cached pointers and logical metrics, 8-bit HiDPI glyph alpha, physical pixel alignment and 1:1 sampling; 2406 bundled HD textures. Full native suite 29 passed, 2 skipped, macOS packaged inventory screenshots and strict signature verification. See audit/evidence/2026-09-30-mac-ui-clarity.md."}
|
||||
{"date": "2026-09-30", "unit": "EterLib/GrpImageInstance.cpp", "action": "improve-platform-rendering", "evidence": "Retina UI clarity: dynamic font atlas rebuild preserving cached pointers and logical metrics, 8-bit HiDPI glyph alpha, physical pixel alignment and 1:1 sampling; 2406 bundled HD textures. Full native suite 29 passed, 2 skipped, macOS packaged inventory screenshots and strict signature verification. See audit/evidence/2026-09-30-mac-ui-clarity.md."}
|
||||
{"date": "2026-09-30", "unit": "UserInterface/PythonPlayerAutoHunt.cpp", "action": "user-authorized-extension", "evidence": "Replace tag-and-chase gather with free server-authoritative cloak-style BeginFight pull, nearest 3/10/20 actual monster groups. Server rate limit and safety checks, request generations, stationary approach wait. host.autohunt_pull and desktop/mobile port.login_flow pass; 203 FreeBSD compile/deploy and live 3-group/9-monster reply verified. See tools/server/autohunt-pull/README.md."}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"status": "PORTED",
|
||||
"impl": [
|
||||
"src/port/UserInterface/PythonNetworkStreamCommand.cpp:CPythonNetworkStream::ServerCommand"
|
||||
]
|
||||
],
|
||||
"note": "User-authorized auto-hunt extension consumes only AutoHuntGather replies before unchanged 40250 command handling. Current request generations reject stale/stopped replies; tests/port/autohunt_pull_checks.inc verifies Python settings and CG_CHAT/GC_CHAT through the real network stream. Original commands remain unchanged."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ metin2-client 是 40250 Windows 客户端的跨平台版本。**首要目标是
|
||||
| 2026-09-29 | 移动端专有功能(触控、自动狩猎、账号注册、帧率设置)以 PORT 模块形式注入,不改 40250 脚本 | 40250 脚本字节保持不变,账本 `RUN_AS_IS` 仍成立 |
|
||||
| 2026-09-29 | **背包扩到 5 页**(225 格,40250 为 2 页 90 格):服务端补丁 + DB 迁移在 `tools/server/inventory-5-pages/`,客户端 `c_Inventory_Page_Count = 5`、`TQuickSlot.Position` 改 WORD、`mt_inventory` 模块补 5 个页签和写死 2 页的脚本;装备/龙魂/腰带格整体后移 135,快捷栏封包长度变化,旧客户端不再兼容 | 用户要求的玩法扩展;203 已部署并实测 |
|
||||
|
||||
2026-09-30 用户授权扩展:自动狩猎引怪改为服务端披风式追击,免费选择 3/10/20 个实际怪物队伍;
|
||||
见 `tools/server/autohunt-pull/README.md`,203 已部署并实测 3 组/9 只怪物。原披风物品保持不变。
|
||||
|
||||
## 3. 三个层和代码位置
|
||||
|
||||
| 层 | 40250 单元 | 做法 | 位置 |
|
||||
|
||||
@@ -36,8 +36,8 @@ RANGE_TEXTS = ("近", "中", "远")
|
||||
HP_VALUES = (0, 30, 50, 70)
|
||||
SP_VALUES = (0, 20, 40, 60)
|
||||
PICKUP_TEXTS = ("不拾取", "全部", "金币装备")
|
||||
GATHER_VALUES = (0, 3, 5, 8)
|
||||
GATHER_TEXTS = ("关", "3只", "5只", "8只")
|
||||
GATHER_VALUES = (0, 3, 10, 20)
|
||||
GATHER_TEXTS = ("关", "3组", "10组", "20组")
|
||||
|
||||
SMALL_BUTTON = "d:/ymir work/ui/public/small_button_%02d.sub"
|
||||
MIDDLE_BUTTON = "d:/ymir work/ui/public/middle_button_%02d.sub"
|
||||
@@ -160,7 +160,7 @@ class SettingDialog(ui.BoardWithTitleBar):
|
||||
y += 28
|
||||
self.pickupRow = ChoiceRow(self, y, "拾取", PICKUP_TEXTS, MIDDLE_BUTTON, 66, self.__Save)
|
||||
y += 28
|
||||
self.gatherRow = ChoiceRow(self, y, "聚怪", GATHER_TEXTS, SMALL_BUTTON, 48, self.__Save)
|
||||
self.gatherRow = ChoiceRow(self, y, "披风引怪", GATHER_TEXTS, SMALL_BUTTON, 48, self.__Save)
|
||||
y += 28
|
||||
|
||||
self.skillLabel = ui.TextLine()
|
||||
@@ -297,7 +297,7 @@ class Ticker(ui.Window):
|
||||
if stateIndex != STATE_OFF:
|
||||
text = L("自动狩猎 · ") + L(STATE_TEXT.get(stateIndex, ""))
|
||||
if gathering:
|
||||
text = L("自动狩猎 · 聚怪中 %d/%d" % (gathered, player.AutoHuntGetConfig()[7]))
|
||||
text = L("自动狩猎 · 披风引怪中(最多%d组)" % player.AutoHuntGetConfig()[7])
|
||||
if noHP:
|
||||
text += L(" · 无红药")
|
||||
if noSP:
|
||||
|
||||
@@ -196,6 +196,8 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
add_executable(port_text_test ${PROJECT_SOURCE_DIR}/tests/port/port_text_test.cpp)
|
||||
add_executable(host_ui_quality_test ${PROJECT_SOURCE_DIR}/tests/host/ui_quality_test.cpp)
|
||||
add_test(NAME host.ui_quality COMMAND host_ui_quality_test)
|
||||
add_executable(host_autohunt_pull_test ${PROJECT_SOURCE_DIR}/tests/host/autohunt_pull_test.cpp)
|
||||
add_test(NAME host.autohunt_pull COMMAND host_autohunt_pull_test)
|
||||
target_link_libraries(port_text_test PRIVATE port_platform)
|
||||
add_test(NAME port.text COMMAND $<TARGET_FILE:port_text_test>)
|
||||
set_tests_properties(port.text PROPERTIES SKIP_RETURN_CODE 77)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "StdAfx.h"
|
||||
#include "PythonPlayer.h" // PORT: auto-hunt pull acknowledgement
|
||||
#include "PythonNetworkStream.h"
|
||||
#include "PythonNonPlayer.h"
|
||||
#include "AbstractApplication.h"
|
||||
@@ -131,6 +132,8 @@ bool SplitToken(const char * c_szLine, CTokenVector * pstTokenVector, const char
|
||||
|
||||
void CPythonNetworkStream::ServerCommand(char * c_szCommand)
|
||||
{
|
||||
// PORT: cloak pulling belongs to the auto-hunt extension, not the unchanged packed scripts.
|
||||
if (CPythonPlayer::Instance().AutoHunt_HandleServerCommand(c_szCommand)) return;
|
||||
// #0000811: [M2EU] 콘솔창 기능 차단
|
||||
if (strcmpi(c_szCommand, "ConsoleEnable") == 0)
|
||||
return;
|
||||
|
||||
@@ -1755,8 +1755,9 @@ CPythonPlayer::CPythonPlayer(void)
|
||||
m_kAutoHuntConfig.iGatherCount = 0;
|
||||
m_isAutoHuntGathering = false;
|
||||
m_fAutoHuntGatherStartTime = 0.0f;
|
||||
m_fAutoHuntHitTime = 0.0f;
|
||||
m_fAutoHuntLastUpdateTime = 0.0f;
|
||||
m_fAutoHuntNextPullTime = 0.0f;
|
||||
m_dwAutoHuntPullRequest = 0;
|
||||
m_iAutoHuntGatheredGroups = 0;
|
||||
m_fAutoHuntResumeTime = 0.0f;
|
||||
m_dwAutoHuntTargetVID = 0;
|
||||
m_fAutoHuntStateTime = 0.0f;
|
||||
|
||||
@@ -718,16 +718,17 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
bool bAttackStone;
|
||||
bool bAttackBoss;
|
||||
DWORD dwSkillSlotMask; // bit i = local quick slot i may be cast
|
||||
int iGatherCount; // gather mode: hit this many mobs once each, then fight them together; 0 = off
|
||||
int iGatherCount; // cloak pull: max 3/10/20 server monster groups; 0 = off
|
||||
};
|
||||
|
||||
void AutoHunt_SetEnabled(bool isEnable);
|
||||
bool AutoHunt_IsEnabled();
|
||||
int AutoHunt_GetState();
|
||||
int AutoHunt_GetStopReason();
|
||||
// Gather mode: still pulling (vs. fighting the pack), and the pulled mobs still alive.
|
||||
// Gather mode: still pulling (vs. fighting the pack), and the number of pulled groups.
|
||||
bool AutoHunt_IsGathering();
|
||||
int AutoHunt_GetGatheredCount();
|
||||
bool AutoHunt_HandleServerCommand(const char* command); // PORT: server-authoritative cloak pull replies
|
||||
bool AutoHunt_HasNoPotion(bool isHP);
|
||||
const SAutoHuntConfig& AutoHunt_GetConfig();
|
||||
void AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig);
|
||||
@@ -740,10 +741,9 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
bool __AutoHunt_UpdateBuff(CInstanceBase& rkInstMain, float fNow);
|
||||
void __AutoHunt_UpdateAttackSkill(CInstanceBase& rkInstMain, CInstanceBase& rkInstVictim, float fNow);
|
||||
bool __AutoHunt_IsSkillReady(DWORD dwSkillSlot, CPythonSkill::TSkillData** ppSkillData);
|
||||
DWORD __AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow, bool isExceptGathered = false);
|
||||
DWORD __AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow);
|
||||
DWORD __AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain);
|
||||
void __AutoHunt_PurgeGathered();
|
||||
bool __AutoHunt_IsGatherDone(float fNow);
|
||||
DWORD __AutoHunt_FindItem(CInstanceBase& rkInstMain, float fNow);
|
||||
bool __AutoHunt_IsInventoryFull();
|
||||
void __AutoHunt_Engage(DWORD dwVID, float fNow);
|
||||
@@ -771,9 +771,10 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
std::map<DWORD, float> m_kMap_dwAutoHuntSkipItem; // ground item vid -> ignored until
|
||||
bool m_isAutoHuntGathering;
|
||||
float m_fAutoHuntGatherStartTime;
|
||||
float m_fAutoHuntHitTime; // attacking the current target this long (gather tagging)
|
||||
float m_fAutoHuntLastUpdateTime;
|
||||
std::set<DWORD> m_kSet_dwAutoHuntGathered; // mobs hit while gathering, until they die
|
||||
float m_fAutoHuntNextPullTime;
|
||||
DWORD m_dwAutoHuntPullRequest;
|
||||
int m_iAutoHuntGatheredGroups;
|
||||
std::set<DWORD> m_kSet_dwAutoHuntGathered; // server-confirmed pulled mobs, until they die
|
||||
|
||||
private:
|
||||
std::map<DWORD, DWORD> m_kMap_dwAffectIndexToSkillIndex;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// Everything here goes through the paths a player's own input takes: the auto-attack target
|
||||
// (__SetAutoAttackTargetActorID -> __Update_AutoAttack -> MODE_CLICK_ACTOR walks up and attacks),
|
||||
// __UseSkill for the quick-slot skills, the MODE_CLICK_ITEM reserve that walks to a ground item and
|
||||
// sends the pick-up, and CG_ITEM_USE for potions. No packet is sent that manual play would not send.
|
||||
// sends the pick-up, and CG_ITEM_USE for potions. Cloak-style pulling is a user-requested server extension
|
||||
// sent through CG_CHAT; it does not spoof item use or client-side monster movement.
|
||||
#include "StdAfx.h"
|
||||
#include "PythonPlayer.h"
|
||||
#include "PythonItem.h"
|
||||
@@ -43,9 +44,8 @@ const float c_fAutoHuntReturnTimeout = 20.0f;
|
||||
const float c_fAutoHuntSkillDistance = 400.0f; // melee skills are cast once this close
|
||||
const BYTE c_byMobRankBoss = 4; // PAWN, S_PAWN, KNIGHT, S_KNIGHT, BOSS, KING
|
||||
const DWORD c_dwGoldVnum = 1;
|
||||
const float c_fAutoHuntTagHitTime = 0.35f; // gather: attacking a mob this long has pulled it
|
||||
const float c_fAutoHuntGatherTimeout = 25.0f;
|
||||
const float c_fAutoHuntGatherRange = 1500.0f; // gather: the next mob this close to the player
|
||||
const float c_fAutoHuntPullCooldown = 10.0f;
|
||||
const float c_fAutoHuntPullReplyTimeout = 3.0f;
|
||||
const int c_iAutoHuntGatherHPPercent = 50; // gather: stop pulling below this
|
||||
|
||||
// Trace: stderr with MT_AUTOHUNT_TRACE=1, logcat (tag mt_autohunt) on Android.
|
||||
@@ -105,8 +105,7 @@ void CPythonPlayer::AutoHunt_SetEnabled(bool isEnable)
|
||||
m_kMap_dwAutoHuntSkipItem.clear();
|
||||
m_kSet_dwAutoHuntGathered.clear();
|
||||
m_isAutoHuntGathering = false;
|
||||
m_fAutoHuntHitTime = 0.0f;
|
||||
m_fAutoHuntLastUpdateTime = m_fAutoHuntStateTime;
|
||||
m_iAutoHuntGatheredGroups = 0;
|
||||
}
|
||||
|
||||
bool CPythonPlayer::AutoHunt_IsEnabled()
|
||||
@@ -131,14 +130,7 @@ bool CPythonPlayer::AutoHunt_IsGathering()
|
||||
|
||||
int CPythonPlayer::AutoHunt_GetGatheredCount()
|
||||
{
|
||||
int iCount = 0;
|
||||
for (DWORD dwVID : m_kSet_dwAutoHuntGathered)
|
||||
{
|
||||
CInstanceBase* pkInst = NEW_FindActorPtr(dwVID);
|
||||
if (pkInst && !pkInst->IsDead())
|
||||
++iCount;
|
||||
}
|
||||
return iCount;
|
||||
return m_kSet_dwAutoHuntGathered.empty() ? 0 : m_iAutoHuntGatheredGroups;
|
||||
}
|
||||
|
||||
bool CPythonPlayer::AutoHunt_HasNoPotion(bool isHP)
|
||||
@@ -161,7 +153,8 @@ void CPythonPlayer::AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig)
|
||||
m_kAutoHuntConfig.iSPPercent = MINMAX(0, m_kAutoHuntConfig.iSPPercent, 95);
|
||||
m_kAutoHuntConfig.iPickup = MINMAX(0, m_kAutoHuntConfig.iPickup, int(AUTO_HUNT_PICKUP_GOLD_EQUIP));
|
||||
m_kAutoHuntConfig.dwSkillSlotMask &= (1 << AUTO_HUNT_SKILL_SLOT_NUM) - 1;
|
||||
m_kAutoHuntConfig.iGatherCount = MINMAX(0, m_kAutoHuntConfig.iGatherCount, 10);
|
||||
m_kAutoHuntConfig.iGatherCount = m_kAutoHuntConfig.iGatherCount <= 0 ? 0 :
|
||||
(m_kAutoHuntConfig.iGatherCount <= 3 ? 3 : (m_kAutoHuntConfig.iGatherCount <= 10 ? 10 : 20));
|
||||
__AutoHunt_SaveConfig();
|
||||
}
|
||||
|
||||
@@ -238,6 +231,7 @@ void CPythonPlayer::__AutoHunt_OnManualInput()
|
||||
m_dwAutoHuntTargetVID = 0;
|
||||
m_dwAutoHuntItemID = 0;
|
||||
m_eAutoHuntState = AUTO_HUNT_PAUSED;
|
||||
m_isAutoHuntGathering = false;
|
||||
}
|
||||
m_fAutoHuntResumeTime = CTimer::Instance().GetCurrentSecond() + c_fAutoHuntResumeDelay;
|
||||
}
|
||||
@@ -250,14 +244,13 @@ void CPythonPlayer::__AutoHunt_Engage(DWORD dwVID, float fNow)
|
||||
m_fAutoHuntProgressTime = fNow;
|
||||
m_fAutoHuntBestDistance = 1e9f;
|
||||
m_fAutoHuntNextActionTime = fNow + 0.5f;
|
||||
m_fAutoHuntHitTime = 0.0f;
|
||||
|
||||
SetTarget(dwVID);
|
||||
__ClearReservedAction();
|
||||
__SetAutoAttackTargetActorID(dwVID);
|
||||
}
|
||||
|
||||
DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow, bool isExceptGathered)
|
||||
DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow)
|
||||
{
|
||||
CPythonCharacterManager& rkChrMgr = CPythonCharacterManager::Instance();
|
||||
CPythonNonPlayer& rkNonPlayer = CPythonNonPlayer::Instance();
|
||||
@@ -287,18 +280,12 @@ DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow
|
||||
if (f != m_kMap_dwAutoHuntSkipVID.end() && fNow < f->second)
|
||||
continue;
|
||||
|
||||
if (isExceptGathered && m_kSet_dwAutoHuntGathered.count(pkInst->GetVirtualID()))
|
||||
continue;
|
||||
|
||||
TPixelPosition kPPos;
|
||||
pkInst->NEW_GetPixelPosition(&kPPos);
|
||||
if (PixelDistance(kPPos, m_kPPosAutoHuntAnchor) > float(m_kAutoHuntConfig.iRange))
|
||||
continue;
|
||||
|
||||
const float fDistance = rkInstMain.GetDistance(pkInst);
|
||||
// The first pull may be anywhere in range; the rest near the pack that follows the character.
|
||||
if (isExceptGathered && !m_kSet_dwAutoHuntGathered.empty() && fDistance > c_fAutoHuntGatherRange)
|
||||
continue;
|
||||
if (fDistance < fBestDistance)
|
||||
{
|
||||
fBestDistance = fDistance;
|
||||
@@ -316,8 +303,11 @@ DWORD CPythonPlayer::__AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain)
|
||||
for (DWORD dwVID : m_kSet_dwAutoHuntGathered)
|
||||
{
|
||||
CInstanceBase* pkInst = NEW_FindActorPtr(dwVID);
|
||||
if (!pkInst || pkInst->IsDead())
|
||||
if (!pkInst || pkInst->IsDead() || pkInst->IsInSafe() || !rkInstMain.IsAttackableInstance(*pkInst))
|
||||
continue;
|
||||
const auto skipped = m_kMap_dwAutoHuntSkipVID.find(dwVID);
|
||||
if (skipped != m_kMap_dwAutoHuntSkipVID.end() && CTimer::Instance().GetCurrentSecond() < skipped->second)
|
||||
continue;
|
||||
const float fDistance = rkInstMain.GetDistance(pkInst);
|
||||
if (fDistance < fBestDistance)
|
||||
{
|
||||
@@ -341,19 +331,24 @@ void CPythonPlayer::__AutoHunt_PurgeGathered()
|
||||
}
|
||||
}
|
||||
|
||||
bool CPythonPlayer::__AutoHunt_IsGatherDone(float fNow)
|
||||
// PORT: only the server can establish aggro. Replies carry a generation so stopped,
|
||||
// paused or previous requests cannot repopulate the current pack.
|
||||
bool CPythonPlayer::AutoHunt_HandleServerCommand(const char* command)
|
||||
{
|
||||
if (int(m_kSet_dwAutoHuntGathered.size()) >= m_kAutoHuntConfig.iGatherCount)
|
||||
return true;
|
||||
if (fNow - m_fAutoHuntGatherStartTime > c_fAutoHuntGatherTimeout)
|
||||
return true;
|
||||
const int iMaxHP = GetStatus(POINT_MAX_HP);
|
||||
if (iMaxHP > 0 && GetStatus(POINT_HP) * 100 < iMaxHP * c_iAutoHuntGatherHPPercent)
|
||||
{
|
||||
AutoHuntTrace("gather stop: hp %d/%d", GetStatus(POINT_HP), iMaxHP);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (strncmp(command, "AutoHuntGather ", 15)) return false;
|
||||
unsigned int request = 0, value = 0;
|
||||
char kind[8] = {}, extra = 0;
|
||||
if (sscanf(command + 15, "%u %7s %u %c", &request, kind, &value, &extra) != 3) return true;
|
||||
if (!AutoHunt_IsEnabled() || !m_isAutoHuntGathering || request != m_dwAutoHuntPullRequest) return true;
|
||||
if (!strcmp(kind, "mob")) {
|
||||
if (value && m_kSet_dwAutoHuntGathered.size() < 1024) m_kSet_dwAutoHuntGathered.insert(value);
|
||||
} else if (!strcmp(kind, "done")) {
|
||||
m_iAutoHuntGatheredGroups = MIN(value, unsigned(m_kAutoHuntConfig.iGatherCount));
|
||||
m_isAutoHuntGathering = false;
|
||||
m_fAutoHuntNextActionTime = 0.0f;
|
||||
AutoHuntTrace("cloak pull: %d groups, %u mobs", m_iAutoHuntGatheredGroups, unsigned(m_kSet_dwAutoHuntGathered.size()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD CPythonPlayer::__AutoHunt_FindItem(CInstanceBase& rkInstMain, float fNow)
|
||||
@@ -626,8 +621,6 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
}
|
||||
|
||||
const float fNow = CTimer::Instance().GetCurrentSecond();
|
||||
const float fElapsed = fMINMAX(0.0f, fNow - m_fAutoHuntLastUpdateTime, 0.1f);
|
||||
m_fAutoHuntLastUpdateTime = fNow;
|
||||
__AutoHunt_UpdatePotion(rkInstMain, fNow);
|
||||
|
||||
if (IsOpenPrivateShop() || IsObserverMode() || rkInstMain.IsFishingMode())
|
||||
@@ -653,28 +646,41 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
return;
|
||||
m_fAutoHuntNextActionTime = fNow + 0.25f;
|
||||
|
||||
// Gather mode: hit up to N mobs once each so they follow, then fight them where they stand.
|
||||
__AutoHunt_PurgeGathered();
|
||||
if (m_kAutoHuntConfig.iGatherCount > 0 && !m_isAutoHuntGathering && m_kSet_dwAutoHuntGathered.empty())
|
||||
{
|
||||
m_isAutoHuntGathering = true;
|
||||
m_fAutoHuntGatherStartTime = fNow;
|
||||
}
|
||||
if (m_isAutoHuntGathering)
|
||||
{
|
||||
DWORD dwVID = __AutoHunt_IsGatherDone(fNow) ? 0 : __AutoHunt_FindTarget(rkInstMain, fNow, true);
|
||||
if (dwVID)
|
||||
{
|
||||
__AutoHunt_Engage(dwVID, fNow);
|
||||
return;
|
||||
}
|
||||
m_isAutoHuntGathering = false;
|
||||
if (!m_kSet_dwAutoHuntGathered.empty())
|
||||
AutoHuntTrace("gather done: %d mobs, %.1fs", int(m_kSet_dwAutoHuntGathered.size()),
|
||||
fNow - m_fAutoHuntGatherStartTime);
|
||||
}
|
||||
// A cloak-style pull stays in place while the server starts the nearby groups.
|
||||
if (m_isAutoHuntGathering) {
|
||||
if (fNow - m_fAutoHuntGatherStartTime < c_fAutoHuntPullReplyTimeout) return;
|
||||
m_isAutoHuntGathering = false;
|
||||
m_kSet_dwAutoHuntGathered.clear();
|
||||
AutoHuntTrace("cloak pull reply timed out; continue ordinary hunt");
|
||||
}
|
||||
__AutoHunt_PurgeGathered();
|
||||
const int maxHP = GetStatus(POINT_MAX_HP);
|
||||
if (m_kAutoHuntConfig.iGatherCount > 0 && m_kSet_dwAutoHuntGathered.empty() &&
|
||||
fNow >= m_fAutoHuntNextPullTime && !rkInstMain.IsInSafe() &&
|
||||
maxHP > 0 && GetStatus(POINT_HP) * 100 >= maxHP * c_iAutoHuntGatherHPPercent &&
|
||||
__AutoHunt_FindTarget(rkInstMain, fNow)) {
|
||||
char command[128];
|
||||
++m_dwAutoHuntPullRequest;
|
||||
snprintf(command, sizeof(command), "/autohunt_pull %d %d %d %u",
|
||||
m_kAutoHuntConfig.iGatherCount, m_kAutoHuntConfig.iRange,
|
||||
m_kAutoHuntConfig.bAttackBoss ? 1 : 0, unsigned(m_dwAutoHuntPullRequest));
|
||||
m_fAutoHuntNextPullTime = fNow + c_fAutoHuntPullCooldown;
|
||||
if (CPythonNetworkStream::Instance().SendChatPacket(command)) {
|
||||
__ClearAutoAttackTargetActorID();
|
||||
__ClearReservedAction();
|
||||
rkInstMain.NEW_Stop();
|
||||
m_isAutoHuntGathering = true;
|
||||
m_iAutoHuntGatheredGroups = 0;
|
||||
m_fAutoHuntGatherStartTime = fNow;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (DWORD dwVID = __AutoHunt_FindGatheredTarget(rkInstMain))
|
||||
{
|
||||
CInstanceBase* pulled = NEW_FindActorPtr(dwVID);
|
||||
if (pulled && rkInstMain.GetDistance(pulled) > c_fAutoHuntSkillDistance &&
|
||||
fNow - m_fAutoHuntGatherStartTime < 8.0f && GetStatus(POINT_HP) * 100 >= maxHP * c_iAutoHuntGatherHPPercent)
|
||||
return; // let the server-controlled monsters approach the stationary player
|
||||
__AutoHunt_Engage(dwVID, fNow);
|
||||
return;
|
||||
}
|
||||
@@ -759,29 +765,6 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
if (rkInstMain.IsAttacking() || rkInstMain.IsUsingSkill())
|
||||
{
|
||||
m_fAutoHuntProgressTime = fNow;
|
||||
m_fAutoHuntHitTime += fElapsed;
|
||||
}
|
||||
|
||||
// Gather: this one is hit and follows now; go and hit the next.
|
||||
if (m_isAutoHuntGathering && m_fAutoHuntHitTime >= c_fAutoHuntTagHitTime &&
|
||||
!m_kSet_dwAutoHuntGathered.count(m_dwAutoHuntTargetVID))
|
||||
{
|
||||
m_kSet_dwAutoHuntGathered.insert(m_dwAutoHuntTargetVID);
|
||||
__AutoHunt_PurgeGathered();
|
||||
AutoHuntTrace("gather tag %u (%d/%d)", (unsigned)m_dwAutoHuntTargetVID, int(m_kSet_dwAutoHuntGathered.size()),
|
||||
m_kAutoHuntConfig.iGatherCount);
|
||||
}
|
||||
if (m_isAutoHuntGathering && m_kSet_dwAutoHuntGathered.count(m_dwAutoHuntTargetVID) && !rkInstMain.IsUsingSkill())
|
||||
{
|
||||
DWORD dwNextVID = __AutoHunt_IsGatherDone(fNow) ? 0 : __AutoHunt_FindTarget(rkInstMain, fNow, true);
|
||||
if (dwNextVID)
|
||||
{
|
||||
__AutoHunt_Engage(dwNextVID, fNow);
|
||||
return;
|
||||
}
|
||||
m_isAutoHuntGathering = false;
|
||||
AutoHuntTrace("gather done: %d mobs, %.1fs", int(m_kSet_dwAutoHuntGathered.size()),
|
||||
fNow - m_fAutoHuntGatherStartTime);
|
||||
}
|
||||
|
||||
// Blocked path, a target out of reach, or a safe zone that refuses the attack.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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