Redo de/serialization, use MessagePack

main
copygirl 4 years ago
parent 310cbe5221
commit b7e7632c00
  1. 5
      YourFortV.csproj
  2. 13
      decal_material.tres
  3. 1
      project.godot
  4. 38
      src/EscapeMenuWorld.cs
  5. 27
      src/IO/ChunkFormatter.cs
  6. 52
      src/IO/DeSerializableWorld.cs
  7. 47
      src/IO/IDeSerializable.cs
  8. 20
      src/Items/CreativeBuilding.cs
  9. 1
      src/Items/Items.cs
  10. 20
      src/Objects/HitDecal.cs
  11. 5
      src/Scenes/Game.cs
  12. 10
      src/Utility/Extensions.cs
  13. 46
      src/World/Block/Block.cs
  14. 6
      src/World/Block/BlockEntity.cs
  15. 50
      src/World/Block/BlockRef.cs
  16. 83
      src/World/Chunk/BlockLayer.cs
  17. 61
      src/World/Chunk/Chunk.Rendering.cs
  18. 64
      src/World/Chunk/Chunk.cs
  19. 219
      src/World/Chunk/ChunkLayer.cs
  20. 27
      src/World/Generation/GeneratorSimple.cs
  21. 22
      src/World/Generation/GeneratorVoid.cs
  22. 126
      src/World/World.cs
  23. 136
      src/World/WorldSave.cs

@ -2,4 +2,9 @@
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="2.3.75" />
<PackageReference Include="Nerdbank.Streams" Version="2.8.46" />
</ItemGroup>
</Project>

@ -1,7 +1,4 @@
[gd_scene load_steps=17 format=2]
[ext_resource path="res://gfx/hit_decal.png" type="Texture" id=1]
[ext_resource path="res://src/Objects/HitDecal.cs" type="Script" id=2]
[gd_resource type="ShaderMaterial" load_steps=14 format=2]
[sub_resource type="VisualShaderNodeInput" id=1]
input_name = "modulate_color"
@ -170,12 +167,6 @@ nodes/fragment/18/node = SubResource( 7 )
nodes/fragment/18/position = Vector2( 640, -180 )
nodes/fragment/connections = PoolIntArray( 3, 0, 2, 2, 2, 1, 6, 0, 6, 0, 14, 1, 13, 0, 14, 0, 14, 0, 0, 1, 7, 0, 15, 0, 16, 0, 15, 1, 15, 0, 8, 0, 15, 1, 17, 0, 8, 1, 17, 1, 17, 0, 6, 1, 10, 0, 18, 0, 2, 0, 18, 1, 18, 0, 0, 0 )
[sub_resource type="ShaderMaterial" id=14]
[resource]
shader = SubResource( 13 )
shader_param/offset = Vector3( 0, 0, 0 )
[node name="HitDecal" type="Sprite"]
material = SubResource( 14 )
texture = ExtResource( 1 )
centered = false
script = ExtResource( 2 )

@ -31,6 +31,7 @@ window/stretch/mode="viewport"
[global]
layer=false
exce=false
[importer_defaults]

