criminal records revival (#22510)

This commit is contained in:
deltanedas
2024-02-04 23:29:35 +00:00
committed by GitHub
parent c856dd7506
commit 683591ab04
34 changed files with 1564 additions and 339 deletions

View File

@@ -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 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
var targetAccessComponent = EntityManager.GetComponent<AccessComponent>(targetId);
var jobProto = string.Empty;
if (_station.GetOwningStation(uid) is { } station
&& EntityManager.TryGetComponent<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
&& keyStorage.Key != null
&& _record.TryGetRecord<GeneralStationRecord>(station, keyStorage.Key.Value, out var record))
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
&& keyStorage.Key is {} key
&& _record.TryGetRecord<GeneralStationRecord>(key, out var record))
{
jobProto = record.JobPrototype;
}
@@ -103,7 +100,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
possibleAccess,
jobProto,
privilegedIdName,
EntityManager.GetComponent<MetaDataComponent>(targetId).EntityName);
Name(targetId));
}
_userInterface.TrySetUiState(uid, IdCardConsoleUiKey.Key, newState);
@@ -184,7 +181,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
if (!Resolve(uid, ref component))
return true;
if (!EntityManager.TryGetComponent<AccessReaderComponent>(uid, out var reader))
if (!TryComp<AccessReaderComponent>(uid, out var reader))
return true;
var privilegedId = component.PrivilegedIdSlot.Item;
@@ -193,10 +190,9 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, string newJobTitle, JobPrototype? newJobProto)
{
if (_station.GetOwningStation(uid) is not { } station
|| !EntityManager.TryGetComponent<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
if (!TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
|| keyStorage.Key is not { } key
|| !_record.TryGetRecord<GeneralStationRecord>(station, key, out var record))
|| !_record.TryGetRecord<GeneralStationRecord>(key, out var record))
{
return;
}
@@ -210,6 +206,6 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
record.JobIcon = newJobProto.Icon;
}
_record.Synchronize(station);
_record.Synchronize(key);
}
}

View File

@@ -349,7 +349,7 @@ namespace Content.Server.Administration.Systems
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 @@ namespace Content.Server.Administration.Systems
continue;
}
_stationRecords.RemoveRecord(key.OriginStation, key);
_stationRecords.RemoveRecord(key);
Del(item);
}
}

View File

@@ -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;
/// <summary>
/// A component for Criminal Record Console storing an active station record key and a currently applied filter
/// </summary>
[RegisterComponent]
[Access(typeof(CriminalRecordsConsoleSystem))]
public sealed partial class CriminalRecordsConsoleComponent : Component
{
/// <summary>
/// Currently active station record key.
/// There is no station parameter as the console uses the current station.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
public uint? ActiveKey;
/// <summary>
/// Currently applied filter.
/// </summary>
[DataField]
public StationRecordsFilter? Filter;
/// <summary>
/// Channel to send messages to when someone's status gets changed.
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype> SecurityChannel = "Security";
/// <summary>
/// Max length of arrest and crime history strings.
/// </summary>
[DataField]
public uint MaxStringLength = 256;
}

View File

