Undo weeks of work

- Remove DeSerialization, RPC and Sync code
- Use Godot's built-in Rpc and Rset methods
- Use built-in Multiplayer with Root set to World
- Add World class, handles spawning,
  getting players and blocks
- No more custom packets
- No more reflection or attributes
- Reintroduce LocalPlayer scene
- Keep it simple, stupid!
main
copygirl 4 years ago
parent fdf1782069
commit 94bd99a478
  1. 12
      scene/GameScene.tscn
  2. 11
      scene/LocalPlayer.tscn
  3. 3
      scene/Player.tscn
  4. 70
      src/CreativeBuilding.cs
  5. 11
      src/EscapeMenuAppearance.cs
  6. 18
      src/EscapeMenuMultiplayer.cs
  7. 69
      src/EscapeMenuWorld.cs
  8. 261
      src/IO/DeSerializer.Impl.cs
  9. 53
      src/IO/DeSerializer.Interfaces.cs
  10. 69
      src/IO/DeSerializerRegistry.cs
  11. 48
      src/IO/PropertyDeSerializer.cs
  12. 103
      src/IO/Save.cs
  13. 69
      src/IO/SaveRegistry.cs
  14. 13
      src/Network/IntegratedServer.cs
  15. 96
      src/Network/NetworkPackets.cs
  16. 205
      src/Network/NetworkRPC.cs
  17. 153
      src/Network/Sync.cs
  18. 45
      src/Network/SyncClient.cs
  19. 65
      src/Network/SyncRegistry.cs
  20. 73
      src/Network/SyncServer.cs
  21. 16
      src/Objects/Block.cs
  22. 60
      src/Objects/LocalPlayer.cs
  23. 77
      src/Objects/ObjectHolder.cs
  24. 121
      src/Objects/Player.cs
  25. 70
      src/Objects/SpawnRegistry.cs
  26. 58
      src/Scenes/Client.cs
  27. 18
      src/Scenes/Game.cs
  28. 119
      src/Scenes/Server.cs
  29. 46
      src/Utility/Extensions.cs
  30. 92
      src/World.cs

@ -1,9 +1,17 @@
[gd_scene load_steps=2 format=2]
[gd_scene load_steps=3 format=2]
[ext_resource path="res://src/World.cs" type="Script" id=1]
[ext_resource path="res://src/Scenes/Game.cs" type="Script" id=3]
[node name="Game" type="Node2D"]
[node name="Game" type="Node"]
pause_mode = 2
script = ExtResource( 3 )
[node name="World" type="Node" parent="."]
script = ExtResource( 1 )
PlayerContainerPath = NodePath("Players")
BlockContainerPath = NodePath("Blocks")
[node name="Players" type="Node" parent="World"]
[node name="Blocks" type="Node" parent="World"]

@ -0,0 +1,11 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://scene/Player.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Objects/LocalPlayer.cs" type="Script" id=2]
[node name="LocalPlayer" instance=ExtResource( 1 )]
script = ExtResource( 2 )
[node name="Camera" type="Camera2D" parent="." index="0"]
pause_mode = 2
current = true

@ -38,8 +38,5 @@ __meta__ = {
z_index = -5
texture = ExtResource( 2 )
[node name="Camera" type="Camera2D" parent="."]
pause_mode = 2
[node name="CreativeBuilding" type="Node2D" parent="."]
script = ExtResource( 4 )

@ -13,9 +13,11 @@ public class CreativeBuilding : Node2D
[Export] public int MaxLength { get; set; } = 6;
private Player _player;
private Texture _blockTex;
public Cursor Cursor { get; private set; }
public Player Player { get; private set; }
private BlockPos _startPos;
private Facing _direction;
private int _length;
@ -25,17 +27,18 @@ public class CreativeBuilding : Node2D
public override void _Ready()
{
_player = GetParent<Player>();
_blockTex = GD.Load<Texture>("res://gfx/block.png");
Cursor = this.GetClient()?.Cursor;
Player = GetParent<Player>();
}
public override void _PhysicsProcess(float delta)
public override void _Process(float delta)
{
if (!_player.IsLocal) return;
var client = this.GetClient();
if (!(Player is LocalPlayer)) return;
Update(); // Make sure _Draw is being called.
if (EscapeMenu.Instance.Visible || !client.Cursor.Visible)
if (EscapeMenu.Instance.Visible || !Cursor.Visible)
{ _currentMode = null; return; }
switch (_currentMode) {
@ -48,14 +51,14 @@ public class CreativeBuilding : Node2D
case BuildMode.Placing:
if (Input.IsActionJustPressed("interact_break")) _currentMode = null;
else if (!Input.IsActionPressed("interact_place")) {
if (_canBuild) this.GetClient()?.RPC(PlaceLine, _startPos, _direction, _length);
if (_canBuild) RpcId(1, nameof(PlaceLine), _startPos.X, _startPos.Y, _direction, _length);
_currentMode = null;
}
break;
case BuildMode.Breaking:
if (Input.IsActionJustPressed("interact_place")) _currentMode = null;
else if (!Input.IsActionPressed("interact_break")) {
this.GetClient()?.RPC(BreakLine, _startPos, _direction, _length);
RpcId(1, nameof(BreakLine), _startPos.X, _startPos.Y, _direction, _length);
_currentMode = null;
}
break;
@ -63,28 +66,30 @@ public class CreativeBuilding : Node2D
if (_currentMode != null) {
var start = _startPos.ToVector();
var angle = client.Cursor.Position.AngleToPoint(start); // angle_to_point appears reversed.
var angle = Cursor.Position.AngleToPoint(start); // angle_to_point appears reversed.
_direction = Facings.FromAngle(angle);
_length = Math.Min(MaxLength, Mathf.RoundToInt(start.DistanceTo(client.Cursor.Position) / 16));
_length = Math.Min(MaxLength, Mathf.RoundToInt(start.DistanceTo(Cursor.Position) / 16));
} else {
_startPos = BlockPos.FromVector(client.Cursor.Position);
_startPos = BlockPos.FromVector(Cursor.Position);
_length = 0;
}
bool IsBlockAt(BlockPos pos) => client.GetBlockAt(pos) != null;
var world = this.GetWorld();
bool IsBlockAt(BlockPos pos) => world.GetBlockAt(pos) != null;
_canBuild = !IsBlockAt(_startPos) && Facings.All.Any(pos => IsBlockAt(_startPos + pos.ToBlockPos()));
}
public override void _Draw()
{
if (!(this.GetGame() is Client client) || !client.Cursor.Visible || EscapeMenu.Instance.Visible) return;
if ((this.GetGame() is Server) || !Cursor.Visible || EscapeMenu.Instance.Visible) return;
var green = Color.FromHsv(1.0F / 3, 1.0F, 1.0F, 0.4F);
var red = Color.FromHsv(0.0F, 1.0F, 1.0F, 0.4F);
var black = new Color(0.0F, 0.0F, 0.0F, 0.65F);
var world = this.GetWorld();
foreach (var pos in GetBlockPositions(_startPos, _direction, _length)) {
var hasBlock = client.GetBlockAt(pos) != null;
var hasBlock = world.GetBlockAt(pos) != null;
var color = (_currentMode != BuildMode.Breaking)
? ((_canBuild && !hasBlock) ? green : red)
: (hasBlock ? black : red);
@ -96,33 +101,36 @@ public class CreativeBuilding : Node2D
=> Enumerable.Range(0, length + 1).Select(i => start + direction.ToBlockPos() * i);
[RPC(PacketDirection.ClientToServer)]
private static void PlaceLine(Server server, NetworkID networkID, BlockPos start, Facing direction, int length)
[Master]
private void PlaceLine(int x, int y, Facing direction, int length)
{
var player = server.GetPlayer(networkID);
// TODO: Test if starting block is valid.
if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
// TODO: Test if starting block is valid.
// FIXME: Test if there is a player in the way.
var validLocations = GetBlockPositions(start, direction, length)
.Where(pos => server.GetBlockAt(pos) == null)
.ToArray();
foreach (var pos in validLocations) {
var block = server.Spawn<Block>();
block.Position = pos;
block.Color = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
var start = new BlockPos(x, y);
var world = this.GetWorld();
foreach (var pos in GetBlockPositions(start, direction, length)) {
if (world.GetBlockAt(pos) != null) continue;
var color = Player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
world.Rpc(nameof(World.SpawnBlock), pos.X, pos.Y, color, false);
}
}
[RPC(PacketDirection.ClientToServer)]
private static void BreakLine(Server server, NetworkID networkID, BlockPos start, Facing direction, int length)
[Master]
private void BreakLine(int x, int y, Facing direction, int length)
{
// var player = server.GetPlayer(networkID);
if (Player.NetworkID != GetTree().GetRpcSenderId()) return;
// TODO: Do additional verification on the packet.
var start = new BlockPos(x, y);
var world = this.GetWorld();
foreach (var pos in GetBlockPositions(start, direction, length)) {
var block = server.GetBlockAt(pos);
var block = world.GetBlockAt(pos);
if (block?.Unbreakable != false) continue;
block.RemoveFromParent();
world.Rpc(nameof(World.Despawn), world.GetPathTo(block));
}
}
}

@ -20,11 +20,12 @@ public class EscapeMenuAppearance : CenterContainer
ColorSlider.Value = GD.Randf();
ColorPreview.Modulate = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F);
this.GetClient().Connected += () =>
this.GetClient().RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate);
// FIXME: LocalPlayer hasn't spawned yet on connection.
// var client = this.GetClient();
// client.Connected += () => client.LocalPlayer.RpcId(1,
// nameof(Player.ChangeAppearance), DisplayName.Text, ColorPreview.Modulate);
}
#pragma warning disable IDE0051
#pragma warning disable IDE1006
@ -49,8 +50,8 @@ public class EscapeMenuAppearance : CenterContainer
{
if (IsVisibleInTree()) return;
var client = this.GetClient();
// TODO: Find a better way to know if we're connected?
if (client.Status == NetworkedMultiplayerPeer.ConnectionStatus.Connected)
client.RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate);
client.LocalPlayer.RpcId(1, nameof(Player.ChangeAppearance),
DisplayName.Text, ColorPreview.Modulate);
}
}

