diff --git a/Content.Client/CriminalRecords/CrimeHistoryWindow.xaml b/Content.Client/CriminalRecords/CrimeHistoryWindow.xaml
new file mode 100644
index 00000000000000..358fade2e3c31a
--- /dev/null
+++ b/Content.Client/CriminalRecords/CrimeHistoryWindow.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CriminalRecords/CrimeHistoryWindow.xaml.cs b/Content.Client/CriminalRecords/CrimeHistoryWindow.xaml.cs
new file mode 100644
index 00000000000000..bccf501d9c5539
--- /dev/null
+++ b/Content.Client/CriminalRecords/CrimeHistoryWindow.xaml.cs
@@ -0,0 +1,105 @@
+using Content.Shared.Administration;
+using Content.Shared.CriminalRecords;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.CriminalRecords;
+
+///
+/// Window opened when Crime History button is pressed
+///
+[GenerateTypedNameReferences]
+public sealed partial class CrimeHistoryWindow : FancyWindow
+{
+ public Action? OnAddHistory;
+ public Action? OnDeleteHistory;
+
+ private uint? _index;
+ private DialogWindow? _dialog;
+
+ public CrimeHistoryWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ OnClose += () =>
+ {
+ _dialog?.Close();
+ // deselect so when reopening the window it doesnt try to use invalid index
+ _index = null;
+ };
+
+ AddButton.OnPressed += _ =>
+ {
+ if (_dialog != null)
+ {
+ _dialog.MoveToFront();
+ return;
+ }
+
+ var field = "line";
+ var prompt = Loc.GetString("criminal-records-console-reason");
+ var placeholder = Loc.GetString("criminal-records-history-placeholder");
+ var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
+ var entries = new List { entry };
+ _dialog = new DialogWindow(Title!, entries);
+
+ _dialog.OnConfirmed += responses =>
+ {
+ var line = responses[field];
+ // TODO: whenever the console is moved to shared unhardcode this
+ if (line.Length < 1 || line.Length > 256)
+ return;
+
+ OnAddHistory?.Invoke(line);
+ // adding deselects so prevent deleting yeah
+ _index = null;
+ DeleteButton.Disabled = true;
+ };
+
+ // prevent MoveToFront being called on a closed window and double closing
+ _dialog.OnClose += () => { _dialog = null; };
+ };
+ DeleteButton.OnPressed += _ =>
+ {
+ if (_index is not {} index)
+ return;
+
+ OnDeleteHistory?.Invoke(index);
+ // prevent total spam wiping
+ History.ClearSelected();
+ _index = null;
+ DeleteButton.Disabled = true;
+ };
+
+ History.OnItemSelected += args =>
+ {
+ _index = (uint) args.ItemIndex;
+ DeleteButton.Disabled = false;
+ };
+ History.OnItemDeselected += args =>
+ {
+ _index = null;
+ DeleteButton.Disabled = true;
+ };
+ }
+
+ public void UpdateHistory(CriminalRecord record, bool access)
+ {
+ History.Clear();
+ Editing.Visible = access;
+
+ NoHistory.Visible = record.History.Count == 0;
+
+ foreach (var entry in record.History)
+ {
+ var time = entry.AddTime;
+ var line = $"{time.Hours:00}:{time.Minutes:00}:{time.Seconds:00} - {entry.Crime}";
+ History.AddItem(line);
+ }
+
+ // deselect if something goes wrong
+ if (_index is {} index && record.History.Count >= index)
+ _index = null;
+ }
+}
diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleBoundUserInterface.cs b/Content.Client/CriminalRecords/CriminalRecordsConsoleBoundUserInterface.cs
new file mode 100644
index 00000000000000..f6c9080a2dbc1f
--- /dev/null
+++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleBoundUserInterface.cs
@@ -0,0 +1,78 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.CriminalRecords;
+using Content.Shared.Security;
+using Content.Shared.StationRecords;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Client.CriminalRecords;
+
+public sealed class CriminalRecordsConsoleBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ private readonly AccessReaderSystem _accessReader;
+
+ private CriminalRecordsConsoleWindow? _window;
+ private CrimeHistoryWindow? _historyWindow;
+
+ public CriminalRecordsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ _accessReader = EntMan.System();
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new(Owner, _playerManager, _proto, _random, _accessReader);
+ _window.OnKeySelected += key =>
+ SendMessage(new SelectStationRecord(key));
+ _window.OnFiltersChanged += (type, filterValue) =>
+ SendMessage(new SetStationRecordFilter(type, filterValue));
+ _window.OnStatusSelected += status =>
+ SendMessage(new CriminalRecordChangeStatus(status, null));
+ _window.OnDialogConfirmed += (_, reason) =>
+ SendMessage(new CriminalRecordChangeStatus(SecurityStatus.Wanted, reason));
+ _window.OnHistoryUpdated += UpdateHistory;
+ _window.OnHistoryClosed += () => _historyWindow?.Close();
+ _window.OnClose += Close;
+
+ _historyWindow = new();
+ _historyWindow.OnAddHistory += line => SendMessage(new CriminalRecordAddHistory(line));
+ _historyWindow.OnDeleteHistory += index => SendMessage(new CriminalRecordDeleteHistory(index));
+
+ _historyWindow.Close(); // leave closed until user opens it
+ }
+
+ ///
+ /// Updates or opens a new history window.
+ ///
+ private void UpdateHistory(CriminalRecord record, bool access, bool open)
+ {
+ _historyWindow!.UpdateHistory(record, access);
+
+ if (open)
+ _historyWindow.OpenCentered();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not CriminalRecordsConsoleState cast)
+ return;
+
+ _window?.UpdateState(cast);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ _window?.Close();
+ _historyWindow?.Close();
+ }
+}
diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml
new file mode 100644
index 00000000000000..77da0ba1b06523
--- /dev/null
+++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
new file mode 100644
index 00000000000000..f5c631ea0bca53
--- /dev/null
+++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
@@ -0,0 +1,263 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access.Systems;
+using Content.Shared.Administration;
+using Content.Shared.CriminalRecords;
+using Content.Shared.Dataset;
+using Content.Shared.Security;
+using Content.Shared.StationRecords;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Client.CriminalRecords;
+
+// TODO: dedupe shitcode from general records theres a lot
+[GenerateTypedNameReferences]
+public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
+{
+ private readonly IPlayerManager _player;
+ private readonly IPrototypeManager _proto;
+ private readonly IRobustRandom _random;
+ private readonly AccessReaderSystem _accessReader;
+
+ public readonly EntityUid Console;
+
+ [ValidatePrototypeId]
+ private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
+
+ public Action? OnKeySelected;
+ public Action? OnFiltersChanged;
+ public Action? OnStatusSelected;
+ public Action? OnHistoryUpdated;
+ public Action? OnHistoryClosed;
+ public Action? OnDialogConfirmed;
+
+ private bool _isPopulating;
+ private bool _access;
+ private uint? _selectedKey;
+ private CriminalRecord? _selectedRecord;
+
+ private DialogWindow? _reasonDialog;
+
+ private StationRecordFilterType _currentFilterType;
+
+ public CriminalRecordsConsoleWindow(EntityUid console, IPlayerManager playerManager, IPrototypeManager prototypeManager, IRobustRandom robustRandom, AccessReaderSystem accessReader)
+ {
+ RobustXamlLoader.Load(this);
+
+ Console = console;
+ _player = playerManager;
+ _proto = prototypeManager;
+ _random = robustRandom;
+ _accessReader = accessReader;
+
+ _currentFilterType = StationRecordFilterType.Name;
+
+ OpenCentered();
+
+ foreach (var item in Enum.GetValues())
+ {
+ FilterType.AddItem(GetTypeFilterLocals(item), (int)item);
+ }
+
+ foreach (var status in Enum.GetValues())
+ {
+ AddStatusSelect(status);
+ }
+
+ OnClose += () => _reasonDialog?.Close();
+
+ RecordListing.OnItemSelected += args =>
+ {
+ if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not uint cast)
+ return;
+
+ OnKeySelected?.Invoke(cast);
+ };
+
+ RecordListing.OnItemDeselected += _ =>
+ {
+ if (!_isPopulating)
+ OnKeySelected?.Invoke(null);
+ };
+
+ FilterType.OnItemSelected += eventArgs =>
+ {
+ var type = (StationRecordFilterType)eventArgs.Id;
+
+ if (_currentFilterType != type)
+ {
+ _currentFilterType = type;
+ FilterListingOfRecords(FilterText.Text);
+ }
+ };
+
+ FilterText.OnTextEntered += args =>
+ {
+ FilterListingOfRecords(args.Text);
+ };
+
+ StatusOptionButton.OnItemSelected += args =>
+ {
+ SetStatus((SecurityStatus) args.Id);
+ };
+
+ HistoryButton.OnPressed += _ =>
+ {
+ if (_selectedRecord is {} record)
+ OnHistoryUpdated?.Invoke(record, _access, true);
+ };
+ }
+
+ public void UpdateState(CriminalRecordsConsoleState state)
+ {
+ if (state.Filter != null)
+ {
+ if (state.Filter.Type != _currentFilterType)
+ {
+ _currentFilterType = state.Filter.Type;
+ }
+
+ if (state.Filter.Value != FilterText.Text)
+ {
+ FilterText.Text = state.Filter.Value;
+ }
+ }
+
+ _selectedKey = state.SelectedKey;
+
+ FilterType.SelectId((int)_currentFilterType);
+
+ // set up the records listing panel
+ RecordListing.Clear();
+
+ var hasRecords = state.RecordListing != null && state.RecordListing.Count > 0;
+ NoRecords.Visible = !hasRecords;
+ if (hasRecords)
+ PopulateRecordListing(state.RecordListing!);
+
+ // set up the selected person's record
+ var selected = _selectedKey != null;
+
+ PersonContainer.Visible = selected;
+ RecordUnselected.Visible = !selected;
+
+ _access = _player.LocalSession?.AttachedEntity is {} player
+ && _accessReader.IsAllowed(player, Console);
+
+ // hide access-required editing parts when no access
+ var editing = _access && selected;
+ StatusOptionButton.Disabled = !editing;
+
+ if (state is { CriminalRecord: not null, StationRecord: not null })
+ {
+ PopulateRecordContainer(state.StationRecord, state.CriminalRecord);
+ OnHistoryUpdated?.Invoke(state.CriminalRecord, _access, false);
+ _selectedRecord = state.CriminalRecord;
+ }
+ else
+ {
+ _selectedRecord = null;
+ OnHistoryClosed?.Invoke();
+ }
+ }
+
+ private void PopulateRecordListing(Dictionary listing)
+ {
+ _isPopulating = true;
+
+ foreach (var (key, name) in listing)
+ {
+ var item = RecordListing.AddItem(name);
+ item.Metadata = key;
+ item.Selected = key == _selectedKey;
+ }
+ _isPopulating = false;
+
+ RecordListing.SortItemsByText();
+ }
+
+ private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
+ {
+ var na = Loc.GetString("generic-not-available-shorthand");
+ PersonName.Text = stationRecord.Name;
+ PersonPrints.Text = Loc.GetString("general-station-record-console-record-fingerprint", ("fingerprint", stationRecord.Fingerprint ?? na));
+ PersonDna.Text = Loc.GetString("general-station-record-console-record-dna", ("dna", stationRecord.DNA ?? na));
+
+ StatusOptionButton.SelectId((int) criminalRecord.Status);
+ if (criminalRecord.Reason is {} reason)
+ {
+ var message = FormattedMessage.FromMarkup(Loc.GetString("criminal-records-console-wanted-reason"));
+ message.AddText($": {reason}");
+ WantedReason.SetMessage(message);
+ WantedReason.Visible = true;
+ }
+ else
+ {
+ WantedReason.Visible = false;
+ }
+ }
+
+ private void AddStatusSelect(SecurityStatus status)
+ {
+ var name = Loc.GetString($"criminal-records-status-{status.ToString().ToLower()}");
+ StatusOptionButton.AddItem(name, (int)status);
+ }
+
+ private void FilterListingOfRecords(string text = "")
+ {
+ if (!_isPopulating)
+ {
+ OnFiltersChanged?.Invoke(_currentFilterType, text);
+ }
+ }
+
+ private void SetStatus(SecurityStatus status)
+ {
+ if (status == SecurityStatus.Wanted)
+ {
+ GetWantedReason();
+ return;
+ }
+
+ OnStatusSelected?.Invoke(status);
+ }
+
+ private void GetWantedReason()
+ {
+ if (_reasonDialog != null)
+ {
+ _reasonDialog.MoveToFront();
+ return;
+ }
+
+ var field = "reason";
+ var title = Loc.GetString("criminal-records-status-wanted");
+ var placeholders = _proto.Index(ReasonPlaceholders);
+ var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders.Values))); // just funny it doesn't actually get used
+ var prompt = Loc.GetString("criminal-records-console-reason");
+ var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
+ var entries = new List() { entry };
+ _reasonDialog = new DialogWindow(title, entries);
+
+ _reasonDialog.OnConfirmed += responses =>
+ {
+ var reason = responses[field];
+ // TODO: same as history unhardcode
+ if (reason.Length < 1 || reason.Length > 256)
+ return;
+
+ OnDialogConfirmed?.Invoke(SecurityStatus.Wanted, reason);
+ };
+
+ _reasonDialog.OnClose += () => { _reasonDialog = null; };
+ }
+
+ private string GetTypeFilterLocals(StationRecordFilterType type)
+ {
+ return Loc.GetString($"criminal-records-{type.ToString().ToLower()}-filter");
+ }
+}
diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs
index 841ea7e79e16fc..3be3d98778db82 100644
--- a/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs
+++ b/Content.Client/StationRecords/GeneralStationRecordConsoleBoundUserInterface.cs
@@ -1,5 +1,4 @@
using Content.Shared.StationRecords;
-using Robust.Client.GameObjects;
namespace Content.Client.StationRecords;
@@ -17,33 +16,21 @@ protected override void Open()
base.Open();
_window = new();
- _window.OnKeySelected += OnKeySelected;
- _window.OnFiltersChanged += OnFiltersChanged;
+ _window.OnKeySelected += key =>
+ SendMessage(new SelectStationRecord(key));
+ _window.OnFiltersChanged += (type, filterValue) =>
+ SendMessage(new SetStationRecordFilter(type, filterValue));
_window.OnClose += Close;
_window.OpenCentered();
}
- private void OnKeySelected((NetEntity, uint)? key)
- {
- SendMessage(new SelectGeneralStationRecord(key));
- }
-
- private void OnFiltersChanged(
- GeneralStationRecordFilterType type, string filterValue)
- {
- GeneralStationRecordsFilterMsg msg = new(type, filterValue);
- SendMessage(msg);
- }
-
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not GeneralStationRecordConsoleState cast)
- {
return;
- }
_window?.UpdateState(cast);
}
diff --git a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs
index c71b115c7a9ddd..fbdd6c2f0b526c 100644
--- a/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs
+++ b/Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
@@ -11,31 +10,29 @@ namespace Content.Client.StationRecords;
[GenerateTypedNameReferences]
public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
{
- public Action<(NetEntity, uint)?>? OnKeySelected;
+ public Action? OnKeySelected;
- public Action? OnFiltersChanged;
+ public Action? OnFiltersChanged;
private bool _isPopulating;
- private GeneralStationRecordFilterType _currentFilterType;
+ private StationRecordFilterType _currentFilterType;
public GeneralStationRecordConsoleWindow()
{
RobustXamlLoader.Load(this);
- _currentFilterType = GeneralStationRecordFilterType.Name;
+ _currentFilterType = StationRecordFilterType.Name;
- foreach (var item in Enum.GetValues())
+ foreach (var item in Enum.GetValues())
{
StationRecordsFilterType.AddItem(GetTypeFilterLocals(item), (int)item);
}
RecordListing.OnItemSelected += args =>
{
- if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not ValueTuple cast)
- {
+ if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not uint cast)
return;
- }
OnKeySelected?.Invoke(cast);
};
@@ -48,7 +45,7 @@ public GeneralStationRecordConsoleWindow()
StationRecordsFilterType.OnItemSelected += eventArgs =>
{
- var type = (GeneralStationRecordFilterType)eventArgs.Id;
+ var type = (StationRecordFilterType) eventArgs.Id;
if (_currentFilterType != type)
{
@@ -123,7 +120,7 @@ public void UpdateState(GeneralStationRecordConsoleState state)
RecordContainer.RemoveAllChildren();
}
}
- private void PopulateRecordListing(Dictionary<(NetEntity, uint), string> listing, (NetEntity, uint)? selected)
+ private void PopulateRecordListing(Dictionary listing, uint? selected)
{
RecordListing.Clear();
RecordListing.ClearSelected();
@@ -134,10 +131,7 @@ private void PopulateRecordListing(Dictionary<(NetEntity, uint), string> listing
{
var item = RecordListing.AddItem(name);
item.Metadata = key;
- if (selected != null && key.Item1 == selected.Value.Item1 && key.Item2 == selected.Value.Item2)
- {
- item.Selected = true;
- }
+ item.Selected = key == selected;
}
_isPopulating = false;
@@ -197,7 +191,7 @@ private void FilterListingOfRecords(string text = "")
}
}
- private string GetTypeFilterLocals(GeneralStationRecordFilterType type)
+ private string GetTypeFilterLocals(StationRecordFilterType type)
{
return Loc.GetString($"general-station-record-{type.ToString().ToLower()}-filter");
}
diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
index 10bf65d0c6102b..b3b2baf28e78c9 100644
--- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs
+++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
@@ -1,4 +1,3 @@
-using Content.Server.Station.Systems;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
@@ -21,7 +20,6 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly StationRecordsSystem _record = default!;
- [Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly AccessSystem _access = default!;
@@ -85,10 +83,9 @@ private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component
var targetAccessComponent = EntityManager.GetComponent(targetId);
var jobProto = string.Empty;
- if (_station.GetOwningStation(uid) is { } station
- && EntityManager.TryGetComponent(targetId, out var keyStorage)
- && keyStorage.Key != null
- && _record.TryGetRecord(station, keyStorage.Key.Value, out var record))
+ if (TryComp(targetId, out var keyStorage)
+ && keyStorage.Key is {} key
+ && _record.TryGetRecord(key, out var record))
{
jobProto = record.JobPrototype;
}
@@ -103,7 +100,7 @@ private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component
possibleAccess,
jobProto,
privilegedIdName,
- EntityManager.GetComponent(targetId).EntityName);
+ Name(targetId));
}
_userInterface.TrySetUiState(uid, IdCardConsoleUiKey.Key, newState);
@@ -184,7 +181,7 @@ private bool PrivilegedIdIsAuthorized(EntityUid uid, IdCardConsoleComponent? com
if (!Resolve(uid, ref component))
return true;
- if (!EntityManager.TryGetComponent(uid, out var reader))
+ if (!TryComp(uid, out var reader))
return true;
var privilegedId = component.PrivilegedIdSlot.Item;
@@ -193,10 +190,9 @@ private bool PrivilegedIdIsAuthorized(EntityUid uid, IdCardConsoleComponent? com
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
{
- if (_station.GetOwningStation(uid) is not { } station
- || !EntityManager.TryGetComponent(targetId, out var keyStorage)
+ if (!TryComp(targetId, out var keyStorage)
|| keyStorage.Key is not { } key
- || !_record.TryGetRecord(station, key, out var record))
+ || !_record.TryGetRecord(key, out var record))
{
return;
}
@@ -210,6 +206,6 @@ private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFu
record.JobIcon = newJobProto.Icon;
}
- _record.Synchronize(station);
+ _record.Synchronize(key);
}
}
diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs
index abaa99ece572d5..2d9e3393f3184f 100644
--- a/Content.Server/Administration/Systems/AdminSystem.cs
+++ b/Content.Server/Administration/Systems/AdminSystem.cs
@@ -349,7 +349,7 @@ public void Erase(ICommonSession player)
if (TryComp(item, out PdaComponent? pda) &&
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
keyStorage.Key is { } key &&
- _stationRecords.TryGetRecord(key.OriginStation, key, out GeneralStationRecord? record))
+ _stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
{
if (TryComp(entity, out DnaComponent? dna) &&
dna.DNA != record.DNA)
@@ -363,7 +363,7 @@ keyStorage.Key is { } key &&
continue;
}
- _stationRecords.RemoveRecord(key.OriginStation, key);
+ _stationRecords.RemoveRecord(key);
Del(item);
}
}
diff --git a/Content.Server/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs b/Content.Server/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs
new file mode 100644
index 00000000000000..de9ada8f8c79f0
--- /dev/null
+++ b/Content.Server/CriminalRecords/Components/CriminalRecordsConsoleComponent.cs
@@ -0,0 +1,45 @@
+using Content.Server.CriminalRecords.Systems;
+using Content.Shared.Radio;
+using Content.Shared.StationRecords;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.CriminalRecords.Components;
+
+///
+/// A component for Criminal Record Console storing an active station record key and a currently applied filter
+///
+[RegisterComponent]
+[Access(typeof(CriminalRecordsConsoleSystem))]
+public sealed partial class CriminalRecordsConsoleComponent : Component
+{
+ ///
+ /// Currently active station record key.
+ /// There is no station parameter as the console uses the current station.
+ ///
+ ///
+ /// TODO: in the future this should be clientside instead of something players can fight over.
+ /// Client selects a record and tells the server the key it wants records for.
+ /// Server then sends a state with just the records, not the listing or filter, and the client updates just that.
+ /// I don't know if it's possible to have multiple bui states right now.
+ ///
+ [DataField]
+ public uint? ActiveKey;
+
+ ///
+ /// Currently applied filter.
+ ///
+ [DataField]
+ public StationRecordsFilter? Filter;
+
+ ///
+ /// Channel to send messages to when someone's status gets changed.
+ ///
+ [DataField]
+ public ProtoId SecurityChannel = "Security";
+
+ ///
+ /// Max length of arrest and crime history strings.
+ ///
+ [DataField]
+ public uint MaxStringLength = 256;
+}
diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs
new file mode 100644
index 00000000000000..67ac1bf13c5d33
--- /dev/null
+++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs
@@ -0,0 +1,224 @@
+using Content.Server.CriminalRecords.Components;
+using Content.Server.Popups;
+using Content.Server.Radio.EntitySystems;
+using Content.Server.Station.Systems;
+using Content.Server.StationRecords;
+using Content.Server.StationRecords.Systems;
+using Content.Shared.Access.Systems;
+using Content.Shared.CriminalRecords;
+using Content.Shared.Security;
+using Content.Shared.StationRecords;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Server.CriminalRecords.Systems;
+
+public sealed class CriminalRecordsConsoleSystem : EntitySystem
+{
+ [Dependency] private readonly AccessReaderSystem _access = default!;
+ [Dependency] private readonly CriminalRecordsSystem _criminalRecords = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly RadioSystem _radio = default!;
+ [Dependency] private readonly SharedIdCardSystem _idCard = default!;
+ [Dependency] private readonly StationRecordsSystem _stationRecords = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(UpdateUserInterface);
+ SubscribeLocalEvent(UpdateUserInterface);
+
+ Subs.BuiEvents(CriminalRecordsConsoleKey.Key, subs =>
+ {
+ subs.Event(UpdateUserInterface);
+ subs.Event(OnKeySelected);
+ subs.Event(OnFiltersChanged);
+ subs.Event(OnChangeStatus);
+ subs.Event(OnAddHistory);
+ subs.Event(OnDeleteHistory);
+ });
+ }
+
+ private void UpdateUserInterface(Entity ent, ref T args)
+ {
+ // TODO: this is probably wasteful, maybe better to send a message to modify the exact state?
+ UpdateUserInterface(ent);
+ }
+
+ private void OnKeySelected(Entity ent, ref SelectStationRecord msg)
+ {
+ // no concern of sus client since record retrieval will fail if invalid id is given
+ ent.Comp.ActiveKey = msg.SelectedKey;
+ UpdateUserInterface(ent);
+ }
+
+ private void OnFiltersChanged(Entity ent, ref SetStationRecordFilter msg)
+ {
+ if (ent.Comp.Filter == null ||
+ ent.Comp.Filter.Type != msg.Type || ent.Comp.Filter.Value != msg.Value)
+ {
+ ent.Comp.Filter = new StationRecordsFilter(msg.Type, msg.Value);
+ UpdateUserInterface(ent);
+ }
+ }
+
+ private void OnChangeStatus(Entity ent, ref CriminalRecordChangeStatus msg)
+ {
+ // prevent malf client violating wanted/reason nullability
+ if ((msg.Status == SecurityStatus.Wanted) != (msg.Reason != null))
+ return;
+
+ if (!CheckSelected(ent, msg.Session, out var mob, out var key))
+ return;
+
+ if (!_stationRecords.TryGetRecord(key.Value, out var record) || record.Status == msg.Status)
+ return;
+
+ // validate the reason
+ string? reason = null;
+ if (msg.Reason != null)
+ {
+ reason = msg.Reason.Trim();
+ if (reason.Length < 1 || reason.Length > ent.Comp.MaxStringLength)
+ return;
+ }
+
+ // when arresting someone add it to history automatically
+ // fallback exists if the player was not set to wanted beforehand
+ if (msg.Status == SecurityStatus.Detained)
+ {
+ var oldReason = record.Reason ?? Loc.GetString("criminal-records-console-unspecified-reason");
+ var history = Loc.GetString("criminal-records-console-auto-history", ("reason", oldReason));
+ _criminalRecords.TryAddHistory(key.Value, history);
+ }
+
+ var oldStatus = record.Status;
+
+ // will probably never fail given the checks above
+ _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason);
+
+ var name = RecordName(key.Value);
+ var officer = Loc.GetString("criminal-records-console-unknown-officer");
+ if (_idCard.TryFindIdCard(mob.Value, out var id) && id.Comp.FullName is {} fullName)
+ officer = fullName;
+
+ // figure out which radio message to send depending on transition
+ var statusString = (oldStatus, msg.Status) switch
+ {
+ // going from wanted or detained on the spot
+ (_, SecurityStatus.Detained) => "detained",
+ // prisoner did their time
+ (SecurityStatus.Detained, SecurityStatus.None) => "released",
+ // going from wanted to none, must have been a mistake
+ (_, SecurityStatus.None) => "not-wanted",
+ // going from none or detained, AOS or prisonbreak / lazy secoff never set them to released and they reoffended
+ (_, SecurityStatus.Wanted) => "wanted",
+ // this is impossible
+ _ => "not-wanted"
+ };
+ var message = Loc.GetString($"criminal-records-console-{statusString}", ("name", name), ("officer", officer),
+ reason != null ? ("reason", reason) : default!);
+ _radio.SendRadioMessage(ent, message, ent.Comp.SecurityChannel, ent);
+
+ UpdateUserInterface(ent);
+ }
+
+ private void OnAddHistory(Entity ent, ref CriminalRecordAddHistory msg)
+ {
+ if (!CheckSelected(ent, msg.Session, out _, out var key))
+ return;
+
+ var line = msg.Line.Trim();
+ if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength)
+ return;
+
+ if (!_criminalRecords.TryAddHistory(key.Value, line))
+ return;
+
+ // no radio message since its not crucial to officers patrolling
+
+ UpdateUserInterface(ent);
+ }
+
+ private void OnDeleteHistory(Entity ent, ref CriminalRecordDeleteHistory msg)
+ {
+ if (!CheckSelected(ent, msg.Session, out _, out var key))
+ return;
+
+ if (!_criminalRecords.TryDeleteHistory(key.Value, msg.Index))
+ return;
+
+ // a bit sus but not crucial to officers patrolling
+
+ UpdateUserInterface(ent);
+ }
+
+ private void UpdateUserInterface(Entity ent)
+ {
+ var (uid, console) = ent;
+ var owningStation = _station.GetOwningStation(uid);
+
+ if (!TryComp(owningStation, out var stationRecords))
+ {
+ _ui.TrySetUiState(uid, CriminalRecordsConsoleKey.Key, new CriminalRecordsConsoleState());
+ return;
+ }
+
+ var listing = _stationRecords.BuildListing((owningStation.Value, stationRecords), console.Filter);
+
+ var state = new CriminalRecordsConsoleState(listing, console.Filter);
+ if (console.ActiveKey is {} id)
+ {
+ // get records to display when a crewmember is selected
+ var key = new StationRecordKey(id, owningStation.Value);
+ _stationRecords.TryGetRecord(key, out state.StationRecord, stationRecords);
+ _stationRecords.TryGetRecord(key, out state.CriminalRecord, stationRecords);
+ state.SelectedKey = id;
+ }
+
+ _ui.TrySetUiState(uid, CriminalRecordsConsoleKey.Key, state);
+ }
+
+ ///
+ /// Boilerplate that most actions use, if they require that a record be selected.
+ /// Obviously shouldn't be used for selecting records.
+ ///
+ private bool CheckSelected(Entity ent, ICommonSession session,
+ [NotNullWhen(true)] out EntityUid? mob, [NotNullWhen(true)] out StationRecordKey? key)
+ {
+ key = null;
+ mob = null;
+ if (session.AttachedEntity is not {} user)
+ return false;
+
+ if (!_access.IsAllowed(user, ent))
+ {
+ _popup.PopupEntity(Loc.GetString("criminal-records-permission-denied"), ent, session);
+ return false;
+ }
+
+ if (ent.Comp.ActiveKey is not {} id)
+ return false;
+
+ // checking the console's station since the user might be off-grid using on-grid console
+ if (_station.GetOwningStation(ent) is not {} station)
+ return false;
+
+ key = new StationRecordKey(id, station);
+ mob = user;
+ return true;
+ }
+
+ ///
+ /// Gets the name from a record, or empty string if this somehow fails.
+ ///
+ private string RecordName(StationRecordKey key)
+ {
+ if (!_stationRecords.TryGetRecord(key, out var record))
+ return "";
+
+ return record.Name;
+ }
+}
diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs
new file mode 100644
index 00000000000000..efec18485c223b
--- /dev/null
+++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsSystem.cs
@@ -0,0 +1,93 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.StationRecords.Systems;
+using Content.Shared.CriminalRecords;
+using Content.Shared.Security;
+using Content.Shared.StationRecords;
+using Robust.Shared.Timing;
+
+namespace Content.Server.CriminalRecords.Systems;
+
+///
+/// Criminal records
+///
+/// Criminal Records inherit Station Records' core and add role-playing tools for Security:
+/// - Ability to track a person's status (Detained/Wanted/None)
+/// - See security officers' actions in Criminal Records in the radio
+/// - See reasons for any action with no need to ask the officer personally
+///
+public sealed class CriminalRecordsSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly StationRecordsSystem _stationRecords = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGeneralRecordCreated);
+ }
+
+ private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
+ {
+ _stationRecords.AddRecordEntry(ev.Key, new CriminalRecord());
+ _stationRecords.Synchronize(ev.Key);
+ }
+
+ ///
+ /// Tries to change the status of the record found by the StationRecordKey.
+ /// Reason should only be passed if status is Wanted.
+ ///
+ /// True if the status is changed, false if not
+ public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason)
+ {
+ // don't do anything if its the same status
+ if (!_stationRecords.TryGetRecord(key, out var record)
+ || status == record.Status)
+ return false;
+
+ record.Status = status;
+ record.Reason = reason;
+
+ _stationRecords.Synchronize(key);
+
+ return true;
+ }
+
+ ///
+ /// Tries to add a history entry to a criminal record.
+ ///
+ /// True if adding succeeded, false if not
+ public bool TryAddHistory(StationRecordKey key, CrimeHistory entry)
+ {
+ if (!_stationRecords.TryGetRecord(key, out var record))
+ return false;
+
+ record.History.Add(entry);
+ return true;
+ }
+
+ ///
+ /// Creates and tries to add a history entry using the current time.
+ ///
+ public bool TryAddHistory(StationRecordKey key, string line)
+ {
+ var entry = new CrimeHistory(_timing.CurTime, line);
+ return TryAddHistory(key, entry);
+ }
+
+ ///
+ /// Tries to delete a sepcific line of history from a criminal record, by index.
+ ///
+ /// True if the line was removed, false if not
+ public bool TryDeleteHistory(StationRecordKey key, uint index)
+ {
+ if (!_stationRecords.TryGetRecord(key, out var record))
+ return false;
+
+ if (index >= record.History.Count)
+ return false;
+
+ record.History.RemoveAt((int) index);
+ return true;
+ }
+}
diff --git a/Content.Server/Mind/Commands/RenameCommand.cs b/Content.Server/Mind/Commands/RenameCommand.cs
index bb7d89ddf59c2c..834453fb198503 100644
--- a/Content.Server/Mind/Commands/RenameCommand.cs
+++ b/Content.Server/Mind/Commands/RenameCommand.cs
@@ -68,18 +68,14 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
// This is done here because ID cards are linked to station records
if (_entManager.TrySystem(out var recordsSystem)
&& _entManager.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
- && keyStorage.Key != null)
+ && keyStorage.Key is {} key)
{
- var origin = keyStorage.Key.Value.OriginStation;
-
- if (recordsSystem.TryGetRecord(origin,
- keyStorage.Key.Value,
- out var generalRecord))
+ if (recordsSystem.TryGetRecord(key, out var generalRecord))
{
generalRecord.Name = name;
}
- recordsSystem.Synchronize(origin);
+ recordsSystem.Synchronize(key);
}
}
}
diff --git a/Content.Server/StationEvents/Events/ClericalErrorRule.cs b/Content.Server/StationEvents/Events/ClericalErrorRule.cs
index c1b4cd9334329d..dd4473952cb4dd 100644
--- a/Content.Server/StationEvents/Events/ClericalErrorRule.cs
+++ b/Content.Server/StationEvents/Events/ClericalErrorRule.cs
@@ -29,15 +29,16 @@ protected override void Started(EntityUid uid, ClericalErrorRuleComponent compon
var min = (int) Math.Max(1, Math.Round(component.MinToRemove * recordCount));
var max = (int) Math.Max(min, Math.Round(component.MaxToRemove * recordCount));
var toRemove = RobustRandom.Next(min, max);
- var keys = new List();
+ var keys = new List();
for (var i = 0; i < toRemove; i++)
{
keys.Add(RobustRandom.Pick(stationRecords.Records.Keys));
}
- foreach (var key in keys)
+ foreach (var id in keys)
{
- _stationRecords.RemoveRecord(chosenStation.Value, key, stationRecords);
+ var key = new StationRecordKey(id, chosenStation.Value);
+ _stationRecords.RemoveRecord(key, stationRecords);
}
}
}
diff --git a/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs b/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs
index e5b7f7a260b4e8..9076bee436fe6e 100644
--- a/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs
+++ b/Content.Server/StationRecords/Components/GeneralStationRecordConsoleComponent.cs
@@ -1,10 +1,21 @@
+using Content.Server.StationRecords.Systems;
using Content.Shared.StationRecords;
-namespace Content.Server.StationRecords;
+namespace Content.Server.StationRecords.Components;
-[RegisterComponent]
+[RegisterComponent, Access(typeof(GeneralStationRecordConsoleSystem))]
public sealed partial class GeneralStationRecordConsoleComponent : Component
{
- public (NetEntity, uint)? ActiveKey { get; set; }
- public GeneralStationRecordsFilter? Filter { get; set; }
+ ///
+ /// Selected crewmember record id.
+ /// Station always uses the station that owns the console.
+ ///
+ [DataField]
+ public uint? ActiveKey;
+
+ ///
+ /// Qualities to filter a search by.
+ ///
+ [DataField]
+ public StationRecordsFilter? Filter;
}
diff --git a/Content.Server/StationRecords/StationRecordSet.cs b/Content.Server/StationRecords/StationRecordSet.cs
index 2f6b220a783215..b5a4501cea7d9c 100644
--- a/Content.Server/StationRecords/StationRecordSet.cs
+++ b/Content.Server/StationRecords/StationRecordSet.cs
@@ -6,9 +6,10 @@
namespace Content.Server.StationRecords;
///
-/// Set of station records. StationRecordsComponent stores these.
-/// Keyed by StationRecordKey, which should be obtained from
+/// Set of station records for a single station. StationRecordsComponent stores these.
+/// Keyed by the record id, which should be obtained from
/// an entity that stores a reference to it.
+/// A StationRecordKey has both the station entity (use to get the record set) and id (use for this).
///
[DataDefinition]
public sealed partial class StationRecordSet
@@ -16,22 +17,31 @@ public sealed partial class StationRecordSet
[DataField("currentRecordId")]
private uint _currentRecordId;
- // TODO add custom type serializer so that keys don't have to be written twice.
- [DataField("keys")]
- public HashSet Keys = new();
+ ///
+ /// Every key id that has a record(s) stored.
+ /// Presumably this is faster than iterating the dictionary to check if any tables have a key.
+ ///
+ [DataField]
+ public HashSet Keys = new();
- [DataField("recentlyAccessed")]
- private HashSet _recentlyAccessed = new();
+ ///
+ /// Recently accessed key ids which are used to synchronize them efficiently.
+ ///
+ [DataField]
+ private HashSet _recentlyAccessed = new();
- [DataField("tables")] // TODO ensure all of this data is serializable.
- private Dictionary> _tables = new();
+ ///
+ /// Dictionary between a record's type and then each record indexed by id.
+ ///
+ [DataField]
+ private Dictionary> _tables = new();
///
/// Gets all records of a specific type stored in the record set.
///
/// The type of record to fetch.
/// An enumerable object that contains a pair of both a station key, and the record associated with it.
- public IEnumerable<(StationRecordKey, T)> GetRecordsOfType()
+ public IEnumerable<(uint, T)> GetRecordsOfType()
{
if (!_tables.ContainsKey(typeof(T)))
{
@@ -52,43 +62,44 @@ public sealed partial class StationRecordSet
}
///
- /// Add an entry into a record.
+ /// Create a new record with an entry.
+ /// Returns an id that can only be used to access the record for this station.
///
/// Entry to add.
/// Type of the entry that's being added.
- public StationRecordKey AddRecordEntry(EntityUid station, T entry)
+ public uint? AddRecordEntry(T entry)
{
if (entry == null)
- return StationRecordKey.Invalid;
+ return null;
- var key = new StationRecordKey(_currentRecordId++, station);
+ var key = _currentRecordId++;
AddRecordEntry(key, entry);
return key;
}
///
- /// Add an entry into a record.
+ /// Add an entry into an existing record.
///
- /// Key for the record.
+ /// Key id for the record.
/// Entry to add.
/// Type of the entry that's being added.
- public void AddRecordEntry(StationRecordKey key, T entry)
+ public void AddRecordEntry(uint key, T entry)
{
if (entry == null)
return;
- if (Keys.Add(key))
- _tables.GetOrNew(typeof(T))[key] = entry;
+ Keys.Add(key);
+ _tables.GetOrNew(typeof(T))[key] = entry;
}
///
/// Try to get an record entry by type, from this record key.
///
- /// The StationRecordKey to get the entries from.
+ /// The record id to get the entries from.
/// The entry that is retrieved from the record set.
/// The type of entry to search for.
/// True if the record exists and was retrieved, false otherwise.
- public bool TryGetRecordEntry(StationRecordKey key, [NotNullWhen(true)] out T? entry)
+ public bool TryGetRecordEntry(uint key, [NotNullWhen(true)] out T? entry)
{
entry = default;
@@ -108,10 +119,10 @@ public bool TryGetRecordEntry(StationRecordKey key, [NotNullWhen(true)] out T
///
/// Checks if the record associated with this key has an entry of a certain type.
///
- /// The record key.
+ /// The record key id.
/// Type to check.
/// True if the entry exists, false otherwise.
- public bool HasRecordEntry(StationRecordKey key)
+ public bool HasRecordEntry(uint key)
{
return Keys.Contains(key)
&& _tables.TryGetValue(typeof(T), out var table)
@@ -122,7 +133,7 @@ public bool HasRecordEntry(StationRecordKey key)
/// Get the recently accessed keys from this record set.
///
/// All recently accessed keys from this record set.
- public IEnumerable GetRecentlyAccessed()
+ public IEnumerable GetRecentlyAccessed()
{
return _recentlyAccessed.ToArray();
}
@@ -135,17 +146,23 @@ public void ClearRecentlyAccessed()
_recentlyAccessed.Clear();
}
+ ///
+ /// Removes a recently accessed key from the set.
+ ///
+ public void RemoveFromRecentlyAccessed(uint key)
+ {
+ _recentlyAccessed.Remove(key);
+ }
+
///
/// Removes all record entries related to this key from this set.
///
/// The key to remove.
/// True if successful, false otherwise.
- public bool RemoveAllRecords(StationRecordKey key)
+ public bool RemoveAllRecords(uint key)
{
if (!Keys.Remove(key))
- {
return false;
- }
foreach (var table in _tables.Values)
{
diff --git a/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs b/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
index f69caaa9a7e357..721eff6f2cfd12 100644
--- a/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
+++ b/Content.Server/StationRecords/Systems/GeneralStationRecordConsoleSystem.cs
@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Station.Systems;
+using Content.Server.StationRecords.Components;
using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
@@ -7,126 +8,78 @@ namespace Content.Server.StationRecords.Systems;
public sealed class GeneralStationRecordConsoleSystem : EntitySystem
{
- [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
- [Dependency] private readonly StationSystem _stationSystem = default!;
- [Dependency] private readonly StationRecordsSystem _stationRecordsSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly StationRecordsSystem _stationRecords = default!;
public override void Initialize()
{
- SubscribeLocalEvent(UpdateUserInterface);
- SubscribeLocalEvent(OnKeySelected);
- SubscribeLocalEvent(OnFiltersChanged);
SubscribeLocalEvent(UpdateUserInterface);
SubscribeLocalEvent(UpdateUserInterface);
SubscribeLocalEvent(UpdateUserInterface);
+
+ Subs.BuiEvents(GeneralStationRecordConsoleKey.Key, subs =>
+ {
+ subs.Event(UpdateUserInterface);
+ subs.Event(OnKeySelected);
+ subs.Event(OnFiltersChanged);
+ });
}
- private void UpdateUserInterface(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
+ private void UpdateUserInterface(Entity ent, ref T args)
{
- UpdateUserInterface(uid, component);
+ UpdateUserInterface(ent);
}
- private void OnKeySelected(EntityUid uid, GeneralStationRecordConsoleComponent component,
- SelectGeneralStationRecord msg)
+ // TODO: instead of copy paste shitcode for each record console, have a shared records console comp they all use
+ // then have this somehow play nicely with creating ui state
+ // if that gets done put it in StationRecordsSystem console helpers section :)
+ private void OnKeySelected(Entity ent, ref SelectStationRecord msg)
{
- component.ActiveKey = msg.SelectedKey;
- UpdateUserInterface(uid, component);
+ ent.Comp.ActiveKey = msg.SelectedKey;
+ UpdateUserInterface(ent);
}
- private void OnFiltersChanged(EntityUid uid,
- GeneralStationRecordConsoleComponent component, GeneralStationRecordsFilterMsg msg)
+ private void OnFiltersChanged(Entity ent, ref SetStationRecordFilter msg)
{
- if (component.Filter == null ||
- component.Filter.Type != msg.Type || component.Filter.Value != msg.Value)
+ if (ent.Comp.Filter == null ||
+ ent.Comp.Filter.Type != msg.Type || ent.Comp.Filter.Value != msg.Value)
{
- component.Filter = new GeneralStationRecordsFilter(msg.Type, msg.Value);
- UpdateUserInterface(uid, component);
+ ent.Comp.Filter = new StationRecordsFilter(msg.Type, msg.Value);
+ UpdateUserInterface(ent);
}
}
- private void UpdateUserInterface(EntityUid uid,
- GeneralStationRecordConsoleComponent? console = null)
+ private void UpdateUserInterface(Entity ent)
{
- if (!Resolve(uid, ref console))
- {
- return;
- }
-
- var owningStation = _stationSystem.GetOwningStation(uid);
+ var (uid, console) = ent;
+ var owningStation = _station.GetOwningStation(uid);
- if (!TryComp(owningStation, out var stationRecordsComponent))
+ if (!TryComp(owningStation, out var stationRecords))
{
- GeneralStationRecordConsoleState state = new(null, null, null, null);
- SetStateForInterface(uid, state);
+ _ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState());
return;
}
- var consoleRecords =
- _stationRecordsSystem.GetRecordsOfType(owningStation.Value, stationRecordsComponent);
-
- var listing = new Dictionary<(NetEntity, uint), string>();
+ var listing = _stationRecords.BuildListing((owningStation.Value, stationRecords), console.Filter);
- foreach (var pair in consoleRecords)
+ switch (listing.Count)
{
- if (console.Filter != null && IsSkippedRecord(console.Filter, pair.Item2))
- {
- continue;
- }
-
- listing.Add(_stationRecordsSystem.Convert(pair.Item1), pair.Item2.Name);
+ case 0:
+ _ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState());
+ return;
+ case 1:
+ console.ActiveKey = listing.Keys.First();
+ break;
}
- if (listing.Count == 0)
- {
- GeneralStationRecordConsoleState state = new(null, null, null, console.Filter);
- SetStateForInterface(uid, state);
+ if (console.ActiveKey is not { } id)
return;
- }
- else if (listing.Count == 1)
- {
- console.ActiveKey = listing.Keys.First();
- }
- GeneralStationRecord? record = null;
- if (console.ActiveKey != null)
- {
- _stationRecordsSystem.TryGetRecord(owningStation.Value, _stationRecordsSystem.Convert(console.ActiveKey.Value), out record,
- stationRecordsComponent);
- }
+ var key = new StationRecordKey(id, owningStation.Value);
+ _stationRecords.TryGetRecord(key, out var record, stationRecords);
- GeneralStationRecordConsoleState newState = new(console.ActiveKey, record, listing, console.Filter);
- SetStateForInterface(uid, newState);
- }
-
- private void SetStateForInterface(EntityUid uid, GeneralStationRecordConsoleState newState)
- {
- _userInterface.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, newState);
- }
-
- private bool IsSkippedRecord(GeneralStationRecordsFilter filter,
- GeneralStationRecord someRecord)
- {
- bool isFilter = filter.Value.Length > 0;
- string filterLowerCaseValue = "";
-
- if (!isFilter)
- return false;
-
- filterLowerCaseValue = filter.Value.ToLower();
-
- return filter.Type switch
- {
- GeneralStationRecordFilterType.Name =>
- !someRecord.Name.ToLower().Contains(filterLowerCaseValue),
- GeneralStationRecordFilterType.Prints => someRecord.Fingerprint != null
- && IsFilterWithSomeCodeValue(someRecord.Fingerprint, filterLowerCaseValue),
- GeneralStationRecordFilterType.DNA => someRecord.DNA != null
- && IsFilterWithSomeCodeValue(someRecord.DNA, filterLowerCaseValue),
- };
- }
-
- private bool IsFilterWithSomeCodeValue(string value, string filter)
- {
- return !value.ToLower().StartsWith(filter);
+ GeneralStationRecordConsoleState newState = new(id, record, listing, console.Filter);
+ _ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, newState);
}
}
diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
index fd5094d5330a10..09a00e5967cd4c 100644
--- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
+++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
@@ -32,8 +32,8 @@ namespace Content.Server.StationRecords.Systems;
///
public sealed class StationRecordsSystem : SharedStationRecordsSystem
{
- [Dependency] private readonly InventorySystem _inventorySystem = default!;
- [Dependency] private readonly StationRecordKeyStorageSystem _keyStorageSystem = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly StationRecordKeyStorageSystem _keyStorage = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
@@ -45,26 +45,22 @@ public override void Initialize()
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
{
- if (!HasComp(args.Station))
+ if (!TryComp(args.Station, out var stationRecords))
return;
- CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId);
+ CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId, stationRecords);
}
private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile,
- string? jobId, StationRecordsComponent? records = null)
+ string? jobId, StationRecordsComponent records)
{
- if (!Resolve(station, ref records)
- || string.IsNullOrEmpty(jobId)
+ // TODO make PlayerSpawnCompleteEvent.JobId a ProtoId
+ if (string.IsNullOrEmpty(jobId)
|| !_prototypeManager.HasIndex(jobId))
- {
return;
- }
- if (!_inventorySystem.TryGetSlotEntity(player, "id", out var idUid))
- {
+ if (!_inventory.TryGetSlotEntity(player, "id", out var idUid))
return;
- }
TryComp(player, out var fingerprintComponent);
TryComp(player, out var dnaComponent);
@@ -100,17 +96,28 @@ private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCh
/// Optional - other systems should anticipate this.
///
/// Station records component.
- public void CreateGeneralRecord(EntityUid station, EntityUid? idUid, string name, int age, string species, Gender gender, string jobId, string? mobFingerprint, string? dna, HumanoidCharacterProfile? profile = null,
- StationRecordsComponent? records = null)
+ public void CreateGeneralRecord(
+ EntityUid station,
+ EntityUid? idUid,
+ string name,
+ int age,
+ string species,
+ Gender gender,
+ string jobId,
+ string? mobFingerprint,
+ string? dna,
+ HumanoidCharacterProfile profile,
+ StationRecordsComponent records)
{
- if (!Resolve(station, ref records))
- {
- return;
- }
+ if (!_prototypeManager.TryIndex(jobId, out var jobPrototype))
+ throw new ArgumentException($"Invalid job prototype ID: {jobId}");
- if (!_prototypeManager.TryIndex(jobId, out JobPrototype? jobPrototype))
+ // when adding a record that already exists use the old one
+ // this happens when respawning as the same character
+ if (GetRecordByName(station, name, records) is {} id)
{
- throw new ArgumentException($"Invalid job prototype ID: {jobId}");
+ SetIdKey(idUid, new StationRecordKey(id, station));
+ return;
}
var record = new GeneralStationRecord()
@@ -129,40 +136,47 @@ public void CreateGeneralRecord(EntityUid station, EntityUid? idUid, string name
var key = AddRecordEntry(station, record);
if (!key.IsValid())
+ {
+ Log.Warning($"Failed to add general record entry for {name}");
return;
+ }
+
+ SetIdKey(idUid, key);
+
+ RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(key, record, profile));
+ }
- if (idUid != null)
+ ///
+ /// Set the station records key for an id/pda.
+ ///
+ public void SetIdKey(EntityUid? uid, StationRecordKey key)
+ {
+ if (uid is not {} idUid)
+ return;
+
+ var keyStorageEntity = idUid;
+ if (TryComp(idUid, out var pda) && pda.ContainedId is {} id)
{
- var keyStorageEntity = idUid;
- if (TryComp(idUid, out PdaComponent? pdaComponent) && pdaComponent.ContainedId != null)
- {
- keyStorageEntity = pdaComponent.IdSlot.Item;
- }
-
- if (keyStorageEntity != null)
- {
- _keyStorageSystem.AssignKey(keyStorageEntity.Value, key);
- }
+ keyStorageEntity = id;
}
- RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(station, key, record, profile));
+ _keyStorage.AssignKey(keyStorageEntity, key);
}
///
/// Removes a record from this station.
///
- /// Station to remove the record from.
- /// The key to remove.
+ /// The station and key to remove.
/// Station records component.
/// True if the record was removed, false otherwise.
- public bool RemoveRecord(EntityUid station, StationRecordKey key, StationRecordsComponent? records = null)
+ public bool RemoveRecord(StationRecordKey key, StationRecordsComponent? records = null)
{
- if (!Resolve(station, ref records))
+ if (!Resolve(key.OriginStation, ref records))
return false;
- if (records.Records.RemoveAllRecords(key))
+ if (records.Records.RemoveAllRecords(key.Id))
{
- RaiseLocalEvent(new RecordRemovedEvent(station, key));
+ RaiseLocalEvent(new RecordRemovedEvent(key));
return true;
}
@@ -174,20 +188,39 @@ public bool RemoveRecord(EntityUid station, StationRecordKey key, StationRecords
/// from the provided station record key. Will always return
/// null if the key does not match the station.
///
- /// Station to get the record from.
- /// Key to try and index from the record set.
+ /// Station and key to try and index from the record set.
/// The resulting entry.
/// Station record component.
/// Type to get from the record set.
/// True if the record was obtained, false otherwise.
- public bool TryGetRecord(EntityUid station, StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
+ public bool TryGetRecord(StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
{
entry = default;
- if (!Resolve(station, ref records))
+ if (!Resolve(key.OriginStation, ref records))
return false;
- return records.Records.TryGetRecordEntry(key, out entry);
+ return records.Records.TryGetRecordEntry(key.Id, out entry);
+ }
+
+ ///
+ /// Returns an id if a record with the same name exists.
+ ///
+ ///
+ /// Linear search so O(n) time complexity.
+ ///
+ public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null)
+ {
+ if (!Resolve(station, ref records))
+ return null;
+
+ foreach (var (id, record) in GetRecordsOfType(station, records))
+ {
+ if (record.Name == name)
+ return id;
+ }
+
+ return null;
}
///
@@ -197,30 +230,47 @@ public bool TryGetRecord(EntityUid station, StationRecordKey key, [NotNullWhe
/// Station records component.
/// Type of record to fetch
/// Enumerable of pairs with a station record key, and the entry in question of type T.
- public IEnumerable<(StationRecordKey, T)> GetRecordsOfType(EntityUid station, StationRecordsComponent? records = null)
+ public IEnumerable<(uint, T)> GetRecordsOfType(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
- {
- return Array.Empty<(StationRecordKey, T)>();
- }
+ return Array.Empty<(uint, T)>();
return records.Records.GetRecordsOfType();
}
///
- /// Adds a record entry to a station's record set.
+ /// Adds a new record entry to a station's record set.
///
/// The station to add the record to.
/// The record to add.
/// Station records component.
/// The type of record to add.
- public StationRecordKey AddRecordEntry(EntityUid station, T record,
- StationRecordsComponent? records = null)
+ public StationRecordKey AddRecordEntry(EntityUid station, T record, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
return StationRecordKey.Invalid;
- return records.Records.AddRecordEntry(station, record);
+ var id = records.Records.AddRecordEntry(record);
+ if (id == null)
+ return StationRecordKey.Invalid;
+
+ return new StationRecordKey(id.Value, station);
+ }
+
+ ///
+ /// Adds a record to an existing entry.
+ ///
+ /// The station and id of the existing entry.
+ /// The record to add.
+ /// Station records component.
+ /// The type of record to add.
+ public void AddRecordEntry(StationRecordKey key, T record,
+ StationRecordsComponent? records = null)
+ {
+ if (!Resolve(key.OriginStation, ref records))
+ return;
+
+ records.Records.AddRecordEntry(key.Id, record);
}
///
@@ -231,17 +281,99 @@ public StationRecordKey AddRecordEntry(EntityUid station, T record,
public void Synchronize(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
- {
return;
- }
foreach (var key in records.Records.GetRecentlyAccessed())
{
- RaiseLocalEvent(new RecordModifiedEvent(station, key));
+ RaiseLocalEvent(new RecordModifiedEvent(new StationRecordKey(key, station)));
}
records.Records.ClearRecentlyAccessed();
}
+
+ ///
+ /// Synchronizes a single record's entries for a station.
+ ///
+ /// The station and id of the record
+ /// Station records component.
+ public void Synchronize(StationRecordKey key, StationRecordsComponent? records = null)
+ {
+ if (!Resolve(key.OriginStation, ref records))
+ return;
+
+ RaiseLocalEvent(new RecordModifiedEvent(key));
+
+ records.Records.RemoveFromRecentlyAccessed(key.Id);
+ }
+
+ #region Console system helpers
+
+ ///
+ /// Checks if a record should be skipped given a filter.
+ /// Takes general record since even if you are using this for e.g. criminal records,
+ /// you don't want to duplicate basic info like name and dna.
+ /// Station records lets you do this nicely with multiple types having their own data.
+ ///
+ public bool IsSkipped(StationRecordsFilter? filter, GeneralStationRecord someRecord)
+ {
+ // if nothing is being filtered, show everything
+ if (filter == null)
+ return false;
+ if (filter.Value.Length == 0)
+ return false;
+
+ var filterLowerCaseValue = filter.Value.ToLower();
+
+ return filter.Type switch
+ {
+ StationRecordFilterType.Name =>
+ !someRecord.Name.ToLower().Contains(filterLowerCaseValue),
+ StationRecordFilterType.Prints => someRecord.Fingerprint != null
+ && IsFilterWithSomeCodeValue(someRecord.Fingerprint, filterLowerCaseValue),
+ StationRecordFilterType.DNA => someRecord.DNA != null
+ && IsFilterWithSomeCodeValue(someRecord.DNA, filterLowerCaseValue),
+ };
+ }
+
+ private bool IsFilterWithSomeCodeValue(string value, string filter)
+ {
+ return !value.ToLower().StartsWith(filter);
+ }
+
+ ///
+ /// Build a record listing of id to name for a station and filter.
+ ///
+ public Dictionary BuildListing(Entity station, StationRecordsFilter? filter)
+ {
+ var listing = new Dictionary();
+
+ var records = GetRecordsOfType(station, station.Comp);
+ foreach (var pair in records)
+ {
+ if (IsSkipped(filter, pair.Item2))
+ continue;
+
+ listing.Add(pair.Item1, pair.Item2.Name);
+ }
+
+ return listing;
+ }
+
+ #endregion
+}
+
+///
+/// Base event for station record events
+///
+public abstract class StationRecordEvent : EntityEventArgs
+{
+ public readonly StationRecordKey Key;
+ public EntityUid Station => Key.OriginStation;
+
+ protected StationRecordEvent(StationRecordKey key)
+ {
+ Key = key;
+ }
}
///
@@ -250,23 +382,19 @@ public void Synchronize(EntityUid station, StationRecordsComponent? records = nu
/// listening to this event, as it contains the character's record key.
/// Also stores the general record reference, to save some time.
///
-public sealed class AfterGeneralRecordCreatedEvent : EntityEventArgs
+public sealed class AfterGeneralRecordCreatedEvent : StationRecordEvent
{
- public readonly EntityUid Station;
- public StationRecordKey Key { get; }
- public GeneralStationRecord Record { get; }
+ public readonly GeneralStationRecord Record;
///
/// Profile for the related player. This is so that other systems can get further information
/// about the player character.
/// Optional - other systems should anticipate this.
///
- public HumanoidCharacterProfile? Profile { get; }
+ public readonly HumanoidCharacterProfile Profile;
- public AfterGeneralRecordCreatedEvent(EntityUid station, StationRecordKey key, GeneralStationRecord record,
- HumanoidCharacterProfile? profile)
+ public AfterGeneralRecordCreatedEvent(StationRecordKey key, GeneralStationRecord record,
+ HumanoidCharacterProfile profile) : base(key)
{
- Station = station;
- Key = key;
Record = record;
Profile = profile;
}
@@ -278,15 +406,10 @@ public AfterGeneralRecordCreatedEvent(EntityUid station, StationRecordKey key, G
/// that store record keys can then remove the key from their internal
/// fields.
///
-public sealed class RecordRemovedEvent : EntityEventArgs
+public sealed class RecordRemovedEvent : StationRecordEvent
{
- public readonly EntityUid Station;
- public StationRecordKey Key { get; }
-
- public RecordRemovedEvent(EntityUid station, StationRecordKey key)
+ public RecordRemovedEvent(StationRecordKey key) : base(key)
{
- Station = station;
- Key = key;
}
}
@@ -295,14 +418,9 @@ public RecordRemovedEvent(EntityUid station, StationRecordKey key)
/// inform other systems that records stored in this key
/// may have changed.
///
-public sealed class RecordModifiedEvent : EntityEventArgs
+public sealed class RecordModifiedEvent : StationRecordEvent
{
- public readonly EntityUid Station;
- public StationRecordKey Key { get; }
-
- public RecordModifiedEvent(EntityUid station, StationRecordKey key)
+ public RecordModifiedEvent(StationRecordKey key) : base(key)
{
- Station = station;
- Key = key;
}
}
diff --git a/Content.Shared/Access/Components/AccessReaderComponent.cs b/Content.Shared/Access/Components/AccessReaderComponent.cs
index 3f6c9e1c052f1f..b157797922362a 100644
--- a/Content.Shared/Access/Components/AccessReaderComponent.cs
+++ b/Content.Shared/Access/Components/AccessReaderComponent.cs
@@ -34,7 +34,7 @@ public sealed partial class AccessReaderComponent : Component
public List> AccessLists = new();
///
- /// A list of s that grant access. Only a single matching key is required tp gaim
+ /// A list of s that grant access. Only a single matching key is required to gain
/// access.
///
[DataField]
diff --git a/Content.Shared/CriminalRecords/CriminalRecord.cs b/Content.Shared/CriminalRecords/CriminalRecord.cs
new file mode 100644
index 00000000000000..0fe23d4395419b
--- /dev/null
+++ b/Content.Shared/CriminalRecords/CriminalRecord.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Security;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CriminalRecords;
+
+///
+/// Criminal record for a crewmember.
+/// Can be viewed and edited in a criminal records console by security.
+///
+[Serializable, NetSerializable, DataRecord]
+public sealed record CriminalRecord
+{
+ ///
+ /// Status of the person (None, Wanted, Detained).
+ ///
+ [DataField]
+ public SecurityStatus Status = SecurityStatus.None;
+
+ ///
+ /// When Status is Wanted, the reason for it.
+ /// Should never be set otherwise.
+ ///
+ [DataField]
+ public string? Reason;
+
+ ///
+ /// Criminal history of the person.
+ /// This should have charges and time served added after someone is detained.
+ ///
+ [DataField]
+ public List History = new();
+}
+
+///
+/// A line of criminal activity and the time it was added at.
+///
+[Serializable, NetSerializable]
+public record struct CrimeHistory(TimeSpan AddTime, string Crime);
diff --git a/Content.Shared/CriminalRecords/CriminalRecordsUi.cs b/Content.Shared/CriminalRecords/CriminalRecordsUi.cs
new file mode 100644
index 00000000000000..287de36ac736d6
--- /dev/null
+++ b/Content.Shared/CriminalRecords/CriminalRecordsUi.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Security;
+using Content.Shared.StationRecords;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CriminalRecords;
+
+[Serializable, NetSerializable]
+public enum CriminalRecordsConsoleKey : byte
+{
+ Key
+}
+
+///
+/// Criminal records console state. There are a few states:
+/// - SelectedKey null, Record null, RecordListing null
+/// - The station record database could not be accessed.
+/// - SelectedKey null, Record null, RecordListing non-null
+/// - Records are populated in the database, or at least the station has
+/// the correct component.
+/// - SelectedKey non-null, Record null, RecordListing non-null
+/// - The selected key does not have a record tied to it.
+/// - SelectedKey non-null, Record non-null, RecordListing non-null
+/// - The selected key has a record tied to it, and the record has been sent.
+///
+/// - there is added new filters and so added new states
+/// -SelectedKey null, Record null, RecordListing null, filters non-null
+/// the station may have data, but they all did not pass through the filters
+///
+/// Other states are erroneous.
+///
+[Serializable, NetSerializable]
+public sealed class CriminalRecordsConsoleState : BoundUserInterfaceState
+{
+ ///
+ /// Currently selected crewmember record key.
+ ///
+ public uint? SelectedKey = null;
+
+ public CriminalRecord? CriminalRecord = null;
+ public GeneralStationRecord? StationRecord = null;
+ public readonly Dictionary? RecordListing;
+ public readonly StationRecordsFilter? Filter;
+
+ public CriminalRecordsConsoleState(Dictionary? recordListing, StationRecordsFilter? newFilter)
+ {
+ RecordListing = recordListing;
+ Filter = newFilter;
+ }
+
+ ///
+ /// Default state for opening the console
+ ///
+ public CriminalRecordsConsoleState() : this(null, null)
+ {
+ }
+
+ public bool IsEmpty() => SelectedKey == null && StationRecord == null && CriminalRecord == null && RecordListing == null;
+}
+
+///
+/// Used to change status, respecting the wanted/reason nullability rules in .
+///
+[Serializable, NetSerializable]
+public sealed class CriminalRecordChangeStatus : BoundUserInterfaceMessage
+{
+ public readonly SecurityStatus Status;
+ public readonly string? Reason;
+
+ public CriminalRecordChangeStatus(SecurityStatus status, string? reason)
+ {
+ Status = status;
+ Reason = reason;
+ }
+}
+
+///
+/// Used to add a single line to the record's crime history.
+///
+[Serializable, NetSerializable]
+public sealed class CriminalRecordAddHistory : BoundUserInterfaceMessage
+{
+ public readonly string Line;
+
+ public CriminalRecordAddHistory(string line)
+ {
+ Line = line;
+ }
+}
+
+///
+/// Used to delete a single line from the crime history, by index.
+///
+[Serializable, NetSerializable]
+public sealed class CriminalRecordDeleteHistory : BoundUserInterfaceMessage
+{
+ public readonly uint Index;
+
+ public CriminalRecordDeleteHistory(uint index)
+ {
+ Index = index;
+ }
+}
diff --git a/Content.Shared/Security/SecurityStatus.cs b/Content.Shared/Security/SecurityStatus.cs
new file mode 100644
index 00000000000000..95250a864598db
--- /dev/null
+++ b/Content.Shared/Security/SecurityStatus.cs
@@ -0,0 +1,15 @@
+namespace Content.Shared.Security;
+
+///
+/// Status used in Criminal Records.
+///
+/// None - the default value
+/// Wanted - the person is being wanted by security
+/// Detained - the person is detained by security
+///
+public enum SecurityStatus : byte
+{
+ None,
+ Wanted,
+ Detained
+}
diff --git a/Content.Shared/StationRecords/SharedGeneralStationRecordConsoleSystem.cs b/Content.Shared/StationRecords/GeneralRecordsUi.cs
similarity index 65%
rename from Content.Shared/StationRecords/SharedGeneralStationRecordConsoleSystem.cs
rename to Content.Shared/StationRecords/GeneralRecordsUi.cs
index 27288a7a1f95f2..860454efde51b5 100644
--- a/Content.Shared/StationRecords/SharedGeneralStationRecordConsoleSystem.cs
+++ b/Content.Shared/StationRecords/GeneralRecordsUi.cs
@@ -30,14 +30,16 @@ public enum GeneralStationRecordConsoleKey : byte
public sealed class GeneralStationRecordConsoleState : BoundUserInterfaceState
{
///
- /// Current selected key.
+ /// Current selected key.
+ /// Station is always the station that owns the console.
///
- public (NetEntity, uint)? SelectedKey { get; }
- public GeneralStationRecord? Record { get; }
- public Dictionary<(NetEntity, uint), string>? RecordListing { get; }
- public GeneralStationRecordsFilter? Filter { get; }
- public GeneralStationRecordConsoleState((NetEntity, uint)? key, GeneralStationRecord? record,
- Dictionary<(NetEntity, uint), string>? recordListing, GeneralStationRecordsFilter? newFilter)
+ public readonly uint? SelectedKey;
+ public readonly GeneralStationRecord? Record;
+ public readonly Dictionary? RecordListing;
+ public readonly StationRecordsFilter? Filter;
+
+ public GeneralStationRecordConsoleState(uint? key, GeneralStationRecord? record,
+ Dictionary? recordListing, StationRecordsFilter? newFilter)
{
SelectedKey = key;
Record = record;
@@ -45,16 +47,24 @@ public GeneralStationRecordConsoleState((NetEntity, uint)? key, GeneralStationRe
Filter = newFilter;
}
+ public GeneralStationRecordConsoleState() : this(null, null, null, null)
+ {
+ }
+
public bool IsEmpty() => SelectedKey == null
&& Record == null && RecordListing == null;
}
+///
+/// Select a specific crewmember's record, or deselect.
+/// Used by any kind of records console including general and criminal.
+///
[Serializable, NetSerializable]
-public sealed class SelectGeneralStationRecord : BoundUserInterfaceMessage
+public sealed class SelectStationRecord : BoundUserInterfaceMessage
{
- public (NetEntity, uint)? SelectedKey { get; }
+ public readonly uint? SelectedKey;
- public SelectGeneralStationRecord((NetEntity, uint)? selectedKey)
+ public SelectStationRecord(uint? selectedKey)
{
SelectedKey = selectedKey;
}
diff --git a/Content.Shared/StationRecords/GeneralStationRecord.cs b/Content.Shared/StationRecords/GeneralStationRecord.cs
index de4cda8f251ccd..2ca34a4ffbde61 100644
--- a/Content.Shared/StationRecords/GeneralStationRecord.cs
+++ b/Content.Shared/StationRecords/GeneralStationRecord.cs
@@ -7,46 +7,46 @@ namespace Content.Shared.StationRecords;
/// General station record. Indicates the crewmember's name and job.
///
[Serializable, NetSerializable]
-public sealed class GeneralStationRecord
+public sealed record GeneralStationRecord
{
///
/// Name tied to this station record.
///
- [ViewVariables]
+ [DataField]
public string Name = string.Empty;
///
/// Age of the person that this station record represents.
///
- [ViewVariables]
+ [DataField]
public int Age;
///
/// Job title tied to this station record.
///
- [ViewVariables]
+ [DataField]
public string JobTitle = string.Empty;
///
/// Job icon tied to this station record.
///
- [ViewVariables]
+ [DataField]
public string JobIcon = string.Empty;
- [ViewVariables]
+ [DataField]
public string JobPrototype = string.Empty;
///
/// Species tied to this station record.
///
- [ViewVariables]
+ [DataField]
public string Species = string.Empty;
///
/// Gender identity tied to this station record.
///
/// Sex should be placed in a medical record, not a general record.
- [ViewVariables]
+ [DataField]
public Gender Gender = Gender.Epicene;
///
@@ -54,18 +54,18 @@ public sealed class GeneralStationRecord
/// This is taken from the 'weight' of a job prototype,
/// usually.
///
- [ViewVariables]
+ [DataField]
public int DisplayPriority;
///
/// Fingerprint of the person.
///
- [ViewVariables]
+ [DataField]
public string? Fingerprint;
///
/// DNA of the person.
///
- [ViewVariables]
+ [DataField]
public string? DNA;
}
diff --git a/Content.Shared/StationRecords/GeneralStationRecordsFilter.cs b/Content.Shared/StationRecords/GeneralStationRecordsFilter.cs
deleted file mode 100644
index f0322420119d89..00000000000000
--- a/Content.Shared/StationRecords/GeneralStationRecordsFilter.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.StationRecords;
-
-[Serializable, NetSerializable]
-public sealed class GeneralStationRecordsFilter
-{
- public GeneralStationRecordFilterType Type { get; set; }
- = GeneralStationRecordFilterType.Name;
- public string Value { get; set; } = "";
- public GeneralStationRecordsFilter(GeneralStationRecordFilterType filterType, string newValue = "")
- {
- Type = filterType;
- Value = newValue;
- }
-}
-
-[Serializable, NetSerializable]
-public sealed class GeneralStationRecordsFilterMsg : BoundUserInterfaceMessage
-{
- public string Value { get; }
- public GeneralStationRecordFilterType Type { get; }
-
- public GeneralStationRecordsFilterMsg(GeneralStationRecordFilterType filterType,
- string filterValue)
- {
- Type = filterType;
- Value = filterValue;
- }
-}
-
-[Serializable, NetSerializable]
-public enum GeneralStationRecordFilterType : byte
-{
- Name,
- Prints,
- DNA,
-}
diff --git a/Content.Shared/StationRecords/StationRecordKey.cs b/Content.Shared/StationRecords/StationRecordKey.cs
index 937c3aa3ef1e4c..3693c0f57d9a78 100644
--- a/Content.Shared/StationRecords/StationRecordKey.cs
+++ b/Content.Shared/StationRecords/StationRecordKey.cs
@@ -1,10 +1,14 @@
namespace Content.Shared.StationRecords;
-// Station record keys. These should be stored somewhere,
-// preferably within an ID card.
+///
+/// Station record keys. These should be stored somewhere,
+/// preferably within an ID card.
+/// This refers to both the id and station. This is suitable for an access reader field etc,
+/// but when you already know the station just store the id itself.
+///
public readonly struct StationRecordKey : IEquatable
{
- [DataField("id")]
+ [DataField]
public readonly uint Id;
[DataField("station")]
diff --git a/Content.Shared/StationRecords/StationRecordsFilter.cs b/Content.Shared/StationRecords/StationRecordsFilter.cs
new file mode 100644
index 00000000000000..10b94dda9983d3
--- /dev/null
+++ b/Content.Shared/StationRecords/StationRecordsFilter.cs
@@ -0,0 +1,44 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.StationRecords;
+
+[Serializable, NetSerializable]
+public sealed class StationRecordsFilter
+{
+ public StationRecordFilterType Type = StationRecordFilterType.Name;
+ public string Value = "";
+
+ public StationRecordsFilter(StationRecordFilterType filterType, string newValue = "")
+ {
+ Type = filterType;
+ Value = newValue;
+ }
+}
+
+///
+/// Message for updating the filter on any kind of records console.
+///
+[Serializable, NetSerializable]
+public sealed class SetStationRecordFilter : BoundUserInterfaceMessage
+{
+ public readonly string Value;
+ public readonly StationRecordFilterType Type;
+
+ public SetStationRecordFilter(StationRecordFilterType filterType,
+ string filterValue)
+ {
+ Type = filterType;
+ Value = filterValue;
+ }
+}
+
+///
+/// Different strings that results can be filtered by.
+///
+[Serializable, NetSerializable]
+public enum StationRecordFilterType : byte
+{
+ Name,
+ Prints,
+ DNA,
+}
diff --git a/Resources/Locale/en-US/criminal-records/criminal-records.ftl b/Resources/Locale/en-US/criminal-records/criminal-records.ftl
new file mode 100644
index 00000000000000..49cd59914ac99b
--- /dev/null
+++ b/Resources/Locale/en-US/criminal-records/criminal-records.ftl
@@ -0,0 +1,44 @@
+criminal-records-console-window-title = Criminal Records Computer
+criminal-records-console-records-list-title = Crewmembers
+criminal-records-console-select-record-info = Select a record.
+criminal-records-console-no-records = No records found!
+criminal-records-console-no-record-found = No record was found for the selected person.
+
+## Status
+
+criminal-records-console-status = Status
+criminal-records-status-none = None
+criminal-records-status-wanted = Wanted
+criminal-records-status-detained = Detained
+
+criminal-records-console-wanted-reason = [color=gray]Wanted Reason[/color]
+criminal-records-console-reason = Reason
+criminal-records-console-reason-placeholder = For example: {$placeholder}
+
+## Crime History
+
+criminal-records-console-crime-history = Crime History
+criminal-records-history-placeholder = Write the crime here
+criminal-records-no-history = This crewmember's record is spotless.
+criminal-records-add-history = Add
+criminal-records-delete-history = Delete
+
+criminal-records-permission-denied = Permission denied
+
+## Security channel notifications
+
+criminal-records-console-wanted = {$name} is wanted by {$officer} for: {$reason}.
+criminal-records-console-detained = {$name} has been detained by {$officer}.
+criminal-records-console-released = {$name} has been released by {$officer}.
+criminal-records-console-not-wanted = {$name} is no longer wanted.
+
+## Filters
+
+criminal-records-filter-placeholder = Input text and press "Enter"
+criminal-records-name-filter = Name
+criminal-records-prints-filter = Fingerprints
+criminal-records-dna-filter = DNA
+
+## Arrest auto history lines
+criminal-records-console-auto-history = ARRESTED: {$reason}
+criminal-records-console-unspecified-reason =
diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl
index 93f613ecbd2668..5571e58e4fd8df 100644
--- a/Resources/Locale/en-US/guidebook/guides.ftl
+++ b/Resources/Locale/en-US/guidebook/guides.ftl
@@ -48,6 +48,7 @@ guide-entry-cyborgs = Cyborgs
guide-entry-security = Security
guide-entry-forensics = Forensics
guide-entry-defusal = Large Bomb Defusal
+guide-entry-criminal-records = Criminal Records
guide-entry-antagonists = Antagonists
guide-entry-nuclear-operatives = Nuclear Operatives
diff --git a/Resources/Locale/en-US/station-records/general-station-records.ftl b/Resources/Locale/en-US/station-records/general-station-records.ftl
index 89775a449e2911..3ee1f834fb3f5b 100644
--- a/Resources/Locale/en-US/station-records/general-station-records.ftl
+++ b/Resources/Locale/en-US/station-records/general-station-records.ftl
@@ -1,4 +1,4 @@
-general-station-record-console-window-title = Station Records Computer
+general-station-record-console-window-title = Station Records Computer
general-station-record-console-select-record-info = Select a record on the left.
general-station-record-console-empty-state = No records found!
general-station-record-console-no-record-found = No record was found for the selected person.
@@ -11,8 +11,5 @@ general-station-record-console-record-fingerprint = Fingerprint: {$fingerprint}
general-station-record-console-record-dna = DNA: {$dna}
general-station-record-for-filter-line-placeholder = Input text and press "Enter"
-general-station-record-name-filter = Name of person
-general-station-record-prints-filter = Fingerprints
-general-station-record-dna-filter = DNA
general-station-record-console-search-records = Search
-general-station-record-console-reset-filters = Reset
\ No newline at end of file
+general-station-record-console-reset-filters = Reset
diff --git a/Resources/Prototypes/Datasets/criminal_records.yml b/Resources/Prototypes/Datasets/criminal_records.yml
new file mode 100644
index 00000000000000..ee283091843678
--- /dev/null
+++ b/Resources/Prototypes/Datasets/criminal_records.yml
@@ -0,0 +1,18 @@
+# "funny" placeholders of extremely minor/non-crimes for wanted reason dialog
+- type: dataset
+ id: CriminalRecordsWantedReasonPlaceholders
+ values:
+ - Ate their own shoes
+ - Being a clown
+ - Being a mime
+ - Breathed the wrong way
+ - Broke into evac
+ - Did literally nothing
+ - Didn't say hello to me
+ - Drank one too many
+ - Lied on common radio
+ - Looked at me funny
+ - Slipped the HoS
+ - Stole the clown's mask
+ - Told an unfunny joke
+ - Wore a gasmask
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 5aa1680aaeacf3..c9cbb34d63e950 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -285,8 +285,15 @@
parent: BaseComputer
id: ComputerCriminalRecords
name: criminal records computer
- description: This can be used to check criminal records.
+ description: This can be used to check criminal records. Only security can modify them.
components:
+ - type: CriminalRecordsConsole
+ - type: UserInterface
+ interfaces:
+ - key: enum.CriminalRecordsConsoleKey.Key
+ type: CriminalRecordsConsoleBoundUserInterface
+ - type: ActivatableUI
+ key: enum.CriminalRecordsConsoleKey.Key
- type: Sprite
layers:
- map: ["computerLayerBody"]
@@ -303,6 +310,11 @@
color: "#1f8c28"
- type: Computer
board: CriminalRecordsComputerCircuitboard
+ - type: AccessReader
+ access: [["Security"]]
+ - type: GuideHelp
+ guides:
+ - CriminalRecords
- type: entity
parent: BaseComputer
diff --git a/Resources/Prototypes/Guidebook/security.yml b/Resources/Prototypes/Guidebook/security.yml
index 8e734b4d137a0d..f5e347082836ef 100644
--- a/Resources/Prototypes/Guidebook/security.yml
+++ b/Resources/Prototypes/Guidebook/security.yml
@@ -3,8 +3,9 @@
name: guide-entry-security
text: "/ServerInfo/Guidebook/Security/Security.xml"
children:
- - Forensics
- - Defusal
+ - Forensics
+ - Defusal
+ - CriminalRecords
- type: guideEntry
id: Forensics
@@ -15,3 +16,8 @@
id: Defusal
name: guide-entry-defusal
text: "/ServerInfo/Guidebook/Security/Defusal.xml"
+
+- type: guideEntry
+ id: CriminalRecords
+ name: guide-entry-criminal-records
+ text: "/ServerInfo/Guidebook/Security/CriminalRecords.xml"
diff --git a/Resources/ServerInfo/Guidebook/Security/CriminalRecords.xml b/Resources/ServerInfo/Guidebook/Security/CriminalRecords.xml
new file mode 100644
index 00000000000000..c7b7ad2098d20d
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/Security/CriminalRecords.xml
@@ -0,0 +1,39 @@
+
+ # Criminal Records
+ The criminal records console is accessible in every station's security department, it serves the purpose of tracking and managing the criminal history and status of anybody part of the crew manifest.
+
+
+
+
+
+ Anyone can open the console's UI, but only those with Security access can modify anything.
+
+ The UI is composed by the following elements:
+ - A search bar that has a filter next to it that lets you filter the crewmembers by their names, fingerprints or DNA.
+
+ - A list of all the crewmembers in the manifest, selecting one of the entries will make the criminal records of a crewmember appear. The list is filtered by the search bar so make sure it's empty if you want an overall overview!
+
+ - The criminal records themselves
+
+ In the record section you can:
+ - See security-related information about a crewmember like their name, fingerprints and DNA.
+
+ - Change the security status between [color=gray]None[/color], [color=yellow]Wanted[/color] and [color=red]Detained[/color]. When setting it to Wanted you will be asked to write a reason.
+
+ - If they are wanted, you can see the reason given below the status dropdown.
+
+ - Once someone has been arrested, update their status on the console so everyone knows they no longer need to be captured.
+
+ - After they've done their time, release them and update their status to None so nobody thinks they are an escaped convict.
+
+ - Open the Crime History window to check or modify it.
+
+ The Crime History window lists someone's crimes and can be modified in multiple ways:
+ - Automatically, just by setting someone's status to arrested. The reason will be added to "ARRESTED:" so it's easy to see the automated entries.
+
+ - Adding a new line by clicking "Add" and writing something in the input box. When adding a record, remember to mention their crime and sentence, the console will automatically insert the shift's time so you don't need to!
+
+ - Select a line of unwanted history and click "Delete" to remove it. Excellent for keeping records clean from the clown's stolen ID antics.
+
+ Now you can be the desk jockey you've always wanted to be.
+