Redo networking, use integrated server

"Singleplayer" now connects to an integrated server
which runs in a completely separate scene tree

- Update to Godot 3.3.0
- Reorganize code into multiple subfolders & files
- Add Client and Server scenes & scripts, extend Game
- Add NetworkSync, syncs state for networked objects
- Add NetworkRPC, allows for C#-friendly RPC by
  adding attribute to instance and static methods
- Replace most packets with RPC
- Allow de/serialization of Node / synced objects
- Remove LocalPlayer for now, add IsLocal to Player
- Add BlockPos and Facing structs
main
copygirl 5 years ago
parent 22c15b6c2a
commit eed70795dd
  1. 2
      YourFortV.csproj
  2. 6
      project.godot
  3. 2
      scene/Block.tscn
  4. 43
      scene/ClientScene.tscn
  5. 18
      scene/EscapeMenu.tscn
  6. 54
      scene/GameScene.tscn
  7. 15
      scene/LocalPlayer.tscn
  8. 11
      scene/Player.tscn
  9. 7
      scene/ServerScene.tscn
  10. 6
      src/Block.cs
  11. 61
      src/BlockPackets.cs
  12. 131
      src/CreativeBuilding.cs
  13. 12
      src/EscapeMenuAppearance.cs
  14. 128
      src/EscapeMenuMultiplayer.cs
  15. 52
      src/Game.cs
  16. 50
      src/LocalPlayer.cs
  17. 234
      src/Network.cs
  18. 257
      src/Network/DeSerializer.Impl.cs
  19. 53
      src/Network/DeSerializer.Interfaces.cs
  20. 65
      src/Network/DeSerializerRegistry.cs
  21. 27
      src/Network/IntegratedServer.cs
  22. 96
      src/Network/NetworkPackets.cs
  23. 205
      src/Network/NetworkRPC.cs
  24. 340
      src/Network/NetworkSync.cs
  25. 208
      src/NetworkAPI.cs
  26. 198
      src/NetworkAPIDeSerializers.cs
  27. 19
      src/Objects/Block.cs
  28. 126
      src/Objects/Player.cs
  29. 126
      src/Player.cs
  30. 74
      src/Scenes/Client.cs
  31. 27
      src/Scenes/Game.cs
  32. 110
      src/Scenes/Server.cs
  33. 7
      src/Utility/Extensions.cs
  34. 99
      src/Utility/Math.cs

@ -1,4 +1,4 @@
<Project Sdk="Godot.NET.Sdk/3.2.3">
<Project Sdk="Godot.NET.Sdk/3.3.0">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>