@ -38,7 +38,7 @@ public class EscapeMenuMultiplayer : Container
private void SetupIntegratedServer()
{
IntegratedServer = new IntegratedServer();
this.GetClient().AddChild(IntegratedServer, true);
this.GetClient().AddChild(IntegratedServer);
CallDeferred(nameof(StartIntegratedServerAndConnect));
}
private void StartIntegratedServerAndConnect()
@ -77,9 +77,10 @@ public class EscapeMenuMultiplayer : Container
ServerOpenClose.Text = (IntegratedServer?.Server.IsSingleplayer == false) ? "Close Server" : "Open Server";
ClientDisConnect.Text = ((IntegratedServer != null) || (status == ConnectionStatus.Disconnected)) ? "Connect" : "Disconnect";
var pauseMode = (IntegratedServer?.Server.IsSingleplayer == true) ? PauseModeEnum.Stop : PauseModeEnum.Process;
this.GetClient().GetNode("World").PauseMode = pauseMode;
if (IntegratedServer != null) IntegratedServer.Server.GetNode("World").PauseMode = pauseMode;
var isSingleplayer = IntegratedServer?.Server.IsSingleplayer == true;
var pauseMode = isSingleplayer ? PauseModeEnum.Stop : PauseModeEnum.Process;
this.GetWorld().PauseMode = pauseMode;
if (IntegratedServer != null) IntegratedServer.Server.GetWorld().PauseMode = pauseMode;
// TODO: Allow starting up the integrated server again when disconnected.
}
@ -135,13 +136,12 @@ public class EscapeMenuMultiplayer : Container
if (IntegratedServer != null) {
IntegratedServer.Server.Stop();
// TODO: Have a single method to "reset" the state?
IntegratedServer.Server.Objects.Clear();
IntegratedServer.RemoveFromParent();
client.RemoveChild(IntegratedServer);
IntegratedServer.QueueFree();
IntegratedServer = null;
client.Disconnect();
client.Objects.Clear();
this.GetWorld().Clear();
}
if (client.Status == ConnectionStatus.Disconnected) {
@ -156,7 +156,7 @@ public class EscapeMenuMultiplayer : Container
client.Connect(address, port);
} else {
client.Disconnect();
client.Objects.Clear();
this.GetWorld().Clear();
}
}
}

@ -25,14 +25,11 @@ public class EscapeMenuWorld : CenterContainer
public FileDialog SaveFileDialog { get; private set; }
public FileDialog LoadFileDialog { get; private set; }
private Node _world;
private TimeSpan _playtime;
private string _currentWorld;
public override void _Ready()
{
_world = this.GetClient().GetNode("World");
FilenameLabel = GetNode<Label>(FilenamePath);
LastSavedLabel = GetNode<Label>(LastSavedPath);
PlaytimeLabel = GetNode<Label>(PlaytimePath);
@ -56,7 +53,7 @@ public class EscapeMenuWorld : CenterContainer
public override void _Process(float delta)
{
if (!GetTree().Paused || (_world.PauseMode != PauseModeEnum.Stop))
if (!GetTree().Paused || (this.GetWorld().PauseMode != PauseModeEnum.Stop))
_playtime += TimeSpan.FromSeconds(delta);
var b = new StringBuilder();
@ -69,7 +66,7 @@ public class EscapeMenuWorld : CenterContainer
private void OnStatusChanged(ConnectionStatus status)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer));
var server = this.GetClient().GetNodeOrNull<IntegratedServer>(nameof(IntegratedServer));
GetParent<TabContainer>().SetTabDisabled(GetIndex(), server == null);
}
@ -88,17 +85,17 @@ public class EscapeMenuWorld : CenterContainer
private void _on_SaveFileDialog_file_selected(string path)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = Save.CreateFromWorld(server, _playtime);
save.WriteToFile(path + ".tmp");
File.Delete(path); // TODO: In later .NET, there is a File.Move(source, dest, overwrite).
File.Move(path + ".tmp", path);
_currentWorld = path;
FilenameLabel.Text = Path.GetFileName(path);
LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
QuickSaveButton.Visible = true;
SaveAsButton.Text = "Save As...";
// var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
// var save = Save.CreateFromWorld(server, _playtime);
// save.WriteToFile(path + ".tmp");
// File.Delete(path); // TODO: In later .NET, there is a File.Move(source, dest, overwrite).
// File.Move(path + ".tmp", path);
// _currentWorld = path;
// FilenameLabel.Text = Path.GetFileName(path);
// LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
// QuickSaveButton.Visible = true;
// SaveAsButton.Text = "Save As...";
}
private void _on_LoadFrom_pressed()
@ -109,25 +106,25 @@ public class EscapeMenuWorld : CenterContainer
private void _on_LoadFileDialog_file_selected(string path)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = Save.ReadFromFile(path);
// Clear out all objects that have a SaveAttribute.
var objectsToRemove = server.Objects.Select(x => x.Item2)
.Where(x => SaveRegistry.GetOrNull(x.GetType()) != null).ToArray();
foreach (var obj in objectsToRemove) obj.RemoveFromParent();
// Reset players' positions.
foreach (var (id, player) in server.Players)
player.RPC(new []{ id }, player.ResetPosition, Vector2.Zero);
save.AddToWorld(server);
_playtime = save.Playtime;
_currentWorld = path;
FilenameLabel.Text = Path.GetFileName(path);
LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
QuickSaveButton.Visible = true;
SaveAsButton.Text = "Save As...";
// var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
// var save = Save.ReadFromFile(path);
// // Clear out all objects that have a SaveAttribute.
// var objectsToRemove = server.Objects.Select(x => x.Item2)
// .Where(x => SaveRegistry.GetOrNull(x.GetType()) != null).ToArray();
// foreach (var obj in objectsToRemove) obj.RemoveFromParent();
// // Reset players' positions.
// foreach (var (id, player) in server.Players)
// player.RPC(new []{ id }, player.ResetPosition, Vector2.Zero);
// save.AddToWorld(server);
// _playtime = save.Playtime;
// _currentWorld = path;
// FilenameLabel.Text = Path.GetFileName(path);
// LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
// QuickSaveButton.Visible = true;
// SaveAsButton.Text = "Save As...";
}
}

