diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..22edb8d
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
\ No newline at end of file
diff --git a/Ragon.Client.Property/Ragon.Client.Property.csproj b/Ragon.Client.Property/Ragon.Client.Property.csproj
new file mode 100644
index 0000000..d0ebad4
--- /dev/null
+++ b/Ragon.Client.Property/Ragon.Client.Property.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Ragon.Client.Property/Sources/RagonBool.cs b/Ragon.Client.Property/Sources/RagonBool.cs
new file mode 100644
index 0000000..2688b07
--- /dev/null
+++ b/Ragon.Client.Property/Sources/RagonBool.cs
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client.Simulation
+{
+ [Serializable]
+ public class RagonBool : RagonProperty
+ {
+ public bool Value
+ {
+ get => _value;
+ set
+ {
+ _value = value;
+
+ MarkAsChanged();
+ }
+ }
+
+ private bool _value;
+
+ public RagonBool(
+ bool initialValue,
+ bool invokeLocal = true,
+ int priority = 0
+ ) : base(priority, invokeLocal)
+ {
+ _value = initialValue;
+ SetFixedSize(1);
+ }
+
+ public override void Serialize(RagonBuffer buffer)
+ {
+ buffer.WriteBool(_value);
+ }
+
+ public override void Deserialize(RagonBuffer buffer)
+ {
+ _value = buffer.ReadBool();
+
+ InvokeChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Property/Sources/RagonFloat.cs b/Ragon.Client.Property/Sources/RagonFloat.cs
new file mode 100644
index 0000000..db14254
--- /dev/null
+++ b/Ragon.Client.Property/Sources/RagonFloat.cs
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ [Serializable]
+ public class RagonFloat : RagonProperty
+ {
+ public float Value
+ {
+ get => _value;
+ set
+ {
+ _value = value;
+
+ if (_value < _min)
+ _value = _min;
+ else if (_value > _max)
+ _value = _max;
+
+ MarkAsChanged();
+ }
+ }
+
+ private float _value;
+ private float _min;
+ private float _max;
+ private int _requiredBits;
+ private float _precision;
+ private uint _mask;
+
+ public RagonFloat(
+ float initialValue,
+ bool invokeLocal = true,
+ int priority = 0
+ ) : base(priority, invokeLocal)
+ {
+ _value = initialValue;
+ _min = -1024.0f;
+ _max = 1024.0f;
+ _precision = 0.01f;
+
+ _requiredBits = DeBruijn.Log2((uint)((_max - _min) * (1.0f / _precision) + 0.5f)) + 1;
+ _mask = (uint)((1L << _requiredBits) - 1);
+
+ SetFixedSize(_requiredBits);
+ }
+
+ public RagonFloat(
+ float initialValue,
+ float min = -1024.0f,
+ float max = 1024.0f,
+ float precision = 0.01f,
+ bool invokeLocal = true,
+ int priority = 0
+ ) : base(priority, invokeLocal)
+ {
+ _value = initialValue;
+ _min = min;
+ _max = max;
+ _precision = precision;
+
+ _requiredBits = DeBruijn.Log2((uint)((_max - _min) * (1.0f / _precision) + 0.5f)) + 1;
+ _mask = (uint)((1L << _requiredBits) - 1);
+
+ SetFixedSize(_requiredBits);
+ }
+
+ public override void Serialize(RagonBuffer buffer)
+ {
+ var compressedValue = (uint)((_value - _min) * (1f / _precision) + 0.5f) & _mask;
+ buffer.Write(compressedValue, _requiredBits);
+ }
+
+ public override void Deserialize(RagonBuffer buffer)
+ {
+ var compressedValue = buffer.Read(_requiredBits);
+ _value = compressedValue * _precision + _min;
+
+ if (_value < _min)
+ _value = _min;
+ else if (_value > _max)
+ _value = _max;
+
+ InvokeChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Property/Sources/RagonInt.cs b/Ragon.Client.Property/Sources/RagonInt.cs
new file mode 100644
index 0000000..063a5b2
--- /dev/null
+++ b/Ragon.Client.Property/Sources/RagonInt.cs
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client.Simulation
+{
+ [Serializable]
+ public class RagonInt : RagonProperty
+ {
+ public int Value
+ {
+ get => _value;
+ set
+ {
+ _value = value;
+ MarkAsChanged();
+ }
+ }
+
+ private int _min;
+ private int _max;
+ private int _requiredBits;
+ private int _value;
+
+ public RagonInt(
+ int initialValue,
+ bool invokeLocal = true,
+ int priority = 0
+ ) : base(priority, invokeLocal)
+ {
+ _value = initialValue;
+ _min = -1000;
+ _max = 1000;
+
+ _max = Math.Max(Math.Abs(_min), Math.Abs(_max));
+ _requiredBits = Bits.Compute(_max);
+
+ SetFixedSize(_requiredBits);
+ }
+
+ public RagonInt(
+ int initialValue,
+ int min = -1000,
+ int max = 1000,
+ bool invokeLocal = true,
+ int priority = 0
+ ) : base(priority, invokeLocal)
+ {
+ _value = initialValue;
+ _min = min;
+ _max = max;
+
+ _requiredBits = Bits.Compute(_max);
+
+ SetFixedSize(_requiredBits);
+ }
+
+ public override void Serialize(RagonBuffer buffer)
+ {
+ uint compressedValue = (uint)((_value << 1) ^ (_value >> 31));
+ buffer.Write(compressedValue, _requiredBits);
+ }
+
+ public override void Deserialize(RagonBuffer buffer)
+ {
+ var compressedValue = buffer.Read(_requiredBits);
+ _value = (int)((compressedValue >> 1) ^ -(int)(compressedValue & 1));
+
+ InvokeChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Property/Sources/RagonString.cs b/Ragon.Client.Property/Sources/RagonString.cs
new file mode 100644
index 0000000..0f7ca94
--- /dev/null
+++ b/Ragon.Client.Property/Sources/RagonString.cs
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using System.Text;
+using Ragon.Protocol;
+
+namespace Ragon.Client.Simulation
+{
+ [Serializable]
+ public class RagonString : RagonProperty
+ {
+ public string Value
+ {
+ get => _value;
+ set
+ {
+ _value = value;
+
+ MarkAsChanged();
+ }
+ }
+
+ private string _value;
+ private readonly UTF8Encoding _utf8Encoding = new UTF8Encoding(false, true);
+
+ public RagonString(
+ string initialValue,
+ bool invokeLocal = true,
+ int priority = 0
+ ) : base(priority, invokeLocal)
+ {
+ _value = initialValue;
+ }
+
+ public override void Serialize(RagonBuffer buffer)
+ {
+ var data = _utf8Encoding.GetBytes(_value);
+ var len = (uint) data.Length;
+
+ buffer.Write(len, 16);
+ buffer.WriteBytes(data);
+ }
+
+ public override void Deserialize(RagonBuffer buffer)
+ {
+ var len = (int) buffer.Read(16);
+ var data = buffer.ReadBytes(len);
+
+ _value = _utf8Encoding.GetString(data);
+
+ InvokeChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Program.cs b/Ragon.Client.Simulation/Program.cs
new file mode 100644
index 0000000..a34cb94
--- /dev/null
+++ b/Ragon.Client.Simulation/Program.cs
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Client.Simulation;
+
+var simulation = new Simulation();
+simulation.Start();
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj b/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj
new file mode 100644
index 0000000..890cd74
--- /dev/null
+++ b/Ragon.Client.Simulation/Ragon.Client.Simulation.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/Ragon.Client.Simulation/Sources/Game.cs b/Ragon.Client.Simulation/Sources/Game.cs
new file mode 100644
index 0000000..60a1c21
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/Game.cs
@@ -0,0 +1,110 @@
+
+
+namespace Ragon.Client.Simulation;
+
+public class Game : IRagonListener
+{
+ private RagonFloat _health;
+ private RagonInt _points;
+ private RagonString _name;
+ private RagonEntity _entity;
+ private RagonClient _client;
+
+ public Game(RagonClient client)
+ {
+ _client = client;
+ }
+
+ public void OnConnected(RagonClient client)
+ {
+ RagonLog.Trace("Connected");
+ _client.Session.AuthorizeWithKey("defaultkey", "Player Eduard", Array.Empty());
+ }
+
+ public void OnAuthorizationSuccess(RagonClient client, string playerId, string playerName)
+ {
+ RagonLog.Trace("Authorized");
+ client.Session.CreateOrJoin("Example", 1, 20);
+ }
+
+ public void OnAuthorizationFailed(RagonClient client, string message)
+ {
+ Console.WriteLine($"Authorization failed: {message}");
+ }
+
+ public void OnJoined(RagonClient client)
+ {
+ RagonLog.Trace("Joined");
+
+ _health = new RagonFloat(100.0f, false, 0);
+ _health.Changed += () => Console.WriteLine($"[Ragon Property] Health: {_health.Value}");
+
+ _points = new RagonInt(0, -1000, 1000, false, 0);
+ _points.Changed += () => Console.WriteLine($"[Ragon Property] Points: {_points.Value}");
+
+ _name = new RagonString("Edmand 000", false);
+ _name.Changed += () => Console.WriteLine($"[Ragon Property] Name: {_name.Value}");
+
+ _entity = new RagonEntity(12, 0);
+ _entity.State.AddProperty(_health);
+ _entity.State.AddProperty(_points);
+ _entity.State.AddProperty(_name);
+
+ client.Room.CreateEntity(_entity);
+ }
+
+ public void OnFailed(RagonClient client, string message)
+ {
+ RagonLog.Trace("Failed to join");
+ }
+
+ public void OnLeft(RagonClient client)
+ {
+ RagonLog.Trace("Left");
+ }
+
+ public void OnDisconnected(RagonClient client)
+ {
+ RagonLog.Trace("Disconnected");
+ }
+
+ public void OnPlayerJoined(RagonClient client, RagonPlayer player)
+ {
+ RagonLog.Trace("Player joined");
+ }
+
+ public void OnPlayerLeft(RagonClient client, RagonPlayer player)
+ {
+ RagonLog.Trace("Player left");
+ }
+
+ public void OnOwnershipChanged(RagonClient client, RagonPlayer player)
+ {
+ RagonLog.Trace("Owner ship changed");
+ }
+
+ public void OnLevel(RagonClient client, string sceneName)
+ {
+ RagonLog.Trace($"New level: {sceneName}");
+
+ client.Room.SceneLoaded();
+ }
+
+ private float _timer = 0;
+
+ public void Update()
+ {
+ if (_client.Status != RagonStatus.ROOM)
+ return;
+
+ _timer += 1 / 60.0f;
+ if (_timer > 1)
+ {
+ _health.Value += 20.0f;
+ _points.Value += 10;
+ _name.Value = $"Edmand 00{_client.Room.Local.PeerId}";
+ Console.WriteLine($"{_health.Value} {_points.Value} {_name.Value}");
+ _timer = 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs b/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs
new file mode 100644
index 0000000..d9bedb0
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/IO/RagonENetConnection.cs
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using ENet;
+using Event = ENet.Event;
+using EventType = ENet.EventType;
+
+namespace Ragon.Client
+{
+ public class RagonENetConnection : INetworkConnection
+ {
+ public ushort Id { get; }
+
+ public NetworkStatistics Statistics { get; private set; }
+ public INetworkChannel Reliable { get; private set; }
+ public INetworkChannel Unreliable { get; private set; }
+
+ public Action OnData { get; set; }
+ public Action OnConnected { get; set; }
+ public Action OnDisconnected { get; set; }
+ public ulong BytesSent { get; }
+ public ulong BytesReceived { get; }
+ public int Ping { get; }
+
+ private static bool _libraryLoaded = false;
+ private Host _host;
+ private Peer _peer;
+ private Event _netEvent;
+
+ public RagonENetConnection()
+ {
+ _host = new Host();
+ _host.Create();
+ }
+
+
+ public void Prepare()
+ {
+ if (!_libraryLoaded)
+ {
+ Library.Initialize();
+ _libraryLoaded = true;
+ }
+ }
+
+ public void Disconnect()
+ {
+ if (_peer.IsSet)
+ _peer.DisconnectNow(0);
+ }
+
+ public void Connect(string server, ushort port, uint protocol)
+ {
+ Address address = new Address();
+ address.SetHost(server);
+ address.Port = port;
+
+ _peer = _host.Connect(address, 2, protocol);
+ _peer.Timeout(32, 5000, 5000);
+ }
+
+ public void Update()
+ {
+ bool polled = false;
+ while (!polled)
+ {
+ if (_host.CheckEvents(out _netEvent) <= 0)
+ {
+ if (_host.Service(0, out _netEvent) <= 0)
+ break;
+
+ polled = true;
+ }
+
+ switch (_netEvent.Type)
+ {
+ case EventType.None:
+ break;
+ case EventType.Connect:
+ Statistics = new NetworkStatistics();
+ Reliable = new ENetReliableChannel(_netEvent.Peer, 0);
+ Unreliable = new ENetUnreliableChannel(_netEvent.Peer, 1);
+
+ OnConnected?.Invoke();
+ break;
+ case EventType.Disconnect:
+ OnDisconnected?.Invoke(DisconnectReason.MANUAL);
+ break;
+ case EventType.Timeout:
+ OnDisconnected?.Invoke(DisconnectReason.TIMEOUT);
+ break;
+ case EventType.Receive:
+ var data = new byte[_netEvent.Packet.Length];
+
+ _netEvent.Packet.CopyTo(data);
+ _netEvent.Packet.Dispose();
+
+ OnData?.Invoke(data);
+ break;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_host.IsSet)
+ {
+ _host?.Flush();
+ _host?.Dispose();
+ }
+
+ if (_libraryLoaded)
+ Library.Deinitialize();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs
new file mode 100644
index 0000000..f46b6f4
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/IO/RagonENetReliableChannel.cs
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using ENet;
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs
new file mode 100644
index 0000000..a95e9ce
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/IO/RagonENetUnreliableChannel.cs
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using ENet;
+
+namespace Ragon.Client;
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs b/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs
new file mode 100644
index 0000000..2ca2758
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/IO/RagonNullConnection.cs
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using ENet;
+using Event = ENet.Event;
+using EventType = ENet.EventType;
+
+namespace Ragon.Client
+{
+ public class RagonNullConnection : INetworkConnection
+ {
+ public ushort Id { get; }
+
+ public NetworkStatistics Statistics { get; private set; }
+ public INetworkChannel Reliable { get; private set; }
+ public INetworkChannel Unreliable { get; private set; }
+
+ public Action OnData { get; set; }
+ public Action OnConnected { get; set; }
+ public Action OnDisconnected { get; set; }
+ public ulong BytesSent { get; }
+ public ulong BytesReceived { get; }
+ public int Ping { get; }
+
+ private static bool _libraryLoaded = false;
+ private Host _host;
+ private Peer _peer;
+ private Event _netEvent;
+
+ public RagonNullConnection()
+ {
+ _host = new Host();
+ _host.Create();
+ }
+
+
+ public void Prepare()
+ {
+ if (!_libraryLoaded)
+ {
+ Library.Initialize();
+ _libraryLoaded = true;
+ }
+ }
+
+ public void Disconnect()
+ {
+ if (_peer.IsSet)
+ _peer.DisconnectNow(0);
+ }
+
+ public void Connect(string server, ushort port, uint protocol)
+ {
+ Address address = new Address();
+ address.SetHost(server);
+ address.Port = port;
+
+ _peer = _host.Connect(address, 2, protocol);
+ _peer.Timeout(32, 5000, 5000);
+
+ Statistics = new NetworkStatistics();
+ Reliable = new NullReliableChannel(_netEvent.Peer, 0);
+ Unreliable = new NullUnreliableChannel(_netEvent.Peer, 1);
+ }
+
+ public void Update()
+ {
+ bool polled = false;
+ while (!polled)
+ {
+ if (_host.CheckEvents(out _netEvent) <= 0)
+ {
+ if (_host.Service(0, out _netEvent) <= 0)
+ break;
+
+ polled = true;
+ }
+
+ switch (_netEvent.Type)
+ {
+ case EventType.None:
+ break;
+ case EventType.Connect:
+
+ OnConnected?.Invoke();
+ break;
+ case EventType.Disconnect:
+ OnDisconnected?.Invoke(DisconnectReason.MANUAL);
+ break;
+ case EventType.Timeout:
+ OnDisconnected?.Invoke(DisconnectReason.TIMEOUT);
+ break;
+ case EventType.Receive:
+ var data = new byte[_netEvent.Packet.Length];
+
+ _netEvent.Packet.CopyTo(data);
+ _netEvent.Packet.Dispose();
+
+ OnData?.Invoke(data);
+ break;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_host.IsSet)
+ {
+ _host?.Flush();
+ _host?.Dispose();
+ }
+
+ if (_libraryLoaded)
+ Library.Deinitialize();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs
new file mode 100644
index 0000000..a685d1d
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/IO/RagonNullReliableChannel.cs
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using ENet;
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public sealed class NullReliableChannel : INetworkChannel
+{
+ private Peer _peer;
+ private byte _channelId;
+
+ public NullReliableChannel(Peer peer, int channelId)
+ {
+ _peer = peer;
+ _channelId = (byte) channelId;
+ }
+
+ public void Send(byte[] data)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs b/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs
new file mode 100644
index 0000000..73b0874
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/IO/RagonNullUnreliableChannel.cs
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using ENet;
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public sealed class NullUnreliableChannel : INetworkChannel
+{
+ private Peer _peer;
+ private byte _channelId;
+
+ public NullUnreliableChannel(Peer peer, int channelId)
+ {
+ _peer = peer;
+ _channelId = (byte) channelId;
+ }
+
+ public void Send(byte[] data)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs b/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs
new file mode 100644
index 0000000..d4b5ca8
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/Player/PlayerPayload.cs
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client.Simulation.Sources;
+
+public class PlayerPayload : IRagonPayload
+{
+ public uint Name { get; set; }
+
+ public void Serialize(RagonBuffer buffer)
+ {
+ buffer.Write(Name, 16);
+ }
+
+ public void Deserialize(RagonBuffer buffer)
+ {
+ Name = buffer.Read(16);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client.Simulation/Sources/Simulation.cs b/Ragon.Client.Simulation/Sources/Simulation.cs
new file mode 100644
index 0000000..db8d223
--- /dev/null
+++ b/Ragon.Client.Simulation/Sources/Simulation.cs
@@ -0,0 +1,42 @@
+namespace Ragon.Client.Simulation;
+
+public class EntityListener : IRagonEntityListener
+{
+ public void OnEntityCreated(RagonEntity entity)
+ {
+ var health = new RagonFloat(100.0f, false, 0);
+ health.Value = 50;
+ health.Changed += () => Console.WriteLine($"[Ragon Property] Another Health: {health.Value}");
+
+ var points = new RagonInt(0, -1000, 1000, false, 0);
+ points.Changed += () => Console.WriteLine($"[Ragon Property] Anther Points: {points.Value}");
+
+ var name = new RagonString("Eduard", false);
+ name.Changed += () => Console.WriteLine($"[Ragon Property] Another Name: {name.Value}");
+
+ entity.State.AddProperty(health);
+ entity.State.AddProperty(points);
+ entity.State.AddProperty(name);
+ }
+}
+
+public class Simulation
+{
+ public void Start()
+ {
+ // INetworkConnection protocol = debug ? new RagonNullConnection() : new RagonENetConnection();
+ // var network = new RagonClient(protocol, new EntityListener(), 30);
+ // var game = new Game(network);
+ // network.AddListener(game);
+ // network.Connect("127.0.0.1", 5001, "1.0.0");
+ // var dt = 1000 / 60.0f;
+ // while (true)
+ // {
+ // game.Update();
+ // network.Update(dt);
+ // Thread.Sleep((int) dt);
+ // }
+ //
+ // network.Disconnect();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Ragon.Client.csproj b/Ragon.Client/Ragon.Client.csproj
new file mode 100644
index 0000000..ec23c4d
--- /dev/null
+++ b/Ragon.Client/Ragon.Client.csproj
@@ -0,0 +1,26 @@
+
+
+
+ netstandard2.1
+ enable
+ enable
+ 10
+ Ragon.Client.Simulation
+ Eduard Kargin (Edmand46)
+
+
+
+ true
+ none
+ /Users/edmand46/RagonProjects/ragon-unity-sdk/Assets/Ragon/Runtime/Plugins
+
+
+
+ /Users/edmand46/RagonProjects/ragon-unity-sdk/Assets/Ragon/Runtime/Plugins
+
+
+
+
+
+
+
diff --git a/Ragon.Client/Sources/Compressor/FloatCompressor.cs b/Ragon.Client/Sources/Compressor/FloatCompressor.cs
new file mode 100644
index 0000000..de051a6
--- /dev/null
+++ b/Ragon.Client/Sources/Compressor/FloatCompressor.cs
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client.Compressor;
+
+public class FloatCompressor
+{
+ public float Min { get; private set; }
+ public float Max { get; private set; }
+ public float Precision { get; private set; }
+ public int RequiredBits { get; private set; }
+
+ private uint _mask;
+
+ public FloatCompressor(float min = -1024.0f, float max = 1024.0f, float precision = 0.01f)
+ {
+ Min = min;
+ Max = max;
+ Precision = precision;
+ RequiredBits = DeBruijn.Log2((uint)((Max - Min) * (1f / Precision) + 0.5f)) + 1;
+
+ _mask = (uint)((1L << RequiredBits) - 1);
+ }
+
+ public uint Compress(float value)
+ {
+ return (uint)((value - Min) * 1f / Precision + 0.5f) & _mask;
+ }
+
+ public float Decompress(uint value)
+ {
+ var result = value * Precision + Min;
+
+ if (result < Min)
+ result = Min;
+ else if (result > Max)
+ result = Max;
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Compressor/IntCompressor.cs b/Ragon.Client/Sources/Compressor/IntCompressor.cs
new file mode 100644
index 0000000..0334f69
--- /dev/null
+++ b/Ragon.Client/Sources/Compressor/IntCompressor.cs
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client.Compressor;
+
+public class IntCompressor
+{
+ public int Min { get; private set; }
+ public int Max { get; private set; }
+ public int RequiredBits { get; private set; }
+
+ public IntCompressor(int min = -1000, int max = 1000)
+ {
+ Min = min;
+ Max = max;
+ RequiredBits = Bits.Compute(Max);
+ }
+
+ public uint Compress(int value)
+ {
+ return (uint)((value << 1) ^ (value >> 31));;
+ }
+
+ public int Decompress(uint value)
+ {
+ return (int)((value >> 1) ^ -(int)(value & 1));
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Entity/RagonEntity.cs b/Ragon.Client/Sources/Entity/RagonEntity.cs
new file mode 100644
index 0000000..818a6ec
--- /dev/null
+++ b/Ragon.Client/Sources/Entity/RagonEntity.cs
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ public sealed class RagonEntity
+ {
+ private delegate void OnEventDelegate(RagonPlayer player, RagonBuffer serializer);
+ private RagonClient _client;
+
+ public ushort Id { get; private set; }
+ public ushort Type { get; private set; }
+ public bool Replication { get; private set; }
+
+ public RagonAuthority Authority { get; private set; }
+ public RagonPlayer Owner { get; private set; }
+ public RagonEntityState State { get; private set; }
+
+ public bool IsAttached { get; private set; }
+ public bool HasAuthority { get; private set; }
+
+ public event Action Attached;
+ public event Action Detached;
+ public event Action OwnershipChanged;
+
+ internal bool PropertiesChanged => _propertiesChanged;
+ internal ushort SceneId => _sceneId;
+
+ private ushort _sceneId;
+ private bool _propertiesChanged;
+
+ private RagonPayload _spawnPayload;
+ private RagonPayload _destroyPayload;
+
+ private Dictionary _events = new();
+ private Dictionary> _localEvents = new();
+
+ public RagonEntity(ushort type = 0, ushort sceneId = 0)
+ {
+ State = new RagonEntityState(this);
+ Type = type;
+
+ _sceneId = sceneId;
+ }
+
+ internal void Attach(RagonClient client, ushort entityId, ushort entityType, bool hasAuthority, RagonPlayer owner, RagonPayload payload)
+ {
+ Type = entityType;
+ Id = entityId;
+ Owner = owner;
+ IsAttached = true;
+ Replication = true;
+ HasAuthority = hasAuthority;
+
+ _client = client;
+ _spawnPayload = payload;
+
+ Attached?.Invoke(this);
+ }
+
+ internal void Detach(RagonPayload payload)
+ {
+ _destroyPayload = payload;
+
+ Detached?.Invoke();
+ }
+
+ internal T GetPayload(RagonPayload data) where T : IRagonPayload, new()
+ {
+ var buffer = new RagonBuffer();
+ data.Write(buffer);
+
+ var payload = new T();
+ payload.Deserialize(buffer);
+
+ return payload;
+ }
+
+ public T GetSpawnPayload() where T : IRagonPayload, new()
+ {
+ return GetPayload(_spawnPayload);
+ }
+
+ public T GetDestroyPayload() where T : IRagonPayload, new()
+ {
+ return GetPayload(_destroyPayload);
+ }
+
+ public void ReplicateEvent(TEvent evnt, RagonPlayer target, RagonReplicationMode replicationMode)
+ where TEvent : IRagonEvent, new()
+ {
+ var evntId = _client.Event.GetEventCode(evnt);
+ var buffer = _client.Buffer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
+ buffer.WriteUShort(Id);
+ buffer.WriteUShort(evntId);
+ buffer.WriteByte((byte) replicationMode);
+ buffer.WriteByte((byte) RagonTarget.Player);
+ buffer.WriteUShort((ushort) target.PeerId);
+
+ evnt.Serialize(buffer);
+
+ var sendData = buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ public void ReplicateEvent(
+ TEvent evnt,
+ RagonTarget target = RagonTarget.All,
+ RagonReplicationMode replicationMode = RagonReplicationMode.Server)
+ where TEvent : IRagonEvent, new()
+ {
+ if (target != RagonTarget.ExceptOwner)
+ {
+ if (replicationMode == RagonReplicationMode.Local)
+ {
+ var eventCode = _client.Event.GetEventCode(evnt);
+ _localEvents[eventCode].Invoke(_client.Room.Local, evnt);
+ return;
+ }
+
+ if (replicationMode == RagonReplicationMode.LocalAndServer)
+ {
+ var eventCode = _client.Event.GetEventCode(evnt);
+ _localEvents[eventCode].Invoke(_client.Room.Local, evnt);
+ }
+ }
+
+ var evntId = _client.Event.GetEventCode(evnt);
+ var buffer = _client.Buffer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
+ buffer.WriteUShort(Id);
+ buffer.WriteUShort(evntId);
+ buffer.WriteByte((byte) replicationMode);
+ buffer.WriteByte((byte) target);
+
+ evnt.Serialize(buffer);
+
+ var sendData = buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ public void OnEvent(Action callback) where TEvent : IRagonEvent, new()
+ {
+ var t = new TEvent();
+ var eventCode = _client.Event.GetEventCode(t);
+
+ if (_events.ContainsKey(eventCode))
+ {
+ RagonLog.Warn($"Event already {eventCode} subscribed");
+ return;
+ }
+
+ _localEvents.Add(eventCode, (player, eventData) => { callback.Invoke(player, (TEvent) eventData); });
+ _events.Add(eventCode, (player, serializer) =>
+ {
+ t.Deserialize(serializer);
+ callback.Invoke(player, t);
+ });
+ }
+
+ internal void Write(RagonBuffer buffer)
+ {
+ buffer.WriteUShort(Id);
+
+ State.WriteState(buffer);
+
+ _propertiesChanged = false;
+ }
+
+
+ internal void Read(RagonBuffer buffer)
+ {
+ State.ReadState(buffer);
+ }
+
+ internal void Event(ushort eventCode, RagonPlayer caller, RagonBuffer buffer)
+ {
+ if (_events.ContainsKey(eventCode))
+ _events[eventCode]?.Invoke(caller, buffer);
+ }
+
+ internal void TrackChangedProperty(RagonProperty property)
+ {
+ _propertiesChanged = true;
+ }
+
+ public void OnOwnershipChanged(RagonPlayer player)
+ {
+ var prevOwner = Owner;
+
+ Owner = player;
+ HasAuthority = player.PeerId == _client.Room.Local.PeerId;
+
+ OwnershipChanged?.Invoke(prevOwner, player);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Entity/RagonEntityState.cs b/Ragon.Client/Sources/Entity/RagonEntityState.cs
new file mode 100644
index 0000000..f70099a
--- /dev/null
+++ b/Ragon.Client/Sources/Entity/RagonEntityState.cs
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public sealed class RagonEntityState
+{
+ private List _properties;
+ private RagonEntity _entity;
+
+ public RagonEntityState(RagonEntity entity)
+ {
+ _entity = entity;
+ _properties = new List(6);
+ }
+
+ public void AddProperty(RagonProperty property)
+ {
+ _properties.Add(property);
+
+ property.AssignEntity(_entity);
+ }
+
+ internal void WriteInfo(RagonBuffer buffer)
+ {
+ buffer.WriteUShort((ushort)_properties.Count);
+ foreach (var property in _properties)
+ {
+ buffer.WriteBool(property.IsFixed);
+ buffer.WriteUShort((ushort)property.Size);
+ }
+ }
+
+ internal void ReadState(RagonBuffer buffer)
+ {
+ foreach (var property in _properties)
+ {
+ var changed = buffer.ReadBool();
+ if (changed)
+ property.Deserialize(buffer);
+ }
+ }
+
+ internal void WriteState(RagonBuffer buffer)
+ {
+ foreach (var prop in _properties)
+ {
+ if (prop.IsDirty)
+ {
+ buffer.WriteBool(true);
+ prop.Write(buffer);
+ prop.Flush();
+ }
+ else
+ {
+ prop.AddTick();
+ buffer.WriteBool(false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Entity/RagonPayload.cs b/Ragon.Client/Sources/Entity/RagonPayload.cs
new file mode 100644
index 0000000..dcb7a77
--- /dev/null
+++ b/Ragon.Client/Sources/Entity/RagonPayload.cs
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public struct RagonPayload
+{
+ private uint[] _data = new uint[128];
+ private int _size = 0;
+
+ public RagonPayload(int capacity)
+ {
+ _size = capacity;
+ }
+ public int Size => _size;
+
+ public void Read(RagonBuffer buffer)
+ {
+ var readOnlySpan = _data.AsSpan();
+
+ buffer.ReadSpan(ref readOnlySpan, _size);
+ }
+
+ public void Write(RagonBuffer buffer)
+ {
+ ReadOnlySpan readOnlySpan = _data.AsSpan();
+
+ buffer.WriteSpan(ref readOnlySpan, _size);
+ }
+
+ public override string ToString()
+ {
+ return $"Payload Size: {_size}";
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Entity/RagonProperty.cs b/Ragon.Client/Sources/Entity/RagonProperty.cs
new file mode 100644
index 0000000..6c55d7a
--- /dev/null
+++ b/Ragon.Client/Sources/Entity/RagonProperty.cs
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ [Serializable]
+ public class RagonProperty
+ {
+ public string Name => _name;
+ public RagonEntity Entity => _entity;
+
+ public event Action Changed;
+ public bool IsDirty => _dirty && _ticks >= _priority;
+ public bool IsFixed => _fixed;
+ public int Size => _size;
+
+ private bool _fixed;
+ private string _name;
+ protected bool _invokeLocal;
+
+ private RagonEntity _entity;
+ private bool _dirty;
+ private int _size;
+ private int _ticks;
+ private int _priority;
+
+ protected RagonProperty(int priority, bool invokeLocal)
+ {
+ _size = 0;
+ _priority = priority;
+ _fixed = false;
+ _invokeLocal = invokeLocal;
+ }
+
+ public void SetName(string name)
+ {
+ _name = name;
+ }
+
+ protected void SetFixedSize(int size)
+ {
+ _size = size;
+ _fixed = true;
+ }
+
+ protected void InvokeChanged()
+ {
+ if (!_invokeLocal)
+ return;
+
+ Changed?.Invoke();
+ }
+
+ protected void MarkAsChanged()
+ {
+ InvokeChanged();
+
+ if (_dirty) return;
+ _dirty = true;
+
+ _entity?.TrackChangedProperty(this);
+ }
+
+ internal void Flush()
+ {
+ _dirty = false;
+ _ticks = 0;
+ }
+
+ internal void AddTick()
+ {
+ _ticks++;
+ }
+
+ internal void AssignEntity(RagonEntity obj)
+ {
+ _entity = obj;
+
+ Changed?.Invoke();
+ }
+
+ internal void Write(RagonBuffer buffer)
+ {
+ if (_fixed)
+ {
+ Serialize(buffer);
+ return;
+ }
+
+ var sizeOffset = buffer.WriteOffset;
+ buffer.Write(0, 16);
+ var propOffset = buffer.WriteOffset;
+
+ Serialize(buffer);
+
+ var propSize = (uint) (buffer.WriteOffset - propOffset);
+ buffer.Write(propSize, 16, sizeOffset);
+ }
+
+ public virtual void Serialize(RagonBuffer buffer)
+ {
+ }
+
+ public virtual void Deserialize(RagonBuffer buffer)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs b/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs
new file mode 100644
index 0000000..36f0ac4
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/AuthorizeFailedHandler.cs
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class AuthorizeFailedHandler: Handler
+{
+ private readonly RagonListenerList _listenerList;
+ public AuthorizeFailedHandler(RagonListenerList list)
+ {
+ _listenerList = list;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var message = buffer.ReadString();
+ _listenerList.OnAuthorizationFailed(message);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs b/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs
new file mode 100644
index 0000000..867fc29
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/AuthorizeSuccessHandler.cs
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class AuthorizeSuccessHandler: Handler
+{
+ private readonly RagonListenerList _listenerList;
+
+ public AuthorizeSuccessHandler(RagonListenerList listenerList)
+ {
+ _listenerList = listenerList;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var playerId = buffer.ReadString();
+ var playerName = buffer.ReadString();
+
+ _listenerList.OnAuthorizationSuccess(playerId, playerName);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/EntityCreateHandler.cs b/Ragon.Client/Sources/Handler/EntityCreateHandler.cs
new file mode 100644
index 0000000..fef3f34
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/EntityCreateHandler.cs
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class EntityCreateHandler : Handler
+{
+ private readonly RagonClient _client;
+ private readonly RagonPlayerCache _playerCache;
+ private readonly RagonEntityCache _entityCache;
+
+ public EntityCreateHandler(
+ RagonClient client,
+ RagonPlayerCache playerCache,
+ RagonEntityCache entityCache
+ )
+ {
+ _client = client;
+ _entityCache = entityCache;
+ _playerCache = playerCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var attachId = buffer.ReadUShort();
+ var entityType = buffer.ReadUShort();
+ var entityId = buffer.ReadUShort();
+ var ownerId = buffer.ReadUShort();
+ var player = _playerCache.GetPlayerByPeer(ownerId);
+ var payload = new RagonPayload(buffer.Capacity);
+ payload.Read(buffer);
+
+ if (player == null)
+ {
+ RagonLog.Warn($"Owner {ownerId}|{player.Name} not found in players");
+ return;
+ }
+
+ var hasAuthority = _playerCache.LocalPlayer.Id == player.Id;
+ var entity = _entityCache.OnCreate(attachId, entityType, 0, entityId, hasAuthority);
+ entity.Attach(_client, entityId, entityType, hasAuthority, player, payload);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/EntityDestroyHandler.cs b/Ragon.Client/Sources/Handler/EntityDestroyHandler.cs
new file mode 100644
index 0000000..2d8639c
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/EntityDestroyHandler.cs
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class EntityDestroyHandler: Handler
+{
+ private readonly RagonEntityCache _entityCache;
+
+ public EntityDestroyHandler(RagonEntityCache entityCache)
+ {
+ _entityCache = entityCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var entityId = buffer.ReadUShort();
+ var payload = new RagonPayload(buffer.Capacity);
+ payload.Read(buffer);
+
+ _entityCache.OnDestroy(entityId, payload);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/EntityEventHandler.cs b/Ragon.Client/Sources/Handler/EntityEventHandler.cs
new file mode 100644
index 0000000..d08e115
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/EntityEventHandler.cs
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class EntityEventHandler : Handler
+{
+ private readonly RagonClient _client;
+ private readonly RagonPlayerCache _playerCache;
+ private readonly RagonEntityCache _entityCache;
+
+ public EntityEventHandler(
+ RagonClient client,
+ RagonPlayerCache playerCache,
+ RagonEntityCache entityCache
+ )
+ {
+ _client = client;
+ _playerCache = playerCache;
+ _entityCache = entityCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var eventCode = buffer.ReadUShort();
+ var peerId = buffer.ReadUShort();
+ var executionMode = (RagonReplicationMode)buffer.ReadByte();
+ var entityId = buffer.ReadUShort();
+
+ var player = _playerCache.GetPlayerByPeer(peerId);
+ if (player == null)
+ {
+ RagonLog.Warn($"Player not found for event {eventCode}");
+ return;
+ }
+
+ if (player.IsMe && executionMode == RagonReplicationMode.LocalAndServer)
+ return;
+
+ _entityCache.OnEvent(player, entityId, eventCode, buffer);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/EntityStateHandler.cs b/Ragon.Client/Sources/Handler/EntityStateHandler.cs
new file mode 100644
index 0000000..d59b5dc
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/EntityStateHandler.cs
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class StateEntityHandler: Handler
+{
+ private readonly RagonEntityCache _entityCache;
+
+ public StateEntityHandler(RagonEntityCache entityCache)
+ {
+ _entityCache = entityCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var entitiesCount = buffer.ReadUShort();
+ for (var i = 0; i < entitiesCount; i++)
+ {
+ var entityId = buffer.ReadUShort();
+ _entityCache.OnState(entityId, buffer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/Handler.cs b/Ragon.Client/Sources/Handler/Handler.cs
new file mode 100644
index 0000000..ba396d6
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/Handler.cs
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public interface Handler
+{
+ public void Handle(RagonBuffer buffer);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/JoinFailedHandler.cs b/Ragon.Client/Sources/Handler/JoinFailedHandler.cs
new file mode 100644
index 0000000..73d3963
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/JoinFailedHandler.cs
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class JoinFailedHandler: Handler
+{
+ private readonly RagonListenerList _listenerList;
+
+ public JoinFailedHandler(RagonListenerList listenerList)
+ {
+ _listenerList = listenerList;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var message = buffer.ReadString();
+ _listenerList.OnFailed(message);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs b/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs
new file mode 100644
index 0000000..1ad67b5
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/JoinSuccessHandler.cs
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public struct RagonRoomInformation
+{
+ public RagonRoomInformation(string roomId, string playerId, string ownerId, ushort min, ushort max)
+ {
+ RoomId = roomId;
+ PlayerId = playerId;
+ OwnerId = ownerId;
+ Min = min;
+ Max = max;
+ }
+
+ public string RoomId { get; private set; }
+ public string PlayerId { get; private set; }
+ public string OwnerId { get; private set; }
+ public ushort Min { get; private set; }
+ public ushort Max { get; private set; }
+}
+
+internal class JoinSuccessHandler : Handler
+{
+ private readonly RagonListenerList _listenerList;
+ private readonly RagonPlayerCache _playerCache;
+ private readonly RagonEntityCache _entityCache;
+ private readonly RagonClient _client;
+ private readonly RagonBuffer _buffer;
+
+ public JoinSuccessHandler(
+ RagonClient client,
+ RagonBuffer buffer,
+ RagonListenerList listenerList,
+ RagonPlayerCache playerCache,
+ RagonEntityCache entityCache
+ )
+ {
+ _buffer = buffer;
+ _client = client;
+ _listenerList = listenerList;
+ _entityCache = entityCache;
+ _playerCache = playerCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var roomId = buffer.ReadString();
+ var localId = buffer.ReadString();
+ var ownerId = buffer.ReadString();
+ var min = buffer.ReadUShort();
+ var max = buffer.ReadUShort();
+ var map = buffer.ReadString();
+
+ var scene = new RagonScene(_client, _playerCache, _entityCache);
+ var roomInfo = new RagonRoomInformation(roomId, localId, ownerId, min, max);
+ var room = new RagonRoom(_client, _entityCache, _playerCache, roomInfo, scene);
+
+ _playerCache.SetOwnerAndLocal(ownerId, localId);
+ _client.AssignRoom(room);
+ _listenerList.OnLevel(map);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs b/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs
new file mode 100644
index 0000000..bd9e6e2
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/LeaveRoomHandler.cs
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class LeaveRoomHandler : Handler
+{
+ private readonly RagonClient _client;
+ private readonly RagonListenerList _listenerList;
+ private readonly RagonEntityCache _entityCache;
+
+ public LeaveRoomHandler(
+ RagonClient client,
+ RagonListenerList listenerList,
+ RagonEntityCache entityCache)
+ {
+ _client = client;
+ _listenerList = listenerList;
+ _entityCache = entityCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ _listenerList.OnLeft();
+ _entityCache.Cleanup();
+ _client.Room.Cleanup();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/LoadSceneHandler.cs b/Ragon.Client/Sources/Handler/LoadSceneHandler.cs
new file mode 100644
index 0000000..1479275
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/LoadSceneHandler.cs
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class SceneLoadHandler: Handler
+{
+ private readonly RagonClient _client;
+ private readonly RagonListenerList _listenerList;
+
+ public SceneLoadHandler(
+ RagonClient ragonClient,
+ RagonListenerList listenerList
+ )
+ {
+ _client = ragonClient;
+ _listenerList = listenerList;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var sceneName = buffer.ReadString();
+ var room = _client.Room;
+
+ room.Cleanup();
+
+ _listenerList.OnLevel(sceneName);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/OwnershipHandler.cs b/Ragon.Client/Sources/Handler/OwnershipHandler.cs
new file mode 100644
index 0000000..2fb75c6
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/OwnershipHandler.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class OwnershipHandler: Handler
+{
+ private readonly RagonListenerList _listenerList;
+ private readonly RagonPlayerCache _playerCache;
+ private readonly RagonEntityCache _entityCache;
+
+ public OwnershipHandler(
+ RagonListenerList listenerList,
+ RagonPlayerCache playerCache,
+ RagonEntityCache entityCache)
+ {
+ _listenerList = listenerList;
+ _playerCache = playerCache;
+ _entityCache = entityCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var newOwnerId = buffer.ReadString();
+ var player = _playerCache.GetPlayerById(newOwnerId);
+
+ _playerCache.OnOwnershipChanged(newOwnerId);
+ _listenerList.OnOwnershipChanged(player);
+
+ var entities = buffer.ReadUShort();
+ for (var i = 0; i < entities; i++)
+ {
+ var entityId = buffer.ReadUShort();
+ _entityCache.OnOwnershipChanged(player, entityId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs b/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs
new file mode 100644
index 0000000..3057d37
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/PlayerJoinHandler.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class PlayerJoinHandler : Handler
+{
+ private RagonPlayerCache _playerCache;
+ private RagonListenerList _listenerList;
+
+ public PlayerJoinHandler(
+ RagonPlayerCache playerCache,
+ RagonListenerList listenerList
+ )
+ {
+ _playerCache = playerCache;
+ _listenerList = listenerList;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var playerPeerId = buffer.ReadUShort();
+ var playerId = buffer.ReadString();
+ var playerName = buffer.ReadString();
+
+ _playerCache.AddPlayer(playerPeerId, playerId, playerName);
+
+ var player = _playerCache.GetPlayerById(playerId);
+ if (player != null)
+ _listenerList.OnPlayerJoined(player);
+ else
+ RagonLog.Trace($"[Joined] {playerId}");
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs b/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs
new file mode 100644
index 0000000..fcff2c9
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/PlayerLeftHandler.cs
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class PlayerLeftHandler : Handler
+{
+ private RagonPlayerCache _playerCache;
+ private RagonEntityCache _entityCache;
+ private RagonListenerList _listenerList;
+
+ public PlayerLeftHandler(
+ RagonEntityCache entityCache,
+ RagonPlayerCache playerCache,
+ RagonListenerList listenerList
+ )
+ {
+ _entityCache = entityCache;
+ _playerCache = playerCache;
+ _listenerList = listenerList;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var playerId = buffer.ReadString();
+ var player = _playerCache.GetPlayerById(playerId);
+ if (player != null)
+ {
+ _playerCache.RemovePlayer(playerId);
+ _listenerList.OnPlayerLeft(player);
+
+ var entities = buffer.ReadUShort();
+ var toDeleteIds = new ushort[entities];
+ for (var i = 0; i < entities; i++)
+ {
+ var entityId = buffer.ReadUShort();
+ toDeleteIds[i] = entityId;
+ }
+
+ foreach (var id in toDeleteIds)
+ _entityCache.OnDestroy(id, new RagonPayload());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Handler/SnapshotHandler.cs b/Ragon.Client/Sources/Handler/SnapshotHandler.cs
new file mode 100644
index 0000000..5cdde43
--- /dev/null
+++ b/Ragon.Client/Sources/Handler/SnapshotHandler.cs
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+internal class SnapshotHandler : Handler
+{
+ private RagonClient _client;
+ private RagonListenerList _listenerList;
+ private RagonEntityCache _entityCache;
+ private RagonPlayerCache _playerCache;
+
+ public SnapshotHandler(
+ RagonClient ragonClient,
+ RagonListenerList listenerList,
+ RagonEntityCache entityCache,
+ RagonPlayerCache playerCache
+ )
+ {
+ _client = ragonClient;
+ _listenerList = listenerList;
+ _entityCache = entityCache;
+ _playerCache = playerCache;
+ }
+
+ public void Handle(RagonBuffer buffer)
+ {
+ var playersCount = buffer.ReadUShort();
+ RagonLog.Trace("Players: " + playersCount);
+ for (var i = 0; i < playersCount; i++)
+ {
+ var playerPeerId = buffer.ReadUShort();
+ var playerId = buffer.ReadString();
+ var playerName = buffer.ReadString();
+
+ _playerCache.AddPlayer(playerPeerId, playerId, playerName);
+ }
+ var dynamicEntities = buffer.ReadUShort();
+ RagonLog.Trace("Dynamic Entities: " + dynamicEntities);
+ for (var i = 0; i < dynamicEntities; i++)
+ {
+ var entityType = buffer.ReadUShort();
+ var entityId = buffer.ReadUShort();
+ var ownerPeerId = buffer.ReadUShort();
+ var payloadSize = buffer.ReadUShort();
+ var player = _playerCache.GetPlayerByPeer(ownerPeerId);
+
+ var payload = new RagonPayload(payloadSize);
+ payload.Read(buffer);
+
+ var hasAuthority = _playerCache.LocalPlayer.Id == player.Id;
+ var entity = _entityCache.OnCreate(0, entityType, 0, entityId, hasAuthority);
+
+ entity.Read(buffer);
+ entity.Attach(_client, entityId, entityType, hasAuthority, player, payload);
+ }
+
+ var staticEntities = buffer.ReadUShort();
+ RagonLog.Trace("Scene Entities: " + staticEntities);
+ for (var i = 0; i < staticEntities; i++)
+ {
+ var entityType = buffer.ReadUShort();
+ var entityId = buffer.ReadUShort();
+ var staticId = buffer.ReadUShort();
+ var ownerPeerId = buffer.ReadUShort();
+ var payloadSize = buffer.ReadUShort();
+ var player = _playerCache.GetPlayerByPeer(ownerPeerId);
+
+ var payload = new RagonPayload(payloadSize);
+ payload.Read(buffer);
+
+ var hasAuthority = _playerCache.LocalPlayer.Id == player.Id;
+ var entity = _entityCache.OnCreate(0, entityType, staticId, entityId, hasAuthority);
+
+ entity.Read(buffer);
+ entity.Attach(_client, entityId, entityType, hasAuthority, player, payload);
+ }
+
+ _listenerList.OnJoined();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IO/DisconnectReason.cs b/Ragon.Client/Sources/IO/DisconnectReason.cs
new file mode 100644
index 0000000..af1901d
--- /dev/null
+++ b/Ragon.Client/Sources/IO/DisconnectReason.cs
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public enum DisconnectReason
+{
+ MANUAL,
+ TIMEOUT,
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IO/INetworkChannel.cs b/Ragon.Client/Sources/IO/INetworkChannel.cs
new file mode 100644
index 0000000..161ce63
--- /dev/null
+++ b/Ragon.Client/Sources/IO/INetworkChannel.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface INetworkChannel
+{
+ void Send(byte[] data);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IO/INetworkConnection.cs b/Ragon.Client/Sources/IO/INetworkConnection.cs
new file mode 100644
index 0000000..f32b730
--- /dev/null
+++ b/Ragon.Client/Sources/IO/INetworkConnection.cs
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Client;
+
+public interface INetworkConnection: IRagonConnection
+{
+ public INetworkChannel Reliable { get; }
+ public INetworkChannel Unreliable { get; }
+ public Action OnData { get; set; }
+ public Action OnConnected { get; set; }
+ public Action OnDisconnected { get; set; }
+ public ulong BytesSent { get; }
+ public ulong BytesReceived { get; }
+ public int Ping { get; }
+ public void Prepare();
+ public void Connect(string address, ushort port, uint protocol);
+ public void Disconnect();
+ public void Update();
+ public void Dispose();
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IO/NetworkStatistics.cs b/Ragon.Client/Sources/IO/NetworkStatistics.cs
new file mode 100644
index 0000000..37aaece
--- /dev/null
+++ b/Ragon.Client/Sources/IO/NetworkStatistics.cs
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Client;
+
+public class NetworkStatistics
+{
+ private const double Interval = 1.0d;
+ private double _upstreamBandwidth = 0d;
+ private double _downstreamBandwidth = 0d;
+ private double _time = 0d;
+ private ulong _upstreamData = 0;
+ private ulong _downstreamData = 0;
+ private ulong _sent = 0;
+ private ulong _received = 0;
+ private int _ping;
+
+ public int Ping => _ping;
+ public double UpstreamBandwidth => _upstreamBandwidth;
+ public double DownstreamBandwidth => _downstreamBandwidth;
+
+ public void Update(ulong sent, ulong received, int ping, float dt)
+ {
+ _sent = sent;
+ _received = received;
+ _ping = ping;
+
+ _time += dt;
+ if (_time >= Interval)
+ {
+ if (_upstreamData > 0)
+ {
+ _upstreamData = _sent - _upstreamData;
+ _upstreamBandwidth = (_upstreamData / _time) / 1000 ;
+ }
+
+ if (_downstreamData > 0)
+ {
+ _downstreamData = _received - _downstreamData;
+ _downstreamBandwidth = (_downstreamData / _time) / 1000;
+ }
+
+ _upstreamData = _sent;
+ _downstreamData = _received;
+ _time -= Interval;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IRagonConnection.cs b/Ragon.Client/Sources/IRagonConnection.cs
new file mode 100644
index 0000000..2a1c116
--- /dev/null
+++ b/Ragon.Client/Sources/IRagonConnection.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonConnection
+{
+
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IRagonEntityListener.cs b/Ragon.Client/Sources/IRagonEntityListener.cs
new file mode 100644
index 0000000..6101d52
--- /dev/null
+++ b/Ragon.Client/Sources/IRagonEntityListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonEntityListener
+{
+ public void OnEntityCreated(RagonEntity entity);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IRagonEvent.cs b/Ragon.Client/Sources/IRagonEvent.cs
new file mode 100644
index 0000000..76a3073
--- /dev/null
+++ b/Ragon.Client/Sources/IRagonEvent.cs
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ public interface IRagonEvent: IRagonSerializable
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IRagonPayload.cs b/Ragon.Client/Sources/IRagonPayload.cs
new file mode 100644
index 0000000..f68fbe0
--- /dev/null
+++ b/Ragon.Client/Sources/IRagonPayload.cs
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ public interface IRagonPayload: IRagonSerializable
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/IRagonSceneCollector.cs b/Ragon.Client/Sources/IRagonSceneCollector.cs
new file mode 100644
index 0000000..f2922e6
--- /dev/null
+++ b/Ragon.Client/Sources/IRagonSceneCollector.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonSceneCollector
+{
+ public RagonEntity[] Collect();
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs b/Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs
new file mode 100644
index 0000000..356bb11
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonAuthorizationListener.cs
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Client;
+
+public interface IRagonAuthorizationListener
+{
+ void OnAuthorizationSuccess(RagonClient client, string playerId, string playerName);
+ void OnAuthorizationFailed(RagonClient client, string message);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonConnectionListener.cs b/Ragon.Client/Sources/Listener/IRagonConnectionListener.cs
new file mode 100644
index 0000000..5cec3e0
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonConnectionListener.cs
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonConnectedListener
+{
+ void OnConnected(RagonClient client);
+ void OnDisconnected(RagonClient client);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonFailedListener.cs b/Ragon.Client/Sources/Listener/IRagonFailedListener.cs
new file mode 100644
index 0000000..332d8e8
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonFailedListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonFailedListener
+{
+ void OnFailed(RagonClient client, string message);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonJoinListener.cs b/Ragon.Client/Sources/Listener/IRagonJoinListener.cs
new file mode 100644
index 0000000..c45e337
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonJoinListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonJoinListener
+{
+ void OnJoined(RagonClient client);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonLeftListener.cs b/Ragon.Client/Sources/Listener/IRagonLeftListener.cs
new file mode 100644
index 0000000..44d5205
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonLeftListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonLeftListener
+{
+ void OnLeft(RagonClient client);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonLevelListener.cs b/Ragon.Client/Sources/Listener/IRagonLevelListener.cs
new file mode 100644
index 0000000..f527caa
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonLevelListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonLevelListener
+{
+ void OnLevel(RagonClient client, string sceneName);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonListener.cs b/Ragon.Client/Sources/Listener/IRagonListener.cs
new file mode 100644
index 0000000..933804e
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonListener.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ public interface IRagonListener :
+ IRagonAuthorizationListener,
+ IRagonConnectedListener,
+ IRagonFailedListener,
+ IRagonJoinListener,
+ IRagonLeftListener,
+ IRagonLevelListener,
+ IRagonOwnershipChangedListener,
+ IRagonPlayerJoinListener,
+ IRagonPlayerLeftListener
+ {
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs b/Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs
new file mode 100644
index 0000000..e7052e9
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonOwnershipChangedListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonOwnershipChangedListener
+{
+ void OnOwnershipChanged(RagonClient client, RagonPlayer player);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs b/Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs
new file mode 100644
index 0000000..6929f1b
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonPlayerJoinListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonPlayerJoinListener
+{
+ void OnPlayerJoined(RagonClient client, RagonPlayer player);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs b/Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs
new file mode 100644
index 0000000..2d28fbe
--- /dev/null
+++ b/Ragon.Client/Sources/Listener/IRagonPlayerLeftListener.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public interface IRagonPlayerLeftListener
+{
+ void OnPlayerLeft(RagonClient client, RagonPlayer player);
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Log/IRagonLogger.cs b/Ragon.Client/Sources/Log/IRagonLogger.cs
new file mode 100644
index 0000000..d1edcb6
--- /dev/null
+++ b/Ragon.Client/Sources/Log/IRagonLogger.cs
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ public interface IRagonLogger
+ {
+ public void Warn(string message);
+ public void Trace(string message);
+ public void Info(string message);
+ public void Error(string message);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs b/Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs
new file mode 100644
index 0000000..3632003
--- /dev/null
+++ b/Ragon.Client/Sources/Log/Loggers/RagonConsoleLogger.cs
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ public class RagonConsoleLogger: IRagonLogger
+ {
+ public void Warn(string message)
+ {
+ Console.WriteLine($"[WARN] {message}");
+ }
+
+ public void Trace(string message)
+ {
+ Console.WriteLine($"[TRACE] {message}");
+ }
+
+ public void Info(string message)
+ {
+ Console.WriteLine($"[INFO] {message}");
+ }
+
+ public void Error(string message)
+ {
+ Console.WriteLine($"[ERROR] {message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Log/RagonLog.cs b/Ragon.Client/Sources/Log/RagonLog.cs
new file mode 100644
index 0000000..803f0e8
--- /dev/null
+++ b/Ragon.Client/Sources/Log/RagonLog.cs
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ public class RagonLog
+ {
+ private static IRagonLogger _ragonLogger;
+ static RagonLog() => _ragonLogger = new RagonConsoleLogger();
+ public static void Set(IRagonLogger logger) => _ragonLogger = logger;
+ public static void Warn(string message) => _ragonLogger.Warn(message);
+ public static void Trace(string message) => _ragonLogger.Trace(message);
+ public static void Info(string message) => _ragonLogger.Info(message);
+ public static void Error(string message) => _ragonLogger.Error(message);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/Log/RagonLogLevel.cs b/Ragon.Client/Sources/Log/RagonLogLevel.cs
new file mode 100644
index 0000000..a808c40
--- /dev/null
+++ b/Ragon.Client/Sources/Log/RagonLogLevel.cs
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public enum RagonLogLevel
+{
+ ALL,
+ CONNECTION,
+ STATE,
+ EVENT,
+ ROOM,
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonClient.cs b/Ragon.Client/Sources/RagonClient.cs
new file mode 100644
index 0000000..642dc69
--- /dev/null
+++ b/Ragon.Client/Sources/RagonClient.cs
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ public sealed class RagonClient
+ {
+ private readonly INetworkConnection _connection;
+ private readonly IRagonEntityListener _entityListener;
+ private readonly IRagonSceneCollector _sceneCollector;
+ private Handler[] _processors;
+ private RagonBuffer _readBuffer;
+ private RagonBuffer _writeBuffer;
+ private RagonRoom _room;
+ private RagonSession _session;
+ private RagonListenerList _listenerList;
+ private RagonPlayerCache _playerCache;
+ private RagonEntityCache _entityCache;
+ private RagonEventCache _eventCache;
+ private RagonStatus _status;
+ private NetworkStatistics _stats;
+
+ private float _replicatationRate = 0;
+ private float _replicatationTime = 0;
+
+ public IRagonConnection Connection => _connection;
+ public RagonStatus Status => _status;
+ public RagonSession Session => _session;
+ public RagonEventCache Event => _eventCache;
+ public RagonEntityCache Entity => _entityCache;
+ public NetworkStatistics Statistics => _stats;
+ public RagonRoom Room => _room;
+
+ internal RagonBuffer Buffer => _writeBuffer;
+ internal INetworkChannel Reliable => _connection.Reliable;
+ internal INetworkChannel Unreliable => _connection.Unreliable;
+
+ #region PUBLIC
+
+ public RagonClient(
+ INetworkConnection connection,
+ IRagonEntityListener entityListener,
+ IRagonSceneCollector sceneCollector,
+ int rate)
+ {
+ _listenerList = new RagonListenerList(this);
+ _entityListener = entityListener;
+ _sceneCollector = sceneCollector;
+
+ _connection = connection;
+ _connection.OnData += OnData;
+ _connection.OnConnected += OnConnected;
+ _connection.OnDisconnected += OnDisconnected;
+
+ _replicatationRate = (1000.0f / rate) / 1000.0f;
+ _replicatationTime = 0;
+
+ _eventCache = new RagonEventCache();
+ _stats = new NetworkStatistics();
+ _status = RagonStatus.DISCONNECTED;
+ }
+
+ public void AddListener(IRagonListener listener)
+ {
+ _listenerList.Add(listener);
+ }
+
+ public void RemoveListener(IRagonListener listener)
+ {
+ _listenerList.Remove(listener);
+ }
+
+ public void Connect(string address, ushort port, string protocol)
+ {
+ _writeBuffer = new RagonBuffer();
+ _readBuffer = new RagonBuffer();
+ _session = new RagonSession(this, _readBuffer);
+
+ _playerCache = new RagonPlayerCache();
+ _entityCache = new RagonEntityCache(this, _playerCache, _entityListener, _sceneCollector);
+
+ _processors = new Handler[byte.MaxValue];
+ _processors[(byte)RagonOperation.AUTHORIZED_SUCCESS] = new AuthorizeSuccessHandler(_listenerList);
+ _processors[(byte)RagonOperation.AUTHORIZED_FAILED] = new AuthorizeFailedHandler(_listenerList);
+ _processors[(byte)RagonOperation.JOIN_SUCCESS] =
+ new JoinSuccessHandler(this, _readBuffer, _listenerList, _playerCache, _entityCache);
+ _processors[(byte)RagonOperation.JOIN_FAILED] = new JoinFailedHandler(_listenerList);
+ _processors[(byte)RagonOperation.LEAVE_ROOM] = new LeaveRoomHandler(this, _listenerList, _entityCache);
+ _processors[(byte)RagonOperation.OWNERSHIP_CHANGED] =
+ new OwnershipHandler(_listenerList, _playerCache, _entityCache);
+ _processors[(byte)RagonOperation.PLAYER_JOINED] = new PlayerJoinHandler(_playerCache, _listenerList);
+ _processors[(byte)RagonOperation.PLAYER_LEAVED] =
+ new PlayerLeftHandler(_entityCache, _playerCache, _listenerList);
+ _processors[(byte)RagonOperation.LOAD_SCENE] = new SceneLoadHandler(this, _listenerList);
+ _processors[(byte)RagonOperation.CREATE_ENTITY] = new EntityCreateHandler(this, _playerCache, _entityCache);
+ _processors[(byte)RagonOperation.DESTROY_ENTITY] = new EntityDestroyHandler(_entityCache);
+ _processors[(byte)RagonOperation.REPLICATE_ENTITY_STATE] = new StateEntityHandler(_entityCache);
+ _processors[(byte)RagonOperation.REPLICATE_ENTITY_EVENT] =
+ new EntityEventHandler(this, _playerCache, _entityCache);
+ _processors[(byte)RagonOperation.SNAPSHOT] =
+ new SnapshotHandler(this, _listenerList, _entityCache, _playerCache);
+
+ var protocolRaw = RagonVersion.Parse(protocol);
+ _connection.Connect(address, port, protocolRaw);
+ }
+
+ public void Disconnect()
+ {
+ _status = RagonStatus.DISCONNECTED;
+ _room.Cleanup();
+ _connection.Disconnect();
+ OnDisconnected(DisconnectReason.MANUAL);
+ }
+
+ public void Update(float dt)
+ {
+ _replicatationTime += dt;
+ if (_replicatationTime >= _replicatationRate)
+ {
+ _entityCache.WriteState(_readBuffer);
+ _replicatationTime = 0;
+ }
+
+ _stats.Update(_connection.BytesSent, _connection.BytesReceived, _connection.Ping, dt);
+ _connection.Update();
+ }
+
+ public void Dispose()
+ {
+ _status = RagonStatus.DISCONNECTED;
+ _connection.Disconnect();
+ _connection.Dispose();
+ }
+
+ #endregion
+
+ #region INTERNAL
+
+ internal void AssignRoom(RagonRoom room)
+ {
+ _room = room;
+ _status = RagonStatus.ROOM;
+ }
+
+ #endregion
+
+ #region PRIVATE
+
+ private void OnConnected()
+ {
+ RagonLog.Trace("Connected");
+
+ _listenerList.OnConnected();
+ _status = RagonStatus.CONNECTED;
+ }
+
+ private void OnDisconnected(DisconnectReason reason)
+ {
+ RagonLog.Trace($"Disconnected: {reason}");
+
+ _listenerList.OnDisconnected();
+ _status = RagonStatus.DISCONNECTED;
+ }
+
+ public void OnData(byte[] data)
+ {
+ _readBuffer.Clear();
+ _readBuffer.FromArray(data);
+
+ var operation = _readBuffer.ReadByte();
+ _processors[operation].Handle(_readBuffer);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonEntityCache.cs b/Ragon.Client/Sources/RagonEntityCache.cs
new file mode 100644
index 0000000..0c95774
--- /dev/null
+++ b/Ragon.Client/Sources/RagonEntityCache.cs
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public sealed class RagonEntityCache
+{
+ private readonly List _entityList = new();
+ private readonly Dictionary _entityMap = new();
+ private readonly Dictionary _pendingEntities = new();
+ private readonly Dictionary _sceneEntities = new();
+
+ private readonly RagonClient _client;
+ private readonly IRagonEntityListener _entityListener;
+ private readonly IRagonSceneCollector _sceneCollector;
+ private readonly RagonPlayerCache _playerCache;
+
+ private int _localEntitiesCounter = 0;
+
+ public RagonEntityCache(
+ RagonClient client,
+ RagonPlayerCache playerCache,
+ IRagonEntityListener listener,
+ IRagonSceneCollector sceneCollector
+ )
+ {
+ _client = client;
+ _entityListener = listener;
+ _sceneCollector = sceneCollector;
+ _playerCache = playerCache;
+ }
+
+ public RagonEntity FindById(ushort id)
+ {
+ return _entityMap[id];
+ }
+
+ public void Create(RagonEntity entity, IRagonPayload? spawnPayload)
+ {
+ var attachId = (ushort) (_playerCache.LocalPlayer.PeerId + _localEntitiesCounter++) ;
+ var buffer = _client.Buffer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.CREATE_ENTITY);
+ buffer.WriteUShort(attachId);
+ buffer.WriteUShort(entity.Type);
+ buffer.WriteByte((byte) entity.Authority);
+
+ entity.State.WriteInfo(buffer);
+
+ spawnPayload?.Serialize(buffer);
+
+ _pendingEntities.Add(attachId, entity);
+
+ var sendData = buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ public void Destroy(RagonEntity entity, IRagonPayload? destroyPayload)
+ {
+ if (!entity.IsAttached)
+ {
+ RagonLog.Warn("Can't destroy object, he is not created");
+ return;
+ }
+ var buffer = _client.Buffer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.DESTROY_ENTITY);
+ buffer.WriteUShort(entity.Id);
+
+ destroyPayload?.Serialize(buffer);
+
+ var sendData = buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ internal void WriteState(RagonBuffer buffer)
+ {
+ var changedEntities = 0u;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_STATE);
+
+ var offset = buffer.WriteOffset;
+ buffer.Write(0, 16);
+
+ foreach (var ent in _entityList)
+ {
+ if (!ent.IsAttached ||
+ !ent.Replication ||
+ !ent.PropertiesChanged) continue;
+
+ ent.Write(buffer);
+
+ changedEntities++;
+ }
+
+ if (changedEntities <= 0) return;
+
+ buffer.Write(changedEntities, 16, offset);
+
+ var data = buffer.ToArray();
+ _client.Unreliable.Send(data);
+ }
+
+ internal void WriteScene(RagonBuffer buffer)
+ {
+ _sceneEntities.Clear();
+
+ var entities = _sceneCollector.Collect();
+ buffer.WriteUShort((ushort) entities.Length);
+ foreach (var entity in entities)
+ {
+ buffer.WriteUShort(entity.Type);
+ buffer.WriteByte((byte) entity.Authority);
+ buffer.WriteUShort(entity.SceneId);
+
+ entity.State.WriteInfo(buffer);
+
+ _sceneEntities.Add(entity.SceneId, entity);
+ }
+ }
+
+ internal void CacheScene()
+ {
+ _sceneEntities.Clear();
+
+ var entities = _sceneCollector.Collect();
+ foreach (var entity in entities)
+ _sceneEntities.Add(entity.SceneId, entity);
+ }
+
+ internal void Cleanup()
+ {
+ var payload = new RagonPayload();
+ foreach (var ent in _entityList)
+ ent.Detach(payload);
+
+ _entityMap.Clear();
+ _entityList.Clear();
+ }
+
+ internal RagonEntity OnCreate(ushort attachId, ushort entityType, ushort sceneId, ushort entityId, bool hasAuthority)
+ {
+ if (sceneId > 0)
+ {
+ if (_sceneEntities.TryGetValue(sceneId, out var entity))
+ {
+ _entityMap.Add(entityId, entity);
+
+ if (hasAuthority)
+ _entityList.Add(entity);
+
+ return entity;
+ }
+ }
+
+ if (_pendingEntities.Remove(attachId, out var existsEntity))
+ {
+ _entityMap.Add(entityId, existsEntity);
+
+ if (hasAuthority)
+ _entityList.Add(existsEntity);
+
+ return existsEntity;
+ }
+ else
+ {
+ var entity = new RagonEntity(entityType, sceneId);
+
+ _entityMap.Add(entityId, entity);
+
+ if (hasAuthority)
+ _entityList.Add(entity);
+
+ _entityListener.OnEntityCreated(entity);
+
+ return entity;
+ }
+ }
+
+
+ internal void OnDestroy(ushort entityId, RagonPayload payload)
+ {
+ if (_entityMap.Remove(entityId, out var ragonEntity))
+ {
+ _entityList.Remove(ragonEntity);
+
+ ragonEntity.Detach(payload);
+ }
+ }
+
+ internal void OnState(ushort entityId, RagonBuffer buffer)
+ {
+ if (_entityMap.TryGetValue(entityId, out var entity))
+ entity.Read(buffer);
+ else
+ RagonLog.Warn($"Entity {entityId} not found!");
+ }
+
+ internal void OnEvent(RagonPlayer player, ushort entityId, ushort eventCode, RagonBuffer buffer)
+ {
+ if (_entityMap.TryGetValue(entityId, out var entity))
+ entity.Event(eventCode, player, buffer);
+ else
+ RagonLog.Warn($"Entity {entityId} not found!");
+ }
+
+ internal void OnOwnershipChanged(RagonPlayer player, ushort entityId)
+ {
+ if (_entityMap.TryGetValue(entityId, out var entity))
+ entity.OnOwnershipChanged(player);
+ else
+ RagonLog.Warn($"Entity {entityId} not found!");
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonEventCache.cs b/Ragon.Client/Sources/RagonEventCache.cs
new file mode 100644
index 0000000..b0b752e
--- /dev/null
+++ b/Ragon.Client/Sources/RagonEventCache.cs
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public class RagonEventCache
+{
+ private readonly Dictionary _eventsRegistryByType = new();
+ private readonly HashSet _codes = new();
+ private readonly HashSet _types = new();
+ private ushort _eventIdGenerator = 0;
+
+ public ushort GetEventCode(TEvent _) where TEvent : IRagonEvent
+ {
+ var type = typeof(TEvent);
+ var evntCode = _eventsRegistryByType[type];
+ return evntCode;
+ }
+
+ public void Register() where T : IRagonEvent, new()
+ {
+ var type = typeof(T);
+ if (_types.Contains(type))
+ {
+ RagonLog.Trace($"[Ragon] Event already registered: {type.Name}");
+ return;
+ }
+
+ RagonLog.Trace($"[Ragon] Registered Event: {type.Name} - {_eventIdGenerator}");
+
+ _eventsRegistryByType.Add(type, _eventIdGenerator);
+ _codes.Add(_eventIdGenerator);
+ _types.Add(type);
+
+ _eventIdGenerator++;
+ }
+
+ public void Register(ushort evntCode) where T : IRagonEvent, new()
+ {
+ var type = typeof(T);
+ if (_codes.Contains(evntCode) || _types.Contains(type))
+ {
+ RagonLog.Warn($"[Ragon] Event already registered: {type.Name} - {evntCode}");
+ return;
+ }
+
+ RagonLog.Trace($"[Ragon] Registered Event: {type.Name} - {evntCode}");
+
+ _codes.Add(evntCode);
+ _types.Add(type);
+ _eventsRegistryByType.Add(type, evntCode);
+ }
+
+ public T Create() where T : IRagonEvent, new()
+ {
+ return new T();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonListenerList.cs b/Ragon.Client/Sources/RagonListenerList.cs
new file mode 100644
index 0000000..cad7677
--- /dev/null
+++ b/Ragon.Client/Sources/RagonListenerList.cs
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ internal class RagonListenerList
+ {
+ public int Count => _listeners.Count;
+ private List _listeners = new();
+ private RagonClient _client;
+
+ public RagonListenerList(RagonClient client)
+ {
+ _client = client;
+ }
+
+ public void Add(IRagonListener listener)
+ {
+ _listeners.Add(listener);
+ }
+
+ public void Remove(IRagonListener listener)
+ {
+ _listeners.Remove(listener);
+ }
+
+ public void OnAuthorizationSuccess(string playerId, string playerName)
+ {
+ foreach (var listener in _listeners)
+ listener.OnAuthorizationSuccess(_client, playerId, playerName);
+ }
+
+ public void OnAuthorizationFailed(string message)
+ {
+ foreach (var listener in _listeners)
+ listener.OnAuthorizationFailed(_client, message);
+ }
+
+ public void OnLeft()
+ {
+ foreach (var listener in _listeners)
+ listener.OnLeft(_client);
+ }
+
+ public void OnFailed(string message)
+ {
+ foreach (var listener in _listeners)
+ listener.OnFailed(_client, message);
+ }
+
+ public void OnOwnershipChanged(RagonPlayer player)
+ {
+ foreach (var listener in _listeners)
+ listener.OnOwnershipChanged(_client, player);
+ }
+
+ public void OnPlayerLeft(RagonPlayer player)
+ {
+ foreach (var listener in _listeners)
+ listener.OnPlayerLeft(_client, player);
+ }
+
+ public void OnPlayerJoined(RagonPlayer player)
+ {
+ foreach (var listener in _listeners)
+ listener.OnPlayerJoined(_client, player);
+ }
+
+ public void OnLevel(string sceneName)
+ {
+ foreach (var listener in _listeners)
+ listener.OnLevel(_client, sceneName);
+ }
+
+ public void OnJoined()
+ {
+ foreach (var listener in _listeners)
+ listener.OnJoined(_client);
+ }
+
+ public void OnConnected()
+ {
+ foreach (var listener in _listeners)
+ listener.OnConnected(_client);
+ }
+
+ public void OnDisconnected()
+ {
+ foreach (var listener in _listeners)
+ listener.OnDisconnected(_client);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonPlayer.cs b/Ragon.Client/Sources/RagonPlayer.cs
new file mode 100644
index 0000000..e8b8f4a
--- /dev/null
+++ b/Ragon.Client/Sources/RagonPlayer.cs
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ [Serializable]
+ public class RagonPlayer
+ {
+ public string Id { get; private set; }
+ public string Name { get; set; }
+ public ushort PeerId { get; set; }
+ public bool IsRoomOwner { get; set; }
+ public bool IsMe { get; set; }
+
+ public RagonPlayer(ushort peerId, string playerId, string name, bool isRoomOwner, bool isMe)
+ {
+ PeerId = peerId;
+ IsRoomOwner = isRoomOwner;
+ IsMe = isMe;
+ Name = name;
+ Id = playerId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonPlayerCache.cs b/Ragon.Client/Sources/RagonPlayerCache.cs
new file mode 100644
index 0000000..c5d8f0a
--- /dev/null
+++ b/Ragon.Client/Sources/RagonPlayerCache.cs
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client;
+
+public sealed class RagonPlayerCache
+{
+ private List _players = new List();
+ private Dictionary _playersById = new();
+ private Dictionary _playersByConnection = new();
+
+ public RagonPlayer Owner { get; private set; }
+ public RagonPlayer LocalPlayer { get; private set; }
+ public bool IsRoomOwner => _ownerId == _localId;
+
+ public RagonPlayer? GetPlayerById(string playerId) => _playersById[playerId];
+ public RagonPlayer? GetPlayerByPeer(ushort peerId) => _playersByConnection[peerId];
+
+ private string _ownerId;
+ private string _localId;
+
+ public void SetOwnerAndLocal(string ownerId, string localId)
+ {
+ _ownerId = ownerId;
+ _localId = localId;
+ }
+
+ public void AddPlayer(ushort peerId, string playerId, string playerName)
+ {
+ if (_playersById.ContainsKey(playerId))
+ return;
+
+ var isOwner = playerId == _ownerId;
+ var isLocal = playerId == _localId;
+
+ RagonLog.Trace($"Added player {peerId}|{playerId}|{playerName} IsOwner: {isOwner} isLocal: {isLocal}");
+
+ var player = new RagonPlayer(peerId, playerId, playerName, isOwner, isLocal);
+
+ if (player.IsMe)
+ LocalPlayer = player;
+
+ if (player.IsRoomOwner)
+ Owner = player;
+
+ _players.Add(player);
+ _playersById.Add(player.Id, player);
+ _playersByConnection.Add(player.PeerId, player);
+ }
+
+ public void RemovePlayer(string playerId)
+ {
+ if (_playersById.Remove(playerId, out var player))
+ {
+ _players.Remove(player);
+ _playersByConnection.Remove(player.PeerId);
+ }
+ }
+
+ public void OnOwnershipChanged(string playerId)
+ {
+ foreach (var player in _players)
+ {
+ if (player.Id == playerId)
+ Owner = player;
+ player.IsRoomOwner = player.Id == playerId;
+ }
+ }
+
+
+ public void Cleanup()
+ {
+ _players.Clear();
+ _playersByConnection.Clear();
+ _playersById.Clear();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonRoom.cs b/Ragon.Client/Sources/RagonRoom.cs
new file mode 100644
index 0000000..b6bdaab
--- /dev/null
+++ b/Ragon.Client/Sources/RagonRoom.cs
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ public class RagonRoom
+ {
+ private RagonClient _client;
+ private RagonScene _scene;
+ private RagonEntityCache _entityCache;
+ private RagonPlayerCache _playerCache;
+ private RagonRoomInformation _information;
+
+ public string Id => _information.RoomId;
+ public int MinPlayers => _information.Min;
+ public int MaxPlayers => _information.Max;
+
+ public RagonPlayer Local => _playerCache.LocalPlayer;
+ public RagonPlayer Owner => _playerCache.Owner;
+
+ public RagonRoom(RagonClient client,
+ RagonEntityCache entityCache,
+ RagonPlayerCache playerCache,
+ RagonRoomInformation information,
+ RagonScene scene)
+ {
+ _client = client;
+ _information = information;
+ _entityCache = entityCache;
+ _playerCache = playerCache;
+ _scene = scene;
+ }
+
+ internal void Cleanup()
+ {
+ _entityCache.Cleanup();
+ _playerCache.Cleanup();
+ }
+
+ public void LoadScene(string map) => _scene.Load(map);
+ public void SceneLoaded() => _scene.SceneLoaded();
+
+ public void CreateEntity(RagonEntity entity) => CreateEntity(entity, null);
+ public void CreateEntity(RagonEntity entity, IRagonPayload? payload) => _entityCache.Create(entity, payload);
+
+ public void DestroyEntity(RagonEntity entityId) => DestroyEntity(entityId, null);
+ public void DestroyEntity(RagonEntity entityId, IRagonPayload? payload) => _entityCache.Destroy(entityId, payload);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonScene.cs b/Ragon.Client/Sources/RagonScene.cs
new file mode 100644
index 0000000..38ccab3
--- /dev/null
+++ b/Ragon.Client/Sources/RagonScene.cs
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client;
+
+public class RagonScene
+{
+ private readonly RagonClient _client;
+ private readonly RagonEntityCache _entityCache;
+ private readonly RagonPlayerCache _playerCache;
+
+ public RagonScene(RagonClient client, RagonPlayerCache playerCache, RagonEntityCache entityCache)
+ {
+ _client = client;
+ _playerCache = playerCache;
+ _entityCache = entityCache;
+ }
+
+ internal void Load(string map)
+ {
+ var buffer = _client.Buffer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.LOAD_SCENE);
+ buffer.WriteString(map);
+
+ var sendData = buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ internal void SceneLoaded()
+ {
+ var buffer = _client.Buffer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.SCENE_LOADED);
+
+ if (_playerCache.IsRoomOwner)
+ _entityCache.WriteScene(buffer);
+ else
+ _entityCache.CacheScene();
+
+ var sendData = buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonSession.cs b/Ragon.Client/Sources/RagonSession.cs
new file mode 100644
index 0000000..29914ec
--- /dev/null
+++ b/Ragon.Client/Sources/RagonSession.cs
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Client
+{
+ public class RagonSession
+ {
+ private readonly RagonClient _client;
+ private readonly RagonBuffer _buffer;
+
+ public RagonSession(RagonClient client, RagonBuffer buffer)
+ {
+ _client = client;
+ _buffer = buffer;
+ }
+
+ public void CreateOrJoin(string map, int minPlayers, int maxPlayers)
+ {
+ var parameters = new RagonRoomParameters() {Map = map, Min = minPlayers, Max = maxPlayers};
+ CreateOrJoin(parameters);
+ }
+
+ public void CreateOrJoin(RagonRoomParameters parameters)
+ {
+ _buffer.Clear();
+ _buffer.WriteOperation(RagonOperation.JOIN_OR_CREATE_ROOM);
+
+ parameters.Serialize(_buffer);
+
+ var sendData = _buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ public void Create(string map, int minPlayers, int maxPlayers)
+ {
+ Create(null, new RagonRoomParameters() {Map = map, Min = minPlayers, Max = maxPlayers});
+ }
+
+ public void Create(string roomId, string map, int minPlayers, int maxPlayers)
+ {
+ Create(roomId, new RagonRoomParameters() {Map = map, Min = minPlayers, Max = maxPlayers});
+ }
+
+ public void Create(string roomId, RagonRoomParameters parameters)
+ {
+ _buffer.Clear();
+ _buffer.WriteOperation(RagonOperation.CREATE_ROOM);
+
+ if (roomId != null)
+ {
+ _buffer.WriteBool(true);
+ _buffer.WriteString(roomId);
+ }
+ else
+ {
+ _buffer.WriteBool(false);
+ }
+
+ parameters.Serialize(_buffer);
+
+ var sendData = _buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ public void Leave()
+ {
+ var sendData = new[] {(byte) RagonOperation.LEAVE_ROOM};
+ _client.Reliable.Send(sendData);
+ }
+
+ public void Join(string roomId)
+ {
+ _buffer.Clear();
+ _buffer.WriteOperation(RagonOperation.JOIN_ROOM);
+ _buffer.WriteString(roomId);
+
+ var sendData = _buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ public void AuthorizeWithKey(string key, string playerName, byte[] additonalData)
+ {
+ _buffer.Clear();
+ _buffer.WriteOperation(RagonOperation.AUTHORIZE);
+ _buffer.WriteString(key);
+ _buffer.WriteString(playerName);
+ _buffer.WriteBytes(additonalData);
+
+ var sendData = _buffer.ToArray();
+ _client.Reliable.Send(sendData);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Client/Sources/RagonStatus.cs b/Ragon.Client/Sources/RagonStatus.cs
new file mode 100644
index 0000000..c64d029
--- /dev/null
+++ b/Ragon.Client/Sources/RagonStatus.cs
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Client
+{
+ public enum RagonStatus
+ {
+ DISCONNECTED,
+ CONNECTED,
+ ROOM,
+ LOBBY,
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Core/Action/IAction.cs b/Ragon.Core/Action/IAction.cs
deleted file mode 100644
index a8b0017..0000000
--- a/Ragon.Core/Action/IAction.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Ragon.Core.Time;
-
-public interface IAction
-{
- public void Tick();
-}
\ No newline at end of file
diff --git a/Ragon.Core/Action/Loop.cs b/Ragon.Core/Action/Loop.cs
deleted file mode 100644
index 88155a1..0000000
--- a/Ragon.Core/Action/Loop.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace Ragon.Core.Time;
-
-public class Loop
-{
- private List _tasks;
-
- public Loop()
- {
-
- _tasks = new List(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();
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Application.cs b/Ragon.Core/Application.cs
deleted file mode 100644
index 074a653..0000000
--- a/Ragon.Core/Application.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-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 _contexts;
-
- public Application(Configuration configuration)
- {
- _configuration = configuration;
- _executor = new Executor();
- _dedicatedThread = new Thread(Execute);
- _dedicatedThread.IsBackground = true;
- _contexts = new Dictionary();
- _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);
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/Entity.cs b/Ragon.Core/Game/Entity.cs
deleted file mode 100644
index 0bbc205..0000000
--- a/Ragon.Core/Game/Entity.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-using Ragon.Common;
-
-namespace Ragon.Core.Game;
-
-public class Entity
-{
- private static ushort _idGenerator = 0;
- public ushort Id { get; private set; }
- public ushort Type { get; private set; }
- public ushort StaticId { get; private set; }
- public RoomPlayer Owner { get; private set; }
- public RagonAuthority Authority { get; private set; }
- public EntityState State { get; private set; }
- public byte[] Payload { get; private set; }
-
- private readonly List _bufferedEvents;
-
- public Entity(RoomPlayer owner, ushort type, ushort staticId, RagonAuthority eventAuthority)
- {
- Owner = owner;
- StaticId = staticId;
- Type = type;
- Id = _idGenerator++;
- Payload = Array.Empty();
- Authority = eventAuthority;
- State = new EntityState(this);
-
- _bufferedEvents = new List();
- }
-
- 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 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 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 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 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;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/EntityEvent.cs b/Ragon.Core/Game/EntityEvent.cs
deleted file mode 100644
index 7d50d5e..0000000
--- a/Ragon.Core/Game/EntityEvent.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-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;
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/EntityList.cs b/Ragon.Core/Game/EntityList.cs
deleted file mode 100644
index c675684..0000000
--- a/Ragon.Core/Game/EntityList.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-namespace Ragon.Core.Game;
-
-public class EntityList
-{
- private readonly List _dynamicEntitiesList = new List();
- private readonly List _staticEntitiesList = new List();
- private readonly Dictionary _entitiesMap = new Dictionary();
-
- public IReadOnlyList StaticList => _staticEntitiesList;
- public IReadOnlyList DynamicList => _dynamicEntitiesList;
- public IReadOnlyDictionary 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;
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/EntityState.cs b/Ragon.Core/Game/EntityState.cs
deleted file mode 100644
index 6056663..0000000
--- a/Ragon.Core/Game/EntityState.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using NLog;
-using Ragon.Common;
-
-namespace Ragon.Core.Game;
-
-public class EntityState
-{
- private List _properties;
- private Entity _entity;
-
- public EntityState(Entity entity, int capacity = 10)
- {
- _entity = entity;
- _properties = new List(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 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);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/EntityStateProperty.cs b/Ragon.Core/Game/EntityStateProperty.cs
deleted file mode 100644
index 2f98956..0000000
--- a/Ragon.Core/Game/EntityStateProperty.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-using Ragon.Common;
-
-namespace Ragon.Core.Game;
-
-public class EntityStateProperty
-{
- public int Size { get; set; }
- public int Capacity { get; set; }
- public bool IsDirty { get; private set; }
- public bool IsFixed { get; private set; }
- private byte[] _data;
-
- public EntityStateProperty(int size, bool isFixed)
- {
- Capacity = 512;
- Size = size;
- IsFixed = isFixed;
- IsDirty = true;
-
- _data = new byte[Capacity];
- }
-
- public ReadOnlySpan Read()
- {
- var dataSpan = _data.AsSpan();
- var src = dataSpan.Slice(0, Size);
- return src;
- }
-
- public void Write(ref ReadOnlySpan src)
- {
- src.CopyTo(_data);
- IsDirty = true;
- }
-
- public void Clear()
- {
- IsDirty = false;
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/RoomInformation.cs b/Ragon.Core/Game/RoomInformation.cs
deleted file mode 100644
index f0acc0e..0000000
--- a/Ragon.Core/Game/RoomInformation.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-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}";
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Game/RoomPlayer.cs b/Ragon.Core/Game/RoomPlayer.cs
deleted file mode 100644
index c6224b0..0000000
--- a/Ragon.Core/Game/RoomPlayer.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-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;
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/HandlerRegistry.cs b/Ragon.Core/HandlerRegistry.cs
deleted file mode 100644
index b6acf25..0000000
--- a/Ragon.Core/HandlerRegistry.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-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;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/Abstract/IHandler.cs b/Ragon.Core/Handlers/Abstract/IHandler.cs
deleted file mode 100644
index 1962506..0000000
--- a/Ragon.Core/Handlers/Abstract/IHandler.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Ragon.Common;
-
-namespace Ragon.Core;
-
-public interface IHandler
-{
- public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer);
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/AuthorizationHandler.cs b/Ragon.Core/Handlers/AuthorizationHandler.cs
deleted file mode 100644
index 05613e2..0000000
--- a/Ragon.Core/Handlers/AuthorizationHandler.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-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");
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/EntityCreateHandler.cs b/Ragon.Core/Handlers/EntityCreateHandler.cs
deleted file mode 100644
index 9d8a192..0000000
--- a/Ragon.Core/Handlers/EntityCreateHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-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}");
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/EntityDestroyHandler.cs b/Ragon.Core/Handlers/EntityDestroyHandler.cs
deleted file mode 100644
index 6a5df1a..0000000
--- a/Ragon.Core/Handlers/EntityDestroyHandler.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-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());
- _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} destoyed entity {entity.Id}");
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/EntityEventHandler.cs b/Ragon.Core/Handlers/EntityEventHandler.cs
deleted file mode 100644
index be1b3b4..0000000
--- a/Ragon.Core/Handlers/EntityEventHandler.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-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 payloadRaw = stackalloc byte[payloadData.Length];
- ReadOnlySpan payload = payloadRaw;
- payloadData.CopyTo(payloadRaw);
-
- _logger.Trace($"Event {eventId} Payload: {payloadData.Length} to {targetMode}");
- ent.ReplicateEvent(player, eventId, payload, eventMode, targetPlayer);
- }
- else
- {
- Span payloadRaw = stackalloc byte[payloadData.Length];
- ReadOnlySpan payload = payloadRaw;
- payloadData.CopyTo(payloadRaw);
-
- _logger.Trace($"Event {eventId} Payload: {payloadData.Length} to {targetMode}");
- ent.ReplicateEvent(player, eventId, payload, eventMode, targetMode);
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/EntityStateHandler.cs b/Ragon.Core/Handlers/EntityStateHandler.cs
deleted file mode 100644
index ad16e17..0000000
--- a/Ragon.Core/Handlers/EntityStateHandler.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-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");
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/RoomLeaveHandler.cs b/Ragon.Core/Handlers/RoomLeaveHandler.cs
deleted file mode 100644
index a541fbb..0000000
--- a/Ragon.Core/Handlers/RoomLeaveHandler.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-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}");
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Lobby/Abstract/ILobby.cs b/Ragon.Core/Lobby/Abstract/ILobby.cs
deleted file mode 100644
index 0cb07b9..0000000
--- a/Ragon.Core/Lobby/Abstract/ILobby.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-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);
-}
\ No newline at end of file
diff --git a/Ragon.Core/Lobby/LobbyInMemory.cs b/Ragon.Core/Lobby/LobbyInMemory.cs
deleted file mode 100644
index 1bdf521..0000000
--- a/Ragon.Core/Lobby/LobbyInMemory.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-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 _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}");
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Lobby/LobbyPlayer.cs b/Ragon.Core/Lobby/LobbyPlayer.cs
deleted file mode 100644
index 8307f88..0000000
--- a/Ragon.Core/Lobby/LobbyPlayer.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-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();
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/PlayerContext.cs b/Ragon.Core/PlayerContext.cs
deleted file mode 100644
index 14d0110..0000000
--- a/Ragon.Core/PlayerContext.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-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);
- }
-}
\ No newline at end of file
diff --git a/Ragon.Core/Ragon.Core.csproj b/Ragon.Core/Ragon.Core.csproj
deleted file mode 100644
index 7583214..0000000
--- a/Ragon.Core/Ragon.Core.csproj
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- net6.0
- enable
- enable
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Ragon.Protocol/Ragon.Protocol.csproj b/Ragon.Protocol/Ragon.Protocol.csproj
index 68be873..935da96 100644
--- a/Ragon.Protocol/Ragon.Protocol.csproj
+++ b/Ragon.Protocol/Ragon.Protocol.csproj
@@ -16,7 +16,8 @@
-
+ C:\Users\edmand46\RagonProjects\ragon-unity-sdk\Assets\Ragon-Unity-SDK\Runtime\Plugins
TRACE;NETSTACK_SPAN
+ none
diff --git a/Ragon.Protocol/Sources/RagonAuthority.cs b/Ragon.Protocol/Sources/RagonAuthority.cs
index 9d13129..1f18721 100644
--- a/Ragon.Protocol/Sources/RagonAuthority.cs
+++ b/Ragon.Protocol/Sources/RagonAuthority.cs
@@ -1,4 +1,21 @@
-namespace Ragon.Common
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Protocol
{
public enum RagonAuthority: byte
{
diff --git a/Ragon.Protocol/Sources/RagonBuffer.cs b/Ragon.Protocol/Sources/RagonBuffer.cs
new file mode 100644
index 0000000..24e04b8
--- /dev/null
+++ b/Ragon.Protocol/Sources/RagonBuffer.cs
@@ -0,0 +1,423 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (c) 2018 Stanislav Denisov, Maxim Munnig
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/*
+ * Copyright (c) 2018 Alexander Shoulson
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ * claim that you wrote the original software. If you use this software
+ * in a product, an acknowledgment in the product documentation would be
+ * appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ * misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ */
+
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Ragon.Protocol
+{
+ public class RagonBuffer
+ {
+ private int _read;
+ private int _write;
+ private uint[] _buckets;
+ private readonly UTF8Encoding _utf8Encoding = new UTF8Encoding(false, true);
+
+ public int ReadOffset => _read;
+ public int WriteOffset => _write;
+ public int Length => ((_write - 1) >> 3) + 1;
+ public int Capacity => _write - _read;
+
+ public RagonBuffer(int capacity = 128)
+ {
+ _buckets = new uint[capacity];
+ _read = 0;
+ _write = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteBool(bool value)
+ {
+ Write(value ? 1u : 0u, 1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool ReadBool()
+ {
+ return Read(1) == 1u;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteByte(byte value)
+ {
+ Write(value, 8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public byte ReadByte()
+ {
+ return (byte)Read(8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteOperation(RagonOperation operation)
+ {
+ Write((byte)operation, 8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public RagonOperation ReadOperation()
+ {
+ return (RagonOperation)Read(8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteFloat(float value, float min, float max, float precision)
+ {
+ var requiredBits = DeBruijn.Log2((uint)((max - min) * (1.0f / precision) + 0.5f)) + 1;
+ var mask = (uint)((1L << requiredBits) - 1);
+ var compressedValue = (uint)((value - min) * (1f / precision) + 0.5f) & mask;
+
+ Write(compressedValue, requiredBits);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public float ReadFloat(float min, float max, float precision)
+ {
+ var requiredBits = DeBruijn.Log2((uint)((max - min) * (1.0f / precision) + 0.5f)) + 1;
+ var compressedValue = Read(requiredBits);
+
+ float adjusted = compressedValue * precision + min;
+
+ if (adjusted < min)
+ adjusted = min;
+ else if (adjusted > max)
+ adjusted = max;
+
+ return adjusted;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteInt(int value, int min, int max)
+ {
+ var maxValue = Math.Max(Math.Abs(min), Math.Abs(max));
+ var requiredBits = Bits.Compute(maxValue);
+ uint compressedValue = (uint)((value << 1) ^ (value >> 31));
+
+ Write(compressedValue, requiredBits);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int ReadInt(int min, int max)
+ {
+ var maxValue = Math.Max(Math.Abs(min), Math.Abs(max));
+ var requiredBits = Bits.Compute(maxValue);
+ var compressedValue = Read(requiredBits);
+ var value = (int)((compressedValue >> 1) ^ (-(int)(compressedValue & 1)));
+
+ return value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteUShort(ushort value)
+ {
+ Write(value, 16);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ushort ReadUShort()
+ {
+ return (ushort)Read(16);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void WriteString(string str)
+ {
+ var data = _utf8Encoding.GetBytes(str);
+ var len = (uint)data.Length;
+ Write(len, 16);
+ WriteBytes(data);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public string ReadString()
+ {
+ var len = (int)Read(16);
+ var data = ReadBytes(len);
+ var str = _utf8Encoding.GetString(data);
+ return str;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public uint Read(int numBits, int offset)
+ {
+ var currentBucketIndex = offset >> 5;
+ var used = offset & 0x0000001F;
+
+ var chunkMask = ((1UL << numBits) - 1) << used;
+ var scratch = (ulong)_buckets[currentBucketIndex];
+
+ if (currentBucketIndex + 1 < _buckets.Length)
+ scratch |= (ulong)_buckets[currentBucketIndex + 1] << 32;
+
+ var result = (scratch & chunkMask) >> used;
+
+ return (uint)result;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(uint value, int numBits, int offset)
+ {
+ Debug.Assert(!(numBits < 0));
+ Debug.Assert(!(numBits > 32));
+
+ var index = offset >> 5;
+ var used = offset & 0x0000001F;
+
+ var valueMask = (1UL << numBits) - 1;
+ var prepared = (value & valueMask) << used;
+ var scratch = _buckets[index] | (ulong)_buckets[index + 1] << 32;
+ var result = scratch | prepared;
+
+ _buckets[index] = (uint)result;
+ _buckets[index + 1] = (uint)(result >> 32);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Write(uint value, int numBits = 16)
+ {
+ Debug.Assert(!(numBits < 0));
+ Debug.Assert(!(numBits > 32));
+
+ var currentBucketIndex = _write >> 5;
+ var used = _write & 0x0000001F;
+ var mask = (1UL << used) - 1;
+ var scratch = _buckets[currentBucketIndex] & mask;
+ var result = scratch | ((ulong)value << used);
+
+ if (currentBucketIndex + 1 >= _buckets.Length)
+ Resize(1);
+
+ _buckets[currentBucketIndex] = (uint)result;
+ _buckets[currentBucketIndex + 1] = (uint)(result >> 32);
+
+ _write += numBits;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public uint Read(int numBits = 16)
+ {
+ var currentBucketIndex = _read >> 5;
+ var used = _read & 0x0000001F;
+
+ var chunkMask = ((1UL << numBits) - 1) << used;
+ var scratch = (ulong)_buckets[currentBucketIndex];
+
+ if (currentBucketIndex + 1 < _buckets.Length)
+ scratch |= (ulong)_buckets[currentBucketIndex + 1] << 32;
+
+ var result = (scratch & chunkMask) >> used;
+
+ _read += numBits;
+
+ return (uint)result;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Obsolete("Do not use this method, will be removed")]
+ public void WriteBytes(byte[] data)
+ {
+ var len = data.Length;
+ for (int i = 0; i < len; i++)
+ Write(data[i], 8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [Obsolete("Do not use this method, will be removed")]
+ public byte[] ReadBytes(int lenght)
+ {
+ var data = new byte[lenght];
+ for (int i = 0; i < lenght; i++)
+ data[i] = (byte)Read(8);
+
+ return data;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void ReadSpan(ref Span data, int size)
+ {
+ var used = _read & 0x0000001F;
+ var index = _read >> 5;
+ var limit = (size + 32 - 1) / 32;
+ var capacity = size;
+
+ for (int i = 0; i < limit; i++)
+ {
+ var dataSize = capacity > 32 ? 32 : capacity;
+ var mask = (1UL << dataSize) - 1;
+ var bucketRaw = (ulong)_buckets[index];
+ if (index + 1 < _buckets.Length)
+ bucketRaw |= (ulong)_buckets[index + 1] << 32;
+
+ var bucket = bucketRaw >> used;
+ var result = bucket & mask;
+
+ data[i] = (uint)result;
+ if (i + 1 < data.Length)
+ data[i + 1] = (uint)(result >> 32);
+
+ index += 1;
+ capacity -= dataSize;
+ }
+
+ _read += size;
+ }
+
+ public void WriteSpan(ref ReadOnlySpan data, int size)
+ {
+ var used = _write & 0x0000001F;
+ var index = _write >> 5;
+ var limit = (size + 32 - 1) / 32;
+
+ if (index + limit >= _buckets.Length)
+ Resize(size);
+
+ for (var i = 0; i < limit; i += 1)
+ {
+ var prepared = (ulong) data[i] << used;
+ var mask = (1UL << used) - 1;
+ var scratch = _buckets[index] & mask;
+ var result = scratch | prepared;
+
+ _buckets[index] = (uint)result;
+ _buckets[index + 1] = (uint)(result >> 32);
+
+ index += 1;
+ }
+
+ _write += size;
+ }
+
+ public void Clear()
+ {
+ _read = 0;
+ _write = 0;
+ }
+
+ public void FromArray(byte[] data)
+ {
+ var length = data.Length;
+ var bucketsCount = length / 4 + 1;
+
+ if (_buckets.Length < bucketsCount)
+ _buckets = new uint[bucketsCount];
+
+ for (var i = 0; i < bucketsCount; i++)
+ {
+ var dataIdx = i * 4;
+ var bucket = 0u;
+
+ if (dataIdx < length)
+ bucket = data[dataIdx];
+
+ if (dataIdx + 1 < length)
+ bucket |= (uint)data[dataIdx + 1] << 8;
+
+ if (dataIdx + 2 < length)
+ bucket |= (uint)data[dataIdx + 2] << 16;
+
+ if (dataIdx + 3 < length)
+ bucket |= (uint)data[dataIdx + 3] << 24;
+
+ _buckets[i] = bucket;
+ }
+
+ int positionInByte = Bits.FindBitPosition(data[length - 1]);
+
+ _write = ((length - 1) * 8) + positionInByte;
+ _read = 0;
+ }
+
+ public byte[] ToArray()
+ {
+ var data = new byte[Length];
+ int bucketsCount = (_write >> 5) + 1;
+ int length = data.Length;
+
+ for (int i = 0; i < bucketsCount; i++)
+ {
+ int dataIdx = i * 4;
+ uint bucket = _buckets[i];
+
+ if (dataIdx < length)
+ data[dataIdx] = (byte)(bucket);
+
+ if (dataIdx + 1 < length)
+ data[dataIdx + 1] = (byte)(bucket >> 8);
+
+ if (dataIdx + 2 < length)
+ data[dataIdx + 2] = (byte)(bucket >> 16);
+
+ if (dataIdx + 3 < length)
+ data[dataIdx + 3] = (byte)(bucket >> 24);
+ }
+
+ return data;
+ }
+
+ private void Resize(int capacity)
+ {
+ var buckets = new uint[_buckets.Length * 2 + capacity];
+ Array.Copy(_buckets, buckets, _buckets.Length);
+ _buckets = buckets;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Protocol/Sources/RagonOperation.cs b/Ragon.Protocol/Sources/RagonOperation.cs
index 5273e44..d4f110f 100644
--- a/Ragon.Protocol/Sources/RagonOperation.cs
+++ b/Ragon.Protocol/Sources/RagonOperation.cs
@@ -1,4 +1,21 @@
-namespace Ragon.Common
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Protocol
{
public enum RagonOperation: byte
{
diff --git a/Ragon.Protocol/Sources/RagonReplicationMode.cs b/Ragon.Protocol/Sources/RagonReplicationMode.cs
index fe1ba72..9c1a40d 100644
--- a/Ragon.Protocol/Sources/RagonReplicationMode.cs
+++ b/Ragon.Protocol/Sources/RagonReplicationMode.cs
@@ -1,4 +1,21 @@
-namespace Ragon.Common
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Protocol
{
public enum RagonReplicationMode: byte
{
diff --git a/Ragon.Protocol/Sources/RagonRoomParameters.cs b/Ragon.Protocol/Sources/RagonRoomParameters.cs
index ecd2263..f219809 100644
--- a/Ragon.Protocol/Sources/RagonRoomParameters.cs
+++ b/Ragon.Protocol/Sources/RagonRoomParameters.cs
@@ -1,5 +1,21 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
-namespace Ragon.Common
+
+namespace Ragon.Protocol
{
public class RagonRoomParameters: IRagonSerializable
{
@@ -7,18 +23,18 @@ namespace Ragon.Common
public int Min { get; set; }
public int Max { get; set; }
- public void Serialize(RagonSerializer buffer)
+ public void Serialize(RagonBuffer buffer)
{
buffer.WriteString(Map);
- buffer.WriteInt(Min);
- buffer.WriteInt(Max);
+ buffer.WriteInt(Min, 1, 32);
+ buffer.WriteInt(Max, 1, 32);
}
- public void Deserialize(RagonSerializer buffer)
+ public void Deserialize(RagonBuffer buffer)
{
Map = buffer.ReadString();
- Min = buffer.ReadInt();
- Max = buffer.ReadInt();
+ Min = buffer.ReadInt(1, 32);
+ Max = buffer.ReadInt(1, 32);
}
}
}
\ No newline at end of file
diff --git a/Ragon.Protocol/Sources/RagonSerializable.cs b/Ragon.Protocol/Sources/RagonSerializable.cs
index 24cf283..10dfe10 100644
--- a/Ragon.Protocol/Sources/RagonSerializable.cs
+++ b/Ragon.Protocol/Sources/RagonSerializable.cs
@@ -1,8 +1,25 @@
-namespace Ragon.Common
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Protocol
{
public interface IRagonSerializable
{
- public void Serialize(RagonSerializer serializer);
- public void Deserialize(RagonSerializer serializer);
+ public void Serialize(RagonBuffer buffer);
+ public void Deserialize(RagonBuffer buffer);
}
}
\ No newline at end of file
diff --git a/Ragon.Protocol/Sources/RagonSerializer.cs b/Ragon.Protocol/Sources/RagonSerializer.cs
deleted file mode 100644
index b447d8f..0000000
--- a/Ragon.Protocol/Sources/RagonSerializer.cs
+++ /dev/null
@@ -1,335 +0,0 @@
-using System;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text;
-
-
-namespace Ragon.Common
-{
- [StructLayout(LayoutKind.Explicit)]
- internal struct ValueConverter
- {
- [FieldOffset(0)] public int Int;
- [FieldOffset(0)] public float Float;
- [FieldOffset(0)] public long Long;
- [FieldOffset(0)] public byte Byte0;
- [FieldOffset(1)] public byte Byte1;
- [FieldOffset(2)] public byte Byte2;
- [FieldOffset(3)] public byte Byte3;
- [FieldOffset(4)] public byte Byte4;
- [FieldOffset(5)] public byte Byte5;
- [FieldOffset(6)] public byte Byte6;
- [FieldOffset(7)] public byte Byte7;
- }
-
- 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 Reset()
- {
- _size = _offset;
- _offset = 0;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void AddOffset(int offset)
- {
- _offset += offset;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteByte(byte value)
- {
- ResizeIfNeed(1);
- _data[_offset] = value;
- _offset += 1;
- return 1;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public byte ReadByte()
- {
- var value = _data[_offset];
- _offset += 1;
- return value;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteBool(bool value)
- {
- ResizeIfNeed(1);
- _data[_offset] = value ? (byte) 1 : (byte) 0;
- _offset += 1;
- return 1;
- }
-
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool ReadBool()
- {
- var value = _data[_offset];
- _offset += 1;
- return value == 1;
- }
-
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteInt(int value)
- {
- ResizeIfNeed(4);
- var converter = new ValueConverter() {Int = value};
- _data[_offset] = converter.Byte0;
- _data[_offset + 1] = converter.Byte1;
- _data[_offset + 2] = converter.Byte2;
- _data[_offset + 3] = converter.Byte3;
- _offset += 4;
- return 4;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteInt(int value, int offset)
- {
- ResizeIfNeed(4);
- var converter = new ValueConverter() {Int = value};
- _data[offset] = converter.Byte0;
- _data[offset + 1] = converter.Byte1;
- _data[offset + 2] = converter.Byte2;
- _data[offset + 3] = converter.Byte3;
- return 4;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int ReadInt()
- {
- var converter = new ValueConverter {Byte0 = _data[_offset], Byte1 = _data[_offset + 1], Byte2 = _data[_offset + 2], Byte3 = _data[_offset + 3]};
- _offset += 4;
- return converter.Int;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void WriteLong(long value)
- {
- ResizeIfNeed(8);
- WriteLong(value, _offset);
- _offset += 8;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteLong(long value, int offset)
- {
- var converter = new ValueConverter() {Long = value};
- _data[offset] = converter.Byte0;
- _data[offset + 1] = converter.Byte1;
- _data[offset + 2] = converter.Byte2;
- _data[offset + 3] = converter.Byte3;
- _data[offset + 4] = converter.Byte4;
- _data[offset + 5] = converter.Byte5;
- _data[offset + 6] = converter.Byte6;
- _data[offset + 7] = converter.Byte7;
- return 8;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public long ReadLong()
- {
- var converter = new ValueConverter
- {
- Byte0 = _data[_offset],
- Byte1 = _data[_offset + 1],
- Byte2 = _data[_offset + 2],
- Byte3 = _data[_offset + 3],
- Byte4 = _data[_offset + 4],
- Byte5 = _data[_offset + 5],
- Byte6 = _data[_offset + 6],
- Byte7 = _data[_offset + 7],
- };
- _offset += 8;
- return converter.Long;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteFloat(float value)
- {
- var converter = new ValueConverter() {Float = value};
- WriteInt(converter.Int);
- return 4;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public float ReadFloat()
- {
- var rawValue = ReadInt();
- var converter = new ValueConverter() {Int = rawValue};
- var value = converter.Float;
- return value;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteString(string value)
- {
- var rawData = Encoding.UTF8.GetBytes(value).AsSpan();
- ResizeIfNeed(2 + rawData.Length);
- WriteUShort((ushort) rawData.Length);
- var data = _data.AsSpan().Slice(_offset, rawData.Length);
- rawData.CopyTo(data);
- _offset += rawData.Length;
-
- return rawData.Length + 2;
- }
-
- [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 ReadData(int lenght)
- {
- var data = _data.AsSpan();
- var payloadData = data.Slice(_offset, lenght);
-
- _offset += payloadData.Length;
- return payloadData;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteData(ref ReadOnlySpan payload)
- {
- ResizeIfNeed(payload.Length);
-
- var data = _data.AsSpan();
- var payloadData = data.Slice(_offset, payload.Length);
-
- payload.CopyTo(payloadData);
- _offset += payload.Length;
- return payload.Length;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Span GetWritableData(int lenght)
- {
- ResizeIfNeed(lenght);
-
- var data = _data.AsSpan();
- var payloadData = data.Slice(_offset, lenght);
-
- _offset += lenght;
- return payloadData;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteOperation(RagonOperation ragonOperation)
- {
- ResizeIfNeed(1);
-
- _data[_offset] = (byte) ragonOperation;
- _offset += 1;
-
- return 1;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public RagonOperation ReadOperation()
- {
- var op = (RagonOperation) _data[_offset];
- _offset += 1;
- return op;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteUShort(ushort value)
- {
- ResizeIfNeed(2);
-
- _data[_offset] = (byte) (value & 0x00FF);
- _data[_offset + 1] = (byte) ((value & 0xFF00) >> 8);
- _offset += 2;
-
- return 2;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int WriteUShort(ushort value, int offset)
- {
- ResizeIfNeed(2);
- _data[offset] = (byte) (value & 0x00FF);
- _data[offset + 1] = (byte) ((value & 0xFF00) >> 8);
- return 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 data)
- {
- var span = _data.AsSpan();
- var dataSpan = span.Slice(0, _offset);
- dataSpan.CopyTo(data);
- }
-
- public void FromSpan(ref ReadOnlySpan 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, data.Length);
- _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;
- }
- }
-}
\ No newline at end of file
diff --git a/Ragon.Protocol/Sources/RagonTarget.cs b/Ragon.Protocol/Sources/RagonTarget.cs
index 25f6678..93898ee 100644
--- a/Ragon.Protocol/Sources/RagonTarget.cs
+++ b/Ragon.Protocol/Sources/RagonTarget.cs
@@ -1,4 +1,21 @@
-namespace Ragon.Common
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Protocol
{
public enum RagonTarget: byte
{
diff --git a/Ragon.Protocol/Sources/RagonUtils.cs b/Ragon.Protocol/Sources/RagonUtils.cs
new file mode 100644
index 0000000..acc995a
--- /dev/null
+++ b/Ragon.Protocol/Sources/RagonUtils.cs
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using System.Runtime.CompilerServices;
+
+namespace Ragon.Protocol
+{
+ public static class DeBruijn
+ {
+ private static readonly int[] _lookup = new int[32]
+ {
+ 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
+ 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
+ };
+
+ public static int Log2(uint value)
+ {
+ value |= value >> 1;
+ value |= value >> 2;
+ value |= value >> 4;
+ value |= value >> 8;
+ value |= value >> 16;
+
+ return _lookup[(value * 0x07C4ACDDU) >> 27];
+ }
+ }
+
+ public static class Bits
+ {
+ static int[] _lookup = new int[256];
+
+ static Bits()
+ {
+ _lookup[0] = 0;
+ for (int i = 0; i < 256; i++)
+ _lookup[i] = (i & 1) + _lookup[i / 2];
+ }
+
+ [MethodImpl(256)]
+ public static int Compute(int value)
+ {
+ var count = 0;
+ do
+ {
+ value >>= 8;
+ count += 8;
+ } while (value > 0);
+
+ return count;
+ }
+
+ [MethodImpl(256)]
+ public static int FindBitPosition(byte data)
+ {
+ int shiftCount = 0;
+
+ while (data > 0)
+ {
+ data >>= 1;
+ shiftCount++;
+ }
+
+ return shiftCount;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Protocol/Sources/RagonVersion.cs b/Ragon.Protocol/Sources/RagonVersion.cs
index 563294e..f5e4bfb 100644
--- a/Ragon.Protocol/Sources/RagonVersion.cs
+++ b/Ragon.Protocol/Sources/RagonVersion.cs
@@ -1,4 +1,21 @@
-namespace Ragon.Common
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+namespace Ragon.Protocol
{
public static class RagonVersion
{
diff --git a/Ragon.Relay/Dockerfile b/Ragon.Relay/Dockerfile
new file mode 100644
index 0000000..4df0c77
--- /dev/null
+++ b/Ragon.Relay/Dockerfile
@@ -0,0 +1,16 @@
+FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
+
+WORKDIR /App
+
+COPY . ./
+
+RUN dotnet restore
+RUN dotnet publish -c Release -o out
+
+FROM mcr.microsoft.com/dotnet/runtime:7.0
+
+WORKDIR /App
+
+COPY --from=build-env /App/out .
+
+ENTRYPOINT ["Ragon.Relay"]
\ No newline at end of file
diff --git a/Ragon.Relay/NLog.config b/Ragon.Relay/NLog.config
old mode 100755
new mode 100644
diff --git a/Ragon.Relay/Program.cs b/Ragon.Relay/Program.cs
old mode 100755
new mode 100644
index 38c5874..39a26be
--- a/Ragon.Relay/Program.cs
+++ b/Ragon.Relay/Program.cs
@@ -1,7 +1,18 @@
-using System;
-using System.Runtime.InteropServices;
-using NLog;
-using Ragon.Core;
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
namespace Ragon.Relay
{
@@ -9,20 +20,8 @@ namespace Ragon.Relay
{
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");
+ var relay = new Relay();
relay.Start();
-
- Console.ReadKey();
-
- relay.Stop();
- logger.Info("Stopped");
}
}
}
\ No newline at end of file
diff --git a/Ragon.Relay/Ragon.Relay.csproj b/Ragon.Relay/Ragon.Relay.csproj
old mode 100755
new mode 100644
index 851f760..b492477
--- a/Ragon.Relay/Ragon.Relay.csproj
+++ b/Ragon.Relay/Ragon.Relay.csproj
@@ -2,8 +2,8 @@
Exe
- net6.0
- Game
+ Ragon.Relay
+ net7.0
@@ -17,10 +17,15 @@
Always
+
+ Always
+
-
+
+
+
diff --git a/Ragon.Relay/Relay.cs b/Ragon.Relay/Relay.cs
new file mode 100644
index 0000000..0192094
--- /dev/null
+++ b/Ragon.Relay/Relay.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Server;
+using Ragon.Server.ENet;
+using Ragon.Server.DotNetWebsockets;
+
+
+namespace Ragon.Relay;
+
+public class Relay
+{
+ public void Start()
+ {
+ var logger = LogManager.GetLogger("Ragon.Relay");
+ logger.Info("Relay Application");
+
+ var configuration = Configuration.Load("relay.config.json");
+ var serverType = Configuration.GetServerType(configuration.ServerType);
+
+ INetworkServer server = null;
+ switch (serverType)
+ {
+ case ServerType.ENET:
+ server = new ENetServer();
+ break;
+ case ServerType.WEBSOCKET:
+ server = new DotNetWebSocketServer();
+ break;
+ default:
+ server = new ENetServer();
+ break;
+ }
+
+ var relay = new RagonServer(server, configuration);
+ logger.Info("Started");
+ relay.Start();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Relay/relay.config.json b/Ragon.Relay/relay.config.json
index 434df7d..715fa3c 100644
--- a/Ragon.Relay/relay.config.json
+++ b/Ragon.Relay/relay.config.json
@@ -1,9 +1,9 @@
{
"serverKey": "defaultkey",
"serverType": "enet",
- "serverTickRate": 20,
+ "serverTickRate": 30,
"gameProtocol": "1.0.0",
- "port": 5000,
+ "port": 5001,
"limitConnections": 4095,
"limitPlayersPerRoom": 20,
"limitRooms": 200
diff --git a/Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj b/Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj
similarity index 87%
rename from Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj
rename to Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj
index 4680532..5feec38 100644
--- a/Ragon.Server.NativeWebSockets/Ragon.Server.NativeWebSockets.csproj
+++ b/Ragon.Server.DotNetWebSockets/Ragon.Server.DotNetWebSockets.csproj
@@ -8,11 +8,11 @@
-
+
-
+
diff --git a/Ragon.Server.NativeWebSockets/WebSocketConnection.cs b/Ragon.Server.DotNetWebSockets/WebSocketConnection.cs
similarity index 61%
rename from Ragon.Server.NativeWebSockets/WebSocketConnection.cs
rename to Ragon.Server.DotNetWebSockets/WebSocketConnection.cs
index c644447..ca8a748 100644
--- a/Ragon.Server.NativeWebSockets/WebSocketConnection.cs
+++ b/Ragon.Server.DotNetWebSockets/WebSocketConnection.cs
@@ -1,7 +1,23 @@
-using System.Net.WebSockets;
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Net.WebSockets;
using NLog;
-namespace Ragon.Server.NativeWebSockets;
+namespace Ragon.Server.DotNetWebsockets;
public sealed class WebSocketConnection : INetworkConnection
{
diff --git a/Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs b/Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs
new file mode 100644
index 0000000..79454df
--- /dev/null
+++ b/Ragon.Server.DotNetWebSockets/WebSocketReliableChannel.cs
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Net.WebSockets;
+using Ragon.Server;
+
+namespace Ragon.Server.DotNetWebsockets;
+
+public class WebSocketReliableChannel : INetworkChannel
+{
+ private Queue _queue;
+ private WebSocket _socket;
+
+ public WebSocketReliableChannel(WebSocket webSocket)
+ {
+ _socket = webSocket;
+ _queue = new Queue(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);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server.NativeWebSockets/WebSocketServer.cs b/Ragon.Server.DotNetWebSockets/WebSocketServer.cs
similarity index 68%
rename from Ragon.Server.NativeWebSockets/WebSocketServer.cs
rename to Ragon.Server.DotNetWebSockets/WebSocketServer.cs
index b6cb544..8e98712 100644
--- a/Ragon.Server.NativeWebSockets/WebSocketServer.cs
+++ b/Ragon.Server.DotNetWebSockets/WebSocketServer.cs
@@ -1,18 +1,30 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using System.Net;
using System.Net.WebSockets;
using NLog;
-using Ragon.Common;
-using Ragon.Core.Server;
-using Ragon.Server;
-using Ragon.Server.NativeWebSockets;
+using Ragon.Protocol;
-namespace Ragon.Core;
+namespace Ragon.Server.DotNetWebsockets;
-public class NativeWebSocketServer : INetworkServer
+public class DotNetWebSocketServer : INetworkServer
{
+ public Executor Executor => _executor;
+
private ILogger _logger = LogManager.GetCurrentClassLogger();
private INetworkListener _networkListener;
private Stack _sequencer;
@@ -21,21 +33,27 @@ public class NativeWebSocketServer : INetworkServer
private WebSocketConnection[] _connections;
private List _activeConnections;
private CancellationTokenSource _cancellationTokenSource;
-
- public NativeWebSocketServer(Executor executor)
+
+ public DotNetWebSocketServer()
{
_sequencer = new Stack();
_connections = Array.Empty();
_activeConnections = new List();
- _executor = executor;
+ _executor = new Executor();
}
-
+
public async void StartAccept(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var context = await _httpListener.GetContextAsync();
- if (!context.Request.IsWebSocketRequest) continue;
+ if (!context.Request.IsWebSocketRequest)
+ {
+ context.Response.StatusCode = 200;
+ context.Response.ContentLength64 = 0;
+ context.Response.Close();
+ continue;
+ }
var webSocketContext = await context.AcceptWebSocketAsync(null);
var webSocket = webSocketContext.WebSocket;
@@ -64,8 +82,8 @@ public class NativeWebSocketServer : INetworkServer
{
var result = await webSocket.ReceiveAsync(buffer, cancellationToken);
var dataRaw = buffer.Slice(0, result.Count);
-
- _networkListener.OnData(connection, dataRaw.ToArray());
+ if (dataRaw.Length > 0)
+ _networkListener.OnData(connection, dataRaw.ToArray());
}
catch (Exception ex)
{
@@ -78,7 +96,7 @@ public class NativeWebSocketServer : INetworkServer
_networkListener.OnDisconnected(connection);
}
- public void Poll()
+ public void Update()
{
Flush();
}
@@ -106,13 +124,13 @@ public class NativeWebSocketServer : INetworkServer
_connections = new WebSocketConnection[configuration.LimitConnections];
_httpListener = new HttpListener();
- _httpListener.Prefixes.Add($"http://127.0.0.1:{configuration.Port}/");
+ _httpListener.Prefixes.Add($"http://+:{configuration.Port}/");
_httpListener.Start();
_executor.Run(() => StartAccept(_cancellationTokenSource.Token));
var protocolDecoded = RagonVersion.Parse(configuration.Protocol);
- _logger.Info($"Listen at http://*:{configuration.Port}/");
+ _logger.Info($"Listen at http://0.0.0.0:{configuration.Port}/");
_logger.Info($"Protocol: {protocolDecoded}");
}
diff --git a/Ragon.Server.ENet/ENetConnection.cs b/Ragon.Server.ENet/ENetConnection.cs
deleted file mode 100644
index dde4943..0000000
--- a/Ragon.Server.ENet/ENetConnection.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-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);
- }
-}
\ No newline at end of file
diff --git a/Ragon.Server.ENet/ENetReliableChannel.cs b/Ragon.Server.ENet/ENetReliableChannel.cs
deleted file mode 100644
index 6cb6571..0000000
--- a/Ragon.Server.ENet/ENetReliableChannel.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-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);
- }
-}
\ No newline at end of file
diff --git a/Ragon.Server.ENet/ENetUnreliableChannel.cs b/Ragon.Server.ENet/ENetUnreliableChannel.cs
deleted file mode 100644
index a147bee..0000000
--- a/Ragon.Server.ENet/ENetUnreliableChannel.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-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);
- }
-}
\ No newline at end of file
diff --git a/Ragon.Server.ENet/Ragon.Server.ENet.csproj b/Ragon.Server.ENet/Ragon.Server.ENet.csproj
index fdbb1e7..91c6df9 100644
--- a/Ragon.Server.ENet/Ragon.Server.ENet.csproj
+++ b/Ragon.Server.ENet/Ragon.Server.ENet.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/Ragon.Server.ENet/Sources/ENetConnection.cs b/Ragon.Server.ENet/Sources/ENetConnection.cs
new file mode 100644
index 0000000..837ca78
--- /dev/null
+++ b/Ragon.Server.ENet/Sources/ENetConnection.cs
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server.ENet/Sources/ENetReliableChannel.cs b/Ragon.Server.ENet/Sources/ENetReliableChannel.cs
new file mode 100644
index 0000000..822ba3a
--- /dev/null
+++ b/Ragon.Server.ENet/Sources/ENetReliableChannel.cs
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server.ENet/ENetServer.cs b/Ragon.Server.ENet/Sources/ENetServer.cs
old mode 100755
new mode 100644
similarity index 76%
rename from Ragon.Server.ENet/ENetServer.cs
rename to Ragon.Server.ENet/Sources/ENetServer.cs
index c5d6763..853d934
--- a/Ragon.Server.ENet/ENetServer.cs
+++ b/Ragon.Server.ENet/Sources/ENetServer.cs
@@ -1,116 +1,138 @@
-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();
- }
-
- public void Start(INetworkListener listener, NetworkConfiguration configuration)
- {
- Library.Initialize();
-
- _connections = new ENetConnection[configuration.LimitConnections];
-
- _listener = listener;
- _protocol = configuration.Protocol;
-
- var address = new Address { Port = (ushort) configuration.Port };
- _host.Create(address, _connections.Length, 2, 0, 0, 1024 * 1024);
-
- var protocolDecoded = RagonVersion.Parse(_protocol);
- _logger.Info($"Listen at 127.0.0.1:{configuration.Port}");
- _logger.Info($"Protocol: {protocolDecoded}");
- }
-
- public void Poll()
- {
- bool polled = false;
- while (!polled)
- {
- if (_host.CheckEvents(out _event) <= 0)
- {
- if (_host.Service(0, out _event) <= 0)
- break;
-
- polled = true;
- }
-
- switch (_event.Type)
- {
- case EventType.None:
- {
- _logger.Trace("None event");
- break;
- }
- case EventType.Connect:
- {
- if (!IsValidProtocol(_event.Data))
- {
- _logger.Warn($"Mismatched protocol Server: {RagonVersion.Parse(_protocol)} Client: {RagonVersion.Parse(_event.Data)}, close connection");
- _event.Peer.DisconnectNow(0);
- break;
- }
-
- var connection = new ENetConnection(_event.Peer);
- _connections[_event.Peer.ID] = connection;
- _listener.OnConnected(connection);
- break;
- }
- case EventType.Disconnect:
- {
- var connection = _connections[_event.Peer.ID];
- _listener.OnDisconnected(connection);
- break;
- }
- case EventType.Timeout:
- {
- var connection = _connections[_event.Peer.ID];
- _listener.OnTimeout(connection);
- break;
- }
- case EventType.Receive:
- {
- var peerId = (ushort) _event.Peer.ID;
- var connection = _connections[peerId];
- var dataRaw = new byte[_event.Packet.Length];
-
- _event.Packet.CopyTo(dataRaw);
- _event.Packet.Dispose();
-
- _listener.OnData(connection, dataRaw);
- break;
- }
- }
- }
- }
-
- public void Stop()
- {
- _host?.Dispose();
-
- Library.Deinitialize();
- }
-
- private bool IsValidProtocol(uint protocol)
- {
- return protocol == _protocol;
- }
- }
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using ENet;
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server.ENet
+{
+ public sealed class ENetServer: INetworkServer
+ {
+ public Executor Executor => _executor;
+
+ private readonly Host _host;
+ private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
+
+ private ENetConnection[] _connections;
+ private INetworkListener _listener;
+ private uint _protocol;
+ private Event _event;
+ private Executor _executor;
+
+ public ENetServer()
+ {
+ _host = new Host();
+ _executor = new Executor();
+ _connections = Array.Empty();
+ }
+
+ public void Start(INetworkListener listener, NetworkConfiguration configuration)
+ {
+ Library.Initialize();
+
+ _connections = new ENetConnection[configuration.LimitConnections];
+
+ _listener = listener;
+ _protocol = configuration.Protocol;
+
+ var address = new Address
+ {
+ Port = (ushort) configuration.Port,
+ };
+
+ _host.Create(address, _connections.Length, 2, 0, 0, 1024 * 1024);
+
+ var protocolDecoded = RagonVersion.Parse(_protocol);
+ _logger.Info($"Listen at 127.0.0.1:{configuration.Port}");
+ _logger.Info($"Protocol: {protocolDecoded}");
+ }
+
+ public void Update()
+ {
+ 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;
+ }
+ }
}
\ No newline at end of file
diff --git a/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs b/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs
new file mode 100644
index 0000000..995fe7c
--- /dev/null
+++ b/Ragon.Server.ENet/Sources/ENetUnreliableChannel.cs
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs b/Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs
deleted file mode 100644
index a456eea..0000000
--- a/Ragon.Server.NativeWebSockets/WebSocketReliableChannel.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Net.WebSockets;
-using Ragon.Server;
-
-namespace Ragon.Server.NativeWebSockets;
-
-public class WebSocketReliableChannel : INetworkChannel
-{
- private Queue _queue;
- private WebSocket _socket;
-
- public WebSocketReliableChannel(WebSocket webSocket)
- {
- _socket = webSocket;
- _queue = new Queue(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);
- }
-}
\ No newline at end of file
diff --git a/Ragon.Server/INetworkChannel.cs b/Ragon.Server/INetworkChannel.cs
deleted file mode 100644
index 803ce66..0000000
--- a/Ragon.Server/INetworkChannel.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Ragon.Server;
-
-public interface INetworkChannel
-{
- void Send(byte[] data);
-}
\ No newline at end of file
diff --git a/Ragon.Server/INetworkConnection.cs b/Ragon.Server/INetworkConnection.cs
deleted file mode 100644
index 5a32411..0000000
--- a/Ragon.Server/INetworkConnection.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ragon.Server;
-
-public interface INetworkConnection
-{
- public ushort Id { get; }
- public INetworkChannel Reliable { get; }
- public INetworkChannel Unreliable { get; }
-}
\ No newline at end of file
diff --git a/Ragon.Server/INetworkListener.cs b/Ragon.Server/INetworkListener.cs
deleted file mode 100644
index f3102f8..0000000
--- a/Ragon.Server/INetworkListener.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Ragon.Server;
-
-public interface INetworkListener
-{
- void OnConnected(INetworkConnection connection);
- void OnDisconnected(INetworkConnection connection);
- void OnTimeout(INetworkConnection connection);
- void OnData(INetworkConnection connection, byte[] data);
-}
\ No newline at end of file
diff --git a/Ragon.Server/INetworkServer.cs b/Ragon.Server/INetworkServer.cs
deleted file mode 100644
index ca6a061..0000000
--- a/Ragon.Server/INetworkServer.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ragon.Server;
-
-public interface INetworkServer
-{
- public void Stop();
- public void Poll();
- public void Start(INetworkListener listener, NetworkConfiguration configuration);
-}
\ No newline at end of file
diff --git a/Ragon.Server/NetworkConfiguration.cs b/Ragon.Server/NetworkConfiguration.cs
deleted file mode 100644
index 15adb9c..0000000
--- a/Ragon.Server/NetworkConfiguration.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-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; }
-}
\ No newline at end of file
diff --git a/Ragon.Server/Ragon.Server.csproj b/Ragon.Server/Ragon.Server.csproj
index 95c97c8..fb83ed3 100644
--- a/Ragon.Server/Ragon.Server.csproj
+++ b/Ragon.Server/Ragon.Server.csproj
@@ -4,8 +4,14 @@
net6.0
enable
enable
+ Ragon.Core
+
+
+
+
+
diff --git a/Ragon.Server/Sources/Entity/RagonEntity.cs b/Ragon.Server/Sources/Entity/RagonEntity.cs
new file mode 100644
index 0000000..c55bacf
--- /dev/null
+++ b/Ragon.Server/Sources/Entity/RagonEntity.cs
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class RagonEntity
+{
+ private static ushort _idGenerator = 100;
+ public ushort Id { get; private set; }
+ public ushort Type { get; private set; }
+ public ushort StaticId { get; private set; }
+ public ushort AttachId { get; private set; }
+ public RagonRoomPlayer Owner { get; private set; }
+ public RagonAuthority Authority { get; private set; }
+ public RagonPayload Payload { get; private set; }
+ public RagonEntityState State { get; private set; }
+
+ private readonly List _bufferedEvents;
+
+ public RagonEntity(RagonRoomPlayer owner, ushort type, ushort staticId, ushort attachId, RagonAuthority eventAuthority)
+ {
+ Owner = owner;
+ StaticId = staticId;
+ Type = type;
+ AttachId = attachId;
+ Id = _idGenerator++;
+ Authority = eventAuthority;
+ State = new RagonEntityState(this);
+ Payload = new RagonPayload();
+
+ _bufferedEvents = new List();
+ }
+
+
+ public void SetOwner(RagonRoomPlayer owner)
+ {
+ Owner = owner;
+ }
+
+ public void RestoreBufferedEvents(RagonRoomPlayer roomPlayer, RagonBuffer writer)
+ {
+ foreach (var evnt in _bufferedEvents)
+ {
+ writer.Clear();
+ writer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
+ writer.WriteUShort(evnt.EventCode);
+ writer.WriteUShort(evnt.Invoker.Connection.Id);
+ writer.WriteByte((byte)RagonReplicationMode.Server);
+ writer.WriteUShort(Id);
+
+ evnt.Write(writer);
+
+ var sendData = writer.ToArray();
+ roomPlayer.Connection.Reliable.Send(sendData);
+ }
+ }
+
+ public void Create()
+ {
+ var room = Owner.Room;
+ var buffer = room.Writer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.CREATE_ENTITY);
+ buffer.WriteUShort(AttachId);
+ buffer.WriteUShort(Type);
+ buffer.WriteUShort(Id);
+ buffer.WriteUShort(Owner.Connection.Id);
+
+ Payload.Write(buffer);
+
+ var sendData = buffer.ToArray();
+ foreach (var player in room.ReadyPlayersList)
+ player.Connection.Reliable.Send(sendData);
+ }
+
+ public void Destroy()
+ {
+ var room = Owner.Room;
+ var buffer = room.Writer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.DESTROY_ENTITY);
+ buffer.WriteUShort(Id);
+
+ Payload.Write(buffer);
+
+ var sendData = buffer.ToArray();
+ foreach (var player in room.ReadyPlayersList)
+ player.Connection.Reliable.Send(sendData);
+ }
+
+ public void Snapshot(RagonBuffer buffer)
+ {
+ buffer.WriteUShort(Type);
+ buffer.WriteUShort(Id);
+ if (StaticId != 0)
+ buffer.WriteUShort(StaticId);
+ buffer.WriteUShort(Owner.Connection.Id);
+
+ buffer.WriteUShort(Payload.Size);
+ Payload.Write(buffer);
+ State.Snapshot(buffer);
+ }
+
+ public void ReplicateEvent(
+ RagonRoomPlayer caller,
+ RagonEvent evnt,
+ RagonReplicationMode eventMode,
+ RagonRoomPlayer targetPlayer
+ )
+ {
+ var room = Owner.Room;
+ var buffer = room.Writer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
+ buffer.WriteUShort(evnt.EventCode);
+ buffer.WriteUShort(caller.Connection.Id);
+ buffer.WriteByte((byte)eventMode);
+ buffer.WriteUShort(Id);
+
+ evnt.Write(buffer);
+
+ var sendData = buffer.ToArray();
+ targetPlayer.Connection.Reliable.Send(sendData);
+ }
+
+ public void ReplicateEvent(
+ RagonRoomPlayer caller,
+ RagonEvent evnt,
+ RagonReplicationMode eventMode,
+ RagonTarget targetMode
+ )
+ {
+ if (Authority == RagonAuthority.OwnerOnly &&
+ Owner.Connection.Id != caller.Connection.Id)
+ {
+ Console.WriteLine($"Player have not enough authority for event with Id {evnt.EventCode}");
+ return;
+ }
+
+ if (eventMode == RagonReplicationMode.Buffered && targetMode != RagonTarget.Owner)
+ {
+ _bufferedEvents.Add(evnt);
+ }
+
+ var room = Owner.Room;
+ var buffer = room.Writer;
+
+ buffer.Clear();
+ buffer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
+ buffer.WriteUShort(evnt.EventCode);
+ buffer.WriteUShort(caller.Connection.Id);
+ buffer.WriteByte((byte)eventMode);
+ buffer.WriteUShort(Id);
+
+ evnt.Write(buffer);
+
+ var sendData = buffer.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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Entity/RagonEntityState.cs b/Ragon.Server/Sources/Entity/RagonEntityState.cs
new file mode 100644
index 0000000..4bfecff
--- /dev/null
+++ b/Ragon.Server/Sources/Entity/RagonEntityState.cs
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class RagonEntityState
+{
+ private List _properties;
+ private RagonEntity _entity;
+
+ public RagonEntityState(RagonEntity entity, int capacity = 10)
+ {
+ _entity = entity;
+ _properties = new List(10);
+ }
+
+ public void AddProperty(RagonProperty property)
+ {
+ _properties.Add(property);
+ }
+
+ public void Write(RagonBuffer buffer)
+ {
+ buffer.WriteUShort(_entity.Id);
+ foreach (var property in _properties)
+ {
+ if (property.IsDirty)
+ {
+ buffer.WriteBool(true);
+ property.Write(buffer);
+ property.Clear();
+ continue;
+ }
+
+ buffer.WriteBool(false);
+ }
+ }
+
+ public void Read(RagonBuffer buffer)
+ {
+ foreach (var property in _properties)
+ {
+ if (buffer.ReadBool())
+ property.Read(buffer);
+ }
+ }
+
+ public void Snapshot(RagonBuffer buffer)
+ {
+ foreach (var property in _properties)
+ {
+ var hasPayload = property.IsFixed || !property.IsFixed && property.Size > 0;
+ if (hasPayload)
+ {
+ buffer.WriteBool(true);
+ property.Write(buffer);
+ continue;
+ }
+
+ buffer.WriteBool(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Entity/RagonEvent.cs b/Ragon.Server/Sources/Entity/RagonEvent.cs
new file mode 100644
index 0000000..48069ec
--- /dev/null
+++ b/Ragon.Server/Sources/Entity/RagonEvent.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class RagonEvent
+{
+ public RagonRoomPlayer Invoker { get; private set; }
+ public ushort EventCode { get; private set; }
+ public ushort Size => (ushort) _size;
+
+ private uint[] _data = new uint[128];
+ private int _size = 0;
+
+ public RagonEvent(
+ RagonRoomPlayer invoker,
+ ushort eventCode
+ )
+ {
+ Invoker = invoker;
+ EventCode = eventCode;
+ }
+
+ public void Read(RagonBuffer buffer)
+ {
+ var readOnlySpan = _data.AsSpan();
+ _size = buffer.Capacity;
+ buffer.ReadSpan(ref readOnlySpan, _size);
+ }
+
+ public void Write(RagonBuffer buffer)
+ {
+ if (_size == 0) return;
+ ReadOnlySpan readOnlySpan = _data.AsSpan();
+ buffer.WriteSpan(ref readOnlySpan, _size);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Entity/RagonPayload.cs b/Ragon.Server/Sources/Entity/RagonPayload.cs
new file mode 100644
index 0000000..064eb5e
--- /dev/null
+++ b/Ragon.Server/Sources/Entity/RagonPayload.cs
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class RagonPayload
+{
+ private uint[] _data = new uint[128];
+ private int _size = 0;
+
+ public ushort Size => (ushort) _size;
+
+ public void Read(RagonBuffer buffer)
+ {
+ var readOnlySpan = _data.AsSpan();
+
+ _size = buffer.Capacity;
+
+ buffer.ReadSpan(ref readOnlySpan, _size);
+ }
+
+ public void Write(RagonBuffer buffer)
+ {
+ if (_size == 0) return;
+
+ ReadOnlySpan readOnlySpan = _data.AsSpan();
+
+ buffer.WriteSpan(ref readOnlySpan, _size);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Entity/RagonProperty.cs b/Ragon.Server/Sources/Entity/RagonProperty.cs
new file mode 100644
index 0000000..3f453c1
--- /dev/null
+++ b/Ragon.Server/Sources/Entity/RagonProperty.cs
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using System.ComponentModel;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class RagonProperty : RagonPayload
+{
+ public int Size { get; set; }
+ public bool IsDirty { get; private set; }
+ public bool IsFixed { get; private set; }
+
+ private uint[] _data;
+
+ public RagonProperty(int size, bool isFixed)
+ {
+ Size = size;
+ IsFixed = isFixed;
+ IsDirty = false;
+
+ _data = new uint[128];
+ }
+
+ public void Read(RagonBuffer buffer)
+ {
+ var readOnlySpan = _data.AsSpan();
+ if (IsFixed)
+ {
+ buffer.ReadSpan(ref readOnlySpan, Size);
+ }
+ else
+ {
+ Size = (int) buffer.Read();
+ buffer.ReadSpan(ref readOnlySpan, Size);
+ }
+
+ IsDirty = true;
+ }
+
+ public void Write(RagonBuffer buffer)
+ {
+ ReadOnlySpan readOnlySpan = _data.AsSpan();
+ buffer.WriteSpan(ref readOnlySpan, Size);
+ }
+
+ public void Clear()
+ {
+ IsDirty = false;
+ }
+
+ public void Dump()
+ {
+ Console.WriteLine( $"[{Size.ToString("00")}] {string.Join("", _data.Take(8).Reverse().Select(b => Convert.ToString(b, 2).PadLeft(32, '0')))}");
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Handler/AuthorizationOperation.cs b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs
new file mode 100644
index 0000000..6dac1f5
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/AuthorizationOperation.cs
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public sealed class AuthorizationOperation: IRagonOperation
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
+ {
+ if (context.LobbyPlayer.Status == LobbyPlayerStatus.Authorized)
+ {
+ _logger.Warn("Player already authorized");
+ return;
+ }
+
+ var key = reader.ReadString();
+ var playerName = reader.ReadString();
+ var additionalPayload = new RagonPayload();
+ additionalPayload.Read(reader);
+
+ context.LobbyPlayer.Name = playerName;
+ context.LobbyPlayer.AdditionalData = Array.Empty();
+ 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($"Connection {context.Connection.Id} as {playerId}|{context.LobbyPlayer.Name} authorized");
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Handler/EntityCreateOperation.cs b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs
new file mode 100644
index 0000000..32c0069
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/EntityCreateOperation.cs
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public sealed class EntityCreateOperation : IRagonOperation
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
+ {
+ var player = context.RoomPlayer;
+ var room = context.Room;
+ var attachId = reader.ReadUShort();
+ var entityType = reader.ReadUShort();
+ var eventAuthority = (RagonAuthority) reader.ReadByte();
+ var propertiesCount = reader.ReadUShort();
+
+ var entity = new RagonEntity(player, entityType, 0, attachId, eventAuthority);
+ for (var i = 0; i < propertiesCount; i++)
+ {
+ var propertyType = reader.ReadBool();
+ var propertySize = reader.ReadUShort();
+
+ entity.State.AddProperty(new RagonProperty(propertySize, propertyType));
+ }
+
+ if (reader.Capacity > 0)
+ entity.Payload.Read(reader);
+
+ room.AttachEntity(entity);
+ player.AttachEntity(entity);
+
+ entity.Create();
+
+ _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}");
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Handler/EntityDestroyOperation.cs b/Ragon.Server/Sources/Handler/EntityDestroyOperation.cs
new file mode 100644
index 0000000..a112529
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/EntityDestroyOperation.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public sealed class EntityDestroyOperation: IRagonOperation
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
+ {
+ var player = context.RoomPlayer;
+ var room = context.Room;
+ var entityId = reader.ReadUShort();
+
+ if (room.Entities.TryGetValue(entityId, out var entity))
+ {
+ var payload = new RagonPayload();
+ payload.Read(reader);
+
+ room.DetachEntity(entity);
+ player.DetachEntity(entity);
+
+ entity.Destroy();
+
+ _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} destoyed entity {entity.Id}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Handler/EntityEventOperation.cs b/Ragon.Server/Sources/Handler/EntityEventOperation.cs
new file mode 100644
index 0000000..284721a
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/EntityEventOperation.cs
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public sealed class EntityEventOperation : IRagonOperation
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer 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 targetPlayerPeerId = (ushort)0;
+
+ if (targetMode == RagonTarget.Player)
+ targetPlayerPeerId = reader.ReadUShort();
+
+ var ragonEvent = new RagonEvent(player, eventId);
+ ragonEvent.Read(reader);
+
+ if (targetMode == RagonTarget.Player &&
+ context.Room.Players.TryGetValue(targetPlayerPeerId, out var targetPlayer))
+ {
+ ent.ReplicateEvent(player, ragonEvent, eventMode, targetPlayer);
+ return;
+ }
+
+ ent.ReplicateEvent(player, ragonEvent, eventMode, targetMode);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Handler/EntityStateOperation.cs b/Ragon.Server/Sources/Handler/EntityStateOperation.cs
new file mode 100644
index 0000000..119ee07
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/EntityStateOperation.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public sealed class EntityStateOperation: IRagonOperation
+{
+ private ILogger _logger = LogManager.GetCurrentClassLogger();
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer 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");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/RoomCreateHandler.cs b/Ragon.Server/Sources/Handler/RoomCreateOperation.cs
similarity index 62%
rename from Ragon.Core/Handlers/RoomCreateHandler.cs
rename to Ragon.Server/Sources/Handler/RoomCreateOperation.cs
index 8b668f1..a97fad2 100644
--- a/Ragon.Core/Handlers/RoomCreateHandler.cs
+++ b/Ragon.Server/Sources/Handler/RoomCreateOperation.cs
@@ -1,16 +1,30 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using NLog;
-using Ragon.Common;
-using Ragon.Core.Game;
-using Ragon.Core.Lobby;
+using Ragon.Protocol;
-namespace Ragon.Core.Handlers;
+namespace Ragon.Server;
-public sealed class CreateHandler: IHandler
+public sealed class RoomCreateOperation: IRagonOperation
{
private RagonRoomParameters _roomParameters = new();
private Logger _logger = LogManager.GetCurrentClassLogger();
- public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
{
@@ -48,26 +62,22 @@ public sealed class CreateHandler: IHandler
};
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;
+ var room = new RagonRoom(roomId, information);
+ context.Scheduler.Run(room);
context.Lobby.Persist(room);
+ var player = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
+ context.SetRoom(room, player);
+
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} create room {room.Id} {information}");
- JoinSuccess(roomPlayer, room, writer);
-
- context.Loop.Run(room);
+ JoinSuccess(player, room, writer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to room {room.Id}");
}
- private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer)
+ private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_SUCCESS);
diff --git a/Ragon.Core/Handlers/RoomJoinHandler.cs b/Ragon.Server/Sources/Handler/RoomJoinOperation.cs
similarity index 50%
rename from Ragon.Core/Handlers/RoomJoinHandler.cs
rename to Ragon.Server/Sources/Handler/RoomJoinOperation.cs
index 74f9e25..a9eda75 100644
--- a/Ragon.Core/Handlers/RoomJoinHandler.cs
+++ b/Ragon.Server/Sources/Handler/RoomJoinOperation.cs
@@ -1,15 +1,29 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using NLog;
-using Ragon.Common;
-using Ragon.Core.Game;
-using Ragon.Core.Lobby;
+using Ragon.Protocol;
-namespace Ragon.Core.Handlers;
+namespace Ragon.Server;
-public sealed class JoinHandler : IHandler
+public sealed class RoomJoinOperation : IRagonOperation
{
private Logger _logger = LogManager.GetCurrentClassLogger();
- public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
{
var roomId = reader.ReadString();
var lobbyPlayer = context.LobbyPlayer;
@@ -22,20 +36,15 @@ public sealed class JoinHandler : IHandler
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);
+ var player = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
+ context.SetRoom(existsRoom, player);
+
+ JoinSuccess(player, existsRoom, writer);
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} joined to {existsRoom.Id}");
}
- private void JoinSuccess(RoomPlayer player, Room room, RagonSerializer writer)
+ private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_SUCCESS);
@@ -50,7 +59,7 @@ public sealed class JoinHandler : IHandler
player.Connection.Reliable.Send(sendData);
}
- private void JoinFailed(LobbyPlayer player, RagonSerializer writer)
+ private void JoinFailed(RagonLobbyPlayer player, RagonBuffer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_FAILED);
diff --git a/Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs b/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs
similarity index 52%
rename from Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs
rename to Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs
index 50ecb22..17f509f 100644
--- a/Ragon.Core/Handlers/RoomJoinOrCreateHandler.cs
+++ b/Ragon.Server/Sources/Handler/RoomJoinOrCreateOperation.cs
@@ -1,16 +1,30 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using NLog;
-using Ragon.Common;
-using Ragon.Core.Game;
-using Ragon.Core.Lobby;
+using Ragon.Protocol;
-namespace Ragon.Core.Handlers;
+namespace Ragon.Server;
-public sealed class JoinOrCreateHandler : IHandler
+public sealed class RoomJoinOrCreateOperation : IRagonOperation
{
private RagonRoomParameters _roomParameters = new();
private Logger _logger = LogManager.GetCurrentClassLogger();
- public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
{
@@ -25,15 +39,10 @@ public sealed class JoinOrCreateHandler : IHandler
if (context.Lobby.FindRoomByMap(_roomParameters.Map, out var existsRoom))
{
- var roomPlayer = new RoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
+ var player = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
+ context.SetRoom(existsRoom, player);
- context.Room?.RemovePlayer(context.RoomPlayer);
- context.Room = existsRoom;
- context.RoomPlayer = roomPlayer;
-
- existsRoom.AddPlayer(roomPlayer);
-
- JoinSuccess(roomPlayer, existsRoom, writer);
+ JoinSuccess(player, existsRoom, writer);
}
else
{
@@ -44,25 +53,20 @@ public sealed class JoinOrCreateHandler : IHandler
Min = _roomParameters.Min,
};
- var room = new Room(roomId, information);
+ var room = new RagonRoom(roomId, information);
context.Lobby.Persist(room);
+ context.Scheduler.Run(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;
+ var roomPlayer = new RagonRoomPlayer(lobbyPlayer.Connection, lobbyPlayer.Id, lobbyPlayer.Name);
+ context.SetRoom(room, 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)
+ private void JoinSuccess(RagonRoomPlayer player, RagonRoom room, RagonBuffer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.JOIN_SUCCESS);
diff --git a/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs b/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs
new file mode 100644
index 0000000..c118216
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/RoomLeaveOperation.cs
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public sealed class RoomLeaveOperation: IRagonOperation
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
+ {
+ var room = context.Room;
+ var roomPlayer = context.RoomPlayer;
+ if (room != null)
+ {
+ context.Room?.DetachPlayer(roomPlayer);
+ _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} leaved from {room.Id}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Handler/SceneLoadOperation.cs b/Ragon.Server/Sources/Handler/SceneLoadOperation.cs
new file mode 100644
index 0000000..7aae87c
--- /dev/null
+++ b/Ragon.Server/Sources/Handler/SceneLoadOperation.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using NLog;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class SceneLoadOperation: IRagonOperation
+{
+ private Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
+ {
+ var roomOwner = context.Room.Owner;
+ var currentPlayer = context.RoomPlayer;
+ var room = context.Room;
+ var sceneName = reader.ReadString();
+
+ if (roomOwner.Connection.Id != currentPlayer.Connection.Id)
+ {
+ _logger.Warn("Only owner can change map!");
+ return;
+ }
+
+ room.UpdateMap(sceneName);
+
+ writer.Clear();
+ writer.WriteOperation(RagonOperation.LOAD_SCENE);
+ writer.WriteString(sceneName);
+
+ var sendData = writer.ToArray();
+ foreach (var player in room.PlayerList)
+ player.Connection.Reliable.Send(sendData);
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Core/Handlers/SceneLoadedHandler.cs b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs
similarity index 59%
rename from Ragon.Core/Handlers/SceneLoadedHandler.cs
rename to Ragon.Server/Sources/Handler/SceneLoadedOperation.cs
index de40a96..7c20e6e 100644
--- a/Ragon.Core/Handlers/SceneLoadedHandler.cs
+++ b/Ragon.Server/Sources/Handler/SceneLoadedOperation.cs
@@ -1,15 +1,29 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using NLog;
-using Ragon.Common;
-using Ragon.Core.Game;
-using Ragon.Core.Lobby;
+using Ragon.Protocol;
-namespace Ragon.Core.Handlers;
+namespace Ragon.Server;
-public sealed class SceneLoadedHandler : IHandler
+public sealed class SceneLoadedOperation : IRagonOperation
{
private Logger _logger = LogManager.GetCurrentClassLogger();
-
- public void Handle(PlayerContext context, RagonSerializer reader, RagonSerializer writer)
+
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer)
{
if (context.LobbyPlayer.Status == LobbyPlayerStatus.Unauthorized)
return;
@@ -24,48 +38,53 @@ public sealed class SceneLoadedHandler : IHandler
for (var staticIndex = 0; staticIndex < statics; staticIndex++)
{
var entityType = reader.ReadUShort();
- var eventAuthority = (RagonAuthority) reader.ReadByte();
+ var eventAuthority = (RagonAuthority)reader.ReadByte();
var staticId = reader.ReadUShort();
var propertiesCount = reader.ReadUShort();
-
- var entity = new Entity(player, entityType, staticId, eventAuthority);
+
+ var entity = new RagonEntity(player, entityType, staticId, 0, eventAuthority);
for (var propertyIndex = 0; propertyIndex < propertiesCount; propertyIndex++)
{
var propertyType = reader.ReadBool();
var propertySize = reader.ReadUShort();
- entity.State.AddProperty(new EntityStateProperty(propertySize, propertyType));
+ entity.State.AddProperty(new RagonProperty(propertySize, propertyType));
}
+
+ var playerInfo = $"Player {context.Connection.Id}|{context.LobbyPlayer.Name}";
+ var entityInfo = $"{entity.Id}:{entity.Type}";
- _logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} created entity {entity.Id}:{entity.Type}");
- room.AttachEntity(player, entity);
+ _logger.Trace($"{playerInfo} created entity {entityInfo}");
+
+ room.AttachEntity(entity);
+ player.AttachEntity(entity);
}
_logger.Trace($"Player {context.Connection.Id}|{context.LobbyPlayer.Name} loaded");
-
+
room.WaitPlayersList.Add(player);
foreach (var roomPlayer in room.WaitPlayersList)
{
DispatchPlayerJoinExcludePlayer(room, roomPlayer, writer);
-
+
roomPlayer.SetReady();
}
-
+
room.UpdateReadyPlayerList();
-
+
DispatchSnapshot(room, room.WaitPlayersList, writer);
-
+
room.WaitPlayersList.Clear();
}
else if (owner.IsLoaded)
{
player.SetReady();
-
+
DispatchPlayerJoinExcludePlayer(room, player, writer);
-
+
room.UpdateReadyPlayerList();
-
- DispatchSnapshot(room, new List() { player }, writer);
+
+ DispatchSnapshot(room, new List() { player }, writer);
foreach (var entity in room.EntityList)
entity.RestoreBufferedEvents(player, writer);
@@ -77,14 +96,14 @@ public sealed class SceneLoadedHandler : IHandler
}
}
- private void DispatchPlayerJoinExcludePlayer(Room room, RoomPlayer roomPlayer, RagonSerializer writer)
+ private void DispatchPlayerJoinExcludePlayer(RagonRoom room, RagonRoomPlayer roomPlayer, RagonBuffer 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)
{
@@ -93,30 +112,30 @@ public sealed class SceneLoadedHandler : IHandler
}
}
- private void DispatchSnapshot(Room room, List receviersList, RagonSerializer writer)
+ private void DispatchSnapshot(RagonRoom room, List receviersList, RagonBuffer writer)
{
writer.Clear();
writer.WriteOperation(RagonOperation.SNAPSHOT);
- writer.WriteUShort((ushort) room.ReadyPlayersList.Count);
+ 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;
+ var dynamicEntitiesCount = (ushort)dynamicEntities.Count;
writer.WriteUShort(dynamicEntitiesCount);
foreach (var entity in dynamicEntities)
- entity.State.Snapshot(writer);
+ entity.Snapshot(writer);
var staticEntities = room.StaticEntitiesList;
- var staticEntitiesCount = (ushort) staticEntities.Count;
+ var staticEntitiesCount = (ushort)staticEntities.Count;
writer.WriteUShort(staticEntitiesCount);
foreach (var entity in staticEntities)
- entity.State.Snapshot(writer);
-
+ entity.Snapshot(writer);
+
var sendData = writer.ToArray();
foreach (var player in receviersList)
player.Connection.Reliable.Send(sendData);
diff --git a/Ragon.Server/IO/Executor.cs b/Ragon.Server/Sources/IO/Executor.cs
similarity index 59%
rename from Ragon.Server/IO/Executor.cs
rename to Ragon.Server/Sources/IO/Executor.cs
index b0451c6..a503546 100644
--- a/Ragon.Server/IO/Executor.cs
+++ b/Ragon.Server/Sources/IO/Executor.cs
@@ -1,8 +1,24 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using System.Threading.Channels;
-namespace Ragon.Core.Server;
+namespace Ragon.Server;
-public class Executor: TaskScheduler
+public class Executor: TaskScheduler, IExecutor
{
private ChannelReader _reader;
private ChannelWriter _writer;
@@ -39,7 +55,7 @@ public class Executor: TaskScheduler
return false;
}
- public void Execute()
+ public void Update()
{
while (_reader.TryRead(out var task))
{
diff --git a/Ragon.Server/Sources/IO/IExecutor.cs b/Ragon.Server/Sources/IO/IExecutor.cs
new file mode 100644
index 0000000..e1560ba
--- /dev/null
+++ b/Ragon.Server/Sources/IO/IExecutor.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public interface IExecutor
+{
+ public void Run(Action action);
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/IO/INetworkChannel.cs b/Ragon.Server/Sources/IO/INetworkChannel.cs
new file mode 100644
index 0000000..fa8c7e2
--- /dev/null
+++ b/Ragon.Server/Sources/IO/INetworkChannel.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public interface INetworkChannel
+{
+ void Send(byte[] data);
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/IO/INetworkConnection.cs b/Ragon.Server/Sources/IO/INetworkConnection.cs
new file mode 100644
index 0000000..4ad3c86
--- /dev/null
+++ b/Ragon.Server/Sources/IO/INetworkConnection.cs
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public interface INetworkConnection
+{
+ public ushort Id { get; }
+ public INetworkChannel Reliable { get; }
+ public INetworkChannel Unreliable { get; }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/IO/INetworkListener.cs b/Ragon.Server/Sources/IO/INetworkListener.cs
new file mode 100644
index 0000000..7dd3928
--- /dev/null
+++ b/Ragon.Server/Sources/IO/INetworkListener.cs
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public interface INetworkListener
+{
+ void OnConnected(INetworkConnection connection);
+ void OnDisconnected(INetworkConnection connection);
+ void OnTimeout(INetworkConnection connection);
+ void OnData(INetworkConnection connection, byte[] data);
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/IO/INetworkServer.cs b/Ragon.Server/Sources/IO/INetworkServer.cs
new file mode 100644
index 0000000..107506a
--- /dev/null
+++ b/Ragon.Server/Sources/IO/INetworkServer.cs
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public interface INetworkServer
+{
+ public Executor Executor { get; }
+ public void Stop();
+ public void Update();
+ public void Start(INetworkListener listener, NetworkConfiguration configuration);
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/IO/NetworkConfiguration.cs b/Ragon.Server/Sources/IO/NetworkConfiguration.cs
new file mode 100644
index 0000000..b76768b
--- /dev/null
+++ b/Ragon.Server/Sources/IO/NetworkConfiguration.cs
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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; }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/IRagonOperation.cs b/Ragon.Server/Sources/IRagonOperation.cs
new file mode 100644
index 0000000..e94882e
--- /dev/null
+++ b/Ragon.Server/Sources/IRagonOperation.cs
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public interface IRagonOperation
+{
+ public void Handle(RagonContext context, RagonBuffer reader, RagonBuffer writer);
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Lobby/IRagonLobby.cs b/Ragon.Server/Sources/Lobby/IRagonLobby.cs
new file mode 100644
index 0000000..7003df4
--- /dev/null
+++ b/Ragon.Server/Sources/Lobby/IRagonLobby.cs
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Ragon.Server;
+
+public interface IRagonLobby
+{
+ public bool FindRoomById(string roomId, [MaybeNullWhen(false)] out RagonRoom room);
+ public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out RagonRoom room);
+ public void Persist(RagonRoom room);
+ public void RemoveIfEmpty(RagonRoom room);
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs b/Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs
new file mode 100644
index 0000000..2f1f5f5
--- /dev/null
+++ b/Ragon.Server/Sources/Lobby/RagonLobbyInMemory.cs
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Diagnostics.CodeAnalysis;
+using NLog;
+
+namespace Ragon.Server;
+
+public class LobbyInMemory : IRagonLobby
+{
+ private readonly List _rooms = new();
+ private readonly Logger _logger = LogManager.GetCurrentClassLogger();
+
+ public bool FindRoomById(string RagonRoomId, [MaybeNullWhen(false)] out RagonRoom room)
+ {
+ foreach (var existRagonRoom in _rooms)
+ {
+ var info = existRagonRoom.Info;
+ if (existRagonRoom.Id == RagonRoomId && info.Min < info.Max)
+ {
+ room = existRagonRoom;
+ return true;
+ }
+ }
+
+ room = default;
+ return false;
+ }
+
+ public bool FindRoomByMap(string map, [MaybeNullWhen(false)] out RagonRoom room)
+ {
+ foreach (var existsRoom in _rooms)
+ {
+ var info = existsRoom.Info;
+ if (info.Map == map && existsRoom.Players.Count < info.Max)
+ {
+ room = existsRoom;
+ return true;
+ }
+ }
+
+ room = default;
+ return false;
+ }
+
+ public void Persist(RagonRoom room)
+ {
+ _rooms.Add(room);
+ _logger.Trace($"New room: {room.Id}");
+
+ foreach (var r in _rooms)
+ _logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count} Entities: {r.Entities.Count}");
+ }
+
+ public void RemoveIfEmpty(RagonRoom room)
+ {
+ if (room.Players.Count == 0)
+ {
+ _rooms.Remove(room);
+ _logger.Trace($"Room {room.Id} removed");
+ }
+
+ foreach (var r in _rooms)
+ _logger.Trace($"Room: {r.Id} {r.Info} Players: {r.Players.Count} Entities: {r.Entities.Count}");
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs b/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs
new file mode 100644
index 0000000..54bde74
--- /dev/null
+++ b/Ragon.Server/Sources/Lobby/RagonLobbyPlayer.cs
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public enum LobbyPlayerStatus
+{
+ Unauthorized,
+ Authorized,
+}
+
+public class RagonLobbyPlayer
+{
+ 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 RagonLobbyPlayer(INetworkConnection connection)
+ {
+ Id = Guid.NewGuid().ToString();
+ Connection = connection;
+ Status = LobbyPlayerStatus.Unauthorized;
+ Name = "None";
+ AdditionalData = Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/RagonContext.cs b/Ragon.Server/Sources/RagonContext.cs
new file mode 100644
index 0000000..1cdb384
--- /dev/null
+++ b/Ragon.Server/Sources/RagonContext.cs
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Core.Time;
+using Ragon.Server;
+
+namespace Ragon.Server;
+
+public class RagonContext
+{
+ public INetworkConnection Connection { get; }
+ public IExecutor Executor { get; private set; }
+
+ public IRagonLobby Lobby { get; private set; }
+ public RagonLobbyPlayer LobbyPlayer { get; private set; }
+
+ public RagonRoom? Room { get; private set; }
+ public RagonRoomPlayer? RoomPlayer { get; private set; }
+
+ public RagonScheduler Scheduler { get; private set; }
+
+ public RagonContext(INetworkConnection connection, IExecutor executor, IRagonLobby lobby, RagonScheduler scheduler, RagonLobbyPlayer lobbyPlayer)
+ {
+ Connection = connection;
+ Executor = executor;
+ Lobby = lobby;
+ Scheduler = scheduler;
+ LobbyPlayer = lobbyPlayer;
+ }
+
+ internal void SetRoom(RagonRoom room, RagonRoomPlayer player)
+ {
+ Room?.DetachPlayer(RoomPlayer);
+
+ Room = room;
+ RoomPlayer = player;
+
+ Room.AttachPlayer(RoomPlayer);
+ }
+
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/RagonEntityCache.cs b/Ragon.Server/Sources/RagonEntityCache.cs
new file mode 100644
index 0000000..9a55352
--- /dev/null
+++ b/Ragon.Server/Sources/RagonEntityCache.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Ragon.Server;
+
+public class RagonEntityCache
+{
+ private readonly List _dynamicEntitiesList = new List();
+ private readonly List _staticEntitiesList = new List();
+ private readonly Dictionary _entitiesMap = new Dictionary();
+
+ public IReadOnlyList StaticList => _staticEntitiesList;
+ public IReadOnlyList DynamicList => _dynamicEntitiesList;
+ public IReadOnlyDictionary Map => _entitiesMap;
+
+ public void Add(RagonEntity entity)
+ {
+ if (entity.StaticId != 0)
+ _staticEntitiesList.Add(entity);
+ else
+ _dynamicEntitiesList.Add(entity);
+
+ _entitiesMap.Add(entity.Id, entity);
+ }
+
+ public bool Remove(RagonEntity entity)
+ {
+ if (_entitiesMap.Remove(entity.Id, out var existEntity))
+ {
+ _staticEntitiesList.Remove(entity);
+ _dynamicEntitiesList.Remove(entity);
+
+ return true;
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Server/Sources/RagonServer.cs b/Ragon.Server/Sources/RagonServer.cs
new file mode 100644
index 0000000..317f3e0
--- /dev/null
+++ b/Ragon.Server/Sources/RagonServer.cs
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Diagnostics;
+using NLog;
+using Ragon.Core.Time;
+using Ragon.Protocol;
+using Ragon.Server;
+
+namespace Ragon.Server;
+
+public class RagonServer : 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 IRagonOperation[] _handlers;
+ private readonly RagonBuffer _reader;
+ private readonly RagonBuffer _writer;
+ private readonly IRagonLobby _lobby;
+ private readonly RagonScheduler _scheduler;
+ private readonly Dictionary _contexts;
+ private long _tickrate = 0;
+ private Stopwatch _timer;
+
+ public RagonServer(INetworkServer server, Configuration configuration)
+ {
+ _server = server;
+ _executor = _server.Executor;
+ _configuration = configuration;
+ _dedicatedThread = new Thread(Execute);
+ _dedicatedThread.IsBackground = true;
+ _contexts = new Dictionary();
+ _lobby = new LobbyInMemory();
+ _scheduler = new RagonScheduler();
+
+ _reader = new RagonBuffer();
+ _writer = new RagonBuffer();
+ _tickrate = _configuration.ServerTickRate;
+ _timer = new Stopwatch();
+
+ _handlers = new IRagonOperation[byte.MaxValue];
+ _handlers[(byte) RagonOperation.AUTHORIZE] = new AuthorizationOperation();
+ _handlers[(byte) RagonOperation.JOIN_OR_CREATE_ROOM] = new RoomJoinOrCreateOperation();
+ _handlers[(byte) RagonOperation.CREATE_ROOM] = new RoomCreateOperation();
+ _handlers[(byte) RagonOperation.JOIN_ROOM] = new RoomJoinOperation();
+ _handlers[(byte) RagonOperation.LEAVE_ROOM] = new RoomLeaveOperation();
+ _handlers[(byte) RagonOperation.LOAD_SCENE] = new SceneLoadOperation();
+ _handlers[(byte) RagonOperation.SCENE_LOADED] = new SceneLoadedOperation();
+ _handlers[(byte) RagonOperation.CREATE_ENTITY] = new EntityCreateOperation();
+ _handlers[(byte) RagonOperation.DESTROY_ENTITY] = new EntityDestroyOperation();
+ _handlers[(byte) RagonOperation.REPLICATE_ENTITY_EVENT] = new EntityEventOperation();
+ _handlers[(byte) RagonOperation.REPLICATE_ENTITY_STATE] = new EntityStateOperation();
+ }
+
+ public void Execute()
+ {
+ _timer.Start();
+ while (true)
+ {
+ if (_timer.ElapsedMilliseconds > _tickrate)
+ {
+ _executor.Update();
+ _timer.Restart();
+ }
+
+ _scheduler.Update();
+ _server.Update();
+
+ Thread.Sleep(1);
+ }
+ }
+
+ public void Start(bool executeInDedicatedThread = false)
+ {
+ var networkConfiguration = new NetworkConfiguration()
+ {
+ LimitConnections = _configuration.LimitConnections,
+ Protocol = RagonVersion.Parse(_configuration.GameProtocol),
+ Address = "0.0.0.0",
+ Port = _configuration.Port,
+ };
+
+ _server.Start(this, networkConfiguration);
+
+ if (executeInDedicatedThread)
+ _dedicatedThread.Start();
+ else
+ Execute();
+ }
+
+ public void Dispose()
+ {
+ _server.Stop();
+ _dedicatedThread.Interrupt();
+ }
+
+ public void OnConnected(INetworkConnection connection)
+ {
+ var lobbyPlayer = new RagonLobbyPlayer(connection);
+ var context = new RagonContext(connection, _executor, _lobby, _scheduler, lobbyPlayer);
+
+ _logger.Trace($"Connected: {connection.Id}");
+ _contexts.Add(connection.Id, context);
+ }
+
+ public void OnDisconnected(INetworkConnection connection)
+ {
+ if (_contexts.Remove(connection.Id, out var context))
+ {
+ var room = context.Room;
+ if (room != null)
+ {
+ room.DetachPlayer(context.RoomPlayer);
+ _lobby.RemoveIfEmpty(room);
+ }
+
+ _logger.Trace($"Disconnected: {connection.Id}|{context.LobbyPlayer.Name}|{context.LobbyPlayer.Id}");
+ }
+ else
+ {
+ _logger.Trace($"Disconnected: {connection.Id}");
+ }
+ }
+
+ public void OnTimeout(INetworkConnection connection)
+ {
+ if (_contexts.Remove(connection.Id, out var context))
+ {
+ var room = context.Room;
+ if (room != null)
+ {
+ room.DetachPlayer(context.RoomPlayer);
+ _lobby.RemoveIfEmpty(room);
+ }
+
+ _logger.Trace($"Timeout: {connection.Id}|{context.LobbyPlayer.Name}|{context.LobbyPlayer.Id}");
+ }
+ else
+ {
+ _logger.Trace($"Timeout: {connection.Id}");
+ }
+ }
+
+ public void OnData(INetworkConnection connection, byte[] data)
+ {
+ try
+ {
+ if (_contexts.TryGetValue(connection.Id, out var context))
+ {
+ _writer.Clear();
+ _reader.Clear();
+ _reader.FromArray(data);
+
+ // Console.WriteLine($"{string.Join(",", data.Select(d => d.ToString()))}");
+ var operation = _reader.ReadByte();
+ _handlers[operation].Handle(context, _reader, _writer);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Ragon.Core/Configuration.cs b/Ragon.Server/Sources/RagonServerConfiguration.cs
similarity index 54%
rename from Ragon.Core/Configuration.cs
rename to Ragon.Server/Sources/RagonServerConfiguration.cs
index dbdc447..5c55ddd 100644
--- a/Ragon.Core/Configuration.cs
+++ b/Ragon.Server/Sources/RagonServerConfiguration.cs
@@ -1,7 +1,29 @@
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
using Newtonsoft.Json;
using NLog;
-namespace Ragon.Core;
+namespace Ragon.Server;
+
+public enum ServerType
+{
+ ENET,
+ WEBSOCKET,
+}
[Serializable]
public struct Configuration
@@ -16,7 +38,12 @@ public struct Configuration
public int LimitRooms;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
- private static readonly string ServerVersion = "1.0.31-rc";
+ private static readonly string ServerVersion = "1.1.0-rc";
+ private static Dictionary _serverTypes = new Dictionary()
+ {
+ {"enet", Server.ServerType.ENET},
+ {"websocket", Server.ServerType.WEBSOCKET}
+ };
private static void CopyrightInfo()
{
@@ -40,4 +67,6 @@ public struct Configuration
var configuration = JsonConvert.DeserializeObject(data);
return configuration;
}
+
+ public static ServerType GetServerType(string type) => _serverTypes[type];
}
\ No newline at end of file
diff --git a/Ragon.Core/Game/Room.cs b/Ragon.Server/Sources/Room/RagonRoom.cs
similarity index 50%
rename from Ragon.Core/Game/Room.cs
rename to Ragon.Server/Sources/Room/RagonRoom.cs
index 26a3ccf..655820d 100644
--- a/Ragon.Core/Game/Room.cs
+++ b/Ragon.Server/Sources/Room/RagonRoom.cs
@@ -1,166 +1,201 @@
-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 Players { get; private set; }
- public List WaitPlayersList { get; private set; }
- public List ReadyPlayersList { get; private set; }
- public List PlayerList { get; private set; }
-
- public Dictionary Entities { get; private set; }
- public List DynamicEntitiesList { get; private set; }
- public List StaticEntitiesList { get; private set; }
- public List EntityList { get; private set; }
-
- private readonly HashSet _entitiesDirtySet;
-
- public Room(string roomId, RoomInformation info)
- {
- Id = roomId;
- Info = info;
-
- Players = new Dictionary(info.Max);
- WaitPlayersList = new List(info.Max);
- ReadyPlayersList = new List(info.Max);
- PlayerList = new List(info.Max);
-
- Entities = new Dictionary();
- DynamicEntitiesList = new List();
- StaticEntitiesList = new List();
- EntityList = new List();
-
- _entitiesDirtySet = new HashSet();
- 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);
- }
+/*
+ * Copyright 2023 Eduard Kargin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using Ragon.Core.Time;
+using Ragon.Protocol;
+
+namespace Ragon.Server;
+
+public class RagonRoom: IRagonAction
+{
+ public string Id { get; private set; }
+ public RoomInformation Info { get; private set; }
+ public RagonRoomPlayer Owner { get; private set; }
+ public RagonBuffer Writer { get; }
+ public Dictionary Players { get; private set; }
+ public List