Improve auto-hunt navigation, recovery and mounted skill casting
This commit is contained in:
@@ -650,3 +650,4 @@
|
||||
{"date": "2026-09-30", "unit": "EterBase/CRC32.cpp", "action": "fix-platform-adapter", "evidence": "MI 8 Lite Android 15 fake-login SIGABRT/std::bad_alloc from directory CRC. Reject non-regular/null FILE handles and failed seeks; preserve normal CRC/position. port_utils_test pass, port_map check 0 errors; rebuilt APK installed and on-device fake-login/map entry succeeds. docs/XIAOMI8LITE-DEVICE-TEST-20260930.md"}
|
||||
{"date": "2026-09-30", "action": "user-authorized-render-diagnostics", "units": ["UserInterface/PythonApplication.cpp", "GameLib/MapOutdoorRenderHTP.cpp", "GameLib/MapOutdoorRenderSTP.cpp"], "evidence": "7 same-scene MI 8 Lite runs, 42 two-second sample windows. Baseline 4.88 FPS/205.3ms, no terrain 15.45FPS/64.7ms, no scene 30.03FPS/17.7ms; repeated baseline 4.83 FPS. Normal rendering defaults unchanged. See docs/XIAOMI8LITE-RENDER-ISOLATION-20260930.md."}
|
||||
{"date": "2026-09-30", "unit": "Client/root/uiaffectshower.py", "result": "RUN_AS_IS unchanged; authorized PORT extension for server rates", "evidence": ["src/platform/ScriptLib/ServerBonusScript.inc", "tools/server/autohunt-pull-range/server_bonuses.h", "build/autohunt-fix-test.log", "build/autohunt-fix-mobile-test.log", "build/autohunt-bonuses-android.png"], "finding": "40250 global privileges emit login notices rather than character AFFECT packets; added distinct status channel, packed scripts unchanged."}
|
||||
{"date": "2026-10-01", "unit": "UserInterface/PythonPlayerAutoHunt.cpp", "action": "user-authorized-extension", "evidence": ["docs/AUTO-HUNT-ENHANCEMENTS.md", "build/autohunt-enhance-test.log", "build/autohunt-enhance-mobile-test.log", "build/autohunt-enhance-mac-build.log", "build/autohunt-enhance-android-build.log", "build/qa/autohunt-enhance-ui/settings.png"], "finding": "Added bounded terrain/scene-collision A* and reachability for combat/loot/return; independent learned skill-ID selection with legacy config migration; optional server restart_here and 5-second/70%-HP recovery; acknowledged dismount/cast/remount batching. Original manual movement and packed Python scripts unchanged. Desktop/mobile fake-server integration and macOS/Android builds pass; real-server soak not claimed."}
|
||||
|
||||
@@ -242,7 +242,17 @@
|
||||
"status": "PORTED",
|
||||
"impl": [
|
||||
"src/port/UserInterface/PythonPlayer.cpp:CPythonPlayer::NotifyDeadMainCharacter"
|
||||
]
|
||||
],
|
||||
"note": "Original auto-attack cleanup retained; user-authorized PORT auto-hunt extension optionally waits for server restart_here and recovery. Default remains stopping on death. See docs/AUTO-HUNT-ENHANCEMENTS.md.",
|
||||
"evidence": {
|
||||
"tests": [
|
||||
"tests/port/autohunt_recovery_checks.inc"
|
||||
],
|
||||
"commands": [
|
||||
"ctest --test-dir build/native -R ^port.login_flow$ --output-on-failure",
|
||||
"MT_TEST_MOBILE=1 ctest --test-dir build/native -R ^port.login_flow$ --output-on-failure"
|
||||
]
|
||||
}
|
||||
},
|
||||
"CPythonPlayer::NotifyChangePKMode": {
|
||||
"status": "PORTED",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# 自动狩猎:寻路、复活和独立技能配置
|
||||
|
||||
2026-10-01 用户授权的 PORT 玩法扩展。自动狩猎继续使用客户端状态机,攻击、施法、拾取、复活和骑乘通过原有接口与封包完成。
|
||||
|
||||
## 使用
|
||||
|
||||
F8 或移动端自动狩猎设置打开面板,选择已学会的技能。选择记录的是技能 ID,移动、删除、换页快捷栏不会改变自动施法选择。
|
||||
|
||||
- **自动复活**:默认关闭。开启后死亡进入等待状态,10 秒后请求 `/restart_here`,未复活则每 10 秒重试;服务端保留复活限制。只有真实角色存活后才进入恢复,至少等待 5 秒且 HP 达到 70% 后继续。启用了自动红药时,恢复阶段临时将喝药阈值提高到 70%,不修改保存的正常战斗阈值;关闭红药则等待自然恢复。
|
||||
- **下马施法**:默认开启。普通技能在马上就绪时,请求 `/ride` 下马,等待角色骑乘状态确实变为下马,施放选中的可用技能,等待技能动画结束后请求 `/ride` 骑乘。一次下马尽量施放多项就绪技能;马战技能保持在马上使用。
|
||||
- 下马/骑乘确认最多等待 8 秒,不反复发送切换命令。下马失败后暂缓 30 秒再尝试;完成一次施法骑乘后至少间隔 10 秒再开启下一轮下马。
|
||||
- 手动移动、世界点击或攻击会取消正在进行的自动骑乘流程并暂停狩猎。复活/恢复期间的这些手动操作会停止自动狩猎。停止、换地图和退出游戏清理自动动作。
|
||||
|
||||
骑乘依赖服务端原有 `/ride` 逻辑:普通马需要仍可骑乘;其他坐骑需要相应道具仍可用。原有命令可能优先选择已召唤的马或背包中第一件可用坐骑道具,不能保证恢复不同种类坐骑中的指定一种。
|
||||
|
||||
## 路径与目标
|
||||
|
||||
`AutoHuntNavigation.h` 提供自动狩猎专用、100 cm 网格的有界 A*,8 个方向移动,并对边、转角和简化后的路径逐段采样,禁止穿墙和切过封闭墙角。
|
||||
|
||||
运行时查询已经加载的 `attr.atr` 阻挡属性和场景对象碰撞体,使用 25 cm 的通行余量;未加载的格子视为不可通行。先尝试直达,不能直达时寻找绕障路径。普通目标和引怪目标均按优先级检查可达性,每轮最多检查 4 个候选,每个搜索最多扩展 3500 个节点。没有找到路径的候选短暂跳过 5 秒;实际移动无进展仍使用原有超时和较长黑名单。
|
||||
|
||||
追怪、拾取和返回挂机点都沿路径节点移动,最终攻击和拾取仍走原有流程。绕障时实际位置变化会更新进展时间,避免因为暂时远离目标而误判卡住;攻击技能施放前还检查当前直达路径,防止目标移动到墙后仍施法。
|
||||
|
||||
这是有界、基于当前加载数据的规划:很长的绕行、过窄通道、临时角色阻挡或场景数据不完整仍可能找不到路线。不会因找不到路径而退回穿墙直走。原有手动移动没有修改。
|
||||
|
||||
## 配置和代码
|
||||
|
||||
`autohunt.cfg` 升级为 `AUTOHUNT2` 格式,保存复活开关、下马施法开关和独立技能 ID。旧 7/8 字段格式仍可读取;进入游戏后将旧勾选快捷栏的技能迁移一次。技能界面只列出已学会且可主动使用的职业/马战技能,不列被动技能。
|
||||
|
||||
- `src/port/UserInterface/PythonPlayerAutoHunt.cpp`:状态机、地图查询、复活和骑乘流程。
|
||||
- `src/port/UserInterface/AutoHuntNavigation.h`:路径规划。
|
||||
- `src/platform/ScriptLib/AutoHuntScript.inc`:设置面板、按技能名称勾选和状态显示。
|
||||
- `src/port/UserInterface/PythonPlayerModule.cpp`:独立技能选择接口。
|
||||
|
||||
## 验证
|
||||
|
||||
- `host.autohunt_navigation`:直达、绕墙、封闭墙、近距离隔墙、墙角、搜索预算、未加载数据、异常坐标。
|
||||
- `port.login_flow` / `MT_TEST_MOBILE=1 port.login_flow`:真实 Python 面板和接口;技能选择不受快捷栏变化影响;关闭下马施法不发送骑乘命令;手动操作取消待确认的流程;模拟服务端角色重建确认下马后施法、施法后重新骑乘;默认死亡停止、可选原地复活请求和 HP 恢复门槛。
|
||||
- macOS `mt_native_render` 和 Android arm64 `main` 构建。
|
||||
|
||||
集成测试使用假服并模拟骑乘/存活确认;真实服务器拒绝条件、复杂地图上的长期寻路和真机长时间挂机尚需实测。
|
||||
|
||||
中文面板已用原生 Vulkan 渲染器检查手机布局,截图位于 `build/qa/autohunt-enhance-ui/settings.png`;截图使用模拟的可选技能列表,实际已学技能过滤由上述集成测试覆盖。
|
||||
@@ -54,6 +54,10 @@ metin2-client 是 40250 Windows 客户端的跨平台版本。**首要目标是
|
||||
2026-09-30 用户授权扩展:移动戒指列出 203 已登记的全部 65 张地图,按六类分页,包含活动、战争和测试地图;
|
||||
受限副本走原有入口,活动保留入场流程,测试传送仅限 GM。见 `tools/server/ring-warp/README.md`。
|
||||
|
||||
2026-10-01 用户授权扩展:自动狩猎增加基于已加载地图阻挡与场景碰撞的有界 A*,追怪/拾取/返回检查可达性;
|
||||
可选原地复活并恢复后继续;技能按独立技能 ID 选择,支持确认下马后施法及施法后重新骑乘。
|
||||
见 `docs/AUTO-HUNT-ENHANCEMENTS.md`,不修改原版打包 Python 脚本及手动移动。
|
||||
|
||||
## 3. 三个层和代码位置
|
||||
|
||||
| 层 | 40250 单元 | 做法 | 位置 |
|
||||
|
||||
@@ -8,14 +8,15 @@ import app
|
||||
import chat
|
||||
import game
|
||||
import player
|
||||
import skill
|
||||
import ui
|
||||
import wndMgr
|
||||
|
||||
L = player.AutoHuntLocalize
|
||||
|
||||
STATE_OFF, STATE_SEARCH, STATE_ENGAGE, STATE_LOOT, STATE_RETURN, STATE_PAUSED = range(6)
|
||||
STATE_OFF, STATE_SEARCH, STATE_ENGAGE, STATE_LOOT, STATE_RETURN, STATE_PAUSED, STATE_REVIVE, STATE_RECOVER = range(8)
|
||||
STOP_NONE, STOP_USER, STOP_DEAD, STOP_MAP, STOP_INVENTORY_FULL = range(5)
|
||||
SKILL_SLOT_NUM = 8
|
||||
SKILL_PAGE_SIZE = 12
|
||||
|
||||
STATE_TEXT = {
|
||||
STATE_SEARCH: "寻找目标",
|
||||
@@ -23,6 +24,8 @@ STATE_TEXT = {
|
||||
STATE_LOOT: "拾取中",
|
||||
STATE_RETURN: "返回挂机点",
|
||||
STATE_PAUSED: "手动操作,3秒后继续",
|
||||
STATE_REVIVE: "等待原地复活",
|
||||
STATE_RECOVER: "恢复中(血量达到70%后继续)",
|
||||
}
|
||||
STOP_TEXT = {
|
||||
STOP_USER: "自动狩猎已停止",
|
||||
@@ -163,15 +166,35 @@ class SettingDialog(ui.BoardWithTitleBar):
|
||||
self.gatherRow = ChoiceRow(self, y, "披风引怪", GATHER_TEXTS, SMALL_BUTTON, 48, self.__Save)
|
||||
y += 28
|
||||
|
||||
self.reviveButton = FlagButton(self, 18, y, MIDDLE_BUTTON, "自动复活", self.__Save)
|
||||
self.mountButton = FlagButton(self, 90, y, MIDDLE_BUTTON, "下马施法", self.__Save)
|
||||
y += 28
|
||||
self.skillLabel = ui.TextLine()
|
||||
self.skillLabel.SetParent(self)
|
||||
self.skillLabel.SetPosition(18, y + 4)
|
||||
self.skillLabel.SetText(L("技能栏"))
|
||||
self.skillLabel.SetText(L("选择自动使用的技能"))
|
||||
self.skillLabel.Show()
|
||||
self.skillPage = 0
|
||||
self.selectedSkills = set()
|
||||
self.availableSkills = ()
|
||||
self.skillButtons = []
|
||||
for i in xrange(SKILL_SLOT_NUM):
|
||||
self.skillButtons.append(FlagButton(self, 70 + i * 31, y, XSMALL_BUTTON, str(i + 1), self.__Save))
|
||||
y += 28
|
||||
self.skillIDs = []
|
||||
self.skillPrev = ui.Button()
|
||||
self.skillNext = ui.Button()
|
||||
for button, x, text, step in ((self.skillPrev, 240, "<", -1), (self.skillNext, 273, ">", 1)):
|
||||
button.SetParent(self)
|
||||
button.SetPosition(x, y)
|
||||
button.SetUpVisual(XSMALL_BUTTON % 1)
|
||||
button.SetOverVisual(XSMALL_BUTTON % 2)
|
||||
button.SetDownVisual(XSMALL_BUTTON % 3)
|
||||
button.SetText(text)
|
||||
button.SetEvent(lambda step=step: self.__ChangeSkillPage(step))
|
||||
y += 24
|
||||
for i in xrange(SKILL_PAGE_SIZE):
|
||||
button = FlagButton(self, 25 + (i % 2) * 150, y + (i // 2) * 24,
|
||||
"d:/ymir work/ui/public/large_button_%02d.sub", "", lambda i=i: self.__SelectSkill(i))
|
||||
self.skillButtons.append(button)
|
||||
y += 24 * (SKILL_PAGE_SIZE // 2)
|
||||
|
||||
self.targetLabel = ui.TextLine()
|
||||
self.targetLabel.SetParent(self)
|
||||
@@ -209,21 +232,28 @@ class SettingDialog(ui.BoardWithTitleBar):
|
||||
def Destroy(self):
|
||||
for row in (self.rangeRow, self.hpRow, self.spRow, self.pickupRow, self.gatherRow):
|
||||
row.Destroy()
|
||||
for button in self.skillButtons + [self.stoneButton, self.bossButton]:
|
||||
for button in self.skillButtons + [self.stoneButton, self.bossButton, self.reviveButton, self.mountButton]:
|
||||
button.Destroy()
|
||||
self.skillButtons = []
|
||||
self.skillPrev.SetEvent(None)
|
||||
self.skillNext.SetEvent(None)
|
||||
self.startButton.SetEvent(None)
|
||||
self.startButton = None
|
||||
self.Hide()
|
||||
|
||||
def Open(self):
|
||||
(range_, hp, sp, pickup, stone, boss, mask, gather) = player.AutoHuntGetConfig()
|
||||
(range_, hp, sp, pickup, stone, boss, mask, gather, revive, dismount) = player.AutoHuntGetConfig()
|
||||
self.rangeRow.Select(_Nearest(RANGE_VALUES, range_), False)
|
||||
self.hpRow.Select(_Nearest(HP_VALUES, hp), False)
|
||||
self.spRow.Select(_Nearest(SP_VALUES, sp), False)
|
||||
self.pickupRow.Select(pickup, False)
|
||||
self.gatherRow.Select(_Nearest(GATHER_VALUES, gather), False)
|
||||
for i in xrange(SKILL_SLOT_NUM):
|
||||
self.skillButtons[i].SetFlag(bool(mask & (1 << i)))
|
||||
selected, self.availableSkills = player.AutoHuntGetSkills()
|
||||
self.selectedSkills = set(selected)
|
||||
self.skillPage = 0
|
||||
self.__RefreshSkills()
|
||||
self.reviveButton.SetFlag(bool(revive))
|
||||
self.mountButton.SetFlag(bool(dismount))
|
||||
self.stoneButton.SetFlag(bool(stone))
|
||||
self.bossButton.SetFlag(bool(boss))
|
||||
self.isStarted = None
|
||||
@@ -243,14 +273,43 @@ class SettingDialog(ui.BoardWithTitleBar):
|
||||
self.isStarted = isStarted
|
||||
self.startButton.SetText(L(isStarted and "停止狩猎" or "开始狩猎"))
|
||||
|
||||
def __RefreshSkills(self):
|
||||
start = self.skillPage * SKILL_PAGE_SIZE
|
||||
self.skillIDs = self.availableSkills[start:start + SKILL_PAGE_SIZE]
|
||||
for i, button in enumerate(self.skillButtons):
|
||||
if i < len(self.skillIDs):
|
||||
index = self.skillIDs[i]
|
||||
button.SetText(skill.GetSkillName(index))
|
||||
button.SetFlag(index in self.selectedSkills)
|
||||
button.Show()
|
||||
else:
|
||||
button.Hide()
|
||||
self.skillLabel.SetText(L(self.availableSkills and "选择自动使用的技能" or "尚未学会可用技能"))
|
||||
for button in (self.skillPrev, self.skillNext):
|
||||
if len(self.availableSkills) > SKILL_PAGE_SIZE:
|
||||
button.Show()
|
||||
else:
|
||||
button.Hide()
|
||||
|
||||
def __ChangeSkillPage(self, step):
|
||||
pages = max(1, (len(self.availableSkills) + SKILL_PAGE_SIZE - 1) // SKILL_PAGE_SIZE)
|
||||
self.skillPage = (self.skillPage + step) % pages
|
||||
self.__RefreshSkills()
|
||||
|
||||
def __SelectSkill(self, slot):
|
||||
if slot >= len(self.skillIDs):
|
||||
return
|
||||
index = self.skillIDs[slot]
|
||||
if self.skillButtons[slot].flag:
|
||||
self.selectedSkills.add(index)
|
||||
else:
|
||||
self.selectedSkills.discard(index)
|
||||
player.AutoHuntSetSkills(sorted(self.selectedSkills))
|
||||
|
||||
def __Save(self):
|
||||
mask = 0
|
||||
for i in xrange(SKILL_SLOT_NUM):
|
||||
if self.skillButtons[i].flag:
|
||||
mask |= 1 << i
|
||||
player.AutoHuntSetConfig(RANGE_VALUES[self.rangeRow.index], HP_VALUES[self.hpRow.index],
|
||||
SP_VALUES[self.spRow.index], self.pickupRow.index, int(self.stoneButton.flag), int(self.bossButton.flag), mask,
|
||||
GATHER_VALUES[self.gatherRow.index])
|
||||
SP_VALUES[self.spRow.index], self.pickupRow.index, int(self.stoneButton.flag), int(self.bossButton.flag), 0,
|
||||
GATHER_VALUES[self.gatherRow.index], int(self.reviveButton.flag), int(self.mountButton.flag))
|
||||
|
||||
def __OnClickStart(self):
|
||||
Toggle()
|
||||
@@ -293,12 +352,14 @@ class Ticker(ui.Window):
|
||||
config = player.AutoHuntGetConfig()
|
||||
displayState = state + (config[7],)
|
||||
if displayState != self.last:
|
||||
(stateIndex, reason, noHP, noSP, gathering, gathered) = state
|
||||
(stateIndex, reason, noHP, noSP, gathering, gathered, mountState) = state
|
||||
wasOn = self.last and self.last[0] != STATE_OFF
|
||||
self.last = displayState
|
||||
if stateIndex != STATE_OFF:
|
||||
text = L("自动狩猎 · ") + L(STATE_TEXT.get(stateIndex, ""))
|
||||
if gathering:
|
||||
if mountState:
|
||||
text = L("自动狩猎 · ") + L({1: "等待下马", 2: "下马施法中", 3: "等待重新骑乘"}.get(mountState, ""))
|
||||
elif gathering:
|
||||
text = L("自动狩猎 · 披风引怪中(最多%d组)" % player.AutoHuntGetConfig()[7])
|
||||
elif config[7]:
|
||||
text += L(" · 本轮引怪%d/%d组" % (gathered, config[7]))
|
||||
|
||||
@@ -198,6 +198,8 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
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)
|
||||
add_executable(host_autohunt_navigation_test ${PROJECT_SOURCE_DIR}/tests/host/autohunt_navigation_test.cpp)
|
||||
add_test(NAME host.autohunt_navigation COMMAND host_autohunt_navigation_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)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
// PORT: bounded A* for auto hunt only. Queries loaded terrain and scene collision through a callback.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace AutoHuntNavigation {
|
||||
struct Point { float x, y; };
|
||||
inline float Distance(Point a, Point b) { return std::hypot(a.x-b.x, a.y-b.y); }
|
||||
using Walkable = std::function<bool(Point)>;
|
||||
inline bool Segment(Point a, Point b, const Walkable& walkable) {
|
||||
const int samples = std::max(1, int(std::ceil(Distance(a,b) / 40.f)));
|
||||
Point previous=a;
|
||||
for (int i=1; i<=samples; ++i) {
|
||||
const float t = float(i)/samples;
|
||||
const Point next{a.x+(b.x-a.x)*t, a.y+(b.y-a.y)*t};
|
||||
if (!walkable(next)) return false;
|
||||
if (next.x!=previous.x && next.y!=previous.y &&
|
||||
(!walkable({next.x,previous.y}) || !walkable({previous.x,next.y}))) return false;
|
||||
previous=next;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Empty means unreachable within the loaded data/search budget, never permission to walk straight.
|
||||
inline std::vector<Point> FindPath(Point start, Point goal, float arrival, const Walkable& walkable,
|
||||
int budget=3500) {
|
||||
if (!std::isfinite(start.x) || !std::isfinite(start.y) || !std::isfinite(goal.x) ||
|
||||
!std::isfinite(goal.y) || arrival < 0 || budget <= 0 || Distance(start,goal)>40000.f) return {};
|
||||
if (Distance(start,goal) <= arrival && Segment(start,goal,walkable)) return {start};
|
||||
if (Segment(start,goal,walkable)) return {goal};
|
||||
constexpr float cell=100.f;
|
||||
// Grid is relative to the exact starting position, so the first step cannot cross an unchecked edge.
|
||||
const int extent = int(std::ceil(Distance(start,goal)/cell))+25;
|
||||
const int width = extent*2+1;
|
||||
auto key = [=](int x,int y) { return (y+extent)*width+x+extent; };
|
||||
auto point = [=](int x,int y) -> Point { return {start.x+x*cell,start.y+y*cell}; };
|
||||
struct Node { float cost=std::numeric_limits<float>::infinity(); int parent=-1; bool closed=false; };
|
||||
struct Entry { float score; int x,y; bool operator<(const Entry& b) const { return score>b.score; } };
|
||||
std::unordered_map<int,Node> nodes;
|
||||
std::unordered_map<int,bool> passable;
|
||||
auto canWalk = [&](int x,int y) {
|
||||
const int id=key(x,y);
|
||||
auto i=passable.find(id);
|
||||
if (i!=passable.end()) return i->second;
|
||||
return passable[id]=walkable(point(x,y));
|
||||
};
|
||||
std::priority_queue<Entry> open;
|
||||
const int first=key(0,0);
|
||||
nodes[first].cost=0;
|
||||
open.push({Distance(start,goal),0,0});
|
||||
int finish=-1;
|
||||
while (!open.empty() && budget>0) {
|
||||
const auto e=open.top(); open.pop();
|
||||
const int id=key(e.x,e.y);
|
||||
if (nodes[id].closed) continue;
|
||||
nodes[id].closed=true;
|
||||
--budget;
|
||||
const float cost=nodes[id].cost; // inserts below may rehash the map
|
||||
const Point p=point(e.x,e.y);
|
||||
if (Distance(p,goal)<=std::max(arrival,cell) &&
|
||||
Segment(p,goal,walkable)) { finish=id; break; }
|
||||
for (int dy=-1;dy<=1;++dy) for (int dx=-1;dx<=1;++dx) {
|
||||
if (!dx && !dy) continue;
|
||||
const int x=e.x+dx,y=e.y+dy;
|
||||
if (std::abs(x)>extent || std::abs(y)>extent || !canWalk(x,y)) continue;
|
||||
if (dx && dy && (!canWalk(e.x+dx,e.y) || !canWalk(e.x,e.y+dy))) continue;
|
||||
const int next=key(x,y);
|
||||
const float nextCost=cost+((dx && dy)?141.42136f:100.f);
|
||||
if (nodes[next].closed || nextCost>=nodes[next].cost || !Segment(p,point(x,y),walkable)) continue;
|
||||
nodes[next].cost=nextCost;
|
||||
nodes[next].parent=id;
|
||||
open.push({nextCost+std::max(0.f,Distance(point(x,y),goal)-arrival),x,y});
|
||||
}
|
||||
}
|
||||
if (finish<0) return {};
|
||||
std::vector<Point> reverse;
|
||||
for (int id=finish;id!=first;id=nodes[id].parent)
|
||||
reverse.push_back(point(id%width-extent,id/width-extent));
|
||||
std::reverse(reverse.begin(),reverse.end());
|
||||
if (reverse.empty()) reverse.push_back(start);
|
||||
if (Distance(reverse.back(),goal)>arrival) reverse.push_back(goal);
|
||||
// Greedy line-of-sight smoothing, retaining every necessary turn.
|
||||
std::vector<Point> result;
|
||||
Point from=start;
|
||||
for (size_t i=0;i<reverse.size();) {
|
||||
size_t end=i;
|
||||
while (end+1<reverse.size() && Segment(from,reverse[end+1],walkable)) ++end;
|
||||
result.push_back(reverse[end]); from=reverse[end]; i=end+1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -526,9 +526,8 @@ void CPythonPlayer::NotifyCharacterUpdate(DWORD dwVID)
|
||||
void CPythonPlayer::NotifyDeadMainCharacter()
|
||||
{
|
||||
__ClearAutoAttackTargetActorID();
|
||||
// PORT: auto hunt stops with the character.
|
||||
if (AutoHunt_IsEnabled())
|
||||
__AutoHunt_Stop(AUTO_HUNT_STOP_DEAD);
|
||||
// PORT: optional auto-hunt recovery uses the normal server restart command.
|
||||
AutoHunt_OnDeath();
|
||||
}
|
||||
|
||||
void CPythonPlayer::NotifyChangePKMode()
|
||||
@@ -1704,6 +1703,8 @@ void CPythonPlayer::Clear()
|
||||
|
||||
// PORT: leaving the game (logout, character select) ends auto hunt without a stop notice.
|
||||
m_eAutoHuntState = AUTO_HUNT_OFF;
|
||||
m_iAutoHuntMountState = 0;
|
||||
m_vecAutoHuntPath.clear();
|
||||
m_eAutoHuntStopReason = AUTO_HUNT_STOP_NONE;
|
||||
m_dwAutoHuntTargetVID = 0;
|
||||
m_dwAutoHuntItemID = 0;
|
||||
|
||||
@@ -686,6 +686,8 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
AUTO_HUNT_LOOT,
|
||||
AUTO_HUNT_RETURN,
|
||||
AUTO_HUNT_PAUSED,
|
||||
AUTO_HUNT_REVIVE,
|
||||
AUTO_HUNT_RECOVER,
|
||||
};
|
||||
|
||||
enum EAutoHuntStopReason
|
||||
@@ -706,7 +708,7 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
|
||||
enum
|
||||
{
|
||||
AUTO_HUNT_SKILL_SLOT_NUM = 8, // the local quick slots on the taskbar's first page
|
||||
AUTO_HUNT_SKILL_SLOT_NUM = 8, // legacy config migration only
|
||||
};
|
||||
|
||||
struct SAutoHuntConfig
|
||||
@@ -717,8 +719,11 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
int iPickup; // EAutoHuntPickup
|
||||
bool bAttackStone;
|
||||
bool bAttackBoss;
|
||||
DWORD dwSkillSlotMask; // bit i = local quick slot i may be cast
|
||||
DWORD dwSkillSlotMask; // legacy config migration only
|
||||
int iGatherCount; // cloak pull: max 3/10/20 server monster groups; 0 = off
|
||||
bool bAutoRevive = false;
|
||||
bool bDismountSkill = true;
|
||||
std::vector<DWORD> vecSkillIndices; // independent of the quick bar
|
||||
};
|
||||
|
||||
void AutoHunt_SetEnabled(bool isEnable);
|
||||
@@ -730,6 +735,10 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
int AutoHunt_GetGatheredCount();
|
||||
bool AutoHunt_HandleServerCommand(const char* command); // PORT: server-authoritative cloak pull replies
|
||||
bool AutoHunt_HasNoPotion(bool isHP);
|
||||
std::vector<DWORD> AutoHunt_GetAvailableSkills();
|
||||
void AutoHunt_SetSkills(const std::vector<DWORD>& indices);
|
||||
void AutoHunt_OnDeath();
|
||||
int AutoHunt_GetMountState() const { return m_iAutoHuntMountState; }
|
||||
const SAutoHuntConfig& AutoHunt_GetConfig();
|
||||
void AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig);
|
||||
|
||||
@@ -742,6 +751,13 @@ 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);
|
||||
bool __AutoHunt_CastSkill(DWORD slot, float now);
|
||||
bool __AutoHunt_UpdateMount(CInstanceBase& main, float now);
|
||||
bool __AutoHunt_UpdateRecovery(CInstanceBase& main, float now);
|
||||
bool __AutoHunt_BuildPath(const TPixelPosition& goal, float arrival, std::vector<TPixelPosition>& path);
|
||||
bool __AutoHunt_MoveTo(const TPixelPosition& goal, float arrival, float now);
|
||||
void __AutoHunt_ResetMovement();
|
||||
void __AutoHunt_MigrateSkills();
|
||||
bool __AutoHunt_IsTargetAllowed(CInstanceBase& main, CInstanceBase& target); // PORT extension
|
||||
bool __AutoHunt_RequestPull(CInstanceBase& main, float now); // PORT extension
|
||||
DWORD __AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow);
|
||||
@@ -753,6 +769,17 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
void __AutoHunt_LoadConfig();
|
||||
void __AutoHunt_SaveConfig();
|
||||
|
||||
bool m_isAutoHuntLegacySkills = false;
|
||||
int m_iAutoHuntMountState = 0; // 0 normal, 1 wait dismount, 2 cast, 3 wait remount
|
||||
DWORD m_dwAutoHuntPendingSkill = 0;
|
||||
float m_fAutoHuntMountDeadline = 0, m_fAutoHuntMountNextTime = 0;
|
||||
float m_fAutoHuntMountCastEnd = 0;
|
||||
float m_fAutoHuntReviveNextTime = 0;
|
||||
std::vector<TPixelPosition> m_vecAutoHuntPath;
|
||||
TPixelPosition m_kAutoHuntPathGoal = {}, m_kAutoHuntPathLastPosition = {};
|
||||
DWORD m_dwAutoHuntScanTarget = 0;
|
||||
float m_fAutoHuntTargetScanTime = 0;
|
||||
float m_fAutoHuntPathTime = 0, m_fAutoHuntPathMoveTime = 0;
|
||||
SAutoHuntConfig m_kAutoHuntConfig;
|
||||
bool m_isAutoHuntConfigLoaded;
|
||||
int m_eAutoHuntState;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// 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
|
||||
// __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"
|
||||
@@ -21,6 +21,9 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
#include "AutoHuntNavigation.h"
|
||||
#include "../EterLib/CullingManager.h"
|
||||
#include "../EterLib/GrpObjectInstance.h"
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
@@ -90,6 +93,9 @@ void CPythonPlayer::AutoHunt_SetEnabled(bool isEnable)
|
||||
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;
|
||||
@@ -157,6 +163,7 @@ void CPythonPlayer::AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig)
|
||||
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);
|
||||
@@ -174,38 +181,92 @@ void CPythonPlayer::__AutoHunt_LoadConfig()
|
||||
m_isAutoHuntConfigLoaded = true;
|
||||
|
||||
FILE* fp = fopen(c_szAutoHuntConfigFileName, "rt");
|
||||
if (!fp)
|
||||
return;
|
||||
|
||||
SAutoHuntConfig kConfig = m_kAutoHuntConfig;
|
||||
int iStone = 0, iBoss = 0, iGather = 0;
|
||||
unsigned int uMask = 0;
|
||||
// 7 fields before gather mode.
|
||||
const int iRead = fscanf(fp, "%d %d %d %d %d %d %u %d", &kConfig.iRange, &kConfig.iHPPercent, &kConfig.iSPPercent,
|
||||
&kConfig.iPickup, &iStone, &iBoss, &uMask, &iGather);
|
||||
if (iRead >= 7)
|
||||
{
|
||||
kConfig.iGatherCount = 8 == iRead ? iGather : 0;
|
||||
kConfig.bAttackStone = iStone != 0;
|
||||
kConfig.bAttackBoss = iBoss != 0;
|
||||
kConfig.dwSkillSlotMask = uMask;
|
||||
fclose(fp);
|
||||
AutoHunt_SetConfig(kConfig);
|
||||
return;
|
||||
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, "%d %d %d %d %d %d %u %d\n", m_kAutoHuntConfig.iRange, m_kAutoHuntConfig.iHPPercent,
|
||||
m_kAutoHuntConfig.iSPPercent, m_kAutoHuntConfig.iPickup, m_kAutoHuntConfig.bAttackStone ? 1 : 0,
|
||||
m_kAutoHuntConfig.bAttackBoss ? 1 : 0, (unsigned int)m_kAutoHuntConfig.dwSkillSlotMask,
|
||||
m_kAutoHuntConfig.iGatherCount);
|
||||
fclose(fp);
|
||||
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()
|
||||
@@ -221,14 +282,11 @@ void CPythonPlayer::__AutoHunt_CancelPull()
|
||||
void CPythonPlayer::__AutoHunt_Stop(int eReason)
|
||||
{
|
||||
__AutoHunt_CancelPull();
|
||||
if (AUTO_HUNT_ENGAGE == m_eAutoHuntState || AUTO_HUNT_LOOT == m_eAutoHuntState || AUTO_HUNT_RETURN == m_eAutoHuntState)
|
||||
{
|
||||
__ClearAutoAttackTargetActorID();
|
||||
__ClearReservedAction();
|
||||
if (AUTO_HUNT_RETURN == m_eAutoHuntState)
|
||||
if (CInstanceBase* pkInstMain = NEW_GetMainActorPtr())
|
||||
pkInstMain->NEW_Stop();
|
||||
}
|
||||
__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;
|
||||
@@ -244,6 +302,11 @@ 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();
|
||||
@@ -267,7 +330,8 @@ void CPythonPlayer::__AutoHunt_Engage(DWORD dwVID, float fNow)
|
||||
|
||||
SetTarget(dwVID);
|
||||
__ClearReservedAction();
|
||||
__SetAutoAttackTargetActorID(dwVID);
|
||||
__ClearAutoAttackTargetActorID();
|
||||
__AutoHunt_ResetMovement();
|
||||
}
|
||||
|
||||
// PORT: stones have their own option; their mob_proto rank must not count as a boss.
|
||||
@@ -311,58 +375,59 @@ bool CPythonPlayer::__AutoHunt_RequestPull(CInstanceBase& main, float now)
|
||||
DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow)
|
||||
{
|
||||
CPythonCharacterManager& rkChrMgr = CPythonCharacterManager::Instance();
|
||||
|
||||
DWORD dwBestVID = 0;
|
||||
float fBestDistance = 1e9f;
|
||||
bool bestStone = false;
|
||||
for (CPythonCharacterManager::CharacterIterator i = rkChrMgr.CharacterInstanceBegin(); i != rkChrMgr.CharacterInstanceEnd(); ++i)
|
||||
{
|
||||
CInstanceBase* pkInst = *i;
|
||||
if (!pkInst || !__AutoHunt_IsTargetAllowed(rkInstMain, *pkInst))
|
||||
continue;
|
||||
|
||||
std::map<DWORD, float>::iterator f = m_kMap_dwAutoHuntSkipVID.find(pkInst->GetVirtualID());
|
||||
if (f != m_kMap_dwAutoHuntSkipVID.end() && fNow < f->second)
|
||||
continue;
|
||||
|
||||
TPixelPosition kPPos;
|
||||
pkInst->NEW_GetPixelPosition(&kPPos);
|
||||
if (PixelDistance(kPPos, m_kPPosAutoHuntAnchor) > float(m_kAutoHuntConfig.iRange))
|
||||
continue;
|
||||
|
||||
const float fDistance = rkInstMain.GetDistance(pkInst);
|
||||
const bool stone = pkInst->IsStone();
|
||||
if ((stone && !bestStone) || (stone == bestStone && fDistance < fBestDistance))
|
||||
{
|
||||
bestStone = stone;
|
||||
fBestDistance = fDistance;
|
||||
dwBestVID = pkInst->GetVirtualID();
|
||||
}
|
||||
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;
|
||||
}
|
||||
return dwBestVID;
|
||||
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)
|
||||
{
|
||||
DWORD dwBestVID = 0;
|
||||
float fBestDistance = 1e9f;
|
||||
for (DWORD dwVID : m_kSet_dwAutoHuntGathered)
|
||||
{
|
||||
CInstanceBase* pkInst = NEW_FindActorPtr(dwVID);
|
||||
if (!pkInst || !__AutoHunt_IsTargetAllowed(rkInstMain, *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)
|
||||
{
|
||||
fBestDistance = fDistance;
|
||||
dwBestVID = dwVID;
|
||||
}
|
||||
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);
|
||||
}
|
||||
return dwBestVID;
|
||||
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.
|
||||
@@ -495,8 +560,9 @@ bool CPythonPlayer::__AutoHunt_IsSkillReady(DWORD dwSkillSlot, CPythonSkill::TSk
|
||||
return false;
|
||||
if (!pSkillData->IsCanUseSkill())
|
||||
return false;
|
||||
if ((pkInstMain->IsMountingHorse() ? true : false) != (pSkillData->IsHorseSkill() ? true : false))
|
||||
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())
|
||||
@@ -519,21 +585,16 @@ bool CPythonPlayer::__AutoHunt_IsSkillReady(DWORD dwSkillSlot, CPythonSkill::TSk
|
||||
return true;
|
||||
}
|
||||
|
||||
// Self buffs from the enabled quick slots whose effect is not on the character.
|
||||
// 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 i = 0; i < AUTO_HUNT_SKILL_SLOT_NUM; ++i)
|
||||
for (DWORD index : m_kAutoHuntConfig.vecSkillIndices)
|
||||
{
|
||||
if (!(m_kAutoHuntConfig.dwSkillSlotMask & (1 << i)))
|
||||
continue;
|
||||
|
||||
DWORD dwType = SLOT_TYPE_NONE, dwSkillSlot = 0;
|
||||
GetLocalQuickSlotData(i, &dwType, &dwSkillSlot);
|
||||
if (SLOT_TYPE_SKILL != dwType)
|
||||
continue;
|
||||
DWORD dwSkillSlot = 0;
|
||||
if (!GetSkillSlotIndex(index, &dwSkillSlot)) continue;
|
||||
|
||||
CPythonSkill::TSkillData* pSkillData;
|
||||
if (!__AutoHunt_IsSkillReady(dwSkillSlot, &pSkillData))
|
||||
@@ -560,7 +621,7 @@ bool CPythonPlayer::__AutoHunt_UpdateBuff(CInstanceBase& rkInstMain, float fNow)
|
||||
}
|
||||
|
||||
m_fAutoHuntNextSkillTime = fNow + 0.5f;
|
||||
if (__UseSkill(dwSkillSlot))
|
||||
if (__AutoHunt_CastSkill(dwSkillSlot, fNow))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -573,15 +634,10 @@ void CPythonPlayer::__AutoHunt_UpdateAttackSkill(CInstanceBase& rkInstMain, CIns
|
||||
m_fAutoHuntNextSkillTime = fNow + 0.3f;
|
||||
|
||||
const float fDistance = rkInstMain.GetDistance(&rkInstVictim);
|
||||
for (DWORD i = 0; i < AUTO_HUNT_SKILL_SLOT_NUM; ++i)
|
||||
for (DWORD index : m_kAutoHuntConfig.vecSkillIndices)
|
||||
{
|
||||
if (!(m_kAutoHuntConfig.dwSkillSlotMask & (1 << i)))
|
||||
continue;
|
||||
|
||||
DWORD dwType = SLOT_TYPE_NONE, dwSkillSlot = 0;
|
||||
GetLocalQuickSlotData(i, &dwType, &dwSkillSlot);
|
||||
if (SLOT_TYPE_SKILL != dwType)
|
||||
continue;
|
||||
DWORD dwSkillSlot = 0;
|
||||
if (!GetSkillSlotIndex(index, &dwSkillSlot)) continue;
|
||||
|
||||
CPythonSkill::TSkillData* pSkillData;
|
||||
if (!__AutoHunt_IsSkillReady(dwSkillSlot, &pSkillData) || !pSkillData->IsAttackSkill())
|
||||
@@ -593,7 +649,7 @@ void CPythonPlayer::__AutoHunt_UpdateAttackSkill(CInstanceBase& rkInstMain, CIns
|
||||
continue;
|
||||
|
||||
SetTarget(rkInstVictim.GetVirtualID());
|
||||
if (__UseSkill(dwSkillSlot))
|
||||
if (__AutoHunt_CastSkill(dwSkillSlot, fNow))
|
||||
{
|
||||
m_fAutoHuntNextSkillTime = fNow + 0.5f;
|
||||
return;
|
||||
@@ -611,7 +667,8 @@ void CPythonPlayer::__AutoHunt_UpdatePotion(CInstanceBase& rkInstMain, float fNo
|
||||
for (int iValue = 0; iValue < 2; ++iValue)
|
||||
{
|
||||
const bool isHP = 0 == iValue;
|
||||
const int iPercent = isHP ? m_kAutoHuntConfig.iHPPercent : m_kAutoHuntConfig.iSPPercent;
|
||||
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;
|
||||
@@ -657,18 +714,19 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
return;
|
||||
CInstanceBase& rkInstMain = *pkInstMain;
|
||||
|
||||
if (rkInstMain.IsDead())
|
||||
{
|
||||
__AutoHunt_Stop(AUTO_HUNT_STOP_DEAD);
|
||||
return;
|
||||
}
|
||||
const float fNow = CTimer::Instance().GetCurrentSecond();
|
||||
if (m_stAutoHuntMapName != CPythonBackground::Instance().GetWarpMapName())
|
||||
{
|
||||
__AutoHunt_Stop(AUTO_HUNT_STOP_MAP);
|
||||
return;
|
||||
}
|
||||
|
||||
const float fNow = CTimer::Instance().GetCurrentSecond();
|
||||
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;
|
||||
@@ -678,6 +736,7 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
|
||||
if (IsOpenPrivateShop() || IsObserverMode() || rkInstMain.IsFishingMode())
|
||||
return;
|
||||
if (__AutoHunt_UpdateMount(rkInstMain,fNow)) return;
|
||||
|
||||
switch (m_eAutoHuntState)
|
||||
{
|
||||
@@ -746,6 +805,7 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
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;
|
||||
@@ -775,8 +835,11 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
m_fAutoHuntStateTime = fNow;
|
||||
return;
|
||||
}
|
||||
if (!rkInstMain.IsWalking())
|
||||
rkInstMain.NEW_MoveToDestPixelPositionDirection(m_kPPosAutoHuntAnchor);
|
||||
if (!__AutoHunt_MoveTo(m_kPPosAutoHuntAnchor,100.f,fNow)) {
|
||||
rkInstMain.NEW_Stop();
|
||||
m_eAutoHuntState=AUTO_HUNT_SEARCH;
|
||||
m_fAutoHuntNextActionTime=fNow+2.f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -785,7 +848,8 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
CInstanceBase* pkInstVictim = NEW_FindActorPtr(m_dwAutoHuntTargetVID);
|
||||
if (!pkInstVictim || !__AutoHunt_IsTargetAllowed(rkInstMain, *pkInstVictim))
|
||||
{
|
||||
__ClearAutoAttackTargetActorID();
|
||||
__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.
|
||||
@@ -826,17 +890,27 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
{
|
||||
m_kMap_dwAutoHuntSkipVID[m_dwAutoHuntTargetVID] = fNow + c_fAutoHuntSkipTime;
|
||||
__ClearAutoAttackTargetActorID();
|
||||
__ClearReservedAction();
|
||||
__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)
|
||||
@@ -870,7 +944,10 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MODE_CLICK_ITEM != m_eReservedMode && !rkInstMain.IsWalking())
|
||||
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;
|
||||
}
|
||||
@@ -902,9 +979,205 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
}
|
||||
else
|
||||
{
|
||||
__ReserveClickItem(dwItemID);
|
||||
__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;
|
||||
}
|
||||
|
||||
@@ -2182,21 +2182,21 @@ PyObject * playerAutoHuntIsEnabled(PyObject* poSelf, PyObject* poArgs)
|
||||
return Py_BuildValue("i", CPythonPlayer::Instance().AutoHunt_IsEnabled());
|
||||
}
|
||||
|
||||
// (state, stop reason, out of HP potions, out of SP potions, gathering, gathered mobs alive)
|
||||
// (state, stop reason, out of HP potions, out of SP potions, gathering, gathered groups, mount stage)
|
||||
PyObject * playerAutoHuntGetState(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
CPythonPlayer& rkPlayer = CPythonPlayer::Instance();
|
||||
return Py_BuildValue("iiiiii", rkPlayer.AutoHunt_GetState(), rkPlayer.AutoHunt_GetStopReason(),
|
||||
return Py_BuildValue("iiiiiii", rkPlayer.AutoHunt_GetState(), rkPlayer.AutoHunt_GetStopReason(),
|
||||
rkPlayer.AutoHunt_HasNoPotion(true), rkPlayer.AutoHunt_HasNoPotion(false),
|
||||
rkPlayer.AutoHunt_IsGathering(), rkPlayer.AutoHunt_GetGatheredCount());
|
||||
rkPlayer.AutoHunt_IsGathering(), rkPlayer.AutoHunt_GetGatheredCount(), rkPlayer.AutoHunt_GetMountState());
|
||||
}
|
||||
|
||||
// (range, hp %, sp %, pickup, stone, boss, quick-slot skill mask, gather count)
|
||||
// (range, hp %, sp %, pickup, stone, boss, legacy mask, gather count, revive, dismount skill)
|
||||
PyObject * playerAutoHuntGetConfig(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
const CPythonPlayer::SAutoHuntConfig& c_rkConfig = CPythonPlayer::Instance().AutoHunt_GetConfig();
|
||||
return Py_BuildValue("iiiiiiii", c_rkConfig.iRange, c_rkConfig.iHPPercent, c_rkConfig.iSPPercent, c_rkConfig.iPickup,
|
||||
c_rkConfig.bAttackStone, c_rkConfig.bAttackBoss, int(c_rkConfig.dwSkillSlotMask), c_rkConfig.iGatherCount);
|
||||
return Py_BuildValue("iiiiiiiiii", c_rkConfig.iRange, c_rkConfig.iHPPercent, c_rkConfig.iSPPercent, c_rkConfig.iPickup,
|
||||
c_rkConfig.bAttackStone, c_rkConfig.bAttackBoss, int(c_rkConfig.dwSkillSlotMask), c_rkConfig.iGatherCount, c_rkConfig.bAutoRevive, c_rkConfig.bDismountSkill);
|
||||
}
|
||||
|
||||
PyObject * playerAutoHuntSetConfig(PyObject* poSelf, PyObject* poArgs)
|
||||
@@ -2206,7 +2206,7 @@ PyObject * playerAutoHuntSetConfig(PyObject* poSelf, PyObject* poArgs)
|
||||
if (!PyTuple_GetInteger(poArgs, i, &aiValue[i]))
|
||||
return Py_BuildException();
|
||||
|
||||
CPythonPlayer::SAutoHuntConfig kConfig;
|
||||
CPythonPlayer::SAutoHuntConfig kConfig=CPythonPlayer::Instance().AutoHunt_GetConfig();
|
||||
kConfig.iRange = aiValue[0];
|
||||
kConfig.iHPPercent = aiValue[1];
|
||||
kConfig.iSPPercent = aiValue[2];
|
||||
@@ -2215,10 +2215,46 @@ PyObject * playerAutoHuntSetConfig(PyObject* poSelf, PyObject* poArgs)
|
||||
kConfig.bAttackBoss = aiValue[5] != 0;
|
||||
kConfig.dwSkillSlotMask = DWORD(aiValue[6]);
|
||||
kConfig.iGatherCount = aiValue[7];
|
||||
if (PyTuple_Size(poArgs)>8) {
|
||||
int revive=0,dismount=0;
|
||||
if (PyTuple_Size(poArgs)!=10 || !PyTuple_GetInteger(poArgs,8,&revive) || !PyTuple_GetInteger(poArgs,9,&dismount)) return Py_BuildException();
|
||||
kConfig.bAutoRevive=revive!=0; kConfig.bDismountSkill=dismount!=0;
|
||||
}
|
||||
CPythonPlayer::Instance().AutoHunt_SetConfig(kConfig);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
// PORT: skill indices persist independently from shortcut positions.
|
||||
PyObject* playerAutoHuntGetSkills(PyObject*,PyObject*)
|
||||
{
|
||||
auto& player=CPythonPlayer::Instance();
|
||||
const auto available=player.AutoHunt_GetAvailableSkills();
|
||||
const auto selected=player.AutoHunt_GetConfig().vecSkillIndices;
|
||||
PyObject* choices=PyTuple_New(available.size());
|
||||
PyObject* selection=PyTuple_New(selected.size());
|
||||
for (size_t i=0;i<available.size();++i) PyTuple_SET_ITEM(choices,i,PyInt_FromLong(available[i]));
|
||||
for (size_t i=0;i<selected.size();++i) PyTuple_SET_ITEM(selection,i,PyInt_FromLong(selected[i]));
|
||||
return Py_BuildValue("NN",selection,choices);
|
||||
}
|
||||
|
||||
PyObject* playerAutoHuntSetSkills(PyObject*,PyObject* args)
|
||||
{
|
||||
PyObject* source=nullptr;
|
||||
if (!PyArg_ParseTuple(args,"O",&source)) return nullptr;
|
||||
PyObject* sequence=PySequence_Fast(source,"skill indices must be a sequence");
|
||||
if (!sequence) return nullptr;
|
||||
const Py_ssize_t count=PySequence_Fast_GET_SIZE(sequence);
|
||||
if (count>32) { Py_DECREF(sequence); PyErr_SetString(PyExc_ValueError,"at most 32 skills"); return nullptr; }
|
||||
std::vector<DWORD> indices;
|
||||
for (Py_ssize_t i=0;i<count;++i) {
|
||||
const long index=PyInt_AsLong(PySequence_Fast_GET_ITEM(sequence,i));
|
||||
if (PyErr_Occurred()) { Py_DECREF(sequence); return nullptr; }
|
||||
if (index>0 && index<=65535) indices.push_back(DWORD(index));
|
||||
}
|
||||
Py_DECREF(sequence); CPythonPlayer::Instance().AutoHunt_SetSkills(indices);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
// The auto hunt window's UTF-8 source strings in the locale's code page (locale.cfg), as the packed
|
||||
// locale_game.txt strings are.
|
||||
PyObject * playerAutoHuntLocalize(PyObject* poSelf, PyObject* poArgs)
|
||||
@@ -2417,6 +2453,8 @@ void initPlayer()
|
||||
{ "AutoHuntGetState", playerAutoHuntGetState, METH_VARARGS },
|
||||
{ "AutoHuntGetConfig", playerAutoHuntGetConfig, METH_VARARGS },
|
||||
{ "AutoHuntSetConfig", playerAutoHuntSetConfig, METH_VARARGS },
|
||||
{ "AutoHuntGetSkills", playerAutoHuntGetSkills, METH_VARARGS },
|
||||
{ "AutoHuntSetSkills", playerAutoHuntSetSkills, METH_VARARGS },
|
||||
{ "AutoHuntLocalize", playerAutoHuntLocalize, METH_VARARGS },
|
||||
|
||||
{ NULL, NULL, NULL },
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
#include "../../src/port/UserInterface/AutoHuntNavigation.h"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
using namespace AutoHuntNavigation;
|
||||
int main() {
|
||||
auto free=[](Point) { return true; };
|
||||
auto direct=FindPath({0,0},{1000,0},100,free);
|
||||
assert(direct.size()==1 && direct.back().x==1000);
|
||||
// A solid wall must be bypassed through its opening, with every returned segment traversable.
|
||||
auto wall=[](Point p) { return !(p.x>=400 && p.x<=600 && p.y>-500 && p.y<500); };
|
||||
auto detour=FindPath({0,0},{1000,0},100,wall);
|
||||
assert(detour.size()>1);
|
||||
Point prev{0,0}; bool turned=false;
|
||||
for (auto p:detour) { assert(Segment(prev,p,wall)); turned |= std::abs(p.y)>=500; prev=p; }
|
||||
assert(turned && Distance(prev,{1000,0})<=100);
|
||||
auto sealed=[](Point p) { return p.x<400 || p.x>600; };
|
||||
assert(FindPath({0,0},{1000,0},100,sealed).empty());
|
||||
// A nearby target across a thin wall is not reachable merely because it is in attack range.
|
||||
auto thin=[](Point p) { return p.x<30 || p.x>70; };
|
||||
assert(FindPath({0,0},{100,0},150,thin).empty());
|
||||
// Corner cutting cannot pass between touching wall cells.
|
||||
auto diagonal=[](Point p) {
|
||||
return (p.x<50 && p.y<50) || (p.x>=50 && p.y>=50);
|
||||
};
|
||||
assert(FindPath({0,0},{200,200},0,diagonal).empty());
|
||||
assert(FindPath({0,0},{1000,0},100,wall,1).empty());
|
||||
assert(FindPath({0,0},{1000,0},100,[](Point){return false;}).empty());
|
||||
assert(FindPath({0,0},{std::numeric_limits<float>::quiet_NaN(),0},100,free).empty());
|
||||
// Start/finish and negative map-relative coordinates retain their exact positions.
|
||||
auto negative=FindPath({-23,-47},{-1023,-47},0,free);
|
||||
assert(negative.size()==1 && negative.back().x==-1023 && negative.back().y==-47);
|
||||
std::puts("autohunt navigation: direct, detour, sealed, thin wall, corners, budget, unloaded and coordinates passed");
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// PORT: actual Python UI/bindings and player frame loop, with actor changes simulating server acknowledgments.
|
||||
if (server && CPythonPlayer::Instance().NEW_GetMainActorPtr()) {
|
||||
auto& player=CPythonPlayer::Instance();
|
||||
auto* actor=player.NEW_GetMainActorPtr();
|
||||
const auto saved=player.AutoHunt_GetConfig();
|
||||
const int savedHP=player.GetStatus(POINT_HP), savedMaxHP=player.GetStatus(POINT_MAX_HP);
|
||||
const int savedSP=player.GetStatus(POINT_SP), savedMaxSP=player.GetStatus(POINT_MAX_SP);
|
||||
TPixelPosition position; actor->NEW_GetPixelPosition(&position);
|
||||
auto config=saved; config.iGatherCount=0; config.iPickup=0; config.iHPPercent=0; config.iSPPercent=0;
|
||||
config.bAttackStone=false; config.bAttackBoss=false; config.bAutoRevive=false; config.bDismountSkill=true;
|
||||
config.vecSkillIndices.clear();
|
||||
player.AutoHunt_SetConfig(config);
|
||||
player.SetStatus(POINT_HP,1000); player.SetStatus(POINT_MAX_HP,1000);
|
||||
player.SetStatus(POINT_SP,1000); player.SetStatus(POINT_MAX_SP,1000);
|
||||
const int previousIndex=player.GetSkillIndex(9), previousLevel=player.GetSkillLevel(9), previousGrade=player.GetSkillGrade(9);
|
||||
player.SetSkill(9,19); player.SetSkillLevel_(19,0,1);
|
||||
CHECK(py("import player, mt_autohunt\n"
|
||||
"player.AutoHuntSetSkills([19,19,0,9999])\n"
|
||||
"assert player.AutoHuntGetSkills()[0] == (19,)\n"
|
||||
"assert 19 in player.AutoHuntGetSkills()[1]\n"
|
||||
"mt_autohunt.g_dialog.Open()\n"
|
||||
"assert 19 in mt_autohunt.g_dialog.skillIDs\n"
|
||||
"assert mt_autohunt.g_dialog.selectedSkills == set([19])\n"
|
||||
"assert mt_autohunt.g_dialog.GetHeight() <= 565\n"
|
||||
"player.AutoHuntSetConfig(3500,0,0,0,0,0,0,0,0,1)\n"
|
||||
"assert player.AutoHuntGetSkills()[0] == (19,)\n"
|
||||
"mt_autohunt.g_dialog.Close()"));
|
||||
// Saving unrelated settings/toggling a shortcut cannot change selected skill IDs.
|
||||
DWORD shortcutType=0,shortcutPosition=0;
|
||||
const DWORD globalShortcut=player.LocalQuickSlotIndexToGlobalQuickSlotIndex(0);
|
||||
player.GetGlobalQuickSlotData(globalShortcut,&shortcutType,&shortcutPosition);
|
||||
player.AddQuickSlot(globalShortcut,SLOT_TYPE_SKILL,9);
|
||||
player.DeleteQuickSlot(globalShortcut);
|
||||
CHECK(player.AutoHunt_GetConfig().vecSkillIndices==std::vector<DWORD>{19});
|
||||
player.AddQuickSlot(globalShortcut,char(shortcutType),WORD(shortcutPosition));
|
||||
CHECK(player.AutoHunt_GetConfig().vecSkillIndices==std::vector<DWORD>{19});
|
||||
// Recreate just the main actor, as the real server's mount status insert does. No game phase reset.
|
||||
auto acknowledgeMount=[&](DWORD mount) {
|
||||
auto* old=player.NEW_GetMainActorPtr(); TPixelPosition here; old->NEW_GetPixelPosition(&here);
|
||||
CInstanceBase::SCreateData data={};
|
||||
data.m_bType=6; data.m_dwRace=0; data.m_dwVID=FakeLoginServer::kMainVID;
|
||||
data.m_dwMovSpd=100; data.m_dwAtkSpd=100; data.m_dwWeapon=19;
|
||||
data.m_lPosX=LONG(here.x); data.m_lPosY=LONG(here.y); data.m_stName=FakeLoginServer::kCharacterName;
|
||||
data.m_isMain=true; data.m_dwMountVnum=mount;
|
||||
CPythonCharacterManager::Instance().DeleteInstance(data.m_dwVID);
|
||||
auto* fresh=CPythonCharacterManager::Instance().CreateInstance(data);
|
||||
player.SetMainCharacterIndex(data.m_dwVID);
|
||||
return fresh;
|
||||
};
|
||||
auto rideCount=[&] {
|
||||
int count=0; for (const auto& event:server->Events()) if (event=="game2:chat 0 /ride") ++count;
|
||||
return count;
|
||||
};
|
||||
TPixelPosition combatPosition;
|
||||
CPythonCharacterManager::Instance().GetInstancePtr(FakeLoginServer::kMobVID)->NEW_GetPixelPosition(&combatPosition);
|
||||
combatPosition.x+=1000;
|
||||
actor->NEW_Stop(); actor->NEW_SetPixelPosition(combatPosition);
|
||||
actor=acknowledgeMount(20103);
|
||||
CHECK(actor && actor->IsMountingHorse() && !actor->IsInSafe());
|
||||
config=player.AutoHunt_GetConfig(); config.bDismountSkill=false; player.AutoHunt_SetConfig(config);
|
||||
const int disabledRideCount=rideCount();
|
||||
player.AutoHunt_SetEnabled(true); pump_frames(12);
|
||||
CHECK(player.AutoHunt_GetMountState()==0 && rideCount()==disabledRideCount && actor->IsMountingHorse());
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
config.bDismountSkill=true; player.AutoHunt_SetConfig(config);
|
||||
player.AutoHunt_SetEnabled(true);
|
||||
CHECK(pump_until(3,[&] { return player.AutoHunt_GetMountState()==1 && rideCount()==disabledRideCount+1; }));
|
||||
player.SetAttackKeyState(true); player.SetAttackKeyState(false);
|
||||
CHECK(player.AutoHunt_GetState()==CPythonPlayer::AUTO_HUNT_PAUSED && player.AutoHunt_GetMountState()==0);
|
||||
player.AutoHunt_SetEnabled(false); pump_frames(12);
|
||||
CHECK(rideCount()==disabledRideCount+1); // canceled cycle cannot send its eventual remount
|
||||
const int beforeRide=rideCount();
|
||||
player.AutoHunt_SetEnabled(true);
|
||||
CHECK(pump_until(3,[&] { return rideCount()==beforeRide+1 && player.AutoHunt_GetMountState()==1; }));
|
||||
CHECK(!player.IsSkillCoolTime(9)); // no cast before the dismount acknowledgment
|
||||
actor=acknowledgeMount(0);
|
||||
CHECK(actor && !actor->IsMountingHorse());
|
||||
CHECK(pump_until(4,[&] { return player.IsSkillCoolTime(9); }));
|
||||
CHECK(pump_until(5,[&] { return rideCount()==beforeRide+2 && player.AutoHunt_GetMountState()==3; }));
|
||||
actor=acknowledgeMount(20103);
|
||||
CHECK(pump_until(2,[&] { return player.AutoHunt_GetMountState()==0; }));
|
||||
CHECK(actor && actor->IsMountingHorse());
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
actor=acknowledgeMount(0);
|
||||
player.AutoHunt_SetSkills({});
|
||||
// Default remains stop-on-death. Opt-in waits for a real alive actor and recovery HP.
|
||||
player.AutoHunt_SetEnabled(true); actor->Die(); player.NotifyDeadMainCharacter();
|
||||
CHECK(!player.AutoHunt_IsEnabled() && player.AutoHunt_GetStopReason()==CPythonPlayer::AUTO_HUNT_STOP_DEAD);
|
||||
actor->Revive(); actor->NEW_Stop();
|
||||
config=player.AutoHunt_GetConfig(); config.bAutoRevive=true; player.AutoHunt_SetConfig(config);
|
||||
player.AutoHunt_SetEnabled(true); actor->Die(); player.SetStatus(POINT_HP,0); player.NotifyDeadMainCharacter();
|
||||
CHECK(player.AutoHunt_IsEnabled() && player.AutoHunt_GetState()==CPythonPlayer::AUTO_HUNT_REVIVE);
|
||||
CHECK(pump_until(12,[&] { return server->Has("game2:chat 0 /restart_here"); }));
|
||||
CHECK(player.AutoHunt_GetState()==CPythonPlayer::AUTO_HUNT_REVIVE); // command alone cannot revive locally
|
||||
actor->Revive(); player.SetStatus(POINT_HP,100);
|
||||
CHECK(pump_until(2,[&] { return player.AutoHunt_GetState()==CPythonPlayer::AUTO_HUNT_RECOVER; }));
|
||||
pump_frames(330); // > 5 seconds even at custom 60 Hz; low HP must still prevent combat
|
||||
CHECK(player.AutoHunt_GetState()==CPythonPlayer::AUTO_HUNT_RECOVER);
|
||||
player.SetStatus(POINT_HP,700);
|
||||
CHECK(pump_until(7,[&] { return player.AutoHunt_GetState()!=CPythonPlayer::AUTO_HUNT_RECOVER; }));
|
||||
CHECK(player.AutoHunt_IsEnabled());
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
// A stopped recovery cannot emit later revival or riding commands.
|
||||
CHECK(player.AutoHunt_GetMountState()==0);
|
||||
player.SetSkillLevel_(19,0,0); player.SetSkill(9,previousIndex);
|
||||
if (previousIndex) player.SetSkillLevel_(previousIndex,previousGrade,previousLevel);
|
||||
player.AutoHunt_SetConfig(saved);
|
||||
player.SetStatus(POINT_HP,savedHP); player.SetStatus(POINT_MAX_HP,savedMaxHP);
|
||||
player.SetStatus(POINT_SP,savedSP); player.SetStatus(POINT_MAX_SP,savedMaxSP);
|
||||
actor->NEW_Stop(); actor->NEW_SetPixelPosition(position);
|
||||
pump_frames(30);
|
||||
}
|
||||
@@ -393,9 +393,10 @@ int main(int argc, char** argv)
|
||||
});
|
||||
CHECK(loading && !failed());
|
||||
CHECK(py_true(loaded));
|
||||
// The map is available during loading. A fast server may have already switched the
|
||||
// network phase to Game before the loading window closes on the next script frame.
|
||||
CHECK(!PythonBoot::CurrentMapName().empty());
|
||||
// CG_CLIENT_VERSION is sent before introLoading's background-load steps. Wait for those
|
||||
// script frames rather than racing the server thread's receipt of the version packet.
|
||||
CHECK(pump_until(live ? 10 : 5, [&] { return failed() || !PythonBoot::CurrentMapName().empty(); }) &&
|
||||
!PythonBoot::CurrentMapName().empty());
|
||||
|
||||
// 6. introLoading's update steps end in net.StartGame(); the next CPythonNetworkStream::Process calls
|
||||
// networkModule's SetGamePhase, which builds game.GameWindow (interfaceModule.MakeInterface) and
|
||||
@@ -745,6 +746,7 @@ int main(int argc, char** argv)
|
||||
}
|
||||
}
|
||||
#include "autohunt_pull_checks.inc"
|
||||
#include "autohunt_recovery_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,
|
||||
|
||||
Reference in New Issue
Block a user