@ -1,261 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Godot;
/// <summary>
/// Implements a simple de/serializer based on a serialize and deserialize
/// function, typically specified using short lambda expressions.
/// A shortcut method for creating an instance of this class can be found at:
/// <see cref="DeSerializerRegistry.Register{T}(Action{BinaryWriter, T}, Func{BinaryReader, T})"/>.
/// </summary>
public class SimpleDeSerializer<T>
: DeSerializer<T>
{
private readonly Action<BinaryWriter, T> _serialize;
private readonly Func<BinaryReader, T> _deserialize;
public SimpleDeSerializer(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize)
{ _serialize = serialize; _deserialize = deserialize; }
public override void Serialize(Game game, BinaryWriter writer, T value) => _serialize(writer, value);
public override T Deserialize(Game game, BinaryReader reader) => _deserialize(reader);
}
public class EnumDeSerializerGenerator
: IDeSerializerGenerator
{
public IDeSerializer GenerateFor(Type type)
{
// TODO: Flagged enums are not supported at this time.
if (!type.IsEnum || (type.GetCustomAttribute<FlagsAttribute>() != null)) return null;
var deSerializerType = typeof(EnumDeSerializer<,>).MakeGenericType(type, type.GetEnumUnderlyingType());
return (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
private class EnumDeSerializer<TEnum, TUnderlying>
: DeSerializer<TEnum>
where TEnum : Enum
{
private readonly IDeSerializer<TUnderlying> _underlyingDeSerializer =
DeSerializerRegistry.Get<TUnderlying>(true);
public override void Serialize(Game game, BinaryWriter writer, TEnum value)
{
if (!Enum.IsDefined(typeof(TEnum), value)) throw new ArgumentException(
$"Invalid enum value {value} for type {typeof(TEnum)}", nameof(value));
_underlyingDeSerializer.Serialize(game, writer, (TUnderlying)(object)value);
}
public override TEnum Deserialize(Game game, BinaryReader reader)
{
var value = (TEnum)(object)_underlyingDeSerializer.Deserialize(game, reader);
if (!Enum.IsDefined(typeof(TEnum), value)) throw new ArgumentException(
$"Invalid enum value {value} for type {typeof(TEnum)}", nameof(value));
return value;
}
}
}
public class ArrayDeSerializerGenerator
: IDeSerializerGenerator
{
public IDeSerializer GenerateFor(Type type)
{
if (!type.IsArray) return null;
var deSerializerType = typeof(ArrayDeSerializer<>).MakeGenericType(type.GetElementType());
return (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
private class ArrayDeSerializer<T>
: DeSerializer<T[]>
{
private readonly IDeSerializer _elementDeSerializer =
DeSerializerRegistry.Get<T>(true);
public override void Serialize(Game game, BinaryWriter writer, T[] value)
{
writer.Write(value.Length);
foreach (var element in value)
_elementDeSerializer.Serialize(game, writer, element);
}
public override T[] Deserialize(Game game, BinaryReader reader)
{
var length = reader.ReadInt32();
var array = new T[length];
for (var i = 0; i < length; i++)
array[i] = (T)_elementDeSerializer.Deserialize(game, reader);
return array;
}
}
}
public class CollectionDeSerializerGenerator
: IDeSerializerGenerator
{
public IDeSerializer GenerateFor(Type type)
{
Type elementType;
if (type.IsInterface) {
// If the type is an interface type, specific interfaces are
// supported and will be populated with certain concrete types.
if (!type.IsGenericType) return null;
elementType = type.GetGenericArguments()[0];
var typeDef = type.GetGenericTypeDefinition();
if (typeDef == typeof(ICollection<>)) type = typeof(List<>).MakeGenericType(elementType);
else if (typeDef == typeof(IList<>)) type = typeof(List<>).MakeGenericType(elementType);
else if (typeDef == typeof(ISet<>)) type = typeof(HashSet<>).MakeGenericType(elementType);
else return null;
} else {
// An empty constructor is required.
if (type.GetConstructor(Type.EmptyTypes) == null) return null;
// Dictionaries are handled by DictionaryDeSerializerGenerator.
if (typeof(IDictionary).IsAssignableFrom(type)) return null;
elementType = type.GetInterfaces()
.Where(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(ICollection<>)))
.Select(i => i.GetGenericArguments()[0])
.FirstOrDefault();
if (elementType == null) return null;
}
var deSerializerType = typeof(CollectionDeSerializer<,>).MakeGenericType(type, elementType);
return (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
private class CollectionDeSerializer<TCollection, TElement>
: DeSerializer<TCollection>
where TCollection : ICollection<TElement>, new()
{
private readonly IDeSerializer _elementDeSerializer =
DeSerializerRegistry.Get<TElement>(true);
public override void Serialize(Game game, BinaryWriter writer, TCollection value)
{
writer.Write(value.Count);
foreach (var element in value)
_elementDeSerializer.Serialize(game, writer, element);
}
public override TCollection Deserialize(Game game, BinaryReader reader)
{
var count = reader.ReadInt32();
var collection = new TCollection();
for (var i = 0; i < count; i++)
collection.Add((TElement)_elementDeSerializer.Deserialize(game, reader));
return collection;
}
}
}
public class DictionaryDeSerializerGenerator
: IDeSerializerGenerator
{
public IDeSerializer GenerateFor(Type type)
{
Type keyType, valueType;
if (type.IsInterface) {
if (!type.IsGenericType || (type.GetGenericTypeDefinition() != typeof(IDictionary<,>))) return null;
keyType = type.GetGenericArguments()[0];
valueType = type.GetGenericArguments()[1];
type = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
} else {
// An empty constructor is required.
if (type.GetConstructor(Type.EmptyTypes) == null) return null;
(keyType, valueType) = type.GetInterfaces()
.Where(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
.Select(i => (i.GetGenericArguments()[0], i.GetGenericArguments()[1]))
.FirstOrDefault();
if (keyType == null) return null;
}
var deSerializerType = typeof(DictionaryDeSerializer<,,>).MakeGenericType(type, keyType, valueType);
return (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
private class DictionaryDeSerializer<TDictionary, TKey, TValue>
: DeSerializer<TDictionary>
where TDictionary : IDictionary<TKey, TValue>, new()
{
private readonly IDeSerializer _keyDeSerializer =
DeSerializerRegistry.Get<TKey>(true);
private readonly IDeSerializer _valueDeSerializer =
DeSerializerRegistry.Get<TKey>(true);
public override void Serialize(Game game, BinaryWriter writer, TDictionary dict)
{
writer.Write(dict.Count);
foreach (var (key, value) in dict) {
_keyDeSerializer.Serialize(game, writer, key);
_valueDeSerializer.Serialize(game, writer, value);
}
}
public override TDictionary Deserialize(Game game, BinaryReader reader)
{
var count = reader.ReadInt32();
var dictionary = new TDictionary();
for (var i = 0; i < count; i++)
dictionary.Add((TKey)_keyDeSerializer.Deserialize(game, reader),
(TValue)_valueDeSerializer.Deserialize(game, reader));
return dictionary;
}
}
}
public class NodeDeSerializerGenerator
: IDeSerializerGenerator
{
public IDeSerializer GenerateFor(Type type)
{
if (!typeof(Node).IsAssignableFrom(type)) return null;
var deSerializerType = typeof(NodeDeSerializer<>).MakeGenericType(type);
return (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
private class NodeDeSerializer<TObj>
: DeSerializer<TObj>
where TObj : Node
{
public override void Serialize(Game game, BinaryWriter writer, TObj value)
=> writer.Write(game.Objects.GetSyncID(value).Value);
public override TObj Deserialize(Game game, BinaryReader reader)
{
var id = new UniqueID(reader.ReadUInt32());
var value = (TObj)game.Objects.GetObjectByID(id);
if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}");
return value;
}
}
}
// TODO: Replace this with something that will generate code at runtime for improved performance.
public class ComplexDeSerializer<T>
: DeSerializer<T>
{
private event Action<Game, BinaryWriter, object> OnSerialize;
private event Action<Game, BinaryReader, object> OnDeserialize;
public ComplexDeSerializer()
{
foreach (var field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) {
var deSerializer = DeSerializerRegistry.Get(field.FieldType, true);
OnSerialize += (game, writer, obj) => deSerializer.Serialize(game, writer, field.GetValue(obj));
OnDeserialize += (game, reader, obj) => field.SetValue(obj, deSerializer.Deserialize(game, reader));
}
if (OnSerialize == null) throw new InvalidOperationException(
$"Unable to create {nameof(ComplexDeSerializer<T>)} for type {typeof(T)}");
}
public override void Serialize(Game game, BinaryWriter writer, T value)
=> OnSerialize(game, writer, value);
public override T Deserialize(Game game, BinaryReader reader)
{
var value = FormatterServices.GetUninitializedObject(typeof(T));
OnDeserialize(game, reader, value);
return (T)value;
}
}

@ -1,53 +0,0 @@
using System;
using System.IO;
/// <summary>
/// Basic binary de/serializer interface, capable of de/serializing a particular
/// type that it was made for. Will typically not be implemented directly, as
/// <see cref="IDeSerializer{T}"/> offers a more type-safe interface.
/// </summary>
public interface IDeSerializer
{
void Serialize(Game game, BinaryWriter writer, object value);
object Deserialize(Game game, BinaryReader reader);
}
/// <summary>
/// Basic type-safe binary de/serializer interface, capable of de/serializing
/// values of type <c>T</c>. <see cref="DeSerializer{T}"/> offers an abstract
/// implementation that already implements <see cref="IDeSerializer"/> methods
/// so you only have to use the type-safe ones.
/// </summary>
public interface IDeSerializer<T>
: IDeSerializer
{
void Serialize(Game game, BinaryWriter writer, T value);
new T Deserialize(Game game, BinaryReader reader);
}
// TODO: Using C# 8 this could be done with default interface implementations on IDeSerializer<>.
public abstract class DeSerializer<T>
: IDeSerializer<T>
{
public abstract void Serialize(Game game, BinaryWriter writer, T value);
public abstract T Deserialize(Game game, BinaryReader reader);
void IDeSerializer.Serialize(Game game, BinaryWriter writer, object value)
=> Serialize(game, writer, (T)value);
object IDeSerializer.Deserialize(Game game, BinaryReader reader)
=> Deserialize(game, reader);
}
/// <summary>
/// This interface allows the dynamic creation of <see cref="IDeSerializer"/>
/// implementations that cannot be covered by simple de/serializer implementations,
/// such as when generics come into play.
/// </summary>
/// <seealso cref="EnumDeSerializerGenerator"/>
/// <seealso cref="ArrayDeSerializerGenerator"/>
/// <seealso cref="CollectionDeSerializerGenerator"/>
/// <seealso cref="DictionaryDeSerializerGenerator"/>
public interface IDeSerializerGenerator
{
IDeSerializer GenerateFor(Type type);
}

@ -1,69 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
public static class DeSerializerRegistry
{
private static readonly Dictionary<Type, IDeSerializer> _byType = new Dictionary<Type, IDeSerializer>();
private static readonly List<IDeSerializerGenerator> _generators = new List<IDeSerializerGenerator>();
static DeSerializerRegistry()
{
Register((w, value) => w.Write(value), r => r.ReadBoolean());
Register((w, value) => w.Write(value), r => r.ReadByte());
Register((w, value) => w.Write(value), r => r.ReadSByte());
Register((w, value) => w.Write(value), r => r.ReadInt16());
Register((w, value) => w.Write(value), r => r.ReadUInt16());
Register((w, value) => w.Write(value), r => r.ReadInt32());
Register((w, value) => w.Write(value), r => r.ReadUInt32());
Register((w, value) => w.Write(value), r => r.ReadInt64());
Register((w, value) => w.Write(value), r => r.ReadUInt64());
Register((w, value) => w.Write(value), r => r.ReadSingle());
Register((w, value) => w.Write(value), r => r.ReadDouble());
Register((w, value) => w.Write(value), r => r.ReadString());
// byte[]
Register((w, value) => { w.Write((ushort)value.Length); w.Write(value); },
r => r.ReadBytes(r.ReadUInt16()));
// Vector2
Register((w, value) => { w.Write(value.x); w.Write(value.y); },
r => new Vector2(r.ReadSingle(), r.ReadSingle()));
// Color
Register((w, value) => w.Write(value.ToRgba32()),
r => new Color(r.ReadInt32()));
RegisterGenerator(new EnumDeSerializerGenerator());
RegisterGenerator(new ArrayDeSerializerGenerator());
RegisterGenerator(new CollectionDeSerializerGenerator());
RegisterGenerator(new DictionaryDeSerializerGenerator());
RegisterGenerator(new NodeDeSerializerGenerator());
}
public static void Register<T>(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize)
=> Register(new SimpleDeSerializer<T>(serialize, deserialize));
public static void Register<T>(IDeSerializer<T> deSerializer)
=> _byType.Add(typeof(T), deSerializer);
public static void RegisterGenerator(IDeSerializerGenerator deSerializerGenerator)
=> _generators.Add(deSerializerGenerator);
public static IDeSerializer<T> Get<T>(bool createIfMissing)
=> (IDeSerializer<T>)Get(typeof(T), createIfMissing);
public static IDeSerializer Get(Type type, bool createIfMissing)
{
if (!_byType.TryGetValue(type, out var value)) {
if (!createIfMissing) throw new InvalidOperationException(
$"No DeSerializer for type {type} found");
value = _generators.Select(g => g.GenerateFor(type))
.FirstOrDefault(x => x != null);
if (value == null) {
var deSerializerType = typeof(ComplexDeSerializer<>).MakeGenericType(type);
value = (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
_byType.Add(type, value);
}
return value;
}
}

@ -1,48 +0,0 @@
using System;
using System.Reflection;
public interface IPropertyDeSerializer
{
PropertyInfo Property { get; }
Type Type { get; }
string Name { get; }
string FullName { get; }
int HashID { get; }
IDeSerializer DeSerializer { get; }
object Get(object obj);
void Set(object obj, object value);
}
public class PropertyDeSerializer<TObj, TProp>
: IPropertyDeSerializer
{
public PropertyInfo Property { get; }
public Type Type => Property.PropertyType;
public string Name => Property.Name;
public string FullName { get; }
public int HashID { get; }
public IDeSerializer<TProp> DeSerializer { get; }
public Func<TObj, TProp> Getter { get; }
public Action<TObj, TProp> Setter { get; }
public PropertyDeSerializer(PropertyInfo property)
{
if ((property.GetMethod == null) || (property.SetMethod == null)) throw new Exception(
$"Property {property.DeclaringType}.{property.Name} must have a getter and setter defined");
Property = property;
FullName = $"{Property.DeclaringType.FullName}.{Property.Name}";
HashID = FullName.GetDeterministicHashCode();
DeSerializer = DeSerializerRegistry.Get<TProp>(true);
Getter = (Func<TObj, TProp>)Property.GetMethod.CreateDelegate(typeof(Func<TObj, TProp>));
Setter = (Action<TObj, TProp>)Property.SetMethod.CreateDelegate(typeof(Action<TObj, TProp>));
}
// IPropertyDeSerializer implementation
IDeSerializer IPropertyDeSerializer.DeSerializer => DeSerializer;
object IPropertyDeSerializer.Get(object obj) => Getter((TObj)obj);
void IPropertyDeSerializer.Set(object obj, object value) => Setter((TObj)obj, (TProp)value);
}

@ -1,103 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
using File = System.IO.File;
public class Save
{
public const string FILE_EXT = ".yf5";
public const int MAGIC_NUMBER = 0x59463573; // "YF5s"
public const int CURRENT_VERSION = 0;
public int Version { get; private set; }
public DateTime LastSaved { get; private set; }
public TimeSpan Playtime { get; set; }
public List<(SaveObjectInfo, List<object>)> Objects { get; private set; }
public static Save ReadFromFile(string path)
{
var save = new Save { LastSaved = File.GetLastAccessTime(path) };
using (var stream = File.OpenRead(path)) {
using (var reader = new BinaryReader(stream)) {
var magic = reader.ReadInt32();
if (magic != MAGIC_NUMBER) throw new IOException(
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})");
// TODO: See how to support multiple versions.
save.Version = reader.ReadUInt16();
if (save.Version != CURRENT_VERSION) throw new IOException(
$"Version does not match ({save.Version} != {CURRENT_VERSION})");
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32());
var numObjects = reader.ReadInt32();
save.Objects = new List<(SaveObjectInfo, List<object>)>(numObjects);
for (var i = 0; i < numObjects; i++) {
var hashID = reader.ReadInt32();
var objInfo = SaveRegistry.GetOrThrow(hashID);
var props = objInfo.PropertiesByID.Select(x => x.DeSerializer.Deserialize(null, reader)).ToList();
save.Objects.Add((objInfo, props));
}
}
}
return save;
}
public void WriteToFile(string path)
{
using (var stream = File.OpenWrite(path)) {
using (var writer = new BinaryWriter(stream)) {
writer.Write(MAGIC_NUMBER);
writer.Write((ushort)CURRENT_VERSION);
writer.Write((uint)Playtime.TotalSeconds);
writer.Write(Objects.Count);
foreach (var (objInfo, props) in Objects) {
writer.Write(objInfo.HashID);
for (var i = 0; i < objInfo.PropertiesByID.Count; i++) {
var propInfo = objInfo.PropertiesByID[i];
var propValue = props[i];
propInfo.DeSerializer.Serialize(null, writer, propValue);
}
}
}
}
LastSaved = File.GetLastAccessTime(path);
}
public static Save CreateFromWorld(Game game, TimeSpan playtime)
{
var save = new Save {
Playtime = playtime,
Objects = new List<(SaveObjectInfo, List<object>)>(),
};
foreach (var (id, obj) in game.Objects) {
var objInfo = SaveRegistry.GetOrNull(obj.GetType());
if (objInfo == null) continue;
var props = objInfo.PropertiesByID.Select(x => x.Get(obj)).ToList();
save.Objects.Add((objInfo, props));
}
return save;
}
public void AddToWorld(Server server)
{
foreach (var (objInfo, props) in Objects) {
var obj = objInfo.SpawnInfo.Scene.Init<Node>();
server.GetNode("World").AddChild(obj, true);
server.Objects.Add(null, obj);
for (var i = 0; i < objInfo.PropertiesByID.Count; i++) {
var propInfo = objInfo.PropertiesByID[i];
var propValue = props[i];
propInfo.Set(obj, propValue);
}
}
}
}

@ -1,69 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Godot;
public static class SaveRegistry
{
private static readonly Dictionary<int, SaveObjectInfo> _byID = new Dictionary<int, SaveObjectInfo>();
private static readonly Dictionary<Type, SaveObjectInfo> _byType = new Dictionary<Type, SaveObjectInfo>();
static SaveRegistry()
{
foreach (var type in typeof(SyncRegistry).Assembly.GetTypes()) {
var syncAttr = type.GetCustomAttribute<SaveAttribute>();
if (syncAttr == null) continue;
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception(
$"Type {type} with {nameof(SyncAttribute)} must be a subclass of {nameof(Node)}");
var spawnInfo = SpawnRegistry.Get(type);
var objInfo = new SaveObjectInfo(type, spawnInfo);
foreach (var property in type.GetProperties()) {
if (property.GetCustomAttribute<SaveAttribute>() == null) continue;
var propType = typeof(PropertyDeSerializer<,>).MakeGenericType(type, property.PropertyType);
var propDeSerializer = (IPropertyDeSerializer)Activator.CreateInstance(propType, property);
objInfo.PropertiesByName.Add(propDeSerializer.Name, propDeSerializer);
}
objInfo.PropertiesByID.AddRange(objInfo.PropertiesByName.Values.OrderBy(x => x.HashID));
_byID.Add(objInfo.HashID, objInfo);
_byType.Add(objInfo.Type, objInfo);
}
}
public static SaveObjectInfo GetOrNull(int hashID)
=> _byID.TryGetValue(hashID, out var value) ? value : null;
public static SaveObjectInfo GetOrThrow(int hashID) => GetOrNull(hashID)
?? throw new Exception($"Unknown {nameof(SaveObjectInfo)} with HashID {hashID}");
public static SaveObjectInfo GetOrNull<T>() => GetOrNull(typeof(T));
public static SaveObjectInfo GetOrNull(Type type) => _byType.TryGetValue(type, out var value) ? value : null;
public static SaveObjectInfo GetOrThrow<T>() => GetOrThrow(typeof(T));
public static SaveObjectInfo GetOrThrow(Type type) => GetOrNull(type) ?? throw new Exception(
$"No {nameof(SaveObjectInfo)} found for type {type} (missing {nameof(SyncAttribute)}?)");
}
public class SaveObjectInfo
{
public Type Type { get; }
public int HashID { get; }
public SpawnInfo SpawnInfo { get; }
public string Name => Type.Name;
public List<IPropertyDeSerializer> PropertiesByID { get; } = new List<IPropertyDeSerializer>();
public Dictionary<string, IPropertyDeSerializer> PropertiesByName { get; } = new Dictionary<string, IPropertyDeSerializer>();
public SaveObjectInfo(Type type, SpawnInfo spawnInfo)
{
Type = type;
HashID = type.FullName.GetDeterministicHashCode();
SpawnInfo = spawnInfo;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class SaveAttribute : Attribute { }

@ -5,25 +5,26 @@ public class IntegratedServer : Node
private SceneTree _sceneTree;
public Server Server { get; private set; }
public IntegratedServer() => Name = "IntegratedServer";
public IntegratedServer()
=> Name = "IntegratedServer";
public override void _Ready()
{
_sceneTree = new SceneTree();
_sceneTree.Init();
_sceneTree.Root.RenderTargetUpdateMode = Godot.Viewport.UpdateMode.Disabled;
// VisualServer.ViewportSetActive(_sceneTree.Root.GetViewportRid(), false);
var scene = GD.Load<PackedScene>("res://scene/ServerScene.tscn").Init<Server>();
_sceneTree.Root.AddChild(scene, true);
_sceneTree.CurrentScene = scene;
Server = _sceneTree.Root.GetChild<Server>(0);
// Spawn default blocks.
var world = Server.GetWorld();
for (var x = -6; x <= 6; x++) {
var block = Server.Spawn<Block>();
block.Position = new BlockPos(x, 3);
block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
block.Unbreakable = true;
var color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
world.SpawnBlock(x, 3, color, true);
}
}

@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
[Flags]
public enum PacketDirection
{
ServerToClient = 0b01,
ClientToServer = 0b10,
Both = ServerToClient | ClientToServer,
}
public static class NetworkPackets
{
private static readonly List<PacketInfo> _packetsById = new List<PacketInfo>();
private static readonly Dictionary<Type, PacketInfo> _packetsByType = new Dictionary<Type, PacketInfo>();
public static void Register<T>(PacketDirection direction, Action<Game, NetworkID, T> onReceived)
{
var info = new PacketInfo((byte)_packetsById.Count, typeof(T),
direction, (game, networkID, packet) => onReceived(game, networkID, (T)packet));
_packetsByType.Add(typeof(T), info);
_packetsById.Add(info);
}
private static byte[] ToBytes(Game game, PacketInfo info, object packet)
{
using (var stream = new MemoryStream()) {
using (var writer = new BinaryWriter(stream)) {
writer.Write(info.ID);
info.DeSerializer.Serialize(game, writer, packet);
}
return stream.ToArray();
}
}
public static void Send<T>(Game game, IEnumerable<NetworkID> targets, T packet)
{
if (!_packetsByType.TryGetValue(typeof(T), out var info))
throw new InvalidOperationException($"No packet of type {typeof(T)} has been registered");
var direction = (game is Server) ? PacketDirection.ServerToClient : PacketDirection.ClientToServer;
if ((direction & info.Direction) == 0) throw new InvalidOperationException(
$"Attempting to send packet {typeof(T)} in invalid direction {direction}");
byte[] bytes = null;
foreach (var networkID in targets) {
// Only serialize the packet if sending to at least 1 player.
bytes = bytes ?? ToBytes(game, info, packet);
game.CustomMultiplayer.SendBytes(bytes, networkID.Value,
Godot.NetworkedMultiplayerPeer.TransferModeEnum.Reliable);
// TODO: Should it be possible to send packets in non-reliable modes?
}
}
public static void Process(Game game, NetworkID networkID, byte[] bytes)
{
using (var stream = new MemoryStream(bytes)) {
using (var reader = new BinaryReader(stream)) {
var packetId = reader.ReadByte();
if (packetId >= _packetsById.Count) throw new Exception(
$"Received packet with invalid ID {packetId}");
var info = _packetsById[packetId];
var validDirection = (game is Server) ? PacketDirection.ClientToServer : PacketDirection.ServerToClient;
if ((validDirection & info.Direction) == 0) throw new Exception($"Received packet {info.Type} on invalid side {game.Name}");
var packet = info.DeSerializer.Deserialize(game, reader);
var bytesLeft = bytes.Length - stream.Position;
if (bytesLeft > 0) throw new Exception(
$"There were {bytesLeft} bytes left after deserializing packet {info.Type}");
info.OnReceived(game, networkID, packet);
}
}
}
public class PacketInfo
{
public byte ID { get; }
public Type Type { get; }
public PacketDirection Direction { get; }
public Action<Game, NetworkID, object> OnReceived { get; }
public IDeSerializer DeSerializer { get; }
public PacketInfo(byte id, Type type,
PacketDirection direction, Action<Game, NetworkID, object> onReceived)
{
ID = id;
Type = type;
Direction = direction;
OnReceived = onReceived;
DeSerializer = DeSerializerRegistry.Get(type, true);
}
}
}

@ -1,205 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Godot;
using static Godot.NetworkedMultiplayerPeer;
public static class NetworkRPC
{
private static readonly Dictionary<int, RPCMethodInfo> _byId = new Dictionary<int, RPCMethodInfo>();
private static readonly Dictionary<MethodInfo, RPCMethodInfo> _byMethod = new Dictionary<MethodInfo, RPCMethodInfo>();
private static readonly List<(NetworkID[], RPCPacket)> _serverPacketBuffer = new List<(NetworkID[], RPCPacket)>();
private static readonly List<RPCPacket> _clientPacketBuffer = new List<RPCPacket>();
static NetworkRPC()
{
DiscoverRPCMethods();
RegisterPackets();
}
// Client to server instance RPC calls.
public static void RPC(this Node obj, Action<Player> action) => CallToServer(obj, action.Method);
public static void RPC<T>(this Node obj, Action<Player, T> action, T arg) => CallToServer(obj, action.Method, arg);
public static void RPC<T0, T1>(this Node obj, Action<Player, T0, T1> action, T0 arg0, T1 arg1) => CallToServer(obj, action.Method, arg0, arg1);
public static void RPC<T0, T1, T2>(this Node obj, Action<Player, T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) => CallToServer(obj, action.Method, arg0, arg1, arg2);
public static void RPC<T0, T1, T2, T3>(this Node obj, Action<Player, T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3);
public static void RPC<T0, T1, T2, T3, T4>(this Node obj, Action<Player, T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4);
public static void RPC<T0, T1, T2, T3, T4, T5>(this Node obj, Action<Player, T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4, arg5);
public static void RPC<T0, T1, T2, T3, T4, T5, T6>(this Node obj, Action<Player, T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
public static void RPC<T0, T1, T2, T3, T4, T5, T6, T7>(this Node obj, Action<Player, T0, T1, T2, T3, T4, T5, T6, T7> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
// Server to client instance RPC calls.
public static void RPC(this Node obj, IEnumerable<NetworkID> targets, Action action) => CallToClient(obj, targets, action.Method);
public static void RPC<T>(this Node obj, IEnumerable<NetworkID> targets, Action<T> action, T arg) => CallToClient(obj, targets, action.Method, arg);
public static void RPC<T0, T1>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1> action, T0 arg0, T1 arg1) => CallToClient(obj, targets, action.Method, arg0, arg1);
public static void RPC<T0, T1, T2>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2);
public static void RPC<T0, T1, T2, T3>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3);
public static void RPC<T0, T1, T2, T3, T4>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4);
public static void RPC<T0, T1, T2, T3, T4, T5>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4, arg5);
public static void RPC<T0, T1, T2, T3, T4, T5, T6>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
public static void RPC<T0, T1, T2, T3, T4, T5, T6, T7>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4, T5, T6, T7> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
private static void CallToServer(Node obj, MethodInfo method, params object[] args)
{ if (obj.GetGame() is Client) Call(obj.GetGame(), new []{ NetworkID.Server }, method, true, args.Prepend(obj)); }
private static void CallToClient(Node obj, IEnumerable<NetworkID> targets, MethodInfo method, params object[] args)
{ if (obj.GetGame() is Server) Call(obj.GetGame(), targets, method, true, args.Prepend(obj)); }
internal static void Call(Game game, IEnumerable<NetworkID> targets, MethodInfo method, bool isInstance, params object[] args)
=> Call(game, targets, method, isInstance, (IEnumerable<object>)args);
internal static void Call(Game game, IEnumerable<NetworkID> targets, MethodInfo method, bool isInstance, IEnumerable<object> args)
{
if (!_byMethod.TryGetValue(method, out var info)) throw new ArgumentException(
$"The specified method {method.DeclaringType}.{method.Name} is missing {nameof(RPCAttribute)}", nameof(method));
if (isInstance == method.IsStatic) throw new ArgumentException(
$"The specified method {method.DeclaringType}.{method.Name} must be {(isInstance ? "non-static" : "static")} for this RPC call", nameof(method));
// TODO: Make sure the instance is the right type.
var direction = (game is Server) ? PacketDirection.ServerToClient : PacketDirection.ClientToServer;
if (info.Attribute.Direction != direction) throw new Exception(
$"Sending {info.Attribute.Direction} RPC packet '{info.Name}' from {game.Name}");
var packet = new RPCPacket(info, new List<object>(args));
if (game is Server) _serverPacketBuffer.Add((targets.ToArray(), packet));
else _clientPacketBuffer.Add(packet);
}
internal static void ProcessPacketBuffer(Game game)
{
if (game is Server) {
foreach (var (targets, packet) in _serverPacketBuffer)
NetworkPackets.Send(game, targets, packet);
_serverPacketBuffer.Clear();
} else {
foreach (var packet in _clientPacketBuffer)
NetworkPackets.Send(game, new []{ NetworkID.Server }, packet);
_clientPacketBuffer.Clear();
}
}
private static void DiscoverRPCMethods()
{
foreach (var type in typeof(NetworkRPC).Assembly.GetTypes()) {
foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) {
var rpc = method.GetCustomAttribute<RPCAttribute>();
if (rpc == null) continue;
if (!method.IsStatic && (type.GetCustomAttribute<SyncAttribute>() == null)) throw new Exception(
$"Type of non-static RPC method '{method.DeclaringType}.{method.Name}' must have {nameof(SyncAttribute)}");
var deSerializers = new List<IDeSerializer>();
var paramEnumerable = ((IEnumerable<ParameterInfo>)method.GetParameters()).GetEnumerator();
var isServer = rpc.Direction == PacketDirection.ClientToServer;
var gameType = isServer ? typeof(Server) : typeof(Client);
if (method.IsStatic && (!paramEnumerable.MoveNext() || (paramEnumerable.Current.ParameterType != gameType))) throw new Exception(
$"First parameter of {rpc.Direction} RPC method '{method.DeclaringType}.{method.Name}' must be {gameType}");
if (isServer && (!paramEnumerable.MoveNext() || (paramEnumerable.Current.ParameterType != typeof(NetworkID)))) throw new Exception(
$"{(method.IsStatic ? "Second" : "First")} parameter of {rpc.Direction} RPC method '{method.DeclaringType}.{method.Name}' must be {nameof(NetworkID)}");
if (!method.IsStatic)
deSerializers.Add(DeSerializerRegistry.Get(type, true));
while (paramEnumerable.MoveNext()) {
var param = paramEnumerable.Current;
var deSerializer = DeSerializerRegistry.Get(param.ParameterType, true);
deSerializers.Add(deSerializer);
}
var info = new RPCMethodInfo(method, deSerializers);
_byId.Add(info.ID, info);
_byMethod.Add(method, info);
}
}
}
private class RPCMethodInfo
{
public string Name { get; }
public int ID { get; }
public MethodInfo Method { get; }
public RPCAttribute Attribute { get; }
public List<IDeSerializer> DeSerializers { get; }
public RPCMethodInfo(MethodInfo method, List<IDeSerializer> deSerializers)
{
Name = $"{method.DeclaringType}.{method.Name}";
ID = Name.GetHashCode();
Method = method;
Attribute = method.GetCustomAttribute<RPCAttribute>();
DeSerializers = deSerializers;
}
}
private static void RegisterPackets()
{
DeSerializerRegistry.Register(new RPCPacketDeSerializer());
NetworkPackets.Register<RPCPacket>(PacketDirection.Both, (game, networkID, packet) => {
var validDirection = (game is Server) ? PacketDirection.ClientToServer : PacketDirection.ServerToClient;
if (packet.Info.Attribute.Direction != validDirection) throw new Exception(
$"Received {packet.Info.Attribute.Direction} RPC packet '{packet.Info.Name}' on side {game.Name}");
Node obj = null;
IEnumerable<object> args = packet.Args;
// If method is instance method, the first argument is the object it is called on.
if (!packet.Info.Method.IsStatic) { obj = (Node)args.First(); args = args.Skip(1); }
// If RPC is called on the server, prepend the NetworkID of the client.
if (game is Server) args = args.Prepend(networkID);
// If method is static, prepend Client/Server to arguments.
if (packet.Info.Method.IsStatic) args = args.Prepend(game);
// TODO: Improve type safety and performance - generate packet for each RPC?
packet.Info.Method.Invoke(obj, args.ToArray());
});
}
private class RPCPacket
{
public RPCMethodInfo Info { get; }
public List<object> Args { get; }
public RPCPacket(RPCMethodInfo info, List<object> args)
{ Info = info; Args = args; }
}
private class RPCPacketDeSerializer
: DeSerializer<RPCPacket>
{
public override void Serialize(Game game, BinaryWriter writer, RPCPacket value)
{
writer.Write(value.Info.ID);
foreach (var (deSerializer, arg) in value.Info.DeSerializers.Zip(value.Args, Tuple.Create))
deSerializer.Serialize(game, writer, arg);
}
public override RPCPacket Deserialize(Game game, BinaryReader reader)
{
var id = reader.ReadInt32();
if (!_byId.TryGetValue(id, out var info)) throw new Exception($"Unknown RPC ID {id}");
var args = info.DeSerializers.Select(x => x.Deserialize(game, reader)).ToList();
return new RPCPacket(info, args);
}
}
}
[AttributeUsage(AttributeTargets.Method)]
public class RPCAttribute : Attribute
{
public PacketDirection Direction { get; }
public TransferModeEnum TransferMode { get; set; }
public RPCAttribute(PacketDirection direction) {
switch (direction) {
case PacketDirection.ServerToClient:
case PacketDirection.ClientToServer: Direction = direction; break;
default: throw new ArgumentException(
$"Direction must be either ServerToClient or ClientToServer.");
}
}
}

@ -1,153 +0,0 @@
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 abstract class Sync
{
protected Game Game { get; }
protected Dictionary<UniqueID, SyncStatus> StatusByID { get; } = new Dictionary<UniqueID, SyncStatus>();
protected Dictionary<Node, SyncStatus> StatusByObject { get; } = new Dictionary<Node, SyncStatus>();
static Sync()
=> DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer());
public Sync(Game game)
{
Game = game;
Game.Objects.Added += OnObjectAdded;
Game.Objects.Removed += OnObjectRemoved;
Game.Objects.Cleared += OnObjectsCleared;
}
private void OnObjectAdded(UniqueID id, Node obj)
{
var info = SyncRegistry.GetOrNull(obj.GetType());
if (info == null) return;
var status = new SyncStatus(id, obj, info);
StatusByID.Add(id, status);
StatusByObject.Add(obj, status);
OnSyncedAdded(status);
}
private void OnObjectRemoved(UniqueID id, Node obj)
{
if (!StatusByObject.TryGetValue(obj, out var status)) return;
StatusByID.Remove(status.ID);
StatusByObject.Remove(status.Object);
OnSyncedRemoved(status);
}
protected virtual void OnSyncedAdded(SyncStatus status) { }
protected virtual void OnSyncedRemoved(SyncStatus status) { }
private void OnObjectsCleared()
{
StatusByID.Clear();
StatusByObject.Clear();
}
public SyncStatus GetStatusOrNull(UniqueID id)
=> StatusByID.TryGetValue(id, out var value) ? value : null;
public SyncStatus GetStatusOrThrow(UniqueID id)
=> GetStatusOrNull(id) ?? throw new Exception(
$"No {nameof(SyncStatus)} found for ID {id}");
public SyncStatus GetStatusOrNull(Node obj)
{
if (obj.GetType().GetCustomAttribute<SyncAttribute>() == null)
throw new ArgumentException($"Type {obj.GetType()} is missing {nameof(SyncAttribute)}");
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 class SyncStatus
{
public UniqueID ID { get; }
public Node Object { get; }
public SyncObjectInfo Info { get; }
public int DirtyProperties { get; set; }
public SyncMode Mode { get; set; }
public SyncStatus(UniqueID id, Node obj, SyncObjectInfo info)
{ ID = id; 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 UniqueID ID { get; }
public SyncMode Mode { get; }
public List<(byte, object)> Values { get; }
public Object(ushort infoID, UniqueID id, SyncMode mode, List<(byte, object)> values)
{ InfoID = infoID; ID = id; 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.ID.Value);
writer.Write((byte)value.Mode);
writer.Write((byte)value.Values.Count);
var objInfo = SyncRegistry.GetOrThrow(value.InfoID);
foreach (var (propID, val) in value.Values) {
writer.Write(propID);
var propInfo = objInfo.PropertiesByID[propID];
propInfo.DeSerializer.Serialize(game, writer, val);
}
}
public override SyncPacket.Object Deserialize(Game game, BinaryReader reader)
{
var infoID = reader.ReadUInt16();
var id = new UniqueID(reader.ReadUInt32());
var mode = (SyncMode)reader.ReadByte();
var count = reader.ReadByte();
var objInfo = SyncRegistry.GetOrThrow(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}");
values.Add((propID, propInfo.DeSerializer.Deserialize(game, reader)));
}
return new SyncPacket.Object(infoID, id, mode, values);
}
}

@ -1,45 +0,0 @@
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.GetOrThrow(packetObj.InfoID);
var status = GetStatusOrNull(packetObj.ID);
if (status == null) {
if (packetObj.Mode != SyncMode.Spawn) throw new Exception(
$"Unknown synced object {info.Name} (ID {packetObj.ID})");
var obj = info.SpawnInfo.Scene.Init<Node>();
Client.GetNode("World").AddChild(obj, true);
Client.Objects.Add(packetObj.ID, obj);
status = GetStatusOrThrow(packetObj.ID);
} else {
if (packetObj.Mode == SyncMode.Spawn) throw new Exception(
$"Spawning object {info.Name} with ID {packetObj.ID}, 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) {
status.Object.RemoveFromParent();
continue;
}
}
foreach (var (propID, value) in packetObj.Values) {
var propDeSerializer = info.PropertiesByID[propID];
propDeSerializer.Set(status.Object, value);
}
}
}
}

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
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 syncAttr = type.GetCustomAttribute<SyncAttribute>();
if (syncAttr == null) continue;
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception(
$"Type {type} with {nameof(SyncAttribute)} must be a subclass of {nameof(Node)}");
var spawnInfo = SpawnRegistry.Get(type);
var objInfo = new SyncObjectInfo(type, spawnInfo);
foreach (var property in type.GetProperties()) {
if (property.GetCustomAttribute<SyncAttribute>() == null) continue;
var propType = typeof(PropertyDeSerializer<,>).MakeGenericType(type, property.PropertyType);
var propDeSerializer = (IPropertyDeSerializer)Activator.CreateInstance(propType, property);
objInfo.PropertiesByName.Add(propDeSerializer.Name, propDeSerializer);
}
objInfo.PropertiesByID.AddRange(objInfo.PropertiesByName.Values.OrderBy(x => x.HashID));
_byType.Add(objInfo.Type, objInfo);
}
_byID.AddRange(_byType.Values.OrderBy(x => x.Name.GetDeterministicHashCode()));
for (ushort i = 0; i < _byID.Count; i++) _byID[i].ID = i;
}
public static SyncObjectInfo GetOrThrow(ushort id)
=> (id < _byID.Count) ? _byID[id] : throw new Exception(
$"Unknown {nameof(SyncObjectInfo)} with ID {id}");
public static SyncObjectInfo GetOrNull<T>() => GetOrNull(typeof(T));
public static SyncObjectInfo GetOrNull(Type type) => _byType.TryGetValue(type, out var value) ? value : null;
public static SyncObjectInfo GetOrThrow<T>() => GetOrThrow(typeof(T));
public static SyncObjectInfo GetOrThrow(Type type) => GetOrNull(type) ?? throw new Exception(
$"No {nameof(SyncObjectInfo)} found for type {type} (missing {nameof(SyncAttribute)}?)");
}
public class SyncObjectInfo
{
public ushort ID { get; internal set; }
public Type Type { get; }
public SpawnInfo SpawnInfo { get; }
public string Name => Type.Name;
public List<IPropertyDeSerializer> PropertiesByID { get; } = new List<IPropertyDeSerializer>();
public Dictionary<string, IPropertyDeSerializer> PropertiesByName { get; } = new Dictionary<string, IPropertyDeSerializer>();
public SyncObjectInfo(Type type, SpawnInfo spawnInfo)
{ Type = type; SpawnInfo = spawnInfo; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class SyncAttribute : Attribute { }

@ -1,73 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
public class SyncServer : Sync
{
private static readonly HashSet<SyncStatus> _dirtyObjects = new HashSet<SyncStatus>();
protected Server Server => (Server)Game;
public SyncServer(Server server) : base(server)
=> server.Objects.Cleared += _dirtyObjects.Clear;
protected override void OnSyncedAdded(SyncStatus status)
{
status.Mode = SyncMode.Spawn;
_dirtyObjects.Add(status);
}
protected override void OnSyncedRemoved(SyncStatus status)
{
status.Mode = SyncMode.Destroy;
_dirtyObjects.Add(status);
}
public void MarkDirty(Node obj, string property)
{
var status = GetStatusOrThrow(obj);
if (!status.Info.PropertiesByName.TryGetValue(property, out var propDeSerializer)) throw new ArgumentException(
$"No {nameof(IPropertyDeSerializer)} found for {obj.GetType()}.{property} (missing {nameof(SyncAttribute)}?)", nameof(property));
if (!(obj.GetGame() is Server)) return;
var index = status.Info.PropertiesByID.IndexOf(propDeSerializer);
status.DirtyProperties |= 1 << index;
_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)>();
if (status.Mode != SyncMode.Destroy)
for (byte i = 0; i < status.Info.PropertiesByID.Count; i++)
if ((status.DirtyProperties & (1 << i)) != 0)
values.Add((i, status.Info.PropertiesByID[i].Get(status.Object)));
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, 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)>();
for (byte i = 0; i < status.Info.PropertiesByID.Count; i++)
values.Add((i, status.Info.PropertiesByID[i].Get(status.Object)));
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, SyncMode.Spawn, values));
}
NetworkPackets.Send(server, new []{ networkID }, packet);
}
}

