You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
5.5 KiB
136 lines
5.5 KiB
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); |
|
} |
|
} |
|
}
|
|
|