From 4cf266895aab89b1c1f7ce932bec52176c497193 Mon Sep 17 00:00:00 2001 From: copygirl Date: Sat, 1 May 2021 14:36:26 +0200 Subject: [PATCH] Reorganize Sync system Splits NetworkSync into 4 distinct classes. - SyncRegistry handles attribute related lookups - Sync is now an abstract instance class available through the Game instance - Specialized implementations are available for the Server and Client, one handling changing and setting of synced objects, the other simply handling Sync packets --- src/CreativeBuilding.cs | 4 +- src/EscapeMenuMultiplayer.cs | 6 +- src/Network/DeSerializer.Impl.cs | 4 +- src/Network/IntegratedServer.cs | 2 +- src/Network/NetworkSync.cs | 366 ------------------------------- src/Network/Sync.cs | 129 +++++++++++ src/Network/SyncClient.cs | 50 +++++ src/Network/SyncRegistry.cs | 110 ++++++++++ src/Network/SyncServer.cs | 94 ++++++++ src/Scenes/Client.cs | 3 + src/Scenes/Game.cs | 2 + src/Scenes/Server.cs | 11 +- src/Utility/Extensions.cs | 7 + 13 files changed, 410 insertions(+), 378 deletions(-) delete mode 100644 src/Network/NetworkSync.cs create mode 100644 src/Network/Sync.cs create mode 100644 src/Network/SyncClient.cs create mode 100644 src/Network/SyncRegistry.cs create mode 100644 src/Network/SyncServer.cs diff --git a/src/CreativeBuilding.cs b/src/CreativeBuilding.cs index b3bb2fc..1228331 100644 --- a/src/CreativeBuilding.cs +++ b/src/CreativeBuilding.cs @@ -104,7 +104,7 @@ public class CreativeBuilding : Node2D .ToArray(); foreach (var pos in validLocations) { - var block = server.Spawn(); + var block = server.Sync.Spawn(); block.Position = pos; block.Color = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F)); } @@ -118,7 +118,7 @@ public class CreativeBuilding : Node2D foreach (var pos in GetBlockPositions(start, direction, length)) { var block = server.GetBlockAt(pos); if (block?.Unbreakable != false) continue; - block.Destroy(); + server.Sync.Destroy(block); } } } diff --git a/src/EscapeMenuMultiplayer.cs b/src/EscapeMenuMultiplayer.cs index c103291..20d5f4c 100644 --- a/src/EscapeMenuMultiplayer.cs +++ b/src/EscapeMenuMultiplayer.cs @@ -135,13 +135,13 @@ public class EscapeMenuMultiplayer : Container if (IntegratedServer != null) { IntegratedServer.Server.Stop(); - NetworkSync.ClearAllObjects(IntegratedServer.Server); + IntegratedServer.Server.Sync.Clear(); IntegratedServer.GetParent().RemoveChild(IntegratedServer); IntegratedServer.QueueFree(); IntegratedServer = null; client.Disconnect(); - NetworkSync.ClearAllObjects(client); + client.Sync.Clear(); } if (client.Status == ConnectionStatus.Disconnected) { @@ -156,7 +156,7 @@ public class EscapeMenuMultiplayer : Container client.Connect(address, port); } else { client.Disconnect(); - NetworkSync.ClearAllObjects(client); + client.Sync.Clear(); } } } diff --git a/src/Network/DeSerializer.Impl.cs b/src/Network/DeSerializer.Impl.cs index 975be69..7eda306 100644 --- a/src/Network/DeSerializer.Impl.cs +++ b/src/Network/DeSerializer.Impl.cs @@ -220,11 +220,11 @@ public class SyncedObjectDeSerializerGenerator where TObj : Node { public override void Serialize(Game game, BinaryWriter writer, TObj value) - => writer.Write(value.GetSyncID()); + => writer.Write(game.Sync.GetStatusOrThrow(value).SyncID); public override TObj Deserialize(Game game, BinaryReader reader) { var id = reader.ReadUInt32(); - var value = (TObj)game.GetObjectBySyncID(id); + var value = (TObj)game.Sync.GetStatusOrThrow(id).Object; if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}"); return value; } diff --git a/src/Network/IntegratedServer.cs b/src/Network/IntegratedServer.cs index bbd5cec..02e5593 100644 --- a/src/Network/IntegratedServer.cs +++ b/src/Network/IntegratedServer.cs @@ -18,7 +18,7 @@ public class IntegratedServer : Node Server = _sceneTree.Root.GetChild(0); // Spawn default blocks. for (var x = -6; x <= 6; x++) { - var block = Server.Spawn(); + var block = Server.Sync.Spawn(); block.Position = new BlockPos(x, 3); block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F); block.Unbreakable = true; diff --git a/src/Network/NetworkSync.cs b/src/Network/NetworkSync.cs deleted file mode 100644 index 696ab14..0000000 --- a/src/Network/NetworkSync.cs +++ /dev/null @@ -1,366 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using Godot; - -// TODO: Allow syncronization of child objects spawned with their parent objects. -// TODO: Specify who properties are syncronized with. (Owner, Friends, Team, Everyone) -public static class NetworkSync -{ - private static readonly List _infoByID = new List(); - private static readonly Dictionary _infoByType = new Dictionary(); - - // TODO: Rework NetworkSync to be an instance on the Game object. - private static readonly Dictionary _serverStatusBySyncID = new Dictionary(); - private static readonly Dictionary _serverStatusByObject = new Dictionary(); - private static readonly Dictionary _clientStatusBySyncID = new Dictionary(); - private static readonly Dictionary _clientStatusByObject = new Dictionary(); - private static readonly HashSet _dirtyObjects = new HashSet(); - private static uint _syncIDCounter = 1; - - static NetworkSync() - { - DiscoverSyncableObjects(); - RegisterPackets(); - } - - - public static T Spawn(this Server server) - where T : Node - { - if (!_infoByType.TryGetValue(typeof(T), out var info)) throw new ArgumentException( - $"No {nameof(SyncObjectInfo)} found for type {typeof(T)} (missing {nameof(SyncObjectAttribute)}?)", nameof(T)); - - var obj = info.InstanceScene.Init(); - var status = new SyncStatus(_syncIDCounter++, obj, info){ Special = Special.Spawn }; - _serverStatusBySyncID.Add(status.SyncID, status); - _serverStatusByObject.Add(status.Object, status); - _dirtyObjects.Add(status); - server.GetNode(info.ContainerNodePath).AddChild(obj); - - return obj; - } - - public static void Destroy(this Node obj) - { - var status = GetSyncStatus(obj); - if (!(obj.GetGame() is Server)) return; - - status.Special = Special.Destroy; - _serverStatusBySyncID.Remove(status.SyncID); - _serverStatusByObject.Remove(status.Object); - _dirtyObjects.Add(status); - - obj.GetParent().RemoveChild(obj); - obj.QueueFree(); - } - - public static TValue SetSync(this TObject obj, TValue value, - [CallerMemberName] string property = null) - where TObject : Node - { MarkDirty(obj, property); return value; } - private static void MarkDirty(Node obj, string property) - { - var status = GetSyncStatus(obj); - if (!status.Info.PropertiesByName.TryGetValue(property, out var propInfo)) throw new ArgumentException( - $"No {nameof(SyncPropertyInfo)} found for {obj.GetType()}.{property} (missing {nameof(SyncPropertyAttribute)}?)", nameof(property)); - if (!(obj.GetGame() is Server)) return; - - status.DirtyProperties |= 1 << propInfo.ID; - _dirtyObjects.Add(status); - } - - - internal static void ProcessDirty(Server server) - { - if (_dirtyObjects.Count == 0) return; - - var packet = new SyncPacket(); - foreach (var status in _dirtyObjects) { - var values = new List<(byte, object)>(); - foreach (var prop in status.Info.PropertiesByID) - if ((status.DirtyProperties & (1 << prop.ID)) != 0) - values.Add((prop.ID, prop.Getter(status.Object))); - packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.SyncID, status.Special, values)); - // If the object has been newly spawned, now is the time to remove the "Spawn" flag. - if (status.Special == Special.Spawn) status.Special = Special.None; - } - // TODO: Need a different way to send packages to all *properly* connected peers. - NetworkPackets.Send(server, server.CustomMultiplayer.GetNetworkConnectedPeers().Select(id => new NetworkID(id)), packet); - - _dirtyObjects.Clear(); - } - - internal static void SendAllObjects(Server server, NetworkID networkID) - { - var packet = new SyncPacket(); - foreach (var status in _serverStatusByObject.Values) { - var values = new List<(byte, object)>(); - foreach (var prop in status.Info.PropertiesByID) - values.Add((prop.ID, prop.Getter(status.Object))); - packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.SyncID, Special.Spawn, values)); - } - NetworkPackets.Send(server, new []{ networkID }, packet); - } - - internal static void ClearAllObjects(Game game) - { - var statusByObject = (game is Server) ? _serverStatusByObject : _clientStatusByObject; - var statusBySyncID = (game is Server) ? _serverStatusBySyncID : _clientStatusBySyncID; - - foreach (var (node, _) in statusByObject) { - if (!Godot.Object.IsInstanceValid(node)) continue; - node.GetParent().RemoveChild(node); - node.QueueFree(); - } - - statusByObject.Clear(); - statusBySyncID.Clear(); - _dirtyObjects.Clear(); - _syncIDCounter = 1; - } - - public static uint GetSyncID(this Node obj) - => GetSyncStatus(obj).SyncID; - public static Node GetObjectBySyncID(this Game game, uint syncID) - { - var statusBySyncID = (game is Server) ? _serverStatusBySyncID : _clientStatusBySyncID; - return statusBySyncID.TryGetValue(syncID, out var value) ? value.Object : null; - } - - private static SyncStatus GetSyncStatus(Node obj) - { - if (obj.GetType().GetCustomAttribute() == null) - throw new ArgumentException($"Type {obj.GetType()} is missing {nameof(SyncObjectAttribute)}"); - var statusByObject = (obj.GetGame() is Server) ? _serverStatusByObject : _clientStatusByObject; - if (!statusByObject.TryGetValue(obj, out var value)) throw new Exception( - $"No {nameof(SyncStatus)} found for '{obj.Name}' ({obj.GetType()})"); - return value; - } - - private class SyncStatus - { - public uint SyncID { get; } - public Node Object { get; } - public SyncObjectInfo Info { get; } - - public int DirtyProperties { get; set; } - public Special Special { get; set; } - - public SyncStatus(uint syncID, Node obj, SyncObjectInfo info) - { SyncID = syncID; Object = obj; Info = info; } - } - public enum Special - { - None, - Spawn, - Destroy, - } - - - private static void DiscoverSyncableObjects() - { - foreach (var type in typeof(NetworkSync).Assembly.GetTypes()) { - var objAttr = type.GetCustomAttribute(); - if (objAttr == null) continue; - - if (!typeof(Node).IsAssignableFrom(type)) throw new Exception( - $"Type {type} with {nameof(SyncObjectAttribute)} must be a subclass of {nameof(Node)}"); - - var objInfo = new SyncObjectInfo((ushort)_infoByID.Count, type); - foreach (var property in type.GetProperties()) { - if (property.GetCustomAttribute() == null) continue; - var propType = typeof(SyncPropertyInfo<,>).MakeGenericType(type, property.PropertyType); - var propInfo = (SyncPropertyInfo)Activator.CreateInstance(propType, (byte)objInfo.PropertiesByID.Count, property); - objInfo.PropertiesByID.Add(propInfo); - objInfo.PropertiesByName.Add(propInfo.Name, propInfo); - - // Ensure that the de/serializer for this type has been generated. - DeSerializerRegistry.Get(propInfo.Type, true); - } - _infoByID.Add(objInfo); - _infoByType.Add(objInfo.Type, objInfo); - } - } - - - private class SyncObjectInfo - { - public ushort ID { get; } - public Type Type { get; } - public string Name => Type.Name; - - public PackedScene InstanceScene { get; } - public string ContainerNodePath { get; } - - public List PropertiesByID { get; } = new List(); - public Dictionary PropertiesByName { get; } = new Dictionary(); - - public SyncObjectInfo(ushort id, Type type) - { - ID = id; - Type = type; - - var attr = type.GetCustomAttribute(); - InstanceScene = GD.Load($"res://scene/{attr.Scene}.tscn"); - ContainerNodePath = attr.Container; - } - } - - private abstract class SyncPropertyInfo - { - public byte ID { get; } - public PropertyInfo Property { get; } - public string Name => Property.Name; - public Type Type => Property.PropertyType; - - public Func Getter { get; } - public Action Setter { get; } - - public SyncPropertyInfo(byte id, PropertyInfo property, - Func getter, Action setter) - { - ID = id; Property = property; - Getter = getter; Setter = setter; - } - } - - private class SyncPropertyInfo : SyncPropertyInfo - { - public SyncPropertyInfo(byte id, PropertyInfo property) : base(id, property, - obj => ((Func)property.GetMethod.CreateDelegate(typeof(Func))).Invoke((TObject)obj), - (obj, value) => ((Action)property.SetMethod.CreateDelegate(typeof(Action))).Invoke((TObject)obj, (TValue)value) - ) { } - } - - - private static void RegisterPackets() - { - DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer()); - NetworkPackets.Register(PacketDirection.ServerToClient, OnSyncPacket); - } - - private static void OnSyncPacket(Game game, NetworkID networkID, SyncPacket packet) - { - foreach (var packetObj in packet.Changes) { - if (packetObj.InfoID >= _infoByID.Count) throw new Exception( - $"Unknown {nameof(SyncObjectInfo)} with ID {packetObj.InfoID}"); - var info = _infoByID[packetObj.InfoID]; - - if (!_clientStatusBySyncID.TryGetValue(packetObj.SyncID, out var status)) { - if (packetObj.Special != Special.Spawn) throw new Exception( - $"Unknown synced object {info.Name} (ID {packetObj.SyncID})"); - - var obj = info.InstanceScene.Init(); - status = new SyncStatus(packetObj.SyncID, obj, info); - _clientStatusBySyncID.Add(status.SyncID, status); - _clientStatusByObject.Add(status.Object, status); - game.GetNode(info.ContainerNodePath).AddChild(obj); - } else { - if (packetObj.Special == Special.Spawn) throw new Exception( - $"Spawning object {info.Name} with ID {packetObj.SyncID}, but it already exists"); - if (info != status.Info) throw new Exception( - $"Info of synced object being modified doesn't match ({info.Name} != {status.Info.Name})"); - - if (packetObj.Special == Special.Destroy) { - _clientStatusBySyncID.Remove(status.SyncID); - _clientStatusByObject.Remove(status.Object); - - status.Object.GetParent().RemoveChild(status.Object); - status.Object.QueueFree(); - continue; - } - } - - foreach (var (propID, value) in packetObj.Values) { - var propInfo = info.PropertiesByID[propID]; - propInfo.Setter(status.Object, value); - } - } - } - - private class SyncPacket - { - public List Changes { get; } = new List(); - - public class Object - { - public ushort InfoID { get; } - public uint SyncID { get; } - public Special Special { get; } - public List<(byte, object)> Values { get; } - public Object(ushort infoID, uint syncID, Special special, List<(byte, object)> values) - { InfoID = infoID; SyncID = syncID; Special = special; Values = values; } - } - } - - private class SyncPacketObjectDeSerializer - : DeSerializer - { - public override void Serialize(Game game, BinaryWriter writer, SyncPacket.Object value) - { - writer.Write(value.InfoID); - writer.Write(value.SyncID); - writer.Write((byte)value.Special); - writer.Write((byte)value.Values.Count); - - if (value.InfoID >= _infoByID.Count) - throw new Exception($"No {nameof(SyncObjectInfo)} with ID {value.InfoID}"); - var objInfo = _infoByID[value.InfoID]; - - foreach (var (propID, val) in value.Values) { - writer.Write(propID); - var propInfo = objInfo.PropertiesByID[propID]; - var deSerializer = DeSerializerRegistry.Get(propInfo.Type, false); - deSerializer.Serialize(game, writer, val); - } - } - - public override SyncPacket.Object Deserialize(Game game, BinaryReader reader) - { - var objectID = reader.ReadUInt16(); - var syncID = reader.ReadUInt32(); - var special = (Special)reader.ReadByte(); - var count = reader.ReadByte(); - - if (objectID >= _infoByID.Count) - throw new Exception($"No sync object with ID {objectID}"); - var objInfo = _infoByID[objectID]; - - if (count > objInfo.PropertiesByID.Count) throw new Exception( - $"Count is higher than possible number of changes"); - - var values = new List<(byte, object)>(count); - var duplicateCheck = new HashSet(); - for (var i = 0; i < count; i++) { - var propID = reader.ReadByte(); - if (propID >= objInfo.PropertiesByID.Count) throw new Exception( - $"No sync property with ID {propID} on {objInfo.Name}"); - var propInfo = objInfo.PropertiesByID[propID]; - if (!duplicateCheck.Add(propID)) throw new Exception( - $"Duplicate entry for property {propInfo.Name}"); - var deSerializer = DeSerializerRegistry.Get(propInfo.Type, false); - values.Add((propID, deSerializer.Deserialize(game, reader))); - } - - return new SyncPacket.Object(objectID, syncID, special, values); - } - } -} - -[AttributeUsage(AttributeTargets.Class)] -public class SyncObjectAttribute : Attribute -{ - public string Scene { get; } - public string Container { get; } - public SyncObjectAttribute(string scene, string container) - { Scene = scene; Container = container; } -} - -[AttributeUsage(AttributeTargets.Property)] -public class SyncPropertyAttribute : Attribute -{ -} diff --git a/src/Network/Sync.cs b/src/Network/Sync.cs new file mode 100644 index 0000000..dafca7b --- /dev/null +++ b/src/Network/Sync.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Godot; + +// TODO: Allow syncronization of child objects spawned with their parent objects. +// TODO: Specify who properties are syncronized with. (Owner, Friends, Team, Everyone) +public class Sync +{ + protected Game Game { get; } + protected Dictionary StatusBySyncID { get; } = new Dictionary(); + protected Dictionary StatusByObject { get; } = new Dictionary(); + + static Sync() => DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer()); + public Sync(Game game) => Game = game; + + public SyncStatus GetStatusOrNull(uint syncID) + => StatusBySyncID.TryGetValue(syncID, out var value) ? value : null; + public SyncStatus GetStatusOrThrow(uint syncID) + => GetStatusOrNull(syncID) ?? throw new Exception( + $"No {nameof(SyncStatus)} found for ID {syncID}"); + + public SyncStatus GetStatusOrNull(Node obj) + { + if (obj.GetType().GetCustomAttribute() == null) + throw new ArgumentException($"Type {obj.GetType()} is missing {nameof(SyncObjectAttribute)}"); + return StatusByObject.TryGetValue(obj, out var value) ? value : null; + } + public SyncStatus GetStatusOrThrow(Node obj) + => GetStatusOrNull(obj) ?? throw new Exception( + $"No {nameof(SyncStatus)} found for '{obj.Name}' ({obj.GetType()})"); + + public virtual void Clear() + { + foreach (var (node, _) in StatusByObject) { + if (!Godot.Object.IsInstanceValid(node)) continue; + node.GetParent().RemoveChild(node); + node.QueueFree(); + } + + StatusByObject.Clear(); + StatusBySyncID.Clear(); + } +} + + +public class SyncStatus +{ + public uint SyncID { get; } + public Node Object { get; } + public SyncObjectInfo Info { get; } + + public int DirtyProperties { get; set; } + public SyncMode Mode { get; set; } + + public SyncStatus(uint syncID, Node obj, SyncObjectInfo info) + { SyncID = syncID; Object = obj; Info = info; } +} + +public enum SyncMode +{ + Default, + Spawn, + Destroy, +} + + +public class SyncPacket +{ + public List Changes { get; } = new List(); + + public class Object + { + public ushort InfoID { get; } + public uint SyncID { get; } + public SyncMode Mode { get; } + public List<(byte, object)> Values { get; } + public Object(ushort infoID, uint syncID, SyncMode mode, List<(byte, object)> values) + { InfoID = infoID; SyncID = syncID; Mode = mode; Values = values; } + } +} + +internal class SyncPacketObjectDeSerializer + : DeSerializer +{ + public override void Serialize(Game game, BinaryWriter writer, SyncPacket.Object value) + { + writer.Write(value.InfoID); + writer.Write(value.SyncID); + writer.Write((byte)value.Mode); + writer.Write((byte)value.Values.Count); + + var objInfo = SyncRegistry.Get(value.InfoID); + foreach (var (propID, val) in value.Values) { + writer.Write(propID); + var propInfo = objInfo.PropertiesByID[propID]; + var deSerializer = DeSerializerRegistry.Get(propInfo.Type, false); + deSerializer.Serialize(game, writer, val); + } + } + + public override SyncPacket.Object Deserialize(Game game, BinaryReader reader) + { + var infoID = reader.ReadUInt16(); + var syncID = reader.ReadUInt32(); + var mode = (SyncMode)reader.ReadByte(); + var count = reader.ReadByte(); + + var objInfo = SyncRegistry.Get(infoID); + if (count > objInfo.PropertiesByID.Count) throw new Exception( + $"Count is higher than possible number of changes"); + + var values = new List<(byte, object)>(count); + var duplicateCheck = new HashSet(); + for (var i = 0; i < count; i++) { + var propID = reader.ReadByte(); + if (propID >= objInfo.PropertiesByID.Count) throw new Exception( + $"No sync property with ID {propID} on {objInfo.Name}"); + var propInfo = objInfo.PropertiesByID[propID]; + if (!duplicateCheck.Add(propID)) throw new Exception( + $"Duplicate entry for property {propInfo.Name}"); + var deSerializer = DeSerializerRegistry.Get(propInfo.Type, false); + values.Add((propID, deSerializer.Deserialize(game, reader))); + } + + return new SyncPacket.Object(infoID, syncID, mode, values); + } +} diff --git a/src/Network/SyncClient.cs b/src/Network/SyncClient.cs new file mode 100644 index 0000000..ea3da06 --- /dev/null +++ b/src/Network/SyncClient.cs @@ -0,0 +1,50 @@ +using System; +using Godot; + +public class SyncClient : Sync +{ + protected Client Client => (Client)Game; + + // FIXME: This works for now, but will break with dedicated servers. We need to register packet types and their handlers separately. + // Fortunately, at this time, there is only two packet types: RPC and Sync. We could even reduce that to just a single one? + public SyncClient(Client client) : base(client) + => NetworkPackets.Register(PacketDirection.ServerToClient, OnSyncPacket); + + private void OnSyncPacket(Game _, NetworkID networkID, SyncPacket packet) + { + foreach (var packetObj in packet.Changes) { + var info = SyncRegistry.Get(packetObj.InfoID); + var status = GetStatusOrNull(packetObj.SyncID); + + if (status == null) { + if (packetObj.Mode != SyncMode.Spawn) throw new Exception( + $"Unknown synced object {info.Name} (ID {packetObj.SyncID})"); + + var obj = info.InstanceScene.Init(); + status = new SyncStatus(packetObj.SyncID, obj, info); + StatusBySyncID.Add(status.SyncID, status); + StatusByObject.Add(status.Object, status); + Client.GetNode(info.ContainerNodePath).AddChild(obj); + } else { + if (packetObj.Mode == SyncMode.Spawn) throw new Exception( + $"Spawning object {info.Name} with ID {packetObj.SyncID}, but it already exists"); + if (info != status.Info) throw new Exception( + $"Info of synced object being modified doesn't match ({info.Name} != {status.Info.Name})"); + + if (packetObj.Mode == SyncMode.Destroy) { + StatusBySyncID.Remove(status.SyncID); + StatusByObject.Remove(status.Object); + + status.Object.GetParent().RemoveChild(status.Object); + status.Object.QueueFree(); + continue; + } + } + + foreach (var (propID, value) in packetObj.Values) { + var propInfo = info.PropertiesByID[propID]; + propInfo.Setter(status.Object, value); + } + } + } +} diff --git a/src/Network/SyncRegistry.cs b/src/Network/SyncRegistry.cs new file mode 100644 index 0000000..98d73ef --- /dev/null +++ b/src/Network/SyncRegistry.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Godot; + +public static class SyncRegistry +{ + private static readonly List _byID = new List(); + private static readonly Dictionary _byType = new Dictionary(); + + static SyncRegistry() + { + foreach (var type in typeof(SyncRegistry).Assembly.GetTypes()) { + var objAttr = type.GetCustomAttribute(); + if (objAttr == null) continue; + + if (!typeof(Node).IsAssignableFrom(type)) throw new Exception( + $"Type {type} with {nameof(SyncObjectAttribute)} must be a subclass of {nameof(Node)}"); + + var objInfo = new SyncObjectInfo((ushort)_byID.Count, type); + foreach (var property in type.GetProperties()) { + if (property.GetCustomAttribute() == null) continue; + var propType = typeof(SyncPropertyInfo<,>).MakeGenericType(type, property.PropertyType); + var propInfo = (SyncPropertyInfo)Activator.CreateInstance(propType, (byte)objInfo.PropertiesByID.Count, property); + objInfo.PropertiesByID.Add(propInfo); + objInfo.PropertiesByName.Add(propInfo.Name, propInfo); + + // Ensure that the de/serializer for this type has been generated. + DeSerializerRegistry.Get(propInfo.Type, true); + } + _byID.Add(objInfo); + _byType.Add(objInfo.Type, objInfo); + } + } + + public static SyncObjectInfo Get(ushort id) + => (id < _byID.Count) ? _byID[id] : throw new Exception( + $"Unknown {nameof(SyncObjectInfo)} with ID {id}"); + + public static SyncObjectInfo Get() + => Get(typeof(T)); + public static SyncObjectInfo Get(Type type) + => _byType.TryGetValue(type, out var value) ? value : throw new Exception( + $"No {nameof(SyncObjectInfo)} found for type {type} (missing {nameof(SyncObjectAttribute)}?)"); +} + + +public class SyncObjectInfo +{ + public ushort ID { get; } + public Type Type { get; } + public string Name => Type.Name; + + public PackedScene InstanceScene { get; } + public string ContainerNodePath { get; } + + public List PropertiesByID { get; } = new List(); + public Dictionary PropertiesByName { get; } = new Dictionary(); + + public SyncObjectInfo(ushort id, Type type) + { + ID = id; + Type = type; + + var attr = type.GetCustomAttribute(); + InstanceScene = GD.Load($"res://scene/{attr.Scene}.tscn"); + ContainerNodePath = attr.Container; + } +} + +public abstract class SyncPropertyInfo +{ + public byte ID { get; } + public PropertyInfo Property { get; } + public string Name => Property.Name; + public Type Type => Property.PropertyType; + + public Func Getter { get; } + public Action Setter { get; } + + public SyncPropertyInfo(byte id, PropertyInfo property, + Func getter, Action setter) + { + ID = id; Property = property; + Getter = getter; Setter = setter; + } +} + +public class SyncPropertyInfo : SyncPropertyInfo +{ + public SyncPropertyInfo(byte id, PropertyInfo property) : base(id, property, + obj => ((Func)property.GetMethod.CreateDelegate(typeof(Func))).Invoke((TObject)obj), + (obj, value) => ((Action)property.SetMethod.CreateDelegate(typeof(Action))).Invoke((TObject)obj, (TValue)value) + ) { } +} + + +[AttributeUsage(AttributeTargets.Class)] +public class SyncObjectAttribute : Attribute +{ + public string Scene { get; } + public string Container { get; } + public SyncObjectAttribute(string scene, string container) + { Scene = scene; Container = container; } +} + +[AttributeUsage(AttributeTargets.Property)] +public class SyncPropertyAttribute : Attribute +{ +} diff --git a/src/Network/SyncServer.cs b/src/Network/SyncServer.cs new file mode 100644 index 0000000..e47b57a --- /dev/null +++ b/src/Network/SyncServer.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Godot; + +public class SyncServer : Sync +{ + private static readonly HashSet _dirtyObjects = new HashSet(); + private static uint _syncIDCounter = 1; + + protected Server Server => (Server)Game; + + public SyncServer(Server server) + : base(server) { } + + public T Spawn() + where T : Node + { + var info = SyncRegistry.Get(); + var obj = info.InstanceScene.Init(); + var status = new SyncStatus(_syncIDCounter++, obj, info){ Mode = SyncMode.Spawn }; + StatusBySyncID.Add(status.SyncID, status); + StatusByObject.Add(status.Object, status); + _dirtyObjects.Add(status); + Server.GetNode(info.ContainerNodePath).AddChild(obj); + + return obj; + } + + // TODO: Do this automatically if the node is removed from the tree? + public void Destroy(Node obj) + { + var status = GetStatusOrThrow(obj); + + status.Mode = SyncMode.Destroy; + StatusBySyncID.Remove(status.SyncID); + StatusByObject.Remove(status.Object); + _dirtyObjects.Add(status); + + obj.GetParent().RemoveChild(obj); + obj.QueueFree(); + } + + public void MarkDirty(Node obj, string property) + { + var status = GetStatusOrThrow(obj); + if (!status.Info.PropertiesByName.TryGetValue(property, out var propInfo)) throw new ArgumentException( + $"No {nameof(SyncPropertyInfo)} found for {obj.GetType()}.{property} (missing {nameof(SyncPropertyAttribute)}?)", nameof(property)); + if (!(obj.GetGame() is Server)) return; + + status.DirtyProperties |= 1 << propInfo.ID; + _dirtyObjects.Add(status); + } + + + public void ProcessDirty(Server server) + { + if (_dirtyObjects.Count == 0) return; + + var packet = new SyncPacket(); + foreach (var status in _dirtyObjects) { + var values = new List<(byte, object)>(); + foreach (var prop in status.Info.PropertiesByID) + if ((status.DirtyProperties & (1 << prop.ID)) != 0) + values.Add((prop.ID, prop.Getter(status.Object))); + packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.SyncID, status.Mode, values)); + // If the object has been newly spawned, now is the time to remove the "Spawn" flag. + if (status.Mode == SyncMode.Spawn) status.Mode = SyncMode.Default; + } + // TODO: Need a different way to send packages to all *properly* connected peers. + NetworkPackets.Send(server, server.CustomMultiplayer.GetNetworkConnectedPeers().Select(id => new NetworkID(id)), packet); + + _dirtyObjects.Clear(); + } + + public void SendAllObjects(Server server, NetworkID networkID) + { + var packet = new SyncPacket(); + foreach (var status in StatusByObject.Values) { + var values = new List<(byte, object)>(); + foreach (var prop in status.Info.PropertiesByID) + values.Add((prop.ID, prop.Getter(status.Object))); + packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.SyncID, SyncMode.Spawn, values)); + } + NetworkPackets.Send(server, new []{ networkID }, packet); + } + + public override void Clear() + { + base.Clear(); + _dirtyObjects.Clear(); + _syncIDCounter = 1; + } +} diff --git a/src/Scenes/Client.cs b/src/Scenes/Client.cs index eebc487..fb6534a 100644 --- a/src/Scenes/Client.cs +++ b/src/Scenes/Client.cs @@ -8,14 +8,17 @@ public class Client : Game [Export] public NodePath CursorPath { get; set; } public Cursor Cursor { get; private set; } + public new SyncClient Sync => (SyncClient)base.Sync; public ConnectionStatus Status => CustomMultiplayer.NetworkPeer?.GetConnectionStatus() ?? ConnectionStatus.Disconnected; public event Action Connected; public event Action Disconnected; public event Action StatusChanged; + public Client() { + base.Sync = new SyncClient(this); CustomMultiplayer = new MultiplayerAPI { RootNode = this }; CustomMultiplayer.Connect("connected_to_server", this, nameof(OnConnectedToServer)); CustomMultiplayer.Connect("connection_failed", this, nameof(Disconnect)); diff --git a/src/Scenes/Game.cs b/src/Scenes/Game.cs index f66def7..b53ec52 100644 --- a/src/Scenes/Game.cs +++ b/src/Scenes/Game.cs @@ -4,6 +4,8 @@ using Godot.Collections; public abstract class Game : Node2D { + public Sync Sync { get; protected set; } + [Export] public NodePath PlayerContainerPath { get; set; } [Export] public NodePath BlockContainerPath { get; set; } diff --git a/src/Scenes/Server.cs b/src/Scenes/Server.cs index 22ccb39..ff6362b 100644 --- a/src/Scenes/Server.cs +++ b/src/Scenes/Server.cs @@ -5,6 +5,8 @@ using Godot; // TODO: Allow for initially private integrated server to open itself up to the public. public class Server : Game { + public new SyncServer Sync => (SyncServer)base.Sync; + private readonly Dictionary _playersByNetworkID = new Dictionary(); private readonly Dictionary _networkIDByPlayer = new Dictionary(); @@ -17,6 +19,7 @@ public class Server : Game public Server() { + base.Sync = new SyncServer(this); CustomMultiplayer = new MultiplayerAPI { RootNode = this }; CustomMultiplayer.Connect("network_peer_connected", this, nameof(OnPeerConnected)); CustomMultiplayer.Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected)); @@ -26,7 +29,7 @@ public class Server : Game public override void _Process(float delta) { CustomMultiplayer.Poll(); - NetworkSync.ProcessDirty(this); + Sync.ProcessDirty(this); NetworkRPC.ProcessPacketBuffer(this); } @@ -103,8 +106,8 @@ public class Server : Game _playersByNetworkID.Add(networkID, player); _networkIDByPlayer[player] = networkID; } else { - NetworkSync.SendAllObjects(this, networkID); - player = this.Spawn(); + Sync.SendAllObjects(this, networkID); + player = Sync.Spawn(); player.Position = Vector2.Zero; player.Color = Colors.Red; @@ -124,7 +127,7 @@ public class Server : Game // Local player stays around for reconnecting. if (_localPlayer == player) return; - player.Destroy(); + Sync.Destroy(player); _playersByNetworkID.Remove(networkID); _networkIDByPlayer.Remove(player); } diff --git a/src/Utility/Extensions.cs b/src/Utility/Extensions.cs index 39db4c6..84441ee 100644 --- a/src/Utility/Extensions.cs +++ b/src/Utility/Extensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using Godot; public static class Extensions @@ -10,6 +11,12 @@ public static class Extensions public static Server GetServer(this Node node) => node.GetGame() as Server; + public static TValue SetSync( + this TObject obj, TValue value, + [CallerMemberName] string property = null) + where TObject : Node + { obj.GetServer()?.Sync.MarkDirty(obj, property); return value; } + public static T Init(this PackedScene @this) where T : Node {