@ -1,19 +1,9 @@
using Godot;
[Spawn, Sync, Save]
public class Block : StaticBody2D
{
[Sync, Save]
public new BlockPos Position {
get => BlockPos.FromVector(base.Position);
set => base.Position = this.SetSync(value).ToVector();
}
[Sync, Save]
public Color Color {
get => Modulate;
set => Modulate = this.SetSync(value);
}
public new BlockPos Position { get => BlockPos.FromVector(base.Position);
set => base.Position = value.ToVector(); }
public Color Color { get => Modulate; set => Modulate = value; }
public bool Unbreakable { get; set; } = false;
}

@ -0,0 +1,60 @@
using System;
using Godot;
public class LocalPlayer : Player
{
// TODO: Implement "low jumps" activated by releasing the jump button early.
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F);
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F);
public float MovementSpeed { get; set; } = 160;
public float JumpVelocity { get; set; } = 240;
public float Gravity { get; set; } = 480;
public float Acceleration { get; set; } = 0.25F;
public float GroundFriction { get; set; } = 0.2F;
public float AirFriction { get; set; } = 0.05F;
public Vector2 Velocity = Vector2.Zero;
private DateTime? _jumpPressed = null;
private DateTime? _lastOnFloor = null;
public override void _Process(float delta)
{
base._Process(delta);
RpcUnreliableId(1, nameof(Player.Move), Position);
}
public override void _PhysicsProcess(float delta)
{
var moveDir = 0.0F;
var jumpPressed = false;
if (!EscapeMenu.Instance.Visible) {
moveDir = Input.GetActionStrength("move_right") - Input.GetActionStrength("move_left");
jumpPressed = Input.IsActionJustPressed("move_jump");
}
var friction = IsOnFloor() ? GroundFriction : AirFriction;
Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * MovementSpeed, Acceleration)
: Mathf.Lerp(Velocity.x, 0, friction);
Velocity.y += Gravity * delta;
Velocity = MoveAndSlide(Velocity, Vector2.Up);
if (jumpPressed) _jumpPressed = DateTime.Now;
if (IsOnFloor()) _lastOnFloor = DateTime.Now;
if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) &&
((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) {
Velocity.y = -JumpVelocity;
_jumpPressed = null;
_lastOnFloor = null;
}
}
[Puppet]
public void ResetPosition(Vector2 position)
{
Position = position;
Velocity = Vector2.Zero;
}
}

