Expand auto-hunt pursuit ranges and show server bonus effects
This commit is contained in:
@@ -253,7 +253,7 @@ public class MainActivity extends SDLActivity {
|
||||
args = DEFAULT_ARGS;
|
||||
if (!args.contains("--safe-inset-px"))
|
||||
args = args.trim() + " --safe-inset-px " + safeInsetPx();
|
||||
// Test phase: FPS readout in the top-left corner. Drop this line for release builds.
|
||||
// Test phase: FPS readout at the top centre. Drop this line for release builds.
|
||||
if (!args.contains("--show-fps"))
|
||||
args = args.trim() + " --show-fps";
|
||||
return args.trim().split("\\s+");
|
||||
|
||||
@@ -649,3 +649,4 @@
|
||||
{"date": "2026-09-30", "unit": "UserInterface/PythonPlayerAutoHunt.cpp", "action": "user-authorized-extension", "evidence": "Replace tag-and-chase gather with free server-authoritative cloak-style BeginFight pull, nearest 3/10/20 actual monster groups. Server rate limit and safety checks, request generations, stationary approach wait. host.autohunt_pull and desktop/mobile port.login_flow pass; 203 FreeBSD compile/deploy and live 3-group/9-monster reply verified. See tools/server/autohunt-pull/README.md."}
|
||||
{"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."}
|
||||
|
||||
@@ -511,7 +511,8 @@
|
||||
],
|
||||
"evidence": [
|
||||
"scripts/verify_root_pack.py: uiaffectshower.py bytes match 40250 source and root.epk",
|
||||
"scripts/python_game_render_test.sh: embedded CPython loads system.py and game modules through CEterPackManager"
|
||||
"scripts/python_game_render_test.sh: embedded CPython loads system.py and game modules through CEterPackManager",
|
||||
"2026-09-30 PORT server-bonus extension: original affect script unchanged; port.login_flow checks separate global/event entries and expiry/update via real CHAT_TYPE_COMMAND; Mi 8 Lite tooltip verified."
|
||||
]
|
||||
},
|
||||
"AffectShower.OnUpdate": {
|
||||
@@ -521,7 +522,8 @@
|
||||
],
|
||||
"evidence": [
|
||||
"scripts/verify_root_pack.py: uiaffectshower.py bytes match 40250 source and root.epk",
|
||||
"scripts/python_game_render_test.sh: embedded CPython loads system.py and game modules through CEterPackManager"
|
||||
"scripts/python_game_render_test.sh: embedded CPython loads system.py and game modules through CEterPackManager",
|
||||
"2026-09-30 PORT server-bonus extension: original affect script unchanged; port.login_flow checks separate global/event entries and expiry/update via real CHAT_TYPE_COMMAND; Mi 8 Lite tooltip verified."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ metin2-client 是 40250 Windows 客户端的跨平台版本。**首要目标是
|
||||
2026-09-30 用户授权扩展:自动狩猎引怪改为服务端披风式追击,免费选择 3/10/20 个实际怪物队伍;
|
||||
见 `tools/server/autohunt-pull/README.md`,203 已部署并实测 3 组/9 只怪物。原披风物品保持不变。
|
||||
|
||||
2026-09-30 后续授权:狩猎范围为 35/100/150 米,引怪搜索及专属追击距离同步;石头优先,无石头再打怪。
|
||||
服务器倍率和经验/金币/掉落活动加成通过独立 PORT 状态消息显示在原增益栏,原版角色增益脚本不修改。
|
||||
见 `tools/server/autohunt-pull-range/README.md`,203 已更新服务端并保留二进制回滚备份。
|
||||
|
||||
2026-09-30 用户授权扩展:移动戒指列出 203 已登记的全部 65 张地图,按六类分页,包含活动、战争和测试地图;
|
||||
受限副本走原有入口,活动保留入场流程,测试传送仅限 GM。见 `tools/server/ring-warp/README.md`。
|
||||
|
||||
|
||||
@@ -321,10 +321,10 @@ void VulkanWindow::prepare_ui(std::uint32_t ui_width, std::uint32_t ui_height, c
|
||||
if (!SkipDiagnosticRender("touch")) touch_controller.append_ui_commands(combined_ui);
|
||||
}
|
||||
if (show_fps_ && fps_ >= 0) {
|
||||
// Top left, inside the safe area and clear of 40250's corner icon.
|
||||
// Top centre, clear of the original affect bar and the mini-map.
|
||||
const float inset = float(std::min<int>(ui_safe_inset(), int(target_ui_w) / 8));
|
||||
TouchController::draw_segment_text(combined_ui, "FPS " + std::to_string(fps_),
|
||||
inset + 40.0f, 8.0f, 12.0f, 0xFF40FF40);
|
||||
std::max(inset, float(target_ui_w) * 0.5f - 40.0f), 8.0f, 12.0f, 0xFF40FF40);
|
||||
}
|
||||
if (!combined_ui.empty()) {
|
||||
build_ui_batches(target_ui_w, target_ui_h, combined_ui, capture_textures, ui_batches, ui_quad_count);
|
||||
|
||||
@@ -31,8 +31,8 @@ STOP_TEXT = {
|
||||
STOP_INVENTORY_FULL: "背包已满,自动狩猎已停止",
|
||||
}
|
||||
|
||||
RANGE_VALUES = (1000, 2000, 3500)
|
||||
RANGE_TEXTS = ("近", "中", "远")
|
||||
RANGE_VALUES = (3500, 10000, 15000)
|
||||
RANGE_TEXTS = ("35米", "100米", "150米")
|
||||
HP_VALUES = (0, 30, 50, 70)
|
||||
SP_VALUES = (0, 20, 40, 60)
|
||||
PICKUP_TEXTS = ("不拾取", "全部", "金币装备")
|
||||
@@ -178,7 +178,7 @@ class SettingDialog(ui.BoardWithTitleBar):
|
||||
self.targetLabel.SetPosition(18, y + 4)
|
||||
self.targetLabel.SetText(L("攻击"))
|
||||
self.targetLabel.Show()
|
||||
self.stoneButton = FlagButton(self, 70, y, MIDDLE_BUTTON, "石头", self.__Save)
|
||||
self.stoneButton = FlagButton(self, 70, y, MIDDLE_BUTTON, "优先石头", self.__Save)
|
||||
self.bossButton = FlagButton(self, 136, y, MIDDLE_BUTTON, "首领", self.__Save)
|
||||
y += 30
|
||||
|
||||
@@ -290,14 +290,18 @@ class Ticker(ui.Window):
|
||||
|
||||
def OnUpdate(self):
|
||||
state = player.AutoHuntGetState()
|
||||
if state != self.last:
|
||||
config = player.AutoHuntGetConfig()
|
||||
displayState = state + (config[7],)
|
||||
if displayState != self.last:
|
||||
(stateIndex, reason, noHP, noSP, gathering, gathered) = state
|
||||
wasOn = self.last and self.last[0] != STATE_OFF
|
||||
self.last = state
|
||||
self.last = displayState
|
||||
if stateIndex != STATE_OFF:
|
||||
text = L("自动狩猎 · ") + L(STATE_TEXT.get(stateIndex, ""))
|
||||
if gathering:
|
||||
text = L("自动狩猎 · 披风引怪中(最多%d组)" % player.AutoHuntGetConfig()[7])
|
||||
elif config[7]:
|
||||
text += L(" · 本轮引怪%d/%d组" % (gathered, config[7]))
|
||||
if noHP:
|
||||
text += L(" · 无红药")
|
||||
if noSP:
|
||||
|
||||
@@ -916,6 +916,7 @@ namespace PythonBoot
|
||||
#include "UIScaleOptionScript.inc"
|
||||
#include "InventoryPagesScript.inc"
|
||||
#include "MallHintScript.inc"
|
||||
#include "ServerBonusScript.inc"
|
||||
|
||||
static void install_script_module(const char* c_szModule, const char* c_szScript)
|
||||
{
|
||||
@@ -975,6 +976,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
install_script_module("mt_uiscale", c_szUIScaleOptionScript);
|
||||
install_script_module("mt_inventory", c_szInventoryPagesScript);
|
||||
install_script_module("mt_mallhint", c_szMallHintScript);
|
||||
install_script_module("mt_bonuses", c_szServerBonusScript);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// PORT: server rates and privilege events are not character AFFECT packets in 40250.
|
||||
// Add distinct entries to the original affect bar; leave packed scripts unchanged.
|
||||
static const char c_szServerBonusScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import app
|
||||
import game
|
||||
import net
|
||||
import player
|
||||
import ui
|
||||
import uiAffectShower
|
||||
|
||||
L = player.AutoHuntLocalize
|
||||
PREFIX = "d:/ymir work/ui/skill/common/affect/"
|
||||
ENTRIES = (
|
||||
("服务器经验 %d%%", "exp_bonus.sub", 100),
|
||||
("服务器金币数量 %d%%", "gold_bonus.sub", 100),
|
||||
("服务器金币掉落率 %d%%", "gold_bonus.sub", 100),
|
||||
("服务器物品掉落率 %d%%", "item_bonus.sub", 100),
|
||||
("活动经验加成 %+d%%", "exp_bonus.sub", 0),
|
||||
("活动物品掉落加成 %+d%%", "item_bonus.sub", 0),
|
||||
("活动金币掉落加成 %+d%%", "gold_bonus.sub", 0),
|
||||
("活动金币礼包掉落加成 %+d%%", "gold_premium.sub", 0),
|
||||
)
|
||||
KEY_START = 20000
|
||||
g_game = None
|
||||
g_ticker = None
|
||||
|
||||
|
||||
def Update(window, *values):
|
||||
if len(values) != len(ENTRIES):
|
||||
return
|
||||
try:
|
||||
values = tuple(int(value) for value in values)
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
bar = window.affectShower
|
||||
if not bar:
|
||||
return
|
||||
for i, ((text, asset, baseline), value) in enumerate(zip(ENTRIES, values)):
|
||||
key = KEY_START + i
|
||||
image = bar.affectImageDict.get(key)
|
||||
if value == baseline:
|
||||
if image:
|
||||
image.Hide()
|
||||
del bar.affectImageDict[key]
|
||||
continue
|
||||
if not image:
|
||||
image = uiAffectShower.AffectImage()
|
||||
image.SetParent(bar)
|
||||
image.LoadImage(PREFIX + asset)
|
||||
image.SetScale(0.7, 0.7)
|
||||
image.SetAffect(key)
|
||||
image.SetSkillAffectFlag(False)
|
||||
image.SetClock(False)
|
||||
bar.affectImageDict[key] = image
|
||||
image.SetDescription(L(text % value))
|
||||
image.UpdateDescription()
|
||||
image.Show()
|
||||
bar._AffectShower__ArrangeImageList()
|
||||
|
||||
|
||||
class Ticker(ui.Window):
|
||||
def __init__(self, window):
|
||||
ui.Window.__init__(self)
|
||||
self.window = window
|
||||
self.nextQuery = app.GetTime() + 3.0
|
||||
self.Show()
|
||||
|
||||
def OnUpdate(self):
|
||||
now = app.GetTime()
|
||||
if self.window and now >= self.nextQuery:
|
||||
self.nextQuery = now + 15.0
|
||||
net.SendChatPacket("/server_bonuses")
|
||||
|
||||
|
||||
_open = game.GameWindow.Open
|
||||
_close = game.GameWindow.Close
|
||||
_command = game.GameWindow.BINARY_ServerCommand_Run
|
||||
|
||||
|
||||
def Open(self):
|
||||
global g_game, g_ticker
|
||||
_open(self)
|
||||
g_game = self
|
||||
g_ticker = Ticker(self)
|
||||
|
||||
|
||||
def Close(self):
|
||||
global g_game, g_ticker
|
||||
if g_ticker:
|
||||
g_ticker.Hide()
|
||||
g_ticker.window = None
|
||||
g_game = g_ticker = None
|
||||
_close(self)
|
||||
|
||||
|
||||
def Command(self, line):
|
||||
words = line.split()
|
||||
if words and words[0] == "ServerBonuses":
|
||||
Update(self, *words[1:])
|
||||
return 1
|
||||
return _command(self, line)
|
||||
|
||||
|
||||
game.GameWindow.Open = Open
|
||||
game.GameWindow.Close = Close
|
||||
game.GameWindow.BINARY_ServerCommand_Run = Command
|
||||
)PY";
|
||||
@@ -1745,7 +1745,7 @@ CPythonPlayer::CPythonPlayer(void)
|
||||
m_isAutoHuntConfigLoaded = false;
|
||||
m_eAutoHuntState = AUTO_HUNT_OFF;
|
||||
m_eAutoHuntStopReason = AUTO_HUNT_STOP_NONE;
|
||||
m_kAutoHuntConfig.iRange = 2000;
|
||||
m_kAutoHuntConfig.iRange = 3500;
|
||||
m_kAutoHuntConfig.iHPPercent = 40;
|
||||
m_kAutoHuntConfig.iSPPercent = 20;
|
||||
m_kAutoHuntConfig.iPickup = AUTO_HUNT_PICKUP_ALL;
|
||||
|
||||
@@ -735,12 +735,15 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
|
||||
|
||||
protected:
|
||||
void __AutoHunt_OnManualInput();
|
||||
void __AutoHunt_CancelPull();
|
||||
void __AutoHunt_Stop(int eReason);
|
||||
void __Update_AutoHunt();
|
||||
void __AutoHunt_UpdatePotion(CInstanceBase& rkInstMain, float fNow);
|
||||
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_IsTargetAllowed(CInstanceBase& main, CInstanceBase& target); // PORT extension
|
||||
bool __AutoHunt_RequestPull(CInstanceBase& main, float now); // PORT extension
|
||||
DWORD __AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow);
|
||||
DWORD __AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain);
|
||||
void __AutoHunt_PurgeGathered();
|
||||
|
||||
@@ -46,6 +46,7 @@ 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.
|
||||
@@ -130,7 +131,7 @@ bool CPythonPlayer::AutoHunt_IsGathering()
|
||||
|
||||
int CPythonPlayer::AutoHunt_GetGatheredCount()
|
||||
{
|
||||
return m_kSet_dwAutoHuntGathered.empty() ? 0 : m_iAutoHuntGatheredGroups;
|
||||
return AutoHunt_IsEnabled() ? m_iAutoHuntGatheredGroups : 0;
|
||||
}
|
||||
|
||||
bool CPythonPlayer::AutoHunt_HasNoPotion(bool isHP)
|
||||
@@ -147,8 +148,16 @@ const CPythonPlayer::SAutoHuntConfig& CPythonPlayer::AutoHunt_GetConfig()
|
||||
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;
|
||||
m_kAutoHuntConfig.iRange = MINMAX(500, m_kAutoHuntConfig.iRange, 5000);
|
||||
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));
|
||||
@@ -199,8 +208,19 @@ void CPythonPlayer::__AutoHunt_SaveConfig()
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
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();
|
||||
if (AUTO_HUNT_ENGAGE == m_eAutoHuntState || AUTO_HUNT_LOOT == m_eAutoHuntState || AUTO_HUNT_RETURN == m_eAutoHuntState)
|
||||
{
|
||||
__ClearAutoAttackTargetActorID();
|
||||
@@ -250,31 +270,56 @@ void CPythonPlayer::__AutoHunt_Engage(DWORD dwVID, float fNow)
|
||||
__SetAutoAttackTargetActorID(dwVID);
|
||||
}
|
||||
|
||||
// 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();
|
||||
CPythonNonPlayer& rkNonPlayer = CPythonNonPlayer::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 || pkInst == &rkInstMain || pkInst->IsDead())
|
||||
continue;
|
||||
|
||||
if (!pkInst->IsEnemy() && !(m_kAutoHuntConfig.bAttackStone && pkInst->IsStone()))
|
||||
continue;
|
||||
|
||||
if (!rkInstMain.IsAttackableInstance(*pkInst) || pkInst->IsInSafe())
|
||||
continue;
|
||||
|
||||
if (!m_kAutoHuntConfig.bAttackBoss)
|
||||
{
|
||||
const CPythonNonPlayer::TMobTable* pMobTable = rkNonPlayer.GetTable(pkInst->GetRace());
|
||||
if (pMobTable && pMobTable->bRank >= c_byMobRankBoss)
|
||||
continue;
|
||||
}
|
||||
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)
|
||||
@@ -286,8 +331,10 @@ DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow
|
||||
continue;
|
||||
|
||||
const float fDistance = rkInstMain.GetDistance(pkInst);
|
||||
if (fDistance < fBestDistance)
|
||||
const bool stone = pkInst->IsStone();
|
||||
if ((stone && !bestStone) || (stone == bestStone && fDistance < fBestDistance))
|
||||
{
|
||||
bestStone = stone;
|
||||
fBestDistance = fDistance;
|
||||
dwBestVID = pkInst->GetVirtualID();
|
||||
}
|
||||
@@ -303,7 +350,7 @@ DWORD CPythonPlayer::__AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain)
|
||||
for (DWORD dwVID : m_kSet_dwAutoHuntGathered)
|
||||
{
|
||||
CInstanceBase* pkInst = NEW_FindActorPtr(dwVID);
|
||||
if (!pkInst || pkInst->IsDead() || pkInst->IsInSafe() || !rkInstMain.IsAttackableInstance(*pkInst))
|
||||
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)
|
||||
@@ -324,7 +371,8 @@ 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())
|
||||
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;
|
||||
@@ -622,6 +670,11 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
|
||||
const float fNow = CTimer::Instance().GetCurrentSecond();
|
||||
__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;
|
||||
@@ -646,45 +699,33 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
return;
|
||||
m_fAutoHuntNextActionTime = fNow + 0.25f;
|
||||
|
||||
// A cloak-style pull stays in place while the server starts the nearby groups.
|
||||
if (m_isAutoHuntGathering) {
|
||||
if (fNow - m_fAutoHuntGatherStartTime < c_fAutoHuntPullReplyTimeout) return;
|
||||
m_isAutoHuntGathering = false;
|
||||
m_kSet_dwAutoHuntGathered.clear();
|
||||
AutoHuntTrace("cloak pull reply timed out; continue ordinary hunt");
|
||||
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;
|
||||
}
|
||||
__AutoHunt_PurgeGathered();
|
||||
const int maxHP = GetStatus(POINT_MAX_HP);
|
||||
if (m_kAutoHuntConfig.iGatherCount > 0 && m_kSet_dwAutoHuntGathered.empty() &&
|
||||
fNow >= m_fAutoHuntNextPullTime && !rkInstMain.IsInSafe() &&
|
||||
maxHP > 0 && GetStatus(POINT_HP) * 100 >= maxHP * c_iAutoHuntGatherHPPercent &&
|
||||
__AutoHunt_FindTarget(rkInstMain, fNow)) {
|
||||
char command[128];
|
||||
++m_dwAutoHuntPullRequest;
|
||||
snprintf(command, sizeof(command), "/autohunt_pull %d %d %d %u",
|
||||
m_kAutoHuntConfig.iGatherCount, m_kAutoHuntConfig.iRange,
|
||||
m_kAutoHuntConfig.bAttackBoss ? 1 : 0, unsigned(m_dwAutoHuntPullRequest));
|
||||
m_fAutoHuntNextPullTime = fNow + c_fAutoHuntPullCooldown;
|
||||
if (CPythonNetworkStream::Instance().SendChatPacket(command)) {
|
||||
__ClearAutoAttackTargetActorID();
|
||||
__ClearReservedAction();
|
||||
rkInstMain.NEW_Stop();
|
||||
m_isAutoHuntGathering = true;
|
||||
m_iAutoHuntGatheredGroups = 0;
|
||||
m_fAutoHuntGatherStartTime = fNow;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (DWORD dwVID = __AutoHunt_FindGatheredTarget(rkInstMain))
|
||||
{
|
||||
CInstanceBase* pulled = NEW_FindActorPtr(dwVID);
|
||||
if (pulled && rkInstMain.GetDistance(pulled) > c_fAutoHuntSkillDistance &&
|
||||
fNow - m_fAutoHuntGatherStartTime < 8.0f && GetStatus(POINT_HP) * 100 >= maxHP * c_iAutoHuntGatherHPPercent)
|
||||
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);
|
||||
@@ -742,7 +783,7 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
case AUTO_HUNT_ENGAGE:
|
||||
{
|
||||
CInstanceBase* pkInstVictim = NEW_FindActorPtr(m_dwAutoHuntTargetVID);
|
||||
if (!pkInstVictim || pkInstVictim->IsDead())
|
||||
if (!pkInstVictim || !__AutoHunt_IsTargetAllowed(rkInstMain, *pkInstVictim))
|
||||
{
|
||||
__ClearAutoAttackTargetActorID();
|
||||
m_dwAutoHuntTargetVID = 0;
|
||||
@@ -756,6 +797,17 @@ void CPythonPlayer::__Update_AutoHunt()
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
using DWORD = uint32_t;
|
||||
constexpr int ENTITY_CHARACTER=1, ATTR_BANPK=4, MOB_RANK_BOSS=4, CHAT_TYPE_COMMAND=5;
|
||||
constexpr int ENTITY_CHARACTER=1, ATTR_BANPK=4, MOB_RANK_BOSS=4, CHAT_TYPE_COMMAND=5, SECTREE_SIZE=6400;
|
||||
struct Character;
|
||||
using LPCHARACTER=Character*;
|
||||
struct Entity { virtual ~Entity() {} bool IsType(int) { return true; } };
|
||||
@@ -22,6 +22,7 @@ struct Sectree {
|
||||
bool safe=false;
|
||||
std::vector<Entity*> entities;
|
||||
DWORD GetAttribute(long,long) { return safe ? ATTR_BANPK : 0; }
|
||||
template<class F> void for_each_entity(F& f) { for(auto* e:entities) f(e); }
|
||||
template<class F> void ForEachAround(F& f) { for(auto* e:entities) f(e); }
|
||||
};
|
||||
struct Character: Entity {
|
||||
@@ -44,6 +45,7 @@ struct Character: Entity {
|
||||
int GetX() { return x; }
|
||||
int GetY() { return y; }
|
||||
int GetMapIndex() { return 1; }
|
||||
DWORD GetPlayerID() { return vid; }
|
||||
int GetMobRank() { return rank; }
|
||||
Sectree* GetSectree() { return sec; }
|
||||
Party* GetParty() { return party; }
|
||||
@@ -62,6 +64,26 @@ struct CHARACTER_MANAGER {
|
||||
static CHARACTER_MANAGER& instance() { static CHARACTER_MANAGER value; return value; }
|
||||
Character* Find(DWORD vid) { return mobs[vid]; }
|
||||
};
|
||||
using LPSECTREE=Sectree*;
|
||||
struct SectreeMap {
|
||||
std::map<std::pair<int,int>, Sectree*> cells;
|
||||
template<class F> void for_each_in_rect(int x0,int y0,int x1,int y1,F& f) {
|
||||
for (const auto& cell:cells)
|
||||
if (cell.first.first>=x0/SECTREE_SIZE && cell.first.first<=x1/SECTREE_SIZE &&
|
||||
cell.first.second>=y0/SECTREE_SIZE && cell.first.second<=y1/SECTREE_SIZE)
|
||||
cell.second->for_each_entity(f);
|
||||
}
|
||||
Sectree* Find(int x,int y) {
|
||||
auto it=cells.find({x/SECTREE_SIZE,y/SECTREE_SIZE});
|
||||
return it==cells.end() ? nullptr : it->second;
|
||||
}
|
||||
};
|
||||
using LPSECTREE_MAP=SectreeMap*;
|
||||
struct SECTREE_MANAGER {
|
||||
SectreeMap map;
|
||||
static SECTREE_MANAGER& instance() { static SECTREE_MANAGER m; return m; }
|
||||
SectreeMap* GetMap(int) { return ↦ }
|
||||
};
|
||||
int now=100;
|
||||
int get_global_time() { return now; }
|
||||
#define DISTANCE_APPROX(x,y) int(std::hypot(x,y))
|
||||
@@ -71,7 +93,8 @@ int get_global_time() { return now; }
|
||||
|
||||
int main() {
|
||||
Sectree sec;
|
||||
Character owner; owner.pc=true; owner.sec=&sec;
|
||||
Character owner; owner.pc=true; owner.sec=&sec; owner.vid=1;
|
||||
SECTREE_MANAGER::instance().map.cells[{0,0}]=&sec;
|
||||
Character other; other.pc=true;
|
||||
std::vector<Party> parties(25);
|
||||
std::vector<Character> mobs(50);
|
||||
@@ -104,9 +127,30 @@ int main() {
|
||||
assert(owner.replies.back()=="AutoHuntGather 11 done 0");
|
||||
sec.safe=false; owner.replies.clear();
|
||||
do_autohunt_pull(&owner,"21 5000 0 12");
|
||||
do_autohunt_pull(&owner,"3 5001 0 12");
|
||||
do_autohunt_pull(&owner,"3 15001 0 12");
|
||||
do_autohunt_pull(&owner,"3 5000 0 12 extra");
|
||||
assert(owner.replies.empty());
|
||||
// Large ranges visit distant sectors and extend only the selected owner's pursuit.
|
||||
Sectree distant;
|
||||
Character far; far.vid=1000; far.x=14000; far.sec=&distant;
|
||||
distant.entities.push_back(&far);
|
||||
SECTREE_MANAGER::instance().map.cells[{2,0}]=&distant;
|
||||
CHARACTER_MANAGER::instance().mobs[far.vid]=&far;
|
||||
sec.entities.clear(); now+=10;
|
||||
do_autohunt_pull(&owner,"20 10000 0 13");
|
||||
assert(owner.replies.back()=="AutoHuntGather 13 done 0" && !far.victim);
|
||||
now+=10;
|
||||
do_autohunt_pull(&owner,"20 15000 0 14");
|
||||
assert(owner.replies.back()=="AutoHuntGather 14 done 1" && far.victim==&owner);
|
||||
assert(AutoHuntPullLeash(&far,&owner)==15001);
|
||||
assert(AutoHuntPullLeash(&mobs[49],&owner)==4000);
|
||||
do_autohunt_pull(&owner,"0 15000 0 15");
|
||||
assert(AutoHuntPullLeash(&far,&owner)==4000);
|
||||
now+=10; do_autohunt_pull(&owner,"3 15000 0 16");
|
||||
assert(AutoHuntPullLeash(&far,&other)==4000);
|
||||
now+=10; do_autohunt_pull(&owner,"3 15000 0 17");
|
||||
now+=180;
|
||||
assert(AutoHuntPullLeash(&far,&owner)==4000);
|
||||
// Unequal group sizes, exact 10-group cap and nearest ordering independent of input order.
|
||||
std::vector<mt_autohunt::Candidate> candidates;
|
||||
for(int g=24;g>=0;--g)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
// PORT extension test: real Python settings → CG_CHAT → GC_CHAT → player state.
|
||||
if (server && CPythonCharacterManager::Instance().GetMainInstancePtr()) {
|
||||
// Real command reply inserts distinct global/event icons without replacing character affects.
|
||||
CHECK(pump_until(2, [&] { return server->Has("game2:chat 0 /server_bonuses"); }));
|
||||
CHECK(py("import mt_bonuses; _bonus_bar = mt_bonuses.g_game.affectShower"));
|
||||
CHECK(pump_until(2, [&] { return py_true("all(k in _bonus_bar.affectImageDict for k in (20000,20001,20002,20003,20004,20006))"); }));
|
||||
CHECK(py("_bonus_image = _bonus_bar.affectImageDict[20000]; assert _bonus_image.IsShow(); assert _bonus_image.description == mt_bonuses.L(mt_bonuses.ENTRIES[0][0] % 200)"));
|
||||
CHECK(py("mt_bonuses.g_game.BINARY_ServerCommand_Run('ServerBonuses 100 100 100 100 0 0 0 0'); assert not any(k >= 20000 for k in _bonus_bar.affectImageDict)"));
|
||||
CHECK(py("mt_bonuses.g_game.BINARY_ServerCommand_Run('ServerBonuses 300 100 100 100 0 0 0 0'); assert _bonus_bar.affectImageDict[20000].description == mt_bonuses.L(mt_bonuses.ENTRIES[0][0] % 300)"));
|
||||
CHECK(py("mt_bonuses.g_game.BINARY_ServerCommand_Run('ServerBonuses bad'); assert 20000 in _bonus_bar.affectImageDict"));
|
||||
auto* main_instance = CPythonCharacterManager::Instance().GetMainInstancePtr();
|
||||
CPythonPlayer& player = CPythonPlayer::Instance();
|
||||
const auto savedConfig = player.AutoHunt_GetConfig();
|
||||
@@ -17,17 +25,59 @@ if (server && CPythonCharacterManager::Instance().GetMainInstancePtr()) {
|
||||
main_instance->NEW_Stop();
|
||||
main_instance->NEW_SetPixelPosition(pullPosition);
|
||||
CHECK(!main_instance->IsInSafe());
|
||||
CHECK(py("player.AutoHuntSetConfig(5000, 0, 0, 0, 0, 0, 0, 3); player.AutoHuntSetEnabled(1)"));
|
||||
CHECK(py("player.AutoHuntSetConfig(3500, 0, 0, 0, 0, 0, 0, 3); player.AutoHuntSetEnabled(1)"));
|
||||
TPixelPosition origin = {};
|
||||
main_instance->NEW_GetPixelPosition(&origin);
|
||||
CHECK(pump_until(2, [&] {
|
||||
return server->Has("game2:chat 0 /autohunt_pull 3 5000 0 1") &&
|
||||
return server->Has("game2:chat 0 /autohunt_pull 3 3500 0 1") &&
|
||||
player.AutoHunt_GetGatheredCount() == 1 && !player.AutoHunt_IsGathering();
|
||||
}));
|
||||
TPixelPosition current = {};
|
||||
main_instance->NEW_GetPixelPosition(¤t);
|
||||
CHECK(std::hypot(current.x-origin.x, current.y-origin.y) < 5.0f);
|
||||
TPixelPosition originalMobPosition = {};
|
||||
pullMob->NEW_GetPixelPosition(&originalMobPosition);
|
||||
TPixelPosition closeMobPosition = origin;
|
||||
closeMobPosition.x += 100;
|
||||
pullMob->NEW_Stop();
|
||||
pullMob->NEW_SetPixelPosition(closeMobPosition);
|
||||
main_instance->NEW_Stop();
|
||||
main_instance->NEW_SetPixelPosition(origin);
|
||||
// A farther stone outranks the nearer pulled monster, even with bosses disabled.
|
||||
CInstanceBase::SCreateData stoneData = {};
|
||||
stoneData.m_bType = CActorInstance::TYPE_STONE;
|
||||
stoneData.m_dwRace = 8001;
|
||||
stoneData.m_dwVID = 0x700001;
|
||||
stoneData.m_dwMovSpd = 100;
|
||||
stoneData.m_dwAtkSpd = 100;
|
||||
stoneData.m_lPosX = LONG(origin.x + 400);
|
||||
stoneData.m_lPosY = LONG(origin.y);
|
||||
stoneData.m_stName = "AutoHuntStoneFixture";
|
||||
auto* stone = CPythonCharacterManager::Instance().CreateInstance(stoneData);
|
||||
CHECK(stone && stone->IsStone() && !stone->IsInSafe());
|
||||
if (stone) {
|
||||
auto cfg = player.AutoHunt_GetConfig();
|
||||
cfg.bAttackStone = true;
|
||||
player.AutoHunt_SetConfig(cfg);
|
||||
CHECK(pump_until(2, [&] { return player.GetTargetVID() == stoneData.m_dwVID; }));
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
cfg.iGatherCount = 0;
|
||||
cfg.bAttackStone = false;
|
||||
player.AutoHunt_SetConfig(cfg);
|
||||
player.AutoHunt_SetEnabled(true);
|
||||
CHECK(pump_until(2, [&] { return player.GetTargetVID() == FakeLoginServer::kMobVID; }));
|
||||
// Changing the option while already fighting takes effect immediately.
|
||||
cfg.bAttackStone = true;
|
||||
player.AutoHunt_SetConfig(cfg);
|
||||
CHECK(pump_until(2, [&] { return player.GetTargetVID() == stoneData.m_dwVID; }));
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
CPythonCharacterManager::Instance().DeleteInstance(stoneData.m_dwVID);
|
||||
player.AutoHunt_SetEnabled(true);
|
||||
CHECK(pump_until(2, [&] { return player.GetTargetVID() == FakeLoginServer::kMobVID; }));
|
||||
}
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
pullMob->NEW_Stop();
|
||||
pullMob->NEW_SetPixelPosition(originalMobPosition);
|
||||
CHECK(player.AutoHunt_HandleServerCommand("AutoHuntGather 1 mob 123"));
|
||||
CHECK(player.AutoHunt_GetGatheredCount() == 0);
|
||||
// Old replies must not complete a new request; cooldown remains across toggles.
|
||||
@@ -35,6 +85,18 @@ if (server && CPythonCharacterManager::Instance().GetMainInstancePtr()) {
|
||||
CHECK(player.AutoHunt_HandleServerCommand("AutoHuntGather 1 done 20"));
|
||||
CHECK(player.AutoHunt_GetGatheredCount() == 0);
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
CHECK(py("player.AutoHuntSetConfig(10000, 0, 0, 0, 0, 0, 0, 0)"));
|
||||
CHECK(player.AutoHunt_GetConfig().iRange == 10000);
|
||||
CHECK(py("player.AutoHuntSetConfig(15000, 0, 0, 0, 0, 0, 0, 0)"));
|
||||
CHECK(player.AutoHunt_GetConfig().iRange == 15000);
|
||||
TPixelPosition farPosition = origin;
|
||||
farPosition.x += 2000;
|
||||
main_instance->NEW_Stop();
|
||||
main_instance->NEW_SetPixelPosition(farPosition);
|
||||
CHECK(py("player.AutoHuntSetConfig(15000, 0, 0, 0, 0, 0, 0, 20); player.AutoHuntSetEnabled(1)"));
|
||||
CHECK(player.AutoHunt_GetConfig().iRange == 15000);
|
||||
CHECK(pump_until(12, [&] { return server->Has("game2:chat 0 /autohunt_pull 20 15000 0 2"); }));
|
||||
player.AutoHunt_SetEnabled(false);
|
||||
player.AutoHunt_SetConfig(savedConfig);
|
||||
player.SetStatus(POINT_HP, hp);
|
||||
player.SetStatus(POINT_MAX_HP, maxHP);
|
||||
|
||||
@@ -718,10 +718,10 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
|
||||
// A ping after the burst proves that GamePhase consumed every packet above and can reply.
|
||||
Blank ping = {HDR_GC_PING};
|
||||
Blank pong = {};
|
||||
if (!c.Send(ping) || read_packet(c, HDR_CG_PONG, &pong, sizeof(pong), &error) != 1)
|
||||
return Fail(name + ": CG_PONG after the entergame burst: " + error);
|
||||
Event(name + ":burst_pong");
|
||||
if (!c.Send(ping)) return Fail(name + ": send burst ping");
|
||||
// A slow phone can request status while still processing the initial burst.
|
||||
// Real CInputMain permits chat commands before this heartbeat reply too.
|
||||
bool burst_pong = false;
|
||||
|
||||
// Then hold the connection until the test stops us, reading what the game phase sends the way
|
||||
// CInputProcessor::Process frames it (static size or TDynamicSizePacketHeader, then the sequence
|
||||
@@ -785,6 +785,10 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
return Fail(name + ": SEQUENCE mismatch on CG header " + std::to_string(header));
|
||||
c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE;
|
||||
}
|
||||
if (header == HDR_CG_PONG) {
|
||||
if (!burst_pong) { Event(name + ":burst_pong"); burst_pong = true; }
|
||||
continue;
|
||||
}
|
||||
// CInputMain::ItemPickup → CHARACTER::PickupItem → CItem::RemoveFromGround → EncodeRemovePacket.
|
||||
if (header == HDR_CG_ITEM_PICKUP)
|
||||
{
|
||||
@@ -864,11 +868,17 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
return Fail(name + ": send GC_SAFEBOX_SET");
|
||||
}
|
||||
}
|
||||
else if (text == "/server_bonuses") {
|
||||
const std::string cmd = "ServerBonuses 200 200 200 200 200 0 200 0";
|
||||
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + cmd.size() + 1), 5, 0, 1};
|
||||
if (!c.Send(chat) || !c.Send(cmd.c_str(), cmd.size() + 1)) return Fail(name + ": server bonuses reply");
|
||||
}
|
||||
else if (text.rfind("/autohunt_pull ", 0) == 0) {
|
||||
int groups = 0, radius = 0, bosses = 0;
|
||||
unsigned request = 0;
|
||||
if (sscanf(text.c_str() + 15, "%d %d %d %u", &groups, &radius, &bosses, &request) != 4)
|
||||
return Fail(name + ": malformed autohunt pull");
|
||||
if (!groups) continue;
|
||||
for (const std::string& cmd : {"AutoHuntGather " + std::to_string(request) + " mob " + std::to_string(kMobVID),
|
||||
"AutoHuntGather " + std::to_string(request) + " done 1"}) {
|
||||
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + cmd.size() + 1), 5, 0, 1};
|
||||
|
||||
@@ -393,9 +393,9 @@ int main(int argc, char** argv)
|
||||
});
|
||||
CHECK(loading && !failed());
|
||||
CHECK(py_true(loaded));
|
||||
// The loading window already warped the map; the touch host's joystick and skill buttons wait for
|
||||
// the game phase.
|
||||
CHECK(!PythonBoot::InGamePhase() && !PythonBoot::CurrentMapName().empty());
|
||||
// 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());
|
||||
|
||||
// 6. introLoading's update steps end in net.StartGame(); the next CPythonNetworkStream::Process calls
|
||||
// networkModule's SetGamePhase, which builds game.GameWindow (interfaceModule.MakeInterface) and
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 自动狩猎范围与服务器加成状态
|
||||
|
||||
2026-09-30 用户要求:引怪追击距离跟随狩猎距离;游戏左上角补充服务器经验、金币等加成。
|
||||
|
||||
本补丁在 `autohunt-pull/server.patch` 之后应用,基础补丁保持字节不变。
|
||||
`../rebuild.sh check/build/deploy` 已处理增量补丁,并强制重新编译包含命令头文件的 `cmd_general.cpp`。
|
||||
|
||||
引怪支持 35/100/150 米,地图分区按半径完整遍历并使用快照;不扫描整张地图。
|
||||
只有本命令引来的怪物获得指定玩家的临时追击距离,原有 40 米战斗目标限制和 50 米离巢限制
|
||||
在这一上下文中扩展。停止狩猎取消扩展,180 秒不刷新自动过期,换目标/地图后恢复正常追击。
|
||||
原披风道具及其他怪物 AI 保持不变;寻路仍受地图阻挡和怪物自身移动规则约束。
|
||||
|
||||
客户端每 15 秒请求 `/server_bonuses`;服务器按角色限制查询频率为至少 2 秒。
|
||||
响应 `ServerBonuses <经验倍率> <金币数量倍率> <金币掉落倍率> <物品掉落倍率> <经验活动加成> <物品活动加成> <金币活动加成> <金币礼包活动加成>`。
|
||||
前四项来自 `CHARACTER_MANAGER` 的全局实际倍率,后四项来自 `CPrivManager::GetPriv`,
|
||||
与奖励结算使用的活动数据来源一致,包含适用于当前角色的国家、公会和角色特权。
|
||||
全局 200% 与活动额外 +200% 分别显示,不假定它们相同,也不伪造持续时间。
|
||||
标准 100% 倍率和零活动加成不显示;活动结束、变化和重新登录会更新图标。
|
||||
|
||||
客户端通过 `ServerBonusScript.inc` 的 PORT 模块向原有增益栏添加独立图标及中文提示。
|
||||
原版角色/技能增益不被覆盖,打包的 `uiaffectshower.py` 和 `game.py` 保持不变。
|
||||
Android 点击图标可看到提示,macOS 悬停显示;FPS 移到顶部中间避免遮挡图标。
|
||||
|
||||
203 源文件备份:`/root/autohunt-range-20260930/`,隔离构建:`/root/rebuild/20261001-074644/`。
|
||||
部署及二进制回滚使用 `rebuild.sh`,不需要数据库迁移。
|
||||
旧停服脚本可能使用缺失的 PID 文件:重建脚本对剩余进程补发 TERM 并等待正常退出,未使用强制终止。
|
||||
|
||||
验证:`host.autohunt_pull` 覆盖远处分区、100/150 米边界、指定玩家追击、取消及过期。
|
||||
`port.login_flow` 覆盖真实 Python 配置和聊天封包链、石头优先与回退、加成图标新增/更新/移除;
|
||||
桌面及 `MT_TEST_MOBILE=1` 均通过。Mi 8 Lite 真机在模拟服务器场景显示图标和“服务器经验 200%”提示。
|
||||
macOS app 与 Android Release native/debug APK 已构建;APK 已安装到 63c36adb。
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
// PORT extension: ordinary monster pursuit remains 40 metres.
|
||||
int AutoHuntPullLeash(LPCHARACTER mob, LPCHARACTER victim);
|
||||
@@ -0,0 +1,176 @@
|
||||
--- a/server/game/src/sectree_manager.h
|
||||
+++ b/server/game/src/sectree_manager.h
|
||||
@@ -88,6 +88,18 @@
|
||||
}
|
||||
*/
|
||||
}
|
||||
+
|
||||
+ // PORT: snapshot traversal for the complete auto-hunt radius.
|
||||
+ template<typename Func>
|
||||
+ void for_each_in_rect(DWORD minX, DWORD minY, DWORD maxX, DWORD maxY, Func& func) {
|
||||
+ FCollectEntity collector;
|
||||
+ for (DWORD x = minX / SECTREE_SIZE * SECTREE_SIZE; x <= maxX; x += SECTREE_SIZE)
|
||||
+ for (DWORD y = minY / SECTREE_SIZE * SECTREE_SIZE; y <= maxY; y += SECTREE_SIZE) {
|
||||
+ LPSECTREE tree = Find(x, y);
|
||||
+ if (tree) tree->for_each_entity(collector);
|
||||
+ }
|
||||
+ collector.ForEach(func);
|
||||
+ }
|
||||
|
||||
void DumpAllToSysErr() {
|
||||
SECTREE_MAP::MapType::iterator i;
|
||||
--- a/server/game/src/autohunt_pull.h
|
||||
+++ b/server/game/src/autohunt_pull.h
|
||||
@@ -3,8 +3,37 @@
|
||||
#include "group_select.h"
|
||||
#else
|
||||
#include "sectree_manager.h"
|
||||
+#include "autohunt_pull_range.h"
|
||||
#include "autohunt_group_select.h"
|
||||
#endif
|
||||
+
|
||||
+// PORT: only mobs pulled by this command receive a temporary extended pursuit leash.
|
||||
+struct AutoHuntPullLease {
|
||||
+ LPCHARACTER mob;
|
||||
+ DWORD ownerPID;
|
||||
+ int radius;
|
||||
+ int expires;
|
||||
+};
|
||||
+static std::map<DWORD, AutoHuntPullLease> s_autoHuntPullLeases;
|
||||
+int AutoHuntPullLeash(LPCHARACTER mob, LPCHARACTER victim) {
|
||||
+ std::map<DWORD, AutoHuntPullLease>::iterator it = s_autoHuntPullLeases.find((DWORD)mob->GetVID());
|
||||
+ if (it == s_autoHuntPullLeases.end()) return 4000;
|
||||
+ const AutoHuntPullLease& lease = it->second;
|
||||
+ if (lease.mob != mob || !victim || !victim->IsPC() || victim->IsDead() ||
|
||||
+ victim->GetPlayerID() != lease.ownerPID || mob->GetMapIndex() != victim->GetMapIndex() ||
|
||||
+ get_global_time() >= lease.expires) {
|
||||
+ s_autoHuntPullLeases.erase(it);
|
||||
+ return 4000;
|
||||
+ }
|
||||
+ return std::max(4000, lease.radius + 1);
|
||||
+}
|
||||
+static void AutoHuntPullPrune(DWORD cancelPID = 0) {
|
||||
+ for (std::map<DWORD, AutoHuntPullLease>::iterator it = s_autoHuntPullLeases.begin(); it != s_autoHuntPullLeases.end();) {
|
||||
+ if (it->second.expires <= get_global_time() || (cancelPID && it->second.ownerPID == cancelPID))
|
||||
+ s_autoHuntPullLeases.erase(it++);
|
||||
+ else ++it;
|
||||
+ }
|
||||
+}
|
||||
|
||||
struct AutoHuntPullCollector {
|
||||
LPCHARACTER owner;
|
||||
@@ -35,8 +64,11 @@
|
||||
unsigned int request = 0;
|
||||
char extra = 0;
|
||||
if (sscanf(argument, "%d %d %d %u %c", &groups, &radius, &bosses, &request, &extra) != 4 ||
|
||||
- (groups != 3 && groups != 10 && groups != 20) || radius < 500 || radius > 5000 ||
|
||||
+ (groups != 0 && groups != 3 && groups != 10 && groups != 20) || radius < 500 || radius > 15000 ||
|
||||
(bosses != 0 && bosses != 1)) return;
|
||||
+ if (!ch->IsPC()) return;
|
||||
+ AutoHuntPullPrune(groups == 0 ? ch->GetPlayerID() : 0);
|
||||
+ if (groups == 0) return;
|
||||
const int now = get_global_time();
|
||||
if (!ch->IsPC() || ch->IsDead() || ch->IsStun() || !ch->CanHandleItem() || !ch->GetSectree() ||
|
||||
(ch->GetSectree()->GetAttribute(ch->GetX(), ch->GetY()) & ATTR_BANPK) ||
|
||||
@@ -47,13 +79,23 @@
|
||||
// Server-side cooldown survives reconnects and switching auto hunt on/off.
|
||||
ch->SetQuestFlag("autohunt.next_pull", now + 10);
|
||||
AutoHuntPullCollector collector(ch, radius, bosses != 0);
|
||||
- ch->GetSectree()->ForEachAround(collector);
|
||||
+ // ForEachAround only covers nine cells: visit every cell intersecting the requested radius.
|
||||
+ LPSECTREE_MAP map = SECTREE_MANAGER::instance().GetMap(ch->GetMapIndex());
|
||||
+ if (map) map->for_each_in_rect(std::max(0, int(ch->GetX()) - radius),
|
||||
+ std::max(0, int(ch->GetY()) - radius), ch->GetX() + radius, ch->GetY() + radius, collector);
|
||||
const mt_autohunt::Selection selected = mt_autohunt::SelectGroups(collector.candidates, groups);
|
||||
for (size_t i = 0; i < selected.mobs.size(); ++i) {
|
||||
LPCHARACTER mob = CHARACTER_MANAGER::instance().Find(selected.mobs[i]);
|
||||
if (!mob) continue;
|
||||
+ if (mob->IsDead() || (mob->GetVictim() && mob->GetVictim() != ch)) continue;
|
||||
+ AutoHuntPullLease lease = {mob, ch->GetPlayerID(), radius, now + 180};
|
||||
+ s_autoHuntPullLeases[(DWORD)mob->GetVID()] = lease;
|
||||
if (!mob->GetVictim()) mob->BeginFight(ch);
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u mob %u", request, selected.mobs[i]);
|
||||
}
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u done %d", request, selected.groups);
|
||||
}
|
||||
+
|
||||
+#ifndef MT_AUTOHUNT_SERVER_TEST
|
||||
+#include "server_bonuses.h"
|
||||
+#endif
|
||||
--- a/server/game/src/char_state.cpp
|
||||
+++ b/server/game/src/char_state.cpp
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "utils.h"
|
||||
#include "vector.h"
|
||||
#include "char.h"
|
||||
+#include "autohunt_pull_range.h"
|
||||
#include "battle.h"
|
||||
#include "char_manager.h"
|
||||
#include "packet.h"
|
||||
@@ -1035,7 +1036,7 @@
|
||||
|
||||
float fDist = DISTANCE_APPROX(GetX() - victim->GetX(), GetY() - victim->GetY());
|
||||
|
||||
- if (fDist >= 4000.0f) // 40미터 이상 멀어지면 포기
|
||||
+ if (fDist >= AutoHuntPullLeash(this, victim)) // 40미터 이상 멀어지면 포기
|
||||
{
|
||||
MonsterLog("타겟이 멀어서 포기");
|
||||
SetVictim(NULL);
|
||||
--- a/server/game/src/cmd.cpp
|
||||
+++ b/server/game/src/cmd.cpp
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "desc.h"
|
||||
|
||||
ACMD(do_autohunt_pull);
|
||||
+ACMD(do_server_bonuses);
|
||||
ACMD(do_user_horse_ride);
|
||||
ACMD(do_user_horse_back);
|
||||
ACMD(do_user_horse_feed);
|
||||
@@ -281,6 +282,7 @@
|
||||
{
|
||||
{ "!RESERVED!", NULL, 0, POS_DEAD, GM_IMPLEMENTOR }, /* 반드시 이 것이 처음이어야 한다. */
|
||||
{ "autohunt_pull", do_autohunt_pull, 0, POS_STANDING, GM_PLAYER },
|
||||
+ { "server_bonuses", do_server_bonuses, 0, POS_DEAD, GM_PLAYER },
|
||||
{ "who", do_who, 0, POS_DEAD, GM_IMPLEMENTOR },
|
||||
{ "war", do_war, 0, POS_DEAD, GM_PLAYER },
|
||||
{ "warp", do_warp, 0, POS_DEAD, GM_LOW_WIZARD },
|
||||
--- /dev/null
|
||||
+++ b/server/game/src/autohunt_pull_range.h
|
||||
@@ -0,0 +1,3 @@
|
||||
+#pragma once
|
||||
+// PORT extension: ordinary monster pursuit remains 40 metres.
|
||||
+int AutoHuntPullLeash(LPCHARACTER mob, LPCHARACTER victim);
|
||||
--- /dev/null
|
||||
+++ b/server/game/src/server_bonuses.h
|
||||
@@ -0,0 +1,12 @@
|
||||
+// PORT: report actual global rates and the character's effective event privileges.
|
||||
+#include "priv_manager.h"
|
||||
+ACMD(do_server_bonuses) {
|
||||
+ if (!ch->IsPC() || get_global_time() < ch->GetQuestFlag("server_bonuses.next_query")) return;
|
||||
+ ch->SetQuestFlag("server_bonuses.next_query", get_global_time() + 2);
|
||||
+ CHARACTER_MANAGER& cm = CHARACTER_MANAGER::instance();
|
||||
+ CPrivManager& pm = CPrivManager::instance();
|
||||
+ ch->ChatPacket(CHAT_TYPE_COMMAND, "ServerBonuses %d %d %d %d %d %d %d %d",
|
||||
+ cm.GetMobExpRate(NULL), cm.GetMobGoldAmountRate(NULL), cm.GetMobGoldDropRate(NULL), cm.GetMobItemRate(NULL),
|
||||
+ pm.GetPriv(ch, PRIV_EXP_PCT), pm.GetPriv(ch, PRIV_ITEM_DROP),
|
||||
+ pm.GetPriv(ch, PRIV_GOLD_DROP), pm.GetPriv(ch, PRIV_GOLD10_DROP));
|
||||
+}
|
||||
--- a/server/game/src/char.cpp
|
||||
+++ b/server/game/src/char.cpp
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../../common/VnumHelper.h"
|
||||
|
||||
#include "char.h"
|
||||
+#include "autohunt_pull_range.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "utils.h"
|
||||
@@ -5440,7 +5441,7 @@
|
||||
if (get_dword_time() - m_pkMobInst->m_dwLastAttackedTime >= 15000) // 마지막으로 공격받은지 15초가 지났고
|
||||
{
|
||||
// 마지막 맞은 곳으로 부터 50미터 이상 차이나면 포기하고 돌아간다.
|
||||
- if (5000 < DISTANCE_APPROX(m_pkMobInst->m_posLastAttacked.x - GetX(), m_pkMobInst->m_posLastAttacked.y - GetY()))
|
||||
+ if (MAX(5000, AutoHuntPullLeash(this, pkChr)) < DISTANCE_APPROX(m_pkMobInst->m_posLastAttacked.x - GetX(), m_pkMobInst->m_posLastAttacked.y - GetY()))
|
||||
if (Return())
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// PORT: report actual global rates and the character's effective event privileges.
|
||||
#include "priv_manager.h"
|
||||
ACMD(do_server_bonuses) {
|
||||
if (!ch->IsPC() || get_global_time() < ch->GetQuestFlag("server_bonuses.next_query")) return;
|
||||
ch->SetQuestFlag("server_bonuses.next_query", get_global_time() + 2);
|
||||
CHARACTER_MANAGER& cm = CHARACTER_MANAGER::instance();
|
||||
CPrivManager& pm = CPrivManager::instance();
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "ServerBonuses %d %d %d %d %d %d %d %d",
|
||||
cm.GetMobExpRate(NULL), cm.GetMobGoldAmountRate(NULL), cm.GetMobGoldDropRate(NULL), cm.GetMobItemRate(NULL),
|
||||
pm.GetPriv(ch, PRIV_EXP_PCT), pm.GetPriv(ch, PRIV_ITEM_DROP),
|
||||
pm.GetPriv(ch, PRIV_GOLD_DROP), pm.GetPriv(ch, PRIV_GOLD10_DROP));
|
||||
}
|
||||
@@ -5,22 +5,32 @@
|
||||
原披风的道具消耗和随机拉怪行为保持不变;本功能按距离确定性选择最多 N 个队伍。
|
||||
|
||||
“组”是服务端实际的怪物 `CParty`,没有队伍的单只怪物算一组。同一队伍在范围内的成员一起拉取。
|
||||
不足所选组数时拉取实际可用的组数。范围使用自动狩猎的范围设置,上限 5000 cm。
|
||||
不足所选组数时拉取实际可用的组数。狩猎边界、披风引怪半径和扩展追击距离跟随 35/100/150 米设置。
|
||||
服务端支持 500..15000 cm,并搜索半径覆盖的全部地图分区。普通怪物原有的 40 米追击限制保持不变;
|
||||
被本命令选中的怪物获得对应玩家专属的临时追击距离,并同步扩展原有 50 米离巢返回限制。
|
||||
每次引怪刷新 180 秒有效期,停止狩猎或修改范围/引怪选项会取消本玩家的扩展追击距离。
|
||||
安全区、死亡/眩晕、低于 50% HP 或不能操作道具时不引怪;默认不拉首领。
|
||||
队伍在范围内存在首领或正在攻击其他玩家的成员时,整组跳过。
|
||||
|
||||
请求 `/autohunt_pull <3|10|20> <500..5000> <boss:0|1> <request-id>` 走现有 CG_CHAT。
|
||||
请求 `/autohunt_pull <3|10|20> <500..15000> <boss:0|1> <request-id>` 走现有 CG_CHAT。
|
||||
组数为 0 时取消本玩家的扩展追击距离,不重置冷却。
|
||||
服务端每次发 `AutoHuntGather <id> mob <vid>`,最后发 `AutoHuntGather <id> done <groups>`,
|
||||
走现有 CHAT_TYPE_COMMAND。每个消息很短,不受一条聊天封包长度限制。
|
||||
客户端只接受当前请求的回包;停用、手动暂停后旧回包不生效。
|
||||
双方有 10 秒冷却,服务端冷却以角色任务标记保存,重新登录或开关狩猎不能绕过。
|
||||
客户端原地等待追击怪物进入近战距离,最多等待 8 秒;3 秒无回包则继续普通狩猎。
|
||||
客户端搜索时原地等待追击怪物进入近战距离,最多等待 8 秒;3 秒无回包则继续普通狩猎。
|
||||
战斗中也会在 10 秒冷却后补拉,不必先杀完整批怪物;补拉不打断当前攻击。
|
||||
尚未进入客户端视野的服务端确认 VID 按距离保留 35/100/150 秒,给怪物时间接近玩家。
|
||||
界面显示本轮服务端确认的实际组数/设置组数,附近不足 20 组时不会产生额外怪物。
|
||||
“优先石头”优先选择狩猎范围内的石头,没有石头再攻击普通怪物;石头不受首领开关过滤。
|
||||
战斗中更改选项或出现石头时可以切换目标,优先攻击石头期间不发起新的引怪。
|
||||
|
||||
## 构建和回滚
|
||||
|
||||
`server.patch` 以 40250 的 `/usr/metin2/src` 为根,`patch -p1` 应用。
|
||||
`autohunt_pull.h` 和 `group_select.h` 是补丁中两份新增头文件的源文件。
|
||||
`../rebuild.sh` 已加入本补丁,沿用 build / deploy 流程。
|
||||
`group_select.h` 是基础补丁中新增的队伍选择头文件,`autohunt_pull.h` 保存当前扩展版本。
|
||||
基础补丁保持不变,范围扩展在 `../autohunt-pull-range/server.patch`,须按顺序应用。
|
||||
`../rebuild.sh` 已加入这两个补丁,沿用 build / deploy 流程,并处理基础头文件被后续补丁更新的状态检查。
|
||||
|
||||
2026-09-30 已在 192.168.21.203 编译并部署。原 `cmd.cpp`、`cmd_general.cpp`、旧 game
|
||||
备份在 `/root/autohunt-pull-20260930/`(旧程序为 `game.before`)。回滚时停 game、恢复旧程序后启动;
|
||||
@@ -31,3 +41,15 @@
|
||||
桌面和移动模式均通过。真服临时角色实测回包为 **3 组、9 只怪物**,随后进入自动战斗。
|
||||
临时角色、账号和关联数据已在刷新对应 DB 缓存后清理;auth/game/db 监听已检查。
|
||||
macOS 原生包及 Android debug APK 已重新构建。
|
||||
|
||||
## 2026-09-30 修正
|
||||
|
||||
修复了石头与首领等级过滤混用、普通怪物优先于石头、必须清空旧怪物批次才再次引怪、
|
||||
远处尚未同步的怪物 VID 被立即删除,以及近距离范围让 20 组请求只搜索很小区域的问题。
|
||||
狩猎边界增加至 35/100/150 米,旧设置低于 35 米会提升到 35 米;3/10/20 组按当前狩猎半径选择最近队伍。
|
||||
狩猎边界用于已同步角色的目标选择,超过当前视野的怪物需要先进入服务器同步范围。
|
||||
范围扩展需要重新构建和部署服务端。
|
||||
|
||||
回归测试 `port.login_flow` 使用真实 Python 配置和消息链,覆盖较远石头优先于近处已引来的怪物、
|
||||
关闭优先石头、战斗中开启优先石头、没有石头时回退打怪,以及 35/100/150 米设置和 20 组随狩猎距离拉怪。
|
||||
`host.autohunt_pull` 继续验证服务端 3/10/20 个真实队伍、冷却和队伍过滤。
|
||||
|
||||
@@ -3,9 +3,38 @@
|
||||
#include "group_select.h"
|
||||
#else
|
||||
#include "sectree_manager.h"
|
||||
#include "autohunt_pull_range.h"
|
||||
#include "autohunt_group_select.h"
|
||||
#endif
|
||||
|
||||
// PORT: only mobs pulled by this command receive a temporary extended pursuit leash.
|
||||
struct AutoHuntPullLease {
|
||||
LPCHARACTER mob;
|
||||
DWORD ownerPID;
|
||||
int radius;
|
||||
int expires;
|
||||
};
|
||||
static std::map<DWORD, AutoHuntPullLease> s_autoHuntPullLeases;
|
||||
int AutoHuntPullLeash(LPCHARACTER mob, LPCHARACTER victim) {
|
||||
std::map<DWORD, AutoHuntPullLease>::iterator it = s_autoHuntPullLeases.find((DWORD)mob->GetVID());
|
||||
if (it == s_autoHuntPullLeases.end()) return 4000;
|
||||
const AutoHuntPullLease& lease = it->second;
|
||||
if (lease.mob != mob || !victim || !victim->IsPC() || victim->IsDead() ||
|
||||
victim->GetPlayerID() != lease.ownerPID || mob->GetMapIndex() != victim->GetMapIndex() ||
|
||||
get_global_time() >= lease.expires) {
|
||||
s_autoHuntPullLeases.erase(it);
|
||||
return 4000;
|
||||
}
|
||||
return std::max(4000, lease.radius + 1);
|
||||
}
|
||||
static void AutoHuntPullPrune(DWORD cancelPID = 0) {
|
||||
for (std::map<DWORD, AutoHuntPullLease>::iterator it = s_autoHuntPullLeases.begin(); it != s_autoHuntPullLeases.end();) {
|
||||
if (it->second.expires <= get_global_time() || (cancelPID && it->second.ownerPID == cancelPID))
|
||||
s_autoHuntPullLeases.erase(it++);
|
||||
else ++it;
|
||||
}
|
||||
}
|
||||
|
||||
struct AutoHuntPullCollector {
|
||||
LPCHARACTER owner;
|
||||
int radius;
|
||||
@@ -35,8 +64,11 @@ ACMD(do_autohunt_pull) {
|
||||
unsigned int request = 0;
|
||||
char extra = 0;
|
||||
if (sscanf(argument, "%d %d %d %u %c", &groups, &radius, &bosses, &request, &extra) != 4 ||
|
||||
(groups != 3 && groups != 10 && groups != 20) || radius < 500 || radius > 5000 ||
|
||||
(groups != 0 && groups != 3 && groups != 10 && groups != 20) || radius < 500 || radius > 15000 ||
|
||||
(bosses != 0 && bosses != 1)) return;
|
||||
if (!ch->IsPC()) return;
|
||||
AutoHuntPullPrune(groups == 0 ? ch->GetPlayerID() : 0);
|
||||
if (groups == 0) return;
|
||||
const int now = get_global_time();
|
||||
if (!ch->IsPC() || ch->IsDead() || ch->IsStun() || !ch->CanHandleItem() || !ch->GetSectree() ||
|
||||
(ch->GetSectree()->GetAttribute(ch->GetX(), ch->GetY()) & ATTR_BANPK) ||
|
||||
@@ -47,13 +79,23 @@ ACMD(do_autohunt_pull) {
|
||||
// Server-side cooldown survives reconnects and switching auto hunt on/off.
|
||||
ch->SetQuestFlag("autohunt.next_pull", now + 10);
|
||||
AutoHuntPullCollector collector(ch, radius, bosses != 0);
|
||||
ch->GetSectree()->ForEachAround(collector);
|
||||
// ForEachAround only covers nine cells: visit every cell intersecting the requested radius.
|
||||
LPSECTREE_MAP map = SECTREE_MANAGER::instance().GetMap(ch->GetMapIndex());
|
||||
if (map) map->for_each_in_rect(std::max(0, int(ch->GetX()) - radius),
|
||||
std::max(0, int(ch->GetY()) - radius), ch->GetX() + radius, ch->GetY() + radius, collector);
|
||||
const mt_autohunt::Selection selected = mt_autohunt::SelectGroups(collector.candidates, groups);
|
||||
for (size_t i = 0; i < selected.mobs.size(); ++i) {
|
||||
LPCHARACTER mob = CHARACTER_MANAGER::instance().Find(selected.mobs[i]);
|
||||
if (!mob) continue;
|
||||
if (mob->IsDead() || (mob->GetVictim() && mob->GetVictim() != ch)) continue;
|
||||
AutoHuntPullLease lease = {mob, ch->GetPlayerID(), radius, now + 180};
|
||||
s_autoHuntPullLeases[(DWORD)mob->GetVID()] = lease;
|
||||
if (!mob->GetVictim()) mob->BeginFight(ch);
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u mob %u", request, selected.mobs[i]);
|
||||
}
|
||||
ch->ChatPacket(CHAT_TYPE_COMMAND, "AutoHuntGather %u done %d", request, selected.groups);
|
||||
}
|
||||
|
||||
#ifndef MT_AUTOHUNT_SERVER_TEST
|
||||
#include "server_bonuses.h"
|
||||
#endif
|
||||
|
||||
+19
-2
@@ -20,10 +20,15 @@ set -eu
|
||||
SRC=${SRC:-/usr/metin2/src}
|
||||
SERVER=${SERVER:-/usr/metin2/server}
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
PATCHES="inventory-5-pages hunt-set shop-item-time autohunt-pull"
|
||||
PATCHES="inventory-5-pages hunt-set shop-item-time autohunt-pull autohunt-pull-range"
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
patch_state() {
|
||||
# The follow-up changes the header added by the base patch.
|
||||
if [ "$1" = autohunt-pull ] && patch -p1 -R -C -s -f -d "$SRC" < "$HERE/autohunt-pull-range/server.patch" > /dev/null 2>&1; then
|
||||
echo applied
|
||||
return
|
||||
fi
|
||||
if patch -p1 -R -C -s -f -d "$SRC" < "$HERE/$1/server.patch" > /dev/null 2>&1; then
|
||||
echo applied
|
||||
elif patch -p1 -C -s -f -d "$SRC" < "$HERE/$1/server.patch" > /dev/null 2>&1; then
|
||||
@@ -36,6 +41,16 @@ patch_state() {
|
||||
stop_server() {
|
||||
(cd "$SERVER" && sh close.sh) > /dev/null 2>&1 || true
|
||||
sleep 8
|
||||
remaining=$(ps ax -o pid,command | awk '$2 ~ /^\.\/(game|db|auth)$/ {print $1}')
|
||||
if [ -n "$remaining" ]; then
|
||||
# Some old shut.sh scripts reference stale/missing pid files. Still request a graceful exit.
|
||||
kill -TERM $remaining
|
||||
attempts=0
|
||||
while ps ax -o command | grep -q '^[.]/\(game\|db\|auth\)' && [ "$attempts" -lt 15 ]; do
|
||||
sleep 2
|
||||
attempts=$((attempts + 1))
|
||||
done
|
||||
fi
|
||||
if ps ax -o command | grep -q '^[.]/\(game\|db\|auth\)'; then echo "server still running" >&2; exit 1; fi
|
||||
}
|
||||
|
||||
@@ -58,7 +73,9 @@ build)
|
||||
conflict) echo "$p/server.patch does not apply" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
OUT=/root/rebuild/$TS
|
||||
# The legacy makefile does not track includes. Recompile commands after header patches.
|
||||
touch "$SRC/server/game/src/cmd_general.cpp"
|
||||
OUT=/root/rebuild/$TS
|
||||
mkdir -p "$OUT"
|
||||
# The Makefiles move the binary to GAMEDIR/DBDIR (default: the live share/bin).
|
||||
(cd "$SRC/server/game/src" && gmake -j2 GAMEDIR="$OUT") > "$OUT/game.log" 2>&1 || { tail "$OUT/game.log" >&2; exit 1; }
|
||||
|
||||
Reference in New Issue
Block a user