Add Saving / Loading support

- Add SaveRegistry and SaveAttribute
- Add Save, which handles Saving / Loading
- Add SpawnRegistry and SpawnAttribute
  Allows spawning objects by ID or Type
- PropertyDeSerializer handles de/serialization
  of any [Sync] and [Save] properties
- Removing objects now done by
  removing it from the scene tree
- Add "World" tab to EscapeMenu
- Modify Heartbit font's 1 and V characters
main
copygirl 4 years ago
parent 7861e524ab
commit fdf1782069
  1. 4
      CREDITS.md
  2. BIN
      HeartbitXX.ttf
  3. 2
      project.godot
  4. 227
      scene/EscapeMenu.tscn
  5. 4
      src/CreativeBuilding.cs
  6. 2
      src/EscapeMenu.cs
  7. 8
      src/EscapeMenuMultiplayer.cs
  8. 133
      src/EscapeMenuWorld.cs
  9. 31
      src/IO/DeSerializer.Impl.cs
  10. 8
      src/IO/DeSerializerRegistry.cs
  11. 48
      src/IO/PropertyDeSerializer.cs
  12. 100
      src/IO/Save.cs
  13. 69
      src/IO/SaveRegistry.cs
  14. 6
      src/Network/IntegratedServer.cs
  15. 66
      src/Network/Sync.cs
  16. 22
      src/Network/SyncClient.cs
  17. 85
      src/Network/SyncRegistry.cs
  18. 54
      src/Network/SyncServer.cs
  19. 6
      src/Objects/Block.cs
  20. 58
      src/Objects/ObjectHolder.cs
  21. 4
      src/Objects/Player.cs
  22. 70
      src/Objects/SpawnRegistry.cs
  23. 7
      src/Scenes/Game.cs
  24. 7
      src/Scenes/Server.cs
  25. 36
      src/Utility/Extensions.cs
  26. 2
      ui_theme.tres

@ -5,5 +5,7 @@
**URL:** https://arcade.itch.io/heartbit
**Author:** Void
**License:** Custom
> You can use this font in free and personal projects, works or games.
> You can use this font in free and personal projects, works or games.
> You'll need to pay the suggested price for commercial projects.
**Notes:** Modified `1` and `V` characters.

Binary file not shown.