@ -1,77 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Godot;
public class ObjectHolder : IReadOnlyCollection<(UniqueID, Node)>
{
private readonly Dictionary<UniqueID, Node> _nodeByID = new Dictionary<UniqueID, Node>();
private readonly Dictionary<Node, UniqueID> _idByNode = new Dictionary<Node, UniqueID>();
private uint _newIDCounter = 1;
public event Action<UniqueID, Node> Added;
public event Action<UniqueID, Node> Removed;
public event Action Cleared;
public UniqueID GetSyncID(Node obj)
=> _idByNode.TryGetValue(obj, out var value) ? value : throw new Exception(
$"The specified object '{obj}' does not have a UniqueID");
public Node GetObjectByID(UniqueID id)
=> _nodeByID.TryGetValue(id, out var value) ? value : throw new Exception(
$"No object associated with {id}");
internal void Add(UniqueID? id, Node obj)
{
if (!(id is UniqueID uid)) {
// If the given UniqueID is null, keep going until we find an unused one.
while (_nodeByID.ContainsKey(uid = new UniqueID(_newIDCounter++))) { }
}
_nodeByID.Add(uid, obj);
_idByNode.Add(obj, uid);
Added?.Invoke(uid, obj);
}
internal void OnNodeRemoved(Node obj)
{
if (!_idByNode.TryGetValue(obj, out var id)) return;
_nodeByID.Remove(id);
_idByNode.Remove(obj);
Removed?.Invoke(id, obj);
}
public void Clear()
{
var objects = _nodeByID.Values.ToArray();
_nodeByID.Clear();
_idByNode.Clear();
Cleared?.Invoke();
foreach (var obj in objects)
obj.RemoveFromParent();
}
// IReadOnlyCollection implementation
public int Count => _nodeByID.Count;
public IEnumerator<(UniqueID, Node)> GetEnumerator()
=> _nodeByID.Select(entry => (entry.Key, entry.Value)).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
public readonly struct UniqueID : IEquatable<UniqueID>
{
public uint Value { get; }
public UniqueID(uint value) => Value = value;
public override bool Equals(object obj) => (obj is UniqueID other) && Equals(other);
public bool Equals(UniqueID other) => Value == other.Value;
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => $"{nameof(UniqueID)}({Value})";
public static bool operator ==(UniqueID left, UniqueID right) => left.Equals(right);
public static bool operator !=(UniqueID left, UniqueID right) => !left.Equals(right);
}

@ -1,9 +1,6 @@
using System;
using Godot;
// TODO: Maybe figure out how we can make different classes (LocalPlayer, NPCPlayer) synchronizable.
[Spawn, Sync]
public class Player : KinematicBody2D, IInitializer
public class Player : KinematicBody2D, IInitializable
{
[Export] public NodePath DisplayNamePath { get; set; }
[Export] public NodePath SpritePath { get; set; }
@ -12,115 +9,47 @@ public class Player : KinematicBody2D, IInitializer
public Sprite Sprite { get; private set; }
public bool IsLocal { get; private set; } = false;
[Sync]
public new Vector2 Position {
get => base.Position;
set { if (!IsLocal) base.Position = this.SetSync(value); }
}
[Sync]
public Color Color {
get => Sprite.Modulate;
set => Sprite.Modulate = this.SetSync(value);
}
[Sync]
public string DisplayName {
get => DisplayNameLabel.Text;
set => DisplayNameLabel.Text = this.SetSync(value);
}
// TODO: Implement "low jumps" activated by releasing the jump button early.
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F);
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F);
public float MovementSpeed { get; set; } = 160;
public float JumpVelocity { get; set; } = 240;
public float Gravity { get; set; } = 480;
public float Acceleration { get; set; } = 0.25F;
public float GroundFriction { get; set; } = 0.2F;
public float AirFriction { get; set; } = 0.05F;
public Vector2 Velocity = Vector2.Zero;
private DateTime? _jumpPressed = null;
private DateTime? _lastOnFloor = null;
public int NetworkID { get => int.Parse(Name); set => Name = value.ToString(); }
public string DisplayName { get => DisplayNameLabel.Text; set => DisplayNameLabel.Text = value; }
public Color Color { get => Sprite.Modulate; set => Sprite.Modulate = value; }
public void Initialize()
{
DisplayNameLabel = GetNode<Label>(DisplayNamePath);
Sprite = GetNode<Sprite>(SpritePath);
}
public override void _Process(float delta)
{
if ((Position.y > 9000) && (this.GetGame() is Server server))
this.RPC(new []{ server.GetNetworkID(this) }, ResetPosition, Vector2.Zero);
if (IsLocal) this.GetClient()?.RPC(Move, Position);
RsetConfig("position", MultiplayerAPI.RPCMode.Puppetsync);
RsetConfig(nameof(NetworkID), MultiplayerAPI.RPCMode.Puppetsync);
RsetConfig(nameof(DisplayName), MultiplayerAPI.RPCMode.Puppetsync);
RsetConfig(nameof(Color), MultiplayerAPI.RPCMode.Puppetsync);
}
public override void _PhysicsProcess(float delta)
{
if (!IsLocal) return;
var moveDir = 0.0F;
var jumpPressed = false;
if (!EscapeMenu.Instance.Visible) {
moveDir = Input.GetActionStrength("move_right") - Input.GetActionStrength("move_left");
jumpPressed = Input.IsActionJustPressed("move_jump");
}
var friction = IsOnFloor() ? GroundFriction : AirFriction;
Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * MovementSpeed, Acceleration)
: Mathf.Lerp(Velocity.x, 0, friction);
Velocity.y += Gravity * delta;
Velocity = MoveAndSlide(Velocity, Vector2.Up);
if (jumpPressed) _jumpPressed = DateTime.Now;
if (IsOnFloor()) _lastOnFloor = DateTime.Now;
if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) &&
((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) {
Velocity.y = -JumpVelocity;
_jumpPressed = null;
_lastOnFloor = null;
}
}
[RPC(PacketDirection.ServerToClient)]
public void SetLocal()
public override void _Process(float delta)
{
IsLocal = true;
GetNode<Camera2D>("Camera").Current = true;
if ((Position.y > 9000) && (this.GetGame() is Server))
RpcId(NetworkID, nameof(LocalPlayer.ResetPosition), Vector2.Zero);
}
[RPC(PacketDirection.ServerToClient)]
public void ResetPosition(Vector2 position)
{
base.Position = position;
Velocity = Vector2.Zero;
}
[RPC(PacketDirection.ClientToServer)]
private static void Move(Server server, NetworkID networkID, Vector2 position)
[Master]
public void Move(Vector2 position)
{
if (GetTree().GetRpcSenderId() != NetworkID) return;
// TODO: Somewhat verify the movement of players.
var player = server.GetPlayer(networkID);
player.Position = position;
Position = position;
foreach (var player in this.GetWorld().Players)
if (player != this)
RsetId(player.NetworkID, "position", Position);
}
[RPC(PacketDirection.ClientToServer)]
public static void ChangeAppearance(Server server, NetworkID networkID, string displayName, Color color)
[Master]
public void ChangeAppearance(string displayName, Color color)
{
if (GetTree().GetRpcSenderId() != NetworkID) return;
// TODO: Validate input.
var player = server.GetPlayer(networkID);
player.DisplayName = displayName;
player.Color = color;
Rset(nameof(DisplayName), displayName);
Rset(nameof(Color), color);
}
}

