]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Access Reader Refactor (#37772)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Thu, 5 Jun 2025 23:28:55 +0000 (18:28 -0500)
committerGitHub <noreply@github.com>
Thu, 5 Jun 2025 23:28:55 +0000 (19:28 -0400)
* Initial commit

* Integration test fix

* Removed redundant dirtying of accessreader

Content.IntegrationTests/Tests/Access/AccessReaderTest.cs
Content.Server/Access/AccessWireAction.cs
Content.Server/Access/AddAccessLogCommand.cs
Content.Server/Access/LogWireAction.cs
Content.Server/Access/Systems/AccessOverriderSystem.cs
Content.Server/Doors/Electronics/Systems/DoorElectronicsSystem.cs
Content.Shared/Access/Components/AccessReaderComponent.cs
Content.Shared/Access/Systems/AccessReaderSystem.cs

index 3f703ce77407950cbafb6fc6cd77566c69abed25..b98f030b0658282c28f5179a885844beafc0210f 100644 (file)
@@ -1,9 +1,9 @@
 using System.Collections.Generic;
-using System.Linq;
 using Content.Shared.Access;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
 using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
 
 namespace Content.IntegrationTests.Tests.Access
@@ -12,6 +12,15 @@ namespace Content.IntegrationTests.Tests.Access
     [TestOf(typeof(AccessReaderComponent))]
     public sealed class AccessReaderTest
     {
+        [TestPrototypes]
+        private const string Prototypes = @"
+- type: entity
+  id: TestAccessReader
+  name: access reader
+  components:
+  - type: AccessReader
+";
+
         [Test]
         public async Task TestTags()
         {
@@ -19,13 +28,13 @@ namespace Content.IntegrationTests.Tests.Access
             var server = pair.Server;
             var entityManager = server.ResolveDependency<IEntityManager>();
 
-
             await server.WaitAssertion(() =>
             {
                 var system = entityManager.System<AccessReaderSystem>();
+                var ent = entityManager.SpawnEntity("TestAccessReader", MapCoordinates.Nullspace);
+                var reader = new Entity<AccessReaderComponent>(ent, entityManager.GetComponent<AccessReaderComponent>(ent));
 
                 // test empty
-                var reader = new AccessReaderComponent();
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
@@ -34,8 +43,7 @@ namespace Content.IntegrationTests.Tests.Access
                 });
 
                 // test deny
-                reader = new AccessReaderComponent();
-                reader.DenyTags.Add("A");
+                system.AddDenyTag(reader, "A");
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
@@ -43,10 +51,10 @@ namespace Content.IntegrationTests.Tests.Access
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "Foo" }, reader), Is.False);
                     Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
                 });
+                system.ClearDenyTags(reader);
 
                 // test one list
-                reader = new AccessReaderComponent();
-                reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
+                system.AddAccess(reader, "A");
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -54,10 +62,10 @@ namespace Content.IntegrationTests.Tests.Access
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
                     Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
                 });
+                system.ClearAccesses(reader);
 
                 // test one list - two items
-                reader = new AccessReaderComponent();
-                reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
+                system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
@@ -65,11 +73,14 @@ namespace Content.IntegrationTests.Tests.Access
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
                     Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
                 });
+                system.ClearAccesses(reader);
 
                 // test two list
-                reader = new AccessReaderComponent();
-                reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
-                reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "B", "C" });
+                var accesses = new List<HashSet<ProtoId<AccessLevelPrototype>>>() {
+                    new HashSet<ProtoId<AccessLevelPrototype>> () { "A" },
+                    new HashSet<ProtoId<AccessLevelPrototype>> () { "B", "C" }
+                };
+                system.AddAccesses(reader, accesses);
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -79,11 +90,11 @@ namespace Content.IntegrationTests.Tests.Access
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
                     Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
                 });
+                system.ClearAccesses(reader);
 
                 // test deny list
-                reader = new AccessReaderComponent();
-                reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
-                reader.DenyTags.Add("B");
+                system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
+                system.AddDenyTag(reader, "B");
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -91,6 +102,8 @@ namespace Content.IntegrationTests.Tests.Access
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
                     Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
                 });
