Compare commits

..
13 Commits
20 changed files with 459 additions and 279 deletions

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
name: Build and publish mistserver
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: alpine-linux
steps:
- name: checkout
uses: actions/checkout@v4
- 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 }}'
+60
View File
@@ -0,0 +1,60 @@
using System;
using System.Threading.Tasks;
using Mistox;
using Mistox.Client;
class clientExample {
public MistoxClient? clientObject;
void onServerConnected() {
Console.WriteLine("Connected to server sucessfully");
}
void onClientConnected(mUDPClient user) {
Console.WriteLine("User connected to server with username: " + user.UserName);
}
void onClientJoined(mUDPClient user) {
Console.WriteLine("User is now in range of you and UDP connections started with user : " + user.UserName);
}
void onReceive(mUDPClient? user, dynamic data) {
switch (data){
case MessagePacket packet:
if (user == null) {
Console.WriteLine($"MSG From [Serverx]: {packet.Data}");
} else {
Console.WriteLine($"MSG From [{user.UserName}]: {packet.Data}");
}
break;
default:
break;
}
}
void onClientLeft(mUDPClient user) {
Console.WriteLine("User is no longer in range of you and UDP connections ended with user : " + user.UserName);
}
void onClientDisconnected(mUDPClient user) {
Console.WriteLine("User disconnected from server with username: " + user.UserName);
}
void onServerDisconnected() {
Console.WriteLine("Disconnected from server");
}
public async Task StartClient(string userName, string host, int port, string password) {
clientObject = GameServer.newClient(userName, host, port, password);
if (clientObject == null) return;
clientObject.onServerConnected += onServerConnected;
clientObject.onUserConnected += onClientConnected;
clientObject.onUserJoined += onClientJoined;
clientObject.onDataReceived += onReceive;
clientObject.onUserLeft += onClientLeft;
clientObject.onUserDisconnected += onClientDisconnected;
clientObject.onServerDisconnected += onServerDisconnected;
}
}
-15
View File
@@ -1,15 +0,0 @@
The servers default port is 25566
TCPServer.cs
Void TCPServer() Initilizes the server object on the default game port of 25566
Void SendAll(string Data) Send data to all the clients
Event onReceived Fires when data is received from a client
TCPClient.cs
Void TCPClient(String ServerIP) Initilizes the client object
Void Send(String Data) Sends data to the server
Event onReceived Fires when data is received from the server
UDPClient.cs
+2 -1
View File
@@ -9,6 +9,7 @@
<Company>Mistox.net</Company>
<Product>Mistox Game Server</Product>
<Description>Game Server for Mistox Games and partners</Description>
<Nullable>enable</Nullable>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
@@ -19,7 +20,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MistoxServer\MistServerModule.csproj" />
<ProjectReference Include="..\MistoxServer\MistServer.csproj" />
</ItemGroup>
</Project>
+42 -83
View File
@@ -1,85 +1,48 @@
using System;
using System.Threading.Tasks;
using MistoxServer;
namespace MistoxHolePunch {
enum Mode {
Server,
Client
}
class Program {
static IMistoxServer serverObj;
static bool running = true;
static serverExample? server;
static clientExample? client;
static Mode mode;
void onConnected( object sender, EventArgs e ) {
// sender and e are always null
// put connected functions in here
}
void slowReceive( object obj, EventArgs e ) {
if ( serverObj is ServerInterface ) {
// If ServerMode is passive obj is byte[] and Send takes in byte[]
// If Servermode is Athoritiative obj is the Class type sent and Send takes in any Class type except generic object
// Check to make sure data is correct before relaying
// Also perform server specific checks in here
// IE player didnt teleport or is shooting from 20 feet from his body
serverObj.Send( obj, SendType.SlowUpdate );
}
Console.Write( "Received TCP : " );
Console.WriteLine( obj );
}
void fastReceive( object obj, EventArgs e ) {
if( serverObj is ServerInterface ) {
// If ServerMode is passive obj is byte[] and Send takes in byte[]
// If Servermode is Athoritiative obj is the Class type sent and Send takes in any Class type except generic object
// Check to make sure data is correct before relaying
// Also perform server specific checks in here
// IE player didnt teleport or is shooting from 20 feet from his body
serverObj.Send( obj, SendType.FastUpdate );
}
Console.Write( "Received UDP : " );
Console.WriteLine( obj );
}
void onDisconnected( object sender, EventArgs e ) {
// sender and e are always null
// put disconnected functions in here
}
static string host = "example.com";
static int port = 7300;
static ServerMode mode = ServerMode.Passive;
async Task RunServer() {
serverObj = mServer.newServer( Convert.ToInt32( port ), mode );
serverObj.onConnected += onConnected;
serverObj.onSlowReceive += slowReceive;
serverObj.onFastReceive += fastReceive;
serverObj.onDisconnected += onDisconnected;
while ( running ) {
// Stop this thread for 1 second so the CPU isnt 100%
// All the real work is on different threads
// While loop needs to exist so our main thread doesnt close
await Task.Delay( 1000 );
async Task StartServer(string name, int port) {
server = new serverExample();
await server.StartServer(name, port);
// On a game this sleep loop isnt needed
if (server.serverObject == null) return;
while (server.serverObject.Alive) {
// becuase this is a console app if we dont keep the main thread alive the app will close
await Task.Delay(5000);
}
}
async Task RunClient() {
serverObj = mServer.newClient( host, Convert.ToInt32( port ) );
serverObj.onConnected += onConnected;
serverObj.onSlowReceive += slowReceive;
serverObj.onFastReceive += fastReceive;
serverObj.onDisconnected += onDisconnected;
while( running ) {
// Stop this thread for 1 second so the CPU isnt 100%
// All the real work is on different threads
// While loop needs to exist so our main thread doesnt close
await Task.Delay( 1000 );
async Task StartClient(string username, string host, int port, string password) {
client = new clientExample();
await client.StartClient(username, host, port, password);
// On a game this sleep loop isnt needed
if (client.clientObject == null) return;
while (client.clientObject.Alive) {
// becuase this is a console app if we dont keep the main thread alive the app will close
await Task.Delay(5000);
}
}
static async Task Main(string[] args) {
string Task = args.Length > 0 ? args[0].ToLower() : null;
async Task Main(string[] args) {
string Job = args.Length > 0 ? args[0].ToLower() : "";
string host = "";
string name = "";
string pass = "";
int port = 0;
for( int i = 0; i < args.Length; i++ ) {
string cur = args[i].ToLower();
@@ -87,25 +50,21 @@ namespace MistoxHolePunch {
host = args [i + 1];
} else if( cur == "/p" || cur == "-p" ) {
port = Convert.ToInt32( args [i + 1] );
} else if( cur == "/a" || cur == "-a" ) {
mode = ServerMode.Authoritative;
} else if( cur == "/n" || cur == "-n" ) {
name = args [i + 1];
} else if( cur == "/u" || cur == "-u" ) {
pass = args [i + 1];
}
}
if (Task == "/?" || Task == "--help") {
if (Job == "/?" || Job == "--help") {
Console.WriteLine(HelpDocumentation.HelpText);
} else if (Task == "/s" || Task == "-s") {
Program prog = new Program();
await prog.RunServer();
} else if (Task == "/c" || Task == "-c") {
Program prog = new Program();
await prog.RunClient();
} else if (Job == "/s" || Job == "-s") {
await StartServer(name, port);
} else if (Job == "/c" || Job == "-c") {
await StartClient(name, host, port, pass);
} else {
host = "example.com";
port = 1500;
Program prog = new Program();
//prog.RunClient();
await prog.RunServer();
await StartServer("example.com", 4500);
}
}
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Threading.Tasks;
using Mistox;
using Mistox.Server;
class serverExample {
public MistoxServer? serverObject;
void onClientConnected(ServerConnection user) {
Console.WriteLine("User connected with username: " + user.UserName);
}
void onReceive(ServerConnection user, dynamic data) {
switch (data){
case MessagePacket packet when user.IsAuthenticated:
Console.WriteLine($"MSG From [{user.UserName}]: {packet.Data}");
break;
default:
break;
}
}
void onClientDisconnected(ServerConnection user) {
Console.WriteLine("User disconnected with username: " + user.UserName);
}
public async Task StartServer(string serverName, int port) {
serverObject = GameServer.newServer(serverName, port);
serverObject.onUserConnected += onClientConnected;
serverObject.onDataReceived += onReceive;
serverObject.onUserDisconnected += onClientDisconnected;
}
}
+6
View File
@@ -0,0 +1,6 @@
using Mistox.Attributes;
[mPacket]
class MessagePacket {
public string Data { get; set; } = "";
}
+137 -70
View File
@@ -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<IPEndPoint, mUDPClient> clientConnections { get; private set; } // UserName, Connection
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 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,88 +42,104 @@ 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<Task> 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<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;
InvokeOnUserJoined(newConnection);
}
}
// Remove users that left
foreach (IPEndPoint key in clientConnections.Keys) {
if (!activeIps.Contains(key.Address)) {
mUDPClient? user;
clientConnections.TryRemove(key, out user);
if (user == null) return;
InvokeOnUserLeft(user);
}
}
// 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...");
if (ServerConnection == null) return;
await ServerConnection.Send(new UserAuthPacket {
UserName = UserName,
ConnectionKey = ConnectionKey
});
}
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<dynamic> 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;
}
}
public event Action onServerConnected;
// When (self) connects to the server
public event Action? onServerConnected;
public void InvokeOnServerConnected(){
Alive = true;
Task.Run(async () => {
@@ -125,26 +150,67 @@ namespace Mistox.Client {
onServerConnected?.Invoke();
}
public event Action<mUDPClient> onUserConnected;
public void InvokeOnUserConnected(mUDPClient ConnectedUser){
onUserConnected?.Invoke(ConnectedUser);
// When any user connects to the server (self) hasn't seen yet
public event Action<mUDPClient>? onUserJoined;
public void InvokeOnUserJoined(mUDPClient User){
onUserJoined?.Invoke(User);
}
public event Action<mUDPClient> onUserDisconnected;
public void InvokeOnUserDisconnected(mUDPClient DisconnectedUser){
onUserDisconnected?.Invoke(DisconnectedUser);
// When user is within range of (self) and network rendering starts
public event Action<mUDPClient>? onUserConnected;
public void InvokeOnUserConnected(mUDPClient User){
onUserConnected?.Invoke(User);
}
public event Action onServerDisconnected;
// When generic data is received
public event Action<mUDPClient?, dynamic>? 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(remote, DataPacket);
break;
}
}
// When user is no longer in range of (self) and network rendering stops
public event Action<mUDPClient>? onUserDisconnected;
public void InvokeOnUserDisconnected(mUDPClient User){
onUserDisconnected?.Invoke(User);
}
// When any user disconnects from the server that isn't (self)
public event Action<mUDPClient>? onUserLeft;
public void InvokeOnUserLeft(mUDPClient User){
onUserLeft?.Invoke(User);
}
// When (self) disconnects to the server
public event Action? onServerDisconnected;
public void InvokeOnServerDisconnected(){
Alive = false;
onServerDisconnected?.Invoke();
}
/////////////////////////////
// Send Methods
/////////////////////////////
public async Task SendAllClients<Packet>(Packet data) {
public async Task SendAllClientsInRange<Packet>(Packet data) {
foreach( mUDPClient cur in clientConnections.Values ){
await cur.Send( data );
if (cur.WithinRange) {
await cur.Send( data );
}
}
}
@@ -158,6 +224,7 @@ namespace Mistox.Client {
}
public async Task SendServer<Packet>(Packet data) {
if (ServerConnection == null) return;
await ServerConnection.Send( data );
}
}
+5 -4
View File
@@ -48,10 +48,11 @@ namespace Mistox.Client {
await ns.ReadExactlyAsync(payloadBuffer, 0, Header.DataLength);
// Create the dataobject
dynamic dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength, Header.DataLength) );
// Return the object
_parent.InvokeOnReceived(dataobject);
object? dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength, Header.DataLength) );
if (dataobject != null) {
// Return the object
_parent.InvokeOnReceived(dataobject);
}
}
} catch (Exception e) {
+4 -2
View File
@@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
@@ -7,9 +8,10 @@ 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 IPEndPoint _remoteHost { get; set; } // The remote IP Address
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
private IPEndPoint _remoteHost { get; set; } // The remote IP Address
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
private readonly MistoxClient _parent;
+8 -6
View File
@@ -21,9 +21,9 @@ namespace Mistox.Client {
UdpReceiveResult udpPacket = await _parent.ClientSocket.ReceiveAsync();
// Try find the user
mUDPClient client = null;
bool clientExists = _parent.clientConnections.TryGetValue(udpPacket.RemoteEndPoint, out client);
if (clientExists) {
mUDPClient? client = null;
_parent.clientConnections.TryGetValue(udpPacket.RemoteEndPoint, out client);
if (client != null) {
// Create the data readers
ReadOnlyMemory<byte> dataMemory = udpPacket.Buffer.AsMemory();
@@ -39,10 +39,12 @@ namespace Mistox.Client {
ReadOnlyMemory<byte> data = dataMemory.Slice(DataPacket.HeaderLength, Header.DataLength);
// Extract the object
dynamic buildObj = DataPacket.Deserialize( Header, data );
object? buildObj = DataPacket.Deserialize( Header, data );
if (buildObj != null) {
// Return the object
_parent.InvokeOnReceived( buildObj, client );
// Return the object
_parent.InvokeOnReceived( buildObj, client );
}
}
} catch( Exception e ) {
+1 -1
View File
@@ -19,7 +19,7 @@ namespace Mistox {
return new MistoxServer(listenerAddress, serverName);
}
public static MistoxClient newClient( string UserName, string ServerIPOrHostName, int Port, string ConnectionKey = "" ) {
public static MistoxClient? newClient( string UserName, string ServerIPOrHostName, int Port, string ConnectionKey = "" ) {
// Build the network packet list
NetworkPacketDictionary.BuildNetworkPacketList();
@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
+61 -25
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Mistox.ResevedPackets;
@@ -8,25 +9,49 @@ 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
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; 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; // All the active connections. key is username
private readonly ServerListener Listener; // The Connection Listener singleton
private readonly ServerListener Listener; // The Connection Listener singleton
public MistoxServer(IPEndPoint ListenerAddress, string serverName) {
Connections = new ConcurrentDictionary<string, ServerConnection>();
ServerName = serverName;
Listener = new ServerListener(ListenerAddress, this);
Console.WriteLine( "The Server is initilized and waiting for clients to connect at port : " + ListenerAddress.Port );
}
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;
@@ -36,30 +61,36 @@ namespace Mistox.Server {
user.UserName = packet.UserName;
Connections[packet.UserName] = user;
user.IsAuthenticated = true;
InvokeUserConnected(user);
} else {
packet.AuthSuccessful = -1;
}
await SendClient(packet.UserName, packet);
await user.Send(packet);
}
// Functions
/////////////////////////////
// Events
/////////////////////////////
public event Action<ServerConnection> onUserConnected;
public event Action<ServerConnection>? onUserConnected;
public void InvokeUserConnected( ServerConnection user ){
Connections[user.UserName] = user;
onUserConnected?.Invoke( user );
}
public event Action<ServerConnection, dynamic> onDataReceived;
public event Action<ServerConnection, object>? onDataReceived;
public void InvokeDataReceived( ServerConnection user, dynamic data ){
switch (data as object) {
case UserAuthPacket packet:
if (!user.IsAuthenticated) {
Task.Run(async () => {
try {
await ProcessAuthRequest(user, packet);
} catch {}
});
}
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.");
@@ -70,11 +101,16 @@ namespace Mistox.Server {
}
}
public event Action<ServerConnection> onUserDisconnected;
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);
+10 -10
View File
@@ -10,7 +10,7 @@ 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 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
private readonly MistoxServer _parent;
@@ -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);
}
@@ -56,10 +55,11 @@ namespace Mistox.Server {
await ns.ReadExactlyAsync(payloadBuffer, 0, Header.DataLength, _cts.Token);
// Create the dataobject
dynamic dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength) );
// Return the object
_parent.InvokeDataReceived(this, dataobject);
object? dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength) );
if (dataobject != null) {
// Return the object
_parent.InvokeDataReceived(this, dataobject);
}
}
} catch (Exception e) {
@@ -82,18 +82,18 @@ namespace Mistox.Server {
}
}
public IPAddress getRemote() {
return _remoteAddress.Address;
}
public void Dispose() {
if (!Connected) return;
Connected = false;
_cts.Cancel();
_client.Close();
_sendLock.Dispose();
_cts.Dispose();
_parent.Connections.TryRemove(UserName, out _);
_parent.InvokeUserDisconnected(this);
_parent.Alive = false;
// Remove this item from the Dictionary on the Server for garbage collection
}
}
+9 -7
View File
@@ -28,17 +28,19 @@ namespace Mistox.Server {
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;
IPEndPoint? remote = client.Client.RemoteEndPoint as IPEndPoint;
if (remote != null) {
string connectionID = Guid.NewGuid().ToString();
_parent.Connections[connectionID] = new ServerConnection( client, _parent, remote) {
UserName = connectionID
};
string connectionID = Guid.NewGuid().ToString();
_parent.Connections[connectionID] = new ServerConnection( client, _parent, remote) {
UserName = connectionID
};
Console.WriteLine( "New User Connected" );
Console.WriteLine( "New User Connected" );
}
}
Dispose();
}
+2 -2
View File
@@ -46,10 +46,10 @@ namespace Mistox {
return writer.WrittenSpan.ToArray();
}
public static object Deserialize( DataPacketHeader Header, ReadOnlyMemory<byte> Data ) {
public static object? Deserialize( DataPacketHeader Header, ReadOnlyMemory<byte> Data ) {
// Get the type from the tag
if (!NetworkPacketDictionary.PacketLookupTable.TryGetValue(Header.Type, out Type resolvedType)) {
if (!NetworkPacketDictionary.PacketLookupTable.TryGetValue(Header.Type, out Type? resolvedType)) {
throw new Exception($"Unknown packet type ID: {Header.Type}");
}
@@ -27,7 +27,7 @@ namespace Mistox.Attributes {
foreach (Assembly assembly in assemblies) {
Type[] allClasses = assembly.GetTypes();
foreach( Type cur in allClasses) {
mPacket attribute = cur.GetCustomAttribute<mPacket>(false);
mPacket? attribute = cur.GetCustomAttribute<mPacket>(false);
if (attribute != null) {
// Generate a deterministic 16-bit ID based on the Type's FullName
+11 -12
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Numerics;
using Mistox.Attributes;
@@ -24,33 +25,31 @@ namespace Mistox.ResevedPackets {
// 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; }
public ushort ObjectId { get; set; } = 0;
// Packet payload data that im matching for in the dictionary
public byte[] InnerPayload { get; set; }
public byte[] InnerPayload { get; set; } = {};
}
// Not implemented
// Packet sent to all clients when a users TCP stream connects to the server
[mPacket]
public class UserConnectedPacket {
public string UserName { get; set; }
public IPAddress ClientIPAddress { get; set; }
public class UserTableObject {
public string UserName { get; set; } = string.Empty;
public IPAddress IpAddress { get; set; } = new([]);
public Vector3 Position { get; set; } = new();
}
// Not implemented
// Packet sent to all clients when a global position update occurrs
[mPacket]
public class UserPositionUpdatePacket {
public string UserName { get; set; }
public Vector3 Position { get; set; }
public class HeartbeatPacket {
public Vector3 Position { get; set; } = new();
public List<UserTableObject> ServerList { get; set; } = new();
}
// 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; }
public string UserName { get; set; } = string.Empty;
}
}
+16 -40
View File
@@ -1,46 +1,22 @@
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
12. User joined and left events are not implimented
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