From 4e2c4bda5da2753ac2bd497cdee53e6ca6c0babe Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Wed, 3 Dec 2025 21:36:33 -0500 Subject: [PATCH] feat: most of BracketScene done, need admin auth --- scenes/bracket_view.tscn | 29 +- scenes/main_menu.tscn | 8 +- scripts/BracketScene.cs | 78 ++- scripts/ConnectButtonUI.cs | 12 +- scripts/Connection.cs | 951 +++++++++++++++++++------------------ 5 files changed, 577 insertions(+), 501 deletions(-) diff --git a/scenes/bracket_view.tscn b/scenes/bracket_view.tscn index 563fcc4..3c66987 100644 --- a/scenes/bracket_view.tscn +++ b/scenes/bracket_view.tscn @@ -1,8 +1,10 @@ -[gd_scene load_steps=2 format=3 uid="uid://rl33x81cxlh0"] +[gd_scene load_steps=4 format=3 uid="uid://rl33x81cxlh0"] [ext_resource type="Script" uid="uid://dm25u0a2lqk2x" path="res://scripts/BracketScene.cs" id="1_dvj3m"] +[ext_resource type="Texture2D" uid="uid://ckmfi0cjgxgyk" path="res://assets/sprites/RedChip.png" id="2_7c11m"] +[ext_resource type="PackedScene" uid="uid://m542qwlp7hl7" path="res://scenes/board_screen.tscn" id="3_1511b"] -[node name="BracketView" type="Control" node_paths=PackedStringArray("players")] +[node name="BracketView" type="Control" node_paths=PackedStringArray("Players", "Matches")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -10,14 +12,29 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_dvj3m") -players = NodePath("Tree") +Players = NodePath("HBoxContainer/PlayerList") +Matches = NodePath("HBoxContainer/MatchList") +JoinButton = ExtResource("2_7c11m") +BoardScene = ExtResource("3_1511b") -[node name="Tree" type="Tree" parent="."] +[node name="HBoxContainer" type="HBoxContainer" parent="."] layout_mode = 1 -anchors_preset = 9 +anchors_preset = 15 +anchor_right = 1.0 anchor_bottom = 1.0 -offset_right = 348.0 +offset_top = 36.0 +grow_horizontal = 2 grow_vertical = 2 + +[node name="PlayerList" type="Tree" parent="HBoxContainer"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 columns = 3 column_titles_visible = true scroll_horizontal_enabled = false + +[node name="MatchList" type="Tree" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +columns = 3 +column_titles_visible = true diff --git a/scenes/main_menu.tscn b/scenes/main_menu.tscn index cca38e9..0b3d822 100644 --- a/scenes/main_menu.tscn +++ b/scenes/main_menu.tscn @@ -28,7 +28,7 @@ grow_vertical = 2 placeholder_text = "Server Address" script = ExtResource("1_l6cm7") -[node name="Button" type="Button" parent="." node_paths=PackedStringArray("addressUI", "errorLabel")] +[node name="Button" type="Button" parent="." node_paths=PackedStringArray("AddressUi", "ErrorLabel")] layout_mode = 1 anchors_preset = 8 anchor_left = 0.5 @@ -43,9 +43,9 @@ grow_horizontal = 2 grow_vertical = 2 text = "Connect" script = ExtResource("2_ekxnf") -addressUI = NodePath("../Address") -errorLabel = NodePath("../Label") -nextScene = ExtResource("3_bqqt6") +AddressUi = NodePath("../Address") +ErrorLabel = NodePath("../Label") +NextScene = ExtResource("3_bqqt6") [node name="Label" type="Label" parent="."] layout_mode = 1 diff --git a/scripts/BracketScene.cs b/scripts/BracketScene.cs index 4bbb9e6..690d268 100644 --- a/scripts/BracketScene.cs +++ b/scripts/BracketScene.cs @@ -4,15 +4,31 @@ using System.Collections.Generic; public partial class BracketScene : Control { - [Export] public Tree players; + [Export] public Tree Players; + [Export] public Tree Matches; + [Export] public Texture2D JoinButton; + [Export] public PackedScene BoardScene; + + private List _playerList; + private List _matchList; public override void _Ready() { - players.SetColumnTitle(0, "Name"); - players.SetColumnTitle(1, "Ready"); - players.SetColumnTitle(2, "Playing"); + Players.SetColumnTitle(0, "Name"); + Players.SetColumnTitle(1, "Ready"); + Players.SetColumnTitle(2, "Playing"); + Players.HideRoot = true; + Players.ButtonClicked += KickPlayer; + + + Matches.SetColumnTitle(0, "Match"); + Matches.SetColumnTitle(1, "Red"); + Matches.SetColumnTitle(2, "Yellow"); + Matches.HideRoot = true; + Matches.ButtonClicked += WatchGame; Connection.Instance.OnUpdatedPlayers += UpdatePlayers; + Connection.Instance.OnUpdatedMatches += UpdateMatches; Connection.Instance.OnBecomeAdmin += BecomeAdmin; Connection.Instance.OnWatchGameAck += TransitionToBoard; } @@ -20,19 +36,59 @@ public partial class BracketScene : Control public override void _ExitTree() { Connection.Instance.OnUpdatedPlayers -= UpdatePlayers; + Connection.Instance.OnUpdatedMatches -= UpdateMatches; + Connection.Instance.OnBecomeAdmin -= BecomeAdmin; + Connection.Instance.OnWatchGameAck -= TransitionToBoard; } private void UpdatePlayers(List playerList) { - players.Clear(); - foreach (var playerData in playerList) + Players.Clear(); + _playerList = playerList; + var root = Players.CreateItem(); + for (int i = 0; i < _playerList.Count; i++) { - var item = players.CreateItem(); - item.SetText(0, playerData.username); - item.SetText(1, playerData.isReady ? "Yes" : "No"); - item.SetText(2, playerData.isPlaying ? "Yes" : "No"); + var item = Players.CreateItem(root); + item.SetText(0, playerList[i].username); + item.SetText(1, playerList[i].isReady ? "Yes" : "No"); + item.SetText(2, playerList[i].isPlaying ? "Yes" : "No"); + if (Connection.Instance.IsAdmin) + { + item.AddButton(0, JoinButton, i, false, "Kick"); // TODO + } } } + + private void UpdateMatches(List matchList) + { + Matches.Clear(); + _matchList = matchList; + var root = Matches.CreateItem(); + for (int i = 0; i < matchList.Count; i++) + { + var item = Matches.CreateItem(root); + item.SetText(0, matchList[i].matchId.ToString()); + item.SetText(1, matchList[i].player1); + item.SetText(2, matchList[i].player2); + item.AddButton(0, JoinButton, i, false, "Watch"); + } + } + + private void WatchGame(TreeItem item, long column, long id, long mouseButtonIndex) + { + if (mouseButtonIndex == 1 && column == 0) + { + Connection.Instance.SendWatchGame(_matchList[(int) id].matchId); + } + } + + private void KickPlayer(TreeItem item, long column, long id, long mouseButtonIndex) + { + if (mouseButtonIndex == 1 && column == 0) + { + Connection.Instance.KickPlayer(_playerList[(int) id].username); + } + } private void BecomeAdmin() { @@ -42,5 +98,7 @@ public partial class BracketScene : Control private void TransitionToBoard() { // TODO + GD.Print("Watch game worked!"); + GetTree().ChangeSceneToPacked(BoardScene); } } diff --git a/scripts/ConnectButtonUI.cs b/scripts/ConnectButtonUI.cs index 989115a..2bd1171 100644 --- a/scripts/ConnectButtonUI.cs +++ b/scripts/ConnectButtonUI.cs @@ -3,20 +3,20 @@ using System; public partial class ConnectButtonUI : Button { - [Export] public TextEdit addressUI; - [Export] public Label errorLabel; - [Export] public PackedScene nextScene; + [Export] public TextEdit AddressUi; + [Export] public Label ErrorLabel; + [Export] public PackedScene NextScene; public override void _Pressed() { - if (Connection.Instance.Connect(addressUI.Text)) + if (Connection.Instance.Connect(AddressUi.Text)) { GD.Print("Success!"); - GetTree().ChangeSceneToPacked(nextScene); + GetTree().ChangeSceneToPacked(NextScene); } else { - errorLabel.Text = "Couldn't connect to server!"; + ErrorLabel.Text = "Couldn't connect to server!"; } base._Pressed(); } diff --git a/scripts/Connection.cs b/scripts/Connection.cs index f7d1d23..73003d7 100644 --- a/scripts/Connection.cs +++ b/scripts/Connection.cs @@ -5,478 +5,479 @@ using System.Threading; public partial class Connection : Node { - public const string WS_DEFAULT_ADDRESS = "wss://connect4.abunchofknowitalls.com"; - - public static Connection Instance { get; private set; } - - private WebSocketPeer _webSocket = new WebSocketPeer(); - private bool _firstConnect = true; - private Thread _gameListThread; - private bool _gameListThreadRunning; - - public event Action OnConnected; - public event Action OnReadyAcknowledged; - public event Action OnGameStart; - public event Action OnGameWin; - public event Action OnGameLoss; - public event Action OnGameDraw; - public event Action OnGameTerminated; - public event Action OnOpponentMove; - - public event Action OnWatchGameAck; - public event Action OnObserveWin; - public event Action OnObserveDraw; - public event Action OnObserveTerminated; - public event Action OnObserveMove; - public event Action> OnUpdatedMatches; - public event Action> OnUpdatedPlayers; - public event Action> OnTournamentEnd; - public event Action OnBecomeAdmin; - - - // Already prints to console - public event Action OnError; - - public bool IsAdmin { get; private set; } - public bool IsPlayer { get; private set; } - - public MatchData CurrentObservingMatch { get; private set; } - - private bool IsSocketOpen => _webSocket.GetReadyState() == WebSocketPeer.State.Open; - - public override void _Ready() - { - Instance = this; - _webSocket.SetHeartbeatInterval(5.0); - _webSocket.HeartbeatInterval = 5.0; - } - - public bool Connect(string address) - { - if (_webSocket.GetReadyState() == WebSocketPeer.State.Open) - { - return false; - } - - Error error = _webSocket.ConnectToUrl(address); - if (error != Error.Ok) - { - return false; - } - - _webSocket.Poll(); - - while (_webSocket.GetReadyState() == WebSocketPeer.State.Connecting) - { - _webSocket.Poll(); - Thread.Sleep(TimeSpan.FromMilliseconds(17)); - } - - if (_webSocket.GetReadyState() != WebSocketPeer.State.Open) - { - return false; - } - - _webSocket.SetHeartbeatInterval(5.0); - _webSocket.HeartbeatInterval = 5.0; - _firstConnect = false; - return true; - } - - public override void _ExitTree() - { - StopGameListRefreshLoop(); - } - - public override void _PhysicsProcess(double delta) - { - _webSocket.Poll(); - WebSocketPeer.State state = _webSocket.GetReadyState(); - if (state == WebSocketPeer.State.Closed && !_firstConnect) - { - StopGameListRefreshLoop(); - GetTree().Quit(); - } - - if (IsSocketOpen) - { - StartGameListRefreshLoop(); - while (_webSocket.GetAvailablePacketCount() > 0) - { - string message = _webSocket.GetPacket().GetStringFromUtf8(); - HandleServerMessage(message); - } - } - } - - // Player commands - public void SendConnect(string clientId) - { - if (string.IsNullOrWhiteSpace(clientId)) - { - GD.PrintErr("Client ID is required to CONNECT."); - return; - } - - clientId = clientId.Trim(); - - if (clientId.Contains(":")) - { - GD.PrintErr("Client ID cannot contain ':' characters."); - return; - } - - SendCommand("CONNECT", clientId); - } - - public void SendReady() - { - SendCommand("READY"); - } - - public void SendPlay(int column) - { - SendCommand("PLAY", column.ToString()); - } - - // Observer commands - public void UpdateGameList() - { - SendCommand("GAME", "LIST"); - } - - private void StartGameListRefreshLoop() - { - if (_gameListThreadRunning) - { - return; - } - - _gameListThreadRunning = true; - _gameListThread = new Thread(() => - { - while (_gameListThreadRunning) - { - if (IsSocketOpen) - { - UpdateGameList(); - UpdatePlayerList(); - } - - Thread.Sleep(TimeSpan.FromSeconds(5)); - } - }) - { - IsBackground = true - }; - _gameListThread.Start(); - } - - private void StopGameListRefreshLoop() - { - if (!_gameListThreadRunning) - { - return; - } - - _gameListThreadRunning = false; - _gameListThread?.Join(); - _gameListThread = null; - } - - public void UpdatePlayerList() - { - SendCommand("PLAYER", "LIST"); - } - - public void SendWatchGame(int matchID) - { - SendCommand("GAME", "WATCH:" + matchID); - } - - public void AdminAuth(string password) - { - if (IsAdmin) return; - SendCommand("ADMIN", "AUTH:" + password); - } - - - // Admin commands - public void KickPlayer(string playerId) - { - if (!IsAdmin) return; - SendCommand("ADMIN", "KICK:" + playerId); - } - - public void StartTournament() - { - if (!IsAdmin) return; - SendCommand("TOURNAMENT", "START"); - } - - public void TerminateGame() - { - if (!IsAdmin) return; - SendCommand("GAME", "TERMINATE"); - } - - public void SetTournamentWait(float waitTime) - { - if (!IsAdmin) return; - SendCommand("TOURNAMENT", "WAIT:" + waitTime); - } - - private void SendCommand(string header, string body = "") - { - if (!IsSocketOpen) - { - GD.PrintErr($"Cannot send {header}, socket is not open."); - return; - } - - string payload = string.IsNullOrEmpty(body) ? header : $"{header}:{body}"; - _webSocket.SendText(payload); - } - - private void HandleServerMessage(string message) - { - if (string.IsNullOrWhiteSpace(message)) - { - return; - } - - message = message.Trim(); - - string header; - string body; - int separatorIndex = message.IndexOf(':'); - if (separatorIndex >= 0) - { - header = message.Substring(0, separatorIndex).Trim(); - body = message.Substring(separatorIndex + 1).Trim(); - } - else - { - header = message.Trim(); - body = string.Empty; - } - - header = header.ToUpperInvariant(); - - switch (header) - { - case "CONNECT": - if (body.Equals("ACK", StringComparison.OrdinalIgnoreCase)) - { - IsPlayer = true; - OnConnected?.Invoke(); - } - - break; - case "READY": - if (body.Equals("ACK", StringComparison.OrdinalIgnoreCase)) - { - OnReadyAcknowledged?.Invoke(); - } - - break; - case "GAME": - HandleGameMessage(body); - break; - case "PLAYER": - HandlePlayerList(body); - break; - case "OPPONENT": - if (int.TryParse(body, out int column)) - { - OnOpponentMove?.Invoke(column); - } - else - { - GD.PrintErr($"Invalid opponent column: {body}"); - } - - break; - case "ADMIN": - if (body == "AUTH:ACK") - { - IsAdmin = true; - OnBecomeAdmin?.Invoke(); - } - - break; - case "TOURNAMENT": - HandleTournamentMessage(body); - break; - case "ERROR": - HandleErrorMessage(body); - GD.PrintErr($"Error: {body}"); - break; - default: - GD.Print($"Unhandled server message: {message}"); - break; - } - } - - private void HandleTournamentMessage(string body) - { - if (string.IsNullOrWhiteSpace(body)) - { - return; - } - - string[] segments = body.Split(':'); - string command = segments[0].Trim().ToUpperInvariant(); - string argument = segments.Length > 1 ? segments[1].Trim() : string.Empty; - - switch (command) - { - case "END": - { - List<(string, int)> playerScoreboard = new List<(string, int)>(); - string[] entries = segments[1].Split("|"); - foreach (string entry in entries) - { - string[] data = entry.Split(','); - playerScoreboard.Add((data[0], int.Parse(data[1]))); - } - - OnTournamentEnd?.Invoke(playerScoreboard); - break; - } - } - } - - private void HandlePlayerList(string body) - { - if (string.IsNullOrWhiteSpace(body)) - { - return; - } - - string[] segments = body.Split(':'); - string command = segments[0].Trim().ToUpperInvariant(); - string argument = segments.Length > 1 ? segments[1].Trim() : string.Empty; - - switch (command) - { - case "LIST": - { - List players = new List(); - - if (segments.Length < 2) - { - OnUpdatedPlayers?.Invoke(players); - break; - } - - string[] entries = segments[1].Split("|"); - foreach (string entry in entries) - { - string[] data = entry.Split(','); - players.Add(new PlayerData(data[0], bool.Parse(data[1]), bool.Parse(data[2]))); - } - - OnUpdatedPlayers?.Invoke(players); - break; - } - } - } - - private void HandleGameMessage(string body) - { - if (string.IsNullOrWhiteSpace(body)) - { - return; - } - - string[] segments = body.Split(':'); - string command = segments[0].Trim().ToUpperInvariant(); - string argument = segments.Length > 1 ? segments[1].Trim() : string.Empty; - - if (IsPlayer) - { - switch (command) - { - case "START": - bool isFirst = argument == "1" || argument.Equals("TRUE", StringComparison.OrdinalIgnoreCase); - OnGameStart?.Invoke(isFirst); - break; - case "WINS": - OnGameWin?.Invoke(); - break; - case "LOSS": - OnGameLoss?.Invoke(); - break; - case "DRAW": - OnGameDraw?.Invoke(); - break; - case "TERMINATED": - OnGameTerminated?.Invoke(); - break; - default: - GD.Print($"Unhandled GAME message: {body}"); - break; - } - } - else // Regular observer/admin - { - switch (command) - { - case "WIN": - OnObserveWin?.Invoke(segments[1]); - break; - case "MOVE": - OnObserveMove?.Invoke(segments[1], int.Parse(segments[2])); - break; - case "DRAW": - OnObserveDraw?.Invoke(); - break; - case "TERMINATED": - OnObserveTerminated?.Invoke(); - break; - case "LIST": - List matches = new List(); - - if (segments.Length < 2) - { - OnUpdatedMatches?.Invoke(matches); - break; - } - - string[] entries = segments[1].Split("|"); - foreach (string entry in entries) - { - string[] data = entry.Split(','); - matches.Add(new MatchData(int.Parse(data[0]), data[1], data[2])); - } - - OnUpdatedMatches?.Invoke(matches); - break; - case "WATCH": - string[] game = segments[2].Split(","); - CurrentObservingMatch = new MatchData(int.Parse(game[0]), game[1], game[2]); - OnWatchGameAck?.Invoke(); - break; - default: - GD.Print($"Unhandled GAME message: {body}"); - break; - } - } - } - - private void HandleErrorMessage(string body) - { - if (string.IsNullOrWhiteSpace(body)) - { - OnError?.Invoke("UNKNOWN", string.Empty); - return; - } - - string[] segments = body.Split(':'); - string code = segments.Length > 0 ? segments[0].Trim().ToUpperInvariant() : "UNKNOWN"; - string detail = segments.Length > 1 ? string.Join(":", segments, 1, segments.Length - 1).Trim() : string.Empty; - - OnError?.Invoke(code, detail); - } -} \ No newline at end of file + public const string WS_DEFAULT_ADDRESS = "wss://connect4.abunchofknowitalls.com"; + + public static Connection Instance { get; private set; } + + private WebSocketPeer _webSocket = new WebSocketPeer(); + private bool _firstConnect = true; + private Thread _gameListThread; + private bool _gameListThreadRunning; + + public event Action OnConnected; + public event Action OnReadyAcknowledged; + public event Action OnGameStart; + public event Action OnGameWin; + public event Action OnGameLoss; + public event Action OnGameDraw; + public event Action OnGameTerminated; + public event Action OnOpponentMove; + + public event Action OnWatchGameAck; + public event Action OnObserveWin; + public event Action OnObserveDraw; + public event Action OnObserveTerminated; + public event Action OnObserveMove; + public event Action> OnUpdatedMatches; + public event Action> OnUpdatedPlayers; + public event Action> OnTournamentEnd; + public event Action OnBecomeAdmin; + + + // Already prints to console + public event Action OnError; + + public bool IsAdmin { get; private set; } + public bool IsPlayer { get; private set; } + + public MatchData CurrentObservingMatch { get; private set; } + + private bool IsSocketOpen => _webSocket.GetReadyState() == WebSocketPeer.State.Open; + + public override void _Ready() + { + Instance = this; + _webSocket.SetHeartbeatInterval(5.0); + _webSocket.HeartbeatInterval = 5.0; + } + + public bool Connect(string address) + { + if (_webSocket.GetReadyState() == WebSocketPeer.State.Open) + { + return false; + } + + Error error = _webSocket.ConnectToUrl(address); + if (error != Error.Ok) + { + return false; + } + + _webSocket.Poll(); + + while (_webSocket.GetReadyState() == WebSocketPeer.State.Connecting) + { + _webSocket.Poll(); + Thread.Sleep(TimeSpan.FromMilliseconds(5)); + } + + if (_webSocket.GetReadyState() != WebSocketPeer.State.Open) + { + return false; + } + + _webSocket.SetHeartbeatInterval(5.0); + _webSocket.HeartbeatInterval = 5.0; + _firstConnect = false; + StartGameListRefreshLoop(); + return true; + } + + public override void _ExitTree() + { + StopGameListRefreshLoop(); + } + + public override void _PhysicsProcess(double delta) + { + _webSocket.Poll(); + WebSocketPeer.State state = _webSocket.GetReadyState(); + if ((state == WebSocketPeer.State.Closed || state == WebSocketPeer.State.Closing) && !_firstConnect) + { + StopGameListRefreshLoop(); + GD.PrintErr("Connection lost."); + //GetTree().Quit(); + } + + if (IsSocketOpen) + { + while (_webSocket.GetAvailablePacketCount() > 0) + { + string message = _webSocket.GetPacket().GetStringFromUtf8(); + HandleServerMessage(message); + } + } + } + + // Player commands + public void SendConnect(string clientId) + { + if (string.IsNullOrWhiteSpace(clientId)) + { + GD.PrintErr("Client ID is required to CONNECT."); + return; + } + + clientId = clientId.Trim(); + + if (clientId.Contains(":")) + { + GD.PrintErr("Client ID cannot contain ':' characters."); + return; + } + + SendCommand("CONNECT", clientId); + } + + public void SendReady() + { + SendCommand("READY"); + } + + public void SendPlay(int column) + { + SendCommand("PLAY", column.ToString()); + } + + // Observer commands + public void UpdateGameList() + { + SendCommand("GAME", "LIST"); + } + + private void StartGameListRefreshLoop() + { + if (_gameListThreadRunning) + { + return; + } + + _gameListThreadRunning = true; + _gameListThread = new Thread(() => + { + while (_gameListThreadRunning) + { + if (IsSocketOpen) + { + UpdateGameList(); + UpdatePlayerList(); + } + + Thread.Sleep(TimeSpan.FromSeconds(5)); + } + }) + { + IsBackground = true + }; + _gameListThread.Start(); + } + + private void StopGameListRefreshLoop() + { + if (!_gameListThreadRunning) + { + return; + } + + _gameListThreadRunning = false; + _gameListThread?.Join(); + _gameListThread = null; + } + + public void UpdatePlayerList() + { + SendCommand("PLAYER", "LIST"); + } + + public void SendWatchGame(int matchID) + { + SendCommand("GAME", "WATCH:" + matchID); + } + + public void AdminAuth(string password) + { + if (IsAdmin) return; + SendCommand("ADMIN", "AUTH:" + password); + } + + + // Admin commands + public void KickPlayer(string playerId) + { + if (!IsAdmin) return; + SendCommand("ADMIN", "KICK:" + playerId); + } + + public void StartTournament() + { + if (!IsAdmin) return; + SendCommand("TOURNAMENT", "START"); + } + + public void TerminateGame() + { + if (!IsAdmin) return; + SendCommand("GAME", "TERMINATE"); + } + + public void SetTournamentWait(float waitTime) + { + if (!IsAdmin) return; + SendCommand("TOURNAMENT", "WAIT:" + waitTime); + } + + private void SendCommand(string header, string body = "") + { + if (!IsSocketOpen) + { + GD.PrintErr($"Cannot send {header}, socket is not open."); + return; + } + + string payload = string.IsNullOrEmpty(body) ? header : $"{header}:{body}"; + _webSocket.SendText(payload); + } + + private void HandleServerMessage(string message) + { + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + + message = message.Trim(); + + string header; + string body; + int separatorIndex = message.IndexOf(':'); + if (separatorIndex >= 0) + { + header = message.Substring(0, separatorIndex).Trim(); + body = message.Substring(separatorIndex + 1).Trim(); + } + else + { + header = message.Trim(); + body = string.Empty; + } + + header = header.ToUpperInvariant(); + + switch (header) + { + case "CONNECT": + if (body.Equals("ACK", StringComparison.OrdinalIgnoreCase)) + { + IsPlayer = true; + OnConnected?.Invoke(); + } + + break; + case "READY": + if (body.Equals("ACK", StringComparison.OrdinalIgnoreCase)) + { + OnReadyAcknowledged?.Invoke(); + } + + break; + case "GAME": + HandleGameMessage(body); + break; + case "PLAYER": + HandlePlayerList(body); + break; + case "OPPONENT": + if (int.TryParse(body, out int column)) + { + OnOpponentMove?.Invoke(column); + } + else + { + GD.PrintErr($"Invalid opponent column: {body}"); + } + + break; + case "ADMIN": + if (body == "AUTH:ACK") + { + IsAdmin = true; + OnBecomeAdmin?.Invoke(); + } + + break; + case "TOURNAMENT": + HandleTournamentMessage(body); + break; + case "ERROR": + HandleErrorMessage(body); + GD.PrintErr($"Error: {body}"); + break; + default: + GD.Print($"Unhandled server message: {message}"); + break; + } + } + + private void HandleTournamentMessage(string body) + { + if (string.IsNullOrWhiteSpace(body)) + { + return; + } + + string[] segments = body.Split(':'); + string command = segments[0].Trim().ToUpperInvariant(); + string argument = segments.Length > 1 ? segments[1].Trim() : string.Empty; + + switch (command) + { + case "END": + { + List<(string, int)> playerScoreboard = new List<(string, int)>(); + string[] entries = segments[1].Split("|"); + foreach (string entry in entries) + { + string[] data = entry.Split(','); + playerScoreboard.Add((data[0], int.Parse(data[1]))); + } + + OnTournamentEnd?.Invoke(playerScoreboard); + break; + } + } + } + + private void HandlePlayerList(string body) + { + if (string.IsNullOrWhiteSpace(body)) + { + return; + } + + string[] segments = body.Split(':'); + string command = segments[0].Trim().ToUpperInvariant(); + string argument = segments.Length > 1 ? segments[1].Trim() : string.Empty; + + switch (command) + { + case "LIST": + { + List players = new List(); + + if (segments.Length < 2) + { + OnUpdatedPlayers?.Invoke(players); + break; + } + + string[] entries = segments[1].Split("|"); + foreach (string entry in entries) + { + string[] data = entry.Split(','); + players.Add(new PlayerData(data[0], bool.Parse(data[1]), bool.Parse(data[2]))); + } + + OnUpdatedPlayers?.Invoke(players); + break; + } + } + } + + private void HandleGameMessage(string body) + { + if (string.IsNullOrWhiteSpace(body)) + { + return; + } + + string[] segments = body.Split(':'); + string command = segments[0].Trim().ToUpperInvariant(); + string argument = segments.Length > 1 ? segments[1].Trim() : string.Empty; + + if (IsPlayer) + { + switch (command) + { + case "START": + bool isFirst = argument == "1" || argument.Equals("TRUE", StringComparison.OrdinalIgnoreCase); + OnGameStart?.Invoke(isFirst); + break; + case "WINS": + OnGameWin?.Invoke(); + break; + case "LOSS": + OnGameLoss?.Invoke(); + break; + case "DRAW": + OnGameDraw?.Invoke(); + break; + case "TERMINATED": + OnGameTerminated?.Invoke(); + break; + default: + GD.Print($"Unhandled GAME message: {body}"); + break; + } + } + else // Regular observer/admin + { + switch (command) + { + case "WIN": + OnObserveWin?.Invoke(segments[1]); + break; + case "MOVE": + OnObserveMove?.Invoke(segments[1], int.Parse(segments[2])); + break; + case "DRAW": + OnObserveDraw?.Invoke(); + break; + case "TERMINATED": + OnObserveTerminated?.Invoke(); + break; + case "LIST": + List matches = new List(); + + if (segments.Length < 2) + { + OnUpdatedMatches?.Invoke(matches); + break; + } + + string[] entries = segments[1].Split("|"); + foreach (string entry in entries) + { + string[] data = entry.Split(','); + matches.Add(new MatchData(int.Parse(data[0]), data[1], data[2])); + } + + OnUpdatedMatches?.Invoke(matches); + break; + case "WATCH": + string[] game = segments[2].Split(","); + CurrentObservingMatch = new MatchData(int.Parse(game[0]), game[1], game[2]); + OnWatchGameAck?.Invoke(); + break; + default: + GD.Print($"Unhandled GAME message: {body}"); + break; + } + } + } + + private void HandleErrorMessage(string body) + { + if (string.IsNullOrWhiteSpace(body)) + { + OnError?.Invoke("UNKNOWN", string.Empty); + return; + } + + string[] segments = body.Split(':'); + string code = segments.Length > 0 ? segments[0].Trim().ToUpperInvariant() : "UNKNOWN"; + string detail = segments.Length > 1 ? string.Join(":", segments, 1, segments.Length - 1).Trim() : string.Empty; + + OnError?.Invoke(code, detail); + } +}