Add free cloak-style auto-hunt pull for 3 10 or 20 monster groups

This commit is contained in:
shen
2026-09-30 17:07:24 -07:00
parent d1f8d3dddd
commit 8a6aeaedf0
18 changed files with 558 additions and 96 deletions
+4 -4
View File
@@ -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:
+2
View File
@@ -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;
+3 -2
View File
@@ -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;
+8 -7
View File
@@ -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;
+64 -81
View File
@@ -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.