From 527cc6945a9da306a7fd732bab61e17c4963f0ca Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 5 Apr 2023 03:26:55 +0100 Subject: [PATCH] Re-add networking-specific code --- Samples/mocha-minimal/code/Game.cs | 14 +- .../Networking/ClientOnlyAttribute.cs | 18 ++ .../Networking/HandlesNetworkedType.cs | 6 + .../Networking/ReplicatedAttribute.cs | 2 + .../Networking/ServerOnlyAttribute.cs | 18 ++ Source/Mocha.Common/Entities/IEntity.cs | 1 + Source/Mocha.Common/Networking/IClient.cs | 11 + Source/Mocha.Common/Types/NetworkId.cs | 97 +++++++ Source/Mocha.Engine/BaseGame.cs | 40 ++- Source/Mocha.Engine/BaseGameClient.cs | 124 +++++++++ Source/Mocha.Engine/BaseGameServer.cs | 248 ++++++++++++++++++ Source/Mocha.Engine/Mocha.Engine.csproj | 1 + Source/Mocha.Engine/World/Base/BaseEntity.cs | 30 ++- Source/Mocha.Engine/World/Base/ModelEntity.cs | 22 +- Source/Mocha.Host/Mocha.Host.vcxproj | 7 + Source/Mocha.Host/Mocha.Host.vcxproj.filters | 7 + .../Networking/networkingmanager.cpp | 26 ++ .../Mocha.Host/Networking/networkingmanager.h | 9 + .../Networking/valvesocketclient.cpp | 127 +++++++++ .../Mocha.Host/Networking/valvesocketclient.h | 33 +++ .../Networking/valvesocketreceivedmessage.h | 9 + .../Networking/valvesocketserver.cpp | 171 ++++++++++++ .../Mocha.Host/Networking/valvesocketserver.h | 41 +++ Source/Mocha.Host/Root/root.cpp | 5 + Source/Mocha.Networking/AssemblyInfo.cs | 19 ++ .../Client/Client.ServerConnection.cs | 29 ++ Source/Mocha.Networking/Client/Client.cs | 66 +++++ Source/Mocha.Networking/Global.cs | 1 + Source/Mocha.Networking/MessageIDs.cs | 9 + .../Messages/BaseNetworkMessage.cs | 6 + .../Messages/ClientInputMessage.cs | 24 ++ .../Messages/HandshakeMessage.cs | 20 ++ .../Messages/KickedMessage.cs | 12 + .../Messages/SnapshotUpdateMessage.cs | 49 ++++ .../Mocha.Networking/Mocha.Networking.csproj | 19 ++ Source/Mocha.Networking/NetworkMessage.cs | 10 + .../Formatters/NetworkIdFormatter.cs | 18 ++ .../Formatters/RotationFormatter.cs | 33 +++ .../Formatters/Vector3Formatter.cs | 31 +++ .../Serialization/NetworkSerializer.cs | 42 +++ .../Serialization/Resolvers/MochaResolver.cs | 55 ++++ .../Server/Server.ClientConnection.cs | 71 +++++ Source/Mocha.Networking/Server/Server.cs | 82 ++++++ Source/Mocha.Networking/Shared/Client.cs | 10 + .../Shared/ConnectionManager.cs | 33 +++ Source/Mocha.Networking/Shared/IConnection.cs | 16 ++ .../Shared/ValveSocketReceivedMessage.cs | 11 + Source/Mocha.Tests/Mocha.Tests.csproj | 1 + Source/Mocha.Tests/NetworkSerializerTests.cs | 117 +++++++++ Source/Mocha.sln | 15 ++ 50 files changed, 1848 insertions(+), 18 deletions(-) create mode 100644 Source/Mocha.Common/Attributes/Networking/ClientOnlyAttribute.cs create mode 100644 Source/Mocha.Common/Attributes/Networking/HandlesNetworkedType.cs create mode 100644 Source/Mocha.Common/Attributes/Networking/ReplicatedAttribute.cs create mode 100644 Source/Mocha.Common/Attributes/Networking/ServerOnlyAttribute.cs create mode 100644 Source/Mocha.Common/Networking/IClient.cs create mode 100644 Source/Mocha.Common/Types/NetworkId.cs create mode 100644 Source/Mocha.Engine/BaseGameClient.cs create mode 100644 Source/Mocha.Engine/BaseGameServer.cs create mode 100644 Source/Mocha.Host/Networking/networkingmanager.cpp create mode 100644 Source/Mocha.Host/Networking/networkingmanager.h create mode 100644 Source/Mocha.Host/Networking/valvesocketclient.cpp create mode 100644 Source/Mocha.Host/Networking/valvesocketclient.h create mode 100644 Source/Mocha.Host/Networking/valvesocketreceivedmessage.h create mode 100644 Source/Mocha.Host/Networking/valvesocketserver.cpp create mode 100644 Source/Mocha.Host/Networking/valvesocketserver.h create mode 100644 Source/Mocha.Networking/AssemblyInfo.cs create mode 100644 Source/Mocha.Networking/Client/Client.ServerConnection.cs create mode 100644 Source/Mocha.Networking/Client/Client.cs create mode 100644 Source/Mocha.Networking/Global.cs create mode 100644 Source/Mocha.Networking/MessageIDs.cs create mode 100644 Source/Mocha.Networking/Messages/BaseNetworkMessage.cs create mode 100644 Source/Mocha.Networking/Messages/ClientInputMessage.cs create mode 100644 Source/Mocha.Networking/Messages/HandshakeMessage.cs create mode 100644 Source/Mocha.Networking/Messages/KickedMessage.cs create mode 100644 Source/Mocha.Networking/Messages/SnapshotUpdateMessage.cs create mode 100644 Source/Mocha.Networking/Mocha.Networking.csproj create mode 100644 Source/Mocha.Networking/NetworkMessage.cs create mode 100644 Source/Mocha.Networking/Serialization/Formatters/NetworkIdFormatter.cs create mode 100644 Source/Mocha.Networking/Serialization/Formatters/RotationFormatter.cs create mode 100644 Source/Mocha.Networking/Serialization/Formatters/Vector3Formatter.cs create mode 100644 Source/Mocha.Networking/Serialization/NetworkSerializer.cs create mode 100644 Source/Mocha.Networking/Serialization/Resolvers/MochaResolver.cs create mode 100644 Source/Mocha.Networking/Server/Server.ClientConnection.cs create mode 100644 Source/Mocha.Networking/Server/Server.cs create mode 100644 Source/Mocha.Networking/Shared/Client.cs create mode 100644 Source/Mocha.Networking/Shared/ConnectionManager.cs create mode 100644 Source/Mocha.Networking/Shared/IConnection.cs create mode 100644 Source/Mocha.Networking/Shared/ValveSocketReceivedMessage.cs create mode 100644 Source/Mocha.Tests/NetworkSerializerTests.cs diff --git a/Samples/mocha-minimal/code/Game.cs b/Samples/mocha-minimal/code/Game.cs index cfc1944a..bb808e28 100644 --- a/Samples/mocha-minimal/code/Game.cs +++ b/Samples/mocha-minimal/code/Game.cs @@ -7,7 +7,7 @@ public class Game : BaseGame { [HotloadSkip] private UIManager Hud { get; set; } - public string NetworkedString { get; set; } + [Sync] public string NetworkedString { get; set; } public override void OnStartup() { @@ -32,9 +32,15 @@ public override void OnStartup() } } - [Event.Tick] - public void Tick() + [Event.Tick, ServerOnly] + public void ServerTick() { - DebugOverlay.ScreenText( $"Tick... ({GetType().Assembly.GetHashCode()})" ); + DebugOverlay.ScreenText( $"Server Tick... ({GetType().Assembly.GetHashCode()})" ); + } + + [Event.Tick, ClientOnly] + public void ClientTick() + { + DebugOverlay.ScreenText( $"Client Tick... ({GetType().Assembly.GetHashCode()})" ); } } diff --git a/Source/Mocha.Common/Attributes/Networking/ClientOnlyAttribute.cs b/Source/Mocha.Common/Attributes/Networking/ClientOnlyAttribute.cs new file mode 100644 index 00000000..693b25bf --- /dev/null +++ b/Source/Mocha.Common/Attributes/Networking/ClientOnlyAttribute.cs @@ -0,0 +1,18 @@ +namespace Mocha.Common; + +[AttributeUsage( AttributeTargets.Class | + AttributeTargets.Interface | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Field | + AttributeTargets.Property | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Delegate | + AttributeTargets.Event, Inherited = true, AllowMultiple = false )] +public sealed class ClientOnlyAttribute : Attribute +{ + public ClientOnlyAttribute() + { + } +} diff --git a/Source/Mocha.Common/Attributes/Networking/HandlesNetworkedType.cs b/Source/Mocha.Common/Attributes/Networking/HandlesNetworkedType.cs new file mode 100644 index 00000000..b983e241 --- /dev/null +++ b/Source/Mocha.Common/Attributes/Networking/HandlesNetworkedType.cs @@ -0,0 +1,6 @@ +namespace Mocha; + +[System.AttributeUsage( AttributeTargets.Class, Inherited = false, AllowMultiple = false )] +internal sealed class HandlesNetworkedTypeAttribute : Attribute +{ +} diff --git a/Source/Mocha.Common/Attributes/Networking/ReplicatedAttribute.cs b/Source/Mocha.Common/Attributes/Networking/ReplicatedAttribute.cs new file mode 100644 index 00000000..119caad9 --- /dev/null +++ b/Source/Mocha.Common/Attributes/Networking/ReplicatedAttribute.cs @@ -0,0 +1,2 @@ +[System.AttributeUsage( AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false )] +public sealed class SyncAttribute : Attribute { } diff --git a/Source/Mocha.Common/Attributes/Networking/ServerOnlyAttribute.cs b/Source/Mocha.Common/Attributes/Networking/ServerOnlyAttribute.cs new file mode 100644 index 00000000..ed0db746 --- /dev/null +++ b/Source/Mocha.Common/Attributes/Networking/ServerOnlyAttribute.cs @@ -0,0 +1,18 @@ +namespace Mocha.Common; + +[AttributeUsage( AttributeTargets.Class | + AttributeTargets.Interface | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Field | + AttributeTargets.Property | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Delegate | + AttributeTargets.Event, Inherited = true, AllowMultiple = false )] +public sealed class ServerOnlyAttribute : Attribute +{ + public ServerOnlyAttribute() + { + } +} diff --git a/Source/Mocha.Common/Entities/IEntity.cs b/Source/Mocha.Common/Entities/IEntity.cs index 760336c3..0cee5526 100644 --- a/Source/Mocha.Common/Entities/IEntity.cs +++ b/Source/Mocha.Common/Entities/IEntity.cs @@ -4,6 +4,7 @@ public interface IEntity { string Name { get; set; } uint NativeHandle { get; } + NetworkId NetworkId { get; set; } Vector3 Position { get; set; } Rotation Rotation { get; set; } diff --git a/Source/Mocha.Common/Networking/IClient.cs b/Source/Mocha.Common/Networking/IClient.cs new file mode 100644 index 00000000..5a75ce0f --- /dev/null +++ b/Source/Mocha.Common/Networking/IClient.cs @@ -0,0 +1,11 @@ +namespace Mocha.Common; + +/// +/// Represents a client connected to a server. +/// +public interface IClient +{ + public abstract string Name { get; set; } + public abstract int Ping { get; set; } + public abstract IEntity Pawn { get; set; } +} diff --git a/Source/Mocha.Common/Types/NetworkId.cs b/Source/Mocha.Common/Types/NetworkId.cs new file mode 100644 index 00000000..398b4002 --- /dev/null +++ b/Source/Mocha.Common/Types/NetworkId.cs @@ -0,0 +1,97 @@ +namespace Mocha.Common; + +/// +/// A NetworkId is a wrapper around a 64-bit unsigned integer value used to identify an entity.
+/// The first bit of the value is used in order to determine whether the value is networked or local.
+/// The binary representation of the value is used to distinguish between local and networked entities.
+/// Note that the same ID (e.g., 12345678) can be used twice - once locally, and once networked - to
+/// refer to two distinct entities. The IDs used within this class should not reflect the native engine
+/// handle for the entity - NetworkIds are unique to a managed context. +///
+/// +/// For example, take an entity with the ID 12345678:
+///
+/// Local
+/// 00000000000000000000000000000000000000000000000000000000010111101
+///
+/// Networked
+/// 10000000000000000000000000000000000000000000000000000000010111101
+///
+/// Note that the first bit is set to 1 in the binary representation of the networked entity. +///
+public class NetworkId : IEquatable +{ + internal ulong Value { get; private set; } + + internal NetworkId( ulong value ) + { + Value = value; + } + + public NetworkId() { } + + public bool IsNetworked() + { + // If first bit of the value is set, it's a networked entity + return (Value & 0x8000000000000000) != 0; + } + public bool IsLocal() + { + // If first bit of the value is not set, it's a local entity + return (Value & 0x8000000000000000) == 0; + } + + public ulong GetValue() + { + // Returns the value without the first bit + return Value & 0x7FFFFFFFFFFFFFFF; + } + + public static NetworkId CreateLocal() + { + // Create a local entity by setting the first bit to 0 + // Use EntityRegistry.Instance to get the next available local id + return new( (uint)EntityRegistry.Instance.Count() << 1 ); + } + + public static NetworkId CreateNetworked() + { + // Create a networked entity by setting the first bit to 1 + // Use EntityRegistry.Instance to get the next available local id + return new( (uint)EntityRegistry.Instance.Count() | 0x8000000000000000 ); + } + + public static implicit operator ulong( NetworkId id ) => id.GetValue(); + public static implicit operator NetworkId( ulong value ) => new( value ); + + public override string ToString() + { + return $"{(IsNetworked() ? "Networked" : "Local")}: {GetValue()} ({Value})"; + } + + public bool Equals( NetworkId? other ) + { + if ( other is null ) + return false; + + return Value == other.Value; + } + + public override bool Equals( object? obj ) + { + if ( obj is NetworkId id ) + return Equals( id ); + + return false; + } + + public static bool operator ==( NetworkId? a, NetworkId? b ) + { + return a?.Equals( b ) ?? false; + } + + public static bool operator !=( NetworkId? a, NetworkId? b ) + { + return !(a?.Equals( b ) ?? false); + } +} diff --git a/Source/Mocha.Engine/BaseGame.cs b/Source/Mocha.Engine/BaseGame.cs index 6c719c99..34427e05 100644 --- a/Source/Mocha.Engine/BaseGame.cs +++ b/Source/Mocha.Engine/BaseGame.cs @@ -1,9 +1,14 @@ -namespace Mocha; +using Mocha.Networking; + +namespace Mocha; public class BaseGame : IGame { public static BaseGame Current { get; set; } + private static Server? s_server; + private static Client? s_client; + public BaseGame() { Current = this; @@ -55,6 +60,10 @@ public void FrameUpdate() public void Update() { + // TODO: This is garbage and should not be here!!! + s_server?.Update(); + s_client?.Update(); + if ( Core.IsClient ) { // HACK: Clear DebugOverlay here because doing it @@ -75,6 +84,19 @@ public void Shutdown() public void Startup() { + if ( Core.IsClient ) + { + s_client = new BaseGameClient( "127.0.0.1" ); + } + else + { + s_server = new BaseGameServer() + { + OnClientConnectedEvent = ( connection ) => OnClientConnected( connection.GetClient() ), + OnClientDisconnectedEvent = ( connection ) => OnClientDisconnected( connection.GetClient() ), + }; + } + OnStartup(); } @@ -93,6 +115,22 @@ public virtual void OnStartup() public virtual void OnShutdown() { + } + + /// + /// Called on the server whenever a client joins + /// + public virtual void OnClientConnected( IClient client ) + { + + } + + /// + /// Called on the server whenever a client leaves + /// + public virtual void OnClientDisconnected( IClient client ) + { + } #endregion diff --git a/Source/Mocha.Engine/BaseGameClient.cs b/Source/Mocha.Engine/BaseGameClient.cs new file mode 100644 index 00000000..b169e1cb --- /dev/null +++ b/Source/Mocha.Engine/BaseGameClient.cs @@ -0,0 +1,124 @@ +using Mocha.Networking; +using System.Reflection; + +namespace Mocha; +public class BaseGameClient : Client +{ + private ServerConnection _connection; + + public BaseGameClient( string ipAddress, ushort port = 10570 ) : base( ipAddress, port ) + { + _connection = new ServerConnection(); + RegisterHandler( OnKickedMessage ); + RegisterHandler( OnSnapshotUpdateMessage ); + RegisterHandler( OnHandshakeMessage ); + } + + public override void OnMessageReceived( byte[] data ) + { + InvokeHandler( _connection, data ); + } + + public void OnKickedMessage( IConnection connection, KickedMessage kickedMessage ) + { + Log.Info( $"BaseGameClient: We were kicked: '{kickedMessage.Reason}'" ); + } + + private Type? LocateType( string typeName ) + { + var type = Type.GetType( typeName )!; + + if ( type != null ) + return type; + + type = Assembly.GetExecutingAssembly().GetType( typeName ); + + if ( type != null ) + return type; + + type = Assembly.GetCallingAssembly().GetType( typeName ); + + if ( type != null ) + return type; + + foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() ) + { + type = assembly.GetType( typeName ); + if ( type != null ) + return type; + } + + return null; + } + + public void OnSnapshotUpdateMessage( IConnection connection, SnapshotUpdateMessage snapshotUpdateMessage ) + { + foreach ( var entityChange in snapshotUpdateMessage.EntityChanges ) + { + // Log.Info( $"BaseGameClient: Entity {entityChange.NetworkId} changed" ); + + // Does this entity already exist? + var entity = EntityRegistry.Instance.FirstOrDefault( x => x.NetworkId == entityChange.NetworkId ); + + if ( entity == null ) + { + // Entity doesn't exist locally - let's create it + var type = LocateType( entityChange.TypeName ); + + if ( type == null ) + { + // Log.Error( $"BaseGameClient: Unable to locate type '{entityChange.TypeName}'" ); + continue; + } + + entity = (Activator.CreateInstance( type ) as IEntity)!; + + // Set the network ID + entity.NetworkId = entityChange.NetworkId; + + // Log.Info( $"BaseGameClient: Created entity {entity.NetworkId}" ); + } + + foreach ( var memberChange in entityChange.MemberChanges ) + { + if ( memberChange.Data == null ) + continue; + + var member = entity.GetType().GetMember( memberChange.FieldName ).First()!; + var value = NetworkSerializer.Deserialize( memberChange.Data, member.GetMemberType() ); + + if ( value == null ) + continue; + + if ( member.MemberType == MemberTypes.Field ) + { + var field = (FieldInfo)member; + field.SetValue( entity, value ); + + // Log.Info( $"BaseGameClient: Entity {entityChange.NetworkId} field {memberChange.FieldName} changed to {value}" ); + } + else if ( member.MemberType == MemberTypes.Property ) + { + var property = (PropertyInfo)member; + property.SetValue( entity, value ); + + // Log.Info( $"BaseGameClient: Entity {entityChange.NetworkId} property {memberChange.FieldName} changed to {value}" ); + } + + //if ( memberChange.FieldName == "PhysicsSetup" ) + //{ + // // Physics setup changed - let's update the physics setup + // var physicsSetup = (ModelEntity.Physics)value; + + // if ( physicsSetup.PhysicsModelPath != null ) + // ((ModelEntity)entity).SetMeshPhysics( physicsSetup.PhysicsModelPath ); + //} + } + } + } + + public void OnHandshakeMessage( IConnection connection, HandshakeMessage handshakeMessage ) + { + Log.Info( $"BaseGameClient: Handshake received. Tick rate: {handshakeMessage.TickRate}, nickname: {handshakeMessage.Nickname}" ); + } +} diff --git a/Source/Mocha.Engine/BaseGameServer.cs b/Source/Mocha.Engine/BaseGameServer.cs new file mode 100644 index 00000000..41b10caa --- /dev/null +++ b/Source/Mocha.Engine/BaseGameServer.cs @@ -0,0 +1,248 @@ +using Mocha.Networking; +using System.Collections.Immutable; +using System.Reflection; + +namespace Mocha; + +internal struct Snapshot +{ + private ImmutableList _entities; + + public static Snapshot Create() + { + // Create a snapshot based on everything in EntityRegistry + var snapshot = new Snapshot(); + snapshot._entities = EntityRegistry.Instance.ToImmutableList(); + + return snapshot; + } + + public static List Delta( Snapshot snapshot1, Snapshot snapshot2 ) + { + var changedMembers = new List(); + + // Get the list of entities from each snapshot + var entities1 = snapshot1._entities; + var entities2 = snapshot2._entities; + + // Loop through each entity in snapshot2 + foreach ( var entity2 in entities2 ) + { + // Find the corresponding entity in snapshot1, if any + var entity1 = entities1.FirstOrDefault( e => e.NetworkId == entity2.NetworkId ); + + // If the entity doesn't exist in snapshot1, it's a new entity and all its members have changed + if ( entity1 == null ) + { + changedMembers.AddRange( entity2.GetType().GetMembers().ToList() ); + continue; + } + + // Loop through each member of the entity + foreach ( var member in entity2.GetType().GetMembers() ) + { + // Skip non-property and non-field members + if ( member is not PropertyInfo && member is not FieldInfo ) + continue; + + // Get the value of the member for each entity + var value1 = GetValueForMember( member, entity1 ); + var value2 = GetValueForMember( member, entity2 ); + + // Compare the values + if ( !object.Equals( value1, value2 ) ) + { + changedMembers.Add( member ); + } + } + } + + return changedMembers; + } + + public SnapshotUpdateMessage CreateSnapshotUpdateMessage() + { + // Send initial SnapshotUpdateMessage + var snapshotUpdateMessage = new SnapshotUpdateMessage + { + PreviousTimestamp = -1, + CurrentTimestamp = Time.Now, + SequenceNumber = 0 + }; + + foreach ( var entity in EntityRegistry.Instance ) + { + var entityChange = new SnapshotUpdateMessage.EntityChange(); + entityChange.NetworkId = entity.NetworkId; + entityChange.MemberChanges = new List(); + entityChange.TypeName = entity.GetType().FullName!; + + if ( entity.NetworkId.IsLocal() ) + continue; // Not networked, skip + + foreach ( var member in entity.GetType().GetMembers() ) + { + // Only replicate fields and properties that are marked with [Replicated]. + if ( member.GetCustomAttribute() == null ) + continue; + + if ( member.MemberType == MemberTypes.Property ) + { + var property = member as PropertyInfo; + + if ( property != null ) + { + var value = property.GetValue( entity ); + var entityMemberChange = new SnapshotUpdateMessage.EntityMemberChange(); + entityMemberChange.FieldName = property.Name; + entityMemberChange.Data = NetworkSerializer.Serialize( value ); + entityChange.MemberChanges.Add( entityMemberChange ); + } + } + else if ( member.MemberType == MemberTypes.Field ) + { + var field = member as FieldInfo; + + if ( field != null ) + { + var value = field.GetValue( entity ); + var entityMemberChange = new SnapshotUpdateMessage.EntityMemberChange(); + entityMemberChange.FieldName = field.Name; + entityMemberChange.Data = NetworkSerializer.Serialize( value ); + entityChange.MemberChanges.Add( entityMemberChange ); + } + } + } + + snapshotUpdateMessage.EntityChanges.Add( entityChange ); + } + + return snapshotUpdateMessage; + } + + // Helper function to get the value of a member for an entity + private static object GetValueForMember( MemberInfo member, IEntity entity ) + { + if ( member is PropertyInfo property ) + { + return property.GetValue( entity ); + } + else if ( member is FieldInfo field ) + { + return field.GetValue( entity ); + } + else + { + throw new ArgumentException( $"Member {member.Name} is not a property or field" ); + } + } +} + +internal class SnapshotList : List +{ + public SnapshotList( int capacity ) : base( capacity ) + { + } + + public SnapshotList( IEnumerable collection ) : base( collection ) + { + } + + public SnapshotList() + { + } + + private new void Add( Snapshot snapshot ) + { + // Add the snapshot to the list + base.Add( snapshot ); + + // Remove the oldest snapshot if the list is too long + if ( Count > 32 ) + { + RemoveAt( 0 ); + } + } + + public void Update() + { + var snapshot = Snapshot.Create(); + Add( snapshot ); + } +} + +public class BaseGameServer : Server +{ + private Dictionary _snapshots = new Dictionary(); + + public Action OnClientConnectedEvent; + public Action OnClientDisconnectedEvent; + + public BaseGameServer() + { + RegisterHandler( OnClientInputMessage ); + } + + public override void OnClientConnected( IConnection connection ) + { + if ( connection is not ClientConnection client ) + return; + + Log.Info( $"BaseGameServer: Client {client} connected" ); + + // Send initial HandshakeMessage + var handshakeMessage = new HandshakeMessage + { + Timestamp = Time.Now, + TickRate = Core.TickRate, + Nickname = client.Nickname + }; + client.Send( handshakeMessage ); + + var snapshot = new Snapshot(); + var snapshotUpdateMessage = snapshot.CreateSnapshotUpdateMessage(); + client.Send( snapshotUpdateMessage ); + + OnClientConnectedEvent?.Invoke( connection ); + } + + public override void OnClientDisconnected( IConnection connection ) + { + Log.Info( $"BaseGameServer: Client {connection} disconnected" ); + + OnClientDisconnectedEvent?.Invoke( connection ); + } + + public override void OnMessageReceived( IConnection client, byte[] data ) + { + InvokeHandler( client, data ); + } + + public void OnClientInputMessage( IConnection client, ClientInputMessage clientInputMessage ) + { + var snapshot = new Snapshot(); + var snapshotUpdateMessage = snapshot.CreateSnapshotUpdateMessage(); + client.Send( snapshotUpdateMessage ); + + return; + + Log.Info( $@"BaseGameServer: Client {client} sent input message: + ViewAngles: {clientInputMessage.ViewAnglesP}, {clientInputMessage.ViewAnglesY}, {clientInputMessage.ViewAnglesR} + Direction: {clientInputMessage.DirectionX}, {clientInputMessage.DirectionY}, {clientInputMessage.DirectionZ} + Left: {clientInputMessage.Left} + Right: {clientInputMessage.Right} + Middle: {clientInputMessage.Middle}" ); + } + + public override void OnUpdate() + { + foreach ( var connection in _connectedClients ) + { + SnapshotList list; + if ( !_snapshots.TryGetValue( connection, out list ) ) + list = _snapshots[connection] = new SnapshotList(); + + list.Update(); + } + } +} diff --git a/Source/Mocha.Engine/Mocha.Engine.csproj b/Source/Mocha.Engine/Mocha.Engine.csproj index 9692d00b..a6da9833 100644 --- a/Source/Mocha.Engine/Mocha.Engine.csproj +++ b/Source/Mocha.Engine/Mocha.Engine.csproj @@ -51,6 +51,7 @@ + diff --git a/Source/Mocha.Engine/World/Base/BaseEntity.cs b/Source/Mocha.Engine/World/Base/BaseEntity.cs index b3dd640a..268d8e23 100644 --- a/Source/Mocha.Engine/World/Base/BaseEntity.cs +++ b/Source/Mocha.Engine/World/Base/BaseEntity.cs @@ -18,28 +18,28 @@ public bool IsValid() return true; } - [Category( "Transform" )] + [Category( "Transform" ), Sync] public Vector3 Scale { get => NativeEntity.GetScale(); set => NativeEntity.SetScale( value ); } - [Category( "Transform" )] + [Category( "Transform" ), Sync] public Vector3 Position { get => NativeEntity.GetPosition(); set => NativeEntity.SetPosition( value ); } - [Category( "Transform" )] + [Category( "Transform" ), Sync] public Rotation Rotation { get => NativeEntity.GetRotation(); set => NativeEntity.SetRotation( value ); } - [HideInInspector] + [HideInInspector, Sync] public string Name { get => NativeEntity.GetName(); @@ -56,11 +56,14 @@ public bool IsUI set => NativeEntity.SetUI( value ); } + public NetworkId NetworkId { get; set; } + public BaseEntity() { EntityRegistry.Instance.RegisterEntity( this ); CreateNativeEntity(); + CreateNetworkId(); Position = new Vector3( 0, 0, 0 ); Rotation = new Rotation( 0, 0, 0, 1 ); @@ -75,6 +78,25 @@ public BaseEntity() Spawn(); } + private void CreateNetworkId() + { + if ( Core.IsClient ) + { + // On client - we don't want to "upstream" this to the server, so we'll + // make this a local entity + NetworkId = NetworkId.CreateLocal(); + + Log.Info( $"Created local entity {Name} with network id {NetworkId}" ); + } + else + { + // On server - we'll network this across to clients + NetworkId = NetworkId.CreateNetworked(); + + Log.Info( $"Created networked entity {Name} with network id {NetworkId}" ); + } + } + protected virtual void Spawn() { } diff --git a/Source/Mocha.Engine/World/Base/ModelEntity.cs b/Source/Mocha.Engine/World/Base/ModelEntity.cs index 70e78eae..a9974fa6 100644 --- a/Source/Mocha.Engine/World/Base/ModelEntity.cs +++ b/Source/Mocha.Engine/World/Base/ModelEntity.cs @@ -1,54 +1,58 @@ -namespace Mocha; +using MessagePack; + +namespace Mocha; [Category( "World" ), Title( "Model Entity" ), Icon( FontAwesome.Cube )] public partial class ModelEntity : BaseEntity { // This is a stop-gap solution until we have a proper physics body implementation + [MessagePackObject] public struct Physics { + [Key( 0 )] public string PhysicsModelPath { get; set; } } [HideInInspector] private Glue.ModelEntity NativeModelEntity => NativeEngine.GetEntityManager().GetModelEntity( NativeHandle ); - [Category( "Physics" )] + [Category( "Physics" ), Sync] public Vector3 Velocity { get => NativeModelEntity.GetVelocity(); set => NativeModelEntity.SetVelocity( value ); } - [Category( "Physics" )] + [Category( "Physics" ), Sync] public float Mass { get => NativeModelEntity.GetMass(); set => NativeModelEntity.SetMass( value ); } - [Category( "Physics" )] + [Category( "Physics" ), Sync] public float Friction { get => NativeModelEntity.GetFriction(); set => NativeModelEntity.SetFriction( value ); } - [Category( "Physics" )] + [Category( "Physics" ), Sync] public float Restitution { get => NativeModelEntity.GetRestitution(); set => NativeModelEntity.SetRestitution( value ); } - [Category( "Physics" )] + [Category( "Physics" ), Sync] public bool IgnoreRigidbodyRotation { get => NativeModelEntity.GetIgnoreRigidbodyRotation(); set => NativeModelEntity.SetIgnoreRigidbodyRotation( value ); } - [Category( "Physics" )] + [Category( "Physics" ), Sync] public bool IgnoreRigidbodyPosition { get => NativeModelEntity.GetIgnoreRigidbodyPosition(); @@ -67,7 +71,7 @@ public IModel Model } } - [Category( "Rendering" )] + [Category( "Rendering" ), Sync] public string ModelPath { get => _modelPath; @@ -78,7 +82,7 @@ public string ModelPath } } - [HideInInspector] + [HideInInspector, Sync] public Physics PhysicsSetup { get; set; } public ModelEntity() diff --git a/Source/Mocha.Host/Mocha.Host.vcxproj b/Source/Mocha.Host/Mocha.Host.vcxproj index 44d5b0a7..23c81477 100644 --- a/Source/Mocha.Host/Mocha.Host.vcxproj +++ b/Source/Mocha.Host/Mocha.Host.vcxproj @@ -202,6 +202,7 @@ git rev-parse --abbrev-ref HEAD >> gitdefs.h + @@ -354,6 +355,8 @@ git rev-parse --abbrev-ref HEAD >> gitdefs.h + + @@ -376,6 +379,7 @@ git rev-parse --abbrev-ref HEAD >> gitdefs.h + @@ -664,6 +668,9 @@ git rev-parse --abbrev-ref HEAD >> gitdefs.h + + + diff --git a/Source/Mocha.Host/Mocha.Host.vcxproj.filters b/Source/Mocha.Host/Mocha.Host.vcxproj.filters index 9fc5c6b6..216a7db6 100644 --- a/Source/Mocha.Host/Mocha.Host.vcxproj.filters +++ b/Source/Mocha.Host/Mocha.Host.vcxproj.filters @@ -544,6 +544,9 @@ Root + + + @@ -1465,7 +1468,11 @@ Util + + + + diff --git a/Source/Mocha.Host/Networking/networkingmanager.cpp b/Source/Mocha.Host/Networking/networkingmanager.cpp new file mode 100644 index 00000000..eae9c379 --- /dev/null +++ b/Source/Mocha.Host/Networking/networkingmanager.cpp @@ -0,0 +1,26 @@ +#include "networkingmanager.h" + +#include +#include +#include +#include + +void NetworkingManager::Startup() +{ + SteamDatagramErrMsg errMsg; + + if ( !GameNetworkingSockets_Init( nullptr, errMsg ) ) + { + std::stringstream ss; + ss << "GameNetworkingSockets_Init failed.\n"; + ss << errMsg; + + ErrorMessage( ss.str() ); + abort(); + } +} + +void NetworkingManager::Shutdown() +{ + GameNetworkingSockets_Kill(); +} diff --git a/Source/Mocha.Host/Networking/networkingmanager.h b/Source/Mocha.Host/Networking/networkingmanager.h new file mode 100644 index 00000000..a68dc823 --- /dev/null +++ b/Source/Mocha.Host/Networking/networkingmanager.h @@ -0,0 +1,9 @@ +#pragma once +#include + +class NetworkingManager : public ISubSystem +{ +public: + void Startup() override; + void Shutdown() override; +}; diff --git a/Source/Mocha.Host/Networking/valvesocketclient.cpp b/Source/Mocha.Host/Networking/valvesocketclient.cpp new file mode 100644 index 00000000..603a74da --- /dev/null +++ b/Source/Mocha.Host/Networking/valvesocketclient.cpp @@ -0,0 +1,127 @@ +#include "valvesocketclient.h" + +void ValveSocketClient::OnConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t* info ) +{ + spdlog::info( "ValveSocketClient::OnConnectionStatusChanged, new state: {}", info->m_info.m_eState ); + + if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_Connected ) + { + SteamNetConnectionInfo_t connectionInfo; + m_interface->GetConnectionInfo( m_connection, &connectionInfo ); + + char* addrBuf; + addrBuf = ( char* )malloc( 48 ); + connectionInfo.m_addrRemote.ToString( addrBuf, 48, true ); + + std::string addrString( addrBuf ); + spdlog::info( "Client: connected to '{}'", addrString ); + + m_isConnected = true; + + free( addrBuf ); + } + else if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer || + info->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally ) + { + // Dump error info into console + spdlog::info( "{}: {}", info->m_info.m_eEndReason, info->m_info.m_szEndDebug ); + + // Show user-facing error + if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ) + ErrorMessage( "A connection has been actively rejected or closed by the remote host." ); + else if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally ) + ErrorMessage( "A problem was detected with the connection, and it has been closed by the local host." ); + + m_isConnected = false; + + abort(); + } + else if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_None || + info->m_info.m_eState == k_ESteamNetworkingConnectionState_Dead ) + { + spdlog::info( "Client: disconnected" ); + m_isConnected = false; + } +} + +static ValveSocketClient* s_client; +static void SteamNetConnectionStatusChangedCallback( SteamNetConnectionStatusChangedCallback_t* pInfo ) +{ + s_client->OnConnectionStatusChanged( pInfo ); +} + +ValveSocketClient::ValveSocketClient( const char* ip, int port ) +{ + m_interface = SteamNetworkingSockets(); + s_client = this; + + SteamNetworkingIPAddr remoteAddress; + remoteAddress.Clear(); + remoteAddress.ParseString( ip ); + remoteAddress.m_port = port; + + SteamNetworkingConfigValue_t options; + options.SetPtr( k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged, &SteamNetConnectionStatusChangedCallback ); + + m_connection = m_interface->ConnectByIPAddress( remoteAddress, 1, &options ); + spdlog::info( "Client: attempting to connect to {} on port {}", ip, port ); +} + +ValveSocketClient::~ValveSocketClient() +{ + m_interface->CloseConnection( m_connection, 0, nullptr, true ); +} + +void ValveSocketClient::PumpEvents() +{ + ISteamNetworkingMessage* incomingMsg{ nullptr }; + int messageCount = m_interface->ReceiveMessagesOnConnection( m_connection, &incomingMsg, 1 ); + + if ( messageCount == 0 ) + return; + + if ( messageCount < 0 ) + { + std::stringstream ss; + ss << "Expected message count 0 or 1, got "; + ss << messageCount; + ss << " instead."; + ErrorMessage( ss.str() ); + abort(); + } + + char* ptrData = ( char* )incomingMsg->m_pData; + + // Convert to string + const char* data = ( const char* )malloc( incomingMsg->m_cbSize ); + memcpy_s( ( void* )data, incomingMsg->m_cbSize, ptrData, incomingMsg->m_cbSize ); + + ValveSocketReceivedMessage receivedMessage{}; + receivedMessage.connectionHandle = ( void* )m_connection; + receivedMessage.size = incomingMsg->m_cbSize; + receivedMessage.data = ( void* )data; + + m_dataReceivedCallback.Invoke( ( void* )&receivedMessage ); + + incomingMsg->Release(); +} + +void ValveSocketClient::SetDataReceivedCallback( Handle callbackHandle ) +{ + spdlog::info( "Registered data received callback" ); + m_dataReceivedCallback = callbackHandle; +} + +void ValveSocketClient::RunCallbacks() +{ + m_interface->RunCallbacks(); +} + +void ValveSocketClient::SendData( UtilArray interopData ) +{ + if ( !m_isConnected ) + return; + + m_interface->SendMessageToConnection( + m_connection, interopData.data, interopData.size, k_nSteamNetworkingSend_Reliable, nullptr ); +} \ No newline at end of file diff --git a/Source/Mocha.Host/Networking/valvesocketclient.h b/Source/Mocha.Host/Networking/valvesocketclient.h new file mode 100644 index 00000000..6990602f --- /dev/null +++ b/Source/Mocha.Host/Networking/valvesocketclient.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ValveSocketClient +{ +private: + HSteamNetConnection m_connection = {}; + ISteamNetworkingSockets* m_interface; + bool m_isConnected{ false }; + + ManagedCallback m_dataReceivedCallback{}; + +public: + void OnConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t* info ); + + GENERATE_BINDINGS ValveSocketClient( const char* ip, int port ); + + GENERATE_BINDINGS void SetDataReceivedCallback( Handle callbackHandle ); + + GENERATE_BINDINGS void PumpEvents(); + GENERATE_BINDINGS void RunCallbacks(); + GENERATE_BINDINGS void SendData( UtilArray interopData ); + + ~ValveSocketClient(); +}; \ No newline at end of file diff --git a/Source/Mocha.Host/Networking/valvesocketreceivedmessage.h b/Source/Mocha.Host/Networking/valvesocketreceivedmessage.h new file mode 100644 index 00000000..8281333a --- /dev/null +++ b/Source/Mocha.Host/Networking/valvesocketreceivedmessage.h @@ -0,0 +1,9 @@ +#pragma once + +struct ValveSocketReceivedMessage +{ + void* connectionHandle; + + int size; + void* data; +}; \ No newline at end of file diff --git a/Source/Mocha.Host/Networking/valvesocketserver.cpp b/Source/Mocha.Host/Networking/valvesocketserver.cpp new file mode 100644 index 00000000..3ec9824e --- /dev/null +++ b/Source/Mocha.Host/Networking/valvesocketserver.cpp @@ -0,0 +1,171 @@ +#include "valvesocketserver.h" + +#include +#include +#include +#include +#include + +void ValveSocketServer::OnConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t* info ) +{ + spdlog::info( "ValveSocketServer::OnConnectionStatusChanged, new state: {}", info->m_info.m_eState ); + + if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_Connecting ) + { + // TODO: Make sure this client isn't already connected + if ( m_connections.Find( info->m_hConn ) != HANDLE_INVALID ) + { + // Get IP address so we can log it + char* addrBuf; + addrBuf = ( char* )malloc( 48 ); + info->m_info.m_addrRemote.ToString( addrBuf, 48, true ); + + std::string addrString( addrBuf ); + + spdlog::error( "'{}' tried to connect, but we already had them in the list of connected clients?", addrString ); + + free( addrBuf ); + return; + } + + // Accept connection + m_interface->AcceptConnection( info->m_hConn ); + + // Assign poll group + m_interface->SetConnectionPollGroup( info->m_hConn, m_pollGroup ); + } + else if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_Connected ) + { + spdlog::info( "New client connected!" ); + + Handle clientHandle = m_connections.Add( info->m_hConn ); + + // Do something with the client now that they're connected + m_clientConnectedCallback.Invoke( ( void* )clientHandle ); + } + else if ( info->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ) + { + spdlog::info( "Client disconnected!" ); + + Handle clientHandle = m_connections.Find( info->m_hConn ); + + // Do something with the client before we remove all traces of the connection... + m_clientDisconnectedCallback.Invoke( ( void* )clientHandle ); + + m_connections.Remove( info->m_hConn ); + } +} + +static ValveSocketServer* s_server; +static void SteamNetConnectionStatusChangedCallback( SteamNetConnectionStatusChangedCallback_t* pInfo ) +{ + s_server->OnConnectionStatusChanged( pInfo ); +} + +ValveSocketServer::ValveSocketServer( int port ) +{ + m_interface = SteamNetworkingSockets(); + s_server = this; + + SteamNetworkingIPAddr localAddress; + localAddress.Clear(); + localAddress.m_port = port; + + SteamNetworkingConfigValue_t options; + options.SetPtr( k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged, &SteamNetConnectionStatusChangedCallback ); + + m_socket = m_interface->CreateListenSocketIP( localAddress, 1, &options ); + m_pollGroup = m_interface->CreatePollGroup(); + + spdlog::info( "Created ValveSocketServer on port {}", port ); +} + +void ValveSocketServer::SetClientConnectedCallback( Handle callbackHandle ) +{ + spdlog::info( "Registered client connected callback" ); + m_clientConnectedCallback = callbackHandle; +} + +void ValveSocketServer::SetClientDisconnectedCallback( Handle callbackHandle ) +{ + spdlog::info( "Registered client disconnected callback" ); + m_clientDisconnectedCallback = callbackHandle; +} + +void ValveSocketServer::SetDataReceivedCallback( Handle callbackHandle ) +{ + spdlog::info( "Registered data received callback" ); + m_dataReceivedCallback = callbackHandle; +} + +void ValveSocketServer::SendData( Handle clientHandle, UtilArray interopMessage ) +{ + std::shared_ptr destination = m_connections.Get( clientHandle ); + + std::vector message = interopMessage.GetData(); + + m_interface->SendMessageToConnection( + *destination.get(), message.data(), message.size(), k_nSteamNetworkingSend_Reliable, nullptr ); +} + +void ValveSocketServer::PumpEvents() +{ + ISteamNetworkingMessage* incomingMsg{ nullptr }; + int messageCount = m_interface->ReceiveMessagesOnPollGroup( m_pollGroup, &incomingMsg, 1 ); + + if ( messageCount == 0 ) + return; + + if ( messageCount < 0 ) + { + std::stringstream ss; + ss << "Expected message count 0 or 1, got "; + ss << messageCount; + ss << " instead."; + ErrorMessage( ss.str() ); + abort(); + } + + char* ptrData = ( char* )incomingMsg->m_pData; + + // Convert to string + const char* data = ( const char* )malloc( incomingMsg->m_cbSize ); + memcpy_s( ( void* )data, incomingMsg->m_cbSize, ptrData, incomingMsg->m_cbSize ); + + ValveSocketReceivedMessage receivedMessage{}; + receivedMessage.connectionHandle = ( void* )m_connections.Find( incomingMsg->m_conn ); + receivedMessage.size = incomingMsg->m_cbSize; + receivedMessage.data = ( void* )data; + + m_dataReceivedCallback.Invoke( ( void* )&receivedMessage ); + + incomingMsg->Release(); +} + +void ValveSocketServer::RunCallbacks() +{ + m_interface->RunCallbacks(); +} + +ValveSocketServer::~ValveSocketServer() +{ + m_interface->CloseListenSocket( m_socket ); +} + +const char* ValveSocketServer::GetRemoteAddress( Handle clientHandle ) +{ + SteamNetConnectionInfo_t connectionInfo; + m_interface->GetConnectionInfo( *m_connections.Get( clientHandle ).get(), &connectionInfo ); + + char* addrBuf; + addrBuf = ( char* )malloc( 48 ); + connectionInfo.m_addrRemote.ToString( addrBuf, 48, true ); + + return addrBuf; +} + +void ValveSocketServer::Disconnect( Handle clientHandle ) +{ + HSteamNetConnection connection = *m_connections.Get( clientHandle ).get(); + m_interface->CloseConnection( connection, k_ESteamNetConnectionEnd_App_Generic, "Disconnecting", true ); +} \ No newline at end of file diff --git a/Source/Mocha.Host/Networking/valvesocketserver.h b/Source/Mocha.Host/Networking/valvesocketserver.h new file mode 100644 index 00000000..6d188342 --- /dev/null +++ b/Source/Mocha.Host/Networking/valvesocketserver.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class ValveSocketServer +{ +private: + HSteamListenSocket m_socket{}; + HSteamNetPollGroup m_pollGroup{}; + ISteamNetworkingSockets* m_interface{ nullptr }; + HandleMap m_connections{}; + + // TODO: remove + ManagedCallback m_connectedCallback{}; + + ManagedCallback m_clientConnectedCallback{}; + ManagedCallback m_clientDisconnectedCallback{}; + ManagedCallback m_dataReceivedCallback{}; + +public: + void OnConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t* info ); + + GENERATE_BINDINGS ValveSocketServer( int port ); + + GENERATE_BINDINGS void SetClientConnectedCallback( Handle callbackHandle ); + GENERATE_BINDINGS void SetClientDisconnectedCallback( Handle callbackHandle ); + GENERATE_BINDINGS void SetDataReceivedCallback( Handle callbackHandle ); + + GENERATE_BINDINGS void SendData( Handle clientHandle, UtilArray interopMessage ); + GENERATE_BINDINGS void PumpEvents(); + GENERATE_BINDINGS void RunCallbacks(); + + GENERATE_BINDINGS void Disconnect( Handle clientHandle ); + GENERATE_BINDINGS const char* GetRemoteAddress( Handle clientHandle ); + + ~ValveSocketServer(); +}; diff --git a/Source/Mocha.Host/Root/root.cpp b/Source/Mocha.Host/Root/root.cpp index 365eec1f..509b7c82 100644 --- a/Source/Mocha.Host/Root/root.cpp +++ b/Source/Mocha.Host/Root/root.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,9 @@ void Root::Startup() Globals::m_inputManager = new InputManager(); Globals::m_inputManager->Startup(); + Globals::m_networkingManager = new NetworkingManager(); + Globals::m_networkingManager->Startup(); + Globals::m_renderManager = new RenderManager(); Globals::m_renderManager->Startup(); @@ -53,6 +57,7 @@ void Root::Shutdown() Globals::m_hostManager->Shutdown(); Globals::m_editorManager->Shutdown(); Globals::m_renderManager->Shutdown(); + Globals::m_networkingManager->Shutdown(); Globals::m_inputManager->Shutdown(); Globals::m_renderdocManager->Shutdown(); Globals::m_physicsManager->Shutdown(); diff --git a/Source/Mocha.Networking/AssemblyInfo.cs b/Source/Mocha.Networking/AssemblyInfo.cs new file mode 100644 index 00000000..62e7a7da --- /dev/null +++ b/Source/Mocha.Networking/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible( false )] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid( "43eea9b1-5264-4475-8763-b7167defa889" )] +[assembly: InternalsVisibleTo( "Mocha.Tests" )] diff --git a/Source/Mocha.Networking/Client/Client.ServerConnection.cs b/Source/Mocha.Networking/Client/Client.ServerConnection.cs new file mode 100644 index 00000000..4005c910 --- /dev/null +++ b/Source/Mocha.Networking/Client/Client.ServerConnection.cs @@ -0,0 +1,29 @@ +using Mocha.Common; + +namespace Mocha.Networking; + +public partial class Client +{ + public readonly struct ServerConnection : IConnection + { + public void Disconnect( string reason ) + { + throw new NotImplementedException(); + } + + public IClient GetClient() + { + throw new NotImplementedException(); + } + + public void Send( T message ) where T : IBaseNetworkMessage, new() + { + throw new NotImplementedException(); + } + + public void SendData( byte[] data ) + { + throw new NotImplementedException(); + } + } +} diff --git a/Source/Mocha.Networking/Client/Client.cs b/Source/Mocha.Networking/Client/Client.cs new file mode 100644 index 00000000..f79c2772 --- /dev/null +++ b/Source/Mocha.Networking/Client/Client.cs @@ -0,0 +1,66 @@ +using Mocha.Common; +using System.Runtime.InteropServices; + +namespace Mocha.Networking; + +public partial class Client : ConnectionManager +{ + private Glue.ValveSocketClient _nativeClient; + + public Client( string ipAddress, ushort port = 10570 ) + { + _nativeClient = new Glue.ValveSocketClient( ipAddress, port ); + RegisterNativeCallbacks(); + } + + private void RegisterNativeCallbacks() + { + _nativeClient.SetDataReceivedCallback( + CallbackDispatcher.RegisterCallback( ( IntPtr receivedMessagePtr ) => + { + var receivedMessage = Marshal.PtrToStructure( receivedMessagePtr ); + var data = new byte[receivedMessage.size]; + Marshal.Copy( receivedMessage.data, data, 0, receivedMessage.size ); + + OnMessageReceived( data ); + } + ) ); + } + + public virtual void OnMessageReceived( byte[] data ) + { + } + + public void Update() + { + _nativeClient.PumpEvents(); + _nativeClient.RunCallbacks(); + + var clientInput = new ClientInputMessage() + { + ViewAnglesP = Input.Rotation.ToEulerAngles().X, + ViewAnglesY = Input.Rotation.ToEulerAngles().Y, + ViewAnglesR = Input.Rotation.ToEulerAngles().Z, + + DirectionX = Input.Direction.X, + DirectionY = Input.Direction.Y, + DirectionZ = Input.Direction.Z, + + Left = Input.Left, + Right = Input.Right, + Middle = Input.Middle + }; + + Send( clientInput ); + } + + public void Send( T message ) where T : IBaseNetworkMessage, new() + { + var wrapper = new NetworkMessageWrapper(); + wrapper.Data = NetworkSerializer.Serialize( message ); + wrapper.Type = message.MessageID; + + var bytes = NetworkSerializer.Serialize( wrapper ); + _nativeClient.SendData( bytes.ToInterop() ); + } +} diff --git a/Source/Mocha.Networking/Global.cs b/Source/Mocha.Networking/Global.cs new file mode 100644 index 00000000..b78d92a3 --- /dev/null +++ b/Source/Mocha.Networking/Global.cs @@ -0,0 +1 @@ +global using static Mocha.Common.Global; diff --git a/Source/Mocha.Networking/MessageIDs.cs b/Source/Mocha.Networking/MessageIDs.cs new file mode 100644 index 00000000..09a5f051 --- /dev/null +++ b/Source/Mocha.Networking/MessageIDs.cs @@ -0,0 +1,9 @@ +namespace Mocha.Networking; + +public enum MessageID +{ + Handshake, + ClientInput, + SnapshotUpdate, + Kicked, +} diff --git a/Source/Mocha.Networking/Messages/BaseNetworkMessage.cs b/Source/Mocha.Networking/Messages/BaseNetworkMessage.cs new file mode 100644 index 00000000..8ae58634 --- /dev/null +++ b/Source/Mocha.Networking/Messages/BaseNetworkMessage.cs @@ -0,0 +1,6 @@ +namespace Mocha.Networking; + +public interface IBaseNetworkMessage +{ + internal MessageID MessageID { get; } +} diff --git a/Source/Mocha.Networking/Messages/ClientInputMessage.cs b/Source/Mocha.Networking/Messages/ClientInputMessage.cs new file mode 100644 index 00000000..e137c2ae --- /dev/null +++ b/Source/Mocha.Networking/Messages/ClientInputMessage.cs @@ -0,0 +1,24 @@ +using MessagePack; + +namespace Mocha.Networking; + +[MessagePackObject( true )] +public class ClientInputMessage : IBaseNetworkMessage +{ + [IgnoreMember] + public MessageID MessageID => MessageID.ClientInput; + + public float Timestamp { get; set; } + + public bool Left { get; set; } + public bool Right { get; set; } + public bool Middle { get; set; } + + public float ViewAnglesP { get; set; } + public float ViewAnglesY { get; set; } + public float ViewAnglesR { get; set; } + + public float DirectionX { get; set; } + public float DirectionY { get; set; } + public float DirectionZ { get; set; } +} diff --git a/Source/Mocha.Networking/Messages/HandshakeMessage.cs b/Source/Mocha.Networking/Messages/HandshakeMessage.cs new file mode 100644 index 00000000..7b2a2be4 --- /dev/null +++ b/Source/Mocha.Networking/Messages/HandshakeMessage.cs @@ -0,0 +1,20 @@ +using MessagePack; +using Mocha.Common; + +namespace Mocha.Networking; + +[MessagePackObject( true )] +public class HandshakeMessage : IBaseNetworkMessage +{ + [IgnoreMember] + public MessageID MessageID => MessageID.Handshake; + + public float Timestamp { get; set; } + public int TickRate { get; set; } + public string? Nickname { get; set; } + + public HandshakeMessage() + { + TickRate = Core.TickRate; + } +} diff --git a/Source/Mocha.Networking/Messages/KickedMessage.cs b/Source/Mocha.Networking/Messages/KickedMessage.cs new file mode 100644 index 00000000..4f9ce5a0 --- /dev/null +++ b/Source/Mocha.Networking/Messages/KickedMessage.cs @@ -0,0 +1,12 @@ +using MessagePack; + +namespace Mocha.Networking; + +[MessagePackObject( true )] +public class KickedMessage : IBaseNetworkMessage +{ + [IgnoreMember] + public MessageID MessageID => MessageID.Kicked; + + public string Reason { get; set; } = "Kicked"; +} diff --git a/Source/Mocha.Networking/Messages/SnapshotUpdateMessage.cs b/Source/Mocha.Networking/Messages/SnapshotUpdateMessage.cs new file mode 100644 index 00000000..22547bf9 --- /dev/null +++ b/Source/Mocha.Networking/Messages/SnapshotUpdateMessage.cs @@ -0,0 +1,49 @@ +using MessagePack; +using Mocha.Common; + +namespace Mocha.Networking; + +[MessagePackObject( true )] +/// +/// A snapshot update contains the delta between two snapshots. +/// +public class SnapshotUpdateMessage : IBaseNetworkMessage +{ + [IgnoreMember] + public MessageID MessageID => MessageID.SnapshotUpdate; + + /// + /// The timestamp of the previous snapshot. + /// + public float PreviousTimestamp { get; set; } + + /// + /// The timestamp of the current snapshot. + /// + public float CurrentTimestamp { get; set; } + + /// + /// The sequence number for this snapshot. + /// + public int SequenceNumber { get; set; } + + [MessagePackObject( true )] + public struct EntityChange + { + public NetworkId NetworkId; + public List MemberChanges; + public string TypeName; + } + + [MessagePackObject( true )] + public struct EntityMemberChange + { + public string FieldName; + public byte[] Data; + } + + /// + /// A list of changes to entities since the last snapshot. + /// + public List EntityChanges { get; set; } = new(); +} diff --git a/Source/Mocha.Networking/Mocha.Networking.csproj b/Source/Mocha.Networking/Mocha.Networking.csproj new file mode 100644 index 00000000..ffeb20fb --- /dev/null +++ b/Source/Mocha.Networking/Mocha.Networking.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + AnyCPU;x64 + + + + + + + + + + + + diff --git a/Source/Mocha.Networking/NetworkMessage.cs b/Source/Mocha.Networking/NetworkMessage.cs new file mode 100644 index 00000000..693d8dbf --- /dev/null +++ b/Source/Mocha.Networking/NetworkMessage.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace Mocha.Networking; + +[MessagePackObject] +public class NetworkMessageWrapper +{ + [Key( 0 )] public MessageID? Type { get; set; } + [Key( 1 )] public byte[]? Data { get; set; } +} diff --git a/Source/Mocha.Networking/Serialization/Formatters/NetworkIdFormatter.cs b/Source/Mocha.Networking/Serialization/Formatters/NetworkIdFormatter.cs new file mode 100644 index 00000000..28a2334c --- /dev/null +++ b/Source/Mocha.Networking/Serialization/Formatters/NetworkIdFormatter.cs @@ -0,0 +1,18 @@ +using MessagePack; +using MessagePack.Formatters; +using Mocha.Common; + +namespace Mocha.Networking; + +internal class NetworkIdFormatter : IMessagePackFormatter +{ + public void Serialize( ref MessagePackWriter writer, NetworkId value, MessagePackSerializerOptions options ) + { + writer.Write( value.Value ); + } + + public NetworkId Deserialize( ref MessagePackReader reader, MessagePackSerializerOptions options ) + { + return new NetworkId( reader.ReadUInt64() ); + } +} diff --git a/Source/Mocha.Networking/Serialization/Formatters/RotationFormatter.cs b/Source/Mocha.Networking/Serialization/Formatters/RotationFormatter.cs new file mode 100644 index 00000000..ce1b4579 --- /dev/null +++ b/Source/Mocha.Networking/Serialization/Formatters/RotationFormatter.cs @@ -0,0 +1,33 @@ +using MessagePack; +using MessagePack.Formatters; +using Mocha.Common; + +namespace Mocha.Networking; + +internal class RotationFormatter : IMessagePackFormatter +{ + public void Serialize( ref MessagePackWriter writer, Rotation value, MessagePackSerializerOptions options ) + { + writer.WriteArrayHeader( 4 ); + writer.Write( value.X ); + writer.Write( value.Y ); + writer.Write( value.Z ); + writer.Write( value.W ); + } + + public Rotation Deserialize( ref MessagePackReader reader, MessagePackSerializerOptions options ) + { + var length = reader.ReadArrayHeader(); + if ( length != 4 ) + { + throw new MessagePackSerializationException( $"Expected array of length 3, got {length}." ); + } + + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var z = reader.ReadSingle(); + var w = reader.ReadSingle(); + + return new Rotation( x, y, z, w ); + } +} diff --git a/Source/Mocha.Networking/Serialization/Formatters/Vector3Formatter.cs b/Source/Mocha.Networking/Serialization/Formatters/Vector3Formatter.cs new file mode 100644 index 00000000..6f6783dc --- /dev/null +++ b/Source/Mocha.Networking/Serialization/Formatters/Vector3Formatter.cs @@ -0,0 +1,31 @@ +using MessagePack; +using MessagePack.Formatters; +using Mocha.Common; + +namespace Mocha.Networking; + +internal class Vector3Formatter : IMessagePackFormatter +{ + public void Serialize( ref MessagePackWriter writer, Vector3 value, MessagePackSerializerOptions options ) + { + writer.WriteArrayHeader( 3 ); + writer.Write( value.X ); + writer.Write( value.Y ); + writer.Write( value.Z ); + } + + public Vector3 Deserialize( ref MessagePackReader reader, MessagePackSerializerOptions options ) + { + var length = reader.ReadArrayHeader(); + if ( length != 3 ) + { + throw new MessagePackSerializationException( $"Expected array of length 3, got {length}." ); + } + + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var z = reader.ReadSingle(); + + return new Vector3( x, y, z ); + } +} diff --git a/Source/Mocha.Networking/Serialization/NetworkSerializer.cs b/Source/Mocha.Networking/Serialization/NetworkSerializer.cs new file mode 100644 index 00000000..429d26e1 --- /dev/null +++ b/Source/Mocha.Networking/Serialization/NetworkSerializer.cs @@ -0,0 +1,42 @@ +using MessagePack; +using MessagePack.Resolvers; +using Mocha.Common; + +namespace Mocha.Networking; + +public static class NetworkSerializer +{ + private const bool UseCompression = true; + + private static IFormatterResolver s_resolver = CompositeResolver.Create( + MochaResolver.Instance, + + // Standard resolver is last + StandardResolver.Instance + ); + + private static MessagePackSerializerOptions s_options = new MessagePackSerializerOptions( s_resolver ); + + public static byte[] Serialize( object obj ) + { + var bytes = MessagePackSerializer.Serialize( obj, s_options ); + + return UseCompression ? Serializer.Compress( bytes ) : bytes; + } + + public static T Deserialize( byte[] data ) + { + data = UseCompression ? Serializer.Decompress( data ) : data; + + var obj = MessagePackSerializer.Deserialize( data, s_options ); + return obj; + } + + public static object? Deserialize( byte[] data, Type type ) + { + data = UseCompression ? Serializer.Decompress( data ) : data; + + var obj = MessagePackSerializer.Deserialize( type, data, s_options ); + return obj; + } +} diff --git a/Source/Mocha.Networking/Serialization/Resolvers/MochaResolver.cs b/Source/Mocha.Networking/Serialization/Resolvers/MochaResolver.cs new file mode 100644 index 00000000..1448c8b9 --- /dev/null +++ b/Source/Mocha.Networking/Serialization/Resolvers/MochaResolver.cs @@ -0,0 +1,55 @@ +using MessagePack; +using MessagePack.Formatters; +using Mocha.Common; + +namespace Mocha.Networking; + +internal class MochaResolver : IFormatterResolver +{ + // Resolver should be singleton. + public static readonly IFormatterResolver Instance = new MochaResolver(); + + private MochaResolver() + { + } + + // GetFormatter's get cost should be minimized so use type cache. + public IMessagePackFormatter GetFormatter() + { + return FormatterCache.Formatter; + } + + private static class FormatterCache + { + public static readonly IMessagePackFormatter Formatter; + + // generic's static constructor should be minimized for reduce type generation size! + // use outer helper method. + static FormatterCache() + { + Formatter = (IMessagePackFormatter)SampleCustomResolverGetFormatterHelper.GetFormatter( typeof( T ) ); + } + } +} + +internal static class SampleCustomResolverGetFormatterHelper +{ + static readonly Dictionary formatterMap = new Dictionary() + { + { typeof( Vector3 ), new Vector3Formatter() }, + { typeof( Rotation ), new RotationFormatter() }, + { typeof( NetworkId ), new NetworkIdFormatter() }, + }; + + internal static object GetFormatter( Type t ) + { + object formatter; + if ( formatterMap.TryGetValue( t, out formatter ) ) + { + return formatter; + } + + // If type can not get, must return null for fallback mechanism. + return null; + } +} diff --git a/Source/Mocha.Networking/Server/Server.ClientConnection.cs b/Source/Mocha.Networking/Server/Server.ClientConnection.cs new file mode 100644 index 00000000..2772ab8d --- /dev/null +++ b/Source/Mocha.Networking/Server/Server.ClientConnection.cs @@ -0,0 +1,71 @@ +using Mocha.Common; + +namespace Mocha.Networking; + +public partial class Server +{ + public readonly struct ClientConnection : IConnection + { + public uint NativeHandle { get; init; } + private string RemoteAddress { get; init; } + public string Nickname { get; init; } + + private ClientConnection( uint nativeHandle ) + { + NativeHandle = nativeHandle; + RemoteAddress = GetAddress(); + + // Generate a random nickname + Nickname = $"Player{new Random().Next( 0, 9999 )}"; + } + + public static ClientConnection CreateFromPointer( IntPtr pointer ) + { + var clientHandle = (uint)pointer; + return new( clientHandle ); + } + + private string GetAddress() + { + return Instance._nativeServer.GetRemoteAddress( NativeHandle ); + } + + public void SendData( byte[] data ) + { + Instance._nativeServer.SendData( NativeHandle, data.ToInterop() ); + } + + public void Send( T message ) where T : IBaseNetworkMessage, new() + { + var wrapper = new NetworkMessageWrapper(); + wrapper.Data = NetworkSerializer.Serialize( message ); + wrapper.Type = message.MessageID; + + var bytes = NetworkSerializer.Serialize( wrapper ); + SendData( bytes ); + } + + public void Disconnect( string? reason = null! ) + { + var kickedMessage = new KickedMessage(); + if ( reason != null ) + kickedMessage.Reason = reason; + + Send( kickedMessage ); + Instance._nativeServer.Disconnect( NativeHandle ); + } + + public override string ToString() + { + return Nickname; + } + + public IClient GetClient() + { + return new ConnectedClient() + { + Name = Nickname + }; + } + } +} diff --git a/Source/Mocha.Networking/Server/Server.cs b/Source/Mocha.Networking/Server/Server.cs new file mode 100644 index 00000000..1aed1b05 --- /dev/null +++ b/Source/Mocha.Networking/Server/Server.cs @@ -0,0 +1,82 @@ +using Mocha.Common; +using System.Runtime.InteropServices; + +namespace Mocha.Networking; + +public partial class Server : ConnectionManager +{ + private static Server Instance { get; set; } + + private Glue.ValveSocketServer _nativeServer; + + // I don't like the idea of managing two separate lists (one native, + // one managed) for this, but I think it might be unavoidable. :( + protected List _connectedClients = new(); + + public Server( ushort port = 10570 ) + { + Instance = this; + + _nativeServer = new Glue.ValveSocketServer( port ); + RegisterNativeCallbacks(); + } + + private void RegisterNativeCallbacks() + { + _nativeServer.SetClientConnectedCallback( + CallbackDispatcher.RegisterCallback( ( IntPtr clientHandlePtr ) => + { + var client = ClientConnection.CreateFromPointer( clientHandlePtr ); + + _connectedClients.Add( client ); + OnClientConnected( client ); + } + ) ); + + _nativeServer.SetClientDisconnectedCallback( + CallbackDispatcher.RegisterCallback( ( IntPtr clientHandlePtr ) => + { + var client = ClientConnection.CreateFromPointer( clientHandlePtr ); + + _connectedClients.Remove( client ); + OnClientDisconnected( client ); + } + ) ); + + _nativeServer.SetDataReceivedCallback( + CallbackDispatcher.RegisterCallback( ( IntPtr receivedMessagePtr ) => + { + var receivedMessage = Marshal.PtrToStructure( receivedMessagePtr ); + var client = ClientConnection.CreateFromPointer( receivedMessage.connectionHandle ); + var data = new byte[receivedMessage.size]; + Marshal.Copy( receivedMessage.data, data, 0, receivedMessage.size ); + + OnMessageReceived( client, data ); + } + ) ); + } + + public void Update() + { + _nativeServer.PumpEvents(); + _nativeServer.RunCallbacks(); + + OnUpdate(); + } + + public virtual void OnClientConnected( IConnection client ) + { + } + + public virtual void OnClientDisconnected( IConnection client ) + { + } + + public virtual void OnMessageReceived( IConnection client, byte[] data ) + { + } + + public virtual void OnUpdate() + { + } +} diff --git a/Source/Mocha.Networking/Shared/Client.cs b/Source/Mocha.Networking/Shared/Client.cs new file mode 100644 index 00000000..8da91c8d --- /dev/null +++ b/Source/Mocha.Networking/Shared/Client.cs @@ -0,0 +1,10 @@ +using Mocha.Common; + +namespace Mocha.Networking; + +internal class ConnectedClient : IClient +{ + public string Name { get; set; } + public int Ping { get; set; } + public IEntity Pawn { get; set; } +} diff --git a/Source/Mocha.Networking/Shared/ConnectionManager.cs b/Source/Mocha.Networking/Shared/ConnectionManager.cs new file mode 100644 index 00000000..5389b9b8 --- /dev/null +++ b/Source/Mocha.Networking/Shared/ConnectionManager.cs @@ -0,0 +1,33 @@ +namespace Mocha.Networking; + +public class ConnectionManager +{ + protected readonly record struct MessageHandler(Type type, Action Action); + private Dictionary _messageHandlers = new(); + + protected void RegisterHandler( Action handler ) where T : IBaseNetworkMessage + { + var instance = Activator.CreateInstance() as IBaseNetworkMessage; + var messageId = instance.MessageID; + + var messageHandler = new MessageHandler( typeof( T ), ( connection, data ) => handler?.Invoke( connection, (T)data ) ); + _messageHandlers.Add( messageId, messageHandler ); + } + + protected void InvokeHandler( IConnection connection, byte[] data ) + { + var message = NetworkSerializer.Deserialize( data )!; + + foreach ( var (type, handler) in _messageHandlers ) + { + if ( type == message.Type ) + { + var messageData = NetworkSerializer.Deserialize( message.Data, handler.type )!; + handler.Action?.Invoke( connection, messageData ); + return; + } + } + + Log.Error( $"ConnectionManager: Unknown message type '{message.Type}'" ); + } +} diff --git a/Source/Mocha.Networking/Shared/IConnection.cs b/Source/Mocha.Networking/Shared/IConnection.cs new file mode 100644 index 00000000..826afa84 --- /dev/null +++ b/Source/Mocha.Networking/Shared/IConnection.cs @@ -0,0 +1,16 @@ +using Mocha.Common; + +namespace Mocha.Networking; + +/// +/// Represents a connection between a client and a server +/// +public interface IConnection +{ + void SendData( byte[] data ); + void Send( T message ) where T : IBaseNetworkMessage, new(); + + void Disconnect( string reason ); + + IClient GetClient(); +} diff --git a/Source/Mocha.Networking/Shared/ValveSocketReceivedMessage.cs b/Source/Mocha.Networking/Shared/ValveSocketReceivedMessage.cs new file mode 100644 index 00000000..9aee2571 --- /dev/null +++ b/Source/Mocha.Networking/Shared/ValveSocketReceivedMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Mocha.Networking; + +[StructLayout( LayoutKind.Sequential )] +struct ValveSocketReceivedMessage +{ + public IntPtr connectionHandle; + public int size; + public IntPtr data; +}; diff --git a/Source/Mocha.Tests/Mocha.Tests.csproj b/Source/Mocha.Tests/Mocha.Tests.csproj index c9c47733..ef2d3b29 100644 --- a/Source/Mocha.Tests/Mocha.Tests.csproj +++ b/Source/Mocha.Tests/Mocha.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/Source/Mocha.Tests/NetworkSerializerTests.cs b/Source/Mocha.Tests/NetworkSerializerTests.cs new file mode 100644 index 00000000..3aebda93 --- /dev/null +++ b/Source/Mocha.Tests/NetworkSerializerTests.cs @@ -0,0 +1,117 @@ +using Mocha.Common; +using Mocha.Networking; +using MochaTool.AssetCompiler; + +namespace Mocha.Tests; + +[TestClass] +public class NetworkSerializerTests +{ + [TestMethod( "Array Serialization" )] + public void TestArraySerialization() + { + Global.Log = new ConsoleLogger(); + + var array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var data = NetworkSerializer.Serialize( array ); + var result = NetworkSerializer.Deserialize( data ); + Assert.IsTrue( array.SequenceEqual( result ) ); + } + + [TestMethod( "List Serialization" )] + public void TestListSerialization() + { + Global.Log = new ConsoleLogger(); + + var list = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var data = NetworkSerializer.Serialize( list ); + var result = NetworkSerializer.Deserialize>( data ); + Assert.IsTrue( list.SequenceEqual( result ) ); + } + + [TestMethod( "String Serialization" )] + public void TestStringSerialization() + { + Global.Log = new ConsoleLogger(); + + var str = "Hello World!"; + var data = NetworkSerializer.Serialize( str ); + var result = NetworkSerializer.Deserialize( data ); + Assert.AreEqual( str, result ); + } + + [TestMethod( "Vector3 Serialization" )] + public void TestVector3Serialization() + { + Global.Log = new ConsoleLogger(); + + var vec = new Vector3( 1, 2, 3 ); + var data = NetworkSerializer.Serialize( vec ); + var result = NetworkSerializer.Deserialize( data ); + Assert.AreEqual( vec, result ); + } + + [TestMethod( "Rotation Serialization" )] + public void TestRotationSerialization() + { + Global.Log = new ConsoleLogger(); + + var rot = new Rotation( 1, 2, 3, 4 ); + var data = NetworkSerializer.Serialize( rot ); + var result = NetworkSerializer.Deserialize( data ); + Assert.AreEqual( rot, result ); + } + + [TestMethod( "NetworkId Serialization" )] + public void TestNetworkIdSerialization() + { + Global.Log = new ConsoleLogger(); + + var id = NetworkId.CreateNetworked(); + var data = NetworkSerializer.Serialize( id ); + var result = NetworkSerializer.Deserialize( data ); + Assert.AreEqual( id, result ); + } + + [TestMethod( "SnapshotUpdateMessage Serialization" )] + public void TestSnapshotUpdateMessageSerialization() + { + Global.Log = new ConsoleLogger(); + + var message = new SnapshotUpdateMessage + { + CurrentTimestamp = 0, + PreviousTimestamp = 0, + EntityChanges = new List() + { + new SnapshotUpdateMessage.EntityChange() + { + MemberChanges = new List() + { + new SnapshotUpdateMessage.EntityMemberChange() + { + FieldName = "Test", + Data = NetworkSerializer.Serialize( "Hello World!" ) + } + }, + NetworkId = NetworkId.CreateNetworked(), + TypeName = "Test" + } + }, + SequenceNumber = 0 + }; + + var data = NetworkSerializer.Serialize( message ); + var result = NetworkSerializer.Deserialize( data ); + + Assert.AreEqual( message.CurrentTimestamp, result.CurrentTimestamp ); + Assert.AreEqual( message.PreviousTimestamp, result.PreviousTimestamp ); + Assert.AreEqual( message.EntityChanges.Count, result.EntityChanges.Count ); + Assert.AreEqual( message.EntityChanges[0].MemberChanges.Count, result.EntityChanges[0].MemberChanges.Count ); + Assert.AreEqual( message.EntityChanges[0].MemberChanges[0].FieldName, result.EntityChanges[0].MemberChanges[0].FieldName ); + Assert.IsTrue( message.EntityChanges[0].MemberChanges[0].Data.SequenceEqual( result.EntityChanges[0].MemberChanges[0].Data ) ); + Assert.AreEqual( message.EntityChanges[0].NetworkId, result.EntityChanges[0].NetworkId ); + Assert.AreEqual( message.EntityChanges[0].TypeName, result.EntityChanges[0].TypeName ); + Assert.AreEqual( message.SequenceNumber, result.SequenceNumber ); + } +} diff --git a/Source/Mocha.sln b/Source/Mocha.sln index d2e7d714..8a9faf8b 100644 --- a/Source/Mocha.sln +++ b/Source/Mocha.sln @@ -50,6 +50,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocha.Editor", "Mocha.Edito EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Editor", "Editor", "{FE285D47-E211-4111-9A3B-C71F65380D60}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mocha.Networking", "Mocha.Networking\Mocha.Networking.csproj", "{CC70EE16-B5B2-423B-843B-BE8C09188DEA}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Mocha", "Mocha\Mocha.vcxproj", "{2BF31211-78F2-42DE-AA9A-E9718C2A9055}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MochaDedicatedServer", "MochaDedicatedServer\MochaDedicatedServer.vcxproj", "{860C57C4-6E4B-445F-9614-9084AF4CD46B}" @@ -166,6 +168,18 @@ Global {E37A990E-4041-4F9C-8202-CACA45803376}.Release|x64.Build.0 = Release|Any CPU {E37A990E-4041-4F9C-8202-CACA45803376}.Release|x86.ActiveCfg = Release|Any CPU {E37A990E-4041-4F9C-8202-CACA45803376}.Release|x86.Build.0 = Release|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Debug|x64.ActiveCfg = Debug|x64 + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Debug|x64.Build.0 = Debug|x64 + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Debug|x86.Build.0 = Debug|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Release|Any CPU.Build.0 = Release|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Release|x64.ActiveCfg = Release|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Release|x64.Build.0 = Release|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Release|x86.ActiveCfg = Release|Any CPU + {CC70EE16-B5B2-423B-843B-BE8C09188DEA}.Release|x86.Build.0 = Release|Any CPU {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|Any CPU.ActiveCfg = Debug|x64 {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|Any CPU.Build.0 = Debug|x64 {2BF31211-78F2-42DE-AA9A-E9718C2A9055}.Debug|x64.ActiveCfg = Debug|x64 @@ -216,6 +230,7 @@ Global {0FD2339A-B0C8-4AFC-B2AB-AF3C0DDCD6F2} = {2F5C4610-1254-4D33-A1A6-5B38197346EE} {E37A990E-4041-4F9C-8202-CACA45803376} = {FE285D47-E211-4111-9A3B-C71F65380D60} {FE285D47-E211-4111-9A3B-C71F65380D60} = {2F5C4610-1254-4D33-A1A6-5B38197346EE} + {CC70EE16-B5B2-423B-843B-BE8C09188DEA} = {2F5C4610-1254-4D33-A1A6-5B38197346EE} {2BF31211-78F2-42DE-AA9A-E9718C2A9055} = {40918016-AB8B-47EC-9B4C-EDF1532D3FAF} {860C57C4-6E4B-445F-9614-9084AF4CD46B} = {40918016-AB8B-47EC-9B4C-EDF1532D3FAF} {40918016-AB8B-47EC-9B4C-EDF1532D3FAF} = {E5E9BDE7-3F7F-4044-ACFD-FE2F0F66AB53}