@ -16,6 +16,8 @@ _global_script_class_icons={
config/name="YourFortV"
run/main_scene="res://scene/ClientScene.tscn"
config/use_custom_user_dir=true
config/custom_user_dir_name="YourFortV"
boot_splash/image="res://gfx/icon.png"
boot_splash/use_filter=false
config/icon="res://gfx/icon.png"

@ -1,10 +1,11 @@
[gd_scene load_steps=6 format=2]
[gd_scene load_steps=7 format=2]
[ext_resource path="res://ui_theme.tres" type="Theme" id=1]
[ext_resource path="res://src/EscapeMenu.cs" type="Script" id=2]
[ext_resource path="res://gfx/player.png" type="Texture" id=3]
[ext_resource path="res://src/EscapeMenuMultiplayer.cs" type="Script" id=4]
[ext_resource path="res://src/EscapeMenuAppearance.cs" type="Script" id=5]
[ext_resource path="res://src/EscapeMenuWorld.cs" type="Script" id=6]
[node name="EscapeMenu" type="Control"]
anchor_right = 1.0
@ -25,6 +26,47 @@ __meta__ = {
"_edit_lock_": true
}
[node name="SaveFileDialog" type="FileDialog" parent="."]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -240.0
margin_top = -160.0
margin_right = 240.0
margin_bottom = 160.0
rect_min_size = Vector2( 0, 0 )
window_title = "Save World As..."
mode_overrides_title = false
access = 2
filters = PoolStringArray( "*.yf5 ; YourFortV Save" )
current_dir = "/"
current_path = "/"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="LoadFileDialog" type="FileDialog" parent="."]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -240.0
margin_top = -160.0
margin_right = 240.0
margin_bottom = 160.0
rect_min_size = Vector2( 0, 0 )
window_title = "Load World From..."
mode_overrides_title = false
mode = 0
access = 2
filters = PoolStringArray( "*.yf5 ; YourFortV Save" )
current_dir = "/"
current_path = "/"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CenterContainer" type="CenterContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
@ -66,8 +108,7 @@ margin_bottom = 17.0
margin_top = 21.0
margin_right = 229.0
margin_bottom = 139.0
custom_constants/side_margin = 4
tab_align = 0
custom_constants/side_margin = 0
use_hidden_tabs_for_min_size = true
[node name="Appearance" type="CenterContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer"]
@ -148,7 +189,7 @@ text = "(Close Menu to apply changes.)"
align = 1
valign = 2
[node name="Multiplayer" type="VBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer"]
[node name="Multiplayer" type="CenterContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer"]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
@ -157,20 +198,24 @@ margin_top = 27.0
margin_right = -4.0
margin_bottom = -4.0
script = ExtResource( 4 )
StatusPath = NodePath("ContainerStatus/Status")
ServerOpenClosePath = NodePath("ContainerServer/ServerOpenClose")
ServerPortPath = NodePath("ContainerServer/ServerPort")
ClientDisConnectPath = NodePath("ContainerClient/ClientDisConnect")
ClientAddressPath = NodePath("ContainerClient/ClientAddress")
StatusPath = NodePath("VBoxContainer/ContainerStatus/Status")
ServerOpenClosePath = NodePath("VBoxContainer/ContainerServer/ServerOpenClose")
ServerPortPath = NodePath("VBoxContainer/ContainerServer/ServerPort")
ClientDisConnectPath = NodePath("VBoxContainer/ContainerClient/ClientDisConnect")
ClientAddressPath = NodePath("VBoxContainer/ContainerClient/ClientAddress")
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer"]
margin_right = 221.0
margin_bottom = 87.0
[node name="ContainerStatus" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer"]
[node name="ContainerStatus" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer"]
margin_right = 221.0
margin_bottom = 13.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerStatus"]
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerStatus"]
margin_top = 2.0
margin_right = 36.0
margin_bottom = 11.0
@ -178,7 +223,7 @@ rect_min_size = Vector2( 36, 0 )
text = "Status:"
align = 2
[node name="Status" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerStatus"]
[node name="Status" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerStatus"]
modulate = Color( 1, 0, 0, 1 )
margin_left = 40.0
margin_right = 221.0
@ -193,12 +238,12 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="ContainerServer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer"]
[node name="ContainerServer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer"]
margin_top = 17.0
margin_right = 221.0
margin_bottom = 36.0
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer"]
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerServer"]
margin_top = 5.0
margin_right = 36.0
margin_bottom = 14.0
@ -206,7 +251,7 @@ rect_min_size = Vector2( 36, 0 )
text = "Port:"
align = 2
[node name="ServerPort" type="LineEdit" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer"]
[node name="ServerPort" type="LineEdit" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerServer"]
margin_left = 40.0
margin_right = 90.0
margin_bottom = 19.0
@ -218,7 +263,7 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="ServerOpenClose" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerServer"]
[node name="ServerOpenClose" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerServer"]
margin_left = 94.0
margin_right = 221.0
margin_bottom = 19.0
@ -228,12 +273,12 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="ContainerClient" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer"]
[node name="ContainerClient" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer"]
margin_top = 40.0
margin_right = 221.0
margin_bottom = 59.0
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerClient"]
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerClient"]
margin_top = 5.0
margin_right = 36.0
margin_bottom = 14.0
@ -241,7 +286,7 @@ rect_min_size = Vector2( 36, 0 )
text = "Address:"
align = 2
[node name="ClientAddress" type="LineEdit" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerClient"]
[node name="ClientAddress" type="LineEdit" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerClient"]
margin_left = 40.0
margin_right = 160.0
margin_bottom = 19.0
@ -249,7 +294,7 @@ rect_min_size = Vector2( 120, 0 )
align = 1
caret_blink = true
[node name="ClientDisConnect" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerClient"]
[node name="ClientDisConnect" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerClient"]
margin_left = 164.0
margin_right = 221.0
margin_bottom = 19.0
@ -257,17 +302,17 @@ rect_min_size = Vector2( 57, 0 )
size_flags_horizontal = 3
text = "Connect"
[node name="ContainerHideAddress" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer"]
[node name="ContainerHideAddress" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer"]
margin_top = 63.0
margin_right = 221.0
margin_bottom = 87.0
[node name="HideAddress" type="CheckBox" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerHideAddress"]
[node name="HideAddress" type="CheckBox" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerHideAddress"]
margin_right = 82.0
margin_bottom = 24.0
text = "Hide Address"
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/ContainerHideAddress"]
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerHideAddress"]
margin_left = 86.0
margin_top = 7.0
margin_right = 173.0
@ -275,6 +320,129 @@ margin_bottom = 16.0
custom_colors/font_color = Color( 0.6, 0.6, 0.6, 1 )
text = "(for streamers etc.)"
[node name="World" type="CenterContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer"]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 4.0
margin_top = 27.0
margin_right = -4.0
margin_bottom = -4.0
script = ExtResource( 6 )
FilenamePath = NodePath("VBoxContainer/HBoxContainer/Filename")
LastSavedPath = NodePath("VBoxContainer/HBoxContainer3/LastSaved")
PlaytimePath = NodePath("VBoxContainer/HBoxContainer2/Playtime")
QuickSavePath = NodePath("VBoxContainer/HBoxContainer4/QuickSave")
SaveAsPath = NodePath("VBoxContainer/HBoxContainer4/SaveAs")
SaveFileDialogPath = NodePath("../../../../../SaveFileDialog")
LoadFileDialogPath = NodePath("../../../../../LoadFileDialog")
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World"]
margin_left = 20.0
margin_top = 1.0
margin_right = 200.0
margin_bottom = 86.0
rect_min_size = Vector2( 180, 0 )
[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer"]
margin_right = 180.0
margin_bottom = 9.0
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer"]
margin_right = 58.0
margin_bottom = 9.0
rect_min_size = Vector2( 58, 0 )
text = "Filename:"
align = 2
[node name="Filename" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer"]
margin_left = 62.0
margin_right = 180.0
margin_bottom = 9.0
size_flags_horizontal = 3
custom_colors/font_color = Color( 0.6, 0.6, 0.6, 1 )
text = "-not saved yet-"
align = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer"]
margin_top = 13.0
margin_right = 180.0
margin_bottom = 22.0
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer2"]
margin_right = 58.0
margin_bottom = 9.0
rect_min_size = Vector2( 58, 0 )
text = "Playtime:"
align = 2
[node name="Playtime" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer2"]
margin_left = 62.0
margin_right = 180.0
margin_bottom = 9.0
size_flags_horizontal = 3
custom_colors/font_color = Color( 0.6, 0.6, 0.6, 1 )
text = "000d 00h 00m 00s"
align = 1
[node name="HBoxContainer3" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer"]
margin_top = 26.0
margin_right = 180.0
margin_bottom = 35.0
[node name="Label" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer3"]
margin_right = 58.0
margin_bottom = 9.0
rect_min_size = Vector2( 58, 0 )
text = "Last Saved:"
align = 2
[node name="LastSaved" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer3"]
margin_left = 62.0
margin_right = 180.0
margin_bottom = 9.0
size_flags_horizontal = 3
custom_colors/font_color = Color( 0.6, 0.6, 0.6, 1 )
text = "0000-00-00 00:00"
align = 1
[node name="HSeparator" type="HSeparator" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer"]
margin_top = 39.0
margin_right = 180.0
margin_bottom = 43.0
[node name="HBoxContainer4" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer"]
margin_top = 47.0
margin_right = 180.0
margin_bottom = 64.0
[node name="QuickSave" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer4"]
margin_right = 112.0
margin_bottom = 17.0
rect_min_size = Vector2( 112, 17 )
size_flags_horizontal = 3
text = "Quick Save"
[node name="SaveAs" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer4"]
margin_left = 116.0
margin_right = 180.0
margin_bottom = 17.0
rect_min_size = Vector2( 0, 17 )
size_flags_horizontal = 3
text = "Save As..."
[node name="HBoxContainer5" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer"]
margin_top = 68.0
margin_right = 180.0
margin_bottom = 85.0
[node name="LoadFrom" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer5"]
margin_right = 180.0
margin_bottom = 17.0
rect_min_size = Vector2( 80, 17 )
size_flags_horizontal = 3
text = "Load World From..."
[node name="HSeparator2" type="HSeparator" parent="CenterContainer/PanelContainer/VBoxContainer"]
margin_top = 143.0
margin_right = 229.0
@ -333,12 +501,17 @@ __meta__ = {
"_edit_use_anchors_": false
}
[connection signal="file_selected" from="SaveFileDialog" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World" method="_on_SaveFileDialog_file_selected"]
[connection signal="file_selected" from="LoadFileDialog" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World" method="_on_LoadFileDialog_file_selected"]
[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/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="text_changed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerServer/ServerPort" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerPort_text_changed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerServer/ServerOpenClose" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ServerOpenClose_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerClient/ClientDisConnect" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_ClientDisConnect_pressed"]
[connection signal="toggled" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer/VBoxContainer/ContainerHideAddress/HideAddress" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/Multiplayer" method="_on_HideAddress_toggled"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer4/QuickSave" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World" method="_on_QuickSave_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer4/SaveAs" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World" method="_on_SaveAs_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World/VBoxContainer/HBoxContainer5/LoadFrom" to="CenterContainer/PanelContainer/VBoxContainer/TabContainer/World" method="_on_LoadFrom_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/Quit" to="." method="_on_Quit_pressed"]
[connection signal="pressed" from="CenterContainer/PanelContainer/VBoxContainer/Return" to="." method="_on_Return_pressed"]

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

@ -14,7 +14,7 @@ public class EscapeMenu : Control
Return = GetNode<Button>(ReturnPath);
}
public override void _Input(InputEvent @event)
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("ui_menu")) Toggle();
}

@ -38,7 +38,7 @@ public class EscapeMenuMultiplayer : Container
private void SetupIntegratedServer()
{
IntegratedServer = new IntegratedServer();
this.GetClient().AddChild(IntegratedServer);
this.GetClient().AddChild(IntegratedServer, true);
CallDeferred(nameof(StartIntegratedServerAndConnect));
}
private void StartIntegratedServerAndConnect()
@ -137,14 +137,11 @@ public class EscapeMenuMultiplayer : Container
IntegratedServer.Server.Stop();
// TODO: Have a single method to "reset" the state?
IntegratedServer.Server.Objects.Clear();
IntegratedServer.Server.Sync.Clear();
IntegratedServer.GetParent().RemoveChild(IntegratedServer);
IntegratedServer.QueueFree();
IntegratedServer.RemoveFromParent();
IntegratedServer = null;
client.Disconnect();
client.Objects.Clear();
client.Sync.Clear();
}
if (client.Status == ConnectionStatus.Disconnected) {
@ -160,7 +157,6 @@ public class EscapeMenuMultiplayer : Container
} else {
client.Disconnect();
client.Objects.Clear();
client.Sync.Clear();
}
}
}

@ -0,0 +1,133 @@
using System;
using System.Text;
using Godot;
using Path = System.IO.Path;
using File = System.IO.File;
using Directory = System.IO.Directory;
using System.Linq;
using static Godot.NetworkedMultiplayerPeer;
public class EscapeMenuWorld : CenterContainer
{
[Export] public NodePath FilenamePath { get; set; }
[Export] public NodePath LastSavedPath { get; set; }
[Export] public NodePath PlaytimePath { get; set; }
[Export] public NodePath QuickSavePath { get; set; }
[Export] public NodePath SaveAsPath { get; set; }
[Export] public NodePath SaveFileDialogPath { get; set; }
[Export] public NodePath LoadFileDialogPath { get; set; }
public Label FilenameLabel { get; private set; }
public Label LastSavedLabel { get; private set; }
public Label PlaytimeLabel { get; private set; }
public Button QuickSaveButton { get; private set; }
public Button SaveAsButton { get; private set; }
public FileDialog SaveFileDialog { get; private set; }
public FileDialog LoadFileDialog { get; private set; }
private Node _world;
private TimeSpan _playtime;
private string _currentWorld;
public override void _Ready()
{
_world = this.GetClient().GetNode("World");
FilenameLabel = GetNode<Label>(FilenamePath);
LastSavedLabel = GetNode<Label>(LastSavedPath);
PlaytimeLabel = GetNode<Label>(PlaytimePath);
QuickSaveButton = GetNode<Button>(QuickSavePath);
SaveAsButton = GetNode<Button>(SaveAsPath);
SaveFileDialog = GetNode<FileDialog>(SaveFileDialogPath);
LoadFileDialog = GetNode<FileDialog>(LoadFileDialogPath);
// TODO: Reset this when going back to singleplayer after having connected to a multiplayer server.
QuickSaveButton.Visible = false;
SaveAsButton.Text = "Save World As...";
SaveFileDialog.GetOk().Text = "Save";
var worldsFolder = OS.GetUserDataDir() + "/worlds/";
Directory.CreateDirectory(worldsFolder);
SaveFileDialog.CurrentPath = worldsFolder;
LoadFileDialog.CurrentPath = worldsFolder;
this.GetClient().StatusChanged += OnStatusChanged;
}
public override void _Process(float delta)
{
if (!GetTree().Paused || (_world.PauseMode != PauseModeEnum.Stop))
_playtime += TimeSpan.FromSeconds(delta);
var b = new StringBuilder();
if (_playtime.Days > 0) b.Append(_playtime.Days).Append("d ");
if (_playtime.Hours > 0) b.Append(_playtime.Hours).Append("h ");
if (_playtime.Minutes < 10) b.Append('0'); b.Append(_playtime.Minutes).Append("m ");
if (_playtime.Seconds < 10) b.Append('0'); b.Append(_playtime.Seconds).Append("s");
PlaytimeLabel.Text = b.ToString();
}
private void OnStatusChanged(ConnectionStatus status)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer));
GetParent<TabContainer>().SetTabDisabled(GetIndex(), server == null);
}
#pragma warning disable IDE0051
#pragma warning disable IDE1006
private void _on_QuickSave_pressed()
=> _on_SaveFileDialog_file_selected(_currentWorld);
private void _on_SaveAs_pressed()
{
SaveFileDialog.Invalidate();
SaveFileDialog.PopupCenteredClamped(new Vector2(480, 320), 0.85F);
}
private void _on_SaveFileDialog_file_selected(string path)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = Save.CreateFromWorld(server, _playtime);
save.WriteToFile(path + ".tmp");
File.Delete(path); // TODO: In later .NET, there is a File.Move(source, dest, overwrite).
File.Move(path + ".tmp", path);
_currentWorld = path;
FilenameLabel.Text = Path.GetFileName(path);
LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
QuickSaveButton.Visible = true;
SaveAsButton.Text = "Save As...";
}
private void _on_LoadFrom_pressed()
{
LoadFileDialog.Invalidate();
LoadFileDialog.PopupCenteredClamped(new Vector2(480, 320), 0.85F);
}
private void _on_LoadFileDialog_file_selected(string path)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = Save.ReadFromFile(path);
// Clear out all objects that have a SaveAttribute.
var objectsToRemove = server.Objects.Select(x => x.Item2)
.Where(x => SaveRegistry.GetOrNull(x.GetType()) != null).ToArray();
foreach (var obj in objectsToRemove) obj.RemoveFromParent();
// Reset players' positions.
foreach (var (id, player) in server.Players)
player.RPC(new []{ id }, player.ResetPosition, Vector2.Zero);
save.AddToWorld(server);
_playtime = save.Playtime;
_currentWorld = path;
FilenameLabel.Text = Path.GetFileName(path);
LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
QuickSaveButton.Visible = true;
SaveAsButton.Text = "Save As...";
}
}

@ -199,7 +199,7 @@ public class DictionaryDeSerializerGenerator
var dictionary = new TDictionary();
for (var i = 0; i < count; i++)
dictionary.Add((TKey)_keyDeSerializer.Deserialize(game, reader),
(TValue)_valueDeSerializer.Deserialize(game, reader));
(TValue)_valueDeSerializer.Deserialize(game, reader));
return dictionary;
}
}
@ -224,7 +224,7 @@ public class NodeDeSerializerGenerator
public override TObj Deserialize(Game game, BinaryReader reader)
{
var id = new UniqueID(reader.ReadUInt32());
var value = (TObj)game.Objects.GetNodeByID(id);
var value = (TObj)game.Objects.GetObjectByID(id);
if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}");
return value;
}
@ -232,31 +232,30 @@ public class NodeDeSerializerGenerator
}
// TODO: Replace this with something that will generate code at runtime for improved performance.
public class ComplexDeSerializer
: IDeSerializer
public class ComplexDeSerializer<T>
: DeSerializer<T>
{
private readonly Type _type;
private event Action<Game, BinaryWriter, object> OnSerialize;
private event Action<Game, BinaryReader, object> OnDeserialize;
public ComplexDeSerializer(Type type)
public ComplexDeSerializer()
{
_type = type;
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) {
foreach (var field in typeof(T).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));
OnSerialize += (game, writer, obj) => deSerializer.Serialize(game, writer, field.GetValue(obj));
OnDeserialize += (game, reader, obj) => field.SetValue(obj, deSerializer.Deserialize(game, reader));
}
if (OnSerialize == null) throw new InvalidOperationException(
$"Unable to create serializer for type {type}");
$"Unable to create {nameof(ComplexDeSerializer<T>)} for type {typeof(T)}");
}
public void Serialize(Game game, BinaryWriter writer, object value)
public override void Serialize(Game game, BinaryWriter writer, T value)
=> OnSerialize(game, writer, value);
public object Deserialize(Game game, BinaryReader reader)
public override T Deserialize(Game game, BinaryReader reader)
{
var instance = FormatterServices.GetUninitializedObject(_type);
OnDeserialize(game, reader, instance);
return instance;
var value = FormatterServices.GetUninitializedObject(typeof(T));
OnDeserialize(game, reader, value);
return (T)value;
}
}

@ -56,8 +56,12 @@ public static class DeSerializerRegistry
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);
value = _generators.Select(g => g.GenerateFor(type))
.FirstOrDefault(x => x != null);
if (value == null) {
var deSerializerType = typeof(ComplexDeSerializer<>).MakeGenericType(type);
value = (IDeSerializer)Activator.CreateInstance(deSerializerType);
}
_byType.Add(type, value);
}
return value;

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

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

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

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

@ -6,14 +6,52 @@ using Godot;
// TODO: Allow syncronization of child objects spawned with their parent objects.
// TODO: Specify who properties are syncronized with. (Owner, Friends, Team, Everyone)
public class Sync
public abstract class Sync
{
protected Game Game { get; }
protected Dictionary<UniqueID, SyncStatus> StatusByID { get; } = new Dictionary<UniqueID, SyncStatus>();
protected Dictionary<Node, SyncStatus> StatusByObject { get; } = new Dictionary<Node, SyncStatus>();
static Sync() => DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer());
public Sync(Game game) => Game = game;
static Sync()
=> DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer());
public Sync(Game game)
{
Game = game;
Game.Objects.Added += OnObjectAdded;
Game.Objects.Removed += OnObjectRemoved;
Game.Objects.Cleared += OnObjectsCleared;
}
private void OnObjectAdded(UniqueID id, Node obj)
{
var info = SyncRegistry.GetOrNull(obj.GetType());
if (info == null) return;
var status = new SyncStatus(id, obj, info);
StatusByID.Add(id, status);
StatusByObject.Add(obj, status);
OnSyncedAdded(status);
}
private void OnObjectRemoved(UniqueID id, Node obj)
{
if (!StatusByObject.TryGetValue(obj, out var status)) return;
StatusByID.Remove(status.ID);
StatusByObject.Remove(status.Object);
OnSyncedRemoved(status);
}
protected virtual void OnSyncedAdded(SyncStatus status) { }
protected virtual void OnSyncedRemoved(SyncStatus status) { }
private void OnObjectsCleared()
{
StatusByID.Clear();
StatusByObject.Clear();
}
public SyncStatus GetStatusOrNull(UniqueID id)
=> StatusByID.TryGetValue(id, out var value) ? value : null;
@ -30,18 +68,6 @@ public class Sync
public SyncStatus GetStatusOrThrow(Node obj)
=> GetStatusOrNull(obj) ?? throw new Exception(
$"No {nameof(SyncStatus)} found for '{obj.Name}' ({obj.GetType()})");
public virtual void Clear()
{
foreach (var (node, _) in StatusByObject) {
if (!Godot.Object.IsInstanceValid(node)) continue;
node.GetParent().RemoveChild(node);
node.QueueFree();
}
StatusByID.Clear();
StatusByObject.Clear();
}
}
@ -91,12 +117,11 @@ internal class SyncPacketObjectDeSerializer
writer.Write((byte)value.Mode);
writer.Write((byte)value.Values.Count);
var objInfo = SyncRegistry.Get(value.InfoID);
var objInfo = SyncRegistry.GetOrThrow(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);
propInfo.DeSerializer.Serialize(game, writer, val);
}
}
@ -107,7 +132,7 @@ internal class SyncPacketObjectDeSerializer
var mode = (SyncMode)reader.ReadByte();
var count = reader.ReadByte();
var objInfo = SyncRegistry.Get(infoID);
var objInfo = SyncRegistry.GetOrThrow(infoID);
if (count > objInfo.PropertiesByID.Count) throw new Exception(
$"Count is higher than possible number of changes");
@ -120,8 +145,7 @@ internal class SyncPacketObjectDeSerializer
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)));
values.Add((propID, propInfo.DeSerializer.Deserialize(game, reader)));
}
return new SyncPacket.Object(infoID, id, mode, values);

@ -13,21 +13,17 @@ public class SyncClient : Sync
private void OnSyncPacket(Game _, NetworkID networkID, SyncPacket packet)
{
foreach (var packetObj in packet.Changes) {
var info = SyncRegistry.Get(packetObj.InfoID);
var info = SyncRegistry.GetOrThrow(packetObj.InfoID);
var status = GetStatusOrNull(packetObj.ID);
if (status == null) {
if (packetObj.Mode != SyncMode.Spawn) throw new Exception(
$"Unknown synced object {info.Name} (ID {packetObj.ID})");
var obj = info.Scene.Init<Node>();
var obj = info.SpawnInfo.Scene.Init<Node>();
Client.GetNode("World").AddChild(obj, true);
Client.Objects.Add(packetObj.ID, obj);
status = new SyncStatus(packetObj.ID, obj, info);
StatusByID.Add(status.ID, status);
StatusByObject.Add(status.Object, status);
Client.GetNode("World").AddChild(obj);
status = GetStatusOrThrow(packetObj.ID);
} else {
if (packetObj.Mode == SyncMode.Spawn) throw new Exception(
$"Spawning object {info.Name} with ID {packetObj.ID}, but it already exists");
@ -35,18 +31,14 @@ public class SyncClient : Sync
$"Info of synced object being modified doesn't match ({info.Name} != {status.Info.Name})");
if (packetObj.Mode == SyncMode.Destroy) {
StatusByID.Remove(status.ID);
StatusByObject.Remove(status.Object);
status.Object.GetParent().RemoveChild(status.Object);
status.Object.QueueFree();
status.Object.RemoveFromParent();
continue;
}
}
foreach (var (propID, value) in packetObj.Values) {
var propInfo = info.PropertiesByID[propID];
propInfo.Setter(status.Object, value);
var propDeSerializer = info.PropertiesByID[propID];
propDeSerializer.Set(status.Object, value);
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Godot;
@ -11,86 +12,54 @@ public static class SyncRegistry
static SyncRegistry()
{
foreach (var type in typeof(SyncRegistry).Assembly.GetTypes()) {
var objAttr = type.GetCustomAttribute<SyncAttribute>();
if (objAttr == null) continue;
var syncAttr = type.GetCustomAttribute<SyncAttribute>();
if (syncAttr == null) continue;
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception(
$"Type {type} with {nameof(SyncAttribute)} must be a subclass of {nameof(Node)}");
var objInfo = new SyncObjectInfo((ushort)_byID.Count, type);
var spawnInfo = SpawnRegistry.Get(type);
var objInfo = new SyncObjectInfo(type, spawnInfo);
foreach (var property in type.GetProperties()) {
if (property.GetCustomAttribute<SyncAttribute>() == null) continue;
var propType = typeof(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);
var propType = typeof(PropertyDeSerializer<,>).MakeGenericType(type, property.PropertyType);
var propDeSerializer = (IPropertyDeSerializer)Activator.CreateInstance(propType, property);
objInfo.PropertiesByName.Add(propDeSerializer.Name, propDeSerializer);
}
_byID.Add(objInfo);
objInfo.PropertiesByID.AddRange(objInfo.PropertiesByName.Values.OrderBy(x => x.HashID));
_byType.Add(objInfo.Type, objInfo);
}
_byID.AddRange(_byType.Values.OrderBy(x => x.Name.GetDeterministicHashCode()));
for (ushort i = 0; i < _byID.Count; i++) _byID[i].ID = i;
}
public static SyncObjectInfo Get(ushort id)
public static SyncObjectInfo GetOrThrow(ushort id)
=> (id < _byID.Count) ? _byID[id] : throw new Exception(
$"Unknown {nameof(SyncObjectInfo)} with ID {id}");
public static SyncObjectInfo Get<T>()
=> Get(typeof(T));
public static SyncObjectInfo Get(Type type)
=> _byType.TryGetValue(type, out var value) ? value : throw new Exception(
$"No {nameof(SyncObjectInfo)} found for type {type} (missing {nameof(SyncAttribute)}?)");
}
public static SyncObjectInfo GetOrNull<T>() => GetOrNull(typeof(T));
public static SyncObjectInfo GetOrNull(Type type) => _byType.TryGetValue(type, out var value) ? value : null;
public static SyncObjectInfo GetOrThrow<T>() => GetOrThrow(typeof(T));
public static SyncObjectInfo GetOrThrow(Type type) => GetOrNull(type) ?? throw new Exception(
$"No {nameof(SyncObjectInfo)} found for type {type} (missing {nameof(SyncAttribute)}?)");
}
public class SyncObjectInfo
{
public ushort ID { get; }
public ushort ID { get; internal set; }
public Type Type { get; }
public PackedScene Scene { get; }
public SpawnInfo SpawnInfo { get; }
public string Name => Type.Name;
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;
Scene = GD.Load<PackedScene>($"res://scene/{type.Name}.tscn");
}
}
public abstract class SyncPropertyInfo
{
public byte ID { get; }
public PropertyInfo Property { get; }
public string Name => Property.Name;
public Type Type => Property.PropertyType;
public Func<object, object> Getter { get; }
public Action<object, object> Setter { get; }
public SyncPropertyInfo(byte id, PropertyInfo property,
Func<object, object> getter, Action<object, object> setter)
{
ID = id; Property = property;
Getter = getter; Setter = setter;
}
}
public List<IPropertyDeSerializer> PropertiesByID { get; } = new List<IPropertyDeSerializer>();
public Dictionary<string, IPropertyDeSerializer> PropertiesByName { get; } = new Dictionary<string, IPropertyDeSerializer>();
public class SyncPropertyInfo<TObject, TValue> : SyncPropertyInfo
{
public SyncPropertyInfo(byte id, PropertyInfo property) : base(id, property,
obj => ((Func<TObject, TValue>)property.GetMethod.CreateDelegate(typeof(Func<TObject, TValue>))).Invoke((TObject)obj),
(obj, value) => ((Action<TObject, TValue>)property.SetMethod.CreateDelegate(typeof(Action<TObject, TValue>))).Invoke((TObject)obj, (TValue)value)
) { }
public SyncObjectInfo(Type type, SpawnInfo spawnInfo)
{ Type = type; SpawnInfo = spawnInfo; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class SyncAttribute : Attribute
{
}
public class SyncAttribute : Attribute { }

@ -9,48 +9,31 @@ public class SyncServer : Sync
protected Server Server => (Server)Game;
public SyncServer(Server server)
: base(server) { }
public SyncServer(Server server) : base(server)
=> server.Objects.Cleared += _dirtyObjects.Clear;
public T Spawn<T>()
where T : Node
protected override void OnSyncedAdded(SyncStatus status)
{
var info = SyncRegistry.Get<T>();
var obj = info.Scene.Init<T>();
var id = Server.Objects.Add(obj);
var status = new SyncStatus(id, obj, info){ Mode = SyncMode.Spawn };
StatusByID.Add(status.ID, status);
StatusByObject.Add(status.Object, status);
status.Mode = SyncMode.Spawn;
_dirtyObjects.Add(status);
Server.GetNode("World").AddChild(obj);
return obj;
}
// TODO: Do this automatically if the node is removed from the tree?
public void Destroy(Node obj)
protected override void OnSyncedRemoved(SyncStatus status)
{
var status = GetStatusOrThrow(obj);
status.Mode = SyncMode.Destroy;
StatusByID.Remove(status.ID);
StatusByObject.Remove(status.Object);
_dirtyObjects.Add(status);
obj.GetParent().RemoveChild(obj);
obj.QueueFree();
}
public void MarkDirty(Node obj, string property)
{
var status = GetStatusOrThrow(obj);
if (!status.Info.PropertiesByName.TryGetValue(property, out var propInfo)) throw new ArgumentException(
$"No {nameof(SyncPropertyInfo)} found for {obj.GetType()}.{property} (missing {nameof(SyncAttribute)}?)", nameof(property));
if (!status.Info.PropertiesByName.TryGetValue(property, out var propDeSerializer)) throw new ArgumentException(
$"No {nameof(IPropertyDeSerializer)} found for {obj.GetType()}.{property} (missing {nameof(SyncAttribute)}?)", nameof(property));
if (!(obj.GetGame() is Server)) return;
status.DirtyProperties |= 1 << propInfo.ID;
var index = status.Info.PropertiesByID.IndexOf(propDeSerializer);
status.DirtyProperties |= 1 << index;
_dirtyObjects.Add(status);
}
@ -62,9 +45,10 @@ public class SyncServer : Sync
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)));
if (status.Mode != SyncMode.Destroy)
for (byte i = 0; i < status.Info.PropertiesByID.Count; i++)
if ((status.DirtyProperties & (1 << i)) != 0)
values.Add((i, status.Info.PropertiesByID[i].Get(status.Object)));
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, status.Mode, values));
// If the object has been newly spawned, now is the time to remove the "Spawn" flag.
if (status.Mode == SyncMode.Spawn) status.Mode = SyncMode.Default;
@ -80,16 +64,10 @@ public class SyncServer : Sync
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)));
for (byte i = 0; i < status.Info.PropertiesByID.Count; i++)
values.Add((i, status.Info.PropertiesByID[i].Get(status.Object)));
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, SyncMode.Spawn, values));
}
NetworkPackets.Send(server, new []{ networkID }, packet);
}
public override void Clear()
{
base.Clear();
_dirtyObjects.Clear();
}
}

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

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