@@ -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<CriminalRecordsConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
SubscribeLocalEvent<CriminalRecordsConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
Subs.BuiEvents<CriminalRecordsConsoleComponent>(CriminalRecordsConsoleKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(UpdateUserInterface);
subs.Event<SelectStationRecord>(OnKeySelected);
subs.Event<SetStationRecordFilter>(OnFiltersChanged);
subs.Event<CriminalRecordChangeStatus>(OnChangeStatus);
subs.Event<CriminalRecordAddHistory>(OnAddHistory);
subs.Event<CriminalRecordDeleteHistory>(OnDeleteHistory);
});
}
private void UpdateUserInterface<T>(Entity<CriminalRecordsConsoleComponent> 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<CriminalRecordsConsoleComponent> 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<CriminalRecordsConsoleComponent> 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<CriminalRecordsConsoleComponent> 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<CriminalRecord>(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<CriminalRecordsConsoleComponent> 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<CriminalRecordsConsoleComponent> 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<CriminalRecordsConsoleComponent> ent)
{
var (uid, console) = ent;
var owningStation = _station.GetOwningStation(uid);
if (!TryComp<StationRecordsComponent>(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);
}
/// <summary>
/// Boilerplate that most actions use, if they require that a record be selected.
/// Obviously shouldn't be used for selecting records.
/// </summary>
private bool CheckSelected(Entity<CriminalRecordsConsoleComponent> 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;
}
/// <summary>
/// Gets the name from a record, or empty string if this somehow fails.
/// </summary>
private string RecordName(StationRecordKey key)
{
if (!_stationRecords.TryGetRecord<GeneralStationRecord>(key, out var record))
return "";
return record.Name;
}
}

View File

@@ -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;
/// <summary>
/// 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
/// </summary>
public sealed class CriminalRecordsSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(OnGeneralRecordCreated);
}
private void OnGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
{
_stationRecords.AddRecordEntry(ev.Key, new CriminalRecord());
_stationRecords.Synchronize(ev.Key);
}
/// <summary>
/// Tries to change the status of the record found by the StationRecordKey.
/// Reason should only be passed if status is Wanted.
/// </summary>
/// <returns>True if the status is changed, false if not</returns>
public bool TryChangeStatus(StationRecordKey key, SecurityStatus status, string? reason)
{
// don't do anything if its the same status
if (!_stationRecords.TryGetRecord<CriminalRecord>(key, out var record)
|| status == record.Status)
return false;
record.Status = status;
record.Reason = reason;
_stationRecords.Synchronize(key);
return true;
}
/// <summary>
/// Tries to add a history entry to a criminal record.
/// </summary>
/// <returns>True if adding succeeded, false if not</returns>
public bool TryAddHistory(StationRecordKey key, CrimeHistory entry)
{
if (!_stationRecords.TryGetRecord<CriminalRecord>(key, out var record))
return false;
record.History.Add(entry);
return true;
}
/// <summary>
/// Creates and tries to add a history entry using the current time.
/// </summary>
public bool TryAddHistory(StationRecordKey key, string line)
{
var entry = new CrimeHistory(_timing.CurTime, line);
return TryAddHistory(key, entry);
}
/// <summary>
/// Tries to delete a sepcific line of history from a criminal record, by index.
/// </summary>
/// <returns>True if the line was removed, false if not</returns>
public bool TryDeleteHistory(StationRecordKey key, uint index)
{
if (!_stationRecords.TryGetRecord<CriminalRecord>(key, out var record))
return false;
if (index >= record.History.Count)
return false;
record.History.RemoveAt((int) index);
return true;
}
}

View File

@@ -68,18 +68,14 @@ public sealed class RenameCommand : IConsoleCommand
// This is done here because ID cards are linked to station records
if (_entManager.TrySystem<StationRecordsSystem>(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<GeneralStationRecord>(origin,
keyStorage.Key.Value,
out var generalRecord))
if (recordsSystem.TryGetRecord<GeneralStationRecord>(key, out var generalRecord))
{
generalRecord.Name = name;
}
recordsSystem.Synchronize(origin);
recordsSystem.Synchronize(key);
}
}
}

View File

@@ -29,15 +29,16 @@ public sealed class ClericalErrorRule : StationEventSystem<ClericalErrorRuleComp
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<StationRecordKey>();
var keys = new List<uint>();
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);
}
}
}

View File

@@ -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; }
/// <summary>
/// Selected crewmember record id.
/// Station always uses the station that owns the console.
/// </summary>
[DataField]
public uint? ActiveKey;
/// <summary>
/// Qualities to filter a search by.
/// </summary>
[DataField]
public StationRecordsFilter? Filter;
}

View File

