Files
RobustToolbox/Robust.Shared/Prototypes/PrototypeManager.cs
Tayrtahn c1737a540f Analyzer & Fixer for redundant Prototype type strings (#5718)
* Add Prototype analyzer

* Add Prototype fixer

* Early return after finding prototype attribute

* Add PrototypeEndsWithPrototypeRule diagnostic

* Oops. Uncomment parallelizable.

* Rework to ignore redundancy for non-literal string values

* Allow redundancy when removal would expose class name not ending in "Prototype"

* Promote PrototypeEndsWithPrototypeRule from warning to error, since it causes a runtime error.

* No need to get the symbol to get the class identifier

* Minor cleanup

* A little more cleanup

* More specific location for redundant name

* Refactor redundant name fixer so argument order is no longer important

* Add failing test

* Use symbol analysis to fix alias handling

* Oops! We have to go back to the previous syntax-based approach.

Now it's a hybrid.

Also fixed tests to not copy the prototype definitions.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-12-17 18:15:32 +01:00

1273 lines
44 KiB
C#

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Robust.Shared.Asynchronous;
using Robust.Shared.Collections;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Random;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Prototypes
{
public abstract partial class PrototypeManager : IPrototypeManagerInternal
{
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] protected readonly IResourceManager Resources = default!;
[Dependency] protected readonly ITaskManager TaskManager = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly ILocalizationManager _locMan = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private readonly Dictionary<string, Dictionary<string, MappingDataNode>> _prototypeDataCache = new();
private EntityDiffContext _context = new();
private readonly Dictionary<string, Type> _kindNames = new();
private readonly Dictionary<Type, int> _kindPriorities = new();
protected ISawmill Sawmill = default!;
private bool _initialized;
private bool _hasEverBeenReloaded;
#region IPrototypeManager members
private FrozenDictionary<Type, KindData> _kinds = FrozenDictionary<Type, KindData>.Empty;
private readonly HashSet<string> _ignoredPrototypeTypes = new();
public virtual void Initialize()
{
if (_initialized)
return;
Sawmill = _logManager.GetSawmill("proto");
_initialized = true;
ReloadPrototypeKinds();
PrototypesReloaded += OnReload;
}
/// <inheritdoc />
public IEnumerable<string> GetPrototypeKinds()
{
return _kindNames.Keys;
}
/// <inheritdoc />
public int Count<T>() where T : class, IPrototype
{
return _kinds[typeof(T)].Instances.Count;
}
/// <inheritdoc />
public IEnumerable<T> EnumeratePrototypes<T>() where T : class, IPrototype
{
return GetInstances<T>().Values;
}
/// <inheritdoc />
public IEnumerable<IPrototype> EnumeratePrototypes(Type kind)
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
return _kinds[kind].Instances.Values;
}
/// <inheritdoc />
public IEnumerable<IPrototype> EnumeratePrototypes(string variant)
{
return EnumeratePrototypes(GetKindType(variant));
}
/// <inheritdoc />
public IEnumerable<T> EnumerateParents<T>(T proto, bool includeSelf = false)
where T : class, IPrototype, IInheritingPrototype
{
return EnumerateParents<T>(proto.ID, includeSelf);
}
/// <inheritdoc />
public IEnumerable<T> EnumerateParents<T>(string id, bool includeSelf = false)
where T : class, IPrototype, IInheritingPrototype
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
if (!TryIndex<T>(id, out var prototype))
yield break;
if (includeSelf)
yield return prototype;
if (prototype.Parents == null)
yield break;
var queue = new Queue<string>(prototype.Parents);
while (queue.TryDequeue(out var prototypeId))
{
if (!TryIndex<T>(prototypeId, out var parent))
continue; // Abstract parent?
yield return parent;
if (parent.Parents == null)
continue;
foreach (var parentId in parent.Parents)
{
queue.Enqueue(parentId);
}
}
}
/// <inheritdoc />
public IEnumerable<IPrototype> EnumerateParents(Type kind, string id, bool includeSelf = false)
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
if (!kind.IsAssignableTo(typeof(IInheritingPrototype)))
{
throw new InvalidOperationException("The provided prototype type is not an inheriting prototype");
}
if (!TryIndex(kind, id, out var prototype))
yield break;
if (includeSelf)
yield return prototype;
var iPrototype = (IInheritingPrototype)prototype;
if (iPrototype.Parents == null)
yield break;
var queue = new Queue<string>(iPrototype.Parents);
while (queue.TryDequeue(out var prototypeId))
{
if (!TryIndex(kind, prototypeId, out var parent))
continue; // Abstract parent?
yield return parent;
iPrototype = (IInheritingPrototype)parent;
if (iPrototype.Parents == null)
continue;
foreach (var parentId in iPrototype.Parents)
{
queue.Enqueue(parentId);
}
}
}
/// <inheritdoc />
public IEnumerable<(string id, T?)> EnumerateAllParents<T>(string id, bool includeSelf = false)
where T : class, IPrototype, IInheritingPrototype
{
if (!_hasEverBeenReloaded)
throw new InvalidOperationException("No prototypes have been loaded yet.");
if (!_kinds.TryGetValue(typeof(T), out var kindData))
throw new UnknownPrototypeException(id, typeof(T));
if (!kindData.Results.ContainsKey(id))
yield break;
IPrototype? uncast;
T? instance;
if (includeSelf)
{
kindData.Instances.TryGetValue(id, out uncast);
instance = uncast as T;
yield return (id, instance);
}
if (!kindData.Inheritance!.TryGetParents(id, out var parents))
yield break;
var queue = new Queue<string>(parents);
while (queue.TryDequeue(out var prototypeId))
{
if (!kindData.Results.ContainsKey(prototypeId))
{
Sawmill.Error($"Encountered invalid prototype while enumerating parents. Kind: {typeof(T).Name}. Child: {id}. Invalid: {prototypeId}");
continue;
}
kindData.Instances.TryGetValue(prototypeId, out uncast);
instance = uncast as T;
yield return (prototypeId, instance);
if (!kindData.Inheritance.TryGetParents(prototypeId, out parents))
continue;
foreach (var parentId in parents)
{
queue.Enqueue(parentId);
}
}
}
public IEnumerable<Type> EnumeratePrototypeKinds()
{
if (!_hasEverBeenReloaded)
throw new InvalidOperationException("No prototypes have been loaded yet.");
return _kinds.Keys;
}
/// <inheritdoc />
public T Index<T>(string id) where T : class, IPrototype
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
try
{
return (T)_kinds[typeof(T)].Instances[id];
}
catch (KeyNotFoundException)
{
throw new UnknownPrototypeException(id, typeof(T));
}
}
/// <inheritdoc />
public EntityPrototype Index(EntProtoId id)
{
return Index<EntityPrototype>(id.Id);
}
/// <inheritdoc />
public T Index<T>(ProtoId<T> id) where T : class, IPrototype
{
return Index<T>(id.Id);
}
/// <inheritdoc />
public IPrototype Index(Type kind, string id)
{
if (!_hasEverBeenReloaded)
{
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
try
{
return _kinds[kind].Instances[id];
}
catch (KeyNotFoundException)
{
throw new UnknownPrototypeException(id, kind);
}
}
/// <inheritdoc />
public void Clear()
{
_kindNames.Clear();
_kinds = FrozenDictionary<Type, KindData>.Empty;
}
/// <inheritdoc />
public void Reset()
{
var removed = _kinds.ToDictionary(
x => x.Key,
x => x.Value.Instances.Keys.ToHashSet());
ReloadPrototypeKinds();
Dictionary<Type, HashSet<string>> prototypes = new();
LoadDefaultPrototypes(prototypes);
foreach (var (kind, ids) in prototypes)
{
if (!removed.TryGetValue(kind, out var removedIds))
continue;
removedIds.ExceptWith(ids);
if (removedIds.Count == 0)
removed.Remove(kind);
}
ReloadPrototypes(prototypes, removed);
_locMan.ReloadLocalizations();
}
/// <inheritdoc />
public abstract void LoadDefaultPrototypes(Dictionary<Type, HashSet<string>>? changed = null);
private int SortPrototypesByPriority(Type a, Type b)
{
return _kindPriorities[b].CompareTo(_kindPriorities[a]);
}
protected void ReloadPrototypes(IEnumerable<ResPath> filePaths)
{
#if TOOLS
var changed = new Dictionary<Type, HashSet<string>>();
foreach (var filePath in filePaths)
{
LoadFile(filePath.ToRootedPath(), true, changed);
}
ReloadPrototypes(changed);
#endif
}
/// <inheritdoc />
public void ReloadPrototypes(
Dictionary<Type, HashSet<string>> modified,
Dictionary<Type, HashSet<string>>? removed = null)
{
var prototypeTypeOrder = modified.Keys.ToList();
prototypeTypeOrder.Sort(SortPrototypesByPriority);
var byType = new Dictionary<Type, PrototypesReloadedEventArgs.PrototypeChangeSet>();
var modifiedKinds = new HashSet<KindData>();
var toProcess = new HashSet<string>();
var processQueue = new Queue<string>();
foreach (var kind in prototypeTypeOrder)
{
var modifiedInstances = new Dictionary<string, IPrototype>();
var kindData = _kinds[kind];
var tree = kindData.Inheritance;
toProcess.Clear();
processQueue.Clear();
DebugTools.AssertEqual(kind.IsAssignableTo(typeof(IInheritingPrototype)), tree != null);
DebugTools.Assert(tree != null || kindData.RawResults == kindData.Results);
foreach (var id in modified[kind])
{
AddToQueue(id);
}
void AddToQueue(string id)
{
if (!toProcess.Add(id))
return;
processQueue.Enqueue(id);
if (tree == null)
return;
if (!tree.TryGetChildren(id, out var children))
return;
foreach (var child in children!)
{
AddToQueue(child);
}
}
while (processQueue.TryDequeue(out var id))
{
DebugTools.Assert(toProcess.Contains(id));
if (tree != null)
{
if (tree.TryGetParents(id, out var parents))
{
DebugTools.Assert(parents.Length > 0);
var nonPushedParent = false;
foreach (var parent in parents)
{
if (!toProcess.Contains(parent))
continue;
// our parent has been modified, but has not yet been processed.
// we re-queue ourselves at the end of the queue.
DebugTools.Assert(processQueue.Contains(parent));
processQueue.Enqueue(id);
nonPushedParent = true;
break;
}
if (nonPushedParent)
continue;
if (parents.Length == 1)
{
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
kind,
kindData.Results[parents[0]],
kindData.RawResults[id]);
}
else
{
var parentMaps = new MappingDataNode[parents.Length];
for (var i = 0; i < parentMaps.Length; i++)
{
parentMaps[i] = kindData.Results[parents[i]];
}
kindData.Results[id] = _serializationManager.PushCompositionWithGenericNode(
kind,
parentMaps,
kindData.RawResults[id]);
}
}
else
{
kindData.Results[id] = kindData.RawResults[id];
}
}
toProcess.Remove(id);
var prototype = TryReadPrototype(kind, id, kindData.Results[id], SerializationHookContext.DontSkipHooks);
if (prototype == null)
continue;
kindData.UnfrozenInstances ??= kindData.Instances.ToDictionary();
kindData.UnfrozenInstances[id] = prototype;
modifiedInstances.Add(id, prototype);
}
if (modifiedInstances.Count == 0)
continue;
byType.Add(kindData.Type, new(modifiedInstances));
modifiedKinds.Add(kindData);
}
Freeze(modifiedKinds);
if (modifiedKinds.Any(x => x.Type == typeof(EntityPrototype) || x.Type == typeof(EntityCategoryPrototype)))
UpdateCategories();
var modifiedTypes = new HashSet<Type>(byType.Keys);
if (removed != null)
modifiedTypes.UnionWith(removed.Keys);
var ev = new PrototypesReloadedEventArgs(modifiedTypes, byType, removed);
PrototypesReloaded?.Invoke(ev);
_entMan.EventBus.RaiseEvent(EventSource.Local, ev);
}
private void Freeze(IEnumerable<KindData> kinds)
{
var st = RStopwatch.StartNew();
foreach (var kind in kinds)
{
kind.Freeze();
}
// fun fact: Sawmill can be null in tests????
Sawmill?.Verbose($"Freezing prototype instances took {st.Elapsed.TotalMilliseconds:f2}ms");
}
/// <summary>
/// Resolves the mappings stored in memory to actual prototypeinstances.
/// </summary>
public void ResolveResults()
{
// Oh god I butchered this poor method in the name of my Ryzen CPU.
// Run inheritance pushing concurrently to the rest of prototype loading.
// Use some basic tasks and .Wait() to make sure it's done by the time we get to the prototypes in question.
// The biggest prototypes by far in SS14 are entity prototypes.
// Entity prototypes have priority -1 right now, so they have to be read last. This works out great!
// We'll already be done pushing inheritance for them by the time we get to reading them.
var inheritanceTasks = new Dictionary<Type, Task>();
foreach (var (k, v) in _kinds)
{
if (v.Inheritance == null)
continue;
var task = Task.Run(() => PushKindInheritance(k, v));
inheritanceTasks.Add(k, task);
}
var priorities = _kinds.Keys
.GroupBy(k => _kindPriorities[k])
.OrderByDescending(k => k.Key);
foreach (var group in priorities)
{
var kinds = group.Select(k => _kinds[k]).ToArray();
InstantiateKinds(kinds, inheritanceTasks);
}
UpdateCategories();
}
private void InstantiateKinds(KindData[] kinds, Dictionary<Type, Task> inheritanceTasks)
{
// Wait for all inheritance pushing in this group to finish.
// This isn't ideal, but since entity prototypes are the big ones in SS14 it's fine.
foreach (var kind in kinds)
{
if (inheritanceTasks.TryGetValue(kind.Type, out var task))
task.Wait();
}
// Process all prototypes in this group in a single parallel operation.
var results = kinds
.SelectMany(data => data.Results,
(data, results) => (KindData: data, Id: results.Key, Mapping: results.Value, Instance: (IPrototype?)null))
.ToArray();
// Randomize to remove any patterns that could cause uneven load.
_random.Shuffle(results.AsSpan());
// Create channel that all AfterDeserialization hooks in this group will be sent into.
var hooksChannelOptions = new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
// Don't use an async job to unblock the read task.
AllowSynchronousContinuations = true
};
var hooksChannel = Channel.CreateUnbounded<ISerializationHooks>(hooksChannelOptions);
var instantiateTask = Task.Run(() => InstantiatePrototypes(kinds, results, hooksChannel));
// On the game thread: process AfterDeserialization hooks from the channel.
var channelReader = hooksChannel.Reader;
#pragma warning disable RA0004
while (channelReader.WaitToReadAsync().AsTask().Result)
#pragma warning restore RA0004
{
while (channelReader.TryRead(out var hooks))
{
hooks.AfterDeserialization();
}
}
// Join task in case an exception was raised.
instantiateTask.Wait();
}
private void InstantiatePrototypes(
KindData[] kinds,
(KindData KindData, string Id, MappingDataNode Mapping, IPrototype? Instance)[] results,
Channel<ISerializationHooks> hooks)
{
var hookCtx = new SerializationHookContext(hooks.Writer, false);
try
{
Parallel.For(0,
results.Length,
i =>
{
ref var item = ref results[i];
item.Instance = TryReadPrototype(item.KindData.Type, item.Id, item.Mapping, hookCtx);
});
foreach (var item in results)
{
if (item.Instance == null)
continue;
item.KindData.UnfrozenInstances ??= item.KindData.Instances.ToDictionary();
item.KindData.UnfrozenInstances[item.Id] = item.Instance;
}
Freeze(kinds.Where(data => data.UnfrozenInstances != null));
}
finally
{
// Mark the hooks channel as complete so the game thread unblocks.
hooks.Writer.Complete();
}
}
private IPrototype? TryReadPrototype(
Type kind,
string id,
MappingDataNode mapping,
SerializationHookContext hookCtx)
{
if (mapping.TryGet<ValueDataNode>(AbstractDataFieldAttribute.Name, out var abstractNode) &&
abstractNode.AsBool())
return null;
try
{
return (IPrototype)_serializationManager.Read(kind, mapping, hookCtx)!;
}
catch (Exception e)
{
Sawmill.Error($"Reading {kind}({id}) threw the following exception: {e}");
return null;
}
}
private async Task PushKindInheritance(Type kind, KindData data)
{
if (data.Inheritance is not { } tree)
return;
// var sw = RStopwatch.StartNew();
var results = data.RawResults.ToDictionary(
k => k.Key,
k => new InheritancePushDatum(k.Value, tree.GetParentsCount(k.Key)));
using var countDown = new CountdownEvent(results.Count);
foreach (var root in tree.RootNodes)
{
ThreadPool.QueueUserWorkItem(_ => { ProcessItem(root, results[root]); });
}
void ProcessItem(string id, InheritancePushDatum datum)
{
try
{
if (tree.TryGetParents(id, out var parents))
{
if (parents.Length == 1)
{
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
results[parents[0]].Result,
datum.Result);
}
else
{
var parentNodes = new MappingDataNode[parents.Length];
for (var i = 0; i < parents.Length; i++)
{
parentNodes[i] = results[parents[i]].Result;
}
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
parentNodes,
datum.Result);
}
}
if (tree.TryGetChildren(id, out var children))
{
foreach (var child in children)
{
var childDatum = results[child];
var val = Interlocked.Decrement(ref childDatum.CountParentsRemaining);
if (val == 0)
{
ThreadPool.QueueUserWorkItem(_ => { ProcessItem(child, childDatum); });
}
}
}
// ReSharper disable once AccessToDisposedClosure
countDown.Signal();
}
catch (Exception e)
{
Sawmill.Error($"Failed to push composition for {kind.Name} prototype with id: {id}. Exception: {e}");
throw;
}
}
await WaitHandleHelpers.WaitOneAsync(countDown.WaitHandle);
data.Results.Clear();
foreach (var (k, v) in results)
{
data.Results[k] = v.Result;
}
// _sawmill.Debug($"Inheritance {kind}: {sw.Elapsed}");
}
private sealed class InheritancePushDatum
{
public MappingDataNode Result;
public int CountParentsRemaining;
public InheritancePushDatum(MappingDataNode result, int countParentsRemaining)
{
Result = result;
CountParentsRemaining = countParentsRemaining;
}
}
#endregion IPrototypeManager members
/// <inheritdoc />
public void ReloadPrototypeKinds()
{
Clear();
var dict = new Dictionary<Type, KindData>();
foreach (var type in _reflectionManager.GetAllChildren<IPrototype>())
{
RegisterKind(type, dict);
}
Freeze(dict);
}
/// <inheritdoc />
public bool HasIndex<T>(string id) where T : class, IPrototype
{
if (!_kinds.TryGetValue(typeof(T), out var index))
{
throw new UnknownPrototypeException(id, typeof(T));
}
return index.Instances.ContainsKey(id);
}
/// <inheritdoc />
public bool HasIndex(EntProtoId id)
{
return HasIndex<EntityPrototype>(id.Id);
}
/// <inheritdoc />
public bool HasIndex<T>(ProtoId<T> id) where T : class, IPrototype
{
return HasIndex<T>(id.Id);
}
/// <inheritdoc />
public bool HasIndex(EntProtoId? id)
{
if (id == null)
return false;
return HasIndex(id.Value);
}
/// <inheritdoc />
public bool HasIndex<T>(ProtoId<T>? id) where T : class, IPrototype
{
if (id == null)
return false;
return HasIndex(id.Value);
}
/// <inheritdoc />
public bool TryIndex<T>(string id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype
{
var returned = TryIndex(typeof(T), id, out var proto);
prototype = (proto ?? null) as T;
return returned;
}
/// <inheritdoc />
public bool TryIndex(Type kind, string id, [NotNullWhen(true)] out IPrototype? prototype)
{
if (!_kinds.TryGetValue(kind, out var index))
{
throw new UnknownPrototypeException(id, kind);
}
return index.Instances.TryGetValue(id, out prototype);
}
// For obsolete APIs.
// ReSharper disable MethodOverloadWithOptionalParameter
public bool Resolve(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype)
{
if (TryIndex(id.Id, out prototype))
return true;
Sawmill.Error($"Attempted to resolve invalid {nameof(EntProtoId)}: {id.Id}\n{Environment.StackTrace}");
return false;
}
[Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")]
public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true)
{
if (logError)
return Resolve(id, out prototype);
return TryIndex(id, out prototype);
}
public bool TryIndex([ForbidLiteral] EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype)
{
return TryIndex(id.Id, out prototype);
}
public bool Resolve<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype
{
if (TryIndex(id.Id, out prototype))
return true;
Sawmill.Error($"Attempted to resolve invalid ProtoId<{typeof(T).Name}>: {id.Id}\n{Environment.StackTrace}");
return false;
}
[Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")]
public bool TryIndex<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype, bool logError = true)
where T : class, IPrototype
{
if (logError)
return Resolve(id, out prototype);
return TryIndex(id, out prototype);
}
public bool TryIndex<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype)
where T : class, IPrototype
{
return TryIndex(id.Id, out prototype);
}
public bool Resolve(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype)
{
if (id == null)
{
prototype = null;
return false;
}
return Resolve(id.Value, out prototype);
}
[Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")]
public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true)
{
if (logError)
return Resolve(id, out prototype);
return TryIndex(id, out prototype);
}
public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype)
{
if (id == null)
{
prototype = null;
return false;
}
return TryIndex(id.Value, out prototype);
}
public bool Resolve<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype
{
if (id == null)
{
prototype = null;
return false;
}
return Resolve(id.Value, out prototype);
}
[Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")]
public bool TryIndex<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype, bool logError = true)
where T : class, IPrototype
{
if (logError)
return Resolve(id, out prototype);
return TryIndex(id, out prototype);
}
public bool TryIndex<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype)
where T : class, IPrototype
{
if (id == null)
{
prototype = null;
return false;
}
return TryIndex(id.Value, out prototype);
}
// ReSharper restore MethodOverloadWithOptionalParameter
/// <inheritdoc />
public bool HasMapping<T>(string id)
{
if (!_kinds.TryGetValue(typeof(T), out var index))
{
throw new UnknownPrototypeException(id, typeof(T));
}
return index.Results.ContainsKey(id);
}
/// <inheritdoc />
public bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingDataNode? mappings)
{
return _kinds[kind].Results.TryGetValue(id, out mappings);
}
public bool HasKind(string kind)
{
return _kindNames.ContainsKey(kind);
}
public Type GetKindType(string kind)
{
return _kindNames[kind];
}
public bool TryGetKindType(string kind, [NotNullWhen(true)] out Type? prototype)
{
return _kindNames.TryGetValue(kind, out prototype);
}
public bool TryGetKindFrom(Type type, [NotNullWhen(true)] out string? kind)
{
kind = null;
if (!_kinds.TryGetValue(type, out var kindData))
return false;
kind = kindData.Name;
return true;
}
public FrozenDictionary<string, T> GetInstances<T>() where T : IPrototype
{
if (TryGetInstances<T>(out var dict))
return dict;
throw new Exception($"Failed to fetch instances for kind {nameof(T)}");
}
public bool TryGetInstances<T>([NotNullWhen(true)] out FrozenDictionary<string, T>? instances)
where T : IPrototype
{
if (!TryGetInstances(typeof(T), out var dict))
{
instances = null;
return false;
}
DebugTools.Assert(dict is FrozenDictionary<string, T> || dict == null);
instances = dict as FrozenDictionary<string, T>;
// Prototypes with no loaded instances never get frozen.
instances ??= FrozenDictionary<string, T>.Empty;
return true;
}
private bool TryGetInstances(Type kind, [NotNullWhen(true)] out object? instances)
{
if (!_hasEverBeenReloaded)
throw new InvalidOperationException("No prototypes have been loaded yet.");
DebugTools.Assert(kind.IsAssignableTo(typeof(IPrototype)));
if (!_kinds.TryGetValue(kind, out var kindData))
{
instances = null;
return false;
}
instances = kindData.InstancesDirect;
return true;
}
public bool TryGetKindFrom(IPrototype prototype, [NotNullWhen(true)] out string? kind)
{
return TryGetKindFrom(prototype.GetType(), out kind);
}
public bool TryGetKindFrom<T>([NotNullWhen(true)] out string? kind) where T : class, IPrototype
{
return TryGetKindFrom(typeof(T), out kind);
}
public bool IsIgnored(string name) => _ignoredPrototypeTypes.Contains(name);
/// <inheritdoc />
public void RegisterIgnore(string name)
{
_ignoredPrototypeTypes.Add(name);
}
static string CalculatePrototypeName(Type type)
{
return PrototypeUtility.CalculatePrototypeName(type.Name);
}
/// <inheritdoc />
public void RegisterKind(params Type[] kinds)
{
var dict = _kinds.ToDictionary();
foreach (var kind in kinds)
{
RegisterKind(kind, dict);
}
Freeze(dict);
}
private void Freeze(Dictionary<Type, KindData> dict)
{
var st = RStopwatch.StartNew();
_kinds = dict.ToFrozenDictionary();
// fun fact: Sawmill can be null in tests????
Sawmill?.Verbose($"Freezing prototype kinds took {st.Elapsed.TotalMilliseconds:f2}ms");
}
private void RegisterKind(Type kind, Dictionary<Type, KindData> kinds)
{
if (!(typeof(IPrototype).IsAssignableFrom(kind)))
throw new InvalidOperationException("Type must implement IPrototype.");
var attribute = (PrototypeAttribute?)Attribute.GetCustomAttribute(kind, typeof(PrototypeAttribute));
if (attribute == null)
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
"No " + nameof(PrototypeAttribute) + " to give it a type string.");
}
var name = attribute.Type ?? CalculatePrototypeName(kind);
if (_ignoredPrototypeTypes.Contains(name))
{
// For whatever reason, we are registering a prototype despite it having been marked as ignored.
// This often happens when someone is moving a server or client prototype to shared. Maybe this should
// log an error, but I want to avoid breaking changes and maaaaybe there some weird instance where you
// want the client to know that a prototype kind exists, without having the client load information
// about the individual prototypes? So for now lets just log a warning instead of introducing breaking
// changes.
Sawmill.Warning($"Registering an ignored prototype {kind}");
}
if (_kindNames.TryGetValue(name, out var existing))
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
$"Duplicate prototype type ID: {attribute.Type}. Current: {existing}");
}
var foundIdAttribute = false;
var foundParentAttribute = false;
var foundAbstractAttribute = false;
foreach (var info in kind.GetAllPropertiesAndFields())
{
var hasId = info.HasAttribute<IdDataFieldAttribute>();
var hasParent = info.HasAttribute<ParentDataFieldAttribute>();
if (hasId)
{
if (foundIdAttribute)
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
$"Found two {nameof(IdDataFieldAttribute)}");
}
foundIdAttribute = true;
}
if (hasParent)
{
if (foundParentAttribute)
{
throw new InvalidImplementationException(kind,
typeof(IInheritingPrototype),
$"Found two {nameof(ParentDataFieldAttribute)}");
}
foundParentAttribute = true;
}
if (hasId && hasParent)
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
$"Prototype {kind} has the Id- & ParentDatafield on single member {info.Name}");
}
if (info.HasAttribute<AbstractDataFieldAttribute>())
{
if (foundAbstractAttribute)
{
throw new InvalidImplementationException(kind,
typeof(IInheritingPrototype),
$"Found two {nameof(AbstractDataFieldAttribute)}");
}
foundAbstractAttribute = true;
}
}
if (!foundIdAttribute)
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
$"Did not find any member annotated with the {nameof(IdDataFieldAttribute)}");
}
if (kind.IsAssignableTo(typeof(IInheritingPrototype)) && (!foundParentAttribute || !foundAbstractAttribute))
{
throw new InvalidImplementationException(kind,
typeof(IInheritingPrototype),
$"Did not find any member annotated with the {nameof(ParentDataFieldAttribute)} and/or {nameof(AbstractDataFieldAttribute)}");
}
_kindNames[name] = kind;
_kindPriorities[kind] = attribute.LoadPriority;
var kindData = new KindData(kind, name);
kinds[kind] = kindData;
if (kind.IsAssignableTo(typeof(IInheritingPrototype)))
kindData.Inheritance = new MultiRootInheritanceGraph<string>();
else
kindData.Results = kindData.RawResults;
}
/// <inheritdoc />
public event Action<PrototypesReloadedEventArgs>? PrototypesReloaded;
private sealed class KindData(Type kind, string name)
{
public Dictionary<string, IPrototype>? UnfrozenInstances;
public FrozenDictionary<string, IPrototype> Instances = FrozenDictionary<string, IPrototype>.Empty;
public Dictionary<string, MappingDataNode> Results = new();
/// <summary>
/// Variant of <see cref="Results"/> prior to inheritance pushing. If the kind does not have inheritance,
/// then this is just the same dictionary.
/// </summary>
public readonly Dictionary<string, MappingDataNode> RawResults = new();
public readonly Type Type = kind;
public readonly string Name = name;
// Only initialized if prototype is inheriting.
public MultiRootInheritanceGraph<string>? Inheritance;
/// <summary>
/// Variant of <see cref="Instances"/> that has a direct mapping to the prototype kind. I.e., no IPrototype interface.
/// </summary>
public object InstancesDirect = default!;
private MethodInfo _freezeDirectInfo = typeof(KindData)
.GetMethod(nameof(FreezeDirect), BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(kind);
private void FreezeDirect<T>()
{
var dict = new Dictionary<string, T>();
foreach (var (id, instance) in Instances)
{
dict.Add(id, (T) instance);
}
InstancesDirect = dict.ToFrozenDictionary();
}
public void Freeze()
{
DebugTools.AssertNotNull(UnfrozenInstances);
Instances = UnfrozenInstances?.ToFrozenDictionary() ?? FrozenDictionary<string, IPrototype>.Empty;
UnfrozenInstances = null;
_freezeDirectInfo.Invoke(this, null);
}
}
private void OnReload(PrototypesReloadedEventArgs args)
{
if (args.ByType.TryGetValue(typeof(EntityPrototype), out var modified))
{
foreach (var id in modified.Modified.Keys)
{
_prototypeDataCache.Remove(id);
}
}
if (args.Removed == null || !args.Removed.TryGetValue(typeof(EntityPrototype), out var removed))
return;
foreach (var id in removed)
{
_prototypeDataCache.Remove(id);
}
}
public IReadOnlyDictionary<string, MappingDataNode> GetPrototypeData(EntityPrototype prototype)
{
if (_prototypeDataCache.TryGetValue(prototype.ID, out var data))
return data;
_context.WritingReadingPrototypes = true;
data = new();
var xform = _factory.GetRegistration(typeof(TransformComponent)).Name;
try
{
foreach (var (compType, comp) in prototype.Components)
{
if (compType == xform)
continue;
var node = _serializationManager.WriteValueAs<MappingDataNode>(comp.Component.GetType(), comp.Component,
alwaysWrite: true, context: _context);
data.Add(compType, node);
}
}
catch (Exception e)
{
Sawmill.Error($"Failed to convert prototype {prototype.ID} into yaml. Exception: {e.Message}");
}
_context.WritingReadingPrototypes = false;
_prototypeDataCache[prototype.ID] = data;
return data;
}
public bool TryGetRandom<T>(IRobustRandom random, [NotNullWhen(true)] out IPrototype? prototype) where T : class, IPrototype
{
var count = Count<T>();
if (count == 0)
{
prototype = null;
return false;
}
var index = 0;
var picked = random.Next(count);
foreach (var proto in EnumeratePrototypes<T>())
{
if (index == picked)
{
prototype = proto;
return true;
}
index++;
}
throw new ArgumentOutOfRangeException($"Unable to pick valid prototype for {typeof(T)}?");
}
}
}