242 lines
8.0 KiB
C#
242 lines
8.0 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 = AuthApi.DefaultWsTemplate;
|
|
|
|
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;
|
|
public event Action<string> OnLoginFailed;
|
|
|
|
BootContext _boot;
|
|
bool _started;
|
|
bool _maintenance;
|
|
bool _everConnected;
|
|
bool _authenticating;
|
|
|
|
/// <summary>Renew the same guest identity after an authenticated game API returns 401.</summary>
|
|
public void RenewSession()
|
|
{
|
|
if (_maintenance || _authenticating) return;
|
|
Socket?.Dispose(); Socket = null; Session = null;
|
|
_started = false;
|
|
StartGuestSession();
|
|
}
|
|
|
|
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;
|
|
}
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
// Serialized Boot scenes used for local Editor testing must not send players to loopback.
|
|
HttpBase = ResolveRemoteEndpoint(HttpBase, "https", AuthApi.DefaultHttpBase);
|
|
WsUrlTemplate = ResolveRemoteEndpoint(WsUrlTemplate, "wss", AuthApi.DefaultWsTemplate);
|
|
_boot.HttpBase = HttpBase;
|
|
_boot.WsUrlTemplate = WsUrlTemplate;
|
|
#endif
|
|
}
|
|
|
|
public static string ResolveRemoteEndpoint(string value, string scheme, string fallback)
|
|
{
|
|
if (Uri.TryCreate(value, UriKind.Absolute, out var uri) &&
|
|
uri.Scheme == scheme && !uri.IsLoopback && !uri.Host.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Migrate the former production defaults stored in already-shipped Boot scenes.
|
|
if (uri.IsDefaultPort && uri.Host.Equals("game.xworld.ren", StringComparison.OrdinalIgnoreCase) &&
|
|
((scheme == "https" && uri.AbsolutePath == "/") || (scheme == "wss" && uri.AbsolutePath == "/ws")))
|
|
return fallback;
|
|
return value;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
public void StartGuestSession()
|
|
{
|
|
if (_started) return;
|
|
_started = true;
|
|
StartCoroutine(RunGuestFlow());
|
|
}
|
|
|
|
/// <summary>UI「立即重连」:重置退避并马上再连。</summary>
|
|
public void RequestReconnect()
|
|
{
|
|
if (_maintenance) return;
|
|
if (Session == null || !Session.IsValid) { StartGuestSession(); 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()
|
|
{
|
|
_authenticating = true;
|
|
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; });
|
|
_authenticating = false;
|
|
|
|
if (auth == null || string.IsNullOrEmpty(auth.token))
|
|
{
|
|
_started = false;
|
|
SetStatus("登录服务连接失败,请重试");
|
|
Debug.LogError($"[GameNet] auth failed: {err}");
|
|
OnLoginFailed?.Invoke(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);
|
|
}
|
|
}
|
|
}
|