using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; 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 = []; /// Reserved def-file "type" that carries content patches rather than defs. public const string PatchTypeKey = "Patch"; private sealed record PatchRule(string DefType, Regex Match, JsonObject Set); private sealed record ValidationRule(string Field, Regex Pattern, string Description); private readonly List _patches = []; private readonly Dictionary> _validators = new( StringComparer.OrdinalIgnoreCase ); 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); } /// /// Registers a load-time validation: the string of every resolved def of /// type must match (a regex), or /// throws. Non-string or absent fields are skipped. Use it to enforce naming /// conventions (e.g. gene ids start with Gene) or key/format rules across a mod's content. /// public void RegisterValidator( string typeKey, string field, string pattern, string? description = null ) { Regex regex; try { regex = new Regex(pattern, RegexOptions.CultureInvariant); } catch (ArgumentException error) { throw new ArgumentException($"Invalid validator regex '{pattern}': {error.Message}"); } if (!_validators.TryGetValue(typeKey, out var list)) { list = []; _validators[typeKey] = list; } list.Add(new ValidationRule(field, regex, description ?? $"pattern /{pattern}/")); } /// /// 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); } } ApplyPatches(); 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 (string.Equals(typeKey, PatchTypeKey, StringComparison.OrdinalIgnoreCase)) { LoadPatches(mod, file, root); return; } 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; // поздний мод/файл полностью заменяет одноимённый деф } } // Парсит файл-патч: операции { defType, match (регэксп по defName), set: {поля} }. private void LoadPatches(Mod mod, string file, JsonNode root) { if (root["patches"] is not JsonArray patches) { throw new InvalidDataException( $"Patch file '{file}' (mod '{mod.Id}') has no \"patches\" array." ); } foreach (var node in patches) { if (node is not JsonObject patch) { throw new InvalidDataException( $"Patch file '{file}' (mod '{mod.Id}') contains a non-object patch." ); } var defType = patch["defType"]?.GetValue() ?? throw new InvalidDataException($"A patch in '{file}' has no \"defType\"."); var match = patch["match"]?.GetValue() ?? throw new InvalidDataException($"A patch in '{file}' has no \"match\"."); if (patch["set"] is not JsonObject set) { throw new InvalidDataException($"A patch in '{file}' has no \"set\" object."); } Regex regex; try { regex = new Regex(match, RegexOptions.CultureInvariant); } catch (ArgumentException error) { throw new InvalidDataException( $"Patch in '{file}' has invalid regex '{match}': {error.Message}" ); } _patches.Add(new PatchRule(defType, regex, (JsonObject)set.DeepClone())); } } // Применяет патчи к сырым дефам (до резолва), в порядке загрузки: каждому дефу нужного типа, // чьё имя подходит под регэксп, проставляются поля set. Поля наследуются детьми как обычно. private void ApplyPatches() { foreach (var patch in _patches) { if (!_byKey.TryGetValue(patch.DefType, out var entry)) { throw new InvalidDataException( $"A patch targets unknown def type '{patch.DefType}'." ); } foreach (var raw in entry.Raw) { if (!patch.Match.IsMatch(raw.Key)) { continue; } foreach (var (key, value) in patch.Set) { raw.Value[key] = value?.DeepClone(); } } } } private void Resolve(TypeEntry entry) { _validators.TryGetValue(entry.Key, out var rules); foreach (var defName in entry.Raw.Keys.Order(StringComparer.Ordinal)) { var merged = MergeChain(entry, defName, []); if (merged["abstract"]?.GetValue() == true) { continue; } if (rules is not null) { Validate(entry.Key, defName, merged, rules); } 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 void Validate( string typeKey, string defName, JsonObject merged, List rules ) { foreach (var rule in rules) { if ( merged[rule.Field] is JsonValue value && value.TryGetValue(out var text) && !rule.Pattern.IsMatch(text) ) { throw new InvalidDataException( $"Def '{defName}' ({typeKey}) field '{rule.Field}'=\"{text}\" violates {rule.Description}." ); } } } 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; } }