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 class Service(object): def __init__(self):self.requests=[] def Request(self,kind,subject,offset,callback):self.requests.append((kind,subject,offset,callback));return len(self.requests) def Reply(self,rows,time=1700000000,error=None): callback=self.requests[-1][3] callback(None if error else dict(kind='bosses',subject=0,offset=0,total=len(rows),mode=0,time=time,rows=rows),error) TRENT=[2307,68,51200,-300,1,0,1,1700000100,9000] SKIPIA=[1192,72,0,0,1,1,1,1700028800,28800] DESERT=[2191,999,10000,20000,99,0,1,0,1800] class BossTests(unittest.TestCase): def setUp(self): self.time=100.;self.chat=[] mods={n:types.ModuleType(n) for n in ('app','chat','game','grp','nonplayer','ui','wndMgr','localeInfo','mt_feature_service','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['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None)) grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None mods['nonplayer'].GetMonsterName=lambda vnum:{2307:'TreeKing',1192:'Skipia',2191:'Turtle'}.get(vnum,'') mods['ui'].Window=Widget mods['wndMgr'].GetScreenWidth=lambda:1280;mods['wndMgr'].GetScreenHeight=lambda:800 mods['localeInfo'].MINIMAP_ZONE_NAME_DICT={'metin2_map_trent02':'RedWood'} self.service=Service();mods['mt_feature_service'].g_service=self.service mods['mt_biolog'].Action=Action;mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:Widget();mods['mt_biolog'].L=lambda t:t self.previous={n:sys.modules.get(n) for n in mods} sys.modules.update(mods) self.ns={'__name__':'mt_boss'};exec(script('BossScript.inc'),self.ns) self.game=sys.modules['game'];self.gw=self.game.GameWindow() self.gw.interface=types.SimpleNamespace(wndMiniMap=Widget()) 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 open(self): self.game.GameWindow.Open(self.gw);w=self.ns['g_window'];w.Open();return w def test_window_lists_server_rows(self): w=self.open();self.assertEqual(self.ns['g_entry'].pos,(4,356)) self.assertEqual(self.service.requests[-1][:3],('bosses',0,0));self.assertFalse(w.refreshButton.enabled) self.service.Reply([TRENT,SKIPIA,DESERT]);self.assertTrue(w.refreshButton.enabled) self.assertEqual(w.lines[0].text,'[CH1] TreeKing RedWood') self.assertEqual(w.details[0].text,'刷新倒计时 00:01:40 · 坐标 (512, -3) · 周期 02:30:00') self.assertIn('存活 1/1',w.details[1].text);self.assertIn('随机位置',w.details[1].text) self.assertEqual(w.lines[2].text,'[共享] Turtle 地图999');self.assertIn('不再刷新',w.details[2].text) self.assertFalse(w.remindButtons[1].enabled) # alive self.assertFalse(w.remindButtons[2].enabled) # no timer self.assertFalse(w.lines[3].shown);self.assertIn('共 3 个',w.status.text) self.time+=40;w.Render();self.assertIn('00:01:00',w.details[0].text) # counts down from the server time def test_pages(self): rows=[[2000+i,68,0,0,1,0,1,1700000100,600] for i in range(11)] w=self.open();self.service.Reply(rows) self.assertFalse(w.prevButton.enabled);self.assertTrue(w.nextButton.enabled) w.nextButton.event();self.assertTrue(w.lines[2].shown);self.assertFalse(w.lines[3].shown);self.assertIn('第 2/2 页',w.status.text) self.assertFalse(w.nextButton.enabled) def test_reminder_fires_only_on_confirmed_spawn(self): w=self.open();self.service.Reply([TRENT,SKIPIA]);w.remindButtons[0].event() self.assertIn('开',w.remindButtons[0].text);w.Hide() tick=self.ns['g_tick'];count=len(self.service.requests) self.time+=50;tick.OnUpdate();self.assertEqual(len(self.service.requests),count) # not due yet self.time+=60;tick.OnUpdate();self.assertEqual(len(self.service.requests),count+1) self.service.Reply([TRENT,SKIPIA],time=1700000110);self.assertEqual(self.chat,[]) # timer passed, not spawned self.assertIn(('2307',),[(str(k[0]),) for k in self.ns['g_tracker'].reminders]) spawned=list(TRENT);spawned[5]=1;spawned[7]=1700009110 self.time+=40;tick.OnUpdate();self.service.Reply([spawned,SKIPIA],time=1700000150) self.assertEqual(self.chat,['TreeKing 已在 RedWood 刷新']);self.assertEqual(self.ns['g_tracker'].reminders,set()) def test_errors_and_close(self): w=self.open();self.service.Reply([],error='unsupported');self.assertIn('未启用',w.status.text) w.Query();self.service.Reply([]);self.assertIn('没有定时刷新',w.status.text) w.Query();self.service.Reply([TRENT]);w.remindButtons[0].event() self.game.GameWindow.Close(self.gw) self.assertIsNone(self.ns['g_window']);self.assertIsNone(self.ns['g_entry']) self.assertEqual(self.ns['g_tracker'].reminders,set());self.assertIsNone(self.ns['g_tracker'].rows) def test_maps_match_server_index(self): maps=self.ns['MAPS'] self.assertEqual((maps[68],maps[72],maps[302]),('metin2_map_trent02','metin2_map_skipia_dungeon_01','metin2_map_dawnmistwood')) if __name__=='__main__':unittest.main()