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;
}