]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add access logs (IC ones) (#17810)
authorChief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Tue, 26 Dec 2023 22:24:53 +0000 (16:24 -0600)
committerGitHub <noreply@github.com>
Tue, 26 Dec 2023 22:24:53 +0000 (18:24 -0400)
28 files changed:
Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs [new file with mode: 0644]
Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs [new file with mode: 0644]
Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/GameTicker.Lobby.cs
Content.Server/GameTicking/GameTicker.RoundFlow.cs
Content.Shared/Access/Components/AccessComponent.cs
Content.Shared/Access/Components/AccessReaderComponent.cs
Content.Shared/Access/Components/IdCardComponent.cs
Content.Shared/Access/Systems/AccessReaderSystem.cs
Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs [new file with mode: 0644]
Content.Shared/GameTicking/SharedGameTicker.cs
Resources/Locale/en-US/access/systems/access-reader-system.ftl [new file with mode: 0644]
Resources/Locale/en-US/cartridge-loader/cartridges.ftl
Resources/Locale/en-US/guidebook/guides.ftl
Resources/Prototypes/Catalog/Fills/Lockers/security.yml
Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml
Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Guidebook/security.yml
Resources/ServerInfo/Guidebook/Security/Forensics.xml [moved from Resources/ServerInfo/Guidebook/Security/DNA.xml with 59% similarity]
Resources/Textures/Objects/Devices/cartridge.rsi/cart-log.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/cartridge.rsi/meta.json

diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
new file mode 100644 (file)
index 0000000..d28d322
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+public sealed partial class LogProbeUi : UIFragment
+{
+    private LogProbeUiFragment? _fragment;
+
+    public override Control GetUIFragmentRoot()
+    {
+        return _fragment!;
+    }
+
+    public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+    {
+        _fragment = new LogProbeUiFragment();
+    }
+
+    public override void UpdateState(BoundUserInterfaceState state)
+    {
+        if (state is not LogProbeUiState logProbeUiState)
+            return;
+
+        _fragment?.UpdateState(logProbeUiState.PulledLogs);
+    }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml
new file mode 100644 (file)
index 0000000..5712b30
--- /dev/null
@@ -0,0 +1,20 @@
+<BoxContainer xmlns="https://spacestation14.io"
+              xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+              Margin="4"
+              Orientation="Vertical">
+    <BoxContainer Orientation="Horizontal">
+        <Label Name="NumberLabel"
+               Align="Center"
+               SetWidth="60"
+               ClipText="True"/>
+        <Label Name="TimeLabel"
+               Align="Center"
+               SetWidth="280"
+               ClipText="True"/>
+        <Label Name="AccessorLabel"
+               Align="Center"
+               SetWidth="110"
+               ClipText="True"/>
+    </BoxContainer>
+    <customControls:HSeparator Margin="0 5 0 5"/>
+</BoxContainer>
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml.cs
new file mode 100644 (file)
index 0000000..369042d
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class LogProbeUiEntry : BoxContainer
+{
+    public LogProbeUiEntry(int numberLabel, string timeText, string accessorText)
+    {
+        RobustXamlLoader.Load(this);
+        NumberLabel.Text = numberLabel.ToString();
+        TimeLabel.Text = timeText;
+        AccessorLabel.Text = accessorText;
+    }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
new file mode 100644 (file)
index 0000000..d369a33
--- /dev/null
@@ -0,0 +1,21 @@
+ <cartridges:LogProbeUiFragment xmlns="https://spacestation14.io"
+                                xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
+                                xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+                                Orientation="Vertical"
+                                VerticalExpand="True">
+    <PanelContainer>
+        <PanelContainer.PanelOverride>
+            <gfx:StyleBoxFlat BackgroundColor="#000000FF"
+                              BorderColor="#5a5a5a"
+                              BorderThickness="0 0 0 1"/>
+        </PanelContainer.PanelOverride>
+        <BoxContainer Orientation="Horizontal" Align="Center" Margin="8">
+            <Label HorizontalExpand="True" Text="{Loc 'log-probe-label-number'}"/>
+            <Label HorizontalExpand="True" Text="{Loc 'log-probe-label-time'}"/>
+            <Label HorizontalExpand="True" Text="{Loc 'log-probe-label-accessor'}"/>
+        </BoxContainer>
+    </PanelContainer>
+    <ScrollContainer VerticalExpand="True" HScrollEnabled="True">
+        <BoxContainer Orientation="Vertical" Name="ProbedDeviceContainer"/>
+    </ScrollContainer>
+</cartridges:LogProbeUiFragment>
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
new file mode 100644 (file)
index 0000000..b22e0bc
--- /dev/null
@@ -0,0 +1,39 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class LogProbeUiFragment : BoxContainer
+{
+    public LogProbeUiFragment()
+    {
+        RobustXamlLoader.Load(this);
+    }
+
+    public void UpdateState(List<PulledAccessLog> logs)
+    {
+        ProbedDeviceContainer.RemoveAllChildren();
+
+        //Reverse the list so the oldest entries appear at the bottom
+        logs.Reverse();
+
+        var count =  1;
+        foreach (var log in logs)
+        {
+            AddAccessLog(log, count);
+            count++;
+        }
+    }
+
+    private void AddAccessLog(PulledAccessLog log, int numberLabelText)
+    {
+        var timeLabelText = TimeSpan.FromSeconds(Math.Truncate(log.Time.TotalSeconds)).ToString();
+        var accessorLabelText = log.Accessor;
+        var entry = new LogProbeUiEntry(numberLabelText, timeLabelText, accessorLabelText);
+
+        ProbedDeviceContainer.AddChild(entry);
+    }
+}
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
new file mode 100644 (file)
index 0000000..cfa92dd
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Shared.Audio;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+[RegisterComponent]
+[Access(typeof(LogProbeCartridgeSystem))]
+public sealed partial class LogProbeCartridgeComponent : Component
+{
+    /// <summary>
+    /// The list of pulled access logs
+    /// </summary>
+    [DataField, ViewVariables]
+    public List<PulledAccessLog> PulledAccessLogs = new();
+
+    /// <summary>
+    /// The sound to make when we scan something with access
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
+}
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
new file mode 100644 (file)
index 0000000..f5ccea9
--- /dev/null
@@ -0,0 +1,71 @@
+using Content.Shared.Access.Components;
+using Content.Shared.Audio;
+using Content.Shared.CartridgeLoader;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared.Popups;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Random;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+public sealed class LogProbeCartridgeSystem : EntitySystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
+        SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract);
+    }
+
+    /// <summary>
+    /// The <see cref="CartridgeAfterInteractEvent" /> gets relayed to this system if the cartridge loader is running
+    /// the LogProbe program and someone clicks on something with it. <br/>
+    /// <br/>
+    /// Updates the program's list of logs with those from the device.
+    /// </summary>
+    private void AfterInteract(Entity<LogProbeCartridgeComponent> ent, ref CartridgeAfterInteractEvent args)
+    {
+        if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target)
+            return;
+
+        if (!TryComp(target, out AccessReaderComponent? accessReaderComponent))
+            return;
+
+        //Play scanning sound with slightly randomized pitch
+        _audioSystem.PlayEntity(ent.Comp.SoundScan, args.InteractEvent.User, target, AudioHelpers.WithVariation(0.25f, _random));
+        _popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
+
+        ent.Comp.PulledAccessLogs.Clear();
+
+        foreach (var accessRecord in accessReaderComponent.AccessLog)
+        {
+            var log = new PulledAccessLog(
+                accessRecord.AccessTime,
+                accessRecord.Accessor
+            );
+
+            ent.Comp.PulledAccessLogs.Add(log);
+        }
+
+        UpdateUiState(ent, args.Loader);
+    }
+
+    /// <summary>
+    /// This gets called when the ui fragment needs to be updated for the first time after activating
+    /// </summary>
+    private void OnUiReady(Entity<LogProbeCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
+    {
+        UpdateUiState(ent, args.Loader);
+    }
+
+    private void UpdateUiState(Entity<LogProbeCartridgeComponent> ent, EntityUid loaderUid)
+    {
+        var state = new LogProbeUiState(ent.Comp.PulledAccessLogs);
+        _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
+    }
+}
index 1943a82617ade2edb103ce410f68ceef05064c25..292e09b6b220a715efb6a87610b5ea7c15953eb0 100644 (file)
@@ -80,7 +80,7 @@ namespace Content.Server.GameTicking
         private TickerLobbyStatusEvent GetStatusMsg(ICommonSession session)
         {
             _playerGameStatuses.TryGetValue(session.UserId, out var status);
-            return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, LobbyBackground,status == PlayerGameStatus.ReadyToPlay, _roundStartTime, RoundPreloadTime, _roundStartTimeSpan, Paused);
+            return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, LobbyBackground,status == PlayerGameStatus.ReadyToPlay, _roundStartTime, RoundPreloadTime, RoundStartTimeSpan, Paused);
         }
 
         private void SendStatusToAll()
index 42810779dd0528a15d304543d350f1d2986d7fca..ea8c980eb39d8cb2968e4e934ca41f887fb089fe 100644 (file)
@@ -40,9 +40,6 @@ namespace Content.Server.GameTicking
         private int _roundStartFailCount = 0;
 #endif
 
-        [ViewVariables]
-        private TimeSpan _roundStartTimeSpan;
-
         [ViewVariables]
         private bool _startingRound;
 
@@ -247,7 +244,7 @@ namespace Content.Server.GameTicking
             _roundStartDateTime = DateTime.UtcNow;
             RunLevel = GameRunLevel.InRound;
 
-            _roundStartTimeSpan = _gameTiming.CurTime;
+            RoundStartTimeSpan = _gameTiming.CurTime;
             SendStatusToAll();
             ReqWindowAttentionAll();
             UpdateLateJoinStatus();
@@ -595,7 +592,7 @@ namespace Content.Server.GameTicking
 
         public TimeSpan RoundDuration()
         {
-            return _gameTiming.CurTime.Subtract(_roundStartTimeSpan);
+            return _gameTiming.CurTime.Subtract(RoundStartTimeSpan);
         }
 
         private void AnnounceRound()
index 6930f2dfd60129cda43766c1a686785ce3b1b7ad..2eacf2aa67b02d4dc24e309979721d6cd632aec4 100644 (file)
@@ -3,58 +3,57 @@ using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
 
