Files
metin2-client/src/port/UserInterface/PythonPlayerAutoHunt.cpp
T

1184 lines
45 KiB
C++

// PORT: auto hunt. 40250 has no counterpart; see PythonPlayer.h.
//
// 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 independently selected skills, the MODE_CLICK_ITEM reserve that walks to a ground item and
// 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"
#include "PythonNonPlayer.h"
#include "PythonBackground.h"
#include "PythonCharacterManager.h"
#include "PythonNetworkStream.h"
#include "InstanceBase.h"
#include "../GameLib/ItemManager.h"
#include "../EterBase/Timer.h"
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include "AutoHuntNavigation.h"
#include "../EterLib/CullingManager.h"
#include "../EterLib/GrpObjectInstance.h"
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace
{
const char c_szAutoHuntConfigFileName[] = "autohunt.cfg";
const float c_fAutoHuntResumeDelay = 3.0f; // after the player's own input
const float c_fAutoHuntNoProgressTime = 4.0f; // neither closer to the target nor hitting it
const float c_fAutoHuntTargetTimeout = 60.0f;
const float c_fAutoHuntSkipTime = 30.0f; // an unreachable target is left alone this long
const float c_fAutoHuntLootDelay = 0.6f; // drops land a moment after the kill
const float c_fAutoHuntLootTimeout = 15.0f;
const float c_fAutoHuntItemTimeout = 5.0f; // one ground item (someone else's, blocked path)
const float c_fAutoHuntItemSkipTime = 60.0f;
const float c_fAutoHuntItemRange = 1500.0f;
const float c_fAutoHuntReturnDistance = 300.0f;
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_fAutoHuntPullCooldown = 10.0f;
const float c_fAutoHuntPullReplyTimeout = 3.0f;
const float c_fAutoHuntPullApproachTimeout = 8.0f;
const int c_iAutoHuntGatherHPPercent = 50; // gather: stop pulling below this
// Trace: stderr with MT_AUTOHUNT_TRACE=1, logcat (tag mt_autohunt) on Android.
void AutoHuntTrace(const char* c_szFormat, ...)
{
static const bool s_isTrace = getenv("MT_AUTOHUNT_TRACE") != NULL;
va_list args;
#ifdef __ANDROID__
va_start(args, c_szFormat);
__android_log_vprint(ANDROID_LOG_INFO, "mt_autohunt", c_szFormat, args);
va_end(args);
#endif
if (!s_isTrace)
return;
va_start(args, c_szFormat);
fprintf(stderr, "autohunt: ");
vfprintf(stderr, c_szFormat, args);
fprintf(stderr, "\n");
va_end(args);
}
float PixelDistance(const TPixelPosition& a, const TPixelPosition& b)
{
const float dx = a.x - b.x;
const float dy = a.y - b.y;
return sqrtf(dx * dx + dy * dy);
}
}
void CPythonPlayer::AutoHunt_SetEnabled(bool isEnable)
{
if (!isEnable)
{
if (AutoHunt_IsEnabled())
__AutoHunt_Stop(AUTO_HUNT_STOP_USER);
return;
}
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (!pkInstMain || pkInstMain->IsDead())
return;
__AutoHunt_LoadConfig();
__AutoHunt_MigrateSkills();
__AutoHunt_ResetMovement();
m_iAutoHuntMountState = 0;
pkInstMain->NEW_GetPixelPosition(&m_kPPosAutoHuntAnchor);
m_stAutoHuntMapName = CPythonBackground::Instance().GetWarpMapName();
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_eAutoHuntStopReason = AUTO_HUNT_STOP_NONE;
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
m_fAutoHuntStateTime = CTimer::Instance().GetCurrentSecond();
m_fAutoHuntNextActionTime = 0.0f;
m_fAutoHuntNextSkillTime = 0.0f;
m_fAutoHuntNextPotionTime = 0.0f;
m_isAutoHuntNoHPPotion = false;
m_isAutoHuntNoSPPotion = false;
m_kMap_dwAutoHuntSkipVID.clear();
m_kMap_dwAutoHuntSkipItem.clear();
m_kSet_dwAutoHuntGathered.clear();
m_isAutoHuntGathering = false;
m_iAutoHuntGatheredGroups = 0;
}
bool CPythonPlayer::AutoHunt_IsEnabled()
{
return AUTO_HUNT_OFF != m_eAutoHuntState;
}
int CPythonPlayer::AutoHunt_GetState()
{
return m_eAutoHuntState;
}
int CPythonPlayer::AutoHunt_GetStopReason()
{
return m_eAutoHuntStopReason;
}
bool CPythonPlayer::AutoHunt_IsGathering()
{
return AutoHunt_IsEnabled() && m_isAutoHuntGathering;
}
int CPythonPlayer::AutoHunt_GetGatheredCount()
{
return AutoHunt_IsEnabled() ? m_iAutoHuntGatheredGroups : 0;
}
bool CPythonPlayer::AutoHunt_HasNoPotion(bool isHP)
{
return isHP ? m_isAutoHuntNoHPPotion : m_isAutoHuntNoSPPotion;
}
const CPythonPlayer::SAutoHuntConfig& CPythonPlayer::AutoHunt_GetConfig()
{
__AutoHunt_LoadConfig();
return m_kAutoHuntConfig;
}
void CPythonPlayer::AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig)
{
m_isAutoHuntConfigLoaded = true;
if (AutoHunt_IsEnabled() && (m_kAutoHuntConfig.iGatherCount != c_rkConfig.iGatherCount ||
m_kAutoHuntConfig.bAttackBoss != c_rkConfig.bAttackBoss || m_kAutoHuntConfig.iRange != c_rkConfig.iRange)) {
__AutoHunt_CancelPull();
++m_dwAutoHuntPullRequest; // invalidate replies for the previous selection
m_isAutoHuntGathering = false;
m_kSet_dwAutoHuntGathered.clear();
m_iAutoHuntGatheredGroups = 0;
}
m_kAutoHuntConfig = c_rkConfig;
__AutoHunt_ResetMovement();
m_kAutoHuntConfig.iRange = MINMAX(3500, m_kAutoHuntConfig.iRange, 15000);
m_kAutoHuntConfig.iHPPercent = MINMAX(0, m_kAutoHuntConfig.iHPPercent, 95);
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 = m_kAutoHuntConfig.iGatherCount <= 0 ? 0 :
(m_kAutoHuntConfig.iGatherCount <= 3 ? 3 : (m_kAutoHuntConfig.iGatherCount <= 10 ? 10 : 20));
__AutoHunt_SaveConfig();
}
void CPythonPlayer::__AutoHunt_LoadConfig()
{
if (m_isAutoHuntConfigLoaded)
return;
m_isAutoHuntConfigLoaded = true;
FILE* fp = fopen(c_szAutoHuntConfigFileName, "rt");
if (!fp) return;
SAutoHuntConfig config = m_kAutoHuntConfig;
char header[32] = {};
int stone=0, boss=0, revive=0, dismount=1, count=0;
unsigned mask=0;
if (fscanf(fp, "%31s", header)==1 && !strcmp(header,"AUTOHUNT2")) {
if (fscanf(fp,"%d %d %d %d %d %d %d %d %d %d", &config.iRange,&config.iHPPercent,
&config.iSPPercent,&config.iPickup,&stone,&boss,&config.iGatherCount,&revive,&dismount,&count)==10 &&
count>=0 && count<=32) {
config.vecSkillIndices.clear();
for (int i=0;i<count;++i) { unsigned index=0; if (fscanf(fp,"%u",&index)!=1) break; config.vecSkillIndices.push_back(index); }
config.bAttackStone=stone!=0; config.bAttackBoss=boss!=0;
config.bAutoRevive=revive!=0; config.bDismountSkill=dismount!=0;
fclose(fp); AutoHunt_SetConfig(config); return;
}
} else {
rewind(fp);
const int read=fscanf(fp,"%d %d %d %d %d %d %u %d", &config.iRange,&config.iHPPercent,
&config.iSPPercent,&config.iPickup,&stone,&boss,&mask,&config.iGatherCount);
if (read>=7) {
if (read==7) config.iGatherCount=0;
config.bAttackStone=stone!=0; config.bAttackBoss=boss!=0; config.dwSkillSlotMask=mask;
m_kAutoHuntConfig=config; m_isAutoHuntLegacySkills=true;
}
}
fclose(fp);
}
void CPythonPlayer::__AutoHunt_SaveConfig()
{
// Defer legacy migration until the game's skill/quickslot packets have arrived.
if (m_isAutoHuntLegacySkills) return;
FILE* fp = fopen(c_szAutoHuntConfigFileName, "wt");
if (!fp) return;
fprintf(fp,"AUTOHUNT2\n%d %d %d %d %d %d %d %d %d %u\n", m_kAutoHuntConfig.iRange,
m_kAutoHuntConfig.iHPPercent,m_kAutoHuntConfig.iSPPercent,m_kAutoHuntConfig.iPickup,
m_kAutoHuntConfig.bAttackStone,m_kAutoHuntConfig.bAttackBoss,m_kAutoHuntConfig.iGatherCount,
m_kAutoHuntConfig.bAutoRevive,m_kAutoHuntConfig.bDismountSkill,unsigned(m_kAutoHuntConfig.vecSkillIndices.size()));
for (DWORD index:m_kAutoHuntConfig.vecSkillIndices) fprintf(fp,"%u ",unsigned(index));
fprintf(fp,"\n"); fclose(fp);
}
std::vector<DWORD> CPythonPlayer::AutoHunt_GetAvailableSkills()
{
__AutoHunt_LoadConfig();
__AutoHunt_MigrateSkills();
std::vector<DWORD> indices;
for (DWORD slot=0;slot<SKILL_MAX_NUM;++slot) {
const auto& instance=m_playerStatus.aSkill[slot];
CPythonSkill::TSkillData* data=nullptr;
if (!instance.dwIndex || instance.iLevel<=0 || !CPythonSkill::Instance().GetSkillData(instance.dwIndex,&data)) continue;
if ((data->byType==CPythonSkill::SKILL_TYPE_ACTIVE || data->byType==CPythonSkill::SKILL_TYPE_HORSE) &&
data->IsCanUseSkill() && std::find(indices.begin(),indices.end(),instance.dwIndex)==indices.end())
indices.push_back(instance.dwIndex);
}
return indices;
}
void CPythonPlayer::__AutoHunt_MigrateSkills()
{
if (!m_isAutoHuntLegacySkills) return;
m_kAutoHuntConfig.vecSkillIndices.clear();
for (DWORD i=0;i<AUTO_HUNT_SKILL_SLOT_NUM;++i) {
DWORD type=0,slot=0;
GetLocalQuickSlotData(i,&type,&slot);
if ((m_kAutoHuntConfig.dwSkillSlotMask & (1<<i)) && type==SLOT_TYPE_SKILL && slot<SKILL_MAX_NUM && GetSkillIndex(slot))
m_kAutoHuntConfig.vecSkillIndices.push_back(GetSkillIndex(slot));
}
m_isAutoHuntLegacySkills=false;
AutoHunt_SetSkills(m_kAutoHuntConfig.vecSkillIndices);
}
void CPythonPlayer::AutoHunt_SetSkills(const std::vector<DWORD>& indices)
{
__AutoHunt_LoadConfig();
m_isAutoHuntLegacySkills=false;
std::vector<DWORD> clean;
for (DWORD index:indices) {
CPythonSkill::TSkillData* data=nullptr;
if (clean.size()>=32) break;
if (index && CPythonSkill::Instance().GetSkillData(index,&data) && data->IsCanUseSkill() &&
(data->byType==CPythonSkill::SKILL_TYPE_ACTIVE || data->byType==CPythonSkill::SKILL_TYPE_HORSE) &&
std::find(clean.begin(),clean.end(),index)==clean.end()) clean.push_back(index);
}
m_kAutoHuntConfig.vecSkillIndices=clean;
__AutoHunt_SaveConfig();
}
void CPythonPlayer::__AutoHunt_CancelPull()
{
if (AutoHunt_IsEnabled() && m_kAutoHuntConfig.iGatherCount > 0) {
char command[96];
snprintf(command, sizeof(command), "/autohunt_pull 0 %d %d %u", m_kAutoHuntConfig.iRange,
m_kAutoHuntConfig.bAttackBoss ? 1 : 0, unsigned(m_dwAutoHuntPullRequest));
CPythonNetworkStream::Instance().SendChatPacket(command);
}
}
void CPythonPlayer::__AutoHunt_Stop(int eReason)
{
__AutoHunt_CancelPull();
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
__AutoHunt_ResetMovement();
m_iAutoHuntMountState = 0;
if (auto* main=NEW_GetMainActorPtr()) main->NEW_Stop();
m_eAutoHuntState = AUTO_HUNT_OFF;
m_eAutoHuntStopReason = eReason;
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
m_isAutoHuntGathering = false;
m_kSet_dwAutoHuntGathered.clear();
}
// Direction keys, the touch stick, a world click or the attack key: the player takes over, and the
// hunt picks up again from wherever they leave the character once they stop for a moment.
void CPythonPlayer::__AutoHunt_OnManualInput()
{
if (!AutoHunt_IsEnabled())
return;
__AutoHunt_ResetMovement();
m_iAutoHuntMountState = 0; // manual control also cancels pending mount/skill commands
if (AUTO_HUNT_REVIVE == m_eAutoHuntState || AUTO_HUNT_RECOVER == m_eAutoHuntState) {
__AutoHunt_Stop(AUTO_HUNT_STOP_USER); return;
}
if (AUTO_HUNT_PAUSED != m_eAutoHuntState)
{
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
m_eAutoHuntState = AUTO_HUNT_PAUSED;
m_isAutoHuntGathering = false;
}
m_fAutoHuntResumeTime = CTimer::Instance().GetCurrentSecond() + c_fAutoHuntResumeDelay;
}
void CPythonPlayer::__AutoHunt_Engage(DWORD dwVID, float fNow)
{
m_dwAutoHuntTargetVID = dwVID;
m_eAutoHuntState = AUTO_HUNT_ENGAGE;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntProgressTime = fNow;
m_fAutoHuntBestDistance = 1e9f;
m_fAutoHuntNextActionTime = fNow + 0.5f;
SetTarget(dwVID);
__ClearReservedAction();
__ClearAutoAttackTargetActorID();
__AutoHunt_ResetMovement();
}
// PORT: stones have their own option; their mob_proto rank must not count as a boss.
bool CPythonPlayer::__AutoHunt_IsTargetAllowed(CInstanceBase& main, CInstanceBase& target)
{
if (&main == &target || target.IsDead() || target.IsInSafe() || !main.IsAttackableInstance(target))
return false;
if (target.IsStone()) return m_kAutoHuntConfig.bAttackStone;
if (!target.IsEnemy()) return false;
const auto* mob = CPythonNonPlayer::Instance().GetTable(target.GetRace());
return m_kAutoHuntConfig.bAttackBoss || !mob || mob->bRank < c_byMobRankBoss;
}
// PORT: cloak collection and the server pursuit leash follow the hunting range.
bool CPythonPlayer::__AutoHunt_RequestPull(CInstanceBase& main, float now)
{
const int maxHP = GetStatus(POINT_MAX_HP);
if (m_kAutoHuntConfig.iGatherCount <= 0 || m_isAutoHuntGathering || now < m_fAutoHuntNextPullTime ||
main.IsInSafe() || maxHP <= 0 || GetStatus(POINT_HP) * 100 < maxHP * c_iAutoHuntGatherHPPercent)
return false;
if (m_kAutoHuntConfig.bAttackStone) {
auto* target = NEW_FindActorPtr(__AutoHunt_FindTarget(main, now));
if (target && target->IsStone()) return false;
}
const int radius = m_kAutoHuntConfig.iRange;
char command[128];
++m_dwAutoHuntPullRequest;
snprintf(command, sizeof(command), "/autohunt_pull %d %d %d %u",
m_kAutoHuntConfig.iGatherCount, radius, m_kAutoHuntConfig.bAttackBoss ? 1 : 0,
unsigned(m_dwAutoHuntPullRequest));
m_fAutoHuntNextPullTime = now + c_fAutoHuntPullCooldown;
if (!CPythonNetworkStream::Instance().SendChatPacket(command)) return false;
m_kSet_dwAutoHuntGathered.clear();
m_iAutoHuntGatheredGroups = 0;
m_isAutoHuntGathering = true;
m_fAutoHuntGatherStartTime = now;
AutoHuntTrace("cloak request: %d groups, radius %d", m_kAutoHuntConfig.iGatherCount, radius);
return true;
}
DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow)
{
CPythonCharacterManager& rkChrMgr = CPythonCharacterManager::Instance();
if (fNow<m_fAutoHuntTargetScanTime) {
auto* cached=NEW_FindActorPtr(m_dwAutoHuntScanTarget);
const auto skip=m_kMap_dwAutoHuntSkipVID.find(m_dwAutoHuntScanTarget);
if (!m_dwAutoHuntScanTarget) return 0;
if (cached && __AutoHunt_IsTargetAllowed(rkInstMain,*cached) &&
(skip==m_kMap_dwAutoHuntSkipVID.end() || fNow>=skip->second)) return m_dwAutoHuntScanTarget;
}
m_fAutoHuntTargetScanTime=fNow+0.5f; m_dwAutoHuntScanTarget=0;
std::vector<CInstanceBase*> candidates;
for (auto i=rkChrMgr.CharacterInstanceBegin();i!=rkChrMgr.CharacterInstanceEnd();++i) {
CInstanceBase* target=*i;
if (!target || !__AutoHunt_IsTargetAllowed(rkInstMain,*target)) continue;
const auto skip=m_kMap_dwAutoHuntSkipVID.find(target->GetVirtualID());
if (skip!=m_kMap_dwAutoHuntSkipVID.end() && fNow<skip->second) continue;
TPixelPosition position; target->NEW_GetPixelPosition(&position);
if (PixelDistance(position,m_kPPosAutoHuntAnchor)<=m_kAutoHuntConfig.iRange) candidates.push_back(target);
}
std::sort(candidates.begin(),candidates.end(),[&](CInstanceBase* a,CInstanceBase* b) {
if (a->IsStone()!=b->IsStone()) return bool(a->IsStone());
return rkInstMain.GetDistance(a)<rkInstMain.GetDistance(b);
});
int attempts=0;
for (auto* target:candidates) {
if (++attempts>4) break;
TPixelPosition goal; target->NEW_GetPixelPosition(&goal);
std::vector<TPixelPosition> path;
if (__AutoHunt_BuildPath(goal,100.f,path)) return m_dwAutoHuntScanTarget=target->GetVirtualID();
m_kMap_dwAutoHuntSkipVID[target->GetVirtualID()]=fNow+5.f;
}
return 0;
}
// Gather mode: the nearest pulled mob still alive.
DWORD CPythonPlayer::__AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain)
{
std::vector<CInstanceBase*> targets;
const float now=CTimer::Instance().GetCurrentSecond();
for (DWORD vid:m_kSet_dwAutoHuntGathered) {
auto* target=NEW_FindActorPtr(vid);
const auto skip=m_kMap_dwAutoHuntSkipVID.find(vid);
if (target && __AutoHunt_IsTargetAllowed(rkInstMain,*target) &&
(skip==m_kMap_dwAutoHuntSkipVID.end() || now>=skip->second)) targets.push_back(target);
}
std::sort(targets.begin(),targets.end(),[&](CInstanceBase* a,CInstanceBase* b) {return rkInstMain.GetDistance(a)<rkInstMain.GetDistance(b);});
int attempts=0;
for (auto* target:targets) {
if (++attempts>4) break;
TPixelPosition goal; target->NEW_GetPixelPosition(&goal);
std::vector<TPixelPosition> path;
if (__AutoHunt_BuildPath(goal,100.f,path)) return target->GetVirtualID();
m_kMap_dwAutoHuntSkipVID[target->GetVirtualID()]=now+5.f;
}
return 0;
}
// Dead or out of sight.
void CPythonPlayer::__AutoHunt_PurgeGathered()
{
for (std::set<DWORD>::iterator i = m_kSet_dwAutoHuntGathered.begin(); i != m_kSet_dwAutoHuntGathered.end();)
{
CInstanceBase* pkInst = NEW_FindActorPtr(*i);
if ((pkInst && pkInst->IsDead()) ||
(!pkInst && CTimer::Instance().GetCurrentSecond() - m_fAutoHuntGatherStartTime >= std::max(c_fAutoHuntPullApproachTimeout, m_kAutoHuntConfig.iRange / 100.0f)))
i = m_kSet_dwAutoHuntGathered.erase(i);
else
++i;
}
}
// 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 (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)
{
CPythonItem& rkItem = CPythonItem::Instance();
std::vector<DWORD> kVecVID;
rkItem.GetGroundItemVIDs(&kVecVID);
TPixelPosition kPPosMain;
rkInstMain.NEW_GetPixelPosition(&kPPosMain);
const float fRange = MIN(float(m_kAutoHuntConfig.iRange), c_fAutoHuntItemRange);
DWORD dwBestID = 0;
float fBestDistance = 1e9f;
for (DWORD dwID : kVecVID)
{
std::map<DWORD, float>::iterator f = m_kMap_dwAutoHuntSkipItem.find(dwID);
if (f != m_kMap_dwAutoHuntSkipItem.end() && fNow < f->second)
continue;
// Someone else's drop: the server refuses it until the ownership runs out.
const char* c_szOwner = NULL;
if (rkItem.GetOwnership(dwID, &c_szOwner) && c_szOwner && *c_szOwner && m_stName != c_szOwner)
continue;
if (AUTO_HUNT_PICKUP_GOLD_EQUIP == m_kAutoHuntConfig.iPickup)
{
const DWORD dwVnum = rkItem.GetVirtualNumberOfGroundItem(dwID);
CItemData* pItemData = NULL;
const bool isGold = c_dwGoldVnum == dwVnum;
const bool isEquip = CItemManager::Instance().GetItemDataPointer(dwVnum, &pItemData) && pItemData &&
(CItemData::ITEM_TYPE_WEAPON == pItemData->GetType() || CItemData::ITEM_TYPE_ARMOR == pItemData->GetType() ||
CItemData::ITEM_TYPE_ELK == pItemData->GetType());
if (!isGold && !isEquip)
continue;
}
TPixelPosition kPPosItem;
if (!rkItem.GetGroundItemPosition(dwID, &kPPosItem))
continue;
const float fDistance = PixelDistance(kPPosItem, kPPosMain);
if (fDistance > fRange)
continue;
if (fDistance < fBestDistance)
{
fBestDistance = fDistance;
dwBestID = dwID;
}
}
return dwBestID;
}
// uiinventory's cells: a big item covers the cells below its own, page by page.
bool CPythonPlayer::__AutoHunt_IsInventoryFull()
{
const DWORD c_dwColumns = 5;
bool abCovered[c_ItemSlot_Count] = {};
for (DWORD i = 0; i < c_ItemSlot_Count; ++i)
{
const DWORD dwVnum = GetItemIndex(TItemPos(INVENTORY, WORD(i)));
if (!dwVnum)
continue;
DWORD dwSize = 1;
CItemData* pItemData = NULL;
if (CItemManager::Instance().GetItemDataPointer(dwVnum, &pItemData) && pItemData)
dwSize = MAX(1, pItemData->GetSize());
const DWORD dwPageEnd = (i / c_Inventory_Page_Size + 1) * c_Inventory_Page_Size;
for (DWORD k = 0; k < dwSize && i + k * c_dwColumns < dwPageEnd; ++k)
abCovered[i + k * c_dwColumns] = true;
}
for (DWORD i = 0; i < c_ItemSlot_Count; ++i)
if (!abCovered[i])
return false;
return true;
}
// __CheckSkillUsable without its "cannot use" chat lines: the hunt only casts what will go through.
bool CPythonPlayer::__AutoHunt_IsSkillReady(DWORD dwSkillSlot, CPythonSkill::TSkillData** ppSkillData)
{
if (dwSkillSlot >= SKILL_MAX_NUM)
return false;
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (!pkInstMain)
return false;
TSkillInstance& rkSkillInst = m_playerStatus.aSkill[dwSkillSlot];
if (!rkSkillInst.dwIndex || rkSkillInst.iLevel <= 0)
return false;
CPythonSkill::TSkillData* pSkillData;
if (!CPythonSkill::Instance().GetSkillData(rkSkillInst.dwIndex, &pSkillData))
return false;
if (CPythonSkill::SKILL_TYPE_ACTIVE != pSkillData->byType && CPythonSkill::SKILL_TYPE_HORSE != pSkillData->byType)
return false;
if (!pSkillData->IsCanUseSkill())
return false;
if (pSkillData->IsHorseSkill() && !pkInstMain->IsMountingHorse()) return false;
if (!pSkillData->IsHorseSkill() && pkInstMain->IsMountingHorse() &&
(!m_kAutoHuntConfig.bDismountSkill || CTimer::Instance().GetCurrentSecond()<m_fAutoHuntMountNextTime)) return false;
if (!pSkillData->CanUseWeaponType(pkInstMain->GetWeaponType()))
return false;
if (pSkillData->IsNeedBow() && !__HasEnoughArrow())
return false;
if (pSkillData->IsNeedEmptyBottle() && !__HasItem(27995))
return false;
if (pSkillData->IsNeedPoisonBottle() && !__HasItem(27996))
return false;
if (__CheckRestSkillCoolTime(dwSkillSlot))
return false;
if (__CheckShortLife(rkSkillInst, *pSkillData))
return false;
const int iNeedSP = pSkillData->GetNeedSP(rkSkillInst.fcurEfficientPercentage);
const int iCurSP = GetStatus(POINT_SP);
if (pSkillData->CanUseIfNotEnough() ? iCurSP <= 0 : iCurSP < iNeedSP)
return false;
*ppSkillData = pSkillData;
return true;
}
// Selected self buffs whose effect is not on the character.
bool CPythonPlayer::__AutoHunt_UpdateBuff(CInstanceBase& rkInstMain, float fNow)
{
if (fNow < m_fAutoHuntNextSkillTime || rkInstMain.IsUsingSkill() || rkInstMain.IsInSafe())
return false;
for (DWORD index : m_kAutoHuntConfig.vecSkillIndices)
{
DWORD dwSkillSlot = 0;
if (!GetSkillSlotIndex(index, &dwSkillSlot)) continue;
CPythonSkill::TSkillData* pSkillData;
if (!__AutoHunt_IsSkillReady(dwSkillSlot, &pSkillData))
continue;
if (pSkillData->IsAttackSkill())
continue;
if (!pSkillData->IsStandingSkill() && !pSkillData->CanUseForMe())
continue;
if (pSkillData->IsToggleSkill())
{
if (IsSkillActive(dwSkillSlot))
continue;
}
else
{
bool isActive = false;
const DWORD dwSkillIndex = m_playerStatus.aSkill[dwSkillSlot].dwIndex;
for (std::map<DWORD, DWORD>::iterator f = m_kMap_dwAffectIndexToSkillIndex.begin(); f != m_kMap_dwAffectIndexToSkillIndex.end(); ++f)
if (f->second == dwSkillIndex && rkInstMain.IsAffect(f->first))
isActive = true;
if (isActive)
continue;
}
m_fAutoHuntNextSkillTime = fNow + 0.5f;
if (__AutoHunt_CastSkill(dwSkillSlot, fNow))
return true;
}
return false;
}
void CPythonPlayer::__AutoHunt_UpdateAttackSkill(CInstanceBase& rkInstMain, CInstanceBase& rkInstVictim, float fNow)
{
if (fNow < m_fAutoHuntNextSkillTime || rkInstMain.IsUsingSkill())
return;
m_fAutoHuntNextSkillTime = fNow + 0.3f;
const float fDistance = rkInstMain.GetDistance(&rkInstVictim);
for (DWORD index : m_kAutoHuntConfig.vecSkillIndices)
{
DWORD dwSkillSlot = 0;
if (!GetSkillSlotIndex(index, &dwSkillSlot)) continue;
CPythonSkill::TSkillData* pSkillData;
if (!__AutoHunt_IsSkillReady(dwSkillSlot, &pSkillData) || !pSkillData->IsAttackSkill())
continue;
// Ranged skills fire from their range; the rest once the auto attack has walked up.
const float fSkillRange = MAX(float(pSkillData->GetTargetRange()), c_fAutoHuntSkillDistance);
if (fDistance > fSkillRange)
continue;
SetTarget(rkInstVictim.GetVirtualID());
if (__AutoHunt_CastSkill(dwSkillSlot, fNow))
{
m_fAutoHuntNextSkillTime = fNow + 0.5f;
return;
}
}
}
// uitaskbar's potions: the first USE_POTION stack in the inventory that restores what is low.
void CPythonPlayer::__AutoHunt_UpdatePotion(CInstanceBase& rkInstMain, float fNow)
{
if (fNow < m_fAutoHuntNextPotionTime)
return;
m_fAutoHuntNextPotionTime = fNow + 0.5f;
for (int iValue = 0; iValue < 2; ++iValue)
{
const bool isHP = 0 == iValue;
int iPercent = isHP ? m_kAutoHuntConfig.iHPPercent : m_kAutoHuntConfig.iSPPercent;
if (isHP && iPercent>0 && m_eAutoHuntState==AUTO_HUNT_RECOVER) iPercent=MAX(iPercent,70);
const int iMax = GetStatus(isHP ? POINT_MAX_HP : POINT_MAX_SP);
const int iCur = GetStatus(isHP ? POINT_HP : POINT_SP);
bool& isNoPotion = isHP ? m_isAutoHuntNoHPPotion : m_isAutoHuntNoSPPotion;
if (iPercent <= 0 || iMax <= 0 || iCur * 100 >= iMax * iPercent)
continue;
isNoPotion = true;
for (DWORD i = 0; i < c_ItemSlot_Count; ++i)
{
const TItemPos kCell(INVENTORY, WORD(i));
CItemData* pItemData = NULL;
if (!CItemManager::Instance().GetItemDataPointer(GetItemIndex(kCell), &pItemData) || !pItemData)
continue;
if (CItemData::ITEM_TYPE_USE != pItemData->GetType() || CItemData::USE_POTION != pItemData->GetSubType())
continue;
if (pItemData->GetValue(iValue) <= 0)
continue;
isNoPotion = false;
CPythonNetworkStream::Instance().SendItemUsePacket(kCell);
m_fAutoHuntNextPotionTime = fNow + 1.0f;
return;
}
}
}
void CPythonPlayer::__Update_AutoHunt()
{
static int s_iLastState = AUTO_HUNT_OFF;
if (s_iLastState != m_eAutoHuntState)
{
AutoHuntTrace("state %d -> %d (reason %d, target %u, item %u, gathering %d, gathered %d)", s_iLastState,
m_eAutoHuntState, m_eAutoHuntStopReason, (unsigned)m_dwAutoHuntTargetVID, (unsigned)m_dwAutoHuntItemID,
m_isAutoHuntGathering ? 1 : 0, int(m_kSet_dwAutoHuntGathered.size()));
s_iLastState = m_eAutoHuntState;
}
if (!AutoHunt_IsEnabled())
return;
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (!pkInstMain)
return;
CInstanceBase& rkInstMain = *pkInstMain;
const float fNow = CTimer::Instance().GetCurrentSecond();
if (m_stAutoHuntMapName != CPythonBackground::Instance().GetWarpMapName())
{
__AutoHunt_Stop(AUTO_HUNT_STOP_MAP);
return;
}
if (rkInstMain.IsDead()) {
AutoHunt_OnDeath();
if (AutoHunt_IsEnabled()) __AutoHunt_UpdateRecovery(rkInstMain,fNow);
return;
}
if (__AutoHunt_UpdateRecovery(rkInstMain,fNow)) return;
__AutoHunt_UpdatePotion(rkInstMain, fNow);
if (m_isAutoHuntGathering && fNow - m_fAutoHuntGatherStartTime >= c_fAutoHuntPullReplyTimeout) {
m_isAutoHuntGathering = false;
AutoHuntTrace("cloak pull reply timed out; continue ordinary hunt");
}
__AutoHunt_PurgeGathered();
if (IsOpenPrivateShop() || IsObserverMode() || rkInstMain.IsFishingMode())
return;
if (__AutoHunt_UpdateMount(rkInstMain,fNow)) return;
switch (m_eAutoHuntState)
{
case AUTO_HUNT_PAUSED:
{
if (fNow < m_fAutoHuntResumeTime)
return;
rkInstMain.NEW_GetPixelPosition(&m_kPPosAutoHuntAnchor);
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
break;
}
case AUTO_HUNT_SEARCH:
{
if (__AutoHunt_UpdateBuff(rkInstMain, fNow))
return;
if (fNow < m_fAutoHuntNextActionTime || rkInstMain.IsUsingSkill())
return;
m_fAutoHuntNextActionTime = fNow + 0.25f;
if (m_isAutoHuntGathering) return;
const DWORD preferred = __AutoHunt_FindTarget(rkInstMain, fNow);
CInstanceBase* preferredActor = NEW_FindActorPtr(preferred);
if (preferredActor && preferredActor->IsStone()) {
__AutoHunt_Engage(preferred, fNow);
return;
}
if (__AutoHunt_RequestPull(rkInstMain, fNow)) {
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
rkInstMain.NEW_Stop();
return;
}
const int maxHP = GetStatus(POINT_MAX_HP);
if (DWORD dwVID = __AutoHunt_FindGatheredTarget(rkInstMain))
{
CInstanceBase* pulled = NEW_FindActorPtr(dwVID);
if (pulled && rkInstMain.GetDistance(pulled) > c_fAutoHuntSkillDistance &&
fNow - m_fAutoHuntGatherStartTime < c_fAutoHuntPullApproachTimeout && GetStatus(POINT_HP) * 100 >= maxHP * c_iAutoHuntGatherHPPercent)
return; // let the server-controlled monsters approach the stationary player
__AutoHunt_Engage(dwVID, fNow);
return;
}
if (!m_kSet_dwAutoHuntGathered.empty() &&
fNow - m_fAutoHuntGatherStartTime < c_fAutoHuntPullApproachTimeout) return;
if (DWORD dwVID = __AutoHunt_FindTarget(rkInstMain, fNow))
{
__AutoHunt_Engage(dwVID, fNow);
return;
}
// Nothing to fight: drops the post-kill window missed (a kill farther than the loot range).
if (AUTO_HUNT_PICKUP_NONE != m_kAutoHuntConfig.iPickup && __AutoHunt_FindItem(rkInstMain, fNow))
{
m_dwAutoHuntItemID = 0;
m_eAutoHuntState = AUTO_HUNT_LOOT;
m_fAutoHuntStateTime = fNow - c_fAutoHuntLootDelay;
m_fAutoHuntNextActionTime = 0.0f;
return;
}
TPixelPosition kPPosMain;
rkInstMain.NEW_GetPixelPosition(&kPPosMain);
if (PixelDistance(kPPosMain, m_kPPosAutoHuntAnchor) > c_fAutoHuntReturnDistance)
{
__AutoHunt_ResetMovement();
m_eAutoHuntState = AUTO_HUNT_RETURN;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntNextActionTime = 0.0f;
}
break;
}
case AUTO_HUNT_RETURN:
{
if (fNow < m_fAutoHuntNextActionTime)
return;
m_fAutoHuntNextActionTime = fNow + 0.5f;
if (DWORD dwVID = __AutoHunt_FindTarget(rkInstMain, fNow))
{
__AutoHunt_Engage(dwVID, fNow);
return;
}
TPixelPosition kPPosMain;
rkInstMain.NEW_GetPixelPosition(&kPPosMain);
if (PixelDistance(kPPosMain, m_kPPosAutoHuntAnchor) < c_fAutoHuntReturnDistance * 0.5f ||
fNow - m_fAutoHuntStateTime > c_fAutoHuntReturnTimeout)
{
rkInstMain.NEW_Stop();
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
return;
}
if (!__AutoHunt_MoveTo(m_kPPosAutoHuntAnchor,100.f,fNow)) {
rkInstMain.NEW_Stop();
m_eAutoHuntState=AUTO_HUNT_SEARCH;
m_fAutoHuntNextActionTime=fNow+2.f;
}
break;
}
case AUTO_HUNT_ENGAGE:
{
CInstanceBase* pkInstVictim = NEW_FindActorPtr(m_dwAutoHuntTargetVID);
if (!pkInstVictim || !__AutoHunt_IsTargetAllowed(rkInstMain, *pkInstVictim))
{
__ClearAutoAttackTargetActorID(); __ClearReservedAction();
__AutoHunt_ResetMovement(); rkInstMain.NEW_Stop();
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
// The rest of the pack is still on the character: loot once it is down.
__AutoHunt_PurgeGathered();
const bool isPackAlive = m_isAutoHuntGathering || !m_kSet_dwAutoHuntGathered.empty();
m_eAutoHuntState = AUTO_HUNT_PICKUP_NONE == m_kAutoHuntConfig.iPickup || isPackAlive ? AUTO_HUNT_SEARCH : AUTO_HUNT_LOOT;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntNextActionTime = 0.0f;
return;
}
if (m_kAutoHuntConfig.bAttackStone && !pkInstVictim->IsStone()) {
const DWORD preferred = __AutoHunt_FindTarget(rkInstMain, fNow);
CInstanceBase* actor = NEW_FindActorPtr(preferred);
if (actor && actor->IsStone()) {
__AutoHunt_Engage(preferred, fNow);
return;
}
}
// Keep the configured pack supplied even while attacking, without interrupting it.
if (!pkInstVictim->IsStone()) __AutoHunt_RequestPull(rkInstMain, fNow);
const float fDistance = rkInstMain.GetDistance(pkInstVictim);
if (fDistance < m_fAutoHuntBestDistance - 30.0f)
{
m_fAutoHuntBestDistance = fDistance;
m_fAutoHuntProgressTime = fNow;
}
if (rkInstMain.IsAttacking() || rkInstMain.IsUsingSkill())
{
m_fAutoHuntProgressTime = fNow;
}
// Blocked path, a target out of reach, or a safe zone that refuses the attack.
const bool isBlockedBySafeZone = rkInstMain.IsInSafe() && rkInstMain.NEW_IsClickableDistanceDestInstance(*pkInstVictim);
if (isBlockedBySafeZone || fNow - m_fAutoHuntProgressTime > c_fAutoHuntNoProgressTime ||
fNow - m_fAutoHuntStateTime > c_fAutoHuntTargetTimeout)
{
m_kMap_dwAutoHuntSkipVID[m_dwAutoHuntTargetVID] = fNow + c_fAutoHuntSkipTime;
__ClearAutoAttackTargetActorID();
__ClearReservedAction(); __AutoHunt_ResetMovement(); rkInstMain.NEW_Stop();
m_dwAutoHuntTargetVID = 0;
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
return;
}
TPixelPosition goal; pkInstVictim->NEW_GetPixelPosition(&goal);
// Use the planner even in attack range: never swing or cast through a wall.
if (!__AutoHunt_MoveTo(goal,100.f,fNow)) {
m_kMap_dwAutoHuntSkipVID[m_dwAutoHuntTargetVID]=fNow+c_fAutoHuntSkipTime;
__ClearAutoAttackTargetActorID(); __ClearReservedAction(); rkInstMain.NEW_Stop();
m_dwAutoHuntTargetVID=0; m_eAutoHuntState=AUTO_HUNT_SEARCH; return;
}
if (!m_vecAutoHuntPath.empty()) return;
__AutoHunt_UpdateBuff(rkInstMain, fNow);
if (m_iAutoHuntMountState) return;
if (!m_isAutoHuntGathering)
__AutoHunt_UpdateAttackSkill(rkInstMain, *pkInstVictim, fNow);
if (m_iAutoHuntMountState) return;
// A cast or an interrupted approach drops the auto-attack target; put it back.
if (m_dwAutoAttackTargetVID != m_dwAutoHuntTargetVID && fNow >= m_fAutoHuntNextActionTime &&
!rkInstMain.IsUsingSkill() && MODE_USE_SKILL != m_eReservedMode)
{
m_fAutoHuntNextActionTime = fNow + 0.5f;
SetTarget(m_dwAutoHuntTargetVID);
__SetAutoAttackTargetActorID(m_dwAutoHuntTargetVID);
}
break;
}
case AUTO_HUNT_LOOT:
{
if (fNow - m_fAutoHuntStateTime < c_fAutoHuntLootDelay || fNow < m_fAutoHuntNextActionTime)
return;
m_fAutoHuntNextActionTime = fNow + 0.2f;
CPythonItem& rkItem = CPythonItem::Instance();
TPixelPosition kPPosItem;
if (m_dwAutoHuntItemID)
{
if (!rkItem.GetGroundItemPosition(m_dwAutoHuntItemID, &kPPosItem))
{
m_dwAutoHuntItemID = 0; // picked up (or gone)
}
else if (fNow - m_fAutoHuntProgressTime > c_fAutoHuntItemTimeout)
{
m_kMap_dwAutoHuntSkipItem[m_dwAutoHuntItemID] = fNow + c_fAutoHuntItemSkipTime;
m_dwAutoHuntItemID = 0;
__ClearReservedAction();
}
else
{
if (!__AutoHunt_MoveTo(kPPosItem,100.f,fNow)) {
m_kMap_dwAutoHuntSkipItem[m_dwAutoHuntItemID]=fNow+c_fAutoHuntItemSkipTime;
m_dwAutoHuntItemID=0; __AutoHunt_ResetMovement();
} else if (m_vecAutoHuntPath.empty() && MODE_CLICK_ITEM != m_eReservedMode)
__ReserveClickItem(m_dwAutoHuntItemID);
return;
}
}
const DWORD dwItemID = fNow - m_fAutoHuntStateTime < c_fAutoHuntLootTimeout ? __AutoHunt_FindItem(rkInstMain, fNow) : 0;
if (!dwItemID || !rkItem.GetGroundItemPosition(dwItemID, &kPPosItem))
{
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntNextActionTime = 0.0f;
return;
}
if (c_dwGoldVnum != rkItem.GetVirtualNumberOfGroundItem(dwItemID) && __AutoHunt_IsInventoryFull())
{
__AutoHunt_Stop(AUTO_HUNT_STOP_INVENTORY_FULL);
return;
}
// __OnPressItem without its once-per-item guard.
m_dwAutoHuntItemID = dwItemID;
m_fAutoHuntProgressTime = fNow;
__ClearReservedAction();
if (rkInstMain.NEW_IsClickableDistanceDestPixelPosition(kPPosItem))
{
rkInstMain.NEW_Stop();
SendClickItemPacket(dwItemID);
}
else
{
__AutoHunt_ResetMovement();
if (!__AutoHunt_MoveTo(kPPosItem,100.f,fNow)) {
m_kMap_dwAutoHuntSkipItem[dwItemID]=fNow+c_fAutoHuntItemSkipTime;
m_dwAutoHuntItemID=0;
}
}
break;
}
}
}
void CPythonPlayer::__AutoHunt_ResetMovement()
{
m_vecAutoHuntPath.clear();
m_fAutoHuntPathTime=0;
m_fAutoHuntPathMoveTime=0;
m_fAutoHuntTargetScanTime=0;
}
bool CPythonPlayer::__AutoHunt_BuildPath(const TPixelPosition& goal, float arrival, std::vector<TPixelPosition>& path)
{
auto* main=NEW_GetMainActorPtr();
auto& background=CPythonBackground::Instance();
if (!main || !background.IsMapReady()) return false;
TPixelPosition start; main->NEW_GetPixelPosition(&start);
const float bound=std::max(float(m_kAutoHuntConfig.iRange),PixelDistance(start,m_kPPosAutoHuntAnchor))+200.f;
auto walkable=[&](AutoHuntNavigation::Point p) {
if (std::hypot(p.x-m_kPPosAutoHuntAnchor.x,p.y-m_kPPosAutoHuntAnchor.y)>bound) return false;
// Missing/unloaded terrain is not a traversable cell. A small footprint keeps turns off wall edges.
for (const auto offset : {AutoHuntNavigation::Point{0,0}, {25,0}, {-25,0}, {0,25}, {0,-25}}) {
BYTE attr=0;
if (!background.GetAttr(p.x+offset.x,p.y+offset.y,&attr) || (attr&CTerrainImpl::ATTRIBUTE_BLOCK)) return false;
}
struct CollisionProbe {
CDynamicSphereInstance sphere;
bool blocked=false;
void operator()(CGraphicObjectInstance* object) {
if (!blocked && object && object->CollisionDynamicSphere(sphere)) blocked=true;
}
} probe;
const float height=background.GetHeight(p.x,p.y);
probe.sphere.v3Position=D3DXVECTOR3(p.x,-p.y,height+50.f);
probe.sphere.v3LastPosition=probe.sphere.v3Position;
probe.sphere.fRadius=25.f;
CCullingManager::Instance().ForInRange(Vector3d(p.x,-p.y,height+50.f),100.f,&probe);
return !probe.blocked;
};
const auto points=AutoHuntNavigation::FindPath({start.x,start.y},{goal.x,goal.y},arrival,walkable);
path.clear();
for (const auto& point:points) {
TPixelPosition position=goal; position.x=point.x; position.y=point.y;
path.push_back(position);
}
return !path.empty();
}
bool CPythonPlayer::__AutoHunt_MoveTo(const TPixelPosition& goal, float arrival, float now)
{
auto* main=NEW_GetMainActorPtr();
if (!main) return false;
TPixelPosition here; main->NEW_GetPixelPosition(&here);
if (m_fAutoHuntPathTime<=0 || (now-m_fAutoHuntPathTime>=1.f &&
(PixelDistance(goal,m_kAutoHuntPathGoal)>100.f || !main->IsWalking()))) {
if (!__AutoHunt_BuildPath(goal,arrival,m_vecAutoHuntPath)) return false;
m_kAutoHuntPathGoal=goal; m_fAutoHuntPathTime=now;
m_fAutoHuntPathMoveTime=0;
}
while (!m_vecAutoHuntPath.empty() && PixelDistance(here,m_vecAutoHuntPath.front())<=50.f)
m_vecAutoHuntPath.erase(m_vecAutoHuntPath.begin());
// The final planned point may be an accessible attack/pickup position beside a blocked target cell.
if (m_vecAutoHuntPath.size()==1 && PixelDistance(here,m_vecAutoHuntPath.front())<=arrival)
m_vecAutoHuntPath.clear();
if (m_vecAutoHuntPath.empty()) return true;
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
if (!main->IsUsingSkill() && now>=m_fAutoHuntPathMoveTime) {
m_fAutoHuntPathMoveTime=now+0.5f;
main->NEW_MoveToDestPixelPositionDirection(m_vecAutoHuntPath.front());
}
// Waypoint progress can temporarily increase distance to the enemy while going around a wall.
if (main->IsWalking() && PixelDistance(here,m_kAutoHuntPathLastPosition)>30.f) {
m_fAutoHuntProgressTime=now;
m_kAutoHuntPathLastPosition=here;
}
return true;
}
void CPythonPlayer::AutoHunt_OnDeath()
{
if (!AutoHunt_IsEnabled()) return;
if (!m_kAutoHuntConfig.bAutoRevive) { __AutoHunt_Stop(AUTO_HUNT_STOP_DEAD); return; }
if (m_eAutoHuntState==AUTO_HUNT_REVIVE) return;
__AutoHunt_CancelPull(); ++m_dwAutoHuntPullRequest;
__ClearAutoAttackTargetActorID(); __ClearReservedAction(); __AutoHunt_ResetMovement();
m_dwAutoHuntTargetVID=0; m_dwAutoHuntItemID=0;
m_iAutoHuntMountState=0;
m_isAutoHuntGathering=false; m_kSet_dwAutoHuntGathered.clear(); m_iAutoHuntGatheredGroups=0;
m_eAutoHuntState=AUTO_HUNT_REVIVE;
m_fAutoHuntStateTime=CTimer::Instance().GetCurrentSecond();
m_fAutoHuntReviveNextTime=m_fAutoHuntStateTime+10.f;
}
bool CPythonPlayer::__AutoHunt_UpdateRecovery(CInstanceBase& main, float now)
{
if (m_eAutoHuntState!=AUTO_HUNT_REVIVE && m_eAutoHuntState!=AUTO_HUNT_RECOVER) return false;
if (!m_kAutoHuntConfig.bAutoRevive) { __AutoHunt_Stop(AUTO_HUNT_STOP_DEAD); return true; }
if (main.IsDead()) {
if (now>=m_fAutoHuntReviveNextTime) {
CPythonNetworkStream::Instance().SendChatPacket("/restart_here");
m_fAutoHuntReviveNextTime=now+10.f; // server decides cooldown and whether revival is legal
}
return true;
}
if (m_eAutoHuntState==AUTO_HUNT_REVIVE) {
m_eAutoHuntState=AUTO_HUNT_RECOVER; m_fAutoHuntStateTime=now;
__ClearAutoAttackTargetActorID(); __ClearReservedAction(); main.NEW_Stop();
m_fAutoHuntNextPotionTime=0;
}
__AutoHunt_UpdatePotion(main,now);
const int maxHP=GetStatus(POINT_MAX_HP);
if (now-m_fAutoHuntStateTime<5.f || maxHP<=0 || int64_t(GetStatus(POINT_HP))*100<int64_t(maxHP)*70) return true;
m_kMap_dwAutoHuntSkipVID.clear(); m_kMap_dwAutoHuntSkipItem.clear();
__AutoHunt_ResetMovement();
m_eAutoHuntState=AUTO_HUNT_SEARCH; m_fAutoHuntStateTime=now;
m_fAutoHuntNextActionTime=0; m_fAutoHuntNextSkillTime=0;
return true;
}
bool CPythonPlayer::__AutoHunt_CastSkill(DWORD slot, float now)
{
auto* main=NEW_GetMainActorPtr();
if (!main || slot>=SKILL_MAX_NUM) return false;
CPythonSkill::TSkillData* data=nullptr;
if (!__AutoHunt_IsSkillReady(slot,&data)) return false;
if (data->IsAttackSkill()) {
auto* target=NEW_FindActorPtr(m_dwAutoHuntTargetVID);
if (!target || !__AutoHunt_IsTargetAllowed(*main,*target)) return false;
TPixelPosition goal; target->NEW_GetPixelPosition(&goal);
std::vector<TPixelPosition> line;
if (!__AutoHunt_BuildPath(goal,0.f,line) || line.size()!=1 || PixelDistance(line.back(),goal)>1.f) return false;
}
if (main->IsMountingHorse() && !data->IsHorseSkill()) {
if (m_iAutoHuntMountState || !m_kAutoHuntConfig.bDismountSkill || now<m_fAutoHuntMountNextTime) return false;
__ClearAutoAttackTargetActorID(); __ClearReservedAction(); main->NEW_Stop(); __AutoHunt_ResetMovement();
if (!CPythonNetworkStream::Instance().SendChatPacket("/ride")) return false;
m_dwAutoHuntPendingSkill=m_playerStatus.aSkill[slot].dwIndex;
m_iAutoHuntMountState=1; m_fAutoHuntMountDeadline=now+8.f;
return true;
}
const bool cast=__UseSkill(slot);
if (cast && m_iAutoHuntMountState==2) m_fAutoHuntMountCastEnd=now+1.f;
return cast;
}
bool CPythonPlayer::__AutoHunt_UpdateMount(CInstanceBase& main, float now)
{
if (!m_iAutoHuntMountState) return false;
__ClearAutoAttackTargetActorID();
if (m_iAutoHuntMountState==1) {
if (!main.IsMountingHorse()) {
m_iAutoHuntMountState=2; m_fAutoHuntMountDeadline=now+12.f;
m_fAutoHuntMountCastEnd=now+1.f;
} else if (now>=m_fAutoHuntMountDeadline) {
m_iAutoHuntMountState=0; m_fAutoHuntMountNextTime=now+30.f;
}
return true;
}
if (m_iAutoHuntMountState==3) {
if (main.IsMountingHorse() || now>=m_fAutoHuntMountDeadline) {
m_iAutoHuntMountState=0; m_fAutoHuntMountNextTime=now+10.f;
__AutoHunt_ResetMovement(); m_fAutoHuntProgressTime=now; m_fAutoHuntStateTime=now;
}
return true;
}
if (main.IsMountingHorse()) { m_iAutoHuntMountState=0; return true; }
if (now>=m_fAutoHuntMountDeadline && MODE_USE_SKILL==m_eReservedMode) __ClearReservedAction();
if (main.IsUsingSkill() || MODE_USE_SKILL==m_eReservedMode) return true;
if (m_dwAutoHuntPendingSkill) {
const DWORD index=m_dwAutoHuntPendingSkill; m_dwAutoHuntPendingSkill=0;
DWORD slot=0;
if (std::find(m_kAutoHuntConfig.vecSkillIndices.begin(),m_kAutoHuntConfig.vecSkillIndices.end(),index)!=m_kAutoHuntConfig.vecSkillIndices.end() &&
GetSkillSlotIndex(index,&slot)) {
CPythonSkill::TSkillData* data=nullptr;
auto* target=NEW_FindActorPtr(m_dwAutoHuntTargetVID);
if (__AutoHunt_IsSkillReady(slot,&data) && (!data->IsAttackSkill() ||
(target && __AutoHunt_IsTargetAllowed(main,*target) && main.GetDistance(target)<=std::max(float(data->GetTargetRange()),c_fAutoHuntSkillDistance)))) {
if (__AutoHunt_CastSkill(slot,now)) { m_fAutoHuntNextSkillTime=now+0.5f; return true; }
}
}
}
// Finish other selected, ready skills while already on foot, then restore riding once animations end.
if (now<m_fAutoHuntMountDeadline) {
if (__AutoHunt_UpdateBuff(main,now)) return true;
auto* target=NEW_FindActorPtr(m_dwAutoHuntTargetVID);
if (target && __AutoHunt_IsTargetAllowed(main,*target)) __AutoHunt_UpdateAttackSkill(main,*target,now);
}
if (main.IsUsingSkill() || MODE_USE_SKILL==m_eReservedMode || now<m_fAutoHuntMountCastEnd) return true;
__ClearReservedAction(); main.NEW_Stop();
CPythonNetworkStream::Instance().SendChatPacket("/ride");
m_iAutoHuntMountState=3; m_fAutoHuntMountDeadline=now+8.f;
return true;
}