diff --git a/src/EscapeMenuMultiplayer.cs b/src/EscapeMenuMultiplayer.cs index c1099ba..cf03e08 100644 --- a/src/EscapeMenuMultiplayer.cs +++ b/src/EscapeMenuMultiplayer.cs @@ -141,7 +141,8 @@ public class EscapeMenuMultiplayer : Container IntegratedServer = null; client.Disconnect(); - this.GetWorld().Clear(); + this.GetWorld().ClearPlayers(); + this.GetWorld().ClearBlocks(); } if (client.Status == ConnectionStatus.Disconnected) { @@ -155,8 +156,8 @@ public class EscapeMenuMultiplayer : Container } client.Connect(address, port); } else { - client.Disconnect(); - this.GetWorld().Clear(); + this.GetWorld().ClearPlayers(); + this.GetWorld().ClearBlocks(); } } } diff --git a/src/EscapeMenuWorld.cs b/src/EscapeMenuWorld.cs index f184d3a..8c843cb 100644 --- a/src/EscapeMenuWorld.cs +++ b/src/EscapeMenuWorld.cs @@ -1,10 +1,6 @@ using System; using System.Text; using Godot; -using Path = System.IO.Path; -using File = System.IO.File; -using Directory = System.IO.Directory; -using System.Linq; using static Godot.NetworkedMultiplayerPeer; public class EscapeMenuWorld : CenterContainer @@ -43,10 +39,9 @@ public class EscapeMenuWorld : CenterContainer SaveAsButton.Text = "Save World As..."; SaveFileDialog.GetOk().Text = "Save"; - var worldsFolder = OS.GetUserDataDir() + "/worlds/"; - Directory.CreateDirectory(worldsFolder); - SaveFileDialog.CurrentPath = worldsFolder; - LoadFileDialog.CurrentPath = worldsFolder; + new Directory().MakeDirRecursive(WorldSave.WORLDS_DIR); + SaveFileDialog.CurrentPath = WorldSave.WORLDS_DIR; + LoadFileDialog.CurrentPath = WorldSave.WORLDS_DIR; this.GetClient().StatusChanged += OnStatusChanged; } @@ -85,17 +80,16 @@ public class EscapeMenuWorld : CenterContainer private void _on_SaveFileDialog_file_selected(string path) { - // var server = this.GetClient().GetNode(nameof(IntegratedServer)).Server; - // var save = Save.CreateFromWorld(server, _playtime); - // save.WriteToFile(path + ".tmp"); - // File.Delete(path); // TODO: In later .NET, there is a File.Move(source, dest, overwrite). - // File.Move(path + ".tmp", path); - - // _currentWorld = path; - // FilenameLabel.Text = Path.GetFileName(path); - // LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm"); - // QuickSaveButton.Visible = true; - // SaveAsButton.Text = "Save As..."; + var server = this.GetClient().GetNode(nameof(IntegratedServer)).Server; + var save = new WorldSave { Playtime = _playtime }; + save.WriteDataFromWorld(server.GetWorld()); + save.WriteToFile(path); + + _currentWorld = path; + FilenameLabel.Text = System.IO.Path.GetFileName(path); + LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm"); + QuickSaveButton.Visible = true; + SaveAsButton.Text = "Save As..."; } private void _on_LoadFrom_pressed() @@ -106,25 +100,20 @@ public class EscapeMenuWorld : CenterContainer private void _on_LoadFileDialog_file_selected(string path) { - // var server = this.GetClient().GetNode(nameof(IntegratedServer)).Server; - // var save = Save.ReadFromFile(path); - - // // Clear out all objects that have a SaveAttribute. - // var objectsToRemove = server.Objects.Select(x => x.Item2) - // .Where(x => SaveRegistry.GetOrNull(x.GetType()) != null).ToArray(); - // foreach (var obj in objectsToRemove) obj.RemoveFromParent(); - - // // Reset players' positions. - // foreach (var (id, player) in server.Players) - // player.RPC(new []{ id }, player.ResetPosition, Vector2.Zero); - - // save.AddToWorld(server); - // _playtime = save.Playtime; - - // _currentWorld = path; - // FilenameLabel.Text = Path.GetFileName(path); - // LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm"); - // QuickSaveButton.Visible = true; - // SaveAsButton.Text = "Save As..."; + var server = this.GetClient().GetNode(nameof(IntegratedServer)).Server; + var save = WorldSave.ReadFromFile(path); + + // Reset players' positions. + foreach (var player in server.GetWorld().Players) + player.RpcId(player.NetworkID, nameof(LocalPlayer.ResetPosition), Vector2.Zero); + + save.ReadDataIntoWorld(server.GetWorld()); + _playtime = save.Playtime; + + _currentWorld = path; + FilenameLabel.Text = System.IO.Path.GetFileName(path); + LastSavedLabel.Text = save.LastSaved.ToString("yyyy-MM-dd HH:mm"); + QuickSaveButton.Visible = true; + SaveAsButton.Text = "Save As..."; } } diff --git a/src/Network/WorldSave.cs b/src/Network/WorldSave.cs new file mode 100644 index 0000000..c73857d --- /dev/null +++ b/src/Network/WorldSave.cs @@ -0,0 +1,82 @@ +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 = 0; + + 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 List<(BlockPos, Color, bool)> Blocks { 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 support multiple versions. + save.Version = reader.ReadUInt16(); + if (save.Version != LATEST_VERSION) throw new IOException( + $"Version does not match ({save.Version} != {LATEST_VERSION})"); + + save.Playtime = TimeSpan.FromSeconds(reader.ReadUInt32()); + + var numBlocks = reader.ReadInt32(); + save.Blocks = new List<(BlockPos, Color, bool)>(); + for (var i = 0; i < numBlocks; i++) + save.Blocks.Add((new BlockPos(reader.ReadInt32(), reader.ReadInt32()), + new Color(reader.ReadInt32()), + reader.ReadBoolean())); + } + } + 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(Blocks.Count); + foreach (var (position, color, unbreakable) in Blocks) { + writer.Write(position.X); + writer.Write(position.Y); + writer.Write(color.ToRgba32()); + writer.Write(unbreakable); + } + } + } + new Godot.Directory().Rename(path + ".tmp", path); + LastSaved = File.GetLastWriteTime(path); + } + + + public void WriteDataFromWorld(World world) + => Blocks = world.Blocks.Select(block => (block.Position, block.Color, block.Unbreakable)).ToList(); + + public void ReadDataIntoWorld(World world) + { + world.Rpc(nameof(World.ClearBlocks)); + foreach (var (position, color, unbreakable) in Blocks) + world.Rpc(nameof(World.SpawnBlock), position.X, position.Y, color, unbreakable); + } +} diff --git a/src/Utility/Extensions.cs b/src/Utility/Extensions.cs index 5db1dd0..c15d3a5 100644 --- a/src/Utility/Extensions.cs +++ b/src/Utility/Extensions.cs @@ -18,6 +18,12 @@ public static class Extensions => node.GetGame() as Server; public static World GetWorld(this Node node) => node.GetGame().GetNode("World"); + + public static void RemoveFromParent(this Node node) + { + node.GetParent().RemoveChild(node); + node.QueueFree(); + } } public interface IInitializable diff --git a/src/World.cs b/src/World.cs index 613f3a4..e3b4a20 100644 --- a/src/World.cs +++ b/src/World.cs @@ -28,22 +28,15 @@ public class World : Node => PlayerContainer.GetChildren().Cast(); public Player GetPlayer(int networkID) => PlayerContainer.GetNode(networkID.ToString()); + public void ClearPlayers() + { foreach (var player in Players) player.RemoveFromParent(); } + public IEnumerable Blocks + => BlockContainer.GetChildren().Cast(); public Block GetBlockAt(BlockPos position) => BlockContainer.GetNodeOrNull(position.ToString()); - - - public void Clear() - { - foreach (var player in Players) { - BlockContainer.RemoveChild(player); - player.QueueFree(); - } - foreach (var node in BlockContainer.GetChildren().Cast()) { - BlockContainer.RemoveChild(node); - node.QueueFree(); - } - } + [PuppetSync] public void ClearBlocks() + { foreach (var block in Blocks) block.RemoveFromParent(); } [PuppetSync]