From b0482dcb6387c2d2edfba603647b12f62768a906 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Tue, 2 Feb 2021 02:11:04 +0100 Subject: [PATCH] Add tag component and test (#2761) * Add tag component and test * Remove 0 capacity * Add tag component extensions * Change tags to be prototypes Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com> --- Content.Client/IgnoredComponents.cs | 1 + Content.IntegrationTests/Tests/Tag/TagTest.cs | 225 ++++++++++++++++ .../Components/Tag/TagComponent.cs | 251 ++++++++++++++++++ .../Components/Tag/TagComponentExtensions.cs | 243 +++++++++++++++++ Content.Shared/Prototypes/Tag/TagPrototype.cs | 31 +++ 5 files changed, 751 insertions(+) create mode 100644 Content.IntegrationTests/Tests/Tag/TagTest.cs create mode 100644 Content.Server/GameObjects/Components/Tag/TagComponent.cs create mode 100644 Content.Server/GameObjects/Components/Tag/TagComponentExtensions.cs create mode 100644 Content.Shared/Prototypes/Tag/TagPrototype.cs diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 101ce6c852..652a75edee 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -228,6 +228,7 @@ namespace Content.Client "MachineFrame", "MachineBoard", "ChemicalAmmo", + "Tag", "BiologicalSurgeryData", "CargoTelepad", "TraitorDeathMatchRedemption", diff --git a/Content.IntegrationTests/Tests/Tag/TagTest.cs b/Content.IntegrationTests/Tests/Tag/TagTest.cs new file mode 100644 index 0000000000..3159f38720 --- /dev/null +++ b/Content.IntegrationTests/Tests/Tag/TagTest.cs @@ -0,0 +1,225 @@ +#nullable enable +using System.Collections.Generic; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Tag; +using Content.Shared.Prototypes.Tag; +using NUnit.Framework; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Tag +{ + [TestFixture] + [TestOf(typeof(TagComponent))] + public class TagTest : ContentIntegrationTest + { + private const string TagEntityId = "TagTestDummy"; + + // Register these three into the prototype manager + private const string StartingTag = "A"; + private const string AddedTag = "EIOU"; + private const string UnusedTag = "E"; + + // Do not register this one + private const string UnregisteredTag = "AAAAAAAAA"; + + private static readonly string Prototypes = $@" +- type: Tag + id: {StartingTag} + +- type: Tag + id: {AddedTag} + +- type: Tag + id: {UnusedTag} + +- type: entity + id: {TagEntityId} + name: {TagEntityId} + components: + - type: Tag + tags: + - {StartingTag}"; + + [Test] + public async Task TagComponentTest() + { + var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes}; + var server = StartServerDummyTicker(options); + + await server.WaitIdleAsync(); + + var sMapManager = server.ResolveDependency(); + var sEntityManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); + + IEntity sTagDummy = null!; + TagComponent sTagComponent = null!; + + await server.WaitPost(() => + { + sMapManager.CreateNewMapEntity(MapId.Nullspace); + sTagDummy = sEntityManager.SpawnEntity(TagEntityId, MapCoordinates.Nullspace); + sTagComponent = sTagDummy.GetComponent(); + }); + + await server.WaitAssertion(() => + { + // Has one tag, the starting tag + Assert.That(sTagComponent.Tags.Count, Is.EqualTo(1)); + + var startingTagPrototype = sPrototypeManager.Index(StartingTag); + Assert.That(sTagComponent.Tags, Contains.Item(startingTagPrototype)); + + // Single + Assert.True(sTagDummy.HasTag(StartingTag)); + Assert.True(sTagComponent.HasTag(StartingTag)); + + // Any + Assert.True(sTagDummy.HasAnyTag(StartingTag)); + Assert.True(sTagComponent.HasAnyTag(StartingTag)); + + // All + Assert.True(sTagDummy.HasAllTags(StartingTag)); + Assert.True(sTagComponent.HasAllTags(StartingTag)); + + // Does not have the added tag + var addedTagPrototype = sPrototypeManager.Index(AddedTag); + Assert.That(sTagComponent.Tags, Does.Not.Contains(addedTagPrototype)); + + // Single + Assert.False(sTagDummy.HasTag(AddedTag)); + Assert.False(sTagComponent.HasTag(AddedTag)); + + // Any + Assert.False(sTagDummy.HasAnyTag(AddedTag)); + Assert.False(sTagComponent.HasAnyTag(AddedTag)); + + // All + Assert.False(sTagDummy.HasAllTags(AddedTag)); + Assert.False(sTagComponent.HasAllTags(AddedTag)); + + // Does not have the unused tag + var unusedTagPrototype = sPrototypeManager.Index(UnusedTag); + Assert.That(sTagComponent.Tags, Does.Not.Contains(unusedTagPrototype)); + + // Single + Assert.False(sTagDummy.HasTag(UnusedTag)); + Assert.False(sTagComponent.HasTag(UnusedTag)); + + // Any + Assert.False(sTagDummy.HasAnyTag(UnusedTag)); + Assert.False(sTagComponent.HasAnyTag(UnusedTag)); + + // All + Assert.False(sTagDummy.HasAllTags(UnusedTag)); + Assert.False(sTagComponent.HasAllTags(UnusedTag)); + + // Throws when checking for an unregistered tag + Assert.Throws(() => + { + sPrototypeManager.Index(UnregisteredTag); + }); + + // Single + Assert.Throws(() => + { + sTagDummy.HasTag(UnregisteredTag); + }); + Assert.Throws(() => + { + sTagComponent.HasTag(UnregisteredTag); + }); + + // Any + Assert.Throws(() => + { + sTagDummy.HasAnyTag(UnregisteredTag); + }); + Assert.Throws(() => + { + sTagComponent.HasAnyTag(UnregisteredTag); + }); + + // All + Assert.Throws(() => + { + sTagDummy.HasAllTags(UnregisteredTag); + }); + Assert.Throws(() => + { + sTagComponent.HasAllTags(UnregisteredTag); + }); + + // Cannot add the starting tag again + Assert.That(sTagComponent.AddTag(StartingTag), Is.False); + Assert.That(sTagComponent.AddTags(StartingTag, StartingTag), Is.False); + Assert.That(sTagComponent.AddTags(new List {StartingTag, StartingTag}), Is.False); + + // Has the starting tag + Assert.That(sTagComponent.HasTag(StartingTag), Is.True); + Assert.That(sTagComponent.HasAllTags(StartingTag, StartingTag), Is.True); + Assert.That(sTagComponent.HasAllTags(new List {StartingTag, StartingTag}), Is.True); + Assert.That(sTagComponent.HasAnyTag(StartingTag, StartingTag), Is.True); + Assert.That(sTagComponent.HasAnyTag(new List {StartingTag, StartingTag}), Is.True); + + // Does not have the added tag yet + Assert.That(sTagComponent.HasTag(AddedTag), Is.False); + Assert.That(sTagComponent.HasAllTags(AddedTag, AddedTag), Is.False); + Assert.That(sTagComponent.HasAllTags(new List {AddedTag, AddedTag}), Is.False); + Assert.That(sTagComponent.HasAnyTag(AddedTag, AddedTag), Is.False); + Assert.That(sTagComponent.HasAnyTag(new List {AddedTag, AddedTag}), Is.False); + + // Has a combination of the two tags + Assert.That(sTagComponent.HasAnyTag(StartingTag, AddedTag), Is.True); + Assert.That(sTagComponent.HasAnyTag(new List {StartingTag, AddedTag}), Is.True); + + // Does not have both tags + Assert.That(sTagComponent.HasAllTags(StartingTag, AddedTag), Is.False); + Assert.That(sTagComponent.HasAllTags(new List {StartingTag, AddedTag}), Is.False); + + // Cannot remove a tag that does not exist + Assert.That(sTagComponent.RemoveTag(AddedTag), Is.False); + Assert.That(sTagComponent.RemoveTags(AddedTag, AddedTag), Is.False); + Assert.That(sTagComponent.RemoveTags(new List {AddedTag, AddedTag}), Is.False); + + // Can add the new tag + Assert.That(sTagComponent.AddTag(AddedTag), Is.True); + + // Cannot add it twice + Assert.That(sTagComponent.AddTag(AddedTag), Is.False); + + // Cannot add existing tags + Assert.That(sTagComponent.AddTags(StartingTag, AddedTag), Is.False); + Assert.That(sTagComponent.AddTags(new List {StartingTag, AddedTag}), Is.False); + + // Now has two tags + Assert.That(sTagComponent.Tags.Count, Is.EqualTo(2)); + + // Has both tags + Assert.That(sTagComponent.HasTag(StartingTag), Is.True); + Assert.That(sTagComponent.HasTag(AddedTag), Is.True); + Assert.That(sTagComponent.HasAllTags(StartingTag, StartingTag), Is.True); + Assert.That(sTagComponent.HasAllTags(AddedTag, StartingTag), Is.True); + Assert.That(sTagComponent.HasAllTags(new List {StartingTag, AddedTag}), Is.True); + Assert.That(sTagComponent.HasAllTags(new List {AddedTag, StartingTag}), Is.True); + Assert.That(sTagComponent.HasAnyTag(StartingTag, AddedTag), Is.True); + Assert.That(sTagComponent.HasAnyTag(AddedTag, StartingTag), Is.True); + + // Remove the existing starting tag + Assert.That(sTagComponent.RemoveTag(StartingTag), Is.True); + + // Remove the existing added tag + Assert.That(sTagComponent.RemoveTags(AddedTag, AddedTag), Is.True); + + // No tags left to remove + Assert.That(sTagComponent.RemoveTags(new List {StartingTag, AddedTag}), Is.False); + + // No tags left in the component + Assert.That(sTagComponent.Tags, Is.Empty); + }); + } + } +} diff --git a/Content.Server/GameObjects/Components/Tag/TagComponent.cs b/Content.Server/GameObjects/Components/Tag/TagComponent.cs new file mode 100644 index 0000000000..4894b0c848 --- /dev/null +++ b/Content.Server/GameObjects/Components/Tag/TagComponent.cs @@ -0,0 +1,251 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Content.Shared.Prototypes.Tag; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Tag +{ + [RegisterComponent] + public class TagComponent : Component + { + public override string Name => "Tag"; + + [ViewVariables] + private readonly HashSet _tags = new(); + + public IReadOnlySet Tags => _tags; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataReadWriteFunction( + "tags", + null!, + (ids) => + { + _tags.Clear(); + + if (ids == null) + { + return; + } + + AddTags(ids); + }, + () => + { + var ids = new HashSet(); + + foreach (var tag in _tags) + { + ids.Add(tag.ID); + } + + return ids; + }); + } + + /// + /// Tries to add a tag if it doesn't already exist. + /// + /// The tag to add. + /// true if it was added, false if it already existed. + /// + /// Thrown if no exists with the given id. + /// + public bool AddTag(string id) + { + var tag = IoCManager.Resolve().Index(id); + + return _tags.Add(tag); + } + + /// + /// Tries to add the given tags if they don't already exist. + /// + /// The tags to add. + /// true if any tags were added, false if they all already existed. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool AddTags(params string[] ids) + { + return AddTags(ids.AsEnumerable()); + } + + /// + /// Tries to add the given tags if they don't already exist. + /// + /// The tags to add. + /// true if any tags were added, false if they all already existed. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool AddTags(IEnumerable ids) + { + var count = _tags.Count; + var prototypeManager = IoCManager.Resolve(); + + foreach (var id in ids) + { + var tag = prototypeManager.Index(id); + + _tags.Add(tag); + } + + return _tags.Count > count; + } + + /// + /// Checks if a tag has been added. + /// + /// The tag to check for. + /// true if it exists, false otherwise. + /// + /// Thrown if no exists with the given id. + /// + public bool HasTag(string id) + { + var tag = IoCManager.Resolve().Index(id); + + return _tags.Contains(tag); + } + + /// + /// Checks if all of the given tags have been added. + /// + /// The tags to check for. + /// true if they all exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool HasAllTags(params string[] ids) + { + return HasAllTags(ids.AsEnumerable()); + } + + /// + /// Checks if all of the given tags have been added. + /// + /// The tags to check for. + /// true if they all exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool HasAllTags(IEnumerable ids) + { + var prototypeManager = IoCManager.Resolve(); + + foreach (var id in ids) + { + var tag = prototypeManager.Index(id); + + if (!_tags.Contains(tag)) + { + return false; + } + } + + return true; + } + + /// + /// Checks if any of the given tags have been added. + /// + /// The tags to check for. + /// true if any of them exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool HasAnyTag(params string[] ids) + { + return HasAnyTag(ids.AsEnumerable()); + } + + /// + /// Checks if any of the given tags have been added. + /// + /// The tags to check for. + /// true if any of them exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool HasAnyTag(IEnumerable ids) + { + var prototypeManager = IoCManager.Resolve(); + + foreach (var id in ids) + { + var tag = prototypeManager.Index(id); + + if (_tags.Contains(tag)) + { + return true; + } + } + + return false; + } + + /// + /// Tries to remove a tag if it exists. + /// + /// The tag to remove. + /// + /// true if it was removed, false otherwise even if it didn't exist. + /// + /// + /// Thrown if no exists with the given id. + /// + public bool RemoveTag(string id) + { + var tag = IoCManager.Resolve().Index(id); + + return _tags.Remove(tag); + } + + /// + /// Tries to remove all of the given tags if they exist. + /// + /// The tags to remove. + /// + /// true if it was removed, false otherwise even if they didn't exist. + /// + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool RemoveTags(params string[] ids) + { + return RemoveTags(ids.AsEnumerable()); + } + + /// + /// Tries to remove all of the given tags if they exist. + /// + /// The tags to remove. + /// true if any tag was removed, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public bool RemoveTags(IEnumerable ids) + { + var count = _tags.Count; + var prototypeManager = IoCManager.Resolve(); + + foreach (var id in ids) + { + var tag = prototypeManager.Index(id); + + _tags.Remove(tag); + } + + return _tags.Count < count; + } + } +} diff --git a/Content.Server/GameObjects/Components/Tag/TagComponentExtensions.cs b/Content.Server/GameObjects/Components/Tag/TagComponentExtensions.cs new file mode 100644 index 0000000000..437b70af88 --- /dev/null +++ b/Content.Server/GameObjects/Components/Tag/TagComponentExtensions.cs @@ -0,0 +1,243 @@ +#nullable enable +using System.Collections.Generic; +using Content.Shared.Prototypes.Tag; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameObjects.Components.Tag +{ + public static class TagComponentExtensions + { + /// + /// Tries to add a tag to an entity if the tag doesn't already exist. + /// + /// The entity to add the tag to. + /// The tag to add. + /// + /// true if it was added, false otherwise even if it already existed. + /// + /// + /// Thrown if no exists with the given id. + /// + public static bool AddTag(this IEntity entity, string id) + { + return entity.EnsureComponent(out TagComponent tagComponent) && + tagComponent.AddTag(id); + } + + /// + /// Tries to add the given tags to an entity if the tags don't already exist. + /// + /// The entity to add the tag to. + /// The tags to add. + /// + /// true if any tags were added, false otherwise even if they all already existed. + /// + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool AddTags(this IEntity entity, params string[] ids) + { + return entity.EnsureComponent(out TagComponent tagComponent) && + tagComponent.AddTags(ids); + } + + /// + /// Tries to add the given tags to an entity if the tags don't already exist. + /// + /// The entity to add the tag to. + /// The tags to add. + /// + /// true if any tags were added, false otherwise even if they all already existed. + /// + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool AddTags(this IEntity entity, IEnumerable ids) + { + return entity.EnsureComponent(out TagComponent tagComponent) && + tagComponent.AddTags(ids); + } + + /// + /// Tries to add a tag to an entity if it has a + /// and the tag doesn't already exist. + /// + /// The entity to add the tag to. + /// The tag to add. + /// + /// true if it was added, false otherwise even if it already existed. + /// + /// + /// Thrown if no exists with the given id. + /// + public static bool TryAddTag(this IEntity entity, string id) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.AddTag(id); + } + + /// + /// Tries to add the given tags to an entity if it has a + /// and the tags don't already exist. + /// + /// The entity to add the tag to. + /// The tags to add. + /// + /// true if any tags were added, false otherwise even if they all already existed. + /// + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool TryAddTags(this IEntity entity, params string[] ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.AddTags(ids); + } + + /// + /// Tries to add the given tags to an entity if it has a + /// and the tags don't already exist. + /// + /// The entity to add the tag to. + /// The tags to add. + /// + /// true if any tags were added, false otherwise even if they all already existed. + /// + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool TryAddTags(this IEntity entity, IEnumerable ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.AddTags(ids); + } + + /// + /// Checks if a tag has been added to an entity. + /// + /// The entity to check. + /// The tag to check for. + /// true if it exists, false otherwise. + /// + /// Thrown if no exists with the given id. + /// + public static bool HasTag(this IEntity entity, string id) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.HasTag(id); + } + + /// + /// Checks if all of the given tags have been added to an entity. + /// + /// The entity to check. + /// The tags to check for. + /// true if they all exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool HasAllTags(this IEntity entity, params string[] ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.HasAllTags(ids); + } + + /// + /// Checks if all of the given tags have been added to an entity. + /// + /// The entity to check. + /// The tags to check for. + /// true if they all exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool HasAllTags(this IEntity entity, IEnumerable ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.HasAllTags(ids); + } + + /// + /// Checks if all of the given tags have been added to an entity. + /// + /// The entity to check. + /// The tags to check for. + /// true if any of them exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool HasAnyTag(this IEntity entity, params string[] ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.HasAnyTag(ids); + } + + /// + /// Checks if all of the given tags have been added to an entity. + /// + /// The entity to check. + /// The tags to check for. + /// true if any of them exist, false otherwise. + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool HasAnyTag(this IEntity entity, IEnumerable ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.HasAnyTag(ids); + } + + /// + /// Tries to remove a tag from an entity if it exists. + /// + /// The entity to remove the tag from. + /// The tag to remove. + /// + /// true if it was removed, false otherwise even if it didn't exist. + /// + /// + /// Thrown if no exists with the given id. + /// + public static bool RemoveTag(this IEntity entity, string id) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.RemoveTag(id); + } + + /// + /// Tries to remove a tag from an entity if it exists. + /// + /// The entity to remove the tag from. + /// The tag to remove. + /// + /// true if it was removed, false otherwise even if it didn't exist. + /// + /// Thrown if one of the ids represents an unregistered . + /// + /// + public static bool RemoveTags(this IEntity entity, params string[] ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.RemoveTags(ids); + } + + /// + /// Tries to remove a tag from an entity if it exists. + /// + /// The entity to remove the tag from. + /// The tag to remove. + /// + /// true if it was removed, false otherwise even if it didn't exist. + /// + /// + /// Thrown if one of the ids represents an unregistered . + /// + public static bool RemoveTags(this IEntity entity, IEnumerable ids) + { + return entity.TryGetComponent(out TagComponent? tagComponent) && + tagComponent.RemoveTags(ids); + } + } +} diff --git a/Content.Shared/Prototypes/Tag/TagPrototype.cs b/Content.Shared/Prototypes/Tag/TagPrototype.cs new file mode 100644 index 0000000000..984faf1816 --- /dev/null +++ b/Content.Shared/Prototypes/Tag/TagPrototype.cs @@ -0,0 +1,31 @@ +#nullable enable +using JetBrains.Annotations; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using YamlDotNet.RepresentationModel; + +namespace Content.Shared.Prototypes.Tag +{ + /// + /// Prototype representing a tag in YAML. + /// Meant to only have an ID property, as that is the only thing that + /// gets saved in TagComponent. + /// + [Prototype("Tag")] + public class TagPrototype : IPrototype, IIndexedPrototype, IExposeData + { + public string ID { get; [UsedImplicitly] private set; } = default!; + + public void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(this, x => x.ID, "id", ""); + } + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + ExposeData(serializer); + } + } +}