@ -10,13 +10,12 @@ config_version=4
_global_script_classes=[ ]
_global_script_class_icons={
}
[application]
config/name="YourFortV"
run/main_scene="res://scene/GameScene.tscn"
run/main_scene="res://scene/ClientScene.tscn"
boot_splash/image="res://gfx/icon.png"
boot_splash/use_filter=false
config/icon="res://gfx/icon.png"
@ -90,6 +89,7 @@ interact_break={
[rendering]
quality/2d/use_pixel_snap=true
2d/snapping/use_gpu_pixel_snap=true
quality/filters/use_nearest_mipmap_filter=true
quality/dynamic_fonts/use_oversampling=false
quality/2d/use_pixel_snap=true

@ -1,7 +1,7 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://gfx/block.png" type="Texture" id=1]
[ext_resource path="res://src/Block.cs" type="Script" id=2]
[ext_resource path="res://src/Objects/Block.cs" type="Script" id=2]
[sub_resource type="RectangleShape2D" id=1]
extents = Vector2( 8, 8 )

@ -0,0 +1,43 @@
[gd_scene load_steps=9 format=2]
[ext_resource path="res://scene/GameScene.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Cursor.cs" type="Script" id=2]
[ext_resource path="res://gfx/cursor.png" type="Texture" id=3]
[ext_resource path="res://gfx/background.png" type="Texture" id=4]
[ext_resource path="res://src/Background.cs" type="Script" id=5]
[ext_resource path="res://src/Viewport.cs" type="Script" id=6]
[ext_resource path="res://scene/EscapeMenu.tscn" type="PackedScene" id=7]
[ext_resource path="res://src/Scenes/Client.cs" type="Script" id=8]
[node name="Client" instance=ExtResource( 1 )]
script = ExtResource( 8 )
CursorPath = NodePath("CursorLayer/Cursor")
[node name="Viewport" type="Node" parent="." index="0"]
script = ExtResource( 6 )
[node name="Background" type="TextureRect" parent="." index="1"]
modulate = Color( 0.278431, 0.286275, 0.301961, 1 )
margin_right = 1280.0
margin_bottom = 720.0
texture = ExtResource( 4 )
stretch_mode = 2
script = ExtResource( 5 )
__meta__ = {
"_edit_lock_": true,
"_edit_use_anchors_": false
}
[node name="HUD" type="CanvasLayer" parent="." index="3"]
[node name="EscapeMenu" parent="HUD" index="0" instance=ExtResource( 7 )]
visible = false
[node name="CursorLayer" type="CanvasLayer" parent="." index="4"]
layer = 2
follow_viewport_enable = true
[node name="Cursor" type="Sprite" parent="CursorLayer" index="0"]
z_index = 1000
texture = ExtResource( 3 )
script = ExtResource( 2 )

@ -12,6 +12,7 @@ anchor_bottom = 1.0
theme = ExtResource( 1 )
script = ExtResource( 2 )
__meta__ = {
"_edit_lock_": true,
"_edit_use_anchors_": false
}
ReturnPath = NodePath("CenterContainer/PanelContainer/VBoxContainer/Return")
@ -20,11 +21,15 @@ ReturnPath = NodePath("CenterContainer/PanelContainer/VBoxContainer/Return")
anchor_right = 1.0
anchor_bottom = 1.0
color = Color( 0, 0, 0, 0.501961 )
__meta__ = {
"_edit_lock_": true
}
[node name="CenterContainer" type="CenterContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_lock_": true,
"_edit_use_anchors_": false
}
@ -134,9 +139,9 @@ step = 0.0
scrollable = false
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer"]
margin_top = 42.0
margin_top = 43.0
margin_right = 182.0
margin_bottom = 54.0
margin_bottom = 55.0
rect_min_size = Vector2( 0, 12 )
custom_colors/font_color = Color( 0.6, 0.6, 0.6, 1 )
text = "(Close Menu to apply changes.)"
@ -153,7 +158,7 @@ margin_right = -4.0
margin_bottom = -4.0
script = ExtResource( 4 )
StatusPath = NodePath("ContainerStatus/Status")
ServerStartStopPath = NodePath("ContainerServer/ServerStartStop")
ServerOpenClosePath = NodePath("ContainerServer/ServerOpenClose")
ServerPortPath = NodePath("ContainerServer/ServerPort")
ClientDisConnectPath = NodePath("ContainerClient/ClientDisConnect")
ClientAddressPath = NodePath("ContainerClient/ClientAddress")
@ -213,12 +218,12 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="ServerStartStop" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer"]
[node name="ServerOpenClose" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer"]
margin_left = 94.0
margin_right = 221.0
margin_bottom = 19.0
size_flags_horizontal = 3
text = "Start Server"
text = "Open Server"
__meta__ = {
"_edit_use_anchors_": false
}
@ -327,11 +332,12 @@ scroll_active = false
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="visibility_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_Appearance_visibility_changed"]
[connection signal="text_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerName/DisplayName" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_DisplayName_text_changed"]
[connection signal="value_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance/VBoxContainer/ContainerColor/Hue" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Appearance" method="_on_Hue_value_changed"]
[connection signal="text_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer/ServerPort" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerPort_text_changed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer/ServerStartStop" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerStartStop_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer/ServerOpenClose" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerOpenClose_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerClient/ClientDisConnect" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ClientDisConnect_pressed"]
[connection signal="toggled" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerHideAddress/HideAddress" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_HideAddress_toggled"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/Quit" to="." method="_on_Quit_pressed"]

@ -1,64 +1,16 @@
[gd_scene load_steps=12 format=2]
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scene/EscapeMenu.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Cursor.cs" type="Script" id=2]
[ext_resource path="res://src/Game.cs" type="Script" id=3]
[ext_resource path="res://gfx/cursor.png" type="Texture" id=4]
[ext_resource path="res://scene/LocalPlayer.tscn" type="PackedScene" id=5]
[ext_resource path="res://scene/Block.tscn" type="PackedScene" id=6]
[ext_resource path="res://src/Viewport.cs" type="Script" id=7]
[ext_resource path="res://src/Network.cs" type="Script" id=8]
[ext_resource path="res://scene/Player.tscn" type="PackedScene" id=9]
[ext_resource path="res://gfx/background.png" type="Texture" id=10]
[ext_resource path="res://src/Background.cs" type="Script" id=11]
[ext_resource path="res://src/Scenes/Game.cs" type="Script" id=3]
[node name="Game" type="Node2D"]
pause_mode = 2
script = ExtResource( 3 )
CursorPath = NodePath("CursorLayer/Cursor")
PlayerContainerPath = NodePath("World/Players")
BlockContainerPath = NodePath("World/Blocks")
BlockScene = ExtResource( 6 )
[node name="Viewport" type="Node" parent="."]
script = ExtResource( 7 )
[node name="Network" type="Node" parent="."]
script = ExtResource( 8 )
PlayerContainerPath = NodePath("../World/Players")
OtherPlayerScene = ExtResource( 9 )
[node name="Background" type="TextureRect" parent="."]
modulate = Color( 0.278431, 0.286275, 0.301961, 1 )
margin_left = 1.0
margin_top = -1.0
margin_right = 1281.0
margin_bottom = 719.0
texture = ExtResource( 10 )
stretch_mode = 2
script = ExtResource( 11 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="World" type="Node" parent="."]
[node name="Players" type="Node" parent="World"]
pause_mode = 1
[node name="LocalPlayer" parent="World/Players" instance=ExtResource( 5 )]
[node name="Blocks" type="Node" parent="World"]
[node name="HUD" type="CanvasLayer" parent="."]
[node name="EscapeMenu" parent="HUD" instance=ExtResource( 1 )]
visible = false
[node name="CursorLayer" type="CanvasLayer" parent="."]
layer = 2
follow_viewport_enable = true
[node name="Cursor" type="Sprite" parent="CursorLayer"]
z_index = 1000
texture = ExtResource( 4 )
script = ExtResource( 2 )

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

@ -1,8 +1,9 @@
[gd_scene load_steps=5 format=2]
[gd_scene load_steps=6 format=2]
[ext_resource path="res://ui_theme.tres" type="Theme" id=1]
[ext_resource path="res://gfx/player.png" type="Texture" id=2]
[ext_resource path="res://src/Player.cs" type="Script" id=3]
[ext_resource path="res://src/Objects/Player.cs" type="Script" id=3]
[ext_resource path="res://src/CreativeBuilding.cs" type="Script" id=4]
[sub_resource type="CircleShape2D" id=1]
radius = 8.0
@ -36,3 +37,9 @@ __meta__ = {
[node name="Sprite" type="Sprite" parent="."]
z_index = -5
texture = ExtResource( 2 )
[node name="Camera" type="Camera2D" parent="."]
pause_mode = 2
[node name="CreativeBuilding" type="Node2D" parent="."]
script = ExtResource( 4 )

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://scene/GameScene.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Scenes/Server.cs" type="Script" id=2]
[node name="Server" instance=ExtResource( 1 )]
script = ExtResource( 2 )

@ -1,6 +0,0 @@
using Godot;
public class Block : StaticBody2D
{
// Empty, but useful to find out whether an object is a "block".
}

@ -1,61 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Godot;
public static class BlockPackets
{
public static void Register()
{
Network.API.RegisterS2CPacket<SpawnBlockPacket>(OnSpawnBlockPacket);
Network.API.RegisterS2CPacket<SpawnBlocksPacket>(OnSpawnBlocksPacket);
Network.API.RegisterS2CPacket<DestroyBlockPacket>(OnDestroyBlockPacket);
}
private static void OnSpawnBlockPacket(SpawnBlockPacket packet)
{
// Delete any block previously at this position.
Game.Instance.GetBlockAt(packet.Position)?.QueueFree();
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = packet.Position;
block.Modulate = packet.Color;
Game.Instance.BlockContainer.AddChild(block);
}
private static void OnSpawnBlocksPacket(SpawnBlocksPacket packet)
{
Game.Instance.ClearBlocks();
foreach (var blockInfo in packet.Blocks) {
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = blockInfo.Position;
block.Modulate = blockInfo.Color;
Game.Instance.BlockContainer.AddChild(block);
}
}
private static void OnDestroyBlockPacket(DestroyBlockPacket packet)
=> Game.Instance.GetBlockAt(packet.Position)?.QueueFree();
}
public class SpawnBlockPacket
{
public Vector2 Position { get; }
public Color Color { get; }
public SpawnBlockPacket(Block block)
{ Position = block.Position; Color = block.Modulate; }
}
public class SpawnBlocksPacket
{
public List<SpawnBlockPacket> Blocks { get; }
public SpawnBlocksPacket()
=> Blocks = Game.Instance.BlockContainer.GetChildren().OfType<Block>()
.Select(block => new SpawnBlockPacket(block)).ToList();
}
public class DestroyBlockPacket
{
public Vector2 Position { get; }
public DestroyBlockPacket(Block block)
{ Position = block.Position; }
}

@ -11,32 +11,27 @@ public class CreativeBuilding : Node2D
Breaking,
}
private static readonly Vector2[] _neighborPositions = new Vector2[]{
Vector2.Left*16, Vector2.Right*16, Vector2.Up*16, Vector2.Down*16 };
[Export] public int MaxLength { get; set; } = 6;
private Texture _blockTex;
private Vector2 _startPos;
private Vector2 _direction;
private BlockPos _startPos;
private Facing _direction;
private int _length;
private bool _canBuild;
private BuildMode? _currentMode = null;
private IEnumerable<Vector2> BlockPositions =>
Enumerable.Range(0, _length + 1).Select(i => _startPos + _direction * (i * 16));
public override void _Ready()
{
_blockTex = GD.Load<Texture>("res://gfx/block.png");
}
public override void _Process(float delta)
public override void _PhysicsProcess(float delta)
{
if (!(this.GetGame() is Client client)) return;
Update();
if (EscapeMenu.Instance.Visible || !Game.Cursor.Visible)
if (EscapeMenu.Instance.Visible || !client.Cursor.Visible)
{ _currentMode = null; return; }
switch (_currentMode) {
@ -49,127 +44,79 @@ public class CreativeBuilding : Node2D
case BuildMode.Placing:
if (Input.IsActionJustPressed("interact_break")) _currentMode = null;
else if (!Input.IsActionPressed("interact_place")) {
if (_canBuild)
foreach (var pos in BlockPositions)
PlaceBlock(pos);
if (_canBuild) this.GetClient()?.RPC(PlaceLine, _startPos, _direction, _length);
_currentMode = null;
}
break;
case BuildMode.Breaking:
if (Input.IsActionJustPressed("interact_place")) _currentMode = null;
else if (!Input.IsActionPressed("interact_break")) {
foreach (var pos in BlockPositions) {
var block = Game.Instance.GetBlockAt(pos);
if (block != null) BreakBlock(block);
}
this.GetClient()?.RPC(BreakLine, _startPos, _direction, _length);
_currentMode = null;
}
break;
}
if (_currentMode != null) {
var rad90 = Mathf.Deg2Rad(90.0F);
var angle = Mathf.Round(_startPos.AngleToPoint(Game.Cursor.Position) / rad90) * rad90;
_direction = new Vector2(-Mathf.Cos(angle), -Mathf.Sin(angle));
_length = Math.Min(MaxLength, Mathf.RoundToInt(_startPos.DistanceTo(Game.Cursor.Position) / 16));
var start = _startPos.ToVector();
var angle = client.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));
} else {
_startPos = (Game.Cursor.Position / 16).Round() * 16;
_startPos = BlockPos.FromVector(client.Cursor.Position);
_length = 0;
}
bool IsBlockAt(Vector2 pos) => Game.Instance.GetBlockAt(pos) != null;
_canBuild = !IsBlockAt(_startPos) && _neighborPositions.Any(pos => IsBlockAt(_startPos + pos));
}
private Block PlaceBlock(Vector2 position)
{
if (Game.Instance.GetBlockAt(position) != null) return null;
// FIXME: Test if there is a player in the way.
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = position;
block.Modulate = Game.LocalPlayer.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
Game.Instance.BlockContainer.AddChild(block);
if (Network.IsMultiplayerReady) {
if (Network.IsServer) Network.API.SendToEveryone(new SpawnBlockPacket(block));
else Network.API.SendToServer(new PlaceBlockPacket(position));
}
return block;
}
private void BreakBlock(Block block)
{
// FIXME: Use a different (safer) way to check if a block is one of the default ones.
if (block.Modulate.s < 0.5F) return;
if (Network.IsMultiplayerReady) {
if (Network.IsServer) Network.API.SendToEveryone(new DestroyBlockPacket(block));
else Network.API.SendToServer(new BreakBlockPacket(block));
}
block.QueueFree();
bool IsBlockAt(BlockPos pos) => client.GetBlockAt(pos) != null;
_canBuild = !IsBlockAt(_startPos) && Facings.All.Any(pos => IsBlockAt(_startPos + pos.ToBlockPos()));
}
public override void _Draw()
{
if (!Game.Cursor.Visible) return;
if (!(this.GetGame() is Client client) || !client.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);
foreach (var pos in BlockPositions) {
var hasBlock = Game.Instance.GetBlockAt(pos) != null;
foreach (var pos in GetBlockPositions(_startPos, _direction, _length)) {
var hasBlock = client.GetBlockAt(pos) != null;
var color = (_currentMode != BuildMode.Breaking)
? ((_canBuild && !hasBlock) ? green : red)
: (hasBlock ? black : red);
DrawTexture(_blockTex, ToLocal(pos - _blockTex.GetSize() / 2), color);
DrawTexture(_blockTex, ToLocal(pos.ToVector() - _blockTex.GetSize() / 2), color);
}
}
private static IEnumerable<BlockPos> GetBlockPositions(BlockPos start, Facing direction, int length)
=> Enumerable.Range(0, length + 1).Select(i => start + direction.ToBlockPos() * i);
public static void RegisterPackets()
[RPC(PacketDirection.ClientToServer)]
private static void PlaceLine(Server server, NetworkID networkID, BlockPos start, Facing direction, int length)
{
Network.API.RegisterC2SPacket<PlaceBlockPacket>(OnPlaceBlockPacket);
Network.API.RegisterC2SPacket<BreakBlockPacket>(OnBreakBlockPacket);
}
var player = server.GetPlayer(networkID);
// TODO: Test if starting block is valid.
foreach (var pos in GetBlockPositions(start, direction, length)) {
if (server.GetBlockAt(pos) != null) continue;
// FIXME: Test if there is a player in the way.
private class PlaceBlockPacket
{
public Vector2 Position { get; }
public PlaceBlockPacket(Vector2 position) => Position = position;
server.Spawn<Block>(block => {
block.Position = pos;
block.Color = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
});
}
private static void OnPlaceBlockPacket(Player player, PlaceBlockPacket packet)
{
if (Game.Instance.GetBlockAt(packet.Position) != null) return;
var block = Game.Instance.BlockScene.Init<Block>();
block.Position = packet.Position;
block.Modulate = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
Game.Instance.BlockContainer.AddChild(block);
Network.API.SendToEveryone(new SpawnBlockPacket(block));
}
private class BreakBlockPacket
{
public Vector2 Position { get; }
public BreakBlockPacket(Block block) => Position = block.Position;
}
private static void OnBreakBlockPacket(Player player, BreakBlockPacket packet)
[RPC(PacketDirection.ClientToServer)]
private static void BreakLine(Server server, NetworkID networkID, BlockPos start, Facing direction, int length)
{
var block = Game.Instance.GetBlockAt(packet.Position);
if (block == null) return;
if (block.Modulate.s < 0.5F) {
// TODO: Respawn the block the client thought it destroyed?
return;
// var player = server.GetPlayer(networkID);
// TODO: Do additional verification on the packet.
foreach (var pos in GetBlockPositions(start, direction, length)) {
var block = server.GetBlockAt(pos);
if (block?.Unbreakable != false) continue;
block.Destroy();
}
// TODO: Further verify whether player can break a block at this position.
Network.API.SendToEveryoneExcept(player, new DestroyBlockPacket(block));
block.QueueFree();
}
}

@ -18,8 +18,7 @@ public class EscapeMenuAppearance : CenterContainer
ColorSlider = GetNode<Slider>(ColorSliderPath);
ColorSlider.Value = GD.Randf();
var color = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F);
Game.LocalPlayer.Color = ColorPreview.Modulate = color;
ColorPreview.Modulate = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F);
}
@ -45,9 +44,10 @@ public class EscapeMenuAppearance : CenterContainer
private void _on_Appearance_visibility_changed()
{
if (!IsVisibleInTree())
Player.ChangeAppearance(Game.LocalPlayer,
DisplayName.Text, ColorPreview.Modulate,
Network.IsClient);
if (IsVisibleInTree()) return;
var client = this.GetClient();
// TODO: Find a better way to know if we're connected?
if (client.CustomMultiplayer.NetworkPeer?.GetConnectionStatus() == NetworkedMultiplayerPeer.ConnectionStatus.Connected)
client.RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate);
}
}

