Add batch pickup on the pickup key for the server mt_pickup command
Embedded module mt_pickup wraps GameWindow.PickUpItem (Z / `): with batch pickup on it sends /mt_pickup 1 MASK (0.5 s gap, one in flight, presses swallowed while waiting); after 3 s without a reply it notes once in chat and falls back to the original single pickup. Settings window (on/off and 8 categories, persisted in mt_pickup.cfg) behind a "拾取" button under the switchbot entry by the minimap. Live tested against 203 (channel-5 test core, port 13040) with the macOS native client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
514eae6745
commit
e22d7ba6fd
@@ -117,4 +117,6 @@
|
||||
|
||||
2026-10-15 再续:龙穴/周任务信息与提醒、共享真实掉落开关的活动日历已实现并在隔离环境验证。副本仅首批龙穴;活动仅月光宝箱/圣诞袜,生产未部署,无生产排期。自动洗练/锁定、战令、模型预览、批量开箱仍待实现。见 AVA2-DUNGEON-CALENDAR.md。
|
||||
|
||||
2026-10-15 自动洗练 v1(阶段 6,不含属性锁定):服务端 mt_switchbot 逐次调用原 UseItem 洗练路径,客户端 mt_switchbot 窗口;隔离环境编译与桩测试通过,未部署 203、未联机验收。属性锁定仍待存储设计。见 AVA2-SWITCHBOT.md。
|
||||
2026-10-15 自动洗练 v1(阶段 6,不含属性锁定):服务端 mt_switchbot 逐次调用原 UseItem 洗练路径,客户端 mt_switchbot 窗口;隔离环境编译与桩测试通过,macOS 客户端已在 203 测试核心联机验收;未部署 203 正式环境、未做 Android 真机测试。属性锁定仍待存储设计。见 AVA2-SWITCHBOT.md。
|
||||
|
||||
2026-10-15 批量拾取(计划外追加,参考 AVA2 do_pickup_all):服务端 mt_pickup 对范围内自己的物品按分类逐件调用原 PickupItem,客户端在拾取键(Z)上接入并提供分类设置窗口;macOS 客户端已在 203 测试核心联机验收,未部署 203 正式环境、未做 Android 真机测试和触屏按钮。见 AVA2-PICKUP.md。
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# AVA2 批量拾取适配进度(v1)
|
||||
|
||||
2026-10-15。修改了 metin2-client 和 metin2-server,AVA2 代码只作参考(`do_pickup_all`、`ENABLE_AUTOMATIC_PICK_UP_SYSTEM`)。服务端规则和协议见 `metin2-server/docs/FEATURE-PICKUP.md`。
|
||||
|
||||
## 与 AVA2 的差异
|
||||
|
||||
| 项目 | AVA2 | 本实现 |
|
||||
| --- | --- | --- |
|
||||
| 触发 | 新包头 `HEADER_CG_ITEMS_PICKUP`,以及付费状态 70002(AFFECT_AUTO_PICK_UP)下的自动拾取 | 聊天命令 `/mt_pickup`,不新增包头;只在按拾取键时触发,不做自动拾取和付费状态 |
|
||||
| 范围 | 自己的物品 | 相同:只拾取自己的或无主的物品;队伍分配的物品不批量拾取 |
|
||||
| 拾取过程 | 自己实现 | 每件物品都调用原 `PickupItem`,距离、堆叠、背包空间、GET 日志、任务触发都不变 |
|
||||
| 分类 | CheckItemCanGet 模式掩码 | 8 个分类位:金币、武器、防具/首饰、技能书、灵石、材料、时装、其他 |
|
||||
|
||||
## 客户端
|
||||
|
||||
- 模块 `mt_pickup`(`src/platform/ScriptLib/PickupScript.inc`),在 `PythonBoot.cpp` 中安装。替换了 `GameWindow.PickUpItem`(Z 键和 ` 键)。
|
||||
- 开启时,按拾取键会发送 `/mt_pickup 1 掩码`。等待回复时,以及两次发送间隔不足 0.5 秒时,按键会被吞掉(防止按住 Z 连发)。关闭时走原来的单个拾取。
|
||||
- 3 秒没有回复:在聊天框提示一次“服务器尚未启用批量拾取”,之后改用单个拾取;再收到回复会自动恢复。
|
||||
- 设置窗口:入口是小地图旁“洗练”下方的“拾取”按钮。窗口内可开关批量拾取、选择 8 个分类(`【】` 表示已选,至少保留一个),并显示上次结果(附近 N 件、拾取 N 件、失败 N 件)。设置保存在客户端目录的 `mt_pickup.cfg`,格式为“开关 掩码”。
|
||||
- 单件物品的提示仍由服务端发送;失败提示(例如背包已满)只发一次。
|
||||
|
||||
## 证据
|
||||
|
||||
- `tests/wiki/test_pickup.py`(CTest:`port.pickup`)共 7 项通过,覆盖:
|
||||
- 发送节流和等待回复;
|
||||
- done/error 解析,畸形行和其他前缀;
|
||||
- 超时后改用单个拾取,迟到的回复会恢复批量拾取;
|
||||
- 关闭开关、分类掩码、至少保留一个分类;
|
||||
- 配置文件读写和损坏的配置;
|
||||
- GameWindow 钩子:入口位置、拾取键、命令拦截、窗口开关;
|
||||
- 和服务端的分类位、错误码、0.5 秒间隔一致。
|
||||
- `tests/wiki/test_switchbot.py` 仍然通过。
|
||||
- 2026-10-15 联机测试(203 测试核心 channel5/first,端口 13040,使用隔离编译的 game;macOS 原生客户端,账号 admin)。先用 `/item` 生成并逐件丢下武器 19、铠甲 11209、材料 30003、灵石 28030、技能书 50300 和 1000 金币:
|
||||
- 掩码 2(武器):`done 1 1 0`,只拾回武器。
|
||||
- 掩码 48(灵石+材料):`done 2 2 0`。
|
||||
- 掩码 255:`done 3 3 0`,拾回铠甲、技能书和金币(金币数恢复)。
|
||||
- 地上已没有物品:`done 0 0 0`;紧接着再发一次:`error rate`。
|
||||
- log.log 有 5 行 GET,每件物品一行,和 DROP 一一对应。原路径拾取金币不写 GET 日志。
|
||||
- 测试核心 syserr 没有新错误。丢金币时出现的 LOCALE_ERROR 来自原有的丢金币提示。
|
||||
|
||||
## 未完成
|
||||
|
||||
- Android 真机测试和触屏拾取按钮(`touch_controller.h` 的按钮发送 DIK 键码,需要再加布局)。
|
||||
- 背包已满时的联机测试(服务端桩测试已覆盖)。
|
||||
- 服务端还没有部署到 203 正式环境。
|
||||
- 自动狩猎自带的客户端拾取(`PICKUP_TEXTS`)没有改动。
|
||||
@@ -0,0 +1,194 @@
|
||||
// PORT: AVA2-style batch pickup (do_pickup_all), driven by the server's /mt_pickup command. With batch
|
||||
// pickup on, the pickup key (GameWindow.PickUpItem, Z / `) asks the server to pick up every own item in
|
||||
// range of the chosen categories; each item still goes through the original pickup path. Against a server
|
||||
// without the command it falls back to the original single pickup. Protocol: metin2-server docs/FEATURE-PICKUP.md.
|
||||
static const char c_szPickupScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import app
|
||||
import chat
|
||||
import game
|
||||
import grp
|
||||
import net
|
||||
import ui
|
||||
import weakref
|
||||
from mt_biolog import Action, Text, L
|
||||
from mt_wiki import B
|
||||
|
||||
g_window=None
|
||||
g_entry=None
|
||||
CONFIG='mt_pickup.cfg'
|
||||
ALL=255
|
||||
CATEGORIES=((1,u'金币'),(2,u'武器'),(4,u'防具/首饰'),(8,u'技能书'),(16,u'灵石'),(32,u'材料'),(64,u'时装'),(128,u'其他'))
|
||||
SEND_GAP=.5
|
||||
TIMEOUT=3.
|
||||
ERRORS={'rate':'操作过快','invalid':'参数无效','busy':'当前状态不能拾取'}
|
||||
|
||||
|
||||
def Number(text,low=0,high=4294967295):
|
||||
if not text.isdigit() or len(text)>10:raise ValueError(text)
|
||||
value=int(text)
|
||||
if not low<=value<=high:raise ValueError(text)
|
||||
return value
|
||||
|
||||
|
||||
class Picker(object):
|
||||
# Protocol state only. send(text) posts a chat command, now() is seconds.
|
||||
def __init__(self,send,now,listener=None):
|
||||
self.send=send;self.now=now;self.listener=listener
|
||||
self.enabled=True;self.mask=ALL;self.waiting=None;self.nextSend=0
|
||||
self.unsupported=False;self.result=None;self.error=None
|
||||
|
||||
def Press(self):
|
||||
# True when the press was handled here; False means use the original single pickup.
|
||||
self.Tick()
|
||||
if not self.enabled or self.unsupported:return False
|
||||
now=self.now()
|
||||
if self.waiting is None and now>=self.nextSend:
|
||||
self.send('/mt_pickup 1 %d'%self.mask);self.waiting=now+TIMEOUT;self.nextSend=now+SEND_GAP
|
||||
return True
|
||||
|
||||
def Tick(self):
|
||||
if self.waiting is not None and self.now()>=self.waiting:
|
||||
self.waiting=None;self.unsupported=True;self.Notify('unsupported')
|
||||
|
||||
def Notify(self,kind):
|
||||
if self.listener:self.listener(kind)
|
||||
|
||||
def Receive(self,line):
|
||||
if not line.startswith('MTPU1 '):return False
|
||||
words=line.split()
|
||||
try:
|
||||
if words[1]=='done' and len(words)==5:
|
||||
found,picked,failed=[Number(w,0,1000) for w in words[2:]]
|
||||
self.result=dict(found=found,picked=picked,failed=failed);self.error=None;kind='done'
|
||||
elif words[1]=='error' and len(words)==3 and words[2] in ERRORS:self.error=words[2];kind='error'
|
||||
else:return True
|
||||
except ValueError:return True
|
||||
self.waiting=None;self.unsupported=False;self.Notify(kind)
|
||||
return True
|
||||
|
||||
def Load(self,text):
|
||||
try:
|
||||
enabled,mask=[Number(w) for w in text.split()]
|
||||
if enabled in (0,1) and 1<=mask<=ALL:self.enabled=enabled==1;self.mask=mask
|
||||
except ValueError:pass
|
||||
|
||||
def Dump(self):return '%d %d'%(int(self.enabled),self.mask)
|
||||
|
||||
def Toggle(self,bit):
|
||||
mask=self.mask^bit
|
||||
if mask:self.mask=mask # at least one category stays on
|
||||
|
||||
|
||||
def LoadConfig(picker):
|
||||
try:
|
||||
f=open(CONFIG,'r');text=f.read();f.close();picker.Load(text)
|
||||
except (IOError,OSError):pass
|
||||
|
||||
|
||||
def SaveConfig(picker):
|
||||
try:
|
||||
f=open(CONFIG,'w');f.write(picker.Dump());f.close()
|
||||
except (IOError,OSError):pass
|
||||
|
||||
|
||||
class PickupWindow(ui.Window):
|
||||
def __init__(self,picker):
|
||||
ui.Window.__init__(self);self.SetWindowName('PickupWindow');self.AddFlag('float');self.AddFlag('movable')
|
||||
self.picker=picker;self.SetSize(332,346)
|
||||
self.heading=Text(self,18,18,'批量拾取',0xffffd68b)
|
||||
self.closeButton=Action(self,256,6,60,'关闭',self.Hide)
|
||||
self.enableButton=Action(self,18,52,296,'',self.ToggleEnabled)
|
||||
self.categoryButtons=[Action(self,18+(i%2)*152,108+(i//2)*52,144,'',lambda bit=bit:self.ToggleCategory(bit))
|
||||
for i,(bit,name) in enumerate(CATEGORIES)]
|
||||
self.status=Text(self,18,322,'',0xffc3b7a1)
|
||||
self.Refresh();self.Hide()
|
||||
|
||||
def Buttons(self):return [self.closeButton,self.enableButton]+self.categoryButtons
|
||||
|
||||
def Open(self):self.Refresh();self.SetCenterPosition();self.Show();self.SetTop()
|
||||
|
||||
def ToggleEnabled(self):
|
||||
self.picker.enabled=not self.picker.enabled;self.picker.unsupported=False;SaveConfig(self.picker);self.Refresh()
|
||||
|
||||
def ToggleCategory(self,bit):self.picker.Toggle(bit);SaveConfig(self.picker);self.Refresh()
|
||||
|
||||
def Refresh(self):
|
||||
p=self.picker
|
||||
self.enableButton.SetText(L('拾取键(Z):批量拾取 已开启' if p.enabled else '拾取键(Z):批量拾取 已关闭(单个拾取)'))
|
||||
for button,(bit,name) in zip(self.categoryButtons,CATEGORIES):
|
||||
button.SetText(L(B(u'【%s】'%name if p.mask&bit else name)))
|
||||
button.SetEnabled(p.enabled)
|
||||
self.status.SetText(L(self.StatusText()))
|
||||
|
||||
def StatusText(self):
|
||||
p=self.picker
|
||||
if p.unsupported:return '!服务器尚未启用批量拾取,已改用单个拾取'
|
||||
if p.error:return '!'+ERRORS[p.error]
|
||||
if p.result:
|
||||
r=p.result;text='上次:附近 %d 件,拾取 %d 件'%(r['found'],r['picked'])
|
||||
return text+(',%d 件未能拾取'%r['failed'] if r['failed'] else '')
|
||||
return '只拾取自己的物品,范围与单个拾取相同'
|
||||
|
||||
def OnRender(self):
|
||||
x,y=self.GetGlobalPosition();grp.SetColor(0xf51b1916);grp.RenderBar(x,y,self.GetWidth(),self.GetHeight());grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,self.GetWidth(),self.GetHeight())
|
||||
|
||||
def OnUpdate(self):self.picker.Tick()
|
||||
|
||||
def OnPressEscapeKey(self):self.Hide();return True
|
||||
|
||||
def Destroy(self):
|
||||
self.Hide()
|
||||
for b in self.Buttons():b.event=None
|
||||
|
||||
|
||||
class Entry(Action):
|
||||
def __init__(self,window):
|
||||
Action.__init__(self,None,0,0,64,'拾取',Toggle)
|
||||
self.minimap=weakref.proxy(window.interface.wndMiniMap);self.SetWindowName('PickupEntry');self.OnUpdate()
|
||||
|
||||
def OnUpdate(self):
|
||||
x,y=self.minimap.GetGlobalPosition();self.SetPosition(max(4,x-68),y+200)
|
||||
|
||||
|
||||
def OnPicker(kind):
|
||||
if kind=='unsupported':chat.AppendChat(chat.CHAT_TYPE_INFO,L('服务器尚未启用批量拾取,已改用单个拾取'))
|
||||
if g_window:g_window.Refresh()
|
||||
|
||||
|
||||
g_picker=Picker(net.SendChatPacket,app.GetTime,OnPicker)
|
||||
LoadConfig(g_picker)
|
||||
|
||||
|
||||
def Toggle():
|
||||
if g_window:
|
||||
if g_window.IsShow():g_window.Hide()
|
||||
else:g_window.Open()
|
||||
|
||||
|
||||
_open=game.GameWindow.Open
|
||||
_close=game.GameWindow.Close
|
||||
_command=game.GameWindow.BINARY_ServerCommand_Run
|
||||
_pick=game.GameWindow.PickUpItem
|
||||
|
||||
def Open(self):
|
||||
global g_window,g_entry
|
||||
_open(self);g_picker.waiting=None;g_window=PickupWindow(g_picker);g_entry=Entry(self)
|
||||
|
||||
def Close(self):
|
||||
global g_window,g_entry
|
||||
if g_entry:g_entry.Hide();g_entry.event=None;g_entry=None
|
||||
if g_window:g_window.Destroy();g_window=None
|
||||
_close(self)
|
||||
|
||||
def Command(self,line):
|
||||
if g_picker.Receive(line):return 1
|
||||
return _command(self,line)
|
||||
|
||||
def PickUpItem(self):
|
||||
if not g_picker.Press():_pick(self)
|
||||
|
||||
game.GameWindow.Open=Open
|
||||
game.GameWindow.Close=Close
|
||||
game.GameWindow.BINARY_ServerCommand_Run=Command
|
||||
game.GameWindow.PickUpItem=PickUpItem
|
||||
)PY";
|
||||
@@ -1000,6 +1000,7 @@ namespace PythonBoot
|
||||
#include "CalendarScript.inc"
|
||||
#include "WikiEntryScript.inc"
|
||||
#include "SwitchbotScript.inc"
|
||||
#include "PickupScript.inc"
|
||||
|
||||
static void install_script_module(const char* c_szModule, const char* c_szScript)
|
||||
{
|
||||
@@ -1074,6 +1075,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
install_script_module("mt_calendar", c_szCalendarScript);
|
||||
install_script_module("mt_wiki_entries", c_szWikiEntryScript);
|
||||
install_script_module("mt_switchbot", c_szSwitchbotScript);
|
||||
install_script_module("mt_pickup", c_szPickupScript);
|
||||
if (g_touch_input)
|
||||
install_script_module("mt_mobile_hud", c_szMobileHUDScript);
|
||||
}
|
||||
|
||||
@@ -318,6 +318,8 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_service.py)
|
||||
add_test(NAME port.switchbot
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_switchbot.py)
|
||||
add_test(NAME port.pickup
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_pickup.py)
|
||||
endif()
|
||||
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
|
||||
add_executable(locale_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/locale_proto_patch.cpp)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
|
||||
def script(name):return (ROOT/'src/platform/ScriptLib'/name).read_text().split('R"PY(',1)[1].rsplit(')PY";',1)[0]
|
||||
|
||||
|
||||
class Widget(object):
|
||||
def __init__(self,*a,**k):self.shown=False;self.text='';self.pos=(0,0);self.size=(0,0)
|
||||
def __getattr__(self,name):return lambda *a,**k:None
|
||||
def Show(self):self.shown=True
|
||||
def Hide(self):self.shown=False
|
||||
def IsShow(self):return self.shown
|
||||
def SetText(self,text):self.text=text
|
||||
def SetPosition(self,x,y):self.pos=(x,y)
|
||||
def GetGlobalPosition(self):return self.pos
|
||||
def SetSize(self,w,h):self.size=(w,h)
|
||||
def GetWidth(self):return self.size[0]
|
||||
def GetHeight(self):return self.size[1]
|
||||
|
||||
|
||||
class Action(Widget):
|
||||
def __init__(self,parent,x,y,width,text,event):
|
||||
Widget.__init__(self);self.pos=(x,y);self.event=event;self.enabled=True;self.text=text;self.shown=True
|
||||
def SetEnabled(self,enabled):self.enabled=enabled
|
||||
|
||||
|
||||
class PickupTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.time=1.;self.sent=[];self.chat=[];self.events=[];self.single=[]
|
||||
self.cwd=os.getcwd();self.tmp=tempfile.TemporaryDirectory();os.chdir(self.tmp.name)
|
||||
mods={n:types.ModuleType(n) for n in ('app','chat','game','grp','net','ui','mt_biolog','mt_wiki')}
|
||||
mods['app'].GetTime=lambda:self.time
|
||||
mods['chat'].CHAT_TYPE_INFO=1;mods['chat'].AppendChat=lambda t,m:self.chat.append(m)
|
||||
mods['net'].SendChatPacket=self.sent.append
|
||||
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None,
|
||||
BINARY_ServerCommand_Run=lambda s,line:7,PickUpItem=lambda s:self.single.append(1)))
|
||||
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
|
||||
mods['ui'].Window=Widget
|
||||
mods['mt_biolog'].Action=Action;mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:Widget();mods['mt_biolog'].L=lambda t:t if isinstance(t,str) else t.decode('utf-8')
|
||||
mods['mt_wiki'].B=lambda t,limit=0:t
|
||||
self.previous={n:sys.modules.get(n) for n in mods}
|
||||
sys.modules.update(mods)
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
self.ns={'__name__':'mt_pickup'};exec(script('PickupScript.inc'),self.ns)
|
||||
self.picker=self.ns['Picker'](self.sent.append,lambda:self.time,self.events.append)
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.cwd);self.tmp.cleanup()
|
||||
for name,value in self.previous.items():
|
||||
if value is None:sys.modules.pop(name,None)
|
||||
else:sys.modules[name]=value
|
||||
|
||||
def test_press_sends_once_per_gap_and_waits_for_reply(self):
|
||||
self.assertTrue(self.picker.Press());self.assertTrue(self.picker.Press())
|
||||
self.assertEqual(self.sent,['/mt_pickup 1 255'])
|
||||
self.time+=.6;self.picker.Press();self.assertEqual(len(self.sent),1) # still waiting for the reply
|
||||
self.assertTrue(self.picker.Receive('MTPU1 done 5 4 1'))
|
||||
self.assertEqual(self.picker.result,dict(found=5,picked=4,failed=1))
|
||||
self.picker.Press();self.assertEqual(len(self.sent),2)
|
||||
self.picker.Receive('MTPU1 done 0 0 0');self.time+=.1;self.picker.Press()
|
||||
self.assertEqual(len(self.sent),2) # inside the 0.5 s gap: swallowed (key repeat)
|
||||
|
||||
def test_errors_malformed_and_foreign_lines(self):
|
||||
self.picker.Press();self.picker.Receive('MTPU1 error busy')
|
||||
self.assertEqual(self.picker.error,'busy');self.assertIsNone(self.picker.waiting)
|
||||
for bad in ('MTPU1 done 1 2','MTPU1 done a 1 1','MTPU1 error boom','MTPU1 done 1 1 1 1'):
|
||||
self.assertTrue(self.picker.Receive(bad))
|
||||
self.assertEqual(self.picker.error,'busy')
|
||||
self.assertFalse(self.picker.Receive('MTSB1 state 0 0 0 0 0 0 0 idle'))
|
||||
|
||||
def test_timeout_falls_back_to_single_pickup(self):
|
||||
self.picker.Press();self.time+=3.1
|
||||
self.assertFalse(self.picker.Press());self.assertTrue(self.picker.unsupported)
|
||||
self.assertEqual(self.events,['unsupported']);self.assertEqual(len(self.sent),1)
|
||||
self.picker.Receive('MTPU1 done 0 0 0');self.assertFalse(self.picker.unsupported) # a late reply re-enables
|
||||
|
||||
def test_disabled_and_category_mask(self):
|
||||
self.picker.enabled=False;self.assertFalse(self.picker.Press());self.assertEqual(self.sent,[])
|
||||
self.picker.enabled=True;self.picker.mask=1
|
||||
self.picker.Toggle(1);self.assertEqual(self.picker.mask,1) # the last category stays on
|
||||
self.picker.Toggle(32);self.picker.Press();self.assertEqual(self.sent,['/mt_pickup 1 33'])
|
||||
|
||||
def test_config_round_trip_and_bad_files(self):
|
||||
self.picker.enabled=False;self.picker.mask=6;self.ns['SaveConfig'](self.picker)
|
||||
other=self.ns['Picker'](None,None);self.ns['LoadConfig'](other)
|
||||
self.assertEqual((other.enabled,other.mask),(False,6))
|
||||
for text in ('1 0','1 256','2 5','x 5','1','1 -3'):
|
||||
p=self.ns['Picker'](None,None);p.Load(text);self.assertEqual((p.enabled,p.mask),(True,255))
|
||||
|
||||
def test_game_window_hooks(self):
|
||||
game=sys.modules['game'];gw=game.GameWindow();gw.interface=types.SimpleNamespace(wndMiniMap=Widget())
|
||||
game.GameWindow.Open(gw);w=self.ns['g_window'];picker=self.ns['g_picker']
|
||||
self.assertEqual(self.ns['g_entry'].pos,(4,200))
|
||||
game.GameWindow.PickUpItem(gw);self.assertEqual(self.sent,['/mt_pickup 1 255']);self.assertEqual(self.single,[])
|
||||
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTPU1 done 3 3 0'),1)
|
||||
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'other'),7)
|
||||
self.assertIn('拾取 3 件',w.status.text if hasattr(w.status,'text') else '')
|
||||
w.Open();w.ToggleCategory(128);self.assertNotIn('【',w.categoryButtons[7].text);self.assertIn('【',w.categoryButtons[0].text)
|
||||
w.ToggleEnabled();self.assertFalse(picker.enabled);self.assertFalse(w.categoryButtons[0].enabled)
|
||||
game.GameWindow.PickUpItem(gw);self.assertEqual(self.single,[1])
|
||||
self.assertEqual(Path('mt_pickup.cfg').read_text(),'0 127')
|
||||
self.time+=1;w.ToggleEnabled();game.GameWindow.PickUpItem(gw);self.time+=3.1;w.OnUpdate()
|
||||
self.assertTrue(picker.unsupported);self.assertIn('单个拾取',self.chat[-1])
|
||||
game.GameWindow.PickUpItem(gw);self.assertEqual(self.single,[1,1])
|
||||
game.GameWindow.Close(gw);self.assertIsNone(self.ns['g_window']);self.assertIsNone(self.ns['g_entry'])
|
||||
|
||||
def test_server_constants_match(self):
|
||||
server=ROOT.parent/'metin2-server/src/server/game/src/port_pickup.h'
|
||||
if not server.exists():self.skipTest('server checkout not installed')
|
||||
text=server.read_text(encoding='latin1')
|
||||
bits=dict((name,int(v)) for name,v in re.findall(r'PORT_PICKUP_([A-Z]+)=(\d+)',text))
|
||||
self.assertEqual(bits.pop('ALL'),self.ns['ALL'])
|
||||
self.assertEqual(sorted(bits.values()),[bit for bit,name in self.ns['CATEGORIES']])
|
||||
self.assertTrue(set(re.findall(r'MTPU1 error ([a-z_]+)"',text))<=set(self.ns['ERRORS']))
|
||||
self.assertIn('kPortPickupGapMs=500',text);self.assertEqual(self.ns['SEND_GAP'],.5)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user