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