using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using Unity.Netcode; using System.Threading.Tasks; namespace TcgEngine.Client { /// /// Main script for the client-side of the game, should be in game scene only /// Will connect to server, then connect to the game on that server (with uid) and then will send game settings /// During the game, will send all actions performed by the player and receive game refreshes /// public class GameClient : MonoBehaviour { //--- These settings are set in the menu scene and when the game start will be sent to server public static GameSettings game_settings = GameSettings.Default; public static PlayerSettings player_settings = PlayerSettings.Default; public static PlayerSettings ai_settings = PlayerSettings.DefaultAI; public static string observe_user = null; //Which user should it observe, null if not an obs //----- public UnityAction onConnectServer; public UnityAction onConnectGame; public UnityAction onPlayerReady; public UnityAction onGameStart; public UnityAction onGameEnd; //winner player_id public UnityAction onNewTurn; //current player_id public UnityAction onCardPlayed; public UnityAction onCardMoved; public UnityAction onCardSummoned; public UnityAction onCardTransformed; public UnityAction onCardDiscarded; public UnityAction onCardDraw; public UnityAction onValueRolled; public UnityAction onAbilityStart; public UnityAction onAbilityTargetCard; //Ability, Caster, Target public UnityAction onAbilityTargetPlayer; public UnityAction onAbilityTargetSlot; public UnityAction onAbilityEnd; public UnityAction onSecretTrigger; //Secret, Triggerer public UnityAction onSecretResolve; //Secret, Triggerer public UnityAction onAttackStart; //Attacker, Defender public UnityAction onAttackEnd; //Attacker, Defender public UnityAction onAttackPlayerStart; public UnityAction onAttackPlayerEnd; public UnityAction onCardDamaged; public UnityAction onPlayerDamaged; public UnityAction onCardHealed; public UnityAction onPlayerHealed; public UnityAction onChatMsg; //player_id, msg public UnityAction< string> onServerMsg; //msg public UnityAction onRefreshAll; private int player_id = 0; //Player playing on this device; private Game game_data; private bool observe_mode = false; private int observe_player_id = 0; private float timer = 0f; private Dictionary registered_commands = new Dictionary(); private static GameClient instance; protected virtual void Awake() { instance = this; Application.targetFrameRate = 120; } protected virtual void Start() { RegisterRefresh(GameAction.Connected, OnConnectedToGame); RegisterRefresh(GameAction.PlayerReady, OnPlayerReady); RegisterRefresh(GameAction.GameStart, OnGameStart); RegisterRefresh(GameAction.GameEnd, OnGameEnd); RegisterRefresh(GameAction.NewTurn, OnNewTurn); RegisterRefresh(GameAction.CardPlayed, OnCardPlayed); RegisterRefresh(GameAction.CardMoved, OnCardMoved); RegisterRefresh(GameAction.CardSummoned, OnCardSummoned); RegisterRefresh(GameAction.CardTransformed, OnCardTransformed); RegisterRefresh(GameAction.CardDiscarded, OnCardDiscarded); RegisterRefresh(GameAction.CardDrawn, OnCardDraw); RegisterRefresh(GameAction.ValueRolled, OnValueRolled); RegisterRefresh(GameAction.AttackStart, OnAttackStart); RegisterRefresh(GameAction.AttackEnd, OnAttackEnd); RegisterRefresh(GameAction.AttackPlayerStart, OnAttackPlayerStart); RegisterRefresh(GameAction.AttackPlayerEnd, OnAttackPlayerEnd); RegisterRefresh(GameAction.CardDamaged, OnCardDamaged); RegisterRefresh(GameAction.PlayerDamaged, OnPlayerDamaged); RegisterRefresh(GameAction.CardHealed, OnCardHealed); RegisterRefresh(GameAction.PlayerHealed, OnPlayerHealed); RegisterRefresh(GameAction.AbilityTrigger, OnAbilityTrigger); RegisterRefresh(GameAction.AbilityTargetCard, OnAbilityTargetCard); RegisterRefresh(GameAction.AbilityTargetPlayer, OnAbilityTargetPlayer); RegisterRefresh(GameAction.AbilityTargetSlot, OnAbilityTargetSlot); RegisterRefresh(GameAction.AbilityEnd, OnAbilityAfter); RegisterRefresh(GameAction.SecretTriggered, OnSecretTrigger); RegisterRefresh(GameAction.SecretResolved, OnSecretResolve); RegisterRefresh(GameAction.ChatMessage, OnChat); RegisterRefresh(GameAction.ServerMessage, OnServerMsg); RegisterRefresh(GameAction.RefreshAll, OnRefreshAll); TcgNetwork.Get().onConnect += OnConnectedServer; TcgNetwork.Get().Messaging.ListenMsg("refresh", OnReceiveRefresh); ConnectToAPI(); ConnectToServer(); } protected virtual void OnDestroy() { TcgNetwork.Get().onConnect -= OnConnectedServer; TcgNetwork.Get().Messaging.UnListenMsg("refresh"); } protected virtual void Update() { bool is_starting = game_data == null || game_data.state == GameState.Connecting; bool is_client = !game_settings.IsHost(); bool is_connecting = TcgNetwork.Get().IsConnecting(); bool is_connected = TcgNetwork.Get().IsConnected(); //Exit game scene if cannot connect after a while if (is_starting && is_client) { timer += Time.deltaTime; if (timer > 10f) { SceneNav.GoTo("Menu"); } } //Reconnect to server if (!is_starting && !is_connecting && is_client && !is_connected) { timer += Time.deltaTime; if (timer > 5f) { timer = 0f; ConnectToServer(); } } } //-------------------- public virtual void ConnectToAPI() { //Should already be logged in from the menu //If not connected, start in test mode (this means game scene was launched directly from Unity) if (!Authenticator.Get().IsSignedIn()) { Authenticator.Get().LoginTest("Player"); if (!player_settings.HasDeck()) { player_settings.deck = new UserDeckData(GameplayData.Get().test_deck); } if (!ai_settings.HasDeck()) { ai_settings.deck = new UserDeckData(GameplayData.Get().test_deck_ai); ai_settings.ai_level = GameplayData.Get().ai_level; } } //Set avatar, cardback based on your api data UserData udata = Authenticator.Get().UserData; if (udata != null) { player_settings.avatar = udata.GetAvatar(); player_settings.cardback = udata.GetCardback(); } } public virtual async void ConnectToServer() { await TimeTool.Delay(100); //Wait for initialization to finish if (TcgNetwork.Get().IsActive()) return; // Already connected if (game_settings.IsHost() && NetworkData.Get().solo_type == SoloType.Offline) { TcgNetwork.Get().StartHostOffline(); //WebGL dont support hosting a game, must join a dedicated server, in solo it starts a offline mode that doesn't use netcode at all } else if (game_settings.IsHost()) { TcgNetwork.Get().StartHost(NetworkData.Get().port); //Host a game, either solo or for P2P, still using netcode in solo to have consistant behavior when testing solo vs multi } else { TcgNetwork.Get().StartClient(game_settings.GetUrl(), NetworkData.Get().port); //Join server } } public virtual async void ConnectToGame(string uid) { await TimeTool.Delay(100); //Wait for initialization to finish if (!TcgNetwork.Get().IsActive()) return; //Not connected to server Debug.Log("Connect to Game: " + uid); MsgPlayerConnect nplayer = new MsgPlayerConnect(); nplayer.user_id = Authenticator.Get().UserID; nplayer.username = Authenticator.Get().Username; nplayer.game_uid = uid; nplayer.nb_players = game_settings.nb_players; nplayer.observer = game_settings.game_type == GameType.Observer; Messaging.SendObject("connect", ServerID, nplayer, NetworkDelivery.Reliable); } public virtual void SendGameSettings() { if (game_settings.IsOffline()) { //Solo mode, send both your settings and AI settings SendGameplaySettings(game_settings); SendPlayerSettingsAI(ai_settings); SendPlayerSettings(player_settings); } else { //Online mode, only send your own settings SendGameplaySettings(game_settings); SendPlayerSettings(player_settings); } } public virtual void Disconnect() { TcgNetwork.Get().Disconnect(); } private void RegisterRefresh(ushort tag, UnityAction callback) { RefreshEvent cmdevt = new RefreshEvent(); cmdevt.tag = tag; cmdevt.callback = callback; registered_commands.Add(tag, cmdevt); } public void OnReceiveRefresh(ulong client_id, FastBufferReader reader) { reader.ReadValueSafe(out ushort type); bool found = registered_commands.TryGetValue(type, out RefreshEvent command); if (found) { command.callback.Invoke(new SerializedData(reader)); } } //-------------------------- public void SendPlayerSettings(PlayerSettings psettings) { SendAction(GameAction.PlayerSettings, psettings, NetworkDelivery.ReliableFragmentedSequenced); } public void SendPlayerSettingsAI(PlayerSettings psettings) { SendAction(GameAction.PlayerSettingsAI, psettings, NetworkDelivery.ReliableFragmentedSequenced); } public void SendGameplaySettings(GameSettings settings) { SendAction(GameAction.GameSettings, settings, NetworkDelivery.ReliableFragmentedSequenced); } public void PlayCard(Card card, Slot slot) { MsgPlayCard mdata = new MsgPlayCard(); mdata.card_uid = card.uid; mdata.slot = slot; SendAction(GameAction.PlayCard, mdata); } public void AttackTarget(Card card, Card target) { MsgAttack mdata = new MsgAttack(); mdata.attacker_uid = card.uid; mdata.target_uid = target.uid; SendAction(GameAction.Attack, mdata); } public void AttackPlayer(Card card, Player target) { MsgAttackPlayer mdata = new MsgAttackPlayer(); mdata.attacker_uid = card.uid; mdata.target_id = target.player_id; SendAction(GameAction.AttackPlayer, mdata); } public void Move(Card card, Slot slot) { MsgPlayCard mdata = new MsgPlayCard(); mdata.card_uid = card.uid; mdata.slot = slot; SendAction(GameAction.Move, mdata); } public void CastAbility(Card card, AbilityData ability) { MsgCastAbility mdata = new MsgCastAbility(); mdata.caster_uid = card.uid; mdata.ability_id = ability.id; mdata.target_uid = ""; SendAction(GameAction.CastAbility, mdata); } public void SelectCard(Card card) { MsgCard mdata = new MsgCard(); mdata.card_uid = card.uid; SendAction(GameAction.SelectCard, mdata); } public void SelectPlayer(Player player) { MsgPlayer mdata = new MsgPlayer(); mdata.player_id = player.player_id; SendAction(GameAction.SelectPlayer, mdata); } public void SelectSlot(Slot slot) { SendAction(GameAction.SelectSlot, slot); } public void SelectChoice(int c) { MsgInt choice = new MsgInt(); choice.value = c; SendAction(GameAction.SelectChoice, choice); } public void SelectCost(int c) { MsgInt choice = new MsgInt(); choice.value = c; SendAction(GameAction.SelectCost, choice); } public void Mulligan(string[] cards) { MsgMulligan mdata = new MsgMulligan(); mdata.cards = cards; SendAction(GameAction.SelectMulligan, mdata); } public void CancelSelection() { SendAction(GameAction.CancelSelect); } public void SendChatMsg(string msg) { MsgChat chat = new MsgChat(); chat.msg = msg; chat.player_id = player_id; SendAction(GameAction.ChatMessage, chat); } public void EndTurn() { SendAction(GameAction.EndTurn); } public void Resign() { SendAction(GameAction.Resign); } public void SetObserverMode(int player_id) { observe_mode = true; observe_player_id = player_id; } public void SetObserverMode(string username) { observe_player_id = 0; //Default value of observe_user not found Game data = GetGameData(); foreach (Player player in data.players) { if (player.username == username) { observe_player_id = player.player_id; } } } public void SendAction(ushort type, T data, NetworkDelivery delivery = NetworkDelivery.Reliable) where T : INetworkSerializable { FastBufferWriter writer = new FastBufferWriter(128, Unity.Collections.Allocator.Temp, TcgNetwork.MsgSizeMax); writer.WriteValueSafe(type); writer.WriteNetworkSerializable(data); Messaging.Send("action", ServerID, writer, delivery); writer.Dispose(); } public void SendAction(ushort type, int data) { FastBufferWriter writer = new FastBufferWriter(128, Unity.Collections.Allocator.Temp, TcgNetwork.MsgSizeMax); writer.WriteValueSafe(type); writer.WriteValueSafe(data); Messaging.Send("action", ServerID, writer, NetworkDelivery.Reliable); writer.Dispose(); } public void SendAction(ushort type) { FastBufferWriter writer = new FastBufferWriter(128, Unity.Collections.Allocator.Temp, TcgNetwork.MsgSizeMax); writer.WriteValueSafe(type); Messaging.Send("action", ServerID, writer, NetworkDelivery.Reliable); writer.Dispose(); } //--- Receive Refresh ---------------------- protected virtual void OnConnectedServer() { ConnectToGame(game_settings.game_uid); onConnectServer?.Invoke(); } protected virtual void OnConnectedToGame(SerializedData sdata) { MsgAfterConnected msg = sdata.Get(); player_id = msg.player_id; game_data = msg.game_data; observe_mode = player_id < 0; //Will usually return -1 if its an observer if (observe_mode) SetObserverMode(observe_user); if (onConnectGame != null) onConnectGame.Invoke(); SendGameSettings(); } protected virtual void OnPlayerReady(SerializedData sdata) { MsgInt msg = sdata.Get(); int pid = msg.value; if (onPlayerReady != null) onPlayerReady.Invoke(pid); } private void OnGameStart(SerializedData sdata) { onGameStart?.Invoke(); } private void OnGameEnd(SerializedData sdata) { MsgPlayer msg = sdata.Get(); onGameEnd?.Invoke(msg.player_id); } private void OnNewTurn(SerializedData sdata) { MsgPlayer msg = sdata.Get(); onNewTurn?.Invoke(msg.player_id); } private void OnCardPlayed(SerializedData sdata) { MsgPlayCard msg = sdata.Get(); Card card = game_data.GetCard(msg.card_uid); onCardPlayed?.Invoke(card, msg.slot); } private void OnCardSummoned(SerializedData sdata) { MsgPlayCard msg = sdata.Get(); onCardSummoned?.Invoke(msg.slot); } private void OnCardMoved(SerializedData sdata) { MsgPlayCard msg = sdata.Get(); Card card = game_data.GetCard(msg.card_uid); onCardMoved?.Invoke(card, msg.slot); } private void OnCardTransformed(SerializedData sdata) { MsgCard msg = sdata.Get(); Card card = game_data.GetCard(msg.card_uid); onCardTransformed?.Invoke(card); } private void OnCardDiscarded(SerializedData sdata) { MsgCard msg = sdata.Get(); Card card = game_data.GetCard(msg.card_uid); onCardDiscarded?.Invoke(card); } private void OnCardDraw(SerializedData sdata) { MsgInt msg = sdata.Get(); onCardDraw?.Invoke(msg.value); } private void OnValueRolled(SerializedData sdata) { MsgInt msg = sdata.Get(); onValueRolled?.Invoke(msg.value); } private void OnAttackStart(SerializedData sdata) { MsgAttack msg = sdata.Get(); Card attacker = game_data.GetCard(msg.attacker_uid); Card target = game_data.GetCard(msg.target_uid); onAttackStart?.Invoke(attacker, target); } private void OnAttackEnd(SerializedData sdata) { MsgAttack msg = sdata.Get(); Card attacker = game_data.GetCard(msg.attacker_uid); Card target = game_data.GetCard(msg.target_uid); onAttackEnd?.Invoke(attacker, target); } private void OnAttackPlayerStart(SerializedData sdata) { MsgAttackPlayer msg = sdata.Get(); Card attacker = game_data.GetCard(msg.attacker_uid); Player target = game_data.GetPlayer(msg.target_id); onAttackPlayerStart?.Invoke(attacker, target); } private void OnAttackPlayerEnd(SerializedData sdata) { MsgAttackPlayer msg = sdata.Get(); Card attacker = game_data.GetCard(msg.attacker_uid); Player target = game_data.GetPlayer(msg.target_id); onAttackPlayerEnd?.Invoke(attacker, target); } private void OnCardDamaged(SerializedData sdata) { MsgCardValue msg = sdata.Get(); Card card = game_data.GetCard(msg.card_uid); onCardDamaged?.Invoke(card, msg.value); } private void OnPlayerDamaged(SerializedData sdata) { MsgPlayerValue msg = sdata.Get(); Player player = game_data.GetPlayer(msg.player_id); onPlayerDamaged?.Invoke(player, msg.value); } private void OnCardHealed(SerializedData sdata) { MsgCardValue msg = sdata.Get(); Card card = game_data.GetCard(msg.card_uid); onCardHealed?.Invoke(card, msg.value); } private void OnPlayerHealed(SerializedData sdata) { MsgPlayerValue msg = sdata.Get(); Player player = game_data.GetPlayer(msg.player_id); onPlayerHealed?.Invoke(player, msg.value); } private void OnAbilityTrigger(SerializedData sdata) { MsgCastAbility msg = sdata.Get(); AbilityData ability = AbilityData.Get(msg.ability_id); Card caster = game_data.GetCard(msg.caster_uid); onAbilityStart?.Invoke(ability, caster); } private void OnAbilityTargetCard(SerializedData sdata) { MsgCastAbility msg = sdata.Get(); AbilityData ability = AbilityData.Get(msg.ability_id); Card caster = game_data.GetCard(msg.caster_uid); Card target = game_data.GetCard(msg.target_uid); onAbilityTargetCard?.Invoke(ability, caster, target); } private void OnAbilityTargetPlayer(SerializedData sdata) { MsgCastAbilityPlayer msg = sdata.Get(); AbilityData ability = AbilityData.Get(msg.ability_id); Card caster = game_data.GetCard(msg.caster_uid); Player target = game_data.GetPlayer(msg.target_id); onAbilityTargetPlayer?.Invoke(ability, caster, target); } private void OnAbilityTargetSlot(SerializedData sdata) { MsgCastAbilitySlot msg = sdata.Get(); AbilityData ability = AbilityData.Get(msg.ability_id); Card caster = game_data.GetCard(msg.caster_uid); onAbilityTargetSlot?.Invoke(ability, caster, msg.slot); } private void OnAbilityAfter(SerializedData sdata) { MsgCastAbility msg = sdata.Get(); AbilityData ability = AbilityData.Get(msg.ability_id); Card caster = game_data.GetCard(msg.caster_uid); onAbilityEnd?.Invoke(ability, caster); } private void OnSecretTrigger(SerializedData sdata) { MsgSecret msg = sdata.Get(); Card secret = game_data.GetCard(msg.secret_uid); Card triggerer = game_data.GetCard(msg.triggerer_uid); onSecretTrigger?.Invoke(secret, triggerer); } private void OnSecretResolve(SerializedData sdata) { MsgSecret msg = sdata.Get(); Card secret = game_data.GetCard(msg.secret_uid); Card triggerer = game_data.GetCard(msg.triggerer_uid); onSecretResolve?.Invoke(secret, triggerer); } private void OnChat(SerializedData sdata) { MsgChat msg = sdata.Get(); onChatMsg?.Invoke(msg.player_id, msg.msg); } private void OnServerMsg(SerializedData sdata) { string msg = sdata.GetString(); onServerMsg?.Invoke(msg); } private void OnRefreshAll(SerializedData sdata) { MsgRefreshAll msg = sdata.Get(); game_data = msg.game_data; onRefreshAll?.Invoke(); } //-------------------------- public virtual bool IsReady() { return game_data != null && TcgNetwork.Get().IsConnected(); } public Player GetPlayer() { Game gdata = GetGameData(); return gdata.GetPlayer(GetPlayerID()); } public Player GetOpponentPlayer() { Game gdata = GetGameData(); return gdata.GetPlayer(GetOpponentPlayerID()); } public int GetPlayerID() { if (observe_mode) return observe_player_id; return player_id; } public int GetOpponentPlayerID() { return GetPlayerID() == 0 ? 1 : 0; } public virtual bool IsYourTurn() { Game game_data = GetGameData(); Player player = GetPlayer(); return IsReady() && game_data.IsPlayerTurn(player); } public bool IsObserveMode() { return observe_mode; } public Game GetGameData() { return game_data; } public bool HasEnded() { return game_data != null && game_data.HasEnded(); } private void OnApplicationQuit() { Resign(); //Auto Resign before closing the app. NOTE: doesn't seem to work since the msg dont have time to be sent before it closes } public bool IsHost { get { return TcgNetwork.Get().IsHost; } } public ulong ServerID { get { return TcgNetwork.Get().ServerID; } } public NetworkMessaging Messaging { get { return TcgNetwork.Get().Messaging; } } public static GameClient Get() { return instance; } } public class RefreshEvent { public ushort tag; public UnityAction callback; } }