]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Devices with access restrictions list those restrictions in their examination descrip...
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Mon, 15 Sep 2025 07:19:25 +0000 (02:19 -0500)
committerGitHub <noreply@github.com>
Mon, 15 Sep 2025 07:19:25 +0000 (10:19 +0300)
14 files changed:
Content.IntegrationTests/Tests/Access/AccessReaderTest.cs
Content.Server/Access/Systems/AccessOverriderSystem.cs
Content.Server/Doors/Electronics/Systems/DoorElectronicsSystem.cs
Content.Shared/Access/Components/AccessReaderComponent.cs
Content.Shared/Access/Components/ShowAccessReaderSettingsComponent.cs [new file with mode: 0644]
Content.Shared/Access/Systems/AccessReaderSystem.cs
Content.Shared/Localizations/ContentLocalizationManager.cs
Resources/Locale/en-US/access/systems/access-reader-system.ftl
Resources/Locale/en-US/generic.ftl
Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml
Resources/Prototypes/Entities/Structures/Machines/holopad.yml
Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml
Resources/Prototypes/Entities/Structures/cryogenic_sleep_unit.yml

index b98f030b0658282c28f5179a885844beafc0210f..a0c8c775b1ad815a0141284eb9cb6b3bafa58615 100644 (file)
@@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Access
                 system.ClearDenyTags(reader);
 
                 // test one list
-                system.AddAccess(reader, "A");
+                system.TryAddAccess(reader, "A");
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -62,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);
+                system.TryClearAccesses(reader);
 
                 // test one list - two items
-                system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
+                system.TryAddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
@@ -73,14 +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);
+                system.TryClearAccesses(reader);
 
                 // test two list
                 var accesses = new List<HashSet<ProtoId<AccessLevelPrototype>>>() {
                     new HashSet<ProtoId<AccessLevelPrototype>> () { "A" },
                     new HashSet<ProtoId<AccessLevelPrototype>> () { "B", "C" }
                 };
-                system.AddAccesses(reader, accesses);
+                system.TryAddAccesses(reader, accesses);
                 Assert.Multiple(() =>
                 {
                     Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -90,10 +90,10 @@ 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);
+                system.TryClearAccesses(reader);
 
                 // test deny list
-                system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
+                system.TryAddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
                 system.AddDenyTag(reader, "B");
                 Assert.Multiple(() =>
                 {
@@ -102,7 +102,7 @@ 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.TryClearAccesses(reader);
                 system.ClearDenyTags(reader);
             });
             await pair.CleanReturnAsync();
index 68bdd6b9a9502235810189acec3eb53c24ee2783..4eaf3c0419956289f91c2504f5e1465d50001023 100644 (file)
@@ -229,7 +229,7 @@ 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)}]");
 
-        _accessReader.SetAccesses(accessReaderEnt.Value, newAccessList);
+        _accessReader.TrySetAccesses(accessReaderEnt.Value, newAccessList);
 
         var ev = new OnAccessOverriderAccessUpdatedEvent(player);
         RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
index af2738d105e797cc6c975919fa91d84009df094b..5579bc59880b73baf0b1ca3ffefc8ede21027886 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.TrySetAccesses((uid, accessReader), args.AccessList);
     }
 
     private void OnAccessReaderChanged(
index 6c2416fdf473174d32758fc622f19ddecae4edd2..c261c7decac1a4adb2d245cabfd9ded288b04d8a 100644 (file)
@@ -34,6 +34,15 @@ public sealed partial class AccessReaderComponent : Component
     [DataField("access")]
     public List<HashSet<ProtoId<AccessLevelPrototype>>> AccessLists = new();
 
+    /// <summary>
+    /// An unmodified copy of the original list of the access groups that grant access to this reader.
+    /// </summary>
+    /// <remarks>
+    /// If null, the access lists of this entity have not been modified yet.
+    /// </remarks>
+    [DataField]
+    public List<HashSet<ProtoId<AccessLevelPrototype>>>? AccessListsOriginal = null;
+
     /// <summary>
     /// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required to gain access.
     /// </summary>
@@ -76,6 +85,16 @@ public sealed partial class AccessReaderComponent : Component
     /// </summary>
     [DataField]
     public bool BreakOnAccessBreaker = true;
+
+    /// <summary>
+    /// The examination text associated with this component.
+    /// </summary>
+    /// <remarks>
+    /// The text can be supplied with the 'access' variable to populate it
+    /// with a comma separated list of the access levels contained in <see cref="AccessLists"/>.
+    /// </remarks>
+    [DataField]
+    public LocId ExaminationText = "access-reader-examination";
 }
 
 [DataDefinition, Serializable, NetSerializable]
