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
+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.