fix(all): stabilize login and proxy-aware game startup

This commit is contained in:
2026-10-04 01:21:58 +08:00
parent 850c95d511
commit e4ce37bd8e
11 changed files with 295 additions and 12 deletions
+32 -8
View File
@@ -228,6 +228,9 @@ internal static class LoginController
private static void ShowLoginPanel(object introUI)
{
// The official manual-login branch cancels the 10-second startup watchdog.
// Browser authentication waits for the user and must not retain that timer.
CancelMaintenanceTimeout.Invoke(introUI, null);
AccessTokens.Clear();
LoginInProgress = false;
EstablishedGameSession = false;
@@ -269,12 +272,15 @@ internal static class LoginController
private static IEnumerator DeviceLogin(object introUI, string provider)
{
string endpoint = new Uri(ServerRoot, "auth/device").AbsoluteUri;
int generation = Volatile.Read(ref RecoveryGeneration);
Uri origin = ServerRoot;
string endpoint = new Uri(origin, "auth/device").AbsoluteUri;
byte[] body = Encoding.UTF8.GetBytes(JsonUtility.ToJson(new DeviceRequest { provider = provider }));
{
ControlProbeResult request = null;
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, body, null, result => request = result);
if (request == null) yield break;
if (!IsCurrentLogin(generation, origin)) { request.Body = null; yield break; }
if (!request.Success)
{
LoginInProgress = false;
@@ -299,24 +305,24 @@ internal static class LoginController
yield break;
}
Application.OpenURL(start.start_url);
yield return PollDevice(introUI, start);
yield return PollDevice(introUI, start, generation, origin);
}
}
private static IEnumerator PollDevice(object introUI, DeviceStart start)
private static IEnumerator PollDevice(object introUI, DeviceStart start, int generation, Uri origin)
{
int generation = Volatile.Read(ref RecoveryGeneration);
int delay = Math.Max(1, start.poll_interval);
float deadline = Time.realtimeSinceStartup + Math.Max(30, start.expires_in);
string endpoint = new Uri(ServerRoot, "auth/device/" + Uri.EscapeDataString(start.transaction_id) + "/poll").AbsoluteUri;
string endpoint = new Uri(origin, "auth/device/" + Uri.EscapeDataString(start.transaction_id) + "/poll").AbsoluteUri;
while (Time.realtimeSinceStartup < deadline)
{
yield return new WaitForSecondsRealtime(delay);
if (generation != Volatile.Read(ref RecoveryGeneration)) yield break;
if (!IsCurrentLogin(generation, origin)) { start.device_secret = null; yield break; }
{
ControlProbeResult request = null;
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, Array.Empty<byte>(), "Device " + start.device_secret, result => request = result);
if (request == null) yield break;
if (!IsCurrentLogin(generation, origin)) { request.Body = null; start.device_secret = null; yield break; }
if (request.StatusCode == 202)
{
continue;
@@ -345,7 +351,7 @@ internal static class LoginController
Log?.LogError("Login transaction returned incomplete credentials");
yield break;
}
CompleteInteractiveLogin(introUI, result);
CompleteInteractiveLogin(introUI, result, generation, origin);
yield break;
}
}
@@ -353,8 +359,17 @@ internal static class LoginController
Log?.LogError("Login transaction expired");
}
private static void CompleteInteractiveLogin(object introUI, TokenResult result)
private static bool IsCurrentLogin(int generation, Uri origin) =>
generation == Volatile.Read(ref RecoveryGeneration) && origin != null && ServerRoot != null && SameOrigin(origin, ServerRoot);
private static void CompleteInteractiveLogin(object introUI, TokenResult result, int generation, Uri origin)
{
if (!IsCurrentLogin(generation, origin))
{
result.access_token = null;
result.refresh_token = null;
return;
}
if (!RefreshCredentials.IsSupported)
{
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
@@ -370,6 +385,12 @@ internal static class LoginController
}
Action confirmed = delegate
{
if (!IsCurrentLogin(generation, origin))
{
result.access_token = null;
result.refresh_token = null;
return;
}
try
{
bool autoLogin = PlayerPrefs.GetInt("StandaloneAutoLogin", 0) != 0;
@@ -483,6 +504,8 @@ internal static class LoginController
bool credentialRejected = (request.StatusCode == 401 || request.StatusCode == 409) && request.RefreshInvalid;
if (credentialRejected)
{
Log?.LogWarning("Automatic-login credential rejected: HTTP=" + request.StatusCode +
", refreshInvalid=" + request.RefreshInvalid + ", classification=invalid-credential");
refreshToken = null;
attemptID = null;
ClearSavedLogin();
@@ -576,6 +599,7 @@ internal static class LoginController
try
{
ContinueMaintenance = true;
Log?.LogInfo("Authenticated login is requesting maintenance information: automatic=" + automatic);
SendMaintenance.Invoke(introUI, new object[] { automatic });
}
finally
+1
View File
@@ -14,6 +14,7 @@ internal static class LoginRuntime
internal static ManualLogSource Log;
internal static MethodInfo SetIntroState;
internal static MethodInfo SendMaintenance;
internal static MethodInfo CancelMaintenanceTimeout;
internal static Uri ServerRoot;
internal static MethodInfo OpenPCLoginPopup;
internal static MethodInfo EnterGame;
+6 -1
View File
@@ -31,6 +31,9 @@ public sealed class Plugin : BaseUnityPlugin
try
{
Log = Logger;
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UNITY_PROXYSERVER")) &&
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UNITY_NOPROXY")))
Logger.LogInfo("Game loopback transport uses launch-time Unity proxy bypass");
AppDomain.CurrentDomain.ProcessExit += (_, _) => DisposeGameRelay();
Game.Validate(typeof(Plugin).Assembly, Bd2Build.Versions.Game, message => Logger.LogInfo(message));
EnsureRecoveryHost();
@@ -49,6 +52,8 @@ public sealed class Plugin : BaseUnityPlugin
throw new MissingMethodException("IntroUI.Awake() was not found (client version mismatch)");
}
SendMaintenance = Game.Method<IntroUI>(ui => ui.SendMaintenanceInfo(false));
CancelMaintenanceTimeout = introUI.GetGameMethod(
"CancelMaintenanceTimeout", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
SetIntroState = introUI.GetGameMethod("SetIntroState", BindingFlags.Instance | BindingFlags.NonPublic);
EnterGame = introUI.GetGameMethod(
"Enter",
@@ -72,7 +77,7 @@ public sealed class Plugin : BaseUnityPlugin
MethodInfo exponentialBackOff = networkManager?.GetGameMethod(
"ExponetialBackOff",
BindingFlags.Instance | BindingFlags.NonPublic);
if (SendMaintenance == null || SetIntroState == null || OpenPCLoginPopup == null ||
if (SendMaintenance == null || CancelMaintenanceTimeout == null || CancelMaintenanceTimeout.ReturnType != typeof(void) || SetIntroState == null || OpenPCLoginPopup == null ||
accessTokenGetter == null || clearPCLocalData == null || disposeWebRequest == null ||
clientNetworkError == null || exponentialBackOff == null || EnterGame == null)
{
+6 -1
View File
@@ -78,7 +78,7 @@ internal sealed class SecureGameRelay : IDisposable
{
try { using (client) await Serve(client).ConfigureAwait(false); }
catch (Exception ex) when (ex is IOException || ex is SocketException || ex is OperationCanceledException || ex is InvalidDataException)
{ /* Close failed transport: the game's existing ConnectionError recovery handles it. */ }
{ log?.LogWarning("Native game relay connection failed: " + ex.GetType().Name); }
catch (Exception ex) { log?.LogWarning("Native game transport failed: " + ex.GetType().Name); }
finally { slots.Release(); }
});
@@ -108,6 +108,8 @@ internal sealed class SecureGameRelay : IDisposable
throw new InvalidDataException("Invalid relay request");
if (!Uri.TryCreate(local, start[1], out Uri target) || !TryResolve(target, out Uri remote))
throw new InvalidDataException("Invalid relay target");
bool maintenance = remote.AbsolutePath.Equals("/game/MaintenanceInfo", StringComparison.Ordinal);
if (maintenance) log?.LogInfo("Game relay received MaintenanceInfo request");
Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
int length = 0;
bool lengthSeen = false;
@@ -139,6 +141,9 @@ internal sealed class SecureGameRelay : IDisposable
}
response = await PlatformControlHttp.Send(remote, start[0], body, null, 4, lifetime.Token,
lifetime.Token, 64 * 1024 * 1024, headers).ConfigureAwait(false);
if (maintenance || response.StatusCode == 0)
log?.LogInfo("Game relay upstream result: path=" + remote.AbsolutePath + " status=" + response.StatusCode +
" error=" + (response.Error ?? "none"));
if (response.StatusCode == 0 || response.Data == null) throw new IOException("Native game transport unavailable");
StringBuilder output = new StringBuilder("HTTP/1.1 ").Append(response.StatusCode.ToString(CultureInfo.InvariantCulture)).Append(" Response\r\nConnection: close\r\nCache-Control: no-store\r\n");
foreach (KeyValuePair<string, string> item in response.Headers)
+36 -1
View File
@@ -225,6 +225,39 @@ internal static class SessionRecovery
{
try
{
try
{
const BindingFlags diagnosticFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
object packet = __0?.GetType().GetGameProperty("PacketData", diagnosticFlags)?.GetValue(__0, null);
string path = packet?.GetType().GetGameProperty("SendPath", diagnosticFlags)?.GetValue(packet, null) as string;
if (path == "MaintenanceInfo")
{
string server = packet?.GetType().GetGameField("RequestServerURL", diagnosticFlags)?.GetValue(packet) as string;
string endpoint = Uri.TryCreate(server, UriKind.Absolute, out Uri parsed) ? parsed.GetLeftPart(UriPartial.Authority) + parsed.AbsolutePath : "invalid";
string message = __0?.GetType().GetGameProperty("Message", diagnosticFlags)?.GetValue(__0, null) as string ?? string.Empty;
string category = message.IndexOf("tim", StringComparison.OrdinalIgnoreCase) >= 0 ? "Timeout" :
message.IndexOf("cert", StringComparison.OrdinalIgnoreCase) >= 0 || message.IndexOf("TLS", StringComparison.OrdinalIgnoreCase) >= 0 || message.IndexOf("SSL", StringComparison.OrdinalIgnoreCase) >= 0 ? "TLS" : "Other";
Log?.LogDebug("Maintenance backoff: endpoint=" + endpoint + ", category=" + category + ", observedRequests=" + GameRequests.Count);
foreach (WeakReference<UnityWebRequest> reference in GameRequests)
{
if (!reference.TryGetTarget(out UnityWebRequest request) || !request.isDone) continue;
if (!TryGetOwnedRequestUri(request.url, out Uri original) || original.AbsolutePath != "/game/MaintenanceInfo") continue;
string error = request.error ?? string.Empty;
string safeError = error.IndexOf("Insecure", StringComparison.OrdinalIgnoreCase) >= 0 ? "InsecureConnectionBlocked" :
error.IndexOf("connect", StringComparison.OrdinalIgnoreCase) >= 0 ? "ConnectionFailure" :
error.IndexOf("tim", StringComparison.OrdinalIgnoreCase) >= 0 ? "Timeout" :
error.IndexOf("HTTP", StringComparison.OrdinalIgnoreCase) >= 0 ? "HttpFailure" :
error.IndexOf("SSL", StringComparison.OrdinalIgnoreCase) >= 0 || error.IndexOf("cert", StringComparison.OrdinalIgnoreCase) >= 0 ? "TlsFailure" : "Unclassified";
Log?.LogWarning("Maintenance completed transport: result=" + request.result + ", status=" + request.responseCode +
", error=" + safeError + ", errorLength=" + error.Length);
}
}
}
catch (Exception ex)
{
Log?.LogDebug("Maintenance transport diagnostic unavailable: " + ex.GetType().Name);
}
if (ServerRoot == null || !IsConfiguredServerFailure(__0) || !IsTransportFailure(__0))
{
return true;
@@ -443,8 +476,10 @@ internal static class SessionRecovery
}
}
internal static bool SuppressNetworkErrorDuringRecovery()
internal static bool SuppressNetworkErrorDuringRecovery(object __0, object __1)
{
Log?.LogWarning("Client network error: type=" + __0 + ", code=" + __1 +
", recovering=" + (Volatile.Read(ref SessionRecoveryInProgress) != 0));
return Volatile.Read(ref SessionRecoveryInProgress) == 0;
}