@@ -96,19 +115,36 @@ public sealed class AccessReaderComponentState : ComponentState
     public bool Enabled;
     public HashSet<ProtoId<AccessLevelPrototype>> DenyTags;
     public List<HashSet<ProtoId<AccessLevelPrototype>>> AccessLists;
+    public List<HashSet<ProtoId<AccessLevelPrototype>>>? AccessListsOriginal;
     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)
+    public AccessReaderComponentState(
+        bool enabled,
+        HashSet<ProtoId<AccessLevelPrototype>> denyTags,
+        List<HashSet<ProtoId<AccessLevelPrototype>>> accessLists,
+        List<HashSet<ProtoId<AccessLevelPrototype>>>? accessListsOriginal,
+        List<(NetEntity, uint)> accessKeys,
+        Queue<AccessRecord> accessLog,
+        int accessLogLimit)
     {
         Enabled = enabled;
         DenyTags = denyTags;
         AccessLists = accessLists;
+        AccessListsOriginal = accessListsOriginal;
         AccessKeys = accessKeys;
         AccessLog = accessLog;
         AccessLogLimit = accessLogLimit;
     }
 }
 
+/// <summary>
+/// Raised after the settings on the access reader are changed.
+/// </summary>
 public sealed class AccessReaderConfigurationChangedEvent : EntityEventArgs;
+
+/// <summary>
+/// Raised before the settings on the access reader are changed. Can be cancelled.
+/// </summary>
+public sealed class AccessReaderConfigurationAttemptEvent : CancellableEntityEventArgs;
diff --git a/Content.Shared/Access/Components/ShowAccessReaderSettingsComponent.cs b/Content.Shared/Access/Components/ShowAccessReaderSettingsComponent.cs
new file mode 100644 (file)
index 0000000..6f72a41
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Access.Components;
+
+/// <summary>
+/// This component allows you to see whether an access reader's settings have been modified.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ShowAccessReaderSettingsComponent : Component, IClothingSlots
+{
+    /// <summary>
+    /// Determines from which equipment slots this entity can provide its benefits.
+    /// </summary>
+    public SlotFlags Slots { get; set; } = ~SlotFlags.POCKET;
+}
index 186aef5305f1084c91dca1f2c1da262ead8b9351..56aa0550cff077c7c6fa50da263d6012bb5b3d85 100644 (file)
@@ -1,12 +1,15 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using System.Text;
 using Content.Shared.Access.Components;
 using Content.Shared.DeviceLinking.Events;
 using Content.Shared.Emag.Systems;
+using Content.Shared.Examine;
 using Content.Shared.GameTicking;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Inventory;
+using Content.Shared.Localizations;
 using Content.Shared.NameIdentifier;
 using Content.Shared.PDA;
 using Content.Shared.StationRecords;
@@ -37,17 +40,74 @@ public sealed class AccessReaderSystem : EntitySystem
     {
         base.Initialize();
 
+        SubscribeLocalEvent<AccessReaderComponent, ExaminedEvent>(OnExamined);
         SubscribeLocalEvent<AccessReaderComponent, GotEmaggedEvent>(OnEmagged);
         SubscribeLocalEvent<AccessReaderComponent, LinkAttemptEvent>(OnLinkAttempt);
+        SubscribeLocalEvent<AccessReaderComponent, AccessReaderConfigurationAttemptEvent>(OnConfigurationAttempt);
 
         SubscribeLocalEvent<AccessReaderComponent, ComponentGetState>(OnGetState);
         SubscribeLocalEvent<AccessReaderComponent, ComponentHandleState>(OnHandleState);
     }
 