@ -2,7 +2,7 @@ using System;
using Godot;
// TODO: Maybe figure out how we can make different classes (LocalPlayer, NPCPlayer) synchronizable.
[Sync]
[Spawn, Sync]
public class Player : KinematicBody2D, IInitializer
{
[Export] public NodePath DisplayNamePath { get; set; }
@ -101,7 +101,7 @@ public class Player : KinematicBody2D, IInitializer
}
[RPC(PacketDirection.ServerToClient)]
private void ResetPosition(Vector2 position)
public void ResetPosition(Vector2 position)
{
base.Position = position;
Velocity = Vector2.Zero;

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

@ -4,9 +4,14 @@ using Godot.Collections;
public abstract class Game : Node2D
{
public ObjectHolder Objects { get; } = new ObjectHolder();
public ObjectHolder Objects { get; }
public Sync Sync { get; protected set; }
public Game() => Objects = new ObjectHolder();
public override void _Ready() => GetTree().Connect("node_removed", this, nameof(OnNodeRemoved));
private void OnNodeRemoved(Node node) => Objects.OnNodeRemoved(node);
// Using _EnterTree to make sure this code runs before any other.
public override void _EnterTree()
=> GD.Randomize();

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
// TODO: Allow for initially private integrated server to open itself up to the public.
@ -79,6 +80,8 @@ public class Server : Game
}
public IEnumerable<(NetworkID, Player)> Players
=> _playersByNetworkID.Select(entry => (entry.Key, entry.Value));
public Player GetPlayer(NetworkID networkID)
=> _playersByNetworkID[networkID];
public NetworkID GetNetworkID(Player player)
@ -107,7 +110,7 @@ public class Server : Game
_networkIDByPlayer[player] = networkID;
} else {
Sync.SendAllObjects(this, networkID);
player = Sync.Spawn<Player>();
player = this.Spawn<Player>();
player.Position = Vector2.Zero;
player.Color = Colors.Red;
@ -127,7 +130,7 @@ public class Server : Game
// Local player stays around for reconnecting.
if (_localPlayer == player) return;
Sync.Destroy(player);
player.RemoveFromParent();
_playersByNetworkID.Remove(networkID);
_networkIDByPlayer.Remove(player);
}

