Compare commits

...

49 Commits

Author SHA1 Message Date
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
edmand46 f83d3ea0c7 chore: update version 2022-11-13 18:54:23 +04:00
edmand46 3564eb2adc feat: added except invoker target 2022-11-13 18:53:56 +04:00
edmand46 3531432758 fixed: crash on concurrent authorization 2022-11-13 00:20:41 +04:00
edmand46 6bda468607 v1.0.23-rc 2022-10-24 22:19:50 +04:00
edmand46 7ddd52bf9d feat: added websocket support 2022-10-23 18:50:05 +04:00
edmand46 ecf3631163 Merge branch 'retry'
# Conflicts:
#	Ragon/Sources/Game/GameRoom.cs
2022-10-23 18:49:32 +04:00
edmand46 8a149eb3b4 fixed: added catch error websocket 2022-10-23 18:45:37 +04:00
edmand46 14ae5e8189 fixed: temporary scheduler 2022-10-22 22:58:15 +04:00
edmand46 b7e8327ca8 feat: websocket 2022-10-22 21:34:35 +04:00
edmand46 b9e79af9d8 fixed: player joined opcode 2022-10-21 22:19:21 +04:00
edmand46 545ec02ecc chore: update version 2022-10-21 00:05:01 +04:00
edmand46 73feb77169 fixed: wrong peerId in restored events 2022-10-21 00:04:33 +04:00
edmand46 cbcf1773aa wip 2022-10-16 18:27:40 +04:00
edmand46 5519ae7679 wip 2022-10-16 18:00:32 +04:00
edmand46 1558b5eefb wip 2022-10-16 17:14:32 +04:00
edmand46 aaa0e4a317 wip 2022-10-16 16:50:22 +04:00
edmand46 ff712dc094 wip 2022-10-16 16:22:27 +04:00
edmand46 c2c75cb513 fixed: double authorization request 2022-10-15 18:48:59 +04:00
edmand46 9a22566f79 fixed: additional data missed 2022-10-15 18:48:48 +04:00
edmand46 783d2ce922 chore: bump version 2022-10-02 16:13:53 +04:00
edmand46 5771ec738b fixed: ownership changing 2022-10-02 16:00:34 +04:00
edmand46 85081e1da7 fix: replication entity event 2022-09-11 15:05:32 +04:00
edmand46 dbaa5d9d92 chore: renaming enums 2022-09-10 10:25:45 +04:00
edmand46 54786c065d wip 2022-09-06 22:39:52 +04:00
edmand46 0b3a0dd1ac refactor: spawning entities with properties 2022-09-04 14:43:17 +04:00
edmand46 72ff37e94a bump version 2022-08-30 01:54:50 +04:00
edmand46 75dab9d16f fixed: on scene entities load 2022-08-30 01:53:47 +04:00
edmand46 25b0e54d13 fixed: changed flow of joining 2022-08-29 01:45:47 +04:00
edmand46 e4f664b557 refactor: renamed plugin api 2022-08-28 19:31:07 +04:00
edmand46 174a4c4422 bump version 2022-08-28 11:51:14 +04:00
edmand46 a51d7c73cd fixed: mistype 2022-08-28 11:46:30 +04:00
edmand46 957622a170 fix: on player leave logic 2022-08-28 00:18:26 +04:00
edmand46 4a07424293 feat: added checks for size of payload 2022-08-27 11:21:51 +04:00
edmand46 568a3282c3 fix: restoring properties 2022-08-27 10:51:57 +04:00
edmand46 6a71fe5fe0 feat: restore properties 2022-08-25 23:12:33 +04:00
edmand46 082e183989 chore: move copyright on top of license 2022-08-24 00:06:37 +04:00
edmand46 51c0482a40 fix: leave player logic 2022-08-21 00:15:07 +04:00
edmand46 9e16c53cad fix: gameloop optimization 2022-08-20 22:05:43 +04:00
100 changed files with 2196 additions and 2257 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
-9
View File
@@ -1,9 +0,0 @@
namespace Ragon.Common
{
public enum RagonTarget: byte
{
OWNER,
EXCEPT_OWNER,
ALL,
}
}
+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.1.0-rc";
private static void CopyrightInfo()
{
Logger.Info($"Server Version: {ServerVersion}");
Logger.Info($"Machine Name: {Environment.MachineName}");
Logger.Info($"OS: {Environment.OSVersion}");
Logger.Info($"Processors: {Environment.ProcessorCount}");
Logger.Info($"Runtime Version: {Environment.Version}");
Logger.Info("==================================");
Logger.Info("| |");
Logger.Info("| Ragon |");
Logger.Info("| |");
Logger.Info("==================================");
}
public static Configuration Load(string filePath)
{
CopyrightInfo();
var data = File.ReadAllText(filePath);
var configuration = JsonConvert.DeserializeObject<Configuration>(data);
return configuration;
}
}
+189
View File
@@ -0,0 +1,189 @@
using Ragon.Common;
namespace Ragon.Core.Game;
public class Entity
{
private static ushort _idGenerator = 0;
public ushort Id { get; private set; }
public RoomPlayer Owner { get; private set; }
public RagonAuthority Authority { get; private set; }
public EntityState State { get; private set; }
public byte[] Payload { get; private set; }
public ushort StaticId { get; private set; }
public ushort Type { get; private set; }
private List<EntityEvent> _bufferedEvents;
public Entity(RoomPlayer owner, ushort type, ushort staticId, RagonAuthority eventAuthority)
{
Owner = owner;
StaticId = staticId;
Type = type;
Id = _idGenerator++;
Payload = Array.Empty<byte>();
Authority = eventAuthority;
State = new EntityState(this);
_bufferedEvents = new List<EntityEvent>();
}
public void SetPayload(byte[] payload)
{
Payload = payload;
}
public void SetOwner(RoomPlayer owner)
{
Owner = owner;
}
public void RestoreBufferedEvents(RoomPlayer roomPlayer, RagonSerializer writer)
{
foreach (var bufferedEvent in _bufferedEvents)
{
writer.Clear();
writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
writer.WriteUShort(bufferedEvent.EventId);
writer.WriteUShort(bufferedEvent.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;
}
}
+100
View File
@@ -0,0 +1,100 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Game;
public class EntityState
{
private Logger _logger = LogManager.GetCurrentClassLogger();
private List<EntityStateProperty> _properties;
private Entity _entity;
public EntityState(Entity entity, int capacity = 10)
{
_entity = entity;
_properties = new List<EntityStateProperty>(10);
}
public void AddProperty(EntityStateProperty property)
{
_properties.Add(property);
}
public void Write(RagonSerializer serializer)
{
serializer.WriteUShort(_entity.Id);
for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++)
{
var property = _properties[propertyIndex];
if (property.IsDirty)
{
serializer.WriteBool(true);
var span = serializer.GetWritableData(property.Size);
var data = property.Read();
data.CopyTo(span);
property.Clear();
}
else
{
serializer.WriteBool(false);
}
}
}
public void Read(RagonSerializer serializer)
{
for (var i = 0; i < _properties.Count; i++)
{
if (serializer.ReadBool())
{
var property = _properties[i];
var size = property.Size;
if (!property.IsFixed)
size = serializer.ReadUShort();
if (size > property.Capacity)
{
Console.WriteLine($"Property {i} payload too large, size: {size}");
continue;
}
var propertyPayload = serializer.ReadData(size);
property.Write(ref propertyPayload);
property.Size = size;
}
}
}
public void Snapshot(RagonSerializer serializer)
{
ReadOnlySpan<byte> payload = _entity.Payload.AsSpan();
serializer.WriteUShort(_entity.Type);
serializer.WriteUShort(_entity.Id);
if (_entity.StaticId != 0)
serializer.WriteUShort(_entity.StaticId);
serializer.WriteUShort(_entity.Owner.Connection.Id);
serializer.WriteUShort((ushort) payload.Length);
serializer.WriteData(ref payload);
for (int propertyIndex = 0; propertyIndex < _properties.Count; propertyIndex++)
{
var property = _properties[propertyIndex];
var hasPayload = property.IsFixed || property.Size > 0 && !property.IsFixed;
if (hasPayload)
{
serializer.WriteBool(true);
var span = serializer.GetWritableData(property.Size);
var data = property.Read();
data.CopyTo(span);
}
else
{
serializer.WriteBool(false);
}
}
}
}
@@ -1,22 +1,24 @@
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 bool IsDirty { get; private set; } public bool IsDirty { get; private set; }
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)
{ {
_data = new byte[512]; Capacity = 512;
Size = size; Size = size;
IsFixed = isFixed; IsFixed = isFixed;
IsDirty = true; IsDirty = true;
_data = new byte[Capacity];
} }
public ReadOnlySpan<byte> Read() public ReadOnlySpan<byte> Read()
+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);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Handlers;
public sealed class EntityStateHandler: IHandler
{
private ILogger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var room = context.Room;
var entitiesCount = reader.ReadUShort();
for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++)
{
var entityId = reader.ReadUShort();
if (room.Entities.TryGetValue(entityId, out var entity))
{
entity.State.Read(reader);
room.Track(entity);
}
else
{
_logger.Error($"Entity with Id {entityId} not found, replication interrupted");
}
}
}
}
+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($"Joined to room {room.Id}");
}
}
+19
View File
@@ -0,0 +1,19 @@
using NLog;
using Ragon.Common;
namespace Ragon.Core.Handlers;
public sealed class LeaveHandler: IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
var room = context.Room;
var roomPlayer = context.RoomPlayer;
if (room != null)
{
context.Room?.RemovePlayer(roomPlayer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} leaved from {room.Id}");
}
}
}
+123
View File
@@ -0,0 +1,123 @@
using NLog;
using Ragon.Common;
using Ragon.Core.Game;
using Ragon.Core.Lobby;
namespace Ragon.Core.Handlers;
public sealed class SceneLoadedHandler : IHandler
{
private Logger _logger = LogManager.GetCurrentClassLogger();
public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
return;
var owner = context.Room.Owner;
var player = context.RoomPlayer;
var room = context.Room;
if (player == owner)
{
var statics = reader.ReadUShort();
for (var staticIndex = 0; staticIndex < statics; staticIndex++)
{
var entityType = reader.ReadUShort();
var eventAuthority = (RagonAuthority) reader.ReadByte();
var staticId = reader.ReadUShort();
var propertiesCount = reader.ReadUShort();
var entity = new Entity(player, entityType, staticId, eventAuthority);
for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++)
{
var propertyType = reader.ReadBool();
var propertySize = reader.ReadUShort();
entity.State.AddProperty(new EntityStateProperty(propertySize, propertyType));
}
room.AttachEntity(player, entity);
}
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} loaded with {statics} scene entities");
room.WaitPlayersList.Add(player);
foreach (var roomPlayer in room.WaitPlayersList)
{
DispatchPlayerJoinExcludePlayer(room, roomPlayer, writer);
roomPlayer.SetReady();
}
room.UpdateReadyPlayerList();
DispatchSnapshot(room, room.WaitPlayersList, writer);
room.WaitPlayersList.Clear();
}
else if (owner.IsLoaded)
{
player.SetReady();
DispatchPlayerJoinExcludePlayer(room, player, writer);
room.UpdateReadyPlayerList();
DispatchSnapshot(room, new List<RoomPlayer>() { player }, writer);
foreach (var entity in room.EntityList)
entity.RestoreBufferedEvents(player, writer);
}
else
{
_logger.Trace($"Player {player.Connection.Id}|{context.LobbyPlayer.Name} waiting owner of room");
room.WaitPlayersList.Add(player);
}
}
private void DispatchPlayerJoinExcludePlayer(Room room, RoomPlayer roomPlayer, RagonSerializer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.PLAYER_JOINED);
writer.WriteUShort(roomPlayer.Connection.Id);
writer.WriteString(roomPlayer.Id);
writer.WriteString(roomPlayer.Name);
var sendData = writer.ToArray();
foreach (var awaiter in room.ReadyPlayersList)
{
if (awaiter != roomPlayer)
awaiter.Connection.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.0.5" />
</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' ">
@@ -2,7 +2,8 @@ namespace Ragon.Common
{ {
public enum RagonAuthority: byte public enum RagonAuthority: byte
{ {
OWNER_ONLY, None,
ALL, OwnerOnly,
All,
} }
} }
@@ -5,7 +5,6 @@ namespace Ragon.Common
AUTHORIZE, AUTHORIZE,
AUTHORIZED_SUCCESS, AUTHORIZED_SUCCESS,
AUTHORIZED_FAILED, AUTHORIZED_FAILED,
JOIN_OR_CREATE_ROOM, JOIN_OR_CREATE_ROOM,
CREATE_ROOM, CREATE_ROOM,
JOIN_ROOM, JOIN_ROOM,
@@ -13,20 +12,14 @@ namespace Ragon.Common
OWNERSHIP_CHANGED, OWNERSHIP_CHANGED,
JOIN_SUCCESS, JOIN_SUCCESS,
JOIN_FAILED, JOIN_FAILED,
LOAD_SCENE, LOAD_SCENE,
SCENE_IS_LOADED, SCENE_LOADED,
PLAYER_JOINED, PLAYER_JOINED,
PLAYER_LEAVED, PLAYER_LEAVED,
CREATE_ENTITY, CREATE_ENTITY,
CREATE_SCENE_ENTITY,
DESTROY_ENTITY, DESTROY_ENTITY,
SNAPSHOT, SNAPSHOT,
REPLICATE_ENTITY_STATE, REPLICATE_ENTITY_STATE,
REPLICATE_ENTITY_EVENT, REPLICATE_ENTITY_EVENT,
REPLICATE_EVENT,
} }
} }
@@ -0,0 +1,10 @@
namespace Ragon.Common
{
public enum RagonReplicationMode: byte
{
Local,
Server,
LocalAndServer,
Buffered,
}
}
@@ -27,6 +27,7 @@ namespace Ragon.Common
private byte[] _data; private byte[] _data;
private int _offset; private int _offset;
private int _size; private int _size;
public int Lenght => _offset; public int Lenght => _offset;
public int Size => _size - _offset; public int Size => _size - _offset;
+11
View File
@@ -0,0 +1,11 @@
namespace Ragon.Common
{
public enum RagonTarget: byte
{
Owner,
ExceptOwner,
ExceptInvoker,
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": "websocket",
"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($"Network listening on {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.0.5" />
</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,121 @@
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 ushort _lastPeerId;
private CancellationTokenSource _cancellationTokenSource;
public NativeWebSocketServer(Executor executor)
{
_sequencer = new Stack<ushort>();
_connections = Array.Empty<WebSocketConnection>();
_executor = executor;
}
public async Task 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);
_lastPeerId = peerId;
_connections[peerId] = connection;
_networkListener.OnConnected(connection);
_executor.Run(StartListen(connection, cancellationToken));
}
}
async Task StartListen(WebSocketConnection connection, CancellationToken cancellationToken)
{
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);
_networkListener.OnDisconnected(connection);
}
public async void Poll()
{
foreach (var conn in _connections)
{
if (conn != null)
{
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($"Network listening on 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);
}
+55
View File
@@ -0,0 +1,55 @@
using System.Threading.Channels;
namespace Ragon.Core.Server;
public class Executor: TaskScheduler
{
private ChannelReader<Task> _reader;
private ChannelWriter<Task> _writer;
private Queue<Task> _pendingTasks;
private TaskFactory _taskFactory;
public void Run(Task task)
{
_taskFactory.StartNew(() => task);
}
public Executor()
{
var channel = Channel.CreateUnbounded<Task>();
_reader = channel.Reader;
_writer = channel.Writer;
_taskFactory = new TaskFactory(this);
_pendingTasks = new Queue<Task>();
}
protected override IEnumerable<Task>? GetScheduledTasks()
{
throw new NotSupportedException();
}
protected override void QueueTask(Task task)
{
_writer.TryWrite(task);
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
public void Execute()
{
while (_reader.TryRead(out var task))
{
TryExecuteTask(task);
if (task.Status == TaskStatus.Running)
_pendingTasks.Enqueue(task);
}
while (_pendingTasks.TryDequeue(out var task))
_writer.TryWrite(task);
}
}
+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>
-18
View File
@@ -1,18 +0,0 @@
using System;
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,29 +0,0 @@
using System;
using System.Threading.Tasks;
using Ragon.Core;
namespace Game.Source;
public class AuthorizationProviderByKey: IAuthorizationProvider
{
private Configuration _configuration;
public AuthorizationProviderByKey(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);
}
}
}
@@ -1,28 +0,0 @@
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({GameRoom.Id})");
}
public override void OnPlayerLeaved(Player player)
{
// _logger.Info($"Player({player.PlayerName}) left from Room({GameRoom.Id})");
}
}
}
@@ -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 IAuthorizationProvider CreateAuthorizationProvider(Configuration configuration)
{
return new AuthorizationProviderByKey(configuration);
}
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"key": "defaultkey",
"protocol": "1.0.0",
"statisticsInterval": 5,
"sendRate": 30,
"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_IS_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>
-31
View File
@@ -1,31 +0,0 @@
using ENet;
using NLog;
namespace Ragon.Core
{
public class Application
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly GameThread _gameThread;
public Application(PluginFactory factory, Configuration configuration)
{
_gameThread = new GameThread(factory, configuration);
}
public void Start()
{
Library.Initialize();
_gameThread.Start();
_logger.Info("Started");
}
public void Stop()
{
_gameThread.Stop();
Library.Deinitialize();
_logger.Info("Stopped");
}
}
}
@@ -1,90 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NLog.Targets;
using Ragon.Common;
namespace Ragon.Core;
public class AuthorizationManager : IAuthorizationManager
{
private IAuthorizationProvider _provider;
private IGameThread _gameThread;
private Lobby _lobby;
private RagonSerializer _serializer;
private readonly Dictionary<uint, Player> _playersByPeers;
private readonly Dictionary<string, Player> _playersByIds;
public AuthorizationManager(IAuthorizationProvider provider, IGameThread 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(uint peerId, string key, string name)
{
var dispatcher = _gameThread.ThreadDispatcher;
_provider.OnAuthorizationRequest(key, name, Array.Empty<byte>(),
(playerId, playerName) => { dispatcher.Dispatch(() => Accepted(peerId, playerId, playerName)); },
(errorCode) => { dispatcher.Dispatch(() => Rejected(peerId, errorCode)); });
}
public void Accepted(uint peerId, string playerId, string playerName)
{
_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<int>(),
};
_playersByIds.Add(playerId, player);
_playersByPeers.Add(peerId, player);
var sendData = _serializer.ToArray();
_gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable);
}
public void Rejected(uint peerId, uint code)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.AUTHORIZED_FAILED);
_serializer.WriteInt((int) code);
var sendData = _serializer.ToArray();
_gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable);
_gameThread.Server.Disconnect(peerId, 0);
}
public void Cleanup(uint peerId)
{
if (_playersByPeers.Remove(peerId, out var player))
_playersByIds.Remove(player.Id);
}
public Player? GetPlayer(uint peerId)
{
if (_playersByPeers.TryGetValue(peerId, out var player))
return player;
return null;
}
public Player GetPlayer(string playerId)
{
return _playersByIds[playerId];
}
}
@@ -1,6 +0,0 @@
namespace Ragon.Core;
public interface IAuthorizationManager
{
}
@@ -1,9 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Ragon.Core;
public interface IAuthorizationProvider
{
Task OnAuthorizationRequest(string key, string playerName, byte[] additionalData, Action<string, string> Accept, Action<uint> Reject);
}
-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 = ConfigurationLoader.Load(filePath);
var app = new Application(factory, configuration);
return app;
}
}
}
@@ -1,19 +0,0 @@
using System;
namespace Ragon.Core
{
[Serializable]
public struct Configuration
{
public string Key;
public string Protocol;
public int StatisticsInterval;
public ushort SendRate;
public ushort Port;
public int SkipTimeout;
public int ReconnectTimeout;
public int MaxConnections;
public int MaxPlayersPerRoom;
public int MaxRooms;
}
}
@@ -1,38 +0,0 @@
using System;
using System.IO;
using Newtonsoft.Json;
using NLog;
using Logger = NLog.Logger;
namespace Ragon.Core
{
public static class ConfigurationLoader
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private static readonly string _serverVersion = "1.0.9-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".PadBoth(32)}=");
_logger.Info("= =");
_logger.Info("==================================");
}
public static Configuration Load(string filePath)
{
CopyrightInfo();
var data = File.ReadAllText(filePath);
var configuration = JsonConvert.DeserializeObject<Configuration>(data);
return configuration;
}
}
}
-26
View File
@@ -1,26 +0,0 @@
using Ragon.Common;
namespace Ragon.Core;
public class Entity
{
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 RagonAuthority Authority { get; private set; }
public EntityProperty[] Properties { get; private set; }
public byte[] Payload { get; set; }
public Entity(ushort ownerId, ushort entityType, ushort staticId, RagonAuthority stateAuthority, RagonAuthority eventAuthority, int props)
{
OwnerId = ownerId;
StaticId = staticId;
EntityType = entityType;
EntityId = _idGenerator++;
Properties = new EntityProperty[props];
Payload = new byte[1024];
Authority = eventAuthority;
}
}
@@ -1,14 +0,0 @@
using System;
namespace Ragon.Core
{
public static class StringExtensions
{
public static string PadBoth(this string str, int length)
{
int spaces = length - str.Length;
int padLeft = spaces / 2 + str.Length;
return str.PadLeft(padLeft).PadRight(length);
}
}
}
@@ -1,14 +0,0 @@
using System;
namespace Ragon.Core
{
public static class ValueExtensions
{
public static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
{
if (val.CompareTo(min) < 0) return min;
else if (val.CompareTo(max) > 0) return max;
else return val;
}
}
}
-555
View File
@@ -1,555 +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 : IGameRoom
{
public int PlayersMin { get; private set; }
public int PlayersMax { get; private set; }
public int PlayersCount => _players.Count;
public int EntitiesCount => _entities.Count;
public string Id { get; private set; }
public string Map { get; private set; }
private ILogger _logger = LogManager.GetCurrentClassLogger();
private Dictionary<uint, Player> _players = new();
private Dictionary<int, Entity> _entities = new();
private uint _owner;
private readonly IScheduler _scheduler;
private readonly IGameThread _gameThread;
private readonly PluginBase _plugin;
private readonly RagonSerializer _serializer = new(512);
// Cache
private uint[] _readyPlayers = Array.Empty<uint>();
private uint[] _allPlayers = Array.Empty<uint>();
private Entity[] _entitiesAll = Array.Empty<Entity>();
private HashSet<Entity> _entitiesDirtySet = new HashSet<Entity>();
private List<Entity> _entitiesDirty = new List<Entity>();
private List<uint> _peersCache = new List<uint>();
public GameRoom(IGameThread gameThread, PluginBase pluginBase, string roomId, string map, int min, int max)
{
_gameThread = gameThread;
_plugin = pluginBase;
_scheduler = new Scheduler();
Map = map;
PlayersMin = min;
PlayersMax = max;
Id = roomId;
_plugin.Attach(this);
}
public void Joined(Player player, ReadOnlySpan<byte> payload)
{
if (_players.Count == 0)
{
_owner = player.PeerId;
}
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.PLAYER_JOINED);
_serializer.WriteUShort((ushort) player.PeerId);
_serializer.WriteString(player.Id);
_serializer.WriteString(player.PlayerName);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
}
_players.Add(player.PeerId, player);
_allPlayers = _players.Select(p => p.Key).ToArray();
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.JOIN_SUCCESS);
_serializer.WriteString(Id);
_serializer.WriteString(player.Id);
_serializer.WriteString(GetOwner().Id);
_serializer.WriteUShort((ushort) PlayersMin);
_serializer.WriteUShort((ushort) PlayersMax);
var sendData = _serializer.ToArray();
Send(player.PeerId, sendData, DeliveryType.Reliable);
}
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.LOAD_SCENE);
_serializer.WriteString(Map);
var sendData = _serializer.ToArray();
Send(player.PeerId, sendData, DeliveryType.Reliable);
}
}
public void Leave(uint peerId)
{
if (_players.Remove(peerId, out var player))
{
_allPlayers = _players.Select(p => p.Key).ToArray();
_readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray();
{
_plugin.OnPlayerLeaved(player);
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.PLAYER_LEAVED);
_serializer.WriteString(player.Id);
_serializer.WriteUShort((ushort) player.EntitiesIds.Count);
foreach (var entityId in player.EntitiesIds)
{
_serializer.WriteInt(entityId);
_entities.Remove(entityId);
}
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData);
}
if (_allPlayers.Length > 0)
{
var nextOwnerId = _allPlayers[0];
_owner = nextOwnerId;
var nextOwner = _players[nextOwnerId];
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED);
_serializer.WriteString(nextOwner.Id);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData);
}
_entitiesAll = _entities.Values.ToArray();
}
}
public void ProcessEvent(ushort peerId, RagonOperation operation, ReadOnlySpan<byte> payloadRawData)
{
_serializer.Clear();
_serializer.FromSpan(ref payloadRawData);
switch (operation)
{
case RagonOperation.REPLICATE_ENTITY_STATE:
{
var entitiesCount = _serializer.ReadUShort();
for (var entityIndex = 0; entityIndex < entitiesCount; entityIndex++)
{
var entityId = _serializer.ReadUShort();
if (_entities.TryGetValue(entityId, out var ent))
{
if (ent.OwnerId != peerId)
{
_logger.Warn($"Not owner can't change properties of object {entityId}");
return;
}
for (var i = 0; i < ent.Properties.Length; i++)
{
if (_serializer.ReadBool())
{
var property = ent.Properties[i];
if (!property.IsFixed)
property.Size = _serializer.ReadUShort();
var propertyPayload = _serializer.ReadData(property.Size);
property.Write(ref propertyPayload);
}
}
if (_entitiesDirtySet.Add(ent))
_entitiesDirty.Add(ent);
}
else
{
_logger.Error($"Entity with Id {entityId} not found, replication interrupted");
break;
}
}
break;
}
case RagonOperation.REPLICATE_ENTITY_EVENT:
{
var evntId = _serializer.ReadUShort();
var evntMode = _serializer.ReadByte();
var targetMode = (RagonTarget) _serializer.ReadByte();
var entityId = _serializer.ReadUShort();
if (!_entities.TryGetValue(entityId, out var ent))
return;
if (ent.Authority == RagonAuthority.OWNER_ONLY && ent.OwnerId != peerId)
return;
Span<byte> payloadRaw = stackalloc byte[_serializer.Size];
var payloadData = _serializer.ReadData(_serializer.Size);
payloadData.CopyTo(payloadRaw);
ReadOnlySpan<byte> payload = payloadRaw;
if (_plugin.InternalHandle(peerId, entityId, evntId, ref payload))
return;
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
_serializer.WriteUShort(evntId);
_serializer.WriteUShort((ushort) peerId);
_serializer.WriteByte(evntMode);
_serializer.WriteUShort(entityId);
_serializer.WriteData(ref payload);
var sendData = _serializer.ToArray();
switch (targetMode)
{
case RagonTarget.OWNER:
{
Send(ent.OwnerId, sendData, DeliveryType.Reliable);
break;
}
case RagonTarget.EXCEPT_OWNER:
{
_peersCache.Clear();
foreach (var playerPeerId in _readyPlayers)
if (playerPeerId != ent.OwnerId)
_peersCache.Add(playerPeerId);
Broadcast(_peersCache.ToArray(), sendData, DeliveryType.Reliable);
break;
}
case RagonTarget.ALL:
{
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
}
break;
}
case RagonOperation.LOAD_SCENE:
{
var sceneName = _serializer.ReadString();
_readyPlayers = Array.Empty<uint>();
_entitiesAll = Array.Empty<Entity>();
_entities.Clear();
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.LOAD_SCENE);
_serializer.WriteString(sceneName);
var sendData = _serializer.ToArray();
Broadcast(_allPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.REPLICATE_EVENT:
{
var evntId = _serializer.ReadUShort();
var evntMode = _serializer.ReadByte();
var targetMode = (RagonTarget) _serializer.ReadByte();
Span<byte> payloadRaw = stackalloc byte[_serializer.Size];
var payloadData = _serializer.ReadData(_serializer.Size);
payloadData.CopyTo(payloadRaw);
ReadOnlySpan<byte> payload = payloadRaw;
if (_plugin.InternalHandle(peerId, evntId, ref payload))
return;
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_EVENT);
_serializer.WriteUShort((ushort) peerId);
_serializer.WriteByte(evntMode);
_serializer.WriteUShort(evntId);
_serializer.WriteData(ref payload);
var sendData = _serializer.ToArray();
switch (targetMode)
{
case RagonTarget.OWNER:
{
Send(_owner, sendData, DeliveryType.Reliable);
break;
}
case RagonTarget.EXCEPT_OWNER:
{
_peersCache.Clear();
foreach (var playerPeerId in _readyPlayers)
if (playerPeerId != _owner)
_peersCache.Add(playerPeerId);
Broadcast(_peersCache.ToArray(), sendData, DeliveryType.Reliable);
break;
}
case RagonTarget.ALL:
{
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
}
break;
}
case RagonOperation.CREATE_SCENE_ENTITY:
{
var entityType = _serializer.ReadUShort();
var staticId = _serializer.ReadUShort();
var propertiesCount = _serializer.ReadUShort();
var entity = new Entity(peerId, entityType, staticId, RagonAuthority.ALL, RagonAuthority.OWNER_ONLY, propertiesCount);
for (var i = 0; i < propertiesCount; i++)
{
var propertyType = _serializer.ReadBool();
var propertySize = _serializer.ReadUShort();
entity.Properties[i] = new EntityProperty(propertySize, propertyType);
}
{
var entityPayload = _serializer.ReadData(_serializer.Size);
entity.Payload = entityPayload.ToArray();
}
var player = _players[peerId];
player.Entities.Add(entity);
player.EntitiesIds.Add(entity.EntityId);
var ownerId = (ushort) peerId;
_entities.Add(entity.EntityId, entity);
_entitiesAll = _entities.Values.ToArray();
_plugin.OnEntityCreated(player, entity);
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.CREATE_SCENE_ENTITY);
_serializer.WriteUShort(entityType);
_serializer.WriteUShort(entity.EntityId);
_serializer.WriteUShort(staticId);
_serializer.WriteUShort(ownerId);
{
ReadOnlySpan<byte> entityPayload = entity.Payload.AsSpan();
_serializer.WriteData(ref entityPayload);
}
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.CREATE_ENTITY:
{
var entityType = _serializer.ReadUShort();
var propertiesCount = _serializer.ReadUShort();
var entity = new Entity(peerId, entityType, 0, RagonAuthority.ALL, RagonAuthority.ALL, propertiesCount);
// _logger.Trace("Created entity with properties: " + propertiesCount);
for (var i = 0; i < propertiesCount; i++)
{
var propertyType = _serializer.ReadBool();
var propertySize = _serializer.ReadUShort();
entity.Properties[i] = new EntityProperty(propertySize, propertyType);
// _logger.Trace($"Property: {i} Size: {propertySize} IsFixed: {propertyType}");
}
{
var entityPayload = _serializer.ReadData(_serializer.Size);
entity.Payload = entityPayload.ToArray();
}
var player = _players[peerId];
player.Entities.Add(entity);
player.EntitiesIds.Add(entity.EntityId);
var ownerId = (ushort) peerId;
_entities.Add(entity.EntityId, entity);
_entitiesAll = _entities.Values.ToArray();
_plugin.OnEntityCreated(player, entity);
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.CREATE_ENTITY);
_serializer.WriteUShort(entityType);
_serializer.WriteUShort(entity.EntityId);
_serializer.WriteUShort(ownerId);
{
ReadOnlySpan<byte> entityPayload = entity.Payload.AsSpan();
_serializer.WriteData(ref entityPayload);
}
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.DESTROY_ENTITY:
{
var entityId = _serializer.ReadInt();
if (_entities.TryGetValue(entityId, out var entity))
{
if (entity.Authority == RagonAuthority.OWNER_ONLY && entity.OwnerId != peerId)
return;
var player = _players[peerId];
var destroyPayload = _serializer.ReadData(_serializer.Size);
player.Entities.Remove(entity);
player.EntitiesIds.Remove(entity.EntityId);
_entities.Remove(entityId);
_entitiesAll = _entities.Values.ToArray();
_plugin.OnEntityDestroyed(player, entity);
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.DESTROY_ENTITY);
_serializer.WriteInt(entityId);
_serializer.WriteData(ref destroyPayload);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
}
break;
}
case RagonOperation.SCENE_IS_LOADED:
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.SNAPSHOT);
_serializer.WriteUShort((ushort) _allPlayers.Length);
foreach (var playerPeerId in _allPlayers)
{
_serializer.WriteString(_players[playerPeerId].Id);
_serializer.WriteUShort((ushort) playerPeerId);
_serializer.WriteString(_players[playerPeerId].PlayerName);
}
var dynamicEntities = _entitiesAll.Where(e => e.StaticId == 0).ToArray();
_serializer.WriteUShort((ushort) dynamicEntities.Length);
foreach (var entity in dynamicEntities)
{
ReadOnlySpan<byte> payload = entity.Payload.AsSpan();
_serializer.WriteUShort(entity.EntityType);
_serializer.WriteUShort(entity.EntityId);
_serializer.WriteUShort((ushort) entity.OwnerId);
_serializer.WriteUShort((ushort) payload.Length);
_serializer.WriteData(ref payload);
}
var staticCount = _entitiesAll.Where(e => e.StaticId != 0).ToArray();
_serializer.WriteUShort((ushort) staticCount.Length);
foreach (var entity in staticCount)
{
ReadOnlySpan<byte> payload = entity.Payload.AsSpan();
_serializer.WriteUShort(entity.EntityType);
_serializer.WriteUShort(entity.EntityId);
_serializer.WriteUShort(entity.StaticId);
_serializer.WriteUShort(entity.OwnerId);
_serializer.WriteUShort((ushort) payload.Length);
_serializer.WriteData(ref payload);
}
var sendData = _serializer.ToArray();
Send(peerId, sendData, DeliveryType.Reliable);
_players[peerId].IsLoaded = true;
_readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray();
_plugin.OnPlayerJoined(_players[peerId]);
break;
}
}
}
public void Tick(float deltaTime)
{
_scheduler.Tick(deltaTime);
if (_entitiesDirty.Count > 0)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE);
_serializer.WriteUShort((ushort) _entitiesDirty.Count);
for (var entityIndex = 0; entityIndex < _entitiesDirty.Count; entityIndex++)
{
var entity = _entitiesDirty[entityIndex];
_serializer.WriteUShort(entity.EntityId);
for (int propertyIndex = 0; propertyIndex < entity.Properties.Length; propertyIndex++)
{
var property = entity.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);
}
}
}
_entitiesDirty.Clear();
_entitiesDirtySet.Clear();
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData);
}
}
public void Start()
{
_plugin.OnStart();
}
public void Stop()
{
foreach (var peerId in _allPlayers)
_gameThread.Server.Disconnect(peerId, 0);
_plugin.OnStop();
_plugin.Detach();
}
public Player GetPlayerById(uint peerId) => _players[peerId];
public Entity GetEntityById(int entityId) => _entities[entityId];
public Player GetOwner() => _players[_owner];
public IDispatcher GetThreadDispatcher() => _gameThread.ThreadDispatcher;
public IScheduler GetScheduler() => _scheduler;
public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
_gameThread.Server.Send(peerId, rawData, deliveryType);
}
public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
_gameThread.Server.Broadcast(peersIds, rawData, deliveryType);
}
public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
_gameThread.Server.Broadcast(_allPlayers, rawData, deliveryType);
}
}
}
-161
View File
@@ -1,161 +0,0 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using Ragon.Common;
using ENet;
using NLog;
namespace Ragon.Core
{
public class GameThread : IGameThread, IHandler
{
private readonly RoomManager _roomManager;
private readonly Thread _thread;
private readonly Stopwatch _gameLoopTimer;
private readonly Stopwatch _statisticsTimer;
private readonly Stopwatch _serverTimer;
private readonly Stopwatch _logicTimer;
private readonly Lobby _lobby;
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private readonly float _deltaTime = 0.0f;
private readonly Configuration _configuration;
private readonly IDispatcherInternal _dispatcherInternal;
public IDispatcher ThreadDispatcher { get; private set; }
public ISocketServer Server { get; private set; }
public GameThread(PluginFactory factory, Configuration configuration)
{
_configuration = configuration;
var authorizationProvider = factory.CreateAuthorizationProvider(configuration);
var dispatcher = new Dispatcher();
_dispatcherInternal = dispatcher;
ThreadDispatcher = dispatcher;
Server = new ENetServer(this);
_deltaTime = 1000.0f / configuration.SendRate;
_roomManager = new RoomManager(factory, this);
_lobby = new Lobby(authorizationProvider, _roomManager, this);
_gameLoopTimer = new Stopwatch();
_statisticsTimer = new Stopwatch();
_serverTimer = new Stopwatch();
_logicTimer = new Stopwatch();
_thread = new Thread(Execute);
_thread.Name = "Game Thread";
_thread.IsBackground = true;
}
public void Start()
{
var strings = _configuration.Protocol.Split(".");
if (strings.Length < 3)
{
_logger.Error("Wrong protocol passed to connect method");
return;
}
var parts = new uint[] {0, 0, 0};
for (int i = 0; i < parts.Length; i++)
{
if (!uint.TryParse(strings[i], out var v))
{
_logger.Error("Wrong protocol");
return;
}
parts[i] = v;
}
uint encoded = (parts[0] << 16) | (parts[1] << 8) | parts[2];
Server.Start(_configuration.Port, _configuration.MaxConnections, encoded);
_gameLoopTimer.Start();
_statisticsTimer.Start();
_logicTimer.Start();
_serverTimer.Start();
_thread.Start();
}
public void Stop()
{
Server.Stop();
_gameLoopTimer.Stop();
_statisticsTimer.Stop();
_thread.Interrupt();
}
private void Execute()
{
while (true)
{
_logicTimer.Restart();
_serverTimer.Restart();
Server.Process();
_dispatcherInternal.Process();
var elapsedMilliseconds = _gameLoopTimer.ElapsedMilliseconds;
if (elapsedMilliseconds > _deltaTime)
{
_roomManager.Tick(elapsedMilliseconds / 1000.0f);
_gameLoopTimer.Restart();
continue;
}
if (_statisticsTimer.Elapsed.Seconds > _configuration.StatisticsInterval && _roomManager.RoomsBySocket.Count > 0)
{
var rooms = _roomManager.Rooms.Count;
var clients = _roomManager.RoomsBySocket.Count;
var entities = _roomManager.Rooms.Select(r => r.EntitiesCount).Sum();
_logger.Trace($"Rooms: {rooms} Clients: {clients} Entities: {entities}");
_statisticsTimer.Restart();
}
}
}
public void OnEvent(Event evnt)
{
if (evnt.Type == EventType.Timeout || evnt.Type == EventType.Disconnect)
{
var player = _lobby.AuthorizationManager.GetPlayer(evnt.Peer.ID);
if (player != null)
_roomManager.Left(player, Array.Empty<byte>());
_lobby.OnDisconnected(evnt.Peer.ID);
}
if (evnt.Type == EventType.Receive)
{
try
{
var peerId = (ushort) evnt.Peer.ID;
var dataRaw = new byte[evnt.Packet.Length];
evnt.Packet.CopyTo(dataRaw);
var data = new ReadOnlySpan<byte>(dataRaw);
var operation = (RagonOperation) data[0];
var payload = data.Slice(1, data.Length - 1);
if (_roomManager.RoomsBySocket.TryGetValue(peerId, out var room))
room.ProcessEvent(peerId, operation, payload);
_lobby.ProcessEvent(peerId, operation, payload);
}
catch (Exception exception)
{
_logger.Error(exception);
}
}
}
}
}
-20
View File
@@ -1,20 +0,0 @@
namespace Ragon.Core;
public interface IGameRoom
{
public string Id { get; }
public string Map { get; }
public int PlayersMin { get; }
public int PlayersMax { get; }
public int PlayersCount { get; }
public Player GetPlayerById(uint peerId);
public Entity GetEntityById(int entityId);
public Player GetOwner();
public IDispatcher GetThreadDispatcher();
public IScheduler GetScheduler();
public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable);
public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable);
public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable);
}
-9
View File
@@ -1,9 +0,0 @@
using Ragon.Common;
namespace Ragon.Core;
public interface IGameThread
{
public IDispatcher ThreadDispatcher { get; }
public ISocketServer Server { get; }
}
-6
View File
@@ -1,6 +0,0 @@
namespace Ragon.Core;
public interface ILobby
{
}
-121
View File
@@ -1,121 +0,0 @@
using System;
using System.Linq;
using NLog;
using Ragon.Common;
namespace Ragon.Core;
public class Lobby : ILobby
{
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private readonly RagonSerializer _serializer;
private readonly RoomManager _roomManager;
private readonly AuthorizationManager _authorizationManager;
private readonly IGameThread _gameThread;
public AuthorizationManager AuthorizationManager => _authorizationManager;
public Lobby(IAuthorizationProvider provider, RoomManager manager, IGameThread gameThread)
{
_roomManager = manager;
_gameThread = gameThread;
_serializer = new RagonSerializer();
_authorizationManager = new AuthorizationManager(provider, gameThread, this, _serializer);
}
public void ProcessEvent(ushort peerId, RagonOperation op, ReadOnlySpan<byte> payload)
{
_serializer.Clear();
_serializer.FromSpan(ref payload);
if (op == RagonOperation.AUTHORIZE)
{
var key = _serializer.ReadString();
var playerName = _serializer.ReadString();
_authorizationManager.OnAuthorization(peerId, key, playerName);
return;
}
var player = _authorizationManager.GetPlayer(peerId);
if (player == null)
{
_logger.Warn($"Peer not authorized {peerId} trying to {op}");
return;
}
switch (op)
{
case RagonOperation.JOIN_ROOM:
{
var roomId = _serializer.ReadString();
var exists = _roomManager.Rooms.Any(r => r.Id == roomId);
if (!exists)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.JOIN_FAILED);
_serializer.WriteString($"Room with id {roomId} not exists");
var sendData = _serializer.ToArray();
_gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable);
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 = _serializer.ReadBool();
if (custom)
{
roomId = _serializer.ReadString();
var exists = _roomManager.Rooms.Any(r => r.Id == roomId);
if (exists)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.JOIN_FAILED);
_serializer.WriteString($"Room with id {roomId} already exists");
var sendData = _serializer.ToArray();
_gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable);
return;
}
}
var roomProperties = new RagonRoomParameters();
roomProperties.Deserialize(_serializer);
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(_serializer);
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(uint peerId)
{
_authorizationManager.Cleanup(peerId);
}
}
-16
View File
@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
namespace Ragon.Core
{
public class Player
{
public string Id { get; set; }
public uint PeerId { get; set; }
public string PlayerName { get; set; }
public bool IsLoaded { get; set; }
public List<Entity> Entities;
public List<int> EntitiesIds;
}
}
-10
View File
@@ -1,10 +0,0 @@
namespace Ragon.Core
{
public interface PluginFactory
{
public PluginBase CreatePlugin(string map);
public IAuthorizationProvider CreateAuthorizationProvider(Configuration configuration);
}
}
-251
View File
@@ -1,251 +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 IGameRoom GameRoom { get; private set; } = null!;
protected ILogger Logger = null!;
public void Attach(GameRoom gameRoom)
{
Logger = LogManager.GetLogger($"Plugin<{GetType().Name}>");
GameRoom = gameRoom;
_globalEvents.Clear();
_entityEvents.Clear();
}
public void Detach()
{
_globalEvents.Clear();
_entityEvents.Clear();
}
public void Subscribe<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 Subscribe(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 Subscribe<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 Subscribe(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 = GameRoom.GetPlayerById(peerId);
var entity = GameRoom.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 = GameRoom.GetPlayerById(peerId);
_globalEvents[evntCode].Invoke(player, ref payload);
return true;
}
return false;
}
public void SendEvent(Player player, uint eventCode, IRagonSerializable payload)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_EVENT);
payload.Serialize(_serializer);
var sendData = _serializer.ToArray();
GameRoom.Send(player.PeerId, sendData);
}
public void BroadcastEvent(ushort eventCode, IRagonSerializable payload)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_EVENT);
payload.Serialize(_serializer);
var sendData = _serializer.ToArray();
GameRoom.Broadcast(sendData, DeliveryType.Reliable);
}
public void SendEntityEvent(Player player, Entity entity, IRagonSerializable payload)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
_serializer.WriteInt(entity.EntityId);
payload.Serialize(_serializer);
var sendData = _serializer.ToArray();
GameRoom.Send(player.PeerId, sendData, DeliveryType.Reliable);
}
public void BroadcastEntityEvent(Entity entity, IRagonSerializable payload)
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
_serializer.WriteInt(entity.EntityId);
payload.Serialize(_serializer);
var sendData = _serializer.ToArray();
GameRoom.Broadcast(sendData);
}
#region VIRTUAL
public virtual void OnPlayerJoined(Player player)
{
}
public virtual void OnPlayerLeaved(Player player)
{
}
public virtual void OnOwnershipChanged(Player player)
{
}
public virtual void OnEntityCreated(Player creator, Entity entity)
{
}
public virtual void OnEntityDestroyed(Player destoyer, Entity entity)
{
}
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 IGameThread _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, IGameThread 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.Joined(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.Joined(creator, payload);
room.Start();
_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.Joined(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.Joined(player, payload);
room.Start();
_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.Leave(player.PeerId);
if (room.PlayersCount < room.PlayersMin)
{
_logger.Trace($"Room with Id {room.Id} destroyed");
room.Stop();
_rooms.Remove(room);
}
_gameThread.Server.Send(player.PeerId, new byte[] {(byte) RagonOperation.LEAVE_ROOM}, DeliveryType.Reliable);
}
}
public void Tick(float deltaTime)
{
foreach (var gameRoom in _rooms)
gameRoom.Tick(deltaTime);
}
}
-7
View File
@@ -1,7 +0,0 @@
namespace Ragon.Core;
public enum DeliveryType
{
Unreliable,
Reliable
}
-151
View File
@@ -1,151 +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 IHandler _handler;
private Stopwatch _timer;
public ENetServer(IHandler handler)
{
_handler = handler;
_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(uint[] 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(uint 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(uint peerId, uint errorCode)
{
_peers[peerId].Reset();
}
public void Process()
{
bool polled = false;
while (!polled)
{
if (_host.CheckEvents(out _netEvent) <= 0)
{
if (_host.Service(15, 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;
_handler.OnEvent(_netEvent);
break;
}
case EventType.Disconnect:
{
_handler.OnEvent(_netEvent);
break;
}
case EventType.Timeout:
{
_handler.OnEvent(_netEvent);
break;
}
case EventType.Receive:
{
_handler.OnEvent(_netEvent);
_netEvent.Packet.Dispose();
break;
}
}
}
}
public void Stop()
{
_host?.Dispose();
}
private bool IsValidProtocol(uint protocol)
{
return protocol == _protocol;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
using ENet;
namespace Ragon.Core;
public interface IHandler
{
public void OnEvent(Event evnt);
}
-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(uint peerId, byte[] data, DeliveryType type);
public void Broadcast(uint[] peersIds, byte[] data, DeliveryType type);
public void Disconnect(uint peerId, uint errorCode);
}
-22
View File
@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
namespace Ragon.Core;
public class Dispatcher: IDispatcher, IDispatcherInternal
{
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();
}
}
-8
View File
@@ -1,8 +0,0 @@
using System;
namespace Ragon.Core;
public interface IDispatcher
{
public void Dispatch(Action action);
}
@@ -1,6 +0,0 @@
namespace Ragon.Core;
public interface IDispatcherInternal
{
public void Process();
}
-12
View File
@@ -1,12 +0,0 @@
using System;
namespace Ragon.Core;
public interface IScheduler
{
public SchedulerTask Schedule(Action<SchedulerTask> action, float interval, int count = 1);
public SchedulerTask ScheduleForever(Action<SchedulerTask> action, float interval);
public void StopSchedule(SchedulerTask schedulerTask);
public void Tick(float deltaTime);
}
-98
View File
@@ -1,98 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Ragon.Core
{
public class Scheduler: IScheduler
{
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;
}
}
}
}
+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
} }
+2 -2
View File
@@ -1,7 +1,7 @@
MIT License
Copyright (c) 2022 Eduard Kargin (theedison4@gmail.com) Copyright (c) 2022 Eduard Kargin (theedison4@gmail.com)
MIT License:
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
+5 -4
View File
@@ -16,8 +16,8 @@ Ragon is fully free, small and high perfomance room based game server with plugi
- Simple matchmaking - Simple matchmaking
- 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 +27,11 @@ 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)