Add MainHUD gameplay UI, net handshake updates, and server maintenance mode.

Introduce UiManager/MainHUD prefab flow, strengthen GameNet/WsClient, and add AIServer maintenance endpoint support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 17:34:42 +08:00
co-authored by Cursor
parent 10d9b98637
commit c94b8fefeb
32 changed files with 1907 additions and 74 deletions
@@ -6,7 +6,7 @@ using UnityEngine;
namespace HotUpdate.Net
{
/// <summary>
/// 登录 → WS 连接编排。
/// 登录 → WS 连接编排(含指数退避重连与维护公告)。
/// </summary>
public sealed class GameNet : MonoBehaviour
{
@@ -21,9 +21,14 @@ namespace HotUpdate.Net
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)
{
@@ -35,7 +40,6 @@ namespace HotUpdate.Net
if (!string.IsNullOrEmpty(_boot.WsUrlTemplate))
{
// 旧模板只有 pid 时补上 token
var tpl = _boot.WsUrlTemplate;
if (!tpl.Contains("{token}"))
{
@@ -54,11 +58,37 @@ namespace HotUpdate.Net
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();
// 服务器要求 username/password;编辑器自动账号
var username = $"guest_{deviceId}";
var password = $"pw_{deviceId}";
@@ -89,6 +119,7 @@ namespace HotUpdate.Net
IEnumerator ConnectWs()
{
if (_maintenance) yield break;
if (Session == null || !Session.IsValid)
{
SetStatus("no session");
@@ -99,14 +130,13 @@ namespace HotUpdate.Net
Socket?.Dispose();
Socket = new WsClient();
Socket.AutoReconnect = true;
Socket.OnConnected += () =>
Socket.OnConnected += HandleConnected;
Socket.OnDisconnected += HandleDisconnected;
Socket.OnReconnecting += attempt =>
{
SetStatus("ws connected");
OnWsReady?.Invoke();
// 应用层握手(服务端当前可无响应,保留扩展)
Socket.SendText($"{{\"op\":\"hello\",\"proto\":1,\"app\":\"{_boot?.AppVersion}\",\"res\":\"{_boot?.ResVersion}\",\"pid\":{Session.Pid}}}");
SetStatus($"ws reconnecting #{attempt}");
OnReconnecting?.Invoke(attempt);
};
Socket.OnDisconnected += reason => SetStatus($"ws down: {reason}");
Socket.OnMessage += msg =>
{
Debug.Log($"[GameNet] << {msg}");
@@ -116,7 +146,6 @@ namespace HotUpdate.Net
var url = Session.BuildWsUrl(WsUrlTemplate);
Socket.Connect(url);
// 等待最多 15s
float t = 0f;
while (t < 15f && Socket.State != WsState.Connected && Socket.State != WsState.Disconnected)
{
@@ -132,6 +161,28 @@ namespace HotUpdate.Net
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();
@@ -38,6 +38,7 @@ namespace HotUpdate.Net
public event Action OnConnected;
public event Action<string> OnDisconnected;
public event Action<string> OnMessage;
public event Action<int> OnReconnecting;
public bool AutoReconnect { get; set; } = true;
public float KeepAliveSeconds { get; set; } = 20f;
@@ -50,6 +51,20 @@ namespace HotUpdate.Net
_ = ConnectInternalAsync();
}
/// <summary>立刻断开并重新连接(重置退避计数)。</summary>
public void ForceReconnect()
{
if (string.IsNullOrEmpty(_url)) return;
_manualClose = false;
AutoReconnect = true;
_reconnectAttempt = 0;
SafeCancel();
try { _ws?.Dispose(); } catch { /* ignore */ }
_ws = null;
State = WsState.Disconnected;
_ = ConnectInternalAsync();
}
public void SendText(string text)
{
if (string.IsNullOrEmpty(text)) return;
@@ -119,6 +134,7 @@ namespace HotUpdate.Net
_reconnectAttempt++;
int delayMs = Math.Min(30000, 1000 * (1 << Math.Min(5, _reconnectAttempt)));
Debug.Log($"[WsClient] reconnect in {delayMs}ms attempt={_reconnectAttempt}");
OnReconnecting?.Invoke(_reconnectAttempt);
try
{
await Task.Delay(delayMs, CancellationToken.None);