@ -3,67 +3,83 @@ using Godot;
public class EscapeMenuMultiplayer : Container
{
private const ushort DEFAULT_PORT = 42005;
[Export] public NodePath StatusPath { get; set; }
[Export] public NodePath ServerStartStopPath { get; set; }
[Export] public NodePath ServerOpenClosePath { get; set; }
[Export] public NodePath ServerPortPath { get; set; }
[Export] public NodePath ClientDisConnectPath { get; set; }
[Export] public NodePath ClientAddressPath { get; set; }
public Label Status { get; private set; }
public Button ServerStartStop { get; private set; }
public Button ServerOpenClose { get; private set; }
public LineEdit ServerPort { get; private set; }
public Button ClientDisConnect { get; private set; }
public LineEdit ClientAddress { get; private set; }
public IntegratedServer Server { get; private set; }
public override void _Ready()
{
Status = GetNode<Label>(StatusPath);
ServerStartStop = GetNode<Button>(ServerStartStopPath);
ServerOpenClose = GetNode<Button>(ServerOpenClosePath);
ServerPort = GetNode<LineEdit>(ServerPortPath);
ClientDisConnect = GetNode<Button>(ClientDisConnectPath);
ClientAddress = GetNode<LineEdit>(ClientAddressPath);
Network.StatusChanged += OnNetworkStatusChanged;
ServerPort.PlaceholderText = Network.DEFAULT_PORT.ToString();
ClientAddress.PlaceholderText = $"localhost:{Network.DEFAULT_PORT}";
}
ServerPort.PlaceholderText = DEFAULT_PORT.ToString();
ClientAddress.PlaceholderText = $"localhost:{DEFAULT_PORT}";
private void OnNetworkStatusChanged(NetworkStatus status)
CallDeferred(nameof(SetupIntegratedServer));
}
private void SetupIntegratedServer()
{
switch (status) {
case NetworkStatus.NoConnection:
Status.Text = "No Connection";
Status.Modulate = Colors.Red;
break;
case NetworkStatus.ServerRunning:
Status.Text = "Server Running";
Status.Modulate = Colors.Green;
break;
case NetworkStatus.Connecting:
Status.Text = "Connecting ...";
Status.Modulate = Colors.Yellow;
break;
case NetworkStatus.Authenticating:
Status.Text = "Authenticating ...";
Status.Modulate = Colors.YellowGreen;
break;
case NetworkStatus.ConnectedToServer:
Status.Text = "Connected to Server";
Status.Modulate = Colors.Green;
break;
Server = new IntegratedServer();
this.GetClient().AddChild(Server);
CallDeferred(nameof(StartIntegratedServerAndConnect));
}
var noConnection = status == NetworkStatus.NoConnection;
ServerPort.Editable = noConnection;
ServerStartStop.Text = (status == NetworkStatus.ServerRunning) ? "Stop Server" : "Start Server";
ServerStartStop.Disabled = status > NetworkStatus.ServerRunning;
ClientAddress.Editable = noConnection;
ClientDisConnect.Text = (status < NetworkStatus.Connecting) ? "Connect" : "Disconnect";
ClientDisConnect.Disabled = status == NetworkStatus.ServerRunning;
private void StartIntegratedServerAndConnect()
{
Server.Start(DEFAULT_PORT);
this.GetClient().Connect("localhost", DEFAULT_PORT);
}
// private void OnNetworkStatusChanged(NetworkStatus status)
// {
// switch (status) {
// case NetworkStatus.NoConnection:
// Status.Text = "No Connection";
// Status.Modulate = Colors.Red;
// break;
// case NetworkStatus.ServerRunning:
// Status.Text = "Server Running";
// Status.Modulate = Colors.Green;
// break;
// case NetworkStatus.Connecting:
// Status.Text = "Connecting ...";
// Status.Modulate = Colors.Yellow;
// break;
// case NetworkStatus.Authenticating:
// Status.Text = "Authenticating ...";
// Status.Modulate = Colors.YellowGreen;
// break;
// case NetworkStatus.ConnectedToServer:
// Status.Text = "Connected to Server";
// Status.Modulate = Colors.Green;
// break;
// }
// var noConnection = status == NetworkStatus.NoConnection;
// ServerPort.Editable = noConnection;
// ServerOpenClose.Text = (status == NetworkStatus.ServerRunning) ? "Stop Server" : "Start Server";
// ServerOpenClose.Disabled = status > NetworkStatus.ServerRunning;
// ClientAddress.Editable = noConnection;
// ClientDisConnect.Text = (status < NetworkStatus.Connecting) ? "Connect" : "Disconnect";
// ClientDisConnect.Disabled = status == NetworkStatus.ServerRunning;
// }
#pragma warning disable IDE0051
#pragma warning disable IDE1006
@ -80,29 +96,29 @@ public class EscapeMenuMultiplayer : Container
}
private void _on_ServerStartStop_pressed()
private void _on_ServerOpenClose_pressed()
{
if (GetTree().NetworkPeer == null) {
var port = Network.DEFAULT_PORT;
if (ServerPort.Text.Length > 0)
port = ushort.Parse(ServerPort.Text);
Network.Instance.StartServer(port);
} else Network.Instance.StopServer();
// if (GetTree().NetworkPeer == null) {
// var port = Network.DEFAULT_PORT;
// if (ServerPort.Text.Length > 0)
// port = ushort.Parse(ServerPort.Text);
// Network.Instance.StartServer(port);
// } else Network.Instance.StopServer();
}
private void _on_ClientDisConnect_pressed()
{
if (GetTree().NetworkPeer == null) {
var address = "localhost";
var port = Network.DEFAULT_PORT;
if (ClientAddress.Text.Length > 0) {
// TODO: Verify input some more, support IPv6?
var split = ClientAddress.Text.Split(':');
address = (split.Length > 1) ? split[0] : ClientAddress.Text;
port = (split.Length > 1) ? ushort.Parse(split[1]) : port;
}
Network.Instance.ConnectToServer(address, port);
} else Network.Instance.DisconnectFromServer();
// if (GetTree().NetworkPeer == null) {
// var address = "localhost";
// var port = DEFAULT_PORT;
// if (ClientAddress.Text.Length > 0) {
// // TODO: Verify input some more, support IPv6?
// var split = ClientAddress.Text.Split(':');
// address = (split.Length > 1) ? split[0] : ClientAddress.Text;
// port = (split.Length > 1) ? ushort.Parse(split[1]) : port;
// }
// Network.Instance.ConnectToServer(address, port);
// } else Network.Instance.DisconnectFromServer();
}
private void _on_HideAddress_toggled(bool pressed)

