Compare commits
30
Commits
531fd2572f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7581fbdeb | ||
|
|
c8a0aef289 | ||
|
|
068c88de4b | ||
|
|
30feb15821 | ||
|
|
9d97ad0975 | ||
|
|
e146eed3b2 | ||
|
|
3464e93aee | ||
|
|
f946ebef1e | ||
|
|
909718d8d5 | ||
|
|
ac08c7d184 | ||
|
|
258b019f1a | ||
|
|
41805d7de9 | ||
|
|
486b53b402 | ||
|
|
974b5c4f0a | ||
|
|
007f87f986 | ||
|
|
d446464f2a | ||
|
|
a78c040b09 | ||
|
|
827b1285a9 | ||
|
|
19c5259528 | ||
|
|
ad5b903d36 | ||
|
|
46efb5bd65 | ||
|
|
b83887cb2a | ||
|
|
37a6b5d59b | ||
|
|
873a6fa455 | ||
|
|
d3d437b0c8 | ||
|
|
6931846a64 | ||
|
|
b439805f3e | ||
|
|
6f910e969a | ||
|
|
875a5ae3e9 | ||
|
|
3992c3debc |
No files matched your search
@@ -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 }}'
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using Mistox.Attributes;
|
||||
|
||||
[mPacket]
|
||||
class MessagePacket {
|
||||
public string Data { get; set; } = "";
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Mistox.Client {
|
||||
|
||||
public class MistoxServerClient {
|
||||
|
||||
public string ServerName { get; set; } = ""; // The Name of the server
|
||||
public bool Alive { get; set; } = true; // Is the server running
|
||||
public int Port { get; set; } = 9126; // What Port to bind on
|
||||
|
||||
|
||||
public event Action onServerConnected;
|
||||
public void InvokeOnServerConnected(){
|
||||
Alive = true;
|
||||
onServerConnected?.Invoke();
|
||||
}
|
||||
|
||||
public event Action<UDPEndpoint> onUserConnected;
|
||||
public void InvokeOnUserConnected(UDPEndpoint ConnectedUser){
|
||||
onUserConnected?.Invoke(ConnectedUser);
|
||||
}
|
||||
|
||||
public event Action<dynamic> onDataReceived;
|
||||
public void InvokeOnReceived(dynamic DataPacket){
|
||||
onDataReceived?.Invoke(DataPacket);
|
||||
}
|
||||
|
||||
public event Action<UDPEndpoint> onUserDisconnected;
|
||||
public void InvokeOnUserDisconnected(UDPEndpoint DisconnectedUser){
|
||||
onUserDisconnected?.Invoke(DisconnectedUser);
|
||||
}
|
||||
|
||||
public event Action onServerDisconnected;
|
||||
public void InvokeOnServerDisconnected(){
|
||||
Alive = false;
|
||||
onServerDisconnected?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Mistox.Client {
|
||||
public class UDPEndpoint {
|
||||
public string UserName { get; set; } = ""; // The Players UserName
|
||||
public bool Alive { get; set; } = true; // Is the connection alive
|
||||
public mUDPClient Connection { get; set; } // The UDP Client Object
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Mistox.Attributes;
|
||||
|
||||
[mPacket(0xFF)]
|
||||
public class UserConnectionPacket {
|
||||
string UserName { get; set; }
|
||||
long RemoteIP { get; set; }
|
||||
int Port { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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; 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 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" );
|
||||
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;
|
||||
}
|
||||
|
||||
// Start the heartbeat
|
||||
_ = RunAsync(Heartbeat);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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<mUDPClient>? onUserJoined;
|
||||
public void InvokeOnUserJoined(mUDPClient User){
|
||||
onUserJoined?.Invoke(User);
|
||||
}
|
||||
|
||||
// When user is within range of (self) and network rendering starts
|
||||
public event Action<mUDPClient>? onUserConnected;
|
||||
public void InvokeOnUserConnected(mUDPClient User){
|
||||
onUserConnected?.Invoke(User);
|
||||
}
|
||||
|
||||
// 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 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) {
|
||||
if (ServerConnection == null) return;
|
||||
await ServerConnection.Send( data );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,53 +9,54 @@ using System.Threading.Tasks;
|
||||
namespace Mistox.Client {
|
||||
public class mTCPClient : IDisposable {
|
||||
|
||||
TcpClient Server;
|
||||
private readonly TcpClient _connection;
|
||||
private readonly MistoxClient _parent;
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
public mTCPClient( IPEndPoint ServerAddress ) {
|
||||
Server = new TcpClient( AddressFamily.InterNetwork );
|
||||
Server.NoDelay = true;
|
||||
Server.Connect( ServerAddress );
|
||||
ClientInterface.ServerClientInstance.InvokeOnServerConnected();
|
||||
Thread RThread = new Thread(async () => { await ReceiveThread(); });
|
||||
RThread.Start();
|
||||
public mTCPClient( MistoxClient parent, IPEndPoint ServerAddress ) {
|
||||
_parent = parent;
|
||||
_connection = new TcpClient( AddressFamily.InterNetwork );
|
||||
_connection.NoDelay = true;
|
||||
_connection.Connect( ServerAddress );
|
||||
_parent.InvokeOnServerConnected();
|
||||
// Start the receive thread
|
||||
Task.Run(ReceiveThread);
|
||||
}
|
||||
|
||||
async Task ReceiveThread() {
|
||||
using( NetworkStream ns = Server.GetStream() ) {
|
||||
using( NetworkStream ns = _connection.GetStream() ) {
|
||||
try {
|
||||
byte[] headerBuffer = new byte[DataPacket.HeaderLength];
|
||||
byte[] payloadBuffer = new byte[65535]; // Upper limit for safety
|
||||
while( ClientInterface.ServerClientInstance.Alive ) {
|
||||
while( _parent.Alive ) {
|
||||
|
||||
// Read the header off
|
||||
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);
|
||||
|
||||
// Get the type from the tag
|
||||
if (!DataPacket.ServerToClientPackets.TryGetValue(packetbyte, out Type resolvedType)) {
|
||||
throw new Exception($"Unknown packet type ID: {packetbyte}");
|
||||
}
|
||||
await ns.ReadExactlyAsync(payloadBuffer, 0, Header.DataLength);
|
||||
|
||||
// Create the dataobject
|
||||
dynamic dataobject = DataPacket.Deserialize( resolvedType, dataLength, payloadBuffer.AsMemory(DataPacket.HeaderLength, dataLength) );
|
||||
|
||||
// Return the object
|
||||
ClientInterface.ServerClientInstance.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) {
|
||||
ClientInterface.ServerClientInstance.Alive = false;
|
||||
_parent.Alive = false;
|
||||
Dispose();
|
||||
Console.WriteLine("TCP Receive exception: " + e.Message);
|
||||
}
|
||||
@@ -64,13 +65,18 @@ namespace Mistox.Client {
|
||||
|
||||
public async Task Send<T>(T packet) {
|
||||
byte[] data = DataPacket.Serialize( packet );
|
||||
await Server.GetStream().WriteAsync( data, 0, data.Length );
|
||||
await _sendLock.WaitAsync();
|
||||
try {
|
||||
await _connection.GetStream().WriteAsync( data, 0, data.Length );
|
||||
} finally {
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ClientInterface.ServerClientInstance.InvokeOnServerDisconnected();
|
||||
Server.Close();
|
||||
Server = null;
|
||||
_parent.InvokeOnServerDisconnected();
|
||||
_connection.Close();
|
||||
_connection.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Socket udpClient;
|
||||
IPEndPoint remoteHost;
|
||||
public string UserName { get; set; } = ""; // The remote players UserName
|
||||
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
|
||||
|
||||
public mUDPClient( long RemoteIP, int Port ) {
|
||||
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 ) );
|
||||
ClientInterface.ServerClientInstance.Alive = true;
|
||||
Thread Client = new Thread(async() => { await ReceiveThread(); });
|
||||
Client.Start();
|
||||
}
|
||||
private IPEndPoint _remoteHost { get; set; } // The remote IP Address
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
private readonly MistoxClient _parent;
|
||||
|
||||
async Task ReceiveThread() {
|
||||
byte[] buffer = new byte[65535];
|
||||
while( ClientInterface.ServerClientInstance.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);
|
||||
|
||||
// Get the type from the tag
|
||||
if (!DataPacket.ClientToClientPackets.TryGetValue(dataType, out Type resolvedType)) {
|
||||
throw new Exception($"Unknown packet type ID: {dataType}");
|
||||
}
|
||||
|
||||
// Extract the object
|
||||
dynamic buildObj = DataPacket.Deserialize( resolvedType, DataLength, data );
|
||||
|
||||
// Return the object
|
||||
ClientInterface.ServerClientInstance.InvokeOnReceived( buildObj );
|
||||
|
||||
} catch( Exception e ) {
|
||||
Console.WriteLine( "UDP Receive exception : " + e.Message );
|
||||
}
|
||||
}
|
||||
public mUDPClient( MistoxClient parent, IPEndPoint remoteHost ) {
|
||||
_parent = parent;
|
||||
_remoteHost = remoteHost;
|
||||
_parent.Alive = true;
|
||||
}
|
||||
|
||||
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() {
|
||||
ClientInterface.ServerClientInstance.Alive = false;
|
||||
udpClient.Close();
|
||||
udpClient = null;
|
||||
_parent.Alive = false;
|
||||
_parent.ClientSocket.Close();
|
||||
_parent.ClientSocket.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
_parent.clientConnections.TryGetValue(udpPacket.RemoteEndPoint, out client);
|
||||
if (client != null) {
|
||||
|
||||
// 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
|
||||
object? buildObj = DataPacket.Deserialize( Header, data );
|
||||
if (buildObj != null) {
|
||||
|
||||
// Return the object
|
||||
_parent.InvokeOnReceived( buildObj, client );
|
||||
}
|
||||
|
||||
}
|
||||
} catch( Exception e ) {
|
||||
Console.WriteLine( "UDP Receive exception : " + e.Message );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_parent.Alive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Mistox.Client;
|
||||
|
||||
namespace Mistox {
|
||||
public class ClientInterface {
|
||||
|
||||
public static MistoxServerClient ServerClientInstance;
|
||||
public static mTCPClient serverConnection;
|
||||
public static ConcurrentDictionary<string, UDPEndpoint> clientConnections;
|
||||
|
||||
public event Action onServerConnected;
|
||||
public event Action<UDPEndpoint> onUserConnected;
|
||||
public event Action<dynamic> onDataReceived;
|
||||
public event Action<UDPEndpoint> onUserDisconnected;
|
||||
public event Action onServerDisconnected;
|
||||
|
||||
public ClientInterface( string IpOrHostName, int Port ) {
|
||||
|
||||
// Wire up the Mistox server client
|
||||
ServerClientInstance = new MistoxServerClient() {
|
||||
Port = Port
|
||||
};
|
||||
ServerClientInstance.onServerConnected += onServerConnected;
|
||||
ServerClientInstance.onUserConnected += (UDPEndpoint endpoint) => { onUserConnected?.Invoke(endpoint); };
|
||||
ServerClientInstance.onDataReceived += (dynamic data) => { onDataReceived?.Invoke(data); };
|
||||
ServerClientInstance.onUserDisconnected += (UDPEndpoint endpoint) => { onUserDisconnected?.Invoke(endpoint); };
|
||||
ServerClientInstance.onServerDisconnected += onServerDisconnected;
|
||||
|
||||
// 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 );
|
||||
}
|
||||
}
|
||||
Console.WriteLine( "The client is initilized and trying to connect to the server at ip : " + ServerDenstination.Address );
|
||||
|
||||
// Make a TCP connection to the server
|
||||
try {
|
||||
serverConnection = new mTCPClient( ServerDenstination );
|
||||
} catch( Exception e ) {
|
||||
Console.WriteLine( "An error has occured with the connection to the server. Error { " );
|
||||
Console.WriteLine( e.ToString() );
|
||||
Console.WriteLine( "}" );
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAllClients<Packet>(Packet data) {
|
||||
foreach(KeyValuePair<string, UDPEndpoint> cur in clientConnections){
|
||||
await cur.Value.Connection.Send( data );
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendClient<Packet>(string UserName, Packet data) {
|
||||
await clientConnections[UserName].Connection.Send( data );
|
||||
}
|
||||
|
||||
public async Task SendServer<Packet>(Packet data) {
|
||||
await serverConnection.Send( data );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Mistox {
|
||||
public class mServer {
|
||||
|
||||
public static ServerInterface newServer( string serverName, int port ) {
|
||||
DataPacket.BuildNetworkPacketList();
|
||||
return new ServerInterface(port, serverName);
|
||||
}
|
||||
|
||||
public static ClientInterface newClient( string ServerIPOrHostName, int Port ) {
|
||||
DataPacket.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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Mistox.Server;
|
||||
|
||||
namespace Mistox {
|
||||
public class ServerInterface {
|
||||
|
||||
public static MistoxServer ServerInstance; // Server Data
|
||||
public static ServerListener Listener; // Server Listener
|
||||
public static ConcurrentDictionary<string, TcpClientConnection> Connections; // Server Connections
|
||||
|
||||
public event Action<TcpClientConnection> onUserConnected; // User connected event handler
|
||||
public event Action<dynamic> onDataReceived; // Data received event handler
|
||||
public event Action<TcpClientConnection> onUserDisconnected; // User disconnected event handler
|
||||
|
||||
public ServerInterface( int port, string serverName ) {
|
||||
|
||||
// Initilize the server
|
||||
ServerInstance = new MistoxServer() {
|
||||
ServerName = serverName,
|
||||
BindAddress = IPAddress.Any,
|
||||
Port = port,
|
||||
MaxClients = 10
|
||||
};
|
||||
Connections = new ConcurrentDictionary<string, TcpClientConnection>();
|
||||
|
||||
// Setup the event handlers
|
||||
ServerInstance.onUserConnected += (TcpClientConnection connectedUser) => { onUserConnected?.Invoke(connectedUser); };
|
||||
ServerInstance.onDataReceived += (dynamic data) => { onDataReceived?.Invoke(data); };
|
||||
ServerInstance.onUserDisconnected += (TcpClientConnection disconnectedUser) => { onUserDisconnected?.Invoke(disconnectedUser); };
|
||||
|
||||
// Start listening for connections and wire up the server
|
||||
Listener = new ServerListener();
|
||||
|
||||
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, TcpClientConnection> cur in Connections ) {
|
||||
await cur.Value.Connection.Send(data);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendClient<Packet>(string UserName, Packet data) {
|
||||
await Connections[UserName].Connection.Send( data );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Mistox.Server {
|
||||
|
||||
public class MistoxServer {
|
||||
public string ServerName { get; set; } = ""; // The name of the server
|
||||
public IPAddress BindAddress { get; set; } = IPAddress.Any; // What IP to bind on
|
||||
public int Port { get; set; } = 9126; // What Port to bind on
|
||||
public int MaxClients { get; set; } = 10; // The max players per server
|
||||
public bool Alive { get; set; } = true; // Is the server running
|
||||
|
||||
public event Action<TcpClientConnection> onUserConnected;
|
||||
public void InvokeUserConnected( TcpClientConnection user ){ onUserConnected?.Invoke( user ); }
|
||||
public event Action<dynamic> onDataReceived;
|
||||
public void InvokeDataReceived( dynamic data ){ onDataReceived?.Invoke( data ); }
|
||||
public event Action<TcpClientConnection> onUserDisconnected;
|
||||
public void InvokeUserDisconnected( TcpClientConnection user ){ onUserDisconnected?.Invoke( user ); }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Mistox.Server {
|
||||
public class TcpClientConnection {
|
||||
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 float ConnectionRange { get; set; } // The ingame distance players need to be to be connected together
|
||||
public DateTime LastHeartbeat { get; set; } = DateTime.UtcNow; // Connection heartbeat for integrity
|
||||
public bool Connected { get; set; } = true; // Is the connection valid
|
||||
public IPAddress RemoteAddress { get; set; } // The remote users IP Address
|
||||
public TCPConnection Connection { get; set; } // The TCP Server Object
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 MistoxServer {
|
||||
|
||||
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
|
||||
|
||||
private readonly ServerListener Listener; // The Connection Listener singleton
|
||||
|
||||
public MistoxServer(IPEndPoint ListenerAddress, string serverName) {
|
||||
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;
|
||||
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<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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mistox.Server {
|
||||
public class ServerConnection : IDisposable {
|
||||
|
||||
public string UserName { get; set; } = ""; // The Players UserName
|
||||
public bool IsAuthenticated { get; set; } = false; // Has the player authenticated
|
||||
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;
|
||||
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, MistoxServer parent, IPEndPoint remote ) {
|
||||
_parent = parent;
|
||||
_remoteAddress = remote;
|
||||
_client = tcpClient;
|
||||
_client.NoDelay = true;
|
||||
_client.ReceiveTimeout = _parent.ConnectionTimeoutSeconds * 1000;
|
||||
|
||||
// Start the receive thread
|
||||
Task.Run(ReceiveThread);
|
||||
}
|
||||
|
||||
async Task ReceiveThread() {
|
||||
using( NetworkStream ns = _client.GetStream() ) {
|
||||
try {
|
||||
byte[] headerBuffer = new byte[DataPacket.HeaderLength];
|
||||
byte[] payloadBuffer = new byte[65535]; // Upper limit for safety
|
||||
while( Connected && _parent.Alive ) {
|
||||
|
||||
// Read the header off
|
||||
await ns.ReadExactlyAsync(headerBuffer, 0, DataPacket.HeaderLength, _cts.Token);
|
||||
|
||||
// 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 (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, Header.DataLength, _cts.Token);
|
||||
|
||||
// Create the dataobject
|
||||
object? dataobject = DataPacket.Deserialize( Header, payloadBuffer.AsMemory(DataPacket.HeaderLength) );
|
||||
if (dataobject != null) {
|
||||
// Return the object
|
||||
_parent.InvokeDataReceived(this, dataobject);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_parent.Alive = false;
|
||||
Dispose();
|
||||
Console.WriteLine($"[TCP] Connection lost ({_remoteAddress}): {e.Message}");
|
||||
} finally {
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Send<T>( T packet ) {
|
||||
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;
|
||||
_cts.Cancel();
|
||||
_client.Close();
|
||||
_sendLock.Dispose();
|
||||
_cts.Dispose();
|
||||
|
||||
_parent.InvokeUserDisconnected(this);
|
||||
// Remove this item from the Dictionary on the Server for garbage collection
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// Client Connections
|
||||
@@ -9,38 +8,46 @@ using System.Threading.Tasks;
|
||||
namespace Mistox.Server {
|
||||
public class ServerListener : IDisposable {
|
||||
|
||||
TcpListener _Listener;
|
||||
private readonly MistoxServer _parent;
|
||||
private readonly IPEndPoint _bindAddress;
|
||||
private readonly TcpListener _Listener;
|
||||
|
||||
public ServerListener() {
|
||||
Thread ConnectionThread = new Thread(async () => await ListenerThread() );
|
||||
ConnectionThread.Start();
|
||||
public ServerListener(IPEndPoint listenerAddress, MistoxServer parent) {
|
||||
_bindAddress = listenerAddress;
|
||||
_parent = parent;
|
||||
|
||||
_Listener = new TcpListener( _bindAddress );
|
||||
_Listener.Start();
|
||||
|
||||
// Start the thread
|
||||
Task.Run(ListenerThread);
|
||||
}
|
||||
|
||||
async Task ListenerThread() {
|
||||
_Listener = new TcpListener( ServerInterface.ServerInstance.BindAddress, ServerInterface.ServerInstance.Port );
|
||||
_Listener.Start();
|
||||
while( ServerInterface.ServerInstance.Alive ) {
|
||||
while( _parent.Alive ) {
|
||||
TcpClient client = await _Listener.AcceptTcpClientAsync();
|
||||
|
||||
if (ServerInterface.Connections.Count + 1 > ServerInterface.ServerInstance.MaxClients) {
|
||||
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();
|
||||
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
|
||||
};
|
||||
|
||||
ServerInterface.Connections[connectionID] = new TcpClientConnection() {
|
||||
Connection = new TCPConnection( client, connectionID ),
|
||||
RemoteAddress = remote.Address
|
||||
};
|
||||
|
||||
Console.WriteLine( "New User Connected" );
|
||||
Console.WriteLine( "New User Connected" );
|
||||
}
|
||||
}
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_Listener.Stop();
|
||||
_Listener = null;
|
||||
_Listener.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mistox.Server {
|
||||
public class TCPConnection : IDisposable {
|
||||
|
||||
TcpClient _client;
|
||||
TcpClientConnection _connection;
|
||||
|
||||
public TCPConnection( TcpClient tcpClient, string ConnectionID ) {
|
||||
_client = tcpClient;
|
||||
_client.NoDelay = true;
|
||||
_connection = ServerInterface.Connections[ConnectionID];
|
||||
ServerInterface.ServerInstance.InvokeUserConnected( ServerInterface.Connections[ConnectionID] );
|
||||
// Start the receive thread
|
||||
Thread receiveThread = new Thread(async () => await ReceiveThread() );
|
||||
receiveThread.Start();
|
||||
}
|
||||
|
||||
async Task ReceiveThread() {
|
||||
using( NetworkStream ns = _client.GetStream() ) {
|
||||
try {
|
||||
byte[] headerBuffer = new byte[DataPacket.HeaderLength];
|
||||
byte[] payloadBuffer = new byte[65535]; // Upper limit for safety
|
||||
while( _connection.Connected ) {
|
||||
|
||||
// Read the header off
|
||||
await ns.ReadExactlyAsync(headerBuffer, 0, 3);
|
||||
|
||||
// Parse the header
|
||||
byte packetbyte = headerBuffer[0];
|
||||
ushort dataLength = (ushort)((headerBuffer[1] << 8) | headerBuffer[2]);
|
||||
|
||||
// Make sure the data looks valid
|
||||
if (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);
|
||||
|
||||
// Get the type from the tag
|
||||
if (!DataPacket.ServerToClientPackets.TryGetValue(packetbyte, out Type resolvedType)) {
|
||||
throw new Exception($"Unknown packet type ID: {packetbyte}");
|
||||
}
|
||||
|
||||
// Create the dataobject
|
||||
dynamic dataobject = DataPacket.Deserialize( resolvedType, dataLength, payloadBuffer.AsMemory(DataPacket.HeaderLength, dataLength) );
|
||||
|
||||
// Return the object
|
||||
ServerInterface.ServerInstance.InvokeDataReceived(dataobject);
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ClientInterface.ServerClientInstance.Alive = false;
|
||||
Dispose();
|
||||
Console.WriteLine("TCP Receive exception: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Send<T>( T packet ) {
|
||||
byte[] byteData = DataPacket.Serialize( packet );
|
||||
await _client.GetStream().WriteAsync( byteData, 0, byteData.Length );
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_connection.Connected = false;
|
||||
ServerInterface.ServerInstance.InvokeUserDisconnected( _connection );
|
||||
ServerInterface.ServerInstance.Alive = false;
|
||||
ServerInterface.Connections.TryRemove( _connection.UserName, out _ );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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( Type PacketType, ushort DataLength, ReadOnlyMemory<byte> Data ) {
|
||||
public static object? Deserialize( DataPacketHeader Header, ReadOnlyMemory<byte> Data ) {
|
||||
|
||||
// Sanity check: Ensure the packet we received actually has the payload bytes it claims to have
|
||||
if ( DataLength <= Data.Length ) {
|
||||
// Get the type from the tag
|
||||
if (!NetworkPacketDictionary.PacketLookupTable.TryGetValue(Header.Type, out Type? resolvedType)) {
|
||||
throw new Exception($"Unknown packet type ID: {Header.Type}");
|
||||
}
|
||||
|
||||
// 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.");
|
||||
}
|
||||
|
||||
// 4. Create the low-level reader struct directly over the memory slice
|
||||
var reader = new MessagePackReader(Data);
|
||||
// Make sure the data is exactly the size requested
|
||||
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( PacketType, ref reader, MessagePackSerializerOptions.Standard );
|
||||
return MessagePackSerializer.Deserialize( resolvedType, ref reader, MessagePackSerializerOptions.Standard );
|
||||
|
||||
return obj;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Fills out the NetworkPacket Lookup Dictionary
|
||||
public static readonly Dictionary<byte, Type> ClientToClientPackets = new();
|
||||
public static readonly Dictionary<byte, Type> ServerToClientPackets = new();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Mistox.Attributes {
|
||||
|
||||
public class NetworkPacketDictionary {
|
||||
|
||||
// Fills out the NetworkPacket Lookup Dictionary
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using Mistox.Attributes;
|
||||
|
||||
namespace Mistox.ResevedPackets {
|
||||
|
||||
// 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; } = 0;
|
||||
|
||||
// Packet payload data that im matching for in the dictionary
|
||||
public byte[] InnerPayload { 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 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; } = string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
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
|
||||
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 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
|
||||
Reference in new issue
Block a user