mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-01 17:47:24 +02:00
531 lines
17 KiB
C#
531 lines
17 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using Robust.Shared.Serialization.Markdown.Value;
|
|
using Robust.Shared.Utility;
|
|
using YamlDotNet.Core;
|
|
using YamlDotNet.RepresentationModel;
|
|
|
|
namespace Robust.Shared.Serialization.Markdown.Mapping
|
|
{
|
|
public sealed class MappingDataNode : DataNode<MappingDataNode>, IDictionary<string, DataNode>
|
|
{
|
|
private readonly Dictionary<string, DataNode> _children;
|
|
private readonly List<KeyValuePair<string,DataNode>> _list;
|
|
|
|
/// <summary>
|
|
/// ValueDataNodes associated with each key. This is used for yaml validation / error reporting.
|
|
/// I.e., if a key is meant to be an EntityPrototype ID, we want to print an error that points to the
|
|
/// corresponding yaml lines.
|
|
/// </summary>
|
|
private IReadOnlyDictionary<string, ValueDataNode>? _keyNodes;
|
|
// TODO avoid populating this unless we are running the yaml linter?
|
|
|
|
public override bool IsEmpty => _children.Count == 0;
|
|
public int Count => _children.Count;
|
|
public bool IsReadOnly => false;
|
|
public IReadOnlyDictionary<string, DataNode> Children => _children;
|
|
|
|
public MappingDataNode() : base(NodeMark.Invalid, NodeMark.Invalid)
|
|
{
|
|
_children = new();
|
|
_list = new();
|
|
}
|
|
|
|
public MappingDataNode(int size) : base(NodeMark.Invalid, NodeMark.Invalid)
|
|
{
|
|
_children = new(size);
|
|
_list = new(size);
|
|
}
|
|
|
|
public MappingDataNode(YamlMappingNode mapping) : base(mapping.Start, mapping.End)
|
|
{
|
|
_children = new(mapping.Children.Count);
|
|
_list = new(mapping.Children.Count);
|
|
var keyNodes = new Dictionary<string, ValueDataNode>(mapping.Children.Count);
|
|
foreach (var (keyNode, val) in mapping.Children)
|
|
{
|
|
if (keyNode is not YamlScalarNode scalarNode)
|
|
throw new NotSupportedException("Mapping data nodes must have a scalar keys");
|
|
|
|
var valueNode = new ValueDataNode(scalarNode);
|
|
Add(valueNode.Value, val.ToDataNode());
|
|
keyNodes.Add(valueNode.Value, valueNode);
|
|
}
|
|
|
|
_keyNodes = keyNodes;
|
|
Tag = mapping.Tag.IsEmpty ? null : mapping.Tag.Value;
|
|
}
|
|
|
|
public MappingDataNode(Dictionary<string, DataNode> nodes) : base(NodeMark.Invalid, NodeMark.Invalid)
|
|
{
|
|
_children = new(nodes);
|
|
_list = new(_children);
|
|
}
|
|
|
|
public KeyValuePair<string, DataNode> this[int key] => _list[key];
|
|
|
|
public MappingDataNode Add(string key, DataNode node)
|
|
{
|
|
_children.Add(key, node);
|
|
_list.Add(new(key, node));
|
|
return this;
|
|
}
|
|
|
|
public DataNode this[string key]
|
|
{
|
|
get => _children[key];
|
|
set
|
|
{
|
|
if (_children.TryAdd(key, value))
|
|
{
|
|
_list.Add(new(key, value));
|
|
return;
|
|
}
|
|
|
|
var index = IndexOf(key);
|
|
if (index == -1)
|
|
throw new Exception("Key exists in Children, but not list?");
|
|
|
|
_list[index] = new(key, value);
|
|
_children[key] = value;
|
|
}
|
|
}
|
|
|
|
public int IndexOf(string key)
|
|
{
|
|
// TODO MappingDataNode
|
|
// Consider having a Dictionary<string,int> for faster lookups?
|
|
// IndexOf() gets called in Remove(), which itself gets called frequently (e.g., per serialized component,
|
|
// per entity, when loading a map.
|
|
//
|
|
// Then again, if most mappings only contain 1-4 entries, this list search is comparable in speed, reduces
|
|
// allocations, and makes adding/inserting entries faster.
|
|
|
|
for (var index = 0; index < _list.Count; index++)
|
|
{
|
|
if (_list[index].Key == key)
|
|
return index;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
void IDictionary<string, DataNode>.Add(string key, DataNode value) => Add(key, value);
|
|
|
|
public bool ContainsKey(string key) => _children.ContainsKey(key);
|
|
|
|
bool IDictionary<string, DataNode>.Remove(string key)
|
|
=> ((IDictionary<string, DataNode>)this).Remove(key);
|
|
|
|
public bool TryGetValue(string key, [NotNullWhen(true)] out DataNode? value)
|
|
=> TryGet(key, out value);
|
|
|
|
// TODO consider changing these to unsorted collections
|
|
// I.e., just redirect to _children.Keys to avoid hidden linq & allocations.
|
|
public ICollection<string> Keys => _list.Select(x => x.Key).ToArray();
|
|
public ICollection<DataNode> Values => _list.Select(x => x.Value).ToArray();
|
|
|
|
public DataNode Get(string key)
|
|
{
|
|
return _children[key];
|
|
}
|
|
|
|
public T Get<T>(string key) where T : DataNode
|
|
{
|
|
return (T) Get(key);
|
|
}
|
|
|
|
public bool TryGet(string key, [NotNullWhen(true)] out DataNode? node)
|
|
{
|
|
return _children.TryGetValue(key, out node);
|
|
}
|
|
|
|
public bool TryGet<T>(string key, [NotNullWhen(true)] out T? node) where T : DataNode
|
|
{
|
|
node = null;
|
|
if (!TryGet(key, out var rawNode) || rawNode is not T castNode)
|
|
return false;
|
|
node = castNode;
|
|
return true;
|
|
}
|
|
|
|
public bool Has(string key)
|
|
{
|
|
return _children.ContainsKey(key);
|
|
}
|
|
|
|
public bool Remove(string key)
|
|
{
|
|
if (!_children.Remove(key))
|
|
return false;
|
|
|
|
var index = IndexOf(key);
|
|
if (index == -1)
|
|
throw new Exception("Key exists in Children, but not list?");
|
|
|
|
_list.RemoveAt(index);
|
|
return true;
|
|
}
|
|
|
|
public T Cast<T>(string key) where T : DataNode
|
|
{
|
|
return (T) this[key];
|
|
}
|
|
|
|
public YamlMappingNode ToYaml()
|
|
{
|
|
var mapping = new YamlMappingNode();
|
|
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
YamlScalarNode yamlKeyNode;
|
|
if (_keyNodes != null && _keyNodes.TryGetValue(key, out var keyNode))
|
|
{
|
|
yamlKeyNode = (YamlScalarNode)keyNode;
|
|
}
|
|
else
|
|
{
|
|
// This is matches the ValueDataNode -> YamlScalarNode cast operator
|
|
yamlKeyNode = new(key)
|
|
{
|
|
Style = ValueDataNode.IsNullLiteral(key) || string.IsNullOrWhiteSpace(key)
|
|
? ScalarStyle.DoubleQuoted
|
|
: ScalarStyle.Any
|
|
};
|
|
}
|
|
|
|
mapping.Add(yamlKeyNode, val.ToYamlNode());
|
|
}
|
|
|
|
mapping.Tag = Tag;
|
|
|
|
return mapping;
|
|
}
|
|
|
|
public ValueDataNode GetKeyNode(string key)
|
|
{
|
|
return _keyNodes?.GetValueOrDefault(key) ?? new ValueDataNode(key);
|
|
}
|
|
|
|
public MappingDataNode Merge(MappingDataNode otherMapping)
|
|
{
|
|
var newMapping = Copy();
|
|
newMapping.Insert(otherMapping);
|
|
|
|
// TODO Serialization: should prob make this smarter
|
|
newMapping.Tag = Tag;
|
|
newMapping.Start = Start;
|
|
newMapping.End = End;
|
|
|
|
return newMapping;
|
|
}
|
|
|
|
public void Insert(MappingDataNode otherMapping, bool skipDuplicates = false)
|
|
{
|
|
foreach (var (key, val) in otherMapping.Children)
|
|
{
|
|
if (!skipDuplicates || !Has(key))
|
|
{
|
|
// Intentionally raises an ArgumentException
|
|
Add(key, val.Copy());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void InsertAt(int index, string key, DataNode value)
|
|
{
|
|
if (index > _list.Count || index < 0)
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
if (!_children.TryAdd(key, value))
|
|
throw new InvalidOperationException($"Already contains key {key}");
|
|
|
|
_list.Insert(index, new(key, value));
|
|
}
|
|
|
|
public override MappingDataNode Copy()
|
|
{
|
|
var newMapping = new MappingDataNode(_children.Count)
|
|
{
|
|
Tag = Tag,
|
|
Start = Start,
|
|
End = End
|
|
};
|
|
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
newMapping.Add(key, val.Copy());
|
|
}
|
|
|
|
newMapping._keyNodes = _keyNodes;
|
|
return newMapping;
|
|
}
|
|
|
|
internal MappingDataNode CopyNoType()
|
|
{
|
|
var newMapping = new MappingDataNode(_children.Count)
|
|
{
|
|
Tag = Tag,
|
|
Start = Start,
|
|
End = End
|
|
};
|
|
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
if (key != "type")
|
|
newMapping.Add(key, val.Copy());
|
|
}
|
|
|
|
newMapping._keyNodes = _keyNodes;
|
|
return newMapping;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Variant of <see cref="Copy"/> that doesn't clone the keys or values.
|
|
/// </summary>
|
|
public MappingDataNode ShallowClone()
|
|
{
|
|
var newMapping = new MappingDataNode(_children.Count)
|
|
{
|
|
Tag = Tag,
|
|
Start = Start,
|
|
End = End
|
|
};
|
|
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
newMapping.Add(key, val);
|
|
}
|
|
|
|
return newMapping;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Variant of <see cref="Except(MappingDataNode)"/> that will recursively call except rather than only checking equality.
|
|
/// </summary>
|
|
public MappingDataNode? RecursiveExcept(MappingDataNode node)
|
|
{
|
|
var mappingNode = new MappingDataNode()
|
|
{
|
|
Tag = Tag,
|
|
Start = Start,
|
|
End = End
|
|
};
|
|
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
if (!node._children.TryGetValue(key, out var otherVal))
|
|
{
|
|
mappingNode.Add(key, val.Copy());
|
|
}
|
|
else if (val.Except(otherVal) is { } newValue)
|
|
{
|
|
mappingNode.Add(key, newValue);
|
|
}
|
|
}
|
|
|
|
return mappingNode._children.Count == 0 ? null : mappingNode;
|
|
}
|
|
|
|
public override MappingDataNode? Except(MappingDataNode node)
|
|
{
|
|
var mappingNode = new MappingDataNode()
|
|
{
|
|
Tag = Tag,
|
|
Start = Start,
|
|
End = End
|
|
};
|
|
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
if (!node._children.TryGetValue(key, out var otherVal) || !val.Equals(otherVal))
|
|
mappingNode.Add(key, val.Copy());
|
|
}
|
|
|
|
return mappingNode._children.Count == 0 ? null : mappingNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if there are any nodes on this node that aren't in the other node.
|
|
/// </summary>
|
|
[Pure]
|
|
public bool AnyExcept(MappingDataNode node)
|
|
{
|
|
foreach (var (key, val) in _list)
|
|
{
|
|
var other = node._list.FirstOrNull(p => p.Key.Equals(key));
|
|
|
|
if (other == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// We only keep the entry if the values are not equal
|
|
if (!val.Equals(other.Value.Value))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override bool Equals(object? obj)
|
|
{
|
|
if (obj is not MappingDataNode other)
|
|
return false;
|
|
|
|
if (_children.Count != other._children.Count)
|
|
return false;
|
|
|
|
if (Tag != other.Tag)
|
|
return false;
|
|
|
|
foreach (var (key, otherValue) in other)
|
|
{
|
|
if (!_children.TryGetValue(key, out var ownValue)
|
|
|| !otherValue.Equals(ownValue))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public List<KeyValuePair<string, DataNode>>.Enumerator GetEnumerator() => _list.GetEnumerator();
|
|
IEnumerator<KeyValuePair<string, DataNode>> IEnumerable<KeyValuePair<string, DataNode>>.GetEnumerator() =>
|
|
_list.GetEnumerator();
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
var code = new HashCode();
|
|
foreach (var (key, value) in _children)
|
|
{
|
|
code.Add(key);
|
|
code.Add(value);
|
|
}
|
|
|
|
return code.ToHashCode();
|
|
}
|
|
|
|
internal override int GetCanonicalHashCode()
|
|
{
|
|
var entriesHash = 0;
|
|
foreach (var (key, value) in _list)
|
|
{
|
|
entriesHash ^= HashCode.Combine(StringComparer.Ordinal.GetHashCode(key), value.GetCanonicalHashCode());
|
|
}
|
|
|
|
return HashCode.Combine(typeof(MappingDataNode), Tag, Count, entriesHash);
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return GetEnumerator();
|
|
}
|
|
|
|
public void Add(KeyValuePair<string, DataNode> item) => Add(item.Key, item.Value);
|
|
|
|
public void Clear()
|
|
{
|
|
_children.Clear();
|
|
_list.Clear();
|
|
}
|
|
|
|
public bool Contains(KeyValuePair<string, DataNode> item) => _children.ContainsKey(item.Key);
|
|
|
|
[Obsolete("Use SerializationManager.PushComposition()")]
|
|
public override MappingDataNode PushInheritance(MappingDataNode node)
|
|
{
|
|
var newNode = Copy();
|
|
foreach (var (key, val) in node)
|
|
{
|
|
if (_children.ContainsKey(key))
|
|
continue;
|
|
|
|
newNode.Remove(key);
|
|
newNode.Add(key, val.Copy());
|
|
}
|
|
|
|
return newNode;
|
|
}
|
|
|
|
public void CopyTo(KeyValuePair<string, DataNode>[] array, int arrayIndex)
|
|
=> _list.CopyTo(array, arrayIndex);
|
|
|
|
public bool Remove(KeyValuePair<string, DataNode> item)
|
|
=> ((IDictionary<string, DataNode>) this).Remove(item.Key);
|
|
|
|
public bool TryAdd(string key, DataNode value)
|
|
{
|
|
if (!_children.TryAdd(key, value))
|
|
return false;
|
|
|
|
_list.Add(new(key, value));
|
|
return true;
|
|
}
|
|
|
|
public bool TryAddCopy(string key, DataNode value)
|
|
{
|
|
ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_children, key, out var exists);
|
|
if (exists)
|
|
return false;
|
|
|
|
entry = value.Copy();
|
|
_list.Add(new(key, entry));
|
|
return true;
|
|
}
|
|
|
|
// These methods are probably fine to keep around as helper methods, but are currently marked as obsolete
|
|
// so that people don't uneccesarily allocate a ValueDataNode. I.e., to prevent people from using code like
|
|
// mapping.TryGet(new ValueDataNode("key"), ...)
|
|
#region ValueDataNode Helpers
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public bool TryGet(ValueDataNode key, [NotNullWhen(true)] out DataNode? value)
|
|
=> TryGet(key.Value, out value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public DataNode this[ValueDataNode key]
|
|
{
|
|
get => this[key.Value];
|
|
set => this[key.Value] = value;
|
|
}
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public bool TryGetValue(ValueDataNode key, [NotNullWhen(true)] out DataNode? value)
|
|
=> TryGet(key.Value, out value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public bool TryGet<T>(ValueDataNode key, [NotNullWhen(true)] out T? node) where T : DataNode
|
|
=> TryGet(key.Value, out node);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public bool Has(ValueDataNode key) => Has(key.Value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public T Cast<T>(ValueDataNode key) where T : DataNode => Cast<T>(key.Value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public void Add(KeyValuePair<ValueDataNode, DataNode> item) => Add(item.Key, item.Value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public MappingDataNode Add(ValueDataNode key, DataNode node) => Add(key.Value, node);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public void InsertAt(int index, ValueDataNode key, DataNode value) => InsertAt(index, key.Value, value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public bool Contains(KeyValuePair<ValueDataNode, DataNode> item) => _children.ContainsKey(item.Key.Value);
|
|
|
|
[Obsolete("Use string keys instead of ValueDataNode")]
|
|
public bool Remove(ValueDataNode key) => Remove(key.Value);
|
|
|
|
#endregion
|
|
}
|
|
}
|