@ -1,52 +0,0 @@
using System.Linq;
using Godot;
using Godot.Collections;
public class Game : Node2D
{
public static Game Instance { get; private set; }
public static LocalPlayer LocalPlayer { get; internal set; }
public static Cursor Cursor { get; private set; }
[Export] public NodePath CursorPath { get; set; }
[Export] public NodePath BlockContainerPath { get; set; }
[Export] public PackedScene BlockScene { get; set; }
public Node BlockContainer { get; private set; }
public Game() => Instance = this;
// Using _EnterTree to make sure this code runs before any other.
public override void _EnterTree()
=> GD.Randomize();
public override void _Ready()
{
Cursor = GetNode<Cursor>(CursorPath);
BlockContainer = GetNode(BlockContainerPath);
SpawnDefaultBlocks();
}
public void ClearBlocks()
{
foreach (var block in BlockContainer.GetChildren())
((Node)block).Free();
}
public void SpawnDefaultBlocks()
{
for (var x = -6; x <= 6; x++) {
var block = BlockScene.Init<Block>();
block.Position = new Vector2(x * 16, 48);
block.Modulate = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
BlockContainer.AddChild(block);
}
}
// FIXME: Can only be called during _physics_process?!
public Block GetBlockAt(Vector2 position)
=> GetWorld2d().DirectSpaceState.IntersectPoint(position).Cast<Dictionary>()
.Select(c => c["collider"]).OfType<Block>().FirstOrDefault();
}

@ -1,50 +0,0 @@
using System;
using Godot;
// TODO: Implement "low jumps" activated by releasing the jump button early.
public class LocalPlayer : Player
{
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F);
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F);
[Export] public float MovementSpeed { get; set; } = 160;
[Export] public float JumpVelocity { get; set; } = 240;
[Export] public float Gravity { get; set; } = 480;
[Export(PropertyHint.Range, "0,1")]
public float Friction { get; set; } = 0.1F;
[Export(PropertyHint.Range, "0,1")]
public float Acceleration { get; set; } = 0.25F;
public Vector2 Velocity = Vector2.Zero;
private DateTime? _jumpPressed = null;
private DateTime? _lastOnFloor = null;
public override void _EnterTree() => Game.LocalPlayer = this;
public override void _ExitTree() => Game.LocalPlayer = null;
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");
}
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;
}
}
}

@ -1,234 +0,0 @@
using System;
using System.Collections.Generic;
using Godot;
public enum NetworkStatus
{
NoConnection,
ServerRunning,
Connecting,
Authenticating,
ConnectedToServer,
}
public class Network : Node
{
public const ushort DEFAULT_PORT = 42005;
public static Network Instance { get; private set; }
public static NetworkAPI API { get; private set; }
public static NetworkStatus Status { get; private set; } = NetworkStatus.NoConnection;
public static bool IsMultiplayerReady => (Status == NetworkStatus.ServerRunning) || (Status == NetworkStatus.ConnectedToServer);
public static bool IsAuthoratative => Status <= NetworkStatus.ServerRunning;
public static bool IsServer => Status == NetworkStatus.ServerRunning;
public static bool IsClient => Status > NetworkStatus.Connecting;
public static int LocalNetworkID => Instance.GetTree().GetNetworkUniqueId();
public static IEnumerable<Player> Players => Instance._playersById.Values;
public static event Action<NetworkStatus> StatusChanged;
public static Player GetPlayer(int id)
=> Instance._playersById.TryGetValue(id, out var value) ? value : null;
public static Player GetPlayerOrThrow(int id)
=> GetPlayer(id) ?? throw new ArgumentException(
$"No player instance found for ID {id}", nameof(id));
private readonly Dictionary<int, Player> _playersById = new Dictionary<int, Player>();
[Export] public NodePath PlayerContainerPath { get; set; }
[Export] public PackedScene OtherPlayerScene { get; set; }
public Node PlayerContainer { get; private set; }
public Network() =>Instance = this;
public override void _Ready()
{
PlayerContainer = GetNode(PlayerContainerPath);
GetTree().Connect("connected_to_server", this, nameof(OnClientConnected));
GetTree().Connect("connection_failed", this, nameof(DisconnectFromServer));
GetTree().Connect("server_disconnected", this, nameof(DisconnectFromServer));
GetTree().Connect("network_peer_connected", this, nameof(OnPeerConnected));
GetTree().Connect("network_peer_disconnected", this, nameof(OnPeerDisconnected));
var multiplayerApi = GetTree().Multiplayer;
API = new NetworkAPI(multiplayerApi);
multiplayerApi.Connect("network_peer_packet", this, nameof(OnPacketReceived));
API.RegisterC2SPacket<ClientAuthPacket>(OnClientAuthPacket);
API.RegisterS2CPacket<SpawnPlayerPacket>(OnSpawnPlayerPacket);
Player.RegisterPackets();
CreativeBuilding.RegisterPackets();
BlockPackets.Register();
}
// Let NetworkAPI handle receiving of custom packages.
private void OnPacketReceived(int id, byte[] bytes)
=> API.OnPacketReceived(id, bytes);
public void ResetGame()
{
Game.LocalPlayer.NetworkID = -1;
// Clear other players.
foreach (var player in _playersById.Values)
if (!player.IsLocal) player.QueueFree();
_playersById.Clear();
// Game.Instance.ClearBlocks();
// Game.Instance.SpawnDefaultBlocks();
}
private void ChangeStatus(NetworkStatus status)
{
if (Status == status) return;
Status = status;
StatusChanged?.Invoke(status);
PlayerContainer.PauseMode = IsMultiplayerReady
? PauseModeEnum.Process : PauseModeEnum.Stop;
}
public Error StartServer(ushort port)
{
if (GetTree().NetworkPeer != null) throw new InvalidOperationException();
var peer = new NetworkedMultiplayerENet();
var error = peer.CreateServer(port);
if (error != Error.Ok) return error;
GetTree().NetworkPeer = peer;
Game.LocalPlayer.NetworkID = 1;
_playersById.Add(1, Game.LocalPlayer);
ChangeStatus(NetworkStatus.ServerRunning);
return Error.Ok;
}
public void StopServer()
{
if ((GetTree().NetworkPeer == null) || !GetTree().IsNetworkServer()) throw new InvalidOperationException();
((NetworkedMultiplayerENet)GetTree().NetworkPeer).CloseConnection();
GetTree().NetworkPeer = null;
ResetGame();
ChangeStatus(NetworkStatus.NoConnection);
}
public Error ConnectToServer(string address, ushort port)
{
if (GetTree().NetworkPeer != null) throw new InvalidOperationException();
var peer = new NetworkedMultiplayerENet();
var error = peer.CreateClient(address, port);
if (error != Error.Ok) return error;
GetTree().NetworkPeer = peer;
ChangeStatus(NetworkStatus.Connecting);
return Error.Ok;
}
private void OnClientConnected()
{
ChangeStatus(NetworkStatus.Authenticating);
var id = GetTree().GetNetworkUniqueId();
Game.LocalPlayer.NetworkID = id;
_playersById.Add(id, Game.LocalPlayer);
API.SendToServer(new ClientAuthPacket(Game.LocalPlayer));
}
public void DisconnectFromServer()
{
if ((GetTree().NetworkPeer == null) || GetTree().IsNetworkServer()) throw new InvalidOperationException();
((NetworkedMultiplayerENet)GetTree().NetworkPeer).CloseConnection();
GetTree().NetworkPeer = null;
ChangeStatus(NetworkStatus.NoConnection);
ResetGame();
}
private Player SpawnOtherPlayer(int networkID, Vector2 position, string displayName, Color color)
{
var player = OtherPlayerScene.Init<Player>();
player.NetworkID = networkID;
// TODO: We need to find a way to sync these property automatically.
player.Position = position;
player.DisplayName = displayName;
player.Color = color;
_playersById.Add(networkID, player);
PlayerContainer.AddChild(player);
return player;
}
private class ClientAuthPacket
{
public string DisplayName { get; }
public Color Color { get; }
public ClientAuthPacket(Player player)
{ DisplayName = player.DisplayName; Color = player.Color; }
}
private void OnClientAuthPacket(int networkID, ClientAuthPacket packet)
{
// Authentication message is only sent once, so once the Player object exists, ignore this message.
if (GetPlayer(networkID) != null) return;
API.SendTo(networkID, new SpawnBlocksPacket());
foreach (var player in _playersById.Values)
API.SendTo(networkID, new SpawnPlayerPacket(player));
var newPlayer = SpawnOtherPlayer(networkID, Vector2.Zero, packet.DisplayName, packet.Color);
API.SendToEveryone(new SpawnPlayerPacket(newPlayer));
}
private class SpawnPlayerPacket
{
public int NetworkID { get; }
public Vector2 Position { get; }
public string DisplayName { get; }
public Color Color { get; }
public SpawnPlayerPacket(Player player)
{
NetworkID = player.NetworkID;
Position = player.Position;
DisplayName = player.DisplayName;
Color = player.Color;
}
}
private void OnSpawnPlayerPacket(SpawnPlayerPacket packet)
{
if (packet.NetworkID == LocalNetworkID) {
var player = Game.LocalPlayer;
player.Position = packet.Position;
player.Velocity = Vector2.Zero;
ChangeStatus(NetworkStatus.ConnectedToServer);
} else SpawnOtherPlayer(packet.NetworkID, packet.Position, packet.DisplayName, packet.Color);
}
private void OnPeerConnected(int id)
{
// Currently unused.
}
private void OnPeerDisconnected(int id)
{
GetPlayer(id)?.QueueFree();
_playersById.Remove(id);
}
}

