mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
* 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
363 lines
11 KiB
C#
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);
|
|
}
|