diff --git a/Ragon.Client.Property/Sources/RagonBool.cs b/Ragon.Client.Property/Sources/RagonBool.cs index 2688b07..ba87207 100644 --- a/Ragon.Client.Property/Sources/RagonBool.cs +++ b/Ragon.Client.Property/Sources/RagonBool.cs @@ -17,7 +17,7 @@ using Ragon.Protocol; -namespace Ragon.Client.Simulation +namespace Ragon.Client.Property { [Serializable] public class RagonBool : RagonProperty diff --git a/Ragon.Client.Property/Sources/RagonInt.cs b/Ragon.Client.Property/Sources/RagonInt.cs index 063a5b2..b8498d7 100644 --- a/Ragon.Client.Property/Sources/RagonInt.cs +++ b/Ragon.Client.Property/Sources/RagonInt.cs @@ -17,7 +17,7 @@ using Ragon.Protocol; -namespace Ragon.Client.Simulation +namespace Ragon.Client.Property { [Serializable] public class RagonInt : RagonProperty diff --git a/Ragon.Client.Property/Sources/RagonQuaternion.cs b/Ragon.Client.Property/Sources/RagonQuaternion.cs new file mode 100644 index 0000000..0b208f7 --- /dev/null +++ b/Ragon.Client.Property/Sources/RagonQuaternion.cs @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Numerics; +using Ragon.Client.Compressor; +using Ragon.Protocol; + +namespace Ragon.Client.Property; + +public class RagonQuaternion : RagonProperty +{ + private Quaternion _value; + + public Quaternion Value + { + get => _value; + set + { + _value = value; + + MarkAsChanged(); + } + } + + private readonly FloatCompressor _compressor; + + public RagonQuaternion(bool invokeLocal = false, int priority = 0) : base(priority, invokeLocal) + { + _compressor = new FloatCompressor(-1.0f, 1f, 0.01f); + + SetFixedSize(_compressor.RequiredBits * 4); + } + + public override void Serialize(RagonBuffer buffer) + { + var compressedX = _compressor.Compress(_value.X); + var compressedY = _compressor.Compress(_value.Y); + var compressedZ = _compressor.Compress(_value.Z); + var compressedW = _compressor.Compress(_value.W); + + buffer.Write(compressedX, _compressor.RequiredBits); + buffer.Write(compressedY, _compressor.RequiredBits); + buffer.Write(compressedZ, _compressor.RequiredBits); + buffer.Write(compressedW, _compressor.RequiredBits); + } + + public override void Deserialize(RagonBuffer buffer) + { + var compressedX = buffer.Read(_compressor.RequiredBits); + var compressedY = buffer.Read(_compressor.RequiredBits); + var compressedZ = buffer.Read(_compressor.RequiredBits); + var compressedW = buffer.Read(_compressor.RequiredBits); + + var x = _compressor.Decompress(compressedX); + var y = _compressor.Decompress(compressedY); + var z = _compressor.Decompress(compressedZ); + var w = _compressor.Decompress(compressedW); + + _value = new Quaternion(x, y, z, w); + + InvokeChanged(); + } +} \ No newline at end of file diff --git a/Ragon.Client.Property/Sources/RagonString.cs b/Ragon.Client.Property/Sources/RagonString.cs index 0f7ca94..7733d26 100644 --- a/Ragon.Client.Property/Sources/RagonString.cs +++ b/Ragon.Client.Property/Sources/RagonString.cs @@ -18,7 +18,7 @@ using System.Text; using Ragon.Protocol; -namespace Ragon.Client.Simulation +namespace Ragon.Client.Property { [Serializable] public class RagonString : RagonProperty diff --git a/Ragon.Client.Property/Sources/RagonVector3.cs b/Ragon.Client.Property/Sources/RagonVector3.cs new file mode 100644 index 0000000..750f2b4 --- /dev/null +++ b/Ragon.Client.Property/Sources/RagonVector3.cs @@ -0,0 +1,330 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Numerics; +using Ragon.Client.Compressor; +using Ragon.Protocol; + +namespace Ragon.Client.Property +{ + [Serializable] + public enum RagonAxis + { + XYZ, + XY, + YZ, + XZ, + X, + Y, + Z + } + + [Serializable] + public class RagonVector3 : RagonProperty + { + private Vector3 _value; + + public Vector3 Value + { + get => _value; + set + { + _value = value; + + MarkAsChanged(); + } + } + + private RagonAxis _axis; + private FloatCompressor _compressorX; + private FloatCompressor _compressorY; + private FloatCompressor _compressorZ; + + + public RagonVector3( + RagonAxis axis = RagonAxis.XYZ, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _axis = axis; + + var defaultCompressor = new FloatCompressor(-1024.0f, 1024f, 0.01f); + + _compressorX = defaultCompressor; + _compressorY = defaultCompressor; + _compressorZ = defaultCompressor; + + switch (_axis) + { + case RagonAxis.XYZ: + SetFixedSize(_compressorX.RequiredBits + _compressorY.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.XY: + SetFixedSize(_compressorX.RequiredBits + _compressorY.RequiredBits); + break; + case RagonAxis.XZ: + SetFixedSize(_compressorX.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.YZ: + SetFixedSize(_compressorY.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.X: + SetFixedSize(_compressorX.RequiredBits); + break; + case RagonAxis.Y: + SetFixedSize(_compressorY.RequiredBits); + break; + case RagonAxis.Z: + SetFixedSize(_compressorZ.RequiredBits); + break; + } + } + + public RagonVector3( + Vector3 initialValue, + RagonAxis axis = RagonAxis.XYZ, + float min = -1024.0f, + float max = 1024.0f, + float precision = 0.1f, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _value = initialValue; + _axis = axis; + + var defaultCompressor = new FloatCompressor(min, max, precision); + + _compressorX = defaultCompressor; + _compressorY = defaultCompressor; + _compressorZ = defaultCompressor; + + switch (_axis) + { + case RagonAxis.XYZ: + SetFixedSize(_compressorX.RequiredBits + _compressorY.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.XY: + SetFixedSize(_compressorX.RequiredBits + _compressorY.RequiredBits); + break; + case RagonAxis.XZ: + SetFixedSize(_compressorX.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.YZ: + SetFixedSize(_compressorY.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.X: + SetFixedSize(_compressorX.RequiredBits); + break; + case RagonAxis.Y: + SetFixedSize(_compressorY.RequiredBits); + break; + case RagonAxis.Z: + SetFixedSize(_compressorZ.RequiredBits); + break; + } + } + + public RagonVector3( + RagonAxis axis = RagonAxis.XYZ, + FloatCompressor compressorX = null, + FloatCompressor compressorY = null, + FloatCompressor compressorZ = null, + bool invokeLocal = true, + int priority = 0 + ) : base(priority, invokeLocal) + { + _axis = axis; + + var defaultCompressor = new FloatCompressor(-1024.0f, 1024f, 0.01f); + + _compressorX = defaultCompressor; + _compressorY = defaultCompressor; + _compressorZ = defaultCompressor; + + if (compressorX != null) + _compressorX = compressorX; + + if (compressorY != null) + _compressorY = compressorY; + + if (compressorZ != null) + _compressorZ = compressorZ; + + switch (_axis) + { + case RagonAxis.XYZ: + SetFixedSize(_compressorX.RequiredBits + _compressorY.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.XY: + SetFixedSize(_compressorX.RequiredBits + _compressorY.RequiredBits); + break; + case RagonAxis.XZ: + SetFixedSize(_compressorX.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.YZ: + SetFixedSize(_compressorY.RequiredBits + _compressorZ.RequiredBits); + break; + case RagonAxis.X: + SetFixedSize(_compressorX.RequiredBits); + break; + case RagonAxis.Y: + SetFixedSize(_compressorY.RequiredBits); + break; + case RagonAxis.Z: + SetFixedSize(_compressorZ.RequiredBits); + break; + } + } + + public override void Serialize(RagonBuffer buffer) + { + switch (_axis) + { + case RagonAxis.XYZ: + { + var compressedX = _compressorX.Compress(_value.X); + var compressedY = _compressorY.Compress(_value.Y); + var compressedZ = _compressorZ.Compress(_value.Z); + + buffer.Write(compressedX, _compressorX.RequiredBits); + buffer.Write(compressedY, _compressorY.RequiredBits); + buffer.Write(compressedZ, _compressorZ.RequiredBits); + } + break; + case RagonAxis.XY: + { + var compressedX = _compressorX.Compress(_value.X); + var compressedY = _compressorY.Compress(_value.Y); + + buffer.Write(compressedX, _compressorX.RequiredBits); + buffer.Write(compressedY, _compressorY.RequiredBits); + } + break; + case RagonAxis.XZ: + { + var compressedX = _compressorX.Compress(_value.X); + var compressedZ = _compressorZ.Compress(_value.Z); + + buffer.Write(compressedX, _compressorX.RequiredBits); + buffer.Write(compressedZ, _compressorZ.RequiredBits); + break; + } + + case RagonAxis.YZ: + { + var compressedY = _compressorY.Compress(_value.Y); + var compressedZ = _compressorZ.Compress(_value.Z); + + buffer.Write(compressedY, _compressorY.RequiredBits); + buffer.Write(compressedZ, _compressorZ.RequiredBits); + break; + } + case RagonAxis.X: + { + var compressedX = _compressorX.Compress(_value.X); + + buffer.Write(compressedX, _compressorX.RequiredBits); + break; + } + case RagonAxis.Y: + { + var compressedY = _compressorY.Compress(_value.Y); + + buffer.Write(compressedY, _compressorY.RequiredBits); + break; + } + case RagonAxis.Z: + { + var compressedZ = _compressorZ.Compress(_value.Z); + + buffer.Write(compressedZ, _compressorZ.RequiredBits); + break; + } + } + } + + public override void Deserialize(RagonBuffer buffer) + { + switch (_axis) + { + case RagonAxis.XYZ: + { + var compressedX = buffer.Read(_compressorX.RequiredBits); + var compressedY = buffer.Read(_compressorY.RequiredBits); + var compressedZ = buffer.Read(_compressorZ.RequiredBits); + + _value.X = _compressorX.Decompress(compressedX); + _value.Y = _compressorY.Decompress(compressedY); + _value.Z = _compressorZ.Decompress(compressedZ); + break; + } + case RagonAxis.XY: + { + var compressedX = buffer.Read(_compressorX.RequiredBits); + var compressedY = buffer.Read(_compressorY.RequiredBits); + + _value.X = _compressorX.Decompress(compressedX); + _value.Z = _compressorY.Decompress(compressedY); + break; + } + case RagonAxis.XZ: + { + var compressedX = buffer.Read(_compressorX.RequiredBits); + var compressedZ = buffer.Read(_compressorZ.RequiredBits); + + _value.X = _compressorX.Decompress(compressedX); + _value.Z = _compressorZ.Decompress(compressedZ); + break; + } + case RagonAxis.YZ: + { + var compressedY = buffer.Read(_compressorY.RequiredBits); + var compressedZ = buffer.Read(_compressorZ.RequiredBits); + + _value.Y = _compressorY.Decompress(compressedY); + _value.Z = _compressorZ.Decompress(compressedZ); + break; + } + case RagonAxis.X: + { + var compressedX = buffer.Read(_compressorX.RequiredBits); + + _value.X = _compressorX.Decompress(compressedX); + break; + } + case RagonAxis.Y: + { + var compressedY = buffer.Read(_compressorY.RequiredBits); + + _value.Y = _compressorY.Decompress(compressedY); + break; + } + case RagonAxis.Z: + { + var compressedZ = buffer.Read(_compressorZ.RequiredBits); + + _value.Z = _compressorZ.Decompress(compressedZ); + break; + } + } + + InvokeChanged(); + } + } +} \ No newline at end of file diff --git a/Ragon.Client/Ragon.Client.csproj b/Ragon.Client/Ragon.Client.csproj index c94409a..b1ce773 100644 --- a/Ragon.Client/Ragon.Client.csproj +++ b/Ragon.Client/Ragon.Client.csproj @@ -12,7 +12,7 @@ true none - /Users/edmand46/RagonProjects/ragon-oss-examples/Assets/Ragon/Plugins/ + /Users/edmand46/RagonProjects/ragon-oss-examples/Assets/Ragon/Runtime/Plugins diff --git a/Ragon.Client/Sources/Entity/RagonEntity.cs b/Ragon.Client/Sources/Entity/RagonEntity.cs index 6a6ad6a..5268eb8 100644 --- a/Ragon.Client/Sources/Entity/RagonEntity.cs +++ b/Ragon.Client/Sources/Entity/RagonEntity.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client { - public sealed class RagonEntity + public sealed class RagonEntity: IDisposable { private delegate void OnEventDelegate(RagonPlayer player, RagonBuffer serializer); @@ -50,7 +50,8 @@ namespace Ragon.Client private RagonPayload _destroyPayload; private readonly Dictionary _events = new Dictionary(); - private readonly Dictionary> _localEvents = new Dictionary>(); + private readonly Dictionary>> _localListeners = new Dictionary>>(); + private readonly Dictionary>> _listeners = new Dictionary>>(); public RagonEntity(ushort type = 0, ushort sceneId = 0) { @@ -76,6 +77,11 @@ namespace Ragon.Client Attached?.Invoke(this); } + internal void SetReplication(bool enabled) + { + Replication = enabled; + } + internal void Detach(RagonPayload payload) { _destroyPayload = payload; @@ -155,12 +161,16 @@ namespace Ragon.Client { if (replicationMode == RagonReplicationMode.Local) { - _localEvents[eventCode].Invoke(_client.Room.Local, evnt); + var localListeners = _localListeners[eventCode]; + foreach (var listener in localListeners) + listener.Invoke(_client.Room.Local, evnt); return; } if (replicationMode == RagonReplicationMode.LocalAndServer) { - _localEvents[eventCode].Invoke(_client.Room.Local, evnt); + var localListeners = _localListeners[eventCode]; + foreach (var listener in localListeners) + listener.Invoke(_client.Room.Local, evnt); } } @@ -179,25 +189,52 @@ namespace Ragon.Client _client.Reliable.Send(sendData); } - public void OnEvent(Action callback) where TEvent : IRagonEvent, new() + public Action OnEvent(Action callback) where TEvent : IRagonEvent, new() { var t = new TEvent(); var eventCode = _client.Event.GetEventCode(t); - - if (_events.ContainsKey(eventCode)) + + var action = (RagonPlayer player, IRagonEvent eventData) => callback.Invoke(player, (TEvent)eventData); + + if (!_listeners.TryGetValue(eventCode, out var callbacks)) { - _events.Remove(eventCode); - _localEvents.Remove(eventCode); - - RagonLog.Warn($"Event already {eventCode} subscribed, removed old one!"); + callbacks = new List>(); + _listeners.Add(eventCode, callbacks); + } + + if (!_localListeners.TryGetValue(eventCode, out var localCallbacks)) + { + localCallbacks = new List>(); + _localListeners.Add(eventCode, localCallbacks); } - _localEvents.Add(eventCode, (player, eventData) => { callback.Invoke(player, (TEvent)eventData); }); - _events.Add(eventCode, (player, serializer) => + callbacks.Add(action); + localCallbacks.Add(action); + + if (!_events.ContainsKey(eventCode)) { - t.Deserialize(serializer); - callback.Invoke(player, t); - }); + _events.Add(eventCode, (player, serializer) => + { + t.Deserialize(serializer); + + foreach (var callbackListener in callbacks) + callbackListener.Invoke(player, t); + }); + } + + return action; + } + + public void OffEvent(Action callback) where TEvent : IRagonEvent, new() + { + var t = new TEvent(); + var eventCode = _client.Event.GetEventCode(t); + + if (_listeners.TryGetValue(eventCode, out var callbacks)) + callbacks.Remove(callback); + + if (_localListeners.TryGetValue(eventCode, out var localCallbacks)) + localCallbacks.Remove(callback); } internal void Write(RagonBuffer buffer) @@ -237,5 +274,12 @@ namespace Ragon.Client OwnershipChanged?.Invoke(prevOwner, player); } + + public void Dispose() + { + _events.Clear(); + _listeners.Clear(); + _localListeners.Clear(); + } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs b/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs index 36f0ac4..dd569d4 100644 --- a/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs +++ b/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class AuthorizeFailedHandler: Handler +internal class AuthorizeFailedHandler: IHandler { private readonly RagonListenerList _listenerList; public AuthorizeFailedHandler(RagonListenerList list) @@ -27,9 +27,9 @@ internal class AuthorizeFailedHandler: Handler _listenerList = list; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var message = buffer.ReadString(); + var message = reader.ReadString(); _listenerList.OnAuthorizationFailed(message); } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs b/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs index bf88717..32d3300 100644 --- a/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs +++ b/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class AuthorizeSuccessHandler: Handler +internal class AuthorizeSuccessHandler: IHandler { private readonly RagonListenerList _listenerList; private readonly RagonClient _client; @@ -32,11 +32,11 @@ internal class AuthorizeSuccessHandler: Handler _listenerList = listenerList; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var playerId = buffer.ReadString(); - var playerName = buffer.ReadString(); - var playerPayload = buffer.ReadString(); + var playerId = reader.ReadString(); + var playerName = reader.ReadString(); + var playerPayload = reader.ReadString(); _client.SetStatus(RagonStatus.LOBBY); _listenerList.OnAuthorizationSuccess(playerId, playerName, playerPayload); diff --git a/Ragon.Client/Sources/Handler/EntityCreateHandler.cs b/Ragon.Client/Sources/Handler/EntityCreateHandler.cs index 7615e23..46f2515 100644 --- a/Ragon.Client/Sources/Handler/EntityCreateHandler.cs +++ b/Ragon.Client/Sources/Handler/EntityCreateHandler.cs @@ -18,12 +18,13 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class EntityCreateHandler : Handler +internal class EntityCreateHandler : IHandler { private readonly RagonClient _client; private readonly RagonPlayerCache _playerCache; private readonly RagonEntityCache _entityCache; private readonly IRagonEntityListener _entityListener; + public EntityCreateHandler( RagonClient client, RagonPlayerCache playerCache, @@ -37,15 +38,15 @@ internal class EntityCreateHandler : Handler _entityListener = entityListener; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var attachId = buffer.ReadUShort(); - var entityType = buffer.ReadUShort(); - var entityId = buffer.ReadUShort(); - var ownerId = buffer.ReadUShort(); + var attachId = reader.ReadUShort(); + var entityType = reader.ReadUShort(); + var entityId = reader.ReadUShort(); + var ownerId = reader.ReadUShort(); var player = _playerCache.GetPlayerByPeer(ownerId); - var payload = new RagonPayload(buffer.Capacity); - payload.Read(buffer); + var payload = new RagonPayload(reader.Capacity); + payload.Read(reader); if (player == null) { diff --git a/Ragon.Client/Sources/Handler/EntityEventHandler.cs b/Ragon.Client/Sources/Handler/EntityEventHandler.cs index fb8593d..6c28d0c 100644 --- a/Ragon.Client/Sources/Handler/EntityEventHandler.cs +++ b/Ragon.Client/Sources/Handler/EntityEventHandler.cs @@ -18,29 +18,26 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class EntityEventHandler : Handler +internal class EntityEventHandler : IHandler { - private readonly RagonClient _client; private readonly RagonPlayerCache _playerCache; private readonly RagonEntityCache _entityCache; public EntityEventHandler( - RagonClient client, RagonPlayerCache playerCache, RagonEntityCache entityCache ) { - _client = client; _playerCache = playerCache; _entityCache = entityCache; } - - public void Handle(RagonBuffer buffer) + + public void Handle(RagonBuffer reader) { - var eventCode = buffer.ReadUShort(); - var peerId = buffer.ReadUShort(); - var executionMode = (RagonReplicationMode)buffer.ReadByte(); - var entityId = buffer.ReadUShort(); + var eventCode = reader.ReadUShort(); + var peerId = reader.ReadUShort(); + var executionMode = (RagonReplicationMode)reader.ReadByte(); + var entityId = reader.ReadUShort(); var player = _playerCache.GetPlayerByPeer(peerId); if (player == null) @@ -52,6 +49,6 @@ internal class EntityEventHandler : Handler if (player.IsLocal && executionMode == RagonReplicationMode.LocalAndServer) return; - _entityCache.OnEvent(player, entityId, eventCode, buffer); + _entityCache.OnEvent(player, entityId, eventCode, reader); } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/EntityOwnershipHandler.cs b/Ragon.Client/Sources/Handler/EntityOwnershipHandler.cs index 6713819..c640a4f 100644 --- a/Ragon.Client/Sources/Handler/EntityOwnershipHandler.cs +++ b/Ragon.Client/Sources/Handler/EntityOwnershipHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class EntityOwnershipHandler: Handler +internal class EntityOwnershipHandler: IHandler { private readonly RagonListenerList _listenerList; private readonly RagonPlayerCache _playerCache; @@ -35,18 +35,18 @@ internal class EntityOwnershipHandler: Handler _entityCache = entityCache; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var newOwnerId = buffer.ReadUShort(); - var entities = buffer.ReadUShort(); + var newOwnerId = reader.ReadUShort(); + var entities = reader.ReadUShort(); var player = _playerCache.GetPlayerByPeer(newOwnerId); for (var i = 0; i < entities; i++) { - var entityId = buffer.ReadUShort(); + var entityId = reader.ReadUShort(); _entityCache.OnOwnershipChanged(player, entityId); RagonLog.Trace("Entity changed owner: " + entityId); - } + } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/EntityRemoveHandler.cs b/Ragon.Client/Sources/Handler/EntityRemoveHandler.cs index 25abafe..f375dae 100644 --- a/Ragon.Client/Sources/Handler/EntityRemoveHandler.cs +++ b/Ragon.Client/Sources/Handler/EntityRemoveHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class EntityRemoveHandler: Handler +internal class EntityRemoveHandler: IHandler { private readonly RagonEntityCache _entityCache; @@ -28,11 +28,11 @@ internal class EntityRemoveHandler: Handler _entityCache = entityCache; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var entityId = buffer.ReadUShort(); - var payload = new RagonPayload(buffer.Capacity); - payload.Read(buffer); + var entityId = reader.ReadUShort(); + var payload = new RagonPayload(reader.Capacity); + payload.Read(reader); _entityCache.OnDestroy(entityId, payload); } diff --git a/Ragon.Client/Sources/Handler/EntityStateHandler.cs b/Ragon.Client/Sources/Handler/EntityStateHandler.cs index d59b5dc..b3feb5d 100644 --- a/Ragon.Client/Sources/Handler/EntityStateHandler.cs +++ b/Ragon.Client/Sources/Handler/EntityStateHandler.cs @@ -18,7 +18,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class StateEntityHandler: Handler +internal class StateEntityHandler: IHandler { private readonly RagonEntityCache _entityCache; @@ -27,13 +27,13 @@ internal class StateEntityHandler: Handler _entityCache = entityCache; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var entitiesCount = buffer.ReadUShort(); + var entitiesCount = reader.ReadUShort(); for (var i = 0; i < entitiesCount; i++) { - var entityId = buffer.ReadUShort(); - _entityCache.OnState(entityId, buffer); + var entityId = reader.ReadUShort(); + _entityCache.OnState(entityId, reader); } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/Handler.cs b/Ragon.Client/Sources/Handler/IHandler.cs similarity index 90% rename from Ragon.Client/Sources/Handler/Handler.cs rename to Ragon.Client/Sources/Handler/IHandler.cs index ba396d6..c265e18 100644 --- a/Ragon.Client/Sources/Handler/Handler.cs +++ b/Ragon.Client/Sources/Handler/IHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -public interface Handler +public interface IHandler { - public void Handle(RagonBuffer buffer); + public void Handle(RagonBuffer reader); } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/JoinFailedHandler.cs b/Ragon.Client/Sources/Handler/JoinFailedHandler.cs index 73d3963..0c6173a 100644 --- a/Ragon.Client/Sources/Handler/JoinFailedHandler.cs +++ b/Ragon.Client/Sources/Handler/JoinFailedHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class JoinFailedHandler: Handler +internal class JoinFailedHandler: IHandler { private readonly RagonListenerList _listenerList; @@ -28,9 +28,9 @@ internal class JoinFailedHandler: Handler _listenerList = listenerList; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var message = buffer.ReadString(); + var message = reader.ReadString(); _listenerList.OnFailed(message); } } \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs b/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs index 41a7aa7..2d68fc0 100644 --- a/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs +++ b/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs @@ -37,7 +37,7 @@ public struct RagonRoomInformation public ushort Max { get; private set; } } -internal class JoinSuccessHandler : Handler +internal class JoinSuccessHandler : IHandler { private readonly RagonListenerList _listenerList; private readonly RagonPlayerCache _playerCache; @@ -46,7 +46,6 @@ internal class JoinSuccessHandler : Handler public JoinSuccessHandler( RagonClient client, - RagonBuffer buffer, RagonListenerList listenerList, RagonPlayerCache playerCache, RagonEntityCache entityCache @@ -58,14 +57,14 @@ internal class JoinSuccessHandler : Handler _playerCache = playerCache; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var roomId = buffer.ReadString(); - var localId = buffer.ReadString(); - var ownerId = buffer.ReadString(); - var min = buffer.ReadUShort(); - var max = buffer.ReadUShort(); - var sceneName = buffer.ReadString(); + var roomId = reader.ReadString(); + var localId = reader.ReadString(); + var ownerId = reader.ReadString(); + var min = reader.ReadUShort(); + var max = reader.ReadUShort(); + var sceneName = reader.ReadString(); var scene = new RagonScene(_client, _playerCache, _entityCache, sceneName); var roomInfo = new RagonRoomInformation(roomId, localId, ownerId, min, max); diff --git a/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs b/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs index bd9e6e2..949fef8 100644 --- a/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs +++ b/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class LeaveRoomHandler : Handler +internal class LeaveRoomHandler : IHandler { private readonly RagonClient _client; private readonly RagonListenerList _listenerList; @@ -35,7 +35,7 @@ internal class LeaveRoomHandler : Handler _entityCache = entityCache; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { _listenerList.OnLeft(); _entityCache.Cleanup(); diff --git a/Ragon.Client/Sources/Handler/LoadSceneHandler.cs b/Ragon.Client/Sources/Handler/LoadSceneHandler.cs index 2f13925..75aec82 100644 --- a/Ragon.Client/Sources/Handler/LoadSceneHandler.cs +++ b/Ragon.Client/Sources/Handler/LoadSceneHandler.cs @@ -18,7 +18,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class SceneLoadHandler: Handler +internal class SceneLoadHandler: IHandler { private readonly RagonClient _client; private readonly RagonListenerList _listenerList; @@ -32,9 +32,9 @@ internal class SceneLoadHandler: Handler _listenerList = listenerList; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var sceneName = buffer.ReadString(); + var sceneName = reader.ReadString(); var room = _client.Room; room.Cleanup(); diff --git a/Ragon.Client/Sources/Handler/OwnershipRoomHandler.cs b/Ragon.Client/Sources/Handler/OwnershipRoomHandler.cs index cb35c6f..d91741b 100644 --- a/Ragon.Client/Sources/Handler/OwnershipRoomHandler.cs +++ b/Ragon.Client/Sources/Handler/OwnershipRoomHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class OwnershipRoomHandler: Handler +internal class OwnershipRoomHandler: IHandler { private readonly RagonListenerList _listenerList; private readonly RagonPlayerCache _playerCache; @@ -35,9 +35,9 @@ internal class OwnershipRoomHandler: Handler _entityCache = entityCache; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var newOwnerId = buffer.ReadUShort(); + var newOwnerId = reader.ReadUShort(); var player = _playerCache.GetPlayerByPeer(newOwnerId); _playerCache.OnOwnershipChanged(newOwnerId); diff --git a/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs b/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs index 3057d37..e270281 100644 --- a/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs +++ b/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class PlayerJoinHandler : Handler +internal class PlayerJoinHandler : IHandler { private RagonPlayerCache _playerCache; private RagonListenerList _listenerList; @@ -33,11 +33,11 @@ internal class PlayerJoinHandler : Handler _listenerList = listenerList; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var playerPeerId = buffer.ReadUShort(); - var playerId = buffer.ReadString(); - var playerName = buffer.ReadString(); + var playerPeerId = reader.ReadUShort(); + var playerId = reader.ReadString(); + var playerName = reader.ReadString(); _playerCache.AddPlayer(playerPeerId, playerId, playerName); diff --git a/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs b/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs index f19080e..4d1988f 100644 --- a/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs +++ b/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs @@ -19,7 +19,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class PlayerLeftHandler : Handler +internal class PlayerLeftHandler : IHandler { private RagonPlayerCache _playerCache; private RagonEntityCache _entityCache; @@ -36,20 +36,20 @@ internal class PlayerLeftHandler : Handler _listenerList = listenerList; } - public void Handle(RagonBuffer buffer) + public void Handle(RagonBuffer reader) { - var playerId = buffer.ReadString(); + var playerId = reader.ReadString(); var player = _playerCache.GetPlayerById(playerId); if (player != null) { _playerCache.RemovePlayer(playerId); _listenerList.OnPlayerLeft(player); - var entities = buffer.ReadUShort(); + var entities = reader.ReadUShort(); var toDeleteIds = new ushort[entities]; for (var i = 0; i < entities; i++) { - var entityId = buffer.ReadUShort(); + var entityId = reader.ReadUShort(); toDeleteIds[i] = entityId; } diff --git a/Ragon.Client/Sources/Handler/RoomDataHandler.cs b/Ragon.Client/Sources/Handler/RoomDataHandler.cs new file mode 100644 index 0000000..b7b78fc --- /dev/null +++ b/Ragon.Client/Sources/Handler/RoomDataHandler.cs @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using Ragon.Protocol; + +namespace Ragon.Client; + +internal class RoomDataHandler: IHandler +{ + private readonly RagonListenerList _listeners; + private readonly RagonPlayerCache _playerCache; + + public RoomDataHandler( + RagonPlayerCache playerCache, + RagonListenerList listeners) + { + _playerCache = playerCache; + _listeners = listeners; + } + + public void Handle(RagonBuffer reader) + { + var rawData = reader.RawData; + var peerId = (ushort)(rawData[1] + (rawData[2] << 8)); + var player = _playerCache.GetPlayerByPeer(peerId); + var headerSize = 3; + var payload = new byte[rawData.Length - headerSize]; + + Array.Copy(rawData, headerSize, payload, 0, payload.Length); + + _listeners.OnData(player, payload); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/RoomEventHandler.cs b/Ragon.Client/Sources/Handler/RoomEventHandler.cs new file mode 100644 index 0000000..e1528ed --- /dev/null +++ b/Ragon.Client/Sources/Handler/RoomEventHandler.cs @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; + +namespace Ragon.Client; + +public class RoomEventHandler: IHandler +{ + private readonly RagonClient _client; + private readonly RagonPlayerCache _playerCache; + + public RoomEventHandler( + RagonClient client, + RagonPlayerCache playerCache + ) + { + _client = client; + _playerCache = playerCache; + } + + public void Handle(RagonBuffer buffer) + { + var eventCode = buffer.ReadUShort(); + var peerId = buffer.ReadUShort(); + var executionMode = (RagonReplicationMode)buffer.ReadByte(); + + var player = _playerCache.GetPlayerByPeer(peerId); + if (player == null) + { + RagonLog.Warn($"Player not found for event {eventCode}"); + return; + } + + if (player.IsLocal && executionMode == RagonReplicationMode.LocalAndServer) + return; + + _client.Room.Event(eventCode, player, buffer); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/Handler/SnapshotHandler.cs b/Ragon.Client/Sources/Handler/SnapshotHandler.cs index aff6245..5ce1a7c 100644 --- a/Ragon.Client/Sources/Handler/SnapshotHandler.cs +++ b/Ragon.Client/Sources/Handler/SnapshotHandler.cs @@ -20,7 +20,7 @@ using Ragon.Protocol; namespace Ragon.Client; -internal class SnapshotHandler : Handler +internal class SnapshotHandler : IHandler { private readonly IRagonEntityListener _entityListener; private readonly RagonClient _client; diff --git a/Ragon.Client/Sources/Handler/TimestampHandler.cs b/Ragon.Client/Sources/Handler/TimestampHandler.cs new file mode 100644 index 0000000..53d40f4 --- /dev/null +++ b/Ragon.Client/Sources/Handler/TimestampHandler.cs @@ -0,0 +1,21 @@ +using Ragon.Protocol; + +namespace Ragon.Client; + +public class TimestampHandler: IHandler +{ + private readonly RagonClient _client; + public TimestampHandler(RagonClient client) + { + _client = client; + } + + public void Handle(RagonBuffer buffer) + { + var timestamp0 = buffer.Read(32); + var timestamp1 = buffer.Read(32); + var value = new DoubleToUInt { Int0 = timestamp0, Int1 = timestamp1 }; + + _client.SetTimestamp(value.Double); + } +} \ No newline at end of file diff --git a/Ragon.Client/Sources/IRagonConnection.cs b/Ragon.Client/Sources/IRagonConnection.cs index 2a1c116..cb5e186 100644 --- a/Ragon.Client/Sources/IRagonConnection.cs +++ b/Ragon.Client/Sources/IRagonConnection.cs @@ -18,5 +18,5 @@ namespace Ragon.Client; public interface IRagonConnection { - + public void Close(); } \ No newline at end of file diff --git a/Ragon.Client/Sources/Listener/IRagonDataListener.cs b/Ragon.Client/Sources/Listener/IRagonDataListener.cs new file mode 100644 index 0000000..006d8d4 --- /dev/null +++ b/Ragon.Client/Sources/Listener/IRagonDataListener.cs @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Ragon.Client; + +public interface IRagonDataListener +{ + public void OnData(RagonPlayer player, byte[] data); +} \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonClient.cs b/Ragon.Client/Sources/RagonClient.cs index 582c2b4..0886aed 100644 --- a/Ragon.Client/Sources/RagonClient.cs +++ b/Ragon.Client/Sources/RagonClient.cs @@ -24,7 +24,7 @@ namespace Ragon.Client private readonly NetworkStatistics _stats; private IRagonEntityListener _entityListener; private IRagonSceneCollector _sceneCollector; - private Handler[] _handlers; + private IHandler[] _handlers; private RagonBuffer _readBuffer; private RagonBuffer _writeBuffer; private RagonRoom _room; @@ -35,9 +35,11 @@ namespace Ragon.Client private RagonEventCache _eventCache; private RagonStatus _status; + private double _serverTimestamp; private float _replicationRate = 0; private float _replicationTime = 0; + public double ServerTimestamp => _serverTimestamp; public IRagonConnection Connection => _connection; public RagonStatus Status => _status; public RagonSession Session => _session; @@ -46,6 +48,7 @@ namespace Ragon.Client public NetworkStatistics Statistics => _stats; public RagonRoom Room => _room; + internal RagonBuffer Buffer => _writeBuffer; internal INetworkChannel Reliable => _connection.Reliable; internal INetworkChannel Unreliable => _connection.Unreliable; @@ -54,13 +57,13 @@ namespace Ragon.Client public RagonClient(INetworkConnection connection, int rate) { + listeners = new RagonListenerList(this); + _connection = connection; _connection.OnData += OnData; _connection.OnConnected += OnConnected; _connection.OnDisconnected += OnDisconnected; - - listeners = new RagonListenerList(this); - + _replicationRate = (1000.0f / rate) / 1000.0f; _replicationTime = 0; @@ -96,15 +99,15 @@ namespace Ragon.Client _writeBuffer = new RagonBuffer(); _readBuffer = new RagonBuffer(); - _session = new RagonSession(this, _readBuffer); + _session = new RagonSession(this, _writeBuffer); _playerCache = new RagonPlayerCache(); _entityCache = new RagonEntityCache(this, _playerCache, _sceneCollector); - _handlers = new Handler[byte.MaxValue]; + _handlers = new IHandler[byte.MaxValue]; _handlers[(byte)RagonOperation.AUTHORIZED_SUCCESS] = new AuthorizeSuccessHandler(this, listeners); _handlers[(byte)RagonOperation.AUTHORIZED_FAILED] = new AuthorizeFailedHandler(listeners); - _handlers[(byte)RagonOperation.JOIN_SUCCESS] = new JoinSuccessHandler(this, _readBuffer, listeners, _playerCache, _entityCache); + _handlers[(byte)RagonOperation.JOIN_SUCCESS] = new JoinSuccessHandler(this, listeners, _playerCache, _entityCache); _handlers[(byte)RagonOperation.JOIN_FAILED] = new JoinFailedHandler(listeners); _handlers[(byte)RagonOperation.LEAVE_ROOM] = new LeaveRoomHandler(this, listeners, _entityCache); _handlers[(byte)RagonOperation.OWNERSHIP_ROOM_CHANGED] = new OwnershipRoomHandler(listeners, _playerCache, _entityCache); @@ -115,8 +118,11 @@ namespace Ragon.Client _handlers[(byte)RagonOperation.CREATE_ENTITY] = new EntityCreateHandler(this, _playerCache, _entityCache, _entityListener); _handlers[(byte)RagonOperation.REMOVE_ENTITY] = new EntityRemoveHandler(_entityCache); _handlers[(byte)RagonOperation.REPLICATE_ENTITY_STATE] = new StateEntityHandler(_entityCache); - _handlers[(byte)RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventHandler(this, _playerCache, _entityCache); + _handlers[(byte)RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventHandler(_playerCache, _entityCache); + _handlers[(byte)RagonOperation.REPLICATE_ROOM_EVENT] = new RoomEventHandler(this, _playerCache); _handlers[(byte)RagonOperation.SNAPSHOT] = new SnapshotHandler(this, listeners, _entityCache, _playerCache, _entityListener); + _handlers[(byte)RagonOperation.TIMESTAMP_SYNCHRONIZATION] = new TimestampHandler(this); + _handlers[(byte)RagonOperation.REPLICATE_RAW_DATA] = new RoomDataHandler(_playerCache, listeners); var protocolRaw = RagonVersion.Parse(protocol); _connection.Connect(address, port, protocolRaw); @@ -138,8 +144,10 @@ namespace Ragon.Client _replicationTime += dt; if (_replicationTime >= _replicationRate) { - _entityCache.WriteState(_readBuffer); _replicationTime = 0; + _entityCache.WriteState(_writeBuffer); + + SendTimestamp(); } _stats.Update(_connection.BytesSent, _connection.BytesReceived, _connection.Ping, dt); @@ -151,8 +159,11 @@ namespace Ragon.Client public void Dispose() { - _status = RagonStatus.DISCONNECTED; - _connection.Disconnect(); + if (_status != RagonStatus.DISCONNECTED) + { + _status = RagonStatus.DISCONNECTED; + _connection.Disconnect(); + } _connection.Dispose(); } @@ -167,6 +178,7 @@ namespace Ragon.Client public void AddListener(IRagonPlayerLeftListener listener) => listeners.Add(listener); public void AddListener(IRagonSceneListener listener) => listeners.Add(listener); public void AddListener(IRagonSceneRequestListener listener) => listeners.Add(listener); + public void AddListener(IRagonDataListener listener) => listeners.Add(listener); public void RemoveListener(IRagonListener listener) => listeners.Remove(listener); public void RemoveListener(IRagonAuthorizationListener listener) => listeners.Remove(listener); @@ -179,6 +191,7 @@ namespace Ragon.Client public void RemoveListener(IRagonPlayerLeftListener listener) => listeners.Remove(listener); public void RemoveListener(IRagonSceneListener listener) => listeners.Remove(listener); public void RemoveListener(IRagonSceneRequestListener listener) => listeners.Remove(listener); + public void RemoveListener(IRagonDataListener listener) => listeners.Remove(listener); #endregion @@ -186,6 +199,7 @@ namespace Ragon.Client internal void AssignRoom(RagonRoom room) { + _room?.Dispose(); _room = room; } @@ -194,10 +208,29 @@ namespace Ragon.Client _status = status; } + internal void SetTimestamp(double time) + { + _serverTimestamp = time; + } + #endregion #region PRIVATE + private void SendTimestamp() + { + var timestamp = RagonTime.CurrentTimestamp(); + var value = new DoubleToUInt() + { + Double = timestamp, + }; + + _writeBuffer.Clear(); + _writeBuffer.WriteOperation(RagonOperation.TIMESTAMP_SYNCHRONIZATION); + _writeBuffer.Write(value.Int0, 32); + _writeBuffer.Write(value.Int1, 32); + } + private void OnConnected() { RagonLog.Trace("Connected"); @@ -214,7 +247,7 @@ namespace Ragon.Client _status = RagonStatus.DISCONNECTED; } - public void OnData(byte[] data) + private void OnData(byte[] data) { _readBuffer.Clear(); _readBuffer.FromArray(data); diff --git a/Ragon.Client/Sources/RagonEntityCache.cs b/Ragon.Client/Sources/RagonEntityCache.cs index 31f841d..ea27049 100644 --- a/Ragon.Client/Sources/RagonEntityCache.cs +++ b/Ragon.Client/Sources/RagonEntityCache.cs @@ -83,12 +83,14 @@ public sealed class RagonEntityCache public void Destroy(RagonEntity entity, RagonPayload destroyPayload) { - if (!entity.IsAttached) + if (!entity.IsAttached && !entity.HasAuthority) { - RagonLog.Warn("Can't destroy object, he is not created"); + RagonLog.Warn("Can't destroy object"); return; } - + + entity.SetReplication(false); + var buffer = _client.Buffer; buffer.Clear(); @@ -215,10 +217,12 @@ public sealed class RagonEntityCache { if (_entityMap.TryGetValue(entityId, out var entity)) { + _entityMap.Remove(entityId); _entityList.Remove(entity); entity.Detach(payload); + entity.Dispose(); } } diff --git a/Ragon.Client/Sources/RagonListenerList.cs b/Ragon.Client/Sources/RagonListenerList.cs index c03455d..24af60d 100644 --- a/Ragon.Client/Sources/RagonListenerList.cs +++ b/Ragon.Client/Sources/RagonListenerList.cs @@ -31,13 +31,14 @@ namespace Ragon.Client private readonly List _ownershipChangedListeners = new(); private readonly List _playerJoinListeners = new(); private readonly List _playerLeftListeners = new(); + private readonly List _dataListeners = new(); private readonly List _delayedActions = new(); - + public RagonListenerList(RagonClient client) { _client = client; } - + public void Add(IRagonListener listener) { _authorizationListeners.Add(listener); @@ -71,108 +72,116 @@ namespace Ragon.Client { foreach (var action in _delayedActions) action.Invoke(); - + _delayedActions.Clear(); } - + + public void Add(IRagonDataListener dataListener) + { + _dataListeners.Add(dataListener); + } public void Add(IRagonAuthorizationListener listener) { _authorizationListeners.Add(listener); } - + public void Add(IRagonSceneRequestListener listener) { _sceneRequestListeners.Add(listener); } - + public void Add(IRagonConnectionListener listener) { _connectionListeners.Add(listener); } - + public void Add(IRagonFailedListener listener) { _failedListeners.Add(listener); } - + public void Add(IRagonJoinListener listener) { _joinListeners.Add(listener); } - - public void Add(IRagonLeftListener listener) + + public void Add(IRagonLeftListener listener) { _leftListeners.Add(listener); } - - public void Add(IRagonSceneListener listener) + + public void Add(IRagonSceneListener listener) { _sceneListeners.Add(listener); } - - public void Add(IRagonOwnershipChangedListener listener) + + public void Add(IRagonOwnershipChangedListener listener) { _ownershipChangedListeners.Add(listener); } - public void Add(IRagonPlayerJoinListener listener) + public void Add(IRagonPlayerJoinListener listener) { _playerJoinListeners.Add(listener); } - - public void Add(IRagonPlayerLeftListener listener) + + public void Add(IRagonPlayerLeftListener listener) { _playerLeftListeners.Add(listener); } - + + public void Remove(IRagonDataListener listener) + { + _delayedActions.Add(() => _dataListeners.Remove(listener)); + } + public void Remove(IRagonSceneRequestListener listener) { _delayedActions.Add(() => _sceneRequestListeners.Remove(listener)); } - + public void Remove(IRagonAuthorizationListener listener) { _delayedActions.Add(() => _authorizationListeners.Remove(listener)); } - + public void Remove(IRagonConnectionListener listener) { - _delayedActions.Add(() => _connectionListeners.Remove(listener)); } - + public void Remove(IRagonFailedListener listener) { _delayedActions.Add(() => _failedListeners.Remove(listener)); } - + public void Remove(IRagonJoinListener listener) { _delayedActions.Add(() => _joinListeners.Remove(listener)); } - - public void Remove(IRagonLeftListener listener) + + public void Remove(IRagonLeftListener listener) { _delayedActions.Add(() => _leftListeners.Remove(listener)); } - - public void Remove(IRagonSceneListener listener) + + public void Remove(IRagonSceneListener listener) { _delayedActions.Add(() => _sceneListeners.Remove(listener)); } - - public void Remove(IRagonOwnershipChangedListener listener) + + public void Remove(IRagonOwnershipChangedListener listener) { _delayedActions.Add(() => _ownershipChangedListeners.Remove(listener)); } - public void Remove(IRagonPlayerJoinListener listener) + public void Remove(IRagonPlayerJoinListener listener) { _delayedActions.Add(() => _playerJoinListeners.Remove(listener)); } - - public void Remove(IRagonPlayerLeftListener listener) + + public void Remove(IRagonPlayerLeftListener listener) { _delayedActions.Add(() => _playerLeftListeners.Remove(listener)); } @@ -182,13 +191,13 @@ namespace Ragon.Client foreach (var listener in _authorizationListeners) listener.OnAuthorizationSuccess(_client, playerId, playerName); } - + public void OnAuthorizationFailed(string message) { foreach (var listener in _authorizationListeners) listener.OnAuthorizationFailed(_client, message); } - + public void OnLeft() { foreach (var listener in _leftListeners) @@ -224,7 +233,7 @@ namespace Ragon.Client foreach (var listener in _sceneListeners) listener.OnSceneLoaded(_client); } - + public void OnSceneRequest(string sceneName) { foreach (var listener in _sceneRequestListeners) @@ -248,5 +257,11 @@ namespace Ragon.Client foreach (var listener in _connectionListeners) listener.OnDisconnected(_client, disconnect); } + + public void OnData(RagonPlayer player, byte[] data) + { + foreach (var listener in _dataListeners) + listener.OnData(player, data); + } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonRoom.cs b/Ragon.Client/Sources/RagonRoom.cs index ca9fd6b..7e4aa8f 100644 --- a/Ragon.Client/Sources/RagonRoom.cs +++ b/Ragon.Client/Sources/RagonRoom.cs @@ -14,10 +14,14 @@ * limitations under the License. */ +using Ragon.Protocol; + namespace Ragon.Client { - public class RagonRoom + public class RagonRoom: IDisposable { + private delegate void OnEventDelegate(RagonPlayer player, RagonBuffer serializer); + private RagonClient _client; private RagonScene _scene; private RagonEntityCache _entityCache; @@ -32,7 +36,11 @@ namespace Ragon.Client public IReadOnlyList Players => _playerCache.Players; public RagonPlayer Local => _playerCache.Local; public RagonPlayer Owner => _playerCache.Owner; - + + private readonly Dictionary _events = new Dictionary(); + private readonly Dictionary>> _localListeners = new Dictionary>>(); + private readonly Dictionary>> _listeners = new Dictionary>>(); + public RagonRoom(RagonClient client, RagonEntityCache entityCache, RagonPlayerCache playerCache, @@ -57,14 +65,83 @@ namespace Ragon.Client _scene.Update(sceneName); } + internal void Event(ushort eventCode, RagonPlayer caller, RagonBuffer buffer) + { + if (_events.TryGetValue(eventCode, out var evnt)) + evnt?.Invoke(caller, buffer); + else + RagonLog.Warn($"Handler event on entity {Id} with eventCode {eventCode} not defined"); + } + + public Action OnEvent(Action callback) where TEvent : IRagonEvent, new() + { + var t = new TEvent(); + var eventCode = _client.Event.GetEventCode(t); + + var action = (RagonPlayer player, IRagonEvent eventData) => callback.Invoke(player, (TEvent)eventData); + + if (!_listeners.TryGetValue(eventCode, out var callbacks)) + { + callbacks = new List>(); + _listeners.Add(eventCode, callbacks); + } + + if (!_localListeners.TryGetValue(eventCode, out var localCallbacks)) + { + localCallbacks = new List>(); + _localListeners.Add(eventCode, localCallbacks); + } + + callbacks.Add(action); + localCallbacks.Add(action); + + if (!_events.ContainsKey(eventCode)) + { + _events.Add(eventCode, (player, serializer) => + { + t.Deserialize(serializer); + + foreach (var callbackListener in callbacks) + callbackListener.Invoke(player, t); + }); + } + + return action; + } + + public void OffEvent(Action callback) where TEvent : IRagonEvent, new() + { + var t = new TEvent(); + var eventCode = _client.Event.GetEventCode(t); + + if (_listeners.TryGetValue(eventCode, out var callbacks)) + callbacks.Remove(callback); + + if (_localListeners.TryGetValue(eventCode, out var localCallbacks)) + localCallbacks.Remove(callback); + } + public void LoadScene(string sceneName) => _scene.Load(sceneName); public void SceneLoaded() => _scene.SceneLoaded(); + public void ReplicateEvent(TEvent evnt, RagonTarget target, RagonReplicationMode mode) where TEvent : IRagonEvent, new() => _scene.ReplicateEvent(evnt, target, mode); + public void ReplicateEvent(TEvent evnt, RagonPlayer target, RagonReplicationMode mode) where TEvent : IRagonEvent, new() => _scene.ReplicateEvent(evnt, target, mode); + public void ReplicateData(byte[] data, bool reliable = false) => _scene.ReplicateData(data, reliable); + public void CreateEntity(RagonEntity entity) => CreateEntity(entity, null); public void CreateEntity(RagonEntity entity, RagonPayload payload) => _entityCache.Create(entity, payload); public void TransferEntity(RagonEntity entity, RagonPlayer player) => _entityCache.Transfer(entity, player); public void DestroyEntity(RagonEntity entityId) => DestroyEntity(entityId, null); public void DestroyEntity(RagonEntity entityId, RagonPayload payload) => _entityCache.Destroy(entityId, payload); + + public void Dispose() + { + Cleanup(); + + _events.Clear(); + _listeners.Clear(); + _localListeners.Clear(); + } } } \ No newline at end of file diff --git a/Ragon.Client/Sources/RagonScene.cs b/Ragon.Client/Sources/RagonScene.cs index fdeee30..186dd49 100644 --- a/Ragon.Client/Sources/RagonScene.cs +++ b/Ragon.Client/Sources/RagonScene.cs @@ -67,4 +67,51 @@ public class RagonScene var sendData = buffer.ToArray(); _client.Reliable.Send(sendData); } + + internal void ReplicateEvent(TEvent evnt, RagonTarget target, RagonReplicationMode replicationMode) + where TEvent : IRagonEvent, new() + { + var evntId = _client.Event.GetEventCode(evnt); + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ROOM_EVENT); + buffer.WriteUShort(evntId); + buffer.WriteByte((byte)replicationMode); + buffer.WriteByte((byte)target); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + internal void ReplicateEvent(TEvent evnt, RagonPlayer target, RagonReplicationMode replicationMode) + where TEvent : IRagonEvent, new() + { + var evntId = _client.Event.GetEventCode(evnt); + var buffer = _client.Buffer; + + buffer.Clear(); + buffer.WriteOperation(RagonOperation.REPLICATE_ROOM_EVENT); + buffer.WriteUShort(evntId); + buffer.WriteByte((byte)replicationMode); + buffer.WriteByte((byte)RagonTarget.Player); + buffer.WriteUShort(target.PeerId); + + evnt.Serialize(buffer); + + var sendData = buffer.ToArray(); + _client.Reliable.Send(sendData); + } + + public void ReplicateData(byte[] data, bool reliable) + { + var sendData = new byte[data.Length + 1]; + sendData[0] = (byte) RagonOperation.REPLICATE_RAW_DATA; + Array.Copy(data, 0, sendData, 1, data.Length); + + if (reliable) + _client.Reliable.Send(sendData); + else + _client.Unreliable.Send(sendData); + } } \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonBuffer.cs b/Ragon.Protocol/Sources/RagonBuffer.cs index f8cb693..5280758 100644 --- a/Ragon.Protocol/Sources/RagonBuffer.cs +++ b/Ragon.Protocol/Sources/RagonBuffer.cs @@ -68,8 +68,10 @@ namespace Ragon.Protocol private int _read; private int _write; private uint[] _buckets; + private byte[] _rawData; private readonly UTF8Encoding _utf8Encoding = new UTF8Encoding(false, true); + public byte[] RawData => _rawData; public int ReadOffset => _read; public int WriteOffset => _write; public int Length => ((_write - 1) >> 3) + 1; @@ -78,6 +80,7 @@ namespace Ragon.Protocol public RagonBuffer(int capacity = 128) { _buckets = new uint[capacity]; + _rawData = Array.Empty(); _read = 0; _write = 0; } @@ -397,10 +400,13 @@ namespace Ragon.Protocol _write = ((length - 1) * 8) + positionInByte; _read = 0; + _rawData = data; } public byte[] ToArray() { + Write(1, 1); + var data = new byte[Length]; int bucketsCount = (_write >> 5) + 1; int length = data.Length; @@ -425,6 +431,32 @@ namespace Ragon.Protocol return data; } + + public void ToArray(byte[] outData) + { + Debug.Assert(outData.Length >= Length); + + var bucketsCount = (_write >> 5) + 1; + var length = Length; + + for (int i = 0; i < bucketsCount; i++) + { + var dataIdx = i * 4; + var bucket = _buckets[i]; + + if (dataIdx < length) + outData[dataIdx] = (byte)bucket; + + if (dataIdx + 1 < length) + outData[dataIdx + 1] = (byte)(bucket >> 8); + + if (dataIdx + 2 < length) + outData[dataIdx + 2] = (byte)(bucket >> 16); + + if (dataIdx + 3 < length) + outData[dataIdx + 3] = (byte)(bucket >> 24); + } + } private void Resize(int capacity) { diff --git a/Ragon.Protocol/Sources/RagonOperation.cs b/Ragon.Protocol/Sources/RagonOperation.cs index a152d1e..2c43def 100644 --- a/Ragon.Protocol/Sources/RagonOperation.cs +++ b/Ragon.Protocol/Sources/RagonOperation.cs @@ -19,27 +19,30 @@ namespace Ragon.Protocol { public enum RagonOperation: byte { - AUTHORIZE, - AUTHORIZED_SUCCESS, - AUTHORIZED_FAILED, - JOIN_OR_CREATE_ROOM, - CREATE_ROOM, - JOIN_ROOM, - LEAVE_ROOM, - OWNERSHIP_ENTITY_CHANGED, - OWNERSHIP_ROOM_CHANGED, - JOIN_SUCCESS, - JOIN_FAILED, - LOAD_SCENE, - SCENE_LOADED, - PLAYER_JOINED, - PLAYER_LEAVED, - CREATE_ENTITY, - REMOVE_ENTITY, - SNAPSHOT, - REPLICATE_ENTITY_STATE, - REPLICATE_ENTITY_EVENT, - TRANSFER_ROOM_OWNERSHIP, - TRANSFER_ENTITY_OWNERSHIP, + AUTHORIZE = 1, + AUTHORIZED_SUCCESS = 2, + AUTHORIZED_FAILED = 3, + JOIN_OR_CREATE_ROOM = 4, + CREATE_ROOM = 5, + JOIN_ROOM = 6, + LEAVE_ROOM = 7, + OWNERSHIP_ENTITY_CHANGED = 8, + OWNERSHIP_ROOM_CHANGED= 9, + JOIN_SUCCESS = 10, + JOIN_FAILED = 11, + LOAD_SCENE = 12, + SCENE_LOADED = 13, + PLAYER_JOINED = 14, + PLAYER_LEAVED = 15, + CREATE_ENTITY = 16, + REMOVE_ENTITY = 17, + SNAPSHOT = 18, + REPLICATE_ENTITY_STATE = 19, + REPLICATE_ENTITY_EVENT = 20, + REPLICATE_RAW_DATA = 21, + REPLICATE_ROOM_EVENT = 22, + TRANSFER_ROOM_OWNERSHIP = 23, + TRANSFER_ENTITY_OWNERSHIP = 24, + TIMESTAMP_SYNCHRONIZATION = 25, } } \ No newline at end of file diff --git a/Ragon.Protocol/Sources/RagonUtils.cs b/Ragon.Protocol/Sources/RagonUtils.cs index acc995a..bc30b20 100644 --- a/Ragon.Protocol/Sources/RagonUtils.cs +++ b/Ragon.Protocol/Sources/RagonUtils.cs @@ -16,9 +16,34 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Ragon.Protocol { + [StructLayout(LayoutKind.Explicit)] + public struct DoubleToUInt + { + [FieldOffset(0)] + public double Double; + + [FieldOffset(0)] + public uint Int0; + + [FieldOffset(4)] + public uint Int1; + } + + public static class RagonTime { + public static double CurrentTimestamp() + { + var currentTime = System.DateTime.UtcNow.ToUniversalTime().Subtract( + new System.DateTime(1970, 1, 1, 0, 0, 0, System.DateTimeKind.Utc) + ).TotalMilliseconds; + + return currentTime; + } + } + public static class DeBruijn { private static readonly int[] _lookup = new int[32] diff --git a/Ragon.Relay/Program.cs b/Ragon.Relay/Program.cs index 39a26be..ef0a056 100644 --- a/Ragon.Relay/Program.cs +++ b/Ragon.Relay/Program.cs @@ -16,12 +16,12 @@ namespace Ragon.Relay { - class Program + class Program + { + static void Main(string[] args) { - static void Main(string[] args) - { - var relay = new Relay(); - relay.Start(); - } + var relay = new Relay(); + relay.Start(); } + } } \ No newline at end of file diff --git a/Ragon.Relay/Ragon.Relay.csproj b/Ragon.Relay/Ragon.Relay.csproj index cf8f643..d03597f 100644 --- a/Ragon.Relay/Ragon.Relay.csproj +++ b/Ragon.Relay/Ragon.Relay.csproj @@ -23,7 +23,7 @@ - + diff --git a/Ragon.Relay/Sources/Relay.cs b/Ragon.Relay/Sources/Relay.cs index 98edb98..dad3948 100644 --- a/Ragon.Relay/Sources/Relay.cs +++ b/Ragon.Relay/Sources/Relay.cs @@ -16,7 +16,7 @@ using NLog; using Ragon.Server; -using Ragon.Server.ENet; +using Ragon.Server.ENetServer; using Ragon.Server.WebSocketServer; using Ragon.Server.IO; using Ragon.Server.Plugin; diff --git a/Ragon.Server.ENet/Ragon.Server.ENet.csproj b/Ragon.Server.ENetServer/Ragon.Server.ENetServer.csproj similarity index 100% rename from Ragon.Server.ENet/Ragon.Server.ENet.csproj rename to Ragon.Server.ENetServer/Ragon.Server.ENetServer.csproj diff --git a/Ragon.Server.ENet/Sources/ENetConnection.cs b/Ragon.Server.ENetServer/Sources/ENetConnection.cs similarity index 85% rename from Ragon.Server.ENet/Sources/ENetConnection.cs rename to Ragon.Server.ENetServer/Sources/ENetConnection.cs index 20b7aba..37b8604 100644 --- a/Ragon.Server.ENet/Sources/ENetConnection.cs +++ b/Ragon.Server.ENetServer/Sources/ENetConnection.cs @@ -17,7 +17,7 @@ using ENet; using Ragon.Server.IO; -namespace Ragon.Server.ENet; +namespace Ragon.Server.ENetServer; public sealed class ENetConnection: INetworkConnection { @@ -31,8 +31,8 @@ public sealed class ENetConnection: INetworkConnection _peer = peer; Id = (ushort) peer.ID; - Reliable = new ENetReliableChannel(peer, 0); - Unreliable = new ENetUnreliableChannel(peer, 1); + Reliable = new ENetReliableChannel(peer, NetworkChannel.RELIABLE); + Unreliable = new ENetUnreliableChannel(peer, NetworkChannel.UNRELIABLE); } public void Close() diff --git a/Ragon.Server.ENet/Sources/ENetReliableChannel.cs b/Ragon.Server.ENetServer/Sources/ENetReliableChannel.cs similarity index 68% rename from Ragon.Server.ENet/Sources/ENetReliableChannel.cs rename to Ragon.Server.ENetServer/Sources/ENetReliableChannel.cs index 762ae3e..8fc9c98 100644 --- a/Ragon.Server.ENet/Sources/ENetReliableChannel.cs +++ b/Ragon.Server.ENetServer/Sources/ENetReliableChannel.cs @@ -14,20 +14,24 @@ * limitations under the License. */ +using System.Net; using ENet; +using Ragon.Protocol; using Ragon.Server.IO; -namespace Ragon.Server.ENet; +namespace Ragon.Server.ENetServer; public sealed class ENetReliableChannel: INetworkChannel { private Peer _peer; private byte _channelId; + private byte[] _data; - public ENetReliableChannel(Peer peer, int channelId) + public ENetReliableChannel(Peer peer, NetworkChannel channel) { _peer = peer; - _channelId = (byte) channelId; + _data = new byte[1500]; + _channelId = (byte) channel; } public void Send(byte[] data) @@ -37,4 +41,14 @@ public sealed class ENetReliableChannel: INetworkChannel _peer.Send(_channelId, ref newPacket); } + + public void Send(RagonBuffer buffer) + { + buffer.ToArray(_data); + + var newPacket = new Packet(); + newPacket.Create(_data, buffer.Length, PacketFlags.Reliable); + + _peer.Send(_channelId, ref newPacket); + } } \ No newline at end of file diff --git a/Ragon.Server.ENet/Sources/ENetServer.cs b/Ragon.Server.ENetServer/Sources/ENetServer.cs similarity index 81% rename from Ragon.Server.ENet/Sources/ENetServer.cs rename to Ragon.Server.ENetServer/Sources/ENetServer.cs index c953a3d..c0f5a41 100644 --- a/Ragon.Server.ENet/Sources/ENetServer.cs +++ b/Ragon.Server.ENetServer/Sources/ENetServer.cs @@ -19,40 +19,33 @@ using NLog; using Ragon.Protocol; using Ragon.Server.IO; -namespace Ragon.Server.ENet +namespace Ragon.Server.ENetServer { - public sealed class ENetServer: INetworkServer + public sealed class ENetServer : INetworkServer { public Executor Executor => _executor; - - private readonly Host _host; + + private readonly Host _host = new(); private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); - - private ENetConnection[] _connections; + + private ENetConnection[] _connections = Array.Empty(); private INetworkListener _listener; private uint _protocol; - private Event _event; - private Executor _executor; - - public ENetServer() - { - _host = new Host(); - _executor = new Executor(); - _connections = Array.Empty(); - } + private ENet.Event _event; + private Executor _executor = new(); public void Start(INetworkListener listener, NetworkConfiguration configuration) { Library.Initialize(); - + _connections = new ENetConnection[configuration.LimitConnections]; - + _listener = listener; _protocol = configuration.Protocol; - + var address = new Address { - Port = (ushort) configuration.Port, + Port = (ushort)configuration.Port, }; _host.Create(address, _connections.Length, 2, 0, 0, 1024 * 1024); @@ -74,7 +67,7 @@ namespace Ragon.Server.ENet polled = true; } - + switch (_event.Type) { case EventType.None: @@ -90,8 +83,9 @@ namespace Ragon.Server.ENet _event.Peer.DisconnectNow(0); break; } + var connection = new ENetConnection(_event.Peer); - + _connections[_event.Peer.ID] = connection; _listener.OnConnected(connection); break; @@ -110,24 +104,34 @@ namespace Ragon.Server.ENet } case EventType.Receive: { - var peerId = (ushort) _event.Peer.ID; + var peerId = (ushort)_event.Peer.ID; var connection = _connections[peerId]; var dataRaw = new byte[_event.Packet.Length]; - + _event.Packet.CopyTo(dataRaw); _event.Packet.Dispose(); - - _listener.OnData(connection, dataRaw); + + _listener.OnData(connection, (NetworkChannel)_event.ChannelID, dataRaw); break; } } } } + public void Broadcast(byte[] data, NetworkChannel channel) + { + var packet = new Packet(); + var flag = channel == NetworkChannel.RELIABLE? PacketFlags.Reliable: PacketFlags.None; + + packet.Create(data, flag); + + _host.Broadcast((byte)channel, ref packet); + } + public void Stop() { _host?.Dispose(); - + Library.Deinitialize(); } diff --git a/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs b/Ragon.Server.ENetServer/Sources/ENetUnreliableChannel.cs similarity index 70% rename from Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs rename to Ragon.Server.ENetServer/Sources/ENetUnreliableChannel.cs index 268128e..3efe2e8 100644 --- a/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs +++ b/Ragon.Server.ENetServer/Sources/ENetUnreliableChannel.cs @@ -15,19 +15,21 @@ */ using ENet; +using Ragon.Protocol; using Ragon.Server.IO; -namespace Ragon.Server.ENet; +namespace Ragon.Server.ENetServer; public sealed class ENetUnreliableChannel: INetworkChannel { private Peer _peer; private byte _channelId; + private byte[] _data; - public ENetUnreliableChannel(Peer peer, int channelId) + public ENetUnreliableChannel(Peer peer, NetworkChannel channel) { _peer = peer; - _channelId = (byte) channelId; + _channelId = (byte) channel; } public void Send(byte[] data) @@ -37,4 +39,14 @@ public sealed class ENetUnreliableChannel: INetworkChannel _peer.Send(_channelId, ref newPacket); } + + public void Send(RagonBuffer buffer) + { + buffer.ToArray(_data); + + var newPacket = new Packet(); + newPacket.Create(_data, buffer.Length, PacketFlags.None); + + _peer.Send(_channelId, ref newPacket); + } } \ No newline at end of file diff --git a/Ragon.Server.WebSocketServer/Sources/WebSocketReliableChannel.cs b/Ragon.Server.WebSocketServer/Sources/WebSocketReliableChannel.cs index c00c528..a3dfd61 100644 --- a/Ragon.Server.WebSocketServer/Sources/WebSocketReliableChannel.cs +++ b/Ragon.Server.WebSocketServer/Sources/WebSocketReliableChannel.cs @@ -15,6 +15,7 @@ */ using System.Net.WebSockets; +using Ragon.Protocol; using Ragon.Server.IO; namespace Ragon.Server.WebSocketServer; @@ -35,9 +36,17 @@ public class WebSocketReliableChannel : INetworkChannel _queue.Enqueue(data); } + public void Send(RagonBuffer buffer) + { + var sendData = buffer.ToArray(); + _queue.Enqueue(sendData); + } + public async Task Flush() { while (_queue.TryDequeue(out var sendData) && _socket.State == WebSocketState.Open) + { await _socket.SendAsync(sendData, WebSocketMessageType.Binary, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + } } } \ No newline at end of file diff --git a/Ragon.Server.WebSocketServer/Sources/WebSocketServer.cs b/Ragon.Server.WebSocketServer/Sources/WebSocketServer.cs index d304238..8361e63 100644 --- a/Ragon.Server.WebSocketServer/Sources/WebSocketServer.cs +++ b/Ragon.Server.WebSocketServer/Sources/WebSocketServer.cs @@ -25,7 +25,7 @@ namespace Ragon.Server.WebSocketServer; public class WebSocketServer : INetworkServer { public Executor Executor => _executor; - + private ILogger _logger = LogManager.GetCurrentClassLogger(); private INetworkListener _networkListener; private Stack _sequencer; @@ -34,7 +34,7 @@ public class WebSocketServer : INetworkServer private WebSocketConnection[] _connections; private List _activeConnections; private CancellationTokenSource _cancellationTokenSource; - + public WebSocketServer() { _sequencer = new Stack(); @@ -42,7 +42,7 @@ public class WebSocketServer : INetworkServer _activeConnections = new List(); _executor = new Executor(); } - + public async void StartAccept(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) @@ -61,7 +61,7 @@ public class WebSocketServer : INetworkServer var peerId = _sequencer.Pop(); var connection = new WebSocketConnection(webSocket, peerId); - + _connections[peerId] = connection; StartListen(connection, cancellationToken); } @@ -82,9 +82,11 @@ public class WebSocketServer : INetworkServer try { var result = await webSocket.ReceiveAsync(buffer, cancellationToken); - var dataRaw = buffer.Slice(0, result.Count); - if (dataRaw.Length > 0) - _networkListener.OnData(connection, dataRaw.ToArray()); + if (result.Count > 0) + { + var payload = buffer.Slice(0, buffer.Length); + _networkListener.OnData(connection, NetworkChannel.RELIABLE, payload.ToArray()); + } } catch (Exception ex) { @@ -102,6 +104,12 @@ public class WebSocketServer : INetworkServer Flush(); } + public void Broadcast(byte[] data, NetworkChannel channel) + { + foreach (var activeConnection in _activeConnections) + activeConnection.Reliable.Send(data); + } + public async void Flush() { foreach (var conn in _activeConnections) diff --git a/Ragon.Server/Ragon.Server.csproj b/Ragon.Server/Ragon.Server.csproj index 785c45a..2575c98 100644 --- a/Ragon.Server/Ragon.Server.csproj +++ b/Ragon.Server/Ragon.Server.csproj @@ -25,4 +25,5 @@ + diff --git a/Ragon.Server/Sources/Entity/RagonEntity.cs b/Ragon.Server/Sources/Entity/RagonEntity.cs index 9331821..d3f6ab5 100644 --- a/Ragon.Server/Sources/Entity/RagonEntity.cs +++ b/Ragon.Server/Sources/Entity/RagonEntity.cs @@ -16,6 +16,7 @@ using Ragon.Protocol; +using Ragon.Server.Event; using Ragon.Server.Room; namespace Ragon.Server.Entity; @@ -117,32 +118,37 @@ public class RagonEntity : IRagonEntity { buffer.WriteUShort(Type); buffer.WriteUShort(Id); - + if (StaticId != 0) buffer.WriteUShort(StaticId); - + buffer.WriteUShort(Owner.Connection.Id); buffer.WriteUShort(Payload.Size); - + Payload.Write(buffer); - + _state.Snapshot(buffer); } public void ReplicateEvent( - RagonRoomPlayer caller, + RagonRoomPlayer invoker, RagonEvent evnt, RagonReplicationMode eventMode, RagonRoomPlayer targetPlayer ) { + if (Authority == RagonAuthority.OwnerOnly && invoker.Connection.Id != Owner.Connection.Id) + { + return; + } + var room = Owner.Room; var buffer = room.Writer; - + buffer.Clear(); buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); buffer.WriteUShort(evnt.EventCode); - buffer.WriteUShort(caller.Connection.Id); + buffer.WriteUShort(invoker.Connection.Id); buffer.WriteByte((byte)eventMode); buffer.WriteUShort(Id); @@ -153,21 +159,18 @@ public class RagonEntity : IRagonEntity } public void ReplicateEvent( - RagonRoomPlayer caller, + RagonRoomPlayer invoker, RagonEvent evnt, RagonReplicationMode eventMode, RagonTarget targetMode ) { - if (Authority == RagonAuthority.OwnerOnly && - Owner.Connection.Id != caller.Connection.Id) + if (Authority == RagonAuthority.OwnerOnly && invoker.Connection.Id != Owner.Connection.Id) { return; } - if (eventMode == RagonReplicationMode.Buffered && - targetMode != RagonTarget.Owner && - _bufferedEvents.Count < _limitBufferedEvents) + if (eventMode == RagonReplicationMode.Buffered && targetMode != RagonTarget.Owner && _bufferedEvents.Count < _limitBufferedEvents) { _bufferedEvents.Add(evnt); } @@ -178,7 +181,7 @@ public class RagonEntity : IRagonEntity buffer.Clear(); buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); buffer.WriteUShort(evnt.EventCode); - buffer.WriteUShort(caller.Connection.Id); + buffer.WriteUShort(invoker.Connection.Id); buffer.WriteByte((byte)eventMode); buffer.WriteUShort(Id); @@ -206,7 +209,7 @@ public class RagonEntity : IRagonEntity { foreach (var roomPlayer in room.ReadyPlayersList) { - if (roomPlayer.Connection.Id != caller.Connection.Id) + if (roomPlayer.Connection.Id != invoker.Connection.Id) roomPlayer.Connection.Reliable.Send(sendData); } diff --git a/Ragon.Server/Sources/Entity/RagonEvent.cs b/Ragon.Server/Sources/Event/RagonEvent.cs similarity index 95% rename from Ragon.Server/Sources/Entity/RagonEvent.cs rename to Ragon.Server/Sources/Event/RagonEvent.cs index 22e20ac..d93d9c1 100644 --- a/Ragon.Server/Sources/Entity/RagonEvent.cs +++ b/Ragon.Server/Sources/Event/RagonEvent.cs @@ -17,7 +17,7 @@ using Ragon.Protocol; using Ragon.Server.Room; -namespace Ragon.Server.Entity; +namespace Ragon.Server.Event; public class RagonEvent { @@ -39,7 +39,7 @@ public class RagonEvent public void Read(RagonBuffer buffer) { - _size = buffer.Capacity; + _size = buffer.Capacity - 1; buffer.ReadArray(_data, _size); } diff --git a/Ragon.Server/Sources/Handler/AuthorizationOperation.cs b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs index c03e795..6ed25bb 100644 --- a/Ragon.Server/Sources/Handler/AuthorizationOperation.cs +++ b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs @@ -16,31 +16,32 @@ using NLog; using Ragon.Protocol; +using Ragon.Server.IO; using Ragon.Server.Lobby; -using Ragon.Server.Plugin; using Ragon.Server.Plugin.Web; namespace Ragon.Server.Handler; -public sealed class AuthorizationOperation: IRagonOperation +public sealed class AuthorizationOperation: BaseOperation { private Logger _logger = LogManager.GetCurrentClassLogger(); - private readonly RagonWebHookPlugin _ragonWebHook; - private readonly RagonContextObserver _contextObserver; + private readonly RagonWebHookPlugin _webhook; + private readonly RagonContextObserver _observer; private readonly RagonBuffer _writer; public AuthorizationOperation( - RagonWebHookPlugin ragonWebHook, - RagonContextObserver contextObserver, - RagonBuffer writer) + RagonBuffer reader, + RagonBuffer writer, + RagonWebHookPlugin webhook, + RagonContextObserver observer): base(reader, writer) { - _ragonWebHook = ragonWebHook; - _contextObserver = contextObserver; + _webhook = webhook; + _observer = observer; _writer = writer; } - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public override void Handle(RagonContext context, NetworkChannel channel) { if (context.ConnectionStatus == ConnectionStatus.Authorized) { @@ -55,13 +56,13 @@ public sealed class AuthorizationOperation: IRagonOperation } var configuration = context.Configuration; - var key = reader.ReadString(); - var name = reader.ReadString(); - var payload = reader.ReadString(); + var key = Reader.ReadString(); + var name = Reader.ReadString(); + var payload = Reader.ReadString(); if (key == configuration.ServerKey) { - if (_ragonWebHook.RequestAuthorization(context, name, payload)) + if (_webhook.RequestAuthorization(context, name, payload)) return; var lobbyPlayer = new RagonLobbyPlayer(context.Connection, Guid.NewGuid().ToString(), name, payload); @@ -79,7 +80,7 @@ public sealed class AuthorizationOperation: IRagonOperation { context.ConnectionStatus = ConnectionStatus.Authorized; - _contextObserver.OnAuthorized(context); + _observer.OnAuthorized(context); var playerId = context.LobbyPlayer.Id; var playerName = context.LobbyPlayer.Name; @@ -109,4 +110,6 @@ public sealed class AuthorizationOperation: IRagonOperation _logger.Trace($"Connection {context.Connection.Id}"); } + + } \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/BaseOperation.cs b/Ragon.Server/Sources/Handler/BaseOperation.cs new file mode 100644 index 0000000..6654691 --- /dev/null +++ b/Ragon.Server/Sources/Handler/BaseOperation.cs @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; +using Ragon.Server.IO; + +namespace Ragon.Server.Handler; + +public abstract class BaseOperation +{ + protected readonly RagonBuffer Reader; + protected readonly RagonBuffer Writer; + + public BaseOperation(RagonBuffer reader, RagonBuffer writer) + { + Reader = reader; + Writer = writer; + } + + public abstract void Handle(RagonContext context, NetworkChannel channel); +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityCreateOperation.cs b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs index c419d8c..546f5d8 100644 --- a/Ragon.Server/Sources/Handler/EntityCreateOperation.cs +++ b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs @@ -17,21 +17,26 @@ using NLog; using Ragon.Protocol; using Ragon.Server.Entity; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public sealed class EntityCreateOperation : IRagonOperation +public sealed class EntityCreateOperation : BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public EntityCreateOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } + + public override void Handle(RagonContext context, NetworkChannel channel) { var player = context.RoomPlayer; var room = context.Room; - var attachId = reader.ReadUShort(); - var entityType = reader.ReadUShort(); - var eventAuthority = (RagonAuthority) reader.ReadByte(); - var propertiesCount = reader.ReadUShort(); + var attachId = Reader.ReadUShort(); + var entityType = Reader.ReadUShort(); + var eventAuthority = (RagonAuthority) Reader.ReadByte(); + var propertiesCount = Reader.ReadUShort(); var entityParameters = new RagonEntityParameters() { @@ -45,14 +50,14 @@ public sealed class EntityCreateOperation : IRagonOperation var entity = new RagonEntity(entityParameters); for (var i = 0; i < propertiesCount; i++) { - var propertyType = reader.ReadBool(); - var propertySize = reader.ReadUShort(); + var propertyType = Reader.ReadBool(); + var propertySize = Reader.ReadUShort(); entity.AddProperty(new RagonProperty(propertySize, propertyType)); } - if (reader.Capacity > 0) - entity.Payload.Read(reader); + if (Reader.Capacity > 0) + entity.Payload.Read(Reader); var plugin = room.Plugin; if (!plugin.OnEntityCreate(player, entity)) @@ -66,4 +71,6 @@ public sealed class EntityCreateOperation : IRagonOperation _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}"); } + + } \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityEventOperation.cs b/Ragon.Server/Sources/Handler/EntityEventOperation.cs index 069c9d3..7227adf 100644 --- a/Ragon.Server/Sources/Handler/EntityEventOperation.cs +++ b/Ragon.Server/Sources/Handler/EntityEventOperation.cs @@ -16,19 +16,24 @@ using NLog; using Ragon.Protocol; -using Ragon.Server.Entity; +using Ragon.Server.Event; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public sealed class EntityEventOperation : IRagonOperation +public sealed class EntityEventOperation : BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public EntityEventOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public override void Handle(RagonContext context, NetworkChannel channel) { var player = context.RoomPlayer; var room = context.Room; - var entityId = reader.ReadUShort(); + var entityId = Reader.ReadUShort(); if (!room.Entities.TryGetValue(entityId, out var ent)) { @@ -36,16 +41,16 @@ public sealed class EntityEventOperation : IRagonOperation return; } - var eventId = reader.ReadUShort(); - var eventMode = (RagonReplicationMode)reader.ReadByte(); - var targetMode = (RagonTarget)reader.ReadByte(); + var eventId = Reader.ReadUShort(); + var eventMode = (RagonReplicationMode)Reader.ReadByte(); + var targetMode = (RagonTarget)Reader.ReadByte(); var targetPlayerPeerId = (ushort)0; if (targetMode == RagonTarget.Player) - targetPlayerPeerId = reader.ReadUShort(); + targetPlayerPeerId = Reader.ReadUShort(); var @event = new RagonEvent(player, eventId); - @event.Read(reader); + @event.Read(Reader); if (targetMode == RagonTarget.Player && room.Players.TryGetValue(targetPlayerPeerId, out var targetPlayer)) { diff --git a/Ragon.Server/Sources/Handler/EntityOwnershipOperation.cs b/Ragon.Server/Sources/Handler/EntityOwnershipOperation.cs index df78d2e..9ae4592 100644 --- a/Ragon.Server/Sources/Handler/EntityOwnershipOperation.cs +++ b/Ragon.Server/Sources/Handler/EntityOwnershipOperation.cs @@ -1,19 +1,41 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + using NLog; using Ragon.Protocol; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public sealed class EntityOwnershipOperation : IRagonOperation +public sealed class EntityOwnershipOperation : BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public EntityOwnershipOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public override void Handle(RagonContext context, NetworkChannel channel) { var currentOwner = context.RoomPlayer; var room = context.Room; - var entityId = reader.ReadUShort(); - var playerPeerId = reader.ReadUShort(); + var entityId = Reader.ReadUShort(); + var playerPeerId = Reader.ReadUShort(); if (!room.Entities.TryGetValue(entityId, out var entity)) { @@ -29,7 +51,7 @@ public sealed class EntityOwnershipOperation : IRagonOperation if (!room.Players.TryGetValue(playerPeerId, out var nextOwner)) { - _logger.Error($"Player not found with id {entityId}"); + _logger.Error($"Player not found with id {playerPeerId}"); return; } @@ -40,13 +62,13 @@ public sealed class EntityOwnershipOperation : IRagonOperation _logger.Trace($"Entity {entity.Id} next owner {nextOwner.Connection.Id}"); - writer.Clear(); - writer.WriteOperation(RagonOperation.OWNERSHIP_ENTITY_CHANGED); - writer.WriteUShort(playerPeerId); - writer.WriteUShort(1); - writer.WriteUShort(entity.Id); + Writer.Clear(); + Writer.WriteOperation(RagonOperation.OWNERSHIP_ENTITY_CHANGED); + Writer.WriteUShort(playerPeerId); + Writer.WriteUShort(1); + Writer.WriteUShort(entity.Id); - var sendData = writer.ToArray(); + var sendData = Writer.ToArray(); foreach (var player in room.PlayerList) player.Connection.Reliable.Send(sendData); } diff --git a/Ragon.Server/Sources/Handler/EntityRemoveOperation.cs b/Ragon.Server/Sources/Handler/EntityRemoveOperation.cs index e3ec76a..9a45b4b 100644 --- a/Ragon.Server/Sources/Handler/EntityRemoveOperation.cs +++ b/Ragon.Server/Sources/Handler/EntityRemoveOperation.cs @@ -17,23 +17,28 @@ using NLog; using Ragon.Protocol; using Ragon.Server.Entity; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public sealed class EntityDestroyOperation: IRagonOperation +public sealed class EntityDestroyOperation: BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public EntityDestroyOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } + + public override void Handle(RagonContext context, NetworkChannel channel) { var player = context.RoomPlayer; var room = context.Room; - var entityId = reader.ReadUShort(); + var entityId = Reader.ReadUShort(); - if (room.Entities.TryGetValue(entityId, out var entity)) + if (room.Entities.TryGetValue(entityId, out var entity) && entity.Owner.Connection.Id == player.Connection.Id) { var payload = new RagonPayload(); - payload.Read(reader); + payload.Read(Reader); room.DetachEntity(entity); player.DetachEntity(entity); @@ -42,5 +47,9 @@ public sealed class EntityDestroyOperation: IRagonOperation _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} destoyed entity {entity.Id}"); } + else + { + _logger.Trace($"Entity {entity.Id} not found or Player {context.Connection.Id}|{context.LobbyPlayer.Name} have not authority"); + } } } \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/EntityStateOperation.cs b/Ragon.Server/Sources/Handler/EntityStateOperation.cs index 639ebde..c5fcca3 100644 --- a/Ragon.Server/Sources/Handler/EntityStateOperation.cs +++ b/Ragon.Server/Sources/Handler/EntityStateOperation.cs @@ -16,23 +16,28 @@ using NLog; using Ragon.Protocol; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public sealed class EntityStateOperation: IRagonOperation +public sealed class EntityStateOperation: BaseOperation { private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); - - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + + public EntityStateOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } + + public override void Handle(RagonContext context, NetworkChannel channel) { var room = context.Room; var player = context.RoomPlayer; - var entitiesCount = reader.ReadUShort(); + var entitiesCount = Reader.ReadUShort(); for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++) { - var entityId = reader.ReadUShort(); - if (room.Entities.TryGetValue(entityId, out var entity) && entity.TryReadState(player, reader)) + var entityId = Reader.ReadUShort(); + if (room.Entities.TryGetValue(entityId, out var entity) && entity.TryReadState(player, Reader)) { room.Track(entity); } diff --git a/Ragon.Server/Sources/Handler/RoomCreateOperation.cs b/Ragon.Server/Sources/Handler/RoomCreateOperation.cs index c36c18b..a2b5b10 100644 --- a/Ragon.Server/Sources/Handler/RoomCreateOperation.cs +++ b/Ragon.Server/Sources/Handler/RoomCreateOperation.cs @@ -16,6 +16,7 @@ using NLog; using Ragon.Protocol; +using Ragon.Server.IO; using Ragon.Server.Lobby; using Ragon.Server.Plugin; using Ragon.Server.Plugin.Web; @@ -23,20 +24,20 @@ using Ragon.Server.Room; namespace Ragon.Server.Handler; -public sealed class RoomCreateOperation: IRagonOperation +public sealed class RoomCreateOperation : BaseOperation { private readonly RagonRoomParameters _roomParameters = new(); private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly IServerPlugin _serverPlugin; private readonly RagonWebHookPlugin _ragonWebHookPlugin; - - public RoomCreateOperation(IServerPlugin serverPlugin, RagonWebHookPlugin ragonWebHook) + + public RoomCreateOperation(RagonBuffer reader, RagonBuffer writer, IServerPlugin serverPlugin, RagonWebHookPlugin ragonWebHook) : base(reader, writer) { _serverPlugin = serverPlugin; _ragonWebHookPlugin = ragonWebHook; } - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public override void Handle(RagonContext context, NetworkChannel channel) { if (context.ConnectionStatus == ConnectionStatus.Unauthorized) { @@ -44,56 +45,56 @@ public sealed class RoomCreateOperation: IRagonOperation return; } - var custom = reader.ReadBool(); + var custom = Reader.ReadBool(); var roomId = Guid.NewGuid().ToString(); - + if (custom) { - roomId = reader.ReadString(); + roomId = Reader.ReadString(); if (context.Lobby.FindRoomById(roomId, out _)) - { - writer.Clear(); - writer.WriteOperation(RagonOperation.JOIN_FAILED); - writer.WriteString($"Room with id {roomId} already exists"); - - var sendData = writer.ToArray(); + { + Writer.Clear(); + Writer.WriteOperation(RagonOperation.JOIN_FAILED); + Writer.WriteString($"Room with id {roomId} already exists"); + + var sendData = Writer.ToArray(); context.Connection.Reliable.Send(sendData); - + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} join failed to room {roomId}, room already exist"); - return; + return; } } - - _roomParameters.Deserialize(reader); - + + _roomParameters.Deserialize(Reader); + var information = new RoomInformation() { - Scene = _roomParameters.Scene, - Max = _roomParameters.Max, - Min = _roomParameters.Min, + Scene = _roomParameters.Scene, + Max = _roomParameters.Max, + Min = _roomParameters.Min, }; var lobbyPlayer = context.LobbyPlayer; var roomPlayer = new RagonRoomPlayer(context.Connection, lobbyPlayer.Id, lobbyPlayer.Name); - + var roomPlugin = _serverPlugin.CreateRoomPlugin(information); var room = new RagonRoom(roomId, information, roomPlugin); - + roomPlayer.OnAttached(room); - + context.Scheduler.Run(room); context.Lobby.Persist(room); context.SetRoom(room, roomPlayer); - + _ragonWebHookPlugin.RoomCreated(context, room, roomPlayer); - + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} with scene {information.Scene}"); - - JoinSuccess(roomPlayer, room, writer); - + + JoinSuccess(roomPlayer, room, Writer); + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to room {room.Id}"); } - + private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer) { writer.Clear(); @@ -101,8 +102,8 @@ public sealed class RoomCreateOperation: IRagonOperation writer.WriteString(room.Id); writer.WriteString(player.Id); writer.WriteString(room.Owner.Id); - writer.WriteUShort((ushort) room.PlayerMin); - writer.WriteUShort((ushort) room.PlayerMax); + writer.WriteUShort((ushort)room.PlayerMin); + writer.WriteUShort((ushort)room.PlayerMax); writer.WriteString(room.Scene); var sendData = writer.ToArray(); diff --git a/Ragon.Server/Sources/Handler/RoomDataOperation.cs b/Ragon.Server/Sources/Handler/RoomDataOperation.cs new file mode 100644 index 0000000..ac42d18 --- /dev/null +++ b/Ragon.Server/Sources/Handler/RoomDataOperation.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NLog; +using Ragon.Protocol; +using Ragon.Server.IO; + +namespace Ragon.Server.Handler; + +public sealed class RoomDataOperation : BaseOperation +{ + public RoomDataOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } + + public override void Handle(RagonContext context, NetworkChannel channel) + { + var player = context.RoomPlayer; + var room = context.Room; + + var data = Reader.RawData; + + Writer.Clear(); + Writer.WriteOperation(RagonOperation.REPLICATE_RAW_DATA); + Writer.WriteUShort(player.Connection.Id); + + var playerData = Writer.ToArray(); + var payloadData = data; + var size = playerData.Length + payloadData.Length; + var sendData = new byte[size]; + + Array.Copy(playerData, 0, sendData, 0, playerData.Length); + Array.Copy(payloadData, 1, sendData, playerData.Length, payloadData.Length - 1); + + room.Broadcast(sendData, channel); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/RoomEventOperation.cs b/Ragon.Server/Sources/Handler/RoomEventOperation.cs new file mode 100644 index 0000000..a74a5c9 --- /dev/null +++ b/Ragon.Server/Sources/Handler/RoomEventOperation.cs @@ -0,0 +1,45 @@ +using Ragon.Protocol; +using Ragon.Server.Event; +using Ragon.Server.IO; + +namespace Ragon.Server.Handler; + +public class RoomEventOperation : BaseOperation +{ + public RoomEventOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } + + public override void Handle(RagonContext context, NetworkChannel channel) + { + var room = context.Room; + var player = context.RoomPlayer; + + var eventId = Reader.ReadUShort(); + var replicationMode = (RagonReplicationMode)Reader.ReadByte(); + var targetMode = (RagonTarget)Reader.ReadByte(); + var targetPlayerPeerId = (ushort)0; + + if (targetMode == RagonTarget.Player) + targetPlayerPeerId = Reader.ReadUShort(); + + var @event = new RagonEvent(player, eventId); + @event.Read(Reader); + + Writer.Clear(); + Writer.WriteUShort(eventId); + Writer.WriteUShort(player.Connection.Id); + Writer.WriteUShort((ushort) replicationMode); + + var sendData = Writer.ToArray(); + + if (targetMode == RagonTarget.Player && room.Players.TryGetValue(targetPlayerPeerId, out var targetPlayer)) + { + targetPlayer.Connection.Reliable.Send(sendData); + return; + } + + foreach (var roomPlayer in room.ReadyPlayersList) + roomPlayer.Connection.Reliable.Send(sendData); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/RoomJoinOperation.cs b/Ragon.Server/Sources/Handler/RoomJoinOperation.cs index dc1e1ec..7ae9166 100644 --- a/Ragon.Server/Sources/Handler/RoomJoinOperation.cs +++ b/Ragon.Server/Sources/Handler/RoomJoinOperation.cs @@ -16,46 +16,46 @@ using NLog; using Ragon.Protocol; -using Ragon.Server.Plugin; +using Ragon.Server.IO; using Ragon.Server.Plugin.Web; using Ragon.Server.Room; namespace Ragon.Server.Handler; -public sealed class RoomJoinOperation : IRagonOperation +public sealed class RoomJoinOperation : BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private readonly RagonWebHookPlugin _webHook; - - public RoomJoinOperation(RagonWebHookPlugin plugin) + + public RoomJoinOperation(RagonBuffer reader, RagonBuffer writer, RagonWebHookPlugin plugin) : base(reader, writer) { _webHook = plugin; } - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + + public override void Handle(RagonContext context, NetworkChannel channel) { - var roomId = reader.ReadString(); + var roomId = Reader.ReadString(); var lobbyPlayer = context.LobbyPlayer; if (!context.Lobby.FindRoomById(roomId, out var existsRoom)) { - JoinFailed(context, writer); - + JoinFailed(context, Writer); + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} failed to join room {roomId}"); return; } var player = new RagonRoomPlayer(context.Connection, lobbyPlayer.Id, lobbyPlayer.Name); context.SetRoom(existsRoom, player); - + if (!existsRoom.Plugin.OnPlayerJoined(player)) return; - + _webHook.RoomJoined(context, existsRoom, player); - - JoinSuccess(context, existsRoom, writer); - + + JoinSuccess(context, existsRoom, Writer); + _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to {existsRoom.Id}"); } @@ -66,8 +66,8 @@ public sealed class RoomJoinOperation : IRagonOperation writer.WriteString(room.Id); writer.WriteString(context.RoomPlayer.Id); writer.WriteString(room.Owner.Id); - writer.WriteUShort((ushort) room.PlayerMin); - writer.WriteUShort((ushort) room.PlayerMax); + writer.WriteUShort((ushort)room.PlayerMin); + writer.WriteUShort((ushort)room.PlayerMax); writer.WriteString(room.Scene); var sendData = writer.ToArray(); diff --git a/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs b/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs index 95226aa..c19938b 100644 --- a/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs +++ b/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs @@ -16,6 +16,7 @@ using NLog; using Ragon.Protocol; +using Ragon.Server.IO; using Ragon.Server.Lobby; using Ragon.Server.Plugin; using Ragon.Server.Plugin.Web; @@ -23,20 +24,20 @@ using Ragon.Server.Room; namespace Ragon.Server.Handler; -public sealed class RoomJoinOrCreateOperation : IRagonOperation +public sealed class RoomJoinOrCreateOperation : BaseOperation { private readonly RagonRoomParameters _roomParameters = new(); private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly IServerPlugin _serverPlugin; private readonly RagonWebHookPlugin _ragonWebHookPlugin; - public RoomJoinOrCreateOperation(IServerPlugin serverPlugin, RagonWebHookPlugin plugin) + public RoomJoinOrCreateOperation(RagonBuffer reader, RagonBuffer writer, IServerPlugin serverPlugin, RagonWebHookPlugin plugin): base(reader, writer) { _serverPlugin = serverPlugin; _ragonWebHookPlugin = plugin; } - - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + + public override void Handle(RagonContext context, NetworkChannel channel) { if (context.ConnectionStatus == ConnectionStatus.Unauthorized) { @@ -47,7 +48,7 @@ public sealed class RoomJoinOrCreateOperation : IRagonOperation var roomId = Guid.NewGuid().ToString(); var lobbyPlayer = context.LobbyPlayer; - _roomParameters.Deserialize(reader); + _roomParameters.Deserialize(Reader); if (context.Lobby.FindRoomByScene(_roomParameters.Scene, out var existsRoom)) { @@ -56,7 +57,7 @@ public sealed class RoomJoinOrCreateOperation : IRagonOperation _ragonWebHookPlugin.RoomJoined(context, existsRoom, player); - JoinSuccess(player, existsRoom, writer); + JoinSuccess(player, existsRoom, Writer); } else { @@ -79,7 +80,7 @@ public sealed class RoomJoinOrCreateOperation : IRagonOperation _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} with scene {information.Scene}"); - JoinSuccess(roomPlayer, room, writer); + JoinSuccess(roomPlayer, room, Writer); } } diff --git a/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs b/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs index 0acb494..3010264 100644 --- a/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs +++ b/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs @@ -16,21 +16,23 @@ using NLog; using Ragon.Protocol; +using Ragon.Server.IO; using Ragon.Server.Plugin; using Ragon.Server.Plugin.Web; namespace Ragon.Server.Handler; -public sealed class RoomLeaveOperation: IRagonOperation +public sealed class RoomLeaveOperation: BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly RagonWebHookPlugin _webHook; - public RoomLeaveOperation(RagonWebHookPlugin plugin) + + public RoomLeaveOperation(RagonBuffer reader, RagonBuffer writer, RagonWebHookPlugin plugin): base(reader, writer) { _webHook = plugin; } - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + public override void Handle(RagonContext context, NetworkChannel channel) { var room = context.Room; var roomPlayer = context.RoomPlayer; diff --git a/Ragon.Server/Sources/Handler/RoomOwnershipOperation.cs b/Ragon.Server/Sources/Handler/RoomOwnershipOperation.cs index 5ef9fa2..32e9a2c 100644 --- a/Ragon.Server/Sources/Handler/RoomOwnershipOperation.cs +++ b/Ragon.Server/Sources/Handler/RoomOwnershipOperation.cs @@ -1,14 +1,35 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + using NLog; using Ragon.Protocol; -using Ragon.Server.Entity; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public sealed class RoomOwnershipOperation : IRagonOperation +public sealed class RoomOwnershipOperation : BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + + public RoomOwnershipOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) { - + } + + public override void Handle(RagonContext context, NetworkChannel channel) + { + } } \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/SceneLoadOperation.cs b/Ragon.Server/Sources/Handler/SceneLoadOperation.cs index 5fee7e6..a8f3498 100644 --- a/Ragon.Server/Sources/Handler/SceneLoadOperation.cs +++ b/Ragon.Server/Sources/Handler/SceneLoadOperation.cs @@ -14,22 +14,24 @@ * limitations under the License. */ - using NLog; using Ragon.Protocol; +using Ragon.Server.IO; namespace Ragon.Server.Handler; -public class SceneLoadOperation: IRagonOperation +public class SceneLoadOperation: BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + + public SceneLoadOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) {} + + public override void Handle(RagonContext context, NetworkChannel channel) { var roomOwner = context.Room.Owner; var currentPlayer = context.RoomPlayer; var room = context.Room; - var sceneName = reader.ReadString(); + var sceneName = Reader.ReadString(); if (roomOwner.Connection.Id != currentPlayer.Connection.Id) { @@ -39,11 +41,11 @@ public class SceneLoadOperation: IRagonOperation room.UpdateMap(sceneName); - writer.Clear(); - writer.WriteOperation(RagonOperation.LOAD_SCENE); - writer.WriteString(sceneName); + Writer.Clear(); + Writer.WriteOperation(RagonOperation.LOAD_SCENE); + Writer.WriteString(sceneName); - var sendData = writer.ToArray(); + var sendData = Writer.ToArray(); foreach (var player in room.PlayerList) player.Connection.Reliable.Send(sendData); } diff --git a/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs index 865f6bc..dc0edc9 100644 --- a/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs +++ b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs @@ -17,21 +17,22 @@ using NLog; using Ragon.Protocol; using Ragon.Server.Entity; +using Ragon.Server.IO; using Ragon.Server.Lobby; using Ragon.Server.Room; namespace Ragon.Server.Handler; -public sealed class SceneLoadedOperation : IRagonOperation +public sealed class SceneLoadedOperation : BaseOperation { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public SceneLoadedOperation() + public SceneLoadedOperation(RagonBuffer reader, RagonBuffer writer): base(reader, writer) { } - - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer) + + public override void Handle(RagonContext context, NetworkChannel channel) { if (context.ConnectionStatus == ConnectionStatus.Unauthorized) return; @@ -39,16 +40,21 @@ public sealed class SceneLoadedOperation : IRagonOperation var owner = context.Room.Owner; var player = context.RoomPlayer; var room = context.Room; + if (player.IsLoaded) + { + _logger.Warn($"Player {player.Name}:{player.Connection.Id} already ready"); + return; + } if (player == owner) { - var statics = reader.ReadUShort(); + var statics = Reader.ReadUShort(); for (var staticIndex = 0; staticIndex < statics; staticIndex++) { - var entityType = reader.ReadUShort(); - var eventAuthority = (RagonAuthority)reader.ReadByte(); - var staticId = reader.ReadUShort(); - var propertiesCount = reader.ReadUShort(); + var entityType = Reader.ReadUShort(); + var eventAuthority = (RagonAuthority)Reader.ReadByte(); + var staticId = Reader.ReadUShort(); + var propertiesCount = Reader.ReadUShort(); var entityParameters = new RagonEntityParameters() { @@ -62,8 +68,8 @@ public sealed class SceneLoadedOperation : IRagonOperation var entity = new RagonEntity(entityParameters); for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++) { - var propertyType = reader.ReadBool(); - var propertySize = reader.ReadUShort(); + var propertyType = Reader.ReadBool(); + var propertySize = Reader.ReadUShort(); entity.AddProperty(new RagonProperty(propertySize, propertyType)); } @@ -86,14 +92,14 @@ public sealed class SceneLoadedOperation : IRagonOperation foreach (var roomPlayer in room.WaitPlayersList) { - DispatchPlayerJoinExcludePlayer(room, roomPlayer, writer); + DispatchPlayerJoinExcludePlayer(room, roomPlayer, Writer); roomPlayer.SetReady(); } room.UpdateReadyPlayerList(); - DispatchSnapshot(room, room.WaitPlayersList, writer); + DispatchSnapshot(room, room.WaitPlayersList, Writer); room.WaitPlayersList.Clear(); } @@ -101,14 +107,14 @@ public sealed class SceneLoadedOperation : IRagonOperation { player.SetReady(); - DispatchPlayerJoinExcludePlayer(room, player, writer); + DispatchPlayerJoinExcludePlayer(room, player, Writer); room.UpdateReadyPlayerList(); - DispatchSnapshot(room, new List() { player }, writer); + DispatchSnapshot(room, new List() { player }, Writer); foreach (var entity in room.EntityList) - entity.RestoreBufferedEvents(player, writer); + entity.RestoreBufferedEvents(player, Writer); } else { diff --git a/Ragon.Server/Sources/Handler/TimestampSyncOperation.cs b/Ragon.Server/Sources/Handler/TimestampSyncOperation.cs new file mode 100644 index 0000000..1d488b8 --- /dev/null +++ b/Ragon.Server/Sources/Handler/TimestampSyncOperation.cs @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Eduard Kargin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ragon.Protocol; +using Ragon.Server.IO; + +namespace Ragon.Server.Handler; + +public class TimestampSyncOperation: BaseOperation +{ + public TimestampSyncOperation(RagonBuffer reader, RagonBuffer writer) : base(reader, writer) + { + } + + public override void Handle(RagonContext context, NetworkChannel channel) + { + var timestamp0 = Reader.Read(32); + var timestamp1 = Reader.Read(32); + var value = new DoubleToUInt() { Int0 = timestamp0, Int1 = timestamp1 }; + + context.RoomPlayer?.SetTimestamp(value.Double); + } +} \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkChannel.cs b/Ragon.Server/Sources/IO/INetworkChannel.cs index f2c8074..67b0c2c 100644 --- a/Ragon.Server/Sources/IO/INetworkChannel.cs +++ b/Ragon.Server/Sources/IO/INetworkChannel.cs @@ -14,9 +14,12 @@ * limitations under the License. */ +using Ragon.Protocol; + namespace Ragon.Server.IO; public interface INetworkChannel { void Send(byte[] data); + void Send(RagonBuffer buffer); } \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkListener.cs b/Ragon.Server/Sources/IO/INetworkListener.cs index ff47db9..a498567 100644 --- a/Ragon.Server/Sources/IO/INetworkListener.cs +++ b/Ragon.Server/Sources/IO/INetworkListener.cs @@ -21,5 +21,5 @@ public interface INetworkListener void OnConnected(INetworkConnection connection); void OnDisconnected(INetworkConnection connection); void OnTimeout(INetworkConnection connection); - void OnData(INetworkConnection connection, byte[] data); + void OnData(INetworkConnection connection, NetworkChannel channel, byte[] data); } \ No newline at end of file diff --git a/Ragon.Server/Sources/IO/INetworkServer.cs b/Ragon.Server/Sources/IO/INetworkServer.cs index 0108e02..f1f6054 100644 --- a/Ragon.Server/Sources/IO/INetworkServer.cs +++ b/Ragon.Server/Sources/IO/INetworkServer.cs @@ -21,5 +21,6 @@ public interface INetworkServer public Executor Executor { get; } public void Stop(); public void Update(); + public void Broadcast(byte[] data, NetworkChannel channel); public void Start(INetworkListener listener, NetworkConfiguration configuration); } \ No newline at end of file diff --git a/Ragon.Server/Sources/Handler/IRagonOperation.cs b/Ragon.Server/Sources/IO/NetworkChannel.cs similarity index 78% rename from Ragon.Server/Sources/Handler/IRagonOperation.cs rename to Ragon.Server/Sources/IO/NetworkChannel.cs index 3db5c0a..36bf39a 100644 --- a/Ragon.Server/Sources/Handler/IRagonOperation.cs +++ b/Ragon.Server/Sources/IO/NetworkChannel.cs @@ -14,11 +14,11 @@ * limitations under the License. */ -using Ragon.Protocol; -namespace Ragon.Server.Handler; +namespace Ragon.Server.IO; -public interface IRagonOperation +public enum NetworkChannel { - public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer); + RELIABLE = 0, + UNRELIABLE = 1, } \ No newline at end of file diff --git a/Ragon.Server/Sources/RagonServer.cs b/Ragon.Server/Sources/RagonServer.cs index f3e40b4..164cd47 100644 --- a/Ragon.Server/Sources/RagonServer.cs +++ b/Ragon.Server/Sources/RagonServer.cs @@ -31,7 +31,7 @@ public class RagonServer : IRagonServer, INetworkListener { private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly INetworkServer _server; - private readonly IRagonOperation[] _handlers; + private readonly BaseOperation[] _handlers; private readonly IRagonLobby _lobby; private readonly IServerPlugin _serverPlugin; private readonly Thread _dedicatedThread; @@ -46,7 +46,7 @@ public class RagonServer : IRagonServer, INetworkListener private readonly Dictionary _contextsByPlayerId; private readonly Stopwatch _timer; private readonly long _tickRate = 0; - + public RagonServer( INetworkServer server, IServerPlugin plugin, @@ -68,26 +68,29 @@ public class RagonServer : IRagonServer, INetworkListener _writer = new RagonBuffer(); _tickRate = 1000 / _configuration.ServerTickRate; _timer = new Stopwatch(); - + var contextObserver = new RagonContextObserver(_contextsByPlayerId); - + _serverPlugin.OnAttached(this); - - _handlers = new IRagonOperation[byte.MaxValue]; - _handlers[(byte) RagonOperation.AUTHORIZE] = new AuthorizationOperation(_webhooks, contextObserver, _writer); - _handlers[(byte) RagonOperation.JOIN_OR_CREATE_ROOM] = new RoomJoinOrCreateOperation(plugin, _webhooks); - _handlers[(byte) RagonOperation.CREATE_ROOM] = new RoomCreateOperation(plugin, _webhooks); - _handlers[(byte) RagonOperation.JOIN_ROOM] = new RoomJoinOperation(_webhooks); - _handlers[(byte) RagonOperation.LEAVE_ROOM] = new RoomLeaveOperation(_webhooks); - _handlers[(byte) RagonOperation.LOAD_SCENE] = new SceneLoadOperation(); - _handlers[(byte) RagonOperation.SCENE_LOADED] = new SceneLoadedOperation(); - _handlers[(byte) RagonOperation.CREATE_ENTITY] = new EntityCreateOperation(); - _handlers[(byte) RagonOperation.REMOVE_ENTITY] = new EntityDestroyOperation(); - _handlers[(byte) RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventOperation(); - _handlers[(byte) RagonOperation.REPLICATE_ENTITY_STATE] = new EntityStateOperation(); - _handlers[(byte) RagonOperation.TRANSFER_ROOM_OWNERSHIP] = new EntityOwnershipOperation(); - _handlers[(byte) RagonOperation.TRANSFER_ENTITY_OWNERSHIP] = new EntityOwnershipOperation(); - + + _handlers = new BaseOperation[byte.MaxValue]; + _handlers[(byte)RagonOperation.AUTHORIZE] = new AuthorizationOperation(_reader, _writer, _webhooks, contextObserver); + _handlers[(byte)RagonOperation.JOIN_OR_CREATE_ROOM] = new RoomJoinOrCreateOperation(_reader, _writer, plugin, _webhooks); + _handlers[(byte)RagonOperation.CREATE_ROOM] = new RoomCreateOperation(_reader, _writer, plugin, _webhooks); + _handlers[(byte)RagonOperation.JOIN_ROOM] = new RoomJoinOperation(_reader, _writer, _webhooks); + _handlers[(byte)RagonOperation.LEAVE_ROOM] = new RoomLeaveOperation(_reader, _writer, _webhooks); + _handlers[(byte)RagonOperation.LOAD_SCENE] = new SceneLoadOperation(_reader, _writer); + _handlers[(byte)RagonOperation.SCENE_LOADED] = new SceneLoadedOperation(_reader, _writer); + _handlers[(byte)RagonOperation.CREATE_ENTITY] = new EntityCreateOperation(_reader, _writer); + _handlers[(byte)RagonOperation.REMOVE_ENTITY] = new EntityDestroyOperation(_reader, _writer); + _handlers[(byte)RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventOperation(_reader, _writer); + _handlers[(byte)RagonOperation.REPLICATE_ENTITY_STATE] = new EntityStateOperation(_reader, _writer); + _handlers[(byte)RagonOperation.TRANSFER_ROOM_OWNERSHIP] = new EntityOwnershipOperation(_reader, _writer); + _handlers[(byte)RagonOperation.TRANSFER_ENTITY_OWNERSHIP] = new EntityOwnershipOperation(_reader, _writer); + _handlers[(byte)RagonOperation.TIMESTAMP_SYNCHRONIZATION] = new TimestampSyncOperation(_reader, _writer); + _handlers[(byte)RagonOperation.REPLICATE_ROOM_EVENT] = new RoomEventOperation(_reader, _writer); + _handlers[(byte)RagonOperation.REPLICATE_RAW_DATA] = new RoomDataOperation(_reader, _writer); + _logger.Trace($"Server Tick Rate: {_configuration.ServerTickRate}"); } @@ -98,10 +101,12 @@ public class RagonServer : IRagonServer, INetworkListener { if (_timer.ElapsedMilliseconds > _tickRate) { - _scheduler.Update(_timer.ElapsedMilliseconds / 1000.0f); _timer.Restart(); + _scheduler.Update(_timer.ElapsedMilliseconds / 1000.0f); + + SendTimestamp(); } - + _executor.Update(); _server.Update(); Thread.Sleep(1); @@ -117,13 +122,13 @@ public class RagonServer : IRagonServer, INetworkListener Address = "0.0.0.0", Port = _configuration.Port, }; - + _httpServer.Start(_configuration); _server.Start(this, networkConfiguration); if (executeInDedicatedThread) _dedicatedThread.Start(); - else + else Execute(); } @@ -137,7 +142,7 @@ public class RagonServer : IRagonServer, INetworkListener public void OnConnected(INetworkConnection connection) { var context = new RagonContext(connection, _configuration, _executor, _lobby, _scheduler); - + _logger.Trace($"Connected: {connection.Id}"); _contextsByConnection.Add(connection.Id, context); } @@ -150,21 +155,21 @@ public class RagonServer : IRagonServer, INetworkListener if (room != null) { room.DetachPlayer(context.RoomPlayer); - if (_lobby.RemoveIfEmpty(room)) + if (_lobby.RemoveIfEmpty(room)) _webhooks.RoomRemoved(context, room); } - + _logger.Trace($"Disconnected: {connection.Id}"); } else { - _logger.Trace($"Disconnected: {connection.Id}"); + _logger.Trace($"Disconnected without context: {connection.Id}"); } } public void OnTimeout(INetworkConnection connection) { - if (_contextsByConnection.Remove(connection.Id, out var context)) + if (_contextsByConnection.Remove(connection.Id, out var context) && context.ConnectionStatus == ConnectionStatus.Authorized) { var room = context.Room; if (room != null) @@ -172,7 +177,7 @@ public class RagonServer : IRagonServer, INetworkListener room.DetachPlayer(context.RoomPlayer); _lobby.RemoveIfEmpty(room); } - + _logger.Trace($"Timeout: {connection.Id}|{context.LobbyPlayer.Name}|{context.LobbyPlayer.Id}"); } else @@ -181,7 +186,7 @@ public class RagonServer : IRagonServer, INetworkListener } } - public void OnData(INetworkConnection connection, byte[] data) + public void OnData(INetworkConnection connection, NetworkChannel channel, byte[] data) { try { @@ -192,7 +197,7 @@ public class RagonServer : IRagonServer, INetworkListener _reader.FromArray(data); var operation = _reader.ReadByte(); - _handlers[operation].Handle(context, _reader, _writer); + _handlers[operation].Handle(context, channel); } } catch (Exception ex) @@ -201,22 +206,35 @@ public class RagonServer : IRagonServer, INetworkListener } } - public IRagonOperation ResolveOperation(RagonOperation operation) + public void SendTimestamp() + { + var timestamp = RagonTime.CurrentTimestamp(); + var value = new DoubleToUInt + { + Double = timestamp, + }; + + _writer.Clear(); + _writer.WriteOperation(RagonOperation.TIMESTAMP_SYNCHRONIZATION); + _writer.Write(value.Int0, 32); + _writer.Write(value.Int1, 32); + + var sendData = _writer.ToArray(); + _server.Broadcast(sendData, NetworkChannel.UNRELIABLE); + } + + public BaseOperation ResolveOperation(RagonOperation operation) { return _handlers[(byte)operation]; } public RagonLobbyPlayer? GetPlayerByConnection(INetworkConnection connection) { - return _contextsByConnection.TryGetValue(connection.Id, out var context) ? - context.LobbyPlayer : - null; + return _contextsByConnection.TryGetValue(connection.Id, out var context) ? context.LobbyPlayer : null; } public RagonLobbyPlayer? GetPlayerById(string playerId) { - return _contextsByPlayerId.TryGetValue(playerId, out var context) ? - context.LobbyPlayer : - null; + return _contextsByPlayerId.TryGetValue(playerId, out var context) ? context.LobbyPlayer : null; } } \ No newline at end of file diff --git a/Ragon.Server/Sources/Room/RagonRoom.cs b/Ragon.Server/Sources/Room/RagonRoom.cs index a76b1eb..8edfb2c 100644 --- a/Ragon.Server/Sources/Room/RagonRoom.cs +++ b/Ragon.Server/Sources/Room/RagonRoom.cs @@ -200,10 +200,18 @@ public class RagonRoom : IRagonRoom, IRagonAction _entitiesDirtySet.Add(entity); } - public void Broadcast(byte[] data) + public void Broadcast(byte[] data, NetworkChannel channel = NetworkChannel.RELIABLE) { - foreach (var readyPlayer in ReadyPlayersList) - readyPlayer.Connection.Reliable.Send(data); + if (channel == NetworkChannel.RELIABLE) + { + foreach (var readyPlayer in ReadyPlayersList) + readyPlayer.Connection.Reliable.Send(data); + } + else + { + foreach (var readyPlayer in ReadyPlayersList) + readyPlayer.Connection.Unreliable.Send(data); + } } public RagonRoomPlayer GetPlayerByConnection(INetworkConnection connection) diff --git a/Ragon.Server/Sources/Room/RagonRoomPlayer.cs b/Ragon.Server/Sources/Room/RagonRoomPlayer.cs index 5cc2c32..b40637a 100644 --- a/Ragon.Server/Sources/Room/RagonRoomPlayer.cs +++ b/Ragon.Server/Sources/Room/RagonRoomPlayer.cs @@ -25,6 +25,7 @@ public class RagonRoomPlayer public string Id { get; } public string Name { get; } public bool IsLoaded { get; private set; } + public double Timestamp { get; private set; } public RagonRoom Room { get; private set; } public RagonEntityCache Entities { get; private set; } @@ -65,4 +66,9 @@ public class RagonRoomPlayer { IsLoaded = false; } + + internal void SetTimestamp(double time) + { + Timestamp = time; + } } \ No newline at end of file diff --git a/Ragon.Client.Simulation/Program.cs b/Ragon.Simulation/Program.cs similarity index 100% rename from Ragon.Client.Simulation/Program.cs rename to Ragon.Simulation/Program.cs diff --git a/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj b/Ragon.Simulation/Ragon.Simulation.csproj similarity index 78% rename from Ragon.Client.Simulation/Ragon.Client.Simulation.csproj rename to Ragon.Simulation/Ragon.Simulation.csproj index 890cd74..cad77a6 100644 --- a/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj +++ b/Ragon.Simulation/Ragon.Simulation.csproj @@ -6,6 +6,7 @@ enable enable Linux + Ragon.Client.Simulation @@ -15,6 +16,7 @@ + @@ -23,4 +25,8 @@ + + + + diff --git a/Ragon.Simulation/Sources/Client/Client.cs b/Ragon.Simulation/Sources/Client/Client.cs new file mode 100644 index 0000000..3180853 --- /dev/null +++ b/Ragon.Simulation/Sources/Client/Client.cs @@ -0,0 +1,143 @@ +using System.Numerics; +using Raylib_cs; +using static Raylib_cs.Raylib; + +namespace Ragon.Simulation; + +public class Client +{ + public void Start() + { + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [models] example - first person maze"); + + // Define the camera to look into our 3d world + Camera3D camera = new(); + camera.Position = new Vector3(0.2f, 0.4f, 0.2f); + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 45.0f; + camera.Projection = CameraProjection.CAMERA_PERSPECTIVE; + + Image imMap = LoadImage("resources/cubicmap.png"); + Texture2D cubicmap = LoadTextureFromImage(imMap); + Mesh mesh = GenMeshCubicmap(imMap, new Vector3(1.0f, 1.0f, 1.0f)); + Model model = LoadModelFromMesh(mesh); + + // NOTE: By default each cube is mapped to one part of texture atlas + Texture2D texture = LoadTexture("resources/cubicmap_atlas.png"); + + // Set map diffuse texture + Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.MATERIAL_MAP_ALBEDO, ref texture); + + // Get map image data to be used for collision detection + Color* mapPixels = LoadImageColors(imMap); + UnloadImage(imMap); + + Vector3 mapPosition = new(-16.0f, 0.0f, -8.0f); + Vector3 playerPosition = camera.Position; + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) + { + // Update + //---------------------------------------------------------------------------------- + Vector3 oldCamPos = camera.Position; + + UpdateCamera(ref camera, CameraMode.CAMERA_FIRST_PERSON); + + // Check player collision (we simplify to 2D collision detection) + Vector2 playerPos = new(camera.Position.X, camera.Position.Z); + + // Collision radius (player is modelled as a cilinder for collision) + float playerRadius = 0.1f; + + int playerCellX = (int)(playerPos.X - mapPosition.X + 0.5f); + int playerCellY = (int)(playerPos.Y - mapPosition.Z + 0.5f); + + // Out-of-limits security check + if (playerCellX < 0) + { + playerCellX = 0; + } + else if (playerCellX >= cubicmap.Width) + { + playerCellX = cubicmap.Width - 1; + } + + if (playerCellY < 0) + { + playerCellY = 0; + } + else if (playerCellY >= cubicmap.Height) + { + playerCellY = cubicmap.Height - 1; + } + + // Check map collisions using image data and player position + // TODO: Improvement: Just check player surrounding cells for collision + for (int y = 0; y < cubicmap.Height; y++) + { + for (int x = 0; x < cubicmap.Width; x++) + { + Color* mapPixelsData = mapPixels; + + // Collision: Color.white pixel, only check R channel + Rectangle rec = new( + mapPosition.X - 0.5f + x * 1.0f, + mapPosition.Z - 0.5f + y * 1.0f, + 1.0f, + 1.0f + ); + + bool collision = CheckCollisionCircleRec(playerPos, playerRadius, rec); + if ((mapPixelsData[y * cubicmap.Width + x].R == 255) && collision) + { + // Collision detected, reset camera position + camera.Position = oldCamPos; + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RAYWHITE); + + // Draw maze map + BeginMode3D(camera); + DrawModel(model, mapPosition, 1.0f, Color.WHITE); + EndMode3D(); + + DrawTextureEx(cubicmap, new Vector2(GetScreenWidth() - cubicmap.Width * 4 - 20, 20), 0.0f, 4.0f, Color.WHITE); + DrawRectangleLines(GetScreenWidth() - cubicmap.Width * 4 - 20, 20, cubicmap.Width * 4, cubicmap.Height * 4, Color.GREEN); + + // Draw player position radar + DrawRectangle(GetScreenWidth() - cubicmap.Width * 4 - 20 + playerCellX * 4, 20 + playerCellY * 4, 4, 4, Color.RED); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadImageColors(mapPixels); + + UnloadTexture(cubicmap); + UnloadTexture(texture); + UnloadModel(model); + + CloseWindow(); + //-------------------------------------------------------------------------------------- + } +} \ No newline at end of file diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs b/Ragon.Simulation/Sources/Client/IO/RagonENetConnection.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs rename to Ragon.Simulation/Sources/Client/IO/RagonENetConnection.cs diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs b/Ragon.Simulation/Sources/Client/IO/RagonENetReliableChannel.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs rename to Ragon.Simulation/Sources/Client/IO/RagonENetReliableChannel.cs diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs b/Ragon.Simulation/Sources/Client/IO/RagonENetUnreliableChannel.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs rename to Ragon.Simulation/Sources/Client/IO/RagonENetUnreliableChannel.cs diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs b/Ragon.Simulation/Sources/Client/IO/RagonNullConnection.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs rename to Ragon.Simulation/Sources/Client/IO/RagonNullConnection.cs diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs b/Ragon.Simulation/Sources/Client/IO/RagonNullReliableChannel.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs rename to Ragon.Simulation/Sources/Client/IO/RagonNullReliableChannel.cs diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs b/Ragon.Simulation/Sources/Client/IO/RagonNullUnreliableChannel.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs rename to Ragon.Simulation/Sources/Client/IO/RagonNullUnreliableChannel.cs diff --git a/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs b/Ragon.Simulation/Sources/Client/Player/PlayerPayload.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs rename to Ragon.Simulation/Sources/Client/Player/PlayerPayload.cs diff --git a/Ragon.Client.Simulation/Sources/Game.cs b/Ragon.Simulation/Sources/Game.cs similarity index 100% rename from Ragon.Client.Simulation/Sources/Game.cs rename to Ragon.Simulation/Sources/Game.cs diff --git a/Ragon.Client.Simulation/Sources/Simulation.cs b/Ragon.Simulation/Sources/Simulation.cs similarity index 89% rename from Ragon.Client.Simulation/Sources/Simulation.cs rename to Ragon.Simulation/Sources/Simulation.cs index db8d223..5d46619 100644 --- a/Ragon.Client.Simulation/Sources/Simulation.cs +++ b/Ragon.Simulation/Sources/Simulation.cs @@ -1,4 +1,7 @@ -namespace Ragon.Client.Simulation; +using Ragon.Client; +using Ragon.Client.Simulation; + +namespace Ragon.Simulation; public class EntityListener : IRagonEntityListener { @@ -24,6 +27,9 @@ public class Simulation { public void Start() { + var client = new Ragon.Simulation.Client(); + client.Start(); + // INetworkConnection protocol = debug ? new RagonNullConnection() : new RagonENetConnection(); // var network = new RagonClient(protocol, new EntityListener(), 30); // var game = new Game(network); diff --git a/Ragon.sln b/Ragon.sln index 80980c2..667a51d 100644 --- a/Ragon.sln +++ b/Ragon.sln @@ -8,11 +8,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server", "Ragon.Serve EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.WebSocketServer", "Ragon.Server.WebSocketServer\Ragon.Server.WebSocketServer.csproj", "{81050343-A9B8-487B-86C8-7A5B7DD9C39B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.ENet", "Ragon.Server.ENet\Ragon.Server.ENet.csproj", "{DD79AC4F-9E5C-4938-850E-805D537E68D0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.ENetServer", "Ragon.Server.ENetServer\Ragon.Server.ENetServer.csproj", "{DD79AC4F-9E5C-4938-850E-805D537E68D0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Client", "Ragon.Client\Ragon.Client.csproj", "{C82D65BF-6D80-4263-ADFE-CB9ED990B6C3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Client.Simulation", "Ragon.Client.Simulation\Ragon.Client.Simulation.csproj", "{0384848D-3B63-4B3A-B15F-A836EBB3E95D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Simulation", "Ragon.Simulation\Ragon.Simulation.csproj", "{0384848D-3B63-4B3A-B15F-A836EBB3E95D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Client.Property", "Ragon.Client.Property\Ragon.Client.Property.csproj", "{46A60DAB-F854-4BB6-A119-BD4C5B2B0D29}" EndProject