@ -0,0 +1,257 @@
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 value)
{
writer.Write(value.Count);
foreach (var element in value) {
_keyDeSerializer.Serialize(game, writer, element.Key);
_valueDeSerializer.Serialize(game, writer, element.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 SyncedObjectDeSerializerGenerator
: IDeSerializerGenerator
{
public IDeSerializer GenerateFor(Type type)
{
if (!typeof(Node).IsAssignableFrom(type) || (type.GetCustomAttribute<SyncObjectAttribute>() == null)) return null;
var deSerializerType = typeof(SyncedObjectDeSerializer<>).MakeGenericType(type);
return (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
private class SyncedObjectDeSerializer<TObj>
: DeSerializer<TObj>
where TObj : Node
{
public override void Serialize(Game game, BinaryWriter writer, TObj value)
=> writer.Write(value.GetSyncID());
public override TObj Deserialize(Game game, BinaryReader reader)
=> (TObj)game.GetObjectBySyncID(reader.ReadUInt32());
}
}
// TODO: Replace this with something that will generate code at runtime for improved performance.
public class ComplexDeSerializer
: IDeSerializer
{
private readonly Type _type;
private event Action<Game, BinaryWriter, object> OnSerialize;
private event Action<Game, BinaryReader, object> OnDeserialize;
public ComplexDeSerializer(Type type)
{
_type = type;
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) {
var deSerializer = DeSerializerRegistry.Get(field.FieldType, true);
OnSerialize += (game, writer, value) => deSerializer.Serialize(game, writer, field.GetValue(value));
OnDeserialize += (game, reader, instance) => field.SetValue(instance, deSerializer.Deserialize(game, reader));
}
if (OnSerialize == null) throw new InvalidOperationException(
$"Unable to create serializer for type {type}");
}
public void Serialize(Game game, BinaryWriter writer, object value)
=> OnSerialize(game, writer, value);
public object Deserialize(Game game, BinaryReader reader)
{
var instance = FormatterServices.GetUninitializedObject(_type);
OnDeserialize(game, reader, instance);
return instance;
}
}

@ -0,0 +1,53 @@
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);
}

@ -0,0 +1,65 @@
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 SyncedObjectDeSerializerGenerator());
}
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) value = new ComplexDeSerializer(type);
_byType.Add(type, value);
}
return value;
}
}

@ -0,0 +1,27 @@
using Godot;
public class IntegratedServer : Node
{
private SceneTree _sceneTree;
public Server Server { get; private set; }
public override void _Ready()
{
_sceneTree = new SceneTree();
_sceneTree.Init();
_sceneTree.Root.RenderTargetUpdateMode = Godot.Viewport.UpdateMode.Disabled;
var scene = GD.Load<PackedScene>("res://scene/ServerScene.tscn").Init<Server>();
_sceneTree.Root.AddChild(scene);
_sceneTree.CurrentScene = scene;
Server = _sceneTree.Root.GetChild<Server>(0);
}
public override void _Process(float delta) => _sceneTree.Idle(delta);
public override void _PhysicsProcess(float delta) => _sceneTree.Iteration(delta);
public override void _ExitTree() => _sceneTree.Finish();
public void Start(ushort port) => Server.Start(port);
public void Stop() => Server.Stop();
}

@ -0,0 +1,96 @@
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);
}
}
}

@ -0,0 +1,205 @@
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<SyncObjectAttribute>() == null)) throw new Exception(
$"Type of non-static RPC method '{method.DeclaringType}.{method.Name}' must have {nameof(SyncObjectAttribute)}");
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.");
}
}
}

@ -0,0 +1,340 @@
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>();
private static readonly Dictionary<(Game, uint), SyncStatus> _statusBySyncID = new Dictionary<(Game, uint), SyncStatus>();
private static readonly Dictionary<Node, SyncStatus> _statusByObject = 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, Action<T> init = null)
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 };
_statusBySyncID.Add((server, status.SyncID), status);
_statusByObject.Add(status.Object, status);
_dirtyObjects.Add(status);
server.GetNode(info.ContainerNodePath).AddChild(obj);
init?.Invoke(obj);
return obj;
}
public static void Destroy(this Node obj)
{
var status = GetSyncStatus(obj);
if (!(obj.GetGame() is Server)) return;
status.Special = Special.Destroy;
_statusBySyncID.Remove((obj.GetGame(), status.SyncID));
_statusByObject.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));
}
// 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 _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, status.Special, values));
}
NetworkPackets.Send(server, new []{ networkID }, packet);
}
public static uint GetSyncID(this Node obj)
=> GetSyncStatus(obj).SyncID;
public static Node GetObjectBySyncID(this Game game, uint syncID)
=> _statusBySyncID.TryGetValue((game, 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)}");
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 (!_statusBySyncID.TryGetValue((game, packetObj.SyncID), out var status)) {
if (packetObj.Special != Special.Spawn)
throw new Exception($"Unknown synced object with ID {packetObj.SyncID}");
var obj = info.InstanceScene.Init<Node>();
status = new SyncStatus(packetObj.SyncID, obj, info);
_statusBySyncID.Add((game, status.SyncID), status);
_statusByObject.Add(status.Object, status);
game.GetNode(info.ContainerNodePath).AddChild(obj);
} else {
if (info != status.Info) throw new Exception(
$"Info doesn't match ({info.Name} != {status.Info.Name})");
if (packetObj.Special == Special.Destroy) {
_statusBySyncID.Remove((game, 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);
}
}
}
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
{
}

