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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user