using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Threading; using JetBrains.Annotations; using Robust.Shared.Asynchronous; using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.IoC.Exceptions; using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Result; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Utility; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; namespace Robust.Shared.Prototypes { /// /// Handle storage and loading of YAML prototypes. /// public interface IPrototypeManager { void Initialize(); /// /// Return an IEnumerable to iterate all prototypes of a certain type. /// /// /// Thrown if the type of prototype is not registered. /// IEnumerable EnumeratePrototypes() where T : class, IPrototype; /// /// Return an IEnumerable to iterate all prototypes of a certain type. /// /// /// Thrown if the type of prototype is not registered. /// IEnumerable EnumeratePrototypes(Type type); /// /// Index for a by ID. /// /// /// Thrown if the type of prototype is not registered. /// T Index(string id) where T : class, IPrototype; /// /// Index for a by ID. /// /// /// Thrown if the ID does not exist or the type of prototype is not registered. /// IPrototype Index(Type type, string id); bool HasIndex(string id) where T : IPrototype; bool TryIndex(string id, [NotNullWhen(true)] out T? prototype) where T : IPrototype; /// /// Load prototypes from files in a directory, recursively. /// List LoadDirectory(ResourcePath path); Dictionary> ValidateDirectory(ResourcePath path); List LoadFromStream(TextReader stream); List LoadString(string str); /// /// Clear out all prototypes and reset to a blank slate. /// void Clear(); /// /// Performs a reload on all prototypes, updating the game state accordingly /// void ReloadPrototypes(ResourcePath file); /// /// Syncs all inter-prototype data. Call this when operations adding new prototypes are done. /// void Resync(); /// /// Registers a specific prototype name to be ignored. /// void RegisterIgnore(string name); /// /// Loads a single prototype class type into the manager. /// /// A prototype class type that implements IPrototype. This type also /// requires a with a non-empty class string. void RegisterType(Type protoClass); event Action? LoadedData; } /// /// Quick attribute to give the prototype its type string. /// To prevent needing to instantiate it because interfaces can't declare statics. /// [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [BaseTypeRequired(typeof(IPrototype))] [MeansImplicitUse] [MeansDataDefinition] public class PrototypeAttribute : Attribute { private readonly string type; public string Type => type; public readonly int LoadPriority = 1; public PrototypeAttribute(string type, int loadPriority = 1) { this.type = type; LoadPriority = loadPriority; } } public class PrototypeManager : IPrototypeManager, IPostInjectInit { [Dependency] private readonly IReflectionManager ReflectionManager = default!; [Dependency] private readonly IDynamicTypeFactoryInternal _dynamicTypeFactory = default!; [Dependency] public readonly IResourceManager Resources = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] public readonly ITaskManager TaskManager = default!; [Dependency] public readonly INetManager NetManager = default!; [Dependency] private readonly ISerializationManager _serializationManager = default!; private readonly Dictionary prototypeTypes = new(); private readonly Dictionary prototypePriorities = new(); private bool _initialized; private bool _hasEverBeenReloaded; private bool _hasEverResynced; #region IPrototypeManager members private readonly Dictionary> prototypes = new(); private readonly Dictionary> _prototypeResults = new(); private readonly Dictionary _inheritanceTrees = new(); private readonly HashSet IgnoredPrototypeTypes = new(); public virtual void Initialize() { if (_initialized) { throw new InvalidOperationException($"{nameof(PrototypeManager)} has already been initialized."); } _initialized = true; } public IEnumerable EnumeratePrototypes() where T : class, IPrototype { if (!_hasEverBeenReloaded) { throw new InvalidOperationException("No prototypes have been loaded yet."); } return prototypes[typeof(T)].Values.Select(p => (T) p); } public IEnumerable EnumeratePrototypes(Type type) { if (!_hasEverBeenReloaded) { throw new InvalidOperationException("No prototypes have been loaded yet."); } return prototypes[type].Values; } public T Index(string id) where T : class, IPrototype { if (!_hasEverBeenReloaded) { throw new InvalidOperationException("No prototypes have been loaded yet."); } try { return (T)prototypes[typeof(T)][id]; } catch (KeyNotFoundException) { throw new UnknownPrototypeException(id); } } public IPrototype Index(Type type, string id) { if (!_hasEverBeenReloaded) { throw new InvalidOperationException("No prototypes have been loaded yet."); } return prototypes[type][id]; } public void Clear() { prototypeTypes.Clear(); prototypes.Clear(); _prototypeResults.Clear(); _inheritanceTrees.Clear(); } private int SortPrototypesByPriority(Type a, Type b) { return prototypePriorities[b].CompareTo(prototypePriorities[a]); } public virtual void ReloadPrototypes(ResourcePath file) { #if !FULL_RELEASE var changed = LoadFile(file.ToRootedPath(), true).ToList(); changed.Sort((prototype, prototype1) => SortPrototypesByPriority(prototype.GetType(), prototype1.GetType())); var pushed = new Dictionary>(); foreach (var prototype in changed) { var type = prototype.GetType(); if (!pushed.ContainsKey(type)) pushed[type] = new HashSet(); var baseNode = prototype.ID; if (pushed[type].Contains(baseNode)) { continue; } var tree = _inheritanceTrees[type]; var currentNode = prototype.Parent; if (currentNode == null) { PushInheritance(type, baseNode, null, pushed[type]); continue; } while (true) { var parent = tree.GetParent(currentNode); if (parent == null) { break; } baseNode = currentNode; currentNode = parent; } PushInheritance(type, currentNode, baseNode, null, pushed[type]); } // TODO filter by entity prototypes changed if (!pushed.ContainsKey(typeof(EntityPrototype))) return; var entityPrototypes = prototypes[typeof(EntityPrototype)]; foreach (var prototype in pushed[typeof(EntityPrototype)]) { foreach (var entity in _entityManager.GetEntities(new PredicateEntityQuery(e => e.Prototype != null && e.Prototype.ID == prototype))) { ((EntityPrototype) entityPrototypes[prototype]).UpdateEntity((Entity) entity); } } #endif } public void Resync() { var trees = _inheritanceTrees.Keys.ToList(); trees.Sort(SortPrototypesByPriority); foreach (var type in trees) { var tree = _inheritanceTrees[type]; foreach (var baseNode in tree.BaseNodes) { PushInheritance(type, baseNode, null, new HashSet()); } } } public void PushInheritance(Type type, string id, string child, DeserializationResult? baseResult, HashSet changed) { changed.Add(id); var myRes = _prototypeResults[type][id]; var newResult = baseResult != null ? myRes.PushInheritanceFrom(baseResult) : myRes; PushInheritance(type, child, newResult, changed); newResult.CallAfterDeserializationHook(); var populatedRes = _serializationManager.PopulateDataDefinition(prototypes[type][id], (IDeserializedDefinition)newResult); prototypes[type][id] = (IPrototype) populatedRes.RawValue!; } public void PushInheritance(Type type, string id, DeserializationResult? baseResult, HashSet changed) { changed.Add(id); var myRes = _prototypeResults[type][id]; var newResult = baseResult != null ? myRes.PushInheritanceFrom(baseResult) : myRes; foreach (var childID in _inheritanceTrees[type].Children(id)) { PushInheritance(type, childID, newResult, changed); } newResult.CallAfterDeserializationHook(); var populatedRes = _serializationManager.PopulateDataDefinition(prototypes[type][id], (IDeserializedDefinition)newResult); prototypes[type][id] = (IPrototype) populatedRes.RawValue!; } /// public List LoadDirectory(ResourcePath path) { var changedPrototypes = new List(); _hasEverBeenReloaded = true; var streams = Resources.ContentFindFiles(path).ToList().AsParallel() .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".")); foreach (var resourcePath in streams) { var filePrototypes = LoadFile(resourcePath); changedPrototypes.AddRange(filePrototypes); } return changedPrototypes; } public Dictionary> ValidateDirectory(ResourcePath path) { var streams = Resources.ContentFindFiles(path).ToList().AsParallel() .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".")); var dict = new Dictionary>(); foreach (var resourcePath in streams) { using var reader = ReadFile(resourcePath); if (reader == null) { continue; } var yamlStream = new YamlStream(); yamlStream.Load(reader); for (var i = 0; i < yamlStream.Documents.Count; i++) { var rootNode = (YamlSequenceNode) yamlStream.Documents[i].RootNode; foreach (YamlMappingNode node in rootNode.Cast()) { var type = node.GetNode("type").AsString(); if (!prototypeTypes.ContainsKey(type)) { if (IgnoredPrototypeTypes.Contains(type)) { continue; } throw new PrototypeLoadException($"Unknown prototype type: '{type}'"); } var mapping = node.ToDataNodeCast(); mapping.RemoveNode("type"); var errorNodes = _serializationManager.ValidateNode(prototypeTypes[type], mapping).GetErrors().ToHashSet(); if (errorNodes.Count != 0) { if (!dict.TryGetValue(resourcePath.ToString(), out var hashSet)) dict[resourcePath.ToString()] = new HashSet(); dict[resourcePath.ToString()].UnionWith(errorNodes); } } } } return dict; } private StreamReader? ReadFile(ResourcePath 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; } Logger.Error($"Error reloading prototypes in file {file}.", e); return null; } retries++; Thread.Sleep(10); } } } public HashSet LoadFile(ResourcePath file, bool overwrite = false) { var changedPrototypes = new HashSet(); try { using var reader = ReadFile(file, !overwrite); if (reader == null) { return changedPrototypes; } var yamlStream = new YamlStream(); yamlStream.Load(reader); LoadedData?.Invoke(yamlStream, file.ToString()); for (var i = 0; i < yamlStream.Documents.Count; i++) { try { var documentPrototypes = LoadFromDocument(yamlStream.Documents[i], overwrite); changedPrototypes.UnionWith(documentPrototypes); } catch (Exception e) { Logger.ErrorS("eng", $"Exception whilst loading prototypes from {file}#{i}:\n{e}"); } } } catch (YamlException e) { var sawmill = Logger.GetSawmill("eng"); sawmill.Error("YamlException whilst loading prototypes from {0}: {1}", file, e.Message); } return changedPrototypes; } public List LoadFromStream(TextReader stream) { var changedPrototypes = new List(); _hasEverBeenReloaded = true; var yaml = new YamlStream(); yaml.Load(stream); for (var i = 0; i < yaml.Documents.Count; i++) { try { var documentPrototypes = LoadFromDocument(yaml.Documents[i]); changedPrototypes.AddRange(documentPrototypes); } catch (Exception e) { throw new PrototypeLoadException($"Failed to load prototypes from document#{i}", e); } } LoadedData?.Invoke(yaml, "anonymous prototypes YAML stream"); return changedPrototypes; } public List LoadString(string str) { return LoadFromStream(new StringReader(str)); } #endregion IPrototypeManager members public void PostInject() { ReflectionManager.OnAssemblyAdded += (_, _) => ReloadPrototypeTypes(); ReloadPrototypeTypes(); } private void ReloadPrototypeTypes() { Clear(); foreach (var type in ReflectionManager.GetAllChildren()) { RegisterType(type); } } private HashSet LoadFromDocument(YamlDocument document, bool overwrite = false) { var changedPrototypes = new HashSet(); var rootNode = (YamlSequenceNode) document.RootNode; foreach (YamlMappingNode node in rootNode.Cast()) { var type = node.GetNode("type").AsString(); if (!prototypeTypes.ContainsKey(type)) { if (IgnoredPrototypeTypes.Contains(type)) { continue; } throw new PrototypeLoadException($"Unknown prototype type: '{type}'"); } var prototypeType = prototypeTypes[type]; var res = _serializationManager.Read(prototypeType, node.ToDataNode(), skipHook: true); var prototype = (IPrototype) res.RawValue!; if (!overwrite && prototypes[prototypeType].ContainsKey(prototype.ID)) { throw new PrototypeLoadException($"Duplicate ID: '{prototype.ID}'"); } _prototypeResults[prototypeType][prototype.ID] = res; _inheritanceTrees[prototypeType].AddId(prototype.ID, prototype.Parent, true); prototypes[prototypeType][prototype.ID] = prototype; changedPrototypes.Add(prototype); } return changedPrototypes; } public bool HasIndex(string id) where T : IPrototype { if (!prototypes.TryGetValue(typeof(T), out var index)) { throw new UnknownPrototypeException(id); } return index.ContainsKey(id); } public bool TryIndex(string id, [NotNullWhen(true)] out T? prototype) where T : IPrototype { if (!prototypes.TryGetValue(typeof(T), out var index)) { throw new UnknownPrototypeException(id); } var returned = index.TryGetValue(id, out var uncast); prototype = (T) uncast!; return returned; } public void RegisterIgnore(string name) { IgnoredPrototypeTypes.Add(name); } /// public void RegisterType(Type type) { if(!(typeof(IPrototype).IsAssignableFrom(type))) throw new InvalidOperationException("Type must implement IPrototype."); var attribute = (PrototypeAttribute?)Attribute.GetCustomAttribute(type, typeof(PrototypeAttribute)); if (attribute == null) { throw new InvalidImplementationException(type, typeof(IPrototype), "No " + nameof(PrototypeAttribute) + " to give it a type string."); } if (prototypeTypes.ContainsKey(attribute.Type)) { throw new InvalidImplementationException(type, typeof(IPrototype), $"Duplicate prototype type ID: {attribute.Type}. Current: {prototypeTypes[attribute.Type]}"); } prototypeTypes[attribute.Type] = type; prototypePriorities[type] = attribute.LoadPriority; if (typeof(IPrototype).IsAssignableFrom(type)) { prototypes[type] = new Dictionary(); _prototypeResults[type] = new Dictionary(); _inheritanceTrees[type] = new PrototypeInheritanceTree(); } } public event Action? LoadedData; } [Serializable] public class PrototypeLoadException : Exception { public PrototypeLoadException() { } public PrototypeLoadException(string message) : base(message) { } public PrototypeLoadException(string message, Exception inner) : base(message, inner) { } public PrototypeLoadException(SerializationInfo info, StreamingContext context) : base(info, context) { } } [Serializable] public class UnknownPrototypeException : Exception { public override string Message => "Unknown prototype: " + Prototype; public readonly string? Prototype; public UnknownPrototypeException(string prototype) { Prototype = prototype; } public UnknownPrototypeException(SerializationInfo info, StreamingContext context) : base(info, context) { Prototype = (string?)info.GetValue("prototype", typeof(string)); } public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("prototype", Prototype, typeof(string)); } } }