+                system.ClearAccesses(reader);
+                system.ClearDenyTags(reader);
             });
             await pair.CleanReturnAsync();
         }
index b3beb3967b9b8b3c29d1ba767375d7ba2f116eb8..2682fff286d5b0f6ce3cf15db684245d9ba98d06 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Wires;
 using Content.Shared.Access;
 using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
 using Content.Shared.Wires;
 
 namespace Content.Server.Access;
@@ -23,23 +24,21 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
     public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
     {
         WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
-        comp.Enabled = false;
-        EntityManager.Dirty(wire.Owner, comp);
+        EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), false);
+
         return true;
     }
 
     public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
     {
-        comp.Enabled = true;
-        EntityManager.Dirty(wire.Owner, comp);
+        EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), true);
 
         return true;
     }
 
     public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
     {
-        comp.Enabled = false;
-        EntityManager.Dirty(wire.Owner, comp);
+        EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), false);
         WiresSystem.StartWireAction(wire.Owner, _pulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
     }
 
@@ -57,8 +56,7 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
         {
             if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
             {
-                access.Enabled = true;
-                EntityManager.Dirty(wire.Owner, access);
+                EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, access), true);
             }
         }
     }
index f55a9b8f1eba017113cbe84d070d9669700d66fe..e68a58d165c995a13d52a094b70f1d57db1f9b8d 100644 (file)
@@ -1,8 +1,8 @@
 using Content.Server.Administration;
 using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
 using Content.Shared.Administration;
 using Robust.Shared.Toolshed;
-using Robust.Shared.Toolshed.Syntax;
 
 namespace Content.Server.Access;
 
@@ -19,7 +19,7 @@ public sealed class AddAccessLogCommand : ToolshedCommand
             ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!");
 
         var accessTime = TimeSpan.FromSeconds(seconds);
-        accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessor));
+        EntityManager.System<AccessReaderSystem>().LogAccess((input, accessReader), accessor, accessTime, true);
         ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " +
                       $"Time of access: {accessTime}\n " +
                       $"Accessed by: {accessor}");
index 837cf420d5eef0436b85c4f08276788bd7c08ab0..d6ba3dbfcd43d7a7d8ac6051c1fd6e9cefe7eb18 100644 (file)
@@ -37,21 +37,21 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
     public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
     {
         WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
-        comp.LoggingDisabled = true;
-        EntityManager.Dirty(wire.Owner, comp);
+        EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), false);
+
         return true;
     }
 
     public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
     {
-        comp.LoggingDisabled = false;
+        EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), true);
         return true;
     }
 
     public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
     {
         _access.LogAccess((wire.Owner, comp), Loc.GetString(PulseLog));
-        comp.LoggingDisabled = true;
+        EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), false);
         WiresSystem.StartWireAction(wire.Owner, PulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
     }
 
@@ -64,7 +64,7 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
     private void AwaitPulseCancel(Wire wire)
     {
         if (!wire.IsCut && EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var comp))
-            comp.LoggingDisabled = false;
+            EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), true);
     }
 
     private enum PulseTimeoutKey : byte
index 4062909d7554762d0c6a05a20422af9918105857..51d35c50a451b68f79d661a7f5189a7b9ccabe5b 100644 (file)
@@ -168,21 +168,6 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
         return accessList;
     }
 
-    private List<HashSet<ProtoId<AccessLevelPrototype>>> ConvertAccessListToHashSet(List<ProtoId<AccessLevelPrototype>> accessList)
-    {
-        List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets = new List<HashSet<ProtoId<AccessLevelPrototype>>>();
-
-        if (accessList != null && accessList.Any())
-        {
-            foreach (ProtoId<AccessLevelPrototype> access in accessList)
-            {
-                accessHashsets.Add(new HashSet<ProtoId<AccessLevelPrototype>>() { access });
-            }
-        }
-
-        return accessHashsets;
-    }
-
     /// <summary>
     /// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
     /// </summary>
@@ -244,12 +229,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
         _adminLogger.Add(LogType.Action, LogImpact.High,
             $"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
 
-        accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
+        _accessReader.SetAccesses(accessReaderEnt.Value, newAccessList);
 
         var ev = new OnAccessOverriderAccessUpdatedEvent(player);
         RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
-
-        Dirty(accessReaderEnt.Value);
     }
 
     /// <summary>
