- Potentially multiple layers per chunk - Add simple and void world generators - Block is now just a C# classmain
parent
3352518f08
commit
1fa14e54d1
20 changed files with 642 additions and 353 deletions
@ -1,18 +0,0 @@ |
|||||||
[gd_scene load_steps=4 format=2] |
|
||||||
|
|
||||||
[ext_resource path="res://gfx/block.png" type="Texture" id=1] |
|
||||||
[ext_resource path="res://src/Objects/Block.cs" type="Script" id=2] |
|
||||||
|
|
||||||
[sub_resource type="RectangleShape2D" id=1] |
|
||||||
extents = Vector2( 8, 8 ) |
|
||||||
|
|
||||||
[node name="Block" type="StaticBody2D"] |
|
||||||
collision_layer = 2 |
|
||||||
collision_mask = 0 |
|
||||||
script = ExtResource( 2 ) |
|
||||||
|
|
||||||
[node name="RectangleShape" type="CollisionShape2D" parent="."] |
|
||||||
shape = SubResource( 1 ) |
|
||||||
|
|
||||||
[node name="Sprite" type="Sprite" parent="."] |
|
||||||
texture = ExtResource( 1 ) |
|
@ -1,72 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Collections.Generic; |
|
||||||
using Godot; |
|
||||||
|
|
||||||
public class Chunk : Node2D |
|
||||||
{ |
|
||||||
public const int LENGTH = 32; |
|
||||||
public const int BIT_SHIFT = 5; |
|
||||||
public const int BIT_MASK = ~(~0 << BIT_SHIFT); |
|
||||||
|
|
||||||
public (int, int) ChunkPosition { get; } |
|
||||||
|
|
||||||
public Chunk(int x, int y) |
|
||||||
{ |
|
||||||
ChunkPosition = (x, y); |
|
||||||
Position = new Vector2(x << (BIT_SHIFT + Block.BIT_SHIFT), y << (BIT_SHIFT + Block.BIT_SHIFT)); |
|
||||||
} |
|
||||||
|
|
||||||
public ChunkLayer<T> GetLayerOrNull<T>() |
|
||||||
=> GetNodeOrNull<ChunkLayer<T>>($"{typeof(T).Name}Layer"); |
|
||||||
public ChunkLayer<T> GetOrCreateLayer<T>() |
|
||||||
{ |
|
||||||
var layer = GetLayerOrNull<T>(); |
|
||||||
if (layer == null) AddChild(layer = new ChunkLayer<T> { Name = $"{typeof(T).Name}Layer" }); |
|
||||||
return layer; |
|
||||||
} |
|
||||||
|
|
||||||
// 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 class ChunkLayer<T> : Node2D |
|
||||||
{ |
|
||||||
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default; |
|
||||||
|
|
||||||
// TODO: Use one-dimensional array? |
|
||||||
private readonly T[,] _data = new T[Chunk.LENGTH, Chunk.LENGTH]; |
|
||||||
private int _numNonDefault = 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]; } |
|
||||||
set { |
|
||||||
EnsureWithin(x, y); |
|
||||||
var previous = _data[x, y]; |
|
||||||
if (COMPARER.Equals(value, previous)) return; |
|
||||||
|
|
||||||
if (!COMPARER.Equals(previous, default)) { |
|
||||||
if (previous is Node node) RemoveChild(node); |
|
||||||
_numNonDefault--; |
|
||||||
} |
|
||||||
if (!COMPARER.Equals(value, default)) { |
|
||||||
if (value is Node node) AddChild(node); |
|
||||||
_numNonDefault++; |
|
||||||
} |
|
||||||
_data[x, y] = value; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public bool IsDefault => _numNonDefault == 0; |
|
||||||
|
|
||||||
private static void EnsureWithin(int x, int y) |
|
||||||
{ |
|
||||||
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"); |
|
||||||
} |
|
||||||
} |
|
@ -1,111 +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 DateTime LastSaved { get; private set; } |
|
||||||
public int Version { get; private set; } = LATEST_VERSION; |
|
||||||
public TimeSpan Playtime { get; set; } = TimeSpan.Zero; |
|
||||||
|
|
||||||
public Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>> Chunks { 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.Chunks = new Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>>(); |
|
||||||
var numBlocks = reader.ReadInt32(); |
|
||||||
for (var i = 0; i < numBlocks; i++) { |
|
||||||
var blockPos = new BlockPos(reader.ReadInt32(), reader.ReadInt32()); |
|
||||||
var blockData = (new Color(reader.ReadInt32()), reader.ReadBoolean()); |
|
||||||
var chunkPos = blockPos.ToChunkPos(); |
|
||||||
if (!save.Chunks.TryGetValue(chunkPos, out var blocks)) |
|
||||||
save.Chunks.Add(chunkPos, blocks = new Dictionary<BlockPos, (Color, bool)>()); |
|
||||||
blocks.Add(blockPos.GlobalToChunkRel(), blockData); |
|
||||||
} |
|
||||||
} else if (save.Version == 1) { |
|
||||||
var numChunks = reader.ReadInt32(); |
|
||||||
save.Chunks = new Dictionary<(int, int), Dictionary<BlockPos, (Color, bool)>>(numChunks); |
|
||||||
for (var i = 0; i < numChunks; i++) { |
|
||||||
var chunkPos = (reader.ReadInt32(), reader.ReadInt32()); |
|
||||||
var numBlocks = (int)reader.ReadUInt16(); |
|
||||||
var blocks = new Dictionary<BlockPos, (Color, bool)>(numBlocks); |
|
||||||
for (var j = 0; j < numBlocks; j++) |
|
||||||
blocks.Add(new BlockPos(reader.ReadByte(), reader.ReadByte()), |
|
||||||
(new Color(reader.ReadInt32()), reader.ReadBoolean())); |
|
||||||
save.Chunks.Add(chunkPos, blocks); |
|
||||||
} |
|
||||||
} 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(Chunks.Count); |
|
||||||
foreach (var ((chunkX, chunkY), blocks) in Chunks) { |
|
||||||
writer.Write(chunkX); |
|
||||||
writer.Write(chunkY); |
|
||||||
writer.Write((ushort)blocks.Count); |
|
||||||
foreach (var ((blockX, blockY), (color, unbreakable)) in blocks) { |
|
||||||
writer.Write((byte)blockX); |
|
||||||
writer.Write((byte)blockY); |
|
||||||
writer.Write(color.ToRgba32()); |
|
||||||
writer.Write(unbreakable); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
new Godot.Directory().Rename(path + ".tmp", path); |
|
||||||
LastSaved = File.GetLastWriteTime(path); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
public void WriteDataFromWorld(World world) |
|
||||||
=> Chunks = world.Chunks.ToDictionary( |
|
||||||
chunk => chunk.ChunkPosition, |
|
||||||
chunk => chunk.GetLayerOrNull<Block>() |
|
||||||
.GetChildren<Block>().ToDictionary( |
|
||||||
block => block.ChunkLocalBlockPos, |
|
||||||
block => (block.Color, block.Unbreakable))); |
|
||||||
|
|
||||||
public void ReadDataIntoWorld(World world) |
|
||||||
{ |
|
||||||
RPC.Reliable(world.ClearChunks); |
|
||||||
foreach (var (chunkPos, blocks) in Chunks) { |
|
||||||
foreach (var (blockPos, (color, unbreakable)) in blocks) { |
|
||||||
var (x, y) = blockPos.ChunkRelToGlobal(chunkPos); |
|
||||||
world.SpawnBlock(x, y, color, unbreakable); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
using Godot; |
|
||||||
|
|
||||||
public class Block : StaticBody2D, IInitializable |
|
||||||
{ |
|
||||||
public const int LENGTH = 16; |
|
||||||
public const int BIT_SHIFT = 4; |
|
||||||
|
|
||||||
public BlockPos GlobalBlockPos { get => BlockPos.FromVector(GlobalPosition); set => GlobalPosition = value.ToVector(); } |
|
||||||
public BlockPos ChunkLocalBlockPos { get => BlockPos.FromVector(Position); set => Position = value.ToVector(); } |
|
||||||
public Color Color { get => Sprite.SelfModulate; set => Sprite.SelfModulate = value; } |
|
||||||
public bool Unbreakable { get; set; } = false; |
|
||||||
|
|
||||||
public Sprite Sprite { get; private set; } |
|
||||||
public void Initialize() => Sprite = GetNode<Sprite>("Sprite"); |
|
||||||
} |
|
@ -0,0 +1,21 @@ |
|||||||
|
using System; |
||||||
|
using Godot; |
||||||
|
|
||||||
|
public static class SceneCache<T> where T : Node |
||||||
|
{ |
||||||
|
private static readonly PackedScene SCENE |
||||||
|
= GD.Load<PackedScene>($"res://scene/{typeof(T).Name}.tscn"); |
||||||
|
|
||||||
|
public static T Instance(Action<T> initFunc = null) |
||||||
|
{ |
||||||
|
var node = SCENE.Instance<T>(); |
||||||
|
(node as IInitializable)?.Initialize(); |
||||||
|
initFunc?.Invoke(node); |
||||||
|
return node; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public interface IInitializable |
||||||
|
{ |
||||||
|
void Initialize(); |
||||||
|
} |
@ -1,107 +0,0 @@ |
|||||||
using System.Collections.Generic; |
|
||||||
using Godot; |
|
||||||
|
|
||||||
public class World : Node |
|
||||||
{ |
|
||||||
private static readonly PackedScene BLOCK = GD.Load<PackedScene>("res://scene/Block.tscn"); |
|
||||||
private static readonly PackedScene PLAYER = GD.Load<PackedScene>("res://scene/Player.tscn"); |
|
||||||
private static readonly PackedScene LOCAL_PLAYER = GD.Load<PackedScene>("res://scene/LocalPlayer.tscn"); |
|
||||||
private static readonly PackedScene HIT_DECAL = GD.Load<PackedScene>("res://scene/HitDecal.tscn"); |
|
||||||
|
|
||||||
internal Node PlayerContainer { get; } |
|
||||||
internal Node ChunkContainer { get; } |
|
||||||
|
|
||||||
public World() |
|
||||||
{ |
|
||||||
AddChild(PlayerContainer = new Node { Name = "Players" }); |
|
||||||
AddChild(ChunkContainer = new Node { Name = "Chunks" }); |
|
||||||
} |
|
||||||
|
|
||||||
public IEnumerable<Player> Players |
|
||||||
=> PlayerContainer.GetChildren<Player>(); |
|
||||||
public Player GetPlayer(int networkID) |
|
||||||
=> PlayerContainer.GetNodeOrNull<Player>(networkID.ToString()); |
|
||||||
public void ClearPlayers() |
|
||||||
{ foreach (var player in Players) player.RemoveFromParent(); } |
|
||||||
|
|
||||||
public IEnumerable<Chunk> Chunks |
|
||||||
=> ChunkContainer.GetChildren<Chunk>(); |
|
||||||
public Chunk GetChunkOrNull((int X, int Y) chunkPos) |
|
||||||
=> ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos.X}, {chunkPos.Y})"); |
|
||||||
public Chunk GetOrCreateChunk((int X, int Y) chunkPos) |
|
||||||
=> ChunkContainer.GetOrCreateChild($"Chunk ({chunkPos.X}, {chunkPos.Y})", () => new Chunk(chunkPos.X, chunkPos.Y)); |
|
||||||
[PuppetSync] public void ClearChunks() |
|
||||||
{ foreach (var chunk in Chunks) chunk.RemoveFromParent(); } |
|
||||||
|
|
||||||
|
|
||||||
public Block GetBlockAt(BlockPos position) |
|
||||||
=> GetChunkOrNull(position.ToChunkPos()) |
|
||||||
?.GetLayerOrNull<Block>()?[position.GlobalToChunkRel()]; |
|
||||||
[PuppetSync] |
|
||||||
public void SpawnBlock(int x, int y, Color color, bool unbreakable) |
|
||||||
{ |
|
||||||
var blockPos = new BlockPos(x, y); |
|
||||||
var block = BLOCK.Init<Block>(); |
|
||||||
block.Name = blockPos.ToString(); |
|
||||||
block.Color = color; |
|
||||||
block.Unbreakable = unbreakable; |
|
||||||
block.ChunkLocalBlockPos = blockPos.GlobalToChunkRel(); |
|
||||||
|
|
||||||
GetOrCreateChunk(blockPos.ToChunkPos()) |
|
||||||
.GetOrCreateLayer<Block>()[block.ChunkLocalBlockPos] = block; |
|
||||||
} |
|
||||||
[PuppetSync] |
|
||||||
public void DespawnBlock(int x, int y) |
|
||||||
{ |
|
||||||
var blockPos = new BlockPos(x, y); |
|
||||||
var blockLayer = GetChunkOrNull(blockPos.ToChunkPos())?.GetLayerOrNull<Block>(); |
|
||||||
if (blockLayer != null) blockLayer[blockPos.GlobalToChunkRel()] = null; |
|
||||||
} |
|
||||||
|
|
||||||
[PuppetSync] |
|
||||||
public void SpawnPlayer(int networkID, Vector2 position) |
|
||||||
{ |
|
||||||
var player = SceneCache<Player>.Instance(); |
|
||||||
player.NetworkID = networkID; |
|
||||||
player.Position = position; |
|
||||||
PlayerContainer.AddChild(player); |
|
||||||
|
|
||||||
if (player.IsLocal) { |
|
||||||
player.AddChild(new PlayerMovement { Name = "PlayerMovement" }); |
|
||||||
player.AddChild(new Camera2D { Name = "Camera", Current = true }); |
|
||||||
this.GetClient().FireLocalPlayerSpawned(player); |
|
||||||
} |
|
||||||
|
|
||||||
if (this.GetGame() is Server) { |
|
||||||
player.VisibilityTracker.ChunkTracked += (chunkPos) => { |
|
||||||
var chunk = GetChunkOrNull(chunkPos); |
|
||||||
if (chunk == null) return; |
|
||||||
foreach (var block in chunk.GetLayerOrNull<Block>().GetChildren<Block>()) |
|
||||||
RPC.Reliable(player.NetworkID, SpawnBlock, |
|
||||||
block.GlobalBlockPos.X, block.GlobalBlockPos.Y, |
|
||||||
block.Color, block.Unbreakable); |
|
||||||
}; |
|
||||||
player.VisibilityTracker.ChunkUntracked += (chunkPos) => { |
|
||||||
var chunk = GetChunkOrNull(chunkPos); |
|
||||||
if (chunk == null) return; |
|
||||||
RPC.Reliable(player.NetworkID, Despawn, GetPathTo(chunk)); |
|
||||||
}; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
[Puppet] |
|
||||||
public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color) |
|
||||||
{ |
|
||||||
var hit = HIT_DECAL.Init<HitDecal>(); |
|
||||||
var sprite = this.GetWorld().GetNode<Sprite>(spritePath); |
|
||||||
hit.Add(sprite, hitPosition, color); |
|
||||||
} |
|
||||||
|
|
||||||
[PuppetSync] |
|
||||||
public void Despawn(NodePath path) |
|
||||||
{ |
|
||||||
var node = GetNode(path); |
|
||||||
node.GetParent().RemoveChild(node); |
|
||||||
node.QueueFree(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,19 @@ |
|||||||
|
using Godot; |
||||||
|
|
||||||
|
public class Block |
||||||
|
{ |
||||||
|
public const int LENGTH = 16; |
||||||
|
public const int BIT_SHIFT = 4; |
||||||
|
|
||||||
|
|
||||||
|
public static readonly Block DEFAULT = new Block( |
||||||
|
GD.Load<Texture>("res://gfx/block.png"), |
||||||
|
new RectangleShape2D { Extents = new Vector2(0.5F, 0.5F) * LENGTH }); |
||||||
|
|
||||||
|
|
||||||
|
public Texture Texture { get; } |
||||||
|
public Shape2D Shape { get; } |
||||||
|
|
||||||
|
public Block(Texture texture, Shape2D shape) |
||||||
|
{ Texture = texture; Shape = shape; } |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
using System.IO; |
||||||
|
using Godot; |
||||||
|
|
||||||
|
public readonly struct BlockData |
||||||
|
{ |
||||||
|
public Block Block { get; } // TODO: Replace with 2-byte or smaller integer identifier? |
||||||
|
public int RawColor { get; } // TODO: Replace with smaller representation? |
||||||
|
// Perhaps we can fit this into 4 bytes total? |
||||||
|
public Color Color => new Color(RawColor); |
||||||
|
public BlockData(Block block, int rawColor) { Block = block; RawColor = rawColor; } |
||||||
|
public BlockData(Block block, Color color) { Block = block; RawColor = color.ToRgba32(); } |
||||||
|
} |
||||||
|
|
||||||
|
public class BlockLayer : BasicChunkLayer<BlockData> |
||||||
|
{ |
||||||
|
private MeshInstance2D _render = null; |
||||||
|
private StaticBody2D _collider = null; |
||||||
|
|
||||||
|
public override void _Process(float delta) |
||||||
|
{ |
||||||
|
if (!Dirty) return; |
||||||
|
Dirty = false; |
||||||
|
|
||||||
|
var st = (SurfaceTool)null; |
||||||
|
if (this.GetGame() is Client) { |
||||||
|
if (_render == null) AddChild(_render = new MeshInstance2D |
||||||
|
{ Texture = GD.Load<Texture>("res://gfx/block.png") }); |
||||||
|
st = new SurfaceTool(); |
||||||
|
st.Begin(Mesh.PrimitiveType.Triangles); |
||||||
|
} |
||||||
|
|
||||||
|
_collider?.RemoveFromParent(); |
||||||
|
if (IsDefault) _collider = null; |
||||||
|
else AddChild(_collider = new StaticBody2D()); |
||||||
|
|
||||||
|
var size = Block.LENGTH; |
||||||
|
var index = 0; |
||||||
|
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) { |
||||||
|
var data = Data[i]; |
||||||
|
if (data.Block == null) continue; |
||||||
|
|
||||||
|
var x = (float)(i & Chunk.BIT_MASK); |
||||||
|
var y = (float)(i >> Chunk.BIT_SHIFT); |
||||||
|
|
||||||
|
if (_render != null) { |
||||||
|
st.AddColor(data.Color); |
||||||
|
st.AddUv(new Vector2(0, 0)); st.AddVertex(new Vector3(x - 0.5F, y - 0.5F, 0) * size); |
||||||
|
st.AddUv(new Vector2(1, 0)); st.AddVertex(new Vector3(x + 0.5F, y - 0.5F, 0) * size); |
||||||
|
st.AddUv(new Vector2(1, 1)); st.AddVertex(new Vector3(x + 0.5F, y + 0.5F, 0) * size); |
||||||
|
st.AddUv(new Vector2(0, 1)); st.AddVertex(new Vector3(x - 0.5F, y + 0.5F, 0) * size); |
||||||
|
|
||||||
|
st.AddIndex(index); st.AddIndex(index + 1); st.AddIndex(index + 2); |
||||||
|
st.AddIndex(index); st.AddIndex(index + 3); st.AddIndex(index + 2); |
||||||
|
index += 4; |
||||||
|
} |
||||||
|
|
||||||
|
var ownerID = _collider.CreateShapeOwner(null); |
||||||
|
_collider.ShapeOwnerAddShape(ownerID, data.Block.Shape); |
||||||
|
_collider.ShapeOwnerSetTransform(ownerID, Transform2D.Identity.Translated(new Vector2(x, y) * size)); |
||||||
|
} |
||||||
|
|
||||||
|
if (_render != null) |
||||||
|
_render.Mesh = st.Commit(); |
||||||
|
} |
||||||
|
|
||||||
|
public override void Read(BinaryReader reader) |
||||||
|
{ |
||||||
|
NonDefaultCount = 0; |
||||||
|
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) { |
||||||
|
var color = reader.ReadInt32(); |
||||||
|
if (color == 0) continue; |
||||||
|
Data[i] = new BlockData(Block.DEFAULT, color); |
||||||
|
NonDefaultCount++; |
||||||
|
} |
||||||
|
Dirty = true; |
||||||
|
} |
||||||
|
|
||||||
|
public override void Write(BinaryWriter writer) |
||||||
|
{ |
||||||
|
for (var i = 0; i < Chunk.LENGTH * Chunk.LENGTH; i++) |
||||||
|
writer.Write(Data[i].RawColor); // Is 0 if block is not set. |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using Godot; |
||||||
|
|
||||||
|
public class Chunk : Node2D |
||||||
|
{ |
||||||
|
public const int LENGTH = 32; |
||||||
|
public const int BIT_SHIFT = 5; |
||||||
|
public const int BIT_MASK = ~(~0 << BIT_SHIFT); |
||||||
|
|
||||||
|
public (int X, int Y) ChunkPosition { get; } |
||||||
|
public IEnumerable<IChunkLayer> Layers |
||||||
|
=> GetChildren().OfType<IChunkLayer>(); |
||||||
|
|
||||||
|
public Chunk((int X, int Y) chunkPos) |
||||||
|
{ |
||||||
|
Name = $"Chunk ({chunkPos})"; |
||||||
|
ChunkPosition = chunkPos; |
||||||
|
Position = new Vector2(chunkPos.X << (BIT_SHIFT + Block.BIT_SHIFT), |
||||||
|
chunkPos.Y << (BIT_SHIFT + Block.BIT_SHIFT)); |
||||||
|
} |
||||||
|
|
||||||
|
public T GetLayerOrNull<T>() => (T)GetLayerOrNull(typeof(T).Name); |
||||||
|
public T GetOrCreateLayer<T>() => (T)GetOrCreateLayer(typeof(T).Name); |
||||||
|
|
||||||
|
public IChunkLayer GetLayerOrNull(string name) |
||||||
|
=> GetNodeOrNull<IChunkLayer>(name); |
||||||
|
public IChunkLayer GetOrCreateLayer(string name) |
||||||
|
{ |
||||||
|
var layer = GetLayerOrNull(name); |
||||||
|
if (layer == null) AddChild((Node)(layer = ChunkLayerRegistry.Create(name))); |
||||||
|
return layer; |
||||||
|
} |
||||||
|
|
||||||
|
// 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); |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using Godot; |
||||||
|
|
||||||
|
public interface IChunkLayer |
||||||
|
{ |
||||||
|
bool IsDefault { get; } |
||||||
|
void Read(BinaryReader reader); |
||||||
|
void Write(BinaryWriter writer); |
||||||
|
} |
||||||
|
|
||||||
|
public interface IChunkLayer<T> : IChunkLayer |
||||||
|
{ |
||||||
|
T this[BlockPos pos] { get; set; } |
||||||
|
T this[int x, int y] { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static class ChunkLayerRegistry |
||||||
|
{ |
||||||
|
private static readonly Dictionary<string, Type> _types = new Dictionary<string, Type>(); |
||||||
|
|
||||||
|
static ChunkLayerRegistry() |
||||||
|
=> Register<BlockLayer>(); |
||||||
|
|
||||||
|
public static void Register<T>() |
||||||
|
where T : Node2D, IChunkLayer |
||||||
|
=> _types.Add(typeof(T).Name, typeof(T)); |
||||||
|
|
||||||
|
public static IChunkLayer Create(string name) |
||||||
|
{ |
||||||
|
var layer = (IChunkLayer)Activator.CreateInstance(_types[name]); |
||||||
|
((Node)layer).Name = name; |
||||||
|
return layer; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static class ChunkLayerExtensions |
||||||
|
{ |
||||||
|
public static void FromBytes(this IChunkLayer layer, byte[] data) |
||||||
|
{ |
||||||
|
using (var stream = new MemoryStream(data)) |
||||||
|
layer.Read(stream); |
||||||
|
} |
||||||
|
public static void Read(this IChunkLayer layer, Stream stream) |
||||||
|
{ |
||||||
|
using (var reader = new BinaryReader(stream)) |
||||||
|
layer.Read(reader); |
||||||
|
} |
||||||
|
|
||||||
|
public static byte[] ToBytes(this IChunkLayer layer) |
||||||
|
{ |
||||||
|
using (var stream = new MemoryStream()) { |
||||||
|
layer.Write(stream); |
||||||
|
return stream.ToArray(); |
||||||
|
} |
||||||
|
} |
||||||
|
public static void Write(this IChunkLayer layer, Stream stream) |
||||||
|
{ |
||||||
|
using (var writer = new BinaryWriter(stream)) |
||||||
|
layer.Write(writer); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public abstract class BasicChunkLayer<T> : Node2D, IChunkLayer<T> |
||||||
|
{ |
||||||
|
private static readonly IEqualityComparer<T> COMPARER = EqualityComparer<T>.Default; |
||||||
|
|
||||||
|
protected T[] Data { get; } = new T[Chunk.LENGTH * Chunk.LENGTH]; |
||||||
|
protected bool Dirty { get; set; } = true; |
||||||
|
|
||||||
|
public Chunk Chunk => GetParent<Chunk>(); |
||||||
|
public int NonDefaultCount { get; protected set; } = 0; |
||||||
|
public bool IsDefault => NonDefaultCount == 0; |
||||||
|
|
||||||
|
public 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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void EnsureWithin(int x, int y) |
||||||
|
{ |
||||||
|
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"); |
||||||
|
} |
||||||
|
|
||||||
|
public abstract void Read(BinaryReader reader); |
||||||
|
public abstract void Write(BinaryWriter writer); |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
using System; |
||||||
|
using Godot; |
||||||
|
|
||||||
|
public class GeneratorSimple : IWorldGenerator |
||||||
|
{ |
||||||
|
private int _seed; |
||||||
|
|
||||||
|
public string Name => "Simple"; |
||||||
|
|
||||||
|
public void SetSeed(int seed) => _seed = seed; |
||||||
|
|
||||||
|
public void Generate(Chunk chunk) |
||||||
|
{ |
||||||
|
if (chunk.ChunkPosition.Y != 0) return; |
||||||
|
|
||||||
|
var rnd = new Random(_seed ^ chunk.ChunkPosition.GetHashCode()); |
||||||
|
var blocks = chunk.GetOrCreateLayer<BlockLayer>(); |
||||||
|
|
||||||
|
for (var x = 0; x < Chunk.LENGTH; x++) { |
||||||
|
var grassDepth = rnd.Next(1, 4); |
||||||
|
var dirtDepth = rnd.Next(6, 9); |
||||||
|
var stoneDepth = rnd.Next(12, 16); |
||||||
|
for (var y = 0; y < stoneDepth; y++) { |
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
using Godot; |
||||||
|
|
||||||
|
public class GeneratorVoid : IWorldGenerator |
||||||
|
{ |
||||||
|
public string Name => "Void"; |
||||||
|
|
||||||
|
public void SetSeed(int seed) { } |
||||||
|
|
||||||
|
public void Generate(Chunk chunk) |
||||||
|
{ |
||||||
|
// TODO: Make it easier to create "structures" that cover multiple chunks. |
||||||
|
// TODO: These are supposed to be unbreakable. |
||||||
|
if (chunk.ChunkPosition == (-1, 0)) { |
||||||
|
var blocks = chunk.GetOrCreateLayer<BlockLayer>(); |
||||||
|
for (var x = Chunk.LENGTH - 6; x < Chunk.LENGTH; x++) |
||||||
|
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F)); |
||||||
|
} else if (chunk.ChunkPosition == (0, 0)) { |
||||||
|
var blocks = chunk.GetOrCreateLayer<BlockLayer>(); |
||||||
|
for (var x = 0; x <= 6; x++) |
||||||
|
blocks[x, 3] = new BlockData(Block.DEFAULT, Color.FromHsv(GD.Randf(), 0.1F, 1.0F)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
public interface IWorldGenerator |
||||||
|
{ |
||||||
|
string Name { get; } |
||||||
|
void SetSeed(int seed); // TODO: Allow loading additional generator options. |
||||||
|
void Generate(Chunk chunk); |
||||||
|
} |
||||||
|
|
||||||
|
public static class WorldGeneratorRegistry |
||||||
|
{ |
||||||
|
private static readonly Dictionary<string, IWorldGenerator> _generators |
||||||
|
= new Dictionary<string, IWorldGenerator>(); |
||||||
|
|
||||||
|
static WorldGeneratorRegistry() |
||||||
|
{ |
||||||
|
Register(new GeneratorVoid()); |
||||||
|
Register(new GeneratorSimple()); |
||||||
|
} |
||||||
|
|
||||||
|
public static void Register(IWorldGenerator generator) |
||||||
|
=> _generators.Add(generator.Name, generator); |
||||||
|
public static IWorldGenerator GetOrNull(string name) |
||||||
|
=> _generators.TryGetValue(name, out var value) ? value : null; |
||||||
|
} |
@ -0,0 +1,133 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using Godot; |
||||||
|
|
||||||
|
public class World : Node |
||||||
|
{ |
||||||
|
internal Node PlayerContainer { get; } |
||||||
|
internal Node ChunkContainer { get; } |
||||||
|
|
||||||
|
public IWorldGenerator Generator { get; set; } = WorldGeneratorRegistry.GetOrNull("Simple"); |
||||||
|
public int Seed { get; set; } = unchecked((int)GD.Randi()); |
||||||
|
|
||||||
|
public World() |
||||||
|
{ |
||||||
|
AddChild(PlayerContainer = new Node { Name = "Players" }); |
||||||
|
AddChild(ChunkContainer = new Node { Name = "Chunks" }); |
||||||
|
} |
||||||
|
|
||||||
|
public IEnumerable<Player> Players |
||||||
|
=> PlayerContainer.GetChildren<Player>(); |
||||||
|
public Player GetPlayer(int networkID) |
||||||
|
=> PlayerContainer.GetNodeOrNull<Player>(networkID.ToString()); |
||||||
|
public void ClearPlayers() |
||||||
|
{ foreach (var player in Players) player.RemoveFromParent(); } |
||||||
|
|
||||||
|
public IEnumerable<Chunk> Chunks |
||||||
|
=> ChunkContainer.GetChildren<Chunk>(); |
||||||
|
public Chunk GetChunkOrNull((int X, int Y) chunkPos) |
||||||
|
=> ChunkContainer.GetNodeOrNull<Chunk>($"Chunk ({chunkPos})"); |
||||||
|
public Chunk GetOrCreateChunk((int X, int Y) chunkPos, bool generate = false) |
||||||
|
=> ChunkContainer.GetOrCreateChild($"Chunk ({chunkPos})", () => { |
||||||
|
var chunk = new Chunk(chunkPos); |
||||||
|
if (generate) Generator.Generate(chunk); |
||||||
|
return chunk; |
||||||
|
}); |
||||||
|
[PuppetSync] public void ClearChunks() |
||||||
|
{ foreach (var chunk in Chunks) chunk.RemoveFromParent(); } |
||||||
|
|
||||||
|
|
||||||
|
public BlockData GetBlockDataAt(BlockPos position) |
||||||
|
=> GetChunkOrNull(position.ToChunkPos()) |
||||||
|
?.GetLayerOrNull<BlockLayer>()?[position.GlobalToChunkRel()] ?? default; |
||||||
|
|
||||||
|
[PuppetSync] |
||||||
|
public void SetBlockData(int x, int y, int color) |
||||||
|
{ |
||||||
|
var blockPos = new BlockPos(x, y); |
||||||
|
GetOrCreateChunk(blockPos.ToChunkPos()) |
||||||
|
.GetOrCreateLayer<BlockLayer>()[blockPos.GlobalToChunkRel()] |
||||||
|
= (color != 0) ? new BlockData(Block.DEFAULT, color) : default; |
||||||
|
} |
||||||
|
|
||||||
|
[PuppetSync] |
||||||
|
public void SpawnChunk(int chunkX, int chunkY, byte[] data) |
||||||
|
{ |
||||||
|
var chunk = GetOrCreateChunk((chunkX, chunkY)); |
||||||
|
using (var stream = new MemoryStream(data)) { |
||||||
|
using (var reader = new BinaryReader(stream)) { |
||||||
|
var numLayers = reader.ReadByte(); |
||||||
|
for (var i = 0; i < numLayers; i++) { |
||||||
|
var name = reader.ReadString(); |
||||||
|
var layer = chunk.GetOrCreateLayer(name); |
||||||
|
layer.Read(reader); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] ChunkToBytes(Chunk chunk) |
||||||
|
{ |
||||||
|
using (var stream = new MemoryStream()) { |
||||||
|
using (var writer = new BinaryWriter(stream)) { |
||||||
|
var layers = chunk.GetChildren() |
||||||
|
.OfType<IChunkLayer>() |
||||||
|
.Where(layer => !layer.IsDefault) |
||||||
|
.ToArray(); |
||||||
|
|
||||||
|
writer.Write((byte)layers.Length); |
||||||
|
foreach (var layer in layers) { |
||||||
|
writer.Write(layer.GetType().Name); |
||||||
|
layer.Write(writer); |
||||||
|
} |
||||||
|
} |
||||||
|
return stream.ToArray(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[PuppetSync] |
||||||
|
public void SpawnPlayer(int networkID, Vector2 position) |
||||||
|
{ |
||||||
|
var player = SceneCache<Player>.Instance(); |
||||||
|
player.NetworkID = networkID; |
||||||
|
player.Position = position; |
||||||
|
PlayerContainer.AddChild(player); |
||||||
|
|
||||||
|
if (player.IsLocal) { |
||||||
|
player.AddChild(new PlayerMovement { Name = "PlayerMovement" }); |
||||||
|
player.AddChild(new Camera2D { Name = "Camera", Current = true }); |
||||||
|
this.GetClient().FireLocalPlayerSpawned(player); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.GetGame() is Server) { |
||||||
|
player.VisibilityTracker.ChunkTracked += (chunkPos) => { |
||||||
|
var chunk = GetOrCreateChunk(chunkPos, generate: true); |
||||||
|
RPC.Reliable(player.NetworkID, SpawnChunk, |
||||||
|
chunk.ChunkPosition.X, chunk.ChunkPosition.Y, ChunkToBytes(chunk)); |
||||||
|
}; |
||||||
|
player.VisibilityTracker.ChunkUntracked += (chunkPos) => { |
||||||
|
var chunk = GetChunkOrNull(chunkPos); |
||||||
|
if (chunk == null) return; |
||||||
|
RPC.Reliable(player.NetworkID, Despawn, GetPathTo(chunk), false); |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[Puppet] |
||||||
|
public void SpawnHit(NodePath spritePath, Vector2 hitPosition, Color color) |
||||||
|
{ |
||||||
|
var hit = SceneCache<HitDecal>.Instance(); |
||||||
|
var sprite = this.GetWorld().GetNode<Sprite>(spritePath); |
||||||
|
hit.Add(sprite, hitPosition, color); |
||||||
|
} |
||||||
|
|
||||||
|
[PuppetSync] |
||||||
|
public void Despawn(NodePath path, bool errorIfMissing) |
||||||
|
{ |
||||||
|
var node = GetNode(path); |
||||||
|
if ((node == null) && !errorIfMissing) return; |
||||||
|
node.GetParent().RemoveChild(node); |
||||||
|
node.QueueFree(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Collections.Generic; |
||||||
|
using Godot; |
||||||
|
using File = System.IO.File; |
||||||
|
|
||||||
|
public class WorldSave |
||||||
|
{ |
||||||
|
public const string FILE_EXT = ".yf5"; |
||||||
|
public const int MAGIC_NUMBER = 0x59463573; // "YF5s" |
||||||
|
public const int LATEST_VERSION = 1; |
||||||
|
|
||||||
|
public static readonly string WORLDS_DIR = OS.GetUserDataDir() + "/worlds/"; |
||||||
|
|
||||||
|
|
||||||
|
public int Version { get; private set; } = LATEST_VERSION; |
||||||
|
public TimeSpan Playtime { get; set; } = TimeSpan.Zero; |
||||||
|
public DateTime LastSaved { get; private set; } |
||||||
|
|
||||||
|
public string Generator { get; private set; } |
||||||
|
public int Seed { get; private set; } |
||||||
|
|
||||||
|
public Dictionary<(int X, int Y), Dictionary<string, byte[]>> ChunkData { get; private set; } |
||||||
|
|
||||||
|
|
||||||
|
public static WorldSave ReadFromFile(string path) |
||||||
|
{ |
||||||
|
var save = new WorldSave { LastSaved = File.GetLastAccessTime(path) }; |
||||||
|
using (var stream = File.OpenRead(path)) { |
||||||
|
using (var reader = new BinaryReader(stream)) { |
||||||
|
var magic = reader.ReadInt32(); |
||||||
|
if (magic != MAGIC_NUMBER) throw new IOException( |
||||||
|
$"Magic number does not match ({magic:X8} != {MAGIC_NUMBER:X8})"); |
||||||
|
|
||||||
|
// TODO: See how to better support multiple versions, improve saving/loading. |
||||||
|
save.Version = reader.ReadUInt16(); |
||||||
|
save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32()); |
||||||
|
|
||||||
|
if (save.Version == 0) { |
||||||
|
save.Seed = unchecked((int)GD.Randi()); |
||||||
|
save.Generator = "Void"; |
||||||
|
|
||||||
|
var tempBlockLayers = new Dictionary<(int X, int Y), BlockLayer>(); |
||||||
|
var numBlocks = reader.ReadInt32(); |
||||||
|
for (var i = 0; i < numBlocks; i++) { |
||||||
|
var blockPos = new BlockPos(reader.ReadInt32(), reader.ReadInt32()); |
||||||
|
var rawColor = reader.ReadInt32(); |
||||||
|
var unbreakable = reader.ReadBoolean(); // TODO |
||||||
|
|
||||||
|
var chunkPos = blockPos.ToChunkPos(); |
||||||
|
if (!tempBlockLayers.TryGetValue(chunkPos, out var blocks)) |
||||||
|
tempBlockLayers.Add(chunkPos, blocks = new BlockLayer()); |
||||||
|
blocks[blockPos.GlobalToChunkRel()] = new BlockData(Block.DEFAULT, rawColor); |
||||||
|
} |
||||||
|
save.ChunkData = tempBlockLayers.ToDictionary(kvp => kvp.Key, |
||||||
|
kvp => new Dictionary<string, byte[]> { [nameof(BlockLayer)] = kvp.Value.ToBytes() }); |
||||||
|
} else if (save.Version == 1) { |
||||||
|
save.Generator = reader.ReadString(); |
||||||
|
save.Seed = reader.ReadInt32(); |
||||||
|
|
||||||
|
var numChunks = reader.ReadInt32(); |
||||||
|
save.ChunkData = new Dictionary<(int X, int Y), Dictionary<string, byte[]>>(); |
||||||
|
for (var i = 0; i < numChunks; i++) { |
||||||
|
var chunkPos = (reader.ReadInt32(), reader.ReadInt32()); |
||||||
|
var chunk = new Dictionary<string, byte[]>(); |
||||||
|
save.ChunkData.Add(chunkPos, chunk); |
||||||
|
|
||||||
|
var numLayers = reader.ReadByte(); |
||||||
|
for (var j = 0; j < numLayers; j++) { |
||||||
|
var name = reader.ReadString(); |
||||||
|
var count = reader.ReadInt32(); |
||||||
|
var data = reader.ReadBytes(count); |
||||||
|
chunk.Add(name, data); |
||||||
|
} |
||||||
|
} |
||||||
|
} else throw new IOException($"Version {save.Version} not supported (latest version: {LATEST_VERSION})"); |
||||||
|
} |
||||||
|
} |
||||||
|
return save; |
||||||
|
} |
||||||
|
|
||||||
|
public void WriteToFile(string path) |
||||||
|
{ |
||||||
|
using (var stream = File.OpenWrite(path + ".tmp")) { |
||||||
|
using (var writer = new BinaryWriter(stream)) { |
||||||
|
writer.Write(MAGIC_NUMBER); |
||||||
|
writer.Write((ushort)LATEST_VERSION); |
||||||
|
writer.Write((uint)Playtime.TotalSeconds); |
||||||
|
|
||||||
|
writer.Write(Generator); |
||||||
|
writer.Write(Seed); |
||||||
|
|
||||||
|
writer.Write(ChunkData.Count); |
||||||
|
foreach (var ((chunkX, chunkY), layers) in ChunkData) { |
||||||
|
writer.Write(chunkX); |
||||||
|
writer.Write(chunkY); |
||||||
|
writer.Write((byte)layers.Count); |
||||||
|
foreach (var (name, data) in layers) { |
||||||
|
writer.Write(name); |
||||||
|
writer.Write(data.Length); |
||||||
|
writer.Write(data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
new Godot.Directory().Rename(path + ".tmp", path); |
||||||
|
LastSaved = File.GetLastWriteTime(path); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void WriteDataFromWorld(World world) |
||||||
|
{ |
||||||
|
Generator = world.Generator.Name; |
||||||
|
Seed = world.Seed; |
||||||
|
|
||||||
|
ChunkData = world.Chunks.ToDictionary( |
||||||
|
chunk => chunk.ChunkPosition, |
||||||
|
chunk => chunk.Layers |
||||||
|
.Where(layer => !layer.IsDefault) |
||||||
|
.ToDictionary(layer => layer.GetType().Name, layer => layer.ToBytes())); |
||||||
|
} |
||||||
|
|
||||||
|
public void ReadDataIntoWorld(World world) |
||||||
|
{ |
||||||
|
world.Generator = WorldGeneratorRegistry.GetOrNull(Generator); |
||||||
|
world.Seed = Seed; |
||||||
|
|
||||||
|
RPC.Reliable(world.ClearChunks); |
||||||
|
foreach (var (chunkPos, layers) in ChunkData) { |
||||||
|
var chunk = world.GetOrCreateChunk(chunkPos); |
||||||
|
foreach (var (name, data) in layers) |
||||||
|
chunk.GetOrCreateLayer(name).FromBytes(data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue