parent
310cbe5221
commit
b7e7632c00
23 changed files with 650 additions and 443 deletions
@ -0,0 +1,27 @@ |
||||
using System; |
||||
using System.Linq; |
||||
using MessagePack; |
||||
using MessagePack.Formatters; |
||||
|
||||
public class ChunkFormatter : IMessagePackFormatter<Chunk> |
||||
{ |
||||
public Chunk Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) |
||||
{ |
||||
var numElements = reader.ReadArrayHeader(); |
||||
if (numElements != 3) throw new Exception("Expected 3 elements"); |
||||
|
||||
var chunkX = reader.ReadInt32(); |
||||
var chunkY = reader.ReadInt32(); |
||||
return new Chunk((chunkX, chunkY)) |
||||
{ Layers = MessagePackSerializer.Deserialize<IChunkLayer[]>(ref reader) }; |
||||
} |
||||
|
||||
public void Serialize(ref MessagePackWriter writer, Chunk chunk, MessagePackSerializerOptions options) |
||||
{ |
||||
writer.WriteArrayHeader(3); |
||||
writer.Write(chunk.ChunkPos.X); |
||||
writer.Write(chunk.ChunkPos.Y); |
||||
MessagePackSerializer.Serialize(ref writer, |
||||
chunk.Layers.Where(l => !l.IsDefault).ToArray()); |
||||
} |
||||
} |
@ -0,0 +1,52 @@ |
||||
using System; |
||||
using MessagePack; |
||||
|
||||
// [MessagePackFormatter(typeof(DeSerializableFormatter<World>))] |
||||
public partial class World : IDeSerializable |
||||
{ |
||||
public void Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) |
||||
{ |
||||
// Restore defaults. |
||||
Playtime = TimeSpan.Zero; |
||||
Seed = 0; |
||||
Generator = WorldGeneratorRegistry.GetOrNull("Simple"); |
||||
ChunkContainer.ClearChildren(); |
||||
|
||||
var numKeys = reader.ReadMapHeader(); |
||||
for (var keyIndex = 0; keyIndex < numKeys; keyIndex++) { |
||||
var key = reader.ReadString(); |
||||
switch (key) { |
||||
case nameof(Playtime): |
||||
Playtime = TimeSpan.FromMilliseconds(reader.ReadUInt64()); |
||||
break; |
||||
case nameof(Seed): |
||||
Seed = reader.ReadInt32(); |
||||
break; |
||||
case nameof(Generator): |
||||
Generator = WorldGeneratorRegistry.GetOrNull(reader.ReadString()) ?? Generator; |
||||
break; |
||||
case nameof(Chunks): |
||||
ChunkContainer.AddRange(MessagePackSerializer.Deserialize<Chunk[]>(ref reader)); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options) |
||||
{ |
||||
writer.WriteMapHeader(4); |
||||
|
||||
writer.Write(nameof(Playtime)); |
||||
writer.Write((ulong)Playtime.TotalMilliseconds); |
||||
|
||||
writer.Write(nameof(Seed)); |
||||
writer.Write(Seed); |
||||
|
||||
writer.Write(nameof(Generator)); |
||||
writer.Write(Generator.Name); |
||||
|
||||
writer.Write(nameof(Chunks)); |
||||
writer.WriteArrayHeader(ChunkContainer.GetChildCount()); |
||||
foreach (var chunk in Chunks) MessagePackSerializer.Serialize(ref writer, chunk, options); |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
using System.Buffers; |
||||
using MessagePack; |
||||
using MessagePack.Formatters; |
||||
using Nerdbank.Streams; |
||||
|
||||
public interface IDeSerializable |
||||
{ |
||||
void Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options); |
||||
|
||||
void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options); |
||||
} |
||||
|
||||
public static class DeSerializableExtensions |
||||
{ |
||||
public static void Deserialize(this IDeSerializable value, byte[] data, MessagePackSerializerOptions options = null) |
||||
{ |
||||
options = options ?? MessagePackSerializerOptions.Standard; |
||||
var reader = new MessagePackReader(data); |
||||
value.Deserialize(ref reader, options); |
||||
} |
||||
|
||||
public static byte[] SerializeToBytes(this IDeSerializable value, MessagePackSerializerOptions options = null) |
||||
{ |
||||
options = options ?? MessagePackSerializerOptions.Standard; |
||||
var sequence = new Sequence<byte>(); |
||||
var writer = new MessagePackWriter(sequence); |
||||
value.Serialize(ref writer, options); |
||||
writer.Flush(); |
||||
return sequence.AsReadOnlySequence.ToArray(); |
||||
} |
||||
} |
||||
|
||||
public class DeSerializableFormatter<T> : IMessagePackFormatter<T> |
||||
where T : IDeSerializable, new() |
||||
{ |
||||
public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) |
||||
{ |
||||
var value = new T(); |
||||
value.Deserialize(ref reader, options); |
||||
return value; |
||||
} |
||||
|
||||
public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options) |
||||
{ |
||||
value.Serialize(ref writer, options); |
||||
} |
||||
} |
@ -0,0 +1,6 @@ |
||||
using Godot; |
||||
|
||||
public class BlockEntity : Node2D |
||||
{ |
||||
|
||||
} |
@ -0,0 +1,50 @@ |
||||
using System; |
||||
using Godot; |
||||
|
||||
public class BlockRef |
||||
{ |
||||
public World World { get; } |
||||
public BlockPos Position { get; } |
||||
|
||||
public BlockRef(World world, BlockPos position) |
||||
{ |
||||
World = world; |
||||
Position = position; |
||||
} |
||||
|
||||
|
||||
public Chunk GetChunk(bool create) |
||||
=> World.GetChunk(Position.ToChunkPos(), create); |
||||
|
||||
public IChunkLayer<T> GetChunkLayer<T>(bool create) |
||||
=> GetChunk(create)?.GetLayer<T>(create); |
||||
|
||||
public BlockEntity GetEntity(bool create) |
||||
=> GetChunk(create)?.GetBlockEntity(Position.GlobalToChunkRel(), create); |
||||
|
||||
|
||||
public T Get<T>() |
||||
{ |
||||
if (ChunkLayerRegistry.TryGetDefault<T>(out var @default)) { |
||||
var layer = GetChunkLayer<T>(false); |
||||
return (layer != null) ? layer[Position.GlobalToChunkRel()] : @default; |
||||
} else if (typeof(Node).IsAssignableFrom(typeof(T))) |
||||
return (T)(object)GetEntity(false)?.GetNodeOrNull(typeof(T).Name); |
||||
else throw new ArgumentException($"Unable to access {typeof(T).Name} on a Block", nameof(T)); |
||||
} |
||||
|
||||
public T GetOrCreate<T>() where T : Node, new() |
||||
=> GetEntity(true).GetOrCreateChild(typeof(T).Name, () => new T()); |
||||
|
||||
public void Set<T>(T value) |
||||
{ |
||||
if (ChunkLayerRegistry.Has<T>()) |
||||
GetChunkLayer<T>(true)[Position.GlobalToChunkRel()] = value; |
||||
else if (typeof(Node).IsAssignableFrom(typeof(T))) { |
||||
var entity = GetEntity(true); |
||||
var existing = entity.GetNodeOrNull(typeof(T).Name); |
||||
existing?.RemoveFromParent(); |
||||
entity.AddChild((Node)(object)value); |
||||
} else throw new ArgumentException($"Unable to access {typeof(T).Name} on a Block", nameof(T)); |
||||
} |
||||
} |
@ -1,83 +0,0 @@ |
||||
using System.IO; |
||||
using Godot; |
||||
|
||||
public readonly struct BlockData |
||||
{ |
||||
public Block Block { get; } // TODO: Replace with 2-byte or smaller integer identifier? |
||||
public int RawColor { get; } // TODO: Replace with smaller representation? |
||||
// Perhaps we can fit this into 4 bytes total? |
||||
public Color Color => new Color(RawColor); |
||||
public BlockData(Block block, int rawColor) { Block = block; RawColor = rawColor; } |
||||
public BlockData(Block block, Color color) { Block = block; RawColor = color.ToRgba32(); } |
||||
} |
||||
|
||||
public class BlockLayer : BasicChunkLayer<BlockData> |
||||
{ |
||||
private MeshInstance2D _render = null; |
||||
private StaticBody2D _collider = null; |
||||
|
||||
public override void _Process(float delta) |
||||
{ |
||||
if (!Dirty) return; |
||||
Dirty = false; |
||||
|
||||
var st = (SurfaceTool)null; |
||||
if (this.GetGame() is Client) { |
||||
if (_render == null) AddChild(_render = new MeshInstance2D |
||||
{ Texture = GD.Load<Texture>("res://gfx/block.png") }); |
||||
st = new SurfaceTool(); |
||||
st.Begin(Mesh.PrimitiveType.Triangles); |
||||
} |
||||
|
||||
_collider?.RemoveFromParent(); |
||||
if (IsDefault) _collider = null; |
||||
else AddChild(_collider = new StaticBody2D()); |
||||
|
||||
var size = Block.LENGTH; |
||||
var index = 0; |
||||
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) { |
||||
var data = Data[i]; |
||||
if (data.Block == null) continue; |
||||
|
||||
var x = (float)(i & Chunk.BIT_MASK); |
||||
var y = (float)(i >> Chunk.BIT_SHIFT); |
||||
|
||||
if (_render != null) { |
||||
st.AddColor(data.Color); |
||||
st.AddUv(new Vector2(0, 0)); st.AddVertex(new Vector3(x - 0.5F, y - 0.5F, 0) * size); |
||||
st.AddUv(new Vector2(1, 0)); st.AddVertex(new Vector3(x + 0.5F, y - 0.5F, 0) * size); |
||||
st.AddUv(new Vector2(1, 1)); st.AddVertex(new Vector3(x + 0.5F, y + 0.5F, 0) * size); |
||||
st.AddUv(new Vector2(0, 1)); st.AddVertex(new Vector3(x - 0.5F, y + 0.5F, 0) * size); |
||||
|
||||
st.AddIndex(index); st.AddIndex(index + 1); st.AddIndex(index + 2); |
||||
st.AddIndex(index); st.AddIndex(index + 3); st.AddIndex(index + 2); |
||||
index += 4; |
||||
} |
||||
|
||||
var ownerID = _collider.CreateShapeOwner(null); |
||||
_collider.ShapeOwnerAddShape(ownerID, data.Block.Shape); |
||||
_collider.ShapeOwnerSetTransform(ownerID, Transform2D.Identity.Translated(new Vector2(x, y) * size)); |
||||
} |
||||
|
||||
if (_render != null) |
||||
_render.Mesh = st.Commit(); |
||||
} |
||||
|
||||
public override void Read(BinaryReader reader) |
||||
{ |
||||
NonDefaultCount = 0; |
||||
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) { |
||||
var color = reader.ReadInt32(); |
||||
if (color == 0) continue; |
||||
Data[i] = new BlockData(Block.DEFAULT, color); |
||||
NonDefaultCount++; |
||||
} |
||||
Dirty = true; |
||||
} |
||||
|
||||
public override void Write(BinaryWriter writer) |
||||
{ |
||||
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) |
||||
writer.Write(Data[i].RawColor); // Is 0 if block is not set. |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
using Godot; |
||||
|
||||
public partial class Chunk |
||||
{ |
||||
private MeshInstance2D _render = null; |
||||
private StaticBody2D _collider = null; |
||||
private bool _dirty = true; |
||||
|
||||
public override void _Process(float delta) |
||||
{ |
||||
if (!_dirty) return; |
||||
_dirty = false; |
||||
|
||||
var blocks = GetLayer<Block>(false); |
||||
var colors = GetLayer<Color>(false); |
||||
|
||||
if ((this.GetGame() is Client) && (blocks != null)) { |
||||
if (_render == null) AddChild(_render = new MeshInstance2D |
||||
{ Texture = GD.Load<Texture>("res://gfx/block.png") }); |
||||
var st = new SurfaceTool(); |
||||
st.Begin(Mesh.PrimitiveType.Triangles); |
||||
|
||||
var index = 0; |
||||
for (var i = 0; i < LENGTH * LENGTH; i++) { |
||||
var texture = blocks[i].Texture; // FIXME: Replace with texture index. |
||||
if (texture == null) continue; |
||||
|
||||
var x = (float)(i & BIT_MASK); |
||||
var y = (float)(i >> BIT_SHIFT); |
||||
|
||||
st.AddColor(colors?[i] ?? Colors.White); |
||||
st.AddUv(new Vector2(0, 0)); st.AddVertex(new Vector3(x - 0.5F, y - 0.5F, 0) * Block.LENGTH); |
||||
st.AddUv(new Vector2(1, 0)); st.AddVertex(new Vector3(x + 0.5F, y - 0.5F, 0) * Block.LENGTH); |
||||
st.AddUv(new Vector2(1, 1)); st.AddVertex(new Vector3(x + 0.5F, y + 0.5F, 0) * Block.LENGTH); |
||||
st.AddUv(new Vector2(0, 1)); st.AddVertex(new Vector3(x - 0.5F, y + 0.5F, 0) * Block.LENGTH); |
||||
|
||||
st.AddIndex(index); st.AddIndex(index + 1); st.AddIndex(index + 2); |
||||
st.AddIndex(index); st.AddIndex(index + 3); st.AddIndex(index + 2); |
||||
index += 4; |
||||
} |
||||
_render.Mesh = st.Commit(); |
||||
} |
||||
|
||||
_collider?.RemoveFromParent(); |
||||
if (blocks?.IsDefault == false) { |
||||
AddChild(_collider = new StaticBody2D()); |
||||
|
||||
for (var i = 0; i < LENGTH * LENGTH; i++) { |
||||
var shape = blocks[i].Shape; |
||||
if (shape == null) continue; |
||||
|
||||
var x = (float)(i & BIT_MASK); |
||||
var y = (float)(i >> BIT_SHIFT); |
||||
|
||||
var ownerID = _collider.CreateShapeOwner(null); |
||||
_collider.ShapeOwnerAddShape(ownerID, shape); |
||||
_collider.ShapeOwnerSetTransform(ownerID, Transform2D.Identity.Translated(new Vector2(x, y) * Block.LENGTH)); |
||||
} |
||||
} else _collider = null; |
||||
} |
||||
} |
@ -1,38 +1,76 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using Godot; |
||||
using MessagePack; |
||||
|
||||
public class Chunk : Node2D |
||||
[MessagePackFormatter(typeof(ChunkFormatter))] |
||||
public partial class Chunk : Node2D |
||||
{ |
||||
public const int LENGTH = 32; |
||||
public const int BIT_SHIFT = 5; |
||||
public const int BIT_MASK = ~(~0 << BIT_SHIFT); |
||||
|
||||
public (int X, int Y) ChunkPosition { get; } |
||||
public IEnumerable<IChunkLayer> Layers |
||||
=> GetChildren().OfType<IChunkLayer>(); |
||||
|
||||
private readonly List<IChunkLayer> _layers = new List<IChunkLayer>(); |
||||
|
||||
public (int X, int Y) ChunkPos { get; } |
||||
public IEnumerable<IChunkLayer> Layers { |
||||
get => _layers.AsReadOnly(); |
||||
internal set { |
||||
foreach (var layer in _layers) |
||||
layer.Changed -= OnLayerChanged; |
||||
_layers.Clear(); |
||||
|
||||
foreach (var layer in value) { |
||||
_layers.Add(layer); |
||||
layer.Changed += OnLayerChanged; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public Chunk((int X, int Y) chunkPos) |
||||
{ |
||||
Name = $"Chunk ({chunkPos})"; |
||||
ChunkPosition = chunkPos; |
||||
ChunkPos = chunkPos; |
||||
Position = new Vector2(chunkPos.X << (BIT_SHIFT + Block.BIT_SHIFT), |
||||
chunkPos.Y << (BIT_SHIFT + Block.BIT_SHIFT)); |
||||
} |
||||
|
||||
public T GetLayerOrNull<T>() => (T)GetLayerOrNull(typeof(T).Name); |
||||
public T GetOrCreateLayer<T>() => (T)GetOrCreateLayer(typeof(T).Name); |
||||
|
||||
public IChunkLayer GetLayerOrNull(string name) |
||||
=> GetNodeOrNull<IChunkLayer>(name); |
||||
public IChunkLayer GetOrCreateLayer(string name) |
||||
public IChunkLayer<T> GetLayer<T>(bool create) |
||||
=> (IChunkLayer<T>)GetLayer(typeof(T), create); |
||||
public IChunkLayer GetLayer(Type type, bool create) |
||||
{ |
||||
var layer = GetLayerOrNull(name); |
||||
if (layer == null) AddChild((Node)(layer = ChunkLayerRegistry.Create(name))); |
||||
var layer = _layers.Find(l => l.AccessType == type); |
||||
if ((layer == null) && create) { |
||||
layer = ChunkLayerRegistry.Create(type); |
||||
layer.Changed += OnLayerChanged; |
||||
_layers.Add(layer); |
||||
} |
||||
return layer; |
||||
} |
||||
public void OnLayerChanged(IChunkLayer layer) |
||||
=> _dirty = true; |
||||
|
||||
|
||||
public BlockEntity GetBlockEntity(BlockPos pos, bool create) |
||||
{ |
||||
EnsureWithinBounds(pos); |
||||
return create ? this.GetOrCreateChild(pos.ToString(), () => new BlockEntity()) |
||||
: GetNode<BlockEntity>(pos.ToString()); |
||||
} |
||||
|
||||
|
||||
public static void EnsureWithinBounds(BlockPos pos) |
||||
{ |
||||
if ((pos.X < 0) || (pos.X >= LENGTH) || (pos.Y < 0) || (pos.Y >= LENGTH)) throw new ArgumentException( |
||||
$"{pos} must be within chunk boundaries - (0,0) inclusive to ({LENGTH},{LENGTH}) exclusive"); |
||||
} |
||||
|
||||
// TODO: How should we handle chunk extends? Blocks can go "outside" of the current extends, since they're centered. |
||||
// public override void _Draw() |
||||
// => DrawRect(new Rect2(Vector2.Zero, Vector2.One * (LENGTH * Block.LENGTH)), Colors.Blue, false); |
||||
|
||||
public static int GetIndex(BlockPos pos) => pos.X | pos.Y << BIT_SHIFT; |
||||
public static int GetIndex(int x, int y) => x | y << BIT_SHIFT; |
||||
} |
||||
|
@ -1,113 +1,190 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
using Godot; |
||||
using MessagePack; |
||||
using Expression = System.Linq.Expressions.Expression; |
||||
|
||||
public interface IChunkLayer |
||||
[Union(0, typeof(BlockLayer))] |
||||
[Union(1, typeof(ColorLayer))] |
||||
public interface IChunkLayer : IDeSerializable |
||||
{ |
||||
Type AccessType { get; } |
||||
bool IsDefault { get; } |
||||
void Read(BinaryReader reader); |
||||
void Write(BinaryWriter writer); |
||||
event Action<IChunkLayer> Changed; |
||||
} |
||||
|
||||
public interface IChunkLayer<T> : IChunkLayer |
||||
{ |
||||
T this[BlockPos pos] { get; set; } |
||||
T this[int x, int y] { get; set; } |
||||
T this[int index] { get; set; } |
||||
} |
||||
|
||||
|
||||
public static class ChunkLayerRegistry |
||||
public class ArrayChunkLayer<T> : IChunkLayer<T> |
||||
{ |
||||
private static readonly Dictionary<string, Type> _types = new Dictionary<string, Type>(); |
||||
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default; |
||||
|
||||
static ChunkLayerRegistry() |
||||
=> Register<BlockLayer>(); |
||||
private T[] _data = new T[Chunk.LENGTH * Chunk.LENGTH]; |
||||
public int NonDefaultCount { get; protected set; } = 0; |
||||
|
||||
public static void Register<T>() |
||||
where T : Node2D, IChunkLayer |
||||
=> _types.Add(typeof(T).Name, typeof(T)); |
||||
public Type AccessType => typeof(T); |
||||
public bool IsDefault => NonDefaultCount == 0; |
||||
|
||||
public static IChunkLayer Create(string name) |
||||
{ |
||||
var layer = (IChunkLayer)Activator.CreateInstance(_types[name]); |
||||
((Node)layer).Name = name; |
||||
return layer; |
||||
public event Action<IChunkLayer> Changed; |
||||
|
||||
public T this[BlockPos pos] { |
||||
get => this[Chunk.GetIndex(pos)]; |
||||
set => this[Chunk.GetIndex(pos)] = value; |
||||
} |
||||
} |
||||
public T this[int x, int y] { |
||||
get => this[Chunk.GetIndex(x, y)]; |
||||
set => this[Chunk.GetIndex(x, y)] = value; |
||||
} |
||||
public T this[int index] { |
||||
get => _data[index]; |
||||
set { |
||||
var previous = _data[index]; |
||||
if (COMPARER.Equals(value, previous)) return; |
||||
_data[index] = value; |
||||
|
||||
public static class ChunkLayerExtensions |
||||
{ |
||||
public static void FromBytes(this IChunkLayer layer, byte[] data) |
||||
{ |
||||
using (var stream = new MemoryStream(data)) |
||||
layer.Read(stream); |
||||
if (!COMPARER.Equals(previous, default)) NonDefaultCount--; |
||||
if (!COMPARER.Equals(value, default)) NonDefaultCount++; |
||||
Changed?.Invoke(this); |
||||
} |
||||
public static void Read(this IChunkLayer layer, Stream stream) |
||||
{ |
||||
using (var reader = new BinaryReader(stream)) |
||||
layer.Read(reader); |
||||
} |
||||
|
||||
public static byte[] ToBytes(this IChunkLayer layer) |
||||
public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options) |
||||
{ |
||||
using (var stream = new MemoryStream()) { |
||||
layer.Write(stream); |
||||
return stream.ToArray(); |
||||
} |
||||
writer.Write(NonDefaultCount); |
||||
MessagePackSerializer.Serialize(ref writer, _data); |
||||
} |
||||
public static void Write(this IChunkLayer layer, Stream stream) |
||||
public void Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) |
||||
{ |
||||
using (var writer = new BinaryWriter(stream)) |
||||
layer.Write(writer); |
||||
NonDefaultCount = reader.ReadInt32(); |
||||
_data = MessagePackSerializer.Deserialize<T[]>(ref reader); |
||||
} |
||||
} |
||||
|
||||
public class TranslationLayer<TData, TAccess> : IChunkLayer<TAccess> |
||||
{ |
||||
private readonly ArrayChunkLayer<TData> _data = new ArrayChunkLayer<TData>(); |
||||
private readonly Func<TData, TAccess> _from; |
||||
private readonly Func<TAccess, TData> _to; |
||||
|
||||
public TranslationLayer(Func<TData, TAccess> from, Func<TAccess, TData> to) |
||||
{ _from = from; _to = to; } |
||||
|
||||
public Type AccessType => typeof(TAccess); |
||||
public bool IsDefault => _data.IsDefault; |
||||
public event Action<IChunkLayer> Changed { add => _data.Changed += value; remove => _data.Changed -= value; } |
||||
public TAccess this[BlockPos pos] { get => _from(_data[pos]); set => _data[pos] = _to(value); } |
||||
public TAccess this[int x, int y] { get => _from(_data[x, y]); set => _data[x, y] = _to(value); } |
||||
public TAccess this[int index] { get => _from(_data[index]); set => _data[index] = _to(value); } |
||||
|
||||
public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options) |
||||
=> _data.Serialize(ref writer, options); |
||||
public void Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) |
||||
=> _data.Deserialize(ref reader, options); |
||||
} |
||||
|
||||
[MessagePackFormatter(typeof(DeSerializableFormatter<BlockLayer>))] |
||||
public class BlockLayer : TranslationLayer<byte, Block> |
||||
{ |
||||
public static readonly Block DEFAULT = Blocks.AIR; |
||||
public BlockLayer() : base(i => BlockRegistry.Get(i), b => (byte)b.ID) { } |
||||
} |
||||
|
||||
public abstract class BasicChunkLayer<T> : Node2D, IChunkLayer<T> |
||||
[MessagePackFormatter(typeof(DeSerializableFormatter<ColorLayer>))] |
||||
public class ColorLayer : TranslationLayer<int, Color> |
||||
{ |
||||
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default; |
||||
public static readonly Color DEFAULT = Colors.White; |
||||
public ColorLayer() : base(i => new Color(i), c => c.ToRgba32()) { } |
||||
} |
||||
|
||||
protected T[] Data { get; } = new T[Chunk.LENGTH * Chunk.LENGTH]; |
||||
protected bool Dirty { get; set; } = true; |
||||
|
||||
public Chunk Chunk => GetParent<Chunk>(); |
||||
public int NonDefaultCount { get; protected set; } = 0; |
||||
public bool IsDefault => NonDefaultCount == 0; |
||||
public static class ChunkLayerRegistry |
||||
{ |
||||
private static readonly Dictionary<Type, Func<IChunkLayer>> _factories |
||||
= new Dictionary<Type, Func<IChunkLayer>>(); |
||||
private static readonly Dictionary<Type, object> _defaults |
||||
= new Dictionary<Type, object>(); |
||||
|
||||
public T this[BlockPos pos] { |
||||
get => this[pos.X, pos.Y]; |
||||
set => this[pos.X, pos.Y] = value; |
||||
} |
||||
public T this[int x, int y] { |
||||
get { |
||||
EnsureWithin(x, y); |
||||
return Data[x | y << Chunk.BIT_SHIFT]; |
||||
} |
||||
set { |
||||
EnsureWithin(x, y); |
||||
var index = x | y << Chunk.BIT_SHIFT; |
||||
var previous = Data[index]; |
||||
if (COMPARER.Equals(value, previous)) return; |
||||
if (!COMPARER.Equals(previous, default)) { |
||||
if (previous is Node node) RemoveChild(node); |
||||
NonDefaultCount--; |
||||
} |
||||
if (!COMPARER.Equals(value, default)) { |
||||
if (value is Node node) AddChild(node); |
||||
NonDefaultCount++; |
||||
} |
||||
Data[index] = value; |
||||
Dirty = true; |
||||
static ChunkLayerRegistry() |
||||
{ |
||||
foreach (var attr in typeof(IChunkLayer).GetCustomAttributes<UnionAttribute>()) { |
||||
var id = (byte)attr.Key; |
||||
var type = attr.SubType; |
||||
var ctor = type.GetConstructor(Type.EmptyTypes); |
||||
var fact = Expression.Lambda<Func<IChunkLayer>>(Expression.New(ctor)); |
||||
var storedType = type.GetInterfaces() |
||||
.Single(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IChunkLayer<>))) |
||||
.GenericTypeArguments[0]; |
||||
_factories.Add(storedType, fact.Compile()); |
||||
_defaults.Add(storedType, type.GetField("DEFAULT").GetValue(null)); |
||||
} |
||||
} |
||||
|
||||
private static void EnsureWithin(int x, int y) |
||||
public static bool Has<T>() |
||||
=> _factories.ContainsKey(typeof(T)); |
||||
|
||||
public static bool TryGetDefault<T>(out T @default) |
||||
{ |
||||
if ((x < 0) || (x >= Chunk.LENGTH) || (y < 0) || (y >= Chunk.LENGTH)) throw new ArgumentException( |
||||
$"x and y ({x},{y}) must be within chunk boundaries - (0,0) inclusive to ({Chunk.LENGTH},{Chunk.LENGTH}) exclusive"); |
||||
if (_defaults.TryGetValue(typeof(T), out var defaultObj)) |
||||
{ @default = (T)defaultObj; return true; } |
||||
else { @default = default; return false; } |
||||
} |
||||
|
||||
public abstract void Read(BinaryReader reader); |
||||
public abstract void Write(BinaryWriter writer); |
||||
public static IChunkLayer<T> Create<T>() |
||||
=> (IChunkLayer<T>)Create(typeof(T)); |
||||
public static IChunkLayer Create(Type type) |
||||
=> _factories[type](); |
||||
|
||||
|
||||
// static ChunkLayerRegistry() |
||||
// { |
||||
// Register(0, Blocks.AIR, () => new BlockLayer()); |
||||
// Register(1, Colors.White, () => new ColorLayer()); |
||||
// } |
||||
|
||||
// public static void Register<T>(byte id, T @default, Func<IChunkLayer<T>> factory) |
||||
// { |
||||
// var info = new Info<T>(id, @default, factory); |
||||
// _byType.Add(typeof(T), info); |
||||
// _byID.Add(id, info); |
||||
// } |
||||
|
||||
// public interface IInfo |
||||
// { |
||||
// Type Type { get; } |
||||
// byte ID { get; } |
||||
// object Default { get; } |
||||
// Func<IChunkLayer> Factory { get; } |
||||
// } |
||||
|
||||
// public class Info<T> : IInfo |
||||
// { |
||||
// public byte ID { get; } |
||||
// public T Default { get; } |
||||
// public Func<IChunkLayer<T>> Factory { get; } |
||||
|
||||
// Type IInfo.Type => typeof(T); |
||||
// object IInfo.Default => Default; |
||||
// Func<IChunkLayer> IInfo.Factory => Factory; |
||||
|
||||
// public Info(byte id, T @default, Func<IChunkLayer<T>> factory) |
||||
// { ID = id; Default = @default; Factory = factory; } |
||||
// } |
||||
|
||||
// public static bool TryGet<T>(out Info<T> info) |
||||
// { |
||||
// if (TryGet(typeof(T), out var infoObj)) |
||||
// { info = (Info<T>)infoObj; return true; } |
||||
// else { info = null; return false; } |
||||
// } |
||||
// public static bool TryGet(Type type, out IInfo info) |
||||
// => _byType.TryGetValue(type, out info); |
||||
// public static bool TryGet(byte id, out IInfo info) |
||||
// => _byID.TryGetValue(id, out info); |
||||
} |
||||
|
@ -1,136 +0,0 @@ |
||||
using System; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Collections.Generic; |
||||
using Godot; |
||||
using File = System.IO.File; |
||||
|
||||
public class WorldSave |
||||
{ |
||||
public const string FILE_EXT = ".yf5"; |
||||
public const int MAGIC_NUMBER = 0x59463573; // "YF5s" |
||||
public const int LATEST_VERSION = 1; |
||||
|
||||
public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/"; |
||||
|
||||
|
||||
public int Version { get; private set; } = LATEST_VERSION; |
||||
public TimeSpan Playtime { get; set; } = TimeSpan.Zero; |
||||
public DateTime LastSaved { get; private set; } |
||||
|
||||
public string Generator { get; private set; } |
||||
public int Seed { get; private set; } |
||||
|
||||
public Dictionary<(int X, int Y), Dictionary<string, byte[]>> ChunkData { get; private set; } |
||||
|
||||
|
||||
public static WorldSave ReadFromFile(string path) |
||||
{ |
||||
var save = new WorldSave { 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 better support multiple versions, improve saving/loading. |
||||
save.Version = reader.ReadUInt16(); |
||||
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32()); |
||||
|
||||
if (save.Version == 0) { |
||||
save.Seed = unchecked((int)GD.Randi()); |
||||
save.Generator = "Void"; |
||||
|
||||
var tempBlockLayers = new Dictionary<(int X, int Y), BlockLayer>(); |
||||
var numBlocks = reader.ReadInt32(); |
||||
for (var i = 0; i < numBlocks; i++) { |
||||
var blockPos = new BlockPos(reader.ReadInt32(), reader.ReadInt32()); |
||||
var rawColor = reader.ReadInt32(); |
||||
var unbreakable = reader.ReadBoolean(); // TODO |
||||
|
||||
var chunkPos = blockPos.ToChunkPos(); |
||||
if (!tempBlockLayers.TryGetValue(chunkPos, out var blocks)) |
||||
tempBlockLayers.Add(chunkPos, blocks = new BlockLayer()); |
||||
blocks[blockPos.GlobalToChunkRel()] = new BlockData(Block.DEFAULT, rawColor); |
||||
} |
||||
save.ChunkData = tempBlockLayers.ToDictionary(kvp => kvp.Key, |
||||
kvp => new Dictionary<string, byte[]> { [nameof(BlockLayer)] = kvp.Value.ToBytes() }); |
||||
} else if (save.Version == 1) { |
||||
save.Generator = reader.ReadString(); |
||||
save.Seed = reader.ReadInt32(); |
||||
|
||||
var numChunks = reader.ReadInt32(); |
||||
save.ChunkData = new Dictionary<(int X, int Y), Dictionary<string, byte[]>>(); |
||||
for (var i = 0; i < numChunks; i++) { |
||||
var chunkPos = (reader.ReadInt32(), reader.ReadInt32()); |
||||
var chunk = new Dictionary<string, byte[]>(); |
||||
save.ChunkData.Add(chunkPos, chunk); |
||||
|
||||
var numLayers = reader.ReadByte(); |
||||
for (var j = 0; j < numLayers; j++) { |
||||
var name = reader.ReadString(); |
||||
var count = reader.ReadInt32(); |
||||
var data = reader.ReadBytes(count); |
||||
chunk.Add(name, data); |
||||
} |
||||
} |
||||
} else throw new IOException($"Version {save.Version} not supported (latest version: {LATEST_VERSION})"); |
||||
} |
||||
} |
||||
return save; |
||||
} |
||||
|
||||
public void WriteToFile(string path) |
||||
{ |
||||
using (var stream = File.OpenWrite(path + ".tmp")) { |
||||
using (var writer = new BinaryWriter(stream)) { |
||||
writer.Write(MAGIC_NUMBER); |
||||
writer.Write((ushort)LATEST_VERSION); |
||||
writer.Write((uint)Playtime.TotalSeconds); |
||||
|
||||
writer.Write(Generator); |
||||
writer.Write(Seed); |
||||
|
||||
writer.Write(ChunkData.Count); |
||||
foreach (var ((chunkX, chunkY), layers) in ChunkData) { |
||||
writer.Write(chunkX); |
||||
writer.Write(chunkY); |
||||
writer.Write((byte)layers.Count); |
||||
foreach (var (name, data) in layers) { |
||||
writer.Write(name); |
||||
writer.Write(data.Length); |
||||
writer.Write(data); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
new Godot.Directory().Rename(path + ".tmp", path); |
||||
LastSaved = File.GetLastWriteTime(path); |
||||
} |
||||
|
||||
|
||||
public void WriteDataFromWorld(World world) |
||||
{ |
||||
Generator = world.Generator.Name; |
||||
Seed = world.Seed; |
||||
|
||||
ChunkData = world.Chunks.ToDictionary( |
||||
chunk => chunk.ChunkPosition, |
||||
chunk => chunk.Layers |
||||
.Where(layer => !layer.IsDefault) |
||||
.ToDictionary(layer => layer.GetType().Name, layer => layer.ToBytes())); |
||||
} |
||||
|
||||
public void ReadDataIntoWorld(World world) |
||||
{ |
||||
world.Generator = WorldGeneratorRegistry.GetOrNull(Generator); |
||||
world.Seed = Seed; |
||||
|
||||
RPC.Reliable(world.ClearChunks); |
||||
foreach (var (chunkPos, layers) in ChunkData) { |
||||
var chunk = world.GetOrCreateChunk(chunkPos); |
||||
foreach (var (name, data) in layers) |
||||
chunk.GetOrCreateLayer(name).FromBytes(data); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue