Files
tcg-client/Assets/TcgEngine/Scripts/Api/ApiClient.cs
2025-09-17 16:22:06 +08:00

491 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading.Tasks;
using TcgEngine.UI;
using UnityEngine.Networking;
using UnityEngine.Events;
namespace TcgEngine
{
/// <summary>
/// API client communicates with the NodeJS web api
/// Can send requests and receive responses
/// </summary>
public class ApiClient : MonoBehaviour
{
public bool is_server;
public UnityAction<RegisterResponse> onRegister; //Triggered after register, even if failed
public UnityAction<LoginResponse> onLogin; //Triggered after login, even if failed
public UnityAction<LoginResponse> onRefresh; //Triggered after login refresh, even if failed
public UnityAction onLogout; //Triggered after logout
private string user_id = "";
private string username = "";
private string access_token = "";
private string refresh_token = "";
private string api_version = "";
private bool logged_in = false;
private bool expired = false;
private UserData udata = null;
private int sending = 0;
private string last_error = "";
private float refresh_timer = 0f;
private float online_timer = 0f;
private long expiration_timestamp = 0;
private const float online_duration = 60f * 5f; //5 min
private static ApiClient instance;
void Awake()
{
//API client should be on OnDestroyOnLoad
//dont assign here if already assigned cause new one will be destroyed in TheNetwork Awake
if(instance == null)
instance = this;
LoadTokens();
}
private void Update()
{
//Refresh access token or online status
Refresh();
}
private void LoadTokens()
{
if (!is_server && string.IsNullOrEmpty(user_id))
{
access_token = PlayerPrefs.GetString("tcg_access_token");
refresh_token = PlayerPrefs.GetString("tcg_refresh_token");
}
}
private void SaveTokens()
{
if (!is_server)
{
PlayerPrefs.SetString("tcg_access_token", access_token);
PlayerPrefs.SetString("tcg_refresh_token", refresh_token);
}
}
private async void Refresh()
{
if (!logged_in)
return;
//Check expiration
if (!expired)
{
long current = GetTimestamp();
expired = current > (expiration_timestamp - 10);
}
//Refresh access token when expired
refresh_timer += Time.deltaTime;
if (expired && refresh_timer > 5f)
{
refresh_timer = 0f;
await RefreshLogin(); //Try to relogin
}
//Refresh online status
online_timer += Time.deltaTime;
if (!expired && online_timer > online_duration)
{
online_timer = 0f;
await KeepOnline();
}
}
public async Task<RegisterResponse> Register(string email, string user, string password)
{
RegisterRequest data = new RegisterRequest();
data.email = email;
data.username = user;
data.password = password;
data.avatar = "";
return await Register(data);
}
public async Task<RegisterResponse> Register(RegisterRequest data)
{
Logout(); //Disconnect
string url = ServerURL + "/users/register";
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
RegisterResponse regist_res = ApiTool.JsonToObject<RegisterResponse>(res.data);
regist_res.success = res.success;
regist_res.error = res.error;
onRegister?.Invoke(regist_res);
return regist_res;
}
public async Task<LoginResponse> Login(string user, string password)
{
Logout(); //Disconnect
LoginRequest data = new LoginRequest();
data.password = password;
if (user.Contains("@"))
data.email = user;
else
data.username = user;
string url = ServerURL + "/auth";
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
LoginResponse login_res = GetLoginRes(res);
AfterLogin(login_res);
onLogin?.Invoke(login_res);
return login_res;
}
public async Task<LoginResponse> SteamLogin(string email, string username, string password)
{
Logout(); //Disconnect
LoginRequest data = new LoginRequest();
data.email = email;
data.username = username;
data.password = password;
string url = ServerURL + "/auth/steam";
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
LoginResponse login_res = GetLoginRes(res);
AfterLogin(login_res);
onLogin?.Invoke(login_res);
return login_res;
}
public async Task<LoginResponse> RefreshLogin()
{
string url = ServerURL + "/auth/refresh";
AutoLoginRequest data = new AutoLoginRequest();
data.refresh_token = refresh_token;
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
LoginResponse login_res = GetLoginRes(res);
AfterLogin(login_res);
onRefresh?.Invoke(login_res);
return login_res;
}
private LoginResponse GetLoginRes(WebResponse res)
{
LoginResponse login_res = ApiTool.JsonToObject<LoginResponse>(res.data);
login_res.success = res.success;
login_res.error = res.error;
//Uncomment to force having same client version as api
/*if (!is_server && !IsVersionValid())
{
login_res.error = "Invalid Version";
login_res.success = false;
}*/
return login_res;
}
private void AfterLogin(LoginResponse login_res)
{
last_error = login_res.error;
if (login_res.success)
{
user_id = login_res.id;
username = login_res.username;
access_token = login_res.access_token;
refresh_token = login_res.refresh_token;
api_version = login_res.version;
expiration_timestamp = GetTimestamp() + login_res.duration;
refresh_timer = 0f;
online_timer = 0f;
logged_in = true;
expired = false;
SaveTokens();
}
}
public async Task<UserData> LoadUserData()
{
udata = await LoadUserData(this.UserID);
return udata;
}
/// <summary>
/// 发送id或者用户名都可以返回玩家数据
/// </summary>
/// <param name="user">ID或用户名</param>
/// <returns></returns>
public async Task<UserData> LoadUserData(string user)
{
if (!IsConnected())
return null;
string url = ServerURL + "/users/" + user;
WebResponse res = await SendGetRequest(url);
UserData udata = null;
if (res.success)
{
udata = ApiTool.JsonToObject<UserData>(res.data);
Debug.Log($"获取玩家数据:{res.data}");
}
return udata;
}
public async Task<bool> KeepOnline()
{
if (!IsConnected())
return false;
//Keep player online
string url = ServerURL + "/auth/keep";
WebResponse res = await SendGetRequest(url);
expired = !res.success;
return res.success;
}
public async Task<bool> Validate()
{
if (!IsConnected())
return false;
//Check if connection is still valid
string url = ServerURL + "/auth/validate";
WebResponse res = await SendGetRequest(url);
expired = !res.success;
return res.success;
}
public void Logout()
{
user_id = "";
username = "";
access_token = "";
refresh_token = "";
api_version = "";
last_error = "";
logged_in = false;
onLogout?.Invoke();
SaveTokens();
}
public async void CreateMatch(Game game_data)
{
if (game_data.settings.game_type != GameType.Multiplayer)
return;
AddMatchRequest req = new AddMatchRequest();
req.players = new string[2];
req.players[0] = game_data.players[0].username;
req.players[1] = game_data.players[1].username;
req.tid = game_data.game_uid;
req.ranked = game_data.settings.IsRanked();
req.mode = game_data.settings.GetGameModeId();
string url = ServerURL + "/matches/add";
string json = ApiTool.ToJson(req);
WebResponse res = await SendPostRequest(url, json);
Debug.Log("Match Started! " + res.success);
}
public async void EndMatch(Game game_data, int winner_id)
{
if (game_data.settings.game_type != GameType.Multiplayer)
return;
Player player = game_data.GetPlayer(winner_id);
CompleteMatchRequest req = new CompleteMatchRequest();
req.tid = game_data.game_uid;
req.winner = player != null ? player.username : "";
string url = ServerURL + "/matches/complete";
string json = ApiTool.ToJson(req);
WebResponse res = await SendPostRequest(url, json);
Debug.Log("Match Completed! " + res.success);
}
public async Task<string> SendGetVersion()
{
string url = ServerURL + "/version";
WebResponse res = await SendGetRequest(url);
if (res.success)
{
VersionResponse version_data = ApiTool.JsonToObject<VersionResponse>(res.data);
api_version = version_data.version;
return api_version;
}
return null;
}
public async Task<WebResponse> SendGetRequest(string url)
{
return await SendRequest(url, WebRequest.METHOD_GET);
}
public async Task<WebResponse> SendPostRequest(string url, string json_data)
{
return await SendRequest(url, WebRequest.METHOD_POST, json_data);
}
public async Task<WebResponse> SendRequest(string url, string method, string json_data = "")
{
UnityWebRequest request = WebRequest.Create(url, method, json_data, access_token);
return await SendRequest(request);
}
private async Task<WebResponse> SendRequest(UnityWebRequest request)
{
int wait = 0;
int wait_max = request.timeout * 1000;
request.timeout += 1; //Add offset to make sure it aborts first
sending++;
var async_oper = request.SendWebRequest();
while (!async_oper.isDone)
{
await TimeTool.Delay(200);
wait += 200;
if (wait >= wait_max)
request.Abort(); //Abort to avoid unity errors on timeout
}
WebResponse response = WebRequest.GetResponse(request);
response.error = GetError(response);
last_error = response.error;
request.Dispose();
sending--;
return response;
}
public async Task<bool> UpdateUsername(string newUsername)
{
if(!IsConnected())
return false;
string url = ServerURL + "/users/username/edit";
EditUsernameRequest req = new EditUsernameRequest();
req.username = newUsername;
string json = ApiTool.ToJson(req);
WebResponse res = await SendPostRequest(url, json);
if (res.success)
{
Debug.Log($"更名前:{username}");
username = newUsername;
MainMenu.Get().RefreshUserData();
PlayerPrefs.SetString("tcg_last_user", newUsername);
Debug.Log($"更名后:{username}");
}
else
{
Debug.LogError("改名失败" + res.error);
}
return res.success;
}
private string GetError(WebResponse res)
{
if (res.success)
return "";
ErrorResponse err = ApiTool.JsonToObject<ErrorResponse>(res.data);
if (err != null)
return err.error;
else
return res.error;
}
public bool IsConnected()
{
return logged_in && !expired;
}
public bool IsLoggedIn()
{
return logged_in;
}
public bool IsExpired()
{
return expired;
}
public bool IsBusy()
{
return sending > 0;
}
public long GetTimestamp()
{
return System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
public string GetLastRequest()
{
return last_error;
}
public string GetLastError()
{
return last_error;
}
//Use this function if you want to prevent players to login with an outdated client
//Call it inside the login and loginrefresh functions after the api_version is set and return error if invalid
public bool IsVersionValid()
{
return ClientVersion == ServerVersion;
}
public UserData UserData { get { return udata; } }
public string UserID { get { return user_id; } set { user_id = value; } }
public string Username { get { return username; } set { username = value; } }
public string AccessToken { get { return access_token; } set { access_token = value; } }
public string RefreshToken { get { return refresh_token; } set { refresh_token = value; } }
public string ServerVersion { get { return api_version; } }
public string ClientVersion { get { return Application.version; } }
public static string ServerURL
{
get
{
NetworkData data = NetworkData.Get();
string protocol = data.api_https ? "https://" : "http://";
return protocol + data.api_url;
}
}
public static ApiClient Get()
{
// if (instance == null)
// instance = FindObjectOfType<ApiClient>();
return instance;
}
}
}