diff --git a/Resources/EnginePrototypes/Debug/rotation.yml b/Resources/EnginePrototypes/Debug/rotation.yml index 047b823fa..d817f5e15 100644 --- a/Resources/EnginePrototypes/Debug/rotation.yml +++ b/Resources/EnginePrototypes/Debug/rotation.yml @@ -1,7 +1,7 @@ - type: entity id: debugRotation abstract: true - categories: [ debug ] + categories: [ Debug ] components: - type: Sprite netsync: false diff --git a/Resources/EnginePrototypes/entityCategory.yml b/Resources/EnginePrototypes/entityCategory.yml index 891c39146..28e629b9e 100644 --- a/Resources/EnginePrototypes/entityCategory.yml +++ b/Resources/EnginePrototypes/entityCategory.yml @@ -1,17 +1,20 @@ # debug related entities - type: entityCategory - id: debug + id: Debug name: entity-category-name-debug description: entity-category-desc-debug + suffix: entity-category-suffix-debug # entities that spawn other entities - type: entityCategory - id: spawner + id: Spawner name: entity-category-name-spawner description: entity-category-desc-spawner -# entities that should be hidden from the spawn menu +# simple category that just exists to hide prototypes in spawn menus - type: entityCategory - id: hideSpawnMenu + id: HideSpawnMenu name: entity-category-name-hide description: entity-category-desc-hide + hideSpawnMenu: true + inheritable: false diff --git a/Resources/Locale/en-US/entity-category.ftl b/Resources/Locale/en-US/entity-category.ftl index 6ccab9533..dfdba8fba 100644 --- a/Resources/Locale/en-US/entity-category.ftl +++ b/Resources/Locale/en-US/entity-category.ftl @@ -1,8 +1,9 @@ entity-category-name-debug = Debug entity-category-desc-debug = Entity prototypes intended for debugging & testing. +entity-category-suffix-debug = Debug entity-category-name-spawner = Spawner entity-category-desc-spawner = Entity prototypes that spawn other entities. entity-category-name-hide = Hidden -entity-category-desc-hide = Entity prototypes that should be hidden from the spawn menu \ No newline at end of file +entity-category-desc-hide = Entity prototypes that should be hidden from entity spawn menus diff --git a/Robust.Shared/Localization/LocalizationManager.Entity.cs b/Robust.Shared/Localization/LocalizationManager.Entity.cs index 2b0277cc8..7ae177f7e 100644 --- a/Robust.Shared/Localization/LocalizationManager.Entity.cs +++ b/Robust.Shared/Localization/LocalizationManager.Entity.cs @@ -125,6 +125,15 @@ namespace Robust.Shared.Localization } } + // Attempt to infer suffix from entity categories + if (suffix == null) + { + var suffixes = _prototype.Index(prototypeId).Categories + .Where(x => x.Suffix != null) + .Select(x => GetString(x.Suffix!)); + suffix = string.Join(", ", suffixes); + } + return new EntityLocData( name ?? "", desc ?? "", diff --git a/Robust.Shared/Prototypes/EntProtoId.cs b/Robust.Shared/Prototypes/EntProtoId.cs index 12902f755..c98596b92 100644 --- a/Robust.Shared/Prototypes/EntProtoId.cs +++ b/Robust.Shared/Prototypes/EntProtoId.cs @@ -23,6 +23,11 @@ public readonly record struct EntProtoId(string Id) : IEquatable, ICompa return protoId.Id; } + public static implicit operator EntProtoId(EntityPrototype proto) + { + return new EntProtoId(proto.ID); + } + public static implicit operator EntProtoId(string id) { return new EntProtoId(id); diff --git a/Robust.Shared/Prototypes/EntityCategoryAttribute.cs b/Robust.Shared/Prototypes/EntityCategoryAttribute.cs new file mode 100644 index 000000000..ce849cbda --- /dev/null +++ b/Robust.Shared/Prototypes/EntityCategoryAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Robust.Shared.Prototypes; + +/// +/// Attribute that can be applied to components to force any entity prototypes with that component to automatically +/// get added to an instance. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class EntityCategoryAttribute(params string[] categories) : Attribute +{ + public readonly string[] Categories = categories; +} diff --git a/Robust.Shared/Prototypes/EntityCategoryPrototype.cs b/Robust.Shared/Prototypes/EntityCategoryPrototype.cs index 3f2331fa5..d1bf0d020 100644 --- a/Robust.Shared/Prototypes/EntityCategoryPrototype.cs +++ b/Robust.Shared/Prototypes/EntityCategoryPrototype.cs @@ -1,25 +1,51 @@ +using System.Collections.Generic; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; namespace Robust.Shared.Prototypes; /// -/// Prototype that represents game entities. +/// Prototype that represents some entity prototype category. +/// Useful for sorting or grouping entity prototypes for mapping/spawning UIs. /// [Prototype("entityCategory")] public sealed partial class EntityCategoryPrototype : IPrototype { - [IdDataField] - public string ID { get; private set; } = default!; + [IdDataField] public string ID { get; private set; } = default!; /// /// Localized name of the category, for use in entity spawn menus. /// - [DataField("name")] - public string? Name { get; private set; } + [DataField] public string? Name; /// /// Localized description of the category, for use in entity spawn menus. /// - [DataField("description")] - public string? Description { get; private set; } + [DataField] public string? Description; + + /// + /// Default suffix to give all entities that belong to this prototype. + /// See . + /// + [DataField] public string? Suffix; + + /// + /// If true, any entity prototypes that belong to this category should not be shown in general entity spawning UIs. + /// Useful for various entities that shouldn't be spawned directly. + /// + [DataField] public bool HideSpawnMenu; + + /// + /// List of components that will cause an entity prototype to be automatically included in this category. + /// + [DataField(customTypeSerializer:typeof(CustomHashSetSerializer))] + public HashSet? Components; + + /// + /// If true, then an entity prototype will automatically get added to this category if any of its parent belonged to + /// it category. + /// + [DataField] + public bool Inheritable = true; } diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 62b251ccf..0e19eaeb4 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -27,9 +27,6 @@ namespace Robust.Shared.Prototypes private static readonly Dictionary LocPropertiesDefault = new(); - [ValidatePrototypeId] - private const string HideCategory = "hideSpawnMenu"; - // LOCALIZATION NOTE: // Localization-related properties in here are manually localized in LocalizationManager. // As such, they should NOT be inherited to avoid confusing the system. @@ -60,9 +57,16 @@ namespace Robust.Shared.Prototypes [DataField("suffix")] public string? SetSuffix { get; private set; } - [DataField] - [AlwaysPushInheritance] - public HashSet> Categories = new(); + [DataField("categories"), Access(typeof(PrototypeManager))] + [NeverPushInheritance] + internal HashSet>? CategoriesInternal; + + /// + /// What categories this prototype belongs to. This includes categories inherited from parents and categories + /// that were automatically inferred from the prototype's components. + /// + [ViewVariables] + public IReadOnlySet Categories { get; internal set; } = new HashSet(); [ViewVariables] public IReadOnlyDictionary LocProperties => _locPropertiesSet ?? LocPropertiesDefault; @@ -99,10 +103,11 @@ namespace Robust.Shared.Prototypes [ViewVariables] [NeverPushInheritance] [DataField("noSpawn")] - [Obsolete("Use the HideSpawnMenu")] + [Obsolete("Use HideSpawnMenu")] public bool NoSpawn { get; private set; } - public bool HideSpawnMenu => Categories.Contains(HideCategory) || NoSpawn; + [Access(typeof(PrototypeManager))] + public bool HideSpawnMenu { get; internal set; } [DataField("placement")] private EntityPlacementProperties PlacementProperties = new(); diff --git a/Robust.Shared/Prototypes/IPrototypeManager.cs b/Robust.Shared/Prototypes/IPrototypeManager.cs index 68f1d0b2b..48b67cdce 100644 --- a/Robust.Shared/Prototypes/IPrototypeManager.cs +++ b/Robust.Shared/Prototypes/IPrototypeManager.cs @@ -64,14 +64,25 @@ public interface IPrototypeManager /// /// Returns an of all parents of a prototype of a certain kind. /// - IEnumerable EnumerateParents(string kind, bool includeSelf = false) + /// + /// Note that this will skip abstract parents, even if the abstract parent may have concrete grand-parents. + /// + IEnumerable EnumerateParents(T proto, bool includeSelf = false) where T : class, IPrototype, IInheritingPrototype; - /// - /// Returns an of parents of a prototype of a certain kind. - /// + /// + IEnumerable EnumerateParents(string id, bool includeSelf = false) + where T : class, IPrototype, IInheritingPrototype; + + /// IEnumerable EnumerateParents(Type kind, string id, bool includeSelf = false); + /// + /// Variant of that includes abstract parents. + /// + IEnumerable<(string id, T?)> EnumerateAllParents(string id, bool includeSelf = false) + where T : class, IPrototype, IInheritingPrototype; + /// /// Returns all of the registered prototype kinds. /// @@ -399,6 +410,11 @@ public interface IPrototypeManager /// Tries to get a random prototype. /// bool TryGetRandom(IRobustRandom random, [NotNullWhen(true)] out IPrototype? prototype) where T : class, IPrototype; + + /// + /// Entity prototypes grouped by their categories. + /// + FrozenDictionary, IReadOnlyList> Categories { get; } } internal interface IPrototypeManagerInternal : IPrototypeManager diff --git a/Robust.Shared/Prototypes/ProtoId.cs b/Robust.Shared/Prototypes/ProtoId.cs index d60254b5e..4871f8fe6 100644 --- a/Robust.Shared/Prototypes/ProtoId.cs +++ b/Robust.Shared/Prototypes/ProtoId.cs @@ -20,6 +20,11 @@ public readonly record struct ProtoId(string Id) : IEquatable, ICompa return protoId.Id; } + public static implicit operator ProtoId(T proto) + { + return new ProtoId(proto.ID); + } + public static implicit operator ProtoId(string id) { return new ProtoId(id); diff --git a/Robust.Shared/Prototypes/PrototypeManager.Categories.cs b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs new file mode 100644 index 000000000..95e97768b --- /dev/null +++ b/Robust.Shared/Prototypes/PrototypeManager.Categories.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +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; + +// This partial class handles entity prototype categories +public abstract partial class PrototypeManager : IPrototypeManagerInternal +{ + /// + /// Cached array of components with the + /// + private (string, EntityCategoryAttribute)[]? _autoComps; + + public FrozenDictionary, IReadOnlyList> Categories { get; private set; } + = FrozenDictionary, IReadOnlyList>.Empty; + + private void UpdateCategories() + { + // Update automatically categorized prototypes + var autoCategories = GetAutomaticCategories(); + + var entityCount = Count(); + var cache = new Dictionary>(entityCount); + + var categoryCount = Count(); + var categories = new Dictionary, List>(categoryCount); + + foreach (var proto in EnumeratePrototypes()) + { + UpdateCategories(proto, cache, autoCategories, categories); + } + + // Ensure all categories have an entry in the dictionary, even if it is empty. + foreach (var category in EnumeratePrototypes()) + { + categories.GetOrNew(category.ID); + } + + DebugTools.Assert(categories.Values.All(x => x.ToHashSet().Count == x.Count)); + Categories = categories.ToFrozenDictionary(x => x.Key, x => (IReadOnlyList)x.Value); + } + + private Dictionary> GetAutomaticCategories() + { + var dict = new Dictionary>(); + foreach (var category in EnumeratePrototypes()) + { + if (category.Components == null) + continue; + + foreach (var comp in category.Components) + { + dict.GetOrNew(comp).Add(category); + } + } + + _autoComps ??= _factory.GetAllRegistrations() + .Where(x => x.Type.HasCustomAttribute()) + .Select(x => (x.Name, x.Type.GetCustomAttribute()!)) + .ToArray(); + + foreach (var (name, attr) in _autoComps) + { + foreach (var categoryId in attr.Categories) + { + if (TryIndex(categoryId, out EntityCategoryPrototype? category)) + dict.GetOrNew(name).Add(category); + else + Sawmill.Error($"Component {name} has invalid {nameof(EntityCategoryAttribute)} argument: {categoryId}"); + } + } + + return dict; + } + + private IReadOnlySet UpdateCategories(EntProtoId id, + Dictionary> cache, + Dictionary> autoCategories, + Dictionary, List> categories) + { + if (cache.TryGetValue(id, out var existing)) + return existing; // Already Updated + + var set = new HashSet(); + + // Get explicitly specified categories + if (!TryGetMapping(typeof(EntityPrototype), id, out var mapping)) + throw new UnknownPrototypeException(id, typeof(EntityPrototype)); + + // Have to rely on the mapping instead of the instance's data-field to support categories being added + // to abstract prototypes + if (mapping.TryGet("categories", out SequenceDataNode? node)) + { + foreach (var dataNode in node) + { + var categoryId = ((ValueDataNode) dataNode).Value; + if (TryIndex(categoryId, out EntityCategoryPrototype? categoryInstance)) + set.Add(categoryInstance); + else + Sawmill.Error($"Entity prototype {id} specifies an invalid {nameof(EntityCategoryPrototype)}: {categoryId}"); + } + } + + DebugTools.Assert(!TryIndex(id, out var instance) + || instance.CategoriesInternal == null + || instance.CategoriesInternal.All(x => + set.Any(y => y.ID == x))); + + // Get inherited categories + foreach (var (parentId, _) in EnumerateAllParents(id)) + { + var parentCategories = UpdateCategories(parentId, cache, autoCategories, categories); + foreach (var category in parentCategories) + { + if (category.Inheritable) + set.Add(category); + } + } + + if (!TryIndex(id, out var protoInstance)) + { + // Prototype is abstract + cache.Add(id, set); + return set; + } + + // Get automated categories inferred from components + foreach (var comp in protoInstance.Components.Keys) + { + if (autoCategories.TryGetValue(comp, out var autoCats)) + set.UnionWith(autoCats); + } + + cache.Add(id, set); + protoInstance.Categories = set; + + foreach (var category in set) + { + if (category.HideSpawnMenu) + protoInstance.HideSpawnMenu = true; + + categories.GetOrNew(category).Add(protoInstance); + } + +#pragma warning disable CS0618 // Type or member is obsolete + protoInstance.HideSpawnMenu |= protoInstance.NoSpawn; +#pragma warning restore CS0618 // Type or member is obsolete + + return set; + } +} diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 6020e602e..497051439 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -101,7 +101,14 @@ namespace Robust.Shared.Prototypes } /// - public IEnumerable EnumerateParents(string kind, bool includeSelf = false) + public IEnumerable EnumerateParents(T proto, bool includeSelf = false) + where T : class, IPrototype, IInheritingPrototype + { + return EnumerateParents(proto.ID, includeSelf); + } + + /// + public IEnumerable EnumerateParents(string id, bool includeSelf = false) where T : class, IPrototype, IInheritingPrototype { if (!_hasEverBeenReloaded) @@ -109,18 +116,24 @@ namespace Robust.Shared.Prototypes throw new InvalidOperationException("No prototypes have been loaded yet."); } - if (!TryIndex(kind, out var prototype)) + if (!TryIndex(id, out var prototype)) + yield break; + + if (includeSelf) + yield return prototype; + + if (prototype.Parents == null) yield break; - if (includeSelf) yield return prototype; - if (prototype.Parents == null) yield break; var queue = new Queue(prototype.Parents); while (queue.TryDequeue(out var prototypeId)) { if (!TryIndex(prototypeId, out var parent)) - yield break; + continue; // Abstract parent? + yield return parent; - if (parent.Parents == null) continue; + if (parent.Parents == null) + continue; foreach (var parentId in parent.Parents) { @@ -144,6 +157,7 @@ namespace Robust.Shared.Prototypes if (!TryIndex(kind, id, out var prototype)) yield break; + if (includeSelf) yield return prototype; @@ -155,7 +169,7 @@ namespace Robust.Shared.Prototypes while (queue.TryDequeue(out var prototypeId)) { if (!TryIndex(kind, prototypeId, out var parent)) - continue; + continue; // Abstract parent? yield return parent; iPrototype = (IInheritingPrototype)parent; @@ -169,6 +183,55 @@ namespace Robust.Shared.Prototypes } } + /// + public IEnumerable<(string id, T?)> EnumerateAllParents(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(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 EnumeratePrototypeKinds() { if (!_hasEverBeenReloaded) @@ -352,6 +415,8 @@ namespace Robust.Shared.Prototypes } Freeze(modifiedKinds); + if (modifiedKinds.Any(x => x.Type == typeof(EntityPrototype) || x.Type == typeof(EntityCategoryPrototype))) + UpdateCategories(); #endif //todo paul i hate it but i am not opening that can of worms in this refactor @@ -469,14 +534,19 @@ namespace Robust.Shared.Prototypes }); var modifiedKinds = new HashSet(); + bool reloadCategories = false; while (protoChannel.Reader.TryRead(out var item)) { var kind = item.KindData; kind.UnfrozenInstances ??= kind.Instances.ToDictionary(); kind.UnfrozenInstances[item.Id] = item.Prototype; modifiedKinds.Add(kind); + if (kind.Type == typeof(EntityPrototype) || kind.Type == typeof(EntityCategoryPrototype)) + reloadCategories = true; } Freeze(modifiedKinds); + if (reloadCategories) + UpdateCategories(); } finally { diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs new file mode 100644 index 000000000..cc82c0138 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/ComponentNameSerializer.cs @@ -0,0 +1,37 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +/// +/// Simple string serializer that just validates that strings correspond to valid component names +/// +public sealed class ComponentNameSerializer : ITypeSerializer +{ + public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node, + IDependencyCollection dependencies, ISerializationContext? context = null) + { + var factory = dependencies.Resolve(); + if (!factory.TryGetRegistration(node.Value, out _)) + return new ErrorNode(node, $"Unknown component kind: {node.Value}"); + + return new ValidatedValueNode(node); + } + + public string Read(ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, + SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate? instanceProvider = null) + { + return node.Value; + } + + public DataNode Write(ISerializationManager serializationManager, string value, IDependencyCollection dependencies, + bool alwaysWrite = false, ISerializationContext? context = null) + { + return new ValueDataNode(value); + } +} diff --git a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs b/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs index 4caecef3a..ba1585e2c 100644 --- a/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs +++ b/Robust.UnitTesting/Server/GameObjects/Components/Transform_Test.cs @@ -52,7 +52,7 @@ namespace Robust.UnitTesting.Server.GameObjects.Components IoCManager.Resolve().Initialize(); var manager = IoCManager.Resolve(); - manager.RegisterKind(typeof(EntityPrototype)); + manager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); manager.LoadFromStream(new StringReader(Prototypes)); manager.ResolveResults(); diff --git a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs b/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs index 9f6130ef0..ea5ac10ec 100644 --- a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs +++ b/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs @@ -69,7 +69,7 @@ entities: resourceManager.MountString("/EnginePrototypes/TestMapEntity.yml", Prototype); var protoMan = IoCManager.Resolve(); - protoMan.RegisterKind(typeof(EntityPrototype)); + protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); protoMan.LoadDirectory(new ("/EnginePrototypes")); protoMan.LoadDirectory(new ("/Prototypes")); diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index 78b073403..a198384ad 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -321,7 +321,7 @@ namespace Robust.UnitTesting.Server var protoMan = container.Resolve(); protoMan.Initialize(); - protoMan.RegisterKind(typeof(EntityPrototype)); + protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); _protoDelegate?.Invoke(protoMan); protoMan.ResolveResults(); diff --git a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs b/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs index e8bd66296..ec5ada895 100644 --- a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs +++ b/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs @@ -28,7 +28,7 @@ namespace Robust.UnitTesting.Shared.Localization var protoMan = IoCManager.Resolve(); - protoMan.RegisterKind(typeof(EntityPrototype)); + protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); protoMan.LoadDirectory(new ResPath("/EnginePrototypes")); protoMan.ResolveResults(); diff --git a/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs b/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs index 79f8efdb9..c8433c1a8 100644 --- a/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs +++ b/Robust.UnitTesting/Shared/Map/EntityCoordinates_Tests.cs @@ -18,7 +18,7 @@ namespace Robust.UnitTesting.Shared.Map { IoCManager.Resolve().Initialize(); var prototypeManager = IoCManager.Resolve(); - prototypeManager.RegisterKind(typeof(EntityPrototype)); + prototypeManager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); prototypeManager.ResolveResults(); var factory = IoCManager.Resolve(); diff --git a/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs b/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs index 387f891a2..e0e90107c 100644 --- a/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs +++ b/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs @@ -44,7 +44,7 @@ namespace Robust.UnitTesting.Shared.Prototypes { IoCManager.Resolve().Initialize(); _prototypes = (PrototypeManager) IoCManager.Resolve(); - _prototypes.RegisterKind(typeof(EntityPrototype)); + _prototypes.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); _prototypes.LoadString(InitialPrototypes); _prototypes.ResolveResults(); diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManagerCategoriesTest.cs b/Robust.UnitTesting/Shared/Prototypes/PrototypeManagerCategoriesTest.cs new file mode 100644 index 000000000..28dcfaf60 --- /dev/null +++ b/Robust.UnitTesting/Shared/Prototypes/PrototypeManagerCategoriesTest.cs @@ -0,0 +1,248 @@ +using System; +using System.Linq; +using JetBrains.Annotations; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.Prototypes; + +[UsedImplicitly] +[TestFixture] +public sealed class PrototypeManagerCategoriesTest : RobustUnitTest +{ + + private IPrototypeManager _protoMan = default!; + protected override Type[] ExtraComponents => [typeof(AutoCategoryComponent)];//, typeof(AttributeAutoCategoryComponent)]; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + _protoMan = IoCManager.Resolve(); + _protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + _protoMan.LoadString(TestPrototypes); + _protoMan.ResolveResults(); + } + + [Test] + public void TestExplicitCategories() + { + var @default = _protoMan.Index("Default"); + Assert.That(@default.Categories, Is.Empty); + Assert.That(@default.CategoriesInternal, Is.Null); + Assert.That(@default.HideSpawnMenu, Is.False); + + var hide = _protoMan.Index("Hide"); + Assert.That(hide.Categories.Count, Is.EqualTo(1)); + Assert.That(hide.CategoriesInternal?.Count, Is.EqualTo(1)); + Assert.That(hide.HideSpawnMenu, Is.True); + } + + [Test] + public void TestInheritance() + { + var child = _protoMan.Index("InheritChild"); + Assert.That(child.Categories.Count, Is.EqualTo(1)); + Assert.That(child.CategoriesInternal, Is.Null); + Assert.That(child.HideSpawnMenu, Is.True); + + var noInheritParent = _protoMan.Index("NoInheritParent"); + Assert.That(noInheritParent.Categories.Count, Is.EqualTo(1)); + Assert.That(noInheritParent.CategoriesInternal?.Count, Is.EqualTo(1)); + + var noInheritChild = _protoMan.Index("NoInheritChild"); + Assert.That(noInheritChild.Categories, Is.Empty); + Assert.That(noInheritChild.CategoriesInternal, Is.Null); + } + + [Test] + public void TestAbstractInheritance() + { + Assert.That(_protoMan.HasIndex("AbstractParent"), Is.False); + Assert.That(_protoMan.HasIndex("AbstractGrandChild"), Is.False); + + var concreteChild = _protoMan.Index("ConcreteChild"); + Assert.That(concreteChild.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Default"})); + Assert.That(concreteChild.CategoriesInternal, Is.Null); + + var composition = _protoMan.Index("CompositionAbstract"); + Assert.That(composition.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Default", "Hide", "Auto"})); + Assert.That(composition.CategoriesInternal, Is.Null); + } + + [Test] + public void TestComposition() + { + var compA = _protoMan.Index("CompositionA"); + Assert.That(compA.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Default", "Hide"})); + Assert.That(compA.CategoriesInternal?.Count, Is.EqualTo(2)); + + var compB = _protoMan.Index("CompositionB"); + Assert.That(compB.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Default", "NoInherit", "Auto"})); + Assert.That(compB.CategoriesInternal?.Count, Is.EqualTo(3)); + + var childA = _protoMan.Index("CompositionChildA"); + Assert.That(childA.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Default", "Hide", "Auto"})); + Assert.That(childA.CategoriesInternal, Is.Null); + + var childB = _protoMan.Index("CompositionChildB"); + Assert.That(childB.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Default", "Hide", "Auto", "Default2"})); + Assert.That(childB.CategoriesInternal?.Count, Is.EqualTo(1)); + } + + [Test] + public void TestAutoCategorization() + { + var auto = _protoMan.Index("Auto"); + Assert.That(auto.Categories.Select(x => x.ID), Is.EquivalentTo(new []{"Auto"})); + Assert.That(auto.CategoriesInternal, Is.Null); + + //var autoAttrib = _protoMan.Index("AutoAttribute"); + //Assert.That(autoAttrib.Categories.Select(x => x.ID), Is.EquivalentTo(new []{"Auto"})); + //Assert.That(autoAttrib.CategoriesInternal, Is.Null); + + var autoChild = _protoMan.Index("AutoChild"); + Assert.That(autoChild.Categories.Select(x => x.ID), + Is.EquivalentTo(new []{"Auto", "Default"})); + Assert.That(autoChild.CategoriesInternal?.Count, Is.EqualTo(1)); + + + } + + [Test] + public void TestCategoryGrouping() + { + var none = _protoMan.Categories[new("None")].Select(x=> x.ID); + Assert.That(none, Is.Empty); + + var @default = _protoMan.Categories[new("Default")].Select(x=> x.ID); + Assert.That(@default, Is.EquivalentTo(new[] {"ConcreteChild", "CompositionAbstract", "CompositionA", "CompositionB", "CompositionChildA", "CompositionChildB", "AutoChild"})); + + var default2 = _protoMan.Categories[new("Default2")].Select(x=> x.ID); + Assert.That(default2, Is.EquivalentTo(new[] {"CompositionChildB"})); + + var hide = _protoMan.Categories[new("Hide")].Select(x=> x.ID); + Assert.That(hide, Is.EquivalentTo(new[] {"Hide", "CompositionAbstract", "CompositionA", "CompositionChildA", "CompositionChildB", "InheritChild"})); + + var noInherit = _protoMan.Categories[new("NoInherit")].Select(x=> x.ID); + Assert.That(noInherit, Is.EquivalentTo(new[] {"NoInheritParent", "CompositionB"})); + + var auto = _protoMan.Categories[new("Auto")].Select(x=> x.ID); + Assert.That(auto, Is.EquivalentTo(new[] {"CompositionAbstract", "CompositionB", "CompositionChildA", "CompositionChildB", "Auto", "AutoChild"}));//, "AutoAttribute"})); + } + + const string TestPrototypes = @" +- type: entityCategory + id: None + +- type: entityCategory + id: Default + +- type: entityCategory + id: Default2 + +- type: entityCategory + id: Hide + hideSpawnMenu: true + +- type: entityCategory + id: NoInherit + inheritable: false + +- type: entityCategory + id: Auto + components: [ AutoCategory ] + +- type: entity + id: Default + +- type: entity + id: Hide + categories: [ Hide ] + +- type: entity + id: InheritChild + parent: Hide + +- type: entity + id: NoInheritParent + categories: [ NoInherit ] + +- type: entity + id: NoInheritChild + parent: NoInheritParent + +- type: entity + id: CompositionA + categories: [ Default, Hide ] + +- type: entity + id: CompositionB + categories: [ Default, NoInherit, Auto ] + +- type: entity + id: CompositionChildA + parent: [CompositionA, CompositionB] + +- type: entity + id: CompositionChildB + parent: [CompositionA, CompositionB] + categories: [ Default2 ] + +- type: entity + id: AbstractParent + abstract: true + categories: [ Default ] + +- type: entity + id: ConcreteChild + parent: AbstractParent + +- type: entity + abstract: true + id: AbstractGrandChild + parent: ConcreteChild + categories: [ Hide ] + +- type: entity + id: CompositionAbstract + parent: [ AbstractGrandChild, CompositionB ] + +- type: entity + id: Auto + components: + - type: AutoCategory + +#- type: entity +# id: AutoAttribute +# components: +# - type: AttributeAutoCategory + +- type: entity + id: AutoParent + abstract: true + categories: [ NoInherit ] + components: + - type: AutoCategory + +- type: entity + id: AutoChild + parent: AutoParent + categories: [ Default ] +"; +} + +public sealed partial class AutoCategoryComponent : Component; + +// TODO test-local IReflectionManager +// [EntityCategory("Auto")] +// public sealed partial class AttributeAutoCategoryComponent : Component; diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs index 78d2574f1..5e7352619 100644 --- a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs +++ b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs @@ -29,7 +29,7 @@ namespace Robust.UnitTesting.Shared.Prototypes { IoCManager.Resolve().Initialize(); manager = IoCManager.Resolve(); - manager.RegisterKind(typeof(EntityPrototype)); + manager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); manager.LoadString(DOCUMENT); manager.ResolveResults(); } diff --git a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs b/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs index d64b083a1..6a24fbd6f 100644 --- a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs @@ -54,7 +54,7 @@ namespace Robust.UnitTesting.Shared.Serialization var prototypeManager = IoCManager.Resolve(); - prototypeManager.RegisterKind(typeof(EntityPrototype)); + prototypeManager.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); prototypeManager.LoadString(Prototypes); prototypeManager.ResolveResults(); diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs index d01a6ca21..5cd6e48fc 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/Custom/Prototype/PrototypeIdListSerializerTest.cs @@ -45,7 +45,7 @@ entitiesImmutableList: { var protoMan = IoCManager.Resolve(); - protoMan.RegisterKind(typeof(EntityPrototype)); + protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); protoMan.LoadString(Prototypes); protoMan.ResolveResults(); }