Add access logs (IC ones) (#17810)

This commit is contained in:
Chief-Engineer
2023-12-26 16:24:53 -06:00
committed by GitHub
parent 4d42d00194
commit 476ea14e8a
28 changed files with 438 additions and 81 deletions

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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");
}

View File

@@ -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);
}
}

View File

@@ -80,7 +80,7 @@ namespace Content.Server.GameTicking
private TickerLobbyStatusEvent GetStatusMsg(ICommonSession session) private TickerLobbyStatusEvent GetStatusMsg(ICommonSession session)
{ {
_playerGameStatuses.TryGetValue(session.UserId, out var status); _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() private void SendStatusToAll()

View File

@@ -40,9 +40,6 @@ namespace Content.Server.GameTicking
private int _roundStartFailCount = 0; private int _roundStartFailCount = 0;
#endif #endif
[ViewVariables]
private TimeSpan _roundStartTimeSpan;
[ViewVariables] [ViewVariables]
private bool _startingRound; private bool _startingRound;
@@ -247,7 +244,7 @@ namespace Content.Server.GameTicking
_roundStartDateTime = DateTime.UtcNow; _roundStartDateTime = DateTime.UtcNow;
RunLevel = GameRunLevel.InRound; RunLevel = GameRunLevel.InRound;
_roundStartTimeSpan = _gameTiming.CurTime; RoundStartTimeSpan = _gameTiming.CurTime;
SendStatusToAll(); SendStatusToAll();
ReqWindowAttentionAll(); ReqWindowAttentionAll();
UpdateLateJoinStatus(); UpdateLateJoinStatus();
@@ -595,7 +592,7 @@ namespace Content.Server.GameTicking
public TimeSpan RoundDuration() public TimeSpan RoundDuration()
{ {
return _gameTiming.CurTime.Subtract(_roundStartTimeSpan); return _gameTiming.CurTime.Subtract(RoundStartTimeSpan);
} }
private void AnnounceRound() private void AnnounceRound()

View File

@@ -3,24 +3,24 @@ using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; 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.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedAccessSystem))]
[AutoGenerateComponentState]
public sealed partial class AccessComponent : Component
{
/// <summary> /// <summary>
/// True if the access provider is enabled and can grant access. /// True if the access provider is enabled and can grant access.
/// </summary> /// </summary>
[DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] [DataField, ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField] [AutoNetworkedField]
public bool Enabled = true; public bool Enabled = true;
[DataField("tags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))] [DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))]
[Access(typeof(SharedAccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends [Access(typeof(SharedAccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
[AutoNetworkedField] [AutoNetworkedField]
public HashSet<string> Tags = new(); public HashSet<string> Tags = new();
@@ -28,27 +28,27 @@ namespace Content.Shared.Access.Components
/// <summary> /// <summary>
/// Access Groups. These are added to the tags during map init. After map init this will have no effect. /// Access Groups. These are added to the tags during map init. After map init this will have no effect.
/// </summary> /// </summary>
[DataField("groups", readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessGroupPrototype>))] [DataField(readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessGroupPrototype>))]
[AutoNetworkedField] [AutoNetworkedField]
public HashSet<string> Groups = new(); public HashSet<string> Groups = new();
} }
/// <summary> /// <summary>
/// Event raised on an entity to find additional entities which provide access. /// Event raised on an entity to find additional entities which provide access.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public struct GetAdditionalAccessEvent public struct GetAdditionalAccessEvent
{ {
public HashSet<EntityUid> Entities = new(); public HashSet<EntityUid> Entities = new();
public GetAdditionalAccessEvent() public GetAdditionalAccessEvent()
{ {
} }
} }
[ByRefEvent] [ByRefEvent]
public record struct GetAccessTagsEvent(HashSet<string> Tags, IPrototypeManager PrototypeManager) 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)) if (!PrototypeManager.TryIndex<AccessGroupPrototype>(group, out var groupPrototype))
@@ -56,5 +56,4 @@ namespace Content.Shared.Access.Components
Tags.UnionWith(groupPrototype.Tags); Tags.UnionWith(groupPrototype.Tags);
} }
}
} }

View File