@ -1,208 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Godot;
[Flags]
public enum PacketDirection
{
ServerToClient = 0b01,
ClientToServer = 0b10,
Both = ServerToClient | ClientToServer,
}
public enum TransferMode
{
Unreliable = NetworkedMultiplayerPeer.TransferModeEnum.Unreliable,
UnreliableOrdered = NetworkedMultiplayerPeer.TransferModeEnum.UnreliableOrdered,
Reliable = NetworkedMultiplayerPeer.TransferModeEnum.Reliable,
}
// TODO: Improve performance and type safety of de/serialization.
// TODO: Support easily spawning and syncronizing objects and their properties.
public partial class NetworkAPI
{
private readonly MultiplayerAPI _multiplayerAPI;
private readonly List<PacketInfo> _packetsById = new List<PacketInfo>();
private readonly Dictionary<Type, PacketInfo> _packetsByType = new Dictionary<Type, PacketInfo>();
private readonly Dictionary<Type, INetworkDeSerializer> _deSerializers = new Dictionary<Type, INetworkDeSerializer>();
private readonly List<INetworkDeSerializerGenerator> _deSerializerGenerators = new List<INetworkDeSerializerGenerator>();
private class PacketInfo
{
public int ID { get; }
public Type Type { get; }
public PacketDirection Direction { get; }
public TransferMode DefaultTransformMode { get; }
public INetworkDeSerializer DeSerializer { get; }
public Action<int, object> OnPacketReceived { get; }
public PacketInfo(int id, Type type, PacketDirection direction,
TransferMode defaultTransferMode, INetworkDeSerializer deSerializer,
Action<int, object> onPacketReceived)
{
ID = id;
Type = type;
Direction = direction;
DefaultTransformMode = defaultTransferMode;
DeSerializer = deSerializer;
OnPacketReceived = onPacketReceived;
}
}
public NetworkAPI(MultiplayerAPI multiplayerAPI)
{
_multiplayerAPI = multiplayerAPI;
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadBoolean());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadByte());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadSByte());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadInt16());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadUInt16());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadInt32());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadUInt32());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadInt64());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadUInt64());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadSingle());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadDouble());
RegisterDeSerializer((writer, value) => writer.Write(value), reader => reader.ReadString());
// byte[]
RegisterDeSerializer((writer, value) => { writer.Write(value.Length); writer.Write(value); },
reader => reader.ReadBytes(reader.ReadInt32()));
// Vector2
RegisterDeSerializer((writer, value) => { writer.Write(value.x); writer.Write(value.y); },
reader => new Vector2(reader.ReadSingle(), reader.ReadSingle()));
// Color
RegisterDeSerializer((writer, value) => writer.Write(value.ToRgba32()),
reader => new Color(reader.ReadInt32()));
RegisterDeSerializerGenerator(new DictionaryDeSerializerGenerator());
RegisterDeSerializerGenerator(new CollectionDeSerializerGenerator());
RegisterDeSerializerGenerator(new ArrayDeSerializerGenerator());
}
public void RegisterDeSerializer<T>(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize)
=> _deSerializers.Add(typeof(T), new SimpleDeSerializer<T>(serialize, deserialize));
public void RegisterDeSerializer<T>(INetworkDeSerializer deSerializer)
=> _deSerializers.Add(typeof(T), deSerializer);
public void RegisterDeSerializerGenerator(INetworkDeSerializerGenerator deSerializerGenerator)
=> _deSerializerGenerators.Add(deSerializerGenerator);
public void RegisterS2CPacket<T>(Action<T> action, TransferMode defaultTransferMode = TransferMode.Reliable)
=> RegisterPacket((int _id, T packet) => action(packet), defaultTransferMode, PacketDirection.ServerToClient);
public void RegisterC2SPacket<T>(Action<int, T> action, TransferMode defaultTransferMode = TransferMode.Reliable)
=> RegisterPacket(action, defaultTransferMode, PacketDirection.ClientToServer);
public void RegisterC2SPacket<T>(Action<Player, T> action, TransferMode defaultTransferMode = TransferMode.Reliable)
=> RegisterPacket(action, defaultTransferMode, PacketDirection.ClientToServer);
public void RegisterPacket<T>(Action<Player, T> action,
TransferMode defaultTransferMode = TransferMode.Reliable,
PacketDirection direction = PacketDirection.Both)
=> RegisterPacket((int id, T packet) => action(Network.GetPlayerOrThrow(id), packet), defaultTransferMode, direction);
public void RegisterPacket<T>(Action<int, T> action,
TransferMode defaultTransferMode = TransferMode.Reliable,
PacketDirection direction = PacketDirection.Both)
{
var deSerializer = GetDeSerializer(typeof(T), true);
var info = new PacketInfo(_packetsById.Count, typeof(T),
direction, defaultTransferMode, deSerializer,
(id, packet) => action(id, (T)packet));
_packetsByType.Add(typeof(T), info);
_packetsById.Add(info);
}
public void SendToServer<T>(T packet, TransferMode? transferMode = null)
=> SendTo(1, packet, transferMode);
public void SendTo<T>(Player player, T packet, TransferMode? transferMode = null)
=> SendTo(player.NetworkID, packet, transferMode);
public void SendTo<T>(int id, T packet, TransferMode? transferMode = null)
=> SendTo(new []{ id }, packet, transferMode);
public void SendToEveryone<T>(T packet, TransferMode? transferMode = null)
=> SendTo(_multiplayerAPI.GetNetworkConnectedPeers(), packet, transferMode);
public void SendToEveryoneExcept<T>(Player except, T packet, TransferMode? transferMode = null)
=> SendToEveryoneExcept(except?.NetworkID ?? 0, packet, transferMode);
public void SendToEveryoneExcept<T>(int except, T packet, TransferMode? transferMode = null)
=> SendTo(_multiplayerAPI.GetNetworkConnectedPeers().Where(id => id != except), packet, transferMode);
public void SendTo<T>(IEnumerable<Player> players, T packet, TransferMode? transferMode = null)
=> SendTo(players.Select(p => p.NetworkID), packet, transferMode);
public void SendTo<T>(IEnumerable<int> ids, T packet, TransferMode? transferMode = null)
{
var info = GetPacketInfoAndVerifyDirection<T>();
var mode = ToPeerTransferMode(info, transferMode);
byte[] bytes = null;
foreach (var id in ids) {
// Only serialize the packet if sending to at least 1 player.
bytes = bytes ?? PacketToBytes(info, packet);
_multiplayerAPI.SendBytes(bytes, id, mode);
}
}
private PacketInfo GetPacketInfoAndVerifyDirection<T>()
{
if (!_packetsByType.TryGetValue(typeof(T), out var info))
throw new InvalidOperationException($"No packet of type {typeof(T)} has been registered");
var direction = Network.IsServer ? PacketDirection.ServerToClient : PacketDirection.ClientToServer;
if ((direction & info.Direction) == 0) throw new InvalidOperationException(
$"Attempting to send packet {typeof(T)} in invalid direction {direction}");
return info;
}
private byte[] PacketToBytes(PacketInfo info, object packet)
{
using (var stream = new MemoryStream()) {
using (var writer = new BinaryWriter(stream)) {
writer.Write((ushort)info.ID);
info.DeSerializer.Serialize(writer, packet);
}
return stream.ToArray();
}
}
private NetworkedMultiplayerPeer.TransferModeEnum ToPeerTransferMode(PacketInfo info, TransferMode? transferMode)
=> (NetworkedMultiplayerPeer.TransferModeEnum)(transferMode ?? info.DefaultTransformMode);
internal void OnPacketReceived(int id, byte[] bytes)
{
if (!Network.IsServer && (id != 1))
throw new Exception($"Received packet from other player (ID {id})");
using (var stream = new MemoryStream(bytes)) {
using (var reader = new BinaryReader(stream)) {
var packetId = reader.ReadUInt16();
if (packetId >= _packetsById.Count) throw new Exception(
$"Received packet with invalid ID {packetId}");
var info = _packetsById[packetId];
var direction = Network.IsServer ? PacketDirection.ClientToServer : PacketDirection.ServerToClient;
if ((direction & info.Direction) == 0) throw new Exception(
$"Received packet {info.Type} on invalid side {(_multiplayerAPI.IsNetworkServer() ? "server" : "client")}");
var packet = info.DeSerializer.Deserialize(reader);
var playerID = Network.IsServer ? id : Network.LocalNetworkID;
info.OnPacketReceived(id, packet);
}
}
}
private INetworkDeSerializer GetDeSerializer(Type type, bool createIfMissing)
{
if (!_deSerializers.TryGetValue(type, out var value)) {
if (!createIfMissing) throw new InvalidOperationException(
$"No DeSerializer for type {type} found");
value = _deSerializerGenerators.Select(g => g.GenerateFor(type)).FirstOrDefault(x => x != null);
if (value == null) value = new ComplexDeSerializer(type);
_deSerializers.Add(type, value);
}
return value;
}
}