index af9ccadd91cf3141bfd03f32ecb8969b50365c4e..af2738d105e797cc6c975919fa91d84009df094b 100644 (file)
@@ -48,7 +48,7 @@ public sealed class DoorElectronicsSystem : EntitySystem
         DoorElectronicsUpdateConfigurationMessage args)
     {
         var accessReader = EnsureComp<AccessReaderComponent>(uid);
-        _accessReader.SetAccesses(uid, accessReader, args.AccessList);
+        _accessReader.SetAccesses((uid, accessReader), args.AccessList);
     }
 
     private void OnAccessReaderChanged(
index 0219fd2b1ad66f4ac12ce13b95e68263f9dacb41..6c2416fdf473174d32758fc622f19ddecae4edd2 100644 (file)
@@ -1,8 +1,8 @@
+using Content.Shared.Access.Systems;
 using Content.Shared.StationRecords;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
 
 namespace Content.Shared.Access.Components;
 
@@ -11,10 +11,11 @@ namespace Content.Shared.Access.Components;
 /// and allows checking if something or somebody is authorized with these access levels.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
+[Access(typeof(AccessReaderSystem))]
 public sealed partial class AccessReaderComponent : Component
 {
     /// <summary>
-    /// Whether or not the accessreader is enabled.
+    /// Whether or not the access reader is enabled.
     /// If not, it will always let people through.
     /// </summary>
     [DataField]
@@ -23,7 +24,6 @@ public sealed partial class AccessReaderComponent : Component
     /// <summary>
     /// The set of tags that will automatically deny an allowed check, if any of them are present.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField]
     public HashSet<ProtoId<AccessLevelPrototype>> DenyTags = new();
 
@@ -31,12 +31,11 @@ public sealed partial class AccessReaderComponent : Component
     /// List of access groups that grant access to this reader. Only a single matching group is required to gain access.
     /// A group matches if it is a subset of the set being checked against.
     /// </summary>
-    [DataField("access")] [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("access")]
     public List<HashSet<ProtoId<AccessLevelPrototype>>> AccessLists = new();
 
     /// <summary>
-    /// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required to gain
-    /// access.
+    /// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required to gain access.
     /// </summary>
     [DataField]
     public HashSet<StationRecordKey> AccessKeys = new();
@@ -54,7 +53,7 @@ public sealed partial class AccessReaderComponent : Component
     public string? ContainerAccessProvider;
 
     /// <summary>
-    /// A list of past authentications
+    /// A list of past authentications.
     /// </summary>
     [DataField]
     public Queue<AccessRecord> AccessLog = new();
@@ -62,7 +61,7 @@ public sealed partial class AccessReaderComponent : Component
     /// <summary>
     /// A limit on the max size of <see cref="AccessLog"/>
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public int AccessLogLimit = 20;
 
     /// <summary>
@@ -95,15 +94,10 @@ public readonly partial record struct AccessRecord(
 public sealed class AccessReaderComponentState : ComponentState
 {
     public bool Enabled;
-
     public HashSet<ProtoId<AccessLevelPrototype>> DenyTags;
-
     public List<HashSet<ProtoId<AccessLevelPrototype>>> AccessLists;
-
     public List<(NetEntity, uint)> AccessKeys;
-
     public Queue<AccessRecord> AccessLog;
-
     public int AccessLogLimit;
 
     public AccessReaderComponentState(bool enabled, HashSet<ProtoId<AccessLevelPrototype>> denyTags, List<HashSet<ProtoId<AccessLevelPrototype>>> accessLists, List<(NetEntity, uint)> accessKeys, Queue<AccessRecord> accessLog, int accessLogLimit)
@@ -117,9 +111,4 @@ public sealed class AccessReaderComponentState : ComponentState
     }
 }
 
-public sealed class AccessReaderConfigurationChangedEvent : EntityEventArgs
-{
-    public AccessReaderConfigurationChangedEvent()
-    {
-    }
-}
+public sealed class AccessReaderConfigurationChangedEvent : EntityEventArgs;
index 74cf74274dfdc058cf1705797299821696c993bd..186aef5305f1084c91dca1f2c1da262ead8b9351 100644 (file)
@@ -3,17 +3,17 @@ using System.Linq;
 using Content.Shared.Access.Components;
 using Content.Shared.DeviceLinking.Events;
 using Content.Shared.Emag.Systems;
+using Content.Shared.GameTicking;
 using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Inventory;
 using Content.Shared.NameIdentifier;
 using Content.Shared.PDA;
 using Content.Shared.StationRecords;
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Content.Shared.GameTicking;
-using Content.Shared.IdentityManagement;
 using Content.Shared.Tag;
+using Robust.Shared.Containers;
 using Robust.Shared.Collections;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 
@@ -128,6 +128,11 @@ public sealed class AccessReaderSystem : EntitySystem
         return true;
     }
 
+    /// <summary>
+    /// Searches an entity for an access reader. This is either the entity itself or an entity in its <see cref="AccessReaderComponent.ContainerAccessProvider"/>.
+    /// </summary>
+    /// <param name="uid">The entity being searched for an access reader.</param>
+    /// <param name="ent">The returned access reader entity.</param>
     public bool GetMainAccessReader(EntityUid uid, [NotNullWhen(true)] out Entity<AccessReaderComponent>? ent)
     {
         ent = null;
@@ -157,6 +162,10 @@ public sealed class AccessReaderSystem : EntitySystem
     /// <summary>
     /// Check whether the given access permissions satisfy an access reader's requirements.
     /// </summary>
+    /// <param name="access">A collection of access permissions being used on the access reader.</param>
+    /// <param name="stationKeys">A collection of station record keys being used on the access reader.</param>
+    /// <param name="target">The entity being checked.</param>
+    /// <param name="reader">The access reader being checked.</param>
     public bool IsAllowed(
         ICollection<ProtoId<AccessLevelPrototype>> access,
         ICollection<StationRecordKey> stationKeys,
@@ -199,8 +208,8 @@ public sealed class AccessReaderSystem : EntitySystem
     /// <summary>
     /// Compares the given tags with the readers access list to see if it is allowed.
     /// </summary>
-    /// <param name="accessTags">A list of access tags</param>
-    /// <param name="reader">An access reader to check against</param>
+    /// <param name="accessTags">A list of access tags.</param>
+    /// <param name="reader">The access reader to check against.</param>
     public bool AreAccessTagsAllowed(ICollection<ProtoId<AccessLevelPrototype>> accessTags, AccessReaderComponent reader)
     {
         if (reader.DenyTags.Overlaps(accessTags))
@@ -228,6 +237,8 @@ public sealed class AccessReaderSystem : EntitySystem
     /// <summary>
     /// Compares the given stationrecordkeys with the accessreader to see if it is allowed.
     /// </summary>
+    /// <param name="keys">The collection of station record keys being used against the access reader.</param>
+    /// <param name="reader">The access reader that is being checked.</param>
     public bool AreStationRecordKeysAllowed(ICollection<StationRecordKey> keys, AccessReaderComponent reader)
     {
         foreach (var key in reader.AccessKeys)
@@ -240,8 +251,9 @@ public sealed class AccessReaderSystem : EntitySystem
     }
 
     /// <summary>
-    /// Finds all the items that could potentially give access to a given entity
+    /// Finds all the items that could potentially give access to an entity.
     /// </summary>
+    /// <param name="uid">The entity that is being searched.</param>
     public HashSet<EntityUid> FindPotentialAccessItems(EntityUid uid)
     {
         FindAccessItemsInventory(uid, out var items);
@@ -261,7 +273,7 @@ public sealed class AccessReaderSystem : EntitySystem
     }
 
     /// <summary>
-    /// Finds the access tags on the given entity
+    /// Finds the access tags on an entity.
     /// </summary>
     /// <param name="uid">The entity that is being searched.</param>
     /// <param name="items">All of the items to search for access. If none are passed in, <see cref="FindPotentialAccessItems"/> will be used.</param>
@@ -277,14 +289,14 @@ public sealed class AccessReaderSystem : EntitySystem
             FindAccessTagsItem(ent, ref tags, ref owned);
         }
 
-        return (ICollection<ProtoId<AccessLevelPrototype>>?) tags ?? Array.Empty<ProtoId<AccessLevelPrototype>>();
+        return (ICollection<ProtoId<AccessLevelPrototype>>?)tags ?? Array.Empty<ProtoId<AccessLevelPrototype>>();
     }
 
     /// <summary>
-    /// Finds the access tags on the given entity
+    /// Finds any station record keys on an entity.
     /// </summary>
     /// <param name="uid">The entity that is being searched.</param>
-    /// <param name="recordKeys"></param>
+    /// <param name="recordKeys">A collection of the station record keys that were found.</param>
     /// <param name="items">All of the items to search for access. If none are passed in, <see cref="FindPotentialAccessItems"/> will be used.</param>
     public bool FindStationRecordKeys(EntityUid uid, out ICollection<StationRecordKey> recordKeys, HashSet<EntityUid>? items = null)
     {
@@ -302,11 +314,12 @@ public sealed class AccessReaderSystem : EntitySystem
     }
 
     /// <summary>
-    ///     Try to find <see cref="AccessComponent"/> on this item
-    ///     or inside this item (if it's pda)
-    ///     This version merges into a set or replaces the set.
-    ///     If owned is false, the existing tag-set "isn't ours" and can't be merged with (is read-only).
+    /// Try to find <see cref="AccessComponent"/> on this item or inside this item (if it's a PDA).
+    /// This version merges into a set or replaces the set.
     /// </summary>
+    /// <param name="uid">The entity that is being searched.</param>
+    /// <param name="tags">The access tags being merged or replaced.</param>
+    /// <param name="owned">If true, the tags will be merged. Otherwise they are replaced.</param>
     private void FindAccessTagsItem(EntityUid uid, ref HashSet<ProtoId<AccessLevelPrototype>>? tags, ref bool owned)
     {
         if (!FindAccessTagsItem(uid, out var targetTags))
@@ -333,26 +346,288 @@ public sealed class AccessReaderSystem : EntitySystem
         }
     }
 
-    public void SetAccesses(EntityUid uid, AccessReaderComponent component, List<ProtoId<AccessLevelPrototype>> accesses)
+    #region: AccessLists API
+
+    /// <summary>
+    /// Clears the entity's <see cref="AccessReaderComponent.AccessLists"/>.
+    /// </summary>
+    /// <param name="ent">The access reader entity which is having its access permissions cleared.</param>
+    public void ClearAccesses(Entity<AccessReaderComponent> ent)
+    {
+        ent.Comp.AccessLists.Clear();
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
+    }
+
+    /// <summary>
+    /// Replaces the access permissions in an entity's <see cref="AccessReaderComponent.AccessLists"/> with a supplied list.
+    /// </summary>
+    /// <param name="ent">The access reader entity which is having its list of access permissions replaced.</param>
+    /// <param name="accesses">The list of access permissions replacing the original one.</param>
+    public void SetAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
+    {
+        ent.Comp.AccessLists.Clear();
+
+        AddAccesses(ent, accesses);
+    }
+
+    /// <inheritdoc cref = "SetAccesses"/>
+    public void SetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    {
+        ent.Comp.AccessLists.Clear();
+
+        AddAccesses(ent, accesses);
+    }
+
+    /// <summary>
+    /// Adds a collection of access permissions to an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
+    /// </summary>
+    /// <param name="ent">The access reader entity to which the new access permissions are being added.</param>
+    /// <param name="accesses">The list of access permissions being added.</param>
+    public void AddAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
     {
-        component.AccessLists.Clear();
         foreach (var access in accesses)
         {
-            component.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>>(){access});
+            AddAccess(ent, access, false);
         }
-        Dirty(uid, component);
-        RaiseLocalEvent(uid, new AccessReaderConfigurationChangedEvent());
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
-    public bool FindAccessItemsInventory(EntityUid uid, out HashSet<EntityUid> items)
+    /// <inheritdoc cref = "AddAccesses"/>
+    public void AddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    {
+        foreach (var access in accesses)
+        {
+            AddAccess(ent, access, false);
+        }
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
+    }
+
+    /// <summary>
+    /// Adds an access permission to an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
+    /// </summary>
+    /// <param name="ent">The access reader entity to which the access permission is being added.</param>
+    /// <param name="access">The access permission being added.</param>
+    /// <param name="dirty">If true, the component will be  marked as changed afterward.</param>
+    public void AddAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
+    {
+        ent.Comp.AccessLists.Add(access);
+
+        if (!dirty)
+            return;
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
+    }
+
+    /// <inheritdoc cref = "AddAccess"/>
+    public void AddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
+    {
+        AddAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access }, dirty);
+    }
+
+    /// <summary>
+    /// Removes a collection of access permissions from an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
+    /// </summary>
+    /// <param name="ent">The access reader entity from which the access permissions are being removed.</param>
+    /// <param name="accesses">The list of access permissions being removed.</param>
+    public void RemoveAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
+    {
+        foreach (var access in accesses)
+        {
+            RemoveAccess(ent, access, false);
+        }
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
+    }
+
+    /// <inheritdoc cref = "RemoveAccesses"/>
+    public void RemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    {
+        foreach (var access in accesses)
+        {
+            RemoveAccess(ent, access, false);
+        }
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
+    }
+
+    /// <summary>
+    /// Removes an access permission from an access reader entity's <see cref="AccessReaderComponent.AccessLists"/>
+    /// </summary>
+    /// <param name="ent">The access reader entity from which the access permission is being removed.</param>
+    /// <param name="access">The access permission being removed.</param>
+    /// <param name="dirty">If true, the component will be marked as changed afterward.</param>
+    public void RemoveAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
+    {
+        for (int i = ent.Comp.AccessLists.Count - 1; i >= 0; i--)
+        {
+            if (ent.Comp.AccessLists[i].SetEquals(access))
+            {
+                ent.Comp.AccessLists.RemoveAt(i);
+            }
+        }
+
+        if (!dirty)
+            return;
+
+        Dirty(ent);
+        RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
+    }
+
+    /// <inheritdoc cref = "RemoveAccess"/>
+    public void RemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
     {
-        items = new();
+        RemoveAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access }, dirty);
+    }
 
