Files
RobustToolbox/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs
Leon Friedrich 2f8f4f2f7a Make MappingDataNode use string keys (#5783)
* string keys

* obsoletions

* Fix ValueTupleSerializer

* Release notes

* fix release note conflict

* cleanup

* Fix yaml validator & tests

* cleanup release notes

* a

* enumerator allocations

* Also sequence enumerator alloc
2025-04-18 19:01:34 +10:00

363 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Utility;
namespace Robust.Shared.Prototypes;
public partial class PrototypeManager
{
/// <summary>
/// Which files to force all prototypes within to be abstract.
/// </summary>
private readonly List<ResPath> _abstractFiles = new();
/// <summary>
/// Which directories to force all prototypes recursively within to be abstract.
/// </summary>
private readonly List<ResPath> _abstractDirectories = new();
public event Action<DataNodeDocument>? LoadedData;
/// <inheritdoc />
public void LoadDirectory(ResPath path, bool overwrite = false,
Dictionary<Type, HashSet<string>>? changed = null)
{
_hasEverBeenReloaded = true;
var streams = Resources.ContentFindFiles(path)
.Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith("."))
.ToArray();
// Shuffle to avoid input data patterns causing uneven thread workloads.
(new System.Random()).Shuffle(streams.AsSpan());
var sawmill = _logManager.GetSawmill("eng");
var results = streams.AsParallel()
.Select<ResPath, (ResPath, IEnumerable<ExtractedMappingData>)>(file =>
{
try
{
var ignored = IsFileAbstract(file);
using var reader = ReadFile(file, !overwrite);
if (reader == null)
return (file, Array.Empty<ExtractedMappingData>());
var extractedList = new List<ExtractedMappingData>();
foreach (var document in DataNodeParser.ParseYamlStream(reader))
{
LoadedData?.Invoke(document);
var seq = (SequenceDataNode)document.Root;
foreach (var mapping in seq.Sequence)
{
var data = ExtractMapping((MappingDataNode)mapping);
if (data != null)
{
if (ignored)
AbstractPrototype(data.Data);
extractedList.Add(data);
}
}
}
return (file, extractedList);
}
catch (Exception e)
{
sawmill.Error($"Exception whilst loading prototypes from {file}:\n{e}");
return (file, Array.Empty<ExtractedMappingData>());
}
});
foreach (var (file, result) in results)
{
foreach (var mapping in result)
{
try
{
MergeMapping(mapping, overwrite, changed);
}
catch (Exception e)
{
sawmill.Error($"Exception whilst loading prototypes from {file}:\n{e}");
}
}
}
}
private StreamReader? ReadFile(ResPath file, bool @throw = true)
{
var retries = 0;
// This might be shit-code, but its pjb-responded-idk-when-asked shit-code.
while (true)
{
try
{
var reader = new StreamReader(Resources.ContentFileRead(file), EncodingHelpers.UTF8);
return reader;
}
catch (IOException e)
{
if (retries > 10)
{
if (@throw)
{
throw;
}
Sawmill.Error($"Error reloading prototypes in file {file}:\n{e}");
return null;
}
retries++;
Thread.Sleep(10);
}
}
}
public void LoadFile(ResPath file, bool overwrite = false, Dictionary<Type, HashSet<string>>? changed = null)
{
try
{
var ignored = IsFileAbstract(file);
using var reader = ReadFile(file, !overwrite);
if (reader == null)
return;
var i = 0;
foreach (var document in DataNodeParser.ParseYamlStream(reader))
{
LoadedData?.Invoke(document);
try
{
var seq = (SequenceDataNode)document.Root;
foreach (var mapping in seq.Sequence)
{
var extracted = ExtractMapping((MappingDataNode) mapping);
if (extracted == null)
continue;
if (ignored)
AbstractPrototype(extracted.Data);
MergeMapping(extracted, overwrite, changed);
}
}
catch (Exception e)
{
Sawmill.Error($"Exception whilst loading prototypes from {file}#{i}:\n{e}");
}
i += 1;
}
}
catch (Exception e)
{
Sawmill.Error("YamlException whilst loading prototypes from {0}: {1}", file, e.Message);
}
}
private ExtractedMappingData? ExtractMapping(MappingDataNode dataNode)
{
var type = dataNode.Get<ValueDataNode>("type").Value;
if (_ignoredPrototypeTypes.Contains(type))
return null;
if (!_kindNames.TryGetValue(type, out var kind))
{
throw new PrototypeLoadException($"Unknown prototype type: '{type}'");
}
var kindData = _kinds[kind];
if (!dataNode.TryGet<ValueDataNode>(IdDataFieldAttribute.Name, out var idNode))
throw new PrototypeLoadException($"Prototype type {type} is missing an 'id' datafield.");
var id = idNode.Value;
string[]? parents = null;
if (kindData.Inheritance != null)
{
if (dataNode.TryGet(ParentDataFieldAttribute.Name, out var parentNode))
{
parents = _serializationManager.Read<string[]>(parentNode, notNullableOverride: true);
}
}
return new ExtractedMappingData(kind, id, parents, dataNode);
}
private void MergeMapping(
ExtractedMappingData mapping,
bool overwrite,
Dictionary<Type, HashSet<string>>? changed)
{
var (kind, id, parents, data) = mapping;
var kindData = _kinds[kind];
if (!overwrite && kindData.RawResults.ContainsKey(id))
throw new PrototypeLoadException($"Duplicate ID: '{id}' for kind '{kind}");
kindData.RawResults[id] = data;
if (kindData.Inheritance is { } inheritance)
{
if (parents != null)
{
inheritance.Add(id, parents);
}
else
{
inheritance.Add(id);
}
}
if (changed == null)
return;
var set = changed.GetOrNew(kind);
set.Add(id);
}
public void LoadFromStream(TextReader stream, bool overwrite = false,
Dictionary<Type, HashSet<string>>? changed = null)
{
_hasEverBeenReloaded = true;
var i = 0;
foreach (var document in DataNodeParser.ParseYamlStream(stream))
{
LoadedData?.Invoke(document);
try
{
var rootNode = (SequenceDataNode)document.Root;
foreach (var node in rootNode.Cast<MappingDataNode>())
{
var extracted = ExtractMapping(node);
if (extracted == null)
continue;
MergeMapping(extracted, overwrite, changed);
}
i += 1;
}
catch (Exception e)
{
throw new PrototypeLoadException($"Failed to load prototypes from document#{i}", e);
}
}
}
public void LoadString(string str, bool overwrite = false, Dictionary<Type, HashSet<string>>? changed = null)
{
LoadFromStream(new StringReader(str), overwrite, changed);
}
public void RemoveString(string prototypes)
{
var reader = new StringReader(prototypes);
var modified = new HashSet<KindData>();
foreach (var document in DataNodeParser.ParseYamlStream(reader))
{
var root = (SequenceDataNode)document.Root;
foreach (var node in root.Cast<MappingDataNode>())
{
var typeString = node.Get<ValueDataNode>("type").Value;
if (!_kindNames.TryGetValue(typeString, out var kind))
{
continue;
}
var kindData = _kinds[kind];
var id = node.Get<ValueDataNode>("id").Value;
if (kindData.Inheritance is { } tree)
tree.Remove(id, true);
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
kindData.UnfrozenInstances.Remove(id);
kindData.Results.Remove(id);
kindData.RawResults.Remove(id);
modified.Add(kindData);
}
}
Freeze(modified);
}
public void AbstractFile(ResPath path)
{
_abstractFiles.Add(path);
}
public void AbstractDirectory(ResPath path)
{
_abstractDirectories.Add(path);
}
private bool IsFileAbstract(ResPath file)
{
if (_abstractFiles.Count > 0)
{
foreach (var abstractFile in _abstractFiles)
{
if (file.TryRelativeTo(abstractFile, out _))
return true;
}
}
if (_abstractDirectories.Count > 0)
{
foreach (var abstractDirectory in _abstractDirectories)
{
if (file.TryRelativeTo(abstractDirectory, out _))
return true;
}
}
return false;
}
private void AbstractPrototype(MappingDataNode mapping)
{
if (mapping.TryGet(AbstractDataFieldAttribute.Name, out var abstractNode))
{
if (abstractNode is not ValueDataNode abstractValueNode)
{
mapping["abstract"] = new ValueDataNode("true");
return;
}
abstractValueNode.Value = "true";
return;
}
mapping.Add("abstract", "true");
}
// All these fields can be null in case the
private sealed record ExtractedMappingData(
Type Kind,
string Id,
string[]? Parents,
MappingDataNode Data);
}