Files
metin2-client/tests/wiki/test_vip.py
T
shenleiandClaude Opus 5.5 5dcac9cbd4 Add VIP cards and the VIP panel (mt_vip)
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>
2026-10-16 02:13:15 +09:00

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()