@@ -16,27 +16,28 @@ public sealed partial class AccessReaderComponent : Component
/// Whether or not the accessreader is enabled. /// Whether or not the accessreader is enabled.
/// If not, it will always let people through. /// If not, it will always let people through.
/// </summary> /// </summary>
[DataField("enabled")] [DataField]
public bool Enabled = true; public bool Enabled = true;
/// <summary> /// <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> /// </summary>
[DataField("denyTags", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))] [ViewVariables(VVAccess.ReadWrite)]
[DataField(customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessLevelPrototype>))]
public HashSet<string> DenyTags = new(); public HashSet<string> DenyTags = new();
/// <summary> /// <summary>
/// List of access groups that grant access to this reader. Only a single matching group is required to gain access. /// 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. /// A group matches if it is a subset of the set being checked against.
/// </summary> /// </summary>
[DataField("access")] [DataField("access")] [ViewVariables(VVAccess.ReadWrite)]
public List<HashSet<string>> AccessLists = new(); public List<HashSet<string>> AccessLists = new();
/// <summary> /// <summary>
/// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required tp gaim /// A list of <see cref="StationRecordKey"/>s that grant access. Only a single matching key is required tp gaim
/// access. /// access.
/// </summary> /// </summary>
[DataField("accessKeys")] [DataField]
public HashSet<StationRecordKey> AccessKeys = new(); public HashSet<StationRecordKey> AccessKeys = new();
/// <summary> /// <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 /// ignored, though <see cref="Enabled"/> is still respected. Access is denied if there are no valid entities or
/// they all deny access. /// they all deny access.
/// </remarks> /// </remarks>
[DataField("containerAccessProvider")] [DataField]
public string? ContainerAccessProvider; 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] [Serializable, NetSerializable]
public sealed class AccessReaderComponentState : ComponentState public sealed class AccessReaderComponentState : ComponentState
{ {
@@ -63,11 +79,17 @@ public sealed class AccessReaderComponentState : ComponentState
public List<(NetEntity, uint)> AccessKeys; 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; Enabled = enabled;
DenyTags = denyTags; DenyTags = denyTags;
AccessLists = accessLists; AccessLists = accessLists;
AccessKeys = accessKeys; AccessKeys = accessKeys;
AccessLog = accessLog;
AccessLogLimit = accessLogLimit;
} }
} }

View File

@@ -34,4 +34,10 @@ public sealed partial class IdCardComponent : Component
[DataField("jobDepartments")] [DataField("jobDepartments")]
[AutoNetworkedField] [AutoNetworkedField]
public List<LocId> JobDepartments = new(); 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;
} }

View File

@@ -10,8 +10,10 @@ using Robust.Shared.Containers;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Content.Shared.GameTicking;
using Robust.Shared.Collections; using Robust.Shared.Collections;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Access.Systems; namespace Content.Shared.Access.Systems;
@@ -19,7 +21,10 @@ public sealed class AccessReaderSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly InventorySystem _inventorySystem = 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 SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedIdCardSystem _idCardSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedStationRecordsSystem _records = 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) private void OnGetState(EntityUid uid, AccessReaderComponent component, ref ComponentGetState args)
{ {
args.State = new AccessReaderComponentState(component.Enabled, component.DenyTags, component.AccessLists, 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) 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.AccessLists = new(state.AccessLists);
component.DenyTags = new(state.DenyTags); component.DenyTags = new(state.DenyTags);
component.AccessLog = new(state.AccessLog);
component.AccessLogLimit = state.AccessLogLimit;
} }
private void OnLinkAttempt(EntityUid uid, AccessReaderComponent component, LinkAttemptEvent args) private void OnLinkAttempt(EntityUid uid, AccessReaderComponent component, LinkAttemptEvent args)
@@ -71,6 +78,7 @@ public sealed class AccessReaderSystem : EntitySystem
{ {
args.Handled = true; args.Handled = true;
reader.Enabled = false; reader.Enabled = false;
reader.AccessLog.Clear();
Dirty(uid, reader); Dirty(uid, reader);
} }
@@ -93,7 +101,13 @@ public sealed class AccessReaderSystem : EntitySystem
var access = FindAccessTags(user, accessSources); var access = FindAccessTags(user, accessSources);
FindStationRecordKeys(user, out var stationKeys, 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> /// <summary>
@@ -326,4 +340,27 @@ public sealed class AccessReaderSystem : EntitySystem
key = null; key = null;
return false; 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));
}
} }

View File

@@ -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;
}
}

View File