@ -21,7 +21,6 @@ public class EscapeMenuWorld : CenterContainer
public FileDialog SaveFileDialog { get; private set; }
public FileDialog LoadFileDialog { get; private set; }
private TimeSpan _playtime;
private string _currentWorld;
public override void _Ready()
@ -39,23 +38,26 @@ public class EscapeMenuWorld : CenterContainer
SaveAsButton.Text = "Save World As...";
SaveFileDialog.GetOk().Text = "Save";
new Directory().MakeDirRecursive(WorldSave.WORLDS_DIR);
SaveFileDialog.CurrentPath = WorldSave.WORLDS_DIR;
LoadFileDialog.CurrentPath = WorldSave.WORLDS_DIR;
new Directory().MakeDirRecursive(World.WORLDS_DIR);
SaveFileDialog.CurrentPath = World.WORLDS_DIR;
LoadFileDialog.CurrentPath = World.WORLDS_DIR;
this.GetClient().StatusChanged += OnStatusChanged;
}
public override void _Process(float delta)
{
if (!GetTree().Paused || (this.GetWorld().PauseMode != PauseModeEnum.Stop))
_playtime += TimeSpan.FromSeconds(delta);
// TODO: Probably move this to World class.
var world = this.GetWorld();
if (!GetTree().Paused || (world.PauseMode != PauseModeEnum.Stop))
world.Playtime += TimeSpan.FromSeconds(delta);
var b = new StringBuilder();
if (_playtime.Days > 0) b.Append(_playtime.Days).Append("d ");
if (_playtime.Hours > 0) b.Append(_playtime.Hours).Append("h ");
if (_playtime.Minutes < 10) b.Append('0'); b.Append(_playtime.Minutes).Append("m ");
if (_playtime.Seconds < 10) b.Append('0'); b.Append(_playtime.Seconds).Append("s");
var p = world.Playtime;
if (p.Days > 0) b.Append(p.Days).Append("d ");
if (p.Hours > 0) b.Append(p.Hours).Append("h ");
if (p.Minutes < 10) b.Append('0'); b.Append(p.Minutes).Append("m ");
if (p.Seconds < 10) b.Append('0'); b.Append(p.Seconds).Append("s");
PlaytimeLabel.Text = b.ToString();
}
@ -81,13 +83,12 @@ public class EscapeMenuWorld : CenterContainer
private void _on_SaveFileDialog_file_selected(string path)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = new WorldSave { Playtime = _playtime };
save.WriteDataFromWorld(server.GetWorld());
save.WriteToFile(path);
var world = server.GetWorld();
world.Save(path);
_currentWorld = path;
FilenameLabel.Text = System.IO.Path.GetFileName(path);
LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
LastSavedLabel.Text = world.LastSaved.ToString("yyyy-MM-dd HH:mm");
QuickSaveButton.Visible = true;
SaveAsButton.Text = "Save As...";
}
@ -101,9 +102,9 @@ public class EscapeMenuWorld : CenterContainer
private void _on_LoadFileDialog_file_selected(string path)
{
var server = this.GetClient().GetNode<IntegratedServer>(nameof(IntegratedServer)).Server;
var save = WorldSave.ReadFromFile(path);
var world = server.GetWorld();
foreach (var player in server.GetWorld().Players) {
foreach (var player in world.Players) {
// Reset players' positions.
// Can't use RPC helper method here since player is not a LocalPlayer here.
player.RsetId(player.NetworkID, "position", Vector2.Zero);
@ -112,12 +113,11 @@ public class EscapeMenuWorld : CenterContainer
player.VisibilityTracker.Reset();
}
save.ReadDataIntoWorld(server.GetWorld());
_playtime = save.Playtime;
world.Load(path);
_currentWorld = path;
FilenameLabel.Text = System.IO.Path.GetFileName(path);
LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm");
LastSavedLabel.Text = world.LastSaved.ToString("yyyy-MM-dd HH:mm");
QuickSaveButton.Visible = true;
SaveAsButton.Text = "Save As...";
}

@ -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);
}
}

@ -78,7 +78,7 @@ public class CreativeBuilding : Node2D
}
var world = this.GetWorld();
bool IsBlockAt(BlockPos pos) => world.GetBlockDataAt(pos).Block != null;
bool IsBlockAt(BlockPos pos) => world[pos].Get<Block>() != Blocks.AIR;
_canBuild = !IsBlockAt(_startPos) && Facings.All.Any(pos => IsBlockAt(_startPos + pos.ToBlockPos()));
Update(); // Make sure _Draw is being called.
@ -94,10 +94,10 @@ public class CreativeBuilding : Node2D
var world = this.GetWorld();
foreach (var pos in GetBlockPositions(_startPos, _direction, _length)) {
var hasBlock = world.GetBlockDataAt(pos).Block != null;
var isReplacable = world[pos].Get<Block>().IsReplacable;
var color = (_currentMode != BuildMode.Breaking)
? ((_canBuild && !hasBlock) ? green : red)
: (hasBlock ? black : red);
? ((_canBuild && isReplacable) ? green : red)
: (!isReplacable ? black : red);
DrawTexture(_blockTex, ToLocal(pos.ToVector() - _blockTex.GetSize() / 2), color);
}
}
@ -122,10 +122,10 @@ public class CreativeBuilding : Node2D
var start = new BlockPos(x, y);
var world = this.GetWorld();
foreach (var pos in GetBlockPositions(start, direction, length)) {
if (world.GetBlockDataAt(pos).Block != null) continue;
if (!world[pos].Get<Block>().IsReplacable) continue;
var color = Player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F));
RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
world.SetBlockData, pos.X, pos.Y, color.ToRgba32());
world.SetBlock, pos.X, pos.Y, color.ToRgba32());
}
}
@ -141,11 +141,9 @@ public class CreativeBuilding : Node2D
var start = new BlockPos(x, y);
var world = this.GetWorld();
foreach (var pos in GetBlockPositions(start, direction, length)) {
var data = world.GetBlockDataAt(pos);
if (data.Block == null) continue;
foreach (var pos in GetBlockPositions(start, direction, length))
if (world[pos].Get<Block>() != Blocks.AIR)
RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
world.SetBlockData, pos.X, pos.Y, 0);
}
world.SetBlock, pos.X, pos.Y, 0);
}
}

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Godot;
// TODO: Add ways to add/move/remove items, including event when changed.

@ -5,24 +5,24 @@ public class HitDecal : Sprite
{
private static readonly TimeSpan LIFE_TIME = TimeSpan.FromSeconds(5.0);
private static readonly TimeSpan FADE_TIME = TimeSpan.FromSeconds(5.0);
private static readonly Material MATERIAL = GD.Load<Material>("res://decal_material.tres");
private readonly float _fadeFactor;
private TimeSpan _age = TimeSpan.Zero;
private float _fadeFactor;
public void Add(Sprite sprite, Vector2 hitVec, Color color)
public HitDecal(Texture own, Texture target, Vector2 hitVec, Color color)
{
Position = (hitVec - Texture.GetSize() / 2).Round();
var offset = Position + sprite.Texture.GetSize() / 2;
Texture = own;
Material = (Material)MATERIAL.Duplicate();
Position = (hitVec - own.GetSize() / 2).Round();
Centered = false;
ShaderMaterial material;
Material = material = (ShaderMaterial)Material.Duplicate();
material.SetShaderParam("offset", new Vector3(offset.x, offset.y, 0));
material.SetShaderParam("mask", sprite.Texture);
var offset = Position + target.GetSize() / 2;
((ShaderMaterial)Material).SetShaderParam("offset", new Vector3(offset.x, offset.y, 0));
((ShaderMaterial)Material).SetShaderParam("mask", target);
Modulate = color;
_fadeFactor = color.a;
sprite.AddChild(this);
}
public override void _Process(float delta)

@ -1,7 +1,12 @@
using Godot;
using MessagePack;
using MessagePack.Resolvers;
public abstract class Game : Node
{
static Game()
=> MessagePackSerializer.DefaultOptions = StandardResolverAllowPrivate.Options;
// Using _EnterTree to make sure this code runs before any other.
public override void _EnterTree()
=> GD.Randomize();

@ -27,6 +27,16 @@ public static class Extensions
}
return child;
}
public static void AddRange(this Node parent, IEnumerable<Node> children)
{ foreach (var child in children) parent.AddChild(child); }
public static void ClearChildren(this Node parent)
{
for (var i = parent.GetChildCount() - 1; i >= 0; i--) {
var child = parent.GetChild(i);
parent.RemoveChild(child);
child.QueueFree();
}
}
public static void RemoveFromParent(this Node node)
{
node.GetParent().RemoveChild(node);

@ -1,3 +1,4 @@
using System;
using Godot;
public class Block
@ -5,15 +6,46 @@ public class Block
public const int LENGTH = 16;
public const int BIT_SHIFT = 4;
public int ID { get; internal set; } = -1;
public string Name { get; }
public static readonly Block DEFAULT = new Block(
GD.Load<Texture>("res://gfx/block.png"),
new RectangleShape2D { Extents = new Vector2(0.5F, 0.5F) * LENGTH });
public Texture Texture { get; set; } = null;
public Shape2D Shape { get; set; } = null;
public bool IsReplacable { get; set; } = false;
public Block(string name) => Name = name;
public override string ToString() => $"Block(\"{Name}\")";
}
public static class Blocks
{
public static readonly Block AIR = BlockRegistry.Register(0, new Block("air"){ IsReplacable = true });
public static readonly Block DEFAULT = BlockRegistry.Register(1, new Block("default"){
Texture = GD.Load<Texture>("res://gfx/block.png"),
Shape = new RectangleShape2D { Extents = new Vector2(0.5F, 0.5F) * Block.LENGTH },
});
}
public static class BlockRegistry
{
public const int MAX_BLOCK_ID = 255;
public Texture Texture { get; }
public Shape2D Shape { get; }
private static readonly Block[] _blocks = new Block[MAX_BLOCK_ID + 1];
public Block(Texture texture, Shape2D shape)
{ Texture = texture; Shape = shape; }
public static T Register<T>(int id, T block) where T : Block
{
if ((id < 0) || (id > MAX_BLOCK_ID)) throw new ArgumentOutOfRangeException(nameof(id));
if (_blocks[id] != null) throw new ArgumentException($"ID {id} is already in use by {_blocks[id]}", nameof(id));
if (block.ID != -1) throw new ArgumentException($"Block {block} has already been registered", nameof(block));
_blocks[id] = block;
block.ID = id;
return block;
}
public static Block Get(int id)
{
if ((id < 0) || (id > MAX_BLOCK_ID)) throw new ArgumentOutOfRangeException(nameof(id));
return _blocks[id];
}
}

@ -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 abstract class BasicChunkLayer<T> : Node2D, IChunkLayer<T>
public class TranslationLayer<TData, TAccess> : IChunkLayer<TAccess>
{
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default;
private readonly ArrayChunkLayer<TData> _data = new ArrayChunkLayer<TData>();
private readonly Func<TData, TAccess> _from;
private readonly Func<TAccess, TData> _to;
protected T[] Data { get; } = new T[Chunk.LENGTH * Chunk.LENGTH];
protected bool Dirty { get; set; } = true;
public TranslationLayer(Func<TData, TAccess> from, Func<TAccess, TData> to)
{ _from = from; _to = to; }
public Chunk Chunk => GetParent<Chunk>();
public int NonDefaultCount { get; protected set; } = 0;
public bool IsDefault => NonDefaultCount == 0;
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 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++;
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);
}
Data[index] = value;
Dirty = true;
[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) { }
}
[MessagePackFormatter(typeof(DeSerializableFormatter<ColorLayer>))]
public class ColorLayer : TranslationLayer<int, Color>
{
public static readonly Color DEFAULT = Colors.White;
public ColorLayer() : base(i => new Color(i), c => c.ToRgba32()) { }
}
private static void EnsureWithin(int x, int y)
public static class ChunkLayerRegistry
{
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");
private static readonly Dictionary<Type, Func<IChunkLayer>> _factories
= new Dictionary<Type, Func<IChunkLayer>>();
private static readonly Dictionary<Type, object> _defaults
= new Dictionary<Type, object>();
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));
}
}
public abstract void Read(BinaryReader reader);
public abstract void Write(BinaryWriter writer);
public static bool Has<T>()
=> _factories.ContainsKey(typeof(T));
public static bool TryGetDefault<T>(out T @default)
{
if (_defaults.TryGetValue(typeof(T), out var defaultObj))
{ @default = (T)defaultObj; return true; }
else { @default = default; return false; }
}
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);
}

@ -11,10 +11,11 @@ public class GeneratorSimple : IWorldGenerator
public void Generate(Chunk chunk)
{
if (chunk.ChunkPosition.Y != 0) return;
if (chunk.ChunkPos.Y != 0) return;
var rnd = new Random(_seed ^ chunk.ChunkPosition.GetHashCode());
var blocks = chunk.GetOrCreateLayer<BlockLayer>();
var rnd = new Random(_seed ^ chunk.ChunkPos.GetHashCode());
var blocks = chunk.GetLayer<Block>(true);
var colors = chunk.GetLayer<Color>(true);
for (var x = 0; x < Chunk.LENGTH; x++) {
var grassDepth = rnd.Next(1, 4);
@ -24,18 +25,22 @@ public class GeneratorSimple : IWorldGenerator
var color = (y < grassDepth) ? Colors.LawnGreen
: (y < dirtDepth) ? Colors.SaddleBrown
: Colors.Gray;
color = color.Lightened(GD.Randf() * 0.15F);
blocks[x, 3 + y] = new BlockData(Block.DEFAULT, color);
blocks[x, 3 + y] = Blocks.DEFAULT;
colors[x, 3 + y] = color.Lightened(GD.Randf() * 0.15F);
}
}
// TODO: Make it easier to create "structures" that cover multiple chunks.
// TODO: These are supposed to be unbreakable.
if (chunk.ChunkPosition == (-1, 0))
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
else if (chunk.ChunkPosition == (0, 0))
for (var x = 0; x <= 6; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
if (chunk.ChunkPos == (-1, 0))
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++) {
blocks[x, 3] = Blocks.DEFAULT;
colors[x, 3] = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
}
else if (chunk.ChunkPos == (0, 0))
for (var x = 0; x <= 6; x++) {
blocks[x, 3] = Blocks.DEFAULT;
colors[x, 3] = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
}
}
}

@ -10,14 +10,20 @@ public class GeneratorVoid : IWorldGenerator
{
// TODO: Make it easier to create "structures" that cover multiple chunks.
// TODO: These are supposed to be unbreakable.
if (chunk.ChunkPosition == (-1, 0)) {
var blocks = chunk.GetOrCreateLayer<BlockLayer>();
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
} else if (chunk.ChunkPosition == (0, 0)) {
var blocks = chunk.GetOrCreateLayer<BlockLayer>();
for (var x = 0; x <= 6; x++)
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F));
if (chunk.ChunkPos == (-1, 0)) {
var blocks = chunk.GetLayer<Block>(true);
var colors = chunk.GetLayer<Color>(false);
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++) {
blocks[x, 3] = Blocks.DEFAULT;
colors[x, 3] = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
}
} else if (chunk.ChunkPos == (0, 0)) {
var blocks = chunk.GetLayer<Block>(true);
var colors = chunk.GetLayer<Color>(false);
for (var x = 0; x <= 6; x++) {
blocks[x, 3] = Blocks.DEFAULT;
colors[x, 3] = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
}
}
}
}

@ -1,15 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Godot;
using MessagePack;
using File = System.IO.File;
public class World : Node
public partial class World : Node
{
public const string FILE_EXT = ".yf5";
public const string MAGIC_NUMBER = "YF5s"; // 0x59463573
public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/";
internal Node PlayerContainer { get; }
internal Node ChunkContainer { get; }
public IWorldGenerator Generator { get; set; } = WorldGeneratorRegistry.GetOrNull("Simple");
public DateTime LastSaved { get; set; }
public TimeSpan Playtime { get; set; } = TimeSpan.Zero;
public int Seed { get; set; } = unchecked((int)GD.Randi());
public IWorldGenerator Generator { get; set; } = WorldGeneratorRegistry.GetOrNull("Simple");
public BlockRef this[BlockPos pos] => new BlockRef(this, pos);
public BlockRef this[int x, int y] => new BlockRef(this, new BlockPos(x, y));
public World()
{
@ -26,64 +40,32 @@ public class World : Node
public IEnumerable<Chunk> Chunks
=> ChunkContainer.GetChildren<Chunk>();
public Chunk GetChunkOrNull((int X, int Y) chunkPos)
=> ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos})");
public Chunk GetOrCreateChunk((int X, int Y) chunkPos, bool generate = false)
=> ChunkContainer.GetOrCreateChild($"Chunk ({chunkPos})", () => {
public Chunk GetChunk((int X, int Y) chunkPos, bool create) => !create
? ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos})")
: ChunkContainer.GetOrCreateChild($"Chunk ({chunkPos})", () => {
var chunk = new Chunk(chunkPos);
if (generate) Generator.Generate(chunk);
if (this.GetGame() is Server)
Generator.Generate(chunk);
return chunk;
});
[PuppetSync] public void ClearChunks()
{ foreach (var chunk in Chunks) chunk.RemoveFromParent(); }
public BlockData GetBlockDataAt(BlockPos position)
=> GetChunkOrNull(position.ToChunkPos())
?.GetLayerOrNull<BlockLayer>()?[position.GlobalToChunkRel()] ?? default;
[PuppetSync]
public void SetBlockData(int x, int y, int color)
public void SetBlock(int x, int y, int color)
{
var blockPos = new BlockPos(x, y);
GetOrCreateChunk(blockPos.ToChunkPos())
.GetOrCreateLayer<BlockLayer>()[blockPos.GlobalToChunkRel()]
= (color != 0) ? new BlockData(Block.DEFAULT, color) : default;
var block = this[x, y];
block.Set((color != 0) ? Blocks.DEFAULT : Blocks.AIR);
block.Set(new Color(color));
}
[PuppetSync]
public void SpawnChunk(int chunkX, int chunkY, byte[] data)
{
var chunk = GetOrCreateChunk((chunkX, chunkY));
using (var stream = new MemoryStream(data)) {
using (var reader = new BinaryReader(stream)) {
var numLayers = reader.ReadByte();
for (var i = 0; i < numLayers; i++) {
var name = reader.ReadString();
var layer = chunk.GetOrCreateLayer(name);
layer.Read(reader);
}
}
}
}
private static byte[] ChunkToBytes(Chunk chunk)
public void SpawnChunk(byte[] data)
{
using (var stream = new MemoryStream()) {
using (var writer = new BinaryWriter(stream)) {
var layers = chunk.GetChildren()
.OfType<IChunkLayer>()
.Where(layer => !layer.IsDefault)
.ToArray();
writer.Write((byte)layers.Length);
foreach (var layer in layers) {
writer.Write(layer.GetType().Name);
layer.Write(writer);
}
}
return stream.ToArray();
}
var chunk = MessagePackSerializer.Deserialize<Chunk>(data);
ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunk.ChunkPos})")?.RemoveFromParent();
ChunkContainer.AddChild(chunk);
}
[PuppetSync]
@ -102,13 +84,12 @@ public class World : Node
}
if (this.GetGame() is Server) {
player.VisibilityTracker.ChunkTracked += (chunkPos) => {
var chunk = GetOrCreateChunk(chunkPos, generate: true);
player.VisibilityTracker.ChunkTracked += (chunkPos) =>
RPC.Reliable(player.NetworkID, SpawnChunk,
chunk.ChunkPosition.X, chunk.ChunkPosition.Y, ChunkToBytes(chunk));
};
MessagePackSerializer.Serialize(GetChunk(chunkPos, true)));
player.VisibilityTracker.ChunkUntracked += (chunkPos) => {
var chunk = GetChunkOrNull(chunkPos);
var chunk = GetChunk(chunkPos, false);
if (chunk == null) return;
RPC.Reliable(player.NetworkID, Despawn, GetPathTo(chunk), false);
};
@ -118,9 +99,10 @@ public class World : Node
[Puppet]
public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color)
{
var hit = SceneCache<HitDecal>.Instance();
var texture = GD.Load<Texture>("res://gfx/hit_decal.png");
var sprite = this.GetWorld().GetNode<Sprite>(spritePath);
hit.Add(sprite, hitPosition, color);
var hit = new HitDecal(texture, sprite.Texture, hitPosition, color);
sprite.AddChild(hit);
}
[PuppetSync]
@ -131,4 +113,38 @@ public class World : Node
node.GetParent().RemoveChild(node);
node.QueueFree();
}
public void Save(string path)
{
using (var stream = File.OpenWrite(path + ".tmp")) {
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) {
writer.Write(MAGIC_NUMBER.ToCharArray());
// TODO: Eventually, write only "header", not chunks.
// Chunks should be stored seperately, in regions or so.
var bytes = this.SerializeToBytes();
writer.Write(bytes.Length);
writer.Write(bytes);
}
}
new Godot.Directory().Rename(path + ".tmp", path);
LastSaved = File.GetLastWriteTime(path);
}
public void Load(string path)
{
using (var stream = File.OpenRead(path)) {
using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) {
var magic = new string(reader.ReadChars(MAGIC_NUMBER.Length));
if (magic != MAGIC_NUMBER) throw new IOException(
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})");
var numBytes = reader.ReadInt32();
var bytes = reader.ReadBytes(numBytes);
this.Deserialize(bytes);
}
}
LastSaved = File.GetLastAccessTime(path);
}
}

@ -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…
Cancel
Save