+    private void OnExamined(Entity<AccessReaderComponent> ent, ref ExaminedEvent args)
+    {
+        if (!GetMainAccessReader(ent, out var mainAccessReader))
+            return;
+
+        mainAccessReader.Value.Comp.AccessListsOriginal ??= new(mainAccessReader.Value.Comp.AccessLists);
+
+        var accessHasBeenModified = mainAccessReader.Value.Comp.AccessLists.Count != mainAccessReader.Value.Comp.AccessListsOriginal.Count;
+
+        if (!accessHasBeenModified)
+        {
+            foreach (var accessSubgroup in mainAccessReader.Value.Comp.AccessLists)
+            {
+                if (!mainAccessReader.Value.Comp.AccessListsOriginal.Any(y => y.SetEquals(accessSubgroup)))
+                {
+                    accessHasBeenModified = true;
+                    break;
+                }
+            }
+        }
+
+        var canSeeAccessModification = accessHasBeenModified &&
+            (HasComp<ShowAccessReaderSettingsComponent>(ent) ||
+            _inventorySystem.TryGetInventoryEntity<ShowAccessReaderSettingsComponent>(args.Examiner, out _));
+
+        if (canSeeAccessModification)
+        {
+            var localizedCurrentNames = GetLocalizedAccessNames(mainAccessReader.Value.Comp.AccessLists);
+            var accessesFormatted = ContentLocalizationManager.FormatListToOr(localizedCurrentNames);
+            var currentSettingsMessage = localizedCurrentNames.Count > 0
+                ? Loc.GetString("access-reader-access-settings-modified-message", ("access", accessesFormatted))
+                : Loc.GetString("access-reader-access-settings-removed-message");
+
+            args.PushMarkup(currentSettingsMessage);
+
+            return;
+        }
+
+        var localizedOriginalNames = GetLocalizedAccessNames(mainAccessReader.Value.Comp.AccessListsOriginal);
+
+        // If the string list is empty either there were no access restrictions or the localized names were invalid
+        if (localizedOriginalNames.Count == 0)
+            return;
+
+        var originalAccessesFormatted = ContentLocalizationManager.FormatListToOr(localizedOriginalNames);
+        var originalSettingsMessage = Loc.GetString(mainAccessReader.Value.Comp.ExaminationText, ("access", originalAccessesFormatted));
+        args.PushMarkup(originalSettingsMessage);
+    }
+
     private void OnGetState(EntityUid uid, AccessReaderComponent component, ref ComponentGetState args)
     {
-        args.State = new AccessReaderComponentState(component.Enabled, component.DenyTags, component.AccessLists,
-            _recordsSystem.Convert(component.AccessKeys), component.AccessLog, component.AccessLogLimit);
+        args.State = new AccessReaderComponentState(
+            component.Enabled,
+            component.DenyTags,
+            component.AccessLists,
+            component.AccessListsOriginal,
+            _recordsSystem.Convert(component.AccessKeys),
+            component.AccessLog,
+            component.AccessLogLimit);
     }
 
     private void OnHandleState(EntityUid uid, AccessReaderComponent component, ref ComponentHandleState args)
@@ -66,6 +126,7 @@ public sealed class AccessReaderSystem : EntitySystem
         }
 
         component.AccessLists = new(state.AccessLists);
+        component.AccessListsOriginal = state.AccessListsOriginal == null ? null : new(state.AccessListsOriginal);
         component.DenyTags = new(state.DenyTags);
         component.AccessLog = new(state.AccessLog);
         component.AccessLogLimit = state.AccessLogLimit;
@@ -100,6 +161,13 @@ public sealed class AccessReaderSystem : EntitySystem
         Dirty(uid, reader);
     }
 
+    private void OnConfigurationAttempt(Entity<AccessReaderComponent> ent, ref AccessReaderConfigurationAttemptEvent args)
+    {
+        // The first time that the access list of the reader is modified,
+        // make a copy of the original settings
+        ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists);
+    }
+
     /// <summary>
     /// Searches the source for access tags
     /// then compares it with the all targets accesses to see if it is allowed.
@@ -348,11 +416,23 @@ public sealed class AccessReaderSystem : EntitySystem
 
     #region: AccessLists API
 
+    /// <summary>
+    /// Tries to clear 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 TryClearAccesses(Entity<AccessReaderComponent> ent)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            ClearAccesses(ent);
+        }
+    }
+
     /// <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)
+    private void ClearAccesses(Entity<AccessReaderComponent> ent)
     {
         ent.Comp.AccessLists.Clear();
 
@@ -360,32 +440,65 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <summary>
+    /// Tries to replace 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 TrySetAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            SetAccesses(ent, accesses);
+        }
+    }
+
     /// <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)
+    private void SetAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
     {
         ent.Comp.AccessLists.Clear();
-
         AddAccesses(ent, accesses);
     }
 
+    /// <inheritdoc cref = "TrySetAccesses"/>
+    public void TrySetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            SetAccesses(ent, accesses);
+        }
+    }
+
     /// <inheritdoc cref = "SetAccesses"/>
