Ctrl + right click on an ITEM_GIFTBOX opens a count window (1/10/50/all, max 200) that sends /mt_chest and sums the MTCH1 gains into chat. Without Ctrl or against a server without the command the original use runs. Exports item.ITEM_TYPE_GIFTBOX. Verified live on the 203 channel5 core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
8.1 KiB
Python
134 lines
8.1 KiB
Python
import re
|
||
import sys
|
||
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
|
||
|
||
|
||
NAMES={50100:'Box',27001:'Potion',110000:'Soul',30000:'Ore'}
|
||
TYPES={50100:23,27001:3,30000:5}
|
||
|
||
|
||
class ChestTests(unittest.TestCase):
|
||
def setUp(self):
|
||
self.time=1.;self.sent=[];self.chat=[];self.used=[];self.ctrl=False;self.bag={}
|
||
mods={n:types.ModuleType(n) for n in ('app','chat','game','grp','item','net','player','ui','uiInventory','mt_biolog','mt_wiki')}
|
||
app=mods['app'];app.GetTime=lambda:self.time;app.DIK_LCONTROL=29;app.DIK_RCONTROL=157
|
||
app.IsPressed=lambda key:self.ctrl and key==29
|
||
mods['chat'].CHAT_TYPE_INFO=1;mods['chat'].AppendChat=lambda t,m:self.chat.append(m)
|
||
mods['net'].SendChatPacket=self.sent.append
|
||
selected=[0];it=mods['item']
|
||
it.ITEM_TYPE_GIFTBOX=23;it.SelectItem=lambda v:selected.__setitem__(0,v)
|
||
it.GetItemName=lambda:NAMES.get(selected[0],'?');it.GetItemType=lambda:TYPES.get(selected[0],0)
|
||
pl=mods['player'];pl.INVENTORY_PAGE_SIZE=45;pl.INVENTORY_PAGE_COUNT=5
|
||
pl.GetItemIndex=lambda c:self.bag.get(c,(0,0))[0]
|
||
pl.GetItemCountByVnum=lambda v:sum(n for vnum,n in self.bag.values() if vnum==v)
|
||
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None,
|
||
BINARY_ServerCommand_Run=lambda s,line:7))
|
||
mods['uiInventory'].InventoryWindow=type('InventoryWindow',(),dict(UseItemSlot=lambda s,slot:self.used.append(slot),
|
||
_InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos=lambda s,slot:slot+45))
|
||
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
|
||
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.ns={'__name__':'mt_chest'};exec(script('ChestScript.inc'),self.ns)
|
||
self.opener=self.ns['Opener'](self.sent.append,lambda:self.time)
|
||
|
||
def tearDown(self):
|
||
for name,value in self.previous.items():
|
||
if value is None:sys.modules.pop(name,None)
|
||
else:sys.modules[name]=value
|
||
|
||
def test_open_sends_once_and_collects_gains_until_done(self):
|
||
self.assertTrue(self.opener.Open(50,7));self.assertFalse(self.opener.Open(50,7))
|
||
self.assertEqual(self.sent,['/mt_chest 1 open 50 7'])
|
||
self.time+=1;self.assertFalse(self.opener.Open(50,7)) # still waiting for the reply
|
||
for line in ('MTCH1 got 27001 7','MTCH1 got 110000 2','MTCH1 done 50100 7 7 done 700'):
|
||
self.assertTrue(self.opener.Receive(line))
|
||
r=self.opener.result
|
||
self.assertEqual((r['vnum'],r['asked'],r['opened'],r['reason'],r['gold']),(50100,7,7,'done',700))
|
||
self.assertEqual(r['gains'],[(27001,7),(110000,2)]);self.assertEqual(self.opener.gains,[])
|
||
self.assertTrue(self.opener.Open(50,500));self.assertEqual(self.sent[-1],'/mt_chest 1 open 50 200') # capped
|
||
self.opener.Receive('MTCH1 done 50100 200 0 rejected -50');self.assertEqual(self.opener.result['gold'],-50)
|
||
self.time+=.1;self.assertFalse(self.opener.Open(50,1)) # inside the send gap
|
||
|
||
def test_errors_malformed_and_foreign_lines(self):
|
||
self.opener.Open(1,1);self.opener.Receive('MTCH1 error kind')
|
||
self.assertEqual(self.opener.error,'kind');self.assertIsNone(self.opener.waiting)
|
||
for bad in ('MTCH1 done 1 1 1 done','MTCH1 done 1 1 1 boom 0','MTCH1 got x 1','MTCH1 got 1 0','MTCH1 error boom','MTCH1 done a 1 1 done 0'):
|
||
self.assertTrue(self.opener.Receive(bad))
|
||
self.assertEqual(self.opener.error,'kind');self.assertIsNone(self.opener.result)
|
||
self.assertFalse(self.opener.Receive('MTPU1 done 1 1 0'))
|
||
|
||
def test_timeout_marks_unsupported(self):
|
||
events=[];self.opener.listener=events.append
|
||
self.opener.Open(1,1);self.time+=3.1;self.assertFalse(self.opener.Open(1,1))
|
||
self.assertTrue(self.opener.unsupported);self.assertEqual(events,['unsupported'])
|
||
self.opener.Receive('MTCH1 done 50100 1 1 done 0');self.assertFalse(self.opener.unsupported) # a late reply re-enables
|
||
|
||
def test_summary_lines(self):
|
||
lines=self.ns['Summary'](dict(vnum=50100,opened=12,reason='full',gold=1200,gains=[(27001,12)]+[(30000,1)]*5))
|
||
self.assertEqual(lines[0],'批量开启 Box:开启 12 个,金币 +1200(背包空位不足,已停止)')
|
||
self.assertEqual(lines[1],'获得:Potion ×12,Ore ×1,Ore ×1,Ore ×1,Ore ×1');self.assertEqual(lines[2],'获得:Ore ×1')
|
||
self.assertEqual(self.ns['Summary'](dict(vnum=50100,opened=1,reason='done',gold=0,gains=[])),['批量开启 Box:开启 1 个'])
|
||
|
||
def test_ctrl_right_click_opens_window_and_hooks(self):
|
||
game=sys.modules['game'];inv=sys.modules['uiInventory'].InventoryWindow();gw=game.GameWindow()
|
||
self.bag={50:(50100,1),51:(50100,1),52:(27001,3),90:(50100,1)}
|
||
game.GameWindow.Open(gw);w=self.ns['g_window']
|
||
inv.UseItemSlot(5);self.assertEqual(self.used,[5]);self.assertFalse(w.IsShow()) # no Ctrl: original use
|
||
self.ctrl=True;inv.UseItemSlot(7);self.assertEqual(self.used,[5,7]) # Ctrl on a potion: original use
|
||
inv.UseItemSlot(5);self.assertEqual(self.used,[5,7]);self.assertTrue(w.IsShow())
|
||
self.assertEqual((w.cell,w.vnum),(50,50100));self.assertIn('3',w.owned.text);self.assertEqual(w.openButton.text,'开启 1 个')
|
||
w.allButton.event();self.assertEqual(w.openButton.text,'开启 3 个')
|
||
del self.bag[50];w.openButton.event();self.assertEqual(self.sent,['/mt_chest 1 open 51 2']) # chosen cell gone: next box, 2 left
|
||
self.assertFalse(w.openButton.enabled)
|
||
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTCH1 got 27001 2'),1)
|
||
self.bag={52:(27001,6)}
|
||
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTCH1 done 50100 2 2 done 200'),1)
|
||
self.assertEqual(self.chat,['批量开启 Box:开启 2 个,金币 +200','获得:Potion ×2'])
|
||
self.assertIn('2/2',w.status.text);self.assertEqual(w.openButton.text,'没有宝箱')
|
||
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'other'),7)
|
||
self.bag={60:(50100,1)};self.time+=1;w.openButton.event();self.time+=3.1;w.OnUpdate()
|
||
self.assertTrue(self.ns['g_opener'].unsupported);self.assertIn('单个开启',self.chat[-1])
|
||
inv.UseItemSlot(15);self.assertEqual(self.used,[5,7,15]) # unsupported server: Ctrl falls back to normal use
|
||
game.GameWindow.Close(gw);self.assertIsNone(self.ns['g_window'])
|
||
|
||
def test_server_constants_match(self):
|
||
server=ROOT.parent/'metin2-server/src/server/game/src/port_chest.h'
|
||
if not server.exists():self.skipTest('server checkout not installed')
|
||
text=server.read_text(encoding='latin1')
|
||
self.assertIn('kPortChestMax=200',text);self.assertEqual(self.ns['MAX'],200)
|
||
self.assertIn('kPortChestGapMs=500',text);self.assertEqual(self.ns['SEND_GAP'],.5)
|
||
self.assertTrue(set(re.findall(r'return "([a-z]+)";',text))<=set(self.ns['ERRORS']))
|
||
self.assertTrue(set(re.findall(r'reason="([a-z]+)"',text))<=set(self.ns['REASONS']))
|
||
|
||
|
||
if __name__=='__main__':unittest.main()
|