-namespace Content.Shared.Access.Components
+namespace Content.Shared.Access.Components;
+
+/// <summary>
+///     Simple mutable access provider found on ID cards and such.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedAccessSystem))]
+[AutoGenerateComponentState]
+public sealed partial class AccessComponent : Component
 {
     /// <summary>
-    ///     Simple mutable access provider found on ID cards and such.
+    /// True if the access provider is enabled and can grant access.
     /// </summary>
-    [RegisterComponent, NetworkedComponent]
-    [Access(typeof(SharedAccessSystem))]
-    [AutoGenerateComponentState]
-    public sealed partial class AccessComponent : Component
-    {
-        /// <summary>
-        /// True if the access provider is enabled and can grant access.
-        /// </summary>
-        [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
-        [AutoNetworkedField]
-        public bool Enabled = true;
-
-        [DataField("tags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))]
-        [Access(typeof(SharedAccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
-        [AutoNetworkedField]
-        public HashSet<string> Tags = new();
-
-        /// <summary>
-        ///     Access Groups. These are added to the tags during map init. After map init this will have no effect.
-        /// </summary>
-        [DataField("groups", readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessGroupPrototype>))]
-        [AutoNetworkedField]
-        public HashSet<string> Groups = new();
-    }
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [AutoNetworkedField]
+    public bool Enabled = true;
+
+    [DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))]
+    [Access(typeof(SharedAccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
+    [AutoNetworkedField]
+    public HashSet<string> Tags = new();
 
     /// <summary>
-    /// Event raised on an entity to find additional entities which provide access.
+    /// Access Groups. These are added to the tags during map init. After map init this will have no effect.
     /// </summary>
-    [ByRefEvent]
-    public struct GetAdditionalAccessEvent
-    {
-        public HashSet<EntityUid> Entities = new();
+    [DataField(readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessGroupPrototype>))]
+    [AutoNetworkedField]
+    public HashSet<string> Groups = new();
+}
 
-        public GetAdditionalAccessEvent()
-        {
-        }
+/// <summary>
+/// Event raised on an entity to find additional entities which provide access.
+/// </summary>
+[ByRefEvent]
+public struct GetAdditionalAccessEvent
+{
+    public HashSet<EntityUid> Entities = new();
+
+    public GetAdditionalAccessEvent()
+    {
     }
+}
 
-    [ByRefEvent]
-    public record struct GetAccessTagsEvent(HashSet<string> Tags, IPrototypeManager PrototypeManager)
+[ByRefEvent]
+public record struct GetAccessTagsEvent(HashSet<string> Tags, IPrototypeManager PrototypeManager)
+{
+    public void AddGroup(string group)
     {
-        public void AddGroup(string group)
-        {
-            if (!PrototypeManager.TryIndex<AccessGroupPrototype>(group, out var groupPrototype))
-                return;
+        if (!PrototypeManager.TryIndex<AccessGroupPrototype>(group, out var groupPrototype))
+            return;
 
-            Tags.UnionWith(groupPrototype.Tags);
-        }
+        Tags.UnionWith(groupPrototype.Tags);
     }
 }
index 796646c83c2c24373b4c33885ab80fce7cf44dee..815e6b4c658af9e931d3ad95dad5f2544de75912 100644 (file)
@@ -6,8 +6,8 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
 namespace Content.Shared.Access.Components;
 
 /// <summary>
-///     Stores access levels necessary to "use" an entity
-///     and allows checking if something or somebody is authorized with these access levels.
+/// Stores access levels necessary to "use" an entity
+/// and allows checking if something or somebody is authorized with these access levels.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class AccessReaderComponent : Component
@@ -16,27 +16,28 @@ public sealed partial class AccessReaderComponent : Component
     /// Whether or not the accessreader is enabled.
     /// If not, it will always let people through.
     /// </summary>
-    [DataField("enabled")]
+    [DataField]
     public bool Enabled = true;
 
     /// <summary>
-    ///     The set of tags that will automatically deny an allowed check, if any of them are present.
+    /// The set of tags that will automatically deny an allowed check, if any of them are present.
     /// </summary>
-    [DataField("denyTags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))]
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))]
     public HashSet<string> DenyTags = new();
 
     /// <summary>
     /// 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")]
+    [DataField("access")] [ViewVariables(VVAccess.ReadWrite)]
     public List<HashSet<string>> AccessLists = new();
 
     /// <summary>
     /// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required tp gaim
     /// access.
     /// </summary>
-    [DataField("accessKeys")]
+    [DataField]
     public HashSet<StationRecordKey> AccessKeys = new();
 
     /// <summary>
@@ -48,10 +49,25 @@ public sealed partial class AccessReaderComponent : Component
     /// ignored, though <see cref="Enabled"/> is still respected. Access is denied if there are no valid entities or
     /// they all deny access.
     /// </remarks>
-    [DataField("containerAccessProvider")]
+    [DataField]
     public string? ContainerAccessProvider;
+
+    /// <summary>
+    /// A list of past authentications
+    /// </summary>
+    [DataField]
+    public Queue<AccessRecord> AccessLog = new();
+
+    /// <summary>
+    /// A limit on the max size of <see cref="AccessLog"/>
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int AccessLogLimit = 20;
 }
 
+[Serializable, NetSerializable]
+public record struct AccessRecord(TimeSpan AccessTime, string Accessor);
+
 [Serializable, NetSerializable]
 public sealed class AccessReaderComponentState : ComponentState
 {
@@ -63,11 +79,17 @@ public sealed class AccessReaderComponentState : ComponentState
 
     public List<(NetEntity, uint)> AccessKeys;
 
-    public AccessReaderComponentState(bool enabled, HashSet<string> denyTags, List<HashSet<string>> accessLists, List<(NetEntity, uint)> accessKeys)
+    public Queue<AccessRecord> AccessLog;
+
+    public int AccessLogLimit;
+
+    public AccessReaderComponentState(bool enabled, HashSet<string> denyTags, List<HashSet<string>> accessLists, List<(NetEntity, uint)> accessKeys, Queue<AccessRecord> accessLog, int accessLogLimit)
     {
         Enabled = enabled;
         DenyTags = denyTags;
         AccessLists = accessLists;
         AccessKeys = accessKeys;
+        AccessLog = accessLog;
+        AccessLogLimit = accessLogLimit;
     }
 }
index 7635716d26a6b52e1fb430ff864a1cf982f62468..26e83c5586e00c12708ffb7e60dc909abc1d38cc 100644 (file)
@@ -34,4 +34,10 @@ public sealed partial class IdCardComponent : Component
     [DataField("jobDepartments")]
     [AutoNetworkedField]
     public List<LocId> JobDepartments = new();
+
+    /// <summary>
+    /// Determines if accesses from this card should be logged by <see cref="AccessReaderComponent"/>
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool BypassLogging;
 }
index 3c8e61d2275210ec4c9472c72b675e5a5a8b0c23..2735b7166b24c377df0e90ff1b4ff2b102bd1c76 100644 (file)
@@ -10,8 +10,10 @@ using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using Content.Shared.GameTicking;
 using Robust.Shared.Collections;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Access.Systems;
 
@@ -19,7 +21,10 @@ public sealed class AccessReaderSystem : EntitySystem
 {
     [Dependency] private readonly IPrototypeManager _prototype = default!;
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly SharedGameTicker _gameTicker = default!;
     [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private readonly SharedIdCardSystem _idCardSystem = default!;
     [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
     [Dependency] private readonly SharedStationRecordsSystem _records = default!;
 
@@ -37,7 +42,7 @@ public sealed class AccessReaderSystem : EntitySystem
     private void OnGetState(EntityUid uid, AccessReaderComponent component, ref ComponentGetState args)
     {
         args.State = new AccessReaderComponentState(component.Enabled, component.DenyTags, component.AccessLists,
-            _records.Convert(component.AccessKeys));
+            _records.Convert(component.AccessKeys), component.AccessLog, component.AccessLogLimit);
     }
 
     private void OnHandleState(EntityUid uid, AccessReaderComponent component, ref ComponentHandleState args)
@@ -57,6 +62,8 @@ public sealed class AccessReaderSystem : EntitySystem
 
         component.AccessLists = new(state.AccessLists);
         component.DenyTags = new(state.DenyTags);
+        component.AccessLog = new(state.AccessLog);
+        component.AccessLogLimit = state.AccessLogLimit;
     }
 
     private void OnLinkAttempt(EntityUid uid, AccessReaderComponent component, LinkAttemptEvent args)
@@ -71,6 +78,7 @@ public sealed class AccessReaderSystem : EntitySystem
     {
         args.Handled = true;
         reader.Enabled = false;
+        reader.AccessLog.Clear();
         Dirty(uid, reader);
     }
 
@@ -93,7 +101,13 @@ public sealed class AccessReaderSystem : EntitySystem
         var access = FindAccessTags(user, accessSources);
         FindStationRecordKeys(user, out var stationKeys, accessSources);
 
-        return IsAllowed(access, stationKeys, target, reader);
+        if (IsAllowed(access, stationKeys, target, reader))
+        {
+            LogAccess((target, reader), user);
+            return true;
+        }
+
+        return false;
     }
 
     /// <summary>
@@ -326,4 +340,27 @@ public sealed class AccessReaderSystem : EntitySystem
         key = null;
         return false;
     }
+
+    /// <summary>
+    /// Logs an access
+    /// </summary>
+    /// <param name="ent">The reader to log the access on</param>
+    /// <param name="accessor">The accessor to log</param>
+    private void LogAccess(Entity<AccessReaderComponent> ent, EntityUid accessor)
+    {
+        if (ent.Comp.AccessLog.Count >= ent.Comp.AccessLogLimit)
+            ent.Comp.AccessLog.Dequeue();
+
+        string? name = null;
+        // TODO pass the ID card on IsAllowed() instead of using this expensive method
+        // Set name if the accessor has a card and that card has a name and allows itself to be recorded
+        if (_idCardSystem.TryFindIdCard(accessor, out var idCard)
+            && idCard.Comp is { BypassLogging: false, FullName: not null })
+            name = idCard.Comp.FullName;
+
+        name ??= Loc.GetString("access-reader-unknown-id");
+
+        var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
+        ent.Comp.AccessLog.Enqueue(new AccessRecord(stationTime, name));
+    }
 }
diff --git a/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs b/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs
new file mode 100644 (file)
index 0000000..9dc507b
--- /dev/null
@@ -0,0 +1,30 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+[Serializable, NetSerializable]
+public sealed class LogProbeUiState : BoundUserInterfaceState
+{
+    /// <summary>
+    /// The list of probed network devices
+    /// </summary>
+    public List<PulledAccessLog> PulledLogs;
+
+    public LogProbeUiState(List<PulledAccessLog> pulledLogs)
+    {
+        PulledLogs = pulledLogs;
+    }
+}
+
+[Serializable, NetSerializable, DataRecord]
+public sealed class PulledAccessLog
+{
+    public readonly TimeSpan Time;
+    public readonly string Accessor;
+
+    public PulledAccessLog(TimeSpan time, string accessor)
+    {
+        Time = time;
+        Accessor = accessor;
+    }
+}
index b4e82184297159de9795dbc1b16a2f2b2e4ccca2..cfb8809c690c76e6462448daf2b0bb8cf984dcbb 100644 (file)
@@ -22,6 +22,7 @@ namespace Content.Shared.GameTicking
         // Probably most useful for replays, round end info, and probably things like lobby menus.
         [ViewVariables]
         public int RoundId { get; protected set; }
+        [ViewVariables] public TimeSpan RoundStartTimeSpan { get; protected set; }
 
         public override void Initialize()
         {
@@ -188,4 +189,3 @@ namespace Content.Shared.GameTicking
         JoinedGame,
     }
 }
