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.Hide() # like ui.Window, before the subclass sets anything self.shown=False;self.text='';self.pos=(0,0);self.size=(0,0);self.enabled=True 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.text=text;self.shown=True def SetEnabled(self,enabled):self.enabled=enabled # vnum -> (type, subtype, antiflags) ITEMS={11299:(1,0,0),11499:(1,0,0x08|0x10|0x20),12049:(2,0,0x04|0x10|0x20|0x40),8000:(1,6,0), 41001:(28,0,0x01),45001:(28,1,0x02),27001:(3,0,0),11259:(2,1,0)} class Targets(object): """renderTarget stub: what exists, and every call in order.""" def __init__(self):self.live=set();self.calls=[];self.models={};self.missing=set();self.motionless=set() def __getattr__(self,name): def call(*a):self.calls.append((name,)+a) return call def Create(self,i,w,h):self.calls.append(('Create',i,w,h));self.live.add(i);return 1 def Remove(self,i):self.calls.append(('Remove',i));self.live.discard(i);self.models.pop(i,None) def Destroy(self):self.calls.append(('Destroy',));self.live.clear();self.models.clear() def SelectModel(self,i,race): self.calls.append(('SelectModel',i,race)) if race in self.missing:return 0 self.models[i]=race;return 1 def SetMotionIndex(self,i,m):self.calls.append(('SetMotionIndex',i,m));return 0 if m in self.motionless else 1 def named(self,name):return [c for c in self.calls if c[0]==name] class PreviewTests(unittest.TestCase): def setUp(self): self.chat=[];self.keys=set();self.mouse=(0,0);self.race=0;self.selected=None self.rt=Targets() names=('app','chat','game','grp','item','player','renderTarget','ui','wndMgr','mt_biolog','uiInventory','uiShop','shop') mods={n:types.ModuleType(n) for n in names} mods['renderTarget']=self.rt app=mods['app'];app.DIK_X=45;app.IsPressed=lambda k:k in self.keys 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 it=mods['item'] for k,v in dict(ITEM_TYPE_WEAPON=1,ITEM_TYPE_ARMOR=2,ITEM_TYPE_COSTUME=28,WEAPON_ARROW=6,ARMOR_BODY=0, COSTUME_TYPE_BODY=0,COSTUME_TYPE_HAIR=1,ITEM_ANTIFLAG_FEMALE=0x01,ITEM_ANTIFLAG_MALE=0x02, ITEM_ANTIFLAG_WARRIOR=0x04,ITEM_ANTIFLAG_ASSASSIN=0x08,ITEM_ANTIFLAG_SURA=0x10, ITEM_ANTIFLAG_SHAMAN=0x20).items():setattr(it,k,v) def select(v):self.selected=ITEMS.get(v,ITEMS[27001]) it.SelectItem=select;it.GetItemType=lambda:self.selected[0];it.GetItemSubType=lambda:self.selected[1] it.IsAntiFlag=lambda f:bool(self.selected[2]&f) mods['player'].GetRace=lambda:self.race self.bag={3:11299,4:27001} mods['player'].GetItemIndex=lambda cell:self.bag.get(cell,0) mods['ui'].Window=Widget mods['wndMgr'].GetMousePosition=lambda:self.mouse 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) self.inventoryClicks=[];self.shopClicks=[] mods['uiInventory'].InventoryWindow=type('InventoryWindow',(),dict( SelectItemSlot=lambda s,slot:self.inventoryClicks.append(slot), _InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos=lambda s,slot:slot+0)) mods['uiShop'].ShopDialog=type('ShopDialog',(),dict(SelectItemSlot=lambda s,slot:self.shopClicks.append(slot), _ShopDialog__GetRealIndex=lambda s,slot:40+slot)) mods['shop'].GetItemID=lambda i:{41:12049,42:27001}.get(i,0) self.previous={n:sys.modules.get(n) for n in mods} sys.modules.update(mods) self.ns={'__name__':'mt_preview'};exec(script('ModelPreviewScript.inc'),self.ns) self.game=mods['game'];self.gw=self.game.GameWindow();self.game.GameWindow.Open(self.gw) self.w=self.ns['g_window'] def text(self,t): w=Widget();w.text=t;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_item_spec(self): spec=self.ns['ItemSpec'] self.assertEqual(spec(11299),('weapon',[0,4,5,1,2,6,7,3])) self.assertEqual(spec(11499),('weapon',[0,4])) # warrior only self.assertEqual(spec(12049),('armor',[5,1])) # assassin only self.assertEqual(spec(41001),('armor',[0,5,2,7])) # anti-female costume self.assertEqual(spec(45001),('hair',[4,1,6,3])) for vnum in (8000,27001,11259):self.assertIsNone(spec(vnum)) # arrow, potion, helmet self.assertTrue(self.ns['Previewable']('mob',101));self.assertFalse(self.ns['Previewable']('item',27001)) def test_item_preview_races_and_reset(self): self.race=2 self.assertTrue(self.ns['Preview']('item',11299));self.assertTrue(self.w.IsShow()) self.assertEqual(self.rt.live,{1});self.assertIn(('Create',1,300,330),self.rt.calls) self.assertEqual(self.rt.models[1],2);self.assertEqual(self.rt.named('SetWeapon')[-1],('SetWeapon',1,11299)) self.assertEqual(self.w.raceName.text,'修罗(男)');self.assertEqual(self.rt.named('Zoom'),[('Zoom',1,1)]*2) self.w.nextRace.event();self.assertEqual(self.rt.models[1],6);self.assertEqual(self.w.raceName.text,'修罗(女)') self.w.prevRace.event();self.w.prevRace.event();self.assertEqual(self.rt.models[1],1) # The player's own race is not allowed: the first allowed race is shown, and race buttons need two races. self.ns['Preview']('item',41001);self.assertEqual(self.rt.models[1],2) self.assertEqual(self.rt.named('SetArmor')[-1],('SetArmor',1,41001)) self.ns['Preview']('item',12049);self.assertEqual(self.rt.models[1],5) self.ns['Preview']('item',45001);self.assertEqual(self.rt.models[1],4) self.assertEqual(self.rt.named('SetHair')[-1],('SetHair',1,45001)) self.assertTrue(self.w.nextRace.enabled) self.ns['Preview']('item',11499);self.race=0;self.ns['Preview']('item',11499) self.assertEqual(self.rt.models[1],0) self.assertFalse(self.ns['Preview']('item',27001));self.assertEqual(self.chat,['该物品没有可预览的模型']) self.assertEqual(self.rt.models[1],0) # the window keeps what it showed def test_mob_motions_zoom_turn(self): self.rt.motionless.add(34) self.assertTrue(self.ns['Preview']('mob',101));self.assertEqual(self.rt.models[1],101) self.assertFalse(self.w.prevRace.IsShow());self.assertTrue(all(b.IsShow() for b in self.w.motions)) self.w.motions[1].event();self.assertEqual(self.rt.named('SetMotionIndex')[-1],('SetMotionIndex',1,13)) self.w.motions[5].event();self.assertEqual(self.w.note.text,'该怪物没有此动作') self.w.motions[0].event();self.assertEqual(self.w.note.text,'') zoom_in,zoom_out,spin,reset=[b.event for b in self.w.controls] for _ in range(20):zoom_in() self.assertEqual(self.w.zoom,-8);self.assertEqual(len(self.rt.named('Zoom')),8) for _ in range(30):zoom_out() self.assertEqual(self.w.zoom,12);self.assertEqual(len(self.rt.named('Zoom')),28) spin();self.assertEqual(self.rt.named('SetRotation')[-1],('SetRotation',1,True));self.assertEqual(self.w.controls[2].text,'停止旋转') # Drag: absolute yaw/pitch from where the press started, pitch clamped. view=self.w.view;self.mouse=(100,100);view.OnMouseLeftButtonDown() self.mouse=(160,1100);view.OnUpdate() self.assertEqual(self.rt.named('RotateEyeAroundTarget')[-1],('RotateEyeAroundTarget',1,60.,30.)) view.OnMouseLeftButtonUp();self.mouse=(0,0);view.OnUpdate() self.assertEqual(self.rt.named('RotateEyeAroundTarget')[-1],('RotateEyeAroundTarget',1,60.,30.)) reset();self.assertEqual((self.w.yaw,self.w.pitch,self.w.zoom),(0.,0.,0)) self.assertEqual(self.rt.named('SetRotation')[-1],('SetRotation',1,True)) # spin survives the reset self.rt.missing.add(999);self.ns['Preview']('mob',999);self.assertEqual(self.w.note.text,'该怪物没有模型') zoom_in();self.assertEqual(self.w.zoom,0) def test_view_draws_target(self): self.ns['Preview']('mob',101);self.w.view.pos=(20,70);self.w.view.OnRender() self.assertEqual(self.rt.named('RenderTexture')[-1],('RenderTexture',1,20,70,320,400)) def test_no_leak_on_reopen_and_close(self): for _ in range(5): self.ns['Preview']('item',11299);self.assertEqual(self.rt.live,{1}) self.w.closeButton.event();self.assertEqual(self.rt.live,set());self.assertFalse(self.w.IsShow()) self.ns['Preview']('mob',101);self.w.OnPressEscapeKey();self.assertEqual(self.rt.live,set()) self.ns['Preview']('mob',101) self.game.GameWindow.Close(self.gw) self.assertIsNone(self.ns['g_window']);self.assertEqual(self.rt.live,set());self.assertEqual(self.rt.calls[-1],('Destroy',)) self.assertFalse(self.ns['Preview']('mob',101)) def test_inventory_and_shop_entries(self): inv=sys.modules['uiInventory'].InventoryWindow() inv.SelectItemSlot(3);self.assertEqual(self.inventoryClicks,[3]);self.assertFalse(self.w.IsShow()) self.keys.add(45) inv.SelectItemSlot(3);self.assertEqual(self.inventoryClicks,[3]);self.assertTrue(self.w.IsShow()) inv.SelectItemSlot(4);inv.SelectItemSlot(9);self.assertEqual(self.inventoryClicks,[3,4,9]) # not wearable, empty dlg=sys.modules['uiShop'].ShopDialog() dlg.SelectItemSlot(1);self.assertEqual(self.shopClicks,[]);self.assertEqual(self.rt.named('SetArmor')[-1],('SetArmor',1,12049)) dlg.SelectItemSlot(2);self.assertEqual(self.shopClicks,[2]) self.keys.clear();dlg.SelectItemSlot(1);self.assertEqual(self.shopClicks,[2,1]) def test_wiki_links(self): wiki=(ROOT/'src/platform/ScriptLib/WikiScript.inc').read_text() self.assertIn("(u'模型预览',('preview','item',vnum))",wiki);self.assertIn("(u'模型预览',('preview','mob',vnum))",wiki) self.assertIn("mt_preview.Preview(vnum[1],vnum[2])",wiki) boot=(ROOT/'src/platform/ScriptLib/PythonBoot.cpp').read_text() self.assertLess(boot.index('install_script_module("mt_mobile_inventory"'),boot.index('install_script_module("mt_preview"')) self.assertLess(boot.index('install_script_module("mt_biolog"'),boot.index('install_script_module("mt_preview"')) if __name__=='__main__':unittest.main()