@@ -6,9 +6,10 @@ using Robust.Shared.Utility;
namespace Content.Server.StationRecords;
/// <summary>
/// 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).
/// </summary>
[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<StationRecordKey> Keys = new();
/// <summary>
/// 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.
/// </summary>
[DataField]
public HashSet<uint> Keys = new();
[DataField("recentlyAccessed")]
private HashSet<StationRecordKey> _recentlyAccessed = new();
/// <summary>
/// Recently accessed key ids which are used to synchronize them efficiently.
/// </summary>
[DataField]
private HashSet<uint> _recentlyAccessed = new();
[DataField("tables")] // TODO ensure all of this data is serializable.
private Dictionary<Type, Dictionary<StationRecordKey, object>> _tables = new();
/// <summary>
/// Dictionary between a record's type and then each record indexed by id.
/// </summary>
[DataField]
private Dictionary<Type, Dictionary<uint, object>> _tables = new();
/// <summary>
/// Gets all records of a specific type stored in the record set.
/// </summary>
/// <typeparam name="T">The type of record to fetch.</typeparam>
/// <returns>An enumerable object that contains a pair of both a station key, and the record associated with it.</returns>
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>()
public IEnumerable<(uint, T)> GetRecordsOfType<T>()
{
if (!_tables.ContainsKey(typeof(T)))
{
@@ -52,43 +62,44 @@ public sealed partial class StationRecordSet
}
/// <summary>
/// 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.
/// </summary>
/// <param name="entry">Entry to add.</param>
/// <typeparam name="T">Type of the entry that's being added.</typeparam>
public StationRecordKey AddRecordEntry<T>(EntityUid station, T entry)
public uint? AddRecordEntry<T>(T entry)
{
if (entry == null)
return StationRecordKey.Invalid;
return null;
var key = new StationRecordKey(_currentRecordId++, station);
var key = _currentRecordId++;
AddRecordEntry(key, entry);
return key;
}
/// <summary>
/// Add an entry into a record.
/// Add an entry into an existing record.
/// </summary>
/// <param name="key">Key for the record.</param>
/// <param name="key">Key id for the record.</param>
/// <param name="entry">Entry to add.</param>
/// <typeparam name="T">Type of the entry that's being added.</typeparam>
public void AddRecordEntry<T>(StationRecordKey key, T entry)
public void AddRecordEntry<T>(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;
}
/// <summary>
/// Try to get an record entry by type, from this record key.
/// </summary>
/// <param name="key">The StationRecordKey to get the entries from.</param>
/// <param name="key">The record id to get the entries from.</param>
/// <param name="entry">The entry that is retrieved from the record set.</param>
/// <typeparam name="T">The type of entry to search for.</typeparam>
/// <returns>True if the record exists and was retrieved, false otherwise.</returns>
public bool TryGetRecordEntry<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry)
public bool TryGetRecordEntry<T>(uint key, [NotNullWhen(true)] out T? entry)
{
entry = default;
@@ -108,10 +119,10 @@ public sealed partial class StationRecordSet
/// <summary>
/// Checks if the record associated with this key has an entry of a certain type.
/// </summary>
/// <param name="key">The record key.</param>
/// <param name="key">The record key id.</param>
/// <typeparam name="T">Type to check.</typeparam>
/// <returns>True if the entry exists, false otherwise.</returns>
public bool HasRecordEntry<T>(StationRecordKey key)
public bool HasRecordEntry<T>(uint key)
{
return Keys.Contains(key)
&& _tables.TryGetValue(typeof(T), out var table)
@@ -122,7 +133,7 @@ public sealed partial class StationRecordSet
/// Get the recently accessed keys from this record set.
/// </summary>
/// <returns>All recently accessed keys from this record set.</returns>
public IEnumerable<StationRecordKey> GetRecentlyAccessed()
public IEnumerable<uint> GetRecentlyAccessed()
{
return _recentlyAccessed.ToArray();
}
@@ -135,17 +146,23 @@ public sealed partial class StationRecordSet
_recentlyAccessed.Clear();
}
/// <summary>
/// Removes a recently accessed key from the set.
/// </summary>
public void RemoveFromRecentlyAccessed(uint key)
{
_recentlyAccessed.Remove(key);
}
/// <summary>
/// Removes all record entries related to this key from this set.
/// </summary>
/// <param name="key">The key to remove.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool RemoveAllRecords(StationRecordKey key)
public bool RemoveAllRecords(uint key)
{
if (!Keys.Remove(key))
{
return false;
}
foreach (var table in _tables.Values)
{

View File

@@ -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<GeneralStationRecordConsoleComponent, BoundUIOpenedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, SelectGeneralStationRecord>(OnKeySelected);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, GeneralStationRecordsFilterMsg>(OnFiltersChanged);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordRemovedEvent>(UpdateUserInterface);
}
private void UpdateUserInterface<T>(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
{
UpdateUserInterface(uid, component);
}
private void OnKeySelected(EntityUid uid, GeneralStationRecordConsoleComponent component,
SelectGeneralStationRecord msg)
{
component.ActiveKey = msg.SelectedKey;
UpdateUserInterface(uid, component);
}
private void OnFiltersChanged(EntityUid uid,
GeneralStationRecordConsoleComponent component, GeneralStationRecordsFilterMsg msg)
{
if (component.Filter == null ||
component.Filter.Type != msg.Type || component.Filter.Value != msg.Value)
Subs.BuiEvents<GeneralStationRecordConsoleComponent>(GeneralStationRecordConsoleKey.Key, subs =>
{
component.Filter = new GeneralStationRecordsFilter(msg.Type, msg.Value);
UpdateUserInterface(uid, component);
subs.Event<BoundUIOpenedEvent>(UpdateUserInterface);
subs.Event<SelectStationRecord>(OnKeySelected);
subs.Event<SetStationRecordFilter>(OnFiltersChanged);
});
}
private void UpdateUserInterface<T>(Entity<GeneralStationRecordConsoleComponent> ent, ref T args)
{
UpdateUserInterface(ent);
}
// 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<GeneralStationRecordConsoleComponent> ent, ref SelectStationRecord msg)
{
ent.Comp.ActiveKey = msg.SelectedKey;
UpdateUserInterface(ent);
}
private void OnFiltersChanged(Entity<GeneralStationRecordConsoleComponent> 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 UpdateUserInterface(EntityUid uid,
GeneralStationRecordConsoleComponent? console = null)
private void UpdateUserInterface(Entity<GeneralStationRecordConsoleComponent> ent)
{
if (!Resolve(uid, ref console))
var (uid, console) = ent;
var owningStation = _station.GetOwningStation(uid);
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecords))
{
_ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState());
return;
}
var owningStation = _stationSystem.GetOwningStation(uid);
var listing = _stationRecords.BuildListing((owningStation.Value, stationRecords), console.Filter);
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecordsComponent))
switch (listing.Count)
{
GeneralStationRecordConsoleState state = new(null, null, null, null);
SetStateForInterface(uid, state);
case 0:
_ui.TrySetUiState(uid, GeneralStationRecordConsoleKey.Key, new GeneralStationRecordConsoleState());
return;
case 1:
console.ActiveKey = listing.Keys.First();
break;
}
if (console.ActiveKey is not { } id)
return;
}
var consoleRecords =
_stationRecordsSystem.GetRecordsOfType<GeneralStationRecord>(owningStation.Value, stationRecordsComponent);
var key = new StationRecordKey(id, owningStation.Value);
_stationRecords.TryGetRecord<GeneralStationRecord>(key, out var record, stationRecords);
var listing = new Dictionary<(NetEntity, uint), string>();
foreach (var pair in consoleRecords)
{
if (console.Filter != null && IsSkippedRecord(console.Filter, pair.Item2))
{
continue;
}
listing.Add(_stationRecordsSystem.Convert(pair.Item1), pair.Item2.Name);
}
if (listing.Count == 0)
{
GeneralStationRecordConsoleState state = new(null, null, null, console.Filter);
SetStateForInterface(uid, state);
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);
}
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);
}
}

