"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 structsmain
parent
22c15b6c2a
commit
eed70795dd
34 changed files with 1704 additions and 1171 deletions
@ -1,4 +1,4 @@ |
|||||||
<Project Sdk="Godot.NET.Sdk/3.2.3"> |
<Project Sdk="Godot.NET.Sdk/3.3.0"> |
||||||
<PropertyGroup> |
<PropertyGroup> |
||||||
<TargetFramework>net472</TargetFramework> |
<TargetFramework>net472</TargetFramework> |
||||||
</PropertyGroup> |
</PropertyGroup> |
||||||
|
@ -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 ) |
@ -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 ) |
|
@ -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; } |
|
||||||
} |
|
@ -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); |
||||||
|
} |
@ -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…
Reference in new issue