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
main
copygirl 5 years ago
parent ada662f3ac
commit 4cf266895a
  1. 4
      src/CreativeBuilding.cs
  2. 6
      src/EscapeMenuMultiplayer.cs
  3. 4
      src/Network/DeSerializer.Impl.cs
  4. 2
      src/Network/IntegratedServer.cs
  5. 366
      src/Network/NetworkSync.cs
  6. 129
      src/Network/Sync.cs
  7. 50
      src/Network/SyncClient.cs
  8. 110
      src/Network/SyncRegistry.cs
  9. 94
      src/Network/SyncServer.cs
  10. 3
      src/Scenes/Client.cs
  11. 2
      src/Scenes/Game.cs
  12. 11
      src/Scenes/Server.cs
  13. 7
      src/Utility/Extensions.cs

@ -104,7 +104,7 @@ public class CreativeBuilding : Node2D
.ToArray(); .ToArray();
foreach (var pos in validLocations) { foreach (var pos in validLocations) {
var block = server.Spawn<Block>(); var block = server.Sync.Spawn<Block>();
block.Position = pos; block.Position = pos;
block.Color = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F)); 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)) { foreach (var pos in GetBlockPositions(start, direction, length)) {
var block = server.GetBlockAt(pos); var block = server.GetBlockAt(pos);
if (block?.Unbreakable != false) continue; if (block?.Unbreakable != false) continue;
block.Destroy(); server.Sync.Destroy(block);
} }
} }
} }

@ -135,13 +135,13 @@ public class EscapeMenuMultiplayer : Container
if (IntegratedServer != null) { if (IntegratedServer != null) {
IntegratedServer.Server.Stop(); IntegratedServer.Server.Stop();
NetworkSync.ClearAllObjects(IntegratedServer.Server); IntegratedServer.Server.Sync.Clear();
IntegratedServer.GetParent().RemoveChild(IntegratedServer); IntegratedServer.GetParent().RemoveChild(IntegratedServer);
IntegratedServer.QueueFree(); IntegratedServer.QueueFree();
IntegratedServer = null; IntegratedServer = null;
client.Disconnect(); client.Disconnect();
NetworkSync.ClearAllObjects(client); client.Sync.Clear();
} }
if (client.Status == ConnectionStatus.Disconnected) { if (client.Status == ConnectionStatus.Disconnected) {
@ -156,7 +156,7 @@ public class EscapeMenuMultiplayer : Container
client.Connect(address, port); client.Connect(address, port);
} else { } else {
client.Disconnect(); client.Disconnect();
NetworkSync.ClearAllObjects(client); client.Sync.Clear();
} }
} }
} }

@ -220,11 +220,11 @@ public class SyncedObjectDeSerializerGenerator
where TObj : Node where TObj : Node
{ {
public override void Serialize(Game game, BinaryWriter writer, TObj value) 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) public override TObj Deserialize(Game game, BinaryReader reader)
{ {
var id = reader.ReadUInt32(); 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}"); if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}");
return value; return value;
} }

