Commit c7d0c79b authored by hex's avatar hex

refactor(mycard): harden request flow and connection resilience

parent d6536a39
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
public class MyCard : WindowServantSP
{
public bool isMatching = false;
public bool isRequesting = false;
//const string mycardTiramisuAddress = "tiramisu.mycard.moe";
//const string mycardTiramisuAthleticPort = "8911";
//const string mycardTiramisuEntertainPort = "7911";
private Coroutine requestCoroutine = null;
private Coroutine requestCoroutine = null;
private Thread joinThread = null;
MyCardHelper mycardHelper;
UIInput inputUsername;
UIInput inputPsw;
......@@ -48,10 +43,19 @@ public class MyCard : WindowServantSP
public void TerminateRequest()
{
if (mycardHelper != null)
{
mycardHelper.CancelCurrentRequest();
}
if (requestCoroutine != null)
{
Program.I().StopCoroutine(requestCoroutine);
requestCoroutine = null;
}
if (isRequesting)
{
isRequesting = false;
Program.PrintToChat(InterString.Get("匹配已中断。"));
}
......@@ -80,56 +84,102 @@ public class MyCard : WindowServantSP
Application.OpenURL("https://ygobbs.com/");
}
IEnumerator MatchCoroutine(string username, string password, string matchType) {
isRequesting = true;
Program.PrintToChat(InterString.Get("正在登录至 MyCard。"));
bool loginSuccess = false;
string failReason = "";
// 启动并等待登录协程完成
yield return Program.I().StartCoroutine(mycardHelper.Login(username, password, (success, reason) => {
loginSuccess = success;
failReason = reason;
}));
if (!loginSuccess) {
Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason);
isRequesting = false;
yield break; // 结束协程
bool TryStartJoinThread(MatchResultObject matchResultObject)
{
if (matchResultObject == null)
{
return false;
}
Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username);
// Program.PrintToChat(InterString.Get("正在获取匹配秘钥。"));
yield return Program.I().StartCoroutine(mycardHelper.GetUserU16Secret((success, reason) =>
if (joinThread != null && joinThread.IsAlive)
{
loginSuccess = success;
failReason = reason;
}));
if (!loginSuccess)
return false;
}
string address = matchResultObject.address;
string userName = mycardHelper.username;
string port = matchResultObject.port.ToString();
string roomPassword = matchResultObject.password;
string version = "0x" + string.Format("{0:X}", Config.ClientVersion);
joinThread = new Thread(() =>
{
Program.PrintToChat(InterString.Get("获取用户密钥失败。请重新登录。原因: ") + failReason);
isRequesting = false;
yield break; // 结束协程
TcpHelper.join(address, userName, port, roomPassword, version);
});
joinThread.Name = "MyCardJoinThread";
joinThread.IsBackground = true;
joinThread.Start();
return true;
}
IEnumerator MatchCoroutine(string username, string password, string matchType)
{
isRequesting = true;
try
{
Program.PrintToChat(InterString.Get("正在登录至 MyCard。"));
bool loginSuccess = false;
string failReason = "";
yield return Program.I().StartCoroutine(mycardHelper.Login(username, password, (success, reason) =>
{
loginSuccess = success;
failReason = reason;
}));
if (!loginSuccess)
{
Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason);
yield break;
}
Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username);
yield return Program.I().StartCoroutine(mycardHelper.GetUserU16Secret((success, reason) =>
{
loginSuccess = success;
failReason = reason;
}));
if (!loginSuccess)
{
Program.PrintToChat(InterString.Get("获取用户密钥失败。请重新登录。原因: ") + failReason);
yield break;
}
Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType);
MatchResultObject matchResultObject = null;
yield return Program.I().StartCoroutine(mycardHelper.RequestMatch(matchType, (result, reason) =>
{
matchResultObject = result;
failReason = reason;
}));
if (matchResultObject == null)
{
Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason);
yield break;
}
if (!TryStartJoinThread(matchResultObject))
{
Program.PrintToChat(InterString.Get("连接任务已存在,请稍后再试。"));
yield break;
}
Program.PrintToChat(InterString.Get("匹配成功。正在进入房间。"));
isMatching = true;
}
// Program.PrintToChat(InterString.Get("获取匹配秘钥成功。"));
Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType);
MatchResultObject matchResultObject = null;
// 启动并等待匹配协程完成
yield return Program.I().StartCoroutine(mycardHelper.RequestMatch(matchType, (result, reason) => {
matchResultObject = result;
failReason = reason;
}));
if (matchResultObject == null) {
Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason);
finally
{
isRequesting = false;
yield break;
requestCoroutine = null;
}
Program.PrintToChat(InterString.Get("匹配成功。正在进入房间。"));
this.isMatching = true;
// TcpHelper.join 内部自己创建并管理线程,这里直接调用是安全的
// 只要 TcpHelper.join 本身及其后续操作不调用 Unity API
(new Thread(() => { TcpHelper.join(matchResultObject.address, mycardHelper.username, matchResultObject.port.ToString(), matchResultObject.password, "0x" + String.Format("{0:X}", Config.ClientVersion)); })).Start();
isRequesting = false;
requestCoroutine = null;
}
void StartMatch(string matchType)
{
string username = inputUsername.value;
......@@ -143,16 +193,20 @@ public class MyCard : WindowServantSP
{
TerminateRequest();
}
SaveUser();
Program.PrintToChat(InterString.Get("已开始匹配。"));
// 启动协程,而不是线程
mycardHelper.ResetCancellation();
requestCoroutine = Program.I().StartCoroutine(MatchCoroutine(username, password, matchType));
}
void onClickJoinAthletic() {
void onClickJoinAthletic()
{
StartMatch("athletic");
}
void onClickJoinEntertain() {
void onClickJoinEntertain()
{
StartMatch("entertain");
}
}
......@@ -16,8 +16,6 @@ using UnityEngine.Networking;
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
[Serializable]
public class LoginUserObject {
......@@ -64,138 +62,328 @@ public class U16SecretObject {
public class MyCardHelper
{
private const string MyCardApiBaseUrl = "https://sapi.moecube.com:444";
private const int RequestTimeoutSeconds = 15;
private const int MaxResponseBodyLength = 256;
public string username = null;
private String token = null;
private string token = null;
private int u16Secret = -1;
private UnityWebRequest currentRequest = null;
private bool cancelRequested = false;
public void ResetCancellation()
{
cancelRequested = false;
}
public void CancelCurrentRequest()
{
cancelRequested = true;
if (currentRequest != null)
{
try
{
currentRequest.Abort();
}
catch { }
}
}
private void ConfigureRequest(UnityWebRequest request)
{
currentRequest = request;
request.timeout = RequestTimeoutSeconds;
request.SetRequestHeader("User-Agent", BuildUserAgent());
request.SetRequestHeader("Accept", "application/json");
}
private void ClearCurrentRequest(UnityWebRequest request)
{
if (currentRequest == request)
{
currentRequest = null;
}
}
private string BuildUserAgent()
{
return "KoshiPro2iOS";
}
private string ReadResponseText(UnityWebRequest request)
{
if (request == null || request.downloadHandler == null)
{
return string.Empty;
}
string responseText = request.downloadHandler.text;
if (string.IsNullOrEmpty(responseText))
{
return string.Empty;
}
if (responseText.Length > MaxResponseBodyLength)
{
return responseText.Substring(0, MaxResponseBodyLength) + "...";
}
return responseText;
}
private string BuildError(UnityWebRequest request, string fallbackMessage)
{
if (cancelRequested)
{
return "请求已取消";
}
long code = request.responseCode;
string responseText = ReadResponseText(request);
if (code > 0)
{
if (!string.IsNullOrEmpty(responseText))
{
return string.Format("{0} (HTTP {1}): {2}", fallbackMessage, code, responseText);
}
return string.Format("{0} (HTTP {1})", fallbackMessage, code);
}
if (!string.IsNullOrEmpty(request.error))
{
return fallbackMessage + ": " + request.error;
}
return fallbackMessage;
}
private void Complete(Action<bool, string> onComplete, bool success, string reason)
{
if (onComplete != null)
{
onComplete(success, reason);
}
}
private void Complete(Action<MatchResultObject, string> onComplete, MatchResultObject result, string reason)
{
if (onComplete != null)
{
onComplete(result, reason);
}
}
public IEnumerator Login(string name, string password, Action<bool, string> onComplete)
{
if (cancelRequested)
{
Complete(onComplete, false, "请求已取消");
yield break;
}
username = null;
token = null;
u16Secret = -1;
LoginRequest data = new LoginRequest();
data.account = name;
data.password = password;
string data_str = JsonUtility.ToJson(data);
byte[] data_bytes = Encoding.UTF8.GetBytes(data_str);
string dataStr = JsonUtility.ToJson(data);
byte[] dataBytes = Encoding.UTF8.GetBytes(dataStr);
using (UnityWebRequest request = new UnityWebRequest("https://sapi.moecube.com:444/accounts/signin", "POST"))
using (UnityWebRequest request = new UnityWebRequest(MyCardApiBaseUrl + "/accounts/signin", UnityWebRequest.kHttpVerbPOST))
{
request.uploadHandler = new UploadHandlerRaw(data_bytes);
ConfigureRequest(request);
request.uploadHandler = new UploadHandlerRaw(dataBytes);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (IsError(request))
try
{
onComplete(false, "请检查用户名和密码是否正确");
}
else
{
try
yield return request.SendWebRequest();
if (IsError(request))
{
string result = request.downloadHandler.text;
LoginObject result_object = JsonUtility.FromJson<LoginObject>(result);
if (result_object == null || result_object.user == null)
Complete(onComplete, false, BuildError(request, "MyCard 登录失败"));
}
else
{
try
{
onComplete(false, "Login failed: Invalid response from server. " + result);
string result = request.downloadHandler.text;
LoginObject resultObject = JsonUtility.FromJson<LoginObject>(result);
if (resultObject == null || resultObject.user == null)
{
Complete(onComplete, false, "登录响应无效: " + ReadResponseText(request));
}
else
{
string resolvedUsername = resultObject.user.username;
if (string.IsNullOrEmpty(resolvedUsername))
{
resolvedUsername = name;
}
string resolvedToken = !string.IsNullOrEmpty(resultObject.token)
? resultObject.token
: resultObject.user.token;
if (string.IsNullOrEmpty(resolvedUsername) || string.IsNullOrEmpty(resolvedToken))
{
Complete(onComplete, false, "登录响应无效: " + ReadResponseText(request));
}
else
{
username = resolvedUsername;
token = resolvedToken;
Complete(onComplete, true, null);
}
}
}
else
catch (Exception e)
{
username = result_object.user.username;
token = result_object.token;
onComplete(true, null);
Complete(onComplete, false, "登录响应解析失败: " + e.Message);
}
}
catch (Exception e)
{
onComplete(false, e.Message);
}
}
finally
{
ClearCurrentRequest(request);
}
}
}
public IEnumerator GetUserU16Secret(Action<bool, string> onComplete)
{
if (token == null)
if (cancelRequested)
{
onComplete(false, "No token");
Complete(onComplete, false, "请求已取消");
yield break;
}
using (UnityWebRequest request = UnityWebRequest.Get("https://sapi.moecube.com:444/accounts/authUser"))
if (string.IsNullOrEmpty(token))
{
string auth_str = "Bearer " + token;
request.SetRequestHeader("Authorization", auth_str);
request.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
Complete(onComplete, false, "No token");
yield break;
}
yield return request.SendWebRequest();
using (UnityWebRequest request = UnityWebRequest.Get(MyCardApiBaseUrl + "/accounts/authUser"))
{
ConfigureRequest(request);
string authStr = "Bearer " + token;
request.SetRequestHeader("Authorization", authStr);
request.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
if (IsError(request))
{
onComplete(false, request.error);
}
else
try
{
try
yield return request.SendWebRequest();
if (IsError(request))
{
Complete(onComplete, false, BuildError(request, "获取用户密钥失败"));
}
else
{
string result = request.downloadHandler.text;
U16SecretObject result_object = JsonUtility.FromJson<U16SecretObject>(result);
if (result_object == null)
try
{
onComplete(false, result);
string result = request.downloadHandler.text;
U16SecretObject resultObject = JsonUtility.FromJson<U16SecretObject>(result);
if (resultObject == null || resultObject.u16Secret < 0)
{
Complete(onComplete, false, "用户密钥响应无效: " + ReadResponseText(request));
}
else
{
u16Secret = resultObject.u16Secret;
Complete(onComplete, true, null);
}
}
else
catch (Exception e)
{
u16Secret = result_object.u16Secret;
onComplete(true, null);
Complete(onComplete, false, "用户密钥解析失败: " + e.Message);
}
}
catch (Exception e)
{
onComplete(false, e.Message);
}
}
finally
{
ClearCurrentRequest(request);
}
}
}
public IEnumerator RequestMatch(string matchType, Action<MatchResultObject, string> onComplete)
{
if (u16Secret == -1) // int 默认值检查
if (cancelRequested)
{
onComplete(null, "Not logged in");
Complete(onComplete, null, "请求已取消");
yield break;
}
string auth_str = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + u16Secret));
string url = "https://sapi.moecube.com:444/ygopro/match?locale=zh-CN&arena=" + matchType;
if (u16Secret < 0)
{
Complete(onComplete, null, "Not logged in");
yield break;
}
byte[] meta = new byte[1];
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
if (string.IsNullOrEmpty(username))
{
Complete(onComplete, null, "No username");
yield break;
}
string authStr = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + u16Secret));
string url = MyCardApiBaseUrl + "/ygopro/match?locale=zh-CN&arena=" + matchType;
byte[] meta = new byte[0];
using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
ConfigureRequest(request);
request.uploadHandler = new UploadHandlerRaw(meta);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Authorization", auth_str);
request.SetRequestHeader("Authorization", authStr);
request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
yield return request.SendWebRequest();
if (IsError(request))
try
{
onComplete(null, request.error);
}
else
{
try
yield return request.SendWebRequest();
if (IsError(request))
{
string result = request.downloadHandler.text;
MatchResultObject matchResultObject = JsonUtility.FromJson<MatchResultObject>(result);
onComplete(matchResultObject, null);
Complete(onComplete, null, BuildError(request, "匹配请求失败"));
}
catch (Exception e)
else
{
onComplete(null, e.Message);
try
{
string result = request.downloadHandler.text;
MatchResultObject matchResultObject = JsonUtility.FromJson<MatchResultObject>(result);
if (matchResultObject == null
|| string.IsNullOrEmpty(matchResultObject.address)
|| matchResultObject.port <= 0)
{
Complete(onComplete, null, "匹配响应无效: " + ReadResponseText(request));
}
else
{
if (matchResultObject.password == null)
{
matchResultObject.password = string.Empty;
}
Complete(onComplete, matchResultObject, null);
}
}
catch (Exception e)
{
Complete(onComplete, null, "匹配响应解析失败: " + e.Message);
}
}
}
finally
{
ClearCurrentRequest(request);
}
}
}
......@@ -203,4 +391,4 @@ public class MyCardHelper
{
return request.result != UnityWebRequest.Result.Success;
}
}
\ No newline at end of file
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment