diff --git a/YourFortV.csproj b/YourFortV.csproj
index db88a8d..3b9b28f 100644
--- a/YourFortV.csproj
+++ b/YourFortV.csproj
@@ -2,4 +2,9 @@
net472
-
\ No newline at end of file
+
+
+
+
+
+
diff --git a/scene/HitDecal.tscn b/decal_material.tres
similarity index 92%
rename from scene/HitDecal.tscn
rename to decal_material.tres
index 02a91c3..f127cdc 100644
--- a/scene/HitDecal.tscn
+++ b/decal_material.tres
@@ -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 )
diff --git a/project.godot b/project.godot
index 00b7bd7..718acf8 100644
--- a/project.godot
+++ b/project.godot
@@ -31,6 +31,7 @@ window/stretch/mode="viewport"
[global]
layer=false
+exce=false
[importer_defaults]
diff --git a/src/EscapeMenuWorld.cs b/src/EscapeMenuWorld.cs
index efbf5de..0888a44 100644
--- a/src/EscapeMenuWorld.cs
+++ b/src/EscapeMenuWorld.cs
@@ -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(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(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...";
}
diff --git a/src/IO/ChunkFormatter.cs b/src/IO/ChunkFormatter.cs
new file mode 100644
index 0000000..0881bed
--- /dev/null
+++ b/src/IO/ChunkFormatter.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Linq;
+using MessagePack;
+using MessagePack.Formatters;
+
+public class ChunkFormatter : IMessagePackFormatter
+{
+ 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(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());
+ }
+}
diff --git a/src/IO/DeSerializableWorld.cs b/src/IO/DeSerializableWorld.cs
new file mode 100644
index 0000000..524dfbe
--- /dev/null
+++ b/src/IO/DeSerializableWorld.cs
@@ -0,0 +1,52 @@
+using System;
+using MessagePack;
+
+// [MessagePackFormatter(typeof(DeSerializableFormatter))]
+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(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);
+ }
+}
diff --git a/src/IO/IDeSerializable.cs b/src/IO/IDeSerializable.cs
new file mode 100644
index 0000000..46cbdb5
--- /dev/null
+++ b/src/IO/IDeSerializable.cs
@@ -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();
+ var writer = new MessagePackWriter(sequence);
+ value.Serialize(ref writer, options);
+ writer.Flush();
+ return sequence.AsReadOnlySequence.ToArray();
+ }
+}
+
+public class DeSerializableFormatter : IMessagePackFormatter
+ 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);
+ }
+}
diff --git a/src/Items/CreativeBuilding.cs b/src/Items/CreativeBuilding.cs
index 033d512..6665168 100644
--- a/src/Items/CreativeBuilding.cs
+++ b/src/Items/CreativeBuilding.cs
@@ -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() != 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 color = (_currentMode != BuildMode.Breaking)
- ? ((_canBuild && !hasBlock) ? green : red)
- : (hasBlock ? black : red);
+ var isReplacable = world[pos].Get().IsReplacable;
+ var color = (_currentMode != BuildMode.Breaking)
+ ? ((_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().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;
- RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
- world.SetBlockData, pos.X, pos.Y, 0);
- }
+ foreach (var pos in GetBlockPositions(start, direction, length))
+ if (world[pos].Get() != Blocks.AIR)
+ RPC.Reliable(world.GetPlayersTracking(pos.ToChunkPos(), true),
+ world.SetBlock, pos.X, pos.Y, 0);
}
}
diff --git a/src/Items/Items.cs b/src/Items/Items.cs
index 3a8a9d7..d30fa7c 100644
--- a/src/Items/Items.cs
+++ b/src/Items/Items.cs
@@ -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.
diff --git a/src/Objects/HitDecal.cs b/src/Objects/HitDecal.cs
index 703c71d..00758f7 100644
--- a/src/Objects/HitDecal.cs
+++ b/src/Objects/HitDecal.cs
@@ -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("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)
diff --git a/src/Scenes/Game.cs b/src/Scenes/Game.cs
index b5a2c03..15c6643 100644
--- a/src/Scenes/Game.cs
+++ b/src/Scenes/Game.cs
@@ -1,11 +1,16 @@
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();
public override void _Ready()
- => Multiplayer.RootNode = this.GetWorld();
+ => Multiplayer.RootNode = this.GetWorld();
}
diff --git a/src/Utility/Extensions.cs b/src/Utility/Extensions.cs
index d0fb9a7..424a099 100644
--- a/src/Utility/Extensions.cs
+++ b/src/Utility/Extensions.cs
@@ -27,6 +27,16 @@ public static class Extensions
}
return child;
}
+ public static void AddRange(this Node parent, IEnumerable 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);
diff --git a/src/World/Block/Block.cs b/src/World/Block/Block.cs
index acb22e6..a8426b2 100644
--- a/src/World/Block/Block.cs
+++ b/src/World/Block/Block.cs
@@ -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("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("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;
+
+ private static readonly Block[] _blocks = new Block[MAX_BLOCK_ID + 1];
+
+ public static T Register(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));
- public Texture Texture { get; }
- public Shape2D Shape { get; }
+ _blocks[id] = block;
+ block.ID = id;
+ return block;
+ }
- public Block(Texture texture, Shape2D shape)
- { Texture = texture; Shape = shape; }
+ public static Block Get(int id)
+ {
+ if ((id < 0) || (id > MAX_BLOCK_ID)) throw new ArgumentOutOfRangeException(nameof(id));
+ return _blocks[id];
+ }
}
diff --git a/src/World/Block/BlockEntity.cs b/src/World/Block/BlockEntity.cs
new file mode 100644
index 0000000..4f5c973
--- /dev/null
+++ b/src/World/Block/BlockEntity.cs
@@ -0,0 +1,6 @@
+using Godot;
+
+public class BlockEntity : Node2D
+{
+
+}
diff --git a/src/World/Block/BlockRef.cs b/src/World/Block/BlockRef.cs
new file mode 100644
index 0000000..a93c87f
--- /dev/null
+++ b/src/World/Block/BlockRef.cs
@@ -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 GetChunkLayer(bool create)
+ => GetChunk(create)?.GetLayer(create);
+
+ public BlockEntity GetEntity(bool create)
+ => GetChunk(create)?.GetBlockEntity(Position.GlobalToChunkRel(), create);
+
+
+ public T Get()
+ {
+ if (ChunkLayerRegistry.TryGetDefault(out var @default)) {
+ var layer = GetChunkLayer(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() where T : Node, new()
+ => GetEntity(true).GetOrCreateChild(typeof(T).Name, () => new T());
+
+ public void Set(T value)
+ {
+ if (ChunkLayerRegistry.Has())
+ GetChunkLayer(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));
+ }
+}
diff --git a/src/World/Chunk/BlockLayer.cs b/src/World/Chunk/BlockLayer.cs
deleted file mode 100644
index 58fdf98..0000000
--- a/src/World/Chunk/BlockLayer.cs
+++ /dev/null
@@ -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
-{
- 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("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.
- }
-}
diff --git a/src/World/Chunk/Chunk.Rendering.cs b/src/World/Chunk/Chunk.Rendering.cs
new file mode 100644
index 0000000..5e67bd9
--- /dev/null
+++ b/src/World/Chunk/Chunk.Rendering.cs
@@ -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(false);
+ var colors = GetLayer(false);
+
+ if ((this.GetGame() is Client) && (blocks != null)) {
+ if (_render == null) AddChild(_render = new MeshInstance2D
+ { Texture = GD.Load("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;
+ }
+}
diff --git a/src/World/Chunk/Chunk.cs b/src/World/Chunk/Chunk.cs
index fe0e901..79d2f27 100644
--- a/src/World/Chunk/Chunk.cs
+++ b/src/World/Chunk/Chunk.cs
@@ -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 Layers
- => GetChildren().OfType();
+
+ private readonly List _layers = new List();
+
+ public (int X, int Y) ChunkPos { get; }
+ public IEnumerable 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)GetLayerOrNull(typeof(T).Name);
- public T GetOrCreateLayer() => (T)GetOrCreateLayer(typeof(T).Name);
- public IChunkLayer GetLayerOrNull(string name)
- => GetNodeOrNull(name);
- public IChunkLayer GetOrCreateLayer(string name)
+ public IChunkLayer GetLayer(bool create)
+ => (IChunkLayer)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(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;
}
diff --git a/src/World/Chunk/ChunkLayer.cs b/src/World/Chunk/ChunkLayer.cs
index fc7f49f..7466fac 100644
--- a/src/World/Chunk/ChunkLayer.cs
+++ b/src/World/Chunk/ChunkLayer.cs
@@ -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 Changed;
}
public interface IChunkLayer : 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 : IChunkLayer
{
- private static readonly Dictionary _types = new Dictionary();
+ private static readonly IEqualityComparer COMPARER = EqualityComparer.Default;
- static ChunkLayerRegistry()
- => Register();
+ private T[] _data = new T[Chunk.LENGTH * Chunk.LENGTH];
+ public int NonDefaultCount { get; protected set; } = 0;
- public static void Register()
- 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 Changed;
-public static class ChunkLayerExtensions
-{
- public static void FromBytes(this IChunkLayer layer, byte[] data)
- {
- using (var stream = new MemoryStream(data))
- layer.Read(stream);
+ public T this[BlockPos pos] {
+ get => this[Chunk.GetIndex(pos)];
+ set => this[Chunk.GetIndex(pos)] = value;
}
- public static void Read(this IChunkLayer layer, Stream stream)
- {
- using (var reader = new BinaryReader(stream))
- layer.Read(reader);
+ 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 byte[] ToBytes(this IChunkLayer layer)
- {
- using (var stream = new MemoryStream()) {
- layer.Write(stream);
- return stream.ToArray();
+ if (!COMPARER.Equals(previous, default)) NonDefaultCount--;
+ if (!COMPARER.Equals(value, default)) NonDefaultCount++;
+ Changed?.Invoke(this);
}
}
- public static void Write(this IChunkLayer layer, Stream stream)
+
+ public void Serialize(ref MessagePackWriter writer, MessagePackSerializerOptions options)
+ {
+ writer.Write(NonDefaultCount);
+ MessagePackSerializer.Serialize(ref writer, _data);
+ }
+ public void Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
- using (var writer = new BinaryWriter(stream))
- layer.Write(writer);
+ NonDefaultCount = reader.ReadInt32();
+ _data = MessagePackSerializer.Deserialize(ref reader);
}
}
+public class TranslationLayer : IChunkLayer
+{
+ private readonly ArrayChunkLayer _data = new ArrayChunkLayer();
+ private readonly Func _from;
+ private readonly Func _to;
+
+ public TranslationLayer(Func from, Func to)
+ { _from = from; _to = to; }
+
+ public Type AccessType => typeof(TAccess);
+ public bool IsDefault => _data.IsDefault;
+ public event Action 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);
+}
-public abstract class BasicChunkLayer : Node2D, IChunkLayer
+[MessagePackFormatter(typeof(DeSerializableFormatter))]
+public class BlockLayer : TranslationLayer
{
- private static readonly IEqualityComparer COMPARER = EqualityComparer.Default;
+ public static readonly Block DEFAULT = Blocks.AIR;
+ public BlockLayer() : base(i => BlockRegistry.Get(i), b => (byte)b.ID) { }
+}
- protected T[] Data { get; } = new T[Chunk.LENGTH * Chunk.LENGTH];
- protected bool Dirty { get; set; } = true;
+[MessagePackFormatter(typeof(DeSerializableFormatter))]
+public class ColorLayer : TranslationLayer
+{
+ public static readonly Color DEFAULT = Colors.White;
+ public ColorLayer() : base(i => new Color(i), c => c.ToRgba32()) { }
+}
- public Chunk Chunk => GetParent();
- public int NonDefaultCount { get; protected set; } = 0;
- public bool IsDefault => NonDefaultCount == 0;
- 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;
+public static class ChunkLayerRegistry
+{
+ private static readonly Dictionary> _factories
+ = new Dictionary>();
+ private static readonly Dictionary _defaults
+ = new Dictionary();
+
+ static ChunkLayerRegistry()
+ {
+ foreach (var attr in typeof(IChunkLayer).GetCustomAttributes()) {
+ var id = (byte)attr.Key;
+ var type = attr.SubType;
+ var ctor = type.GetConstructor(Type.EmptyTypes);
+ var fact = Expression.Lambda>(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()
+ => _factories.ContainsKey(typeof(T));
+
+ public static bool TryGetDefault(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 Create()
+ => (IChunkLayer)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(byte id, T @default, Func> factory)
+ // {
+ // var info = new Info(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 Factory { get; }
+ // }
+
+ // public class Info : IInfo
+ // {
+ // public byte ID { get; }
+ // public T Default { get; }
+ // public Func> Factory { get; }
+
+ // Type IInfo.Type => typeof(T);
+ // object IInfo.Default => Default;
+ // Func IInfo.Factory => Factory;
+
+ // public Info(byte id, T @default, Func> factory)
+ // { ID = id; Default = @default; Factory = factory; }
+ // }
+
+ // public static bool TryGet(out Info info)
+ // {
+ // if (TryGet(typeof(T), out var infoObj))
+ // { info = (Info)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);
}
diff --git a/src/World/Generation/GeneratorSimple.cs b/src/World/Generation/GeneratorSimple.cs
index 7bc06db..4de8265 100644
--- a/src/World/Generation/GeneratorSimple.cs
+++ b/src/World/Generation/GeneratorSimple.cs
@@ -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();
+ var rnd = new Random(_seed ^ chunk.ChunkPos.GetHashCode());
+ var blocks = chunk.GetLayer(true);
+ var colors = chunk.GetLayer(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);
+ }
}
}
diff --git a/src/World/Generation/GeneratorVoid.cs b/src/World/Generation/GeneratorVoid.cs
index 3fbaaac..f0ef093 100644
--- a/src/World/Generation/GeneratorVoid.cs
+++ b/src/World/Generation/GeneratorVoid.cs
@@ -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();
- 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();
- 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(true);
+ var colors = chunk.GetLayer(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(true);
+ var colors = chunk.GetLayer(false);
+ for (var x = 0; x <= 6; x++) {
+ blocks[x, 3] = Blocks.DEFAULT;
+ colors[x, 3] = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
+ }
}
}
}
diff --git a/src/World/World.cs b/src/World/World.cs
index e1d0170..70b2351 100644
--- a/src/World/World.cs
+++ b/src/World/World.cs
@@ -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 Chunks
=> ChunkContainer.GetChildren();
- public Chunk GetChunkOrNull((int X, int Y) chunkPos)
- => ChunkContainer.GetNodeOrNull($"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 ({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()?[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()[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)
+ public void SpawnChunk(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)
- {
- using (var stream = new MemoryStream()) {
- using (var writer = new BinaryWriter(stream)) {
- var layers = chunk.GetChildren()
- .OfType()
- .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(data);
+ ChunkContainer.GetNodeOrNull($"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.Instance();
- var sprite = this.GetWorld().GetNode(spritePath);
- hit.Add(sprite, hitPosition, color);
+ var texture = GD.Load("res://gfx/hit_decal.png");
+ var sprite = this.GetWorld().GetNode(spritePath);
+ 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);
+ }
}
diff --git a/src/World/WorldSave.cs b/src/World/WorldSave.cs
deleted file mode 100644
index 176a5a4..0000000
--- a/src/World/WorldSave.cs
+++ /dev/null
@@ -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> 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 { [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>();
- for (var i = 0; i < numChunks; i++) {
- var chunkPos = (reader.ReadInt32(), reader.ReadInt32());
- var chunk = new Dictionary();
- 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);
- }
- }
-}