From f946ebef1e5b6a7e1668240bae9b4c08d5c6acc2 Mon Sep 17 00:00:00 2001 From: "derek.holloway" Date: Thu, 6 Aug 2026 09:36:18 -0700 Subject: [PATCH] Build heartbeat chain --- MistoxServer/Client/MistoxClient.cs | 183 ++++++++++++++++-------- MistoxServer/Server/MistoxServer.cs | 54 ++++--- MistoxServer/Server/ServerConnection.cs | 2 - MistoxServer/mPacket/ResevedPackets.cs | 1 + Todo.txt | 55 ++----- 5 files changed, 173 insertions(+), 122 deletions(-) diff --git a/MistoxServer/Client/MistoxClient.cs b/MistoxServer/Client/MistoxClient.cs index f25ece2..0cb0564 100644 --- a/MistoxServer/Client/MistoxClient.cs +++ b/MistoxServer/Client/MistoxClient.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Numerics; +using System.Threading; using System.Threading.Tasks; using Mistox.ResevedPackets; @@ -13,15 +16,21 @@ namespace Mistox.Client { public string UserName { get; private set; } = ""; // My UserName public bool Alive { get; set; } = true; // Is the server running public bool isAuthenticated { get; private set; } = false; // Is the server authenticated - public ConcurrentDictionary clientConnections { get; private set; } // UserName, Connection + public ConcurrentDictionary clientConnections { get; private set; } = new(); // UserName, Connection + public Vector3 Position { get; set; } = new Vector3(0, 0, 0); // My world position + public float MaxNetworkRenderDistance { get; private set; } = 0; // The distance for connections public mTCPClient ServerConnection {get; private set; } // Global TCP Server Connection public UdpClient ClientSocket { get; private set; } // Global UDP Sender public UDPReceiver UdpReceiver { get; private set; } // Global UDP Receiver + private readonly string ConnectionKey = ""; // Server Connection Key - + private readonly int Port = 0; // The port of the connected socket + private readonly int HeartbeatInterval = 5000; // How often should we heartbeat + public MistoxClient(string userName, string connectionKey, IPEndPoint Server) { // Create and listen on UDP + Port = Server.Port; ClientSocket = new UdpClient( Server.Port ); UdpReceiver = new UDPReceiver( this ); UserName = userName; @@ -33,15 +42,79 @@ namespace Mistox.Client { ServerConnection = new mTCPClient( this, Server ); Console.WriteLine( "Connected to the server sucessfully" ); Alive = true; - } catch( Exception e ) { + } catch ( Exception e ) { Console.WriteLine( "An error has occured with the connection to the server. Error { " ); Console.WriteLine( e.ToString() ); Console.WriteLine( "}" ); Alive = false; } + + // Start the heartbeat + _ = RunAsync(Heartbeat); } - public async Task TryAuth() { + async Task RunAsync(Func work) { + try{ + await work(); + } catch (Exception ex) { + Console.WriteLine($"Error during background packet processing: {ex.Message}"); + } + } + +///////////////////////////// +// Heartbeat +///////////////////////////// + + async Task Heartbeat() { + while (Alive) { + await SendServer(new HeartbeatPacket { + Position = Position + }); + Thread.Sleep(HeartbeatInterval); + } + } + + void HeartbeatReceived(HeartbeatPacket packet) { + // Build a HashSet of server IP addresses for O(1) removal checks + var activeIps = new HashSet(packet.ServerList.Count); + for (int i = 0; i < packet.ServerList.Count; i++) { + activeIps.Add(packet.ServerList[i].IpAddress); + } + + // Add users that joined + foreach (UserTableObject cur in packet.ServerList) { + IPEndPoint remoteHost = new IPEndPoint(cur.IpAddress, Port); + if (!clientConnections.ContainsKey(remoteHost)) { + mUDPClient newConnection = new mUDPClient(this, remoteHost); + clientConnections[remoteHost] = newConnection; + } + } + + // Remove users that left + foreach (IPEndPoint key in clientConnections.Keys) { + if (!activeIps.Contains(key.Address)) { + clientConnections.TryRemove(key, out _); + } + } + + // Update distance status + float maxDistanceSq = MaxNetworkRenderDistance * MaxNetworkRenderDistance; + foreach (mUDPClient cur in clientConnections.Values) { + float dx = Position.X - cur.Position.X; + float dy = Position.Y - cur.Position.Y; + float dz = Position.Z - cur.Position.Z; + + // Fixed bug (dz * dz instead of dz + dz) & avoided Sqrt + float distanceSq = (dx * dx) + (dy * dy) + (dz * dz); + cur.WithinRange = distanceSq < maxDistanceSq; + } + } + +///////////////////////////// +// Auth +///////////////////////////// + + async Task TryAuth() { Console.WriteLine("Attempting to authenticate against the server..."); await ServerConnection.Send(new UserAuthPacket { UserName = UserName, @@ -49,71 +122,18 @@ namespace Mistox.Client { }); } - public async Task CheckAuthStatus(UserAuthPacket packet) { + async Task CheckAuthStatus(UserAuthPacket packet) { if (packet.AuthSuccessful == 1) { isAuthenticated = true; } ServerName = packet.ServerName; } -///////////////////////////// -// Receive Methods -///////////////////////////// - - public void UserConnected(UserConnectedPacket packet) { - IPEndPoint remoteUser = new IPEndPoint(packet.ClientIPAddress, packet.ClientPort); - mUDPClient user = new mUDPClient(this, remoteUser) { - UserName = packet.UserName - }; - clientConnections[remoteUser] = user; - InvokeOnUserConnected(user); - } - - public void UserDisconnected(UserDisconnectedPacket packet) { - mUDPClient client = null; - foreach(mUDPClient cur in clientConnections.Values) { - if (cur.UserName.ToLower() == packet.UserName.ToLower()) { - client = cur; - break; - } - } - if (client != null) { - InvokeOnUserDisconnected( client ); - client.Alive = false; - client.Dispose(); - clientConnections[client._remoteHost] = null; - } - } - ///////////////////////////// // Events ///////////////////////////// - public event Action onDataReceived; - public void InvokeOnReceived(dynamic DataPacket, mUDPClient remote = null){ - switch (DataPacket as object) { - case UserConnectedPacket packet: - UserConnected(packet); - break; - case UserDisconnectedPacket packet: - UserDisconnected(packet); - break; - case UserAuthPacket packet: - Task.Run(async () => { - try { - await CheckAuthStatus(packet); - } catch {} - }); - break; - case null: - Console.WriteLine("Received a null packet."); - break; - default: - onDataReceived?.Invoke(DataPacket); - break; - } - } - + // When (self) connects to the server public event Action onServerConnected; public void InvokeOnServerConnected(){ Alive = true; @@ -125,26 +145,67 @@ namespace Mistox.Client { onServerConnected?.Invoke(); } + // When any user connects to the server (self) hasn't seen yet + public event Action onUserJoined; + public void InvokeOnUserJoined(string UserName){ + onUserJoined?.Invoke(UserName); + } + + // When user is within range of (self) and network rendering starts public event Action onUserConnected; public void InvokeOnUserConnected(mUDPClient ConnectedUser){ onUserConnected?.Invoke(ConnectedUser); } + // When generic data is received + public event Action onDataReceived; + public void InvokeOnReceived(dynamic DataPacket, mUDPClient remote = null){ + switch (DataPacket as object) { + case HeartbeatPacket packet: + HeartbeatReceived(packet); + break; + case UserAuthPacket packet: + _ = RunAsync(async () => { + await CheckAuthStatus(packet); + }); + break; + case null: + Console.WriteLine("Received a null packet."); + break; + default: + onDataReceived?.Invoke(DataPacket); + break; + } + } + + // When user is no longer in range of (self) and network rendering stops public event Action onUserDisconnected; public void InvokeOnUserDisconnected(mUDPClient DisconnectedUser){ onUserDisconnected?.Invoke(DisconnectedUser); } + // When any user disconnects from the server that isn't (self) + public event Action onUserLeft; + public void InvokeOnUserLeft(string UserName){ + onUserLeft?.Invoke(UserName); + } + + // When (self) disconnects to the server public event Action onServerDisconnected; public void InvokeOnServerDisconnected(){ Alive = false; onServerDisconnected?.Invoke(); } +///////////////////////////// +// Send Methods +///////////////////////////// - public async Task SendAllClients(Packet data) { + public async Task SendAllClientsInRange(Packet data) { foreach( mUDPClient cur in clientConnections.Values ){ - await cur.Send( data ); + if (cur.WithinRange) { + await cur.Send( data ); + } } } diff --git a/MistoxServer/Server/MistoxServer.cs b/MistoxServer/Server/MistoxServer.cs index 091e056..e597a0a 100644 --- a/MistoxServer/Server/MistoxServer.cs +++ b/MistoxServer/Server/MistoxServer.cs @@ -9,8 +9,6 @@ namespace Mistox.Server { public class MistoxServer { - // Class Definitions - public string ServerName { get; private set; } = ""; // The name of the server public int MaxClients { get; set; } = 10; // The max players per server public float ConnectionRange { get; set; } = 40; // InGame distance for valid conections @@ -28,20 +26,18 @@ namespace Mistox.Server { Console.WriteLine( "The Server is initilized and waiting for clients to connect at port : " + ListenerAddress.Port ); } - async Task ProcessAuthRequest(ServerConnection user, UserAuthPacket packet) { - packet.ServerName = ServerName; - if (packet.ConnectionKey == ConnectionKey) { - packet.AuthSuccessful = 1; - Connections.TryRemove(user.UserName, out _); - user.UserName = packet.UserName; - Connections[packet.UserName] = user; - user.IsAuthenticated = true; - } else { - packet.AuthSuccessful = -1; + async Task RunAsync(Func work) { + try{ + await work(); + } catch (Exception ex) { + Console.WriteLine($"Error during background packet processing: {ex.Message}"); } - await user.Send(packet); } +///////////////////////////// +// Heartbeat +///////////////////////////// + async Task BuildServerList(ServerConnection user, HeartbeatPacket packet) { packet.ServerList = new List(); foreach( ServerConnection cur in Connections.Values) { @@ -54,18 +50,32 @@ namespace Mistox.Server { await user.Send( packet ); } - async Task RunAsync(Func work) { - try{ - await work(); - } catch (Exception ex) { - Console.WriteLine($"Error during background packet processing: {ex.Message}"); +///////////////////////////// +// Auth +///////////////////////////// + + async Task ProcessAuthRequest(ServerConnection user, UserAuthPacket packet) { + packet.ServerName = ServerName; + if (packet.ConnectionKey == ConnectionKey) { + packet.AuthSuccessful = 1; + Connections.TryRemove(user.UserName, out _); + user.UserName = packet.UserName; + Connections[packet.UserName] = user; + user.IsAuthenticated = true; + InvokeUserConnected(user); + } else { + packet.AuthSuccessful = -1; } + await user.Send(packet); } -// Functions +///////////////////////////// +// Events +///////////////////////////// public event Action onUserConnected; public void InvokeUserConnected( ServerConnection user ){ + Connections[user.UserName] = user; onUserConnected?.Invoke( user ); } @@ -78,6 +88,7 @@ namespace Mistox.Server { }); break; case HeartbeatPacket packet when user.IsAuthenticated: + user.Position = packet.Position; _ = RunAsync(async () => { await BuildServerList(user, packet); }); @@ -93,9 +104,14 @@ namespace Mistox.Server { public event Action onUserDisconnected; public void InvokeUserDisconnected( ServerConnection user ){ + Connections.Remove(user.UserName, out _); onUserDisconnected?.Invoke( user ); } +///////////////////////////// +// Send Methods +///////////////////////////// + public async Task SendAllClients(Packet data) { foreach( ServerConnection cur in Connections.Values ) { await cur.Send(data); diff --git a/MistoxServer/Server/ServerConnection.cs b/MistoxServer/Server/ServerConnection.cs index 6392baf..8843226 100644 --- a/MistoxServer/Server/ServerConnection.cs +++ b/MistoxServer/Server/ServerConnection.cs @@ -26,7 +26,6 @@ namespace Mistox.Server { _client.NoDelay = true; _client.ReceiveTimeout = _parent.ConnectionTimeoutSeconds * 1000; - _parent.InvokeUserConnected( this ); // Start the receive thread Task.Run(ReceiveThread); } @@ -93,7 +92,6 @@ namespace Mistox.Server { _sendLock.Dispose(); _cts.Dispose(); - _parent.Connections.TryRemove(UserName, out _); _parent.InvokeUserDisconnected(this); // Remove this item from the Dictionary on the Server for garbage collection } diff --git a/MistoxServer/mPacket/ResevedPackets.cs b/MistoxServer/mPacket/ResevedPackets.cs index 7e140cf..d2a015b 100644 --- a/MistoxServer/mPacket/ResevedPackets.cs +++ b/MistoxServer/mPacket/ResevedPackets.cs @@ -41,6 +41,7 @@ namespace Mistox.ResevedPackets { // Packet sent to all clients when a global position update occurrs [mPacket] public class HeartbeatPacket { + public Vector3 Position { get; set; } public List ServerList { get; set; } } diff --git a/Todo.txt b/Todo.txt index e40a64e..2638b38 100644 --- a/Todo.txt +++ b/Todo.txt @@ -1,46 +1,21 @@ The server receive thread needs to push the user it was received from up the chain -1. Allow the server to setup a server name Done -2. Allow the client to setup a user name Done -3. When a user connects add them to the Connections dictionary with a GUID. Done -3. Have the client send the server a welcome packet Done -4. Have the server use the welcome packet to fillout the user information on the server Done -5. Respond to the client from the server with a server list of UDPEndpoints -6. Instead of looping through a list of avalible users have a local user dictionary that can be used to lookup clients IPEndPoints +1. Allow the server to setup a server name Done +2. Allow the client to setup a user name Done +3. When a user connects add them to the Connections dictionary with a GUID. Done +4. Have the client send the server a welcome packet Done +5. Have the server use the welcome packet to fillout the user information on the server Done +6. Send the server the heartbeat location update every 5 seconds Done +7. Respond to the client with a server list of UDPEndpoints and heartbeat locations Done +8. Each client can figure out what connections it needs to make or drop via the list Done +10. When the TCP connection drops update the heartbeat list so all the clients remove the connection Done +11. Build a network cache table on the clients so obj's only send updates and not repeat packets + + +Set the network bubble distance [ render distance ] on the server and pass to the clients +Send a packet back to the client for if the lobby is full, notify them of why the connection failed Test that the datapacket is formatted correctly. The packet size might be off from the actual packet size -Have each client update the ingame location to the server every 5 seconds [ Heartbeat ] -For each user in the server calculate which other players are close enough for a connection -So the server needs to hold a table of what clients a client is already connected to -if a new player is now close enough update the client and the servers client table with that information -have the clients udp together -if a player goes out of the range update the clients that they no longer need the connection -drop the player from the table and notify the end user they can close the connection - -When user a joins a server - The IP gets added to the servers master list - The user needs to authenticate for username - -when the server connection closes - Notify all clients to drop the connections - Remove the user from the server table - -Use the Server TCP connection to notify all clients when a user disconnects - -Only send updates across the network. Dont send same state events -Need to setup a network buffer for Tagged Objects. ID - - -Connection Flow: - 1. Connection to server is made working - Server object is created working - ServerConnection gets added to the Connections table working - Server fires user connected event working - - 2. User sends an Auth Packet to server working - Server responds with Auth sucess or fail and marks the users auth status in the server table. needs work - After sucessfull auth share the users IPEndPoint with close connections not started - - 3. If the TCP connection closes not started + 1. If the TCP connection closes not started Disconnect the user and tell all clients that the user is gone not started \ No newline at end of file