@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Godot;
public static class SpawnRegistry
{
private static readonly Dictionary<int, SpawnInfo> _byID = new Dictionary<int, SpawnInfo>();
private static readonly Dictionary<Type, SpawnInfo> _byType = new Dictionary<Type, SpawnInfo>();
static SpawnRegistry()
{
foreach (var type in typeof(SpawnRegistry).Assembly.GetTypes()) {
var objAttr = type.GetCustomAttribute<SpawnAttribute>();
if (objAttr == null) continue;
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception(
$"Type {type} with {nameof(SpawnAttribute)} must be a subclass of {nameof(Node)}");
var objInfo = new SpawnInfo(type);
_byID.Add(objInfo.HashID, objInfo);
_byType.Add(objInfo.Type, objInfo);
}
}
public static T Spawn<T>(this Server server)
where T : Node
{
var info = Get<T>();
var obj = info.Scene.Init<T>();
server.GetNode("World").AddChild(obj, true);
server.Objects.Add(null, obj);
return obj;
}
public static SpawnInfo Get(int id)
=> _byID.TryGetValue(id, out var value) ? value : throw new Exception(
$"No {nameof(SpawnInfo)} found with ID {id}");
public static SpawnInfo Get<T>()
=> Get(typeof(T));
public static SpawnInfo Get(Type type)
=> _byType.TryGetValue(type, out var value) ? value : throw new Exception(
$"No {nameof(SpawnInfo)} found for type {type} (missing {nameof(SpawnAttribute)}?)");
}
public class SpawnInfo
{
public Type Type { get; }
public int HashID { get; }
public PackedScene Scene { get; }
public SpawnInfo(Type type)
{
Type = type;
HashID = type.FullName.GetDeterministicHashCode();
var sceneStr = Type.GetCustomAttribute<SpawnAttribute>().Scene;
if (sceneStr == null) sceneStr = $"res://scene/{Type.Name}.tscn";
Scene = GD.Load<PackedScene>(sceneStr);
}
}
[AttributeUsage(AttributeTargets.Class)]
public class SpawnAttribute : Attribute
{
public string Scene { get; }
public SpawnAttribute(string scene = null)
=> Scene = scene;
}

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Godot;
using static Godot.NetworkedMultiplayerPeer;
@ -8,84 +7,51 @@ 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 NetworkedMultiplayerENet Peer => (NetworkedMultiplayerENet)Multiplayer.NetworkPeer;
public ConnectionStatus Status => Peer?.GetConnectionStatus() ?? ConnectionStatus.Disconnected;
public LocalPlayer LocalPlayer => (LocalPlayer)this.GetWorld().GetPlayer(GetTree().GetNetworkUniqueId());
public event Action Connected;
public event Action Disconnected;
public event Action<ConnectionStatus> 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));
CustomMultiplayer.Connect("server_disconnected", this, nameof(Disconnect));
CustomMultiplayer.Connect("network_peer_packet", this, nameof(OnPacketReceived));
}
public override void _Ready()
{
base._Ready();
Cursor = GetNode<Cursor>(CursorPath);
}
public override void _Process(float delta)
{
CustomMultiplayer.Poll();
NetworkRPC.ProcessPacketBuffer(this);
Multiplayer.Connect("connected_to_server", this, nameof(OnConnectedToServer));
Multiplayer.Connect("connection_failed", this, nameof(Disconnect));
Multiplayer.Connect("server_disconnected", this, nameof(Disconnect));
}
public void Connect(string address, ushort port)
{
if (CustomMultiplayer.NetworkPeer != null)
throw new InvalidOperationException("Client connection is already open");
if (Peer != null) throw new InvalidOperationException("Client connection is already open");
var peer = new NetworkedMultiplayerENet();
var peer = new NetworkedMultiplayerENet { ServerRelay = false };
var error = peer.CreateClient(address, port);
if (error != Error.Ok) throw new Exception($"Error when connecting: {error}");
CustomMultiplayer.NetworkPeer = peer;
Multiplayer.NetworkPeer = peer;
StatusChanged?.Invoke(Status);
}
public void Disconnect()
{
if (CustomMultiplayer.NetworkPeer == null) return;
((NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer).CloseConnection();
CustomMultiplayer.NetworkPeer = null;
if (Peer == null) return;
Peer.CloseConnection();
Multiplayer.NetworkPeer = null;
Disconnected?.Invoke();
StatusChanged?.Invoke(Status);
}
private void OnConnectedToServer()
{
Connected?.Invoke();
StatusChanged?.Invoke(Status);
}
private void OnPacketReceived(int id, byte[] bytes)
{
var networkID = new NetworkID(id);
if (networkID != NetworkID.Server) throw new Exception(
$"Received packet from other player {networkID}");
NetworkPackets.Process(this, networkID, bytes);
}
private static readonly IEnumerable<NetworkID> ToServer = new []{ NetworkID.Server };
public void RPC(Action<Server, NetworkID> action) => NetworkRPC.Call(this, ToServer, action.Method, false);
public void RPC<T>(Action<Server, NetworkID, T> action, T arg) => NetworkRPC.Call(this, ToServer, action.Method, false, arg);
public void RPC<T0, T1>(Action<Server, NetworkID, T0, T1> action, T0 arg0, T1 arg1) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1);
public void RPC<T0, T1, T2>(Action<Server, NetworkID, T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1, arg2);
public void RPC<T0, T1, T2, T3>(Action<Server, NetworkID, T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1, arg2, arg3);
public void RPC<T0, T1, T2, T3, T4>(Action<Server, NetworkID, T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1, arg2, arg3, arg4);
public void RPC<T0, T1, T2, T3, T4, T5>(Action<Server, NetworkID, T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1, arg2, arg3, arg4, arg5);
public void RPC<T0, T1, T2, T3, T4, T5, T6>(Action<Server, NetworkID, T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
public void RPC<T0, T1, T2, T3, T4, T5, T6, T7>(Action<Server, NetworkID, T0, T1, T2, T3, T4, T5, T6, T7> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => NetworkRPC.Call(this, ToServer, action.Method, false, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
}

@ -1,23 +1,11 @@
using System.Linq;
using Godot;
using Godot.Collections;
public abstract class Game : Node2D
public abstract class Game : Node
{
public ObjectHolder Objects { get; }
public Sync Sync { get; protected set; }
public Game() => Objects = new ObjectHolder();
public override void _Ready() => GetTree().Connect("node_removed", this, nameof(OnNodeRemoved));
private void OnNodeRemoved(Node node) => Objects.OnNodeRemoved(node);
// Using _EnterTree to make sure this code runs before any other.
public override void _EnterTree()
=> GD.Randomize();
// NOTE: When multithreaded physics are enabled, DirectSpaceState can only be used in _PhysicsProcess.
public Block GetBlockAt(BlockPos position)
=> GetWorld2d().DirectSpaceState.IntersectPoint(position.ToVector()).Cast<Dictionary>()
.Select(c => c["collider"]).OfType<Block>().FirstOrDefault();
public override void _Ready()
=> Multiplayer.RootNode = this.GetWorld();
}

@ -1,40 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
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<NetworkID, Player> _playersByNetworkID = new Dictionary<NetworkID, Player>();
private readonly Dictionary<Player, NetworkID> _networkIDByPlayer = new Dictionary<Player, NetworkID>();
private Player _localPlayer = null;
private bool _isLocalPlayerConnected = false;
public NetworkedMultiplayerENet Peer => (NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer;
public NetworkedMultiplayerENet Peer => (NetworkedMultiplayerENet)Multiplayer.NetworkPeer;
public bool IsRunning => Peer != null;
public bool IsSingleplayer { get; private set; }
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));
CustomMultiplayer.Connect("network_peer_packet", this, nameof(OnPacketReceived));
}
public override void _Process(float delta)
public override void _Ready()
{
CustomMultiplayer.Poll();
Sync.ProcessDirty(this);
NetworkRPC.ProcessPacketBuffer(this);
base._Ready();
Multiplayer.Connect("network_peer_connected", this, nameof(OnPeerConnected));
Multiplayer.Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected));
}
public ushort StartSingleplayer()
{
for (var retries = 0; ; retries++) {
@ -47,7 +31,7 @@ public class Server : Game
return port;
} catch (Exception ex) {
// Do throw the "Server is already running" exception.
// 3 retries should be well enough to find a random unused port.
// 3 retries should be good enough to find a random unused port.
if ((ex is InvalidOperationException) || (retries == 2)) throw;
}
}
@ -65,7 +49,7 @@ public class Server : Game
var error = peer.CreateServer(port, maxClients);
if (error != Error.Ok) throw new Exception($"Error when starting the server: {error}");
CustomMultiplayer.NetworkPeer = peer;
Multiplayer.NetworkPeer = peer;
}
public void Stop()
@ -73,92 +57,51 @@ public class Server : Game
if (!IsRunning) throw new InvalidOperationException("Server is not running");
Peer.CloseConnection();
CustomMultiplayer.NetworkPeer = null;
Multiplayer.NetworkPeer = null;
IsSingleplayer = false;
_isLocalPlayerConnected = false;
}
public IEnumerable<(NetworkID, Player)> Players
=> _playersByNetworkID.Select(entry => (entry.Key, entry.Value));
public Player GetPlayer(NetworkID networkID)
=> _playersByNetworkID[networkID];
public NetworkID GetNetworkID(Player player)
=> _networkIDByPlayer[player];
private void OnPeerConnected(int id)
private void OnPeerConnected(int networkID)
{
var networkID = new NetworkID(id);
if (IsSingleplayer) {
if (Peer.GetPeerAddress(id) != "127.0.0.1")
{ Peer.DisconnectPeer(id, true); return; }
CustomMultiplayer.RefuseNewNetworkConnections = true;
if (Peer.GetPeerAddress(networkID) != "127.0.0.1")
{ Peer.DisconnectPeer(networkID, true); return; }
Multiplayer.RefuseNewNetworkConnections = true;
}
Player player;
if ((_localPlayer != null) && !_isLocalPlayerConnected &&
(Peer.GetPeerAddress(id) == "127.0.0.1")) {
player = _localPlayer;
(Peer.GetPeerAddress(networkID) == "127.0.0.1")) {
_localPlayer.RsetId(networkID, nameof(Player.NetworkID), networkID);
_localPlayer.NetworkID = networkID;
_isLocalPlayerConnected = true;
var oldNetworkID = GetNetworkID(player);
_playersByNetworkID.Remove(oldNetworkID);
_playersByNetworkID.Add(networkID, player);
_networkIDByPlayer[player] = networkID;
} else {
Sync.SendAllObjects(this, networkID);
player = this.Spawn<Player>();
player.Position = Vector2.Zero;
player.Color = Colors.Red;
var world = this.GetWorld();
_playersByNetworkID.Add(networkID, player);
_networkIDByPlayer.Add(player, networkID);
}
foreach (var player in world.Players)
world.RpcId(networkID, nameof(World.SpawnPlayer),
player.NetworkID, player.Position, player.DisplayName, player.Color);
foreach (var block in world.BlockContainer.GetChildren().Cast<Block>())
world.RpcId(networkID, nameof(World.SendBlock),
block.Position.X, block.Position.Y,
block.Color, block.Unbreakable);
if (IsSingleplayer) _localPlayer = player;
player.RPC(new []{ networkID }, player.SetLocal);
world.Rpc(nameof(World.SpawnPlayer), networkID, Vector2.Zero, "", Colors.Red);
if (IsSingleplayer) _localPlayer = world.GetPlayer(networkID);
}
}
private void OnPeerDisconnected(int id)
private void OnPeerDisconnected(int networkID)
{
var networkID = new NetworkID(id);
var player = GetPlayer(networkID);
var world = this.GetWorld();
var player = world.GetPlayer(networkID);
// Local player stays around for reconnecting.
if (_localPlayer == player) return;
player.RemoveFromParent();
_playersByNetworkID.Remove(networkID);
_networkIDByPlayer.Remove(player);
world.Rpc(nameof(World.Despawn), world.GetPathTo(player));
}
private void OnPacketReceived(int networkID, byte[] bytes)
=> NetworkPackets.Process(this, new NetworkID(networkID), bytes);
public void RPC(IEnumerable<NetworkID> targets, Action<Client> action) => NetworkRPC.Call(this, targets, action.Method, false);
public void RPC<T>(IEnumerable<NetworkID> targets, Action<Client, T> action, T arg) => NetworkRPC.Call(this, targets, action.Method, false, arg);
public void RPC<T0, T1>(IEnumerable<NetworkID> targets, Action<Client, T0, T1> action, T0 arg0, T1 arg1) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1);
public void RPC<T0, T1, T2>(IEnumerable<NetworkID> targets, Action<Client, T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1, arg2);
public void RPC<T0, T1, T2, T3>(IEnumerable<NetworkID> targets, Action<Client, T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1, arg2, arg3);
public void RPC<T0, T1, T2, T3, T4>(IEnumerable<NetworkID> targets, Action<Client, T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1, arg2, arg3, arg4);
public void RPC<T0, T1, T2, T3, T4, T5>(IEnumerable<NetworkID> targets, Action<Client, T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1, arg2, arg3, arg4, arg5);
public void RPC<T0, T1, T2, T3, T4, T5, T6>(IEnumerable<NetworkID> targets, Action<Client, T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1, arg2, arg3, arg4, arg5, arg6);
public void RPC<T0, T1, T2, T3, T4, T5, T6, T7>(IEnumerable<NetworkID> targets, Action<Client, T0, T1, T2, T3, T4, T5, T6, T7> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => NetworkRPC.Call(this, targets, action.Method, false, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
}
public readonly struct NetworkID : IEquatable<NetworkID>
{
public static readonly NetworkID Server = new NetworkID(1);
public int Value { get; }
public NetworkID(int value) => Value = value;
public override bool Equals(object obj) => (obj is NetworkID other) && Equals(other);
public bool Equals(NetworkID other) => Value == other.Value;
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => $"{nameof(NetworkID)}({Value})";
public static bool operator ==(NetworkID left, NetworkID right) => left.Equals(right);
public static bool operator !=(NetworkID left, NetworkID right) => !left.Equals(right);
}

@ -1,58 +1,26 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Godot;
public static class Extensions
{
public static void RemoveFromParent(this Node node)
{
node.GetParent().RemoveChild(node);
node.QueueFree();
}
public static T Init<T>(this PackedScene @this)
public static T Init<T>(this PackedScene scene)
where T : Node
{
var instance = (T)@this.Instance();
(instance as IInitializer)?.Initialize();
return instance;
var node = scene.Instance<T>();
(node as IInitializable)?.Initialize();
return node;
}
public static Game GetGame(this Node node)
=> node.GetTree().Root.GetChild<Game>(0);
public static Client GetClient(this Node node)
=> node.GetGame() as Client;
public static Server GetServer(this Node node)
=> 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 void Deconstruct<TKey, TValue>(
this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{ key = kvp.Key; value = kvp.Value; }
public static int GetDeterministicHashCode(this string str)
{ unchecked {
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < str.Length; i += 2) {
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1) break;
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
return hash1 + (hash2 * 1566083941);
} }
public static World GetWorld(this Node node)
=> node.GetGame().GetNode<World>("World");
}
public interface IInitializer
public interface IInitializable
{
void Initialize();
}

@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Linq;
using Godot;
public class World : Node
{
[Export] public NodePath PlayerContainerPath { get; set; }
[Export] public NodePath BlockContainerPath { get; set; }
public Node PlayerContainer { get; private set; }
public Node BlockContainer { get; private set; }
public PackedScene BlockScene { get; private set; }
public PackedScene PlayerScene { get; private set; }
public PackedScene LocalPlayerScene { get; private set; }
public override void _Ready()
{
PlayerContainer = GetNode(PlayerContainerPath);
BlockContainer = GetNode(BlockContainerPath);
BlockScene = GD.Load<PackedScene>("res://scene/Block.tscn");
PlayerScene = GD.Load<PackedScene>("res://scene/Player.tscn");
LocalPlayerScene = GD.Load<PackedScene>("res://scene/LocalPlayer.tscn");
}
public IEnumerable<Player> Players
=> PlayerContainer.GetChildren().Cast<Player>();
public Player GetPlayer(int networkID)
=> PlayerContainer.GetNode<Player>(networkID.ToString());
public Block GetBlockAt(BlockPos position)
=> BlockContainer.GetNodeOrNull<Block>(position.ToString());
public void Clear()
{
foreach (var player in Players) {
BlockContainer.RemoveChild(player);
player.QueueFree();
}
foreach (var node in BlockContainer.GetChildren().Cast<Node>()) {
BlockContainer.RemoveChild(node);
node.QueueFree();
}
}
[PuppetSync]
public void SpawnBlock(int x, int y, Color color, bool unbreakable)
=> SpawnBlockInternal(x, y, color, unbreakable);
[Puppet]
public void SendBlock(int x, int y, Color color, bool unbreakable)
=> SpawnBlockInternal(x, y, color, unbreakable);
private void SpawnBlockInternal(int x, int y, Color color, bool unbreakable)
{
var position = new BlockPos(x, y);
var block = BlockScene.Init<Block>();
block.Name = position.ToString();
block.Position = position;
block.Color = color;
block.Unbreakable = unbreakable;
BlockContainer.AddChild(block);
}
[PuppetSync]
public void SpawnPlayer(int networkID, Vector2 position, string displayName, Color color)
=> SpawnPlayerInternal(networkID, position, displayName, color);
[Puppet]
public void SendPlayer(int networkID, Vector2 position, string displayName, Color color)
=> SpawnPlayerInternal(networkID, position, displayName, color);
private void SpawnPlayerInternal(int networkID, Vector2 position, string displayName, Color color)
{
var isLocal = networkID == GetTree().GetNetworkUniqueId();
var player = (isLocal ? LocalPlayerScene : PlayerScene).Init<Player>();
player.NetworkID = networkID;
player.Position = position;
player.DisplayName = displayName;
player.Color = color;
PlayerContainer.AddChild(player);
}
[PuppetSync]
public void Despawn(NodePath path)
{
var node = GetNode(path);
node.GetParent().RemoveChild(node);
node.QueueFree();
}
}
Loading…
Cancel
Save