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.CriminalRecords.Components; using Content.Shared.CriminalRecords.Systems; using Content.Shared.Security; using Content.Shared.StationRecords; using Robust.Server.GameObjects; using System.Diagnostics.CodeAnalysis; using Content.Shared.IdentityManagement; using Content.Shared.Security.Components; using System.Linq; using Content.Shared.Roles.Jobs; namespace Content.Server.CriminalRecords.Systems; /// /// Handles all UI for criminal records console /// public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem { [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 StationRecordsSystem _records = 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); subs.Event(OnStatusFilterPressed); }); } 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 OnStatusFilterPressed(Entity ent, ref CriminalRecordSetStatusFilter msg) { ent.Comp.FilterStatus = msg.FilterStatus; 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 GetOfficer(EntityUid uid, out string officer) { var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, uid); RaiseLocalEvent(tryGetIdentityShortInfoEvent); officer = tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("criminal-records-console-unknown-officer"); } private void OnChangeStatus(Entity ent, ref CriminalRecordChangeStatus msg) { // prevent malf client violating wanted/reason nullability if (msg.Status == SecurityStatus.Wanted != (msg.Reason != null) && msg.Status == SecurityStatus.Suspected != (msg.Reason != null)) return; if (!CheckSelected(ent, msg.Actor, out var mob, out var key)) return; if (!_records.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; } var oldStatus = record.Status; var name = _records.RecordName(key.Value); GetOfficer(mob.Value, out var officer); // 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, officer); } // will probably never fail given the checks above name = _records.RecordName(key.Value); officer = Loc.GetString("criminal-records-console-unknown-officer"); var jobName = "Unknown"; _records.TryGetRecord(key.Value, out var entry); if (entry != null) jobName = entry.JobTitle; var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value); RaiseLocalEvent(tryGetIdentityShortInfoEvent); if (tryGetIdentityShortInfoEvent.Title != null) officer = tryGetIdentityShortInfoEvent.Title; _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer); (string, object)[] args; if (reason != null) args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason), ("job", jobName) }; else args = new (string, object)[] { ("name", name), ("officer", officer), ("job", jobName) }; // figure out which radio message to send depending on transition var statusString = (oldStatus, msg.Status) switch { // person has been detained (_, SecurityStatus.Detained) => "detained", // person did something sus (_, SecurityStatus.Suspected) => "suspected", // released on parole (_, SecurityStatus.Paroled) => "paroled", // prisoner did their time (_, SecurityStatus.Discharged) => "released", // going from any other state to wanted, AOS or prisonbreak / lazy secoff never set them to released and they reoffended (_, SecurityStatus.Wanted) => "wanted", // person is no longer sus (SecurityStatus.Suspected, SecurityStatus.None) => "not-suspected", // going from wanted to none, must have been a mistake (SecurityStatus.Wanted, SecurityStatus.None) => "not-wanted", // criminal status removed (SecurityStatus.Detained, SecurityStatus.None) => "released", // criminal is no longer on parole (SecurityStatus.Paroled, SecurityStatus.None) => "not-parole", // this is impossible _ => "not-wanted" }; _radio.SendRadioMessage(ent, Loc.GetString($"criminal-records-console-{statusString}", args), ent.Comp.SecurityChannel, ent); UpdateUserInterface(ent); } private void OnAddHistory(Entity ent, ref CriminalRecordAddHistory msg) { if (!CheckSelected(ent, msg.Actor, out var mob, out var key)) return; var line = msg.Line.Trim(); if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength) return; GetOfficer(mob.Value, out var officer); if (!_criminalRecords.TryAddHistory(key.Value, line, officer)) 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.Actor, 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.SetUiState(uid, CriminalRecordsConsoleKey.Key, new CriminalRecordsConsoleState()); return; } // get the listing of records to display var listing = _records.BuildListing((owningStation.Value, stationRecords), console.Filter); // filter the listing by the selected criminal record status //if NONE, dont filter by status, just show all crew if (console.FilterStatus != SecurityStatus.None) { listing = listing .Where(x => _records.TryGetRecord(new StationRecordKey(x.Key, owningStation.Value), out var record) && record.Status == console.FilterStatus) .ToDictionary(x => x.Key, x => x.Value); } 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); _records.TryGetRecord(key, out state.StationRecord, stationRecords); _records.TryGetRecord(key, out state.CriminalRecord, stationRecords); state.SelectedKey = id; } // Set the Current Tab aka the filter status type for the records list state.FilterStatus = console.FilterStatus; _ui.SetUiState(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, EntityUid user, [NotNullWhen(true)] out EntityUid? mob, [NotNullWhen(true)] out StationRecordKey? key) { key = null; mob = null; if (!_access.IsAllowed(user, ent)) { _popup.PopupEntity(Loc.GetString("criminal-records-permission-denied"), ent, user); 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; } }