Compare commits

...

15 Commits

Author SHA1 Message Date
edmand46 b793ca3e68 chore: update default socket typoe 2022-12-25 07:34:22 -08:00
edmand46 3936e5c95b chore: update version 2022-12-25 03:16:59 -08:00
edmand46 e9418f4b22 fixed: websocket server wrong execution thread 2022-12-25 03:13:01 -08:00
edmand46 f34b05e6ff chore: update version libs 2022-12-24 01:47:49 -08:00
edmand46 a5a67963be feat: websockets 2022-12-20 12:20:52 -08:00
edmand46 ab85578ccf wip 2022-12-17 21:16:02 +04:00
edmand46 13044357a5 refactor: remove some warnings 2022-12-17 18:58:37 +04:00
edmand46 a9be230960 Merge branch 'develop'
# Conflicts:
#	Ragon/Sources/Application.cs
#	Ragon/Sources/Authorization.cs
#	Ragon/Sources/Configuration.cs
#	Ragon/Sources/Entity/Entity.cs
#	Ragon/Sources/GameRoom.cs
#	Ragon/Sources/IApplicationHandler.cs
#	Ragon/Sources/RoomManager.cs
#	Ragon/Sources/Server/ENet/ENetServer.cs
#	Ragon/Sources/Server/Http/WebSocketServer.cs
2022-12-17 14:08:28 +04:00
edmand46 5a4bf0c24e refactor: entity structure changes 2022-12-17 14:05:53 +04:00
edmand46 fa6ace4dc8 wip 2022-12-16 23:36:51 +04:00
edmand46 4d8ed1105a wip 2022-12-16 00:05:46 +04:00
edmand46 e2ef761bd7 fixed: windows server 2022-12-15 23:55:59 +04:00
edmand46 828112855f fixed: crash on send in websocket server 2022-12-04 23:01:05 +04:00
edmand46 06ff76fe0b fixed: second migration 2022-12-01 22:24:03 +04:00
edmand46 c92b5a5bc4 chore: update readme 2022-11-30 23:56:17 +04:00
90 changed files with 2131 additions and 2439 deletions
+5 -5
View File
@@ -19,7 +19,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}
release_name: Ragon.SimpleServer-${{ github.ref }} release_name: Ragon.Relay-${{ github.ref }}
prerelease: true prerelease: true
build: build:
@@ -53,10 +53,10 @@ jobs:
- name: Build - name: Build
shell: bash shell: bash
run: | run: |
release_name="Ragon.SimpleServer-${{ env.TAG }}-${{ matrix.target }}" release_name="Ragon.Relay-${{ env.TAG }}-${{ matrix.target }}"
# Build everything # Build everything
dotnet publish Ragon.SimpleServer/Ragon.SimpleServer.csproj -c Release --runtime "${{ matrix.target }}" -p:PublishSingleFile=true -o "$release_name" dotnet publish Ragon.Relay/Ragon.Relay.csproj -c Release --runtime "${{ matrix.target }}" -p:PublishSingleFile=true -o "$release_name"
# Pack files # Pack files
7z a -tzip "${release_name}.zip" "./${release_name}/*" 7z a -tzip "${release_name}.zip" "./${release_name}/*"
@@ -69,6 +69,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ needs.release.outputs.upload_url }} upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: Ragon.SimpleServer-${{ env.TAG }}-${{ matrix.target }}.zip asset_path: Ragon.Relay-${{ env.TAG }}-${{ matrix.target }}.zip
asset_name: Ragon.SimpleServer-${{ env.TAG }}-${{ matrix.target }}.zip asset_name: Ragon.Relay-${{ env.TAG }}-${{ matrix.target }}.zip
asset_content_type: application/zip asset_content_type: application/zip
+6
View File
@@ -0,0 +1,6 @@
namespace Ragon.Core.Time;
public interface IAction
{
public void Tick();
}
+28
View File
@@ -0,0 +1,28 @@
namespace Ragon.Core.Time;
public class Loop
{
private List<IAction> _tasks;
public Loop()
{
_tasks = new List<IAction>(35);
}
public void Run(IAction task)
{
_tasks.Add(task);
}
public void Stop(IAction task)
{
_tasks.Remove(task);
}
public void Tick()
{
foreach (var task in _tasks)
task.Tick();
}
}
+115
View File
@@ -0,0 +1,115 @@
using System.Diagnostics;
using NLog;
using Ragon.Common;
using Ragon.Core.Lobby;
using Ragon.Core.Server;
using Ragon.Core.Time;
using Ragon.Server;
using Ragon.Server.ENet;
namespace Ragon.Core;
public class Application : INetworkListener
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly INetworkServer _server;
private readonly Thread _dedicatedThread;
private readonly Executor _executor;
private readonly Configuration _configuration;
private readonly HandlerRegistry _handlerRegistry;
private readonly ILobby _lobby;
private readonly Loop _loop;
private readonly Dictionary<ushort, PlayerContext> _contexts;
public Application(Configuration configuration)
{
_configuration = configuration;
_executor = new Executor();
_dedicatedThread = new Thread(Execute);
_dedicatedThread.IsBackground = true;
_contexts = new Dictionary<ushort, PlayerContext>();
_handlerRegistry = new HandlerRegistry();
_lobby = new LobbyInMemory();
_loop = new Loop();
if (configuration.ServerType == "enet")
_server = new ENetServer();
if (configuration.ServerType == "websocket")
_server = new NativeWebSocketServer(_executor);
Debug.Assert(_server != null, $"Socket type not supported: {configuration.ServerType}. Supported: [enet, websocket]");
}
public void Execute()
{
while (true)
{
_executor.Execute();
_loop.Tick();
_server.Poll();
Thread.Sleep((int)1000.0f / _configuration.ServerTickRate);
}
}
public void Start()
{
var networkConfiguration = new NetworkConfiguration()
{
LimitConnections = _configuration.LimitConnections,
Protocol = RagonVersion.Parse(_configuration.GameProtocol),
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.Loop = _loop;
_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))
{
var room = context.Room;
if (room != null)
{
room.RemovePlayer(context.RoomPlayer);
_lobby.RemoveIfEmpty(room);
}
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);
}
}
+43
View File
@@ -0,0 +1,43 @@
using Newtonsoft.Json;
using NLog;
namespace Ragon.Core;
[Serializable]
public struct Configuration
{
public string ServerKey;
public string ServerType;
public ushort ServerTickRate;
public string GameProtocol;
public ushort Port;
public int LimitConnections;
public int LimitPlayersPerRoom;
public int LimitRooms;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private static readonly string ServerVersion = "1.0.31-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;
}
}
+188
View File
@@ -0,0 +1,188 @@
using Ragon.Common;
namespace Ragon.Core.Game;
public class Entity
{
private static ushort _idGenerator = 0;
public ushort Id { get; private set; }
public ushort Type { get; private set; }
public ushort StaticId { 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; }
private readonly 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.Invoker.Connection.Id);
writer.WriteByte((byte)RagonReplicationMode.Server);
writer.WriteUShort(Id);
ReadOnlySpan<byte> data = bufferedEvent.EventData.AsSpan();
writer.WriteData(ref data);
var sendData = writer.ToArray();
roomPlayer.Connection.Reliable.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.Reliable.Send(sendData);
}
public void Destroy(byte[] payload)
{
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.Reliable.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.Reliable.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(caller, eventId, payload.ToArray(), targetMode);
_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.Reliable.Send(sendData);
break;
}
case RagonTarget.ExceptOwner:
{
foreach (var roomPlayer in room.ReadyPlayersList)
{
if (roomPlayer.Connection.Id != Owner.Connection.Id)
roomPlayer.Connection.Reliable.Send(sendData);
}
break;
}
case RagonTarget.ExceptInvoker:
{
foreach (var roomPlayer in room.ReadyPlayersList)
{
if (roomPlayer.Connection.Id != caller.Connection.Id)
roomPlayer.Connection.Reliable.Send(sendData);
}
break;
}
case RagonTarget.All:
{
foreach (var roomPlayer in room.ReadyPlayersList)
roomPlayer.Connection.Reliable.Send(sendData);
break;
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using Ragon.Common;
namespace Ragon.Core.Game;
public class EntityEvent
{
public RoomPlayer Invoker { get; private set; }
public ushort EventId { get; private set; }
public byte[] EventData { get; private set; }
public RagonTarget Target { set; private get; }
public EntityEvent(
RoomPlayer invoker,
ushort eventId,
byte[] payload,
RagonTarget target
)
{
Invoker = invoker;
EventId = eventId;
EventData = payload;
Target = target;
}
}
+34
View File
@@ -0,0 +1,34 @@
namespace Ragon.Core.Game;
public class EntityList
{
private readonly List<Entity> _dynamicEntitiesList = new List<Entity>();
private readonly List<Entity> _staticEntitiesList = new List<Entity>();
private readonly Dictionary<ushort, Entity> _entitiesMap = new Dictionary<ushort, Entity>();
public IReadOnlyList<Entity> StaticList => _staticEntitiesList;
public IReadOnlyList<Entity> DynamicList => _dynamicEntitiesList;
public IReadOnlyDictionary<ushort, Entity> Map => _entitiesMap;
public void Add(Entity entity)
{
if (entity.StaticId != 0)
_staticEntitiesList.Add(entity);
else
_dynamicEntitiesList.Add(entity);
_entitiesMap.Add(entity.Id, entity);
}
public bool Remove(Entity entity)
{
if (_entitiesMap.Remove(entity.Id, out var existEntity))
{
_staticEntitiesList.Remove(entity);
_dynamicEntitiesList.Remove(entity);
return true;
}
return false;
}
}
+99
View File
@@ -0,0 +1,99 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Game;
public class EntityState
{
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);
}
}
}
}
@@ -1,9 +1,9 @@
using System; using System;
using Ragon.Common; using Ragon.Common;
namespace Ragon.Core; namespace Ragon.Core.Game;
public class EntityProperty public class EntityStateProperty
{ {
public int Size { get; set; } public int Size { get; set; }
public int Capacity { get; set; } public int Capacity { get; set; }
@@ -11,7 +11,7 @@ public class EntityProperty
public bool IsFixed { get; private set; } public bool IsFixed { get; private set; }
private byte[] _data; private byte[] _data;
public EntityProperty(int size, bool isFixed) public EntityStateProperty(int size, bool isFixed)
{ {
Capacity = 512; Capacity = 512;
Size = size; Size = size;
@@ -24,8 +24,8 @@ public class EntityProperty
public ReadOnlySpan<byte> Read() public ReadOnlySpan<byte> Read()
{ {
var dataSpan = _data.AsSpan(); var dataSpan = _data.AsSpan();
var src = dataSpan.Slice(0, Size);
return dataSpan.Slice(0, Size); return src;
} }
public void Write(ref ReadOnlySpan<byte> src) public void Write(ref ReadOnlySpan<byte> src)
+166
View File
@@ -0,0 +1,166 @@
using Ragon.Common;
using Ragon.Core.Time;
namespace Ragon.Core.Game;
public class Room: IAction
{
public string Id { get; private set; }
public RoomInformation Info { get; private set; }
public RoomPlayer Owner { get; private set; }
public RagonSerializer Writer { get; }
public Dictionary<ushort, RoomPlayer> Players { get; private set; }
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 readonly HashSet<Entity> _entitiesDirtySet;
public Room(string roomId, RoomInformation info)
{
Id = roomId;
Info = info;
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(payload);
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.Unreliable.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.Reliable.Send(data);
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Ragon.Core.Game;
public class RoomInformation
{
public string Map { get; init; } = "none";
public int Min { get; init; }
public int Max { get; init; }
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 AuthorizationHandler();
_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);
}
@@ -0,0 +1,39 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class AuthorizationHandler: 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.Reliable.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);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
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");
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
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.Reliable.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);
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);
context.Loop.Run(room);
_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.Reliable.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.Reliable.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.Reliable.Send(sendData);
}
}
@@ -0,0 +1,81 @@
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);
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);
context.Loop.Run(room);
}
}
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.Reliable.Send(sendData);
_logger.Trace($"{player.Connection.Id}|{player.Name} 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}");
}
}
}
+124
View File
@@ -0,0 +1,124 @@
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));
}
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}");
room.AttachEntity(player, entity);
}
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} loaded");
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.Reliable.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.Reliable.Send(sendData);
}
}
+12
View File
@@ -0,0 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using Ragon.Core.Game;
namespace Ragon.Core.Lobby;
public interface ILobby
{
public bool FindRoomById(string roomId, [MaybeNullWhen(false)] out Room room);
public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out Room room);
public void Persist(Room room);
public void RemoveIfEmpty(Room room);
}
+61
View File
@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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, [MaybeNullWhen(false)] 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 = default;
return false;
}
public bool FindRoomByMap(string map, [MaybeNullWhen(false)] 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 = default;
return false;
}
public void Persist(Room room)
{
_rooms.Add(room);
foreach (var r in _rooms)
_logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count}");
}
public void RemoveIfEmpty(Room room)
{
if (room.Players.Count == 0)
_rooms.Remove(room);
foreach (var r in _rooms)
_logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count}");
}
}
+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 Loop Loop;
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.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ragon.Protocol\Ragon.Protocol.csproj" />
<ProjectReference Include="..\Ragon.Server.ENet\Ragon.Server.ENet.csproj" />
<ProjectReference Include="..\Ragon.Server.NativeWebSockets\Ragon.Server.NativeWebSockets.csproj" />
<ProjectReference Include="..\Ragon.Server\Ragon.Server.csproj" />
</ItemGroup>
</Project>
@@ -5,6 +5,7 @@
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<LangVersion>8</LangVersion> <LangVersion>8</LangVersion>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>Ragon.Common</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -6,5 +6,6 @@ namespace Ragon.Common
ExceptOwner, ExceptOwner,
ExceptInvoker, ExceptInvoker,
All, All,
Player,
} }
} }
+28
View File
@@ -0,0 +1,28 @@
namespace Ragon.Common
{
public static class RagonVersion
{
public static uint Parse(string version)
{
var strings = version.Split(".");
if (strings.Length < 3)
return 0;
var parts = new uint[] {0, 0, 0};
for (int i = 0; i < parts.Length; i++)
{
if (!uint.TryParse(strings[i], out var v))
return 0;
parts[i] = v;
}
return (parts[0] << 16) | (parts[1] << 8) | parts[2];
}
public static string Parse(uint version)
{
return (version >> 16 & 0xFF) + "." + (version >> 8 & 0xFF) + "." + (version & 0xFF);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices;
using NLog;
using Ragon.Core;
namespace Ragon.Relay
{
class Program
{
static void Main(string[] args)
{
var logger = LogManager.GetLogger("Ragon.Relay");
logger.Info("Relay Application");
var configuration = Configuration.Load("relay.config.json");
var relay = new Application(configuration);
logger.Info("Started");
relay.Start();
Console.ReadKey();
relay.Stop();
logger.Info("Stopped");
}
}
}
@@ -14,13 +14,13 @@
<None Update="NLog.config"> <None Update="NLog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="config.json"> <None Update="relay.config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ragon\Ragon.csproj" /> <ProjectReference Include="..\Ragon.Core\Ragon.Core.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+10
View File
@@ -0,0 +1,10 @@
{
"serverKey": "defaultkey",
"serverType": "enet",
"serverTickRate": 20,
"gameProtocol": "1.0.0",
"port": 5000,
"limitConnections": 4095,
"limitPlayersPerRoom": 20,
"limitRooms": 200
}
+17
View File
@@ -0,0 +1,17 @@
using ENet;
namespace Ragon.Server.ENet;
public sealed class ENetConnection: INetworkConnection
{
public ushort Id { get; }
public INetworkChannel Reliable { get; private set; }
public INetworkChannel Unreliable { get; private set; }
public ENetConnection(Peer peer)
{
Id = (ushort) peer.ID;
Reliable = new ENetReliableChannel(peer, 0);
Unreliable = new ENetUnreliableChannel(peer, 1);
}
}
+23
View File
@@ -0,0 +1,23 @@
using ENet;
namespace Ragon.Server.ENet;
public sealed class ENetReliableChannel: INetworkChannel
{
private Peer _peer;
private byte _channelId;
public ENetReliableChannel(Peer peer, int channelId)
{
_peer = peer;
_channelId = (byte) channelId;
}
public void Send(byte[] data)
{
var newPacket = new Packet();
newPacket.Create(data, data.Length, PacketFlags.Reliable);
_peer.Send(_channelId, ref newPacket);
}
}
+116
View File
@@ -0,0 +1,116 @@
using System.Diagnostics;
using ENet;
using NLog;
using Ragon.Common;
namespace Ragon.Server.ENet
{
public sealed class ENetServer: INetworkServer
{
private readonly Host _host;
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private ENetConnection[] _connections;
private uint _protocol;
private INetworkListener _listener;
private Event _event;
public ENetServer()
{
_host = new Host();
_connections = Array.Empty<ENetConnection>();
}
public void Start(INetworkListener listener, NetworkConfiguration configuration)
{
Library.Initialize();
_connections = new ENetConnection[configuration.LimitConnections];
_listener = listener;
_protocol = configuration.Protocol;
var address = new Address { Port = (ushort) configuration.Port };
_host.Create(address, _connections.Length, 2, 0, 0, 1024 * 1024);
var protocolDecoded = RagonVersion.Parse(_protocol);
_logger.Info($"Listen at 127.0.0.1:{configuration.Port}");
_logger.Info($"Protocol: {protocolDecoded}");
}
public void Poll()
{
bool polled = false;
while (!polled)
{
if (_host.CheckEvents(out _event) <= 0)
{
if (_host.Service(0, out _event) <= 0)
break;
polled = true;
}
switch (_event.Type)
{
case EventType.None:
{
_logger.Trace("None event");
break;
}
case EventType.Connect:
{
if (!IsValidProtocol(_event.Data))
{
_logger.Warn($"Mismatched protocol Server: {RagonVersion.Parse(_protocol)} Client: {RagonVersion.Parse(_event.Data)}, close connection");
_event.Peer.DisconnectNow(0);
break;
}
var connection = new ENetConnection(_event.Peer);
_connections[_event.Peer.ID] = connection;
_listener.OnConnected(connection);
break;
}
case EventType.Disconnect:
{
var connection = _connections[_event.Peer.ID];
_listener.OnDisconnected(connection);
break;
}
case EventType.Timeout:
{
var connection = _connections[_event.Peer.ID];
_listener.OnTimeout(connection);
break;
}
case EventType.Receive:
{
var peerId = (ushort) _event.Peer.ID;
var connection = _connections[peerId];
var dataRaw = new byte[_event.Packet.Length];
_event.Packet.CopyTo(dataRaw);
_event.Packet.Dispose();
_listener.OnData(connection, dataRaw);
break;
}
}
}
}
public void Stop()
{
_host?.Dispose();
Library.Deinitialize();
}
private bool IsValidProtocol(uint protocol)
{
return protocol == _protocol;
}
}
}
@@ -0,0 +1,23 @@
using ENet;
namespace Ragon.Server.ENet;
public sealed class ENetUnreliableChannel: INetworkChannel
{
private Peer _peer;
private byte _channelId;
public ENetUnreliableChannel(Peer peer, int channelId)
{
_peer = peer;
_channelId = (byte) channelId;
}
public void Send(byte[] data)
{
var newPacket = new Packet();
newPacket.Create(data, data.Length, PacketFlags.None);
_peer.Send(_channelId, ref newPacket);
}
}
@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>StressTest</RootNamespace> <RootNamespace>Ragon.ENet</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ragon.Common\Ragon.Common.csproj" /> <PackageReference Include="ENet-CSharp" Version="2.4.8" />
<PackageReference Include="NLog" Version="5.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ENet-CSharp" Version="2.4.8" /> <ProjectReference Include="..\Ragon.Server\Ragon.Server.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Ragon.WebSockets</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ragon.Server\Ragon.Server.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.1.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
using System.Net.WebSockets;
using NLog;
namespace Ragon.Server.NativeWebSockets;
public sealed class WebSocketConnection : INetworkConnection
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public ushort Id { get; }
public INetworkChannel Reliable { get; private set; }
public INetworkChannel Unreliable { get; private set; }
public WebSocket Socket { get; private set; }
private WebSocketReliableChannel[] _channels;
public WebSocketConnection(WebSocket webSocket, ushort peerId)
{
Id = peerId;
Socket = webSocket;
var reliableChannel = new WebSocketReliableChannel(webSocket);
var unreliableChannel = new WebSocketReliableChannel(webSocket);
_channels = new[] { reliableChannel, unreliableChannel };
Reliable = reliableChannel;
Unreliable = unreliableChannel;
}
public async Task Flush()
{
foreach (var channel in _channels)
{
try
{
await channel.Flush();
}
catch (Exception ex)
{
_logger.Error(ex);
}
}
}
}
@@ -0,0 +1,27 @@
using System.Net.WebSockets;
using Ragon.Server;
namespace Ragon.Server.NativeWebSockets;
public class WebSocketReliableChannel : INetworkChannel
{
private Queue<byte[]> _queue;
private WebSocket _socket;
public WebSocketReliableChannel(WebSocket webSocket)
{
_socket = webSocket;
_queue = new Queue<byte[]>(512);
}
public void Send(byte[] data)
{
_queue.Enqueue(data);
}
public async Task Flush()
{
while (_queue.TryDequeue(out var sendData) && _socket.State == WebSocketState.Open)
await _socket.SendAsync(sendData, WebSocketMessageType.Binary, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets;
using NLog;
using Ragon.Common;
using Ragon.Core.Server;
using Ragon.Server;
using Ragon.Server.NativeWebSockets;
namespace Ragon.Core;
public class NativeWebSocketServer : INetworkServer
{
private ILogger _logger = LogManager.GetCurrentClassLogger();
private INetworkListener _networkListener;
private Stack<ushort> _sequencer;
private Executor _executor;
private HttpListener _httpListener;
private WebSocketConnection[] _connections;
private List<WebSocketConnection> _activeConnections;
private CancellationTokenSource _cancellationTokenSource;
public NativeWebSocketServer(Executor executor)
{
_sequencer = new Stack<ushort>();
_connections = Array.Empty<WebSocketConnection>();
_activeConnections = new List<WebSocketConnection>();
_executor = executor;
}
public async void StartAccept(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var context = await _httpListener.GetContextAsync();
if (!context.Request.IsWebSocketRequest) continue;
var webSocketContext = await context.AcceptWebSocketAsync(null);
var webSocket = webSocketContext.WebSocket;
var peerId = _sequencer.Pop();
var connection = new WebSocketConnection(webSocket, peerId);
_connections[peerId] = connection;
StartListen(connection, cancellationToken);
}
}
async void StartListen(WebSocketConnection connection, CancellationToken cancellationToken)
{
_activeConnections.Add(connection);
_networkListener.OnConnected(connection);
var webSocket = connection.Socket;
var bytes = new byte[2048];
var buffer = new Memory<byte>(bytes);
while (
webSocket.State == WebSocketState.Open ||
!cancellationToken.IsCancellationRequested)
{
try
{
var result = await webSocket.ReceiveAsync(buffer, cancellationToken);
var dataRaw = buffer.Slice(0, result.Count);
_networkListener.OnData(connection, dataRaw.ToArray());
}
catch (Exception ex)
{
break;
}
}
_sequencer.Push(connection.Id);
_activeConnections.Remove(connection);
_networkListener.OnDisconnected(connection);
}
public void Poll()
{
Flush();
}
public async void Flush()
{
foreach (var conn in _activeConnections)
await conn.Flush();
}
public void Start(
INetworkListener listener,
NetworkConfiguration configuration
)
{
_networkListener = listener;
_cancellationTokenSource = new CancellationTokenSource();
var limit = (ushort)configuration.LimitConnections;
for (ushort i = limit; i != 0; i--)
_sequencer.Push(i);
_sequencer.Push(0);
_connections = new WebSocketConnection[configuration.LimitConnections];
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://127.0.0.1:{configuration.Port}/");
_httpListener.Start();
_executor.Run(() => StartAccept(_cancellationTokenSource.Token));
var protocolDecoded = RagonVersion.Parse(configuration.Protocol);
_logger.Info($"Listen at http://*:{configuration.Port}/");
_logger.Info($"Protocol: {protocolDecoded}");
}
public void Stop()
{
_cancellationTokenSource.Cancel();
_httpListener.Stop();
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Ragon.Server;
public interface INetworkChannel
{
void Send(byte[] data);
}
+8
View File
@@ -0,0 +1,8 @@
namespace Ragon.Server;
public interface INetworkConnection
{
public ushort Id { get; }
public INetworkChannel Reliable { get; }
public INetworkChannel Unreliable { get; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace Ragon.Server;
public interface INetworkListener
{
void OnConnected(INetworkConnection connection);
void OnDisconnected(INetworkConnection connection);
void OnTimeout(INetworkConnection connection);
void OnData(INetworkConnection connection, byte[] data);
}
+8
View File
@@ -0,0 +1,8 @@
namespace Ragon.Server;
public interface INetworkServer
{
public void Stop();
public void Poll();
public void Start(INetworkListener listener, NetworkConfiguration configuration);
}
@@ -1,23 +1,27 @@
using System;
using System.Collections.Generic;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks;
using NLog.LayoutRenderers.Wrappers;
namespace Ragon.Core; namespace Ragon.Core.Server;
public class WebSocketTaskScheduler: TaskScheduler public class Executor: TaskScheduler
{ {
private ChannelReader<Task> _reader; private ChannelReader<Task> _reader;
private ChannelWriter<Task> _writer; private ChannelWriter<Task> _writer;
private Queue<Task> _pendingTasks; private Queue<Task> _pendingTasks;
private TaskFactory _taskFactory;
public WebSocketTaskScheduler() public void Run(Action action)
{
_taskFactory.StartNew(action);
}
public Executor()
{ {
var channel = Channel.CreateUnbounded<Task>(); var channel = Channel.CreateUnbounded<Task>();
_pendingTasks = new Queue<Task>();
_reader = channel.Reader; _reader = channel.Reader;
_writer = channel.Writer; _writer = channel.Writer;
_taskFactory = new TaskFactory(this);
_pendingTasks = new Queue<Task>();
} }
protected override IEnumerable<Task>? GetScheduledTasks() protected override IEnumerable<Task>? GetScheduledTasks()
@@ -35,7 +39,7 @@ public class WebSocketTaskScheduler: TaskScheduler
return false; return false;
} }
public void Process() public void Execute()
{ {
while (_reader.TryRead(out var task)) while (_reader.TryRead(out var task))
{ {
+9
View File
@@ -0,0 +1,9 @@
namespace Ragon.Server;
public struct NetworkConfiguration
{
public int LimitConnections { get; set; }
public int Port { get; set; }
public uint Protocol { get; set; }
public string Address { get; set; }
}
+13
View File
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ragon.Protocol\Ragon.Protocol.csproj" />
</ItemGroup>
</Project>
-19
View File
@@ -1,19 +0,0 @@
using System;
using System.Threading;
using Game.Source;
using Ragon.Core;
namespace SimpleServer
{
class Program
{
static void Main(string[] args)
{
var bootstrap = new Bootstrap();
var app = bootstrap.Configure(new SimplePluginFactory());
app.Start();
Console.Read();
app.Stop();
}
}
}
@@ -1,44 +0,0 @@
using System;
using System.Threading.Tasks;
using Ragon.Core;
namespace Game.Source;
public class ApplicationHandlerByKey: IApplicationHandler
{
private Configuration _configuration;
public ApplicationHandlerByKey(Configuration configuration)
{
_configuration = configuration;
}
public async Task OnAuthorizationRequest(string key, string name, byte[] additionalData, Action<string, string> accept, Action<uint> reject)
{
if (key == _configuration.Key)
{
var playerId = Guid.NewGuid().ToString();
var playerName = name;
accept(playerId, playerName);
}
else
{
reject(0);
}
}
public void OnCustomEvent(ushort peerId, ReadOnlySpan<byte> payload)
{
}
public void OnJoin(ushort peerId)
{
}
public void OnLeave(ushort peerId)
{
}
}
@@ -1,41 +0,0 @@
using NLog.Fluent;
using Ragon.Core;
namespace Game.Source
{
public class SimplePlugin: PluginBase
{
public override void OnStart()
{
// _logger.Info("Plugin started");
}
public override void OnStop()
{
// _logger.Info("Plugin stopped");
}
public override void OnPlayerJoined(Player player)
{
// Logger.Info($"Player({player.PlayerName}) joined to Room({Room.Id})");
}
public override void OnPlayerLeaved(Player player)
{
// Logger.Info($"Player({player.PlayerName}) left from Room({Room.Id})");
}
public override bool OnEntityCreated(Player player, Entity entity)
{
return false;
// Logger.Info($"Player({player.PlayerName}) create entity {entity.EntityId}:{entity.EntityType}");
}
public override bool OnEntityDestroyed(Player player, Entity entity)
{
return false;
// Logger.Info($"Player({player.PlayerName}) destroy entity {entity.EntityId}:{entity.EntityType}");
}
}
}
@@ -1,18 +0,0 @@
using System.Runtime.InteropServices;
using Ragon.Core;
namespace Game.Source
{
public class SimplePluginFactory : PluginFactory
{
public PluginBase CreatePlugin(string map)
{
return new SimplePlugin();
}
public IApplicationHandler CreateAuthorizationProvider(Configuration configuration)
{
return new ApplicationHandlerByKey(configuration);
}
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"key": "defaultkey",
"socket": "udp",
"protocol": "1.0.0",
"statisticsInterval": 5,
"sendRate": 50,
"port": 4444,
"skipTimeout": 60,
"reconnectTimeout": 300,
"maxConnections": 4095,
"maxPlayersPerRoom": 20,
"maxRooms": 200
}
-206
View File
@@ -1,206 +0,0 @@
using System;
using ENet;
using Ragon.Common;
namespace Stress
{
class SimulationClient
{
public Host Host;
public Peer Peer;
public bool InRoom;
public List<int> Entities = new List<int>();
}
class SimulationThread
{
private List<SimulationClient> _clients = new List<SimulationClient>();
public void Start(string url, ushort port, int numClients)
{
for (var i = 0; i < numClients; i++)
{
var client = CreateClient(url, port);
_clients.Add(client);
}
var thread = new Thread(Execute);
thread.IsBackground = true;
thread.Start();
}
public void Execute()
{
var ragonSerializer = new RagonSerializer();
while (true)
{
foreach (SimulationClient simulationClient in _clients)
{
bool polled = false;
Event netEvent;
while (!polled)
{
if (simulationClient.Host.CheckEvents(out netEvent) <= 0)
{
if (simulationClient.Host.Service(0, out netEvent) <= 0)
break;
polled = true;
}
switch (netEvent.Type)
{
case EventType.None:
break;
case EventType.Connect:
{
ragonSerializer.Clear();
ragonSerializer.WriteOperation(RagonOperation.AUTHORIZE);
ragonSerializer.WriteString("defaultkey");
ragonSerializer.WriteString("Player " + DateTime.Now.Ticks);
ragonSerializer.WriteByte(0);
var sendData = ragonSerializer.ToArray();
var packet = new Packet();
packet.Create(sendData, PacketFlags.Reliable);
simulationClient.Peer.Send(0, ref packet);
Console.WriteLine("Client connected to server");
break;
}
case EventType.Disconnect:
Console.WriteLine("Client disconnected from server");
break;
case EventType.Timeout:
Console.WriteLine("Client connection timeout");
break;
case EventType.Receive:
var data = new byte[netEvent.Packet.Length];
netEvent.Packet.CopyTo(data);
var op = (RagonOperation) data[0];
switch (op)
{
case RagonOperation.AUTHORIZED_SUCCESS:
{
ragonSerializer.Clear();
ragonSerializer.WriteOperation(RagonOperation.JOIN_OR_CREATE_ROOM);
ragonSerializer.WriteString("map");
ragonSerializer.WriteInt(1);
ragonSerializer.WriteInt(5);
var sendData = ragonSerializer.ToArray();
var packet = new Packet();
packet.Create(sendData, PacketFlags.Reliable);
simulationClient.Peer.Send(0, ref packet);
break;
}
case RagonOperation.JOIN_SUCCESS:
{
simulationClient.InRoom = true;
ragonSerializer.Clear();
ragonSerializer.WriteOperation(RagonOperation.SCENE_LOADED);
var sendData = ragonSerializer.ToArray();
var packet = new Packet();
packet.Create(sendData, PacketFlags.Reliable);
simulationClient.Peer.Send(0, ref packet);
break;
}
case RagonOperation.SNAPSHOT:
{
ragonSerializer.Clear();
ragonSerializer.WriteOperation(RagonOperation.CREATE_ENTITY);
ragonSerializer.WriteUShort(0);
ragonSerializer.WriteUShort(0);
ragonSerializer.WriteUShort(0);
var sendData = ragonSerializer.ToArray();
var packet = new Packet();
packet.Create(sendData, PacketFlags.Reliable);
simulationClient.Peer.Send(0, ref packet);
break;
}
case RagonOperation.CREATE_ENTITY:
{
ReadOnlySpan<byte> payload = data.AsSpan().Slice(1, data.Length - 1);
ragonSerializer.Clear();
ragonSerializer.FromSpan(ref payload);
var entityType = ragonSerializer.ReadUShort();
var state = ragonSerializer.ReadByte();
var ennt = ragonSerializer.ReadByte();
var entityId = ragonSerializer.ReadInt();
simulationClient.Entities.Add(entityId);
break;
}
}
// Console.WriteLine(op);
// Console.WriteLine("Packet received from server - Channel ID: " + netEvent.ChannelID + ", Data length: " + netEvent.Packet.Length);
netEvent.Packet.Dispose();
break;
}
}
if (simulationClient.InRoom)
{
foreach (var entity in simulationClient.Entities)
{
ragonSerializer.Clear();
ragonSerializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE);
ragonSerializer.WriteInt(entity);
ragonSerializer.WriteInt(100);
ragonSerializer.WriteInt(200);
ragonSerializer.WriteInt(300);
var sendData = ragonSerializer.ToArray();
var packet = new Packet();
packet.Create(sendData, PacketFlags.Instant);
simulationClient.Peer.Send(1, ref packet);
}
}
}
Thread.Sleep(33);
}
}
SimulationClient CreateClient(string url, ushort port)
{
Host client = new Host();
Address address = new Address();
address.SetHost(url);
address.Port = port;
client.Create();
Console.WriteLine("Created client");
Peer peer = client.Connect(address);
return new SimulationClient() {Host = client, Peer = peer};
}
}
class Program
{
static void Main(string[] args)
{
Library.Initialize();
for (var i = 0; i < 80; i ++)
{
var thread = new SimulationThread();
thread.Start("49.12.70.233", 4444, 50);
Thread.Sleep(300);
}
Console.ReadKey();
Library.Deinitialize();
}
}
}
+24 -12
View File
@@ -1,12 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon", "Ragon\Ragon.csproj", "{BABA1AF0-CF91-43F2-9577-53800068ACCF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Relay", "Ragon.Relay\Ragon.Relay.csproj", "{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.SimpleServer", "Ragon.SimpleServer\Ragon.SimpleServer.csproj", "{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Protocol", "Ragon.Protocol\Ragon.Protocol.csproj", "{F478B2A2-36F4-43B9-9BB7-382A57C449B2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Common", "Ragon.Common\Ragon.Common.csproj", "{F478B2A2-36F4-43B9-9BB7-382A57C449B2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server", "Ragon.Server\Ragon.Server.csproj", "{45D3B686-8960-4656-91B2-6F8BFCCAA225}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Stress", "Ragon.Stress\Ragon.Stress.csproj", "{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Core", "Ragon.Core\Ragon.Core.csproj", "{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.NativeWebSockets", "Ragon.Server.NativeWebSockets\Ragon.Server.NativeWebSockets.csproj", "{81050343-A9B8-487B-86C8-7A5B7DD9C39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Server.ENet", "Ragon.Server.ENet\Ragon.Server.ENet.csproj", "{DD79AC4F-9E5C-4938-850E-805D537E68D0}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -14,10 +18,6 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BABA1AF0-CF91-43F2-9577-53800068ACCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BABA1AF0-CF91-43F2-9577-53800068ACCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BABA1AF0-CF91-43F2-9577-53800068ACCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BABA1AF0-CF91-43F2-9577-53800068ACCF}.Release|Any CPU.Build.0 = Release|Any CPU
{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -26,9 +26,21 @@ Global
{F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Release|Any CPU.Build.0 = Release|Any CPU {F478B2A2-36F4-43B9-9BB7-382A57C449B2}.Release|Any CPU.Build.0 = Release|Any CPU
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Release|Any CPU.Build.0 = Release|Any CPU {45D3B686-8960-4656-91B2-6F8BFCCAA225}.Release|Any CPU.Build.0 = Release|Any CPU
{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4AA86B9-2486-4B53-BA77-43D958A2FDC3}.Release|Any CPU.Build.0 = Release|Any CPU
{81050343-A9B8-487B-86C8-7A5B7DD9C39B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81050343-A9B8-487B-86C8-7A5B7DD9C39B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81050343-A9B8-487B-86C8-7A5B7DD9C39B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81050343-A9B8-487B-86C8-7A5B7DD9C39B}.Release|Any CPU.Build.0 = Release|Any CPU
{DD79AC4F-9E5C-4938-850E-805D537E68D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD79AC4F-9E5C-4938-850E-805D537E68D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD79AC4F-9E5C-4938-850E-805D537E68D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD79AC4F-9E5C-4938-850E-805D537E68D0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
-29
View File
@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;NETSTACK_SPAN</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants>TRACE;NETSTACK_SPAN</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ENet-CSharp" Version="2.4.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta1" />
<PackageReference Include="NLog" Version="5.0.0-rc2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ragon.Common\Ragon.Common.csproj" />
</ItemGroup>
</Project>
-144
View File
@@ -1,144 +0,0 @@
using System;
using System.Threading;
using Ragon.Common;
using NLog;
namespace Ragon.Core
{
public class Application : IEventHandler
{
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)
{
var authorizationProvider = factory.CreateAuthorizationProvider(configuration);
_configuration = configuration;
_serializer = new RagonSerializer();
var dispatcher = new Dispatcher();
_dispatcher = dispatcher;
if (configuration.Socket == "udp")
{
_socketServer = new ENetServer(this);
}
else if (configuration.Socket == "websocket")
{
_socketServer = new WebSocketServer(this);
}
else
{
_logger.Error($"Unknown socket type {configuration.Socket}");
}
_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];
_socketServer.Start(_configuration.Port, _configuration.MaxConnections, encoded);
_thread.Start();
}
public void Stop()
{
_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<byte>());
_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<byte>());
_lobby.OnDisconnected(peerId);
}
}
}
-102
View File
@@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using NLog;
using Ragon.Common;
namespace Ragon.Core;
public class AuthorizationManager
{
private Logger _logger = LogManager.GetCurrentClassLogger();
private IApplicationHandler _provider;
private Application _gameThread;
private Lobby _lobby;
private RagonSerializer _serializer;
private readonly Dictionary<uint, Player> _playersByPeers;
private readonly Dictionary<string, Player> _playersByIds;
public AuthorizationManager(IApplicationHandler provider, Application gameThread, Lobby lobby, RagonSerializer serializer)
{
_serializer = serializer;
_lobby = lobby;
_provider = provider;
_gameThread = gameThread;
_playersByIds = new Dictionary<string, Player>();
_playersByPeers = new Dictionary<uint, Player>();
}
public void OnAuthorization(ushort peerId, string key, string name, ReadOnlySpan<byte> additionalData)
{
if (_playersByPeers.ContainsKey(peerId))
{
_logger.Warn($"Connection already authorized {peerId}");
return;
}
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(ushort peerId, string playerId, string playerName)
{
if (_playersByPeers.ContainsKey(peerId))
{
_logger.Warn($"Connection already authorized {peerId}");
return;
}
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.AUTHORIZED_SUCCESS);
_serializer.WriteString(playerId);
_serializer.WriteString(playerName);
var player = new Player()
{
Id = playerId,
PlayerName = playerName,
PeerId = peerId,
IsLoaded = false,
Entities = new List<Entity>(),
EntitiesIds = new List<ushort>(),
};
_playersByIds.Add(playerId, player);
_playersByPeers.Add(peerId, player);
var sendData = _serializer.ToArray();
_gameThread.SocketServer.Send(peerId, sendData, DeliveryType.Reliable);
}
public void Rejected(ushort peerId, uint code)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.AUTHORIZED_FAILED);
_serializer.WriteInt((int) code);
var sendData = _serializer.ToArray();
_gameThread.SocketServer.Send(peerId, sendData, DeliveryType.Reliable);
_gameThread.SocketServer.Disconnect(peerId, 0);
}
public void Cleanup(ushort peerId)
{
if (_playersByPeers.Remove(peerId, out var player))
_playersByIds.Remove(player.Id);
}
public Player? GetPlayer(ushort peerId)
{
if (_playersByPeers.TryGetValue(peerId, out var player))
return player;
return null;
}
public Player GetPlayer(string playerId)
{
return _playersByIds[playerId];
}
}
-22
View File
@@ -1,22 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using NLog;
namespace Ragon.Core
{
public class Bootstrap
{
private ILogger _logger = LogManager.GetCurrentClassLogger();
public Application Configure(PluginFactory factory)
{
_logger.Info("Configure application...");
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
var configuration = Configuration.Load(filePath);
var app = new Application(factory, configuration);
return app;
}
}
}
-49
View File
@@ -1,49 +0,0 @@
using System;
using System.IO;
using Newtonsoft.Json;
using NLog;
namespace Ragon.Core
{
[Serializable]
public struct Configuration
{
public string Key;
public string Protocol;
public string Socket;
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.25-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;
}
}
}
-22
View File
@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
namespace Ragon.Core;
public class Dispatcher
{
public Queue<Action> _actions = new Queue<Action>();
public void Dispatch(Action action)
{
lock (_actions)
_actions.Enqueue(action);
}
public void Process()
{
lock(_actions)
while(_actions.TryDequeue(out var action))
action?.Invoke();
}
}
-254
View File
@@ -1,254 +0,0 @@
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; }
private List<EntityProperty> _properties;
private List<EntityEvent> _bufferedEvents;
public Entity(GameRoom room, ushort ownerId, ushort entityType, ushort staticId, RagonAuthority eventAuthority)
{
OwnerId = ownerId;
StaticId = staticId;
EntityType = entityType;
EntityId = _idGenerator++;
Payload = Array.Empty<byte>();
Authority = eventAuthority;
_room = room;
_properties = new List<EntityProperty>();
_bufferedEvents = new List<EntityEvent>();
}
public void SetPayload(byte[] payload)
{
Payload = payload;
}
public void SetOwner(ushort ownerId)
{
OwnerId = ownerId;
}
public void AddProperty(EntityProperty property)
{
_properties.Add(property);
}
public void ReplicateEvent(ushort peerId, ushort eventId, ReadOnlySpan<byte> 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();
RouteEvent(peerId, 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.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 WriteSnapshot(RagonSerializer serializer)
{
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);
}
}
}
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<byte> 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<byte> 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<byte> 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 RouteEvent(ushort peerId, 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.ExceptInvoker:
{
_room.BroadcastToReady(sendData, new[] {peerId}, 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<byte> payloadRaw = stackalloc byte[payloadData.Length];
ReadOnlySpan<byte> payload = payloadRaw;
payloadData.CopyTo(payloadRaw);
if (_room.Plugin.InternalHandle(player.PeerId, EntityId, eventId, ref payload))
return;
ReplicateEvent(player.PeerId, eventId, payload, eventMode, targetMode);
}
}
-11
View File
@@ -1,11 +0,0 @@
using Ragon.Common;
namespace Ragon.Core;
public class EntityEvent
{
public ushort PeerId { get; set; }
public ushort EventId { get; set; }
public byte[] EventData { get; set; }
public RagonTarget Target { set; get; }
}
-503
View File
@@ -1,503 +0,0 @@
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<ushort, Player> _players = new();
private Dictionary<int, Entity> _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<ushort>();
private ushort[] _allPlayers = Array.Empty<ushort>();
private Entity[] _entitiesAll = Array.Empty<Entity>();
private HashSet<Entity> _entitiesDirtySet = new HashSet<Entity>();
private List<Entity> _entitiesDirty = new List<Entity>();
private List<ushort> _peersCache = new List<ushort>();
private List<ushort> _awaitingPeers = new List<ushort>();
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<byte> 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<byte> 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<byte> 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<byte> 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<ushort>();
_entitiesAll = Array.Empty<Entity>();
_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);
}
}
}
-12
View File
@@ -1,12 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Ragon.Core;
public interface IApplicationHandler
{
Task OnAuthorizationRequest(string key, string playerName, byte[] additionalData, Action<string, string> Accept, Action<uint> Reject);
public void OnCustomEvent(ushort peerId, ReadOnlySpan<byte> payload);
public void OnJoin(ushort peerId);
public void OnLeave(ushort peerId);
}
-125
View File
@@ -1,125 +0,0 @@
using System;
using System.Linq;
using NLog;
using Ragon.Common;
namespace Ragon.Core;
public class Lobby
{
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private readonly Application _application;
private readonly RagonSerializer _writer;
private readonly RoomManager _roomManager;
private readonly AuthorizationManager _authorizationManager;
public AuthorizationManager AuthorizationManager => _authorizationManager;
public Lobby(IApplicationHandler provider, RoomManager manager, Application application)
{
_roomManager = manager;
_application = application;
_writer = new RagonSerializer();
_authorizationManager = new AuthorizationManager(provider, application, this, _writer);
}
public void ProcessEvent(ushort peerId, RagonOperation op, RagonSerializer reader)
{
var player = _authorizationManager.GetPlayer(peerId);
if (op == RagonOperation.AUTHORIZE)
{
if (player != null)
{
_logger.Warn("Player already authorized");
return;
}
var key = reader.ReadString();
var playerName = reader.ReadString();
var additionalData = reader.ReadData(reader.Size);
_authorizationManager.OnAuthorization(peerId, key, playerName, additionalData);
return;
}
if (player == null)
{
_logger.Warn($"Peer not authorized {peerId} trying to {op}");
return;
}
switch (op)
{
case RagonOperation.JOIN_ROOM:
{
var roomId = reader.ReadString();
var exists = _roomManager.Rooms.Any(r => r.Id == roomId);
if (!exists)
{
_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;
}
if (_roomManager.RoomsBySocket.ContainsKey(peerId))
_roomManager.Left(player, Array.Empty<byte>());
_roomManager.Join(player, roomId, Array.Empty<byte>());
break;
}
case RagonOperation.CREATE_ROOM:
{
var roomId = Guid.NewGuid().ToString();
var custom = reader.ReadBool();
if (custom)
{
roomId = reader.ReadString();
var exists = _roomManager.Rooms.Any(r => r.Id == roomId);
if (exists)
{
_writer.Clear();
_writer.WriteOperation(RagonOperation.JOIN_FAILED);
_writer.WriteString($"Room with id {roomId} already exists");
var sendData = _writer.ToArray();
_application.SocketServer.Send(peerId, sendData, DeliveryType.Reliable);
return;
}
}
var roomProperties = new RagonRoomParameters();
roomProperties.Deserialize(reader);
if (_roomManager.RoomsBySocket.ContainsKey(peerId))
_roomManager.Left(player, Array.Empty<byte>());
_roomManager.Create(player, roomId, roomProperties, Array.Empty<byte>());
break;
}
case RagonOperation.JOIN_OR_CREATE_ROOM:
{
var roomId = Guid.NewGuid().ToString();
var roomProperties = new RagonRoomParameters();
roomProperties.Deserialize(reader);
if (_roomManager.RoomsBySocket.ContainsKey(peerId))
_roomManager.Left(player, Array.Empty<byte>());
_roomManager.JoinOrCreate(player, roomId, roomProperties, Array.Empty<byte>());
break;
}
case RagonOperation.LEAVE_ROOM:
{
_roomManager.Left(player, Array.Empty<byte>());
break;
}
}
}
public void OnDisconnected(ushort peerId)
{
_authorizationManager.Cleanup(peerId);
}
}
-28
View File
@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
namespace Ragon.Core
{
public class Player
{
public string Id { get; set; }
public string PlayerName { get; set; }
public ushort PeerId { get; set; }
public bool IsLoaded { get; set; }
public List<Entity> Entities;
public List<ushort> 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);
}
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace Ragon.Core
{
public interface PluginFactory
{
public PluginBase CreatePlugin(string map);
public IApplicationHandler CreateAuthorizationProvider(Configuration configuration);
}
}
-206
View File
@@ -1,206 +0,0 @@
using System;
using System.Collections.Generic;
using NLog;
using Ragon.Common;
namespace Ragon.Core
{
public class PluginBase
{
private delegate void SubscribeDelegate(Player player, ref ReadOnlySpan<byte> data);
private delegate void SubscribeEntityDelegate(Player player, Entity entity, ref ReadOnlySpan<byte> data);
private Dictionary<ushort, SubscribeDelegate> _globalEvents = new();
private Dictionary<int, Dictionary<ushort, SubscribeEntityDelegate>> _entityEvents = new();
private readonly RagonSerializer _serializer = new();
protected GameRoom Room { get; private set; } = null!;
protected ILogger Logger = null!;
public void Attach(GameRoom gameRoom)
{
Logger = LogManager.GetLogger($"Plugin<{GetType().Name}>");
Room = gameRoom;
_globalEvents.Clear();
_entityEvents.Clear();
}
public void Detach()
{
_globalEvents.Clear();
_entityEvents.Clear();
}
public void OnEvent<T>(ushort evntCode, Action<Player, T> action) where T : IRagonSerializable, new()
{
if (_globalEvents.ContainsKey(evntCode))
{
Logger.Warn($"Event subscriber already added {evntCode}");
return;
}
var data = new T();
_globalEvents.Add(evntCode, (Player player, ref ReadOnlySpan<byte> raw) =>
{
if (raw.Length == 0)
{
Logger.Warn($"Payload is empty for event {evntCode}");
return;
}
_serializer.Clear();
_serializer.FromSpan(ref raw);
data.Deserialize(_serializer);
action.Invoke(player, data);
});
}
public void OnEvent(ushort evntCode, Action<Player> action)
{
if (_globalEvents.ContainsKey(evntCode))
{
Logger.Warn($"Event subscriber already added {evntCode}");
return;
}
_globalEvents.Add(evntCode, (Player player, ref ReadOnlySpan<byte> raw) => { action.Invoke(player); });
}
public void OnEvent<T>(Entity entity, ushort evntCode, Action<Player, Entity, T> action) where T : IRagonSerializable, new()
{
if (_entityEvents.ContainsKey(entity.EntityId))
{
if (_entityEvents[entity.EntityId].ContainsKey(evntCode))
{
Logger.Warn($"Event subscriber already added {evntCode} for {entity.EntityId}");
return;
}
var data = new T();
_entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) =>
{
if (raw.Length == 0)
{
Logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
return;
}
_serializer.Clear();
_serializer.FromSpan(ref raw);
data.Deserialize(_serializer);
action.Invoke(player, ent, data);
});
return;
}
{
var data = new T();
_entityEvents.Add(entity.EntityId, new Dictionary<ushort, SubscribeEntityDelegate>());
_entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) =>
{
if (raw.Length == 0)
{
Logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
return;
}
_serializer.Clear();
_serializer.FromSpan(ref raw);
data.Deserialize(_serializer);
action.Invoke(player, ent, data);
});
}
}
public void OnEvent(Entity entity, ushort evntCode, Action<Player, Entity> action)
{
if (_entityEvents.ContainsKey(entity.EntityId))
{
if (_entityEvents[entity.EntityId].ContainsKey(evntCode))
{
Logger.Warn($"Event subscriber already added {evntCode} for {entity.EntityId}");
return;
}
_entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) => { action.Invoke(player, ent); });
return;
}
{
_entityEvents.Add(entity.EntityId, new Dictionary<ushort, SubscribeEntityDelegate>());
_entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) => { action.Invoke(player, ent); });
}
}
public void UnsubscribeAll()
{
_globalEvents.Clear();
_entityEvents.Clear();
}
public bool InternalHandle(uint peerId, int entityId, ushort evntCode, ref ReadOnlySpan<byte> payload)
{
if (!_entityEvents.ContainsKey(entityId))
return false;
if (!_entityEvents[entityId].ContainsKey(evntCode))
return false;
// var player = Room.GetPlayerById(peerId);
var entity = Room.GetEntityById(entityId);
// _entityEvents[entityId][evntCode].Invoke(player, entity, ref payload);
return true;
}
public bool InternalHandle(uint peerId, ushort evntCode, ref ReadOnlySpan<byte> payload)
{
if (_globalEvents.ContainsKey(evntCode))
{
// var player = Room.GetPlayerById(peerId);
// _globalEvents[evntCode].Invoke(player, ref payload);
return true;
}
return false;
}
#region VIRTUAL
public virtual void OnPlayerJoined(Player player)
{
}
public virtual void OnPlayerLeaved(Player player)
{
}
public virtual void OnOwnershipChanged(Player player)
{
}
public virtual bool OnEntityCreated(Player player, Entity entity)
{
return false;
}
public virtual bool OnEntityDestroyed(Player player, Entity entity)
{
return false;
}
public virtual void OnStart()
{
}
public virtual void OnStop()
{
}
#endregion
}
}
-122
View File
@@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using NLog;
using Ragon.Common;
namespace Ragon.Core;
public class RoomManager
{
private readonly Application _gameThread;
private readonly PluginFactory _factory;
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly List<GameRoom> _rooms = new();
private readonly Dictionary<uint, GameRoom> _roomsBySocket;
public IReadOnlyDictionary<uint, GameRoom> RoomsBySocket => _roomsBySocket;
public IReadOnlyList<GameRoom> Rooms => _rooms;
public RoomManager(PluginFactory factory, Application gameThread)
{
_gameThread = gameThread;
_factory = factory;
_roomsBySocket = new Dictionary<uint, GameRoom>();
}
public void Join(Player player, string roomId, byte[] payload)
{
_logger.Trace($"Player ({player.PlayerName}|{player.Id}) joined to room with Id {roomId}");
if (_rooms.Count > 0)
{
foreach (var existRoom in _rooms)
{
if (existRoom.Id == roomId && existRoom.PlayersCount < existRoom.PlayersMax)
{
existRoom.AddPlayer(player, payload);
_roomsBySocket.Add(player.PeerId, existRoom);
return;
}
}
}
}
public void Create(Player creator, string roomId, RagonRoomParameters parameters, byte[] payload)
{
var map = parameters.Map;
var min = parameters.Min;
var max = parameters.Max;
_logger.Trace($"Player ({creator.PlayerName}|{creator.Id}) create room with Id {roomId} and params ({map}|{min}|{max})");
var plugin = _factory.CreatePlugin(map);
if (plugin == null)
throw new NullReferenceException($"Plugin for map {map} is null");
var room = new GameRoom(_gameThread, plugin, roomId, map, min, max);
room.AddPlayer(creator, payload);
room.OnStart();
_roomsBySocket.Add(creator.PeerId, room);
_rooms.Add(room);
}
public void JoinOrCreate(Player player, string roomId, RagonRoomParameters parameters, byte[] payload)
{
var map = parameters.Map;
var min = parameters.Min;
var max = parameters.Max;
if (_rooms.Count > 0)
{
foreach (var existRoom in _rooms)
{
if (existRoom.Map == map && existRoom.PlayersCount < existRoom.PlayersMax)
{
_logger.Trace($"Player ({player.PlayerName}|{player.Id}) joined to room with Id {roomId}");
existRoom.AddPlayer(player, payload);
_roomsBySocket.Add(player.PeerId, existRoom);
return;
}
}
}
_logger.Trace($"Room not found for Player ({player.PlayerName}|{player.Id}), create room with Id {roomId} and params ({map}|{min}|{max})");
var plugin = _factory.CreatePlugin(map);
if (plugin == null)
throw new NullReferenceException($"Plugin for map {map} is null");
var room = new GameRoom(_gameThread, plugin, roomId, map, min, max);
room.AddPlayer(player, payload);
room.OnStart();
_roomsBySocket.Add(player.PeerId, room);
_rooms.Add(room);
}
public void Left(Player player, byte[] payload)
{
if (_roomsBySocket.Remove(player.PeerId, out var room))
{
_logger.Trace($"Player ({player.PlayerName}|{player.Id}) left room with Id {room.Id}");
room.RemovePlayer(player.PeerId);
if (room.PlayersCount < room.PlayersMin)
{
_logger.Trace($"Room with Id {room.Id} destroyed");
room.OnStop();
_rooms.Remove(room);
}
_gameThread.SocketServer.Send(player.PeerId, new byte[] {(byte) RagonOperation.LEAVE_ROOM}, DeliveryType.Reliable);
}
}
public void Tick(float deltaTime)
{
foreach (var gameRoom in _rooms)
gameRoom.Tick(deltaTime);
}
}
-98
View File
@@ -1,98 +0,0 @@
using System;
using System.Collections.Generic;
namespace Ragon.Core
{
public class Scheduler: IDisposable
{
List<SchedulerTask> _scheduledTasks;
public Scheduler(int defaultCapacity = 100)
{
_scheduledTasks = new List<SchedulerTask>(defaultCapacity);
}
public SchedulerTask Schedule(Action<SchedulerTask> action, float interval, int count = 1)
{
var newTask = new SchedulerTask(action, interval, count - 1);
_scheduledTasks.Add(newTask);
return newTask;
}
public SchedulerTask ScheduleForever(Action<SchedulerTask> action, float interval)
{
var newTask = new SchedulerTask(action, interval, -1);
_scheduledTasks.Add(newTask);
return newTask;
}
public void StopSchedule(SchedulerTask schedulerTask)
{
if (_scheduledTasks.Contains(schedulerTask))
_scheduledTasks.Remove(schedulerTask);
}
public void Tick(float deltaTime)
{
for (int i = _scheduledTasks.Count - 1; i >= 0; i--)
{
var scheduledTask = _scheduledTasks[i];
scheduledTask.Tick(deltaTime);
if (!scheduledTask.IsActive)
_scheduledTasks.Remove(scheduledTask);
}
}
public void Dispose()
{
_scheduledTasks.Clear();
}
}
public class SchedulerTask
{
private Action<SchedulerTask> _action;
private float _timer = 0;
private float _interval = 0;
private int _repeats = 0;
private bool _active;
public int Repeats => _repeats;
public bool IsActive => _active;
public SchedulerTask(Action<SchedulerTask> task, float interval, int repeatCount = 0)
{
_action = task;
_interval = interval;
_timer = 0;
_active = true;
_repeats = repeatCount;
}
public void Tick(float deltaTime)
{
_timer += deltaTime;
if (_timer >= _interval)
{
_action.Invoke(this);
if (_repeats == -1)
{
_timer = 0;
return;
}
if (_repeats > 0)
{
_timer = 0;
_repeats--;
return;
}
if (_repeats == 0)
_active = false;
}
}
}
}
-7
View File
@@ -1,7 +0,0 @@
namespace Ragon.Core;
public enum DeliveryType
{
Unreliable,
Reliable
}
-156
View File
@@ -1,156 +0,0 @@
using System;
using System.Diagnostics;
using System.Timers;
using ENet;
using NLog;
namespace Ragon.Core
{
public class ENetServer : ISocketServer
{
private ILogger _logger = LogManager.GetCurrentClassLogger();
private Host _host;
private uint _protocol;
private Address _address;
private Event _netEvent;
private Peer[] _peers;
private IEventHandler _eventHandler;
private Stopwatch _timer;
public ENetServer(IEventHandler eventHandler)
{
_eventHandler = eventHandler;
_timer = Stopwatch.StartNew();
_peers = Array.Empty<Peer>();
_host = new Host();
}
public void Start(ushort port, int connections, uint protocol)
{
_address = default;
_address.Port = port;
_peers = new Peer[connections];
_protocol = protocol;
_host.Create(_address, connections, 2, 0, 0, 1024 * 1024);
var protocolDecoded = (protocol >> 16 & 0xFF) + "." + (protocol >> 8 & 0xFF) + "." + (protocol & 0xFF);
_logger.Info($"Network listening on {port}");
_logger.Info($"Protocol: {protocolDecoded}");
}
public void Broadcast(ushort[] peersIds, byte[] data, DeliveryType type)
{
var newPacket = new Packet();
var packetFlags = PacketFlags.Instant;
byte channel = 1;
if (type == DeliveryType.Reliable)
{
packetFlags = PacketFlags.Reliable;
channel = 0;
}
else if (type == DeliveryType.Unreliable)
{
channel = 1;
packetFlags = PacketFlags.UnreliableFragmented;
}
newPacket.Create(data, data.Length, packetFlags);
foreach (var peerId in peersIds)
_peers[peerId].Send(channel, ref newPacket);
}
public void Send(ushort peerId, byte[] data, DeliveryType type)
{
var newPacket = new Packet();
var packetFlags = PacketFlags.Instant;
byte channel = 1;
if (type == DeliveryType.Reliable)
{
packetFlags = PacketFlags.Reliable;
channel = 0;
}
else if (type == DeliveryType.Unreliable)
{
channel = 1;
packetFlags = PacketFlags.None;
}
newPacket.Create(data, data.Length, packetFlags);
_peers[peerId].Send(channel, ref newPacket);
}
public void Disconnect(ushort peerId, uint errorCode)
{
_peers[peerId].Reset();
}
public void Process()
{
bool polled = false;
while (!polled)
{
if (_host.CheckEvents(out _netEvent) <= 0)
{
if (_host.Service(0, out _netEvent) <= 0)
break;
polled = true;
}
switch (_netEvent.Type)
{
case EventType.None:
{
_logger.Trace("None event");
break;
}
case EventType.Connect:
{
// if (IsValidProtocol(_netEvent.Data))
// {
// _logger.Warn("Mismatched protocol, close connection");
// break;
// }
_peers[_netEvent.Peer.ID] = _netEvent.Peer;
_eventHandler.OnConnected((ushort)_netEvent.Peer.ID);
break;
}
case EventType.Disconnect:
{
_eventHandler.OnDisconnected((ushort)_netEvent.Peer.ID);
break;
}
case EventType.Timeout:
{
_eventHandler.OnTimeout((ushort)_netEvent.Peer.ID);
break;
}
case EventType.Receive:
{
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;
}
}
}
}
public void Stop()
{
_host?.Dispose();
}
private bool IsValidProtocol(uint protocol)
{
return protocol == _protocol;
}
}
}
@@ -1,7 +0,0 @@
namespace Ragon.Core;
public class WebSocketPacket
{
public ushort PeerId;
public byte[] Data;
}
@@ -1,123 +0,0 @@
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<ushort, WebSocket> _webSockets = new Dictionary<ushort, WebSocket>();
private Queue<WebSocketPacket> _events;
private IEventHandler _eventHandler;
private WebSocketTaskScheduler _webSocketScheduler;
private TaskFactory _taskFactory;
private HttpListener _httpListener;
public WebSocketServer(IEventHandler eventHandler)
{
_eventHandler = eventHandler;
_events = new Queue<WebSocketPacket>(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<byte>(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);
}
}
-11
View File
@@ -1,11 +0,0 @@
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);
}
-11
View File
@@ -1,11 +0,0 @@
namespace Ragon.Core;
public interface ISocketServer
{
public void Start(ushort port, int connections, uint protocol);
public void Process();
public void Stop();
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);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "5.0.0", "version": "6.0.0",
"rollForward": "latestMajor", "rollForward": "latestMajor",
"allowPrerelease": true "allowPrerelease": true
} }
+6 -8
View File
@@ -6,18 +6,16 @@
Ragon is fully free, small and high perfomance room based game server with plugin based architecture. Ragon is fully free, small and high perfomance room based game server with plugin based architecture.
<a href="https://ragon-server.com/docs/category/basics">Documentation</a> <a href="http://localhost:3000/docs/installation">Documentation</a>
<br>
<a href="https://ragon-server.com/docs/get-started">Get started</a>
### Features: ### Features:
- Effective - Effective
- Free - Free
- Simple matchmaking - Lobby
- Room based architecture - Room based architecture
- Сustomizable authorization - Сustomizable authorization
- Сustomizable server-side logic via plugins with flexible API - Сustomizable server-side logic via plugins with flexible API*(2)
- No CCU limitations* - No CCU limitations*(1)
- No Room count limitations - No Room count limitations
- Reliable UDP - Reliable UDP
@@ -27,10 +25,10 @@ Ragon is fully free, small and high perfomance room based game server with plugi
### Dependencies ### Dependencies
* ENet-Sharp [v2.4.8] * ENet-Sharp [v2.4.8]
* NetStack [latest]
### License ### License
MIT MIT
### Tips ### Tips
\* Limited to 4095 CCU by library ENet-Sharp \* Limited to 4095 CCU by library ENet-Sharp (1)
\* Non finally (2)