-    public void SetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    private void SetAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
     {
         ent.Comp.AccessLists.Clear();
-
         AddAccesses(ent, accesses);
     }
 
+    /// <summary>
+    /// Tries to add 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 TryAddAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            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)
+    private void AddAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
     {
         foreach (var access in accesses)
         {
@@ -396,8 +509,17 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <inheritdoc cref = "TryAddAccesses"/>
+    public void TryAddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            AddAccesses(ent, accesses);
+        }
+    }
+
     /// <inheritdoc cref = "AddAccesses"/>
-    public void AddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    private void AddAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
     {
         foreach (var access in accesses)
         {
@@ -408,13 +530,27 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <summary>
+    /// Tries to add 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 TryAddAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            AddAccess(ent, access);
+        }
+    }
+
     /// <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)
+    private void AddAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
     {
         ent.Comp.AccessLists.Add(access);
 
@@ -425,18 +561,40 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <inheritdoc cref = "TryAddAccess"/>
+    public void TryAddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            AddAccess(ent, access);
+        }
+    }
+
     /// <inheritdoc cref = "AddAccess"/>
-    public void AddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
+    private void AddAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
     {
         AddAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access }, dirty);
     }
 
+    /// <summary>
+    /// Tries to remove 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 TryRemoveAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            RemoveAccesses(ent, accesses);
+        }
+    }
+
     /// <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)
+    private void RemoveAccesses(Entity<AccessReaderComponent> ent, List<HashSet<ProtoId<AccessLevelPrototype>>> accesses)
     {
         foreach (var access in accesses)
         {
@@ -447,8 +605,17 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <inheritdoc cref = "TryRemoveAccesses"/>
+    public void TryRemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            RemoveAccesses(ent, accesses);
+        }
+    }
+
     /// <inheritdoc cref = "RemoveAccesses"/>
-    public void RemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
+    private void RemoveAccesses(Entity<AccessReaderComponent> ent, List<ProtoId<AccessLevelPrototype>> accesses)
     {
         foreach (var access in accesses)
         {
@@ -459,13 +626,27 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <summary>
+    /// Tries to 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 TryRemoveAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            RemoveAccess(ent, access);
+        }
+    }
+
     /// <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)
+    private void RemoveAccess(Entity<AccessReaderComponent> ent, HashSet<ProtoId<AccessLevelPrototype>> access, bool dirty = true)
     {
         for (int i = ent.Comp.AccessLists.Count - 1; i >= 0; i--)
         {
@@ -482,12 +663,29 @@ public sealed class AccessReaderSystem : EntitySystem
         RaiseLocalEvent(ent, new AccessReaderConfigurationChangedEvent());
     }
 
+    /// <inheritdoc cref = "TryRemoveAccess"/>
+    public void TryRemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access)
+    {
+        if (CanConfigureAccessReader(ent))
+        {
+            RemoveAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access });
+        }
+    }
+
     /// <inheritdoc cref = "RemoveAccess"/>
-    public void RemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
+    private void RemoveAccess(Entity<AccessReaderComponent> ent, ProtoId<AccessLevelPrototype> access, bool dirty = true)
     {
         RemoveAccess(ent, new HashSet<ProtoId<AccessLevelPrototype>>() { access }, dirty);
     }
 
+    private bool CanConfigureAccessReader(Entity<AccessReaderComponent> ent)
+    {
+        var ev = new AccessReaderConfigurationAttemptEvent();
+        RaiseLocalEvent(ent, ev);
+
+        return !ev.Cancelled;
+    }
+
     #endregion
 
     #region: AccessKeys API
@@ -727,4 +925,38 @@ public sealed class AccessReaderSystem : EntitySystem
 
         Dirty(ent);
     }
+
+    private List<string> GetLocalizedAccessNames(List<HashSet<ProtoId<AccessLevelPrototype>>> accessLists)
+    {
+        var localizedNames = new List<string>();
+        string? andSeparator = null;
+
+        foreach (var accessHashSet in accessLists)
+        {
+            var sb = new StringBuilder();
+            var accessSubset = accessHashSet.ToList();
+
+            // Combine the names of all access levels in the subset into a single string
+            foreach (var access in accessSubset)
+            {
+                var accessName = Loc.GetString("access-reader-unknown-id");
+
+                if (_prototype.Resolve(access, out var accessProto) && !string.IsNullOrWhiteSpace(accessProto.Name))
+                    accessName = Loc.GetString(accessProto.Name);
+
+                sb.Append(Loc.GetString("access-reader-access-label", ("access", accessName)));
+
+                if (accessSubset.IndexOf(access) < (accessSubset.Count - 1))
+                {
+                    andSeparator ??= " " + Loc.GetString("generic-and") + " ";
+                    sb.Append(andSeparator);
+                }
+            }
+
+            // Add this string to the list
+            localizedNames.Add(sb.ToString());
+        }
+
+        return localizedNames;
+    }
 }
