This commit is contained in:
yaoyanwei
2025-08-04 16:45:48 +08:00
parent 565aa16389
commit 2f2a601227
2296 changed files with 522745 additions and 93 deletions

View File

@@ -0,0 +1,439 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading.Tasks;
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> 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.username);
return udata;
}
public async Task<UserData> LoadUserData(string username)
{
if (!IsConnected())
return null;
string url = ServerURL + "/users/" + username;
WebResponse res = await SendGetRequest(url);
UserData udata = null;
if (res.success)
{
udata = ApiTool.JsonToObject<UserData>(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;
}
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bbcd533fe8d4d0a4aaa364f2ae5e4077
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,211 @@
using System;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// List of data structure used by the ApiClient requests and responses
/// </summary>
//--------- Requests -----------
[Serializable]
public struct LoginRequest
{
public string email;
public string username;
public string password;
}
[Serializable]
public struct AutoLoginRequest
{
public string refresh_token;
}
[Serializable]
public struct RegisterRequest
{
public string email;
public string username;
public string password;
public string avatar;
}
[Serializable]
public struct EditUserRequest
{
public string avatar;
public string cardback;
}
[Serializable]
public struct EditEmailRequest
{
public string email;
}
[Serializable]
public struct EditPasswordRequest
{
public string password_previous;
public string password_new;
}
[Serializable]
public struct FriendAddRequest
{
public string username;
}
[Serializable]
public struct AddMatchRequest
{
public string tid;
public string[] players;
public string mode;
public bool ranked;
}
[Serializable]
public struct CompleteMatchRequest
{
public string tid;
public string winner;
}
[Serializable]
public struct RewardGainRequest
{
public string reward;
}
[Serializable]
public struct BuyPackRequest
{
public string pack;
public int quantity;
}
[Serializable]
public struct BuyCardRequest
{
public string card;
public string variant;
public int quantity;
}
[Serializable]
public struct SellDuplicateRequest
{
//public string variant;
//public string rarity;
public int keep;
}
[Serializable]
public struct OpenPackRequest
{
public string pack;
}
//--------- Response -----------
[Serializable]
public struct VersionResponse
{
public string version;
}
[Serializable]
public struct RegisterResponse
{
public string id;
public string username;
public string version;
public bool success;
public string error;
}
[Serializable]
public struct LoginResponse
{
public string id;
public string username;
public string refresh_token;
public string access_token;
public int permission_level;
public int validation_level;
public int duration;
public string version;
public string error;
public bool success;
}
[Serializable]
public struct UserIdResponse
{
public string id;
public string username;
public string error;
}
[Serializable]
public struct MatchResponse
{
public string tid;
public string[] players;
public DateTime start;
public DateTime end;
public string winner;
public bool completed;
public MatchDataResponse[] udata;
}
[Serializable]
public struct MatchDataResponse
{
public string username;
public int rank;
public DeckData deck;
public RewardResponse reward;
}
[Serializable]
public struct RewardResponse
{
public string tid;
public int coins;
public int elo;
public int xp;
public string[] cards;
public string[] decks;
}
[Serializable]
public struct MarketResponse
{
public string seller;
public string card;
public int price;
public int quantity;
}
[Serializable]
public struct FriendResponse
{
public string username;
public string server_time;
public FriendData[] friends;
public FriendData[] friends_requests;
}
[System.Serializable]
public struct FriendData
{
public string username;
public string avatar;
public string last_online_time;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d5b5a0543605d2459b2da189f0c814b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using UnityEngine;
using System;
using System.Text;
using System.Security.Cryptography;
using UnityEngine.Events;
namespace TcgEngine
{
/// <summary>
/// Useful tool static functions for the ApiClient
/// </summary>
public class ApiTool : MonoBehaviour
{
// ----- Convertions ------
public static T JsonToObject<T>(string json)
{
try
{
T value = JsonUtility.FromJson<T>(json);
return value;
}
catch (Exception) { }
return (T)Activator.CreateInstance(typeof(T));
}
public static T[] JsonToArray<T>(string json)
{
ListJson<T> list = new ListJson<T>();
list.list = new T[0];
try
{
string wrap_json = "{ \"list\": " + json + "}";
list = JsonUtility.FromJson<ListJson<T>>(wrap_json);
return list.list;
}
catch (Exception) { }
return new T[0];
}
public static string ToJson(object data)
{
return JsonUtility.ToJson(data);
}
public static int ParseInt(string int_str, int default_val = 0)
{
bool success = int.TryParse(int_str, out int val);
return success ? val : default_val;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a80590b3916cb84fb73b14768bdf168
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,425 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Netcode;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Contain UserData retrieved from the web api database
/// </summary>
[System.Serializable]
public class UserData
{
public string id;
public string username;
public string email;
public string avatar;
public string cardback;
public int permission_level = 1;
public int validation_level = 1;
public int coins;
public int xp;
public int elo;
public int matches;
public int victories;
public int defeats;
public UserCardData[] cards;
public UserCardData[] packs;
public UserDeckData[] decks;
public string[] rewards;
public string[] avatars;
public string[] cardbacks;
public string[] friends;
public UserData()
{
cards = new UserCardData[0];
packs = new UserCardData[0];
decks = new UserDeckData[0];
rewards = new string[0];
avatars = new string[0];
cardbacks = new string[0];
friends = new string[0];
permission_level = 1;
coins = 10000;
elo = 1000;
}
public int GetLevel()
{
return Mathf.FloorToInt(xp / 1000) + 1;
}
public string GetAvatar()
{
if (avatar != null)
return avatar;
return "";
}
public string GetCardback()
{
if (cardback != null)
return cardback;
return "";
}
public void SetDeck(UserDeckData deck)
{
for(int i=0; i<decks.Length; i++)
{
if (decks[i].tid == deck.tid)
{
decks[i] = deck;
return;
}
}
//Not found
List<UserDeckData> ldecks = new List<UserDeckData>(decks);
ldecks.Add(deck);
this.decks = ldecks.ToArray();
}
public UserDeckData GetDeck(string tid)
{
foreach (UserDeckData deck in decks)
{
if (deck.tid == tid)
return deck;
}
return null;
}
public UserCardData GetCard(string tid, string variant)
{
foreach (UserCardData card in cards)
{
if (card.tid == tid && card.variant == variant)
return card;
}
return null;
}
public int GetCardQuantity(CardData card, VariantData variant)
{
return GetCardQuantity(card.id, variant.id, variant.is_default);
}
public int GetCardQuantity(string tid, string variant, bool default_variant = false)
{
if (cards == null)
return 0;
foreach (UserCardData card in cards)
{
if (card.tid == tid && card.variant == variant)
return card.quantity;
if (card.tid == tid && card.variant == "" && default_variant)
return card.quantity;
}
return 0;
}
public UserCardData GetPack(string tid)
{
foreach (UserCardData pack in packs)
{
if (pack.tid == tid)
return pack;
}
return null;
}
public int GetPackQuantity(string tid)
{
if (packs == null)
return 0;
foreach (UserCardData pack in packs)
{
if (pack.tid == tid)
return pack.quantity;
}
return 0;
}
public int CountUniqueCards()
{
if (cards == null)
return 0;
HashSet<string> unique_cards = new HashSet<string>();
foreach (UserCardData card in cards)
{
if (!unique_cards.Contains(card.tid))
unique_cards.Add(card.tid);
}
return unique_cards.Count;
}
public int CountCardType(VariantData variant)
{
int value = 0;
foreach (UserCardData card in cards)
{
if (card.variant == variant.id)
value += 1;
}
return value;
}
public bool HasDeckCards(UserDeckData deck)
{
foreach (UserCardData card in deck.cards)
{
bool default_variant = true; //Count "" variant as valid for compatibilty with older vers
if (GetCardQuantity(card.tid, card.variant, default_variant) < card.quantity)
return false;
}
return true;
}
public bool IsDeckValid(UserDeckData deck)
{
if (Authenticator.Get().IsApi())
return HasDeckCards(deck) && deck.IsValid();
return deck.IsValid();
}
public void AddDeck(UserDeckData deck)
{
List<UserDeckData> udecks = new List<UserDeckData>(decks);
udecks.Add(deck);
decks = udecks.ToArray();
foreach (UserCardData card in deck.cards)
{
AddCard(card.tid, card.variant, 1);
}
}
public void AddPack(string tid, int quantity)
{
bool found = false;
foreach (UserCardData pack in packs)
{
if (pack.tid == tid)
{
found = true;
pack.quantity += quantity;
}
}
if (!found)
{
UserCardData npack = new UserCardData();
npack.tid = tid;
npack.quantity = quantity;
List<UserCardData> apacks = new List<UserCardData>(packs);
apacks.Add(npack);
packs = apacks.ToArray();
}
}
public void AddCard(string tid, string variant, int quantity)
{
bool found = false;
foreach (UserCardData card in cards)
{
if (card.tid == tid && card.variant == variant)
{
found = true;
card.quantity += quantity;
}
}
if (!found)
{
UserCardData ncard = new UserCardData();
ncard.tid = tid;
ncard.variant = variant;
ncard.quantity = quantity;
List<UserCardData> acards = new List<UserCardData>(cards);
acards.Add(ncard);
cards = acards.ToArray();
}
}
public void AddReward(string tid)
{
if (!HasReward(tid))
{
List<string> arewards = new List<string>(rewards);
arewards.Add(tid);
rewards = arewards.ToArray();
}
}
public bool HasCard(string card_tid, string variant, int quantity = 1)
{
foreach (UserCardData card in cards)
{
if (card.tid == card_tid && card.variant == variant && card.quantity >= quantity)
return true;
}
return false;
}
public bool HasPack(string pack_tid, int quantity=1)
{
foreach (UserCardData pack in packs)
{
if (pack.tid == pack_tid && pack.quantity >= quantity)
return true;
}
return false;
}
public bool HasAvatar(string avatar_tid)
{
return avatars.Contains(avatar_tid);
}
public bool HasCardback(string cardback_tid)
{
return cardbacks.Contains(cardback_tid);
}
public bool HasReward(string reward_id)
{
foreach (string reward in rewards)
{
if (reward == reward_id)
return true;
}
return false;
}
public string GetCoinsString()
{
return coins.ToString();
}
public bool HasFriend(string username)
{
List<string> flist = new List<string>(friends);
return flist.Contains(username);
}
public void AddFriend(string username)
{
List<string> flist = new List<string>(friends);
if (!flist.Contains(username))
flist.Add(username);
friends = flist.ToArray();
}
public void RemoveFriend(string username)
{
List<string> flist = new List<string>(friends);
if (flist.Contains(username))
flist.Remove(username);
friends = flist.ToArray();
}
}
[System.Serializable]
public class UserDeckData : INetworkSerializable
{
public string tid;
public string title;
public UserCardData hero;
public UserCardData[] cards;
public UserDeckData() {}
public UserDeckData(string tid, string title)
{
this.tid = tid;
this.title = title;
hero = new UserCardData();
cards = new UserCardData[0];
}
public UserDeckData(DeckData deck)
{
tid = deck.id;
title = deck.title;
hero = new UserCardData(deck.hero, VariantData.GetDefault());
cards = new UserCardData[deck.cards.Length];
for (int i = 0; i < deck.cards.Length; i++)
{
cards[i] = new UserCardData(deck.cards[i], VariantData.GetDefault());
}
}
public int GetQuantity()
{
int count = 0;
foreach (UserCardData card in cards)
count += card.quantity;
return count;
}
public bool IsValid()
{
return !string.IsNullOrEmpty(tid) && !string.IsNullOrWhiteSpace(title) && GetQuantity() >= GameplayData.Get().deck_size;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref tid);
serializer.SerializeValue(ref title);
serializer.SerializeValue(ref hero);
NetworkTool.NetSerializeArray(serializer, ref cards);
}
public static UserDeckData Default
{
get
{
UserDeckData deck = new UserDeckData();
deck.tid = "";
deck.title = "";
deck.hero = new UserCardData();
deck.cards = new UserCardData[0];
return deck;
}
}
}
[System.Serializable]
public class UserCardData : INetworkSerializable
{
public string tid;
public string variant;
public int quantity;
public UserCardData() { tid = ""; variant = ""; quantity = 1; }
public UserCardData(string id, string v) { tid = id; variant = v; quantity = 1; }
public UserCardData(CardData card, VariantData variant)
{
this.tid = card != null ? card.id : "";
this.variant = variant != null ? variant.id : "";
this.quantity = 1;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref tid);
serializer.SerializeValue(ref variant);
serializer.SerializeValue(ref quantity);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3b3bfd879cfe2d4a920e9db8c539b62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: