Add client-only channel switch (AVA2 ENABLE_CHANNEL_CHANGE)
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
aeb67f3c9c
commit
13a462b6fc
@@ -0,0 +1,240 @@
|
||||
// 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";
|
||||
Reference in New Issue
Block a user