index 3cfe3f2a3df61ce652caecb1df66a09b62bc366c..7af34f01d8a09f10d5d52236823fe9342f92446e 100644 (file)
@@ -132,7 +132,7 @@ namespace Content.Shared.Localizations
                 <= 0 => string.Empty,
                 1 => list[0],
                 2 => $"{list[0]} or {list[1]}",
-                _ => $"{string.Join(" or ", list)}"
+                _ => $"{string.Join(", ", list.GetRange(0, list.Count - 1))}, or {list[^1]}"
             };
         }
 
index d66989f6cf826b185de1ad586b4bec2bdbc71466..bf3bfe4d960c88a332b14edadf0908255cc83a82 100644 (file)
@@ -1 +1,6 @@
 access-reader-unknown-id = Unknown
+access-reader-access-label = [color=yellow]{$access}[/color]
+access-reader-examination = Access is generally restricted to personnel with {$access} access.
+access-reader-examination-functionality-restricted = {$access} access may be required to use certain functions.
+access-reader-access-settings-modified-message = [italic]The access reader has been modified to accept personnel with {$access} access.[/italic]
+access-reader-access-settings-removed-message = [italic]The settings on the access reader have been deleted.[/italic]
\ No newline at end of file
index 350409788521c2a92dd9f076557fb80075518af7..cdca0f24934420d3140b17f0d194551f0ed36c0f 100644 (file)
@@ -4,6 +4,9 @@ generic-not-available-shorthand = N/A
 generic-article-a = a
 generic-article-an = an
 
+generic-and = and
+generic-or = or
+
 generic-unknown = unknown
 generic-unknown-title = Unknown
 generic-error = error
index b35109ce17f4980ef1582f4e0497eb10d86cf0b5..5ecabc41eb1af4b06a7c4d6085dbf4b6f804404c 100644 (file)
@@ -30,6 +30,7 @@
     damageContainers:
     - Inorganic
     - Silicon
+  - type: ShowAccessReaderSettings
 
 - type: entity
   parent: [ClothingEyesBase, ShowMedicalIcons]
index 489411bc28ddd60ca9ba4cb24f808f4e40ebb459..bde406f5cb770c3908f812b53d8a65965481831b 100644 (file)
       price: 150
     - type: AccessReader
       access: [ [ "Engineering" ] ]
+      examinationText: access-reader-examination-functionality-restricted
     - type: PryUnpowered
       pryModifier: 0.5
     - type: PointLight
index 95a1fba489631fa1bcee20edc2e2e64b21519a83..8c68710d76e233a64d0339e480c37e74e19bdf9b 100644 (file)
@@ -76,6 +76,7 @@
     speakerVolume: Speak
   - type: AccessReader
     access: [[ "Command" ]]
+    examinationText: access-reader-examination-functionality-restricted
   - type: ActivatableUI
     key: enum.HolopadUiKey.InteractionWindow
   - type: ActivatableUIRequiresPower
index 52f29168fdcba71f90228bf22ba88db45eda0618..0fd9f1fab5b902f22d6f1a4e7793d66b80949cb6 100644 (file)
         type: GenpopLockerBoundUserInterface
   - type: AccessReader # note! this access is for the UI, not the door. door access is handled on GenpopLocker
     access: [["Security"]]
+    examinationText: access-reader-examination-functionality-restricted
   - type: Lock
     locked: false
     useAccess: false
index 7458cd2b6998187a31c92ec350aeff0bbdafe0ed..131caec4a30357f7c1cab5b370b0a045b6bc128b 100644 (file)
@@ -23,6 +23,7 @@
   - type: AccessReader
     breakOnAccessBreaker: false
     access: [["Cryogenics"]]
+    examinationText: access-reader-examination-functionality-restricted
   - type: InteractionOutline
   - type: Cryostorage
   - type: Fixtures