This commit is contained in:
2022-12-16 00:05:46 +04:00
parent 6bda468607
commit 4d8ed1105a
83 changed files with 1872 additions and 2387 deletions
+102
View File
@@ -0,0 +1,102 @@
using System.Diagnostics;
using NLog;
using Ragon.Common;
using Ragon.Core.Lobby;
using Ragon.Core.Time;
using Ragon.Server;
using Ragon.Server.ENet;
namespace Ragon.Core;
public class Application : INetworkListener
{
private Logger _logger = LogManager.GetCurrentClassLogger();
private INetworkServer _server;
private Thread _dedicatedThread;
private Configuration _configuration;
private HandlerRegistry _handlerRegistry;
private ILobby _lobby;
private Scheduler _scheduler;
private Dictionary<ushort, PlayerContext> _contexts = new();
public Application(Configuration configuration)
{
_configuration = configuration;
_dedicatedThread = new Thread(Execute);
_dedicatedThread.IsBackground = true;
_handlerRegistry = new HandlerRegistry();
_lobby = new LobbyInMemory();
_scheduler = new Scheduler();
if (configuration.Socket == "enet")
_server = new ENetServer();
Debug.Assert(_server != null, $"Socket type not supported: {configuration.Socket}. Supported: [enet, websocket]");
}
public void Execute()
{
while (true)
{
_scheduler.Tick();
_server.Poll();
Thread.Sleep((int) 1000.0f / _configuration.SendRate);
}
}
public void Start()
{
var networkConfiguration = new NetworkConfiguration()
{
LimitConnections = _configuration.LimitConnections,
Protocol = RagonVersion.Parse(_configuration.Protocol),
Address = "0.0.0.0",
Port = _configuration.Port,
};
_server.Start(this, networkConfiguration);
_dedicatedThread.Start();
}
public void Stop()
{
_server.Stop();
_dedicatedThread.Interrupt();
}
public void OnConnected(INetworkConnection connection)
{
var context = new PlayerContext(connection, new LobbyPlayer(connection));
context.Lobby = _lobby;
context.Scheduler = _scheduler;
_logger.Trace($"Connected {connection.Id}");
_contexts.Add(connection.Id, context);
}
public void OnDisconnected(INetworkConnection connection)
{
_logger.Trace($"Disconnected {connection.Id}");
if (_contexts.Remove(connection.Id, out var context))
{
context.Room?.RemovePlayer(context.RoomPlayer);
context.Dispose();
}
}
public void OnTimeout(INetworkConnection connection)
{
if (_contexts.Remove(connection.Id, out var context))
context.Dispose();
}
public void OnData(INetworkConnection connection, byte[] data)
{
if (_contexts.TryGetValue(connection.Id, out var context))
_handlerRegistry.Handle(context, data);
}
}
+45
View File
@@ -0,0 +1,45 @@
using Newtonsoft.Json;
using NLog;
namespace Ragon.Core;
[Serializable]
public struct Configuration
{
public string Key;
public string Protocol;
public string Socket;
public ushort SendRate;
public ushort Port;
public int SkipTimeout;
public int ReconnectTimeout;
public int LimitConnections;
public int LimitPlayersPerRoom;
public int LimitRooms;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly string ServerVersion = "1.1.0-rc";
private static void CopyrightInfo()
{
Logger.Info($"Server Version: {ServerVersion}");
Logger.Info($"Machine Name: {Environment.MachineName}");
Logger.Info($"OS: {Environment.OSVersion}");
Logger.Info($"Processors: {Environment.ProcessorCount}");
Logger.Info($"Runtime Version: {Environment.Version}");
Logger.Info("==================================");
Logger.Info("| |");
Logger.Info("| Ragon |");
Logger.Info("| |");
Logger.Info("==================================");
}
public static Configuration Load(string filePath)
{
CopyrightInfo();
var data = File.ReadAllText(filePath);
var configuration = JsonConvert.DeserializeObject<Configuration>(data);
return configuration;
}
}
+57
View File
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Threading.Channels;
using System.Threading.Tasks;
using NLog.LayoutRenderers.Wrappers;
namespace Ragon.Core;
public class Executor: TaskScheduler
{
private ChannelReader<Task> _reader;
private ChannelWriter<Task> _writer;
private Queue<Task> _pendingTasks;
public void Run(Task task)
{
task.Start(this);
}
public Executor()
{
var channel = Channel.CreateUnbounded<Task>();
_reader = channel.Reader;
_writer = channel.Writer;
_pendingTasks = new Queue<Task>();
}
protected override IEnumerable<Task>? 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 Execute()
{
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);
}
}
+195
View File
@@ -0,0 +1,195 @@
using Ragon.Common;
namespace Ragon.Core.Game;
public class Entity
{
private static ushort _idGenerator = 0;
public ushort Id { get; private set; }
public RoomPlayer Owner { get; private set; }
public RagonAuthority Authority { get; private set; }
public EntityState State { get; private set; }
public byte[] Payload { get; private set; }
public ushort StaticId { get; private set; }
public ushort Type { get; private set; }
private List<EntityEvent> _bufferedEvents;
public Entity(RoomPlayer owner, ushort type, ushort staticId, RagonAuthority eventAuthority)
{
Owner = owner;
StaticId = staticId;
Type = type;
Id = _idGenerator++;
Payload = Array.Empty<byte>();
Authority = eventAuthority;
State = new EntityState(this);
_bufferedEvents = new List<EntityEvent>();
}
public void SetPayload(byte[] payload)
{
Payload = payload;
}
public void SetOwner(RoomPlayer owner)
{
Owner = owner;
}
public void RestoreBufferedEvents(RoomPlayer roomPlayer, RagonSerializer writer)
{
foreach (var bufferedEvent in _bufferedEvents)
{
writer.Clear();
writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
writer.WriteUShort(bufferedEvent.EventId);
writer.WriteUShort(bufferedEvent.PeerId);
writer.WriteByte((byte) RagonReplicationMode.Server);
writer.WriteUShort(Id);
ReadOnlySpan<byte> data = bufferedEvent.EventData.AsSpan();
writer.WriteData(ref data);
var sendData = writer.ToArray();
roomPlayer.Connection.ReliableChannel.Send(sendData);
}
}
public void Create()
{
var room = Owner.Room;
var serializer = room.Writer;
serializer.Clear();
serializer.WriteOperation(RagonOperation.CREATE_ENTITY);
serializer.WriteUShort(Type);
serializer.WriteUShort(Id);
serializer.WriteUShort(Owner.Connection.Id);
ReadOnlySpan<byte> entityPayload = Payload.AsSpan();
serializer.WriteUShort((ushort) entityPayload.Length);
serializer.WriteData(ref entityPayload);
var sendData = serializer.ToArray();
foreach (var player in room.ReadyPlayersList)
player.Connection.ReliableChannel.Send(sendData);
}
public void Destroy()
{
var room = Owner.Room;
var serializer = room.Writer;
serializer.Clear();
serializer.WriteOperation(RagonOperation.DESTROY_ENTITY);
serializer.WriteInt(Id);
serializer.WriteUShort(0);
// serializer.WriteData(ref Payload);
var sendData = serializer.ToArray();
foreach (var player in room.ReadyPlayersList)
player.Connection.ReliableChannel.Send(sendData);
}
public void ReplicateEvent(
RoomPlayer caller,
ushort eventId,
ReadOnlySpan<byte> payload,
RagonReplicationMode eventMode,
RoomPlayer targetPlayer
)
{
var room = Owner.Room;
var serializer = room.Writer;
serializer.Clear();
serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
serializer.WriteUShort(eventId);
serializer.WriteUShort(caller.Connection.Id);
serializer.WriteByte((byte) eventMode);
serializer.WriteUShort(Id);
serializer.WriteData(ref payload);
var sendData = serializer.ToArray();
targetPlayer.Connection.ReliableChannel.Send(sendData);
}
public void ReplicateEvent(
RoomPlayer caller,
ushort eventId,
ReadOnlySpan<byte> payload,
RagonReplicationMode eventMode,
RagonTarget targetMode
)
{
if (Authority == RagonAuthority.OwnerOnly &&
Owner.Connection.Id != caller.Connection.Id)
{
Console.WriteLine($"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 = caller.Connection.Id,
};
_bufferedEvents.Add(bufferedEvent);
}
var room = Owner.Room;
var serializer = room.Writer;
serializer.Clear();
serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
serializer.WriteUShort(eventId);
serializer.WriteUShort(caller.Connection.Id);
serializer.WriteByte((byte) eventMode);
serializer.WriteUShort(Id);
serializer.WriteData(ref payload);
var sendData = serializer.ToArray();
switch (targetMode)
{
case RagonTarget.Owner:
{
Owner.Connection.ReliableChannel.Send(sendData);
break;
}
case RagonTarget.ExceptOwner:
{
foreach (var roomPlayer in room.ReadyPlayersList)
{
if (roomPlayer.Connection.Id != Owner.Connection.Id)
roomPlayer.Connection.ReliableChannel.Send(sendData);
}
break;
}
case RagonTarget.ExceptInvoker:
{
foreach (var roomPlayer in room.ReadyPlayersList)
{
if (roomPlayer.Connection.Id != caller.Connection.Id)
roomPlayer.Connection.ReliableChannel.Send(sendData);
}
break;
}
case RagonTarget.All:
{
foreach (var roomPlayer in room.ReadyPlayersList)
roomPlayer.Connection.ReliableChannel.Send(sendData);
break;
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
using Ragon.Common;
namespace Ragon.Core.Game;
public class EntityEvent
{
public ushort PeerId { get; set; }
public ushort EventId { get; set; }
public byte[] EventData { get; set; }
public RagonTarget Target { set; get; }
}
+35
View File
@@ -0,0 +1,35 @@
namespace Ragon.Core.Game;
public class EntityList
{
private List<Entity> _dynamicEntitiesList = new List<Entity>();
private List<Entity> _staticEntitesList = new List<Entity>();
private Dictionary<ushort, Entity> _entitiesMap = new Dictionary<ushort, Entity>();
public IReadOnlyList<Entity> StaticList => _staticEntitesList;
public IReadOnlyList<Entity> DynamicList => _dynamicEntitiesList;
public IReadOnlyDictionary<ushort, Entity> Map => _entitiesMap;
public void Add(Entity entity)
{
if (entity.StaticId != 0)
_staticEntitesList.Add(entity);
else
_dynamicEntitiesList.Add(entity);
_entitiesMap.Add(entity.Id, entity);
}
public Entity Remove(Entity entity)
{
if (_entitiesMap.Remove(entity.Id, out var existEntity))
{
_staticEntitesList.Remove(entity);
_dynamicEntitiesList.Remove(entity);
return existEntity;
}
return null;
}
}
+100
View File
@@ -0,0 +1,100 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Game;
public class EntityState
{
private Logger _logger = LogManager.GetCurrentClassLogger();
private List<EntityStateProperty> _properties;
private Entity _entity;
public EntityState(Entity entity, int capacity = 10)
{
_entity = entity;
_properties = new List<EntityStateProperty>(10);
}
public void AddProperty(EntityStateProperty property)
{
_properties.Add(property);
}
public void Write(RagonSerializer serializer)
{
serializer.WriteUShort(_entity.Id);
for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++)
{
var property = _properties[propertyIndex];
if (property.IsDirty)
{
serializer.WriteBool(true);
var span = serializer.GetWritableData(property.Size);
var data = property.Read();
data.CopyTo(span);
property.Clear();
}
else
{
serializer.WriteBool(false);
}
}
}
public void Read(RagonSerializer serializer)
{
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)
{
Console.WriteLine($"Property {i} payload too large, size: {size}");
continue;
}
var propertyPayload = serializer.ReadData(size);
property.Write(ref propertyPayload);
property.Size = size;
}
}
}
public void Snapshot(RagonSerializer serializer)
{
ReadOnlySpan<byte> payload = _entity.Payload.AsSpan();
serializer.WriteUShort(_entity.Type);
serializer.WriteUShort(_entity.Id);
if (_entity.StaticId != 0)
serializer.WriteUShort(_entity.StaticId);
serializer.WriteUShort(_entity.Owner.Connection.Id);
serializer.WriteUShort((ushort) payload.Length);
serializer.WriteData(ref payload);
for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++)
{
var property = _properties[propertyIndex];
var hasPayload = property.IsFixed || property.Size > 0 && !property.IsFixed;
if (hasPayload)
{
serializer.WriteBool(true);
var span = serializer.GetWritableData(property.Size);
var data = property.Read();
data.CopyTo(span);
}
else
{
serializer.WriteBool(false);
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System;
using Ragon.Common;
namespace Ragon.Core.Game;
public class EntityStateProperty
{
public int Size { get; set; }
public int Capacity { get; set; }
public bool IsDirty { get; private set; }
public bool IsFixed { get; private set; }
private byte[] _data;
public EntityStateProperty(int size, bool isFixed)
{
Capacity = 512;
Size = size;
IsFixed = isFixed;
IsDirty = true;
_data = new byte[Capacity];
}
public ReadOnlySpan<byte> Read()
{
var dataSpan = _data.AsSpan();
return dataSpan.Slice(0, Size);
}
public void Write(ref ReadOnlySpan<byte> src)
{
src.CopyTo(_data);
IsDirty = true;
}
public void Clear()
{
IsDirty = false;
}
}
+171
View File
@@ -0,0 +1,171 @@
using Ragon.Common;
namespace Ragon.Core.Game;
public class Room
{
public string Id { get; }
public RoomInformation Info { get; }
public RoomPlayer Owner { get; set; }
public Dictionary<ushort, RoomPlayer> Players { get; }
public List<RoomPlayer> WaitPlayersList { get; private set; }
public List<RoomPlayer> ReadyPlayersList { get; private set; }
public List<RoomPlayer> PlayerList { get; private set; }
public Dictionary<ushort, Entity> Entities { get; private set; }
public List<Entity> DynamicEntitiesList { get; private set; }
public List<Entity> StaticEntitiesList { get; private set; }
public List<Entity> EntityList { get; private set; }
private HashSet<Entity> _entitiesDirtySet;
private RagonSerializer _writer;
public RagonSerializer Writer => _writer;
public PluginBase Plugin { get; set; }
public Room(string roomId, RoomInformation info, PluginBase plugin)
{
Id = roomId;
Info = info;
Plugin = plugin;
Players = new Dictionary<ushort, RoomPlayer>(info.Max);
WaitPlayersList = new List<RoomPlayer>(info.Max);
ReadyPlayersList = new List<RoomPlayer>(info.Max);
PlayerList = new List<RoomPlayer>(info.Max);
Entities = new Dictionary<ushort, Entity>();
DynamicEntitiesList = new List<Entity>();
StaticEntitiesList = new List<Entity>();
EntityList = new List<Entity>();
_entitiesDirtySet = new HashSet<Entity>();
_writer = new RagonSerializer(512);
}
public void AttachEntity(RoomPlayer newOwner, Entity entity)
{
Entities.Add(entity.Id, entity);
EntityList.Add(entity);
if (entity.StaticId == 0)
DynamicEntitiesList.Add(entity);
else
StaticEntitiesList.Add(entity);
entity.Create();
newOwner.Entities.Add(entity);
}
public void DetachEntity(RoomPlayer currentOwner, Entity entity, byte[] payload)
{
Entities.Remove(entity.Id);
EntityList.Remove(entity);
StaticEntitiesList.Remove(entity);
DynamicEntitiesList.Remove(entity);
_entitiesDirtySet.Remove(entity);
entity.Destroy();
currentOwner.Entities.Remove(entity);
}
public void Tick()
{
var entities = (ushort) _entitiesDirtySet.Count;
if (entities > 0)
{
_writer.Clear();
_writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE);
_writer.WriteUShort(entities);
foreach (var entity in _entitiesDirtySet)
entity.State.Write(_writer);
_entitiesDirtySet.Clear();
var sendData = _writer.ToArray();
foreach (var roomPlayer in ReadyPlayersList)
roomPlayer.Connection.UnreliableChannel.Send(sendData);
}
}
public void AddPlayer(RoomPlayer player)
{
if (Players.Count == 0)
Owner = player;
player.Attach(this);
PlayerList.Add(player);
Players.Add(player.Connection.Id, player);
}
public void RemovePlayer(RoomPlayer roomPlayer)
{
if (Players.Remove(roomPlayer.Connection.Id, out var player))
{
PlayerList.Remove(player);
{
_writer.Clear();
_writer.WriteOperation(RagonOperation.PLAYER_LEAVED);
_writer.WriteString(player.Id);
var entitiesToDelete = player.Entities.DynamicList;
_writer.WriteUShort((ushort) entitiesToDelete.Count);
foreach (var entity in entitiesToDelete)
{
_writer.WriteUShort(entity.Id);
EntityList.Remove(entity);
}
var sendData = _writer.ToArray();
Broadcast(sendData);
}
if (roomPlayer.Connection.Id == Owner.Connection.Id && PlayerList.Count > 0)
{
var nextOwner = PlayerList[0];
Owner = nextOwner;
var entitiesToUpdate = roomPlayer.Entities.StaticList;
_writer.Clear();
_writer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED);
_writer.WriteString(Owner.Id);
_writer.WriteUShort((ushort) entitiesToUpdate.Count);
foreach (var entity in entitiesToUpdate)
{
_writer.WriteUShort(entity.Id);
entity.SetOwner(nextOwner);
nextOwner.Entities.Add(entity);
}
var sendData = _writer.ToArray();
Broadcast(sendData);
}
}
}
public void UpdateReadyPlayerList()
{
ReadyPlayersList = PlayerList.Where(p => p.IsLoaded).ToList();
}
public void Track(Entity entity)
{
_entitiesDirtySet.Add(entity);
}
public void Broadcast(byte[] data)
{
foreach (var readyPlayer in ReadyPlayersList)
readyPlayer.Connection.ReliableChannel.Send(data);
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Ragon.Core.Game;
public class RoomInformation
{
public string Map { get; set; }
public int Min { get; set; }
public int Max { get; set; }
public override string ToString()
{
return $"Map: {Map} Count: {Min}/{Max}";
}
}
+36
View File
@@ -0,0 +1,36 @@
using Ragon.Server;
namespace Ragon.Core.Game;
public class RoomPlayer
{
public INetworkConnection Connection { get; }
public string Id { get; }
public string Name { get; }
public bool IsLoaded { get; private set; }
public Room Room { get; private set; }
public EntityList Entities { get; private set; }
public RoomPlayer(INetworkConnection connection, string id, string name)
{
Id = id;
Name = name;
Connection = connection;
Entities = new EntityList();
}
public void Attach(Room room)
{
Room = room;
}
public void Detach()
{
Room = null!;
}
public void SetReady()
{
IsLoaded = true;
}
}
+109
View File
@@ -0,0 +1,109 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Handlers;
namespace Ragon.Core;
public sealed class HandlerRegistry
{
private IHandler _entityEventHandler;
private IHandler _entityCreateHandler;
private IHandler _entityDestroyHandler;
private IHandler _entityStateHandler;
private IHandler _sceneLoadedHandler;
private IHandler _authorizationHandler;
private IHandler _joinOrCreateHandler;
private IHandler _createHandler;
private IHandler _joinHandler;
private IHandler _leaveHandler;
private Logger _logger = LogManager.GetCurrentClassLogger();
private RagonSerializer _reader;
private RagonSerializer _writer;
public HandlerRegistry()
{
_reader = new RagonSerializer(2048);
_writer = new RagonSerializer(2048);
_authorizationHandler = new AuthHandler();
_joinOrCreateHandler = new JoinOrCreateHandler();
_sceneLoadedHandler = new SceneLoadedHandler();
_createHandler = new CreateHandler();
_joinHandler = new JoinHandler();
_leaveHandler = new LeaveHandler();
_entityEventHandler = new EntityEventHandler();
_entityCreateHandler = new EntityCreateHandler();
_entityDestroyHandler = new EntityDestroyHandler();
_entityStateHandler = new EntityStateHandler();
}
public void Handle(PlayerContext context, byte[] data)
{
_writer.Clear();
_reader.Clear();
_reader.FromArray(data);
var operation = _reader.ReadOperation();
switch (operation)
{
case RagonOperation.REPLICATE_ENTITY_EVENT:
{
if (context.RoomPlayer != null)
_entityEventHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.REPLICATE_ENTITY_STATE:
{
if (context.RoomPlayer != null)
_entityStateHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.CREATE_ENTITY:
{
if (context.RoomPlayer != null)
_entityCreateHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.DESTROY_ENTITY:
{
if (context.RoomPlayer != null)
_entityDestroyHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.SCENE_LOADED:
{
if (context.RoomPlayer != null)
_sceneLoadedHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.JOIN_OR_CREATE_ROOM:
{
_joinOrCreateHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.CREATE_ROOM:
{
_createHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.JOIN_ROOM:
{
_joinHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.LEAVE_ROOM:
{
_leaveHandler.Handle(context, _reader, _writer);
break;
}
case RagonOperation.AUTHORIZE:
{
_authorizationHandler.Handle(context, _reader, _writer);
break;
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
using Ragon.Common;
namespace Ragon.Core;
public interface IHandler
{
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer);
}
+39
View File
@@ -0,0 +1,39 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class AuthHandler: IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Authorized)
{
_logger.Warn("Player already authorized");
return;
}
var key = reader.ReadString();
var playerName = reader.ReadString();
var additionalData = reader.ReadData(reader.Size);
context.LobbyPlayer.Name = playerName;
context.LobbyPlayer.AdditionalData = additionalData.ToArray();
context.LobbyPlayer.Status = LobbyPlayerStatus.Authorized;
var playerId = context.LobbyPlayer.Id;
writer.Clear();
writer.WriteOperation(RagonOperation.AUTHORIZED_SUCCESS);
writer.WriteString(playerId);
writer.WriteString(playerName);
var sendData = writer.ToArray();
context.Connection.ReliableChannel.Send(sendData);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} authorized");
}
}
@@ -0,0 +1,32 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Game;
namespace Ragon.Core.Handlers;
public sealed class EntityCreateHandler: IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var entityType = reader.ReadUShort();
var eventAuthority = (RagonAuthority) reader.ReadByte();
var propertiesCount = reader.ReadUShort();
var entity = new Entity(context.RoomPlayer, entityType, 0, eventAuthority);
for (var i = 0; i < propertiesCount; i++)
{
var propertyType = reader.ReadBool();
var propertySize = reader.ReadUShort();
entity.State.AddProperty(new EntityStateProperty(propertySize, propertyType));
}
var entityPayload = reader.ReadData(reader.Size);
entity.SetPayload(entityPayload.ToArray());
context.Room.AttachEntity(context.RoomPlayer, entity);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}");
}
}
@@ -0,0 +1,21 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Handlers;
public sealed class EntityDestroyHandler: IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var entityId = reader.ReadUShort();
if (context.Room.Entities.TryGetValue(entityId, out var entity))
{
var player = context.RoomPlayer;
var payload = reader.ReadData(reader.Size);
context.Room.DetachEntity(player, entity, Array.Empty<byte>());
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} destoyed entity {entity.Id}");
}
}
}
+47
View File
@@ -0,0 +1,47 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Handlers;
public sealed class EntityEventHandler: IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var player = context.RoomPlayer;
var room = context.Room;
var entityId = reader.ReadUShort();
if (!room.Entities.TryGetValue(entityId, out var ent))
{
_logger.Warn($"Entity not found for event with Id {entityId}");
return;
}
var eventId = reader.ReadUShort();
var eventMode = (RagonReplicationMode) reader.ReadByte();
var targetMode = (RagonTarget) reader.ReadByte();
var payloadData = reader.ReadData(reader.Size);
var targetPlayerPeerId = reader.ReadUShort();
if (targetMode == RagonTarget.Player && context.Room.Players.TryGetValue(targetPlayerPeerId, out var targetPlayer))
{
Span<byte> payloadRaw = stackalloc byte[payloadData.Length];
ReadOnlySpan<byte> payload = payloadRaw;
payloadData.CopyTo(payloadRaw);
_logger.Trace($"Event {eventId} Payload: {payloadData.Length} to {targetMode}");
ent.ReplicateEvent(player, eventId, payload, eventMode, targetPlayer);
}
else
{
Span<byte> payloadRaw = stackalloc byte[payloadData.Length];
ReadOnlySpan<byte> payload = payloadRaw;
payloadData.CopyTo(payloadRaw);
_logger.Trace($"Event {eventId} Payload: {payloadData.Length} to {targetMode}");
ent.ReplicateEvent(player, eventId, payload, eventMode, targetMode);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Handlers;
public sealed class EntityStateHandler: IHandler
{
private ILogger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var room = context.Room;
var entitiesCount = reader.ReadUShort();
for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++)
{
var entityId = reader.ReadUShort();
if (room.Entities.TryGetValue(entityId, out var entity))
{
entity.State.Read(reader);
room.Track(entity);
}
else
{
_logger.Error($"Entity with Id {entityId} not found, replication interrupted");
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Game;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class CreateHandler: IHandler
{
private RagonRoomParameters _roomParameters = new();
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
{
_logger.Warn($"Player {context.Connection.Id} not authorized for this request");
return;
}
var custom = reader.ReadBool();
var roomId = Guid.NewGuid().ToString();
if (custom)
{
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();
context.Connection.ReliableChannel.Send(sendData);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} join failed to room {roomId}, room already exist");
return;
}
}
_roomParameters.Deserialize(reader);
var information = new RoomInformation()
{
Map = _roomParameters.Map,
Max = _roomParameters.Max,
Min = _roomParameters.Min,
};
var lobbyPlayer = context.LobbyPlayer;
var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
var room = new Room(roomId, information, new PluginBase());
room.AddPlayer(roomPlayer);
context.Room?.RemovePlayer(context.RoomPlayer);
context.Room = room;
context.RoomPlayer = roomPlayer;
context.Lobby.Persist(room);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} {information}");
JoinSuccess(roomPlayer, room, writer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to room {room.Id}");
}
private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_SUCCESS);
writer.WriteString(room.Id);
writer.WriteString(player.Id);
writer.WriteString(room.Owner.Id);
writer.WriteUShort((ushort) room.Info.Min);
writer.WriteUShort((ushort) room.Info.Max);
writer.WriteString(room.Info.Map);
var sendData = writer.ToArray();
player.Connection.ReliableChannel.Send(sendData);
}
}
+62
View File
@@ -0,0 +1,62 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Game;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class JoinHandler : IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var roomId = reader.ReadString();
var lobbyPlayer = context.LobbyPlayer;
if (!context.Lobby.FindRoomById(roomId, out var existsRoom))
{
JoinFailed(lobbyPlayer, writer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} failed to join room {roomId}");
return;
}
var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
context.Room?.RemovePlayer(context.RoomPlayer);
context.Room = existsRoom;
context.RoomPlayer = roomPlayer;
existsRoom.AddPlayer(roomPlayer);
JoinSuccess(roomPlayer, existsRoom, writer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to {existsRoom.Id}");
}
private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_SUCCESS);
writer.WriteString(room.Id);
writer.WriteString(player.Id);
writer.WriteString(room.Owner.Id);
writer.WriteUShort((ushort) room.Info.Min);
writer.WriteUShort((ushort) room.Info.Max);
writer.WriteString(room.Info.Map);
var sendData = writer.ToArray();
player.Connection.ReliableChannel.Send(sendData);
}
private void JoinFailed(LobbyPlayer player, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_FAILED);
writer.WriteString($"Room not exists");
var sendData = writer.ToArray();
player.Connection.ReliableChannel.Send(sendData);
}
}
@@ -0,0 +1,79 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Game;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class JoinOrCreateHandler : IHandler
{
private RagonRoomParameters _roomParameters = new();
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
{
_logger.Warn("Player not authorized for this request");
return;
}
var roomId = Guid.NewGuid().ToString();
var lobbyPlayer = context.LobbyPlayer;
_roomParameters.Deserialize(reader);
if (context.Lobby.FindRoomByMap(_roomParameters.Map, out var existsRoom))
{
var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
context.Room?.RemovePlayer(context.RoomPlayer);
context.Room = existsRoom;
context.RoomPlayer = roomPlayer;
existsRoom.AddPlayer(roomPlayer);
JoinSuccess(roomPlayer, existsRoom, writer);
}
else
{
var information = new RoomInformation()
{
Map = _roomParameters.Map,
Max = _roomParameters.Max,
Min = _roomParameters.Min,
};
var room = new Room(roomId, information, new PluginBase());
context.Lobby.Persist(room);
var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
room.AddPlayer(roomPlayer);
context.Room?.RemovePlayer(context.RoomPlayer);
context.Room = room;
context.RoomPlayer = roomPlayer;
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} {information}");
JoinSuccess(roomPlayer, room, writer);
}
}
private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_SUCCESS);
writer.WriteString(room.Id);
writer.WriteString(player.Id);
writer.WriteString(room.Owner.Id);
writer.WriteUShort((ushort) room.Info.Min);
writer.WriteUShort((ushort) room.Info.Max);
writer.WriteString(room.Info.Map);
var sendData = writer.ToArray();
player.Connection.ReliableChannel.Send(sendData);
_logger.Trace($"Joined to room {room.Id}");
}
}
+19
View File
@@ -0,0 +1,19 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Handlers;
public sealed class LeaveHandler: IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var room = context.Room;
var roomPlayer = context.RoomPlayer;
if (room != null)
{
context.Room?.RemovePlayer(roomPlayer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} leaved from {room.Id}");
}
}
}
+123
View File
@@ -0,0 +1,123 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Game;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class SceneLoadedHandler : IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
return;
var owner = context.Room.Owner;
var player = context.RoomPlayer;
var room = context.Room;
if (player == owner)
{
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 entity = new Entity(player, entityType, staticId, eventAuthority);
for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++)
{
var propertyType = reader.ReadBool();
var propertySize = reader.ReadUShort();
entity.State.AddProperty(new EntityStateProperty(propertySize, propertyType));
}
room.AttachEntity(player, entity);
}
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} loaded with {statics} scene entities");
room.WaitPlayersList.Add(player);
foreach (var roomPlayer in room.WaitPlayersList)
{
DispatchPlayerJoinExcludePlayer(room, roomPlayer, writer);
roomPlayer.SetReady();
}
room.UpdateReadyPlayerList();
DispatchSnapshot(room, room.WaitPlayersList, writer);
room.WaitPlayersList.Clear();
}
else if (owner.IsLoaded)
{
player.SetReady();
DispatchPlayerJoinExcludePlayer(room, player, writer);
room.UpdateReadyPlayerList();
DispatchSnapshot(room, new List<RoomPlayer>() { player }, writer);
foreach (var entity in room.EntityList)
entity.RestoreBufferedEvents(player, writer);
}
else
{
_logger.Trace($"Player {player.Connection.Id}|{context.LobbyPlayer.Name} waiting owner of room");
room.WaitPlayersList.Add(player);
}
}
private void DispatchPlayerJoinExcludePlayer(Room room, RoomPlayer roomPlayer, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.PLAYER_JOINED);
writer.WriteUShort(roomPlayer.Connection.Id);
writer.WriteString(roomPlayer.Id);
writer.WriteString(roomPlayer.Name);
var sendData = writer.ToArray();
foreach (var awaiter in room.ReadyPlayersList)
{
if (awaiter != roomPlayer)
awaiter.Connection.ReliableChannel.Send(sendData);
}
}
private void DispatchSnapshot(Room room, List<RoomPlayer> receviersList, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.SNAPSHOT);
writer.WriteUShort((ushort) room.ReadyPlayersList.Count);
foreach (var roomPlayer in room.ReadyPlayersList)
{
writer.WriteUShort(roomPlayer.Connection.Id);
writer.WriteString(roomPlayer.Id);
writer.WriteString(roomPlayer.Name);
}
var dynamicEntities = room.DynamicEntitiesList;
var dynamicEntitiesCount = (ushort) dynamicEntities.Count;
writer.WriteUShort(dynamicEntitiesCount);
foreach (var entity in dynamicEntities)
entity.State.Snapshot(writer);
var staticEntities = room.StaticEntitiesList;
var staticEntitiesCount = (ushort) staticEntities.Count;
writer.WriteUShort(staticEntitiesCount);
foreach (var entity in staticEntities)
entity.State.Snapshot(writer);
var sendData = writer.ToArray();
foreach (var player in receviersList)
player.Connection.ReliableChannel.Send(sendData);
}
}
+11
View File
@@ -0,0 +1,11 @@
using Ragon.Core.Game;
namespace Ragon.Core.Lobby;
public interface ILobby
{
public bool FindRoomById(string roomId, out Room room);
public bool FindRoomByMap(string map, out Room room);
public void Persist(Room room);
public void Remove(Room room);
}
+56
View File
@@ -0,0 +1,56 @@
using System.Collections.Generic;
using NLog;
using Ragon.Core.Game;
namespace Ragon.Core.Lobby;
public class LobbyInMemory: ILobby
{
private readonly List<Room> _rooms = new();
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
public bool FindRoomById(string roomId, out Room room)
{
foreach (var existRoom in _rooms)
{
var info = existRoom.Info;
if (existRoom.Id == roomId && info.Min < info.Max)
{
room = existRoom;
return true;
}
}
room = null;
return false;
}
public bool FindRoomByMap(string map, out Room room)
{
foreach (var existRoom in _rooms)
{
var info = existRoom.Info;
if (info.Map == map && existRoom.Players.Count < info.Max)
{
room = existRoom;
return true;
}
}
room = null;
return false;
}
public void Persist(Room room)
{
_rooms.Add(room);
foreach (var r in _rooms)
_logger.Trace($"{r.Id} {r.Info}");
}
public void Remove(Room room)
{
_rooms.Remove(room);
}
}
+27
View File
@@ -0,0 +1,27 @@
using Ragon.Server;
namespace Ragon.Core.Lobby;
public enum LobbyPlayerStatus
{
Unauthorized,
Authorized,
}
public class LobbyPlayer
{
public string Id { get; private set; }
public string Name { get; set; }
public byte[] AdditionalData { get; set; }
public LobbyPlayerStatus Status { get; set; }
public INetworkConnection Connection { get; private set; }
public LobbyPlayer(INetworkConnection connection)
{
Id = Guid.NewGuid().ToString();
Connection = connection;
Status = LobbyPlayerStatus.Unauthorized;
Name = "None";
AdditionalData = Array.Empty<byte>();
}
}
+28
View File
@@ -0,0 +1,28 @@
using NLog;
using Ragon.Core.Game;
using Ragon.Core.Lobby;
using Ragon.Core.Time;
using Ragon.Server;
namespace Ragon.Core;
public class PlayerContext: IDisposable
{
public INetworkConnection Connection { get; }
public Scheduler Scheduler;
public ILobby Lobby { get; set; }
public LobbyPlayer LobbyPlayer { private set; get; }
public Room? Room { get; set; }
public RoomPlayer? RoomPlayer { get; set; }
public PlayerContext(INetworkConnection conn, LobbyPlayer player)
{
Connection = conn;
LobbyPlayer = player;
}
public void Dispose()
{
RoomPlayer?.Room.RemovePlayer(RoomPlayer);
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
<PackageReference Include="NLog" Version="5.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ragon.Protocol\Ragon.Protocol.csproj" />
<ProjectReference Include="..\Ragon.Server.ENet\Ragon.Server.ENet.csproj" />
<ProjectReference Include="..\Ragon.Server.WebSockets\Ragon.Server.WebSockets.csproj" />
<ProjectReference Include="..\Ragon.Server\Ragon.Server.csproj" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
namespace Ragon.Core.Time;
public interface IScheduleTask
{
public void Tick();
}
+27
View File
@@ -0,0 +1,27 @@
namespace Ragon.Core.Time;
public class Scheduler
{
private List<IScheduleTask> _tasks;
public Scheduler()
{
_tasks = new List<IScheduleTask>(35);
}
public void Add(IScheduleTask task)
{
_tasks.Add(task);
}
public void Remove(IScheduleTask task)
{
_tasks.Remove(task);
}
public void Tick()
{
foreach (var task in _tasks)
task.Tick();
}
}