-
diff --git a/Resources/Locale/en-US/access/systems/access-reader-system.ftl b/Resources/Locale/en-US/access/systems/access-reader-system.ftl
new file mode 100644 (file)
index 0000000..d66989f
--- /dev/null
@@ -0,0 +1 @@
+access-reader-unknown-id = Unknown
index f324da7be61cf2571d5e49eb7a7d4d163f8c36bd..d154a16a8445e3264513f4f3d28a18a1d819454f 100644 (file)
@@ -11,3 +11,9 @@ net-probe-label-name = Name
 net-probe-label-address = Address
 net-probe-label-frequency = Frequency
 net-probe-label-network = Network
+
+log-probe-program-name = LogProbe
+log-probe-scan = Downloaded logs from {$device}!
+log-probe-label-time = Time
+log-probe-label-accessor = Accessed by
+log-probe-label-number = Number
index fe8e9230b24c41485385b241dec8fdf82cb6e33d..93f613ecbd2668e98841744bda6907929c6204b3 100644 (file)
@@ -46,7 +46,7 @@ guide-entry-machine-upgrading = Machine Upgrading
 guide-entry-robotics = Robotics
 guide-entry-cyborgs = Cyborgs
 guide-entry-security = Security
-guide-entry-dna = DNA
+guide-entry-forensics = Forensics
 guide-entry-defusal = Large Bomb Defusal
 
 guide-entry-antagonists = Antagonists
index e8346db494404dd950ebdcc10181238ce65335af..a8272a873c85c92284f15dfcc1052cc7bb16e395 100644 (file)
       - id: ClothingOuterCoatDetective
       - id: FlashlightSeclite
       - id: ForensicScanner
+      - id: LogProbeCartridge
       - id: BoxForensicPad
       - id: DrinkDetFlask
       - id: ClothingHandsGlovesForensic
index dc1c4fcf48d11f181388e15e80fcbbcf9fc8fa92..1607bbfbb1640da1a193f6745d90a7deef174c69 100644 (file)
   - type: FingerprintMask
   - type: GuideHelp
     guides:
-    - DNA
+    - Forensics
 
 # TODO Make lubed items not slip in hands
 - type: entity
index 739e0f394f57ad228ec3d3fe4c8a1f554844c11c..7e494c24671ec8ff5e84e1af3bf2adaae017c980 100644 (file)
         state: server
     - type: NetProbeCartridge
 
-
+- type: entity
+  parent: BaseItem
+  id: LogProbeCartridge
+  name: LogProbe cartridge
+  description: A program for getting access logs from devices
+  components:
+    - type: Sprite
+      sprite: Objects/Devices/cartridge.rsi
+      state: cart-log
+    - type: Icon
+      sprite: Objects/Devices/cartridge.rsi
+      state: cart-log
+    - type: UIFragment
+      ui: !type:LogProbeUi
+    - type: Cartridge
+      programName: log-probe-program-name
+      icon:
+        sprite: Structures/Doors/Airlocks/Standard/security.rsi
+        state: closed
+    - type: LogProbeCartridge
+      guides:
+        - Forensics
index ebfba2f918f305573d8f439362ca571fb344bde1..7436ae7c988e9f3dfe563cf3d3f25b7f00c1223d 100644 (file)
@@ -25,7 +25,7 @@
   - type: ForensicScanner
   - type: GuideHelp
     guides:
-    - DNA
+      - Forensics
   - type: StealTarget
     stealGroup: ForensicScanner
 
@@ -55,4 +55,4 @@
     maxWritableArea: 368.0, 256.0
   - type: GuideHelp
     guides:
-    - DNA
+    - Forensics
index 628ec7ee972a046fb1f8def8043155a1741a2e73..2e81ea4b58ab84fa683f6344168d433ae8d274f8 100644 (file)
@@ -17,4 +17,4 @@
     - Document
   - type: GuideHelp
     guides:
-    - DNA
+    - Forensics
index 51bf12f8ef54b1907f275e1f1cef10de30829007..d4d8fd54494d71ee275f8ee469ff8946041eb24f 100644 (file)
     board: StationRecordsComputerCircuitboard
   - type: GuideHelp
     guides:
-    - DNA
+    - Forensics
 
 - type: entity
   parent: BaseComputer
index 75fad71051aa6cfdfe993c4e46f3b05e501e67eb..8e734b4d137a0dfe6dc39f8f8080d5376ea00a32 100644 (file)
@@ -3,13 +3,13 @@
   name: guide-entry-security
   text: "/ServerInfo/Guidebook/Security/Security.xml"
   children:
-    - DNA
+    - Forensics
     - Defusal
 
 - type: guideEntry
-  id: DNA
-  name: guide-entry-dna
-  text: "/ServerInfo/Guidebook/Security/DNA.xml"
+  id: Forensics
+  name: guide-entry-forensics
+  text: "/ServerInfo/Guidebook/Security/Forensics.xml"
 
 - type: guideEntry
   id: Defusal
similarity index 59%
rename from Resources/ServerInfo/Guidebook/Security/DNA.xml
rename to Resources/ServerInfo/Guidebook/Security/Forensics.xml
index fa0a62c9615d4d400c3028947fadbf7754c89d37..2189488c6b2e4557768defd5a774629f6e489e75 100644 (file)
@@ -1,4 +1,21 @@
 <Document>
+  # Forensics
+
+  There are a lot of tools to help you gather and examine the evidence at your disposal
+
+  # Log probe
+
+  This little add-on to your PDA is incredibly useful, just install the cartridge and your PDA will acquire the ability to scan anything with access (like airlocks) and see who has used them recently.
+
+  You can normally find it inside the detective locker. After inserting it on your PDA, go to the programs tab and the log probe application should be there, to use the application you just have to interact with anything that requires access with your PDA while the application is open and the information will be instantly displayed in it.
+
+  It should be noted that the name shown in the application is not to be trusted 100% of the time since it gets the name from the identification card of whoever used the thing we are scanning, so if for example someone opened an airlock with no card the application would display "Unknown" as the name.
+
+  <Box>
+    <GuideEntityEmbed Entity="LockerDetective"/>
+    <GuideEntityEmbed Entity="LogProbeCartridge"/>
+  </Box>
+
   # DNA and Fingerprints
 
   ## How to get someone’s DNA?
     <GuideEntityEmbed Entity="ForensicScanner"/>
   </Box>
   So be careful before fighting with someone.
-  
+
   Same scanner can also get fingerprint information about who touched pretty much any object. If the possible perpetrator was using gloves, then your scanner will print out which fibers were left on the crimescene.
 
   ## I got DNA. How do I recognize whose it is?
 
   You can print the forensic information of the object you scanned so you never miss it. Now with the paper containing DNA you can simply find a [color=#a4885c]Station Records Computer[/color] and look for a person whose DNA matches. Same applies to finding whose fingerprint is is.
-  
+
   ## Taking Fingerprints
   It is also possible to take someones fingerprints while on scene if you make them take off their gloves and appy a forensic pad to their fingers. No need to run back to [color=#a4885c]Station Records Computer[/color] check if the butler did it!
-  
+
   <GuideEntityEmbed Entity="ForensicPad"/>
-  
+
   ## Fibers
   Whenenever people wearing gloves touch anything on the station, they are bound to leave behind some fibers. This complicates things, but nothing is unsolvable for a real detective.
-  
+
   There are up to [color=red]16[/color] different types of fibers possible. Can that stop you from solving the case?
-  
+
   <Box>
     <GuideEntityEmbed Entity="ComputerStationRecords"/>
   </Box>
diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/cart-log.png b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-log.png
new file mode 100644 (file)
index 0000000..cedafb3
Binary files /dev/null and b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-log.png differ
index 431381c4a9201670f502abb2e40f3fe0c6e0f54d..c7eb96d964ede885c38573ede669477c4d32c299 100644 (file)
@@ -1,7 +1,7 @@
 {
   "version": 1,
   "license": "CC-BY-SA-3.0",
-  "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d",
+  "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1cdfb0230cc96d0ba751fa002d04f8aa2f25ad7d , cart-log made by Skarletto (github)",
   "size": {
     "x": 32,
     "y": 32
@@ -72,6 +72,9 @@
     },
     {
       "name": "cart-y"
+    },
+    {
+      "name": "cart-log"
     }
   ]
 }