using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.GameTicking; using Content.Server.Station.Systems; using Content.Shared.Access.Components; using Content.Server.Forensics; using Content.Shared.Inventory; using Content.Shared.Nuke; using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.StationRecords; using Robust.Shared.Enums; using Robust.Shared.Prototypes; namespace Content.Server.StationRecords.Systems; /// /// Station records. /// /// A station record is tied to an ID card, or anything that holds /// a station record's key. This key will determine access to a /// station record set's record entries, and it is imperative not /// to lose the item that holds the key under any circumstance. /// /// Records are mostly a roleplaying tool, but can have some /// functionality as well (i.e., security records indicating that /// a specific person holding an ID card with a linked key is /// currently under warrant, showing a crew manifest with user /// settable, custom titles). /// /// General records are tied into this system, as most crewmembers /// should have a general record - and most systems should probably /// depend on this general record being created. This is subject /// to change. /// public sealed class StationRecordsSystem : EntitySystem { [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly StationRecordKeyStorageSystem _keyStorageSystem = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnPlayerSpawn); } private void OnPlayerSpawn(PlayerSpawnCompleteEvent args) { if (!HasComp(args.Station)) return; CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId); } private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile, string? jobId, StationRecordsComponent? records = null) { if (!Resolve(station, ref records) || String.IsNullOrEmpty(jobId) || !_prototypeManager.HasIndex(jobId)) { return; } if (!_inventorySystem.TryGetSlotEntity(player, "id", out var idUid)) { return; } TryComp(player, out var fingerprintComponent); TryComp(player, out var dnaComponent); CreateGeneralRecord(station, idUid.Value, profile.Name, profile.Age, profile.Species, profile.Gender, jobId, fingerprintComponent?.Fingerprint, dnaComponent?.DNA, profile, records); } /// /// Create a general record to store in a station's record set. /// /// /// This is tied into the record system, as any crew member's /// records should generally be dependent on some generic /// record with the bare minimum of information involved. /// /// The entity uid of the station. /// The entity uid of an entity's ID card. Can be null. /// Name of the character. /// Species of the character. /// Gender of the character. /// /// The job to initially tie this record to. This must be a valid job loaded in, otherwise /// this call will cause an exception. Ensure that a general record starts out with a job /// that is currently a valid job prototype. /// /// Fingerprint of the character. /// DNA of the character. /// /// /// 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. /// /// 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) { if (!Resolve(station, ref records)) { return; } if (!_prototypeManager.TryIndex(jobId, out JobPrototype? jobPrototype)) { throw new ArgumentException($"Invalid job prototype ID: {jobId}"); } var record = new GeneralStationRecord() { Name = name, Age = age, JobTitle = jobPrototype.LocalizedName, JobIcon = jobPrototype.Icon, JobPrototype = jobId, Species = species, Gender = gender, DisplayPriority = jobPrototype.Weight, Fingerprint = mobFingerprint, DNA = dna }; var key = AddRecord(station, records); AddRecordEntry(key, record, records); 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); } } RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(key, record, profile)); } /// /// Removes a record from this station. /// /// Station to remove the record from. /// The key to remove. /// Station records component. /// True if the record was removed, false otherwise. public bool RemoveRecord(EntityUid station, StationRecordKey key, StationRecordsComponent? records = null) { if (station != key.OriginStation || !Resolve(station, ref records)) { return false; } RaiseLocalEvent(new RecordRemovedEvent(key)); return records.Records.RemoveAllRecords(key); } /// /// Try to get a record from this station's record entries, /// 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. /// 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) { entry = default; if (key.OriginStation != station || !Resolve(station, ref records)) { return false; } return records.Records.TryGetRecordEntry(key, out entry); } /// /// Gets all records of a specific type from a station. /// /// The station to get the records from. /// 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) { if (!Resolve(station, ref records)) { return new (StationRecordKey, T)[]{}; } return records.Records.GetRecordsOfType(); } /// /// Adds a record to a station's record set. /// /// The station to add a record to. /// Station records component. /// /// A station record key, which can be used to add and get records. /// /// /// Occurs when the entity given does not have a station records component. /// public StationRecordKey AddRecord(EntityUid station, StationRecordsComponent? records) { if (!Resolve(station, ref records)) { throw new ArgumentException($"Could not retrieve a {nameof(StationRecordsComponent)} from entity {station}"); } return records.Records.AddRecord(station); } /// /// Adds a record entry to a station's record set. /// /// The key to add the record to. /// 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, record); } /// /// Synchronizes a station's records with any systems that need it. /// /// The station to synchronize any recently accessed records with.. /// Station records component. public void Synchronize(EntityUid station, StationRecordsComponent? records = null) { if (!Resolve(station, ref records)) { return; } foreach (var key in records.Records.GetRecentlyAccessed()) { RaiseLocalEvent(new RecordModifiedEvent(key)); } records.Records.ClearRecentlyAccessed(); } } /// /// Event raised after the player's general profile is created. /// Systems that modify records on a station would have more use /// 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 StationRecordKey Key { get; } public GeneralStationRecord Record { get; } /// /// 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 AfterGeneralRecordCreatedEvent(StationRecordKey key, GeneralStationRecord record, HumanoidCharacterProfile? profile) { Key = key; Record = record; Profile = profile; } } /// /// Event raised after a record is removed. Only the key is given /// when the record is removed, so that any relevant systems/components /// that store record keys can then remove the key from their internal /// fields. /// public sealed class RecordRemovedEvent : EntityEventArgs { public StationRecordKey Key { get; } public RecordRemovedEvent(StationRecordKey key) { Key = key; } } /// /// Event raised after a record is modified. This is to /// inform other systems that records stored in this key /// may have changed. /// public sealed class RecordModifiedEvent : EntityEventArgs { public StationRecordKey Key { get; } public RecordModifiedEvent(StationRecordKey key) { Key = key; } }