@ -4,6 +4,21 @@ using Godot;
public static class Extensions
{
public static void RemoveFromParent(this Node node)
{
node.GetParent().RemoveChild(node);
node.QueueFree();
}
public static T Init<T>(this PackedScene @this)
where T : Node
{
var instance = (T)@this.Instance();
(instance as IInitializer)?.Initialize();
return instance;
}
public static Game GetGame(this Node node)
=> node.GetTree().Root.GetChild<Game>(0);
public static Client GetClient(this Node node)
@ -11,23 +26,30 @@ public static class Extensions
public static Server GetServer(this Node node)
=> node.GetGame() as Server;
public static TValue SetSync<TObject, TValue>(
this TObject obj, TValue value,
[CallerMemberName] string property = null)
where TObject : Node
{ obj.GetServer()?.Sync.MarkDirty(obj, property); return value; }
public static T Init<T>(this PackedScene @this)
where T : Node
{
var instance = (T)@this.Instance();
(instance as IInitializer)?.Initialize();
return instance;
}
public static void Deconstruct<TKey, TValue>(
this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{ key = kvp.Key; value = kvp.Value; }
public static int GetDeterministicHashCode(this string str)
{ unchecked {
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < str.Length; i += 2) {
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1) break;
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
return hash1 + (hash2 * 1566083941);
} }
}
public interface IInitializer

@ -4,7 +4,7 @@
[sub_resource type="DynamicFont" id=1]
outline_size = 1
outline_color = Color( 0, 0, 0, 1 )
outline_color = Color( 0, 0, 0, 0.5 )
extra_spacing_top = -3
extra_spacing_bottom = -1
font_data = ExtResource( 1 )

Loading…
Cancel
Save