Commit c7d0c79b authored by hex's avatar hex

refactor(mycard): harden request flow and connection resilience

parent d6536a39
using UnityEngine; using UnityEngine;
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading; using System.Threading;
public class MyCard : WindowServantSP public class MyCard : WindowServantSP
{ {
public bool isMatching = false; public bool isMatching = false;
public bool isRequesting = 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; MyCardHelper mycardHelper;
UIInput inputUsername; UIInput inputUsername;
UIInput inputPsw; UIInput inputPsw;
...@@ -48,10 +43,19 @@ public class MyCard : WindowServantSP ...@@ -48,10 +43,19 @@ public class MyCard : WindowServantSP
public void TerminateRequest() public void TerminateRequest()
{ {
if (mycardHelper != null)
{
mycardHelper.CancelCurrentRequest();
}
if (requestCoroutine != null) if (requestCoroutine != null)
{ {
Program.I().StopCoroutine(requestCoroutine); Program.I().StopCoroutine(requestCoroutine);
requestCoroutine = null; requestCoroutine = null;
}
if (isRequesting)
{
isRequesting = false; isRequesting = false;
Program.PrintToChat(InterString.Get("匹配已中断。")); Program.PrintToChat(InterString.Get("匹配已中断。"));
} }
...@@ -80,56 +84,102 @@ public class MyCard : WindowServantSP ...@@ -80,56 +84,102 @@ public class MyCard : WindowServantSP
Application.OpenURL("https://ygobbs.com/"); Application.OpenURL("https://ygobbs.com/");
} }
IEnumerator MatchCoroutine(string username, string password, string matchType) { bool TryStartJoinThread(MatchResultObject matchResultObject)
{
if (matchResultObject == null)
{
return false;
}
if (joinThread != null && joinThread.IsAlive)
{
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(() =>
{
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; isRequesting = true;
try
{
Program.PrintToChat(InterString.Get("正在登录至 MyCard。")); Program.PrintToChat(InterString.Get("正在登录至 MyCard。"));
bool loginSuccess = false; bool loginSuccess = false;
string failReason = ""; string failReason = "";
// 启动并等待登录协程完成
yield return Program.I().StartCoroutine(mycardHelper.Login(username, password, (success, reason) => { yield return Program.I().StartCoroutine(mycardHelper.Login(username, password, (success, reason) =>
{
loginSuccess = success; loginSuccess = success;
failReason = reason; failReason = reason;
})); }));
if (!loginSuccess) {
if (!loginSuccess)
{
Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason); Program.PrintToChat(InterString.Get("MyCard 登录失败。原因: ") + failReason);
isRequesting = false; yield break;
yield break; // 结束协程
} }
Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username); Program.PrintToChat(InterString.Get("MyCard 登录成功,用户名: ") + mycardHelper.username);
// Program.PrintToChat(InterString.Get("正在获取匹配秘钥。"));
yield return Program.I().StartCoroutine(mycardHelper.GetUserU16Secret((success, reason) => yield return Program.I().StartCoroutine(mycardHelper.GetUserU16Secret((success, reason) =>
{ {
loginSuccess = success; loginSuccess = success;
failReason = reason; failReason = reason;
})); }));
if (!loginSuccess) if (!loginSuccess)
{ {
Program.PrintToChat(InterString.Get("获取用户密钥失败。请重新登录。原因: ") + failReason); Program.PrintToChat(InterString.Get("获取用户密钥失败。请重新登录。原因: ") + failReason);
isRequesting = false; yield break;
yield break; // 结束协程
} }
// Program.PrintToChat(InterString.Get("获取匹配秘钥成功。"));
Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType); Program.PrintToChat(InterString.Get("正在请求匹配。匹配类型: ") + matchType);
MatchResultObject matchResultObject = null; MatchResultObject matchResultObject = null;
// 启动并等待匹配协程完成
yield return Program.I().StartCoroutine(mycardHelper.RequestMatch(matchType, (result, reason) => { yield return Program.I().StartCoroutine(mycardHelper.RequestMatch(matchType, (result, reason) =>
{
matchResultObject = result; matchResultObject = result;
failReason = reason; failReason = reason;
})); }));
if (matchResultObject == null) {
if (matchResultObject == null)
{
Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason); Program.PrintToChat(InterString.Get("匹配请求失败。原因: ") + failReason);
isRequesting = false;
yield break; yield break;
} }
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();
if (!TryStartJoinThread(matchResultObject))
{
Program.PrintToChat(InterString.Get("连接任务已存在,请稍后再试。"));
yield break;
}
Program.PrintToChat(InterString.Get("匹配成功。正在进入房间。"));
isMatching = true;
}
finally
{
isRequesting = false; isRequesting = false;
requestCoroutine = null; requestCoroutine = null;
} }
}
void StartMatch(string matchType) void StartMatch(string matchType)
{ {
string username = inputUsername.value; string username = inputUsername.value;
...@@ -143,16 +193,20 @@ public class MyCard : WindowServantSP ...@@ -143,16 +193,20 @@ public class MyCard : WindowServantSP
{ {
TerminateRequest(); TerminateRequest();
} }
SaveUser(); SaveUser();
Program.PrintToChat(InterString.Get("已开始匹配。")); Program.PrintToChat(InterString.Get("已开始匹配。"));
// 启动协程,而不是线程 mycardHelper.ResetCancellation();
requestCoroutine = Program.I().StartCoroutine(MatchCoroutine(username, password, matchType)); requestCoroutine = Program.I().StartCoroutine(MatchCoroutine(username, password, matchType));
} }
void onClickJoinAthletic() { void onClickJoinAthletic()
{
StartMatch("athletic"); StartMatch("athletic");
} }
void onClickJoinEntertain() {
void onClickJoinEntertain()
{
StartMatch("entertain"); StartMatch("entertain");
} }
} }
...@@ -16,8 +16,6 @@ using UnityEngine.Networking; ...@@ -16,8 +16,6 @@ using UnityEngine.Networking;
using System; using System;
using System.Text; using System.Text;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Threading;
[Serializable] [Serializable]
public class LoginUserObject { public class LoginUserObject {
...@@ -64,124 +62,296 @@ public class U16SecretObject { ...@@ -64,124 +62,296 @@ public class U16SecretObject {
public class MyCardHelper 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; public string username = null;
private String token = null; private string token = null;
private int u16Secret = -1; 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) 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(); LoginRequest data = new LoginRequest();
data.account = name; data.account = name;
data.password = password; data.password = password;
string data_str = JsonUtility.ToJson(data); string dataStr = JsonUtility.ToJson(data);
byte[] data_bytes = Encoding.UTF8.GetBytes(data_str); 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.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Content-Type", "application/json");
try
{
yield return request.SendWebRequest(); yield return request.SendWebRequest();
if (IsError(request)) if (IsError(request))
{ {
onComplete(false, "请检查用户名和密码是否正确"); Complete(onComplete, false, BuildError(request, "MyCard 登录失败"));
} }
else else
{ {
try try
{ {
string result = request.downloadHandler.text; string result = request.downloadHandler.text;
LoginObject result_object = JsonUtility.FromJson<LoginObject>(result); LoginObject resultObject = JsonUtility.FromJson<LoginObject>(result);
if (result_object == null || result_object.user == null) if (resultObject == null || resultObject.user == null)
{ {
onComplete(false, "Login failed: Invalid response from server. " + result); Complete(onComplete, false, "登录响应无效: " + ReadResponseText(request));
} }
else else
{ {
username = result_object.user.username; string resolvedUsername = resultObject.user.username;
token = result_object.token; if (string.IsNullOrEmpty(resolvedUsername))
onComplete(true, null); {
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);
}
} }
} }
catch (Exception e) catch (Exception e)
{ {
onComplete(false, e.Message); Complete(onComplete, false, "登录响应解析失败: " + e.Message);
} }
} }
} }
finally
{
ClearCurrentRequest(request);
}
}
} }
public IEnumerator GetUserU16Secret(Action<bool, string> onComplete) public IEnumerator GetUserU16Secret(Action<bool, string> onComplete)
{ {
if (token == null) if (cancelRequested)
{
Complete(onComplete, false, "请求已取消");
yield break;
}
if (string.IsNullOrEmpty(token))
{ {
onComplete(false, "No token"); Complete(onComplete, false, "No token");
yield break; yield break;
} }
using (UnityWebRequest request = UnityWebRequest.Get("https://sapi.moecube.com:444/accounts/authUser")) using (UnityWebRequest request = UnityWebRequest.Get(MyCardApiBaseUrl + "/accounts/authUser"))
{ {
string auth_str = "Bearer " + token; ConfigureRequest(request);
request.SetRequestHeader("Authorization", auth_str); string authStr = "Bearer " + token;
request.SetRequestHeader("Authorization", authStr);
request.SetRequestHeader("Content-Type", "application/json; charset=utf-8"); request.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
try
{
yield return request.SendWebRequest(); yield return request.SendWebRequest();
if (IsError(request)) if (IsError(request))
{ {
onComplete(false, request.error); Complete(onComplete, false, BuildError(request, "获取用户密钥失败"));
} }
else else
{ {
try try
{ {
string result = request.downloadHandler.text; string result = request.downloadHandler.text;
U16SecretObject result_object = JsonUtility.FromJson<U16SecretObject>(result); U16SecretObject resultObject = JsonUtility.FromJson<U16SecretObject>(result);
if (result_object == null) if (resultObject == null || resultObject.u16Secret < 0)
{ {
onComplete(false, result); Complete(onComplete, false, "用户密钥响应无效: " + ReadResponseText(request));
} }
else else
{ {
u16Secret = result_object.u16Secret; u16Secret = resultObject.u16Secret;
onComplete(true, null); Complete(onComplete, true, null);
} }
} }
catch (Exception e) catch (Exception e)
{ {
onComplete(false, e.Message); Complete(onComplete, false, "用户密钥解析失败: " + e.Message);
} }
} }
} }
finally
{
ClearCurrentRequest(request);
}
}
} }
public IEnumerator RequestMatch(string matchType, Action<MatchResultObject, string> onComplete) public IEnumerator RequestMatch(string matchType, Action<MatchResultObject, string> onComplete)
{ {
if (u16Secret == -1) // int 默认值检查 if (cancelRequested)
{
Complete(onComplete, null, "请求已取消");
yield break;
}
if (u16Secret < 0)
{
Complete(onComplete, null, "Not logged in");
yield break;
}
if (string.IsNullOrEmpty(username))
{ {
onComplete(null, "Not logged in"); Complete(onComplete, null, "No username");
yield break; yield break;
} }
string auth_str = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + u16Secret)); string authStr = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + u16Secret));
string url = "https://sapi.moecube.com:444/ygopro/match?locale=zh-CN&arena=" + matchType; string url = MyCardApiBaseUrl + "/ygopro/match?locale=zh-CN&arena=" + matchType;
byte[] meta = new byte[1]; byte[] meta = new byte[0];
using (UnityWebRequest request = new UnityWebRequest(url, "POST")) using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{ {
ConfigureRequest(request);
request.uploadHandler = new UploadHandlerRaw(meta); request.uploadHandler = new UploadHandlerRaw(meta);
request.downloadHandler = new DownloadHandlerBuffer(); request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Authorization", authStr);
request.SetRequestHeader("Authorization", auth_str);
request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded"); request.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded");
try
{
yield return request.SendWebRequest(); yield return request.SendWebRequest();
if (IsError(request)) if (IsError(request))
{ {
onComplete(null, request.error); Complete(onComplete, null, BuildError(request, "匹配请求失败"));
} }
else else
{ {
...@@ -189,14 +359,32 @@ public class MyCardHelper ...@@ -189,14 +359,32 @@ public class MyCardHelper
{ {
string result = request.downloadHandler.text; string result = request.downloadHandler.text;
MatchResultObject matchResultObject = JsonUtility.FromJson<MatchResultObject>(result); MatchResultObject matchResultObject = JsonUtility.FromJson<MatchResultObject>(result);
onComplete(matchResultObject, null); 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) catch (Exception e)
{ {
onComplete(null, e.Message); Complete(onComplete, null, "匹配响应解析失败: " + e.Message);
} }
} }
} }
finally
{
ClearCurrentRequest(request);
}
}
} }
private bool IsError(UnityWebRequest request) private bool IsError(UnityWebRequest request)
......
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