CriminalRecordsConsoleSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. using Content.Server.Popups;
  2. using Content.Server.Radio.EntitySystems;
  3. using Content.Server.Station.Systems;
  4. using Content.Server.StationRecords;
  5. using Content.Server.StationRecords.Systems;
  6. using Content.Shared.Access.Systems;
  7. using Content.Shared.CriminalRecords;
  8. using Content.Shared.CriminalRecords.Components;
  9. using Content.Shared.CriminalRecords.Systems;
  10. using Content.Shared.Security;
  11. using Content.Shared.StationRecords;
  12. using Robust.Server.GameObjects;
  13. using System.Diagnostics.CodeAnalysis;
  14. using Content.Shared.IdentityManagement;
  15. using Content.Shared.Security.Components;
  16. using System.Linq;
  17. using Content.Shared.Roles.Jobs;
  18. namespace Content.Server.CriminalRecords.Systems;
  19. /// <summary>
  20. /// Handles all UI for criminal records console
  21. /// </summary>
  22. public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleSystem
  23. {
  24. [Dependency] private readonly AccessReaderSystem _access = default!;
  25. [Dependency] private readonly CriminalRecordsSystem _criminalRecords = default!;
  26. [Dependency] private readonly PopupSystem _popup = default!;
  27. [Dependency] private readonly RadioSystem _radio = default!;
  28. [Dependency] private readonly StationRecordsSystem _records = default!;
  29. [Dependency] private readonly StationSystem _station = default!;
  30. [Dependency] private readonly UserInterfaceSystem _ui = default!;
  31. public override void Initialize()
  32. {
  33. SubscribeLocalEvent<CriminalRecordsConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
  34. SubscribeLocalEvent<CriminalRecordsConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
  35. Subs.BuiEvents<CriminalRecordsConsoleComponent>(CriminalRecordsConsoleKey.Key, subs =>
  36. {
  37. subs.Event<BoundUIOpenedEvent>(UpdateUserInterface);
  38. subs.Event<SelectStationRecord>(OnKeySelected);
  39. subs.Event<SetStationRecordFilter>(OnFiltersChanged);
  40. subs.Event<CriminalRecordChangeStatus>(OnChangeStatus);
  41. subs.Event<CriminalRecordAddHistory>(OnAddHistory);
  42. subs.Event<CriminalRecordDeleteHistory>(OnDeleteHistory);
  43. subs.Event<CriminalRecordSetStatusFilter>(OnStatusFilterPressed);
  44. });
  45. }
  46. private void UpdateUserInterface<T>(Entity<CriminalRecordsConsoleComponent> ent, ref T args)
  47. {
  48. // TODO: this is probably wasteful, maybe better to send a message to modify the exact state?
  49. UpdateUserInterface(ent);
  50. }
  51. private void OnKeySelected(Entity<CriminalRecordsConsoleComponent> ent, ref SelectStationRecord msg)
  52. {
  53. // no concern of sus client since record retrieval will fail if invalid id is given
  54. ent.Comp.ActiveKey = msg.SelectedKey;
  55. UpdateUserInterface(ent);
  56. }
  57. private void OnStatusFilterPressed(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordSetStatusFilter msg)
  58. {
  59. ent.Comp.FilterStatus = msg.FilterStatus;
  60. UpdateUserInterface(ent);
  61. }
  62. private void OnFiltersChanged(Entity<CriminalRecordsConsoleComponent> ent, ref SetStationRecordFilter msg)
  63. {
  64. if (ent.Comp.Filter == null ||
  65. ent.Comp.Filter.Type != msg.Type || ent.Comp.Filter.Value != msg.Value)
  66. {
  67. ent.Comp.Filter = new StationRecordsFilter(msg.Type, msg.Value);
  68. UpdateUserInterface(ent);
  69. }
  70. }
  71. private void GetOfficer(EntityUid uid, out string officer)
  72. {
  73. var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, uid);
  74. RaiseLocalEvent(tryGetIdentityShortInfoEvent);
  75. officer = tryGetIdentityShortInfoEvent.Title ?? Loc.GetString("criminal-records-console-unknown-officer");
  76. }
  77. private void OnChangeStatus(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordChangeStatus msg)
  78. {
  79. // prevent malf client violating wanted/reason nullability
  80. if (msg.Status == SecurityStatus.Wanted != (msg.Reason != null) &&
  81. msg.Status == SecurityStatus.Suspected != (msg.Reason != null))
  82. return;
  83. if (!CheckSelected(ent, msg.Actor, out var mob, out var key))
  84. return;
  85. if (!_records.TryGetRecord<CriminalRecord>(key.Value, out var record) || record.Status == msg.Status)
  86. return;
  87. // validate the reason
  88. string? reason = null;
  89. if (msg.Reason != null)
  90. {
  91. reason = msg.Reason.Trim();
  92. if (reason.Length < 1 || reason.Length > ent.Comp.MaxStringLength)
  93. return;
  94. }
  95. var oldStatus = record.Status;
  96. var name = _records.RecordName(key.Value);
  97. GetOfficer(mob.Value, out var officer);
  98. // when arresting someone add it to history automatically
  99. // fallback exists if the player was not set to wanted beforehand
  100. if (msg.Status == SecurityStatus.Detained)
  101. {
  102. var oldReason = record.Reason ?? Loc.GetString("criminal-records-console-unspecified-reason");
  103. var history = Loc.GetString("criminal-records-console-auto-history", ("reason", oldReason));
  104. _criminalRecords.TryAddHistory(key.Value, history, officer);
  105. }
  106. // will probably never fail given the checks above
  107. name = _records.RecordName(key.Value);
  108. officer = Loc.GetString("criminal-records-console-unknown-officer");
  109. var jobName = "Unknown";
  110. _records.TryGetRecord<GeneralStationRecord>(key.Value, out var entry);
  111. if (entry != null)
  112. jobName = entry.JobTitle;
  113. var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value);
  114. RaiseLocalEvent(tryGetIdentityShortInfoEvent);
  115. if (tryGetIdentityShortInfoEvent.Title != null)
  116. officer = tryGetIdentityShortInfoEvent.Title;
  117. _criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer);
  118. (string, object)[] args;
  119. if (reason != null)
  120. args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason), ("job", jobName) };
  121. else
  122. args = new (string, object)[] { ("name", name), ("officer", officer), ("job", jobName) };
  123. // figure out which radio message to send depending on transition
  124. var statusString = (oldStatus, msg.Status) switch
  125. {
  126. // person has been detained
  127. (_, SecurityStatus.Detained) => "detained",
  128. // person did something sus
  129. (_, SecurityStatus.Suspected) => "suspected",
  130. // released on parole
  131. (_, SecurityStatus.Paroled) => "paroled",
  132. // prisoner did their time
  133. (_, SecurityStatus.Discharged) => "released",
  134. // going from any other state to wanted, AOS or prisonbreak / lazy secoff never set them to released and they reoffended
  135. (_, SecurityStatus.Wanted) => "wanted",
  136. // person is no longer sus
  137. (SecurityStatus.Suspected, SecurityStatus.None) => "not-suspected",
  138. // going from wanted to none, must have been a mistake
  139. (SecurityStatus.Wanted, SecurityStatus.None) => "not-wanted",
  140. // criminal status removed
  141. (SecurityStatus.Detained, SecurityStatus.None) => "released",
  142. // criminal is no longer on parole
  143. (SecurityStatus.Paroled, SecurityStatus.None) => "not-parole",
  144. // this is impossible
  145. _ => "not-wanted"
  146. };
  147. _radio.SendRadioMessage(ent, Loc.GetString($"criminal-records-console-{statusString}", args),
  148. ent.Comp.SecurityChannel, ent);
  149. UpdateUserInterface(ent);
  150. }
  151. private void OnAddHistory(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordAddHistory msg)
  152. {
  153. if (!CheckSelected(ent, msg.Actor, out var mob, out var key))
  154. return;
  155. var line = msg.Line.Trim();
  156. if (line.Length < 1 || line.Length > ent.Comp.MaxStringLength)
  157. return;
  158. GetOfficer(mob.Value, out var officer);
  159. if (!_criminalRecords.TryAddHistory(key.Value, line, officer))
  160. return;
  161. // no radio message since its not crucial to officers patrolling
  162. UpdateUserInterface(ent);
  163. }
  164. private void OnDeleteHistory(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordDeleteHistory msg)
  165. {
  166. if (!CheckSelected(ent, msg.Actor, out _, out var key))
  167. return;
  168. if (!_criminalRecords.TryDeleteHistory(key.Value, msg.Index))
  169. return;
  170. // a bit sus but not crucial to officers patrolling
  171. UpdateUserInterface(ent);
  172. }
  173. private void UpdateUserInterface(Entity<CriminalRecordsConsoleComponent> ent)
  174. {
  175. var (uid, console) = ent;
  176. var owningStation = _station.GetOwningStation(uid);
  177. if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecords))
  178. {
  179. _ui.SetUiState(uid, CriminalRecordsConsoleKey.Key, new CriminalRecordsConsoleState());
  180. return;
  181. }
  182. // get the listing of records to display
  183. var listing = _records.BuildListing((owningStation.Value, stationRecords), console.Filter);
  184. // filter the listing by the selected criminal record status
  185. //if NONE, dont filter by status, just show all crew
  186. if (console.FilterStatus != SecurityStatus.None)
  187. {
  188. listing = listing
  189. .Where(x => _records.TryGetRecord<CriminalRecord>(new StationRecordKey(x.Key, owningStation.Value), out var record) && record.Status == console.FilterStatus)
  190. .ToDictionary(x => x.Key, x => x.Value);
  191. }
  192. var state = new CriminalRecordsConsoleState(listing, console.Filter);
  193. if (console.ActiveKey is { } id)
  194. {
  195. // get records to display when a crewmember is selected
  196. var key = new StationRecordKey(id, owningStation.Value);
  197. _records.TryGetRecord(key, out state.StationRecord, stationRecords);
  198. _records.TryGetRecord(key, out state.CriminalRecord, stationRecords);
  199. state.SelectedKey = id;
  200. }
  201. // Set the Current Tab aka the filter status type for the records list
  202. state.FilterStatus = console.FilterStatus;
  203. _ui.SetUiState(uid, CriminalRecordsConsoleKey.Key, state);
  204. }
  205. /// <summary>
  206. /// Boilerplate that most actions use, if they require that a record be selected.
  207. /// Obviously shouldn't be used for selecting records.
  208. /// </summary>
  209. private bool CheckSelected(Entity<CriminalRecordsConsoleComponent> ent, EntityUid user,
  210. [NotNullWhen(true)] out EntityUid? mob, [NotNullWhen(true)] out StationRecordKey? key)
  211. {
  212. key = null;
  213. mob = null;
  214. if (!_access.IsAllowed(user, ent))
  215. {
  216. _popup.PopupEntity(Loc.GetString("criminal-records-permission-denied"), ent, user);
  217. return false;
  218. }
  219. if (ent.Comp.ActiveKey is not { } id)
  220. return false;
  221. // checking the console's station since the user might be off-grid using on-grid console
  222. if (_station.GetOwningStation(ent) is not { } station)
  223. return false;
  224. key = new StationRecordKey(id, station);
  225. mob = user;
  226. return true;
  227. }
  228. /// <summary>
  229. /// Checks if the new identity's name has a criminal record attached to it, and gives the entity the icon that
  230. /// belongs to the status if it does.
  231. /// </summary>
  232. public void CheckNewIdentity(EntityUid uid)
  233. {
  234. var name = Identity.Name(uid, EntityManager);
  235. var xform = Transform(uid);
  236. // TODO use the entity's station? Not the station of the map that it happens to currently be on?
  237. var station = _station.GetStationInMap(xform.MapID);
  238. if (station != null && _records.GetRecordByName(station.Value, name) is { } id)
  239. {
  240. if (_records.TryGetRecord<CriminalRecord>(new StationRecordKey(id, station.Value),
  241. out var record))
  242. {
  243. if (record.Status != SecurityStatus.None)
  244. {
  245. _criminalRecords.SetCriminalIcon(name, record.Status, uid);
  246. return;
  247. }
  248. }
  249. }
  250. RemComp<CriminalRecordComponent>(uid);
  251. }
  252. }