Cards 38001 (7 days) and 38002 (30 days) extend all seven original premium services of the account; the server side is metin2-server port_premium.h (docs/FEATURE-VIP.md). - mt_vip: /mt_vip 1 status, VIP button by the minimap, remaining time per service, chat line on card use. - tools/server/vip-card: 40250 ships both cards with size 0 (two share a cell, neither moves); give them one cell, stacking, a log and no NPC sale, and name them on the server. - locale_proto_patch: optional item shape table (size, flags, anti flags) so the client proto matches; data/zh_40250/item_shapes_server.tsv. - Item mall (9010): 7 days 5,000,000, 30 days 18,000,000 gold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
6.7 KiB
Python
126 lines
6.7 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);self.color=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 SetPackedFontColor(self,color):self.color=color
|
|
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.text=text;self.shown=True
|
|
|
|
|
|
class Minimap(Widget):
|
|
def GetGlobalPosition(self):return (700,10)
|
|
|
|
|
|
class VipTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.time=100.;self.sent=[];self.chat=[]
|
|
mods={n:types.ModuleType(n) for n in ('app','chat','game','grp','net','ui','mt_biolog')}
|
|
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))
|
|
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
|
|
mods['ui'].Window=Widget
|
|
mods['mt_biolog'].Action=Action;mods['mt_biolog'].L=lambda t:t
|
|
mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:self.text(t,c)
|
|
self.previous={n:sys.modules.get(n) for n in mods}
|
|
sys.modules.update(mods)
|
|
self.ns={'__name__':'mt_vip'};exec(script('VipScript.inc'),self.ns)
|
|
self.status=self.ns['Status'](self.sent.append,lambda:self.time)
|
|
|
|
def text(self,t,c):
|
|
w=Widget();w.text=t;w.color=c;return w
|
|
|
|
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_query_parses_times_and_counts_down(self):
|
|
self.assertTrue(self.status.Query());self.assertFalse(self.status.Query())
|
|
self.assertEqual(self.sent,['/mt_vip 1 status'])
|
|
self.assertTrue(self.status.Receive('MTVIP1 times 604800 604800 0 604800 604800 604800 90'))
|
|
self.assertIsNone(self.status.waiting);self.assertEqual(self.status.Left(2),0)
|
|
self.time+=100;self.assertEqual(self.status.Left(0),604700);self.assertEqual(self.status.Left(6),0)
|
|
self.time+=.5;self.assertTrue(self.status.Query()) # past the send gap
|
|
|
|
def test_added_error_malformed_and_foreign(self):
|
|
events=[];self.status.listener=events.append
|
|
self.status.Receive('MTVIP1 added 38002 30');self.assertEqual(self.status.added,(38002,30))
|
|
self.status.Receive('MTVIP1 error full');self.assertEqual(self.status.error,'full')
|
|
for bad in ('MTVIP1 times 1 2 3','MTVIP1 times 1 2 3 4 5 6 x','MTVIP1 times 1 2 3 4 5 6 -7','MTVIP1 added 38001 0',
|
|
'MTVIP1 added 38001 401','MTVIP1 error boom','MTVIP1 boom'):
|
|
self.assertTrue(self.status.Receive(bad))
|
|
self.assertEqual(events,['added','error']);self.assertIsNone(self.status.times)
|
|
self.assertFalse(self.status.Receive('MTCH1 error kind'))
|
|
|
|
def test_timeout_marks_unsupported(self):
|
|
events=[];self.status.listener=events.append
|
|
self.status.Query();self.time+=3.1;self.status.Tick()
|
|
self.assertTrue(self.status.unsupported);self.assertEqual(events,['unsupported'])
|
|
self.status.Receive('MTVIP1 times 0 0 0 0 0 0 0');self.assertFalse(self.status.unsupported)
|
|
|
|
def test_remaining_text(self):
|
|
R=self.ns['Remaining']
|
|
self.assertEqual(R(0),'未开通');self.assertEqual(R(-5),'未开通')
|
|
self.assertEqual(R(7*86400+3*3600+59),'剩余 7 天 3 小时')
|
|
self.assertEqual(R(5*3600+120),'剩余 5 小时 2 分');self.assertEqual(R(30),'剩余 1 分')
|
|
|
|
def test_window_entry_and_chat(self):
|
|
game=sys.modules['game'];gw=game.GameWindow()
|
|
gw.interface=types.SimpleNamespace(wndMiniMap=Minimap())
|
|
game.GameWindow.Open(gw);w=self.ns['g_window'];entry=self.ns['g_entry']
|
|
self.assertEqual(entry.pos,(632,418))
|
|
entry.event();self.assertTrue(w.IsShow());self.assertEqual(self.sent,['/mt_vip 1 status'])
|
|
self.assertEqual(w.note.text,'正在查询……');self.assertEqual(w.values[0].text,'……')
|
|
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 times 0 0 0 0 0 0 0'),1)
|
|
self.assertEqual(w.note.text,'尚未开通 VIP');self.assertEqual(w.values[3].text,'未开通')
|
|
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 added 38001 7'),1)
|
|
self.assertEqual(self.chat,['VIP 会员已延长 7 天,全部特权立即生效'])
|
|
game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 times 604800 604800 604800 604800 604800 604800 604800')
|
|
self.assertEqual(w.values[6].text,'剩余 7 天 0 小时');self.assertEqual(w.note.text,'VIP 生效中')
|
|
game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 error full')
|
|
self.assertEqual(self.chat[-1],'VIP 剩余时间不能超过 400 天');self.assertIn('400',w.note.text)
|
|
self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'other'),7)
|
|
entry.event();self.assertFalse(w.IsShow())
|
|
self.time+=2;entry.event();self.time+=3.1;w.OnUpdate()
|
|
self.assertEqual(w.note.text,'!服务器尚未启用 VIP 卡')
|
|
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_premium.h'
|
|
if not server.exists():self.skipTest('server checkout not installed')
|
|
text=server.read_text(encoding='latin1')
|
|
self.assertIn('kPortVipMaxDays=400',text);self.assertEqual(self.ns['MAX_DAYS'],400)
|
|
self.assertIn('kPortVipGapMs=1000',text);self.assertEqual(self.ns['SEND_GAP'],1.)
|
|
cards={int(v):int(d) for v,d in re.findall(r'\{(\d+),(\d+)\}',text.split('kPortVipCards',1)[1].split(';',1)[0])}
|
|
self.assertEqual(cards,self.ns['CARDS'])
|
|
self.assertEqual(text.count('_expire"'),len(self.ns['SERVICES']))
|
|
self.assertTrue(set(re.findall(r'MTVIP1 error ([a-z]+)',text))<=set(self.ns['ERRORS']))
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|