@ -18,7 +18,7 @@ public class IntegratedServer : Node
Server = _sceneTree.Root.GetChild<Server>(0); Server = _sceneTree.Root.GetChild<Server>(0);
// Spawn default blocks. // Spawn default blocks.
for (var x = -6; x <= 6; x++) { for (var x = -6; x <= 6; x++) {
var block = Server.Spawn<Block>(); var block = Server.Sync.Spawn<Block>();
block.Position = new BlockPos(x, 3); block.Position = new BlockPos(x, 3);
block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F); block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
block.Unbreakable = true; block.Unbreakable = true;

@ -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<SyncObjectInfo> _infoByID = new List<SyncObjectInfo>();
private static readonly Dictionary<Type, SyncObjectInfo> _infoByType = new Dictionary<Type, SyncObjectInfo>();
// TODO: Rework NetworkSync to be an instance on the Game object.
private static readonly Dictionary<uint, SyncStatus> _serverStatusBySyncID = new Dictionary<uint, SyncStatus>();
private static readonly Dictionary<Node, SyncStatus> _serverStatusByObject = new Dictionary<Node, SyncStatus>();
private static readonly Dictionary<uint, SyncStatus> _clientStatusBySyncID = new Dictionary<uint, SyncStatus>();
private static readonly Dictionary<Node, SyncStatus> _clientStatusByObject = new Dictionary<Node, SyncStatus>();
private static readonly HashSet<SyncStatus> _dirtyObjects = new HashSet<SyncStatus>();
private static uint _syncIDCounter = 1;
static NetworkSync()
{
DiscoverSyncableObjects();
RegisterPackets();
}
public static T Spawn<T>(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<T>();
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<TObject, TValue>(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<SyncObjectAttribute>() == 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<SyncObjectAttribute>();
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<SyncPropertyAttribute>() == 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<SyncPropertyInfo> PropertiesByID { get; } = new List<SyncPropertyInfo>();
public Dictionary<string, SyncPropertyInfo> PropertiesByName { get; } = new Dictionary<string, SyncPropertyInfo>();
public SyncObjectInfo(ushort id, Type type)
{
ID = id;
Type = type;
var attr = type.GetCustomAttribute<SyncObjectAttribute>();
InstanceScene = GD.Load<PackedScene>($"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<object, object> Getter { get; }
public Action<object, object> Setter { get; }
public SyncPropertyInfo(byte id, PropertyInfo property,
Func<object, object> getter, Action<object, object> setter)
{
ID = id; Property = property;
Getter = getter; Setter = setter;
}
}
private class SyncPropertyInfo<TObject, TValue> : SyncPropertyInfo
{
public SyncPropertyInfo(byte id, PropertyInfo property) : base(id, property,
obj => ((Func<TObject, TValue>)property.GetMethod.CreateDelegate(typeof(Func<TObject, TValue>))).Invoke((TObject)obj),
(obj, value) => ((Action<TObject, TValue>)property.SetMethod.CreateDelegate(typeof(Action<TObject, TValue>))).Invoke((TObject)obj, (TValue)value)
) { }
}
private static void RegisterPackets()
{
DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer());
NetworkPackets.Register<SyncPacket>(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<Node>();
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<Object> Changes { get; } = new List<Object>();
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<SyncPacket.Object>
{
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<byte>();
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
{
}

@ -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<uint, SyncStatus> StatusBySyncID { get; } = new Dictionary<uint, SyncStatus>();
protected Dictionary<Node, SyncStatus> StatusByObject { get; } = new Dictionary<Node, SyncStatus>();
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<SyncObjectAttribute>() == 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<Object> Changes { get; } = new List<Object>();
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<SyncPacket.Object>
{
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<byte>();
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);
}
}

@ -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<SyncPacket>(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<Node>();
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);
}
}
}
}

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Godot;
public static class SyncRegistry
{
private static readonly List<SyncObjectInfo> _byID = new List<SyncObjectInfo>();
private static readonly Dictionary<Type, SyncObjectInfo> _byType = new Dictionary<Type, SyncObjectInfo>();
static SyncRegistry()
{
foreach (var type in typeof(SyncRegistry).Assembly.GetTypes()) {
var objAttr = type.GetCustomAttribute<SyncObjectAttribute>();
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<SyncPropertyAttribute>() == 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<T>()
=> 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<SyncPropertyInfo> PropertiesByID { get; } = new List<SyncPropertyInfo>();
public Dictionary<string, SyncPropertyInfo> PropertiesByName { get; } = new Dictionary<string, SyncPropertyInfo>();
public SyncObjectInfo(ushort id, Type type)
{
ID = id;
Type = type;
var attr = type.GetCustomAttribute<SyncObjectAttribute>();
InstanceScene = GD.Load<PackedScene>($"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<object, object> Getter { get; }
public Action<object, object> Setter { get; }
public SyncPropertyInfo(byte id, PropertyInfo property,
Func<object, object> getter, Action<object, object> setter)
{
ID = id; Property = property;
Getter = getter; Setter = setter;
}
}
public class SyncPropertyInfo<TObject, TValue> : SyncPropertyInfo
{
public SyncPropertyInfo(byte id, PropertyInfo property) : base(id, property,
obj => ((Func<TObject, TValue>)property.GetMethod.CreateDelegate(typeof(Func<TObject, TValue>))).Invoke((TObject)obj),
(obj, value) => ((Action<TObject, TValue>)property.SetMethod.CreateDelegate(typeof(Action<TObject, TValue>))).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
{
}

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
public class SyncServer : Sync
{
private static readonly HashSet<SyncStatus> _dirtyObjects = new HashSet<SyncStatus>();
private static uint _syncIDCounter = 1;
protected Server Server => (Server)Game;
public SyncServer(Server server)
: base(server) { }
public T Spawn<T>()
where T : Node
{
var info = SyncRegistry.Get<T>();
var obj = info.InstanceScene.Init<T>();
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;
}
}

@ -8,14 +8,17 @@ public class Client : Game
[Export] public NodePath CursorPath { get; set; } [Export] public NodePath CursorPath { get; set; }
public Cursor Cursor { get; private set; } public Cursor Cursor { get; private set; }
public new SyncClient Sync => (SyncClient)base.Sync;
public ConnectionStatus Status => CustomMultiplayer.NetworkPeer?.GetConnectionStatus() ?? ConnectionStatus.Disconnected; public ConnectionStatus Status => CustomMultiplayer.NetworkPeer?.GetConnectionStatus() ?? ConnectionStatus.Disconnected;
public event Action Connected; public event Action Connected;
public event Action Disconnected; public event Action Disconnected;
public event Action<ConnectionStatus> StatusChanged; public event Action<ConnectionStatus> StatusChanged;
public Client() public Client()
{ {
base.Sync = new SyncClient(this);
CustomMultiplayer = new MultiplayerAPI { RootNode = this }; CustomMultiplayer = new MultiplayerAPI { RootNode = this };
CustomMultiplayer.Connect("connected_to_server", this, nameof(OnConnectedToServer)); CustomMultiplayer.Connect("connected_to_server", this, nameof(OnConnectedToServer));
CustomMultiplayer.Connect("connection_failed", this, nameof(Disconnect)); CustomMultiplayer.Connect("connection_failed", this, nameof(Disconnect));

@ -4,6 +4,8 @@ using Godot.Collections;
public abstract class Game : Node2D public abstract class Game : Node2D
{ {
public Sync Sync { get; protected set; }
[Export] public NodePath PlayerContainerPath { get; set; } [Export] public NodePath PlayerContainerPath { get; set; }
[Export] public NodePath BlockContainerPath { get; set; } [Export] public NodePath BlockContainerPath { get; set; }

@ -5,6 +5,8 @@ using Godot;
// TODO: Allow for initially private integrated server to open itself up to the public. // TODO: Allow for initially private integrated server to open itself up to the public.
public class Server : Game public class Server : Game
{ {
public new SyncServer Sync => (SyncServer)base.Sync;
private readonly Dictionary<NetworkID, Player> _playersByNetworkID = new Dictionary<NetworkID, Player>(); private readonly Dictionary<NetworkID, Player> _playersByNetworkID = new Dictionary<NetworkID, Player>();
private readonly Dictionary<Player, NetworkID> _networkIDByPlayer = new Dictionary<Player, NetworkID>(); private readonly Dictionary<Player, NetworkID> _networkIDByPlayer = new Dictionary<Player, NetworkID>();
@ -17,6 +19,7 @@ public class Server : Game
public Server() public Server()
{ {
base.Sync = new SyncServer(this);
CustomMultiplayer = new MultiplayerAPI { RootNode = this }; CustomMultiplayer = new MultiplayerAPI { RootNode = this };
CustomMultiplayer.Connect("network_peer_connected", this, nameof(OnPeerConnected)); CustomMultiplayer.Connect("network_peer_connected", this, nameof(OnPeerConnected));
CustomMultiplayer.Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected)); CustomMultiplayer.Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected));
@ -26,7 +29,7 @@ public class Server : Game
public override void _Process(float delta) public override void _Process(float delta)
{ {
CustomMultiplayer.Poll(); CustomMultiplayer.Poll();
NetworkSync.ProcessDirty(this); Sync.ProcessDirty(this);
NetworkRPC.ProcessPacketBuffer(this); NetworkRPC.ProcessPacketBuffer(this);
} }
@ -103,8 +106,8 @@ public class Server : Game
_playersByNetworkID.Add(networkID, player); _playersByNetworkID.Add(networkID, player);
_networkIDByPlayer[player] = networkID; _networkIDByPlayer[player] = networkID;
} else { } else {
NetworkSync.SendAllObjects(this, networkID); Sync.SendAllObjects(this, networkID);
player = this.Spawn<Player>(); player = Sync.Spawn<Player>();
player.Position = Vector2.Zero; player.Position = Vector2.Zero;
player.Color = Colors.Red; player.Color = Colors.Red;
@ -124,7 +127,7 @@ public class Server : Game
// Local player stays around for reconnecting. // Local player stays around for reconnecting.
if (_localPlayer == player) return; if (_localPlayer == player) return;
player.Destroy(); Sync.Destroy(player);
_playersByNetworkID.Remove(networkID); _playersByNetworkID.Remove(networkID);
_networkIDByPlayer.Remove(player); _networkIDByPlayer.Remove(player);
} }

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Godot; using Godot;
public static class Extensions public static class Extensions
@ -10,6 +11,12 @@ public static class Extensions
public static Server GetServer(this Node node) public static Server GetServer(this Node node)
=> node.GetGame() as Server; => node.GetGame() as Server;
public static TValue SetSync<TObject, TValue>(
this TObject obj, TValue value,
[CallerMemberName] string property = null)
where TObject : Node
{ obj.GetServer()?.Sync.MarkDirty(obj, property); return value; }
public static T Init<T>(this PackedScene @this) public static T Init<T>(this PackedScene @this)
where T : Node where T : Node
{ {

Loading…
Cancel
Save