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:
shenlei
2026-10-15 21:37:02 +09:00
co-authored by Claude Opus 5.5
parent aeb67f3c9c
commit 13a462b6fc
6 changed files with 442 additions and 0 deletions
+240
View File
@@ -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";