Introduce UiManager/MainHUD prefab flow, strengthen GameNet/WsClient, and add AIServer maintenance endpoint support. Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
6.3 KiB
C#
205 lines
6.3 KiB
C#
using System;
|
|
using System.Collections;
|
|
using AOT.Boot;
|
|
using UnityEngine;
|
|
|
|
namespace HotUpdate.Net
|
|
{
|
|
/// <summary>
|
|
/// 登录 → WS 连接编排(含指数退避重连与维护公告)。
|
|
/// </summary>
|
|
public sealed class GameNet : MonoBehaviour
|
|
{
|
|
public string HttpBase = AuthApi.DefaultHttpBase;
|
|
public string WsUrlTemplate = "wss://game.xworld.ren/ws?pid={pid}&token={token}";
|
|
|
|
public SessionData Session { get; private set; }
|
|
public WsClient Socket { get; private set; }
|
|
public string Status { get; private set; } = "idle";
|
|
|
|
public event Action<SessionData> OnAuthed;
|
|
public event Action OnWsReady;
|
|
public event Action<string> OnStatus;
|
|
public event Action<string> OnWsMessage;
|
|
public event Action<string> OnWsDisconnected;
|
|
public event Action<int> OnReconnecting;
|
|
public event Action<string> OnMaintenance;
|
|
|
|
BootContext _boot;
|
|
bool _started;
|
|
bool _maintenance;
|
|
bool _everConnected;
|
|
|
|
public void Bind(BootContext boot)
|
|
{
|
|
_boot = boot;
|
|
if (_boot == null) return;
|
|
|
|
if (!string.IsNullOrEmpty(_boot.HttpBase))
|
|
HttpBase = _boot.HttpBase;
|
|
|
|
if (!string.IsNullOrEmpty(_boot.WsUrlTemplate))
|
|
{
|
|
var tpl = _boot.WsUrlTemplate;
|
|
if (!tpl.Contains("{token}"))
|
|
{
|
|
tpl = tpl.Contains("?")
|
|
? tpl + (tpl.EndsWith("?") || tpl.EndsWith("&") ? "token={token}" : "&token={token}")
|
|
: tpl + "?pid={pid}&token={token}";
|
|
}
|
|
WsUrlTemplate = tpl;
|
|
}
|
|
}
|
|
|
|
public void StartGuestSession()
|
|
{
|
|
if (_started) return;
|
|
_started = true;
|
|
StartCoroutine(RunGuestFlow());
|
|
}
|
|
|
|
/// <summary>UI「立即重连」:重置退避并马上再连。</summary>
|
|
public void RequestReconnect()
|
|
{
|
|
if (_maintenance || Session == null || !Session.IsValid) return;
|
|
SetStatus("手动重连…");
|
|
if (Socket != null)
|
|
{
|
|
Socket.AutoReconnect = true;
|
|
Socket.ForceReconnect();
|
|
}
|
|
else
|
|
StartCoroutine(ConnectWs());
|
|
}
|
|
|
|
public void NotifyMaintenance(string message)
|
|
{
|
|
if (_maintenance) return;
|
|
_maintenance = true;
|
|
if (Socket != null)
|
|
{
|
|
Socket.AutoReconnect = false;
|
|
Socket.Close();
|
|
}
|
|
SetStatus("maintenance");
|
|
OnMaintenance?.Invoke(message ?? "服务器维护中");
|
|
}
|
|
|
|
IEnumerator RunGuestFlow()
|
|
{
|
|
SetStatus("auth…");
|
|
var deviceId = DeviceId.GetOrCreate();
|
|
var username = $"guest_{deviceId}";
|
|
var password = $"pw_{deviceId}";
|
|
|
|
AuthResponse auth = null;
|
|
string err = null;
|
|
yield return AuthApi.EnsureAccount(HttpBase, username, password, (r, e) => { auth = r; err = e; });
|
|
|
|
if (auth == null || string.IsNullOrEmpty(auth.token))
|
|
{
|
|
SetStatus($"auth fail: {err}");
|
|
Debug.LogError($"[GameNet] auth failed: {err}");
|
|
yield break;
|
|
}
|
|
|
|
Session = new SessionData
|
|
{
|
|
Username = username,
|
|
Token = auth.token,
|
|
Pid = auth.pid,
|
|
DeviceId = deviceId,
|
|
};
|
|
Debug.Log($"[GameNet] authed pid={Session.Pid} user={username}");
|
|
OnAuthed?.Invoke(Session);
|
|
SetStatus($"authed pid={Session.Pid}");
|
|
|
|
yield return ConnectWs();
|
|
}
|
|
|
|
IEnumerator ConnectWs()
|
|
{
|
|
if (_maintenance) yield break;
|
|
if (Session == null || !Session.IsValid)
|
|
{
|
|
SetStatus("no session");
|
|
yield break;
|
|
}
|
|
|
|
SetStatus("ws connecting…");
|
|
Socket?.Dispose();
|
|
Socket = new WsClient();
|
|
Socket.AutoReconnect = true;
|
|
Socket.OnConnected += HandleConnected;
|
|
Socket.OnDisconnected += HandleDisconnected;
|
|
Socket.OnReconnecting += attempt =>
|
|
{
|
|
SetStatus($"ws reconnecting #{attempt}");
|
|
OnReconnecting?.Invoke(attempt);
|
|
};
|
|
Socket.OnMessage += msg =>
|
|
{
|
|
Debug.Log($"[GameNet] << {msg}");
|
|
OnWsMessage?.Invoke(msg);
|
|
};
|
|
|
|
var url = Session.BuildWsUrl(WsUrlTemplate);
|
|
Socket.Connect(url);
|
|
|
|
float t = 0f;
|
|
while (t < 15f && Socket.State != WsState.Connected && Socket.State != WsState.Disconnected)
|
|
{
|
|
t += Time.unscaledDeltaTime;
|
|
yield return null;
|
|
}
|
|
|
|
if (Socket.State == WsState.Connected)
|
|
SetStatus("ws connected");
|
|
else if (Socket.State == WsState.Disconnected && !string.IsNullOrEmpty(Socket.LastError))
|
|
SetStatus($"ws fail: {Socket.LastError}");
|
|
else if (Socket.State != WsState.Connected)
|
|
SetStatus($"ws state={Socket.State}");
|
|
}
|
|
|
|
void HandleConnected()
|
|
{
|
|
if (_maintenance) return;
|
|
SetStatus(_everConnected ? "ws reconnected" : "ws connected");
|
|
_everConnected = true;
|
|
SendHello();
|
|
OnWsReady?.Invoke();
|
|
}
|
|
|
|
void HandleDisconnected(string reason)
|
|
{
|
|
SetStatus($"ws down: {reason}");
|
|
OnWsDisconnected?.Invoke(reason ?? "closed");
|
|
}
|
|
|
|
void SendHello()
|
|
{
|
|
if (Socket == null || Session == null) return;
|
|
Socket.SendText(
|
|
$"{{\"op\":\"hello\",\"proto\":1,\"app\":\"{_boot?.AppVersion}\",\"res\":\"{_boot?.ResVersion}\",\"pid\":{Session.Pid}}}");
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
Socket?.Tick();
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
Socket?.Dispose();
|
|
Socket = null;
|
|
}
|
|
|
|
void SetStatus(string s)
|
|
{
|
|
Status = s;
|
|
Debug.Log($"[GameNet] {s}");
|
|
OnStatus?.Invoke(s);
|
|
}
|
|
}
|
|
}
|