diff --git a/tools/AetherDb/AetherDb.csproj b/tools/AetherDb/AetherDb.csproj index 2287148e9..fd4c8067a 100644 --- a/tools/AetherDb/AetherDb.csproj +++ b/tools/AetherDb/AetherDb.csproj @@ -12,5 +12,7 @@ + + diff --git a/tools/AetherDb/AuthoredSettings.cs b/tools/AetherDb/AuthoredSettings.cs index 551613d18..d5b73c37c 100644 --- a/tools/AetherDb/AuthoredSettings.cs +++ b/tools/AetherDb/AuthoredSettings.cs @@ -3,58 +3,41 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Unity.Mathematics; +using YamlDotNet.RepresentationModel; // Reads the authored gameplay and generation settings out of Assets/Resources/Settings.asset, the Unity asset whose // GameSettings ScriptableObject holds plain ServerShared classes. Those classes carry curves and tiers with no // defaults, so a fixture that invented numbers would measure a game nobody plays; this reads the real ones. // -// The asset is Unity YAML whose leaves under these subtrees are scalars, sequences of mappings, or float3 as x/y/z. -// Parsed by indentation and matched to fields by name, so a renamed or added field simply goes unset rather than -// silently mapping to the wrong thing. Anything it could not place is reported by Unplaced. +// Unity assets are YAML, tags and all, so YamlDotNet does the parsing. What remains here is mapping a parsed node +// onto a settings class by field name: an unknown key goes to Unplaced rather than silently landing somewhere wrong. public sealed class AuthoredSettings { - private readonly Node _root; + private readonly YamlMappingNode _root; public List Unplaced { get; } = new List(); - private AuthoredSettings(Node root) => _root = root; - - // Prints the parsed tree under a subtree, for checking that sequences landed on the field that declares them - public void Dump(string subtree, int maxDepth = 3) - { - var node = Find(_root, subtree); - if (node == null) - { - Console.WriteLine($"{subtree}: not found"); - return; - } - Console.WriteLine($"{subtree}: {node.Children.Count} children, {node.Items.Count} items"); - DumpNode(node, 1, maxDepth); - } - - private static void DumpNode(Node node, int depth, int maxDepth) - { - if (depth > maxDepth) return; - var pad = new string(' ', depth * 2); - foreach (var child in node.Children) - { - Console.WriteLine($"{pad}{child.Key}: scalar={child.Value.Scalar ?? "-"} children={child.Value.Children.Count} items={child.Value.Items.Count}"); - DumpNode(child.Value, depth + 1, maxDepth); - } - for (var i = 0; i < node.Items.Count; i++) - Console.WriteLine($"{pad}[{i}] scalar={node.Items[i].Scalar ?? "-"} children={node.Items[i].Children.Count}"); - } + private AuthoredSettings(YamlMappingNode root) => _root = root; public static AuthoredSettings Load(string repositoryRoot) { var path = Path.Combine(repositoryRoot, "Assets", "Resources", "Settings.asset"); if (!File.Exists(path)) throw new FileNotFoundException($"no settings asset at {path}"); - return new AuthoredSettings(Parse(File.ReadAllLines(path))); + + var stream = new YamlStream(); + using (var reader = new StreamReader(path)) stream.Load(reader); + + // A Unity asset is one document per object; the settings live under the MonoBehaviour mapping + var document = stream.Documents + .Select(d => d.RootNode as YamlMappingNode) + .FirstOrDefault(n => n != null && n.Children.Keys.OfType().Any(k => k.Value == "MonoBehaviour")); + if (document == null) throw new InvalidDataException($"{path} holds no MonoBehaviour document"); + + return new AuthoredSettings((YamlMappingNode) document.Children[new YamlScalarNode("MonoBehaviour")]); } // Fills a new instance of T from the named subtree, by field name @@ -67,161 +50,125 @@ public sealed class AuthoredSettings return value; } - private Node Find(Node node, string key) + // Prints the parsed shape under a subtree + public void Dump(string subtree, int maxDepth = 2) { - if (node.Children.TryGetValue(key, out var found)) return found; - foreach (var child in node.Children.Values) + var node = Find(_root, subtree); + if (node == null) { - var deeper = Find(child, key); + Console.WriteLine($"{subtree}: not found"); + return; + } + Console.WriteLine($"{subtree}:"); + DumpNode(node, 1, maxDepth); + } + + private static void DumpNode(YamlNode node, int depth, int maxDepth) + { + if (depth > maxDepth) return; + var pad = new string(' ', depth * 2); + switch (node) + { + case YamlMappingNode mapping: + foreach (var child in mapping.Children) + { + Console.WriteLine($"{pad}{child.Key}: {Describe(child.Value)}"); + DumpNode(child.Value, depth + 1, maxDepth); + } + break; + case YamlSequenceNode sequence: + for (var i = 0; i < sequence.Children.Count; i++) + { + Console.WriteLine($"{pad}[{i}] {Describe(sequence.Children[i])}"); + DumpNode(sequence.Children[i], depth + 1, maxDepth); + } + break; + } + } + + private static string Describe(YamlNode node) => node switch + { + YamlScalarNode scalar => scalar.Value, + YamlSequenceNode sequence => $"<{sequence.Children.Count} items>", + YamlMappingNode mapping => $"<{mapping.Children.Count} keys>", + _ => "" + }; + + private static YamlMappingNode Find(YamlNode node, string key) + { + if (!(node is YamlMappingNode mapping)) return null; + foreach (var child in mapping.Children) + { + if (child.Key is YamlScalarNode scalar && scalar.Value == key) return child.Value as YamlMappingNode; + var deeper = Find(child.Value, key); if (deeper != null) return deeper; } return null; } - private void Populate(object target, Node node, string path) + private void Populate(object target, YamlMappingNode node, string path) { foreach (var entry in node.Children) { - var field = target.GetType().GetField(entry.Key, BindingFlags.Public | BindingFlags.Instance); + if (!(entry.Key is YamlScalarNode key)) continue; + var field = target.GetType().GetField(key.Value, BindingFlags.Public | BindingFlags.Instance); if (field == null) { - Unplaced.Add($"{path}.{entry.Key}"); + Unplaced.Add($"{path}.{key.Value}"); continue; } - var converted = Convert(entry.Value, field.FieldType, $"{path}.{entry.Key}"); + var converted = Convert(entry.Value, field.FieldType, $"{path}.{key.Value}"); if (converted != null) field.SetValue(target, converted); } } - private object Convert(Node node, Type type, string path) + private object Convert(YamlNode node, Type type, string path) { - if (type == typeof(float)) return float.TryParse(node.Scalar, out var f) ? f : 0f; - if (type == typeof(int)) return int.TryParse(node.Scalar, out var i) ? i : 0; - if (type == typeof(uint)) return uint.TryParse(node.Scalar, out var u) ? u : 0u; - if (type == typeof(bool)) return node.Scalar == "1" || string.Equals(node.Scalar, "true", StringComparison.OrdinalIgnoreCase); - if (type == typeof(string)) return node.Scalar; + if (node is YamlScalarNode scalar) + { + if (type == typeof(float)) return float.TryParse(scalar.Value, out var f) ? f : 0f; + if (type == typeof(int)) return int.TryParse(scalar.Value, out var i) ? i : 0; + if (type == typeof(uint)) return uint.TryParse(scalar.Value, out var u) ? u : 0u; + if (type == typeof(bool)) return scalar.Value == "1" || string.Equals(scalar.Value, "true", StringComparison.OrdinalIgnoreCase); + if (type == typeof(string)) return scalar.Value; + Unplaced.Add($"{path} (scalar into {type.Name})"); + return null; + } - if (type == typeof(float3)) - return float3(Leaf(node, "x"), Leaf(node, "y"), Leaf(node, "z")); - - if (type.IsArray) + if (node is YamlSequenceNode sequence && type.IsArray) { var elementType = type.GetElementType(); - var array = Array.CreateInstance(elementType, node.Items.Count); - for (var index = 0; index < node.Items.Count; index++) - { - // A sequence of scalars carries its value on the item itself; a sequence of mappings has children - var item = node.Items[index]; - var converted = item.Scalar != null && item.Children.Count == 0 - ? Convert(new Node { Scalar = item.Scalar }, elementType, $"{path}[{index}]") - : Convert(item, elementType, $"{path}[{index}]"); - array.SetValue(converted, index); - } + var array = Array.CreateInstance(elementType, sequence.Children.Count); + for (var index = 0; index < sequence.Children.Count; index++) + array.SetValue(Convert(sequence.Children[index], elementType, $"{path}[{index}]"), index); return array; } - if (type.IsClass) + if (node is YamlMappingNode mapping) { - var value = Activator.CreateInstance(type); - Populate(value, node, path); - return value; + if (type == typeof(float3)) + return new float3(Leaf(mapping, "x"), Leaf(mapping, "y"), Leaf(mapping, "z")); + + // A Unity object reference ({fileID, guid}) is an asset nothing here reads + if (mapping.Children.Keys.OfType().Any(k => k.Value == "fileID")) return null; + + if (type.IsClass) + { + var value = Activator.CreateInstance(type); + Populate(value, mapping, path); + return value; + } } - Unplaced.Add($"{path} (unsupported type {type.Name})"); + Unplaced.Add($"{path} (unsupported {node.NodeType} into {type.Name})"); return null; } - private static float Leaf(Node node, string key) => - node.Children.TryGetValue(key, out var child) && float.TryParse(child.Scalar, out var value) ? value : 0f; - - private static float3 float3(float x, float y, float z) => new float3(x, y, z); - - // One YAML node: a scalar, a mapping of children, or a sequence of items. - private sealed class Node - { - public string Scalar; - public Dictionary Children { get; } = new Dictionary(); - public List Items { get; } = new List(); - } - - // Indentation-driven parse of the three shapes this asset uses: - // key: value / key: a scalar or a mapping - // - value a sequence of scalars (NeutralFactions, MinimapZoomLevels) - // - key: value + deeper keys a sequence of mappings (Tiers, BodySettingsCollections) - // A mapping item's first key sits at the dash's own column while its siblings are indented past it, so one - // frame stands for the whole item and accepts both depths. Inline flow mappings ({fileID: ...}) are Unity - // object references, which nothing here reads, so they are kept as raw scalars rather than half parsed. - private static Node Parse(string[] lines) - { - var root = new Node(); - // A frame is either a mapping a key opened, or one item of a sequence. The distinction matters because a - // dash shares its column with its container: an item must pop the previous item without popping the - // container that holds them both. - var stack = new List<(int indent, bool isItemFrame, Node node)> { (-1, false, root) }; - - foreach (var raw in lines) - { - var trimmed = raw.TrimEnd(); - if (trimmed.Length == 0) continue; - - var indent = trimmed.Length - trimmed.TrimStart(' ').Length; - var content = trimmed.TrimStart(' '); - if (content.StartsWith("#") || content.StartsWith("%") || content.StartsWith("---")) continue; - - var isItem = content.StartsWith("- "); - if (isItem) content = content.Substring(2).TrimStart(); - - // A key pops every frame at or inside its column. A dash pops frames deeper than its column, plus any - // item frame at its column (the preceding item of the same sequence), leaving that sequence's container. - while (stack.Count > 1) - { - var top = stack[stack.Count - 1]; - var pop = isItem - ? top.indent > indent || (top.indent == indent && top.isItemFrame) - : top.indent >= indent; - if (!pop) break; - stack.RemoveAt(stack.Count - 1); - } - var container = stack[stack.Count - 1].node; - - if (isItem) - { - if (content.StartsWith("{")) - { - container.Items.Add(new Node { Scalar = content }); - continue; - } - - var item = new Node(); - container.Items.Add(item); - // The item's own keys sit at this column and deeper, so its frame claims the dash's column - stack.Add((indent, true, item)); - container = item; - - if (!content.Contains(":")) - { - item.Scalar = content; - continue; - } - } - - var separator = content.IndexOf(':', StringComparison.Ordinal); - if (separator < 0) continue; - - var key = content.Substring(0, separator); - var value = content.Substring(separator + 1).Trim(); - if (value.Length == 0) - { - // A key with no value opens a container. Its frame sits past its own column so that a sequence - // whose dashes share that column still attaches to it rather than to its parent. - var child = new Node(); - container.Children[key] = child; - stack.Add((indent, false, child)); - } - else container.Children[key] = new Node { Scalar = value }; - } - - return root; - } + private static float Leaf(YamlMappingNode mapping, string key) => + mapping.Children.TryGetValue(new YamlScalarNode(key), out var child) + && child is YamlScalarNode scalar + && float.TryParse(scalar.Value, out var value) + ? value + : 0f; }