using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Systems; using Content.Server.Forensics; using Content.Shared.Access.Components; using Content.Shared.Forensics.Components; using Content.Server.Overlays; // Namespace for FactionIconsSystem using Content.Shared.GameTicking; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.StationRecords; using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Content.Shared.Overlays; // Namespace for ShowFactionIconsComponent using Content.Shared.Civ14.CivTDMFactions; using Content.Shared.NPC.Components; // Namespace for CivTDMFactionsComponent 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 : SharedStationRecordsSystem { [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly StationRecordKeyStorageSystem _keyStorage = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IdCardSystem _idCard = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly FactionIconsSystem _factionIcons = default!; // Added dependency public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnPlayerSpawn); SubscribeLocalEvent(OnRename); } private void OnPlayerSpawn(PlayerSpawnCompleteEvent args) { if (!TryComp(args.Station, out var stationRecords)) return; // Create the general record first CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId, stationRecords); // --- Attempt Squad Assignment --- if (TryComp(args.Mob, out var sfiComponent)) { // Determine if player wants to be a sergeant (e.g., based on job) bool wantsToBeSergeant = sfiComponent.JobIcon == "JobIconISgt"; // Example: "JobIconISgt" implies sergeant role // Determine player's CivFaction (this is crucial and needs proper game logic) // For this example, let's assume a simple alternating assignment or based on JobId. // In a real game, this would come from team selection, game mode logic, etc. string? playerCivFactionId = null; // Query for the CivTDMFactionsComponent (assuming one exists on a game rule or map entity) var civQuery = EntityQueryEnumerator(); CivTDMFactionsComponent? civTDMComp = null; if (civQuery.MoveNext(out _, out civTDMComp)) { // Example: Assign to Faction1Id if JobId contains "Faction1", else Faction2Id // This is placeholder logic. Replace with your actual faction assignment logic. if (args.JobId != null) // Ensure JobId is not null { if (TryComp(args.Mob, out var factionComp)) { foreach (var faction in factionComp.Factions) { if (civTDMComp.Faction1Id == null || faction == civTDMComp.Faction1Id) { playerCivFactionId = civTDMComp.Faction1Id; // This is already a string, no need for ProtoId conversion here break; } else { playerCivFactionId = civTDMComp.Faction2Id; break; } } } } // Fallback or default assignment if not determined by job if (playerCivFactionId == null) { // Simplistic: assign to Faction1Id by default if not specified or if factionComp.Factions was empty/null. // Or implement round-robin, or based on current team populations. playerCivFactionId = civTDMComp.Faction1Id; } } if (playerCivFactionId != null) { sfiComponent.BelongsToCivFactionId = playerCivFactionId; // Store it on the component _factionIcons.AttemptAssignPlayerToSquad(args.Mob, playerCivFactionId, wantsToBeSergeant, sfiComponent); } else { Log.Warning($"Could not determine CivFaction for player {ToPrettyString(args.Mob)} with job {args.JobId}. Squad assignment skipped."); } } // --- End Squad Assignment --- } private void OnRename(ref EntityRenamedEvent ev) { // When a player gets renamed their card gets changed to match. // Unfortunately this means that an event is called for it as well, and since TryFindIdCard will succeed if the // given entity is a card and the card itself is the key the record will be mistakenly renamed to the card's name // if we don't return early. // We also do not include the PDA itself being renamed, as that triggers the same event (e.g. for chameleon PDAs). if (HasComp(ev.Uid) || HasComp(ev.Uid)) return; if (_idCard.TryFindIdCard(ev.Uid, out var idCard)) { if (TryComp(idCard, out StationRecordKeyStorageComponent? keyStorage) && keyStorage.Key is { } key) { if (TryGetRecord(key, out var generalRecord)) { generalRecord.Name = ev.NewName; } Synchronize(key); } } } private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile, string? jobId, StationRecordsComponent records) { // TODO make PlayerSpawnCompleteEvent.JobId a ProtoId if (string.IsNullOrEmpty(jobId) || !_prototypeManager.HasIndex(jobId)) return; if (!_inventory.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, StationRecordsComponent records) { if (!_prototypeManager.TryIndex(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() { Name = name, Age = age, JobTitle = jobPrototype.LocalizedName, JobIcon = jobPrototype.Icon, JobPrototype = jobId, Species = species, Gender = gender, DisplayPriority = jobPrototype.RealDisplayWeight, Fingerprint = mobFingerprint, DNA = dna }; 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)); } /// /// 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) { keyStorageEntity = id; } _keyStorage.AssignKey(keyStorageEntity, key); } /// /// Removes a record from this station. /// /// The station and key to remove. /// Station records component. /// True if the record was removed, false otherwise. public bool RemoveRecord(StationRecordKey key, StationRecordsComponent? records = null) { if (!Resolve(key.OriginStation, ref records)) return false; if (records.Records.RemoveAllRecords(key.Id)) { RaiseLocalEvent(new RecordRemovedEvent(key)); return true; } return false; } /// /// 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 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(StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null) { entry = default; if (!Resolve(key.OriginStation, ref records)) return false; return records.Records.TryGetRecordEntry(key.Id, out entry); } /// /// Gets a random record from the station's record entries. /// /// The EntityId of the station from which you want to get the record. /// The resulting entry. /// Type to get from the record set. /// True if a record was obtained. False otherwise. public bool TryGetRandomRecord(Entity ent, [NotNullWhen(true)] out T? entry) { entry = default; if (!Resolve(ent.Owner, ref ent.Comp)) return false; if (ent.Comp.Records.Keys.Count == 0) return false; var key = _random.Pick(ent.Comp.Records.Keys); return ent.Comp.Records.TryGetRecordEntry(key, 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, false)) return null; foreach (var (id, record) in GetRecordsOfType(station, records)) { if (record.Name == name) return id; } return null; } /// /// Get the name for a record, or an empty string if it has no record. /// public string RecordName(StationRecordKey key) { if (!TryGetRecord(key, out var record)) return string.Empty; return record.Name; } /// /// 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<(uint, T)> GetRecordsOfType(EntityUid station, StationRecordsComponent? records = null) { if (!Resolve(station, ref records)) return Array.Empty<(uint, T)>(); return records.Records.GetRecordsOfType(); } /// /// 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) { if (!Resolve(station, ref records)) return StationRecordKey.Invalid; 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); } /// /// 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(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), _ => throw new IndexOutOfRangeException(nameof(filter.Type)), }; } 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; } } /// /// 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 : StationRecordEvent { 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 readonly HumanoidCharacterProfile Profile; public AfterGeneralRecordCreatedEvent(StationRecordKey key, GeneralStationRecord record, HumanoidCharacterProfile profile) : base(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 : StationRecordEvent { public RecordRemovedEvent(StationRecordKey key) : base(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 : StationRecordEvent { public RecordModifiedEvent(StationRecordKey key) : base(key) { } }