From 873321ff8ecb02170e6a9150e93d22ad11382c85 Mon Sep 17 00:00:00 2001 From: Pelle Bruinsma Date: Tue, 19 Dec 2023 16:26:22 +0100 Subject: [PATCH] Imported changes post-7dfps. --- .../NetworkObject.cs} | 37 ++- Bindings/Godot/{GodotHelper.cs => Helper.cs} | 8 +- .../{MiscGodotBindings.cs => MiscBindings.cs} | 5 + Bindings/Godot/NetworkObject.cs | 45 ++++ .../Godot/{GodotSpawnData.cs => SpawnData.cs} | 4 +- Bindings/Unity/Helper.cs | 57 +++++ Bindings/Unity/MiscBindings.cs | 38 +++ Bindings/Unity/NetworkObject.cs | 43 ++++ Bindings/Unity/SpawnData.cs | 52 ++++ Commands/Command.cs | 46 ++++ Commands/CommandUtility.cs | 67 +++++ Common/HiHiConfiguration.cs | 28 +++ Common/HiHiUtility.cs | 82 ++++-- Core/IHiHiObject/INetworkObject.cs | 57 +++-- Core/ISerializable/PeerInfo.cs | 54 ++-- Core/ISerializable/Quaternion.cs | 11 +- Core/ISerializable/Vectors.cs | 45 +++- Core/PeerNetwork.cs | 59 ++--- Discovery/PeerFinder.cs | 23 +- ...PBroadcastFinder.cs => BroadcastFinder.cs} | 27 +- ...UDPSignalerFinder.cs => SignalerFinder.cs} | 16 +- Images/HiHi_Black_F_Square_128h.png | Bin 2237 -> 0 bytes Images/HiHi_Black_Long_128h.png | Bin 5133 -> 0 bytes Images/HiHi_Black_Square_128h.png | Bin 1891 -> 0 bytes Images/HiHi_Color_F_Square_128h.png | Bin 2964 -> 0 bytes Images/HiHi_Color_Itch_1200w.png | Bin 17988 -> 0 bytes Images/HiHi_Color_Itch_500h.png | Bin 15286 -> 0 bytes Images/HiHi_Color_Long_128h.png | Bin 6387 -> 0 bytes Images/HiHi_Color_Square_128h.png | Bin 2701 -> 0 bytes Images/HiHi_White_F_Square_128h.png | Bin 2369 -> 0 bytes Images/HiHi_White_Long_128h.png | Bin 5548 -> 0 bytes Images/HiHi_White_Square_128h.png | Bin 1962 -> 0 bytes Peer/Peer.cs | 109 ++++---- Peer/PeerMessage.cs | 17 +- Peer/Transports/UDPTransport.cs | 117 --------- README.md | 42 +--- Roadmap.md | 38 +++ Signaling/Signaler.cs | 13 +- Signaling/SignalerConnectionInfo.cs | 1 + Signaling/SignalerLobby.cs | 6 +- .../LiteNetSignaler.cs} | 42 ++-- SyncObjects/Democracy.cs | 181 ++++++++++++++ {Core => SyncObjects}/Question.cs | 115 +++++---- {Core => SyncObjects}/RPC.cs | 0 {Core => SyncObjects}/Sync.cs | 8 + {Core => SyncObjects}/SyncObject.cs | 21 +- {Core => SyncObjects}/SyncPhysicsBody.cs | 0 {Core => SyncObjects}/SyncTransform.cs | 2 +- Transports/LiteNetTransport.cs | 233 ++++++++++++++++++ {Peer => Transports}/PeerTransport.cs | 20 +- 50 files changed, 1318 insertions(+), 451 deletions(-) rename Bindings/{Godot/GodotNetworkObject.cs => General/NetworkObject.cs} (70%) rename Bindings/Godot/{GodotHelper.cs => Helper.cs} (88%) rename Bindings/Godot/{MiscGodotBindings.cs => MiscBindings.cs} (89%) create mode 100644 Bindings/Godot/NetworkObject.cs rename Bindings/Godot/{GodotSpawnData.cs => SpawnData.cs} (94%) create mode 100644 Bindings/Unity/Helper.cs create mode 100644 Bindings/Unity/MiscBindings.cs create mode 100644 Bindings/Unity/NetworkObject.cs create mode 100644 Bindings/Unity/SpawnData.cs create mode 100644 Commands/Command.cs create mode 100644 Commands/CommandUtility.cs rename Discovery/PeerFinders/{UDPBroadcastFinder.cs => BroadcastFinder.cs} (70%) rename Discovery/PeerFinders/{UDPSignalerFinder.cs => SignalerFinder.cs} (87%) delete mode 100644 Images/HiHi_Black_F_Square_128h.png delete mode 100644 Images/HiHi_Black_Long_128h.png delete mode 100644 Images/HiHi_Black_Square_128h.png delete mode 100644 Images/HiHi_Color_F_Square_128h.png delete mode 100644 Images/HiHi_Color_Itch_1200w.png delete mode 100644 Images/HiHi_Color_Itch_500h.png delete mode 100644 Images/HiHi_Color_Long_128h.png delete mode 100644 Images/HiHi_Color_Square_128h.png delete mode 100644 Images/HiHi_White_F_Square_128h.png delete mode 100644 Images/HiHi_White_Long_128h.png delete mode 100644 Images/HiHi_White_Square_128h.png delete mode 100644 Peer/Transports/UDPTransport.cs create mode 100644 Roadmap.md rename Signaling/Signalers/{UDPSignaler/UDPSignaler.cs => LiteNetSignaler/LiteNetSignaler.cs} (83%) create mode 100644 SyncObjects/Democracy.cs rename {Core => SyncObjects}/Question.cs (57%) rename {Core => SyncObjects}/RPC.cs (100%) rename {Core => SyncObjects}/Sync.cs (93%) rename {Core => SyncObjects}/SyncObject.cs (90%) rename {Core => SyncObjects}/SyncPhysicsBody.cs (100%) rename {Core => SyncObjects}/SyncTransform.cs (99%) create mode 100644 Transports/LiteNetTransport.cs rename {Peer => Transports}/PeerTransport.cs (84%) diff --git a/Bindings/Godot/GodotNetworkObject.cs b/Bindings/General/NetworkObject.cs similarity index 70% rename from Bindings/Godot/GodotNetworkObject.cs rename to Bindings/General/NetworkObject.cs index e354286..4b1384e 100644 --- a/Bindings/Godot/GodotNetworkObject.cs +++ b/Bindings/General/NetworkObject.cs @@ -1,7 +1,5 @@ -#if GODOT - -using Godot; -using System; +using System; +using System.Collections.Generic; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -27,7 +25,7 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi { - public abstract partial class GodotNetworkObject : Node, INetworkObject { + public abstract partial class NetworkObject : INetworkObject { #region NetworkObject Implementation Action INetworkObject.OnOwnershipChanged { get; set; } @@ -40,34 +38,33 @@ public abstract partial class GodotNetworkObject : Node, INetworkObject { ushort? INetworkObject.ownerID { get; set; } NetworkObjectAbandonmentPolicy INetworkObject.abandonmentPolicy { get; set; } = HiHiConfiguration.DEFAULT_ABANDONMENT_POLICY; - SyncObject[] INetworkObject.syncObjects { get; set; } - byte INetworkObject.syncObjectCount { get; set; } + Dictionary INetworkObject.syncObjects { get; set; } void INetworkObject.OnRegister() => OnRegister(); void INetworkObject.OnUnregister() => OnUnregister(); void INetworkObject.DestroyLocally() => QueueFree(); - void INetworkObject.Update() => Update(); + void INetworkObject.Update() => UpdateInstance(); #endregion - #region Godot + #region Members + + public INetworkObject Interface => this as INetworkObject; - public INetworkObject NetworkObject => this as INetworkObject; + public Action OnOwnershipChanged { get => Interface.OnOwnershipChanged; set => Interface.OnOwnershipChanged = value; } + public Action OnAbandonmentPolicyChanged { get => Interface.OnAbandonmentPolicyChanged; set => Interface.OnAbandonmentPolicyChanged = value; } + public ISpawnData OriginSpawnData { get => Interface.OriginSpawnData; } - public override void _ExitTree() { - base._ExitTree(); + public bool Registered { get => Interface.Registered; } + public ushort UniqueID { get => Interface.UniqueID; } - if (NetworkObject.Registered) { - NetworkObject.UnRegister(); - } - } + public ushort? OwnerID { get => Interface.OwnerID; } + public NetworkObjectAbandonmentPolicy AbandonmentPolicy { get => Interface.AbandonmentPolicy; set => Interface.AbandonmentPolicy = value; } protected virtual void OnRegister() { } protected virtual void OnUnregister() { } - protected virtual void Update() { } + protected virtual void UpdateInstance() { } #endregion } -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/Bindings/Godot/GodotHelper.cs b/Bindings/Godot/Helper.cs similarity index 88% rename from Bindings/Godot/GodotHelper.cs rename to Bindings/Godot/Helper.cs index d2566ea..55710a9 100644 --- a/Bindings/Godot/GodotHelper.cs +++ b/Bindings/Godot/Helper.cs @@ -29,11 +29,11 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi { - public partial class GodotHelper : Node, IHelper { - public static GodotHelper Instance { get; private set; } + public partial class Helper : Node, IHelper { + public static Helper Instance { get; private set; } [ExportGroup("Spawning")] - [Export] public Array SpawnDataRegistry = new Array(); + [Export] public Array SpawnDataRegistry = new Array(); public override void _EnterTree() { base._EnterTree(); @@ -49,7 +49,7 @@ ISpawnData IHelper.DeserializeSpawnData(BitBuffer buffer) { byte spawnDataIndex = buffer.ReadByte(); if (SpawnDataRegistry.Count <= spawnDataIndex) { - throw new HiHiException($"Received spawn message referencing spawn index {spawnDataIndex}. Which doesn't exist in the {nameof(GodotHelper)}.{nameof(SpawnDataRegistry)}. Make sure your spawndata is the same across peers."); + throw new HiHiException($"Received spawn message referencing spawn index {spawnDataIndex}. Which doesn't exist in the {nameof(Helper)}.{nameof(SpawnDataRegistry)}. Make sure your spawndata is the same across peers."); } return SpawnDataRegistry[spawnDataIndex]; diff --git a/Bindings/Godot/MiscGodotBindings.cs b/Bindings/Godot/MiscBindings.cs similarity index 89% rename from Bindings/Godot/MiscGodotBindings.cs rename to Bindings/Godot/MiscBindings.cs index 9af07dc..5f0b6c8 100644 --- a/Bindings/Godot/MiscGodotBindings.cs +++ b/Bindings/Godot/MiscBindings.cs @@ -29,6 +29,11 @@ public partial struct HiHiVector3 { public static implicit operator HiHiVector3(Godot.Vector3 from) => new HiHiVector3(from.X, from.Y, from.Z); } + public partial struct HiHiVector2 { + public static implicit operator Godot.Vector2(HiHiVector2 from) => new Godot.Vector2(from.X, from.Y); + public static implicit operator HiHiVector2(Godot.Vector2 from) => new HiHiVector2(from.X, from.Y); + } + public partial struct HiHiQuaternion { public static implicit operator Godot.Quaternion(HiHiQuaternion from) => new Godot.Quaternion(from.X, from.Y, from.Z, from.W); public static implicit operator HiHiQuaternion(Godot.Quaternion from) => new HiHiQuaternion(from.X, from.Y, from.Z, from.W); diff --git a/Bindings/Godot/NetworkObject.cs b/Bindings/Godot/NetworkObject.cs new file mode 100644 index 0000000..a68af8e --- /dev/null +++ b/Bindings/Godot/NetworkObject.cs @@ -0,0 +1,45 @@ +#if GODOT + +using Godot; +using System; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + public abstract partial class NetworkObject : Node { + #region Godot + + public override void _ExitTree() { + base._ExitTree(); + + if (Interface.Registered) { + Interface.UnRegister(); + } + } + + #endregion + } +} + +#endif diff --git a/Bindings/Godot/GodotSpawnData.cs b/Bindings/Godot/SpawnData.cs similarity index 94% rename from Bindings/Godot/GodotSpawnData.cs rename to Bindings/Godot/SpawnData.cs index 575b1f9..efdd11a 100644 --- a/Bindings/Godot/GodotSpawnData.cs +++ b/Bindings/Godot/SpawnData.cs @@ -27,12 +27,12 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi { - public partial class GodotSpawnData : Resource, ISpawnData { + public partial class SpawnData : Resource, ISpawnData { public int Index => helper.SpawnDataRegistry.IndexOf(this); [Export] public PackedScene Scene; - private GodotHelper helper => Peer.Helper as GodotHelper; + private Helper helper => Peer.Helper as Helper; void ISpawnData.Serialize(BitBuffer buffer) { buffer.AddByte((byte)Index); diff --git a/Bindings/Unity/Helper.cs b/Bindings/Unity/Helper.cs new file mode 100644 index 0000000..e64cfc1 --- /dev/null +++ b/Bindings/Unity/Helper.cs @@ -0,0 +1,57 @@ +#if UNITY_EDITOR || UNITY_STANDALONE + +using HiHi.Common; +using HiHi.Serialization; +using UnityEngine; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + public class Helper : MonoBehaviour, IHelper { + public static UnityHelper Instance { get; private set; } + + [Header("Spawning")] + [SerializeField] public SpawnData[] SpawnDataRegistry = new SpawnData[0]; + + public virtual void OnEnable() { + Instance = this; + } + + void IHelper.SerializeSpawnData(ISpawnData spawnData, BitBuffer buffer) { + spawnData.Serialize(buffer); + } + + ISpawnData IHelper.DeserializeSpawnData(BitBuffer buffer) { + byte spawnDataIndex = buffer.ReadByte(); + + if (SpawnDataRegistry.Length <= spawnDataIndex) { + throw new HiHiException($"Received spawn message referencing spawn index {spawnDataIndex}. Which doesn't exist in the {nameof(UnityHelper)}.{nameof(SpawnDataRegistry)}. Make sure your spawndata is the same across peers."); + } + + return SpawnDataRegistry[spawnDataIndex]; + } + } +} + +#endif diff --git a/Bindings/Unity/MiscBindings.cs b/Bindings/Unity/MiscBindings.cs new file mode 100644 index 0000000..f3e3ab0 --- /dev/null +++ b/Bindings/Unity/MiscBindings.cs @@ -0,0 +1,38 @@ +#if UNITY_EDITOR || UNITY_STANDALONE + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + public partial struct HiHiVector3 { + public static implicit operator UnityEngine.Vector3(HiHiVector3 from) => new UnityEngine.Vector3(from.X, from.Y, from.Z); + public static implicit operator HiHiVector3(UnityEngine.Vector3 from) => new HiHiVector3(from.x, from.y, from.z); + } + + public partial struct HiHiQuaternion { + public static implicit operator UnityEngine.Quaternion(HiHiQuaternion from) => new UnityEngine.Quaternion(from.X, from.Y, from.Z, from.W); + public static implicit operator HiHiQuaternion(UnityEngine.Quaternion from) => new HiHiQuaternion(from.x, from.y, from.z, from.w); + } +} + +#endif \ No newline at end of file diff --git a/Bindings/Unity/NetworkObject.cs b/Bindings/Unity/NetworkObject.cs new file mode 100644 index 0000000..9cabcb2 --- /dev/null +++ b/Bindings/Unity/NetworkObject.cs @@ -0,0 +1,43 @@ +#if UNITY_EDITOR || UNITY_STANDALONE + +using UnityEngine; +using System; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + public abstract partial class NetworkObject : MonoBehaviour, INetworkObject { + #region Unity + + public virtual void OnDestroy() { + if (Interface.Registered) { + Interface.UnRegister(); + } + } + + #endregion + } +} + +#endif \ No newline at end of file diff --git a/Bindings/Unity/SpawnData.cs b/Bindings/Unity/SpawnData.cs new file mode 100644 index 0000000..4586218 --- /dev/null +++ b/Bindings/Unity/SpawnData.cs @@ -0,0 +1,52 @@ +#if UNITY_EDITOR || UNITY_STANDALONE + +using UnityEngine; +using HiHi.Serialization; +using System; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + [CreateAssetMenu(fileName = "SpawnData", menuName = "HiHi/SpawnData")] + public class SpawnData : ScriptableObject, ISpawnData { + public int Index => Array.IndexOf(helper.SpawnDataRegistry, this); + + [SerializeField] public NetworkObject Prefab; + + private UnityHelper helper => Peer.Helper as UnityHelper; + + void ISpawnData.Serialize(BitBuffer buffer) { + buffer.AddByte((byte)Index); + } + + INetworkObject ISpawnData.Spawn() { + UnityNetworkObject spawnedInstance = Instantiate(Prefab); + spawnedInstance.transform.parent = helper.transform; + + return spawnedInstance as INetworkObject; + } + } +} + +#endif \ No newline at end of file diff --git a/Commands/Command.cs b/Commands/Command.cs new file mode 100644 index 0000000..d7bf5c5 --- /dev/null +++ b/Commands/Command.cs @@ -0,0 +1,46 @@ +using System; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi.Commands { + public class Command { + public string CommandString { get; private set; } + public Func CommandFunc { get; private set; } + + public Command(string commandString, Func commandFunc) { + this.CommandString = commandString; + this.CommandFunc = commandFunc; + } + + public bool TryInvoke(string command, out string result) { + if (!command.Contains(CommandString, StringComparison.OrdinalIgnoreCase)) { + result = string.Empty; + return false; + } + + result = CommandFunc.Invoke(command.Substring(command.IndexOf(CommandString) + CommandString.Length).Trim()); + return true; + } + } +} diff --git a/Commands/CommandUtility.cs b/Commands/CommandUtility.cs new file mode 100644 index 0000000..509d5cc --- /dev/null +++ b/Commands/CommandUtility.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi.Commands { + public static class CommandUtility { + public static bool Initialized => initialized; + + private static List commands = new List(); + private static bool initialized = false; + + public static void Initialize() { + if (initialized) { return; } + + foreach (Command command in HiHiConfiguration.DEFAULT_COMMANDS) { + RegisterCommand(command); + } + + initialized = true; + } + + public static void RegisterCommand(Command command) { + commands.Add(command); + } + + public static void UnregisterCommand(Command command) { + commands.Remove(command); + } + + public static bool TryInvokeCommand(string commandString, out string result) { + if (!initialized) { + result = string.Empty; + return false; + } + + foreach (Command command in commands) { + if (command.TryInvoke(commandString, out result)) { + return true; + } + } + + result = string.Empty; + return false; + } + } +} \ No newline at end of file diff --git a/Common/HiHiConfiguration.cs b/Common/HiHiConfiguration.cs index f60fe77..87b4498 100644 --- a/Common/HiHiConfiguration.cs +++ b/Common/HiHiConfiguration.cs @@ -1,7 +1,12 @@ +using HiHi.Commands; +using System.Net; + namespace HiHi { public static class HiHiConfiguration { // Peer public const int BROADCAST_RECEIVE_PORT = 9050; + public const int DEFAULT_PUBLIC_PORT = 8050; + public static readonly IPEndPoint BROADCAST_RECEIVE_ENDPOINT = new IPEndPoint(IPAddress.Broadcast, BROADCAST_RECEIVE_PORT); public static int HEARTBEAT_SEND_INTERVAL_MS { get; set; } = 1000; public static int HEARTBEAT_TIMEOUT_INTERVAL_MS { get; set; } = 5000; @@ -11,9 +16,32 @@ public static class HiHiConfiguration { // NetworkObject public static NetworkObjectAbandonmentPolicy DEFAULT_ABANDONMENT_POLICY = NetworkObjectAbandonmentPolicy.RemainOwnedRandomly; + // Commands + public static bool COMMANDS_ENABLED = true; + public static Command[] DEFAULT_COMMANDS = new Command[] { + new Command("/info", (address) => { + Peer.Connect(address); + string infoString = $"Local Peer ID: {Peer.Info.UniqueID} Local EndPoint: {Peer.Info.LocalEndPoint} Remote EndPoint: {Peer.Info.RemoteEndPoint}\n\nConnected to {PeerNetwork.RemotePeerCount} peers:\n"; + + foreach(ushort peerID in PeerNetwork.RemotePeerIDs) { + PeerInfo peerInfo = PeerNetwork.GetPeerInfo(peerID); + + infoString += $"{peerInfo}\n"; + } + + return infoString; + }), + + new Command("/connect", (address) => { + Peer.Connect(address); + return $"Sent connect message to {address}"; + }) + }; + // Signaler public static int SIGNALER_DEFAULT_PORT { get; set; } = 28910; public static int SIGNALER_HEARTBEAT_SEND_INTERVAL_MS { get; set; } = 5000; public static int SIGNALER_HEARTBEAT_TIMEOUT_INTERVAL_MS { get; set; } = 25000; + public static int SIGNALER_DEFAULT_LOBBY_SIZE { get; set; } = 16; } } \ No newline at end of file diff --git a/Common/HiHiUtility.cs b/Common/HiHiUtility.cs index b00dfe7..2f36b86 100644 --- a/Common/HiHiUtility.cs +++ b/Common/HiHiUtility.cs @@ -2,7 +2,7 @@ using System.Net; using System.Net.Sockets; using System.Linq; -using Godot.Collections; +using System.Collections.Generic; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -32,6 +32,7 @@ public static class HiHiUtility { public const string END_POINT_STRING_TEMPLATE = "{0}:{1}"; private static Dictionary HostNameCache = new Dictionary(); + private static Dictionary EndPointCache = new Dictionary(); public static int GetFreePort(int preferredPort = 0) { return CheckUDPPortAvailable(preferredPort) @@ -65,21 +66,36 @@ public static bool CheckTCPPortAvailable(int port) { return true; } - public static bool TryGetLocalAddressString(out string ip) { + public static bool TryGetLocalAddress(out IPAddress iPaddress) { IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName()); foreach (IPAddress address in host.AddressList) { if (address.AddressFamily == AddressFamily.InterNetwork) { - ip = address.ToString(); + iPaddress = address; return true; } } - ip = ""; + iPaddress = null; return false; } - public static string GetLocalAddressString() { - if(!TryGetLocalAddressString(out string ip)) { + public static IPAddress GetLocalAddress() { + if (!TryGetLocalAddress(out IPAddress ip)) { + throw new HiHiException("Failed to get local IP address."); + } + + return ip; + } + + public static bool TryGetLocalAddressString(out string iPaddressString) { + bool result = TryGetLocalAddress(out IPAddress address); + iPaddressString = address?.ToString(); + + return result; + } + + public static string GetLocalAddressString() { + if (!TryGetLocalAddressString(out string ip)) { throw new HiHiException("Failed to get local IP address."); } @@ -87,11 +103,19 @@ public static string GetLocalAddressString() { } public static string ToEndPointString(this IPEndPoint endpoint) { - return ToEndPointString(endpoint.Address.ToString(), endpoint.Port); + return ToEndPointString(IPAddressToString(endpoint.Address), endpoint.Port); } public static string ToEndPointString(object address, object port) => string.Format(END_POINT_STRING_TEMPLATE, address, port); + public static string IPAddressToString(IPAddress iPAddress) { + if (iPAddress.Equals(IPAddress.Any)) { + iPAddress = GetLocalAddress(); + } + + return iPAddress.ToString(); + } + public static bool TryParseHostName(string hostName, out IPAddress[] addresses) { if (!HostNameCache.ContainsKey(hostName)) { HostNameCache.Add(hostName, Dns.GetHostAddresses(hostName).Select(a => a.ToString()).ToArray()); @@ -102,21 +126,47 @@ public static bool TryParseHostName(string hostName, out IPAddress[] addresses) return addresses.Length > 0; } - public static IPEndPoint ParseStringToIPEndpoint(string endPointString) { - IPEndPoint endPoint; - if (IPEndPoint.TryParse(endPointString, out endPoint)) { + public static IPEndPoint ParseStringToIPEndPoint(string endPointString) { + if (TryParseStringToIPEndPoint(endPointString, out IPEndPoint endPoint)) { return endPoint; } - string hostname = endPointString.Substring(0, endPointString.LastIndexOf(':')); - string port = endPointString.Substring(endPointString.LastIndexOf(':') + 1); + throw new HiHiException($"Couldn't parse string: {endPointString}"); + } + + public static bool TryParseStringToIPEndPoint(string endPointString, out IPEndPoint endPoint) { + if (EndPointCache.ContainsKey(endPointString)) { + endPoint = EndPointCache[endPointString]; + return true; + } + + try { + IPAddress iPAddress; - if (TryParseHostName(hostname, out IPAddress[] addresses)) { - IPAddress address = addresses.OrderBy(a => a.AddressFamily).FirstOrDefault(); - return new IPEndPoint(address, int.Parse(port)); + string address = endPointString.Substring(0, endPointString.LastIndexOf(':')); + int port = int.Parse(endPointString.Substring(endPointString.LastIndexOf(':') + 1)); + + if (IPAddress.TryParse(address, out iPAddress)) { + if (iPAddress.Equals(IPAddress.Any)) { + iPAddress = GetLocalAddress(); + } + + endPoint = new IPEndPoint(iPAddress, port); + EndPointCache.Add(endPointString, endPoint); + return true; + } + + if (TryParseHostName(address, out IPAddress[] addresses)) { + iPAddress = addresses.OrderBy(a => a.AddressFamily).FirstOrDefault(); + endPoint = new IPEndPoint(iPAddress, port); + EndPointCache.Add(endPointString, endPoint); + return true; + } } + catch { } - throw new HiHiException($"Couldn't parse string: {endPointString}"); + endPoint = null; + return false; } } } diff --git a/Core/IHiHiObject/INetworkObject.cs b/Core/IHiHiObject/INetworkObject.cs index 2dff2ff..1e0c644 100644 --- a/Core/IHiHiObject/INetworkObject.cs +++ b/Core/IHiHiObject/INetworkObject.cs @@ -1,4 +1,5 @@ using HiHi.Common; +using HiHi.Serialization; using System; using System.Collections.Generic; using System.Linq; @@ -68,20 +69,21 @@ public NetworkObjectAbandonmentPolicy AbandonmentPolicy { SendAbandonmentPolicyChange(UniqueID, AbandonmentPolicy); } } - public SyncObject[] SyncObjects { + + protected Dictionary SyncObjects { get { - syncObjects ??= new SyncObject[byte.MaxValue + 1]; + syncObjects ??= new Dictionary(); return syncObjects; } } - protected ushort? ownerID { get; set; } protected NetworkObjectAbandonmentPolicy abandonmentPolicy { get; set; } - protected SyncObject[] syncObjects { get; set; } - protected byte syncObjectCount { get; set; } + protected Dictionary syncObjects { get; set; } + protected static Queue availableSyncObjectIDs { get; set; } + protected static Random random; static INetworkObject() { - Random random = new Random(); + random = new Random(); for(ushort i = 0; i < ushort.MaxValue; i++) { availableIDs.Enqueue(i); @@ -97,16 +99,25 @@ public static void UpdateInstances() { } } + public static bool IsIDAvailable(ushort ID) { + return !Instances.ContainsKey(ID); + } + public ushort Register(ushort? proposedUniqueID = null, ushort? ownerID = null, ISpawnData originSpawnData = null) { if (Registered) { return UniqueID; } UniqueID = proposedUniqueID ?? UniqueID; OwnerID = ownerID; OriginSpawnData = originSpawnData; - if (Instances.ContainsKey(UniqueID)) { + if (!IsIDAvailable(UniqueID)) { UniqueID = availableIDs.Dequeue(); } + availableSyncObjectIDs = new Queue(); + for (byte i = 0; i < byte.MaxValue; i++) { + availableSyncObjectIDs.Enqueue(i); + } + Instances.Add(UniqueID, this); Registered = true; @@ -118,6 +129,10 @@ public ushort Register(ushort? proposedUniqueID = null, ushort? ownerID = null, public void UnRegister() { if (!Registered) { return; } + while(SyncObjects.Count > 0) { + UnregisterSyncObject(SyncObjects[syncObjects.Keys.FirstOrDefault()]); + } + availableIDs.Enqueue(UniqueID); Instances.Remove(UniqueID); @@ -128,13 +143,13 @@ public void UnRegister() { #region Spawning - public static INetworkObject SyncSpawn(ISpawnData spawnData, ushort? ownerID = null) { + public static T SyncSpawn(ISpawnData spawnData, ushort? ownerID = null) where T : class, INetworkObject { INetworkObject spawnedObject = spawnData.Spawn(); spawnedObject.Register(null, ownerID, spawnData); SendSpawn(spawnData, spawnedObject.UniqueID, ownerID); - return spawnedObject; + return spawnedObject as T; } public static void SendSpawn(ISpawnData spawnData, ushort uniqueID, ushort? ownerID) { @@ -226,7 +241,7 @@ public static void ReceiveOwnershipChange(PeerMessage message) { public void Claim() => GiveToPeer(Peer.Info.UniqueID); public void Forfeit() => GiveToPeer(null); - public void GiveToRandomPeer() => GiveToPeer(Peer.Network.GetRandomPeerID()); + public void GiveToRandomPeer() => GiveToPeer(PeerNetwork.GetElectedPeer(random.Next(int.MaxValue))); public void GiveToPeer(ushort? peerID) => GiveToPeer(peerID, false); protected void GiveToPeer(ushort? peerID, bool forceLocally = false) { @@ -305,10 +320,7 @@ protected void HandleAbandonment() { case NetworkObjectAbandonmentPolicy.RemainOwnedRandomly: if (!Owned) { break; } - IEnumerable candidates = Peer.Network.PeerIDs.Concat(new ushort[1] { Peer.Info.UniqueID }).OrderBy(p => p); - ushort pickedID = candidates.Skip(UniqueID % candidates.Count()).First(); - - GiveToPeer(pickedID, true); + GiveToPeer(PeerNetwork.GetElectedPeer(UniqueID), true); break; case NetworkObjectAbandonmentPolicy.BecomeShared: @@ -341,16 +353,23 @@ public static void ReceiveSyncObjectData(PeerMessage message) { syncObject.Deserialize(message.SenderPeerID, message.Buffer); } - public byte RegisterSyncObject(SyncObject syncObject) { - byte uniqueID = syncObjectCount++; + public void RegisterSyncObject(SyncObject syncObject) { + byte uniqueID = availableSyncObjectIDs.Dequeue(); SyncObjects[uniqueID] = syncObject; - return uniqueID; + syncObject.OnRegister(uniqueID); + } + + public void UnregisterSyncObject(SyncObject syncObject) { + SyncObjects.Remove(syncObject.UniqueID); + availableSyncObjectIDs.Enqueue(syncObject.UniqueID); + + syncObject.OnUnregister(); } public void UpdateSyncObjects() { - for (int s = 0; s < syncObjectCount; s++) { - SyncObjects[s]?.Update(); + foreach (KeyValuePair syncObjectPair in SyncObjects) { + syncObjectPair.Value.Update(); } } diff --git a/Core/ISerializable/PeerInfo.cs b/Core/ISerializable/PeerInfo.cs index f45d821..3503a82 100644 --- a/Core/ISerializable/PeerInfo.cs +++ b/Core/ISerializable/PeerInfo.cs @@ -1,5 +1,6 @@ using System; using System.Drawing; +using HiHi.Common; using HiHi.Serialization; /* @@ -51,15 +52,16 @@ public ushort UniqueID { } public ushort SelfAssignedID { get; private set; } public string ConnectionKey { get; set; } - public string EndPoint { - get { - return endPoint; - } - set { - endPoint = value; - } + public string RemoteEndPoint { + get => remoteEndPoint; + set => remoteEndPoint = value; } - public Color Color { + public string LocalEndPoint { + get => localEndPoint; + set => localEndPoint = value; + } + public double Hue => UniqueID / (double)ushort.MaxValue; + public Color Color { get { if(color == null) { double hue = UniqueID / (double)ushort.MaxValue * 360d; @@ -94,17 +96,25 @@ public Color Color { } public string ColorCode => $"#{Color.R:X2}{Color.G:X2}{Color.B:X2}"; - private string endPoint; + private string remoteEndPoint = string.Empty; + private string localEndPoint = string.Empty; private ushort? uniqueID; private Color? color; public PeerInfo() { - SelfAssignedID = (ushort)random.Next(ushort.MaxValue); - ConnectionKey = Peer.ConnectionKey; - - RegisterHeartbeat(); + RegisterHeartbeat(); } + public static PeerInfo CreateLocal() { + PeerInfo peerInfo = new PeerInfo(); + + peerInfo.SelfAssignedID = (ushort)random.Next(ushort.MaxValue); + peerInfo.ConnectionKey = Peer.ConnectionKey; + peerInfo.LocalEndPoint = Peer.Transport.LocalEndPoint; + + return peerInfo; + } + public void RegisterHeartbeat() { HeartbeatTick = Environment.TickCount; } @@ -117,29 +127,35 @@ public void SetPing(float ping) { Ping = ping; } - public void Verify(ushort uniqueID, string endpoint) { + public void Verify(ushort uniqueID, string remoteEndpoint) { this.UniqueID = uniqueID; - this.EndPoint = endpoint; + this.RemoteEndPoint = remoteEndpoint; this.Verified = true; } - void ISerializable.Serialize(BitBuffer buffer) { + public void RerollSelfAssignedID() { + SelfAssignedID = (ushort)random.Next(ushort.MaxValue); + } + + void ISerializable.Serialize(BitBuffer buffer) { buffer.AddUShort(UniqueID); buffer.AddString(ConnectionKey); - buffer.AddString(EndPoint); + buffer.AddString(RemoteEndPoint); + buffer.AddString(LocalEndPoint); buffer.AddBool(Verified); } void ISerializable.Deserialize(BitBuffer buffer) { UniqueID = buffer.ReadUShort(); ConnectionKey = buffer.ReadString(); - EndPoint = buffer.ReadString(); + RemoteEndPoint = buffer.ReadString(); + LocalEndPoint = buffer.ReadString(); Verified = buffer.ReadBool(); } public override string ToString() { - return $"{nameof(UniqueID)} = {UniqueID}, {nameof(ConnectionKey)} = {ConnectionKey}, {nameof(EndPoint)} = {EndPoint}, {nameof(Verified)} = {Verified}"; + return $"{nameof(UniqueID)} = {UniqueID}, {nameof(ConnectionKey)} = {ConnectionKey}, {nameof(RemoteEndPoint)} = {RemoteEndPoint}, {nameof(LocalEndPoint)} = {LocalEndPoint}, {nameof(Verified)} = {Verified}"; } } } diff --git a/Core/ISerializable/Quaternion.cs b/Core/ISerializable/Quaternion.cs index 5b44dc9..618f292 100644 --- a/Core/ISerializable/Quaternion.cs +++ b/Core/ISerializable/Quaternion.cs @@ -25,12 +25,13 @@ */ namespace HiHi { public partial struct HiHiQuaternion : ISerializable { - public float X = 0f; - public float Y = 0f; - public float Z = 0f; - public float W = 1f; + public float X; + public float Y; + public float Z; + public float W; - public HiHiQuaternion() { } + public HiHiQuaternion Identity => new HiHiQuaternion(0f, 0f, 0f, 1f); + public HiHiQuaternion(float X, float Y, float Z, float W) { this.X = X; this.Y = Y; diff --git a/Core/ISerializable/Vectors.cs b/Core/ISerializable/Vectors.cs index 7b6986d..82daab5 100644 --- a/Core/ISerializable/Vectors.cs +++ b/Core/ISerializable/Vectors.cs @@ -24,11 +24,31 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi { + public struct HiHiFloat : ISerializable { + public float Value; + + public HiHiFloat(float Value) { + this.Value = Value; + } + + void ISerializable.Serialize(BitBuffer buffer) { + buffer.AddUShort(HalfPrecision.Quantize(Value)); + } + + void ISerializable.Deserialize(BitBuffer buffer) { + Value = HalfPrecision.Dequantize(buffer.ReadUShort()); + } + + public static implicit operator float(HiHiFloat from) => from.Value; + public static implicit operator HiHiFloat(float from) => new HiHiFloat(from); + } + public partial struct HiHiVector2 : ISerializable { - public float X = 0f; - public float Y = 0f; + public float X; + public float Y; + + public HiHiVector2 Zero => new HiHiVector2(0f, 0f); - public HiHiVector2() { } public HiHiVector2(float X, float Y) { this.X = X; this.Y = Y; @@ -46,11 +66,12 @@ void ISerializable.Deserialize(BitBuffer buffer) { } public partial struct HiHiVector3 : ISerializable { - public float X = 0f; - public float Y = 0f; - public float Z = 0f; + public float X; + public float Y; + public float Z; + + public HiHiVector3 Zero => new HiHiVector3(0f, 0f, 0f); - public HiHiVector3() { } public HiHiVector3(float X, float Y, float Z) { this.X = X; this.Y = Y; @@ -71,12 +92,12 @@ void ISerializable.Deserialize(BitBuffer buffer) { } public partial struct HiHiVector4 : ISerializable { - public float X = 0f; - public float Y = 0f; - public float Z = 0f; - public float W = 0f; + public float X; + public float Y; + public float Z; + public float W; - public HiHiVector4() { } + public HiHiVector4 Zero => new HiHiVector4(0f, 0f, 0f, 0f); public HiHiVector4(float X, float Y, float Z, float W) { this.X = X; this.Y = Y; diff --git a/Core/PeerNetwork.cs b/Core/PeerNetwork.cs index e76f9af..a3ce1d2 100644 --- a/Core/PeerNetwork.cs +++ b/Core/PeerNetwork.cs @@ -29,49 +29,47 @@ */ namespace HiHi { public class PeerNetwork { - public ICollection PeerIDs => connections.Keys; - public int Connections => connections.Count; - public bool Connected => connections.Count > 0; + public static IEnumerable PeerIDs => connections.Keys.Append(Peer.Info.UniqueID); + public static int PeerCount => RemotePeerCount + 1; + public static ICollection RemotePeerIDs => connections.Keys; + public static int RemotePeerCount => connections.Count; + public static bool Connected => connections.Count > 0; + public static uint Hash => (uint)PeerIDs + .Sum(c => c); - private ConcurrentDictionary connections { get; set; } - private Random syncedRandom; + private static ConcurrentDictionary connections { get; set; } - public PeerNetwork() { + static PeerNetwork() { connections = new ConcurrentDictionary(); - - ResetSyncedRandom(); } - public PeerInfo this[ushort ID] => ID == Peer.Info.UniqueID - ? Peer.Info + public static PeerInfo GetPeerInfo(ushort ID) => ID == Peer.Info.UniqueID + ? Peer.Info : connections[ID]; - public bool TryAddConnection(PeerInfo info) { - if (info.UniqueID == Peer.Info.UniqueID) { return false; } - if (connections.ContainsKey(info.UniqueID)) { return false; } + public static bool TryAddConnection(PeerInfo info) { + if (Contains(info.UniqueID)) { return false; } connections.AddOrUpdate(info.UniqueID, info, (id, p) => info); - ResetSyncedRandom(); return true; } - public bool TryRemoveConnection(ushort peerID) { - if (!connections.ContainsKey(peerID)) { return false; } + public static bool TryRemoveConnection(ushort peerID) { + if (!Contains(peerID)) { return false; } connections.Remove(peerID, out _); - ResetSyncedRandom(); return true; } - public bool Contains(ushort ID) { + public static bool Contains(ushort ID) { return ID == Peer.Info.UniqueID ? true : connections.ContainsKey(ID); } - public bool TryGetIDFromEndpoint(string endpoint, out ushort id) { + public static bool TryGetIDFromEndPointString(string endpoint, out ushort id) { foreach(KeyValuePair connection in connections) { - if (string.Equals(connection.Value.EndPoint, endpoint, StringComparison.OrdinalIgnoreCase)) { + if (string.Equals(connection.Value.RemoteEndPoint, endpoint, StringComparison.OrdinalIgnoreCase)) { id = connection.Key; return true; } @@ -81,30 +79,21 @@ public bool TryGetIDFromEndpoint(string endpoint, out ushort id) { return false; } - public ushort GetSyncedRandomUShort() { - return (ushort)syncedRandom.Next(ushort.MaxValue); - } - - public ushort GetRandomPeerID() { - IEnumerable peers = Peer.Network.PeerIDs.Append(Peer.Info.UniqueID); - return peers.Skip(Peer.Network.GetSyncedRandomUShort() % peers.Count()).First(); - } - - private void ResetSyncedRandom() { - syncedRandom = new Random(connections.Sum(c => c.Key)); + public static ushort GetElectedPeer(int? sharedNumber = null) { + return PeerIDs.Skip((sharedNumber ?? (int)Hash) % PeerIDs.Count()).First(); } #region Serialization - public void SerializeConnections(BitBuffer buffer) { - buffer.AddUShort((ushort)PeerIDs.Count); + public static void SerializeConnections(BitBuffer buffer) { + buffer.AddUShort((ushort)RemotePeerIDs.Count); - foreach(ushort peerID in PeerIDs) { + foreach(ushort peerID in RemotePeerIDs) { connections[peerID].Serialize(buffer); } } - public PeerInfo[] DeserializeConnections(BitBuffer buffer) { + public static PeerInfo[] DeserializeConnections(BitBuffer buffer) { ushort connectionCount = buffer.ReadUShort(); PeerInfo[] connections = new PeerInfo[connectionCount]; diff --git a/Discovery/PeerFinder.cs b/Discovery/PeerFinder.cs index c6e49f3..f7dcdce 100644 --- a/Discovery/PeerFinder.cs +++ b/Discovery/PeerFinder.cs @@ -1,5 +1,6 @@ using HiHi.Common; using System.Threading; +using System.Threading.Tasks; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -28,26 +29,30 @@ namespace HiHi.Discovery { public abstract class PeerFinder { public bool Running { get; private set; } - protected virtual int FindRoutineIntervalMS => 5000; + protected virtual int FindRoutineIntervalMS => 1000; - protected Thread thread; + protected Task task; protected ThreadTimer threadTimer; public PeerFinder() { - thread = new Thread(() => FindRoutine()); threadTimer = new ThreadTimer(FindRoutineIntervalMS); } public virtual void Start() { + if (Running) { return; } + Running = true; - thread.Start(); - } - public virtual void Stop() { - Running = false; - } + task = Task.Run(() => FindRoutine()); + } + + public virtual void Stop() { + if (!Running) { return; } + + Running = false; + } - public virtual void Find() { } + public virtual void Find() { } protected virtual void FindRoutine() { while (Running) { diff --git a/Discovery/PeerFinders/UDPBroadcastFinder.cs b/Discovery/PeerFinders/BroadcastFinder.cs similarity index 70% rename from Discovery/PeerFinders/UDPBroadcastFinder.cs rename to Discovery/PeerFinders/BroadcastFinder.cs index 29d37ba..7c5d93e 100644 --- a/Discovery/PeerFinders/UDPBroadcastFinder.cs +++ b/Discovery/PeerFinders/BroadcastFinder.cs @@ -1,6 +1,4 @@ using HiHi.Serialization; -using System.Net; -using System.Net.Sockets; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -26,24 +24,12 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi.Discovery { - public class UDPBroadcastFinder : PeerFinder { - public const int BROADCAST_RECEIVE_PORT = HiHiConfiguration.BROADCAST_RECEIVE_PORT; - - private static IPEndPoint BROADCAST_RECEIVE_ENDPOINT = new IPEndPoint(IPAddress.Broadcast, BROADCAST_RECEIVE_PORT); - - private UdpClient broadcastClient; - private byte[] peerInfoDatagram; - private int peerInfoDatagramLength; - - public UDPBroadcastFinder() : base() { - broadcastClient = new UdpClient(); - } + public class BroadcastFinder : PeerFinder { + public BroadcastFinder() : base() { } public override void Start() { - peerInfoDatagram = new byte[Peer.Transport.MaxPacketSize]; Peer.Transport.ReceiveBroadcast = true; - - Peer.Info.Verify(Peer.Info.UniqueID, Peer.Transport.LocalEndPoint); + Peer.Info.Verify(Peer.Info.SelfAssignedID, string.IsNullOrEmpty(Peer.Info.RemoteEndPoint) ? Peer.Transport.LocalEndPoint : Peer.Info.RemoteEndPoint); base.Start(); } @@ -52,18 +38,17 @@ public override void Stop() { base.Stop(); Peer.Transport.ReceiveBroadcast = false; + Peer.Info.RemoteEndPoint = string.Empty; } public override void Find() { if (!Peer.Info.Verified) { return; } - if(Peer.Connected) { return; } + if (Peer.Connected) { return; } PeerMessage message = Peer.NewMessage(PeerMessageType.Connect); Peer.Info.Serialize(message.Buffer); - peerInfoDatagramLength = message.Buffer.ToArray(peerInfoDatagram); - - broadcastClient.Send(peerInfoDatagram, peerInfoDatagramLength, BROADCAST_RECEIVE_ENDPOINT); + Peer.Transport.SendBroadcast(message); } } } diff --git a/Discovery/PeerFinders/UDPSignalerFinder.cs b/Discovery/PeerFinders/SignalerFinder.cs similarity index 87% rename from Discovery/PeerFinders/UDPSignalerFinder.cs rename to Discovery/PeerFinders/SignalerFinder.cs index 6081c7c..ab6e78d 100644 --- a/Discovery/PeerFinders/UDPSignalerFinder.cs +++ b/Discovery/PeerFinders/SignalerFinder.cs @@ -1,5 +1,6 @@ using HiHi.Common; using HiHi.Serialization; +using HiHi.Signaling; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -25,23 +26,24 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi.Discovery { - public class UDPSignalerFinder : PeerFinder { + public class SignalerFinder : PeerFinder { + public string Address { get; set; } public int Port { get; set; } public string EndPoint => HiHiUtility.ToEndPointString(Address, Port); + public int LobbySize { get; set; } protected override int FindRoutineIntervalMS => HiHiConfiguration.SIGNALER_HEARTBEAT_SEND_INTERVAL_MS; - public UDPSignalerFinder(string address, int port) : base() { + public SignalerFinder(string address, int port, int? lobbySize = null) : base() { this.Address = address; this.Port = port; + this.LobbySize = lobbySize ?? HiHiConfiguration.SIGNALER_DEFAULT_LOBBY_SIZE; } public override void Start() { if (Running) { return; } - Peer.Transport.ReceiveBroadcast = true; - base.Start(); } @@ -49,7 +51,6 @@ public override void Stop() { if (!Running) { return; } SendDisconnect(); - Peer.Transport.ReceiveBroadcast = false; base.Stop(); } @@ -68,15 +69,18 @@ public override void Find() { SendHeartBeat(); } - private void SendVerificationRequest() { + public void SendVerificationRequest() { PeerMessage message = PeerMessage.Borrow(PeerMessageType.VerifiedPeerInfoRequest, default, EndPoint); Peer.Info.Serialize(message.Buffer); + message.Buffer.AddInt(LobbySize); Peer.Transport.Send(message); } private void SendRemotePeerInfoRequest() { PeerMessage message = PeerMessage.Borrow(PeerMessageType.RemotePeerInfoRequest, default, EndPoint); + Peer.Info.Serialize(message.Buffer); + message.Buffer.AddInt(LobbySize); Peer.Transport.Send(message); } diff --git a/Images/HiHi_Black_F_Square_128h.png b/Images/HiHi_Black_F_Square_128h.png deleted file mode 100644 index 5622872f342ccc965f3a38f76591bc6ba4fac7e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2237 zcma)8c|6mNAOFtPGUO=aaVB!jibcz|-1l-u^Hg}s&E&o{EKHaVR#UE`%Y#MEBF}^z zkEt9v@)+jITxHs@NB{j^ujilNAMf|)^*R20f8L)D(eb>MsIZ(c005#k*5=Op5&sKd zp?#*Iy?gh=!Ryx82mpXc{Q?M}Je1upf+C!)Oo6Ju?(@kR5M9002AN zn47r9f>s`3!e<(fbnz&xE2Qw$_HVDO)q4)gC8b8YW3+1ZBlr^Bu#||@dIx2;Za&4; z=tN$4YBXBdD;NQ^E_fJ4#q35E25Q-Pjq&q(e`t589OF^LlXEN9MrKfaz0PLAMqbKxCda~W zx@G9B-wrm@5Yth+Ee^w3=tV;4BAD{F>PxtudH?}d`~hjTy%h0G+xYa!LsR990j-#{ zNS~r~q@U9^!c9=aQkx6cN)r}D03O&l2Ib|1Por-BrplT1DFXa?Pa-MuCe(E7@QYn) zMNmvQ|Etc3Z@I2JL2%5QZBL;aZcR#B?gBS*o1HH_hH0}y=5UzgJETK<*v0}2b~dfL z3`w|Ha^C)uo-)~=a%t##lr+ghJCyEUmRMx59$tCqxFMqV^Xv+Z-B1xMHmkf~=WxpE zlg_feirFXVDAS{E{PEF+M;WV2!R|9uG#Zdo`G~XJ-5>PlaZ2an(<`vH!zjdrQbRft61 zzQ9^lPE(Pqf9}ajGvFpoV#-9Y_vR(sTKHBJ<-5Hi`+<5BL!2Hwe=)mvP)9WA{vC>> z*4Pn$#JqAEH9)8a(xL+*D~x;_14j5H6VoRLZ8;#?x?W=E)vT!!7B5sPpkR74r>^m@RLA=OPFaHBsj~MW`T%KYsN14@MqOBnmyma4D*!}LYqnyKACu#ZXxPKu zJqR)WFdBKfId|`}^=3$;uipW&|4Oj?lW*$odcKim?SO6q=au19l$&w3O$W#7&!}sw zdwj2l9Qu&6nhD$oO(TVQ=lkBHyEs_dnM21J&xZEJg?;jkOho#su%;Z(6>gJEn_-U| zk1o~Tq4+s9t>z`SIz^=as7>MQ>PSqL)(=yM-wqd_H7to6Pxx}zED&~IHODaSqA!s< z?iU9A*+=C(99E8!Lrqy#77@S4nqKo){8)HBgDLn^BPZRi9IJZ%G|cW1b8v0@OjB}v zr4k>tkBT#3*<-`+lFd{dsqbQ4q?4GMZpCJ+QN*ozyQ9^)l1WZj2F0I@tVp>4wq_TPAAb)q;L}HX$iD{c@Qim?&*io^@(oa;2-omsyJe2v(w?TZxXi@+iz*lJ4J zn3OVU?z2XWeNOFEM{-^Rn(`F4X)q}>Ni03Kj$Jx7e^#pE0D6ypr?V7sxEaz6s2xc> zd@1Eens$RqUujYelU}EsG-#&zTv(Tm{ytVTFmOlmQ1!}Jgrs=H)dLBqh&!FerJx%YCfQh~*Si<(EXh6LL;1y*t z5Y^EuHP_%n9MFglX7O!^#$90$RVdfU=nMReeDp0EC-i51zdV67TGfUYUFXUR^yd}5 zpS=j<>2#o`R0piO#0yPQFH1tBts`z_pqk&>{Zu;U@IM9|J#$o18mq}wT~eFld2J6F zlg7knWER+I!{>~)u)+m0%c-8pDJ?-c6S$J@`i1<11F3DsS3P0NtP|=OGUhFKAZYV) zL!p8jTl{OuJWnXUjWc#t4^j0sHv{a0&~)5m|FKcMO4eR)<#6hnx0hEt(i6j z@P5I-cG){YaA=n#s*ZPMb6ksuHhn;D+nbb!v818-urqIBsTzJ(=vfMENmR$;M ztb13P~l2yfPUa`2tY=4B5&hTnu#x*g0g z0gYyY--WHo$X)>|EoBzO@mzJT7|p4m_^JT%yX-%MC!1R3hBbVg`{ga)IYRO98Hl_X zRU!DLd`788@2#R4JUZ?z4J>O5d_K-`>|1kL+v$p!c$=sD!CQ$nBC_|Y1MDT^)wn8D zgsOe|Ai5;c{lcE6Wb;1!D9)O^S@L4%Msl&Yxd;NHas3R= zXhUg&a*BpN zdRm=$cH6I>gWJu20$f{bNGLpttA1D^?9uwT+DH|0>|?Nt*oZ#qt7|QP3{2#|mb!oG dk(PT0+U^{T)K)8z+5a{G8;kSiRi<94{{jW)BbERF diff --git a/Images/HiHi_Black_Long_128h.png b/Images/HiHi_Black_Long_128h.png deleted file mode 100644 index 683c9ee95d9d8f846d20378701e945aab5f93d6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5133 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 Z^utulBDhKh|G9SneH~-%1}&$!{{yJly1@Vd diff --git a/Images/HiHi_Black_Square_128h.png b/Images/HiHi_Black_Square_128h.png deleted file mode 100644 index 1dfa40665342f09139057a26b3569a2807548192..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1891 zcma)7dpy$%AO3A)S{sE?r*Qc0qTz%bLjF5!h8HUorr|m45&P{x;qb*R~uPNLaWJowS901hb zfNv6I0RSpFWsmcug68kcM}08gmm;8T-oWm%!VSZc-1m~eb;_f6e(jTq*3GhFe1_e3 zFS!Ut+ibYZ%KRld7!I=qoP5iU6fBSxGJ_}Pz^h;xF%FWv*LK&z|C_RyHOE)4&1j+b z+9ai`tGD--g3=2E5$&8fbAQoTcmKF(3_5}dYaqWB(RPaqL}Tx}-%*9EvEyw%ac5_+ zE|cO^31+x?qxHq|9ZX(LcfZ@%_ZZ#dhl0*?DcB27G+Dar9<&8{x94qSTa~?jlUJq} z-ZcceaXlbbT!E9Ya=lZF-FwTIH#EnJp{DC8H9fQ`mHr5=WWQ41ircz;1HB=miL#4| zF&PXw#xs1$eyjUtTray+*hA+QyP;pxXJXWzhiR1s;=PM3G$aGH5f&`n7*$$M7I$&U z2N^!RoZN=Ws-ccV1q-M1wbVLoMDlr_P*Sc(qDZuoU={CdWCyp4Z~|!1dZKy_=~C(0Nmp zyBP*C1CD%OWJZqg3Q}O8JOhPjv=3MmwP9!hV++R@iVULi(pE=U1=NreB_2{9J8V2k z*!f)W+VAoijXpc=2pGmr^W0!Ts=!>)k-w@n&uk2c78R(7{B*u5L5BVbn$$bIidw9v znr7W(49j^M^*7ghc7!6|u(m#mj1R}&`+ds9qHuyzS}+y#wNjy z?|X3G^?m1ZDQ!Ck{(i>psj7VWb-Ss}fAt#gf6R`QhgR%?P;@a`GRQtt&=M$@;1WFFvo5GcM@{39H7;l`9_PTItKa7l42Jb5U(6 z0t6K*Hbl`@)66I)IU2#$SwAN`4|tU5lNl1flX~9aHPP&?SJDcHoMTWe87(`;v`mV4 z(|%3=$XW_oelvZ>8`rmnPGRbS; zJy15XmScdz-qUe?ViERrK&JooJE@?rn-vI46cLL~1x}aO&SoVtqPd6_61HWyyOWPi z4-|tDgp?`*+zf8GN@o6&q3o6Kpf%T7Ug4$U^iW^2FW4&@we3PF7|LC8zPFNbs!ye# z+H+C-`K~5(?NQM!@ce_MLd$dBWtk5a7`zR12MQ){e1t>?%9-1-`xv z*58GH@iIwsJuls~FMQ~5i#)lrVy$OYMhQ=pY|jVnH><*#+?8XPD_+W6 z{|U|G&=A2*t-6Vb_i|N}X{u$q3CV`T%Q8CIJ%6esrp6?)&-XMUHauS{yDJW0PM4mG zL~`&<5fcT&UJUZ1V0S#+NS)N8D~x!ZDA+P;R-=P%Z(nvYE-j9I#RZtenqVvdG1&Hf zCHr%fY)aRm2mpH6TRK>r#y-PQH<7YKvxC!y2>8TXu`T4b(k*zvB#WrH078t;-vIj) zzmdT5cQ?5yt0GFVYqecGw{B6<#?NhajXR(J$S$ylt#N_^4lWN-?8yhjJVS|OgNxw`P z(jH}(&neYDIfTNgfAI)^dxcngfVUcq(l<0u4a9^DB-QlE2w}P!k_N7KywlV^Wt??l zNodAvC7aTK)+VzlMLdU#iA}3I7ei!wpTJQ1_gZ%iw={ubO|PlZ zDUJKPIzm1$J|5ml>j>oNnSu6hpU)PN+nH`B*WV&fa|RmeN?o-t%eF&jgvAhR@W~&! z9t8d?{b&u_&I8ASbnc6*w+@(d%{+cr$nK)JGXGZ{Wxef4+{I=(Yg-+Bywi!mDF=7^ JYFk3mzX8?nfw2Gp diff --git a/Images/HiHi_Color_F_Square_128h.png b/Images/HiHi_Color_F_Square_128h.png deleted file mode 100644 index bf73d7f067c8c7b7719e2a055558075ba0ba1431..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2964 zcmcJR`8U)J8^*sgjL6t!9a+j2jV+AqTS&;*mGudsWX(2>LC9_%yU5rEWl6}IB~lNQ zB}6d>W8WsSWt-Rg2fXh&?+@<}*L|JOeeQGrc%3W$uBidjMZSvw05BOD>LJe^^Ka49 zozET(mw|I*@HMmx002gge+vZU<@1~`K>hkC*ck6)^iwuo)PPdz-IemF)fi&yV|$o

)~py&-Ec&OCb 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 XuYVqA{tZ7b(SVV@sa}nabJV{8T3dZ1 diff --git a/Images/HiHi_Color_Itch_1200w.png b/Images/HiHi_Color_Itch_1200w.png deleted file mode 100644 index 61f8520c1b36d895f33f4ecf1783a0e3b26f0972..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17988 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 Dw+m-P diff --git a/Images/HiHi_Color_Itch_500h.png b/Images/HiHi_Color_Itch_500h.png deleted file mode 100644 index 481979000043f0dc011d920801a567652de2f190..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15286 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 A$N&HU diff --git a/Images/HiHi_White_F_Square_128h.png b/Images/HiHi_White_F_Square_128h.png deleted file mode 100644 index 5652c1f3651be4d9816ecd0ec0d9f244e585bf33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2369 zcma)8dpHw{8=l78awMH*Zk5}pwxP(%Z6@S$Tq2jbCb67j9GB%b;#i4Nv5d0v)SxQ|NUbSV*Y_UX$BaWjl~p zs+8RtdV6C_XO(XuC8l+9<{iqcM@Ai~IaT|w*GBEpIplOpyhOX3zR0r!dX3M5s!>`y zivKsvBa4u+lYLoKqLPKyq7Yrnyw%pO#hBT=phUG{Ue}mb@QS+AlQwwkgg3C!Idok@ zV-mjTh=_Z-iO#=Uyx=iSeBkd^>b<6a4N!MFAL%De7RpGD8{Sux66(!@*!Ys@gF8s5Cj>ZGw21`Q_d!K`{1Y(hkT zG~xsI7qSj};C{_oygqh={#fOW(6>Y74SP}z;Z%N#ych0V*xO8#y+R(-ni&~y_>rdC zb0g6;V+(yY;lXD3lPJiQ#TBr{ZBs9)K5D5!!6oyO%i1M>PiJF|U0D-+NYC zlwefTe%%7Gb+knQMsM(RaaL3^P=$y^X(GHOR|2Vt=+s3}3fo79jlI z(gkChE=h9HEJ3wsPJAEqWnZ(yV=O*smU3V&4``Gfw0YT^{h3R`mZ|ieLf+(SmKcZ4 zmosgK5dp$0)+eGxJtmdfhT{YbMLS+U)7YMWE;vdNawX~u5zPu3RneKaqmQYZ1aB;HLE>qeYLwEbStzZ4bsW%~#NxCA3@qYx`!PU~l za8nNj8_-p@A=gS%8lQ!&4duyDG)InVuMA%Qnr)snCw1ziI`aU|HXtH173^z)9So+WnG*;o@jd9Ty_&y-}iZw1)^R|Zup4v8ilVFxPvzK^{UsmLZ- z7_k#x!!yh+0}C!%X~mp}d+g_~tnBSlPw{`FuQzRMI<#i&ZjZUx^&Hi?pSsap(-fF~ zGFSau#pG7Sgz2otj1B2_qrdA&pd`+aX`9GiPu|-R%QLgP2svivg2DP-oO+^)zoU<% z%xNo!rz$pRtY3rq!%WOi0nH)}tXu=sT@L8^v56XC$sCCE$_F+IGgbYd#XX{?B3OfouZ~P~ z9=?NXeyGXmF}W=|OUM`(yXTHg8^T3Ac;>eyF9E4LDpOM(6qu@m-#=PlX-mY!2bw5K#YZL+qg{RqkPrLZb<>ojavIAKm*1+VcUCDM|Awk!L0JM}BwPDzT=k zUi~OGVo3h)ZpzD|MI86=EkGHL`BiYX3;ZTueM`US$jc>Xe3}fQ%Anvm!mU*h2|C{T zQTSX?_jN>iB7g1aSKj(J41I}f8IdxxJ&c!Gy2m>FJL$*OIyvw6$@qA=y_I_sx@W{? zirbsx=baXZyrJ-g+Wg*YonEzdnoozWlYaxtQ}iAkUKPu;hb5z61e4#HtF+aJyB1-S2gJsY z3tz^5;+?&zQG6@|`Sl^IUnlPjn67r&aHQ6%D32@{7}aja%0I2oa<;Rg?V-wA#Apry z*0vWggDy<7sr35CQl&*zzL~wvmE{y+-}o=VfKy7qWq4R5kDrk(@p(V(39;q7A%rIlBOgFy%eAQc$<)9#zmqWk&t+)?}@!J)+P0p?uxSv zz!rYv!1bVR(njZia&sz{9v`hz5)KmkyD7H8+MZDrQnslz{(j8Y%h1X#Wu9yRq%EU5 zXu%;XH1U%g9~s~Cg}3yr{YIOC*lL3GQTt?x)bYgD)fM_tJabvUDHeC$Xjea-fPVRM z`;!Caz16gQla$WqI_PZWs+5W~a3Qyx&H(QiiZIO}90^L7XgnAtDe`Uw03+VMP%>AX zmhU$E>+|!}R~IFUhiMq;&z^RN2Q1ekFkO~S!>AWVKR=)UqEw3xn3@Td18OSV_&Ggu L`ZrsOO>oA)=)7nj diff --git a/Images/HiHi_White_Long_128h.png b/Images/HiHi_White_Long_128h.png deleted file mode 100644 index 675ccbec7f9c106a6102ec1fd0a2b2ddab721551..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5548 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 YpzuFCYrC(dS^qphIeQ_i90`g40N2rs>Hq)$ diff --git a/Peer/Peer.cs b/Peer/Peer.cs index 4a7c368..55a2c51 100644 --- a/Peer/Peer.cs +++ b/Peer/Peer.cs @@ -1,4 +1,5 @@ -using HiHi.Common; +using HiHi.Commands; +using HiHi.Common; using HiHi.Serialization; using System; @@ -32,7 +33,11 @@ public static class Peer { public static string ConnectionKey { get => connectionKey; set { - Info.ConnectionKey = connectionKey = value; + connectionKey = value; + + if (Info != null) { + Info.ConnectionKey = connectionKey; + } } } @@ -42,29 +47,27 @@ public static string ConnectionKey { public static Action OnLog; public static bool Initialized { get; private set; } - public static bool Connected => Network.Connected; + public static bool Connected => PeerNetwork.Connected; public static bool AcceptingConnections { get; set; } = true; public static PeerInfo Info { get; private set; } public static PeerTransport Transport { get; private set; } public static IHelper Helper { get; private set; } - public static PeerNetwork Network => ISingleton.Instance; private static string connectionKey = DEFAULT_CONNECTION_KEY; - static Peer() { - Info = new PeerInfo(); - } - public static void Initialize(PeerTransport transport, IHelper helper) { if (Initialized) { return; } HiHiTime.Reset(); + CommandUtility.Initialize(); Transport = transport; Helper = helper; Transport.Start(); + Info = PeerInfo.CreateLocal(); + Initialized = true; } @@ -98,33 +101,46 @@ public static void Update(float deltaTime) { INetworkObject.UpdateInstances(); - foreach (ushort peerID in Network.PeerIDs) { - if (Network[peerID].ShouldRequestPing) { + foreach (ushort peerID in PeerNetwork.RemotePeerIDs) { + if (PeerNetwork.GetPeerInfo(peerID).ShouldRequestPing) { PeerMessage pingMessage = NewMessage(PeerMessageType.PingRequest); pingMessage.Buffer.AddUShort(HalfPrecision.Quantize(HiHiTime.RealTime)); Transport.Send(pingMessage); - Network[peerID].RegisterPingRequest(); + PeerNetwork.GetPeerInfo(peerID).RegisterPingRequest(); } - if (Network[peerID].HeartbeatTimedOut) { + if (PeerNetwork.GetPeerInfo(peerID).HeartbeatTimedOut) { Disconnect(peerID, PeerDisconnectReason.TimedOut); } } } + public static PeerMessage NewMessage(PeerMessageType messageType, string destinationEndPoint) => PeerMessage.Borrow(messageType, + Info.UniqueID, + destinationEndPoint); public static PeerMessage NewMessage(PeerMessageType messageType, ushort? destinationPeerID = null) => PeerMessage.Borrow(messageType, Info.UniqueID, - destinationPeerID == null ? string.Empty : Network[destinationPeerID ?? default].EndPoint); + destinationPeerID == null ? string.Empty : PeerNetwork.GetPeerInfo(destinationPeerID ?? default).RemoteEndPoint); #region Outgoing + public static void Connect(string destinationEndPoint) { + if (!Initialized) { return; } + if (!AcceptingConnections) { return; } + + PeerMessage connectMessage = NewMessage(PeerMessageType.Connect, destinationEndPoint); + Info.Serialize(connectMessage.Buffer); + Transport.Send(connectMessage); + } + public static void Connect(PeerInfo destinationInfo, PeerConnectReason reason = PeerConnectReason.Unknown) { if (!Initialized) { return; } - if(!AcceptingConnections) { return; } - if(destinationInfo.ConnectionKey != ConnectionKey) { return; } + if (!AcceptingConnections) { return; } + if (!destinationInfo.Verified) { return; } + if (destinationInfo.ConnectionKey != ConnectionKey) { return; } - if (!Network.TryAddConnection(destinationInfo)) { return; } + if (!PeerNetwork.TryAddConnection(destinationInfo)) { return; } // Connect message PeerMessage connectMessage = NewMessage(PeerMessageType.Connect, destinationInfo.UniqueID); @@ -133,7 +149,7 @@ public static void Connect(PeerInfo destinationInfo, PeerConnectReason reason = // PeerNetwork message PeerMessage peernetworkMessage = NewMessage(PeerMessageType.PeerNetwork, destinationInfo.UniqueID); - Network.SerializeConnections(peernetworkMessage.Buffer); + PeerNetwork.SerializeConnections(peernetworkMessage.Buffer); Transport.Send(peernetworkMessage); // Time message @@ -155,12 +171,12 @@ public static void Connect(PeerInfo destinationInfo, PeerConnectReason reason = public static void Disconnect(ushort destinationPeerID, PeerDisconnectReason reason = PeerDisconnectReason.UnknownReason) { if (!Initialized) { return; } - if (!Network.Contains(destinationPeerID)) { return; } + if (!PeerNetwork.Contains(destinationPeerID)) { return; } PeerMessage message = NewMessage(PeerMessageType.Disconnect, destinationPeerID); Transport.Send(message); - if (!Network.TryRemoveConnection(destinationPeerID)) { return; } + if (!PeerNetwork.TryRemoveConnection(destinationPeerID)) { return; } OnDisconnect?.Invoke(destinationPeerID, reason); @@ -177,14 +193,25 @@ public static void Disconnect(ushort destinationPeerID, PeerDisconnectReason rea public static void DisconnectAll() { if (!Initialized) { return; } - foreach (ushort connection in Network.PeerIDs) { + foreach (ushort connection in PeerNetwork.RemotePeerIDs) { Disconnect(connection, PeerDisconnectReason.LocalPeerDisconnected); } } + public static void SendMessage(PeerMessage message) { + if (!Initialized) { return; } + + Transport.Send(message); + } + public static void SendLog(string message) { if (!Initialized) { return; } + if (CommandUtility.TryInvokeCommand(message, out string result)) { + Log(Info.UniqueID, result); + return; + } + PeerMessage outgoingMessage = NewMessage(PeerMessageType.Log); outgoingMessage.Buffer.AddString(message); Transport.Send(outgoingMessage); @@ -192,12 +219,6 @@ public static void SendLog(string message) { Log(Info.UniqueID, message); } - public static void SendMessage(PeerMessage message) { - if (!Initialized) { return; } - - Transport.Send(message); - } - #endregion #region Incoming @@ -220,23 +241,11 @@ private static void ProcessPeerMessage(PeerMessage message) { case PeerMessageType.Connect: PeerInfo incomingPeerInfo = new PeerInfo(); incomingPeerInfo.Deserialize(message.Buffer); + incomingPeerInfo.RemoteEndPoint = message.SenderEndPoint; Connect(incomingPeerInfo, PeerConnectReason.ExternalReferrer); break; - case PeerMessageType.Unknown: - default: - // OOPS BROKEN MESSAGE :( - // TODO LOG WARNING HERE - break; - } - - if (!Network.Contains(message.SenderPeerID)) { - OnMessageProcessed?.Invoke(message.Type, message.SenderPeerID); - return; - } - - switch (message.Type) { case PeerMessageType.Disconnect: Disconnect(message.SenderPeerID, PeerDisconnectReason.RemotePeerDisconnected); break; @@ -288,17 +297,23 @@ private static void ProcessPeerMessage(PeerMessage message) { case PeerMessageType.ObjectAbandonmentPolicyChange: INetworkObject.ReceiveAbandonmentPolicyChange(message); break; + + case PeerMessageType.Unknown: + default: + // OOPS BROKEN MESSAGE :( + // TODO LOG WARNING HERE + break; } - if (Network.Contains(message.SenderPeerID)) { - Network[message.SenderPeerID].RegisterHeartbeat(); + if (PeerNetwork.Contains(message.SenderPeerID)) { + PeerNetwork.GetPeerInfo(message.SenderPeerID).RegisterHeartbeat(); } OnMessageProcessed?.Invoke(message.Type, message.SenderPeerID); } private static void HandlePeerNetworkMessage(PeerMessage message) { - PeerInfo[] connections = Network.DeserializeConnections(message.Buffer); + PeerInfo[] connections = PeerNetwork.DeserializeConnections(message.Buffer); foreach(PeerInfo info in connections) { Connect(info, PeerConnectReason.PeerNetwork); @@ -306,6 +321,8 @@ private static void HandlePeerNetworkMessage(PeerMessage message) { } private static void ProcessPingRequest(PeerMessage message) { + if (!PeerNetwork.Contains(message.SenderPeerID)) { return; } + float sentPing = HiHiTime.RealTime - HalfPrecision.Dequantize(message.Buffer.ReadUShort()); PeerMessage pingMessage = NewMessage(PeerMessageType.PingResponse); @@ -314,12 +331,18 @@ private static void ProcessPingRequest(PeerMessage message) { } private static void ProcessPingResponse(PeerMessage message) { + if (!PeerNetwork.Contains(message.SenderPeerID)) { return; } + float receivedPing = HalfPrecision.Dequantize(message.Buffer.ReadUShort()); - Network[message.SenderPeerID].SetPing(receivedPing); + PeerNetwork.GetPeerInfo(message.SenderPeerID).SetPing(receivedPing); } #endregion + #region Misc + private static void Log(ushort peerID, string message) => OnLog?.Invoke(peerID, message); + + #endregion } } \ No newline at end of file diff --git a/Peer/PeerMessage.cs b/Peer/PeerMessage.cs index cae04e1..f994b6b 100644 --- a/Peer/PeerMessage.cs +++ b/Peer/PeerMessage.cs @@ -36,7 +36,10 @@ public class PeerMessage { public PeerMessageType Type { get; private set; } public ushort SenderPeerID { get; private set; } - public string DestinationEndPoint => destinationEndPoint; + public string DestinationEndPoint { + get => destinationEndPoint; + set => destinationEndPoint = value; + } public string SenderEndPoint => senderEndPoint; public bool DestinationAll => destinationEndPoint.Equals(string.Empty); public BitBuffer Buffer { get; private set; } @@ -78,17 +81,17 @@ public static PeerMessage Borrow(byte[] data, int length) { return message; } - public static PeerMessage Borrow(string endPoint, byte[] data, int length) { + public static PeerMessage Borrow(string endPointString, byte[] data, int length) { PeerMessage message = Borrow(); - message.senderEndPoint = endPoint; + message.senderEndPoint = endPointString; message.Buffer.FromArray(data, length); message.Type = (PeerMessageType)message.Buffer.Read(PEER_MESSAGE_TYPE_BITS); - if (Peer.Initialized) { - if (Peer.Network.TryGetIDFromEndpoint(message.senderEndPoint, out ushort peerID)) { - message.SenderPeerID = peerID; - } + if (!Peer.Initialized) { return message; } + + if (PeerNetwork.TryGetIDFromEndPointString(message.senderEndPoint, out ushort peerID)) { + message.SenderPeerID = peerID; } return message; diff --git a/Peer/Transports/UDPTransport.cs b/Peer/Transports/UDPTransport.cs deleted file mode 100644 index 5e2141f..0000000 --- a/Peer/Transports/UDPTransport.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using HiHi.Common; - -/* - * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) - * - * Copyright © 2023 Pelle Bruinsma - * - * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. - * - * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. - * - * 2. The User is one of the following: - * a. An individual person, laboring for themselves - * b. A non-profit organization - * c. An educational institution - * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor - * - * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. - * - * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. - */ -namespace HiHi { - public class UDPTransport : PeerTransport { - public const int MAX_PACKET_SIZE = ushort.MaxValue - 8 /*UDP header*/ - 20 /*IPv4 header*/; - - public override int MaxPacketSize => MAX_PACKET_SIZE; - public override string LocalEndPoint { - get { - return HiHiUtility.ToEndPointString(client.Client.LocalEndPoint as IPEndPoint); - } - } - public override string LocalAddress => (client.Client.LocalEndPoint as IPEndPoint).Address.ToString(); - public override int Port => (client.Client.LocalEndPoint as IPEndPoint).Port; - - private const int PREFERRED_RECEIVE_PORT = HiHiConfiguration.BROADCAST_RECEIVE_PORT; - - private UdpClient client; - private IPEndPoint outgoingRemoteEndpoint = new IPEndPoint(IPAddress.Any, 0); - private IPEndPoint incomingRemoteEndpoint = new IPEndPoint(IPAddress.Any, 0); - private byte[] incomingBuffer; - private byte[] outgoingBuffer; - - public UDPTransport(int preferredPort = PREFERRED_RECEIVE_PORT) : base() { - client = new UdpClient(HiHiUtility.GetFreePort(preferredPort)); - outgoingBuffer = new byte[MAX_PACKET_SIZE]; - } - - public override void Start() { - base.Start(); - } - - public override void Stop() { - base.Stop(); - } - - protected override void ReceiveIncomingMessages() { - client.EnableBroadcast = ReceiveBroadcast; - - while (client.Available > 0) { - try { - incomingBuffer = client.Receive(ref incomingRemoteEndpoint); - } - catch (SocketException) { continue; } - - if (!PeerMessage.ContainsValidHeader(incomingBuffer)) { continue; } - - PeerMessage message = PeerMessage.Borrow(incomingRemoteEndpoint.ToEndPointString(), incomingBuffer, incomingBuffer.Length); - IncomingMessages.Enqueue(message); - } - } - - protected override void SendOutgoingMessages() { - while (!OutgoingMessages.IsEmpty) { - if (!OutgoingMessages.TryDequeue(out PeerMessage message)) { continue; } - int bufferLength = message.Buffer.ToArray(outgoingBuffer); - - if (!PeerMessage.ContainsValidHeader(outgoingBuffer)) { - message.Return(); - throw new HiHiException($"Produced buffer with invalid header."); - } - - if(bufferLength > MAX_PACKET_SIZE) { - message.Return(); - throw new HiHiException($"Buffer length exceeded {nameof(MAX_PACKET_SIZE)} ({bufferLength} vs {MAX_PACKET_SIZE})."); - } - - if (message.DestinationAll) { - foreach(ushort peerID in Peer.Network.PeerIDs) { - if (!Peer.Network.Contains(peerID)) { continue; } - if (!IPEndPoint.TryParse(Peer.Network[peerID].EndPoint, out outgoingRemoteEndpoint)) { continue; } - - try { - client.Send(outgoingBuffer, bufferLength, outgoingRemoteEndpoint); - } - catch (SocketException) { continue; } - } - } - else { - outgoingRemoteEndpoint = HiHiUtility.ParseStringToIPEndpoint(message.DestinationEndPoint); - - try { - client.Send(outgoingBuffer, bufferLength, outgoingRemoteEndpoint); - } - catch (SocketException) { continue; } - } - - message.Return(); - } - } - } -} diff --git a/README.md b/README.md index d8749e5..6af2a20 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ ![HiHi Color Long](Images/HiHi_Color_Itch_1200w.png) -`Work in progress` +`WORK IN PROGRESS` An engine-agnostic P2P high-level multiplayer solution written in C#. Made out of a passion for multiplayer games and spite for engine companies' anti-consumer/anti-indie practices. For indies with not a cent to spare on servers. -Still very much a work-in-progress currently. Many features required for the development of proper multiplayer games are currently missing and examples are being worked on. +Still very much a work-in-progress currently. Many features required for the development of proper multiplayer games are currently missing and examples are being worked on. For progress, check the [roadmap](Roadmap.md). [Licensed](LICENSE) under the ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4). Need a different license? Contact me @ [stupidplusplus@gmail.com](mailto:stupidplusplus@gmail.com). @@ -152,39 +152,19 @@ HiHi currently includes engine bindings for [Godot](https://godotengine.org/). M Required bindings for implementation are currently: -- **IHelper** - *Handles the serialization and deserialization of SpawnData.* -- **INetworkObject** - *Implements* -- **ISpawnData** - *Serializable object containing information required to spawn objects on remote peers* +- **Helper** - *Handles the serialization and deserialization of SpawnData.* +- **NetworkObject** - *Implements networked entities.* +- **SpawnData** - *Serializable object containing information required to spawn objects on remote peers* - **MiscBindings** *(Optional)* - *Provides implicit conversion between HiHi's Vectors, Quaterions, etc and the engine's version of these objects.* -## Roadmap - -- [x] Connections -- [x] Local discovery -- [x] Serialization -- [x] Messaging -- [x] Networked objects -- [x] Ownership -- [x] Synchronized variables -- [x] RPC's -- [x] Time synchronization -- [x] Synchronized transforms -- [x] Synchronized physics bodies -- [x] Synchronized spawning & destruction -- [x] Spawn history synchronization for new connections -- [x] Abandonment -- [x] Questions -- [x] Message allocation optimization -- [x] Prettify NetworkObject implementation -- [x] PeerMessage sender header optimization -- [x] Signaling -- [x] NAT punching *To be more extensively tested.* -- [ ] Democracy -- [ ] Unity Bindings -- [ ] Example project -- [ ] Getting started tutorial +## Transports + +Transports currently included with HiHi are: + +- **LiteNetTransport** - A transport based on the [LiteNetLib](https://github.com/RevenantX/LiteNetLib) reliable UDP networking library. + - Requires [LiteNetLib 1.1.0](https://github.com/RevenantX/LiteNetLib/releases/tag/v1.1.0). diff --git a/Roadmap.md b/Roadmap.md new file mode 100644 index 0000000..b5f02e0 --- /dev/null +++ b/Roadmap.md @@ -0,0 +1,38 @@ +# Roadmap + +- [x] Connections +- [x] Local discovery +- [x] Serialization +- [x] Messaging +- [x] Networked objects +- [x] Ownership +- [x] Synchronized variables +- [x] RPC's +- [x] Time synchronization +- [x] Synchronized transforms +- [x] Synchronized physics bodies +- [x] Synchronized spawning & destruction +- [x] Spawn history synchronization for new connections +- [x] Abandonment +- [x] Questions +- [x] Message allocation optimization +- [x] Prettify NetworkObject implementation +- [x] PeerMessage sender header optimization +- [x] Signaling +- [x] NAT punching *To be more extensively tested.* +- [x] Democracy +- [x] Unity Bindings +- [x] Rename engine implementations (I.E. UnityNetworkObject -> NetworkObject) +- [x] Add HiHiVector2 conversion +- [x] Rework PeerNetwork to distinguish between all peers and remote peers +- [x] Rework PeerNetwork to be static +- [x] Rework SyncSpawn to return type that was spawned +- [x] PeerNetwork player election +- [x] Retire UDPTransport +- [x] NetworkObject Interface properties +- [ ] Update Unity Bindings +- [ ] INetworkObject reference SyncObject +- [ ] Include LocalAutoConnect with engine implementations +- [ ] Asset store / Godot asset library uploads +- [ ] Example project +- [ ] Getting started tutorial \ No newline at end of file diff --git a/Signaling/Signaler.cs b/Signaling/Signaler.cs index 2b357fe..59df4cb 100644 --- a/Signaling/Signaler.cs +++ b/Signaling/Signaler.cs @@ -1,5 +1,6 @@ using HiHi.Common; using System.Threading; +using System.Threading.Tasks; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -30,20 +31,24 @@ public class Signaler { protected virtual int SignalRoutineIntervalMS => 1000; - protected Thread thread; + protected Task task; protected ThreadTimer threadTimer; public Signaler() { - thread = new Thread(() => SignalRoutine()); - threadTimer = new ThreadTimer(SignalRoutineIntervalMS); + threadTimer = new ThreadTimer(SignalRoutineIntervalMS); } public virtual void Start() { + if (Running) { return; } + Running = true; - thread.Start(); + + task = Task.Run(() => SignalRoutine()); } public virtual void Stop() { + if (!Running) { return; } + Running = false; } diff --git a/Signaling/SignalerConnectionInfo.cs b/Signaling/SignalerConnectionInfo.cs index 1f9f88f..2aba94b 100644 --- a/Signaling/SignalerConnectionInfo.cs +++ b/Signaling/SignalerConnectionInfo.cs @@ -23,6 +23,7 @@ */ namespace HiHi.Signaling { public class SignalerConnectionInfo : PeerInfo { + public int DesiredLobbySize { get; set; } public SignalerLobby Lobby { get; set; } = null; public SignalerConnectionInfo() { } diff --git a/Signaling/SignalerLobby.cs b/Signaling/SignalerLobby.cs index 8586d9d..cd8926f 100644 --- a/Signaling/SignalerLobby.cs +++ b/Signaling/SignalerLobby.cs @@ -26,7 +26,7 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi.Signaling { - public class SignalerLobby where T : PeerInfo { + public class SignalerLobby where T : SignalerConnectionInfo { protected static Random random = new Random(); public int Size { get; private set; } @@ -50,11 +50,11 @@ public SignalerLobby(int size, string connectionKey) { } public virtual bool TryAdd(T connection) { - if(Full ||connection.ConnectionKey != ConnectionKey) { + if(Full || Size != connection.DesiredLobbySize ||connection.ConnectionKey != ConnectionKey) { return false; } - connection.Verify(availableIDs.Dequeue(), connection.EndPoint); + connection.Verify(availableIDs.Dequeue(), connection.RemoteEndPoint); Connections.Add(connection); return true; } diff --git a/Signaling/Signalers/UDPSignaler/UDPSignaler.cs b/Signaling/Signalers/LiteNetSignaler/LiteNetSignaler.cs similarity index 83% rename from Signaling/Signalers/UDPSignaler/UDPSignaler.cs rename to Signaling/Signalers/LiteNetSignaler/LiteNetSignaler.cs index 9909cb1..caf5ce7 100644 --- a/Signaling/Signalers/UDPSignaler/UDPSignaler.cs +++ b/Signaling/Signalers/LiteNetSignaler/LiteNetSignaler.cs @@ -26,25 +26,21 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi.Signaling { - public class UDPSignaler : Signaler { - public const int DEFAULT_LOBBY_SIZE = 16; - + public class LiteNetSignaler : Signaler { public event Action OnPeerConnected; public event Action OnPeerLobbied; public event Action OnPeerDisconnected; public string LocalEndPoint => transport.LocalEndPoint; public string LocalAddress => transport.LocalAddress; - public int Port => transport.Port; - public int LobbySize { get; set; } + public int Port => transport.LocalPort; public List> Lobbies = new List>(); public Dictionary Connections = new Dictionary(); - private UDPTransport transport; + private LiteNetTransport transport; - public UDPSignaler(int port, int lobbySize = DEFAULT_LOBBY_SIZE) : base() { - this.LobbySize = lobbySize; - this.transport = new UDPTransport(port); + public LiteNetSignaler(int? port = null) : base() { + this.transport = new LiteNetTransport(port ?? HiHiConfiguration.SIGNALER_DEFAULT_PORT); } public override void Start() { @@ -74,7 +70,7 @@ private void ReceiveMessages() { private void ProcessMessage(PeerMessage message) { if (!Connections.ContainsKey(message.SenderEndPoint)) { - SignalerConnectionInfo info = new SignalerConnectionInfo() { EndPoint = message.SenderEndPoint }; + SignalerConnectionInfo info = new SignalerConnectionInfo() { RemoteEndPoint = message.SenderEndPoint }; Connections.Add(message.SenderEndPoint, info); OnPeerConnected?.Invoke(info); } @@ -84,12 +80,23 @@ private void ProcessMessage(PeerMessage message) { switch (message.Type) { case PeerMessageType.VerifiedPeerInfoRequest: connection.Deserialize(message.Buffer); - connection.EndPoint = message.SenderEndPoint; + connection.DesiredLobbySize = message.Buffer.ReadInt(); + connection.RemoteEndPoint = message.SenderEndPoint; connection.RegisterHeartbeat(); + LobbyConnection(connection); break; case PeerMessageType.RemotePeerInfoRequest: + connection.Deserialize(message.Buffer); + connection.DesiredLobbySize = message.Buffer.ReadInt(); + connection.RemoteEndPoint = message.SenderEndPoint; + connection.RegisterHeartbeat(); + + if (connection.Lobby == null) { + LobbyConnection(connection); + } + SendRemotePeerInfo(connection.Lobby); break; @@ -103,7 +110,7 @@ private void ProcessMessage(PeerMessage message) { private void Disconnect(SignalerConnectionInfo info) { info.Lobby?.Remove(info); - Connections.Remove(info.EndPoint); + Connections.Remove(info.RemoteEndPoint); OnPeerDisconnected?.Invoke(info); } @@ -145,7 +152,7 @@ private void LobbyConnection(SignalerConnectionInfo info) { } if (lobby == null) { - SignalerLobby newLobby = new SignalerLobby(LobbySize, info.ConnectionKey); + SignalerLobby newLobby = new SignalerLobby(info.DesiredLobbySize, info.ConnectionKey); newLobby.TryAdd(info); Lobbies.Add(newLobby); lobby = newLobby; @@ -154,13 +161,12 @@ private void LobbyConnection(SignalerConnectionInfo info) { SendVerifiedPeerInfo(info); info.Lobby = lobby; - SendRemotePeerInfo(lobby); - OnPeerLobbied?.Invoke(info); } private void SendVerifiedPeerInfo(SignalerConnectionInfo destinationInfo) { - PeerMessage message = PeerMessage.Borrow(PeerMessageType.VerifiedPeerInfo, default, destinationInfo.EndPoint); + PeerMessage message = PeerMessage.Borrow(PeerMessageType.VerifiedPeerInfo, default, destinationInfo.RemoteEndPoint); + destinationInfo.Verified = true; destinationInfo.Serialize(message.Buffer); transport.Send(message); } @@ -176,9 +182,7 @@ private void SendRemotePeerInfo(SignalerLobby lobby) { SignalerConnectionInfo peer1 = lobby.Connections[c1]; - PeerMessage message = PeerMessage.Borrow(PeerMessageType.RemotePeerInfo, default, peer0.EndPoint); - peer1.Serialize(message.Buffer); - transport.Send(message); + transport.SendNATIntroduction(peer0.LocalEndPoint, peer0.RemoteEndPoint, peer1.LocalEndPoint, peer1.RemoteEndPoint); } } } diff --git a/SyncObjects/Democracy.cs b/SyncObjects/Democracy.cs new file mode 100644 index 0000000..88f052c --- /dev/null +++ b/SyncObjects/Democracy.cs @@ -0,0 +1,181 @@ +using HiHi.Serialization; +using HiHi.Common; +using System; +using System.Linq; +using System.Collections.Generic; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + public class Democracy : SyncObject where T : struct { + public event Action OnAsked; + public event Action OnAnswered; + public event Action OnConsensusReached; + + public int ExpectingAnswers => PeerNetwork.RemotePeerCount + 1; + public int ReceivedAnswers => answers.Where(i => i.Value != null).Count(); + public bool AllPeersAnswered => ReceivedAnswers >= ExpectingAnswers; + public bool ConsensusReached => AllPeersAnswered; + public Dictionary Answers => answers + .Where(i => i.Value != null) + .Select(i => new KeyValuePair(i.Key, i.Value ?? default)) + .ToDictionary(i => i.Key, i => i.Value); + public T Consensus { + get { + IEnumerable orderedAnswers = answers + .Where(i => i.Value != null) + .Select(i => i.Value ?? default) + .GroupBy(i => i) + .OrderByDescending(grp => grp.Count()) + .ThenBy(grp => grp.Key) + .Select(grp => grp.Key); + + return orderedAnswers.FirstOrDefault(); + } + } + + protected override bool RequiresAuthorization => false; + + private Func question; + private Dictionary answers = new Dictionary(); + + public Democracy(INetworkObject parent, Func question) : base(parent) { + this.question = question; + } + + public override void Update() { } + + public void AskAll() { + foreach(ushort peerID in PeerNetwork.RemotePeerIDs) { + Ask(peerID); + } + + Answer(); + } + + public void Clear() { + ClearExpectedAnswers(); + } + + public void Answer(ushort? destinationPeerID = null) { + T answer = question.Invoke(); + PeerMessage message = NewMessage(destinationPeerID); + + message.Buffer.AddBool(false); // isQuestion + SerializationHelper.Serialize(answer, message.Buffer); + + Peer.SendMessage(message); + + RegisterAnswer(Peer.Info.UniqueID, answer); + } + + public override void Synchronize(ushort? destinationPeerID = null) { + throw new HiHiException($"{nameof(Democracy)} doesn't use {nameof(Synchronize)}. Instead use {nameof(Ask)}, {nameof(AskAll)} and {nameof(Answer)}."); + } + + public override void Serialize(BitBuffer buffer) { + base.Serialize(buffer); + } + + public override void Deserialize(ushort senderPeerID, BitBuffer buffer) { + bool isQuestion = buffer.ReadBool(); + + if (isQuestion) { + ClearExpectedAnswers(); + + foreach (ushort peerID in PeerNetwork.RemotePeerIDs) { + ExpectAnswer(peerID); + } + + OnAsked?.Invoke(senderPeerID); + + Answer(); + } + else { + T receivedAnswer = SerializationHelper.Deserialize(default, buffer); + + RegisterAnswer(senderPeerID, receivedAnswer); + } + + base.Deserialize(senderPeerID, buffer); + } + + public override void OnRegister(byte uniqueID) { + base.OnRegister(uniqueID); + + Peer.OnDisconnect += HandleDisconnect; + } + + public override void OnUnregister() { + base.OnUnregister(); + + Peer.OnDisconnect -= HandleDisconnect; + } + + protected void Ask(ushort destinationPeer) { + ExpectAnswer(destinationPeer); + + PeerMessage message = NewMessage(destinationPeer); + + message.Buffer.AddBool(true); + + Peer.SendMessage(message); + } + + protected void RegisterAnswer(ushort peerID, T answer) { + answers[peerID] = answer; + OnAnswered?.Invoke(peerID, answer); + + CheckForConsensus(); + } + + protected void ClearExpectedAnswers() { + answers.Clear(); + } + + protected void ExpectAnswer(ushort peerID) { + if (answers.ContainsKey(peerID)) { return; } + + answers.Add(peerID, null); + } + + protected void UnexpectAnswer(ushort peerID) { + if (!answers.ContainsKey(peerID)) { return; } + + answers.Remove(peerID); + + CheckForConsensus(); + } + + protected void CheckForConsensus() { + if (ConsensusReached) { + OnConsensusReached?.Invoke(Consensus); + } + } + + protected void HandleDisconnect(ushort peerID, PeerDisconnectReason reason) { + UnexpectAnswer(peerID); + } + } +} diff --git a/Core/Question.cs b/SyncObjects/Question.cs similarity index 57% rename from Core/Question.cs rename to SyncObjects/Question.cs index c921542..ead4d49 100644 --- a/Core/Question.cs +++ b/SyncObjects/Question.cs @@ -28,47 +28,34 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. */ namespace HiHi { - public class Question : SyncObject { - public Action OnAsked; - public Action OnAnswered; - - public bool AllPeersAnswered => ReceivedAnswers >= expectingAnswers; - public int ReceivedAnswers => receivedAnswers.Count; - public int ExpectingAnswers => expectingAnswers; - /*public T Consensus => receivedAnswers - .GroupBy(i => i.Value) - .OrderByDescending(grp => grp.Count()) - .ThenBy(grp => grp.Key) - .Select(grp => grp.Key) - .FirstOrDefault();*/ - + public class Question : SyncObject where T : struct { + public event Action OnQuestionReceived; + public event Action OnAnswerReceived; + + public int ExpectingAnswers => answers.Count; + public int ReceivedAnswers => answers.Where(i => i.Value != null).Count(); + public Dictionary Answers => answers + .Where(i => i.Value != null) + .Select(i => new KeyValuePair(i.Key, i.Value ?? default)) + .ToDictionary(i => i.Key, i => i.Value); public T Consensus { get { - IEnumerable answers = receivedAnswers - .GroupBy(i => i.Value) + IEnumerable orderedAnswers = answers + .Where(i => i.Value != null) + .Select(i => i.Value ?? default) + .GroupBy(i => i) .OrderByDescending(grp => grp.Count()) .ThenBy(grp => grp.Key) .Select(grp => grp.Key); - string debugString = string.Empty; - - foreach (T answer in answers) { - debugString += $"{answer}, "; - } - - debugString += $"Picked {answers.FirstOrDefault()}"; - - Peer.SendLog(debugString); - - return answers.FirstOrDefault(); + return orderedAnswers.FirstOrDefault(); } } protected override bool RequiresAuthorization => false; private Func question; - private Dictionary receivedAnswers = new Dictionary(); - private int expectingAnswers = Peer.Network.PeerIDs.Count; + private Dictionary answers = new Dictionary(); public Question(INetworkObject parent, Func question) : base(parent) { this.question = question; @@ -76,40 +63,32 @@ public Question(INetworkObject parent, Func question) : base(parent) { public override void Update() { } - public void Ask(ushort? destinationPeer = null) { - ClearReceivedAnswers(); + public void Ask(ushort destinationPeer) { + ExpectAnswer(destinationPeer); PeerMessage message = NewMessage(destinationPeer); - message.Buffer.AddBool(true); + message.Buffer.AddBool(true); // isQuestion Peer.SendMessage(message); } - public void Answer(ushort? destinationPeerID = null) { - PeerMessage message = NewMessage(destinationPeerID); - - message.Buffer.AddBool(false); - SerializationHelper.Serialize(question.Invoke(), message.Buffer); - - Peer.SendMessage(message); + public void Clear() { + ClearExpectedAnswers(); } - public void AnswerSelf() { + public void Answer(ushort? destinationPeerID = null) { T answer = question.Invoke(); + PeerMessage message = NewMessage(destinationPeerID); - expectingAnswers++; - receivedAnswers.Add(Peer.Info.UniqueID, answer); - OnAnswered?.Invoke(Peer.Info.UniqueID, answer); - } + message.Buffer.AddBool(false); // isQuestion + SerializationHelper.Serialize(answer, message.Buffer); - public void ClearReceivedAnswers() { - receivedAnswers.Clear(); - expectingAnswers = Peer.Network.Connections; + Peer.SendMessage(message); } public override void Synchronize(ushort? destinationPeerID = null) { - throw new HiHiException($"Question doesn't use {nameof(Synchronize)}. Instead use {nameof(Ask)} and {nameof(Answer)}."); + throw new HiHiException($"{nameof(Question)} doesn't use {nameof(Synchronize)}. Instead use {nameof(Ask)} and {nameof(Answer)}."); } public override void Serialize(BitBuffer buffer) { @@ -120,18 +99,52 @@ public override void Deserialize(ushort senderPeerID, BitBuffer buffer) { bool isQuestion = buffer.ReadBool(); if (isQuestion) { - OnAsked?.Invoke(senderPeerID); + OnQuestionReceived?.Invoke(senderPeerID); Answer(); } else { T receivedAnswer = SerializationHelper.Deserialize(default, buffer); - receivedAnswers.Add(senderPeerID, receivedAnswer); - OnAnswered?.Invoke(senderPeerID, receivedAnswer); + RegisterAnswer(senderPeerID, receivedAnswer); } base.Deserialize(senderPeerID, buffer); } + + public override void OnRegister(byte uniqueID) { + base.OnRegister(uniqueID); + + Peer.OnDisconnect += HandleDisconnect; + } + + public override void OnUnregister() { + base.OnUnregister(); + + Peer.OnDisconnect -= HandleDisconnect; + } + + protected void RegisterAnswer(ushort peerID, T answer) { + answers[peerID] = answer; + OnAnswerReceived?.Invoke(peerID, answer); + } + + protected void ClearExpectedAnswers() { + answers.Clear(); + } + + protected void ExpectAnswer(ushort peerID) { + answers.Add(peerID, null); + } + + protected void UnexpectAnswer(ushort peerID) { + if (!answers.ContainsKey(peerID)) { return; } + + answers.Remove(peerID); + } + + protected void HandleDisconnect(ushort peerID, PeerDisconnectReason reason) { + UnexpectAnswer(peerID); + } } } diff --git a/Core/RPC.cs b/SyncObjects/RPC.cs similarity index 100% rename from Core/RPC.cs rename to SyncObjects/RPC.cs diff --git a/Core/Sync.cs b/SyncObjects/Sync.cs similarity index 93% rename from Core/Sync.cs rename to SyncObjects/Sync.cs index 171b18f..5ad8c34 100644 --- a/Core/Sync.cs +++ b/SyncObjects/Sync.cs @@ -1,4 +1,5 @@ using HiHi.Serialization; +using System; /* * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) @@ -25,6 +26,8 @@ */ namespace HiHi { public class Sync : SyncObject { + public event Action OnValueChanged; + public T Value { get { return value; @@ -37,6 +40,10 @@ public T Value { : !this.value.Equals(value); this.value = value; + + if (Dirty) { + OnValueChanged?.Invoke(value); + } } } public bool Dirty { get; private set; } @@ -65,6 +72,7 @@ public override void Serialize(BitBuffer buffer) { public override void Deserialize(BitBuffer buffer) { value = value.Deserialize(buffer); + OnValueChanged?.Invoke(value); base.Deserialize(buffer); } diff --git a/Core/SyncObject.cs b/SyncObjects/SyncObject.cs similarity index 90% rename from Core/SyncObject.cs rename to SyncObjects/SyncObject.cs index c7adc9e..447135a 100644 --- a/Core/SyncObject.cs +++ b/SyncObjects/SyncObject.cs @@ -48,12 +48,21 @@ public SyncObject(INetworkObject parent) { public void Register(INetworkObject parent) { if (Registered) { return; } - UniqueID = parent.RegisterSyncObject(this); + parent.RegisterSyncObject(this); this.parent = parent; - Registered = true; } + public void Unregister() { + if (!Registered) { return; } + + parent.UnregisterSyncObject(this); + + UniqueID = default; + parent = null; + Registered = false; + } + #region Checks protected void RegistrationCheck() { @@ -68,14 +77,18 @@ protected void AuthorizationCheck() { #region Virtual + public virtual void OnRegister(byte uniqueID) { + this.UniqueID = uniqueID; + } + + public virtual void OnUnregister() { } + public virtual void Synchronize(ushort? destinationPeer = null) { RegistrationCheck(); AuthorizationCheck(); PeerMessage message = NewMessage(destinationPeer); - Serialize(message.Buffer); - Peer.SendMessage(message); } diff --git a/Core/SyncPhysicsBody.cs b/SyncObjects/SyncPhysicsBody.cs similarity index 100% rename from Core/SyncPhysicsBody.cs rename to SyncObjects/SyncPhysicsBody.cs diff --git a/Core/SyncTransform.cs b/SyncObjects/SyncTransform.cs similarity index 99% rename from Core/SyncTransform.cs rename to SyncObjects/SyncTransform.cs index 1c37bd9..d1e58f2 100644 --- a/Core/SyncTransform.cs +++ b/SyncObjects/SyncTransform.cs @@ -114,7 +114,7 @@ public void Set(HiHiVector3? position = null, HiHiQuaternion? rotation = null, H public bool TryGetPosition(HiHiVector3 fromPosition, out HiHiVector3 returnedPosition) { if(HiHiVector3.Distance(fromPosition, newPosition) >= TeleportDistance) { - returnedPosition = newPosition - fromPosition; + returnedPosition = newPosition; return true; } diff --git a/Transports/LiteNetTransport.cs b/Transports/LiteNetTransport.cs new file mode 100644 index 0000000..bc1dc08 --- /dev/null +++ b/Transports/LiteNetTransport.cs @@ -0,0 +1,233 @@ +using System.Net; +using System.Collections.Generic; +using LiteNetLib; +using HiHi.Common; + +/* + * ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) + * + * Copyright © 2023 Pelle Bruinsma + * + * This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. + * + * Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. + * + * 2. The User is one of the following: + * a. An individual person, laboring for themselves + * b. A non-profit organization + * c. An educational institution + * d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + * + * 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + * + * 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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. + */ +namespace HiHi { + public class LiteNetTransport : PeerTransport { + public const int MAX_PACKET_SIZE = ushort.MaxValue - 8 /*UDP header*/ - 20 /*IPv4 header*/; + + public override int MaxPacketSize => MAX_PACKET_SIZE; + public override string LocalEndPoint { + get { + return HiHiUtility.ToEndPointString(LocalAddress, LocalPort); + } + } + public override string LocalAddress => HiHiUtility.GetLocalAddressString(); + public override int LocalPort => client.LocalPort; + + private const int PREFERRED_RECEIVE_PORT = HiHiConfiguration.BROADCAST_RECEIVE_PORT; + + private int preferredPort; + private EventBasedNetListener listener; + private EventBasedNatPunchListener natListener; + private NetManager client; + private Dictionary peers; + private IPEndPoint outgoingRemoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + private IPEndPoint incomingRemoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + private byte[] incomingBuffer; + private byte[] outgoingBuffer; + private int outgoingBufferLength; + + public LiteNetTransport(int preferredPort = PREFERRED_RECEIVE_PORT) : base() { + this.preferredPort = preferredPort; + + listener = new EventBasedNetListener(); + natListener = new EventBasedNatPunchListener(); + client = new NetManager(listener) { + IPv6Enabled = true, + NatPunchEnabled = true + }; + client.NatPunchModule.Init(natListener); + peers = new Dictionary(); + + incomingBuffer = new byte[MAX_PACKET_SIZE]; + outgoingBuffer = new byte[MAX_PACKET_SIZE]; + } + + public override void Start() { + client.Start(HiHiUtility.GetFreePort(preferredPort)); + + listener.ConnectionRequestEvent += HandleConnectionRequest; + listener.PeerConnectedEvent += HandlePeerConnected; + listener.PeerDisconnectedEvent += HandlePeerDisconnected; + listener.NetworkReceiveEvent += HandleNetworkReceive; + listener.NetworkReceiveUnconnectedEvent += HandleNetworkReceiveUnconnected; + + natListener.NatIntroductionSuccess += HandleNatIntroductionSuccess; + + base.Start(); + } + + public override void Stop() { + client.Stop(); + + listener.ConnectionRequestEvent -= HandleConnectionRequest; + listener.PeerConnectedEvent -= HandlePeerConnected; + listener.PeerDisconnectedEvent -= HandlePeerDisconnected; + listener.NetworkReceiveEvent -= HandleNetworkReceive; + listener.NetworkReceiveUnconnectedEvent -= HandleNetworkReceiveUnconnected; + + natListener.NatIntroductionSuccess -= HandleNatIntroductionSuccess; + + base.Stop(); + } + + #region Receive + + protected override void ReceiveIncomingMessages() { + client.BroadcastReceiveEnabled = ReceiveBroadcast; + client.PollEvents(); + client.NatPunchModule.PollEvents(); + } + + private void HandleConnectionRequest(ConnectionRequest request) { + request.Accept(); + } + + private void HandlePeerConnected(NetPeer netPeer) { + if (peers.ContainsKey(netPeer.EndPoint)) { return; } + + peers.Add(netPeer.EndPoint, netPeer); + } + + private void HandlePeerDisconnected(NetPeer netPeer, DisconnectInfo info) { + if (!peers.ContainsKey(netPeer.EndPoint)) { return; } + + peers.Remove(netPeer.EndPoint); + } + + private void HandleNetworkReceive(NetPeer netPeer, NetPacketReader reader, byte channel, DeliveryMethod method) { + if(reader.IsNull || reader.AvailableBytes <= 0) { return; } + + incomingRemoteEndPoint = netPeer.EndPoint; + reader.GetBytes(incomingBuffer, 0, reader.AvailableBytes); + + if (!PeerMessage.ContainsValidHeader(incomingBuffer)) { return; } + + PeerMessage message = PeerMessage.Borrow(incomingRemoteEndPoint.ToEndPointString(), incomingBuffer, incomingBuffer.Length); + IncomingMessages.Enqueue(message); + + reader.Recycle(); + } + + private void HandleNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { + if (reader.IsNull || reader.AvailableBytes <= 0) { return; } + + incomingRemoteEndPoint = remoteEndPoint; + reader.GetBytes(incomingBuffer, 0, reader.AvailableBytes); + + + if (!PeerMessage.ContainsValidHeader(incomingBuffer)) { return; } + + PeerMessage message = PeerMessage.Borrow(incomingRemoteEndPoint.ToEndPointString(), incomingBuffer, incomingBuffer.Length); + IncomingMessages.Enqueue(message); + + reader.Recycle(); + } + + private void HandleNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType natAddressType, string token) { + client.Connect(targetEndPoint, string.Empty); + + if(!Peer.Initialized || Peer.Transport != this) { return; } + + Peer.Connect(targetEndPoint.ToEndPointString()); + } + + #endregion + + #region Send + + public override void SendBroadcast(PeerMessage message) { + byte[] outgoingBuffer = new byte[MAX_PACKET_SIZE]; + int outgoingBufferLength = message.Buffer.ToArray(outgoingBuffer); + + client.SendBroadcast(outgoingBuffer, 0, outgoingBufferLength, HiHiConfiguration.BROADCAST_RECEIVE_PORT); + } + + public override void SendNATIntroduction(string internalEndPointA, string externalEndPointA, string internalEndPointB, string externalEndPointB) { + if (!HiHiUtility.TryParseStringToIPEndPoint(internalEndPointA, out IPEndPoint internalA)) { return; } + if (!HiHiUtility.TryParseStringToIPEndPoint(externalEndPointA, out IPEndPoint externalA)) { return; } + if (!HiHiUtility.TryParseStringToIPEndPoint(internalEndPointB, out IPEndPoint internalB)) { return; } + if (!HiHiUtility.TryParseStringToIPEndPoint(externalEndPointB, out IPEndPoint externalB)) { return; } + + client.NatPunchModule.NatIntroduce(internalA, externalA, internalB, externalB, string.Empty); + } + + protected override void SendOutgoingMessages() { + while (!OutgoingMessages.IsEmpty) { + if (!OutgoingMessages.TryDequeue(out PeerMessage message)) { continue; } + outgoingBufferLength = message.Buffer.ToArray(outgoingBuffer); + + if (!PeerMessage.ContainsValidHeader(outgoingBuffer)) { + message.Return(); + throw new HiHiException($"Produced buffer with invalid header."); + } + + if (outgoingBufferLength > MAX_PACKET_SIZE) { + message.Return(); + throw new HiHiException($"Buffer length exceeded {nameof(MAX_PACKET_SIZE)} ({outgoingBufferLength} vs {MAX_PACKET_SIZE})."); + } + + bool returnMessage = true; + if (message.DestinationAll) { + foreach (ushort peerID in PeerNetwork.RemotePeerIDs) { + if (!PeerNetwork.Contains(peerID)) { continue; } + + Send(message, PeerNetwork.GetPeerInfo(peerID).RemoteEndPoint, out returnMessage); + } + } + else { + Send(message, message.DestinationEndPoint, out returnMessage); + } + + if (returnMessage) { + message.Return(); + } + } + } + + private void Send(PeerMessage message, string endPoint, out bool returnMessage) { + if (!HiHiUtility.TryParseStringToIPEndPoint(endPoint, out outgoingRemoteEndPoint)) { + returnMessage = true; + return; + } + + if (!peers.ContainsKey(outgoingRemoteEndPoint)) { + client.Connect(outgoingRemoteEndPoint, string.Empty); + + OutgoingMessages.Enqueue(message); + returnMessage = false; + return; + } + + peers[outgoingRemoteEndPoint].Send(outgoingBuffer, 0, outgoingBufferLength, DeliveryMethod.ReliableOrdered); + returnMessage = true; + } + + #endregion + } +} diff --git a/Peer/PeerTransport.cs b/Transports/PeerTransport.cs similarity index 84% rename from Peer/PeerTransport.cs rename to Transports/PeerTransport.cs index 03fac6d..9d01b0f 100644 --- a/Peer/PeerTransport.cs +++ b/Transports/PeerTransport.cs @@ -1,5 +1,7 @@ using HiHi.Common; +using System; using System.Collections.Concurrent; +using System.Net; using System.Threading; /* @@ -29,15 +31,15 @@ namespace HiHi { public abstract class PeerTransport { public const int THREAD_TIMER_INTERVAL_MS = 5; - public bool Running { get; private set; } - public bool ReceiveBroadcast { get; set; } + public bool Running { get; private set; } = false; + public bool ReceiveBroadcast { get; set; } = false; public bool IncomingMessagesAvailable => !IncomingMessages.IsEmpty; public ConcurrentQueue IncomingMessages { get; private set; } public ConcurrentQueue OutgoingMessages { get; private set; } public virtual int MaxPacketSize => 0; public abstract string LocalEndPoint { get; } public abstract string LocalAddress { get; } - public abstract int Port { get; } + public abstract int LocalPort { get; } private Thread incomingThread; private ThreadTimer incomingThreadTimer = new ThreadTimer(THREAD_TIMER_INTERVAL_MS); @@ -53,6 +55,8 @@ public PeerTransport() { } public virtual void Start() { + if (Running) { return; } + IncomingMessages.Clear(); OutgoingMessages.Clear(); @@ -63,6 +67,8 @@ public virtual void Start() { } public virtual void Stop() { + if (!Running) { return; } + Running = false; } @@ -70,6 +76,14 @@ public void Send(PeerMessage message) { OutgoingMessages.Enqueue(message); } + public virtual void SendBroadcast(PeerMessage message) { + throw new NotImplementedException($"{nameof(SendBroadcast)} is not implemented on this {nameof(PeerTransport)}."); + } + + public virtual void SendNATIntroduction(string internalEndPointA, string externalEndPointA, string internalEndPointB, string externalEndPointB) { + throw new NotImplementedException($"{nameof(SendNATIntroduction)} is not implemented on this {nameof(PeerTransport)}."); + } + public PeerMessage Receive() { if(!IncomingMessages.TryDequeue(out PeerMessage message)) { throw new HiHiException("Couldn't dequeue from incoming messages.");