using System.Text.Json; using System.Text.Json.Nodes; namespace MrGameEng.Mods; /// /// Database of data definitions loaded from mod JSON. Each file under a mod's /// Defs/ folder is an envelope { "type": "<key>", "defs": [ … ] }; the /// game registers the CLR type for every key before . Defs from later /// mods replace same-named defs of earlier mods; parent chains are merged /// field-by-field (own fields win, nested objects are replaced whole); defs marked /// abstract serve only as parents. /// public sealed class DefDatabase { private sealed class TypeEntry { public required string Key; public required Type ClrType; public readonly Dictionary Raw = new(StringComparer.Ordinal); public readonly SortedDictionary Resolved = new(StringComparer.Ordinal); } private readonly Dictionary _byKey = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _byType = []; private static readonly JsonDocumentOptions DocumentOptions = new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; /// Registers the CLR type behind a def-type key (the "type" field of def files). public void RegisterType(string typeKey) where T : Def { var entry = new TypeEntry { Key = typeKey, ClrType = typeof(T) }; if (!_byKey.TryAdd(typeKey, entry)) { throw new InvalidOperationException($"Def type '{typeKey}' is already registered."); } _byType.Add(typeof(T), entry); } /// /// Loads every Defs/**/*.json of (in load order) and /// resolves inheritance. Call once after registering all def types. /// public void Load(IReadOnlyList mods) { foreach (var mod in mods) { var defsDir = mod.ContentPath("Defs"); if (!Directory.Exists(defsDir)) { continue; } var files = Directory .EnumerateFiles(defsDir, "*.json", SearchOption.AllDirectories) .OrderBy(f => f, StringComparer.Ordinal); foreach (var file in files) { LoadFile(mod, file); } } foreach (var entry in _byKey.Values) { Resolve(entry); } } /// Returns the def of type named ; throws when missing. public T Get(string defName) where T : Def => TryGet(defName, out var def) ? def : throw new KeyNotFoundException($"No {typeof(T).Name} def named '{defName}'."); /// Returns the def of type named , or false. public bool TryGet(string defName, out T def) where T : Def { if (Entry().Resolved.TryGetValue(defName, out var found)) { def = (T)found; return true; } def = null!; return false; } /// All resolved defs of type , sorted by def name (deterministic). public IReadOnlyList All() where T : Def => Entry().Resolved.Values.Cast().ToList(); /// Registered def-type keys, sorted. public IReadOnlyList TypeKeys => _byKey.Values.Select(e => e.Key).Order(StringComparer.Ordinal).ToList(); /// Resolved def names of the given type key, sorted; empty for unknown keys. public IReadOnlyList NamesOf(string typeKey) => _byKey.TryGetValue(typeKey, out var entry) ? entry.Resolved.Keys.ToList() : []; private TypeEntry Entry() where T : Def => _byType.TryGetValue(typeof(T), out var entry) ? entry : throw new InvalidOperationException($"Def type {typeof(T).Name} is not registered."); private void LoadFile(Mod mod, string file) { JsonNode root; try { root = JsonNode.Parse(File.ReadAllText(file), documentOptions: DocumentOptions) ?? throw new InvalidDataException("file is empty"); } catch (JsonException exception) { throw new InvalidDataException( $"Invalid def file '{file}' (mod '{mod.Id}'): {exception.Message}", exception ); } var typeKey = root["type"]?.GetValue() ?? throw new InvalidDataException( $"Def file '{file}' (mod '{mod.Id}') has no \"type\" field." ); if (!_byKey.TryGetValue(typeKey, out var entry)) { throw new InvalidDataException( $"Def file '{file}' (mod '{mod.Id}') uses unknown def type '{typeKey}'; " + $"registered: {string.Join(", ", TypeKeys)}." ); } if (root["defs"] is not JsonArray defs) { throw new InvalidDataException( $"Def file '{file}' (mod '{mod.Id}') has no \"defs\" array." ); } foreach (var node in defs) { if (node is not JsonObject def) { throw new InvalidDataException( $"Def file '{file}' (mod '{mod.Id}') contains a non-object def entry." ); } var defName = def["defName"]?.GetValue(); if (string.IsNullOrWhiteSpace(defName)) { throw new InvalidDataException( $"A def in '{file}' (mod '{mod.Id}') has no \"defName\"." ); } entry.Raw[defName] = def; // поздний мод/файл полностью заменяет одноимённый деф } } private static void Resolve(TypeEntry entry) { foreach (var defName in entry.Raw.Keys.Order(StringComparer.Ordinal)) { var merged = MergeChain(entry, defName, []); if (merged["abstract"]?.GetValue() == true) { continue; } var def = (Def?)merged.Deserialize(entry.ClrType, ModInfo.JsonOptions) ?? throw new InvalidDataException( $"Def '{defName}' ({entry.Key}) deserialized to null." ); entry.Resolved[defName] = def; } } private static JsonObject MergeChain(TypeEntry entry, string defName, HashSet seen) { if (!seen.Add(defName)) { throw new InvalidDataException( $"Cyclic def inheritance involving '{defName}' ({entry.Key})." ); } if (!entry.Raw.TryGetValue(defName, out var node)) { throw new InvalidDataException($"Unknown parent def '{defName}' ({entry.Key})."); } var parentName = node["parent"]?.GetValue(); if (parentName is null) { return (JsonObject)node.DeepClone(); } var merged = MergeChain(entry, parentName, seen); merged.Remove("abstract"); // абстрактность не наследуется merged.Remove("defName"); foreach (var (key, value) in node) { merged[key] = value?.DeepClone(); } return merged; } }