View File

@@ -32,8 +32,8 @@ namespace Content.Server.StationRecords.Systems;
/// </summary>
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 sealed class StationRecordsSystem : SharedStationRecordsSystem
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
{
if (!HasComp<StationRecordsComponent>(args.Station))
if (!TryComp<StationRecordsComponent>(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<JobPrototype>(jobId))
{
return;
}
if (!_inventorySystem.TryGetSlotEntity(player, "id", out var idUid))
{
if (!_inventory.TryGetSlotEntity(player, "id", out var idUid))
return;
}
TryComp<FingerprintComponent>(player, out var fingerprintComponent);
TryComp<DnaComponent>(player, out var dnaComponent);
@@ -100,17 +96,28 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
/// Optional - other systems should anticipate this.
/// </param>
/// <param name="records">Station records component.</param>
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 JobPrototype? jobPrototype))
{
if (!_prototypeManager.TryIndex<JobPrototype>(jobId, out var jobPrototype))
throw new ArgumentException($"Invalid job prototype ID: {jobId}");
// 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)
{
SetIdKey(idUid, new StationRecordKey(id, station));
return;
}
var record = new GeneralStationRecord()
@@ -129,40 +136,47 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
var key = AddRecordEntry(station, record);
if (!key.IsValid())
return;
if (idUid != null)
{
var keyStorageEntity = idUid;
if (TryComp(idUid, out PdaComponent? pdaComponent) && pdaComponent.ContainedId != null)
{
keyStorageEntity = pdaComponent.IdSlot.Item;
}
if (keyStorageEntity != null)
{
_keyStorageSystem.AssignKey(keyStorageEntity.Value, key);
}
Log.Warning($"Failed to add general record entry for {name}");
return;
}
RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(station, key, record, profile));
SetIdKey(idUid, key);
RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(key, record, profile));
}
/// <summary>
/// Set the station records key for an id/pda.
/// </summary>
public void SetIdKey(EntityUid? uid, StationRecordKey key)
{
if (uid is not {} idUid)
return;
var keyStorageEntity = idUid;
if (TryComp<PdaComponent>(idUid, out var pda) && pda.ContainedId is {} id)
{
keyStorageEntity = id;
}
_keyStorage.AssignKey(keyStorageEntity, key);
}
/// <summary>
/// Removes a record from this station.
/// </summary>
/// <param name="station">Station to remove the record from.</param>
/// <param name="key">The key to remove.</param>
/// <param name="key">The station and key to remove.</param>
/// <param name="records">Station records component.</param>
/// <returns>True if the record was removed, false otherwise.</returns>
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 sealed class StationRecordsSystem : SharedStationRecordsSystem
/// from the provided station record key. Will always return
/// null if the key does not match the station.
/// </summary>
/// <param name="station">Station to get the record from.</param>
/// <param name="key">Key to try and index from the record set.</param>
/// <param name="key">Station and key to try and index from the record set.</param>
/// <param name="entry">The resulting entry.</param>
/// <param name="records">Station record component.</param>
/// <typeparam name="T">Type to get from the record set.</typeparam>
/// <returns>True if the record was obtained, false otherwise.</returns>
public bool TryGetRecord<T>(EntityUid station, StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
public bool TryGetRecord<T>(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);
}
/// <summary>
/// Returns an id if a record with the same name exists.
/// </summary>
/// <remarks>
/// Linear search so O(n) time complexity.
/// </remarks>
public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
return null;
foreach (var (id, record) in GetRecordsOfType<GeneralStationRecord>(station, records))
{
if (record.Name == name)
return id;
}
return null;
}
/// <summary>
@@ -197,30 +230,47 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
/// <param name="records">Station records component.</param>
/// <typeparam name="T">Type of record to fetch</typeparam>
/// <returns>Enumerable of pairs with a station record key, and the entry in question of type T.</returns>
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
public IEnumerable<(uint, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return Array.Empty<(StationRecordKey, T)>();
}
return Array.Empty<(uint, T)>();
return records.Records.GetRecordsOfType<T>();
}
/// <summary>
/// Adds a record entry to a station's record set.
/// Adds a new record entry to a station's record set.
/// </summary>
/// <param name="station">The station to add the record to.</param>
/// <param name="record">The record to add.</param>
/// <param name="records">Station records component.</param>
/// <typeparam name="T">The type of record to add.</typeparam>
public StationRecordKey AddRecordEntry<T>(EntityUid station, T record,
StationRecordsComponent? records = null)
public StationRecordKey AddRecordEntry<T>(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);
}
/// <summary>
/// Adds a record to an existing entry.
/// </summary>
/// <param name="key">The station and id of the existing entry.</param>
/// <param name="record">The record to add.</param>
/// <param name="records">Station records component.</param>
/// <typeparam name="T">The type of record to add.</typeparam>
public void AddRecordEntry<T>(StationRecordKey key, T record,
StationRecordsComponent? records = null)
{
if (!Resolve(key.OriginStation, ref records))
return;
records.Records.AddRecordEntry(key.Id, record);
}
/// <summary>
@@ -231,17 +281,99 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
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();
}
/// <summary>
/// Synchronizes a single record's entries for a station.
/// </summary>
/// <param name="key">The station and id of the record</param>
/// <param name="records">Station records component.</param>
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
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Build a record listing of id to name for a station and filter.
/// </summary>
public Dictionary<uint, string> BuildListing(Entity<StationRecordsComponent> station, StationRecordsFilter? filter)
{
var listing = new Dictionary<uint, string>();
var records = GetRecordsOfType<GeneralStationRecord>(station, station.Comp);
foreach (var pair in records)
{
if (IsSkipped(filter, pair.Item2))
continue;
listing.Add(pair.Item1, pair.Item2.Name);
}
return listing;
}
#endregion
}
/// <summary>
/// Base event for station record events
/// </summary>
public abstract class StationRecordEvent : EntityEventArgs
{
public readonly StationRecordKey Key;
public EntityUid Station => Key.OriginStation;
protected StationRecordEvent(StationRecordKey key)
{
Key = key;
}
}
/// <summary>
@@ -250,23 +382,19 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
/// listening to this event, as it contains the character's record key.
/// Also stores the general record reference, to save some time.
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
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 sealed class AfterGeneralRecordCreatedEvent : EntityEventArgs
/// that store record keys can then remove the key from their internal
/// fields.
/// </summary>
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 sealed class RecordRemovedEvent : EntityEventArgs
/// inform other systems that records stored in this key
/// may have changed.
/// </summary>
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;
}
}