diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
new file mode 100644
index 0000000000..d28d3228c9
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
@@ -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
index 0000000000..5712b301c3
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml.cs
new file mode 100644
index 0000000000..369042d991
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiEntry.xaml.cs
@@ -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
index 0000000000..d369a33c6c
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
new file mode 100644
index 0000000000..b22e0bc196
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
@@ -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 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
index 0000000000..cfa92dd67f
--- /dev/null
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
@@ -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
+{
+ ///
+ /// The list of pulled access logs
+ ///
+ [DataField, ViewVariables]
+ public List PulledAccessLogs = new();
+
+ ///
+ /// The sound to make when we scan something with access
+ ///
+ [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
index 0000000000..f5ccea9590
--- /dev/null
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
@@ -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(OnUiReady);
+ SubscribeLocalEvent(AfterInteract);
+ }
+
+ ///
+ /// The gets relayed to this system if the cartridge loader is running
+ /// the LogProbe program and someone clicks on something with it.
+ ///
+ /// Updates the program's list of logs with those from the device.
+ ///
+ private void AfterInteract(Entity 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);
+ }
+
+ ///
+ /// This gets called when the ui fragment needs to be updated for the first time after activating
+ ///
+ private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args)
+ {
+ UpdateUiState(ent, args.Loader);
+ }
+
+ private void UpdateUiState(Entity ent, EntityUid loaderUid)
+ {
+ var state = new LogProbeUiState(ent.Comp.PulledAccessLogs);
+ _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
+ }
+}
diff --git a/Content.Server/GameTicking/GameTicker.Lobby.cs b/Content.Server/GameTicking/GameTicker.Lobby.cs
index 1943a82617..292e09b6b2 100644
--- a/Content.Server/GameTicking/GameTicker.Lobby.cs
+++ b/Content.Server/GameTicking/GameTicker.Lobby.cs
@@ -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()
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index 42810779dd..ea8c980eb3 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -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()
diff --git a/Content.Shared/Access/Components/AccessComponent.cs b/Content.Shared/Access/Components/AccessComponent.cs
index 6930f2dfd6..2eacf2aa67 100644
--- a/Content.Shared/Access/Components/AccessComponent.cs
+++ b/Content.Shared/Access/Components/AccessComponent.cs
@@ -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;
+
+///
+/// Simple mutable access provider found on ID cards and such.
+///
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedAccessSystem))]
+[AutoGenerateComponentState]
+public sealed partial class AccessComponent : Component
{
///
- /// Simple mutable access provider found on ID cards and such.
+ /// True if the access provider is enabled and can grant access.
///
- [RegisterComponent, NetworkedComponent]
- [Access(typeof(SharedAccessSystem))]
- [AutoGenerateComponentState]
- public sealed partial class AccessComponent : Component
- {
- ///
- /// True if the access provider is enabled and can grant access.
- ///
- [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
- [AutoNetworkedField]
- public bool Enabled = true;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [AutoNetworkedField]
+ public bool Enabled = true;
- [DataField("tags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
- [Access(typeof(SharedAccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
- [AutoNetworkedField]
- public HashSet Tags = new();
-
- ///
- /// Access Groups. These are added to the tags during map init. After map init this will have no effect.
- ///
- [DataField("groups", readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
- [AutoNetworkedField]
- public HashSet Groups = new();
- }
+ [DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ [Access(typeof(SharedAccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
+ [AutoNetworkedField]
+ public HashSet Tags = new();
///
- /// 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.
///
- [ByRefEvent]
- public struct GetAdditionalAccessEvent
+ [DataField(readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ [AutoNetworkedField]
+ public HashSet Groups = new();
+}
+
+///
+/// Event raised on an entity to find additional entities which provide access.
+///
+[ByRefEvent]
+public struct GetAdditionalAccessEvent
+{
+ public HashSet Entities = new();
+
+ public GetAdditionalAccessEvent()
{
- public HashSet Entities = new();
-
- public GetAdditionalAccessEvent()
- {
- }
- }
-
- [ByRefEvent]
- public record struct GetAccessTagsEvent(HashSet Tags, IPrototypeManager PrototypeManager)
- {
- public void AddGroup(string group)
- {
- if (!PrototypeManager.TryIndex(group, out var groupPrototype))
- return;
-
- Tags.UnionWith(groupPrototype.Tags);
- }
+ }
+}
+
+[ByRefEvent]
+public record struct GetAccessTagsEvent(HashSet Tags, IPrototypeManager PrototypeManager)
+{
+ public void AddGroup(string group)
+ {
+ if (!PrototypeManager.TryIndex(group, out var groupPrototype))
+ return;
+
+ Tags.UnionWith(groupPrototype.Tags);
}
}
diff --git a/Content.Shared/Access/Components/AccessReaderComponent.cs b/Content.Shared/Access/Components/AccessReaderComponent.cs
index 796646c83c..815e6b4c65 100644
--- a/Content.Shared/Access/Components/AccessReaderComponent.cs
+++ b/Content.Shared/Access/Components/AccessReaderComponent.cs
@@ -6,8 +6,8 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
namespace Content.Shared.Access.Components;
///
-/// 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.
///
[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.
///
- [DataField("enabled")]
+ [DataField]
public bool Enabled = true;
///
- /// 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.
///
- [DataField("denyTags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer))]
public HashSet DenyTags = new();
///
/// 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.
///
- [DataField("access")]
+ [DataField("access")] [ViewVariables(VVAccess.ReadWrite)]
public List> AccessLists = new();
///
/// A list of s that grant access. Only a single matching key is required tp gaim
/// access.
///
- [DataField("accessKeys")]
+ [DataField]
public HashSet AccessKeys = new();
///
@@ -48,10 +49,25 @@ public sealed partial class AccessReaderComponent : Component
/// ignored, though is still respected. Access is denied if there are no valid entities or
/// they all deny access.
///
- [DataField("containerAccessProvider")]
+ [DataField]
public string? ContainerAccessProvider;
+
+ ///
+ /// A list of past authentications
+ ///
+ [DataField]
+ public Queue AccessLog = new();
+
+ ///
+ /// A limit on the max size of
+ ///
+ [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 denyTags, List> accessLists, List<(NetEntity, uint)> accessKeys)
+ public Queue AccessLog;
+
+ public int AccessLogLimit;
+
+ public AccessReaderComponentState(bool enabled, HashSet denyTags, List> accessLists, List<(NetEntity, uint)> accessKeys, Queue accessLog, int accessLogLimit)
{
Enabled = enabled;
DenyTags = denyTags;
AccessLists = accessLists;
AccessKeys = accessKeys;
+ AccessLog = accessLog;
+ AccessLogLimit = accessLogLimit;
}
}
diff --git a/Content.Shared/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs
index 7635716d26..26e83c5586 100644
--- a/Content.Shared/Access/Components/IdCardComponent.cs
+++ b/Content.Shared/Access/Components/IdCardComponent.cs
@@ -34,4 +34,10 @@ public sealed partial class IdCardComponent : Component
[DataField("jobDepartments")]
[AutoNetworkedField]
public List JobDepartments = new();
+
+ ///
+ /// Determines if accesses from this card should be logged by
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public bool BypassLogging;
}
diff --git a/Content.Shared/Access/Systems/AccessReaderSystem.cs b/Content.Shared/Access/Systems/AccessReaderSystem.cs
index 3c8e61d227..2735b7166b 100644
--- a/Content.Shared/Access/Systems/AccessReaderSystem.cs
+++ b/Content.Shared/Access/Systems/AccessReaderSystem.cs
@@ -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;
}
///
@@ -326,4 +340,27 @@ public sealed class AccessReaderSystem : EntitySystem
key = null;
return false;
}
+
+ ///
+ /// Logs an access
+ ///
+ /// The reader to log the access on
+ /// The accessor to log
+ private void LogAccess(Entity 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
index 0000000000..9dc507b7e5
--- /dev/null
+++ b/Content.Shared/CartridgeLoader/Cartridges/LogProbeUiState.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+[Serializable, NetSerializable]
+public sealed class LogProbeUiState : BoundUserInterfaceState
+{
+ ///
+ /// The list of probed network devices
+ ///
+ public List PulledLogs;
+
+ public LogProbeUiState(List 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;
+ }
+}
diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs
index b4e8218429..cfb8809c69 100644
--- a/Content.Shared/GameTicking/SharedGameTicker.cs
+++ b/Content.Shared/GameTicking/SharedGameTicker.cs
@@ -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
index 0000000000..d66989f6cf
--- /dev/null
+++ b/Resources/Locale/en-US/access/systems/access-reader-system.ftl
@@ -0,0 +1 @@
+access-reader-unknown-id = Unknown
diff --git a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl
index f324da7be6..d154a16a84 100644
--- a/Resources/Locale/en-US/cartridge-loader/cartridges.ftl
+++ b/Resources/Locale/en-US/cartridge-loader/cartridges.ftl
@@ -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
diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl
index fe8e9230b2..93f613ecbd 100644
--- a/Resources/Locale/en-US/guidebook/guides.ftl
+++ b/Resources/Locale/en-US/guidebook/guides.ftl
@@ -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
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
index e8346db494..a8272a873c 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
@@ -123,6 +123,7 @@
- id: ClothingOuterCoatDetective
- id: FlashlightSeclite
- id: ForensicScanner
+ - id: LogProbeCartridge
- id: BoxForensicPad
- id: DrinkDetFlask
- id: ClothingHandsGlovesForensic
diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
index dc1c4fcf48..1607bbfbb1 100644
--- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
+++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
@@ -390,7 +390,7 @@
- type: FingerprintMask
- type: GuideHelp
guides:
- - DNA
+ - Forensics
# TODO Make lubed items not slip in hands
- type: entity
diff --git a/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
index 739e0f394f..7e494c2467 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
@@ -70,4 +70,25 @@
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
diff --git a/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml b/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml
index ebfba2f918..7436ae7c98 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml
@@ -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
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml b/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
index 628ec7ee97..2e81ea4b58 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
@@ -17,4 +17,4 @@
- Document
- type: GuideHelp
guides:
- - DNA
+ - Forensics
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 51bf12f8ef..d4d8fd5449 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -321,7 +321,7 @@
board: StationRecordsComputerCircuitboard
- type: GuideHelp
guides:
- - DNA
+ - Forensics
- type: entity
parent: BaseComputer
diff --git a/Resources/Prototypes/Guidebook/security.yml b/Resources/Prototypes/Guidebook/security.yml
index 75fad71051..8e734b4d13 100644
--- a/Resources/Prototypes/Guidebook/security.yml
+++ b/Resources/Prototypes/Guidebook/security.yml
@@ -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
diff --git a/Resources/ServerInfo/Guidebook/Security/DNA.xml b/Resources/ServerInfo/Guidebook/Security/Forensics.xml
similarity index 59%
rename from Resources/ServerInfo/Guidebook/Security/DNA.xml
rename to Resources/ServerInfo/Guidebook/Security/Forensics.xml
index fa0a62c961..2189488c6b 100644
--- a/Resources/ServerInfo/Guidebook/Security/DNA.xml
+++ b/Resources/ServerInfo/Guidebook/Security/Forensics.xml
@@ -1,4 +1,21 @@
+ # 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.
+
+
+
+
+
+
# DNA and Fingerprints
## How to get someone’s DNA?
@@ -8,23 +25,23 @@
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!
-
+
-
+
## 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?
-
+
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
index 0000000000..cedafb3f8e
Binary files /dev/null and b/Resources/Textures/Objects/Devices/cartridge.rsi/cart-log.png differ
diff --git a/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json b/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json
index 431381c4a9..c7eb96d964 100644
--- a/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json
+++ b/Resources/Textures/Objects/Devices/cartridge.rsi/meta.json
@@ -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"
}
]
}