diff --git a/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs b/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs new file mode 100644 index 0000000000..d64f2a934d --- /dev/null +++ b/Content.IntegrationTests/Tests/Access/AccessReaderTest.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Content.IntegrationTests; +using Content.Server.Access.Components; +using Content.Server.Access.Systems; +using NUnit.Framework; +using Robust.Shared.GameObjects; + +namespace Content.Tests.Server.GameObjects.Components.Access +{ + [TestFixture] + [TestOf(typeof(AccessReader))] + public class AccessReaderTest : ContentIntegrationTest + { + [Test] + public async Task TestTags() + { + var server = StartServerDummyTicker(); + await server.WaitAssertion(() => + { + var system = EntitySystem.Get(); + + // test empty + var reader = new AccessReader(); + Assert.That(system.IsAllowed(reader, new[] { "Foo" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "Bar" }), Is.True); + Assert.That(system.IsAllowed(reader, new string[] { }), Is.True); + + // test deny + reader = new AccessReader(); + reader.DenyTags.Add("A"); + Assert.That(system.IsAllowed(reader, new[] { "Foo" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "A" }), Is.False); + Assert.That(system.IsAllowed(reader, new[] { "A", "Foo" }), Is.False); + Assert.That(system.IsAllowed(reader, new string[] { }), Is.True); + + // test one list + reader = new AccessReader(); + reader.AccessLists.Add(new HashSet { "A" }); + Assert.That(system.IsAllowed(reader, new[] { "A" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "B" }), Is.False); + Assert.That(system.IsAllowed(reader, new[] { "A", "B" }), Is.True); + Assert.That(system.IsAllowed(reader, new string[] { }), Is.False); + + // test one list - two items + reader = new AccessReader(); + reader.AccessLists.Add(new HashSet { "A", "B" }); + Assert.That(system.IsAllowed(reader, new[] { "A" }), Is.False); + Assert.That(system.IsAllowed(reader, new[] { "B" }), Is.False); + Assert.That(system.IsAllowed(reader, new[] { "A", "B" }), Is.True); + Assert.That(system.IsAllowed(reader, new string[] { }), Is.False); + + // test two list + reader = new AccessReader(); + reader.AccessLists.Add(new HashSet { "A" }); + reader.AccessLists.Add(new HashSet { "B", "C" }); + Assert.That(system.IsAllowed(reader, new[] { "A" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "B" }), Is.False); + Assert.That(system.IsAllowed(reader, new[] { "A", "B" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "C", "B" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "C", "B", "A" }), Is.True); + Assert.That(system.IsAllowed(reader, new string[] { }), Is.False); + + // test deny list + reader = new AccessReader(); + reader.AccessLists.Add(new HashSet { "A" }); + reader.DenyTags.Add("B"); + Assert.That(system.IsAllowed(reader, new[] { "A" }), Is.True); + Assert.That(system.IsAllowed(reader, new[] { "B" }), Is.False); + Assert.That(system.IsAllowed(reader, new[] { "A", "B" }), Is.False); + Assert.That(system.IsAllowed(reader, new string[] { }), Is.False); + }); + } + + } +} diff --git a/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs b/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs index 24c4e44de5..48328e95b8 100644 --- a/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs +++ b/Content.Server/AI/Pathfinding/Accessible/AiReachableSystem.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.AI.Pathfinding.Pathfinders; using Content.Shared.AI; using Content.Shared.GameTicking; @@ -38,6 +39,7 @@ namespace Content.Server.AI.Pathfinding.Accessible [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!; + [Dependency] private readonly AccessReaderSystem _accessReader = default!; /// /// Queued region updates @@ -184,7 +186,7 @@ namespace Content.Server.AI.Pathfinding.Accessible collisionMask = physics.CollisionMask; } - var access = AccessReader.FindAccessTags(entity); + var access = _accessReader.FindAccessTags(entity.Uid); // We'll do a quick traversable check before going through regions // If we can't access it we'll try to get a valid node in range (this is essentially an early-out) diff --git a/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs b/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs index 8ea9eab95e..b503f124a8 100644 --- a/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs +++ b/Content.Server/AI/Pathfinding/Accessible/ReachableArgs.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.AI.Components; using Robust.Shared.GameObjects; using Robust.Shared.Physics; @@ -32,7 +33,8 @@ namespace Content.Server.AI.Pathfinding.Accessible collisionMask = physics.CollisionMask; } - var access = AccessReader.FindAccessTags(entity); + var accessSystem = EntitySystem.Get(); + var access = accessSystem.FindAccessTags(entity.Uid); var visionRadius = entity.GetComponent().VisionRadius; return new ReachableArgs(visionRadius, access, collisionMask); diff --git a/Content.Server/AI/Pathfinding/PathfindingHelpers.cs b/Content.Server/AI/Pathfinding/PathfindingHelpers.cs index 62e19cd01b..c648070417 100644 --- a/Content.Server/AI/Pathfinding/PathfindingHelpers.cs +++ b/Content.Server/AI/Pathfinding/PathfindingHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Content.Server.Access.Systems; using Content.Server.AI.Pathfinding.Accessible; using Content.Server.AI.Pathfinding.Pathfinders; using Robust.Shared.GameObjects; @@ -117,9 +118,10 @@ namespace Content.Server.AI.Pathfinding return false; } + var accessSystem = EntitySystem.Get(); foreach (var reader in node.AccessReaders) { - if (!reader.IsAllowed(access)) + if (!accessSystem.IsAllowed(reader, access)) { return false; } diff --git a/Content.Server/AI/Pathfinding/PathfindingSystem.cs b/Content.Server/AI/Pathfinding/PathfindingSystem.cs index f64dda9272..213d5e38f0 100644 --- a/Content.Server/AI/Pathfinding/PathfindingSystem.cs +++ b/Content.Server/AI/Pathfinding/PathfindingSystem.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using Content.Server.Access; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.AI.Pathfinding.Pathfinders; using Content.Server.CPUJob.JobQueues; using Content.Server.CPUJob.JobQueues.Queues; @@ -31,6 +32,7 @@ namespace Content.Server.AI.Pathfinding { [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly AccessReaderSystem _accessReader = default!; public IReadOnlyDictionary> Graph => _graph; private readonly Dictionary> _graph = new(); @@ -375,11 +377,10 @@ namespace Content.Server.AI.Pathfinding return false; } - var access = AccessReader.FindAccessTags(entity); - + var access = _accessReader.FindAccessTags(entity.Uid); foreach (var reader in node.AccessReaders) { - if (!reader.IsAllowed(access)) + if (!_accessReader.IsAllowed(reader, access)) { return false; } diff --git a/Content.Server/AI/Steering/AiSteeringSystem.cs b/Content.Server/AI/Steering/AiSteeringSystem.cs index b7e1e468c6..ae2b190106 100644 --- a/Content.Server/AI/Steering/AiSteeringSystem.cs +++ b/Content.Server/AI/Steering/AiSteeringSystem.cs @@ -4,6 +4,7 @@ using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.AI.Components; using Content.Server.AI.Pathfinding; using Content.Server.AI.Pathfinding.Pathfinders; @@ -28,6 +29,7 @@ namespace Content.Server.AI.Steering [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPauseManager _pauseManager = default!; [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!; + [Dependency] private readonly AccessReaderSystem _accessReader = default!; /// /// Whether we try to avoid non-blocking physics objects @@ -423,7 +425,7 @@ namespace Content.Server.AI.Steering collisionMask = physics.CollisionMask; } - var access = AccessReader.FindAccessTags(entity); + var access = _accessReader.FindAccessTags(entity.Uid); var job = _pathfindingSystem.RequestPath(new PathfindingArgs( entity.Uid, diff --git a/Content.Server/Access/Components/AccessComponent.cs b/Content.Server/Access/Components/AccessComponent.cs index fd85a535d5..b552642521 100644 --- a/Content.Server/Access/Components/AccessComponent.cs +++ b/Content.Server/Access/Components/AccessComponent.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Content.Server.Access.Systems; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; @@ -9,22 +11,13 @@ namespace Content.Server.Access.Components /// Simple mutable access provider found on ID cards and such. /// [RegisterComponent] - [ComponentReference(typeof(IAccess))] - public class AccessComponent : Component, IAccess + [Friend(typeof(AccessSystem))] + public class AccessComponent : Component { public override string Name => "Access"; [DataField("tags")] [ViewVariables] - private readonly HashSet _tags = new(); - - public ISet Tags => _tags; - public bool IsReadOnly => false; - - public void SetTags(IEnumerable newTags) - { - _tags.Clear(); - _tags.UnionWith(newTags); - } + public HashSet Tags = new(); } } diff --git a/Content.Server/Access/Components/AccessReaderComponent.cs b/Content.Server/Access/Components/AccessReaderComponent.cs index 2eafac583a..e71e71460e 100644 --- a/Content.Server/Access/Components/AccessReaderComponent.cs +++ b/Content.Server/Access/Components/AccessReaderComponent.cs @@ -20,13 +20,15 @@ namespace Content.Server.Access.Components /// Stores access levels necessary to "use" an entity /// and allows checking if something or somebody is authorized with these access levels. /// - [PublicAPI] [RegisterComponent] public class AccessReader : Component { public override string Name => "AccessReader"; - private readonly HashSet _denyTags = new(); + /// + /// The set of tags that will automatically deny an allowed check, if any of them are present. + /// + public HashSet DenyTags = new(); /// /// List of access lists to check allowed against. For an access check to pass @@ -34,90 +36,6 @@ namespace Content.Server.Access.Components /// [DataField("access")] [ViewVariables] - public List> AccessLists { get; } = new(); - - /// - /// The set of tags that will automatically deny an allowed check, if any of them are present. - /// - [ViewVariables] public ISet DenyTags => _denyTags; - - /// - /// Searches an in the entity itself, in its active hand or in its ID slot. - /// Then compares the found access with the configured access lists to see if it is allowed. - /// - /// - /// If no access is found, an empty set is used instead. - /// - /// The entity to be searched for access. - public bool IsAllowed(IEntity entity) - { - var tags = FindAccessTags(entity); - return IsAllowed(tags); - } - - public bool IsAllowed(IAccess access) - { - return IsAllowed(access.Tags); - } - - public bool IsAllowed(ICollection accessTags) - { - if (_denyTags.Overlaps(accessTags)) - { - // Sec owned by cargo. - return false; - } - - return AccessLists.Count == 0 || AccessLists.Any(a => a.IsSubsetOf(accessTags)); - } - - public static ICollection FindAccessTags(IEntity entity) - { - if (entity.TryGetComponent(out IAccess? accessComponent)) - { - return accessComponent.Tags; - } - - if (entity.TryGetComponent(out IHandsComponent? handsComponent)) - { - var activeHandEntity = handsComponent.GetActiveHand?.Owner; - if (activeHandEntity != null && - activeHandEntity.TryGetComponent(out IAccess? handAccessComponent)) - { - return handAccessComponent.Tags; - } - } - else - { - return Array.Empty(); - } - - if (entity.TryGetComponent(out InventoryComponent? inventoryComponent)) - { - if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) && - inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item) && - item.Owner.TryGetComponent(out IAccess? idAccessComponent) - ) - { - return idAccessComponent.Tags; - } - } - - return Array.Empty(); - } - - protected override void Initialize() - { - base.Initialize(); - - var proto = IoCManager.Resolve(); - foreach (var level in AccessLists.SelectMany(c => c).Union(DenyTags)) - { - if (!proto.HasIndex(level)) - { - Logger.ErrorS("access", $"Invalid access level: {level}"); - } - } - } + public List> AccessLists = new(); } } diff --git a/Content.Server/Access/Components/IAccess.cs b/Content.Server/Access/Components/IAccess.cs deleted file mode 100644 index 46f91f40a3..0000000000 --- a/Content.Server/Access/Components/IAccess.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Content.Server.Access.Components -{ - /// - /// Contains access levels that can be checked to see if somebody has access with an . - /// - public interface IAccess - { - /// - /// The set of access tags this thing has. - /// - /// - /// This set may be read-only. Check if you want to mutate it. - /// - ISet Tags { get; } - - /// - /// Whether the list is read-only. - /// - bool IsReadOnly { get; } - - /// - /// Replaces the set of access tags we have with the provided set. - /// - /// The new access tags - /// If this access tag list is read-only. - void SetTags(IEnumerable newTags); - } -} diff --git a/Content.Server/Access/Components/IdCardComponent.cs b/Content.Server/Access/Components/IdCardComponent.cs index 6e9039b955..7e0f15a8ec 100644 --- a/Content.Server/Access/Components/IdCardComponent.cs +++ b/Content.Server/Access/Components/IdCardComponent.cs @@ -1,77 +1,24 @@ +using Content.Server.Access.Systems; +using Content.Server.PDA; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.Localization; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.ViewVariables; namespace Content.Server.Access.Components { [RegisterComponent] + [Friend(typeof(IdCardSystem), typeof(PDASystem))] public class IdCardComponent : Component { public override string Name => "IdCard"; - /// See . [DataField("originalOwnerName")] - private string _originalOwnerName = default!; + public string OriginalOwnerName = default!; [DataField("fullName")] - private string? _fullName; - [ViewVariables(VVAccess.ReadWrite)] - public string? FullName - { - get => _fullName; - set - { - _fullName = value; - UpdateEntityName(); - } - } + public string? FullName; [DataField("jobTitle")] - private string? _jobTitle; - [ViewVariables(VVAccess.ReadWrite)] - public string? JobTitle - { - get => _jobTitle; - set - { - _jobTitle = value; - UpdateEntityName(); - } - } - - /// - /// Changes the of . - /// - /// - /// If either or is empty, it's replaced by placeholders. - /// If both are empty, the original entity's name is restored. - /// - private void UpdateEntityName() - { - if (string.IsNullOrWhiteSpace(FullName) && string.IsNullOrWhiteSpace(JobTitle)) - { - Owner.Name = _originalOwnerName; - return; - } - - var jobSuffix = string.IsNullOrWhiteSpace(JobTitle) ? string.Empty : $" ({JobTitle})"; - - Owner.Name = string.IsNullOrWhiteSpace(FullName) - ? Loc.GetString("access-id-card-component-owner-name-job-title-text", - ("originalOwnerName", _originalOwnerName), - ("jobSuffix", jobSuffix)) - : Loc.GetString("access-id-card-component-owner-full-name-job-title-text", - ("fullName", FullName), - ("jobSuffix", jobSuffix)); - } - - protected override void Initialize() - { - base.Initialize(); - // ReSharper disable once ConstantNullCoalescingCondition - _originalOwnerName ??= Owner.Name; - UpdateEntityName(); - } + public string? JobTitle; } } diff --git a/Content.Server/Access/Components/IdCardConsoleComponent.cs b/Content.Server/Access/Components/IdCardConsoleComponent.cs index db0e0dcf5f..9ce2433ba1 100644 --- a/Content.Server/Access/Components/IdCardConsoleComponent.cs +++ b/Content.Server/Access/Components/IdCardConsoleComponent.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Content.Server.Access.Systems; using Content.Server.Power.Components; using Content.Server.UserInterface; using Content.Shared.Access; @@ -91,7 +92,8 @@ namespace Content.Server.Access.Components } var privilegedIdEntity = PrivilegedIdContainer.ContainedEntity; - return privilegedIdEntity != null && reader.IsAllowed(privilegedIdEntity); + var accessSystem = EntitySystem.Get(); + return privilegedIdEntity != null && accessSystem.IsAllowed(reader, privilegedIdEntity.Uid); } /// @@ -107,17 +109,18 @@ namespace Content.Server.Access.Components var targetIdEntity = TargetIdContainer.ContainedEntity; - var targetIdComponent = targetIdEntity.GetComponent(); - targetIdComponent.FullName = newFullName; - targetIdComponent.JobTitle = newJobTitle; + var cardSystem = EntitySystem.Get(); + cardSystem.TryChangeFullName(targetIdEntity.Uid, newFullName); + cardSystem.TryChangeJobTitle(targetIdEntity.Uid, newJobTitle); if (!newAccessList.TrueForAll(x => _prototypeManager.HasIndex(x))) { Logger.Warning("Tried to write unknown access tag."); return; } - var targetIdAccess = targetIdEntity.GetComponent(); - targetIdAccess.SetTags(newAccessList); + + var accessSystem = EntitySystem.Get(); + accessSystem.TrySetTags(targetIdEntity.Uid, newAccessList); } /// diff --git a/Content.Server/Access/Components/PresetIdCardComponent.cs b/Content.Server/Access/Components/PresetIdCardComponent.cs index 58c07df48c..56588d3640 100644 --- a/Content.Server/Access/Components/PresetIdCardComponent.cs +++ b/Content.Server/Access/Components/PresetIdCardComponent.cs @@ -7,27 +7,11 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Access.Components { [RegisterComponent] - public class PresetIdCardComponent : Component, IMapInit + public class PresetIdCardComponent : Component { public override string Name => "PresetIdCard"; [DataField("job")] - private string? _jobName; - - void IMapInit.MapInit() - { - if (_jobName == null) - { - return; - } - - var prototypes = IoCManager.Resolve(); - var job = prototypes.Index(_jobName); - var access = Owner.GetComponent(); - var idCard = Owner.GetComponent(); - - access.SetTags(job.Access); - idCard.JobTitle = job.Name; - } + public readonly string? JobName; } } diff --git a/Content.Server/Access/Systems/AccessReaderSystem.cs b/Content.Server/Access/Systems/AccessReaderSystem.cs new file mode 100644 index 0000000000..e1d3b16e76 --- /dev/null +++ b/Content.Server/Access/Systems/AccessReaderSystem.cs @@ -0,0 +1,119 @@ +using Content.Server.Access.Components; +using Content.Server.Inventory.Components; +using Content.Server.Items; +using Content.Server.PDA; +using Content.Shared.Access; +using Content.Shared.Hands.Components; +using Content.Shared.Inventory; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Server.Access.Systems +{ + public class AccessReaderSystem : EntitySystem + { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + } + + private void OnInit(EntityUid uid, AccessReader reader, ComponentInit args) + { + var allTags = reader.AccessLists.SelectMany(c => c).Union(reader.DenyTags); + foreach (var level in allTags) + { + if (!_prototypeManager.HasIndex(level)) + { + Logger.ErrorS("access", $"Invalid access level: {level}"); + } + } + } + + /// + /// Searches an in the entity itself, in its active hand or in its ID slot. + /// Then compares the found access with the configured access lists to see if it is allowed. + /// + /// + /// If no access is found, an empty set is used instead. + /// + /// The entity to be searched for access. + public bool IsAllowed(AccessReader reader, EntityUid entity) + { + var tags = FindAccessTags(entity); + return IsAllowed(reader, tags); + } + + public bool IsAllowed(AccessReader reader, ICollection accessTags) + { + if (reader.DenyTags.Overlaps(accessTags)) + { + // Sec owned by cargo. + return false; + } + + return reader.AccessLists.Count == 0 || reader.AccessLists.Any(a => a.IsSubsetOf(accessTags)); + } + + public ICollection FindAccessTags(EntityUid uid) + { + // check entity itself + if (FindAccessTagsItem(uid, out var tags)) + return tags; + + // maybe access component inside its hands? + if (EntityManager.TryGetComponent(uid, out SharedHandsComponent? hands)) + { + if (hands.TryGetActiveHeldEntity(out var heldItem) && + FindAccessTagsItem(heldItem.Uid, out tags)) + { + return tags; + } + } + + // maybe its inside an inventory slot? + if (EntityManager.TryGetComponent(uid, out InventoryComponent? inventoryComponent)) + { + if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) && + inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item) && + FindAccessTagsItem(item.Owner.Uid, out tags) + ) + { + return tags; + } + } + + return Array.Empty(); + } + + /// + /// Try to find on this item + /// or inside this item (if it's pda) + /// + private bool FindAccessTagsItem(EntityUid uid, [NotNullWhen(true)] out HashSet? tags) + { + if (EntityManager.TryGetComponent(uid, out AccessComponent? access)) + { + tags = access.Tags; + return true; + } + + if (EntityManager.TryGetComponent(uid, out PDAComponent? pda)) + { + tags = pda?.ContainedID?.Owner?.GetComponent()?.Tags; + return tags != null; + } + + tags = null; + return false; + } + } +} diff --git a/Content.Server/Access/Systems/AccessSystem.cs b/Content.Server/Access/Systems/AccessSystem.cs new file mode 100644 index 0000000000..7424e10fd8 --- /dev/null +++ b/Content.Server/Access/Systems/AccessSystem.cs @@ -0,0 +1,24 @@ +using Content.Server.Access.Components; +using Robust.Shared.GameObjects; +using System.Collections.Generic; + +namespace Content.Server.Access.Systems +{ + public class AccessSystem : EntitySystem + { + /// + /// Replaces the set of access tags we have with the provided set. + /// + /// The new access tags + public bool TrySetTags(EntityUid uid, IEnumerable newTags, AccessComponent? access = null) + { + if (!Resolve(uid, ref access)) + return false; + + access.Tags.Clear(); + access.Tags.UnionWith(newTags); + + return true; + } + } +} diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs new file mode 100644 index 0000000000..02b113a6a4 --- /dev/null +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -0,0 +1,70 @@ +using Content.Server.Access.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; + +namespace Content.Server.Access.Systems +{ + public class IdCardSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + } + + private void OnInit(EntityUid uid, IdCardComponent id, ComponentInit args) + { + id.OriginalOwnerName ??= id.Owner.Name; + UpdateEntityName(uid, id); + } + + public bool TryChangeJobTitle(EntityUid uid, string jobTitle, IdCardComponent? id = null) + { + if (!Resolve(uid, ref id)) + return false; + + id.JobTitle = jobTitle; + UpdateEntityName(uid, id); + return true; + } + + public bool TryChangeFullName(EntityUid uid, string fullName, IdCardComponent? id = null) + { + if (!Resolve(uid, ref id)) + return false; + + id.FullName = fullName; + UpdateEntityName(uid, id); + return true; + } + + /// + /// Changes the of . + /// + /// + /// If either or is empty, it's replaced by placeholders. + /// If both are empty, the original entity's name is restored. + /// + private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null) + { + if (!Resolve(uid, ref id)) + return; + + if (string.IsNullOrWhiteSpace(id.FullName) && string.IsNullOrWhiteSpace(id.JobTitle)) + { + id.Owner.Name = id.OriginalOwnerName; + return; + } + + var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})"; + + id.Owner.Name = string.IsNullOrWhiteSpace(id.FullName) + ? Loc.GetString("access-id-card-component-owner-name-job-title-text", + ("originalOwnerName", id.OriginalOwnerName), + ("jobSuffix", jobSuffix)) + : Loc.GetString("access-id-card-component-owner-full-name-job-title-text", + ("fullName", id.FullName), + ("jobSuffix", jobSuffix)); + } + } +} diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs new file mode 100644 index 0000000000..4d6b0a6ffa --- /dev/null +++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs @@ -0,0 +1,40 @@ +using Content.Server.Access.Components; +using Content.Shared.Roles; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using System; + +namespace Content.Server.Access.Systems +{ + public class PresetIdCardSystem : EntitySystem + { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IdCardSystem _cardSystem = default!; + [Dependency] private readonly AccessSystem _accessSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, PresetIdCardComponent id, MapInitEvent args) + { + if (id.JobName == null) return; + + if (!_prototypeManager.TryIndex(id.JobName, out JobPrototype? job)) + { + Logger.ErrorS("access", $"Invalid job id ({id.JobName}) for preset card"); + return; + } + + // set access for access component + _accessSystem.TrySetTags(uid, job.Access); + + // and also change job title on a card id + _cardSystem.TryChangeJobTitle(uid, job.Name); + } + } +} diff --git a/Content.Server/Doors/Components/ServerDoorComponent.cs b/Content.Server/Doors/Components/ServerDoorComponent.cs index 1ae5d01f3b..08d9e55c7c 100644 --- a/Content.Server/Doors/Components/ServerDoorComponent.cs +++ b/Content.Server/Doors/Components/ServerDoorComponent.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Access; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Construction.Components; @@ -311,12 +312,13 @@ namespace Content.Server.Doors.Components var doorSystem = EntitySystem.Get(); var isAirlockExternal = HasAccessType("External"); + var accessSystem = EntitySystem.Get(); return doorSystem.AccessType switch { DoorSystem.AccessTypes.AllowAll => true, - DoorSystem.AccessTypes.AllowAllIdExternal => isAirlockExternal || access.IsAllowed(user), + DoorSystem.AccessTypes.AllowAllIdExternal => isAirlockExternal || accessSystem.IsAllowed(access, user.Uid), DoorSystem.AccessTypes.AllowAllNoExternal => !isAirlockExternal, - _ => access.IsAllowed(user) + _ => accessSystem.IsAllowed(access, user.Uid) }; } @@ -430,7 +432,8 @@ namespace Content.Server.Doors.Components return true; } - return access.IsAllowed(user); + var accessSystem = EntitySystem.Get(); + return accessSystem.IsAllowed(access, user.Uid); } /// diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 546c41855c..3fda57c694 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Globalization; using Content.Server.Access.Components; +using Content.Server.Access.Systems; +using Content.Server.CharacterAppearance.Components; using Content.Server.Ghost.Components; using Content.Server.Hands.Components; using Content.Server.Inventory.Components; @@ -19,6 +21,7 @@ using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Server.Player; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Random; @@ -32,6 +35,8 @@ namespace Content.Server.GameTicking private const string PlayerPrototypeName = "MobHuman"; private const string ObserverPrototypeName = "MobObserver"; + [Dependency] private readonly IdCardSystem _cardSystem = default!; + [ViewVariables(VVAccess.ReadWrite)] private EntityCoordinates _spawnPoint; @@ -228,8 +233,8 @@ namespace Content.Server.GameTicking return; var card = pdaComponent.ContainedID; - card.FullName = characterName; - card.JobTitle = jobPrototype.Name; + _cardSystem.TryChangeFullName(card.Owner.Uid, characterName, card); + _cardSystem.TryChangeJobTitle(card.Owner.Uid, jobPrototype.Name, card); var access = card.Owner.GetComponent(); var accessTags = access.Tags; diff --git a/Content.Server/Lock/LockSystem.cs b/Content.Server/Lock/LockSystem.cs index c2da4d635d..0cb71d03c2 100644 --- a/Content.Server/Lock/LockSystem.cs +++ b/Content.Server/Lock/LockSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.Storage.Components; using Content.Shared.Examine; using Content.Shared.Interaction; @@ -9,6 +10,7 @@ using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Player; @@ -20,6 +22,8 @@ namespace Content.Server.Lock [UsedImplicitly] public class LockSystem : EntitySystem { + [Dependency] private readonly AccessReaderSystem _accessReader = default!; + /// public override void Initialize() { @@ -144,7 +148,7 @@ namespace Content.Server.Lock if (!Resolve(uid, ref reader)) return true; - if (!reader.IsAllowed(user)) + if (!_accessReader.IsAllowed(reader, user.Uid)) { if (!quiet) reader.Owner.PopupMessage(user, Loc.GetString("lock-comp-has-user-access-fail")); diff --git a/Content.Server/PDA/PDAComponent.cs b/Content.Server/PDA/PDAComponent.cs index 88696b7713..414d87b7d9 100644 --- a/Content.Server/PDA/PDAComponent.cs +++ b/Content.Server/PDA/PDAComponent.cs @@ -8,8 +8,7 @@ using Robust.Shared.ViewVariables; namespace Content.Server.PDA { [RegisterComponent] - [ComponentReference(typeof(IAccess))] - public class PDAComponent : Component, IAccess + public class PDAComponent : Component { public override string Name => "PDA"; @@ -26,29 +25,5 @@ namespace Content.Server.PDA [ViewVariables] public bool FlashlightOn; [ViewVariables] public string? OwnerName; - - // TODO: Move me to ECS after Access refactoring - #region Acces Logic - [ViewVariables] private readonly PDAAccessSet _accessSet; - - public PDAComponent() - { - _accessSet = new PDAAccessSet(this); - } - - public ISet? GetContainedAccess() - { - return ContainedID?.Owner?.GetComponent()?.Tags; - } - - ISet IAccess.Tags => _accessSet; - - bool IAccess.IsReadOnly => true; - - void IAccess.SetTags(IEnumerable newTags) - { - throw new NotSupportedException("PDA access list is read-only."); - } - #endregion } } diff --git a/Content.Server/PDA/PdaAccessSet.cs b/Content.Server/PDA/PdaAccessSet.cs deleted file mode 100644 index bd28b40d23..0000000000 --- a/Content.Server/PDA/PdaAccessSet.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Content.Server.PDA -{ - public sealed class PDAAccessSet : ISet - { - private readonly PDAComponent _pdaComponent; - private static readonly HashSet EmptySet = new(); - - public PDAAccessSet(PDAComponent pdaComponent) - { - _pdaComponent = pdaComponent; - } - - public IEnumerator GetEnumerator() - { - var contained = _pdaComponent.GetContainedAccess() ?? EmptySet; - return contained.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - void ICollection.Add(string item) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public void ExceptWith(IEnumerable other) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public void IntersectWith(IEnumerable other) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public bool IsProperSubsetOf(IEnumerable other) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - return set.IsProperSubsetOf(other); - } - - public bool IsProperSupersetOf(IEnumerable other) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - return set.IsProperSupersetOf(other); - } - - public bool IsSubsetOf(IEnumerable other) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - return set.IsSubsetOf(other); - } - - public bool IsSupersetOf(IEnumerable other) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - return set.IsSupersetOf(other); - } - - public bool Overlaps(IEnumerable other) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - return set.Overlaps(other); - } - - public bool SetEquals(IEnumerable other) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - return set.SetEquals(other); - } - - public void SymmetricExceptWith(IEnumerable other) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public void UnionWith(IEnumerable other) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - bool ISet.Add(string item) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public void Clear() - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public bool Contains(string item) - { - return _pdaComponent.GetContainedAccess()?.Contains(item) ?? false; - } - - public void CopyTo(string[] array, int arrayIndex) - { - var set = _pdaComponent.GetContainedAccess() ?? EmptySet; - set.CopyTo(array, arrayIndex); - } - - public bool Remove(string item) - { - throw new NotSupportedException("PDA access list is read-only."); - } - - public int Count => _pdaComponent.GetContainedAccess()?.Count ?? 0; - public bool IsReadOnly => true; - } -} diff --git a/Content.Server/Power/Components/ApcComponent.cs b/Content.Server/Power/Components/ApcComponent.cs index e94f7e5a78..91281a4b98 100644 --- a/Content.Server/Power/Components/ApcComponent.cs +++ b/Content.Server/Power/Components/ApcComponent.cs @@ -1,5 +1,6 @@ using System; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.Power.NodeGroups; using Content.Server.UserInterface; using Content.Shared.APC; @@ -91,7 +92,8 @@ namespace Content.Server.Power.Components var user = serverMsg.Session.AttachedEntity; if (user == null) return; - if (_accessReader == null || _accessReader.IsAllowed(user)) + var accessSystem = EntitySystem.Get(); + if (_accessReader == null || accessSystem.IsAllowed(_accessReader, user.Uid)) { MainBreakerEnabled = !MainBreakerEnabled; Owner.GetComponent().CanDischarge = MainBreakerEnabled; diff --git a/Content.Server/Sandbox/SandboxManager.cs b/Content.Server/Sandbox/SandboxManager.cs index ea109c4af6..b1a6453632 100644 --- a/Content.Server/Sandbox/SandboxManager.cs +++ b/Content.Server/Sandbox/SandboxManager.cs @@ -19,6 +19,7 @@ using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.ViewVariables; using static Content.Shared.Inventory.EquipmentSlotDefines; +using Content.Server.Access.Systems; namespace Content.Server.Sandbox { @@ -158,8 +159,8 @@ namespace Content.Server.Sandbox void UpgradeId(IEntity id) { - var access = id.GetComponent(); - access.SetTags(allAccess); + var accessSystem = EntitySystem.Get(); + accessSystem.TrySetTags(id.Uid, allAccess); if (id.TryGetComponent(out SpriteComponent? sprite)) { diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index 5bca57f942..625ad1e52b 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Threading; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.Power.EntitySystems; using Content.Server.Projectiles.Components; using Content.Server.Singularity.Components; @@ -26,6 +27,7 @@ namespace Content.Server.Singularity.EntitySystems public class EmitterSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly AccessReaderSystem _accessReader = default!; public override void Initialize() { @@ -40,12 +42,12 @@ namespace Content.Server.Singularity.EntitySystems { if(args.Handled) return; - if (component.AccessReader == null || !args.Used.TryGetComponent(out IAccess? access)) + if (component.AccessReader == null) { return; } - if (component.AccessReader.IsAllowed(access)) + if (_accessReader.IsAllowed(component.AccessReader, args.Used.Uid)) { component.IsLocked ^= true; diff --git a/Content.Server/VendingMachines/VendingMachineComponent.cs b/Content.Server/VendingMachines/VendingMachineComponent.cs index f3a2ddf1b3..fbdfecbdb3 100644 --- a/Content.Server/VendingMachines/VendingMachineComponent.cs +++ b/Content.Server/VendingMachines/VendingMachineComponent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Content.Server.Access.Components; +using Content.Server.Access.Systems; using Content.Server.Advertise; using Content.Server.Popups; using Content.Server.Power.Components; @@ -191,7 +192,8 @@ namespace Content.Server.VendingMachines { if (Owner.TryGetComponent(out var accessReader)) { - if (sender == null || !accessReader.IsAllowed(sender)) + var accessSystem = EntitySystem.Get(); + if (sender == null || !accessSystem.IsAllowed(accessReader, sender.Uid)) { Owner.PopupMessageEveryone(Loc.GetString("vending-machine-component-try-eject-access-denied")); Deny(); diff --git a/Content.Tests/Server/GameObjects/Components/Access/AccessReaderTest.cs b/Content.Tests/Server/GameObjects/Components/Access/AccessReaderTest.cs deleted file mode 100644 index fef352109b..0000000000 --- a/Content.Tests/Server/GameObjects/Components/Access/AccessReaderTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using Content.Server.Access.Components; -using NUnit.Framework; - -namespace Content.Tests.Server.GameObjects.Components.Access -{ - [TestFixture] - [TestOf(typeof(AccessReader))] - public class AccessReaderTest : ContentUnitTest - { - [Test] - public void TestEmpty() - { - var reader = new AccessReader(); - - Assert.That(reader.IsAllowed(new[] {"Foo"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"Bar"}), Is.True); - Assert.That(reader.IsAllowed(new string[] {}), Is.True); - } - - [Test] - public void TestDeny() - { - var reader = new AccessReader(); - reader.DenyTags.Add("A"); - - Assert.That(reader.IsAllowed(new[] {"Foo"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"A"}), Is.False); - Assert.That(reader.IsAllowed(new[] {"A", "Foo"}), Is.False); - Assert.That(reader.IsAllowed(new string[] {}), Is.True); - } - - [Test] - public void TestOneList() - { - var reader = new AccessReader(); - reader.AccessLists.Add(new HashSet {"A"}); - - Assert.That(reader.IsAllowed(new[] {"A"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"B"}), Is.False); - Assert.That(reader.IsAllowed(new[] {"A", "B"}), Is.True); - Assert.That(reader.IsAllowed(new string[] {}), Is.False); - } - - [Test] - public void TestOneListTwoItems() - { - var reader = new AccessReader(); - reader.AccessLists.Add(new HashSet {"A", "B"}); - - Assert.That(reader.IsAllowed(new[] {"A"}), Is.False); - Assert.That(reader.IsAllowed(new[] {"B"}), Is.False); - Assert.That(reader.IsAllowed(new[] {"A", "B"}), Is.True); - Assert.That(reader.IsAllowed(new string[] {}), Is.False); - } - - [Test] - public void TestTwoList() - { - var reader = new AccessReader(); - reader.AccessLists.Add(new HashSet {"A"}); - reader.AccessLists.Add(new HashSet {"B", "C"}); - - Assert.That(reader.IsAllowed(new[] {"A"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"B"}), Is.False); - Assert.That(reader.IsAllowed(new[] {"A", "B"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"C", "B"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"C", "B", "A"}), Is.True); - Assert.That(reader.IsAllowed(new string[] {}), Is.False); - } - - [Test] - public void TestDenyList() - { - var reader = new AccessReader(); - reader.AccessLists.Add(new HashSet {"A"}); - reader.DenyTags.Add("B"); - - Assert.That(reader.IsAllowed(new[] {"A"}), Is.True); - Assert.That(reader.IsAllowed(new[] {"B"}), Is.False); - Assert.That(reader.IsAllowed(new[] {"A", "B"}), Is.False); - Assert.That(reader.IsAllowed(new string[] {}), Is.False); - } - } -}