XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AshfallFrontier

【Unity】灰烬边境,一款生存策略游戏

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace Ashfall.Core
{
    public interface IWorldCodec
    {
        string Encode(WorldState world);
        WorldState Decode(string json);
    }

    /// <summary>Checksummed snapshots, atomic replacement, three independently readable backups.</summary>
    public sealed class SaveStore
    {
        private readonly string path;
        private readonly IWorldCodec codec;
        public string LastLoadedPath { get; private set; }
        public SaveStore(string path, IWorldCodec codec)
        { this.path = path; this.codec = codec; }
        private static string Digest(string value)
        { using (var hash = SHA256.Create()) return Convert.ToBase64String(hash.ComputeHash(Encoding.UTF8.GetBytes(value))); }
        private string Envelope(WorldState world)
        {
            string json = codec.Encode(world);
            return "ASHFALL/1\n" + Digest(json) + "\n" + json;
        }
        public void Save(WorldState world)
        {
            Validate(world);
            string directory = Path.GetDirectoryName(Path.GetFullPath(path)); Directory.CreateDirectory(directory);
            string temporary = path + ".pending";
            byte[] bytes = Encoding.UTF8.GetBytes(Envelope(world));
            using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
            { stream.Write(bytes, 0, bytes.Length); stream.Flush(true); }
            // Never rotate a corrupt primary over a known-good backup.
            bool primaryGood = false;
            if (File.Exists(path)) { try { Read(path); primaryGood = true; } catch (Exception ex) when (Recoverable(ex)) { } }
            if (primaryGood)
            {
                for (int i = 3; i >= 2; i--) if (File.Exists(path + ".bak" + (i - 1))) File.Copy(path + ".bak" + (i - 1), path + ".bak" + i, true);
                File.Copy(path, path + ".bak1", true);
            }
            if (File.Exists(path)) File.Replace(temporary, path, null);
            else File.Move(temporary, path);
        }
        public WorldState Load()
        {
            Exception last = null;
            foreach (var candidate in new[] { path, path + ".bak1", path + ".bak2", path + ".bak3" })
            {
                if (!File.Exists(candidate)) continue;
                try { var world = Read(candidate); LastLoadedPath = candidate; return world; }
                catch (Exception ex) when (Recoverable(ex)) { last = ex; }
            }
            throw new InvalidDataException("没有可恢复的有效存档。原文件与备份未被修改。", last);
        }
        public bool Exists => File.Exists(path) || Enumerable.Range(1, 3).Any(i => File.Exists(path + ".bak" + i));
        private WorldState Read(string candidate)
        {
            string data = File.ReadAllText(candidate, Encoding.UTF8);
            int a = data.IndexOf('\n'), b = a < 0 ? -1 : data.IndexOf('\n', a + 1);
            if (a < 0 || b < 0 || data.Substring(0, a) != "ASHFALL/1") throw new InvalidDataException("不支持的存档格式");
            string json = data.Substring(b + 1), checksum = data.Substring(a + 1, b - a - 1);
            if (Digest(json) != checksum) throw new InvalidDataException("存档校验失败");
            WorldState world;
            try { world = codec.Decode(json); }
            catch (Exception ex) { throw new InvalidDataException("无法读取存档数据", ex); }
            Validate(world); return world;
        }
        private static bool Recoverable(Exception ex) => ex is IOException || ex is InvalidDataException || ex is ArgumentException || ex is InvalidOperationException;
        public static void Validate(WorldState w)
        {
            if (w == null || w.version != WorldState.CurrentVersion || w.spaces == null || w.players == null || w.vehicles == null
                || w.facilities == null || w.portals == null || w.pieces == null || w.enemies == null || w.resources == null || w.research == null
                || w.warehouse == null || w.shots == null || double.IsNaN(w.time) || double.IsInfinity(w.time) || w.time < 0)
                throw new InvalidDataException("无效世界快照");
            if (w.spaces.Any(s => s == null || string.IsNullOrEmpty(s.id) || s.floors == null || s.floors.Count == 0
                    || s.floors.Any(f => f == null || f.walls == null || !f.bounds.center.Finite || !f.bounds.size.Finite || f.walls.Any(wall => wall == null)))
                || w.players.Any(p => p == null || p.input == null) || w.vehicles.Any(v => v == null || v.stations == null || v.hardpoints == null
                    || v.stations.Any(s => s == null || s.location == null) || v.hardpoints.Any(h => h == null) || !v.position.Finite)
                || w.facilities.Any(f => f == null || f.location == null || f.jobs == null || f.jobs.Any(j => j == null))
                || w.portals.Any(p => p == null || p.from == null || p.to == null)
                || w.pieces.Any(p => p == null || p.location == null) || w.enemies.Any(e => e == null || e.location == null)
                || w.resources.Any(r => r == null || r.location == null)) throw new InvalidDataException("存档缺少必要实体数据");
            if (w.spaces.Select(s => s.id).Distinct().Count() != w.spaces.Count || w.players.Select(p => p.id).Distinct().Count() != w.players.Count)
                throw new InvalidDataException("重复实体 ID");
            if (w.Space("world") == null || w.players.Any(p => p.location == null || !p.location.position.Finite || !Enum.IsDefined(typeof(BroodForm), p.broodForm)))
                throw new InvalidDataException("无效角色位置或形态");
            foreach (var inventory in w.players.Select(p => p.inventory).Concat(w.vehicles.Select(v => v.cargo)).Concat(new[] { w.warehouse }))
                if (inventory == null || inventory.items == null || inventory.capacity <= 0 || inventory.items.Any(i => i == null || i.amount < 0 || string.IsNullOrEmpty(i.id))
                    || inventory.items.Sum(i => (long)i.amount) > inventory.capacity || inventory.items.Select(i => i.id).Distinct().Count() != inventory.items.Count)
                    throw new InvalidDataException("无效库存");
        }
    }
}