diff --git a/Ragon.Common/Sources/RagonOperation.cs b/Ragon.Common/Sources/RagonOperation.cs index 32e9b7a..5273e44 100644 --- a/Ragon.Common/Sources/RagonOperation.cs +++ b/Ragon.Common/Sources/RagonOperation.cs @@ -5,7 +5,6 @@ namespace Ragon.Common AUTHORIZE, AUTHORIZED_SUCCESS, AUTHORIZED_FAILED, - JOIN_OR_CREATE_ROOM, CREATE_ROOM, JOIN_ROOM, @@ -13,19 +12,14 @@ namespace Ragon.Common OWNERSHIP_CHANGED, JOIN_SUCCESS, JOIN_FAILED, - LOAD_SCENE, SCENE_LOADED, - PLAYER_JOINED, PLAYER_LEAVED, - CREATE_ENTITY, DESTROY_ENTITY, SNAPSHOT, - REPLICATE_ENTITY_STATE, REPLICATE_ENTITY_EVENT, - REPLICATE_EVENT, } } \ No newline at end of file diff --git a/Ragon.SimpleServer/Program.cs b/Ragon.SimpleServer/Program.cs index 36173e6..5c5d2a6 100755 --- a/Ragon.SimpleServer/Program.cs +++ b/Ragon.SimpleServer/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Game.Source; using Ragon.Core; diff --git a/Ragon.SimpleServer/Source/AuthorizationProvider.cs b/Ragon.SimpleServer/Source/AuthorizationProvider.cs index f263aef..6d6cfe6 100644 --- a/Ragon.SimpleServer/Source/AuthorizationProvider.cs +++ b/Ragon.SimpleServer/Source/AuthorizationProvider.cs @@ -4,10 +4,10 @@ using Ragon.Core; namespace Game.Source; -public class AuthorizationProviderByKey: IAuthorizationProvider +public class ApplicationHandlerByKey: IApplicationHandler { private Configuration _configuration; - public AuthorizationProviderByKey(Configuration configuration) + public ApplicationHandlerByKey(Configuration configuration) { _configuration = configuration; } @@ -26,4 +26,19 @@ public class AuthorizationProviderByKey: IAuthorizationProvider reject(0); } } + + public void OnCustomEvent(ushort peerId, ReadOnlySpan payload) + { + + } + + public void OnJoin(ushort peerId) + { + + } + + public void OnLeave(ushort peerId) + { + + } } \ No newline at end of file diff --git a/Ragon.SimpleServer/Source/Plugins/EmptyPlugin.cs b/Ragon.SimpleServer/Source/Plugins/EmptyPlugin.cs index 8c69455..681d602 100755 --- a/Ragon.SimpleServer/Source/Plugins/EmptyPlugin.cs +++ b/Ragon.SimpleServer/Source/Plugins/EmptyPlugin.cs @@ -26,13 +26,15 @@ namespace Game.Source // Logger.Info($"Player({player.PlayerName}) left from Room({Room.Id})"); } - public override void OnEntityCreated(Player player, Entity entity) + public override bool OnEntityCreated(Player player, Entity entity) { + return false; // Logger.Info($"Player({player.PlayerName}) create entity {entity.EntityId}:{entity.EntityType}"); } - public override void OnEntityDestroyed(Player player, Entity entity) + public override bool OnEntityDestroyed(Player player, Entity entity) { + return false; // Logger.Info($"Player({player.PlayerName}) destroy entity {entity.EntityId}:{entity.EntityType}"); } } diff --git a/Ragon.SimpleServer/Source/SimplePluginFactory.cs b/Ragon.SimpleServer/Source/SimplePluginFactory.cs index a0131db..39bcbe3 100644 --- a/Ragon.SimpleServer/Source/SimplePluginFactory.cs +++ b/Ragon.SimpleServer/Source/SimplePluginFactory.cs @@ -10,9 +10,9 @@ namespace Game.Source return new SimplePlugin(); } - public IAuthorizationProvider CreateAuthorizationProvider(Configuration configuration) + public IApplicationHandler CreateAuthorizationProvider(Configuration configuration) { - return new AuthorizationProviderByKey(configuration); + return new ApplicationHandlerByKey(configuration); } } } \ No newline at end of file diff --git a/Ragon.SimpleServer/config.json b/Ragon.SimpleServer/config.json index 10e516a..b099613 100755 --- a/Ragon.SimpleServer/config.json +++ b/Ragon.SimpleServer/config.json @@ -2,7 +2,7 @@ "key": "defaultkey", "protocol": "1.0.0", "statisticsInterval": 5, - "sendRate": 30, + "sendRate": 50, "port": 4444, "skipTimeout": 60, "reconnectTimeout": 300, diff --git a/Ragon/Sources/Application.cs b/Ragon/Sources/Application.cs index 300d00b..40c1bf3 100755 --- a/Ragon/Sources/Application.cs +++ b/Ragon/Sources/Application.cs @@ -1,31 +1,132 @@ -using ENet; +using System; +using System.Threading; +using Ragon.Common; using NLog; namespace Ragon.Core { - public class Application + public class Application : IEventHandler { - private readonly Logger _logger = LogManager.GetCurrentClassLogger(); - private readonly GameThread _gameThread; + private readonly RoomManager _roomManager; + private readonly Thread _thread; + private readonly Lobby _lobby; + private readonly ISocketServer _socketServer; + private readonly Dispatcher _dispatcher; + private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); + private readonly float _deltaTime = 0.0f; + private readonly Configuration _configuration; + private readonly RagonSerializer _serializer; + public ISocketServer SocketServer => _socketServer; + public Dispatcher Dispatcher => _dispatcher; + public Application(PluginFactory factory, Configuration configuration) { - _gameThread = new GameThread(factory, configuration); + var authorizationProvider = factory.CreateAuthorizationProvider(configuration); + + _configuration = configuration; + _serializer = new RagonSerializer(); + + var dispatcher = new Dispatcher(); + _dispatcher = dispatcher; + + _socketServer = new ENetServer(this); + _deltaTime = 1000.0f / configuration.SendRate; + + _roomManager = new RoomManager(factory, this); + _lobby = new Lobby(authorizationProvider, _roomManager, this); + + _thread = new Thread(Execute); + _thread.Name = "Game Thread"; + _thread.IsBackground = true; } - + public void Start() { - Library.Initialize(); - _gameThread.Start(); - _logger.Info("Started"); + var strings = _configuration.Protocol.Split("."); + if (strings.Length < 3) + { + _logger.Error("Wrong protocol passed to connect method"); + return; + } + + var parts = new uint[] {0, 0, 0}; + for (int i = 0; i < parts.Length; i++) + { + if (!uint.TryParse(strings[i], out var v)) + { + _logger.Error("Wrong protocol"); + return; + } + + parts[i] = v; + } + + uint encoded = (parts[0] << 16) | (parts[1] << 8) | parts[2]; + _socketServer.Start(_configuration.Port, _configuration.MaxConnections, encoded); + _thread.Start(); } public void Stop() { - _gameThread.Stop(); - Library.Deinitialize(); - _logger.Info("Stopped"); + _socketServer.Stop(); + _thread.Interrupt(); + } + + private void Execute() + { + while (true) + { + _socketServer.Process(); + _dispatcher.Process(); + _roomManager.Tick(_deltaTime); + // + Thread.Sleep((int) _deltaTime); + } + } + + public void OnConnected(ushort peerId) + { + _logger.Trace("Connected " + peerId); + } + + public void OnDisconnected(ushort peerId) + { + _logger.Trace("Disconnected " + peerId); + var player = _lobby.AuthorizationManager.GetPlayer(peerId); + if (player != null) + _roomManager.Left(player, Array.Empty()); + + _lobby.OnDisconnected(peerId); + } + + public void OnData(ushort peerId, byte[] data) + { + try + { + _serializer.Clear(); + _serializer.FromArray(data); + + var operation = _serializer.ReadOperation(); + if (_roomManager.RoomsBySocket.TryGetValue(peerId, out var room)) + room.ProcessEvent(peerId, operation, _serializer); + + _lobby.ProcessEvent(peerId, operation, _serializer); + } + catch (Exception exception) + { + _logger.Error(exception); + } + } + + public void OnTimeout(ushort peerId) + { + var player = _lobby.AuthorizationManager.GetPlayer(peerId); + if (player != null) + _roomManager.Left(player, Array.Empty()); + + _lobby.OnDisconnected(peerId); } } } \ No newline at end of file diff --git a/Ragon/Sources/Authorization/AuthorizationManager.cs b/Ragon/Sources/Authorization.cs similarity index 71% rename from Ragon/Sources/Authorization/AuthorizationManager.cs rename to Ragon/Sources/Authorization.cs index f3f0f80..88c1d2f 100644 --- a/Ragon/Sources/Authorization/AuthorizationManager.cs +++ b/Ragon/Sources/Authorization.cs @@ -5,17 +5,17 @@ using Ragon.Common; namespace Ragon.Core; -public class AuthorizationManager : IAuthorizationManager +public class AuthorizationManager { private Logger _logger = LogManager.GetCurrentClassLogger(); - private IAuthorizationProvider _provider; - private IGameThread _gameThread; + private IApplicationHandler _provider; + private Application _gameThread; private Lobby _lobby; private RagonSerializer _serializer; private readonly Dictionary _playersByPeers; private readonly Dictionary _playersByIds; - public AuthorizationManager(IAuthorizationProvider provider, IGameThread gameThread, Lobby lobby, RagonSerializer serializer) + public AuthorizationManager(IApplicationHandler provider, Application gameThread, Lobby lobby, RagonSerializer serializer) { _serializer = serializer; _lobby = lobby; @@ -25,7 +25,7 @@ public class AuthorizationManager : IAuthorizationManager _playersByPeers = new Dictionary(); } - public void OnAuthorization(uint peerId, string key, string name, ReadOnlySpan additionalData) + public void OnAuthorization(ushort peerId, string key, string name, ReadOnlySpan additionalData) { if (_playersByPeers.ContainsKey(peerId)) { @@ -33,14 +33,14 @@ public class AuthorizationManager : IAuthorizationManager return; } - var dispatcher = _gameThread.ThreadDispatcher; + var dispatcher = _gameThread.Dispatcher; _provider.OnAuthorizationRequest(key, name, additionalData.ToArray(), (playerId, playerName) => { dispatcher.Dispatch(() => Accepted(peerId, playerId, playerName)); }, (errorCode) => { dispatcher.Dispatch(() => Rejected(peerId, errorCode)); }); } - public void Accepted(uint peerId, string playerId, string playerName) + public void Accepted(ushort peerId, string playerId, string playerName) { _serializer.Clear(); _serializer.WriteOperation(RagonOperation.AUTHORIZED_SUCCESS); @@ -61,27 +61,27 @@ public class AuthorizationManager : IAuthorizationManager _playersByPeers.Add(peerId, player); var sendData = _serializer.ToArray(); - _gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable); + _gameThread.SocketServer.Send(peerId, sendData, DeliveryType.Reliable); } - public void Rejected(uint peerId, uint code) + public void Rejected(ushort peerId, uint code) { _serializer.Clear(); _serializer.WriteOperation(RagonOperation.AUTHORIZED_FAILED); _serializer.WriteInt((int) code); var sendData = _serializer.ToArray(); - _gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable); - _gameThread.Server.Disconnect(peerId, 0); + _gameThread.SocketServer.Send(peerId, sendData, DeliveryType.Reliable); + _gameThread.SocketServer.Disconnect(peerId, 0); } - public void Cleanup(uint peerId) + public void Cleanup(ushort peerId) { if (_playersByPeers.Remove(peerId, out var player)) _playersByIds.Remove(player.Id); } - public Player? GetPlayer(uint peerId) + public Player? GetPlayer(ushort peerId) { if (_playersByPeers.TryGetValue(peerId, out var player)) return player; diff --git a/Ragon/Sources/Authorization/IAuthorizationManager.cs b/Ragon/Sources/Authorization/IAuthorizationManager.cs deleted file mode 100644 index f0371b1..0000000 --- a/Ragon/Sources/Authorization/IAuthorizationManager.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ragon.Core; - -public interface IAuthorizationManager -{ - -} \ No newline at end of file diff --git a/Ragon/Sources/Bootstrap.cs b/Ragon/Sources/Bootstrap.cs index 77c666c..9c69cb2 100755 --- a/Ragon/Sources/Bootstrap.cs +++ b/Ragon/Sources/Bootstrap.cs @@ -13,7 +13,7 @@ namespace Ragon.Core { _logger.Info("Configure application..."); var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); - var configuration = ConfigurationLoader.Load(filePath); + var configuration = Configuration.Load(filePath); var app = new Application(factory, configuration); return app; } diff --git a/Ragon/Sources/Configuration/ConfigurationLoader.cs b/Ragon/Sources/Configuration.cs similarity index 65% rename from Ragon/Sources/Configuration/ConfigurationLoader.cs rename to Ragon/Sources/Configuration.cs index a44899d..281879a 100755 --- a/Ragon/Sources/Configuration/ConfigurationLoader.cs +++ b/Ragon/Sources/Configuration.cs @@ -2,12 +2,23 @@ using System.IO; using Newtonsoft.Json; using NLog; -using Logger = NLog.Logger; namespace Ragon.Core { - public static class ConfigurationLoader + [Serializable] + public struct Configuration { + public string Key; + public string Protocol; + public int StatisticsInterval; + public ushort SendRate; + public ushort Port; + public int SkipTimeout; + public int ReconnectTimeout; + public int MaxConnections; + public int MaxPlayersPerRoom; + public int MaxRooms; + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private static readonly string _serverVersion = "1.0.21-rc"; @@ -18,11 +29,10 @@ namespace Ragon.Core _logger.Info($"OS: {Environment.OSVersion}"); _logger.Info($"Processors: {Environment.ProcessorCount}"); _logger.Info($"Runtime Version: {Environment.Version}"); - _logger.Info("=================================="); - _logger.Info("= ="); - _logger.Info($"={"Ragon".PadBoth(32)}="); - _logger.Info("= ="); + _logger.Info("| |"); + _logger.Info("| Ragon |"); + _logger.Info("| |"); _logger.Info("=================================="); } diff --git a/Ragon/Sources/Configuration/Configuration.cs b/Ragon/Sources/Configuration/Configuration.cs deleted file mode 100755 index 890fd03..0000000 --- a/Ragon/Sources/Configuration/Configuration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Ragon.Core -{ - [Serializable] - public struct Configuration - { - public string Key; - public string Protocol; - public int StatisticsInterval; - public ushort SendRate; - public ushort Port; - public int SkipTimeout; - public int ReconnectTimeout; - public int MaxConnections; - public int MaxPlayersPerRoom; - public int MaxRooms; - } -} \ No newline at end of file diff --git a/Ragon/Sources/Utils/Dispatcher.cs b/Ragon/Sources/Dispatcher.cs similarity index 86% rename from Ragon/Sources/Utils/Dispatcher.cs rename to Ragon/Sources/Dispatcher.cs index d8d8eed..33b1b71 100644 --- a/Ragon/Sources/Utils/Dispatcher.cs +++ b/Ragon/Sources/Dispatcher.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Ragon.Core; -public class Dispatcher: IDispatcher, IDispatcherInternal +public class Dispatcher { public Queue _actions = new Queue(); diff --git a/Ragon/Sources/Entity/Entity.cs b/Ragon/Sources/Entity/Entity.cs index 87539d3..fc04bb1 100644 --- a/Ragon/Sources/Entity/Entity.cs +++ b/Ragon/Sources/Entity/Entity.cs @@ -1,42 +1,126 @@ using System; using System.Collections.Generic; +using NLog; using Ragon.Common; namespace Ragon.Core; public class Entity { + private ILogger _logger = LogManager.GetCurrentClassLogger(); + private GameRoom _room; + private static ushort _idGenerator = 0; public ushort EntityId { get; private set; } public ushort StaticId { get; private set; } public ushort EntityType { get; private set; } public ushort OwnerId { get; private set; } + public byte[] Payload { get; private set; } public RagonAuthority Authority { get; private set; } - public EntityProperty[] Properties { get; private set; } - public List BufferedEvents = new List(); - public byte[] Payload { get; set; } - - public Entity(ushort ownerId, ushort entityType, ushort staticId, RagonAuthority eventAuthority, int props) + private List _properties; + private List _bufferedEvents; + + public Entity(GameRoom room, ushort ownerId, ushort entityType, ushort staticId, RagonAuthority eventAuthority) { OwnerId = ownerId; StaticId = staticId; EntityType = entityType; EntityId = _idGenerator++; - Properties = new EntityProperty[props]; Payload = Array.Empty(); Authority = eventAuthority; + + _room = room; + _properties = new List(); + _bufferedEvents = new List(); } - public void UpdateOwner(ushort ownerId) => OwnerId = ownerId; + public void SetPayload(byte[] payload) + { + Payload = payload; + } + + public void SetOwner(ushort ownerId) + { + OwnerId = ownerId; + } - public void ReplicateProperties(RagonSerializer serializer) + public void AddProperty(EntityProperty property) + { + _properties.Add(property); + } + + public void ReplicateEvent(ushort peerId, ushort eventId, ReadOnlySpan payload, RagonReplicationMode eventMode, RagonTarget targetMode) + { + if (Authority == RagonAuthority.OwnerOnly && OwnerId != peerId) + { + _logger.Warn($"Player have not enought authority for event with Id {eventId}"); + return; + } + + if (eventMode == RagonReplicationMode.Buffered && targetMode != RagonTarget.Owner) + { + var bufferedEvent = new EntityEvent() + { + EventData = payload.ToArray(), + Target = targetMode, + EventId = eventId, + PeerId = peerId, + }; + _bufferedEvents.Add(bufferedEvent); + } + + var serializer = _room.GetSharedSerializer(); + + serializer.Clear(); + serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + serializer.WriteUShort(eventId); + serializer.WriteUShort(peerId); + serializer.WriteByte((byte) eventMode); + serializer.WriteUShort(EntityId); + serializer.WriteData(ref payload); + + var sendData = serializer.ToArray(); + Send(targetMode, sendData); + } + + public void ReadState(uint peerId, RagonSerializer serializer) + { + if (OwnerId != peerId) + { + _logger.Warn($"Not owner can't change properties of object {EntityId}"); + return; + } + + for (var i = 0; i < _properties.Count; i++) + { + if (serializer.ReadBool()) + { + var property = _properties[i]; + var size = property.Size; + if (!property.IsFixed) + size = serializer.ReadUShort(); + + if (size > property.Capacity) + { + _logger.Warn($"Property {i} payload too large, size: {size}"); + continue; + } + + var propertyPayload = serializer.ReadData(size); + property.Write(ref propertyPayload); + property.Size = size; + } + } + } + + public void WriteProperties(RagonSerializer serializer) { serializer.WriteUShort(EntityId); - for (int propertyIndex = 0; propertyIndex < Properties.Length; propertyIndex++) + for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++) { - var property = Properties[propertyIndex]; + var property = _properties[propertyIndex]; if (property.IsDirty) { serializer.WriteBool(true); @@ -51,12 +135,12 @@ public class Entity } } } - - public void Snapshot(RagonSerializer serializer) + + public void WriteSnapshot(RagonSerializer serializer) { - for (int propertyIndex = 0; propertyIndex < Properties.Length; propertyIndex++) + for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++) { - var property = Properties[propertyIndex]; + var property = _properties[propertyIndex]; var hasPayload = property.IsFixed || property.Size > 0 && !property.IsFixed; if (hasPayload) { @@ -71,5 +155,94 @@ public class Entity } } } - + + public void RestoreBufferedEvents(ushort peerId) + { + var serializer = _room.GetSharedSerializer(); + foreach (var bufferedEvent in _bufferedEvents) + { + serializer.Clear(); + serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); + serializer.WriteUShort(bufferedEvent.EventId); + serializer.WriteUShort(bufferedEvent.PeerId); + serializer.WriteByte((byte) RagonReplicationMode.Server); + serializer.WriteUShort(EntityId); + + ReadOnlySpan data = bufferedEvent.EventData.AsSpan(); + serializer.WriteData(ref data); + + var sendData = serializer.ToArray(); + _room.Send(peerId, sendData, DeliveryType.Reliable); + } + } + + public void Create() + { + var serializer = _room.GetSharedSerializer(); + + serializer.Clear(); + serializer.WriteOperation(RagonOperation.CREATE_ENTITY); + serializer.WriteUShort(EntityType); + serializer.WriteUShort(EntityId); + serializer.WriteUShort(OwnerId); + + ReadOnlySpan entityPayload = Payload.AsSpan(); + serializer.WriteUShort((ushort) entityPayload.Length); + serializer.WriteData(ref entityPayload); + + var sendData = serializer.ToArray(); + _room.BroadcastToReady(sendData, DeliveryType.Reliable); + } + + public void Destroy(ReadOnlySpan payload) + { + var serializer = _room.GetSharedSerializer(); + serializer.Clear(); + serializer.WriteOperation(RagonOperation.DESTROY_ENTITY); + serializer.WriteInt(EntityId); + serializer.WriteUShort((ushort) payload.Length); + serializer.WriteData(ref payload); + + var sendData = serializer.ToArray(); + _room.BroadcastToReady(sendData, DeliveryType.Reliable); + } + + void Send(RagonTarget targetMode, byte[] sendData) + { + switch (targetMode) + { + case RagonTarget.Owner: + { + _room.Send(OwnerId, sendData, DeliveryType.Reliable); + break; + } + case RagonTarget.ExceptOwner: + { + _room.BroadcastToReady(sendData, new [] { OwnerId }, DeliveryType.Reliable); + break; + } + case RagonTarget.All: + { + _room.BroadcastToReady(sendData, DeliveryType.Reliable); + break; + } + } + } + + public void ProcessEvent(Player player, RagonSerializer reader) + { + var eventId = reader.ReadUShort(); + var eventMode = (RagonReplicationMode) reader.ReadByte(); + var targetMode = (RagonTarget) reader.ReadByte(); + var payloadData = reader.ReadData(reader.Size); + + Span payloadRaw = stackalloc byte[reader.Size]; + ReadOnlySpan payload = payloadRaw; + payloadData.CopyTo(payloadRaw); + + if (_room.Plugin.InternalHandle(player.PeerId, EntityId, eventId, ref payload)) + return; + + ReplicateEvent(player.PeerId, eventId, payload, eventMode, targetMode); + } } \ No newline at end of file diff --git a/Ragon/Sources/Extensions/StringExtensions.cs b/Ragon/Sources/Extensions/StringExtensions.cs deleted file mode 100755 index d8ec5ac..0000000 --- a/Ragon/Sources/Extensions/StringExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Ragon.Core -{ - public static class StringExtensions - { - public static string PadBoth(this string str, int length) - { - int spaces = length - str.Length; - int padLeft = spaces / 2 + str.Length; - return str.PadLeft(padLeft).PadRight(length); - } - } -} \ No newline at end of file diff --git a/Ragon/Sources/Extensions/ValueExtensions.cs b/Ragon/Sources/Extensions/ValueExtensions.cs deleted file mode 100755 index c51a904..0000000 --- a/Ragon/Sources/Extensions/ValueExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Ragon.Core -{ - public static class ValueExtensions - { - public static T Clamp(this T val, T min, T max) where T : IComparable - { - if (val.CompareTo(min) < 0) return min; - else if (val.CompareTo(max) > 0) return max; - else return val; - } - } -} \ No newline at end of file diff --git a/Ragon/Sources/Game/GameRoom.cs b/Ragon/Sources/Game/GameRoom.cs deleted file mode 100755 index 51f1216..0000000 --- a/Ragon/Sources/Game/GameRoom.cs +++ /dev/null @@ -1,578 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using NLog; -using Ragon.Common; - -namespace Ragon.Core -{ - // TODO: Replace all serialization and packing into dedicated structures - // TODO: Split class on different managers - public class GameRoom : IGameRoom - { - public int PlayersMin { get; private set; } - public int PlayersMax { get; private set; } - public int PlayersCount => _players.Count; - public int EntitiesCount => _entities.Count; - public string Id { get; private set; } - public string Map { get; private set; } - - private ILogger _logger = LogManager.GetCurrentClassLogger(); - private Dictionary _players = new(); - private Dictionary _entities = new(); - private uint _owner; - - private readonly IScheduler _scheduler; - private readonly IGameThread _gameThread; - private readonly PluginBase _plugin; - private readonly RagonSerializer _serializer = new(512); - - // Cache - private uint[] _readyPlayers = Array.Empty(); - private uint[] _allPlayers = Array.Empty(); - private Entity[] _entitiesAll = Array.Empty(); - private HashSet _entitiesDirtySet = new HashSet(); - private List _entitiesDirty = new List(); - private List _peersCache = new List(); - private List _awaitingPeers = new List(); - - public GameRoom(IGameThread gameThread, PluginBase pluginBase, string roomId, string map, int min, int max) - { - _gameThread = gameThread; - _plugin = pluginBase; - _scheduler = new Scheduler(); - - Map = map; - PlayersMin = min; - PlayersMax = max; - Id = roomId; - - _plugin.Attach(this); - } - - public void AddPlayer(Player player, ReadOnlySpan payload) - { - if (_players.Count == 0) - { - _owner = player.PeerId; - } - - _players.Add(player.PeerId, player); - _allPlayers = _players.Select(p => p.Key).ToArray(); - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.JOIN_SUCCESS); - _serializer.WriteString(Id); - _serializer.WriteString(player.Id); - _serializer.WriteString(GetOwner().Id); - _serializer.WriteUShort((ushort) PlayersMin); - _serializer.WriteUShort((ushort) PlayersMax); - _serializer.WriteString(Map); - var sendData = _serializer.ToArray(); - Send(player.PeerId, sendData, DeliveryType.Reliable); - } - - public void RemovePlayer(uint peerId) - { - if (_players.Remove(peerId, out var player)) - { - _allPlayers = _players.Select(p => p.Key).ToArray(); - _readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray(); - - { - _plugin.OnPlayerLeaved(player); - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.PLAYER_LEAVED); - _serializer.WriteString(player.Id); - - var entitiesToDelete = player.Entities.Where(e => e.StaticId == 0).ToArray(); - _serializer.WriteUShort((ushort) entitiesToDelete.Length); - foreach (var entity in entitiesToDelete) - { - _serializer.WriteUShort(entity.EntityId); - _entities.Remove(entity.EntityId); - } - - var sendData = _serializer.ToArray(); - Broadcast(_readyPlayers, sendData); - } - - if (_allPlayers.Length > 0 && player.PeerId == _owner) - { - var nextOwnerId = _allPlayers[0]; - _owner = nextOwnerId; - var nextOwner = _players[nextOwnerId]; - - var entitiesToUpdate = player.Entities.Where(e => e.StaticId > 0).ToArray(); - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED); - _serializer.WriteString(nextOwner.Id); - _serializer.WriteUShort((ushort) entitiesToUpdate.Length); - foreach (var entity in entitiesToUpdate) - { - _serializer.WriteUShort(entity.EntityId); - entity.UpdateOwner((ushort) nextOwnerId); - } - - var sendData = _serializer.ToArray(); - Broadcast(_readyPlayers, sendData); - } - - _entitiesAll = _entities.Values.ToArray(); - } - } - - // TODO: Move this processing to specialized classes with structures - public void ProcessEvent(ushort peerId, RagonOperation operation, ReadOnlySpan payloadRawData) - { - _serializer.Clear(); - _serializer.FromSpan(ref payloadRawData); - switch (operation) - { - case RagonOperation.LOAD_SCENE: - { - var sceneName = _serializer.ReadString(); - _readyPlayers = Array.Empty(); - _entitiesAll = Array.Empty(); - _entities.Clear(); - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.LOAD_SCENE); - _serializer.WriteString(sceneName); - - var sendData = _serializer.ToArray(); - Broadcast(_allPlayers, sendData, DeliveryType.Reliable); - break; - } - case RagonOperation.SCENE_LOADED: - { - var player = _players[peerId]; - if (peerId == _owner) - { - var statics = _serializer.ReadUShort(); - for (var staticIndex = 0; staticIndex < statics; staticIndex++) - { - var entityType = _serializer.ReadUShort(); - var entityAuthority = (RagonAuthority) _serializer.ReadByte(); - var staticId = _serializer.ReadUShort(); - - var propertiesCount = _serializer.ReadUShort(); - var entity = new Entity(peerId, entityType, staticId, entityAuthority, propertiesCount); - for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++) - { - var propertyType = _serializer.ReadBool(); - var propertySize = _serializer.ReadUShort(); - entity.Properties[propertyIndex] = new EntityProperty(propertySize, propertyType); - } - - player.Entities.Add(entity); - player.EntitiesIds.Add(entity.EntityId); - - _entities.Add(entity.EntityId, entity); - } - - _entitiesAll = _entities.Values.ToArray(); - _logger.Trace($"Scene entities: {statics}"); - - _awaitingPeers.Add(peerId); - - foreach (var peer in _awaitingPeers) - { - var joinedPlayer = _players[peer]; - joinedPlayer.IsLoaded = true; - _plugin.OnPlayerJoined(joinedPlayer); - _logger.Trace($"[{_owner}][{peer}] Player {joinedPlayer.Id} restored"); - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.PLAYER_JOINED); - _serializer.WriteUShort((ushort) player.PeerId); - _serializer.WriteString(player.Id); - _serializer.WriteString(player.PlayerName); - - var sendData = _serializer.ToArray(); - var readyPlayersWithExcludedPeer = _readyPlayers.Where(p => p != peerId).ToArray(); - Broadcast(readyPlayersWithExcludedPeer, sendData, DeliveryType.Reliable); - } - } - - _readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray(); - foreach (var peer in _awaitingPeers) - { - ReplicateSnapshot(peer); - } - - _awaitingPeers.Clear(); - } - else if (GetOwner().IsLoaded) - { - _logger.Trace($"[{_owner}][{peerId}] Player {player.Id} restored instantly"); - player.IsLoaded = true; - - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.PLAYER_JOINED); - _serializer.WriteUShort((ushort) player.PeerId); - _serializer.WriteString(player.Id); - _serializer.WriteString(player.PlayerName); - - var sendData = _serializer.ToArray(); - var readyPlayersWithExcludedPeer = _readyPlayers.Where(p => p != peerId).ToArray(); - Broadcast(readyPlayersWithExcludedPeer, sendData, DeliveryType.Reliable); - } - - _readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray(); - _plugin.OnPlayerJoined(player); - - ReplicateSnapshot(peerId); - - foreach (var (key, value) in _entities) - { - foreach (var bufferedEvent in value.BufferedEvents) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - _serializer.WriteUShort(bufferedEvent.EventId); - _serializer.WriteUShort(bufferedEvent.PeerId); - _serializer.WriteByte((byte) RagonReplicationMode.Server); - _serializer.WriteUShort(value.EntityId); - - ReadOnlySpan data = bufferedEvent.EventData.AsSpan(); - _serializer.WriteData(ref data); - - _logger.Trace($"[{peerId}] Restored buffered event {bufferedEvent.EventId}"); - var sendData = _serializer.ToArray(); - Send(peerId, sendData, DeliveryType.Reliable); - } - } - } - else - { - _logger.Trace($"[{_owner}][{peerId}] Player {player.Id} waiting"); - _awaitingPeers.Add(peerId); - } - - break; - } - case RagonOperation.REPLICATE_ENTITY_STATE: - { - var entitiesCount = _serializer.ReadUShort(); - for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++) - { - var entityId = _serializer.ReadUShort(); - if (_entities.TryGetValue(entityId, out var ent)) - { - if (ent.OwnerId != peerId) - { - _logger.Warn($"Not owner can't change properties of object {entityId}"); - return; - } - - for (var i = 0; i < ent.Properties.Length; i++) - { - if (_serializer.ReadBool()) - { - var property = ent.Properties[i]; - var size = property.Size; - if (!property.IsFixed) - size = _serializer.ReadUShort(); - - if (size > property.Capacity) - { - _logger.Warn($"Property {i} payload too large, size: {size}"); - continue; - } - - var propertyPayload = _serializer.ReadData(size); - property.Write(ref propertyPayload); - property.Size = size; - } - } - - if (_entitiesDirtySet.Add(ent)) - _entitiesDirty.Add(ent); - } - else - { - _logger.Error($"Entity with Id {entityId} not found, replication interrupted"); - break; - } - } - - break; - } - case RagonOperation.REPLICATE_ENTITY_EVENT: - { - var eventId = _serializer.ReadUShort(); - var eventMode = (RagonReplicationMode) _serializer.ReadByte(); - var targetMode = (RagonTarget) _serializer.ReadByte(); - var entityId = _serializer.ReadUShort(); - - if (!_entities.TryGetValue(entityId, out var ent)) - { - _logger.Warn($"Entity not found for event with Id {eventId}"); - return; - } - - if (ent.Authority == RagonAuthority.OwnerOnly && ent.OwnerId != peerId) - { - _logger.Warn($"Player have not enought authority for event with Id {eventId}"); - return; - } - - Span payloadRaw = stackalloc byte[_serializer.Size]; - var payloadData = _serializer.ReadData(_serializer.Size); - payloadData.CopyTo(payloadRaw); - - ReadOnlySpan payload = payloadRaw; - if (_plugin.InternalHandle(peerId, entityId, eventId, ref payload)) - return; - - if (eventMode == RagonReplicationMode.Buffered && targetMode != RagonTarget.Owner) - { - var bufferedEvent = new EntityEvent() - { - EventData = payload.ToArray(), - Target = targetMode, - EventId = eventId, - PeerId = peerId, - }; - ent.BufferedEvents.Add(bufferedEvent); - } - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - _serializer.WriteUShort(eventId); - _serializer.WriteUShort(peerId); - _serializer.WriteByte((byte) eventMode); - _serializer.WriteUShort(entityId); - _serializer.WriteData(ref payload); - - var sendData = _serializer.ToArray(); - SendEvent(ent, targetMode, sendData); - break; - } - case RagonOperation.CREATE_ENTITY: - { - var entityType = _serializer.ReadUShort(); - var eventAuthority = (RagonAuthority) _serializer.ReadByte(); - var propertiesCount = _serializer.ReadUShort(); - - _logger.Trace($"[{peerId}] Create Entity {entityType}"); - - var entity = new Entity(peerId, entityType, 0, eventAuthority, propertiesCount); - for (var i = 0; i < propertiesCount; i++) - { - var propertyType = _serializer.ReadBool(); - var propertySize = _serializer.ReadUShort(); - entity.Properties[i] = new EntityProperty(propertySize, propertyType); - } - - { - var entityPayload = _serializer.ReadData(_serializer.Size); - entity.Payload = entityPayload.ToArray(); - } - - var player = _players[peerId]; - player.Entities.Add(entity); - player.EntitiesIds.Add(entity.EntityId); - - var ownerId = peerId; - - _entities.Add(entity.EntityId, entity); - _entitiesAll = _entities.Values.ToArray(); - - _plugin.OnEntityCreated(player, entity); - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.CREATE_ENTITY); - _serializer.WriteUShort(entityType); - _serializer.WriteUShort(entity.EntityId); - _serializer.WriteUShort(ownerId); - - { - ReadOnlySpan entityPayload = entity.Payload.AsSpan(); - _serializer.WriteUShort((ushort) entityPayload.Length); - _serializer.WriteData(ref entityPayload); - } - - var sendData = _serializer.ToArray(); - Broadcast(_readyPlayers, sendData, DeliveryType.Reliable); - break; - } - case RagonOperation.DESTROY_ENTITY: - { - var entityId = _serializer.ReadInt(); - if (_entities.TryGetValue(entityId, out var entity)) - { - if (entity.Authority == RagonAuthority.OwnerOnly && entity.OwnerId != peerId) - return; - - var player = _players[peerId]; - var destroyPayload = _serializer.ReadData(_serializer.Size); - - player.Entities.Remove(entity); - player.EntitiesIds.Remove(entity.EntityId); - - _entities.Remove(entityId); - _entitiesAll = _entities.Values.ToArray(); - - _plugin.OnEntityDestroyed(player, entity); - - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.DESTROY_ENTITY); - _serializer.WriteInt(entityId); - _serializer.WriteUShort((ushort) destroyPayload.Length); - _serializer.WriteData(ref destroyPayload); - - var sendData = _serializer.ToArray(); - Broadcast(_readyPlayers, sendData, DeliveryType.Reliable); - } - - break; - } - } - } - - public void Tick(float deltaTime) - { - _scheduler.Tick(deltaTime); - - ReplicateProperties(); - } - - // TODO: Move this to specialized class - void ReplicateSnapshot(uint peerId) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.SNAPSHOT); - _serializer.WriteUShort((ushort) _readyPlayers.Length); - foreach (var playerPeerId in _readyPlayers) - { - _serializer.WriteUShort((ushort) playerPeerId); - _serializer.WriteString(_players[playerPeerId].Id); - _serializer.WriteString(_players[playerPeerId].PlayerName); - } - - var dynamicEntities = _entitiesAll.Where(e => e.StaticId == 0).ToArray(); - var dynamicEntitiesCount = (ushort) dynamicEntities.Length; - _serializer.WriteUShort(dynamicEntitiesCount); - foreach (var entity in dynamicEntities) - { - ReadOnlySpan payload = entity.Payload.AsSpan(); - - _serializer.WriteUShort(entity.EntityType); - _serializer.WriteUShort(entity.EntityId); - _serializer.WriteUShort((ushort) entity.OwnerId); - _serializer.WriteUShort((ushort) payload.Length); - _serializer.WriteData(ref payload); - - entity.Snapshot(_serializer); - } - - var staticEntities = _entitiesAll.Where(e => e.StaticId != 0).ToArray(); - var staticEntitiesCount = (ushort) staticEntities.Length; - _serializer.WriteUShort(staticEntitiesCount); - foreach (var entity in staticEntities) - { - ReadOnlySpan payload = entity.Payload.AsSpan(); - - _serializer.WriteUShort(entity.EntityType); - _serializer.WriteUShort(entity.EntityId); - _serializer.WriteUShort(entity.StaticId); - _serializer.WriteUShort(entity.OwnerId); - _serializer.WriteUShort((ushort) payload.Length); - _serializer.WriteData(ref payload); - - entity.Snapshot(_serializer); - } - - var sendData = _serializer.ToArray(); - Send(peerId, sendData, DeliveryType.Reliable); - } - - private void ReplicateProperties() - { - var entities = (ushort) _entitiesDirty.Count; - if (entities > 0) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE); - _serializer.WriteUShort(entities); - - foreach (var entity in _entitiesDirty) - entity.ReplicateProperties(_serializer); - - _entitiesDirty.Clear(); - _entitiesDirtySet.Clear(); - - var sendData = _serializer.ToArray(); - Broadcast(_readyPlayers, sendData); - } - } - - public void Start() - { - _plugin.OnStart(); - } - - public void Stop() - { - foreach (var peerId in _allPlayers) - _gameThread.Server.Disconnect(peerId, 0); - - _plugin.OnStop(); - _plugin.Detach(); - } - - public Player GetPlayerById(uint peerId) => _players[peerId]; - - public Entity GetEntityById(int entityId) => _entities[entityId]; - - public Player GetOwner() => _players[_owner]; - - public IDispatcher GetThreadDispatcher() => _gameThread.ThreadDispatcher; - - public IScheduler GetScheduler() => _scheduler; - - - // TODO: Move this to Entity Event Manager - public void SendEvent(Entity ent, RagonTarget targetMode, byte[] sendData) - { - switch (targetMode) - { - case RagonTarget.Owner: - { - Send(ent.OwnerId, sendData, DeliveryType.Reliable); - break; - } - case RagonTarget.ExceptOwner: - { - _peersCache.Clear(); - foreach (var playerPeerId in _readyPlayers) - if (playerPeerId != ent.OwnerId) - _peersCache.Add(playerPeerId); - - Broadcast(_peersCache.ToArray(), sendData, DeliveryType.Reliable); - break; - } - case RagonTarget.All: - { - Broadcast(_readyPlayers, sendData, DeliveryType.Reliable); - break; - } - } - } - - public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) => - _gameThread.Server.Send(peerId, rawData, deliveryType); - - public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) => - _gameThread.Server.Broadcast(peersIds, rawData, deliveryType); - - public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) => - _gameThread.Server.Broadcast(_allPlayers, rawData, deliveryType); - } -} \ No newline at end of file diff --git a/Ragon/Sources/Game/GameThread.cs b/Ragon/Sources/Game/GameThread.cs deleted file mode 100755 index 8d77e67..0000000 --- a/Ragon/Sources/Game/GameThread.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Threading; -using Ragon.Common; -using ENet; -using NLog; - -namespace Ragon.Core -{ - public class GameThread : IGameThread, IHandler - { - private readonly RoomManager _roomManager; - private readonly Thread _thread; - private readonly Lobby _lobby; - private readonly ISocketServer _server; - private readonly IDispatcherInternal _dispatcherInternal; - private readonly IDispatcher _dispatcher; - private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); - private readonly float _deltaTime = 0.0f; - private readonly Configuration _configuration; - - public ISocketServer Server => _server; - public IDispatcher ThreadDispatcher => _dispatcher; - - public GameThread(PluginFactory factory, Configuration configuration) - { - var authorizationProvider = factory.CreateAuthorizationProvider(configuration); - - _configuration = configuration; - - var dispatcher = new Dispatcher(); - _dispatcherInternal = dispatcher; - _dispatcher = dispatcher; - - _server = new ENetServer(this); - _deltaTime = 1000.0f / configuration.SendRate; - - _roomManager = new RoomManager(factory, this); - _lobby = new Lobby(authorizationProvider, _roomManager, this); - - _thread = new Thread(Execute); - _thread.Name = "Game Thread"; - _thread.IsBackground = true; - } - - public void Start() - { - var strings = _configuration.Protocol.Split("."); - if (strings.Length < 3) - { - _logger.Error("Wrong protocol passed to connect method"); - return; - } - - var parts = new uint[] {0, 0, 0}; - for (int i = 0; i < parts.Length; i++) - { - if (!uint.TryParse(strings[i], out var v)) - { - _logger.Error("Wrong protocol"); - return; - } - - parts[i] = v; - } - - uint encoded = (parts[0] << 16) | (parts[1] << 8) | parts[2]; - _server.Start(_configuration.Port, _configuration.MaxConnections, encoded); - _thread.Start(); - } - - public void Stop() - { - _server.Stop(); - _thread.Interrupt(); - } - - private void Execute() - { - while (true) - { - _server.Process(); - _dispatcherInternal.Process(); - _roomManager.Tick(_deltaTime); - - Thread.Sleep((int) _deltaTime); - } - } - - - public void OnEvent(Event evnt) - { - if (evnt.Type == EventType.Timeout || evnt.Type == EventType.Disconnect) - { - var player = _lobby.AuthorizationManager.GetPlayer(evnt.Peer.ID); - if (player != null) - _roomManager.Left(player, Array.Empty()); - - _lobby.OnDisconnected(evnt.Peer.ID); - } - - if (evnt.Type == EventType.Receive) - { - try - { - var peerId = (ushort) evnt.Peer.ID; - var dataRaw = new byte[evnt.Packet.Length]; - evnt.Packet.CopyTo(dataRaw); - - var data = new ReadOnlySpan(dataRaw); - var operation = (RagonOperation) data[0]; - var payload = data.Slice(1, data.Length - 1); - - if (_roomManager.RoomsBySocket.TryGetValue(peerId, out var room)) - room.ProcessEvent(peerId, operation, payload); - - _lobby.ProcessEvent(peerId, operation, payload); - } - catch (Exception exception) - { - _logger.Error(exception); - } - } - } - } -} \ No newline at end of file diff --git a/Ragon/Sources/Game/IGameRoom.cs b/Ragon/Sources/Game/IGameRoom.cs deleted file mode 100644 index 83bb907..0000000 --- a/Ragon/Sources/Game/IGameRoom.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Ragon.Core; - -public interface IGameRoom -{ - public string Id { get; } - public string Map { get; } - public int PlayersMin { get; } - public int PlayersMax { get; } - public int PlayersCount { get; } - - public Player GetPlayerById(uint peerId); - public Entity GetEntityById(int entityId); - public Player GetOwner(); - public IDispatcher GetThreadDispatcher(); - public IScheduler GetScheduler(); - - public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable); - public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable); - public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable); -} \ No newline at end of file diff --git a/Ragon/Sources/Game/IGameThread.cs b/Ragon/Sources/Game/IGameThread.cs deleted file mode 100644 index a788fac..0000000 --- a/Ragon/Sources/Game/IGameThread.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ragon.Common; - -namespace Ragon.Core; - -public interface IGameThread -{ - public IDispatcher ThreadDispatcher { get; } - public ISocketServer Server { get; } -} \ No newline at end of file diff --git a/Ragon/Sources/GameRoom.cs b/Ragon/Sources/GameRoom.cs new file mode 100755 index 0000000..2de71bd --- /dev/null +++ b/Ragon/Sources/GameRoom.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NLog; +using Ragon.Common; + +namespace Ragon.Core +{ + public class GameRoom + { + public int PlayersMin { get; private set; } + public int PlayersMax { get; private set; } + public int PlayersCount => _players.Count; + public int EntitiesCount => _entities.Count; + public string Id { get; private set; } + public string Map { get; private set; } + public PluginBase Plugin => _plugin; + private ILogger _logger = LogManager.GetCurrentClassLogger(); + private Dictionary _players = new(); + private Dictionary _entities = new(); + private ushort _owner; + + private readonly ISocketServer _socketServer; + private readonly Scheduler _scheduler; + private readonly Application _application; + private readonly PluginBase _plugin; + private readonly RagonSerializer _writer = new(512); + + // Cache + private ushort[] _readyPlayers = Array.Empty(); + private ushort[] _allPlayers = Array.Empty(); + private Entity[] _entitiesAll = Array.Empty(); + private HashSet _entitiesDirtySet = new HashSet(); + private List _entitiesDirty = new List(); + private List _peersCache = new List(); + private List _awaitingPeers = new List(); + + public Player GetPlayerByPeer(ushort peerId) => _players[peerId]; + + public Player GetPlayerById(string id) => _players.Values.FirstOrDefault(p => p.Id == id)!; + + public Entity GetEntityById(int entityId) => _entities[entityId]; + + public Player GetOwner() => _players[_owner]; + + public RagonSerializer GetSharedSerializer() => _writer; + + public GameRoom(Application application, PluginBase pluginBase, string roomId, string map, int min, int max) + { + _application = application; + _socketServer = application.SocketServer; + _plugin = pluginBase; + _scheduler = new Scheduler(); + + Map = map; + PlayersMin = min; + PlayersMax = max; + Id = roomId; + + _plugin.Attach(this); + } + + public void AddPlayer(Player player, ReadOnlySpan payload) + { + if (_players.Count == 0) + { + _owner = player.PeerId; + } + + _players.Add(player.PeerId, player); + _allPlayers = _players.Select(p => p.Key).ToArray(); + + AcceptPlayer(player); + } + + public void RemovePlayer(ushort peerId) + { + if (_players.Remove(peerId, out var player)) + { + _allPlayers = _players.Select(p => p.Key).ToArray(); + _readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray(); + + _plugin.OnPlayerLeaved(player); + + BroadcastLeaved(player); + + if (_allPlayers.Length > 0 && player.PeerId == _owner) + { + var nextOwnerId = _allPlayers[0]; + var nextOwner = _players[nextOwnerId]; + + _owner = nextOwnerId; + + BroadcastNewOwner(player, nextOwner); + } + + _entitiesAll = _entities.Values.ToArray(); + } + } + + public void ProcessEvent(ushort peerId, RagonOperation operation, RagonSerializer reader) + { + switch (operation) + { + case RagonOperation.LOAD_SCENE: + { + var sceneName = reader.ReadString(); + BroadcastNewScene(sceneName); + break; + } + case RagonOperation.SCENE_LOADED: + { + var player = _players[peerId]; + if (peerId == _owner) + { + var statics = reader.ReadUShort(); + for (var staticIndex = 0; staticIndex < statics; staticIndex++) + { + var entityType = reader.ReadUShort(); + var entityAuthority = (RagonAuthority) reader.ReadByte(); + var staticId = reader.ReadUShort(); + var propertiesCount = reader.ReadUShort(); + + var entity = new Entity(this, player.PeerId, entityType, staticId, entityAuthority); + for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++) + { + var propertyType = reader.ReadBool(); + var propertySize = reader.ReadUShort(); + entity.AddProperty(new EntityProperty(propertySize, propertyType)); + } + + player.AttachEntity(entity); + AttachEntity(player, entity); + } + + _entitiesAll = _entities.Values.ToArray(); + _logger.Trace($"Scene entities: {statics}"); + + _awaitingPeers.Add(peerId); + + foreach (var peer in _awaitingPeers) + { + var joinedPlayer = _players[peer]; + joinedPlayer.IsLoaded = true; + + _plugin.OnPlayerJoined(joinedPlayer); + _logger.Trace($"[{_owner}][{peer}] Player {joinedPlayer.Id} restored"); + + BroadcastJoined(player); + } + + _readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray(); + + BroadcastSnapshot(_awaitingPeers.ToArray()); + + _awaitingPeers.Clear(); + } + else if (GetOwner().IsLoaded) + { + _logger.Trace($"[{_owner}][{peerId}] Player {player.Id} restored instantly"); + player.IsLoaded = true; + + BroadcastJoined(player); + + _readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray(); + _plugin.OnPlayerJoined(player); + + BroadcastSnapshot(new[] {peerId}); + + foreach (var (key, value) in _entities) + value.RestoreBufferedEvents(peerId); + } + else + { + _logger.Trace($"[{_owner}][{peerId}] Player {player.Id} waiting"); + _awaitingPeers.Add(peerId); + } + + break; + } + case RagonOperation.REPLICATE_ENTITY_STATE: + { + var entitiesCount = reader.ReadUShort(); + for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++) + { + var entityId = reader.ReadUShort(); + if (_entities.TryGetValue(entityId, out var entity)) + { + entity.ReadState(peerId, reader); + + if (_entitiesDirtySet.Add(entity)) + _entitiesDirty.Add(entity); + } + else + { + _logger.Error($"Entity with Id {entityId} not found, replication interrupted"); + break; + } + } + break; + } + case RagonOperation.REPLICATE_ENTITY_EVENT: + { + var entityId = reader.ReadUShort(); + if (!_entities.TryGetValue(entityId, out var ent)) + { + _logger.Warn($"Entity not found for event with Id {entityId}"); + return; + } + + var player = _players[peerId]; + ent.ProcessEvent(player, reader); + break; + } + case RagonOperation.CREATE_ENTITY: + { + var entityType = reader.ReadUShort(); + var eventAuthority = (RagonAuthority) reader.ReadByte(); + var propertiesCount = reader.ReadUShort(); + + _logger.Trace($"[{peerId}] Create Entity {entityType}"); + + var player = _players[peerId]; + var entity = new Entity(this, player.PeerId, entityType, 0, eventAuthority); + for (var i = 0; i < propertiesCount; i++) + { + var propertyType = reader.ReadBool(); + var propertySize = reader.ReadUShort(); + entity.AddProperty(new EntityProperty(propertySize, propertyType)); + } + + var entityPayload = reader.ReadData(reader.Size); + entity.SetPayload(entityPayload.ToArray()); + + if (_plugin.OnEntityCreated(player, entity)) + return; + + player.AttachEntity(entity); + AttachEntity(player, entity); + + entity.Create(); + break; + } + case RagonOperation.DESTROY_ENTITY: + { + var entityId = reader.ReadInt(); + if (_entities.TryGetValue(entityId, out var entity)) + { + var player = _players[peerId]; + var payload = reader.ReadData(reader.Size); + DetachEntity(player, entity, payload); + } + break; + } + } + } + + public void Tick(float deltaTime) + { + _scheduler.Tick(deltaTime); + BroadcastState(); + } + + public void OnStart() + { + _plugin.OnStart(); + } + + public void OnStop() + { + foreach (var peerId in _allPlayers) + _application.SocketServer.Disconnect(peerId, 0); + + _plugin.OnStop(); + _plugin.Detach(); + _scheduler.Dispose(); + } + + public void AttachEntity(Player player, Entity entity) + { + _entities.Add(entity.EntityId, entity); + _entitiesAll = _entities.Values.ToArray(); + } + + public void DetachEntity(Player player, Entity entity, ReadOnlySpan payload) + { + if (entity.Authority == RagonAuthority.OwnerOnly && entity.OwnerId != player.PeerId) + return; + + if (_plugin.OnEntityDestroyed(player, entity)) + return; + + player.DetachEntity(entity); + entity.Destroy(payload); + + _entities.Remove(entity.EntityId); + _entitiesAll = _entities.Values.ToArray(); + } + + void BroadcastNewOwner(Player prev, Player next) + { + var entitiesToUpdate = prev.Entities.Where(e => e.StaticId > 0).ToArray(); + + _writer.Clear(); + _writer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED); + _writer.WriteString(next.Id); + _writer.WriteUShort((ushort) entitiesToUpdate.Length); + foreach (var entity in entitiesToUpdate) + { + _writer.WriteUShort(entity.EntityId); + entity.SetOwner((ushort) next.PeerId); + } + + BroadcastToReady(_writer, DeliveryType.Reliable); + } + + void BroadcastJoined(Player player) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.PLAYER_JOINED); + _writer.WriteUShort(player.PeerId); + _writer.WriteString(player.Id); + _writer.WriteString(player.PlayerName); + + BroadcastToReady(_writer, new [] { player.PeerId }, DeliveryType.Reliable); + } + + void BroadcastLeaved(Player player) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.PLAYER_LEAVED); + _writer.WriteString(player.Id); + + var entitiesToDelete = player.Entities.Where(e => e.StaticId == 0).ToArray(); + _writer.WriteUShort((ushort) entitiesToDelete.Length); + foreach (var entity in entitiesToDelete) + { + _writer.WriteUShort(entity.EntityId); + _entities.Remove(entity.EntityId); + } + + BroadcastToReady(_writer, DeliveryType.Reliable); + } + + void BroadcastSnapshot(ushort[] peersIds) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.SNAPSHOT); + _writer.WriteUShort((ushort) _readyPlayers.Length); + foreach (var playerPeerId in _readyPlayers) + { + _writer.WriteUShort(playerPeerId); + _writer.WriteString(_players[playerPeerId].Id); + _writer.WriteString(_players[playerPeerId].PlayerName); + } + + var dynamicEntities = _entitiesAll.Where(e => e.StaticId == 0).ToArray(); + var dynamicEntitiesCount = (ushort) dynamicEntities.Length; + _writer.WriteUShort(dynamicEntitiesCount); + foreach (var entity in dynamicEntities) + { + ReadOnlySpan payload = entity.Payload.AsSpan(); + + _writer.WriteUShort(entity.EntityType); + _writer.WriteUShort(entity.EntityId); + _writer.WriteUShort(entity.OwnerId); + _writer.WriteUShort((ushort) payload.Length); + _writer.WriteData(ref payload); + + entity.WriteSnapshot(_writer); + } + + var staticEntities = _entitiesAll.Where(e => e.StaticId != 0).ToArray(); + var staticEntitiesCount = (ushort) staticEntities.Length; + _writer.WriteUShort(staticEntitiesCount); + foreach (var entity in staticEntities) + { + ReadOnlySpan payload = entity.Payload.AsSpan(); + + _writer.WriteUShort(entity.EntityType); + _writer.WriteUShort(entity.EntityId); + _writer.WriteUShort(entity.StaticId); + _writer.WriteUShort(entity.OwnerId); + _writer.WriteUShort((ushort) payload.Length); + _writer.WriteData(ref payload); + + entity.WriteSnapshot(_writer); + } + + var sendData = _writer.ToArray(); + Broadcast(peersIds, sendData, DeliveryType.Reliable); + } + + void BroadcastState() + { + var entities = (ushort) _entitiesDirty.Count; + if (entities > 0) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE); + _writer.WriteUShort(entities); + + foreach (var entity in _entitiesDirty) + entity.WriteProperties(_writer); + + _entitiesDirty.Clear(); + _entitiesDirtySet.Clear(); + + BroadcastToReady(_writer, DeliveryType.Reliable); + } + } + + void AcceptPlayer(Player player) + { + _writer.Clear(); + _writer.WriteOperation(RagonOperation.JOIN_SUCCESS); + _writer.WriteString(Id); + _writer.WriteString(player.Id); + _writer.WriteString(GetOwner().Id); + _writer.WriteUShort((ushort) PlayersMin); + _writer.WriteUShort((ushort) PlayersMax); + _writer.WriteString(Map); + + Send(player.PeerId, _writer, DeliveryType.Reliable); + } + + void BroadcastNewScene(string sceneName) + { + _readyPlayers = Array.Empty(); + _entitiesAll = Array.Empty(); + _entities.Clear(); + + _writer.Clear(); + _writer.WriteOperation(RagonOperation.LOAD_SCENE); + _writer.WriteString(sceneName); + + BroadcastToAll(_writer, DeliveryType.Reliable); + } + + public void Send(ushort peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) + { + _socketServer.Send(peerId, rawData, deliveryType); + } + + public void Send(ushort peerId, RagonSerializer writer, DeliveryType deliveryType = DeliveryType.Unreliable) + { + var sendData = writer.ToArray(); + _socketServer.Send(peerId, sendData, deliveryType); + } + + public void Broadcast(ushort[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) + { + _socketServer.Broadcast(peersIds, rawData, deliveryType); + } + + public void BroadcastToAll(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) + { + _socketServer.Broadcast(_allPlayers, rawData, deliveryType); + } + + public void BroadcastToAll(RagonSerializer writer, DeliveryType deliveryType = DeliveryType.Unreliable) + { + var sendData = writer.ToArray(); + _socketServer.Broadcast(_allPlayers, sendData, deliveryType); + } + + public void BroadcastToReady(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable) + { + _socketServer.Broadcast(_readyPlayers, rawData, deliveryType); + } + + public void BroadcastToReady(RagonSerializer writer, DeliveryType deliveryType = DeliveryType.Unreliable) + { + var sendData = writer.ToArray(); + _socketServer.Broadcast(_readyPlayers, sendData, deliveryType); + } + + public void BroadcastToReady(byte[] rawData, ushort[] excludePeersIds, DeliveryType deliveryType = DeliveryType.Unreliable) + { + _peersCache.Clear(); + foreach (var playerPeerId in _readyPlayers) + { + foreach (var excludePeersId in excludePeersIds) + { + if (playerPeerId != excludePeersId) + { + _peersCache.Add(playerPeerId); + } + } + } + + var peersIds = _peersCache.ToArray(); + _socketServer.Broadcast(peersIds, rawData, deliveryType); + } + + public void BroadcastToReady(RagonSerializer writer, ushort[] excludePeersIds, DeliveryType deliveryType = DeliveryType.Unreliable) + { + var sendData = writer.ToArray(); + BroadcastToReady(sendData, excludePeersIds, deliveryType); + } + } +} \ No newline at end of file diff --git a/Ragon/Sources/Authorization/IAuthorizationProvider.cs b/Ragon/Sources/Handler.cs similarity index 53% rename from Ragon/Sources/Authorization/IAuthorizationProvider.cs rename to Ragon/Sources/Handler.cs index be8967a..dedf5f8 100644 --- a/Ragon/Sources/Authorization/IAuthorizationProvider.cs +++ b/Ragon/Sources/Handler.cs @@ -3,7 +3,10 @@ using System.Threading.Tasks; namespace Ragon.Core; -public interface IAuthorizationProvider +public interface IApplicationHandler { Task OnAuthorizationRequest(string key, string playerName, byte[] additionalData, Action Accept, Action Reject); + public void OnCustomEvent(ushort peerId, ReadOnlySpan payload); + public void OnJoin(ushort peerId); + public void OnLeave(ushort peerId); } \ No newline at end of file diff --git a/Ragon/Sources/Lobby/Lobby.cs b/Ragon/Sources/Lobby.cs similarity index 61% rename from Ragon/Sources/Lobby/Lobby.cs rename to Ragon/Sources/Lobby.cs index db15d7d..fcb3c14 100644 --- a/Ragon/Sources/Lobby/Lobby.cs +++ b/Ragon/Sources/Lobby.cs @@ -8,26 +8,23 @@ namespace Ragon.Core; public class Lobby { private readonly ILogger _logger = LogManager.GetCurrentClassLogger(); - private readonly RagonSerializer _serializer; + private readonly Application _application; + private readonly RagonSerializer _writer; private readonly RoomManager _roomManager; private readonly AuthorizationManager _authorizationManager; - private readonly IGameThread _gameThread; public AuthorizationManager AuthorizationManager => _authorizationManager; - public Lobby(IAuthorizationProvider provider, RoomManager manager, IGameThread gameThread) + public Lobby(IApplicationHandler provider, RoomManager manager, Application application) { _roomManager = manager; - _gameThread = gameThread; - _serializer = new RagonSerializer(); - _authorizationManager = new AuthorizationManager(provider, gameThread, this, _serializer); + _application = application; + _writer = new RagonSerializer(); + _authorizationManager = new AuthorizationManager(provider, application, this, _writer); } - public void ProcessEvent(ushort peerId, RagonOperation op, ReadOnlySpan payload) + public void ProcessEvent(ushort peerId, RagonOperation op, RagonSerializer reader) { - _serializer.Clear(); - _serializer.FromSpan(ref payload); - var player = _authorizationManager.GetPlayer(peerId); if (op == RagonOperation.AUTHORIZE) { @@ -37,9 +34,9 @@ public class Lobby return; } - var key = _serializer.ReadString(); - var playerName = _serializer.ReadString(); - var additionalData = _serializer.ReadData(_serializer.Size); + var key = reader.ReadString(); + var playerName = reader.ReadString(); + var additionalData = reader.ReadData(reader.Size); _authorizationManager.OnAuthorization(peerId, key, playerName, additionalData); return; } @@ -54,15 +51,15 @@ public class Lobby { case RagonOperation.JOIN_ROOM: { - var roomId = _serializer.ReadString(); + var roomId = reader.ReadString(); var exists = _roomManager.Rooms.Any(r => r.Id == roomId); if (!exists) { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.JOIN_FAILED); - _serializer.WriteString($"Room with id {roomId} not exists"); - var sendData = _serializer.ToArray(); - _gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable); + _writer.Clear(); + _writer.WriteOperation(RagonOperation.JOIN_FAILED); + _writer.WriteString($"Room with id {roomId} not exists"); + var sendData = _writer.ToArray(); + _application.SocketServer.Send(peerId, sendData, DeliveryType.Reliable); return; } @@ -75,25 +72,25 @@ public class Lobby case RagonOperation.CREATE_ROOM: { var roomId = Guid.NewGuid().ToString(); - var custom = _serializer.ReadBool(); + var custom = reader.ReadBool(); if (custom) { - roomId = _serializer.ReadString(); + roomId = reader.ReadString(); var exists = _roomManager.Rooms.Any(r => r.Id == roomId); if (exists) { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.JOIN_FAILED); - _serializer.WriteString($"Room with id {roomId} already exists"); + _writer.Clear(); + _writer.WriteOperation(RagonOperation.JOIN_FAILED); + _writer.WriteString($"Room with id {roomId} already exists"); - var sendData = _serializer.ToArray(); - _gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable); + var sendData = _writer.ToArray(); + _application.SocketServer.Send(peerId, sendData, DeliveryType.Reliable); return; } } var roomProperties = new RagonRoomParameters(); - roomProperties.Deserialize(_serializer); + roomProperties.Deserialize(reader); if (_roomManager.RoomsBySocket.ContainsKey(peerId)) _roomManager.Left(player, Array.Empty()); @@ -105,7 +102,7 @@ public class Lobby { var roomId = Guid.NewGuid().ToString(); var roomProperties = new RagonRoomParameters(); - roomProperties.Deserialize(_serializer); + roomProperties.Deserialize(reader); if (_roomManager.RoomsBySocket.ContainsKey(peerId)) _roomManager.Left(player, Array.Empty()); @@ -121,7 +118,7 @@ public class Lobby } } - public void OnDisconnected(uint peerId) + public void OnDisconnected(ushort peerId) { _authorizationManager.Cleanup(peerId); } diff --git a/Ragon/Sources/Player/Player.cs b/Ragon/Sources/Player.cs similarity index 50% rename from Ragon/Sources/Player/Player.cs rename to Ragon/Sources/Player.cs index 4b87c3e..bf4a574 100755 --- a/Ragon/Sources/Player/Player.cs +++ b/Ragon/Sources/Player.cs @@ -7,10 +7,22 @@ namespace Ragon.Core { public string Id { get; set; } public string PlayerName { get; set; } - public uint PeerId { get; set; } + public ushort PeerId { get; set; } public bool IsLoaded { get; set; } public List Entities; public List EntitiesIds; + + public void AttachEntity(Entity entity) + { + Entities.Add(entity); + EntitiesIds.Add((entity.EntityId)); + } + + public void DetachEntity(Entity entity) + { + Entities.Remove(entity); + EntitiesIds.Remove(entity.EntityId); + } } } \ No newline at end of file diff --git a/Ragon/Sources/Plugin/IPluginFactory.cs b/Ragon/Sources/Plugin/IPluginFactory.cs index 227569b..2a79149 100644 --- a/Ragon/Sources/Plugin/IPluginFactory.cs +++ b/Ragon/Sources/Plugin/IPluginFactory.cs @@ -3,8 +3,6 @@ namespace Ragon.Core public interface PluginFactory { public PluginBase CreatePlugin(string map); - public IAuthorizationProvider CreateAuthorizationProvider(Configuration configuration); + public IApplicationHandler CreateAuthorizationProvider(Configuration configuration); } - - } \ No newline at end of file diff --git a/Ragon/Sources/Plugin/PluginBase.cs b/Ragon/Sources/Plugin/PluginBase.cs index 8d644b8..98f98ba 100755 --- a/Ragon/Sources/Plugin/PluginBase.cs +++ b/Ragon/Sources/Plugin/PluginBase.cs @@ -14,7 +14,7 @@ namespace Ragon.Core private Dictionary> _entityEvents = new(); private readonly RagonSerializer _serializer = new(); - protected IGameRoom Room { get; private set; } = null!; + protected GameRoom Room { get; private set; } = null!; protected ILogger Logger = null!; public void Attach(GameRoom gameRoom) @@ -149,10 +149,10 @@ namespace Ragon.Core if (!_entityEvents[entityId].ContainsKey(evntCode)) return false; - var player = Room.GetPlayerById(peerId); + // var player = Room.GetPlayerById(peerId); var entity = Room.GetEntityById(entityId); - _entityEvents[entityId][evntCode].Invoke(player, entity, ref payload); + // _entityEvents[entityId][evntCode].Invoke(player, entity, ref payload); return true; } @@ -161,61 +161,14 @@ namespace Ragon.Core { if (_globalEvents.ContainsKey(evntCode)) { - var player = Room.GetPlayerById(peerId); - _globalEvents[evntCode].Invoke(player, ref payload); + // var player = Room.GetPlayerById(peerId); + // _globalEvents[evntCode].Invoke(player, ref payload); return true; } return false; } - - public void ReplicateEvent(Player player, uint eventCode, IRagonSerializable payload) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_EVENT); - - payload.Serialize(_serializer); - - var sendData = _serializer.ToArray(); - Room.Send(player.PeerId, sendData); - } - - public void ReplicateEvent(ushort eventCode, IRagonSerializable payload) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_EVENT); - - payload.Serialize(_serializer); - - var sendData = _serializer.ToArray(); - Room.Broadcast(sendData, DeliveryType.Reliable); - } - - public void ReplicateEntityEvent(Player player, Entity entity, IRagonSerializable payload) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - _serializer.WriteInt(entity.EntityId); - - payload.Serialize(_serializer); - - var sendData = _serializer.ToArray(); - Room.Send(player.PeerId, sendData, DeliveryType.Reliable); - } - - public void ReplicateEntityEvent(Entity entity, IRagonSerializable payload) - { - _serializer.Clear(); - _serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT); - _serializer.WriteInt(entity.EntityId); - - payload.Serialize(_serializer); - - var sendData = _serializer.ToArray(); - Room.Broadcast(sendData); - } - - + #region VIRTUAL public virtual void OnPlayerJoined(Player player) @@ -230,12 +183,14 @@ namespace Ragon.Core { } - public virtual void OnEntityCreated(Player creator, Entity entity) + public virtual bool OnEntityCreated(Player player, Entity entity) { + return false; } - public virtual void OnEntityDestroyed(Player destoyer, Entity entity) + public virtual bool OnEntityDestroyed(Player player, Entity entity) { + return false; } public virtual void OnStart() diff --git a/Ragon/Sources/Room/RoomManager.cs b/Ragon/Sources/RoomManager.cs similarity index 92% rename from Ragon/Sources/Room/RoomManager.cs rename to Ragon/Sources/RoomManager.cs index 889542f..f181162 100644 --- a/Ragon/Sources/Room/RoomManager.cs +++ b/Ragon/Sources/RoomManager.cs @@ -8,7 +8,7 @@ namespace Ragon.Core; public class RoomManager { - private readonly IGameThread _gameThread; + private readonly Application _gameThread; private readonly PluginFactory _factory; private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly List _rooms = new(); @@ -17,7 +17,7 @@ public class RoomManager public IReadOnlyDictionary RoomsBySocket => _roomsBySocket; public IReadOnlyList Rooms => _rooms; - public RoomManager(PluginFactory factory, IGameThread gameThread) + public RoomManager(PluginFactory factory, Application gameThread) { _gameThread = gameThread; _factory = factory; @@ -56,7 +56,7 @@ public class RoomManager var room = new GameRoom(_gameThread, plugin, roomId, map, min, max); room.AddPlayer(creator, payload); - room.Start(); + room.OnStart(); _roomsBySocket.Add(creator.PeerId, room); _rooms.Add(room); @@ -91,7 +91,7 @@ public class RoomManager var room = new GameRoom(_gameThread, plugin, roomId, map, min, max); room.AddPlayer(player, payload); - room.Start(); + room.OnStart(); _roomsBySocket.Add(player.PeerId, room); _rooms.Add(room); @@ -106,11 +106,11 @@ public class RoomManager if (room.PlayersCount < room.PlayersMin) { _logger.Trace($"Room with Id {room.Id} destroyed"); - room.Stop(); + room.OnStop(); _rooms.Remove(room); } - _gameThread.Server.Send(player.PeerId, new byte[] {(byte) RagonOperation.LEAVE_ROOM}, DeliveryType.Reliable); + _gameThread.SocketServer.Send(player.PeerId, new byte[] {(byte) RagonOperation.LEAVE_ROOM}, DeliveryType.Reliable); } } diff --git a/Ragon/Sources/Utils/Scheduler.cs b/Ragon/Sources/Scheduler.cs similarity index 97% rename from Ragon/Sources/Utils/Scheduler.cs rename to Ragon/Sources/Scheduler.cs index c0c75f8..9e22f8a 100644 --- a/Ragon/Sources/Utils/Scheduler.cs +++ b/Ragon/Sources/Scheduler.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; + namespace Ragon.Core { - public class Scheduler: IScheduler + public class Scheduler: IDisposable { List _scheduledTasks; diff --git a/Ragon/Sources/Server/ENet/ENetServer.cs b/Ragon/Sources/Server/ENet/ENetServer.cs index 0454d9f..c9e9552 100755 --- a/Ragon/Sources/Server/ENet/ENetServer.cs +++ b/Ragon/Sources/Server/ENet/ENetServer.cs @@ -14,12 +14,12 @@ namespace Ragon.Core private Address _address; private Event _netEvent; private Peer[] _peers; - private IHandler _handler; + private IEventHandler _eventHandler; private Stopwatch _timer; - public ENetServer(IHandler handler) + public ENetServer(IEventHandler eventHandler) { - _handler = handler; + _eventHandler = eventHandler; _timer = Stopwatch.StartNew(); _peers = Array.Empty(); _host = new Host(); @@ -39,7 +39,7 @@ namespace Ragon.Core _logger.Info($"Protocol: {protocolDecoded}"); } - public void Broadcast(uint[] peersIds, byte[] data, DeliveryType type) + public void Broadcast(ushort[] peersIds, byte[] data, DeliveryType type) { var newPacket = new Packet(); var packetFlags = PacketFlags.Instant; @@ -61,7 +61,7 @@ namespace Ragon.Core _peers[peerId].Send(channel, ref newPacket); } - public void Send(uint peerId, byte[] data, DeliveryType type) + public void Send(ushort peerId, byte[] data, DeliveryType type) { var newPacket = new Packet(); var packetFlags = PacketFlags.Instant; @@ -82,7 +82,7 @@ namespace Ragon.Core _peers[peerId].Send(channel, ref newPacket); } - public void Disconnect(uint peerId, uint errorCode) + public void Disconnect(ushort peerId, uint errorCode) { _peers[peerId].Reset(); } @@ -115,23 +115,28 @@ namespace Ragon.Core // break; // } _peers[_netEvent.Peer.ID] = _netEvent.Peer; - _handler.OnEvent(_netEvent); + _eventHandler.OnConnected((ushort)_netEvent.Peer.ID); break; } case EventType.Disconnect: { - _handler.OnEvent(_netEvent); + _eventHandler.OnDisconnected((ushort)_netEvent.Peer.ID); break; } case EventType.Timeout: { - _handler.OnEvent(_netEvent); + _eventHandler.OnTimeout((ushort)_netEvent.Peer.ID); break; } case EventType.Receive: { - _handler.OnEvent(_netEvent); + var peerId = (ushort) _netEvent.Peer.ID; + var dataRaw = new byte[_netEvent.Packet.Length]; + + _netEvent.Packet.CopyTo(dataRaw); _netEvent.Packet.Dispose(); + + _eventHandler.OnData(peerId, dataRaw); break; } } diff --git a/Ragon/Sources/Server/Http/WebSocketPacket.cs b/Ragon/Sources/Server/Http/WebSocketPacket.cs new file mode 100644 index 0000000..dda1364 --- /dev/null +++ b/Ragon/Sources/Server/Http/WebSocketPacket.cs @@ -0,0 +1,7 @@ +namespace Ragon.Core; + +public class WebSocketPacket +{ + public ushort PeerId; + public byte[] Data; +} \ No newline at end of file diff --git a/Ragon/Sources/Server/Http/WebSocketServer.cs b/Ragon/Sources/Server/Http/WebSocketServer.cs new file mode 100644 index 0000000..33ca38e --- /dev/null +++ b/Ragon/Sources/Server/Http/WebSocketServer.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using NLog; +using Ragon.Common; + +namespace Ragon.Core; + +public class WebSocketServer : ISocketServer +{ + private ushort _idSequencer = 0; + private ILogger _logger = LogManager.GetCurrentClassLogger(); + private Dictionary _webSockets = new Dictionary(); + private Queue _events; + private IEventHandler _eventHandler; + private WebSocketTaskScheduler _webSocketScheduler; + private TaskFactory _taskFactory; + private HttpListener _httpListener; + + public WebSocketServer(IEventHandler eventHandler) + { + _eventHandler = eventHandler; + _events = new Queue(1024); + _webSocketScheduler = new WebSocketTaskScheduler(); + _taskFactory = new TaskFactory(_webSocketScheduler); + } + + async void StartAccept() + { + while (true) + { + var context = await _httpListener.GetContextAsync(); + if (!context.Request.IsWebSocketRequest) continue; + + var webSocketContext = await context.AcceptWebSocketAsync(null); + var webSocket = webSocketContext.WebSocket; + + _idSequencer++; + _webSockets.Add(_idSequencer, webSocket); + + _ = _taskFactory.StartNew(() => StartListen(webSocket, _idSequencer)); + } + } + + async void StartListen(WebSocket webSocket, ushort peerId) + { + _eventHandler.OnConnected(peerId); + + var bytes = new byte[2048]; + var buffer = new Memory(bytes); + while (webSocket.State == WebSocketState.Open) + { + try + { + var result = await webSocket.ReceiveAsync(buffer, CancellationToken.None); + var dataRaw = buffer.Slice(0, result.Count); + _eventHandler.OnData(peerId, dataRaw.ToArray()); + } + catch (Exception ex) + { + break; + } + } + + _eventHandler.OnDisconnected(peerId); + } + + async void ProcessQueue() + { + while (_events.TryDequeue(out var evnt)) + { + if (_webSockets.TryGetValue(evnt.PeerId, out var ws) && ws.State == WebSocketState.Open) + { + await ws.SendAsync(evnt.Data, WebSocketMessageType.Binary, WebSocketMessageFlags.EndOfMessage, CancellationToken.None); + } + } + } + + public void Start(ushort port, int connections, uint protocol) + { + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add($"http://*:{port}/"); + _httpListener.Start(); + + _taskFactory.StartNew(StartAccept); + + var protocolDecoded = (protocol >> 16 & 0xFF) + "." + (protocol >> 8 & 0xFF) + "." + (protocol & 0xFF); + _logger.Info($"Network listening on http://*:{port}/"); + _logger.Info($"Protocol: {protocolDecoded}"); + } + + public void Process() + { + _webSocketScheduler.Process(); + + ProcessQueue(); + } + + public void Stop() + { + _httpListener.Stop(); + } + + public void Send(ushort peerId, byte[] data, DeliveryType type) + { + _events.Enqueue(new WebSocketPacket() {PeerId = peerId, Data = data}); + } + + public void Broadcast(ushort[] peersIds, byte[] data, DeliveryType type) + { + foreach (var peerId in peersIds) + _events.Enqueue(new WebSocketPacket() {PeerId = peerId, Data = data}); + } + + public void Disconnect(ushort peerId, uint errorCode) + { + _webSockets[peerId].CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + } +} \ No newline at end of file diff --git a/Ragon/Sources/Server/Http/WebSocketTaskScheduler.cs b/Ragon/Sources/Server/Http/WebSocketTaskScheduler.cs new file mode 100644 index 0000000..1f8429d --- /dev/null +++ b/Ragon/Sources/Server/Http/WebSocketTaskScheduler.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Threading.Channels; +using System.Threading.Tasks; +using NLog.LayoutRenderers.Wrappers; + +namespace Ragon.Core; + +public class WebSocketTaskScheduler: TaskScheduler +{ + private ChannelReader _reader; + private ChannelWriter _writer; + private Queue _pendingTasks; + + public WebSocketTaskScheduler() + { + var channel = Channel.CreateUnbounded(); + _pendingTasks = new Queue(); + _reader = channel.Reader; + _writer = channel.Writer; + } + + protected override IEnumerable? GetScheduledTasks() + { + throw new NotSupportedException(); + } + + protected override void QueueTask(Task task) + { + _writer.TryWrite(task); + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + return false; + } + + public void Process() + { + while (_reader.TryRead(out var task)) + { + TryExecuteTask(task); + + if (task.Status == TaskStatus.Running) + _pendingTasks.Enqueue(task); + } + + while (_pendingTasks.TryDequeue(out var task)) + _writer.TryWrite(task); + } +} \ No newline at end of file diff --git a/Ragon/Sources/Server/IEventHandler.cs b/Ragon/Sources/Server/IEventHandler.cs new file mode 100644 index 0000000..a1f75ef --- /dev/null +++ b/Ragon/Sources/Server/IEventHandler.cs @@ -0,0 +1,11 @@ +using ENet; + +namespace Ragon.Core; + +public interface IEventHandler +{ + public void OnConnected(ushort peerId); + public void OnDisconnected(ushort peerId); + public void OnTimeout(ushort peerId); + public void OnData(ushort peerId, byte[] data); +} \ No newline at end of file diff --git a/Ragon/Sources/Server/ISocketHandler.cs b/Ragon/Sources/Server/ISocketHandler.cs deleted file mode 100644 index eb8df6b..0000000 --- a/Ragon/Sources/Server/ISocketHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ENet; - -namespace Ragon.Core; - -public interface IHandler -{ - public void OnEvent(Event evnt); -} \ No newline at end of file diff --git a/Ragon/Sources/Server/ISocketServer.cs b/Ragon/Sources/Server/ISocketServer.cs index e4b27f2..90dc2f8 100644 --- a/Ragon/Sources/Server/ISocketServer.cs +++ b/Ragon/Sources/Server/ISocketServer.cs @@ -5,7 +5,7 @@ public interface ISocketServer public void Start(ushort port, int connections, uint protocol); public void Process(); public void Stop(); - public void Send(uint peerId, byte[] data, DeliveryType type); - public void Broadcast(uint[] peersIds, byte[] data, DeliveryType type); - public void Disconnect(uint peerId, uint errorCode); + public void Send(ushort peerId, byte[] data, DeliveryType type); + public void Broadcast(ushort[] peersIds, byte[] data, DeliveryType type); + public void Disconnect(ushort peerId, uint errorCode); } \ No newline at end of file diff --git a/Ragon/Sources/Utils/IDispatcher.cs b/Ragon/Sources/Utils/IDispatcher.cs deleted file mode 100644 index ace4de7..0000000 --- a/Ragon/Sources/Utils/IDispatcher.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Ragon.Core; - -public interface IDispatcher -{ - public void Dispatch(Action action); -} \ No newline at end of file diff --git a/Ragon/Sources/Utils/IDispatcherInternal.cs b/Ragon/Sources/Utils/IDispatcherInternal.cs deleted file mode 100644 index b3ad25e..0000000 --- a/Ragon/Sources/Utils/IDispatcherInternal.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ragon.Core; - -public interface IDispatcherInternal -{ - public void Process(); -} \ No newline at end of file diff --git a/Ragon/Sources/Utils/IScheduler.cs b/Ragon/Sources/Utils/IScheduler.cs deleted file mode 100644 index 9f5fa67..0000000 --- a/Ragon/Sources/Utils/IScheduler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Ragon.Core; - -public interface IScheduler -{ - public SchedulerTask Schedule(Action action, float interval, int count = 1); - public SchedulerTask ScheduleForever(Action action, float interval); - public void StopSchedule(SchedulerTask schedulerTask); - - public void Tick(float deltaTime); -} \ No newline at end of file