@ -1,198 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
public interface INetworkDeSerializer
{
void Serialize(BinaryWriter writer, object value);
object Deserialize(BinaryReader reader);
}
public interface INetworkDeSerializerGenerator
{
INetworkDeSerializer GenerateFor(Type type);
}
public partial class NetworkAPI
{
private class SimpleDeSerializer<T>
: INetworkDeSerializer
{
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 void Serialize(BinaryWriter writer, object value) => _serialize(writer, (T)value);
public object Deserialize(BinaryReader reader) => _deserialize(reader);
}
private class ArrayDeSerializerGenerator
: INetworkDeSerializerGenerator
{
public INetworkDeSerializer GenerateFor(Type type)
{
if (!type.IsArray) return null;
var deSerializerType = typeof(ArrayDeSerializer<>).MakeGenericType(type.GetElementType());
return (INetworkDeSerializer)Activator.CreateInstance(deSerializerType);
}
}
private class ArrayDeSerializer<T>
: INetworkDeSerializer
{
private readonly INetworkDeSerializer _elementDeSerializer =
Network.API.GetDeSerializer(typeof(T), true);
public void Serialize(BinaryWriter writer, object value)
{
var array = (T[])value;
writer.Write(array.Length);
foreach (var element in array) _elementDeSerializer.Serialize(writer, element);
}
public object Deserialize(BinaryReader reader)
{
var length = reader.ReadInt32();
var array = new T[length];
for (var i = 0; i < length; i++)
array[i] = (T)_elementDeSerializer.Deserialize(reader);
return array;
}
}
private class CollectionDeSerializerGenerator
: INetworkDeSerializerGenerator
{
public INetworkDeSerializer GenerateFor(Type type)
{
Type elementType;
if (type.IsInterface) {
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 {
if (type.GetConstructor(Type.EmptyTypes) == null) 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 (INetworkDeSerializer)Activator.CreateInstance(deSerializerType);
}
}
private class CollectionDeSerializer<TCollection, TElement>
: INetworkDeSerializer
where TCollection : ICollection<TElement>, new()
{
private readonly INetworkDeSerializer _elementDeSerializer =
Network.API.GetDeSerializer(typeof(TElement), true);
public void Serialize(BinaryWriter writer, object value)
{
var collection = (TCollection)value;
writer.Write(collection.Count);
foreach (var element in collection)
_elementDeSerializer.Serialize(writer, element);
}
public object Deserialize(BinaryReader reader)
{
var count = reader.ReadInt32();
var collection = new TCollection();
for (var i = 0; i < count; i++)
collection.Add((TElement)_elementDeSerializer.Deserialize(reader));
return collection;
}
}
private class DictionaryDeSerializerGenerator
: INetworkDeSerializerGenerator
{
public INetworkDeSerializer 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 {
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 (INetworkDeSerializer)Activator.CreateInstance(deSerializerType);
}
}
private class DictionaryDeSerializer<TDictionary, TKey, TValue>
: INetworkDeSerializer
where TDictionary : IDictionary<TKey, TValue>, new()
{
private readonly INetworkDeSerializer _keyDeSerializer =
Network.API.GetDeSerializer(typeof(TKey), true);
private readonly INetworkDeSerializer _valueDeSerializer =
Network.API.GetDeSerializer(typeof(TKey), true);
public void Serialize(BinaryWriter writer, object value)
{
var dictionary = (TDictionary)value;
writer.Write(dictionary.Count);
foreach (var element in dictionary) {
_keyDeSerializer.Serialize(writer, element.Key);
_valueDeSerializer.Serialize(writer, element.Value);
}
}
public object Deserialize(BinaryReader reader)
{
var count = reader.ReadInt32();
var dictionary = new TDictionary();
for (var i = 0; i < count; i++)
dictionary.Add((TKey)_keyDeSerializer.Deserialize(reader),
(TValue)_valueDeSerializer.Deserialize(reader));
return dictionary;
}
}
// TODO: Replace this with something that will generate code at runtime for improved performance.
private class ComplexDeSerializer
: INetworkDeSerializer
{
private readonly Type _type;
private event Action<BinaryWriter, object> OnSerialize;
private event Action<BinaryReader, object> OnDeserialize;
public ComplexDeSerializer(Type type)
{
_type = type;
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) {
var deSerializer = Network.API.GetDeSerializer(field.FieldType, true);
OnSerialize += (writer, value) => deSerializer.Serialize(writer, field.GetValue(value));
OnDeserialize += (reader, instance) => field.SetValue(instance, deSerializer.Deserialize(reader));
}
if (OnSerialize == null) throw new InvalidOperationException(
$"Unable to create serializer for type {type}");
}
public void Serialize(BinaryWriter writer, object value)
=> OnSerialize(writer, value);
public object Deserialize(BinaryReader reader)
{
var instance = FormatterServices.GetUninitializedObject(_type);
OnDeserialize(reader, instance);
return instance;
}
}
}

@ -0,0 +1,19 @@
using Godot;
[SyncObject("Block", "World/Blocks")]
public class Block : StaticBody2D
{
[SyncProperty]
public new BlockPos Position {
get => BlockPos.FromVector(base.Position);
set => base.Position = this.SetSync(value).ToVector();
}
[SyncProperty]
public Color Color {
get => Modulate;
set => Modulate = this.SetSync(value);
}
public bool Unbreakable { get; set; } = false;
}

@ -0,0 +1,126 @@
using System;
using Godot;
// TODO: Maybe figure out how we can make different classes (LocalPlayer, NPCPlayer) synchronizable.
[SyncObject("Player", "World/Players")]
public class Player : KinematicBody2D, IInitializer
{
[Export] public NodePath DisplayNamePath { get; set; }
[Export] public NodePath SpritePath { get; set; }
public Label DisplayNameLabel { get; private set; }
public Sprite Sprite { get; private set; }
public bool IsLocal { get; private set; }
[SyncProperty]
public new Vector2 Position {
get => base.Position;
set { if (!IsLocal) base.Position = this.SetSync(value); }
}
[SyncProperty]
public Color Color {
get => Sprite.Modulate;
set => Sprite.Modulate = this.SetSync(value);
}
[SyncProperty]
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 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);
this.GetClient()?.RPC(Move, Position);
}
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()
{
IsLocal = true;
GetNode<Camera2D>("Camera").Current = true;
}
[RPC(PacketDirection.ServerToClient)]
private void ResetPosition(Vector2 position)
{
base.Position = position;
Velocity = Vector2.Zero;
}
[RPC(PacketDirection.ClientToServer)]
private static void Move(Server server, NetworkID networkID, Vector2 position)
{
// TODO: Somewhat verify the movement of players.
var player = server.GetPlayer(networkID);
player.Position = position;
}
[RPC(PacketDirection.ClientToServer)]
public static void ChangeAppearance(Server server, NetworkID networkID, string displayName, Color color)
{
// TODO: Validate input.
var player = server.GetPlayer(networkID);
player.DisplayName = displayName;
player.Color = color;
}
}

@ -1,126 +0,0 @@
using Godot;
public class Player : KinematicBody2D, IInitializer
{
[Export] public NodePath DisplayNamePath { get; set; }
[Export] public NodePath SpritePath { get; set; }
public Label DisplayNameLabel { get; private set; }
public Sprite Sprite { get; private set; }
public bool IsLocal => this is LocalPlayer;
private int _networkId = -1;
public int NetworkID {
get => _networkId;
set { Name = ((_networkId = value) > 0) ? value.ToString() : "LocalPlayer"; }
}
public Color Color {
get => Sprite.Modulate;
set { Sprite.Modulate = value; }
}
public string DisplayName {
get => DisplayNameLabel.Text;
set { DisplayNameLabel.Text = value; }
}
public void Initialize()
{
DisplayNameLabel = GetNode<Label>(DisplayNamePath);
Sprite = GetNode<Sprite>(SpritePath);
}
public override void _Ready()
{
Initialize();
}
public override void _Process(float delta)
{
if (Network.IsAuthoratative && (Position.y > 9000)) {
Position = Vector2.Zero;
if (this is LocalPlayer localPlayer) localPlayer.Velocity = Vector2.Zero;
else Network.API.SendTo(this, new PositionChangedPacket(this), TransferMode.Reliable);
}
if (Network.IsMultiplayerReady) {
if (Network.IsServer) Network.API.SendToEveryoneExcept(this, new PositionChangedPacket(this));
else if (IsLocal) Network.API.SendToServer(new MovePacket(Position));
}
}
public static void RegisterPackets()
{
Network.API.RegisterS2CPacket<PositionChangedPacket>(packet => {
var player = Network.GetPlayerOrThrow(packet.ID);
player.Position = packet.Position;
if (player is LocalPlayer localPlayer)
localPlayer.Velocity = Vector2.Zero;
}, TransferMode.UnreliableOrdered);
Network.API.RegisterS2CPacket<ColorChangedPacket>(packet =>
Network.GetPlayerOrThrow(packet.ID).Color = packet.Color);
Network.API.RegisterS2CPacket<DisplayNameChangedPacket>(packet =>
Network.GetPlayerOrThrow(packet.ID).DisplayName = packet.DisplayName);
Network.API.RegisterC2SPacket<MovePacket>((player, packet) => {
// TODO: Somewhat verify the movement of players.
player.Position = packet.Position;
}, TransferMode.UnreliableOrdered);
Network.API.RegisterC2SPacket<ChangeAppearancePacket>((player, packet) =>
ChangeAppearance(player, packet.DisplayName, packet.Color, false));
}
public static void ChangeAppearance(Player player,
string displayName, Color color, bool sendPacket)
{
if (!sendPacket) {
player.DisplayName = displayName;
player.Color = color;
if (Network.IsServer) {
Network.API.SendToEveryone(new DisplayNameChangedPacket(player));
Network.API.SendToEveryone(new ColorChangedPacket(player));
}
} else Network.API.SendToServer(new ChangeAppearancePacket(displayName, color));
}
private class PositionChangedPacket
{
public int ID { get; }
public Vector2 Position { get; }
public PositionChangedPacket(Player player)
{ ID = player.NetworkID; Position = player.Position; }
}
private class DisplayNameChangedPacket
{
public int ID { get; }
public string DisplayName { get; }
public DisplayNameChangedPacket(Player player)
{ ID = player.NetworkID; DisplayName = player.DisplayName; }
}
private class ColorChangedPacket
{
public int ID { get; }
public Color Color { get; }
public ColorChangedPacket(Player player)
{ ID = player.NetworkID; Color = player.Color; }
}
private class MovePacket
{
public Vector2 Position { get; }
public MovePacket(Vector2 position) => Position = position;
}
private class ChangeAppearancePacket
{
public string DisplayName { get; }
public Color Color { get; }
public ChangeAppearancePacket(string displayName, Color color)
{ DisplayName = displayName; Color = color; }
}
}

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using Godot;
public class Client : Game
{
[Export] public NodePath CursorPath { get; set; }
public Cursor Cursor { get; private set; }
public Client()
{
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);
}
public void Connect(string address, ushort port)
{
if (CustomMultiplayer.NetworkPeer != null)
throw new InvalidOperationException("Client connection is already open");
var peer = new NetworkedMultiplayerENet();
var error = peer.CreateClient(address, port);
if (error != Error.Ok) throw new Exception($"Error when connecting: {error}");
CustomMultiplayer.NetworkPeer = peer;
}
public void Disconnect()
{
if (CustomMultiplayer.NetworkPeer == null) return;
((NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer).CloseConnection();
CustomMultiplayer.NetworkPeer = null;
}
private void OnConnectedToServer()
{
// TODO: Send initial appearance.
}
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);
}

