Port of AVA2's CRenderTarget / GrpRenderTargetTexture / renderTarget module. The host gains an RGBA8 offscreen colour pass for D3D colour render targets; mt_preview shows wearable items on every allowed race and monsters with motions, reached by X+click in the inventory and shop and from the wiki. Player races are created as TYPE_PC so their parts exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
392 lines
20 KiB
C++
392 lines
20 KiB
C++
// PORT: AVA2-inspired encyclopedia, adapted to current server templates and touch controls.
|
||
// Static templates plus negotiated read-only server views; no gameplay mutation or loot RNG here.
|
||
static const char c_szWikiScript[] = R"PY(# -*- coding: utf-8 -*-
|
||
import app
|
||
import game
|
||
import grp
|
||
import item
|
||
import pack
|
||
import player
|
||
import ui
|
||
import wndMgr
|
||
import weakref
|
||
import mt_feature_service
|
||
from mt_biolog import Action, Text, L
|
||
from mt_wiki_data import Catalogue
|
||
|
||
PATH='wiki/catalogue.txt'
|
||
g_window=None
|
||
g_entry=None
|
||
def ServerCapabilities():
|
||
mask=mt_feature_service.g_service.mask if mt_feature_service.g_service else None
|
||
return {'schema':1,'static_catalogue':True,'model_preview':False,
|
||
'live_drops':None if mask is None else bool(mask&1),
|
||
'chest_preview':None if mask is None else bool(mask&2),
|
||
'attributes':None if mask is None else bool(mask&4),
|
||
'sources':None if mask is None else bool(mask&8),
|
||
'dungeons':None if mask is None else bool(mask&16),
|
||
'calendar':None if mask is None else bool(mask&32)}
|
||
|
||
|
||
def U(text):
|
||
return text if isinstance(text,unicode) else text.decode('utf-8')
|
||
|
||
|
||
def B(text, limit=0):
|
||
text=U(text)
|
||
if limit and len(text)>limit:text=text[:limit-1]+u'…'
|
||
return text.encode('utf-8')
|
||
|
||
|
||
KINDS=(('', '全部物品'),('ITEM_WEAPON','武器'),('ITEM_ARMOR','防具首饰'),('ITEM_MATERIAL','材料'),
|
||
('ITEM_QUEST','任务物品'),('ITEM_USE','消耗品'),('ITEM_GIFTBOX','宝箱'),('ITEM_METIN','宝石'))
|
||
MOBKINDS=(('', '全部怪物'),('MONSTER','怪物'),('STONE','矿石'))
|
||
TYPE_NAMES=dict(KINDS)
|
||
TYPE_NAMES.update({'ITEM_NONE':'特殊道具','ITEM_UNIQUE':'特殊装备','ITEM_TOOL':'工具','ITEM_FISH':'鱼类','ITEM_ROD':'鱼竿','ITEM_PICK':'采矿镐','ITEM_RESOURCE':'资源','ITEM_SKILLBOOK':'技能书','ITEM_SKILLFORGET':'技能忘却书','ITEM_COSTUME':'时装','ITEM_SPECIAL':'特殊道具'})
|
||
RANK_NAMES={'PAWN':'普通','S_PAWN':'弱小','KNIGHT':'精英','S_KNIGHT':'强力','BOSS':'首领','KING':'王者'}
|
||
|
||
|
||
class WikiWindow(ui.Window):
|
||
def __init__(self):
|
||
ui.Window.__init__(self)
|
||
self.SetWindowName('WikiWindow')
|
||
self.AddFlag('float')
|
||
self.AddFlag('movable')
|
||
self.mode='items';self.category=0;self.page=0;self.detailPage=0
|
||
self.catalogue=None;self.error='';self.selected=None;self.results=[];self.links=[]
|
||
self.dimensions=None;self.lastQuery=None;self.history=[]
|
||
self.remote=None;self.remoteData=None;self.remoteError=None;self.remoteSerial=0
|
||
self.heading=Text(self,18,18,'游戏百科',0xffffd68b)
|
||
self.detailsButton=Action(self,130,6,120,'详细属性',self.OpenDetails)
|
||
self.dungeonButton=Action(self,258,6,96,'副本信息',self.OpenDungeons)
|
||
self.calendarButton=Action(self,362,6,80,'活动日历',self.OpenCalendar)
|
||
self.closeButton=Action(self,0,6,52,'关闭',self.Hide)
|
||
self.backButton=Action(self,0,6,72,'返回',self.Back)
|
||
self.searchBox=ui.SlotBar();self.searchBox.SetParent(self);self.searchBox.SetPosition(18,54)
|
||
self.searchBox.SetSize(320,28);self.searchBox.Show()
|
||
self.search=ui.EditLine();self.search.SetParent(self.searchBox);self.search.SetPosition(8,6)
|
||
self.search.SetSize(302,20);self.search.SetMax(80);self.search.SetReturnEvent(self.Search)
|
||
self.search.SetEscapeEvent(self.Hide);self.search.Show()
|
||
self.searchButton=Action(self,346,50,72,'搜索',self.Search)
|
||
self.itemTab=Action(self,18,100,96,'物品',lambda:self.SetMode('items'))
|
||
self.mobTab=Action(self,122,100,96,'怪物',lambda:self.SetMode('mobs'))
|
||
self.categoryButton=Action(self,226,100,164,'全部物品',self.NextCategory)
|
||
self.status=Text(self,18,85,'输入名称或 ID 搜索',0xffc3b7a1)
|
||
self.title=Text(self,0,112,'选择左侧条目',0xffffd68b)
|
||
self.icon=ui.ExpandedImageBox();self.icon.SetParent(self);self.icon.AddFlag('not_pick')
|
||
self.listButtons=[Action(self,0,0,100,'',lambda i=i:self.SelectResult(i)) for i in range(8)]
|
||
self.detailButtons=[Action(self,0,0,100,'',lambda i=i:self.Follow(i)) for i in range(8)]
|
||
self.prev=Action(self,18,0,72,'上一页',lambda:self.Page(-1))
|
||
self.next=Action(self,0,0,72,'下一页',lambda:self.Page(1))
|
||
self.pageText=Text(self,110,0)
|
||
self.dprev=Action(self,0,0,72,'详情上页',lambda:self.DetailPage(-1))
|
||
self.dnext=Action(self,0,0,72,'详情下页',lambda:self.DetailPage(1))
|
||
self.revision=Text(self,430,66,'未加载资料',0xffaaa08b)
|
||
self.Reflow()
|
||
self.Hide()
|
||
|
||
def OpenCalendar(self):
|
||
import mt_calendar
|
||
mt_calendar.Toggle()
|
||
|
||
def OpenDungeons(self):
|
||
import mt_dungeons
|
||
mt_dungeons.Toggle()
|
||
|
||
def OpenDetails(self):
|
||
import mt_details
|
||
mt_details.Toggle()
|
||
|
||
def Load(self):
|
||
if self.catalogue:return True
|
||
try:
|
||
if not pack.Exist(PATH):raise ValueError('missing catalogue')
|
||
self.catalogue=Catalogue(pack.Get(PATH))
|
||
self.revision.SetText(L(B(u'资料版本 '+self.catalogue.version[:12])))
|
||
self.error=''
|
||
return True
|
||
except (ValueError,TypeError,UnicodeError,RuntimeError,IOError):
|
||
self.error='百科资料缺失或版本不支持,请更新资源包。'
|
||
self.status.SetText(L(B(self.error,24)))
|
||
return False
|
||
|
||
def Open(self,vnum=0):
|
||
self.Reflow();self.SetCenterPosition();self.Show();self.SetTop()
|
||
if not self.Load():return
|
||
self.Search()
|
||
if vnum:
|
||
row=self.catalogue.FindItem(int(vnum))
|
||
if row:self.Select('items',row)
|
||
else:self.status.SetText(L(B('当前资料未收录此物品。')))
|
||
|
||
def Query(self):
|
||
value=self.search.GetText()
|
||
return player.FeatureLocaleToUTF8(value).decode('utf-8').strip()
|
||
|
||
def Search(self):
|
||
self.page=0;self.lastQuery=self.Query();self.Refresh()
|
||
|
||
def SetMode(self,mode):
|
||
self.mode=mode;self.category=0;self.page=0;self.Refresh()
|
||
|
||
def NextCategory(self):
|
||
choices=KINDS if self.mode=='items' else MOBKINDS
|
||
self.category=(self.category+1)%len(choices);self.page=0;self.Refresh()
|
||
|
||
def Page(self,step):
|
||
self.page+=step;self.Refresh()
|
||
|
||
def DetailPage(self,step):
|
||
if self.remote and self.remoteData:
|
||
pages=max(1,(len(self.links)+self.rowCount-1)//self.rowCount)
|
||
kind,subject,offset=self.remote
|
||
if step>0 and self.detailPage+1>=pages and offset+len(self.remoteData['rows'])<self.remoteData['total']:
|
||
self.QueryRemote(kind,subject,offset+8);return
|
||
if step<0 and self.detailPage==0 and offset>0:
|
||
self.QueryRemote(kind,subject,max(0,offset-8));return
|
||
self.detailPage+=step;self.RefreshDetail()
|
||
|
||
def Refresh(self):
|
||
for button in self.listButtons:button.Hide()
|
||
if not self.catalogue:return
|
||
choices=KINDS if self.mode=='items' else MOBKINDS
|
||
kind,label=choices[self.category]
|
||
self.categoryButton.SetText(label)
|
||
self.results,count,self.page,pages=self.catalogue.Search(self.mode,self.Query(),kind,self.page,self.rowCount)
|
||
for i,row in enumerate(self.results):
|
||
self.listButtons[i].SetText(B(row['name'],max(9,(self.split-130)//12))+' #%d'%row['id']);self.listButtons[i].Show()
|
||
self.status.SetText(L(B('%d 项;模板资料不保证可获取。'%count)))
|
||
self.pageText.SetText(L(B('%d / %d'%(self.page+1,pages))))
|
||
self.prev.SetEnabled(self.page>0);self.next.SetEnabled(self.page+1<pages)
|
||
|
||
def SelectResult(self,index):
|
||
if index<len(self.results):self.Select(self.mode,self.results[index])
|
||
|
||
def Select(self,mode,row,remember=True):
|
||
if remember and self.selected:self.history=(self.history+[self.selected])[-24:]
|
||
self.remote=None;self.remoteData=None;self.remoteSerial+=1
|
||
self.selected=(mode,row['id']);self.detailPage=0;self.RefreshDetail()
|
||
|
||
def Back(self):
|
||
if self.remote:
|
||
self.remote=None;self.remoteData=None;self.remoteSerial+=1;self.detailPage=0;self.RefreshDetail();return
|
||
if self.history:
|
||
self.selected=self.history.pop();self.detailPage=0;self.RefreshDetail()
|
||
|
||
def Follow(self,index):
|
||
offset=self.detailPage*self.rowCount+index
|
||
if offset<len(self.links) and self.links[offset][1]:
|
||
vnum=self.links[offset][1]
|
||
if isinstance(vnum,tuple):
|
||
if vnum[0]=='query':self.QueryRemote(vnum[1],vnum[2],0)
|
||
elif vnum[0]=='mobs':
|
||
row=self.catalogue.mobs.get(vnum[1])
|
||
if row:self.Select('mobs',row)
|
||
else:self.QueryRemote('mob',vnum[1],0)
|
||
elif vnum[0]=='items':self.Open(vnum[1])
|
||
elif vnum[0]=='preview':
|
||
import mt_preview
|
||
mt_preview.Preview(vnum[1],vnum[2])
|
||
return
|
||
row=self.catalogue.FindItem(vnum)
|
||
if row:self.Select('items',row)
|
||
else:self.status.SetText(L(B('目标物品未收录:#%d'%vnum)))
|
||
|
||
def RefreshDetail(self):
|
||
for b in self.detailButtons:b.Hide()
|
||
self.links=[];self.icon.Hide()
|
||
self.backButton.SetEnabled(bool(self.history) or self.remote is not None)
|
||
if not self.selected or not self.catalogue:
|
||
self.dprev.SetEnabled(False);self.dnext.SetEnabled(False)
|
||
return
|
||
mode,vnum=self.selected
|
||
row=(self.catalogue.items if mode=='items' else self.catalogue.mobs)[vnum]
|
||
self.title.SetText(L(B(row['name'],max(8,(self.split-150)//12))+' #%d'%vnum))
|
||
lines=[]
|
||
if mode=='items':
|
||
lines.append((u'类型:'+U(TYPE_NAMES.get(row['kind'],'其他物品')),0))
|
||
lines.append((u'最低等级:%d 占格:%d'%(row['level'],row['size']),0))
|
||
if row['end']!=vnum:lines.append((u'模板 ID 范围:%d—%d'%(vnum,row['end']),0))
|
||
try:
|
||
path=item.FeatureGetIconPath(vnum)
|
||
if path:self.icon.LoadImage(path);self.icon.Show()
|
||
except (RuntimeError,ValueError):pass
|
||
target=row['refined'];recipe=self.catalogue.recipes.get(row['recipe'])
|
||
if target:
|
||
name=self.catalogue.FindItem(target)
|
||
lines.append((u'升级 → '+(name['name'] if name else u'#%d'%target),target if name else 0))
|
||
if recipe:
|
||
lines.append((u'基础配方:%d 金币 / %d%%'%(recipe['cost'],recipe['probability']),0))
|
||
for mat,count in recipe['materials']:
|
||
ingredient=self.catalogue.FindItem(mat)
|
||
lines.append((u'材料:'+(ingredient['name'] if ingredient else u'#%d'%mat)+u' ×%d'%count,mat if ingredient else 0))
|
||
lines.append((u'实际费用/概率以精炼窗口为准',0))
|
||
else:lines.append((u'升级配方尚未收录',0))
|
||
else:lines.append((u'没有配置下一阶升级',0))
|
||
import mt_biolog
|
||
for stage in sorted(mt_biolog.CATALOG):
|
||
bio=mt_biolog.CATALOG[stage]
|
||
if vnum in (bio['material'],bio['stone']):
|
||
lines.append((u'生物学家:%d 级研究'%bio['level'],0))
|
||
if vnum==bio['material']:lines.append((u'阶段所需:%d 个'%bio['count'],0))
|
||
offer=next((o for o in mt_biolog.SHOP if o['vnum']==vnum),None)
|
||
if offer:
|
||
if offer['gold']:lines.append((u'研究商店:%d 金币'%offer['gold'],0))
|
||
else:
|
||
cost=self.catalogue.FindItem(offer['cost'])
|
||
lines.append((u'研究兑换:'+(cost['name'] if cost else u'#%d'%offer['cost'])+u' ×%d'%offer['amount'],offer['cost'] if cost else 0))
|
||
if row['kind'] in ('ITEM_WEAPON','ITEM_ARMOR','ITEM_COSTUME'):
|
||
import mt_preview
|
||
if mt_preview.Previewable('item',vnum):lines.append((u'模型预览',('preview','item',vnum)))
|
||
lines.append((u'查看获取来源',('query','sources',vnum)))
|
||
if row['kind'] in ('ITEM_GIFTBOX','ITEM_TREASURE_BOX'):lines.append((u'宝箱奖励预览',('query','chest',vnum)))
|
||
else:
|
||
lines=[(u'类型:'+U('矿石' if row['kind']=='STONE' else '怪物')+u' / '+U(RANK_NAMES.get(row['rank'],'普通')),0),(u'等级:%d 生命:%d'%(row['level'],row['hp']),0),
|
||
(u'基础攻击:%d—%d'%(row['attack_min'],row['attack_max']),0),
|
||
(u'基础防御:%d 经验:%d'%(row['defense'],row['exp']),0),(u'模型预览',('preview','mob',vnum)),(u'查看服务端掉落列表',('query','mob',vnum))]
|
||
if self.remote:lines=self.RemoteLines()
|
||
self.links=lines
|
||
pages=max(1,(len(lines)+self.rowCount-1)//self.rowCount)
|
||
self.detailPage=max(0,min(self.detailPage,pages-1))
|
||
for i,(label,link) in enumerate(lines[self.detailPage*self.rowCount:(self.detailPage+1)*self.rowCount]):
|
||
self.detailButtons[i].SetText(B(label,max(15,(self.split-40)//12)));self.detailButtons[i].SetEnabled(bool(link));self.detailButtons[i].label.SetPackedFontColor(0xffefdbac if link else 0xffc8bea8);self.detailButtons[i].Show()
|
||
before=bool(self.remoteData and self.remote and self.remote[2]>0)
|
||
after=bool(self.remoteData and self.remote and self.remote[2]+len(self.remoteData['rows'])<self.remoteData['total'])
|
||
self.dprev.SetEnabled(self.detailPage>0 or before);self.dnext.SetEnabled(self.detailPage+1<pages or after)
|
||
|
||
def QueryRemote(self,kind,subject,offset=0):
|
||
self.remote=(kind,subject,offset);self.remoteData=None;self.remoteError=None;self.detailPage=0;self.remoteSerial+=1
|
||
serial=self.remoteSerial
|
||
self.RefreshDetail()
|
||
def received(data,error):
|
||
if self.remoteSerial!=serial or self.remote!=(kind,subject,offset):return
|
||
self.remoteData=data;self.remoteError=error;self.RefreshDetail()
|
||
if mt_feature_service.g_service:mt_feature_service.g_service.Request(kind,subject,offset,received)
|
||
else:received(None,'unsupported')
|
||
|
||
def RemoteLines(self):
|
||
if self.remoteError:
|
||
text={'unsupported':'服务端尚未启用此查询','unsupported_chest':'此道具未接入通用宝箱预览',
|
||
'missing':'服务端不存在此目标','timeout':'查询超时,请返回后重试','bad_reply':'查询数据异常,请更新客户端',
|
||
'rate':'操作过快,请稍后重试','too_many':'来源过多,请按怪物分别查询'}.get(self.remoteError,'查询失败,请返回后重试')
|
||
return [(U(text),0)]
|
||
if self.remoteData is None:return [(U('正在向服务器查询…'),0)]
|
||
data=self.remoteData;kind,subject,offset=self.remote
|
||
lines=[]
|
||
if kind=='chest':
|
||
lines.append((U('各项独立判定,可能多项或无奖励' if data['mode']==11 else '按权重随机抽取一项'),0))
|
||
lines.append((U('仅预览;条件、钥匙与消耗仍由服务器校验'),0))
|
||
else:
|
||
lines.append((U('已加载常规掉落;任务/活动/矿石额外掉落未全覆盖'),0))
|
||
lines.append((U('列出候选及限制,不表示最终中奖率'),0))
|
||
for vnum,count,rule,low,high,weight,total,rare in data['rows']:
|
||
target=0
|
||
if kind=='chest' and vnum<=7:
|
||
label={1:U('金币'),2:U('经验'),3:U('召唤怪物'),4:U('减速效果'),5:U('生命损失'),6:U('中毒效果'),7:U('召唤怪物组')}.get(vnum,U('特殊效果'))
|
||
label+=u' %d'%count
|
||
elif kind=='sources' and rule<20:
|
||
row=self.catalogue.mobs.get(vnum);label=row['name'] if row else u'怪物 #%d'%vnum
|
||
target=('mobs',vnum);label+=u' ×%d'%count
|
||
else:
|
||
row=self.catalogue.FindItem(vnum);label=row['name'] if row else u'物品 #%d'%vnum
|
||
target=('items',vnum)
|
||
if kind!='sources':label+=u' ×%d'%count
|
||
lines.append((label,target))
|
||
if kind=='chest':
|
||
detail=(u'独立判定:%d/100'%min(weight,100)) if rule==11 else (u'权重:%d / %d'%(weight,total))
|
||
if rare:detail+=u' 可能附带随机属性'
|
||
elif rule>=20:detail=U('宝箱来源;点击可继续查看预览')
|
||
elif rule==0:detail=u'通用掉落;角色等级 %d—%d'%(low,high)
|
||
elif rule==2:detail=u'击杀组候选权重 %d/%d'%(weight,total)
|
||
elif rule==3:detail=u'等级组;角色至少 %d 级'%low
|
||
elif rule==4:detail=U('需掉宝手套或对应增益资格')
|
||
elif rule==5:detail=U('怪物模板附加掉落')
|
||
else:detail=U('怪物独立掉落组')
|
||
lines.append((detail,0))
|
||
if not data['rows']:lines.append((U('当前查询范围内无配置;不代表所有途径均不存在'),0))
|
||
lines.append((U('第 %d—%d 条,共 %d 条')%(offset+1 if data['total'] else 0,offset+len(data['rows']),data['total']),0))
|
||
return lines
|
||
|
||
def Reflow(self):
|
||
dims=(wndMgr.GetScreenWidth(),wndMgr.GetScreenHeight())
|
||
if dims==self.dimensions:return
|
||
self.dimensions=dims
|
||
width=min(760,max(600,dims[0]-16));height=min(480,max(300,dims[1]-16))
|
||
self.SetSize(width,height);self.split=width//2
|
||
self.categoryButton.SetSize(self.split-234,48);self.categoryButton.label.SetPosition((self.split-234)//2,24)
|
||
self.rowCount=min(8,max(2,(height-216)//48))
|
||
self.closeButton.SetPosition(width-70,6);self.backButton.SetPosition(width-150,6)
|
||
self.title.SetPosition(self.split+48,122);self.icon.SetPosition(self.split+8,112)
|
||
for i,b in enumerate(self.listButtons):
|
||
b.SetPosition(18,154+i*48);b.SetSize(self.split-30,44);b.label.SetPosition((self.split-30)//2,22)
|
||
for i,b in enumerate(self.detailButtons):
|
||
b.SetPosition(self.split+8,154+i*48);b.SetSize(self.split-26,44);b.label.SetPosition((self.split-26)//2,22)
|
||
self.prev.SetPosition(18,height-54);self.next.SetPosition(self.split-90,height-54);self.pageText.SetPosition(112,height-30)
|
||
self.dprev.SetPosition(self.split+8,height-54);self.dnext.SetPosition(width-90,height-54)
|
||
self.SetCenterPosition();self.Refresh();self.RefreshDetail()
|
||
|
||
def OnUpdate(self):
|
||
if self.IsShow():self.Reflow()
|
||
|
||
def OnRender(self):
|
||
x,y=self.GetGlobalPosition();grp.SetColor(0xf51b1916);grp.RenderBar(x,y,self.GetWidth(),self.GetHeight())
|
||
grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,self.GetWidth(),self.GetHeight())
|
||
grp.RenderLine(x+self.split,y+108,0,self.GetHeight()-170)
|
||
|
||
def OnMouseWheel(self,delta):
|
||
x,y=self.GetGlobalPosition();mx,my=wndMgr.GetMousePosition()
|
||
if mx>=x+self.split:self.DetailPage(-1 if delta>0 else 1)
|
||
else:self.Page(-1 if delta>0 else 1)
|
||
return True
|
||
|
||
def Hide(self):
|
||
if hasattr(self,'search'):self.search.KillFocus()
|
||
ui.Window.Hide(self)
|
||
|
||
def OnPressEscapeKey(self):
|
||
self.Hide();return True
|
||
|
||
def Destroy(self):
|
||
self.Hide();self.search.SetReturnEvent(None);self.search.SetEscapeEvent(None)
|
||
for b in self.listButtons+self.detailButtons+[self.detailsButton,self.dungeonButton,self.calendarButton,self.closeButton,self.backButton,self.searchButton,self.itemTab,self.mobTab,self.categoryButton,self.prev,self.next,self.dprev,self.dnext]:b.event=None
|
||
self.remoteSerial+=1;self.catalogue=None
|
||
|
||
|
||
class Entry(Action):
|
||
def __init__(self,window):
|
||
Action.__init__(self,None,0,0,64,'百科',Toggle)
|
||
self.minimap=weakref.proxy(window.interface.wndMiniMap);self.SetWindowName('WikiEntry');self.OnUpdate()
|
||
|
||
def OnUpdate(self):
|
||
x,y=self.minimap.GetGlobalPosition();self.SetPosition(max(4,x-68),y+96)
|
||
|
||
|
||
def Toggle():
|
||
if g_window:
|
||
if g_window.IsShow():g_window.Hide()
|
||
else:g_window.Open()
|
||
|
||
|
||
def OpenItem(vnum):
|
||
if g_window:g_window.Open(vnum)
|
||
|
||
|
||
_open=game.GameWindow.Open
|
||
_close=game.GameWindow.Close
|
||
|
||
def Open(self):
|
||
global g_window,g_entry
|
||
_open(self);g_window=WikiWindow();g_entry=Entry(self)
|
||
|
||
|
||
def Close(self):
|
||
global g_window,g_entry
|
||
if g_entry:g_entry.Hide();g_entry.event=None;g_entry=None
|
||
if g_window:g_window.Destroy();g_window=None
|
||
_close(self)
|
||
|
||
|
||
game.GameWindow.Open=Open
|
||
game.GameWindow.Close=Close
|
||
)PY";
|