-        foreach (var item in _handsSystem.EnumerateHeld(uid))
+    #endregion
+
+    #region: AccessKeys API
+
+    /// <summary>
+    /// Clears all access keys from an access reader.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    public void ClearAccessKeys(Entity<AccessReaderComponent> ent)
+    {
+        ent.Comp.AccessKeys.Clear();
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Replaces all access keys on an access reader with those from a supplied list.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="keys">The new access keys that are replacing the old ones.</param>
+    public void SetAccessKeys(Entity<AccessReaderComponent> ent, HashSet<StationRecordKey> keys)
+    {
+        ent.Comp.AccessKeys.Clear();
+
+        foreach (var key in keys)
         {
-            items.Add(item);
+            ent.Comp.AccessKeys.Add(key);
         }
 
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Adds an access key to an access reader.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="key">The access key being added.</param>
+    public void AddAccessKey(Entity<AccessReaderComponent> ent, StationRecordKey key)
+    {
+        ent.Comp.AccessKeys.Add(key);
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Removes an access key from an access reader.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="key">The access key being removed.</param>
+    public void RemoveAccessKey(Entity<AccessReaderComponent> ent, StationRecordKey key)
+    {
+        ent.Comp.AccessKeys.Remove(key);
+        Dirty(ent);
+    }
+
+    #endregion
+
+    #region: DenyTags API
+
+    /// <summary>
+    /// Clears all deny tags from an access reader.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    public void ClearDenyTags(Entity<AccessReaderComponent> ent)
+    {
+        ent.Comp.DenyTags.Clear();
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Replaces all deny tags on an access reader with those from a supplied list.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="tag">The new tags that are replacing the old.</param>
+    public void SetDenyTags(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> tags)
+    {
+        ent.Comp.DenyTags.Clear();
+
+        foreach (var tag in tags)
+        {
+            ent.Comp.DenyTags.Add(tag);
+        }
+
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Adds a tag to an access reader that will be used to deny access.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="tag">The tag being added.</param>
+    public void AddDenyTag(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> tag)
+    {
+        ent.Comp.DenyTags.Add(tag);
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Removes a tag from an access reader that denied a user access.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="tag">The tag being removed.</param>
+    public void RemoveDenyTag(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> tag)
+    {
+        ent.Comp.DenyTags.Remove(tag);
+        Dirty(ent);
+    }
+
+    #endregion
+
+    /// <summary>
+    /// Enables/disables the access reader on an entity.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="enabled">Enable/disable the access reader.</param>
+    public void SetActive(Entity<AccessReaderComponent> ent, bool enabled)
+    {
+        ent.Comp.Enabled = enabled;
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Enables/disables the logging of access attempts on an access reader entity.
+    /// </summary>
+    /// <param name="ent">The access reader entity.</param>
+    /// <param name="enabled">Enable/disable logging.</param>
+    public void SetLoggingActive(Entity<AccessReaderComponent> ent, bool enabled)
+    {
+        ent.Comp.LoggingDisabled = !enabled;
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Searches an entity's hand and ID slot for any contained items.
+    /// </summary>
+    /// <param name="uid">The entity being searched.</param>
+    /// <param name="items">The collection of found items.</param>
+    /// <returns>True if one or more items were found.</returns>
+    public bool FindAccessItemsInventory(EntityUid uid, out HashSet<EntityUid> items)
+    {
+        items = new(_handsSystem.EnumerateHeld(uid));
+
         // maybe its inside an inventory slot?
         if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid))
         {
@@ -363,9 +638,11 @@ public sealed class AccessReaderSystem : EntitySystem
     }
 
     /// <summary>
-    ///     Try to find <see cref="AccessComponent"/> on this item
-    ///     or inside this item (if it's pda)
+    /// Try to find <see cref="AccessComponent"/> on this entity or inside it (if it's a PDA).
     /// </summary>
+    /// <param name="uid">The entity being searched.</param>
+    /// <param name="tags">The access tags that were found.</param>
+    /// <returns>True if one or more access tags were found.</returns>
     private bool FindAccessTagsItem(EntityUid uid, out HashSet<ProtoId<AccessLevelPrototype>> tags)
     {
         tags = new();
@@ -376,9 +653,11 @@ public sealed class AccessReaderSystem : EntitySystem
     }
 
     /// <summary>
-    ///     Try to find <see cref="StationRecordKeyStorageComponent"/> on this item
-    ///     or inside this item (if it's pda)
+    /// Try to find <see cref="StationRecordKeyStorageComponent"/> on this entity or inside it (if it's a PDA).
     /// </summary>
+    /// <param name="uid">The entity being searched.</param>
+    /// <param name="key">The station record key that was found.</param>
+    /// <returns>True if a station record key was found.</returns>
     private bool FindStationRecordKeyItem(EntityUid uid, [NotNullWhen(true)] out StationRecordKey? key)
     {
         if (TryComp(uid, out StationRecordKeyStorageComponent? storage) && storage.Key != null)
@@ -432,15 +711,20 @@ public sealed class AccessReaderSystem : EntitySystem
     /// </summary>
     /// <param name="ent">The reader to log the access on</param>
     /// <param name="name">The name to log as</param>
-    public void LogAccess(Entity<AccessReaderComponent> ent, string name)
+    public void LogAccess(Entity<AccessReaderComponent> ent, string name, TimeSpan? accessTime = null, bool force = false)
     {
-        if (IsPaused(ent) || ent.Comp.LoggingDisabled)
-            return;
+        if (!force)
+        {
+            if (IsPaused(ent) || ent.Comp.LoggingDisabled)
+                return;
 
-        if (ent.Comp.AccessLog.Count >= ent.Comp.AccessLogLimit)
-            ent.Comp.AccessLog.Dequeue();
+            if (ent.Comp.AccessLog.Count >= ent.Comp.AccessLogLimit)
+                ent.Comp.AccessLog.Dequeue();
+        }
 
-        var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
+        var stationTime = accessTime ?? _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
         ent.Comp.AccessLog.Enqueue(new AccessRecord(stationTime, name));
+
+        Dirty(ent);
     }
 }