- Remove DeSerialization, RPC and Sync code - Use Godot's built-in Rpc and Rset methods - Use built-in Multiplayer with Root set to World - Add World class, handles spawning, getting players and blocks - No more custom packets - No more reflection or attributes - Reintroduce LocalPlayer scene - Keep it simple, stupid!main
parent
fdf1782069
commit
94bd99a478
30 changed files with 348 additions and 1776 deletions
@ -1,9 +1,17 @@ |
||||
[gd_scene load_steps=2 format=2] |
||||
[gd_scene load_steps=3 format=2] |
||||
|
||||
[ext_resource path="res://src/World.cs" type="Script" id=1] |
||||
[ext_resource path="res://src/Scenes/Game.cs" type="Script" id=3] |
||||
|
||||
[node name="Game" type="Node2D"] |
||||
[node name="Game" type="Node"] |
||||
pause_mode = 2 |
||||
script = ExtResource( 3 ) |
||||
|
||||
[node name="World" type="Node" parent="."] |
||||
script = ExtResource( 1 ) |
||||
PlayerContainerPath = NodePath("Players") |
||||
BlockContainerPath = NodePath("Blocks") |
||||
|
||||
[node name="Players" type="Node" parent="World"] |
||||
|
||||
[node name="Blocks" type="Node" parent="World"] |
||||
|
@ -0,0 +1,11 @@ |
||||
[gd_scene load_steps=3 format=2] |
||||
|
||||
[ext_resource path="res://scene/Player.tscn" type="PackedScene" id=1] |
||||
[ext_resource path="res://src/Objects/LocalPlayer.cs" type="Script" id=2] |
||||
|
||||
[node name="LocalPlayer" instance=ExtResource( 1 )] |
||||
script = ExtResource( 2 ) |
||||
|
||||
[node name="Camera" type="Camera2D" parent="." index="0"] |
||||
pause_mode = 2 |
||||
current = true |
@ -1,261 +0,0 @@ |
||||
using System; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using System.Runtime.Serialization; |
||||
using Godot; |
||||
|
||||
/// <summary> |
||||
/// Implements a simple de/serializer based on a serialize and deserialize |
||||
/// function, typically specified using short lambda expressions. |
||||
/// A shortcut method for creating an instance of this class can be found at: |
||||
/// <see cref="DeSerializerRegistry.Register{T}(Action{BinaryWriter, T}, Func{BinaryReader, T})"/>. |
||||
/// </summary> |
||||
public class SimpleDeSerializer<T> |
||||
: DeSerializer<T> |
||||
{ |
||||
private readonly Action<BinaryWriter, T> _serialize; |
||||
private readonly Func<BinaryReader, T> _deserialize; |
||||
public SimpleDeSerializer(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize) |
||||
{ _serialize = serialize; _deserialize = deserialize; } |
||||
public override void Serialize(Game game, BinaryWriter writer, T value) => _serialize(writer, value); |
||||
public override T Deserialize(Game game, BinaryReader reader) => _deserialize(reader); |
||||
} |
||||
|
||||
public class EnumDeSerializerGenerator |
||||
: IDeSerializerGenerator |
||||
{ |
||||
public IDeSerializer GenerateFor(Type type) |
||||
{ |
||||
// TODO: Flagged enums are not supported at this time. |
||||
if (!type.IsEnum || (type.GetCustomAttribute<FlagsAttribute>() != null)) return null; |
||||
var deSerializerType = typeof(EnumDeSerializer<,>).MakeGenericType(type, type.GetEnumUnderlyingType()); |
||||
return (IDeSerializer)Activator.CreateInstance(deSerializerType); |
||||
} |
||||
|
||||
private class EnumDeSerializer<TEnum, TUnderlying> |
||||
: DeSerializer<TEnum> |
||||
where TEnum : Enum |
||||
{ |
||||
private readonly IDeSerializer<TUnderlying> _underlyingDeSerializer = |
||||
DeSerializerRegistry.Get<TUnderlying>(true); |
||||
|
||||
public override void Serialize(Game game, BinaryWriter writer, TEnum value) |
||||
{ |
||||
if (!Enum.IsDefined(typeof(TEnum), value)) throw new ArgumentException( |
||||
$"Invalid enum value {value} for type {typeof(TEnum)}", nameof(value)); |
||||
_underlyingDeSerializer.Serialize(game, writer, (TUnderlying)(object)value); |
||||
} |
||||
|
||||
public override TEnum Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var value = (TEnum)(object)_underlyingDeSerializer.Deserialize(game, reader); |
||||
if (!Enum.IsDefined(typeof(TEnum), value)) throw new ArgumentException( |
||||
$"Invalid enum value {value} for type {typeof(TEnum)}", nameof(value)); |
||||
return value; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public class ArrayDeSerializerGenerator |
||||
: IDeSerializerGenerator |
||||
{ |
||||
public IDeSerializer GenerateFor(Type type) |
||||
{ |
||||
if (!type.IsArray) return null; |
||||
var deSerializerType = typeof(ArrayDeSerializer<>).MakeGenericType(type.GetElementType()); |
||||
return (IDeSerializer)Activator.CreateInstance(deSerializerType); |
||||
} |
||||
|
||||
private class ArrayDeSerializer<T> |
||||
: DeSerializer<T[]> |
||||
{ |
||||
private readonly IDeSerializer _elementDeSerializer = |
||||
DeSerializerRegistry.Get<T>(true); |
||||
|
||||
public override void Serialize(Game game, BinaryWriter writer, T[] value) |
||||
{ |
||||
writer.Write(value.Length); |
||||
foreach (var element in value) |
||||
_elementDeSerializer.Serialize(game, writer, element); |
||||
} |
||||
|
||||
public override T[] Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var length = reader.ReadInt32(); |
||||
var array = new T[length]; |
||||
for (var i = 0; i < length; i++) |
||||
array[i] = (T)_elementDeSerializer.Deserialize(game, reader); |
||||
return array; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public class CollectionDeSerializerGenerator |
||||
: IDeSerializerGenerator |
||||
{ |
||||
public IDeSerializer GenerateFor(Type type) |
||||
{ |
||||
Type elementType; |
||||
if (type.IsInterface) { |
||||
// If the type is an interface type, specific interfaces are |
||||
// supported and will be populated with certain concrete types. |
||||
if (!type.IsGenericType) return null; |
||||
elementType = type.GetGenericArguments()[0]; |
||||
var typeDef = type.GetGenericTypeDefinition(); |
||||
if (typeDef == typeof(ICollection<>)) type = typeof(List<>).MakeGenericType(elementType); |
||||
else if (typeDef == typeof(IList<>)) type = typeof(List<>).MakeGenericType(elementType); |
||||
else if (typeDef == typeof(ISet<>)) type = typeof(HashSet<>).MakeGenericType(elementType); |
||||
else return null; |
||||
} else { |
||||
// An empty constructor is required. |
||||
if (type.GetConstructor(Type.EmptyTypes) == null) return null; |
||||
// Dictionaries are handled by DictionaryDeSerializerGenerator. |
||||
if (typeof(IDictionary).IsAssignableFrom(type)) return null; |
||||
|
||||
elementType = type.GetInterfaces() |
||||
.Where(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(ICollection<>))) |
||||
.Select(i => i.GetGenericArguments()[0]) |
||||
.FirstOrDefault(); |
||||
if (elementType == null) return null; |
||||
} |
||||
var deSerializerType = typeof(CollectionDeSerializer<,>).MakeGenericType(type, elementType); |
||||
return (IDeSerializer)Activator.CreateInstance(deSerializerType); |
||||
} |
||||
|
||||
private class CollectionDeSerializer<TCollection, TElement> |
||||
: DeSerializer<TCollection> |
||||
where TCollection : ICollection<TElement>, new() |
||||
{ |
||||
private readonly IDeSerializer _elementDeSerializer = |
||||
DeSerializerRegistry.Get<TElement>(true); |
||||
|
||||
public override void Serialize(Game game, BinaryWriter writer, TCollection value) |
||||
{ |
||||
writer.Write(value.Count); |
||||
foreach (var element in value) |
||||
_elementDeSerializer.Serialize(game, writer, element); |
||||
} |
||||
|
||||
public override TCollection Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var count = reader.ReadInt32(); |
||||
var collection = new TCollection(); |
||||
for (var i = 0; i < count; i++) |
||||
collection.Add((TElement)_elementDeSerializer.Deserialize(game, reader)); |
||||
return collection; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public class DictionaryDeSerializerGenerator |
||||
: IDeSerializerGenerator |
||||
{ |
||||
public IDeSerializer GenerateFor(Type type) |
||||
{ |
||||
Type keyType, valueType; |
||||
if (type.IsInterface) { |
||||
if (!type.IsGenericType || (type.GetGenericTypeDefinition() != typeof(IDictionary<,>))) return null; |
||||
keyType = type.GetGenericArguments()[0]; |
||||
valueType = type.GetGenericArguments()[1]; |
||||
type = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); |
||||
} else { |
||||
// An empty constructor is required. |
||||
if (type.GetConstructor(Type.EmptyTypes) == null) return null; |
||||
|
||||
(keyType, valueType) = type.GetInterfaces() |
||||
.Where(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))) |
||||
.Select(i => (i.GetGenericArguments()[0], i.GetGenericArguments()[1])) |
||||
.FirstOrDefault(); |
||||
if (keyType == null) return null; |
||||
} |
||||
var deSerializerType = typeof(DictionaryDeSerializer<,,>).MakeGenericType(type, keyType, valueType); |
||||
return (IDeSerializer)Activator.CreateInstance(deSerializerType); |
||||
} |
||||
|
||||
private class DictionaryDeSerializer<TDictionary, TKey, TValue> |
||||
: DeSerializer<TDictionary> |
||||
where TDictionary : IDictionary<TKey, TValue>, new() |
||||
{ |
||||
private readonly IDeSerializer _keyDeSerializer = |
||||
DeSerializerRegistry.Get<TKey>(true); |
||||
private readonly IDeSerializer _valueDeSerializer = |
||||
DeSerializerRegistry.Get<TKey>(true); |
||||
|
||||
public override void Serialize(Game game, BinaryWriter writer, TDictionary dict) |
||||
{ |
||||
writer.Write(dict.Count); |
||||
foreach (var (key, value) in dict) { |
||||
_keyDeSerializer.Serialize(game, writer, key); |
||||
_valueDeSerializer.Serialize(game, writer, value); |
||||
} |
||||
} |
||||
|
||||
public override TDictionary Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var count = reader.ReadInt32(); |
||||
var dictionary = new TDictionary(); |
||||
for (var i = 0; i < count; i++) |
||||
dictionary.Add((TKey)_keyDeSerializer.Deserialize(game, reader), |
||||
(TValue)_valueDeSerializer.Deserialize(game, reader)); |
||||
return dictionary; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public class NodeDeSerializerGenerator |
||||
: IDeSerializerGenerator |
||||
{ |
||||
public IDeSerializer GenerateFor(Type type) |
||||
{ |
||||
if (!typeof(Node).IsAssignableFrom(type)) return null; |
||||
var deSerializerType = typeof(NodeDeSerializer<>).MakeGenericType(type); |
||||
return (IDeSerializer)Activator.CreateInstance(deSerializerType); |
||||
} |
||||
|
||||
private class NodeDeSerializer<TObj> |
||||
: DeSerializer<TObj> |
||||
where TObj : Node |
||||
{ |
||||
public override void Serialize(Game game, BinaryWriter writer, TObj value) |
||||
=> writer.Write(game.Objects.GetSyncID(value).Value); |
||||
public override TObj Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var id = new UniqueID(reader.ReadUInt32()); |
||||
var value = (TObj)game.Objects.GetObjectByID(id); |
||||
if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}"); |
||||
return value; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// TODO: Replace this with something that will generate code at runtime for improved performance. |
||||
public class ComplexDeSerializer<T> |
||||
: DeSerializer<T> |
||||
{ |
||||
private event Action<Game, BinaryWriter, object> OnSerialize; |
||||
private event Action<Game, BinaryReader, object> OnDeserialize; |
||||
|
||||
public ComplexDeSerializer() |
||||
{ |
||||
foreach (var field in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { |
||||
var deSerializer = DeSerializerRegistry.Get(field.FieldType, true); |
||||
OnSerialize += (game, writer, obj) => deSerializer.Serialize(game, writer, field.GetValue(obj)); |
||||
OnDeserialize += (game, reader, obj) => field.SetValue(obj, deSerializer.Deserialize(game, reader)); |
||||
} |
||||
if (OnSerialize == null) throw new InvalidOperationException( |
||||
$"Unable to create {nameof(ComplexDeSerializer<T>)} for type {typeof(T)}"); |
||||
} |
||||
|
||||
public override void Serialize(Game game, BinaryWriter writer, T value) |
||||
=> OnSerialize(game, writer, value); |
||||
|
||||
public override T Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var value = FormatterServices.GetUninitializedObject(typeof(T)); |
||||
OnDeserialize(game, reader, value); |
||||
return (T)value; |
||||
} |
||||
} |
@ -1,53 +0,0 @@ |
||||
using System; |
||||
using System.IO; |
||||
|
||||
/// <summary> |
||||
/// Basic binary de/serializer interface, capable of de/serializing a particular |
||||
/// type that it was made for. Will typically not be implemented directly, as |
||||
/// <see cref="IDeSerializer{T}"/> offers a more type-safe interface. |
||||
/// </summary> |
||||
public interface IDeSerializer |
||||
{ |
||||
void Serialize(Game game, BinaryWriter writer, object value); |
||||
object Deserialize(Game game, BinaryReader reader); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Basic type-safe binary de/serializer interface, capable of de/serializing |
||||
/// values of type <c>T</c>. <see cref="DeSerializer{T}"/> offers an abstract |
||||
/// implementation that already implements <see cref="IDeSerializer"/> methods |
||||
/// so you only have to use the type-safe ones. |
||||
/// </summary> |
||||
public interface IDeSerializer<T> |
||||
: IDeSerializer |
||||
{ |
||||
void Serialize(Game game, BinaryWriter writer, T value); |
||||
new T Deserialize(Game game, BinaryReader reader); |
||||
} |
||||
|
||||
// TODO: Using C# 8 this could be done with default interface implementations on IDeSerializer<>. |
||||
public abstract class DeSerializer<T> |
||||
: IDeSerializer<T> |
||||
{ |
||||
public abstract void Serialize(Game game, BinaryWriter writer, T value); |
||||
public abstract T Deserialize(Game game, BinaryReader reader); |
||||
|
||||
void IDeSerializer.Serialize(Game game, BinaryWriter writer, object value) |
||||
=> Serialize(game, writer, (T)value); |
||||
object IDeSerializer.Deserialize(Game game, BinaryReader reader) |
||||
=> Deserialize(game, reader); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// This interface allows the dynamic creation of <see cref="IDeSerializer"/> |
||||
/// implementations that cannot be covered by simple de/serializer implementations, |
||||
/// such as when generics come into play. |
||||
/// </summary> |
||||
/// <seealso cref="EnumDeSerializerGenerator"/> |
||||
/// <seealso cref="ArrayDeSerializerGenerator"/> |
||||
/// <seealso cref="CollectionDeSerializerGenerator"/> |
||||
/// <seealso cref="DictionaryDeSerializerGenerator"/> |
||||
public interface IDeSerializerGenerator |
||||
{ |
||||
IDeSerializer GenerateFor(Type type); |
||||
} |
@ -1,69 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using Godot; |
||||
|
||||
public static class DeSerializerRegistry |
||||
{ |
||||
private static readonly Dictionary<Type, IDeSerializer> _byType = new Dictionary<Type, IDeSerializer>(); |
||||
private static readonly List<IDeSerializerGenerator> _generators = new List<IDeSerializerGenerator>(); |
||||
|
||||
static DeSerializerRegistry() |
||||
{ |
||||
Register((w, value) => w.Write(value), r => r.ReadBoolean()); |
||||
Register((w, value) => w.Write(value), r => r.ReadByte()); |
||||
Register((w, value) => w.Write(value), r => r.ReadSByte()); |
||||
Register((w, value) => w.Write(value), r => r.ReadInt16()); |
||||
Register((w, value) => w.Write(value), r => r.ReadUInt16()); |
||||
Register((w, value) => w.Write(value), r => r.ReadInt32()); |
||||
Register((w, value) => w.Write(value), r => r.ReadUInt32()); |
||||
Register((w, value) => w.Write(value), r => r.ReadInt64()); |
||||
Register((w, value) => w.Write(value), r => r.ReadUInt64()); |
||||
Register((w, value) => w.Write(value), r => r.ReadSingle()); |
||||
Register((w, value) => w.Write(value), r => r.ReadDouble()); |
||||
Register((w, value) => w.Write(value), r => r.ReadString()); |
||||
|
||||
// byte[] |
||||
Register((w, value) => { w.Write((ushort)value.Length); w.Write(value); }, |
||||
r => r.ReadBytes(r.ReadUInt16())); |
||||
// Vector2 |
||||
Register((w, value) => { w.Write(value.x); w.Write(value.y); }, |
||||
r => new Vector2(r.ReadSingle(), r.ReadSingle())); |
||||
// Color |
||||
Register((w, value) => w.Write(value.ToRgba32()), |
||||
r => new Color(r.ReadInt32())); |
||||
|
||||
RegisterGenerator(new EnumDeSerializerGenerator()); |
||||
RegisterGenerator(new ArrayDeSerializerGenerator()); |
||||
RegisterGenerator(new CollectionDeSerializerGenerator()); |
||||
RegisterGenerator(new DictionaryDeSerializerGenerator()); |
||||
RegisterGenerator(new NodeDeSerializerGenerator()); |
||||
} |
||||
|
||||
public static void Register<T>(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize) |
||||
=> Register(new SimpleDeSerializer<T>(serialize, deserialize)); |
||||
public static void Register<T>(IDeSerializer<T> deSerializer) |
||||
=> _byType.Add(typeof(T), deSerializer); |
||||
public static void RegisterGenerator(IDeSerializerGenerator deSerializerGenerator) |
||||
=> _generators.Add(deSerializerGenerator); |
||||
|
||||
public static IDeSerializer<T> Get<T>(bool createIfMissing) |
||||
=> (IDeSerializer<T>)Get(typeof(T), createIfMissing); |
||||
public static IDeSerializer Get(Type type, bool createIfMissing) |
||||
{ |
||||
if (!_byType.TryGetValue(type, out var value)) { |
||||
if (!createIfMissing) throw new InvalidOperationException( |
||||
$"No DeSerializer for type {type} found"); |
||||
|
||||
value = _generators.Select(g => g.GenerateFor(type)) |
||||
.FirstOrDefault(x => x != null); |
||||
if (value == null) { |
||||
var deSerializerType = typeof(ComplexDeSerializer<>).MakeGenericType(type); |
||||
value = (IDeSerializer)Activator.CreateInstance(deSerializerType); |
||||
} |
||||
_byType.Add(type, value); |
||||
} |
||||
return value; |
||||
} |
||||
} |
@ -1,48 +0,0 @@ |
||||
using System; |
||||
using System.Reflection; |
||||
|
||||
public interface IPropertyDeSerializer |
||||
{ |
||||
PropertyInfo Property { get; } |
||||
Type Type { get; } |
||||
string Name { get; } |
||||
string FullName { get; } |
||||
int HashID { get; } |
||||
|
||||
IDeSerializer DeSerializer { get; } |
||||
object Get(object obj); |
||||
void Set(object obj, object value); |
||||
} |
||||
|
||||
public class PropertyDeSerializer<TObj, TProp> |
||||
: IPropertyDeSerializer |
||||
{ |
||||
public PropertyInfo Property { get; } |
||||
public Type Type => Property.PropertyType; |
||||
public string Name => Property.Name; |
||||
public string FullName { get; } |
||||
public int HashID { get; } |
||||
|
||||
public IDeSerializer<TProp> DeSerializer { get; } |
||||
public Func<TObj, TProp> Getter { get; } |
||||
public Action<TObj, TProp> Setter { get; } |
||||
|
||||
public PropertyDeSerializer(PropertyInfo property) |
||||
{ |
||||
if ((property.GetMethod == null) || (property.SetMethod == null)) throw new Exception( |
||||
$"Property {property.DeclaringType}.{property.Name} must have a getter and setter defined"); |
||||
|
||||
Property = property; |
||||
FullName = $"{Property.DeclaringType.FullName}.{Property.Name}"; |
||||
HashID = FullName.GetDeterministicHashCode(); |
||||
|
||||
DeSerializer = DeSerializerRegistry.Get<TProp>(true); |
||||
Getter = (Func<TObj, TProp>)Property.GetMethod.CreateDelegate(typeof(Func<TObj, TProp>)); |
||||
Setter = (Action<TObj, TProp>)Property.SetMethod.CreateDelegate(typeof(Action<TObj, TProp>)); |
||||
} |
||||
|
||||
// IPropertyDeSerializer implementation |
||||
IDeSerializer IPropertyDeSerializer.DeSerializer => DeSerializer; |
||||
object IPropertyDeSerializer.Get(object obj) => Getter((TObj)obj); |
||||
void IPropertyDeSerializer.Set(object obj, object value) => Setter((TObj)obj, (TProp)value); |
||||
} |
@ -1,103 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using Godot; |
||||
using File = System.IO.File; |
||||
|
||||
public class Save |
||||
{ |
||||
public const string FILE_EXT = ".yf5"; |
||||
public const int MAGIC_NUMBER = 0x59463573; // "YF5s" |
||||
public const int CURRENT_VERSION = 0; |
||||
|
||||
public int Version { get; private set; } |
||||
public DateTime LastSaved { get; private set; } |
||||
public TimeSpan Playtime { get; set; } |
||||
|
||||
public List<(SaveObjectInfo, List<object>)> Objects { get; private set; } |
||||
|
||||
|
||||
public static Save ReadFromFile(string path) |
||||
{ |
||||
var save = new Save { LastSaved = File.GetLastAccessTime(path) }; |
||||
using (var stream = File.OpenRead(path)) { |
||||
using (var reader = new BinaryReader(stream)) { |
||||
var magic = reader.ReadInt32(); |
||||
if (magic != MAGIC_NUMBER) throw new IOException( |
||||
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})"); |
||||
|
||||
// TODO: See how to support multiple versions. |
||||
save.Version = reader.ReadUInt16(); |
||||
if (save.Version != CURRENT_VERSION) throw new IOException( |
||||
$"Version does not match ({save.Version} != {CURRENT_VERSION})"); |
||||
|
||||
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32()); |
||||
|
||||
var numObjects = reader.ReadInt32(); |
||||
save.Objects = new List<(SaveObjectInfo, List<object>)>(numObjects); |
||||
for (var i = 0; i < numObjects; i++) { |
||||
var hashID = reader.ReadInt32(); |
||||
var objInfo = SaveRegistry.GetOrThrow(hashID); |
||||
var props = objInfo.PropertiesByID.Select(x => x.DeSerializer.Deserialize(null, reader)).ToList(); |
||||
save.Objects.Add((objInfo, props)); |
||||
} |
||||
} |
||||
} |
||||
return save; |
||||
} |
||||
|
||||
public void WriteToFile(string path) |
||||
{ |
||||
using (var stream = File.OpenWrite(path)) { |
||||
using (var writer = new BinaryWriter(stream)) { |
||||
writer.Write(MAGIC_NUMBER); |
||||
writer.Write((ushort)CURRENT_VERSION); |
||||
writer.Write((uint)Playtime.TotalSeconds); |
||||
|
||||
writer.Write(Objects.Count); |
||||
foreach (var (objInfo, props) in Objects) { |
||||
writer.Write(objInfo.HashID); |
||||
for (var i = 0; i < objInfo.PropertiesByID.Count; i++) { |
||||
var propInfo = objInfo.PropertiesByID[i]; |
||||
var propValue = props[i]; |
||||
propInfo.DeSerializer.Serialize(null, writer, propValue); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
LastSaved = File.GetLastAccessTime(path); |
||||
} |
||||
|
||||
|
||||
public static Save CreateFromWorld(Game game, TimeSpan playtime) |
||||
{ |
||||
var save = new Save { |
||||
Playtime = playtime, |
||||
Objects = new List<(SaveObjectInfo, List<object>)>(), |
||||
}; |
||||
foreach (var (id, obj) in game.Objects) { |
||||
var objInfo = SaveRegistry.GetOrNull(obj.GetType()); |
||||
if (objInfo == null) continue; |
||||
|
||||
var props = objInfo.PropertiesByID.Select(x => x.Get(obj)).ToList(); |
||||
save.Objects.Add((objInfo, props)); |
||||
} |
||||
return save; |
||||
} |
||||
|
||||
public void AddToWorld(Server server) |
||||
{ |
||||
foreach (var (objInfo, props) in Objects) { |
||||
var obj = objInfo.SpawnInfo.Scene.Init<Node>(); |
||||
server.GetNode("World").AddChild(obj, true); |
||||
server.Objects.Add(null, obj); |
||||
|
||||
for (var i = 0; i < objInfo.PropertiesByID.Count; i++) { |
||||
var propInfo = objInfo.PropertiesByID[i]; |
||||
var propValue = props[i]; |
||||
propInfo.Set(obj, propValue); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,69 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using Godot; |
||||
|
||||
public static class SaveRegistry |
||||
{ |
||||
private static readonly Dictionary<int, SaveObjectInfo> _byID = new Dictionary<int, SaveObjectInfo>(); |
||||
private static readonly Dictionary<Type, SaveObjectInfo> _byType = new Dictionary<Type, SaveObjectInfo>(); |
||||
|
||||
static SaveRegistry() |
||||
{ |
||||
foreach (var type in typeof(SyncRegistry).Assembly.GetTypes()) { |
||||
var syncAttr = type.GetCustomAttribute<SaveAttribute>(); |
||||
if (syncAttr == null) continue; |
||||
|
||||
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception( |
||||
$"Type {type} with {nameof(SyncAttribute)} must be a subclass of {nameof(Node)}"); |
||||
|
||||
var spawnInfo = SpawnRegistry.Get(type); |
||||
var objInfo = new SaveObjectInfo(type, spawnInfo); |
||||
|
||||
foreach (var property in type.GetProperties()) { |
||||
if (property.GetCustomAttribute<SaveAttribute>() == null) continue; |
||||
var propType = typeof(PropertyDeSerializer<,>).MakeGenericType(type, property.PropertyType); |
||||
var propDeSerializer = (IPropertyDeSerializer)Activator.CreateInstance(propType, property); |
||||
objInfo.PropertiesByName.Add(propDeSerializer.Name, propDeSerializer); |
||||
} |
||||
objInfo.PropertiesByID.AddRange(objInfo.PropertiesByName.Values.OrderBy(x => x.HashID)); |
||||
|
||||
_byID.Add(objInfo.HashID, objInfo); |
||||
_byType.Add(objInfo.Type, objInfo); |
||||
} |
||||
} |
||||
|
||||
public static SaveObjectInfo GetOrNull(int hashID) |
||||
=> _byID.TryGetValue(hashID, out var value) ? value : null; |
||||
public static SaveObjectInfo GetOrThrow(int hashID) => GetOrNull(hashID) |
||||
?? throw new Exception($"Unknown {nameof(SaveObjectInfo)} with HashID {hashID}"); |
||||
|
||||
public static SaveObjectInfo GetOrNull<T>() => GetOrNull(typeof(T)); |
||||
public static SaveObjectInfo GetOrNull(Type type) => _byType.TryGetValue(type, out var value) ? value : null; |
||||
|
||||
public static SaveObjectInfo GetOrThrow<T>() => GetOrThrow(typeof(T)); |
||||
public static SaveObjectInfo GetOrThrow(Type type) => GetOrNull(type) ?? throw new Exception( |
||||
$"No {nameof(SaveObjectInfo)} found for type {type} (missing {nameof(SyncAttribute)}?)"); |
||||
} |
||||
|
||||
public class SaveObjectInfo |
||||
{ |
||||
public Type Type { get; } |
||||
public int HashID { get; } |
||||
public SpawnInfo SpawnInfo { get; } |
||||
public string Name => Type.Name; |
||||
|
||||
public List<IPropertyDeSerializer> PropertiesByID { get; } = new List<IPropertyDeSerializer>(); |
||||
public Dictionary<string, IPropertyDeSerializer> PropertiesByName { get; } = new Dictionary<string, IPropertyDeSerializer>(); |
||||
|
||||
public SaveObjectInfo(Type type, SpawnInfo spawnInfo) |
||||
{ |
||||
Type = type; |
||||
HashID = type.FullName.GetDeterministicHashCode(); |
||||
SpawnInfo = spawnInfo; |
||||
} |
||||
} |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] |
||||
public class SaveAttribute : Attribute { } |
@ -1,96 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
|
||||
[Flags] |
||||
public enum PacketDirection |
||||
{ |
||||
ServerToClient = 0b01, |
||||
ClientToServer = 0b10, |
||||
Both = ServerToClient | ClientToServer, |
||||
} |
||||
|
||||
public static class NetworkPackets |
||||
{ |
||||
private static readonly List<PacketInfo> _packetsById = new List<PacketInfo>(); |
||||
private static readonly Dictionary<Type, PacketInfo> _packetsByType = new Dictionary<Type, PacketInfo>(); |
||||
|
||||
public static void Register<T>(PacketDirection direction, Action<Game, NetworkID, T> onReceived) |
||||
{ |
||||
var info = new PacketInfo((byte)_packetsById.Count, typeof(T), |
||||
direction, (game, networkID, packet) => onReceived(game, networkID, (T)packet)); |
||||
_packetsByType.Add(typeof(T), info); |
||||
_packetsById.Add(info); |
||||
} |
||||
|
||||
private static byte[] ToBytes(Game game, PacketInfo info, object packet) |
||||
{ |
||||
using (var stream = new MemoryStream()) { |
||||
using (var writer = new BinaryWriter(stream)) { |
||||
writer.Write(info.ID); |
||||
info.DeSerializer.Serialize(game, writer, packet); |
||||
} |
||||
return stream.ToArray(); |
||||
} |
||||
} |
||||
|
||||
public static void Send<T>(Game game, IEnumerable<NetworkID> targets, T packet) |
||||
{ |
||||
if (!_packetsByType.TryGetValue(typeof(T), out var info)) |
||||
throw new InvalidOperationException($"No packet of type {typeof(T)} has been registered"); |
||||
|
||||
var direction = (game is Server) ? PacketDirection.ServerToClient : PacketDirection.ClientToServer; |
||||
if ((direction & info.Direction) == 0) throw new InvalidOperationException( |
||||
$"Attempting to send packet {typeof(T)} in invalid direction {direction}"); |
||||
|
||||
byte[] bytes = null; |
||||
foreach (var networkID in targets) { |
||||
// Only serialize the packet if sending to at least 1 player. |
||||
bytes = bytes ?? ToBytes(game, info, packet); |
||||
game.CustomMultiplayer.SendBytes(bytes, networkID.Value, |
||||
Godot.NetworkedMultiplayerPeer.TransferModeEnum.Reliable); |
||||
// TODO: Should it be possible to send packets in non-reliable modes? |
||||
} |
||||
} |
||||
|
||||
public static void Process(Game game, NetworkID networkID, byte[] bytes) |
||||
{ |
||||
using (var stream = new MemoryStream(bytes)) { |
||||
using (var reader = new BinaryReader(stream)) { |
||||
var packetId = reader.ReadByte(); |
||||
if (packetId >= _packetsById.Count) throw new Exception( |
||||
$"Received packet with invalid ID {packetId}"); |
||||
var info = _packetsById[packetId]; |
||||
|
||||
var validDirection = (game is Server) ? PacketDirection.ClientToServer : PacketDirection.ServerToClient; |
||||
if ((validDirection & info.Direction) == 0) throw new Exception($"Received packet {info.Type} on invalid side {game.Name}"); |
||||
|
||||
var packet = info.DeSerializer.Deserialize(game, reader); |
||||
var bytesLeft = bytes.Length - stream.Position; |
||||
if (bytesLeft > 0) throw new Exception( |
||||
$"There were {bytesLeft} bytes left after deserializing packet {info.Type}"); |
||||
|
||||
info.OnReceived(game, networkID, packet); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public class PacketInfo |
||||
{ |
||||
public byte ID { get; } |
||||
public Type Type { get; } |
||||
public PacketDirection Direction { get; } |
||||
public Action<Game, NetworkID, object> OnReceived { get; } |
||||
public IDeSerializer DeSerializer { get; } |
||||
|
||||
public PacketInfo(byte id, Type type, |
||||
PacketDirection direction, Action<Game, NetworkID, object> onReceived) |
||||
{ |
||||
ID = id; |
||||
Type = type; |
||||
Direction = direction; |
||||
OnReceived = onReceived; |
||||
DeSerializer = DeSerializerRegistry.Get(type, true); |
||||
} |
||||
} |
||||
} |
@ -1,205 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using Godot; |
||||
using static Godot.NetworkedMultiplayerPeer; |
||||
|
||||
public static class NetworkRPC |
||||
{ |
||||
private static readonly Dictionary<int, RPCMethodInfo> _byId = new Dictionary<int, RPCMethodInfo>(); |
||||
private static readonly Dictionary<MethodInfo, RPCMethodInfo> _byMethod = new Dictionary<MethodInfo, RPCMethodInfo>(); |
||||
|
||||
private static readonly List<(NetworkID[], RPCPacket)> _serverPacketBuffer = new List<(NetworkID[], RPCPacket)>(); |
||||
private static readonly List<RPCPacket> _clientPacketBuffer = new List<RPCPacket>(); |
||||
|
||||
static NetworkRPC() |
||||
{ |
||||
DiscoverRPCMethods(); |
||||
RegisterPackets(); |
||||
} |
||||
|
||||
|
||||
// Client to server instance RPC calls. |
||||
public static void RPC(this Node obj, Action<Player> action) => CallToServer(obj, action.Method); |
||||
public static void RPC<T>(this Node obj, Action<Player, T> action, T arg) => CallToServer(obj, action.Method, arg); |
||||
public static void RPC<T0, T1>(this Node obj, Action<Player, T0, T1> action, T0 arg0, T1 arg1) => CallToServer(obj, action.Method, arg0, arg1); |
||||
public static void RPC<T0, T1, T2>(this Node obj, Action<Player, T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) => CallToServer(obj, action.Method, arg0, arg1, arg2); |
||||
public static void RPC<T0, T1, T2, T3>(this Node obj, Action<Player, T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3); |
||||
public static void RPC<T0, T1, T2, T3, T4>(this Node obj, Action<Player, T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4); |
||||
public static void RPC<T0, T1, T2, T3, T4, T5>(this Node obj, Action<Player, T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4, arg5); |
||||
public static void RPC<T0, T1, T2, T3, T4, T5, T6>(this Node obj, Action<Player, T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6); |
||||
public static void RPC<T0, T1, T2, T3, T4, T5, T6, T7>(this Node obj, Action<Player, T0, T1, T2, T3, T4, T5, T6, T7> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => CallToServer(obj, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); |
||||
|
||||
// Server to client instance RPC calls. |
||||
public static void RPC(this Node obj, IEnumerable<NetworkID> targets, Action action) => CallToClient(obj, targets, action.Method); |
||||
public static void RPC<T>(this Node obj, IEnumerable<NetworkID> targets, Action<T> action, T arg) => CallToClient(obj, targets, action.Method, arg); |
||||
public static void RPC<T0, T1>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1> action, T0 arg0, T1 arg1) => CallToClient(obj, targets, action.Method, arg0, arg1); |
||||
public static void RPC<T0, T1, T2>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2> action, T0 arg0, T1 arg1, T2 arg2) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2); |
||||
public static void RPC<T0, T1, T2, T3>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3); |
||||
public static void RPC<T0, T1, T2, T3, T4>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4); |
||||
public static void RPC<T0, T1, T2, T3, T4, T5>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4, T5> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4, arg5); |
||||
public static void RPC<T0, T1, T2, T3, T4, T5, T6>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4, T5, T6> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6); |
||||
public static void RPC<T0, T1, T2, T3, T4, T5, T6, T7>(this Node obj, IEnumerable<NetworkID> targets, Action<T0, T1, T2, T3, T4, T5, T6, T7> action, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => CallToClient(obj, targets, action.Method, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); |
||||
|
||||
private static void CallToServer(Node obj, MethodInfo method, params object[] args) |
||||
{ if (obj.GetGame() is Client) Call(obj.GetGame(), new []{ NetworkID.Server }, method, true, args.Prepend(obj)); } |
||||
private static void CallToClient(Node obj, IEnumerable<NetworkID> targets, MethodInfo method, params object[] args) |
||||
{ if (obj.GetGame() is Server) Call(obj.GetGame(), targets, method, true, args.Prepend(obj)); } |
||||
|
||||
|
||||
internal static void Call(Game game, IEnumerable<NetworkID> targets, MethodInfo method, bool isInstance, params object[] args) |
||||
=> Call(game, targets, method, isInstance, (IEnumerable<object>)args); |
||||
internal static void Call(Game game, IEnumerable<NetworkID> targets, MethodInfo method, bool isInstance, IEnumerable<object> args) |
||||
{ |
||||
if (!_byMethod.TryGetValue(method, out var info)) throw new ArgumentException( |
||||
$"The specified method {method.DeclaringType}.{method.Name} is missing {nameof(RPCAttribute)}", nameof(method)); |
||||
if (isInstance == method.IsStatic) throw new ArgumentException( |
||||
$"The specified method {method.DeclaringType}.{method.Name} must be {(isInstance ? "non-static" : "static")} for this RPC call", nameof(method)); |
||||
// TODO: Make sure the instance is the right type. |
||||
|
||||
var direction = (game is Server) ? PacketDirection.ServerToClient : PacketDirection.ClientToServer; |
||||
if (info.Attribute.Direction != direction) throw new Exception( |
||||
$"Sending {info.Attribute.Direction} RPC packet '{info.Name}' from {game.Name}"); |
||||
|
||||
var packet = new RPCPacket(info, new List<object>(args)); |
||||
if (game is Server) _serverPacketBuffer.Add((targets.ToArray(), packet)); |
||||
else _clientPacketBuffer.Add(packet); |
||||
} |
||||
|
||||
internal static void ProcessPacketBuffer(Game game) |
||||
{ |
||||
if (game is Server) { |
||||
foreach (var (targets, packet) in _serverPacketBuffer) |
||||
NetworkPackets.Send(game, targets, packet); |
||||
_serverPacketBuffer.Clear(); |
||||
} else { |
||||
foreach (var packet in _clientPacketBuffer) |
||||
NetworkPackets.Send(game, new []{ NetworkID.Server }, packet); |
||||
_clientPacketBuffer.Clear(); |
||||
} |
||||
} |
||||
|
||||
|
||||
private static void DiscoverRPCMethods() |
||||
{ |
||||
foreach (var type in typeof(NetworkRPC).Assembly.GetTypes()) { |
||||
foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { |
||||
var rpc = method.GetCustomAttribute<RPCAttribute>(); |
||||
if (rpc == null) continue; |
||||
|
||||
if (!method.IsStatic && (type.GetCustomAttribute<SyncAttribute>() == null)) throw new Exception( |
||||
$"Type of non-static RPC method '{method.DeclaringType}.{method.Name}' must have {nameof(SyncAttribute)}"); |
||||
|
||||
var deSerializers = new List<IDeSerializer>(); |
||||
var paramEnumerable = ((IEnumerable<ParameterInfo>)method.GetParameters()).GetEnumerator(); |
||||
|
||||
var isServer = rpc.Direction == PacketDirection.ClientToServer; |
||||
var gameType = isServer ? typeof(Server) : typeof(Client); |
||||
if (method.IsStatic && (!paramEnumerable.MoveNext() || (paramEnumerable.Current.ParameterType != gameType))) throw new Exception( |
||||
$"First parameter of {rpc.Direction} RPC method '{method.DeclaringType}.{method.Name}' must be {gameType}"); |
||||
if (isServer && (!paramEnumerable.MoveNext() || (paramEnumerable.Current.ParameterType != typeof(NetworkID)))) throw new Exception( |
||||
$"{(method.IsStatic ? "Second" : "First")} parameter of {rpc.Direction} RPC method '{method.DeclaringType}.{method.Name}' must be {nameof(NetworkID)}"); |
||||
|
||||
if (!method.IsStatic) |
||||
deSerializers.Add(DeSerializerRegistry.Get(type, true)); |
||||
|
||||
while (paramEnumerable.MoveNext()) { |
||||
var param = paramEnumerable.Current; |
||||
var deSerializer = DeSerializerRegistry.Get(param.ParameterType, true); |
||||
deSerializers.Add(deSerializer); |
||||
} |
||||
|
||||
var info = new RPCMethodInfo(method, deSerializers); |
||||
_byId.Add(info.ID, info); |
||||
_byMethod.Add(method, info); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class RPCMethodInfo |
||||
{ |
||||
public string Name { get; } |
||||
public int ID { get; } |
||||
public MethodInfo Method { get; } |
||||
public RPCAttribute Attribute { get; } |
||||
public List<IDeSerializer> DeSerializers { get; } |
||||
|
||||
public RPCMethodInfo(MethodInfo method, List<IDeSerializer> deSerializers) |
||||
{ |
||||
Name = $"{method.DeclaringType}.{method.Name}"; |
||||
ID = Name.GetHashCode(); |
||||
Method = method; |
||||
Attribute = method.GetCustomAttribute<RPCAttribute>(); |
||||
DeSerializers = deSerializers; |
||||
} |
||||
} |
||||
|
||||
|
||||
private static void RegisterPackets() |
||||
{ |
||||
DeSerializerRegistry.Register(new RPCPacketDeSerializer()); |
||||
NetworkPackets.Register<RPCPacket>(PacketDirection.Both, (game, networkID, packet) => { |
||||
var validDirection = (game is Server) ? PacketDirection.ClientToServer : PacketDirection.ServerToClient; |
||||
if (packet.Info.Attribute.Direction != validDirection) throw new Exception( |
||||
$"Received {packet.Info.Attribute.Direction} RPC packet '{packet.Info.Name}' on side {game.Name}"); |
||||
|
||||
Node obj = null; |
||||
IEnumerable<object> args = packet.Args; |
||||
|
||||
// If method is instance method, the first argument is the object it is called on. |
||||
if (!packet.Info.Method.IsStatic) { obj = (Node)args.First(); args = args.Skip(1); } |
||||
// If RPC is called on the server, prepend the NetworkID of the client. |
||||
if (game is Server) args = args.Prepend(networkID); |
||||
// If method is static, prepend Client/Server to arguments. |
||||
if (packet.Info.Method.IsStatic) args = args.Prepend(game); |
||||
|
||||
// TODO: Improve type safety and performance - generate packet for each RPC? |
||||
packet.Info.Method.Invoke(obj, args.ToArray()); |
||||
}); |
||||
} |
||||
|
||||
private class RPCPacket |
||||
{ |
||||
public RPCMethodInfo Info { get; } |
||||
public List<object> Args { get; } |
||||
public RPCPacket(RPCMethodInfo info, List<object> args) |
||||
{ Info = info; Args = args; } |
||||
} |
||||
|
||||
private class RPCPacketDeSerializer |
||||
: DeSerializer<RPCPacket> |
||||
{ |
||||
public override void Serialize(Game game, BinaryWriter writer, RPCPacket value) |
||||
{ |
||||
writer.Write(value.Info.ID); |
||||
foreach (var (deSerializer, arg) in value.Info.DeSerializers.Zip(value.Args, Tuple.Create)) |
||||
deSerializer.Serialize(game, writer, arg); |
||||
} |
||||
|
||||
public override RPCPacket Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var id = reader.ReadInt32(); |
||||
if (!_byId.TryGetValue(id, out var info)) throw new Exception($"Unknown RPC ID {id}"); |
||||
var args = info.DeSerializers.Select(x => x.Deserialize(game, reader)).ToList(); |
||||
return new RPCPacket(info, args); |
||||
} |
||||
} |
||||
} |
||||
|
||||
[AttributeUsage(AttributeTargets.Method)] |
||||
public class RPCAttribute : Attribute |
||||
{ |
||||
public PacketDirection Direction { get; } |
||||
public TransferModeEnum TransferMode { get; set; } |
||||
|
||||
public RPCAttribute(PacketDirection direction) { |
||||
switch (direction) { |
||||
case PacketDirection.ServerToClient: |
||||
case PacketDirection.ClientToServer: Direction = direction; break; |
||||
default: throw new ArgumentException( |
||||
$"Direction must be either ServerToClient or ClientToServer."); |
||||
} |
||||
} |
||||
} |
@ -1,153 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Reflection; |
||||
using Godot; |
||||
|
||||
// TODO: Allow syncronization of child objects spawned with their parent objects. |
||||
// TODO: Specify who properties are syncronized with. (Owner, Friends, Team, Everyone) |
||||
public abstract class Sync |
||||
{ |
||||
protected Game Game { get; } |
||||
protected Dictionary<UniqueID, SyncStatus> StatusByID { get; } = new Dictionary<UniqueID, SyncStatus>(); |
||||
protected Dictionary<Node, SyncStatus> StatusByObject { get; } = new Dictionary<Node, SyncStatus>(); |
||||
|
||||
static Sync() |
||||
=> DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer()); |
||||
|
||||
public Sync(Game game) |
||||
{ |
||||
Game = game; |
||||
Game.Objects.Added += OnObjectAdded; |
||||
Game.Objects.Removed += OnObjectRemoved; |
||||
Game.Objects.Cleared += OnObjectsCleared; |
||||
} |
||||
|
||||
private void OnObjectAdded(UniqueID id, Node obj) |
||||
{ |
||||
var info = SyncRegistry.GetOrNull(obj.GetType()); |
||||
if (info == null) return; |
||||
|
||||
var status = new SyncStatus(id, obj, info); |
||||
StatusByID.Add(id, status); |
||||
StatusByObject.Add(obj, status); |
||||
OnSyncedAdded(status); |
||||
} |
||||
|
||||
private void OnObjectRemoved(UniqueID id, Node obj) |
||||
{ |
||||
if (!StatusByObject.TryGetValue(obj, out var status)) return; |
||||
|
||||
StatusByID.Remove(status.ID); |
||||
StatusByObject.Remove(status.Object); |
||||
OnSyncedRemoved(status); |
||||
} |
||||
|
||||
protected virtual void OnSyncedAdded(SyncStatus status) { } |
||||
protected virtual void OnSyncedRemoved(SyncStatus status) { } |
||||
|
||||
private void OnObjectsCleared() |
||||
{ |
||||
StatusByID.Clear(); |
||||
StatusByObject.Clear(); |
||||
} |
||||
|
||||
|
||||
public SyncStatus GetStatusOrNull(UniqueID id) |
||||
=> StatusByID.TryGetValue(id, out var value) ? value : null; |
||||
public SyncStatus GetStatusOrThrow(UniqueID id) |
||||
=> GetStatusOrNull(id) ?? throw new Exception( |
||||
$"No {nameof(SyncStatus)} found for ID {id}"); |
||||
|
||||
public SyncStatus GetStatusOrNull(Node obj) |
||||
{ |
||||
if (obj.GetType().GetCustomAttribute<SyncAttribute>() == null) |
||||
throw new ArgumentException($"Type {obj.GetType()} is missing {nameof(SyncAttribute)}"); |
||||
return StatusByObject.TryGetValue(obj, out var value) ? value : null; |
||||
} |
||||
public SyncStatus GetStatusOrThrow(Node obj) |
||||
=> GetStatusOrNull(obj) ?? throw new Exception( |
||||
$"No {nameof(SyncStatus)} found for '{obj.Name}' ({obj.GetType()})"); |
||||
} |
||||
|
||||
|
||||
public class SyncStatus |
||||
{ |
||||
public UniqueID ID { get; } |
||||
public Node Object { get; } |
||||
public SyncObjectInfo Info { get; } |
||||
|
||||
public int DirtyProperties { get; set; } |
||||
public SyncMode Mode { get; set; } |
||||
|
||||
public SyncStatus(UniqueID id, Node obj, SyncObjectInfo info) |
||||
{ ID = id; Object = obj; Info = info; } |
||||
} |
||||
|
||||
public enum SyncMode |
||||
{ |
||||
Default, |
||||
Spawn, |
||||
Destroy, |
||||
} |
||||
|
||||
|
||||
public class SyncPacket |
||||
{ |
||||
public List<Object> Changes { get; } = new List<Object>(); |
||||
|
||||
public class Object |
||||
{ |
||||
public ushort InfoID { get; } |
||||
public UniqueID ID { get; } |
||||
public SyncMode Mode { get; } |
||||
public List<(byte, object)> Values { get; } |
||||
public Object(ushort infoID, UniqueID id, SyncMode mode, List<(byte, object)> values) |
||||
{ InfoID = infoID; ID = id; Mode = mode; Values = values; } |
||||
} |
||||
} |
||||
|
||||
internal class SyncPacketObjectDeSerializer |
||||
: DeSerializer<SyncPacket.Object> |
||||
{ |
||||
public override void Serialize(Game game, BinaryWriter writer, SyncPacket.Object value) |
||||
{ |
||||
writer.Write(value.InfoID); |
||||
writer.Write(value.ID.Value); |
||||
writer.Write((byte)value.Mode); |
||||
writer.Write((byte)value.Values.Count); |
||||
|
||||
var objInfo = SyncRegistry.GetOrThrow(value.InfoID); |
||||
foreach (var (propID, val) in value.Values) { |
||||
writer.Write(propID); |
||||
var propInfo = objInfo.PropertiesByID[propID]; |
||||
propInfo.DeSerializer.Serialize(game, writer, val); |
||||
} |
||||
} |
||||
|
||||
public override SyncPacket.Object Deserialize(Game game, BinaryReader reader) |
||||
{ |
||||
var infoID = reader.ReadUInt16(); |
||||
var id = new UniqueID(reader.ReadUInt32()); |
||||
var mode = (SyncMode)reader.ReadByte(); |
||||
var count = reader.ReadByte(); |
||||
|
||||
var objInfo = SyncRegistry.GetOrThrow(infoID); |
||||
if (count > objInfo.PropertiesByID.Count) throw new Exception( |
||||
$"Count is higher than possible number of changes"); |
||||
|
||||
var values = new List<(byte, object)>(count); |
||||
var duplicateCheck = new HashSet<byte>(); |
||||
for (var i = 0; i < count; i++) { |
||||
var propID = reader.ReadByte(); |
||||
if (propID >= objInfo.PropertiesByID.Count) throw new Exception( |
||||
$"No sync property with ID {propID} on {objInfo.Name}"); |
||||
var propInfo = objInfo.PropertiesByID[propID]; |
||||
if (!duplicateCheck.Add(propID)) throw new Exception( |
||||
$"Duplicate entry for property {propInfo.Name}"); |
||||
values.Add((propID, propInfo.DeSerializer.Deserialize(game, reader))); |
||||
} |
||||
|
||||
return new SyncPacket.Object(infoID, id, mode, values); |
||||
} |
||||
} |
@ -1,45 +0,0 @@ |
||||
using System; |
||||
using Godot; |
||||
|
||||
public class SyncClient : Sync |
||||
{ |
||||
protected Client Client => (Client)Game; |
||||
|
||||
// FIXME: This works for now, but will break with dedicated servers. We need to register packet types and their handlers separately. |
||||
// Fortunately, at this time, there is only two packet types: RPC and Sync. We could even reduce that to just a single one? |
||||
public SyncClient(Client client) : base(client) |
||||
=> NetworkPackets.Register<SyncPacket>(PacketDirection.ServerToClient, OnSyncPacket); |
||||
|
||||
private void OnSyncPacket(Game _, NetworkID networkID, SyncPacket packet) |
||||
{ |
||||
foreach (var packetObj in packet.Changes) { |
||||
var info = SyncRegistry.GetOrThrow(packetObj.InfoID); |
||||
var status = GetStatusOrNull(packetObj.ID); |
||||
|
||||
if (status == null) { |
||||
if (packetObj.Mode != SyncMode.Spawn) throw new Exception( |
||||
$"Unknown synced object {info.Name} (ID {packetObj.ID})"); |
||||
|
||||
var obj = info.SpawnInfo.Scene.Init<Node>(); |
||||
Client.GetNode("World").AddChild(obj, true); |
||||
Client.Objects.Add(packetObj.ID, obj); |
||||
status = GetStatusOrThrow(packetObj.ID); |
||||
} else { |
||||
if (packetObj.Mode == SyncMode.Spawn) throw new Exception( |
||||
$"Spawning object {info.Name} with ID {packetObj.ID}, but it already exists"); |
||||
if (info != status.Info) throw new Exception( |
||||
$"Info of synced object being modified doesn't match ({info.Name} != {status.Info.Name})"); |
||||
|
||||
if (packetObj.Mode == SyncMode.Destroy) { |
||||
status.Object.RemoveFromParent(); |
||||
continue; |
||||
} |
||||
} |
||||
|
||||
foreach (var (propID, value) in packetObj.Values) { |
||||
var propDeSerializer = info.PropertiesByID[propID]; |
||||
propDeSerializer.Set(status.Object, value); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,65 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using Godot; |
||||
|
||||
public static class SyncRegistry |
||||
{ |
||||
private static readonly List<SyncObjectInfo> _byID = new List<SyncObjectInfo>(); |
||||
private static readonly Dictionary<Type, SyncObjectInfo> _byType = new Dictionary<Type, SyncObjectInfo>(); |
||||
|
||||
static SyncRegistry() |
||||
{ |
||||
foreach (var type in typeof(SyncRegistry).Assembly.GetTypes()) { |
||||
var syncAttr = type.GetCustomAttribute<SyncAttribute>(); |
||||
if (syncAttr == null) continue; |
||||
|
||||
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception( |
||||
$"Type {type} with {nameof(SyncAttribute)} must be a subclass of {nameof(Node)}"); |
||||
|
||||
var spawnInfo = SpawnRegistry.Get(type); |
||||
var objInfo = new SyncObjectInfo(type, spawnInfo); |
||||
|
||||
foreach (var property in type.GetProperties()) { |
||||
if (property.GetCustomAttribute<SyncAttribute>() == null) continue; |
||||
var propType = typeof(PropertyDeSerializer<,>).MakeGenericType(type, property.PropertyType); |
||||
var propDeSerializer = (IPropertyDeSerializer)Activator.CreateInstance(propType, property); |
||||
objInfo.PropertiesByName.Add(propDeSerializer.Name, propDeSerializer); |
||||
} |
||||
objInfo.PropertiesByID.AddRange(objInfo.PropertiesByName.Values.OrderBy(x => x.HashID)); |
||||
|
||||
_byType.Add(objInfo.Type, objInfo); |
||||
} |
||||
_byID.AddRange(_byType.Values.OrderBy(x => x.Name.GetDeterministicHashCode())); |
||||
for (ushort i = 0; i < _byID.Count; i++) _byID[i].ID = i; |
||||
} |
||||
|
||||
public static SyncObjectInfo GetOrThrow(ushort id) |
||||
=> (id < _byID.Count) ? _byID[id] : throw new Exception( |
||||
$"Unknown {nameof(SyncObjectInfo)} with ID {id}"); |
||||
|
||||
public static SyncObjectInfo GetOrNull<T>() => GetOrNull(typeof(T)); |
||||
public static SyncObjectInfo GetOrNull(Type type) => _byType.TryGetValue(type, out var value) ? value : null; |
||||
|
||||
public static SyncObjectInfo GetOrThrow<T>() => GetOrThrow(typeof(T)); |
||||
public static SyncObjectInfo GetOrThrow(Type type) => GetOrNull(type) ?? throw new Exception( |
||||
$"No {nameof(SyncObjectInfo)} found for type {type} (missing {nameof(SyncAttribute)}?)"); |
||||
} |
||||
|
||||
public class SyncObjectInfo |
||||
{ |
||||
public ushort ID { get; internal set; } |
||||
public Type Type { get; } |
||||
public SpawnInfo SpawnInfo { get; } |
||||
public string Name => Type.Name; |
||||
|
||||
public List<IPropertyDeSerializer> PropertiesByID { get; } = new List<IPropertyDeSerializer>(); |
||||
public Dictionary<string, IPropertyDeSerializer> PropertiesByName { get; } = new Dictionary<string, IPropertyDeSerializer>(); |
||||
|
||||
public SyncObjectInfo(Type type, SpawnInfo spawnInfo) |
||||
{ Type = type; SpawnInfo = spawnInfo; } |
||||
} |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] |
||||
public class SyncAttribute : Attribute { } |
@ -1,73 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using Godot; |
||||
|
||||
public class SyncServer : Sync |
||||
{ |
||||
private static readonly HashSet<SyncStatus> _dirtyObjects = new HashSet<SyncStatus>(); |
||||
|
||||
protected Server Server => (Server)Game; |
||||
|
||||
public SyncServer(Server server) : base(server) |
||||
=> server.Objects.Cleared += _dirtyObjects.Clear; |
||||
|
||||
protected override void OnSyncedAdded(SyncStatus status) |
||||
{ |
||||
status.Mode = SyncMode.Spawn; |
||||
_dirtyObjects.Add(status); |
||||
} |
||||
|
||||
protected override void OnSyncedRemoved(SyncStatus status) |
||||
{ |
||||
status.Mode = SyncMode.Destroy; |
||||
_dirtyObjects.Add(status); |
||||
} |
||||
|
||||
|
||||
public void MarkDirty(Node obj, string property) |
||||
{ |
||||
var status = GetStatusOrThrow(obj); |
||||
if (!status.Info.PropertiesByName.TryGetValue(property, out var propDeSerializer)) throw new ArgumentException( |
||||
$"No {nameof(IPropertyDeSerializer)} found for {obj.GetType()}.{property} (missing {nameof(SyncAttribute)}?)", nameof(property)); |
||||
if (!(obj.GetGame() is Server)) return; |
||||
|
||||
var index = status.Info.PropertiesByID.IndexOf(propDeSerializer); |
||||
status.DirtyProperties |= 1 << index; |
||||
_dirtyObjects.Add(status); |
||||
} |
||||
|
||||
|
||||
public void ProcessDirty(Server server) |
||||
{ |
||||
if (_dirtyObjects.Count == 0) return; |
||||
|
||||
var packet = new SyncPacket(); |
||||
foreach (var status in _dirtyObjects) { |
||||
var values = new List<(byte, object)>(); |
||||
if (status.Mode != SyncMode.Destroy) |
||||
for (byte i = 0; i < status.Info.PropertiesByID.Count; i++) |
||||
if ((status.DirtyProperties & (1 << i)) != 0) |
||||
values.Add((i, status.Info.PropertiesByID[i].Get(status.Object))); |
||||
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, status.Mode, values)); |
||||
// If the object has been newly spawned, now is the time to remove the "Spawn" flag. |
||||
if (status.Mode == SyncMode.Spawn) status.Mode = SyncMode.Default; |
||||
} |
||||
// TODO: Need a different way to send packages to all *properly* connected peers. |
||||
NetworkPackets.Send(server, server.CustomMultiplayer.GetNetworkConnectedPeers().Select(id => new NetworkID(id)), packet); |
||||
|
||||
_dirtyObjects.Clear(); |
||||
} |
||||
|
||||
public void SendAllObjects(Server server, NetworkID networkID) |
||||
{ |
||||
var packet = new SyncPacket(); |
||||
foreach (var status in StatusByObject.Values) { |
||||
var values = new List<(byte, object)>(); |
||||
for (byte i = 0; i < status.Info.PropertiesByID.Count; i++) |
||||
values.Add((i, status.Info.PropertiesByID[i].Get(status.Object))); |
||||
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, SyncMode.Spawn, values)); |
||||
} |
||||
NetworkPackets.Send(server, new []{ networkID }, packet); |
||||
} |
||||
} |
@ -1,19 +1,9 @@ |
||||
using Godot; |
||||
|
||||
[Spawn, Sync, Save] |
||||
public class Block : StaticBody2D |
||||
{ |
||||
[Sync, Save] |
||||
public new BlockPos Position { |
||||
get => BlockPos.FromVector(base.Position); |
||||
set => base.Position = this.SetSync(value).ToVector(); |
||||
} |
||||
|
||||
[Sync, Save] |
||||
public Color Color { |
||||
get => Modulate; |
||||
set => Modulate = this.SetSync(value); |
||||
} |
||||
|
||||
public new BlockPos Position { get => BlockPos.FromVector(base.Position); |
||||
set => base.Position = value.ToVector(); } |
||||
public Color Color { get => Modulate; set => Modulate = value; } |
||||
public bool Unbreakable { get; set; } = false; |
||||
} |
||||
|
@ -0,0 +1,60 @@ |
||||
using System; |
||||
using Godot; |
||||
|
||||
public class LocalPlayer : Player |
||||
{ |
||||
// TODO: Implement "low jumps" activated by releasing the jump button early. |
||||
public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F); |
||||
public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F); |
||||
|
||||
public float MovementSpeed { get; set; } = 160; |
||||
public float JumpVelocity { get; set; } = 240; |
||||
public float Gravity { get; set; } = 480; |
||||
|
||||
public float Acceleration { get; set; } = 0.25F; |
||||
public float GroundFriction { get; set; } = 0.2F; |
||||
public float AirFriction { get; set; } = 0.05F; |
||||
|
||||
public Vector2 Velocity = Vector2.Zero; |
||||
private DateTime? _jumpPressed = null; |
||||
private DateTime? _lastOnFloor = null; |
||||
|
||||
public override void _Process(float delta) |
||||
{ |
||||
base._Process(delta); |
||||
RpcUnreliableId(1, nameof(Player.Move), Position); |
||||
} |
||||
|
||||
public override void _PhysicsProcess(float delta) |
||||
{ |
||||
var moveDir = 0.0F; |
||||
var jumpPressed = false; |
||||
if (!EscapeMenu.Instance.Visible) { |
||||
moveDir = Input.GetActionStrength("move_right") - Input.GetActionStrength("move_left"); |
||||
jumpPressed = Input.IsActionJustPressed("move_jump"); |
||||
} |
||||
|
||||
var friction = IsOnFloor() ? GroundFriction : AirFriction; |
||||
Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * MovementSpeed, Acceleration) |
||||
: Mathf.Lerp(Velocity.x, 0, friction); |
||||
Velocity.y += Gravity * delta; |
||||
Velocity = MoveAndSlide(Velocity, Vector2.Up); |
||||
|
||||
if (jumpPressed) _jumpPressed = DateTime.Now; |
||||
if (IsOnFloor()) _lastOnFloor = DateTime.Now; |
||||
|
||||
if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) && |
||||
((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) { |
||||
Velocity.y = -JumpVelocity; |
||||
_jumpPressed = null; |
||||
_lastOnFloor = null; |
||||
} |
||||
} |
||||
|
||||
[Puppet] |
||||
public void ResetPosition(Vector2 position) |
||||
{ |
||||
Position = position; |
||||
Velocity = Vector2.Zero; |
||||
} |
||||
} |
@ -1,77 +0,0 @@ |
||||
using System; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using Godot; |
||||
|
||||
public class ObjectHolder : IReadOnlyCollection<(UniqueID, Node)> |
||||
{ |
||||
private readonly Dictionary<UniqueID, Node> _nodeByID = new Dictionary<UniqueID, Node>(); |
||||
private readonly Dictionary<Node, UniqueID> _idByNode = new Dictionary<Node, UniqueID>(); |
||||
private uint _newIDCounter = 1; |
||||
|
||||
public event Action<UniqueID, Node> Added; |
||||
public event Action<UniqueID, Node> Removed; |
||||
public event Action Cleared; |
||||
|
||||
|
||||
public UniqueID GetSyncID(Node obj) |
||||
=> _idByNode.TryGetValue(obj, out var value) ? value : throw new Exception( |
||||
$"The specified object '{obj}' does not have a UniqueID"); |
||||
public Node GetObjectByID(UniqueID id) |
||||
=> _nodeByID.TryGetValue(id, out var value) ? value : throw new Exception( |
||||
$"No object associated with {id}"); |
||||
|
||||
|
||||
internal void Add(UniqueID? id, Node obj) |
||||
{ |
||||
if (!(id is UniqueID uid)) { |
||||
// If the given UniqueID is null, keep going until we find an unused one. |
||||
while (_nodeByID.ContainsKey(uid = new UniqueID(_newIDCounter++))) { } |
||||
} |
||||
|
||||
_nodeByID.Add(uid, obj); |
||||
_idByNode.Add(obj, uid); |
||||
Added?.Invoke(uid, obj); |
||||
} |
||||
|
||||
internal void OnNodeRemoved(Node obj) |
||||
{ |
||||
if (!_idByNode.TryGetValue(obj, out var id)) return; |
||||
|
||||
_nodeByID.Remove(id); |
||||
_idByNode.Remove(obj); |
||||
Removed?.Invoke(id, obj); |
||||
} |
||||
|
||||
public void Clear() |
||||
{ |
||||
var objects = _nodeByID.Values.ToArray(); |
||||
|
||||
_nodeByID.Clear(); |
||||
_idByNode.Clear(); |
||||
Cleared?.Invoke(); |
||||
|
||||
foreach (var obj in objects) |
||||
obj.RemoveFromParent(); |
||||
} |
||||
|
||||
// IReadOnlyCollection implementation |
||||
public int Count => _nodeByID.Count; |
||||
public IEnumerator<(UniqueID, Node)> GetEnumerator() |
||||
=> _nodeByID.Select(entry => (entry.Key, entry.Value)).GetEnumerator(); |
||||
IEnumerator IEnumerable.GetEnumerator() |
||||
=> GetEnumerator(); |
||||
} |
||||
|
||||
public readonly struct UniqueID : IEquatable<UniqueID> |
||||
{ |
||||
public uint Value { get; } |
||||
public UniqueID(uint value) => Value = value; |
||||
public override bool Equals(object obj) => (obj is UniqueID other) && Equals(other); |
||||
public bool Equals(UniqueID other) => Value == other.Value; |
||||
public override int GetHashCode() => Value.GetHashCode(); |
||||
public override string ToString() => $"{nameof(UniqueID)}({Value})"; |
||||
public static bool operator ==(UniqueID left, UniqueID right) => left.Equals(right); |
||||
public static bool operator !=(UniqueID left, UniqueID right) => !left.Equals(right); |
||||
} |
@ -1,70 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Reflection; |
||||
using Godot; |
||||
|
||||
public static class SpawnRegistry |
||||
{ |
||||
private static readonly Dictionary<int, SpawnInfo> _byID = new Dictionary<int, SpawnInfo>(); |
||||
private static readonly Dictionary<Type, SpawnInfo> _byType = new Dictionary<Type, SpawnInfo>(); |
||||
|
||||
static SpawnRegistry() |
||||
{ |
||||
foreach (var type in typeof(SpawnRegistry).Assembly.GetTypes()) { |
||||
var objAttr = type.GetCustomAttribute<SpawnAttribute>(); |
||||
if (objAttr == null) continue; |
||||
|
||||
if (!typeof(Node).IsAssignableFrom(type)) throw new Exception( |
||||
$"Type {type} with {nameof(SpawnAttribute)} must be a subclass of {nameof(Node)}"); |
||||
|
||||
var objInfo = new SpawnInfo(type); |
||||
_byID.Add(objInfo.HashID, objInfo); |
||||
_byType.Add(objInfo.Type, objInfo); |
||||
} |
||||
} |
||||
|
||||
public static T Spawn<T>(this Server server) |
||||
where T : Node |
||||
{ |
||||
var info = Get<T>(); |
||||
var obj = info.Scene.Init<T>(); |
||||
server.GetNode("World").AddChild(obj, true); |
||||
server.Objects.Add(null, obj); |
||||
return obj; |
||||
} |
||||
|
||||
public static SpawnInfo Get(int id) |
||||
=> _byID.TryGetValue(id, out var value) ? value : throw new Exception( |
||||
$"No {nameof(SpawnInfo)} found with ID {id}"); |
||||
|
||||
public static SpawnInfo Get<T>() |
||||
=> Get(typeof(T)); |
||||
public static SpawnInfo Get(Type type) |
||||
=> _byType.TryGetValue(type, out var value) ? value : throw new Exception( |
||||
$"No {nameof(SpawnInfo)} found for type {type} (missing {nameof(SpawnAttribute)}?)"); |
||||
} |
||||
|
||||
public class SpawnInfo |
||||
{ |
||||
public Type Type { get; } |
||||
public int HashID { get; } |
||||
public PackedScene Scene { get; } |
||||
|
||||
public SpawnInfo(Type type) |
||||
{ |
||||
Type = type; |
||||
HashID = type.FullName.GetDeterministicHashCode(); |
||||
|
||||
var sceneStr = Type.GetCustomAttribute<SpawnAttribute>().Scene; |
||||
if (sceneStr == null) sceneStr = $"res://scene/{Type.Name}.tscn"; |
||||
Scene = GD.Load<PackedScene>(sceneStr); |
||||
} |
||||
} |
||||
|
||||
[AttributeUsage(AttributeTargets.Class)] |
||||
public class SpawnAttribute : Attribute |
||||
{ |
||||
public string Scene { get; } |
||||
public SpawnAttribute(string scene = null) |
||||
=> Scene = scene; |
||||
} |
@ -1,23 +1,11 @@ |
||||
using System.Linq; |
||||
using Godot; |
||||
using Godot.Collections; |
||||
|
||||
public abstract class Game : Node2D |
||||
public abstract class Game : Node |
||||
{ |
||||
public ObjectHolder Objects { get; } |
||||
public Sync Sync { get; protected set; } |
||||
|
||||
public Game() => Objects = new ObjectHolder(); |
||||
|
||||
public override void _Ready() => GetTree().Connect("node_removed", this, nameof(OnNodeRemoved)); |
||||
private void OnNodeRemoved(Node node) => Objects.OnNodeRemoved(node); |
||||
|
||||
// Using _EnterTree to make sure this code runs before any other. |
||||
public override void _EnterTree() |
||||
=> GD.Randomize(); |
||||
|
||||
// NOTE: When multithreaded physics are enabled, DirectSpaceState can only be used in _PhysicsProcess. |
||||
public Block GetBlockAt(BlockPos position) |
||||
=> GetWorld2d().DirectSpaceState.IntersectPoint(position.ToVector()).Cast<Dictionary>() |
||||
.Select(c => c["collider"]).OfType<Block>().FirstOrDefault(); |
||||
public override void _Ready() |
||||
=> Multiplayer.RootNode = this.GetWorld(); |
||||
} |
||||
|
@ -1,58 +1,26 @@ |
||||
using System.Collections.Generic; |
||||
using System.Runtime.CompilerServices; |
||||
using Godot; |
||||
|
||||
public static class Extensions |
||||
{ |
||||
public static void RemoveFromParent(this Node node) |
||||
{ |
||||
node.GetParent().RemoveChild(node); |
||||
node.QueueFree(); |
||||
} |
||||
|
||||
public static T Init<T>(this PackedScene @this) |
||||
public static T Init<T>(this PackedScene scene) |
||||
where T : Node |
||||
{ |
||||
var instance = (T)@this.Instance(); |
||||
(instance as IInitializer)?.Initialize(); |
||||
return instance; |
||||
var node = scene.Instance<T>(); |
||||
(node as IInitializable)?.Initialize(); |
||||
return node; |
||||
} |
||||
|
||||
|
||||
public static Game GetGame(this Node node) |
||||
=> node.GetTree().Root.GetChild<Game>(0); |
||||
public static Client GetClient(this Node node) |
||||
=> node.GetGame() as Client; |
||||
public static Server GetServer(this Node node) |
||||
=> node.GetGame() as Server; |
||||
|
||||
|
||||
public static TValue SetSync<TObject, TValue>( |
||||
this TObject obj, TValue value, |
||||
[CallerMemberName] string property = null) |
||||
where TObject : Node |
||||
{ obj.GetServer()?.Sync.MarkDirty(obj, property); return value; } |
||||
|
||||
|
||||
public static void Deconstruct<TKey, TValue>( |
||||
this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value) |
||||
{ key = kvp.Key; value = kvp.Value; } |
||||
|
||||
public static int GetDeterministicHashCode(this string str) |
||||
{ unchecked { |
||||
int hash1 = (5381 << 16) + 5381; |
||||
int hash2 = hash1; |
||||
for (int i = 0; i < str.Length; i += 2) { |
||||
hash1 = ((hash1 << 5) + hash1) ^ str[i]; |
||||
if (i == str.Length - 1) break; |
||||
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; |
||||
} |
||||
return hash1 + (hash2 * 1566083941); |
||||
} } |
||||
|
||||
public static World GetWorld(this Node node) |
||||
=> node.GetGame().GetNode<World>("World"); |
||||
} |
||||
|
||||
public interface IInitializer |
||||
public interface IInitializable |
||||
{ |
||||
void Initialize(); |
||||
} |
||||
|
@ -0,0 +1,92 @@ |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using Godot; |
||||
|
||||
public class World : Node |
||||
{ |
||||
[Export] public NodePath PlayerContainerPath { get; set; } |
||||
[Export] public NodePath BlockContainerPath { get; set; } |
||||
|
||||
public Node PlayerContainer { get; private set; } |
||||
public Node BlockContainer { get; private set; } |
||||
|
||||
public PackedScene BlockScene { get; private set; } |
||||
public PackedScene PlayerScene { get; private set; } |
||||
public PackedScene LocalPlayerScene { get; private set; } |
||||
|
||||
public override void _Ready() |
||||
{ |
||||
PlayerContainer = GetNode(PlayerContainerPath); |
||||
BlockContainer = GetNode(BlockContainerPath); |
||||
|
||||
BlockScene = GD.Load<PackedScene>("res://scene/Block.tscn"); |
||||
PlayerScene = GD.Load<PackedScene>("res://scene/Player.tscn"); |
||||
LocalPlayerScene = GD.Load<PackedScene>("res://scene/LocalPlayer.tscn"); |
||||
} |
||||
|
||||
public IEnumerable<Player> Players |
||||
=> PlayerContainer.GetChildren().Cast<Player>(); |
||||
public Player GetPlayer(int networkID) |
||||
=> PlayerContainer.GetNode<Player>(networkID.ToString()); |
||||
|
||||
public Block GetBlockAt(BlockPos position) |
||||
=> BlockContainer.GetNodeOrNull<Block>(position.ToString()); |
||||
|
||||
|
||||
public void Clear() |
||||
{ |
||||
foreach (var player in Players) { |
||||
BlockContainer.RemoveChild(player); |
||||
player.QueueFree(); |
||||
} |
||||
foreach (var node in BlockContainer.GetChildren().Cast<Node>()) { |
||||
BlockContainer.RemoveChild(node); |
||||
node.QueueFree(); |
||||
} |
||||
} |
||||
|
||||
|
||||
[PuppetSync] |
||||
public void SpawnBlock(int x, int y, Color color, bool unbreakable) |
||||
=> SpawnBlockInternal(x, y, color, unbreakable); |
||||
[Puppet] |
||||
public void SendBlock(int x, int y, Color color, bool unbreakable) |
||||
=> SpawnBlockInternal(x, y, color, unbreakable); |
||||
|
||||
private void SpawnBlockInternal(int x, int y, Color color, bool unbreakable) |
||||
{ |
||||
var position = new BlockPos(x, y); |
||||
var block = BlockScene.Init<Block>(); |
||||
block.Name = position.ToString(); |
||||
block.Position = position; |
||||
block.Color = color; |
||||
block.Unbreakable = unbreakable; |
||||
BlockContainer.AddChild(block); |
||||
} |
||||
|
||||
[PuppetSync] |
||||
public void SpawnPlayer(int networkID, Vector2 position, string displayName, Color color) |
||||
=> SpawnPlayerInternal(networkID, position, displayName, color); |
||||
[Puppet] |
||||
public void SendPlayer(int networkID, Vector2 position, string displayName, Color color) |
||||
=> SpawnPlayerInternal(networkID, position, displayName, color); |
||||
|
||||
private void SpawnPlayerInternal(int networkID, Vector2 position, string displayName, Color color) |
||||
{ |
||||
var isLocal = networkID == GetTree().GetNetworkUniqueId(); |
||||
var player = (isLocal ? LocalPlayerScene : PlayerScene).Init<Player>(); |
||||
player.NetworkID = networkID; |
||||
player.Position = position; |
||||
player.DisplayName = displayName; |
||||
player.Color = color; |
||||
PlayerContainer.AddChild(player); |
||||
} |
||||
|
||||
[PuppetSync] |
||||
public void Despawn(NodePath path) |
||||
{ |
||||
var node = GetNode(path); |
||||
node.GetParent().RemoveChild(node); |
||||
node.QueueFree(); |
||||
} |
||||
} |
Loading…
Reference in new issue