Compare commits

...

22 Commits

Author SHA1 Message Date
edmand46 679608bc48 fixed: ci/cd 2022-06-23 22:02:20 +04:00
edmand46 e9c129e2f2 version 2022-06-23 21:57:45 +04:00
edmand46 66181c612a Merge remote-tracking branch 'origin/main' 2022-06-23 21:57:31 +04:00
edmand46 189278e17c added: stress testing 2022-06-23 21:57:24 +04:00
edmand46 08c399a030 added: stress testing 2022-06-23 20:45:57 +04:00
edmand46 05c8904601 wip 2022-06-23 20:45:41 +04:00
edmand46 04b1b16b11 chore: updated ci/cd 2022-05-29 16:56:17 +04:00
edmand46 530c6109ea fixed: serializer resize 2022-05-29 16:36:25 +04:00
edmand46 3efd73d8cb chore: update server version 2022-05-29 16:18:55 +04:00
edmand46 a85ac99a3c fixed: event replication 2022-05-29 16:15:26 +04:00
edmand46 e295e9f7db fixed: serializer 2022-05-28 12:11:35 +04:00
edmand46 06dd23ee8d chore: remove logs 2022-05-28 11:58:54 +04:00
edmand46 62d3f7acdd fixed: incorrected snapshot on reconnecting, added resizble serializer 2022-05-28 11:53:42 +04:00
edmand46 e2d07eb396 fixed: snapshot 2022-05-28 10:33:06 +04:00
edmand46 4f00c36cd9 feat: reworked room with new serializer, reduce header of packet, added snapshot, added player join, left, ownership changed events events, added dispatch of states
fixed: missed authority bug
2022-05-26 21:09:53 +04:00
edmand46 8c1945e352 feat: added RagonSerializer, removed RagonHeader, reduced boilerplate code 2022-05-26 21:06:26 +04:00
edmand46 2ef1da5c90 feat: added player id for identify user in external services 2022-05-26 21:05:54 +04:00
edmand46 ec65b9f305 feat: added spawn payload in snapshot, reduce allocations in entity state 2022-05-26 21:05:05 +04:00
edmand46 35ca016520 chore: move to thread application logic, updated logo 2022-05-18 22:17:25 +04:00
edmand46 8481cb89ad Merge pull request #2 from OlegDzhuraev/fixes
Recursion fix and room bytes count corrected
2022-05-18 21:39:45 +04:00
Oleg Dzhuraev bcec99cff1 Recursion fix and room bytes count corrected 2022-05-17 13:59:46 +03:00
edmand46 bb7cccb61a chore: update logo 2022-05-15 13:05:39 +04:00
64 changed files with 1745 additions and 1214 deletions
+6 -5
View File
@@ -19,7 +19,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}
release_name: SimpleServer-${{ github.ref }} release_name: Ragon.SimpleServer-${{ github.ref }}
prerelease: true
build: build:
name: Release name: Release
@@ -52,10 +53,10 @@ jobs:
- name: Build - name: Build
shell: bash shell: bash
run: | run: |
release_name="SimpleServer-${{ env.TAG }}-${{ matrix.target }}" release_name="Ragon.SimpleServer-${{ env.TAG }}-${{ matrix.target }}"
# Build everything # Build everything
dotnet publish SimpleServer/SimpleServer.csproj -c Release --runtime "${{ matrix.target }}" -p:PublishSingleFile=true -o "$release_name" dotnet publish Ragon.SimpleServer/Ragon.SimpleServer.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}/*"
@@ -68,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: SimpleServer-${{ env.TAG }}-${{ matrix.target }}.zip asset_path: Ragon.SimpleServer-${{ env.TAG }}-${{ matrix.target }}.zip
asset_name: SimpleServer-${{ env.TAG }}-${{ matrix.target }}.zip asset_name: Ragon.SimpleServer-${{ env.TAG }}-${{ matrix.target }}.zip
asset_content_type: application/zip asset_content_type: application/zip
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

-35
View File
@@ -1,35 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using NetStack.Buffers;
namespace Ragon.Common
{
public static class RagonHeader
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteUShort(ushort id, ref Span<byte> data) {
data[0] = (byte)(id & 0x00FF);
data[1] = (byte)((id & 0xFF00) >> 8);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort ReadUShort(ref ReadOnlySpan<byte> data)
{
return (ushort)(data[0] + (data[1] << 8));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteInt(int id, ref Span<byte> data) {
data[0] = (byte)(id & 0x00FF);
data[1] = (byte)((id & 0xFF00) >> 8);
data[2] = (byte)((id & 0xFF00) >> 16);
data[3] = (byte)((id & 0xFF00) >> 24);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ReadInt(ref ReadOnlySpan<byte> data)
{
return (ushort)(data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24));
}
}
}
+6 -4
View File
@@ -1,13 +1,17 @@
namespace Ragon.Common namespace Ragon.Common
{ {
public enum RagonOperation: ushort public enum RagonOperation: byte
{ {
AUTHORIZE, AUTHORIZE,
AUTHORIZED_SUCCESS, AUTHORIZED_SUCCESS,
AUTHORIZED_FAILED, AUTHORIZED_FAILED,
JOIN_OR_CREATE_ROOM,
JOIN_ROOM, JOIN_ROOM,
LEAVE_ROOM, LEAVE_ROOM,
OWNERSHIP_CHANGED,
JOIN_SUCCESS,
JOIN_FAILED,
LOAD_SCENE, LOAD_SCENE,
SCENE_IS_LOADED, SCENE_IS_LOADED,
@@ -18,9 +22,7 @@ namespace Ragon.Common
CREATE_ENTITY, CREATE_ENTITY,
DESTROY_ENTITY, DESTROY_ENTITY,
RESTORE_BEGIN, SNAPSHOT,
RESTORE_END,
RESTORED,
REPLICATE_ENTITY_STATE, REPLICATE_ENTITY_STATE,
REPLICATE_ENTITY_EVENT, REPLICATE_ENTITY_EVENT,
+200
View File
@@ -0,0 +1,200 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
namespace Ragon.Common
{
public class RagonSerializer
{
private byte[] _data;
private int _offset;
private int _size;
public int Lenght => _offset;
public int Size => _size - _offset;
public RagonSerializer(int capacity = 256)
{
_data = new byte[capacity];
_offset = 0;
_size = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
ResizeIfNeed(1);
_data[_offset] = value;
_offset += 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte()
{
var value = _data[_offset];
_offset += 1;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt(int value)
{
ResizeIfNeed(4);
_data[_offset] = (byte) (value & 0x00FF);
_data[_offset + 1] = (byte) ((value & 0xFF00) >> 8);
_data[_offset + 2] = (byte) ((value & 0xFF00) >> 16);
_data[_offset + 3] = (byte) ((value & 0xFF00) >> 24);
_offset += 4;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ReadInt()
{
var value = _data[_offset] + (_data[_offset + 1] << 8) + (_data[_offset + 2] << 16) + (_data[_offset + 3] << 24);
_offset += 4;
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteString(string value)
{
var stringRaw = Encoding.UTF8.GetBytes(value).AsSpan();
ResizeIfNeed(2 + stringRaw.Length);
WriteUShort((ushort) stringRaw.Length);
var data = _data.AsSpan().Slice(_offset, stringRaw.Length);
stringRaw.CopyTo(data);
_offset += stringRaw.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString()
{
var lenght = ReadUShort();
var stringRaw = _data.AsSpan().Slice(_offset, lenght);
var str = Encoding.UTF8.GetString(stringRaw);
_offset += lenght;
return str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> ReadData(int lenght)
{
var data = _data.AsSpan();
var payloadData = data.Slice(_offset, lenght);
_offset += payloadData.Length;
return payloadData;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteData(ref ReadOnlySpan<byte> payload)
{
ResizeIfNeed(payload.Length);
var data = _data.AsSpan();
var payloadData = data.Slice(_offset, payload.Length);
payload.CopyTo(payloadData);
_offset += payload.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> GetWritableData(int lenght)
{
ResizeIfNeed(lenght);
var data = _data.AsSpan();
var payloadData = data.Slice(_offset, lenght);
_offset += lenght;
return payloadData;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteOperation(RagonOperation ragonOperation)
{
ResizeIfNeed(1);
_data[_offset] = (byte) ragonOperation;
_offset += 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public RagonOperation ReadOperation()
{
var op = (RagonOperation) _data[_offset];
_offset += 1;
return op;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUShort(ushort value)
{
ResizeIfNeed(2);
_data[_offset] = (byte) (value & 0x00FF);
_data[_offset + 1] = (byte) ((value & 0xFF00) >> 8);
_offset += 2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort ReadUShort()
{
var value = (ushort) (_data[_offset] + (_data[_offset + 1] << 8));
_offset += 2;
return value;
}
public void Clear()
{
_offset = 0;
_size = 0;
}
public void ToSpan(ref Span<byte> data)
{
var span = _data.AsSpan();
var dataSpan = span.Slice(0, _offset);
dataSpan.CopyTo(data);
}
public void FromSpan(ref ReadOnlySpan<byte> data)
{
Clear();
ResizeIfNeed(data.Length);
var dataSpan = _data.AsSpan();
data.CopyTo(dataSpan);
_size = data.Length;
}
public void FromArray(byte[] data)
{
Clear();
ResizeIfNeed(data.Length);
Buffer.BlockCopy(data, 0, _data, 0, _offset);
_size = data.Length;
}
public byte[] ToArray()
{
var bytes = new byte[_offset];
Buffer.BlockCopy(_data, 0, bytes, 0, _offset);
return bytes;
}
private void ResizeIfNeed(int lenght)
{
if (_offset + lenght < _data.Length)
return;
var newData = new byte[_data.Length * 4 + lenght];
Buffer.BlockCopy(_data, 0, newData, 0, _data.Length);
_data = newData;
}
}
}
@@ -9,9 +9,10 @@ namespace SimpleServer
static void Main(string[] args) static void Main(string[] args)
{ {
var bootstrap = new Bootstrap(); var bootstrap = new Bootstrap();
bootstrap.Configure(new SimplePluginFactory()); var app = bootstrap.Configure(new SimplePluginFactory());
app.Start();
Console.Read(); Console.Read();
app.Stop();
} }
} }
} }
@@ -23,8 +23,4 @@
<ProjectReference Include="..\Ragon\Ragon.csproj" /> <ProjectReference Include="..\Ragon\Ragon.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Source\Managers" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,29 @@
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 protocol, 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,8 +1,4 @@
using System.Runtime.InteropServices; using Ragon.Core;
using Game.Source.Events;
using NLog;
using Ragon.Common;
using Ragon.Core;
namespace Game.Source namespace Game.Source
{ {
@@ -20,12 +16,13 @@ namespace Game.Source
public override void OnPlayerJoined(Player player) public override void OnPlayerJoined(Player player)
{ {
_logger.Info("Player joined " + player.PlayerName);
// _logger.Info($"Player({player.PlayerName}) joined to Room({GameRoom.Id})");
} }
public override void OnPlayerLeaved(Player player) public override void OnPlayerLeaved(Player player)
{ {
_logger.Info("Player leaved " + player.PlayerName); // _logger.Info($"Player({player.PlayerName}) left from Room({GameRoom.Id})");
} }
} }
} }
@@ -5,16 +5,14 @@ namespace Game.Source
{ {
public class SimplePluginFactory : PluginFactory public class SimplePluginFactory : PluginFactory
{ {
public string PluginName { get; set; } = "SimplePlugin";
public PluginBase CreatePlugin(string map) public PluginBase CreatePlugin(string map)
{ {
return new SimplePlugin(); return new SimplePlugin();
} }
public AuthorizationManager CreateManager(Configuration configuration) public IAuthorizationProvider CreateAuthorizationProvider(Configuration configuration)
{ {
return new AuthorizerByKey(configuration); return new AuthorizationProviderByKey(configuration);
} }
} }
} }
+8
View File
@@ -0,0 +1,8 @@
{
"key": "defaultkey",
"tickRate": 30,
"skipTimeout": 60,
"server": {
"port": 4444
}
}
+258
View File
@@ -0,0 +1,258 @@
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.WriteInt(2);
ragonSerializer.WriteInt(20);
ragonSerializer.WriteString("map");
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(16);
}
}
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();
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
Console.ReadKey();
Library.Deinitialize();
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>StressTest</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ragon.Common\Ragon.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ENet-CSharp" Version="2.4.8" />
</ItemGroup>
</Project>
+7 -1
View File
@@ -2,10 +2,12 @@
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", "Ragon\Ragon.csproj", "{BABA1AF0-CF91-43F2-9577-53800068ACCF}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleServer", "SimpleServer\SimpleServer.csproj", "{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.SimpleServer", "Ragon.SimpleServer\Ragon.SimpleServer.csproj", "{C2B87ADE-A122-4366-8EB7-24DAE11EBAF0}"
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.Common", "Ragon.Common\Ragon.Common.csproj", "{F478B2A2-36F4-43B9-9BB7-382A57C449B2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ragon.Stress", "Ragon.Stress\Ragon.Stress.csproj", "{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -24,5 +26,9 @@ 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
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45E4C6A4-6AB5-4BEA-82DD-1F75C1648EC4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+1 -1
View File
@@ -18,7 +18,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ENet-CSharp" Version="2.4.8" /> <PackageReference Include="ENet-CSharp" Version="2.4.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta1" />
<PackageReference Include="NLog" Version="5.0.0-rc2" /> <PackageReference Include="NLog" Version="5.0.0-rc2" />
</ItemGroup> </ItemGroup>
+14 -69
View File
@@ -2,89 +2,34 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using ENet;
using NLog; using NLog;
namespace Ragon.Core namespace Ragon.Core
{ {
public class Application : IDisposable public class Application
{ {
private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly List<RoomThread> _roomThreads = new(); private readonly GameThread _gameThread;
private readonly Dictionary<uint, RoomThread> _socketByRoomThreads = new(); private readonly ENetServer _netServer;
private readonly Dictionary<RoomThread, int> _roomThreadCounter = new(); public Application(PluginFactory factory, Configuration configuration)
private readonly Configuration _configuration;
private readonly ENetServer _socketServer;
private int _roomThreadBalancer = 0;
public Application(PluginFactory factory, Configuration configuration, int threadsCount)
{ {
_socketServer = new ENetServer(); _gameThread = new GameThread(factory, configuration);
_configuration = configuration;
for (var i = 0; i < threadsCount; i++)
{
var roomThread = new RoomThread(factory, configuration);
_roomThreadCounter.Add(roomThread, 0);
_roomThreads.Add(roomThread);
}
} }
public void Start() public void Start()
{ {
_socketServer.Start(_configuration.Server.Port); Library.Initialize();
_gameThread.Start();
_logger.Info("Started");
}
foreach (var roomThread in _roomThreads) public void Stop()
roomThread.Start();
while (true)
{ {
foreach (var roomThread in _roomThreads) _gameThread.Stop();
while (roomThread.ReadOutEvent(out var evnt)) Library.Deinitialize();
_socketServer.WriteEvent(evnt); _logger.Info("Stopped");
while (_socketServer.ReadEvent(out var evnt))
{
if (evnt.Type == EventType.CONNECTED)
{
if (_roomThreadBalancer >= _roomThreads.Count)
_roomThreadBalancer = 0;
var roomThread = _roomThreads[_roomThreadBalancer];
_roomThreadCounter[roomThread] += 1;
_socketByRoomThreads.Add(evnt.PeerId, roomThread);
// TODO: Todo room manager matchmaking across all room threads
// TEMP_FIX: Remove this magical number
if (_roomThreadCounter[roomThread] > 20)
_roomThreadBalancer++;
}
if (_socketByRoomThreads.TryGetValue(evnt.PeerId, out var existsRoomThread))
existsRoomThread.WriteInEvent(evnt);
if (evnt.Type == EventType.DISCONNECTED)
{
_socketByRoomThreads.Remove(evnt.PeerId, out var roomThread);
_roomThreadCounter[roomThread] =- 1;
}
if (evnt.Type == EventType.TIMEOUT)
{
_socketByRoomThreads.Remove(evnt.PeerId, out var roomThread);
_roomThreadCounter[roomThread] =- 1;
}
}
Thread.Sleep(1);
}
}
public void Dispose()
{
foreach (var roomThread in _roomThreads)
roomThread.Dispose();
_roomThreads.Clear();
} }
} }
} }
@@ -0,0 +1,89 @@
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, byte protocol)
{
var dispatcher = _gameThread.Dispatcher;
_provider.OnAuthorizationRequest(key, name, protocol, 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)
{
return _playersByPeers[peerId];
}
public Player GetPlayer(string playerId)
{
return _playersByIds[playerId];
}
}
@@ -0,0 +1,6 @@
namespace Ragon.Core;
public interface IAuthorizationManager
{
}
@@ -0,0 +1,9 @@
using System;
using System.Threading.Tasks;
namespace Ragon.Core;
public interface IAuthorizationProvider
{
Task OnAuthorizationRequest(string key, string playerName, byte protocol, byte[] additionalData, Action<string, string> Accept, Action<uint> Reject);
}
-11
View File
@@ -1,11 +0,0 @@
using System;
namespace Ragon.Core;
public class AuthorizationManager
{
public virtual bool OnAuthorize(uint peerId, ref ReadOnlySpan<byte> payload)
{
return true;
}
}
+8 -5
View File
@@ -1,4 +1,6 @@
using NLog; using System;
using System.IO;
using NLog;
namespace Ragon.Core namespace Ragon.Core
{ {
@@ -6,12 +8,13 @@ namespace Ragon.Core
{ {
private ILogger _logger = LogManager.GetCurrentClassLogger(); private ILogger _logger = LogManager.GetCurrentClassLogger();
public void Configure(PluginFactory factory) public Application Configure(PluginFactory factory)
{ {
_logger.Info("Configure application..."); _logger.Info("Configure application...");
var configuration = ConfigurationLoader.Load("config.json"); var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
var app = new Application(factory, configuration, 2); var configuration = ConfigurationLoader.Load(filePath);
app.Start(); var app = new Application(factory, configuration);
return app;
} }
} }
} }
+1 -1
View File
@@ -6,13 +6,13 @@ namespace Ragon.Core
public struct Server public struct Server
{ {
public ushort Port; public ushort Port;
public ushort TickRate;
} }
[Serializable] [Serializable]
public struct Configuration public struct Configuration
{ {
public string Key; public string Key;
public ushort TickRate;
public Server Server; public Server Server;
} }
} }
@@ -9,7 +9,7 @@ namespace Ragon.Core
public static class ConfigurationLoader public static class ConfigurationLoader
{ {
private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private static readonly string _serverVersion = "1.0.0-rc"; private static readonly string _serverVersion = "1.0.5-rc";
private static void CopyrightInfo() private static void CopyrightInfo()
{ {
@@ -0,0 +1,6 @@
namespace Ragon.Core;
public interface Receiver<T>
{
public bool Receive(out T data);
}
@@ -0,0 +1,6 @@
namespace Ragon.Core;
public interface ISender<T>
{
public void Send(T data);
}
+21
View File
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace Ragon.Core;
public class Dispatcher: IDispatcher
{
public Queue<DispatcherTask> _actions = new Queue<DispatcherTask>();
public void Dispatch(Action action)
{
lock (_actions)
_actions.Enqueue(new DispatcherTask() { Action = action });
}
public void Process()
{
lock(_actions)
while(_actions.TryDequeue(out var action))
action.Execute();
}
}
+15
View File
@@ -0,0 +1,15 @@
using System;
using System.Diagnostics;
namespace Ragon.Core;
public class DispatcherTask
{
public Action Action;
public Action Callback;
public void Execute()
{
Action?.Invoke();
}
}
+9
View File
@@ -0,0 +1,9 @@
using System;
namespace Ragon.Core;
public interface IDispatcher
{
public void Dispatch(Action action);
public void Process();
}
@@ -0,0 +1,130 @@
using System.Collections.Generic;
using System.Threading;
namespace Ragon.Core.Core.Utils;
public class SynchronizedCache
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<int, string> innerCache = new Dictionary<int, string>();
public int Count
{ get { return innerCache.Count; } }
public string Read(int key)
{
cacheLock.EnterReadLock();
try
{
return innerCache[key];
}
finally
{
cacheLock.ExitReadLock();
}
}
public void Add(int key, string value)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public bool AddWithTimeout(int key, string value, int timeout)
{
if (cacheLock.TryEnterWriteLock(timeout))
{
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return true;
}
else
{
return false;
}
}
public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
cacheLock.EnterUpgradeableReadLock();
try
{
string result = null;
if (innerCache.TryGetValue(key, out result))
{
if (result == value)
{
return AddOrUpdateStatus.Unchanged;
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache[key] = value;
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
}
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Added;
}
}
finally
{
cacheLock.ExitUpgradeableReadLock();
}
}
public void Delete(int key)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Remove(key);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public enum AddOrUpdateStatus
{
Added,
Updated,
Unchanged
};
~SynchronizedCache()
{
if (cacheLock != null) cacheLock.Dispose();
}
}
+2
View File
@@ -10,6 +10,7 @@ public class Entity
public ushort EntityType { get; private set; } public ushort EntityType { get; private set; }
public RagonAuthority Authority { get; private set; } public RagonAuthority Authority { get; private set; }
public EntityState State { get; private set; } public EntityState State { get; private set; }
public EntityState Payload { get; private set; }
public Entity(uint ownerId, ushort entityType, RagonAuthority stateAuthority, RagonAuthority eventAuthority) public Entity(uint ownerId, ushort entityType, RagonAuthority stateAuthority, RagonAuthority eventAuthority)
{ {
@@ -17,6 +18,7 @@ public class Entity
EntityType = entityType; EntityType = entityType;
EntityId = _idGenerator++; EntityId = _idGenerator++;
State = new EntityState(stateAuthority); State = new EntityState(stateAuthority);
Payload = new EntityState(stateAuthority);
Authority = eventAuthority; Authority = eventAuthority;
} }
} }
+16 -10
View File
@@ -8,25 +8,31 @@ public class EntityState
{ {
public bool isDirty { get; private set; } public bool isDirty { get; private set; }
public RagonAuthority Authority { get; private set; } public RagonAuthority Authority { get; private set; }
public int Size => _size;
public byte[] Data private int _size = 0;
{ private byte[] _data = new byte[2048];
get => Data;
set
{
Data = value;
isDirty = true;
}
}
public EntityState(RagonAuthority ragonAuthority) public EntityState(RagonAuthority ragonAuthority)
{ {
Authority = ragonAuthority; Authority = ragonAuthority;
isDirty = false;
}
public ReadOnlySpan<byte> Read()
{
return _data.AsSpan().Slice(0, _size);
}
public void Write(ref ReadOnlySpan<byte> src)
{
src.CopyTo(_data);
_size = src.Length;
isDirty = true; isDirty = true;
} }
public void Clear() public void Clear()
{ {
isDirty = true; isDirty = false;
} }
} }
-39
View File
@@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using DisruptorUnity3d;
namespace Ragon.Core;
public class EventStream
{
private Stack<Event> _pool = new Stack<Event>(1024);
private RingBuffer<Event> _events = new RingBuffer<Event>(1024);
// public ref Event Reserve()
// {
// if (_pool.Count == 0)
// {
// var evnt = new Event();
// return ref evnt;
// }
// // var evnt = _pool.Pop();
// // ref Event evntRef = ref evnt;
// // return evntRef;
// }
// public void Retain(ref Event @event)
// {
//
// }
//
// public void WriteEvent(ref Event evnt)
// {
// // _pool.Push(evnt);
// _events.Enqueue(evnt);
// }
//
// public ref Event ReadEvent()
// {
//
// }
}
@@ -2,7 +2,7 @@ using System;
namespace Ragon.Core namespace Ragon.Core
{ {
public struct Event public struct SocketEvent
{ {
public EventType Type; public EventType Type;
public DeliveryType Delivery; public DeliveryType Delivery;
+389
View File
@@ -0,0 +1,389 @@
using System;
using System.Collections.Generic;
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 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 uint _ticks;
private readonly PluginBase _plugin;
private readonly IGameThread _gameThread;
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>();
public GameRoom(IGameThread gameThread, PluginBase pluginBase, string map, int min, int max)
{
_gameThread = gameThread;
_plugin = pluginBase;
Map = map;
PlayersMin = min;
PlayersMax = max;
Id = Guid.NewGuid().ToString();
_logger.Info($"Room created with plugin: {_plugin.GetType().Name}");
_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();
var isOwnershipChange = player.PeerId == _owner;
{
_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 && isOwnershipChange)
{
var newRoomOwnerId = _allPlayers[0];
var newRoomOwner = _players[newRoomOwnerId];
{
_plugin.OnOwnershipChanged(newRoomOwner);
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.OWNERSHIP_CHANGED);
_serializer.WriteString(newRoomOwner.Id);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData);
}
}
_entitiesAll = _entities.Values.ToArray();
}
}
public void ProcessEvent(uint peerId, ReadOnlySpan<byte> rawData)
{
var operation = (RagonOperation) rawData[0];
var payloadRawData = rawData.Slice(1, rawData.Length - 1);
_serializer.Clear();
_serializer.FromSpan(ref payloadRawData);
switch (operation)
{
case RagonOperation.REPLICATE_ENTITY_STATE:
{
var entityId = _serializer.ReadInt();
if (_entities.TryGetValue(entityId, out var ent))
{
if (ent.State.Authority == RagonAuthority.OWNER_ONLY && ent.OwnerId != peerId)
return;
var entityStateData = _serializer.ReadData(_serializer.Size);
ent.State.Write(ref entityStateData);
}
break;
}
case RagonOperation.REPLICATE_ENTITY_EVENT:
{
var evntId = _serializer.ReadUShort();
var entityId = _serializer.ReadInt();
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.WriteInt(entityId);
_serializer.WriteData(ref payload);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.REPLICATE_EVENT:
{
var evntId = _serializer.ReadUShort();
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(evntId);
_serializer.WriteData(ref payload);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.CREATE_ENTITY:
{
var entityType = _serializer.ReadUShort();
var stateAuthority = (RagonAuthority) _serializer.ReadByte();
var eventAuthority = (RagonAuthority) _serializer.ReadByte();
var entity = new Entity(peerId, entityType, stateAuthority, eventAuthority);
{
var entityPayload = _serializer.ReadData(_serializer.Size);
entity.Payload.Write(ref entityPayload);
}
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.WriteByte((byte) stateAuthority);
_serializer.WriteByte((byte) eventAuthority);
_serializer.WriteInt(entity.EntityId);
_serializer.WriteUShort(ownerId);
{
var entityPayload = entity.Payload.Read();
_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.WriteInt(_allPlayers.Length);
foreach (var playerPeerId in _allPlayers)
{
_serializer.WriteString(_players[playerPeerId].Id);
_serializer.WriteUShort((ushort) playerPeerId);
_serializer.WriteString(_players[playerPeerId].PlayerName);
}
_serializer.WriteInt(_entitiesAll.Length);
foreach (var entity in _entitiesAll)
{
var payload = entity.Payload.Read();
var state = entity.State.Read();
_serializer.WriteInt(entity.EntityId);
_serializer.WriteByte((byte) entity.State.Authority);
_serializer.WriteByte((byte) entity.Authority);
_serializer.WriteUShort(entity.EntityType);
_serializer.WriteUShort((ushort) entity.OwnerId);
_serializer.WriteUShort((ushort) payload.Length);
_serializer.WriteData(ref payload);
_serializer.WriteUShort((ushort) state.Length);
_serializer.WriteData(ref state);
}
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)
{
_ticks++;
_plugin.OnTick(_ticks, deltaTime);
foreach (var entity in _entitiesAll)
{
if (entity.State.isDirty)
{
var state = entity.State.Read();
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE);
_serializer.WriteInt(entity.EntityId);
_serializer.WriteData(ref state);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Unreliable);
entity.State.Clear();
}
}
}
public void Start()
{
_logger.Info("Room started");
_plugin.OnStart();
}
public void Stop()
{
_logger.Info("Room stopped");
_plugin.OnStop();
_plugin.Detach();
}
public Player GetPlayerById(uint peerId) => _players[peerId];
public Entity GetEntityById(int entityId) => _entities[entityId];
public Player GetOwner() => _players[_owner];
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)
{
foreach (var peer in peersIds)
{
_gameThread.Server.Send(peer, rawData, deliveryType);
}
}
public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
foreach (var peer in _allPlayers)
{
_gameThread.Server.Send(peer, rawData, deliveryType);
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using ENet;
using NLog;
namespace Ragon.Core
{
public class GameThread : IGameThread, IHandler
{
private readonly Dictionary<uint, GameRoom> _socketByRooms;
private readonly RoomManager _roomManager;
private readonly ISocketServer _server;
private readonly Thread _thread;
private readonly Server _serverConfiguration;
private readonly Stopwatch _gameLoopTimer;
private readonly Lobby _lobby;
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private readonly float _deltaTime = 0.0f;
private readonly Stopwatch _packetsTimer;
private int _packets = 0;
public IDispatcher Dispatcher { get; private set; }
public ISocketServer Server { get; private set; }
public GameThread(PluginFactory factory, Configuration configuration)
{
var authorizationProvider = factory.CreateAuthorizationProvider(configuration);
Dispatcher = new Dispatcher();
Server = new ENetServer(this);
_serverConfiguration = configuration.Server;
_deltaTime = 1000.0f / configuration.TickRate;
_roomManager = new RoomManager(factory, this);
_lobby = new Lobby(authorizationProvider, _roomManager, this);
_gameLoopTimer = new Stopwatch();
_packetsTimer = new Stopwatch();
_socketByRooms = new Dictionary<uint, GameRoom>();
_thread = new Thread(Execute);
_thread.Name = "Game Thread";
_thread.IsBackground = true;
}
public void Start()
{
Server.Start(_serverConfiguration.Port);
_gameLoopTimer.Start();
_packetsTimer.Start();
_thread.Start();
}
public void Stop()
{
Server.Stop();
_gameLoopTimer.Stop();
_packetsTimer.Stop();
_thread.Interrupt();
}
private void Execute()
{
while (true)
{
Server.Process();
Dispatcher.Process();
var elapsedMilliseconds = _gameLoopTimer.ElapsedMilliseconds;
if (elapsedMilliseconds > _deltaTime)
{
_roomManager.Tick(elapsedMilliseconds / 1000.0f);
_gameLoopTimer.Restart();
continue;
}
if (_packetsTimer.Elapsed.Seconds > 1)
{
_logger.Trace($"Clients: {_socketByRooms.Keys.Count} Packets: {_packets} per sec");
_packetsTimer.Restart();
_packets = 0;
}
Thread.Sleep(15);
}
}
public void Attach(uint peerId, GameRoom room)
{
_socketByRooms.Add(peerId, room);
}
public void Detach(uint peerId)
{
_socketByRooms.Remove(peerId);
}
public void OnEvent(Event evnt)
{
if (evnt.Type == ENet.EventType.Timeout || evnt.Type == ENet.EventType.Disconnect)
{
if (_socketByRooms.Remove(evnt.Peer.ID, out var room))
room.Leave(evnt.Peer.ID);
_lobby.OnDisconnected(evnt.Peer.ID);
}
if (evnt.Type == ENet.EventType.Receive)
{
_packets += 1;
try
{
var peerId = evnt.Peer.ID;
var dataRaw = new byte[evnt.Packet.Length];
evnt.Packet.CopyTo(dataRaw);
var data = new ReadOnlySpan<byte>(dataRaw);
if (_socketByRooms.TryGetValue(peerId, out var room))
{
room.ProcessEvent(peerId, data);
}
else
{
_lobby.ProcessEvent(peerId, data);
}
}
catch (Exception exception)
{
_logger.Error(exception);
}
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
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 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);
}
+11
View File
@@ -0,0 +1,11 @@
using Ragon.Common;
namespace Ragon.Core;
public interface IGameThread
{
public void Attach(uint peerId, GameRoom room);
public void Detach(uint peerId);
public IDispatcher Dispatcher { get; }
public ISocketServer Server { get; }
}
+66
View File
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using Ragon.Common;
namespace Ragon.Core;
public class Lobby
{
private readonly RagonSerializer _serializer;
private readonly AuthorizationManager _authorizationManager;
private readonly RoomManager _roomManager;
public Lobby(IAuthorizationProvider provider, RoomManager manager, IGameThread gameThread)
{
_roomManager = manager;
_serializer = new RagonSerializer();
_authorizationManager = new AuthorizationManager(provider, gameThread, this, _serializer);
}
public void ProcessEvent(uint peerId, ReadOnlySpan<byte> data)
{
var op = (RagonOperation) data[0];
var payload = data.Slice(1, data.Length - 1);
_serializer.Clear();
_serializer.FromSpan(ref payload);
switch (op)
{
case RagonOperation.AUTHORIZE:
{
var key = _serializer.ReadString();
var playerName = _serializer.ReadString();
var protocol = _serializer.ReadByte();
_authorizationManager.OnAuthorization(peerId, key, playerName, protocol);
break;
}
case RagonOperation.JOIN_ROOM:
{
var roomId = _serializer.ReadString();
var player = _authorizationManager.GetPlayer(peerId);
_roomManager.Join(player, roomId, Array.Empty<byte>());
break;
}
case RagonOperation.JOIN_OR_CREATE_ROOM:
{
var map = _serializer.ReadString();
var min = _serializer.ReadInt();
var max = _serializer.ReadInt();
var player = _authorizationManager.GetPlayer(peerId);
_roomManager.JoinOrCreate(player, map, min, max, Array.Empty<byte>());
break;
}
case RagonOperation.LEAVE_ROOM:
{
var player = _authorizationManager.GetPlayer(peerId);
_roomManager.Left(player, Array.Empty<byte>());
break;
}
}
}
public void OnDisconnected(uint peerId)
{
_authorizationManager.Cleanup(peerId);
}
}
+1
View File
@@ -5,6 +5,7 @@ namespace Ragon.Core
{ {
public class Player public class Player
{ {
public string Id { get; set; }
public uint PeerId { get; set; } public uint PeerId { get; set; }
public string PlayerName { get; set; } public string PlayerName { get; set; }
public bool IsLoaded { get; set; } public bool IsLoaded { get; set; }
+48 -63
View File
@@ -7,28 +7,31 @@ using Ragon.Common;
namespace Ragon.Core namespace Ragon.Core
{ {
public class PluginBase: IDisposable public class PluginBase
{ {
private delegate void SubscribeDelegate(Player player, ref ReadOnlySpan<byte> data); private delegate void SubscribeDelegate(Player player, ref ReadOnlySpan<byte> data);
private delegate void SubscribeEntityDelegate(Player player, Entity entity, ref ReadOnlySpan<byte> data); private delegate void SubscribeEntityDelegate(Player player, Entity entity, ref ReadOnlySpan<byte> data);
private Dictionary<ushort, SubscribeDelegate> _globalEvents = new(); private Dictionary<ushort, SubscribeDelegate> _globalEvents = new();
private Dictionary<int, Dictionary<ushort, SubscribeEntityDelegate>> _entityEvents = new(); private Dictionary<int, Dictionary<ushort, SubscribeEntityDelegate>> _entityEvents = new();
private BitBuffer _buffer = new BitBuffer(8192); private readonly BitBuffer _buffer = new();
private readonly RagonSerializer _serializer = new();
protected Room Room { get; private set; } protected IGameRoom GameRoom { get; private set; }
protected ILogger _logger; protected ILogger _logger;
public void Attach(Room room) public void Attach(GameRoom gameRoom)
{ {
_logger = LogManager.GetLogger($"Plugin<{GetType().Name}>"); _logger = LogManager.GetLogger($"Plugin<{GetType().Name}>");
Room = room; GameRoom = gameRoom;
_globalEvents.Clear(); _globalEvents.Clear();
_entityEvents.Clear(); _entityEvents.Clear();
} }
public void Dispose()
public void Detach()
{ {
_globalEvents.Clear(); _globalEvents.Clear();
_entityEvents.Clear(); _entityEvents.Clear();
@@ -66,10 +69,7 @@ namespace Ragon.Core
return; return;
} }
_globalEvents.Add(evntCode, (Player player, ref ReadOnlySpan<byte> raw) => _globalEvents.Add(evntCode, (Player player, ref ReadOnlySpan<byte> raw) => { action.Invoke(player); });
{
action.Invoke(player);
});
} }
public void Subscribe<T>(Entity entity, ushort evntCode, Action<Player, Entity, T> action) where T : IRagonSerializable, new() public void Subscribe<T>(Entity entity, ushort evntCode, Action<Player, Entity, T> action) where T : IRagonSerializable, new()
@@ -110,6 +110,7 @@ namespace Ragon.Core
_logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}"); _logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
return; return;
} }
_buffer.Clear(); _buffer.Clear();
_buffer.FromSpan(ref raw, raw.Length); _buffer.FromSpan(ref raw, raw.Length);
data.Deserialize(_buffer); data.Deserialize(_buffer);
@@ -128,19 +129,13 @@ namespace Ragon.Core
return; return;
} }
_entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) => _entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) => { action.Invoke(player, ent); });
{
action.Invoke(player, ent);
});
return; return;
} }
{ {
_entityEvents.Add(entity.EntityId, new Dictionary<ushort, SubscribeEntityDelegate>()); _entityEvents.Add(entity.EntityId, new Dictionary<ushort, SubscribeEntityDelegate>());
_entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) => _entityEvents[entity.EntityId].Add(evntCode, (Player player, Entity ent, ref ReadOnlySpan<byte> raw) => { action.Invoke(player, ent); });
{
action.Invoke(player, ent);
});
} }
} }
@@ -158,8 +153,8 @@ namespace Ragon.Core
if (!_entityEvents[entityId].ContainsKey(evntCode)) if (!_entityEvents[entityId].ContainsKey(evntCode))
return false; return false;
var player = Room.GetPlayerById(peerId); var player = GameRoom.GetPlayerById(peerId);
var entity = Room.GetEntityById(entityId); var entity = GameRoom.GetEntityById(entityId);
_entityEvents[entityId][evntCode].Invoke(player, entity, ref payload); _entityEvents[entityId][evntCode].Invoke(player, entity, ref payload);
return true; return true;
@@ -169,7 +164,7 @@ namespace Ragon.Core
{ {
if (_globalEvents.ContainsKey(evntCode)) if (_globalEvents.ContainsKey(evntCode))
{ {
var player = Room.GetPlayerById(peerId); var player = GameRoom.GetPlayerById(peerId);
_globalEvents[evntCode].Invoke(player, ref payload); _globalEvents[evntCode].Invoke(player, ref payload);
return true; return true;
} }
@@ -179,72 +174,64 @@ namespace Ragon.Core
public void SendEvent(Player player, uint eventCode, IRagonSerializable payload) public void SendEvent(Player player, uint eventCode, IRagonSerializable payload)
{ {
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_EVENT);
_buffer.Clear(); _buffer.Clear();
payload.Serialize(_buffer); payload.Serialize(_buffer);
var sendData = new byte[_buffer.Length + 4]; var payloadData = _serializer.GetWritableData(_buffer.Length);
Span<byte> data = sendData.AsSpan();
Span<byte> operationData = data.Slice(0, 2);
Span<byte> eventCodeData = data.Slice(2, 2);
Span<byte> payloadData = data.Slice(4, data.Length - 4);
_buffer.ToSpan(ref payloadData); _buffer.ToSpan(ref payloadData);
RagonHeader.WriteUShort((ushort) RagonOperation.REPLICATE_EVENT, ref operationData); var sendData = _serializer.ToArray();
RagonHeader.WriteUShort((ushort) eventCode, ref eventCodeData); GameRoom.Send(player.PeerId, sendData);
Room.Send(player.PeerId, sendData);
} }
public void SendEvent(ushort eventCode, IRagonSerializable payload) public void BroadcastEvent(ushort eventCode, IRagonSerializable payload)
{ {
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_EVENT);
_buffer.Clear(); _buffer.Clear();
payload.Serialize(_buffer); payload.Serialize(_buffer);
var sendData = new byte[_buffer.Length + 4]; var payloadData = _serializer.GetWritableData(_buffer.Length);
Span<byte> data = sendData.AsSpan();
Span<byte> operationData = data.Slice(0, 2);
Span<byte> eventCodeData = data.Slice(2, 2);
Span<byte> payloadData = data.Slice(4, _buffer.Length);
RagonHeader.WriteUShort((ushort) RagonOperation.REPLICATE_EVENT,ref operationData);
RagonHeader.WriteUShort( eventCode, ref eventCodeData);
_buffer.ToSpan(ref payloadData); _buffer.ToSpan(ref payloadData);
Room.Broadcast(sendData); var sendData = _serializer.ToArray();
GameRoom.Broadcast(sendData, DeliveryType.Reliable);
} }
public void SendEntityEvent(Player player, Entity entity, IRagonSerializable payload) public void SendEntityEvent(Player player, Entity entity, IRagonSerializable payload)
{ {
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
_serializer.WriteInt(entity.EntityId);
_buffer.Clear(); _buffer.Clear();
payload.Serialize(_buffer); payload.Serialize(_buffer);
var sendData = new byte[_buffer.Length + 6]; var payloadData = _serializer.GetWritableData(_buffer.Length);
Span<byte> data = sendData.AsSpan(); _buffer.ToSpan(ref payloadData);
Span<byte> operationData = data.Slice(0, 2);
Span<byte> entityData = data.Slice(2, 4);
RagonHeader.WriteUShort((ushort) RagonOperation.REPLICATE_EVENT, ref operationData); var sendData = _serializer.ToArray();
RagonHeader.WriteInt(entity.EntityId, ref entityData); GameRoom.Send(player.PeerId, sendData, DeliveryType.Reliable);
Room.Send(player.PeerId, sendData);
} }
public void SendEntityEvent(Entity entity, IRagonSerializable payload) public void BroadcastEntityEvent(Entity entity, IRagonSerializable payload)
{ {
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
_serializer.WriteInt(entity.EntityId);
_buffer.Clear(); _buffer.Clear();
payload.Serialize(_buffer); payload.Serialize(_buffer);
var sendData = new byte[_buffer.Length + 6]; var payloadData = _serializer.GetWritableData(_buffer.Length);
Span<byte> data = sendData.AsSpan(); _buffer.ToSpan(ref payloadData);
Span<byte> operationData = data.Slice(0, 2);
Span<byte> entityData = data.Slice(2, 4);
RagonHeader.WriteUShort((ushort) RagonOperation.REPLICATE_EVENT, ref operationData); var sendData = _serializer.ToArray();
RagonHeader.WriteInt(entity.EntityId, ref entityData); GameRoom.Broadcast(sendData);
Room.Broadcast(sendData);
} }
@@ -258,18 +245,16 @@ namespace Ragon.Core
{ {
} }
public virtual void OnOwnerChanged(Player player) public virtual void OnOwnershipChanged(Player player)
{ {
} }
public virtual void OnEntityCreated(Player creator, Entity entity) public virtual void OnEntityCreated(Player creator, Entity entity)
{ {
} }
public virtual void OnEntityDestroyed(Player destoyer, Entity entity) public virtual void OnEntityDestroyed(Player destoyer, Entity entity)
{ {
} }
public virtual void OnStart() public virtual void OnStart()
+1 -1
View File
@@ -3,7 +3,7 @@ namespace Ragon.Core
public interface PluginFactory public interface PluginFactory
{ {
public PluginBase CreatePlugin(string map); public PluginBase CreatePlugin(string map);
public AuthorizationManager CreateManager(Configuration configuration); public IAuthorizationProvider CreateAuthorizationProvider(Configuration configuration);
} }
+77
View File
@@ -0,0 +1,77 @@
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 List<GameRoom> _rooms = new List<GameRoom>();
public RoomManager(PluginFactory factory, IGameThread gameThread)
{
_gameThread = gameThread;
_factory = factory;
}
public void Join(Player player, string roomId, byte[] payload)
{
if (_rooms.Count > 0)
{
foreach (var existRoom in _rooms)
{
if (existRoom.Id == roomId && existRoom.PlayersCount < existRoom.PlayersMax)
{
existRoom.Joined(player, payload);
_gameThread.Attach(player.PeerId, existRoom);
break;
}
}
}
}
public void JoinOrCreate(Player player, string map, int min, int max, byte[] payload)
{
if (_rooms.Count > 0)
{
foreach (var existRoom in _rooms)
{
if (existRoom.Map == map && existRoom.PlayersCount < existRoom.PlayersMax)
{
existRoom.Joined(player, payload);
_gameThread.Attach(player.PeerId, existRoom);
return;
}
}
}
var plugin = _factory.CreatePlugin(map);
if (plugin == null)
throw new NullReferenceException($"Plugin for map {map} is null");
var room = new GameRoom(_gameThread, plugin, map, min, max);
room.Joined(player, payload);
room.Start();
_gameThread.Attach(player.PeerId, room);
_rooms.Add(room);
}
public void Left(Player player, byte[] payload)
{
}
public void Tick(float deltaTime)
{
foreach (var gameRoom in _rooms)
gameRoom.Tick(deltaTime);
}
}
-393
View File
@@ -1,393 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using NetStack.Serialization;
using NLog;
using Ragon.Common;
namespace Ragon.Core
{
public class Room : IDisposable
{
public int PlayersMin { get; private set; }
public int PlayersMax { get; private set; }
public int PlayersCount => _players.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 uint _ticks;
private readonly PluginBase _plugin;
private readonly RoomThread _roomThread;
// Cache
private uint[] _readyPlayers = Array.Empty<uint>();
private uint[] _allPlayers = Array.Empty<uint>();
private Entity[] _entitiesAll = Array.Empty<Entity>();
public Room(RoomThread roomThread, PluginBase pluginBase, string map, int min, int max)
{
_roomThread = roomThread;
_plugin = pluginBase;
Map = map;
PlayersMin = min;
PlayersMax = max;
Id = Guid.NewGuid().ToString();
_logger.Info($"Room created with plugin: {_plugin.GetType().Name}");
_plugin.Attach(this);
}
public void Joined(uint peerId, ReadOnlySpan<byte> payload)
{
if (_players.Count == 0)
{
_owner = peerId;
}
var player = new Player()
{
PlayerName = "Player " + peerId,
PeerId = peerId,
IsLoaded = false,
Entities = new List<Entity>(),
EntitiesIds = new List<int>(),
};
_players.Add(peerId, player);
_allPlayers = _players.Select(p => p.Key).ToArray();
{
var idRaw = Encoding.UTF8.GetBytes(Id).AsSpan();
var sendData = new byte[idRaw.Length + 18];
var data = sendData.AsSpan();
Span<byte> operationData = data.Slice(0, 2);
Span<byte> peerData = data.Slice(2, 4);
Span<byte> ownerData = data.Slice(4, 4);
Span<byte> minData = data.Slice(10, 4);
Span<byte> maxData = data.Slice(14, 4);
Span<byte> idData = data.Slice(18, idRaw.Length);
RagonHeader.WriteUShort((ushort) RagonOperation.JOIN_ROOM, ref operationData);
RagonHeader.WriteInt((int) peerId, ref peerData);
RagonHeader.WriteInt((int) _owner, ref ownerData);
RagonHeader.WriteInt(PlayersMin, ref minData);
RagonHeader.WriteInt(PlayersMax, ref maxData);
idRaw.CopyTo(idData);
Send(peerId, sendData);
}
{
var sceneRawData = Encoding.UTF8.GetBytes(Map).AsSpan();
var sendData = new byte[sceneRawData.Length + 2];
var data = sendData.AsSpan();
Span<byte> operationData = data.Slice(0, 2);
Span<byte> sceneData = data.Slice(2, sceneRawData.Length);
RagonHeader.WriteUShort((ushort) RagonOperation.LOAD_SCENE, ref operationData);
sceneRawData.CopyTo(sceneData);
Send(peerId, sendData, DeliveryType.Reliable);
}
}
public void Leave(uint peerId)
{
if (_players.Remove(peerId, out var player))
{
_allPlayers = _players.Select(p => p.Key).ToArray();
_plugin.OnPlayerLeaved(player);
foreach (var entityId in player.EntitiesIds)
{
var sendData = new byte[6];
var entityData = sendData.AsSpan();
var operationData = entityData.Slice(0, 2);
RagonHeader.WriteUShort((ushort) RagonOperation.DESTROY_ENTITY, ref operationData);
RagonHeader.WriteInt(entityId, ref entityData);
Broadcast(_allPlayers, sendData);
_entities.Remove(entityId);
}
}
}
public void ProcessEvent(RagonOperation operation, uint peerId, ReadOnlySpan<byte> rawData)
{
switch (operation)
{
case RagonOperation.REPLICATE_ENTITY_STATE:
{
var entityData = rawData.Slice(2, 4);
var entityId = RagonHeader.ReadInt(ref entityData);
if (_entities.TryGetValue(entityId, out var ent))
{
if (ent.State.Authority == RagonAuthority.OWNER_ONLY && ent.OwnerId != peerId)
return;
ent.State.Data = rawData.Slice(6, rawData.Length - 6).ToArray();
var data = new byte[rawData.Length];
rawData.CopyTo(data);
Broadcast(_readyPlayers, data);
}
break;
}
case RagonOperation.REPLICATE_ENTITY_EVENT:
{
var evntCodeData = rawData.Slice(2, 2);
var entityIdData = rawData.Slice(4, 4);
var evntId = RagonHeader.ReadUShort(ref evntCodeData);
var entityId = RagonHeader.ReadInt(ref entityIdData);
if (!_entities.TryGetValue(entityId, out var ent))
return;
if (ent.Authority == RagonAuthority.OWNER_ONLY && ent.OwnerId != peerId)
return;
var payload = rawData.Slice(8, rawData.Length - 8);
if (_plugin.InternalHandle(peerId, entityId, evntId, ref payload))
return;
var data = new byte[rawData.Length];
rawData.CopyTo(data);
Broadcast(_readyPlayers, data, DeliveryType.Reliable);
break;
}
case RagonOperation.REPLICATE_EVENT:
{
var evntCodeData = rawData.Slice(2, 2);
var evntId = RagonHeader.ReadUShort(ref evntCodeData);
var payload = rawData.Slice(4, rawData.Length - 4);
if (_plugin.InternalHandle(peerId, evntId, ref payload))
return;
var data = new byte[rawData.Length];
rawData.CopyTo(data);
Broadcast(_readyPlayers, data, DeliveryType.Reliable);
break;
}
case RagonOperation.CREATE_ENTITY:
{
var typeData = rawData.Slice(2, 2);
var authorityData = rawData.Slice(2, 2);
var entityPayloadData = rawData.Slice(4, rawData.Length - 4);
var entityType = RagonHeader.ReadUShort(ref typeData);
var stateAuthority = (RagonAuthority) authorityData[0];
var eventAuthority = (RagonAuthority) authorityData[1];
var entity = new Entity(peerId, entityType, stateAuthority, eventAuthority);
entity.State.Data = entityPayloadData.ToArray();
var player = _players[peerId];
player.Entities.Add(entity);
player.EntitiesIds.Add(entity.EntityId);
_entities.Add(entity.EntityId, entity);
_entitiesAll = _entities.Values.ToArray();
_plugin.OnEntityCreated(player, entity);
var data = new byte[entityPayloadData.Length + 14];
var sendData = data.AsSpan();
var operationData = sendData.Slice(0, 2);
var entityTypeData = sendData.Slice(2, 2);
var authority = sendData.Slice(4, 2);
var entityIdData = sendData.Slice(6, 4);
var peerData = sendData.Slice(10, 4);
var payload = sendData.Slice(14, entityPayloadData.Length);
entityPayloadData.CopyTo(payload);
authority[0] = authorityData[0];
authority[1] = authorityData[1];
RagonHeader.WriteUShort((ushort) RagonOperation.CREATE_ENTITY, ref operationData);
RagonHeader.WriteUShort(entityType, ref entityTypeData);
RagonHeader.WriteInt(entity.EntityId, ref entityIdData);
RagonHeader.WriteInt((int) peerId, ref peerData);
Broadcast(_allPlayers, data, DeliveryType.Reliable);
break;
}
case RagonOperation.DESTROY_ENTITY:
{
var entityData = rawData.Slice(2, 4);
var entityId = RagonHeader.ReadInt(ref entityData);
if (_entities.TryGetValue(entityId, out var entity))
{
if (entity.Authority == RagonAuthority.OWNER_ONLY && entity.OwnerId != peerId)
return;
var player = _players[peerId];
player.Entities.Remove(entity);
player.EntitiesIds.Remove(entity.EntityId);
_entities.Remove(entityId);
_entitiesAll = _entities.Values.ToArray();
_plugin.OnEntityDestroyed(player, entity);
var data = new byte[rawData.Length];
Span<byte> sendData = data.AsSpan();
rawData.CopyTo(sendData);
Broadcast(_readyPlayers, data, DeliveryType.Reliable);
}
break;
}
case RagonOperation.SCENE_IS_LOADED:
{
Send(peerId, RagonOperation.RESTORE_BEGIN, DeliveryType.Reliable);
foreach (var entity in _entities.Values)
{
var entityState = entity.State.Data.AsSpan();
var data = new byte[entity.State.Data.Length + 12];
Span<byte> sendData = data.AsSpan();
Span<byte> operationData = sendData.Slice(0, 2);
Span<byte> entityTypeData = sendData.Slice(2, 2);
Span<byte> authorityData = sendData.Slice(4, 2);
Span<byte> entityData = sendData.Slice(6, 4);
Span<byte> ownerData = sendData.Slice(10, 4);
Span<byte> entityStateData = sendData.Slice(14, entity.State.Data.Length);
RagonHeader.WriteUShort((ushort) RagonOperation.CREATE_ENTITY, ref operationData);
RagonHeader.WriteUShort(entity.EntityType, ref entityTypeData);
RagonHeader.WriteInt(entity.EntityId, ref entityData);
RagonHeader.WriteInt((int) entity.OwnerId, ref ownerData);
authorityData[0] = (byte) entity.State.Authority;
authorityData[1] = (byte) entity.Authority;
entityState.CopyTo(entityStateData);
Send(peerId, data, DeliveryType.Reliable);
}
Send(peerId, RagonOperation.RESTORE_END, DeliveryType.Reliable);
break;
}
case RagonOperation.RESTORED:
{
_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)
{
_ticks++;
_plugin.OnTick(_ticks, deltaTime);
}
public void Start()
{
_logger.Info("Room started");
_plugin.OnStart();
}
public void Stop()
{
_logger.Info("Room stopped");
_plugin.OnStop();
}
public void Dispose()
{
_logger.Info("Room destroyed");
_plugin.Dispose();
}
public Player GetPlayerById(uint peerId) => _players[peerId];
public Entity GetEntityById(int entityId) => _entities[entityId];
public Player GetOwner() => _players[_owner];
public void Send(uint peerId, RagonOperation operation, DeliveryType deliveryType = DeliveryType.Unreliable)
{
var rawData = new byte[2];
var rawDataSpan = new Span<byte>(rawData);
RagonHeader.WriteUShort((ushort) operation, ref rawDataSpan);
_roomThread.WriteOutEvent(new Event()
{
PeerId = peerId,
Data = rawData,
Type = EventType.DATA,
Delivery = deliveryType,
});
}
public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
_roomThread.WriteOutEvent(new Event()
{
PeerId = peerId,
Data = rawData,
Type = EventType.DATA,
Delivery = deliveryType,
});
}
public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
foreach (var peer in peersIds)
{
_roomThread.WriteOutEvent(new Event()
{
PeerId = peer,
Data = rawData,
Type = EventType.DATA,
Delivery = deliveryType,
});
}
}
public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
foreach (var player in _players.Values.ToArray())
{
_roomThread.WriteOutEvent(new Event()
{
PeerId = player.PeerId,
Data = rawData,
Type = EventType.DATA,
Delivery = deliveryType,
});
}
}
}
}
-166
View File
@@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using NLog;
using Ragon.Common;
namespace Ragon.Core
{
public class RoomManager
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private List<Room> _rooms;
private Dictionary<uint, Room> _peersByRoom;
private PluginFactory _factory;
private AuthorizationManager _manager;
private RoomThread _roomThread;
public Action<(uint, Room)> OnJoined;
public Action<(uint, Room)> OnLeaved;
public RoomManager(RoomThread roomThread, PluginFactory factory)
{
_roomThread = roomThread;
_factory = factory;
_manager = _factory.CreateManager(roomThread.Configuration);
_rooms = new List<Room>();
_peersByRoom = new Dictionary<uint, Room>();
}
public void ProcessEvent(RagonOperation operation, uint peerId, ReadOnlySpan<byte> payload)
{
switch (operation)
{
case RagonOperation.AUTHORIZE:
{
OnAuthorize(peerId, payload);
break;
}
case RagonOperation.JOIN_ROOM:
{
var room = Join(peerId, payload);
OnJoined?.Invoke((peerId, room));
break;
}
case RagonOperation.LEAVE_ROOM:
{
var room = Left(peerId, payload);
OnLeaved((peerId, room));
break;
}
}
}
public void OnAuthorize(uint peerId, ReadOnlySpan<byte> payload)
{
if (_manager.OnAuthorize(peerId, ref payload))
{
var sendData = new byte[2];
Span<byte> data = sendData.AsSpan();
RagonHeader.WriteUShort((ushort) RagonOperation.AUTHORIZED_SUCCESS, ref data);
_roomThread.WriteOutEvent(new Event()
{
Delivery = DeliveryType.Reliable,
Type = EventType.DATA,
Data = sendData,
PeerId = peerId,
});
}
else
{
var sendData = new byte[2];
var data = sendData.AsSpan();
RagonHeader.WriteUShort((ushort) RagonOperation.AUTHORIZED_FAILED, ref data);
_roomThread.WriteOutEvent(new Event()
{
Delivery = DeliveryType.Reliable,
Type = EventType.DATA,
Data = sendData,
PeerId = peerId,
});
_roomThread.WriteOutEvent(new Event()
{
Delivery = DeliveryType.Reliable,
Type = EventType.DISCONNECTED,
Data = Array.Empty<byte>(),
PeerId = peerId,
});
}
}
public Room Join(uint peerId, ReadOnlySpan<byte> payload)
{
var minData = payload.Slice(0, 2);
var maxData = payload.Slice(2, 2);
var mapData = payload.Slice(4, payload.Length - 4);
var map = Encoding.UTF8.GetString(mapData);
var min = RagonHeader.ReadUShort(ref minData);
var max = RagonHeader.ReadUShort(ref maxData);
Room room = null;
if (_rooms.Count > 0)
{
foreach (var existRoom in _rooms)
{
if (existRoom.Map == map && existRoom.PlayersCount < existRoom.PlayersMax)
{
room = existRoom;
room.Joined(peerId, payload);
_peersByRoom.Add(peerId, room);
return room;
}
}
}
var plugin = _factory.CreatePlugin(map);
if (plugin == null)
throw new NullReferenceException($"Plugin for map {map} is null");
room = new Room(_roomThread, plugin, map, min, max);
room.Joined(peerId, payload);
room.Start();
_peersByRoom.Add(peerId, room);
_rooms.Add(room);
return room;
}
public Room Left(uint peerId, ReadOnlySpan<byte> payload)
{
_peersByRoom.Remove(peerId, out var room);
return room;
}
public void Disconnected(uint peerId)
{
_peersByRoom.Remove(peerId, out var room);
if (room != null)
{
room.Leave(peerId);
if (room.PlayersCount <= 0)
{
_rooms.Remove(room);
room.Stop();
room.Dispose();
}
}
}
public void Tick(float deltaTime)
{
foreach (Room room in _rooms)
room.Tick(deltaTime);
}
}
}
-110
View File
@@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using DisruptorUnity3d;
using NLog;
using Ragon.Common;
namespace Ragon.Core
{
public class RoomThread : IDisposable
{
private readonly RoomManager _roomManager;
private readonly Dictionary<uint, Room> _socketByRooms;
private readonly Thread _thread;
private readonly Stopwatch _timer;
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private readonly float _deltaTime = 0.0f;
private RingBuffer<Event> _receiveBuffer = new RingBuffer<Event>(8192 + 8192);
private RingBuffer<Event> _sendBuffer = new RingBuffer<Event>(8192 + 8192);
public Configuration Configuration { get; private set; }
public bool ReadOutEvent(out Event evnt) => _sendBuffer.TryDequeue(out evnt);
public void WriteOutEvent(Event evnt) => _sendBuffer.Enqueue(evnt);
public bool ReadIntEvent(out Event evnt) => _receiveBuffer.TryDequeue(out evnt);
public void WriteInEvent(Event evnt) => _receiveBuffer.Enqueue(evnt);
public RoomThread(PluginFactory factory, Configuration configuration)
{
_thread = new Thread(Execute);
_thread.IsBackground = true;
_timer = new Stopwatch();
_socketByRooms = new Dictionary<uint, Room>();
Configuration = configuration;
_deltaTime = 1000.0f / Configuration.Server.TickRate;
_roomManager = new RoomManager(this, factory);
_roomManager.OnJoined += (tuple) => _socketByRooms.Add(tuple.Item1, tuple.Item2);
_roomManager.OnLeaved += (tuple) => _socketByRooms.Remove(tuple.Item1);
}
public void Start()
{
_timer.Start();
_thread.Start();
}
public void Stop()
{
_thread.Interrupt();
}
private void Execute()
{
while (true)
{
while (_receiveBuffer.TryDequeue(out var evnt))
{
if (evnt.Type == EventType.DISCONNECTED || evnt.Type == EventType.TIMEOUT)
{
if (_socketByRooms.ContainsKey(evnt.PeerId))
{
_roomManager.Disconnected(evnt.PeerId);
_socketByRooms.Remove(evnt.PeerId);
}
}
if (evnt.Type == EventType.DATA)
{
var data = new ReadOnlySpan<byte>(evnt.Data);
var operationData = data.Slice(0, 2);
var operation = (RagonOperation) RagonHeader.ReadUShort(ref operationData);
if (_socketByRooms.TryGetValue(evnt.PeerId, out var room))
{
try
{
room.ProcessEvent(operation, evnt.PeerId, data);
}
catch (Exception exception)
{
_logger.Error(exception);
}
}
else
{
var payload = data.Slice(2, data.Length - 2);
_roomManager.ProcessEvent(operation, evnt.PeerId, payload);
}
}
}
var elapsedMilliseconds = _timer.ElapsedMilliseconds;
if (elapsedMilliseconds > _deltaTime)
{
_roomManager.Tick(elapsedMilliseconds / 1000.0f);
_timer.Restart();
continue;
}
Thread.Sleep(15);
}
}
public void Dispose()
{
}
}
}
+27 -55
View File
@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using DisruptorUnity3d; using DisruptorUnity3d;
@@ -18,84 +17,69 @@ namespace Ragon.Core
Connected Connected
} }
public class ENetServer : IDisposable public class ENetServer : ISocketServer
{ {
public Status Status { get; private set; } public Status Status { get; private set; }
private ILogger _logger = LogManager.GetCurrentClassLogger(); private ILogger _logger = LogManager.GetCurrentClassLogger();
private Thread _thread;
private Host _host; private Host _host;
private Address _address; private Address _address;
private ENet.Event _netEvent; private Event _netEvent;
private Peer[] _peers; private Peer[] _peers;
private IHandler _handler;
private RingBuffer<Event> _receiveBuffer; public ENetServer(IHandler handler)
private RingBuffer<Event> _sendBuffer; {
_handler = handler;
public void WriteEvent(Event evnt) => _sendBuffer.Enqueue(evnt); }
public bool ReadEvent(out Event evnt) => _receiveBuffer.TryDequeue(out evnt);
public void Start(ushort port) public void Start(ushort port)
{ {
Library.Initialize();
_address = default; _address = default;
_address.Port = port; _address.Port = port;
_peers = new Peer[2048];
_host = new Host(); _host = new Host();
_host.Create(_address, 4095, 2, 0, 0, 1024 * 1024); _host.Create(_address, 2048, 2, 0, 0, 1024 * 1024);
_peers = new Peer[4095];
_sendBuffer = new RingBuffer<Event>(8192 + 8192);
_receiveBuffer = new RingBuffer<Event>(8192 + 8192);
Status = Status.Listening; Status = Status.Listening;
_logger.Info($"Network listening on {port}");
_thread = new Thread(Execute);
_thread.Name = "NetworkThread";
_thread.Start();
_logger.Info($"ENet Server Started at port {port}");
} }
private void Execute() public void Send(uint peerId, byte[] data, DeliveryType type)
{
while (true)
{
while (_sendBuffer.TryDequeue(out var data))
{
if (data.Type == EventType.DATA)
{ {
var newPacket = new Packet(); var newPacket = new Packet();
var packetFlags = PacketFlags.Instant; var packetFlags = PacketFlags.Instant;
byte channel = 1; byte channel = 1;
if (data.Delivery == DeliveryType.Reliable) if (type == DeliveryType.Reliable)
{ {
packetFlags = PacketFlags.Reliable; packetFlags = PacketFlags.Reliable;
channel = 0; channel = 0;
} }
else if (data.Delivery == DeliveryType.Unreliable) else if (type == DeliveryType.Unreliable)
{ {
channel = 1; channel = 1;
packetFlags = PacketFlags.Instant; packetFlags = PacketFlags.None;
} }
newPacket.Create(data.Data, data.Data.Length, packetFlags); newPacket.Create(data, data.Length, packetFlags);
_peers[data.PeerId].Send(channel, ref newPacket); _peers[peerId].Send(channel, ref newPacket);
} }
else if (data.Type == EventType.DISCONNECTED)
public void Disconnect(uint peerId, uint errorCode)
{ {
_peers[data.PeerId].DisconnectNow(0); _peers[peerId].Reset();
_receiveBuffer.Enqueue(data);
}
} }
public void Process()
{
bool polled = false; bool polled = false;
while (!polled) while (!polled)
{ {
if (_host.CheckEvents(out _netEvent) <= 0) if (_host.CheckEvents(out _netEvent) <= 0)
{ {
if (_host.Service(16, out _netEvent) <= 0) if (_host.Service(0, out _netEvent) <= 0)
break; break;
polled = true; polled = true;
@@ -109,44 +93,32 @@ namespace Ragon.Core
case ENet.EventType.Connect: case ENet.EventType.Connect:
{ {
var @event = new Event {PeerId = _netEvent.Peer.ID, Type = EventType.CONNECTED};
_peers[_netEvent.Peer.ID] = _netEvent.Peer; _peers[_netEvent.Peer.ID] = _netEvent.Peer;
_receiveBuffer.Enqueue(@event); _handler.OnEvent(_netEvent);
break; break;
} }
case ENet.EventType.Disconnect: case ENet.EventType.Disconnect:
{ {
var @event = new Event {PeerId = _netEvent.Peer.ID, Type = EventType.DISCONNECTED}; _handler.OnEvent(_netEvent);
_receiveBuffer.Enqueue(@event);
break; break;
} }
case ENet.EventType.Timeout: case ENet.EventType.Timeout:
{ {
var @event = new Event {PeerId = _netEvent.Peer.ID, Type = EventType.TIMEOUT}; _handler.OnEvent(_netEvent);
_receiveBuffer.Enqueue(@event);
break; break;
} }
case ENet.EventType.Receive: case ENet.EventType.Receive:
{ {
var data = new byte[_netEvent.Packet.Length]; _handler.OnEvent(_netEvent);
_netEvent.Packet.CopyTo(data);
_netEvent.Packet.Dispose(); _netEvent.Packet.Dispose();
var @event = new Event {PeerId = _netEvent.Peer.ID, Type = EventType.DATA, Data = data };
_receiveBuffer.Enqueue(@event);
break; break;
} }
} }
} }
} }
}
public void Dispose() public void Stop()
{ {
Library.Deinitialize();
_host?.Dispose(); _host?.Dispose();
} }
} }
+8
View File
@@ -0,0 +1,8 @@
using ENet;
namespace Ragon.Core;
public interface IHandler
{
public void OnEvent(Event evnt);
}
+10
View File
@@ -0,0 +1,10 @@
namespace Ragon.Core;
public interface ISocketServer
{
public void Start(ushort port);
public void Process();
public void Stop();
public void Send(uint peerId, byte[] data, DeliveryType type);
public void Disconnect(uint peerId, uint errorCode);
}
-60
View File
@@ -1,60 +0,0 @@
using System;
using System.Net;
using System.Net.WebSockets;
using System.Threading;
using DisruptorUnity3d;
using NLog;
namespace Ragon.Core;
public class WebsocketServer : IDisposable
{
private HttpListener _httpListener;
private ILogger _logger = LogManager.GetCurrentClassLogger();
private Thread _thread;
private ENet.Event _netEvent;
private RingBuffer<Event> _receiveBuffer;
private RingBuffer<Event> _sendBuffer;
public void WriteEvent(Event evnt) => _sendBuffer.Enqueue(evnt);
public bool ReadEvent(out Event evnt) => _receiveBuffer.TryDequeue(out evnt);
public void Start(ushort port)
{
// _httpListener = new HttpListener();
// _httpListener.Prefixes.Add("http://localhost/");
// _httpListener.Start();
//
// _thread = new Thread(Execute);
// _thread.Name = "NetworkThread";
// _thread.Start();
// _logger.Info($"Socket Server Started at port {port}");
}
public void Execute()
{
}
public async void ExecuteAsync()
{
// while (true)
// {
// HttpListenerContext context = await _httpListener.GetContextAsync();
// if (context.Request.IsWebSocketRequest)
// {
// HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
// WebSocket webSocket = webSocketContext.WebSocket;
// while (webSocket.State == WebSocketState.Open)
// {
// await webSocket.SendAsync(... );
// }
// }
// }
}
public void Dispose()
{
}
}
-6
View File
@@ -1,6 +0,0 @@
namespace Ragon.Core.Storage;
public struct EntityInfo
{
}
-6
View File
@@ -1,6 +0,0 @@
namespace Ragon.Core.Storage;
public struct PlayerInfo
{
}
-6
View File
@@ -1,6 +0,0 @@
namespace Ragon.Core.Storage;
public struct RoomInfo
{
}
-29
View File
@@ -1,29 +0,0 @@
namespace Ragon.Core.Storage;
public class Storage
{
// private ConnectionMultiplexer _connection;
public Storage(Configuration _configuration)
{
// _connection = ConnectionMultiplexer.Connect(_configuration.Key);
}
public void UpdateEntity(int entityId)
{
// var db = _connection.GetDatabase();
// db.set("entity_", )
}
public void UpdatePlayer()
{
}
public void UpdateRoom()
{
}
}
-20
View File
@@ -1,20 +0,0 @@
using System;
using System.Text;
using Ragon.Core;
namespace Game.Source;
public class AuthorizerByKey: AuthorizationManager
{
private Configuration _configuration;
public AuthorizerByKey(Configuration configuration)
{
_configuration = configuration;
}
public override bool OnAuthorize(uint peerId, ref ReadOnlySpan<byte> payload)
{
var key = Encoding.UTF8.GetString(payload);
return _configuration.Key == key;
}
}
-19
View File
@@ -1,19 +0,0 @@
using NetStack.Serialization;
using Ragon.Common;
namespace Game.Source.Events;
public class SimpleEvent: IRagonSerializable
{
public string Name;
public void Serialize(BitBuffer buffer)
{
buffer.AddString(Name);
}
public void Deserialize(BitBuffer buffer)
{
Name = buffer.ReadString();
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"key": "defaultkey",
"server": {
"port": 4444,
"skipTimeout": 60,
"tickRate": 30
}
}
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -1
View File
@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="Images/logo.png" width="200" > <img src="Images/ragon-logo.png" width="200" >
</p> </p>
## Ragon Server ## Ragon Server
@@ -27,6 +27,7 @@ Ragon is fully free high perfomance room based game server with plugin based arc
### Roadmap: ### Roadmap:
- Allow customize matchmaking - Allow customize matchmaking
- Refactoring some moments(a lot duplications of code, etc...)
- Use native memory - Use native memory
- Reduce allocations - Reduce allocations
- Dashboard for monitoring entities and players in realtime - Dashboard for monitoring entities and players in realtime
@@ -37,6 +38,7 @@ Ragon is fully free high perfomance room based game server with plugin based arc
### Requirements ### Requirements
- OSX, Windows, Linux(Ubuntu, Debian) - OSX, Windows, Linux(Ubuntu, Debian)
- .NET 6.0 - .NET 6.0
### Dependencies ### Dependencies
* ENet-Sharp v2.4.8 * ENet-Sharp v2.4.8
* NetStack latest * NetStack latest