@ -0,0 +1,27 @@
using System.Linq;
using Godot;
using Godot.Collections;
public abstract class Game : Node2D
{
[Export] public NodePath PlayerContainerPath { get; set; }
[Export] public NodePath BlockContainerPath { get; set; }
public Node PlayerContainer { get; private set; }
public Node BlockContainer { get; private set; }
// Using _EnterTree to make sure this code runs before any other.
public override void _EnterTree()
=> GD.Randomize();
public override void _Ready()
{
PlayerContainer = GetNode(PlayerContainerPath);
BlockContainer = GetNode(BlockContainerPath);
}
// 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();
}

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using Godot;
// TODO: Allow for initially private integrated server to open itself up to the public.
public class Server : Game
{
private readonly Dictionary<NetworkID, Player> _playersByNetworkID = new Dictionary<NetworkID, Player>();
private readonly Dictionary<Player, NetworkID> _networkIDByPlayer = new Dictionary<Player, NetworkID>();
public Server()
{
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)
{
CustomMultiplayer.Poll();
NetworkSync.ProcessDirty(this);
NetworkRPC.ProcessPacketBuffer(this);
}
public void Start(ushort port)
{
if (CustomMultiplayer.NetworkPeer != null)
throw new InvalidOperationException("Server is already running");
var peer = new NetworkedMultiplayerENet();
var error = peer.CreateServer(port);
if (error != Error.Ok) throw new Exception($"Error when starting the server: {error}");
CustomMultiplayer.NetworkPeer = peer;
// Spawn default blocks.
for (var x = -6; x <= 6; x++)
this.Spawn<Block>(block => {
block.Position = new BlockPos(x, 3);
block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
block.Unbreakable = true;
});
}
public void Stop()
{
if (CustomMultiplayer.NetworkPeer != null)
throw new InvalidOperationException("Server is not running");
((NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer).CloseConnection();
CustomMultiplayer.NetworkPeer = null;
}
public Player GetPlayer(NetworkID networkID)
=> _playersByNetworkID[networkID];
public NetworkID GetNetworkID(Player player)
=> _networkIDByPlayer[player];
private void OnPeerConnected(int id)
{
var networkID = new NetworkID(id);
NetworkSync.SendAllObjects(this, networkID);
var player = this.Spawn<Player>(p => {
p.Position = Vector2.Zero;
p.Color = Colors.Red;
});
_playersByNetworkID.Add(networkID, player);
_networkIDByPlayer.Add(player, networkID);
player.RPC(new []{ networkID }, player.SetLocal);
}
private void OnPeerDisconnected(int id)
{
var networkID = new NetworkID(id);
var player = GetPlayer(networkID);
player.Destroy();
_playersByNetworkID.Remove(networkID);
_networkIDByPlayer.Remove(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() => $"NetworkID({Value})";
public static bool operator ==(NetworkID left, NetworkID right) => left.Equals(right);
public static bool operator !=(NetworkID left, NetworkID right) => !left.Equals(right);
}

@ -2,6 +2,13 @@ using Godot;
public static class Extensions
{
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 T Init<T>(this PackedScene @this)
where T : Node
{

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using Godot;
public readonly struct BlockPos : IEquatable<BlockPos>
{
public static readonly BlockPos Zero = new BlockPos( 0, 0);
public static readonly BlockPos One = new BlockPos( 1, 1);
public static readonly BlockPos Left = new BlockPos(-1, 0);
public static readonly BlockPos Right = new BlockPos( 1, 0);
public static readonly BlockPos Up = new BlockPos( 0, -1);
public static readonly BlockPos Down = new BlockPos( 0, 1);
public int X { get; }
public int Y { get; }
public BlockPos(int x, int y) { X = x; Y = y; }
public static BlockPos FromVector(Vector2 vec)
=> new BlockPos(Mathf.RoundToInt(vec.x / 16), Mathf.RoundToInt(vec.y / 16));
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
public Vector2 ToVector() => new Vector2(X * 16, Y * 16);
public override string ToString() => $"({X}, {Y})";
public override bool Equals(object obj) => (obj is BlockPos other) && Equals(other);
public bool Equals(BlockPos other) => (other.X == X) && (other.Y == Y);
public override int GetHashCode()
{
int hashCode = 1861411795;
hashCode = hashCode * -1521134295 + X.GetHashCode();
hashCode = hashCode * -1521134295 + Y.GetHashCode();
return hashCode;
}
public static bool operator ==(BlockPos left, BlockPos right) => left.Equals(right);
public static bool operator !=(BlockPos left, BlockPos right) => !left.Equals(right);
public static BlockPos operator -(BlockPos value)
=> new BlockPos(-value.X, -value.Y);
public static BlockPos operator +(BlockPos left, BlockPos right)
=> new BlockPos(left.X + right.X, left.Y + right.Y);
public static BlockPos operator -(BlockPos left, BlockPos right)
=> new BlockPos(left.X - right.X, left.Y - right.Y);
public static BlockPos operator *(BlockPos left, int right)
=> new BlockPos(left.X * right, left.Y * right);
public static BlockPos operator /(BlockPos left, int right)
=> new BlockPos(left.X / right, left.Y / right);
}
public enum Facing
{
Left,
Right,
Up,
Down,
}
public static class Facings
{
public static readonly IReadOnlyCollection<Facing> All = new []{ Facing.Left, Facing.Right, Facing.Up, Facing.Down };
public static readonly IReadOnlyCollection<Facing> Horizontal = new []{ Facing.Left, Facing.Right };
public static readonly IReadOnlyCollection<Facing> Vertical = new []{ Facing.Up, Facing.Down };
public static BlockPos ToBlockPos(this Facing facing)
{
switch (facing) {
case Facing.Left: return BlockPos.Left;
case Facing.Right: return BlockPos.Right;
case Facing.Up: return BlockPos.Up;
case Facing.Down: return BlockPos.Down;
default: throw new ArgumentException();
}
}
public static Vector2 ToVector(this Facing facing)
=> facing.ToBlockPos().ToVector();
public static Facing FromAngle(float radians)
{
radians = ((radians % (Mathf.Pi*2)) + Mathf.Pi*2) % (Mathf.Pi*2);
if (radians < Mathf.Pi / 4) return Facing.Right;
else if (radians < Mathf.Pi / 4 * 3) return Facing.Down;
else if (radians < Mathf.Pi / 4 * 5) return Facing.Left;
else if (radians < Mathf.Pi / 4 * 7) return Facing.Up;
else return Facing.Right;
}
public static float ToAngle(this Facing facing)
{
switch (facing) {
case Facing.Right: return 0;
case Facing.Down: return Mathf.Pi / 2;
case Facing.Left: return Mathf.Pi;
case Facing.Up: return Mathf.Pi / 2 * 3;
default: throw new ArgumentException();
}
}
}
Loading…
Cancel
Save