mt_channel lists the serverInfo channels of the current server and switches by running the original paths: /logout, then the login window connects to the chosen channel with the account kept by MainStream, and the original stream.isAutoSelect starts the same character. No server change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
241 lines
9.9 KiB
C++
241 lines
9.9 KiB
C++
// PORT: AVA2-style channel switch (ENABLE_CHANNEL_CHANGE), client-only. Instead of a server-side warp it runs
|
||
// the original paths in order: /logout (the server's 3 s / 10 s countdown, which also refuses while fighting),
|
||
// the disconnect brings back introLogin.LoginWindow, which then connects to the chosen channel of serverInfo
|
||
// with the account kept by networkModule.MainStream, and stream.isAutoSelect makes introSelect start the same
|
||
// character. See docs/AVA2-CHANNEL.md.
|
||
static const char c_szChannelScript[] = R"PY(# -*- coding: utf-8 -*-
|
||
import app
|
||
import chat
|
||
import game
|
||
import grp
|
||
import introLogin
|
||
import net
|
||
import ui
|
||
import weakref
|
||
from mt_biolog import Action, Text, L
|
||
from mt_wiki import B
|
||
|
||
try:
|
||
import serverInfo
|
||
except ImportError:
|
||
serverInfo=None
|
||
|
||
g_window=None
|
||
g_entry=None
|
||
CHANNEL_FILE='channel.inf' # the file introLogin's server board reads
|
||
LOGOUT_TIMEOUT=20. # countdown is 3 s, 10 s while fighting; nothing back means the server refused
|
||
LOGIN_TIMEOUT=30.
|
||
|
||
|
||
def Locate(addr,port):
|
||
# (region, server, channel) of serverInfo whose game address is addr:port; channel 0 when none matches.
|
||
regions=getattr(serverInfo,'REGION_DICT',{})
|
||
for region in sorted(regions):
|
||
for server in sorted(regions[region]):
|
||
for channel,data in sorted(regions[region][server].get('channel',{}).items()):
|
||
if data.get('ip')==addr and data.get('tcp_port')==port:return region,server,channel
|
||
try:
|
||
f=open(CHANNEL_FILE,'r');words=f.read().split();f.close()
|
||
server=int(words[0]);region=int(words[2]) if len(words)>2 else 0
|
||
if server in regions.get(region,{}):return region,server,0
|
||
except (IOError,OSError,ValueError,IndexError):pass
|
||
for region in sorted(regions):
|
||
for server in sorted(regions[region]):return region,server,0
|
||
return None
|
||
|
||
|
||
def Channels(place):
|
||
if not place:return []
|
||
region,server,current=place
|
||
data=serverInfo.REGION_DICT[region][server]
|
||
return [(channel,info) for channel,info in sorted(data.get('channel',{}).items())]
|
||
|
||
|
||
class Switcher(object):
|
||
# stage: None, 'logout' (waiting for the server to close the session), 'login' (connecting to the target).
|
||
def __init__(self,send,now,listener=None):
|
||
self.send=send;self.now=now;self.listener=listener
|
||
self.stage=None;self.deadline=0;self.target=None;self.account=None;self.error=None;self.stream=None
|
||
|
||
def Busy(self):return self.stage is not None
|
||
|
||
def Start(self,stream,place,channel):
|
||
if self.Busy():return False
|
||
region,server,current=place
|
||
info=serverInfo.REGION_DICT[region][server]['channel'].get(channel)
|
||
if not info or channel==current:self.error='channel';self.Notify('error');return False
|
||
if not getattr(stream,'id','') or not getattr(stream,'pwd',''):self.error='account';self.Notify('error');return False
|
||
auth=getattr(serverInfo,'REGION_AUTH_SERVER_DICT',{}).get(region,{}).get(server)
|
||
account=(auth['ip'],auth['port']) if auth else (stream.account_addr,stream.account_port)
|
||
self.target=dict(region=region,server=server,channel=channel,ip=info['ip'],port=info['tcp_port'],
|
||
account=account,label='%s, %s'%(serverInfo.REGION_DICT[region][server].get('name',''),info.get('name','')))
|
||
self.account=(stream.id,stream.pwd,stream.GetCharacterSlot());self.stream=stream
|
||
self.error=None;self.stage='logout';self.deadline=self.now()+LOGOUT_TIMEOUT
|
||
self.send('/logout');self.Notify('start')
|
||
return True
|
||
|
||
def Tick(self):
|
||
if self.stage and self.now()>=self.deadline:
|
||
self.error='timeout' if self.stage=='logout' else 'login';self.Clear();self.Notify('error')
|
||
|
||
def Clear(self):
|
||
if self.stream:self.stream.isAutoSelect=0 # never leave the original auto-select on for a manual login
|
||
self.stage=None;self.target=None;self.account=None;self.stream=None
|
||
|
||
def Notify(self,kind):
|
||
if self.listener:self.listener(kind)
|
||
|
||
def OnLoginWindow(self,window):
|
||
# LoginWindow.Open has just reset the connect info from loginInfo.xml; point it at the target channel.
|
||
if self.stage!='logout':return False
|
||
t=self.target;user,pwd,slot=self.account;stream=window.stream
|
||
stream.SetConnectInfo(t['ip'],t['port'],t['account'][0],t['account'][1])
|
||
stream.SetCharacterSlot(slot);stream.isAutoSelect=1
|
||
net.SetServerInfo(t['label'].strip())
|
||
try:
|
||
f=open(CHANNEL_FILE,'w');f.write('%d %d %d'%(t['server'],t['channel'],t['region']));f.close()
|
||
except (IOError,OSError):pass
|
||
self.stage='login';self.deadline=self.now()+LOGIN_TIMEOUT
|
||
window.Connect(user,pwd)
|
||
return True
|
||
|
||
def OnLoginFailure(self,error):
|
||
if self.stage=='login':self.error='login';self.Clear();self.Notify('error')
|
||
|
||
def OnGame(self,stream):
|
||
if self.stage=='login':
|
||
channel=self.target['channel'];self.Clear();self.Notify('done')
|
||
return channel
|
||
return 0
|
||
|
||
|
||
ERRORS={'channel':'无法换到该频道','account':'没有保存的登录信息,请重新登录后再换线',
|
||
'timeout':'服务器没有退出(战斗中或不允许退出),换线已取消','login':'登录目标频道失败'}
|
||
|
||
|
||
class ChannelWindow(ui.Window):
|
||
def __init__(self,switcher,stream):
|
||
ui.Window.__init__(self);self.SetWindowName('ChannelWindow');self.AddFlag('float');self.AddFlag('movable')
|
||
self.switcher=switcher;self.stream=stream;self.place=None;self.channelButtons=[]
|
||
self.SetSize(332,260)
|
||
self.heading=Text(self,18,18,'换线',0xffffd68b)
|
||
self.closeButton=Action(self,256,6,60,'关闭',self.Hide)
|
||
self.current=Text(self,18,52,'',0xffc3b7a1)
|
||
self.status=Text(self,18,232,'',0xffc3b7a1)
|
||
self.Hide()
|
||
|
||
def Buttons(self):return [self.closeButton]+self.channelButtons
|
||
|
||
def Build(self):
|
||
for b in self.channelButtons:b.Hide();b.event=None
|
||
self.place=Locate(self.stream.addr,self.stream.port)
|
||
channels=Channels(self.place)
|
||
self.channelButtons=[Action(self,18+(i%2)*152,82+(i//2)*52,144,'',lambda channel=channel:self.Switch(channel))
|
||
for i,(channel,info) in enumerate(channels)]
|
||
self.SetSize(332,max(160,82+(len(channels)+1)//2*52+40));self.status.SetPosition(18,self.GetHeight()-28)
|
||
|
||
def Open(self):self.Build();self.Refresh();self.SetCenterPosition();self.Show();self.SetTop()
|
||
|
||
def Switch(self,channel):
|
||
if self.place:self.switcher.Start(self.stream,self.place,channel)
|
||
self.Refresh()
|
||
|
||
def Refresh(self):
|
||
current=self.place[2] if self.place else 0
|
||
channels=Channels(self.place);busy=self.switcher.Busy()
|
||
name=dict(channels).get(current,{}).get('name')
|
||
self.current.SetText(L('当前频道:'+(name if name else '未知')))
|
||
for button,(channel,info) in zip(self.channelButtons,channels):
|
||
button.SetText(L(B(u'【%s】'%info.get('name','') if channel==current else info.get('name',''))))
|
||
button.SetEnabled(not busy and channel!=current)
|
||
self.status.SetText(L(self.StatusText()))
|
||
|
||
def StatusText(self):
|
||
s=self.switcher
|
||
if s.stage=='logout':return '正在退出,稍后自动登录 %s'%s.target['label'].split(', ')[-1]
|
||
if s.stage=='login':return '正在登录 %s'%s.target['label'].split(', ')[-1]
|
||
if s.error:return '!'+ERRORS[s.error]
|
||
if not self.place:return '!没有可用的频道列表'
|
||
return '退出后自动登录所选频道,并进入同一角色'
|
||
|
||
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())
|
||
|
||
def OnPressEscapeKey(self):self.Hide();return True
|
||
|
||
def Destroy(self):
|
||
self.Hide()
|
||
for b in self.Buttons():b.event=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('ChannelEntry');self.OnUpdate()
|
||
|
||
def OnUpdate(self):
|
||
x,y=self.minimap.GetGlobalPosition();self.SetPosition(max(4,x-68),y+304)
|
||
|
||
|
||
def OnSwitch(kind):
|
||
if kind=='error':chat.AppendChat(chat.CHAT_TYPE_INFO,L(ERRORS[g_switcher.error]))
|
||
if g_window:g_window.Refresh()
|
||
|
||
|
||
g_switcher=Switcher(net.SendChatPacket,app.GetTime,OnSwitch)
|
||
|
||
|
||
def Toggle():
|
||
if g_window:
|
||
if g_window.IsShow():g_window.Hide()
|
||
else:g_window.Open()
|
||
|
||
|
||
_open=game.GameWindow.Open
|
||
_close=game.GameWindow.Close
|
||
_update=game.GameWindow.OnUpdate
|
||
_login_open=introLogin.LoginWindow.Open
|
||
_login_update=introLogin.LoginWindow.OnUpdate
|
||
_login_failure=introLogin.LoginWindow.OnLoginFailure
|
||
_connect_failure=introLogin.LoginWindow.OnConnectFailure
|
||
|
||
def Open(self):
|
||
global g_window,g_entry
|
||
_open(self)
|
||
channel=g_switcher.OnGame(self.stream)
|
||
g_window=ChannelWindow(g_switcher,self.stream);g_entry=Entry(self)
|
||
if channel:
|
||
name=dict(Channels(Locate(self.stream.addr,self.stream.port))).get(channel,{}).get('name','')
|
||
chat.AppendChat(chat.CHAT_TYPE_INFO,L('已换到 %s'%name))
|
||
|
||
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)
|
||
|
||
def OnUpdate(self):
|
||
_update(self)
|
||
if g_switcher.stage=='logout':g_switcher.Tick()
|
||
|
||
def LoginOpen(self):
|
||
_login_open(self);g_switcher.OnLoginWindow(self)
|
||
|
||
def LoginUpdate(self):
|
||
_login_update(self);g_switcher.Tick()
|
||
|
||
def LoginFailure(self,error):
|
||
g_switcher.OnLoginFailure(error);_login_failure(self,error)
|
||
|
||
def ConnectFailure(self):
|
||
g_switcher.OnLoginFailure('connect');_connect_failure(self)
|
||
|
||
game.GameWindow.Open=Open
|
||
game.GameWindow.Close=Close
|
||
game.GameWindow.OnUpdate=OnUpdate
|
||
introLogin.LoginWindow.Open=LoginOpen
|
||
introLogin.LoginWindow.OnUpdate=LoginUpdate
|
||
introLogin.LoginWindow.OnLoginFailure=LoginFailure
|
||
introLogin.LoginWindow.OnConnectFailure=ConnectFailure
|
||
)PY";
|