using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using MiGu.Server.Launcher; namespace MiGu.Server.Signal; public sealed record SignalColumn(string Key, string Label, string Type, IReadOnlyList? Options = null, string? Group = null); public sealed record SignalTableDef( string Id, string Title, string Category, string FileName, IReadOnlyList Columns); /// /// 读写 Simple3 工作目录 Config/Signal/*.json,供迷毂「数据中心」表格编辑。 /// 表结构只从 Simple3 plugins/StandardScene.Signal.dll 反射;无插件时才读 signal-tables.json。 /// public sealed class SignalDataStore { private static readonly JsonSerializerOptions FileJson = new() { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; private static readonly object FileLock = new(); private readonly Simple3Launcher _launcher; private IReadOnlyList? _cachedTables; private long _cachedSignature; public SignalDataStore(Simple3Launcher launcher) => _launcher = launcher; public IReadOnlyList GetTables() { var sig = SignalTableManifestLoader.ComputeManifestSignature(_launcher); if (_cachedTables == null || sig != _cachedSignature) { _cachedTables = SignalTableManifestLoader.Load(_launcher); _cachedSignature = sig; } return _cachedTables; } public SignalTableDef? Find(string id) => GetTables().FirstOrDefault(t => string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase)); public string? ResolveWorkingDirectory() => _launcher.ResolveWorkingDirectory(); public (string? Path, string? Error) ResolveFile(SignalTableDef table, bool createDir) { var wd = _launcher.ResolveWorkingDirectory(); if (string.IsNullOrWhiteSpace(wd)) return (null, "未找到 Simple3 工作目录,无法定位 Config/Signal"); if (!TryResolveUnderSignalDir(wd, table.FileName, out var dest, out var pathError)) return (null, pathError); if (File.Exists(dest)) return (dest, null); if (createDir) { try { var dir = Path.GetDirectoryName(dest); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); } catch (Exception ex) { return (null, ex.Message); } } return (dest, null); } public JsonArray LoadRows(SignalTableDef table) { var (path, _) = ResolveFile(table, createDir: false); if (path == null || !File.Exists(path)) return new JsonArray(); lock (FileLock) { var json = File.ReadAllText(path); if (string.IsNullOrWhiteSpace(json)) return new JsonArray(); var node = JsonNode.Parse(json); if (node is JsonArray arr) return arr; return new JsonArray(); } } public void SaveRows(SignalTableDef table, JsonArray rows) { var (path, error) = ResolveFile(table, createDir: true); if (path == null) throw new InvalidOperationException(error ?? "无法解析信号配置文件路径"); var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); lock (FileLock) File.WriteAllText(path, rows.ToJsonString(FileJson)); } private static bool TryResolveUnderSignalDir(string workingDirectory, string fileName, out string dest, out string error) { dest = ""; error = ""; if (string.IsNullOrWhiteSpace(fileName) || Path.IsPathRooted(fileName) || fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) { error = "信号表文件名非法"; return false; } var root = Path.GetFullPath(Path.Combine(workingDirectory, "Config", "Signal")); dest = Path.GetFullPath(Path.Combine(root, fileName)); var prefix = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; if (!dest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { error = "信号表路径超出 Config/Signal"; dest = ""; return false; } return true; } }