@@ -22,6 +22,7 @@ namespace Content.Shared.GameTicking
// Probably most useful for replays, round end info, and probably things like lobby menus. // Probably most useful for replays, round end info, and probably things like lobby menus.
[ViewVariables] [ViewVariables]
public int RoundId { get; protected set; } public int RoundId { get; protected set; }
[ViewVariables] public TimeSpan RoundStartTimeSpan { get; protected set; }
public override void Initialize() public override void Initialize()
{ {
@@ -188,4 +189,3 @@ namespace Content.Shared.GameTicking
JoinedGame, JoinedGame,
} }
} }

View File

@@ -0,0 +1 @@
access-reader-unknown-id = Unknown

View File

@@ -11,3 +11,9 @@ net-probe-label-name = Name
net-probe-label-address = Address net-probe-label-address = Address
net-probe-label-frequency = Frequency net-probe-label-frequency = Frequency
net-probe-label-network = Network 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

View File

@@ -46,7 +46,7 @@ guide-entry-machine-upgrading = Machine Upgrading
guide-entry-robotics = Robotics guide-entry-robotics = Robotics
guide-entry-cyborgs = Cyborgs guide-entry-cyborgs = Cyborgs
guide-entry-security = Security guide-entry-security = Security
guide-entry-dna = DNA guide-entry-forensics = Forensics
guide-entry-defusal = Large Bomb Defusal guide-entry-defusal = Large Bomb Defusal
guide-entry-antagonists = Antagonists guide-entry-antagonists = Antagonists

View File

@@ -123,6 +123,7 @@
- id: ClothingOuterCoatDetective - id: ClothingOuterCoatDetective
- id: FlashlightSeclite - id: FlashlightSeclite
- id: ForensicScanner - id: ForensicScanner
- id: LogProbeCartridge
- id: BoxForensicPad - id: BoxForensicPad
- id: DrinkDetFlask - id: DrinkDetFlask
- id: ClothingHandsGlovesForensic - id: ClothingHandsGlovesForensic

View File

@@ -390,7 +390,7 @@
- type: FingerprintMask - type: FingerprintMask
- type: GuideHelp - type: GuideHelp
guides: guides:
- DNA - Forensics
# TODO Make lubed items not slip in hands # TODO Make lubed items not slip in hands
- type: entity - type: entity

View File

@@ -70,4 +70,25 @@
state: server state: server
- type: NetProbeCartridge - 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

View File

@@ -25,7 +25,7 @@
- type: ForensicScanner - type: ForensicScanner
- type: GuideHelp - type: GuideHelp
guides: guides:
- DNA - Forensics
- type: StealTarget - type: StealTarget
stealGroup: ForensicScanner stealGroup: ForensicScanner
@@ -55,4 +55,4 @@
maxWritableArea: 368.0, 256.0 maxWritableArea: 368.0, 256.0
- type: GuideHelp - type: GuideHelp
guides: guides:
- DNA - Forensics

View File

@@ -17,4 +17,4 @@
- Document - Document
- type: GuideHelp - type: GuideHelp
guides: guides:
- DNA - Forensics

View File

@@ -321,7 +321,7 @@
board: StationRecordsComputerCircuitboard board: StationRecordsComputerCircuitboard
- type: GuideHelp - type: GuideHelp
guides: guides:
- DNA - Forensics
- type: entity - type: entity
parent: BaseComputer parent: BaseComputer

View File

@@ -3,13 +3,13 @@
name: guide-entry-security name: guide-entry-security
text: "/ServerInfo/Guidebook/Security/Security.xml" text: "/ServerInfo/Guidebook/Security/Security.xml"
children: children:
- DNA - Forensics
- Defusal - Defusal
- type: guideEntry - type: guideEntry
id: DNA id: Forensics
name: guide-entry-dna name: guide-entry-forensics
text: "/ServerInfo/Guidebook/Security/DNA.xml" text: "/ServerInfo/Guidebook/Security/Forensics.xml"
- type: guideEntry - type: guideEntry
id: Defusal id: Defusal

View File

@@ -1,4 +1,21 @@
<Document> <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 # DNA and Fingerprints
## How to get someones DNA? ## How to get someones DNA?

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"license": "CC-BY-SA-3.0", "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": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
@@ -72,6 +72,9 @@
}, },
{ {
"name": "cart-y" "name": "cart-y"
},
{
"name": "cart-log"
} }
] ]
} }