Compare commits

...
27 Commits
Author SHA1 Message Date
derek.holloway 93badcb22d Try gitea release
Build and publish mistserver / build (push) Successful in 31s
2026-08-06 10:07:56 -07:00
derek.holloway b14715e759 Build the correct project
Build and publish mistserver / build (push) Failing after 15s
2026-08-06 10:03:19 -07:00
derek.holloway f93b78e5c1 fix directorys
Build and publish mistserver / build (push) Failing after 17s
2026-08-06 10:02:23 -07:00
derek.holloway e868d2116f Try using subdirectory
Build and publish mistserver / build (push) Failing after 4s
2026-08-06 10:00:43 -07:00
derek.holloway 4dc7d6b075 try package manager dotnet
Build and publish mistserver / build (push) Failing after 14s
2026-08-06 09:57:57 -07:00
derek.holloway 6f40e44224 remove old docker stuff
Build and publish mistserver / build (push) Failing after 1m5s
2026-08-06 09:54:20 -07:00
derek.holloway 291d0c3f07 Auto builder
Build and publish mistserver / build (push) Failing after 48s
2026-08-06 09:53:11 -07:00
derek.holloway e146eed3b2 Rename the dll 2026-08-06 09:49:55 -07:00
derek.holloway 3464e93aee Make sure everythings init correctly on boot 2026-08-06 09:45:28 -07:00
derek.holloway f946ebef1e Build heartbeat chain 2026-08-06 09:36:18 -07:00
derek.holloway 909718d8d5 Attach position to remote host 2026-08-05 14:20:24 -07:00
derek.holloway ac08c7d184 Change networked packets 2026-08-05 13:57:35 -07:00
derek.holloway 258b019f1a Cleanup async and respond with user list 2026-08-05 13:57:06 -07:00
derek.holloway 41805d7de9 Allow external remote address get 2026-08-05 13:56:12 -07:00
derek.holloway 486b53b402 Add comment for future task 2026-08-05 13:55:45 -07:00
derek.holloway 974b5c4f0a Impliment user auth on server 2026-08-04 08:55:33 -07:00
derek.holloway 007f87f986 Set all the required information on server when auth. 2026-08-04 08:26:27 -07:00
derek.holloway d446464f2a Genereal data cleanup 2026-08-04 07:56:25 -07:00
derek.holloway a78c040b09 Setup user auth packets 2026-08-03 13:43:44 -07:00
derek.holloway 827b1285a9 Move and rename 2026-08-03 13:29:17 -07:00
derek.holloway 19c5259528 minor fix 2026-08-03 13:28:06 -07:00
derek.holloway ad5b903d36 Part of the last commit 2026-08-03 13:26:46 -07:00
derek.holloway 46efb5bd65 Cleanup unnessisary interface classes 2026-08-03 13:25:28 -07:00
derek.holloway b83887cb2a Change manual packet numbering to a dictionary 2026-08-03 10:57:17 -07:00
derek.holloway 37a6b5d59b Tons of minor fixes 2026-08-03 10:01:31 -07:00
derek.holloway 873a6fa455 Chage remoteAddress to mUDPClient 2026-07-10 14:29:31 -07:00
derek.holloway d3d437b0c8 Split out the UDP listener for a single instance 2026-07-10 14:24:24 -07:00
19 changed files with 648 additions and 361 deletions

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
name: Build and publish mistserver
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: alpine-linux
steps:
- name: checkout
uses: actions/checkout@v4
- name: Setup .NET SDK
run: apk add dotnet10-sdk
- name: Restore Dependencies
run: dotnet restore ./MistoxServer
- name: Dotnet Publish
run: dotnet publish ./MistoxServer --configuration Release --output ./publish --no-restore
- name: Archive Production Artifacts
run: |
cd ./publish
zip -r ../mistserver-${{ github.ref_name }}.zip .
cd ..
# 2. Publish directly to Gitea Releases
- name: Create Gitea Release
uses: https://gitea.com/actions/release-action@main
with:
# Automatically uses the tag string pushed (e.g., v1.2.3)
name: "Release ${{ github.ref_name }}"
# Change this text to your desired version description (Supports Markdown)
body: |
## Mistserver Version ${{ github.ref_name }}
Automated deployment build compiled on Alpine Linux.
### Changelog
- Compiled via Gitea Actions
- Run ID: ${{ github.run_id }}
# Target the zipped artifact we generated in step 1
files: |
mistserver-*.zip
# Uses Gitea's automatically injected repository token for authorization
api_key: '${{ secrets.GITHUB_TOKEN }}'
+1 -1
View File
@@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MistoxServer\MistServerModule.csproj" />
<ProjectReference Include="..\MistoxServer\MistServer.csproj" />
</ItemGroup>
</Project>
+164 -25
View File
@@ -1,67 +1,173 @@
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;
namespace Mistox.Client {
public class MistoxClient {
public string ServerName { get; set; } = ""; // The Name of the server
public string ServerName { get; private set; } = ""; // The Name of the server
public string UserName { get; private set; } = ""; // My UserName
public bool Alive { get; set; } = true; // Is the server running
public mTCPClient ServerConnection;
public ConcurrentDictionary<string, mUDPClient> clientConnections;
public MistoxClient(IPEndPoint Server) {
public bool isAuthenticated { get; private set; } = false; // Is the server authenticated
public ConcurrentDictionary<IPEndPoint, mUDPClient> 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;
ConnectionKey = connectionKey;
// Create and connect to server on TCP
Console.WriteLine( "The client is initilized and trying to connect to the server at ip : " + Server.Address );
try {
ServerConnection = new mTCPClient( this, Server );
Console.WriteLine( "Connected to the server sucessfully" );
} catch( Exception e ) {
Alive = true;
} catch ( Exception e ) {
Console.WriteLine( "An error has occured with the connection to the server. Error { " );
Console.WriteLine( e.ToString() );
Console.WriteLine( "}" );
Alive = false;
}
Alive = true;
// Start the heartbeat
_ = RunAsync(Heartbeat);
}
public void UserConnected(UserConnectedPacket packet) {
mUDPClient user = new mUDPClient(this, packet.ClientIPAddress, packet.ClientPort) {
UserName = packet.UserName
};
clientConnections[packet.UserName] = user;
InvokeOnUserConnected(user);
async Task RunAsync(Func<Task> work) {
try{
await work();
} catch (Exception ex) {
Console.WriteLine($"Error during background packet processing: {ex.Message}");
}
}
public void UserDisconnected(UserDisconnectedPacket packet) {
mUDPClient copy = clientConnections[packet.UserName];
clientConnections[packet.UserName].Alive = false;
clientConnections[packet.UserName].Dispose();
clientConnections[packet.UserName] = null;
InvokeOnUserDisconnected( copy );
/////////////////////////////
// Heartbeat
/////////////////////////////
async Task Heartbeat() {
while (Alive) {
await SendServer(new HeartbeatPacket {
Position = Position
});
Thread.Sleep(HeartbeatInterval);
}
}
// Events
void HeartbeatReceived(HeartbeatPacket packet) {
// Build a HashSet of server IP addresses for O(1) removal checks
var activeIps = new HashSet<IPAddress>(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,
ConnectionKey = ConnectionKey
});
}
async Task CheckAuthStatus(UserAuthPacket packet) {
if (packet.AuthSuccessful == 1) {
isAuthenticated = true;
}
ServerName = packet.ServerName;
}
/////////////////////////////
// Events
/////////////////////////////
// When (self) connects to the server
public event Action onServerConnected;
public void InvokeOnServerConnected(){
Alive = true;
Task.Run(async () => {
try {
await TryAuth();
} catch {}
});
onServerConnected?.Invoke();
}
// When any user connects to the server (self) hasn't seen yet
public event Action<string> onUserJoined;
public void InvokeOnUserJoined(string UserName){
onUserJoined?.Invoke(UserName);
}
// When user is within range of (self) and network rendering starts
public event Action<mUDPClient> onUserConnected;
public void InvokeOnUserConnected(mUDPClient ConnectedUser){
onUserConnected?.Invoke(ConnectedUser);
}
// When generic data is received
public event Action<dynamic> onDataReceived;
public void InvokeOnReceived(dynamic DataPacket){
public void InvokeOnReceived(dynamic DataPacket, mUDPClient remote = null){
switch (DataPacket as object) {
case UserConnectedPacket packet:
UserConnected(packet);
case HeartbeatPacket packet:
HeartbeatReceived(packet);
break;
case UserDisconnectedPacket packet:
UserDisconnected(packet);
case UserAuthPacket packet:
_ = RunAsync(async () => {
await CheckAuthStatus(packet);
});
break;
case null:
Console.WriteLine("Received a null packet.");
@@ -72,16 +178,49 @@ namespace Mistox.Client {
}
}
// When user is no longer in range of (self) and network rendering stops
public event Action<mUDPClient> onUserDisconnected;
public void InvokeOnUserDisconnected(mUDPClient DisconnectedUser){
onUserDisconnected?.Invoke(DisconnectedUser);
}
// When any user disconnects from the server that isn't (self)
public event Action<string> 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 SendAllClientsInRange<Packet>(Packet data) {
foreach( mUDPClient cur in clientConnections.Values ){
if (cur.WithinRange) {
await cur.Send( data );
}
}
}
public async Task SendClient<Packet>(string UserName, Packet data) {
foreach( mUDPClient cur in clientConnections.Values ) {
if (cur.UserName.ToLower() == UserName.ToLower()) {
await cur.Send( data );
break;
}
}
}
public async Task SendServer<Packet>(Packet data) {
await ServerConnection.Send( data );
}
}
}
+20 -11
View File
@@ -9,16 +9,18 @@ using System.Threading.Tasks;
namespace Mistox.Client {
public class mTCPClient : IDisposable {
TcpClient _connection;
MistoxClient _parent;
private readonly TcpClient _connection;
private readonly MistoxClient _parent;
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
public mTCPClient( MistoxClient parent, IPEndPoint ServerAddress ) {
_parent = parent;
_connection = new TcpClient( AddressFamily.InterNetwork );
_connection.NoDelay = true;
_connection.Connect( ServerAddress );
_parent.InvokeOnServerConnected();
Thread RThread = new Thread(async () => { await ReceiveThread(); });
RThread.Start();
// Start the receive thread
Task.Run(ReceiveThread);
}
async Task ReceiveThread() {
@@ -32,19 +34,21 @@ namespace Mistox.Client {
await ns.ReadExactlyAsync(headerBuffer, 0, 3);
// Parse the header
byte packetbyte = headerBuffer[0];
ushort dataLength = (ushort)((headerBuffer[1] << 8) | headerBuffer[2]);
DataPacketHeader Header = new DataPacketHeader {
Type = (ushort)((headerBuffer[0] << 8) | headerBuffer[1]),
DataLength = (ushort)((headerBuffer[2] << 8) | headerBuffer[3])
};
// Make sure the data looks valid
if (dataLength > payloadBuffer.Length) {
if (Header.DataLength > payloadBuffer.Length) {
throw new Exception("Incoming TCP payload is too large for the pre-allocated buffer.");
}
// Read off the data
await ns.ReadExactlyAsync(payloadBuffer, 0, dataLength);
await ns.ReadExactlyAsync(payloadBuffer, 0, Header.DataLength);
// Create the dataobject
dynamic dataobject = DataPacket.Deserialize( packetbyte, dataLength, payloadBuffer.AsMemory(DataPacket.HeaderLength, dataLength) );
dynamic dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength, Header.DataLength) );
// Return the object
_parent.InvokeOnReceived(dataobject);
@@ -60,13 +64,18 @@ namespace Mistox.Client {
public async Task Send<T>(T packet) {
byte[] data = DataPacket.Serialize( packet );
await _connection.GetStream().WriteAsync( data, 0, data.Length );
await _sendLock.WaitAsync();
try {
await _connection.GetStream().WriteAsync( data, 0, data.Length );
} finally {
_sendLock.Release();
}
}
public void Dispose() {
_parent.InvokeOnServerDisconnected();
_connection.Close();
_connection = null;
_connection.Dispose();
}
}
}
+16 -48
View File
@@ -1,72 +1,40 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
// Network Sync
namespace Mistox.Client {
public class mUDPClient : IDisposable {
public string UserName { get; set; } = ""; // The remote players UserName
public bool Alive { get; set; } = true; // Is the connection alive
public bool WithinRange { get; set; } = true; // Is the user close enough?
public Vector3 Position { get; set; } = new Vector3(0, 0, 0); // The remote location of the player
Socket _udpClient;
IPEndPoint _remoteHost;
MistoxClient _parent;
private IPEndPoint _remoteHost { get; set; } // The remote IP Address
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
private readonly MistoxClient _parent;
public mUDPClient( MistoxClient parent, long RemoteIP, int Port ) {
public mUDPClient( MistoxClient parent, IPEndPoint remoteHost ) {
_parent = parent;
_remoteHost = new IPEndPoint( RemoteIP, Port );
_udpClient = new Socket( AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp );
_udpClient.SetSocketOption( SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true );
_udpClient.SetSocketOption( SocketOptionLevel.IP, SocketOptionName.IpTimeToLive, 128 );
_udpClient.Bind( new IPEndPoint( IPAddress.Any, Port ) );
_remoteHost = remoteHost;
_parent.Alive = true;
Thread Client = new Thread(async() => { await ReceiveThread(); });
Client.Start();
}
async Task ReceiveThread() {
byte[] buffer = new byte[65535];
while( _parent.Alive ) {
try {
// Read the data out of the UDP packet with a buffer that has the maximum possible UDP packet size
// As UDP reads the whole next packet then drops. Im guaranteed to receive 1 packet at a time
int bytesRead = await _udpClient.ReceiveAsync( buffer );
// Create the data readers
ReadOnlyMemory<byte> dataMemory = buffer.AsMemory(0, bytesRead);
ReadOnlySpan<byte> dataPacket = dataMemory.Span;
// Split apart the packet
byte dataType = dataPacket[0];
ushort DataLength = (ushort)((dataPacket[1] << 8) | dataPacket[2]);
ReadOnlyMemory<byte> data = dataMemory.Slice(DataPacket.HeaderLength, DataLength);
// Extract the object
dynamic buildObj = DataPacket.Deserialize( dataType, DataLength, data );
// Return the object
_parent.InvokeOnReceived( buildObj );
} catch( Exception e ) {
Console.WriteLine( "UDP Receive exception : " + e.Message );
}
}
}
public async Task Send<T>( T Data ) {
byte[] byteData = DataPacket.Serialize( Data );
await _udpClient.SendToAsync( byteData, SocketFlags.None, _remoteHost );
await _sendLock.WaitAsync();
try {
await _parent.ClientSocket.SendAsync( byteData, byteData.Length, _remoteHost );
} finally {
_sendLock.Release();
}
}
public void Dispose() {
_parent.Alive = false;
_udpClient.Close();
_udpClient = null;
_parent.ClientSocket.Close();
_parent.ClientSocket.Dispose();
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace Mistox.Client {
public class UDPReceiver : IDisposable {
private readonly MistoxClient _parent;
public UDPReceiver( MistoxClient parent) {
_parent = parent;
// Start the receive thread
Task.Run(ReceiveThread);
}
async Task ReceiveThread() {
while( _parent.Alive ) {
try {
// Read the data out of the UDP packet with a buffer that has the maximum possible UDP packet size
// As UDP reads the whole next packet then drops. Im guaranteed to receive 1 packet at a time
UdpReceiveResult udpPacket = await _parent.ClientSocket.ReceiveAsync();
// Try find the user
mUDPClient client = null;
bool clientExists = _parent.clientConnections.TryGetValue(udpPacket.RemoteEndPoint, out client);
if (clientExists) {
// Create the data readers
ReadOnlyMemory<byte> dataMemory = udpPacket.Buffer.AsMemory();
ReadOnlySpan<byte> dataPacket = dataMemory.Span;
// Create the packet header
DataPacketHeader Header = new DataPacketHeader {
Type = (ushort)((dataPacket[0] << 8) | dataPacket[1]),
DataLength = (ushort)((dataPacket[2] << 8) | dataPacket[3])
};
// Slice out the data part of the packet
ReadOnlyMemory<byte> data = dataMemory.Slice(DataPacket.HeaderLength, Header.DataLength);
// Extract the object
dynamic buildObj = DataPacket.Deserialize( Header, data );
// Return the object
_parent.InvokeOnReceived( buildObj, client );
}
} catch( Exception e ) {
Console.WriteLine( "UDP Receive exception : " + e.Message );
}
}
}
public void Dispose() {
_parent.Alive = false;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using System;
using System.Net;
using Mistox.Attributes;
using Mistox.Client;
using Mistox.Server;
namespace Mistox {
public class GameServer {
public static MistoxServer newServer( string serverName, int port ) {
// Build the network packet list
NetworkPacketDictionary.BuildNetworkPacketList();
// Set the listener address
IPEndPoint listenerAddress = new IPEndPoint( IPAddress.Any, port );
// Return the initlized server instance
return new MistoxServer(listenerAddress, serverName);
}
public static MistoxClient newClient( string UserName, string ServerIPOrHostName, int Port, string ConnectionKey = "" ) {
// Build the network packet list
NetworkPacketDictionary.BuildNetworkPacketList();
// Get the remote IP from the DNSName
int index = 0;
IPAddress[] remoteAddress = Dns.GetHostAddresses(ServerIPOrHostName);
for( int i=0; i< remoteAddress.Length; i++ ) {
if( remoteAddress[i].AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork || remoteAddress [i].AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 ) {
index = i;
}
}
string ipAddress = remoteAddress[index].ToString();
// If a remote address exists
if ( remoteAddress.Length == 0) {
Console.WriteLine("The server at " + ServerIPOrHostName + " doesn't exit or cannot be found");
return null;
}
// Resolve to an IPEndPoint
IPHostEntry host = Dns.GetHostEntry( ipAddress );
IPEndPoint ServerDenstination = new IPEndPoint(0, 0);
foreach( IPAddress entry in host.AddressList ) {
if ( entry.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ) {
ServerDenstination = new IPEndPoint( entry, Port );
}
}
// Return the initlized client object
return new MistoxClient( UserName, ConnectionKey, ServerDenstination );
}
}
}
-52
View File
@@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Mistox.Client;
namespace Mistox {
public class ClientInterface {
public static MistoxClient serverInstance;
public event Action onServerConnected;
public event Action<mUDPClient> onUserConnected;
public event Action<dynamic> onDataReceived;
public event Action<mUDPClient> onUserDisconnected;
public event Action onServerDisconnected;
public ClientInterface( string IpOrHostName, int Port ) {
// Get Server IP
IPHostEntry host = Dns.GetHostEntry( IpOrHostName );
IPEndPoint ServerDenstination = new IPEndPoint(0, 0);
foreach( IPAddress entry in host.AddressList ) {
if ( entry.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ) {
ServerDenstination = new IPEndPoint( entry, Port );
}
}
// Start and wire up the Mistox server client
serverInstance = new MistoxClient( ServerDenstination );
serverInstance.onServerConnected += onServerConnected;
serverInstance.onUserConnected += (mUDPClient endpoint) => { onUserConnected?.Invoke(endpoint); };
serverInstance.onDataReceived += (dynamic data) => { onDataReceived?.Invoke(data); };
serverInstance.onUserDisconnected += (mUDPClient endpoint) => { onUserDisconnected?.Invoke(endpoint); };
serverInstance.onServerDisconnected += onServerDisconnected;
}
public async Task SendAllClients<Packet>(Packet data) {
foreach(KeyValuePair<string, mUDPClient> cur in serverInstance.clientConnections){
await cur.Value.Send( data );
}
}
public async Task SendClient<Packet>(string UserName, Packet data) {
await serverInstance.clientConnections[UserName].Send( data );
}
public async Task SendServer<Packet>(Packet data) {
await serverInstance.ServerConnection.Send( data );
}
}
}
-33
View File
@@ -1,33 +0,0 @@
using System;
using System.Net;
using Mistox.Attributes;
namespace Mistox {
public class mServer {
public static ServerInterface newServer( string serverName, int port ) {
NetworkPacketDictionary.BuildNetworkPacketList();
return new ServerInterface(port, serverName);
}
public static ClientInterface newClient( string ServerIPOrHostName, int Port ) {
NetworkPacketDictionary.BuildNetworkPacketList();
int index = 0;
IPAddress[] remoteAddress = Dns.GetHostAddresses(ServerIPOrHostName);
for( int i=0; i< remoteAddress.Length; i++ ) {
if( remoteAddress[i].AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork || remoteAddress [i].AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 ) {
index = i;
}
}
string ipAddress = remoteAddress[index].ToString();
if ( remoteAddress.Length > 0) {
return new ClientInterface(ipAddress, Port);
} else {
Console.WriteLine("The server at " + ServerIPOrHostName + " doesn't exit or cannot be found");
return null;
}
}
}
}
-42
View File
@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Mistox.Server;
namespace Mistox {
public class ServerInterface {
public static ServerEntity ServerInstance; // Server Data
public event Action<ServerConnection> onUserConnected; // User connected event handler
public event Action<dynamic> onDataReceived; // Data received event handler
public event Action<ServerConnection> onUserDisconnected; // User disconnected event handler
public ServerInterface( int port, string serverName ) {
IPEndPoint listenerAddress = new IPEndPoint( IPAddress.Any, port );
// Initilize the server
ServerInstance = new ServerEntity(listenerAddress);
ServerInstance.ServerName = serverName;
// Setup the event handlers
ServerInstance.onUserConnected += (ServerConnection connectedUser) => { onUserConnected?.Invoke(connectedUser); };
ServerInstance.onDataReceived += (dynamic data) => { onDataReceived?.Invoke(data); };
ServerInstance.onUserDisconnected += (ServerConnection disconnectedUser) => { onUserDisconnected?.Invoke(disconnectedUser); };
Console.WriteLine( "The Server is initilized and waiting for clients to connect at port : " + port );
}
public async Task SendAllClients<Packet>(Packet data) {
foreach( KeyValuePair<string, ServerConnection> cur in ServerInstance.Connections ) {
await cur.Value.Send(data);
}
}
public async Task SendClient<Packet>(string UserName, Packet data) {
await ServerInstance.Connections[UserName].Send( data );
}
}
}
File renamed without changes.
+98 -16
View File
@@ -1,43 +1,125 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Mistox.ResevedPackets;
namespace Mistox.Server {
public class ServerEntity {
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
public string ConnectionKey { get; set; } = ""; // The connection key for the server
public int ConnectionTimeoutSeconds { get; set; } = 10; // If no reply in time kill the connection
public bool Alive { get; set; } = true; // Is the server running
public ConcurrentDictionary<string, ServerConnection> Connections = new(); // All the active connections. key is username
public string ServerName { get; 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
public int ConnectionTimeoutSeconds { get; set; } = 10; // If no reply in time kill the connection
public bool Alive { get; set; } = true; // Is the server running
ServerListener Listener { get; set;}
public ConcurrentDictionary<string, ServerConnection> Connections;
private readonly ServerListener Listener; // The Connection Listener singleton
public ServerEntity(IPEndPoint ListenerAddress) {
public MistoxServer(IPEndPoint ListenerAddress, string serverName) {
ServerName = serverName;
Listener = new ServerListener(ListenerAddress, this);
Connections = new ConcurrentDictionary<string, ServerConnection>();
Console.WriteLine( "The Server is initilized and waiting for clients to connect at port : " + ListenerAddress.Port );
}
// Functions
async Task RunAsync(Func<Task> work) {
try{
await work();
} catch (Exception ex) {
Console.WriteLine($"Error during background packet processing: {ex.Message}");
}
}
/////////////////////////////
// Heartbeat
/////////////////////////////
async Task BuildServerList(ServerConnection user, HeartbeatPacket packet) {
packet.ServerList = new List<UserTableObject>();
foreach( ServerConnection cur in Connections.Values) {
packet.ServerList.Add( new UserTableObject() {
UserName = cur.UserName,
Position = cur.Position,
IpAddress = cur.getRemote()
} );
}
await user.Send( packet );
}
/////////////////////////////
// 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);
}
/////////////////////////////
// Events
/////////////////////////////
public event Action<ServerConnection> onUserConnected;
public void InvokeUserConnected( ServerConnection user ){
Connections[user.UserName] = user;
onUserConnected?.Invoke( user );
}
public event Action<dynamic> onDataReceived;
public void InvokeDataReceived( dynamic data ){
onDataReceived?.Invoke( data );
public event Action<ServerConnection, object> onDataReceived;
public void InvokeDataReceived( ServerConnection user, dynamic data ){
switch (data) {
case UserAuthPacket packet when !user.IsAuthenticated:
_ = RunAsync(async () => {
await ProcessAuthRequest(user, packet);
});
break;
case HeartbeatPacket packet when user.IsAuthenticated:
user.Position = packet.Position;
_ = RunAsync(async () => {
await BuildServerList(user, packet);
});
break;
case null:
Console.WriteLine("Received a null packet.");
break;
default:
onDataReceived?.Invoke(user, data);
break;
}
}
public event Action<ServerConnection> onUserDisconnected;
public void InvokeUserDisconnected( ServerConnection user ){
Connections.Remove(user.UserName, out _);
onUserDisconnected?.Invoke( user );
}
/////////////////////////////
// Send Methods
/////////////////////////////
public async Task SendAllClients<Packet>(Packet data) {
foreach( ServerConnection cur in Connections.Values ) {
await cur.Send(data);
}
}
public async Task SendClient<Packet>(string UserName, Packet data) {
await Connections[UserName].Send( data );
}
}
}
+40 -33
View File
@@ -10,24 +10,24 @@ namespace Mistox.Server {
public string UserName { get; set; } = ""; // The Players UserName
public bool IsAuthenticated { get; set; } = false; // Has the player authenticated
public Vector3 Position { get; set; } // The ingame position of the player
public DateTime LastHeartbeat { get; set; } = DateTime.UtcNow; // Connection heartbeat for integrity
public Vector3 Position { get; set; } = new Vector3(0, 0, 0); // The ingame position of the player
public bool Connected { get; set; } = true; // Is the connection valid
ServerEntity _parent;
TcpClient _client;
IPEndPoint _remoteAddress;
private readonly MistoxServer _parent;
private readonly TcpClient _client;
private readonly IPEndPoint _remoteAddress;
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
private readonly CancellationTokenSource _cts = new();
public ServerConnection( TcpClient tcpClient, ServerEntity parent, IPEndPoint remote ) {
public ServerConnection( TcpClient tcpClient, MistoxServer parent, IPEndPoint remote ) {
_parent = parent;
_client = tcpClient;
_remoteAddress = remote;
_client = tcpClient;
_client.NoDelay = true;
_client.ReceiveTimeout = _parent.ConnectionTimeoutSeconds * 1000;
ServerInterface.ServerInstance.InvokeUserConnected( this );
// Start the receive thread
Thread receiveThread = new Thread(async () => await ReceiveThread() );
receiveThread.Start();
Task.Run(ReceiveThread);
}
async Task ReceiveThread() {
@@ -36,56 +36,63 @@ namespace Mistox.Server {
byte[] headerBuffer = new byte[DataPacket.HeaderLength];
byte[] payloadBuffer = new byte[65535]; // Upper limit for safety
while( Connected && _parent.Alive ) {
// Tick the last heartbeat or drop
DateTime now = DateTime.UtcNow;
if (LastHeartbeat.AddSeconds(_parent.ConnectionTimeoutSeconds) >= now) {
Connected = false;
Dispose();
break;
}
LastHeartbeat = DateTime.UtcNow;
// Read the header off
await ns.ReadExactlyAsync(headerBuffer, 0, 3);
await ns.ReadExactlyAsync(headerBuffer, 0, DataPacket.HeaderLength, _cts.Token);
// Parse the header
byte packetbyte = headerBuffer[0];
ushort dataLength = (ushort)((headerBuffer[1] << 8) | headerBuffer[2]);
// Pack the header variables for faster memory passing
DataPacketHeader Header = new DataPacketHeader {
Type = (ushort)((headerBuffer[0] << 8) | headerBuffer[1]),
DataLength = (ushort)((headerBuffer[2] << 8) | headerBuffer[3])
};
// Make sure the data looks valid
if (dataLength > payloadBuffer.Length) {
if (Header.DataLength > payloadBuffer.Length) {
throw new Exception("Incoming TCP payload is too large for the pre-allocated buffer.");
}
// Read off the data
await ns.ReadExactlyAsync(payloadBuffer, 0, dataLength);
await ns.ReadExactlyAsync(payloadBuffer, 0, Header.DataLength, _cts.Token);
// Create the dataobject
dynamic dataobject = DataPacket.Deserialize( packetbyte, dataLength, payloadBuffer.AsMemory(DataPacket.HeaderLength, dataLength) );
dynamic dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength) );
// Return the object
_parent.InvokeDataReceived(dataobject);
_parent.InvokeDataReceived(this, dataobject);
}
} catch (Exception e) {
_parent.Alive = false;
Dispose();
Console.WriteLine("TCP Receive exception: " + e.Message);
Console.WriteLine($"[TCP] Connection lost ({_remoteAddress}): {e.Message}");
} finally {
Dispose();
}
}
}
public async Task Send<T>( T packet ) {
byte[] byteData = DataPacket.Serialize( packet );
await _client.GetStream().WriteAsync( byteData, 0, byteData.Length );
byte[] byteData = DataPacket.Serialize(packet);
await _sendLock.WaitAsync();
try {
await _client.GetStream().WriteAsync(byteData, 0, byteData.Length);
} finally {
_sendLock.Release();
}
}
public IPAddress getRemote() {
return _remoteAddress.Address;
}
public void Dispose() {
Connected = false;
_parent.InvokeUserDisconnected( this );
Connected = false;
_parent.Alive = false;
_cts.Cancel();
_client.Close();
_sendLock.Dispose();
_cts.Dispose();
_parent.InvokeUserDisconnected(this);
// Remove this item from the Dictionary on the Server for garbage collection
}
}
+17 -12
View File
@@ -1,7 +1,6 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
// Client Connections
@@ -9,39 +8,45 @@ using System.Threading.Tasks;
namespace Mistox.Server {
public class ServerListener : IDisposable {
ServerEntity _parent;
IPEndPoint _bindAddress;
TcpListener _Listener;
private readonly MistoxServer _parent;
private readonly IPEndPoint _bindAddress;
private readonly TcpListener _Listener;
public ServerListener(IPEndPoint listenerAddress, ServerEntity parent) {
public ServerListener(IPEndPoint listenerAddress, MistoxServer parent) {
_bindAddress = listenerAddress;
_parent = parent;
Thread ConnectionThread = new Thread(async () => await ListenerThread() );
ConnectionThread.Start();
_Listener = new TcpListener( _bindAddress );
_Listener.Start();
// Start the thread
Task.Run(ListenerThread);
}
async Task ListenerThread() {
_Listener = new TcpListener( _bindAddress );
_Listener.Start();
while( _parent.Alive ) {
TcpClient client = await _Listener.AcceptTcpClientAsync();
if (_parent.Connections.Count + 1 > _parent.MaxClients) {
//client.Client.SendAsync(); // Send the client a packet saying the servername and reject reason 'server full'
continue;
}
IPEndPoint remote = client.Client.RemoteEndPoint as IPEndPoint;
string connectionID = Guid.NewGuid().ToString();
_parent.Connections[connectionID] = new ServerConnection( client, _parent, remote );
string connectionID = Guid.NewGuid().ToString();
_parent.Connections[connectionID] = new ServerConnection( client, _parent, remote) {
UserName = connectionID
};
Console.WriteLine( "New User Connected" );
}
Dispose();
}
public void Dispose() {
_Listener.Stop();
_Listener = null;
_Listener.Dispose();
}
}
+36 -34
View File
@@ -1,69 +1,71 @@
using System;
using System.Reflection;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using MessagePack;
using Mistox.Attributes;
namespace Mistox {
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DataPacketHeader {
public ushort Type { get; set; }
public ushort DataLength { get; set; }
}
public class DataPacket {
// The length of the packet header in bytes
public static readonly int HeaderLength = 3;
// 8b. 16b. value of datalength
// Data Packet [ Type | DataLength | Data ]
public static int HeaderLength => Unsafe.SizeOf<DataPacketHeader>();
// Helper classes
public static byte[] Serialize<T>( T Object ) {
// Get your custom packet attribute to find the ID
mPacket attribute = typeof(T).GetCustomAttribute<mPacket>(false);
if (attribute == null) {
throw new Exception($"The datastructure {typeof(T).Name} is not an mPacket");
}
// Use an expandable buffer writer from the system stack
var writer = new ArrayBufferWriter<byte>(256);
// Serialize the payload directly to a temporary byte array.
byte[] payloadData = MessagePackSerializer.Serialize(Object);
ushort dataLength = (ushort)payloadData.Length;
// Advance the writer past the header slot so MessagePack doesn't overwrite it
writer.Advance(HeaderLength);
// Allocate an array for the final output (1 byte Type + 2 bytes Length + Payload)
byte[] finalPacket = new byte[HeaderLength + dataLength];
// Serialize the payload DIRECTLY into the final buffer memory
MessagePackSerializer.Serialize(writer, Object);
// Write the 3-byte header directly into the final array
finalPacket[0] = attribute.ID;
finalPacket[1] = (byte)((dataLength >> 8) & 0xFF); // High byte of ushort
finalPacket[2] = (byte)(dataLength & 0xFF); // Low byte of ushort
// Build our header struct on the stack
DataPacketHeader header = new DataPacketHeader {
Type = NetworkPacketDictionary.PacketIDTable[typeof(T)],
DataLength = (ushort)(writer.WrittenCount - HeaderLength)
};
// Copy the payload directly into the final array right after the header
// This replaces all your .Join() allocations with a single ultra-fast CPU copy
Buffer.BlockCopy(payloadData, 0, finalPacket, HeaderLength, dataLength);
return finalPacket;
// Map the struct directly onto the reservation we left at the front
writer.WrittenMemory.Slice(0, HeaderLength);
MemoryMarshal.Write(writer.GetSpan(HeaderLength).Slice(0, HeaderLength), in header);
// 7. Extract the finished byte array.
// Note: In a production socket pipeline, you would pass writer.WrittenMemory directly
// to the socket instead of converting it to an array here!
return writer.WrittenSpan.ToArray();
}
public static object Deserialize( byte PacketByte, ushort DataLength, ReadOnlyMemory<byte> Data ) {
public static object Deserialize( DataPacketHeader Header, ReadOnlyMemory<byte> Data ) {
// Get the type from the tag
if (!NetworkPacketDictionary.ServerToClientPackets.TryGetValue(PacketByte, out Type resolvedType)) {
throw new Exception($"Unknown packet type ID: {PacketByte}");
if (!NetworkPacketDictionary.PacketLookupTable.TryGetValue(Header.Type, out Type resolvedType)) {
throw new Exception($"Unknown packet type ID: {Header.Type}");
}
// Sanity check: Ensure the packet we received actually has the payload bytes it claims to have
if ( DataLength < Data.Length ) {
// Ensure the packet we received actually has the payload bytes it claims to have
if ( Header.DataLength < Data.Length ) {
throw new Exception("Malformed packet: Payload size mismatch.");
}
// Make sure the data is exactly the size requested
ReadOnlyMemory<byte> payloadSlice = Data.Slice(0, DataLength);
ReadOnlyMemory<byte> payloadSlice = Data.Slice(0, Header.DataLength);
// Create the low-level reader struct directly over the memory slice
var reader = new MessagePackReader(payloadSlice);
// Extract the object
dynamic obj = MessagePackSerializer.Deserialize( resolvedType, ref reader, MessagePackSerializerOptions.Standard );
return obj;
return MessagePackSerializer.Deserialize( resolvedType, ref reader, MessagePackSerializerOptions.Standard );
}
+31 -10
View File
@@ -7,17 +7,38 @@ namespace Mistox.Attributes {
public class NetworkPacketDictionary {
// Fills out the NetworkPacket Lookup Dictionary
public static readonly Dictionary<byte, Type> ClientToClientPackets = new();
public static readonly Dictionary<byte, Type> ServerToClientPackets = new();
public static readonly Dictionary<ushort, Type> PacketLookupTable = new();
public static readonly Dictionary<Type, ushort> PacketIDTable = new();
// Have a way to assign a number to the packet that will run the same ever time
static ushort GetDeterministicHashCode(string str) {
unchecked {
uint hash = 2166136261;
foreach (char c in str) {
hash = (hash ^ c) * 16777619;
}
return (ushort)(hash % 0xFFFF);
}
}
// Map all packets to a number [ Error on packet collisions ]
public static void BuildNetworkPacketList() {
Type[] allClasses = Assembly.GetExecutingAssembly().GetTypes();
foreach( Type cur in allClasses) {
mPacket attribute = cur.GetCustomAttribute<mPacket>(false);
if (attribute != null) {
if ( attribute.PacketUse.HasFlag(PacketUsage.ClientToClient) ) {
ClientToClientPackets[attribute.ID] = cur;
} else if ( attribute.PacketUse.HasFlag(PacketUsage.ClientToServer) ) {
ServerToClientPackets[attribute.ID] = cur;
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies) {
Type[] allClasses = assembly.GetTypes();
foreach( Type cur in allClasses) {
mPacket attribute = cur.GetCustomAttribute<mPacket>(false);
if (attribute != null) {
// Generate a deterministic 16-bit ID based on the Type's FullName
string identifier = cur.FullName ?? cur.Name;
ushort packetId = GetDeterministicHashCode(identifier);
if (PacketLookupTable.ContainsKey(packetId)) {
throw new Exception($"CRITICAL: Duplicate Packet ID detected! ID {packetId} is used by both {PacketLookupTable[packetId].Name} and {cur.Name}");
}
PacketLookupTable[packetId] = cur;
PacketIDTable[cur] = packetId;
}
}
}
+1 -15
View File
@@ -4,21 +4,7 @@ namespace Mistox.Attributes {
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
public sealed class mPacket : Attribute {
public byte ID { get; }
public PacketUsage PacketUse { get; }
public mPacket(byte id) {
ID = id;
PacketUse = PacketUsage.ClientToClient | PacketUsage.ClientToServer;
}
public mPacket(byte id, PacketUsage packetUse) {
ID = id;
PacketUse = packetUse;
}
}
public enum PacketUsage {
ClientToClient = 0x01,
ClientToServer = 0x10,
public mPacket() {}
}
}
+39 -7
View File
@@ -1,21 +1,53 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Numerics;
using Mistox.Attributes;
namespace Mistox.ResevedPackets {
[mPacket(0xFF)]
// Implimented
// Packet send to server for Auth and responded to the client with Auth Success
[mPacket]
public class UserAuthPacket {
// Sent to the server
public string UserName { get; set; } = "";
public string ConnectionKey { get; set; } = "";
// Response from the server
public string ServerName { get; set; } = "";
public int AuthSuccessful { get; set; } = 0; // 0 is standard, -1 is auth failure, 1 is auth success
}
// Not Implimented
// Generic networked object packet
[mPacket]
public class EntityPacket {
// Tag to make sure the object on the other side is the same object i want to update here
// Need to map these Object ID's to a dictionary to make sure I only send network updates when there is a change
public ushort ObjectId { get; set; }
// Packet payload data that im matching for in the dictionary
public byte[] InnerPayload { get; set; }
}
[mPacket(0xFE)]
public class UserConnectedPacket {
public class UserTableObject {
public string UserName { get; set; }
public long ClientIPAddress { get; set; }
public int ClientPort { get; set; }
public IPAddress IpAddress { get; set; }
public Vector3 Position { get; set; }
}
[mPacket(0xFD)]
// Not implemented
// Packet sent to all clients when a global position update occurrs
[mPacket]
public class HeartbeatPacket {
public Vector3 Position { get; set; }
public List<UserTableObject> ServerList { get; set; }
}
// Not implimented
// Packet sent to all clients when a users TCP stream disconnects from the server
[mPacket]
public class UserDisconnectedPacket {
public string UserName { get; set; }
}
+18 -22
View File
@@ -1,25 +1,21 @@
0. Messages sent from clients to clients or clients to the server dont have any information about who it came from on the receiving side?
Probably need to route the event handlers throught the client objects to be able to pull that information off of the packets as well.
1. Allow the server to setup a server name
2. Allow the client to setup a user name
3. Have the client connect to the server with a welcome packet
4. Have the server use the welcome packet to fillout the user information on the server
5. Respond to the client from the server with a server list of UDPEndpoints
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
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 user leaves a server
Notify all clients to drop the connections
Remove the user from the server table
1. If the TCP connection closes not started
Disconnect the user and tell all clients that the user is gone not started