]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict identity (#40185)
authorāda <ss.adasts@gmail.com>
Tue, 23 Sep 2025 23:32:20 +0000 (18:32 -0500)
committerGitHub <noreply@github.com>
Tue, 23 Sep 2025 23:32:20 +0000 (01:32 +0200)
* crossing the pond

* share some station records

* share some criminal records

* single system

* comments

* minor touchups

* I always forget this part

* requested changes

* revert predicted spawn

* requested changes

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
18 files changed:
Content.Client/IdentityManagement/IdentitySystem.cs [deleted file]
Content.Server/Clothing/Systems/ChameleonClothingSystem.cs
Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs
Content.Server/Delivery/DeliverySystem.Spawning.cs
Content.Server/IdentityManagement/IdentitySystem.cs [deleted file]
Content.Server/Station/Systems/StationSpawningSystem.cs
Content.Server/StationRecords/Systems/StationRecordsSystem.cs
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsConsoleSystem.cs
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs
Content.Shared/IdentityManagement/Components/IdentityComponent.cs
Content.Shared/IdentityManagement/IdentitySystem.cs [new file with mode: 0644]
Content.Shared/IdentityManagement/SharedIdentitySystem.cs [deleted file]
Content.Shared/StationRecords/SharedStationRecordsSystem.cs
Content.Shared/StationRecords/StationRecordSet.cs [moved from Content.Server/StationRecords/StationRecordSet.cs with 98% similarity]
Content.Shared/StationRecords/StationRecordsComponent.cs [moved from Content.Server/StationRecords/Components/StationRecordsComponent.cs with 70% similarity]
Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs

diff --git a/Content.Client/IdentityManagement/IdentitySystem.cs b/Content.Client/IdentityManagement/IdentitySystem.cs
deleted file mode 100644 (file)
index 15d4ee2..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.IdentityManagement;
-
-namespace Content.Client.IdentityManagement;
-
-public sealed class IdentitySystem : SharedIdentitySystem
-{
-}
index 0d281d70751c5746a72dfa05b29fef9664b4fc0e..f734d3eb3ecfec651d61a62a29ba63275ec5655e 100644 (file)
@@ -1,9 +1,9 @@
 using System.Linq;
 using Content.Server.Emp;
-using Content.Server.IdentityManagement;
 using Content.Shared.Clothing.Components;
 using Content.Shared.Clothing.EntitySystems;
 using Content.Shared.Emp;
+using Content.Shared.IdentityManagement;
 using Content.Shared.IdentityManagement.Components;
 using Content.Shared.Inventory;
 using Content.Shared.Prototypes;
index 72d66c76382f6f8914ec0ccf691809df0cc4595f..1b7e50c6515a6cd7e9e50151869318956fdc8478 100644 (file)
@@ -269,31 +269,4 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
         mob = user;
         return true;
     }
-
-    /// <summary>
-    /// Checks if the new identity's name has a criminal record attached to it, and gives the entity the icon that
-    /// belongs to the status if it does.
-    /// </summary>
-    public void CheckNewIdentity(EntityUid uid)
-    {
-        var name = Identity.Name(uid, EntityManager);
-        var xform = Transform(uid);
-
-        // TODO use the entity's station? Not the station of the map that it happens to currently be on?
-        var station = _station.GetStationInMap(xform.MapID);
-
-        if (station != null && _records.GetRecordByName(station.Value, name) is { } id)
-        {
-            if (_records.TryGetRecord<CriminalRecord>(new StationRecordKey(id, station.Value),
-                    out var record))
-            {
-                if (record.Status != SecurityStatus.None)
-                {
-                    _criminalRecords.SetCriminalIcon(name, record.Status, uid);
-                    return;
-                }
-            }
-        }
-        RemComp<CriminalRecordComponent>(uid);
-    }
 }
index a7496a343b04be1d4d3f2571176b6ae12c00a340..14662e58c690ebff71f36c3df69ceb29a539d755 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Shared.Delivery;
 using Content.Shared.Power.EntitySystems;
-using Content.Server.StationRecords;
 using Content.Shared.EntityTable;
+using Content.Shared.StationRecords;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
 
diff --git a/Content.Server/IdentityManagement/IdentitySystem.cs b/Content.Server/IdentityManagement/IdentitySystem.cs
deleted file mode 100644 (file)
index 131544e..0000000
+++ /dev/null
@@ -1,180 +0,0 @@
-using Content.Server.Access.Systems;
-using Content.Server.Administration.Logs;
-using Content.Server.CriminalRecords.Systems;
-using Content.Server.Humanoid;
-using Content.Shared.Clothing;
-using Content.Shared.Database;
-using Content.Shared.Hands;
-using Content.Shared.Humanoid;
-using Content.Shared.IdentityManagement;
-using Content.Shared.IdentityManagement.Components;
-using Content.Shared.Inventory;
-using Content.Shared.Inventory.Events;
-using Robust.Shared.Containers;
-using Robust.Shared.Enums;
-using Robust.Shared.GameObjects.Components.Localization;
-
-namespace Content.Server.IdentityManagement;
-
-/// <summary>
-///     Responsible for updating the identity of an entity on init or clothing equip/unequip.
-/// </summary>
-public sealed class IdentitySystem : SharedIdentitySystem
-{
-    [Dependency] private readonly IdCardSystem _idCard = default!;
-    [Dependency] private readonly IAdminLogManager _adminLog = default!;
-    [Dependency] private readonly MetaDataSystem _metaData = default!;
-    [Dependency] private readonly SharedContainerSystem _container = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
-    [Dependency] private readonly CriminalRecordsConsoleSystem _criminalRecordsConsole = default!;
-    [Dependency] private readonly GrammarSystem _grammarSystem = default!;
-
-    private HashSet<EntityUid> _queuedIdentityUpdates = new();
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<IdentityComponent, DidEquipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
-        SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
-        SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
-        SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
-        SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
-        SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
-        SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
-    }
-
-    public override void Update(float frameTime)
-    {
-        base.Update(frameTime);
-
-        foreach (var ent in _queuedIdentityUpdates)
-        {
-            if (!TryComp<IdentityComponent>(ent, out var identity))
-                continue;
-
-            UpdateIdentityInfo(ent, identity);
-        }
-
-        _queuedIdentityUpdates.Clear();
-    }
-
-    // This is where the magic happens
-    private void OnMapInit(EntityUid uid, IdentityComponent component, MapInitEvent args)
-    {
-        var ident = Spawn(null, Transform(uid).Coordinates);
-
-        _metaData.SetEntityName(ident, "identity");
-        QueueIdentityUpdate(uid);
-        _container.Insert(ident, component.IdentityEntitySlot);
-    }
-
-    /// <summary>
-    ///     Queues an identity update to the start of the next tick.
-    /// </summary>
-    public override void QueueIdentityUpdate(EntityUid uid)
-    {
-        _queuedIdentityUpdates.Add(uid);
-    }
-
-    #region Private API
-
-    /// <summary>
-    ///     Updates the metadata name for the id(entity) from the current state of the character.
-    /// </summary>
-    private void UpdateIdentityInfo(EntityUid uid, IdentityComponent identity)
-    {
-        if (identity.IdentityEntitySlot.ContainedEntity is not { } ident)
-            return;
-
-        var representation = GetIdentityRepresentation(uid);
-        var name = GetIdentityName(uid, representation);
-
-        // Clone the old entity's grammar to the identity entity, for loc purposes.
-        if (TryComp<GrammarComponent>(uid, out var grammar))
-        {
-            var identityGrammar = EnsureComp<GrammarComponent>(ident);
-            identityGrammar.Attributes.Clear();
-
-            foreach (var (k, v) in grammar.Attributes)
-            {
-                identityGrammar.Attributes.Add(k, v);
-            }
-
-            // If presumed name is null and we're using that, we set proper noun to be false ("the old woman")
-            if (name != representation.TrueName && representation.PresumedName == null)
-                _grammarSystem.SetProperNoun((ident, identityGrammar), false);
-
-            Dirty(ident, identityGrammar);
-        }
-
-        if (name == Name(ident))
-            return;
-
-        _metaData.SetEntityName(ident, name);
-
-        _adminLog.Add(LogType.Identity, LogImpact.Medium, $"{ToPrettyString(uid)} changed identity to {name}");
-        var identityChangedEvent = new IdentityChangedEvent(uid, ident);
-        RaiseLocalEvent(uid, ref identityChangedEvent);
-        SetIdentityCriminalIcon(uid);
-    }
-
-    private string GetIdentityName(EntityUid target, IdentityRepresentation representation)
-    {
-        var ev = new SeeIdentityAttemptEvent();
-
-        RaiseLocalEvent(target, ev);
-        return representation.ToStringKnown(!ev.Cancelled);
-    }
-
-    /// <summary>
-    ///     When the identity of a person is changed, searches the criminal records to see if the name of the new identity
-    ///     has a record. If the new name has a criminal status attached to it, the person will get the criminal status
-    ///     until they change identity again.
-    /// </summary>
-    private void SetIdentityCriminalIcon(EntityUid uid)
-    {
-        _criminalRecordsConsole.CheckNewIdentity(uid);
-    }
-
-    /// <summary>
-    ///     Gets an 'identity representation' of an entity, with their true name being the entity name
-    ///     and their 'presumed name' and 'presumed job' being the name/job on their ID card, if they have one.
-    /// </summary>
-    private IdentityRepresentation GetIdentityRepresentation(EntityUid target,
-        InventoryComponent? inventory=null,
-        HumanoidAppearanceComponent? appearance=null)
-    {
-        int age = 18;
-        Gender gender = Gender.Epicene;
-        string species = SharedHumanoidAppearanceSystem.DefaultSpecies;
-
-        // Always use their actual age and gender, since that can't really be changed by an ID.
-        if (Resolve(target, ref appearance, false))
-        {
-            gender = appearance.Gender;
-            age = appearance.Age;
-            species = appearance.Species;
-        }
-
-        var ageString = _humanoid.GetAgeRepresentation(species, age);
-        var trueName = Name(target);
-        if (!Resolve(target, ref inventory, false))
-            return new(trueName, gender, ageString, string.Empty);
-
-        string? presumedJob = null;
-        string? presumedName = null;
-
-        // Get their name and job from their ID for their presumed name.
-        if (_idCard.TryFindIdCard(target, out var id))
-        {
-            presumedName = string.IsNullOrWhiteSpace(id.Comp.FullName) ? null : id.Comp.FullName;
-            presumedJob = id.Comp.LocalizedJobTitle?.ToLowerInvariant();
-        }
-
-        // If it didn't find a job, that's fine.
-        return new(trueName, gender, ageString, presumedName, presumedJob);
-    }
-
-    #endregion
-}
index 3967e320a8e22bab9a91ee8917af024e306be0de..ba9487b031f5e6feb1a92a734af7752a345dd1cd 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Server.Access.Systems;
 using Content.Server.Humanoid;
-using Content.Server.IdentityManagement;
 using Content.Server.Mind;
 using Content.Server.PDA;
 using Content.Server.Station.Components;
@@ -11,6 +10,7 @@ using Content.Shared.Clothing;
 using Content.Shared.DetailExaminable;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.IdentityManagement;
 using Content.Shared.PDA;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
index a3d0b4997065a7dfad1354211ba3fc078220d861..10eedd7562dcb7eb0513854163dc4499912aa29b 100644 (file)
@@ -215,26 +215,6 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
         return false;
     }
 
-    /// <summary>
-    ///     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.
-    /// </summary>
-    /// <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>(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);
-    }
-
     /// <summary>
     /// Gets a random record from the station's record entries.
     /// </summary>
@@ -257,26 +237,6 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
         return ent.Comp.Records.TryGetRecordEntry(key, 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, false))
-            return null;
-
-        foreach (var (id, record) in GetRecordsOfType<GeneralStationRecord>(station, records))
-        {
-            if (record.Name == name)
-                return id;
-        }
-
-        return null;
-    }
-
     /// <summary>
     /// Get the name for a record, or an empty string if it has no record.
     /// </summary>
@@ -288,21 +248,6 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
         return record.Name;
     }
 
-    /// <summary>
-    ///     Gets all records of a specific type from a station.
-    /// </summary>
-    /// <param name="station">The station to get the records from.</param>
-    /// <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<(uint, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
-    {
-        if (!Resolve(station, ref records))
-            return Array.Empty<(uint, T)>();
-
-        return records.Records.GetRecordsOfType<T>();
-    }
-
     /// <summary>
     ///     Adds a new record entry to a station's record set.
     /// </summary>
index 8b5db4561cca3d8cc62003c0ce12235a331e30ae..7cdcec78c2aff93fa68cf9971a28d204253ee2d5 100644 (file)
@@ -6,7 +6,6 @@ using Content.Server.Chat.Managers;
 using Content.Server.Ghost;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Humanoid;
-using Content.Server.IdentityManagement;
 using Content.Server.Inventory;
 using Content.Server.Mind;
 using Content.Server.NPC;
@@ -39,6 +38,7 @@ using Content.Shared.Prying.Components;
 using Content.Shared.Traits.Assorted;
 using Robust.Shared.Audio.Systems;
 using Content.Shared.Ghost.Roles.Components;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Tag;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
index d3d366ecf95d102a09e1acb1233fdf589d2a8736..7b894f0087c2ce69e58c321a7739e35e96c8abef 100644 (file)
@@ -1,6 +1,44 @@
+using Content.Shared.IdentityManagement;
+using Content.Shared.Security;
+using Content.Shared.Security.Components;
+using Content.Shared.Station;
+using Content.Shared.StationRecords;
+
 namespace Content.Shared.CriminalRecords.Systems;
 
 /// <summary>
 /// Station records aren't predicted, just exists for access.
 /// </summary>
-public abstract class SharedCriminalRecordsConsoleSystem : EntitySystem;
+public abstract class SharedCriminalRecordsConsoleSystem : EntitySystem
+{
+    [Dependency] private readonly SharedCriminalRecordsSystem _criminalRecords = default!;
+    [Dependency] private readonly SharedStationRecordsSystem _records = default!;
+    [Dependency] private readonly SharedStationSystem _station = default!;
+
+    /// <summary>
+    /// Checks if the new identity's name has a criminal record attached to it, and gives the entity the icon that
+    /// belongs to the status if it does.
+    /// </summary>
+    public void CheckNewIdentity(EntityUid uid)
+    {
+        var name = Identity.Name(uid, EntityManager);
+        var xform = Transform(uid);
+
+        // TODO use the entity's station? Not the station of the map that it happens to currently be on?
+        var station = _station.GetStationInMap(xform.MapID);
+
+        if (station != null && _records.GetRecordByName(station.Value, name) is { } id)
+        {
+            if (_records.TryGetRecord<CriminalRecord>(new StationRecordKey(id, station.Value),
+                    out var record))
+            {
+                if (record.Status != SecurityStatus.None)
+                {
+                    _criminalRecords.SetCriminalIcon(name, record.Status, uid);
+                    return;
+                }
+            }
+        }
+        RemComp<CriminalRecordComponent>(uid);
+    }
+}
index e88b99b5939ac0287c3cfcd7d98f0c61f8ad117e..401ba0404f8da401063e998820b6e3b0ff4f918a 100644 (file)
@@ -40,7 +40,7 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
     [Dependency] private readonly ISerializationManager _serManager = default!;
     [Dependency] private readonly MarkingManager _markingManager = default!;
     [Dependency] private readonly GrammarSystem _grammarSystem = default!;
-    [Dependency] private readonly SharedIdentitySystem _identity = default!;
+    [Dependency] private readonly IdentitySystem _identity = default!;
 
     public static readonly ProtoId<SpeciesPrototype> DefaultSpecies = "Human";
 
index 308d9c0bf7aa666ee281d67f353c5086ae58599b..cc92a4c078eb5f7bdb6dc57c5eabc3a21c1cae84 100644 (file)
@@ -1,12 +1,13 @@
 using Content.Shared.Inventory;
 using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.IdentityManagement.Components;
 
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class IdentityBlockerComponent : Component
 {
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool Enabled = true;
 
     /// <summary>
@@ -16,6 +17,8 @@ public sealed partial class IdentityBlockerComponent : Component
     public IdentityBlockerCoverage Coverage = IdentityBlockerCoverage.FULL;
 }
 
+[Flags]
+[Serializable, NetSerializable]
 public enum IdentityBlockerCoverage
 {
     NONE  = 0,
index 5e4c4531c17c48529d3ec2b1f03ff6f5af5427ec..86c07b307f33ce5e9d729a237860f81797379dd8 100644 (file)
@@ -1,5 +1,6 @@
 using Robust.Shared.Containers;
 using Robust.Shared.Enums;
+using Robust.Shared.GameStates;
 
 namespace Content.Shared.IdentityManagement.Components;
 
@@ -10,7 +11,7 @@ namespace Content.Shared.IdentityManagement.Components;
 /// <remarks>
 ///     This is a <see cref="ContainerSlot"/> and not just a datum entity because we do sort of care that it gets deleted and sent with the user.
 /// </remarks>
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent]
 public sealed partial class IdentityComponent : Component
 {
     [ViewVariables]
diff --git a/Content.Shared/IdentityManagement/IdentitySystem.cs b/Content.Shared/IdentityManagement/IdentitySystem.cs
new file mode 100644 (file)
index 0000000..7c559df
--- /dev/null
@@ -0,0 +1,241 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Clothing;
+using Content.Shared.CriminalRecords.Systems;
+using Content.Shared.Database;
+using Content.Shared.Hands;
+using Content.Shared.Humanoid;
+using Content.Shared.IdentityManagement.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Inventory.Events;
+using Robust.Shared.Containers;
+using Robust.Shared.Enums;
+using Robust.Shared.GameObjects.Components.Localization;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.IdentityManagement;
+
+/// <summary>
+/// Responsible for updating the identity of an entity on init or clothing equip/unequip.
+/// </summary>
+public sealed class IdentitySystem : EntitySystem
+{
+    [Dependency] private readonly GrammarSystem _grammarSystem = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedCriminalRecordsConsoleSystem _criminalRecordsConsole = default!;
+    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly SharedIdCardSystem _idCard = default!;
+
+    // The name of the container holding the identity entity
+    private const string SlotName = "identity";
+
+    // Recycled hashset for tracking identities each tick that need to update
+    private readonly HashSet<EntityUid> _queuedIdentityUpdates = new();
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<IdentityBlockerComponent, SeeIdentityAttemptEvent>(OnSeeIdentity);
+        SubscribeLocalEvent<IdentityBlockerComponent, InventoryRelayedEvent<SeeIdentityAttemptEvent>>(OnRelaySeeIdentity);
+        SubscribeLocalEvent<IdentityBlockerComponent, ItemMaskToggledEvent>(OnMaskToggled);
+
+        SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<IdentityComponent, ComponentInit>(OnComponentInit);
+
+        SubscribeLocalEvent<IdentityComponent, DidEquipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+        SubscribeLocalEvent<IdentityComponent, DidEquipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+        SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+        SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+        SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+        SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
+    }
+
+    /// <summary>
+    /// Iterates through all identities that need to be updated.
+    /// </summary>
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        foreach (var ent in _queuedIdentityUpdates)
+        {
+            if (!TryComp<IdentityComponent>(ent, out var identity))
+                continue;
+
+            UpdateIdentityInfo((ent, identity));
+        }
+
+        _queuedIdentityUpdates.Clear();
+    }
+
+    #region Event Handlers
+
+    // Creates an identity entity, and store it in the identity container
+    private void OnMapInit(Entity<IdentityComponent> ent, ref MapInitEvent args)
+    {
+        var ident = Spawn(null, Transform(ent).Coordinates);
+
+        _metaData.SetEntityName(ident, "identity");
+        QueueIdentityUpdate(ent);
+        _container.Insert(ident, ent.Comp.IdentityEntitySlot);
+    }
+
+    private void OnComponentInit(Entity<IdentityComponent> ent, ref ComponentInit args)
+    {
+        ent.Comp.IdentityEntitySlot = _container.EnsureContainer<ContainerSlot>(ent, SlotName);
+    }
+
+    // Adds an identity blocker's coverage, and cancels the event if coverage is complete.
+    private void OnSeeIdentity(Entity<IdentityBlockerComponent> ent, ref SeeIdentityAttemptEvent args)
+    {
+        if (ent.Comp.Enabled)
+        {
+            args.TotalCoverage |= ent.Comp.Coverage;
+            if (args.TotalCoverage == IdentityBlockerCoverage.FULL)
+                args.Cancel();
+        }
+    }
+
+    private void OnRelaySeeIdentity(Entity<IdentityBlockerComponent> ent, ref InventoryRelayedEvent<SeeIdentityAttemptEvent> args)
+    {
+        OnSeeIdentity(ent, ref args.Args);
+    }
+
+    // Toggles if a mask is hiding the identity.
+    private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
+    {
+        ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
+        Dirty(ent);
+    }
+
+    #endregion
+
+    /// <summary>
+    /// Queues an identity update to the start of the next tick.
+    /// </summary>
+    public void QueueIdentityUpdate(EntityUid uid)
+    {
+        if (_timing.ApplyingState)
+            return;
+
+        _queuedIdentityUpdates.Add(uid);
+    }
+    #region Private API
+
+    /// <summary>
+    /// Updates the metadata name for the id(entity) from the current state of the character.
+    /// </summary>
+    private void UpdateIdentityInfo(Entity<IdentityComponent> ent)
+    {
+        if (ent.Comp.IdentityEntitySlot.ContainedEntity is not { } ident)
+            return;
+
+        var representation = GetIdentityRepresentation(ent.Owner);
+        var name = GetIdentityName(ent, representation);
+
+        // Clone the old entity's grammar to the identity entity, for loc purposes.
+        if (TryComp<GrammarComponent>(ent, out var grammar))
+        {
+            var identityGrammar = EnsureComp<GrammarComponent>(ident);
+            identityGrammar.Attributes.Clear();
+
+            foreach (var (k, v) in grammar.Attributes)
+            {
+                identityGrammar.Attributes.Add(k, v);
+            }
+
+            // If presumed name is null and we're using that, we set proper noun to be false ("the old woman")
+            if (name != representation.TrueName && representation.PresumedName == null)
+                _grammarSystem.SetProperNoun((ident, identityGrammar), false);
+
+            Dirty(ident, identityGrammar);
+        }
+
+        if (name == Name(ident))
+            return;
+
+        _metaData.SetEntityName(ident, name);
+
+        _adminLog.Add(LogType.Identity, LogImpact.Medium, $"{ToPrettyString(ent)} changed identity to {name}");
+        var identityChangedEvent = new IdentityChangedEvent(ent, ident);
+        RaiseLocalEvent(ent, ref identityChangedEvent);
+        SetIdentityCriminalIcon(ent);
+    }
+
+    /// <summary>
+    /// When the identity of a person is changed, searches the criminal records to see if the name of the new identity
+    /// has a record. If the new name has a criminal status attached to it, the person will get the criminal status
+    /// until they change identity again.
+    /// </summary>
+    private void SetIdentityCriminalIcon(EntityUid uid)
+    {
+        _criminalRecordsConsole.CheckNewIdentity(uid);
+    }
+
+    /// <summary>
+    /// Attempts to get an entity's name. Cancelled if the entity has full coverage from <see cref="IdentityBlockerComponent"/>.
+    /// </summary>
+    /// <param name="target">The entity being targeted.</param>
+    /// <param name="representation">The data structure containing an entity's identities.</param>
+    /// <returns>
+    /// An entity's real name if <see cref="SeeIdentityAttemptEvent"/> isn't cancelled,
+    /// or a hidden identity such as a fake ID or fully hidden identity like "middle-aged man".
+    /// </returns>
+    private string GetIdentityName(EntityUid target, IdentityRepresentation representation)
+    {
+        var ev = new SeeIdentityAttemptEvent();
+
+        RaiseLocalEvent(target, ev);
+        return representation.ToStringKnown(!ev.Cancelled);
+    }
+
+    /// <summary>
+    /// Gets an 'identity representation' of an entity, with their true name being the entity name
+    /// and their 'presumed name' and 'presumed job' being the name/job on their ID card, if they have one.
+    /// </summary>
+    private IdentityRepresentation GetIdentityRepresentation(Entity<InventoryComponent?, HumanoidAppearanceComponent?> target)
+    {
+        var age = 18;
+        var gender = Gender.Epicene;
+        var species = SharedHumanoidAppearanceSystem.DefaultSpecies;
+
+        // Always use their actual age and gender, since that can't really be changed by an ID.
+        if (Resolve(target, ref target.Comp2, false))
+        {
+            gender = target.Comp2.Gender;
+            age = target.Comp2.Age;
+            species = target.Comp2.Species;
+        }
+
+        var ageString = _humanoid.GetAgeRepresentation(species, age);
+        var trueName = Name(target);
+        if (!Resolve(target, ref target.Comp1, false))
+            return new(trueName, gender, ageString, string.Empty);
+
+        string? presumedJob = null;
+        string? presumedName = null;
+
+        // Get their name and job from their ID for their presumed name.
+        if (_idCard.TryFindIdCard(target, out var id))
+        {
+            presumedName = string.IsNullOrWhiteSpace(id.Comp.FullName) ? null : id.Comp.FullName;
+            presumedJob = id.Comp.LocalizedJobTitle?.ToLowerInvariant();
+        }
+
+        // If it didn't find a job, that's fine.
+        return new(trueName, gender, ageString, presumedName, presumedJob);
+    }
+
+    #endregion
+}
+
+/// <summary>
+/// Gets called whenever an entity changes their identity.
+/// </summary>
+[ByRefEvent]
+public record struct IdentityChangedEvent(EntityUid CharacterEntity, EntityUid IdentityEntity);
diff --git a/Content.Shared/IdentityManagement/SharedIdentitySystem.cs b/Content.Shared/IdentityManagement/SharedIdentitySystem.cs
deleted file mode 100644 (file)
index 6b03dc3..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-using Content.Shared.Clothing;
-using Content.Shared.IdentityManagement.Components;
-using Content.Shared.Inventory;
-using Robust.Shared.Containers;
-
-namespace Content.Shared.IdentityManagement;
-
-public abstract class SharedIdentitySystem : EntitySystem
-{
-    [Dependency] private readonly SharedContainerSystem _container = default!;
-    private static string SlotName = "identity";
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<IdentityComponent, ComponentInit>(OnComponentInit);
-        SubscribeLocalEvent<IdentityBlockerComponent, SeeIdentityAttemptEvent>(OnSeeIdentity);
-        SubscribeLocalEvent<IdentityBlockerComponent, InventoryRelayedEvent<SeeIdentityAttemptEvent>>((e, c, ev) => OnSeeIdentity(e, c, ev.Args));
-        SubscribeLocalEvent<IdentityBlockerComponent, ItemMaskToggledEvent>(OnMaskToggled);
-    }
-
-    private void OnSeeIdentity(EntityUid uid, IdentityBlockerComponent component, SeeIdentityAttemptEvent args)
-    {
-        if (component.Enabled)
-        {
-            args.TotalCoverage |= component.Coverage;
-            if(args.TotalCoverage == IdentityBlockerCoverage.FULL)
-                args.Cancel();
-        }
-    }
-
-    protected virtual void OnComponentInit(EntityUid uid, IdentityComponent component, ComponentInit args)
-    {
-        component.IdentityEntitySlot = _container.EnsureContainer<ContainerSlot>(uid, SlotName);
-    }
-
-    private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
-    {
-        ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
-    }
-
-    /// <summary>
-    /// Queues an identity update to the start of the next tick.
-    /// </summary>
-    public virtual void QueueIdentityUpdate(EntityUid uid) { }
-}
-/// <summary>
-///     Gets called whenever an entity changes their identity.
-/// </summary>
-[ByRefEvent]
-public record struct IdentityChangedEvent(EntityUid CharacterEntity, EntityUid IdentityEntity);
index c2cc418f54737cb91176771333f0c40005ff681a..e04de09d658469cb07ce83313665116423d1ecff 100644 (file)
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
 namespace Content.Shared.StationRecords;
 
 public abstract class SharedStationRecordsSystem : EntitySystem
@@ -40,4 +42,60 @@ public abstract class SharedStationRecordsSystem : EntitySystem
         }
         return result;
     }
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    /// <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. Always false on client.</returns>
+    public bool TryGetRecord<T>(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);
+    }
+
+    /// <summary>
+    ///     Gets all records of a specific type from a station.
+    /// </summary>
+    /// <param name="station">The station to get the records from.</param>
+    /// <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. Always empty on client.</returns>
+    public IEnumerable<(uint, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
+    {
+        if (!Resolve(station, ref records))
+            return Array.Empty<(uint, T)>();
+
+        return records.Records.GetRecordsOfType<T>();
+    }
+
+    /// <summary>
+    /// Returns an id if a record with the same name exists.
+    /// </summary>
+    /// <remarks>
+    /// Linear search so O(n) time complexity.
+    /// </remarks>
+    /// <returns>Returns a station record id. Always null on client.</returns>
+    public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null)
+    {
+        if (!Resolve(station, ref records, false))
+            return null;
+
+        foreach (var (id, record) in GetRecordsOfType<GeneralStationRecord>(station, records))
+        {
+            if (record.Name == name)
+                return id;
+        }
+
+        return null;
+    }
 }
similarity index 98%
rename from Content.Server/StationRecords/StationRecordSet.cs
rename to Content.Shared/StationRecords/StationRecordSet.cs
index b5a4501cea7d9c540afd87a7199e9492637e27a7..169e5843d24a7d4fe4a8eb0dc5f67331c558f101 100644 (file)
@@ -1,9 +1,8 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using Content.Shared.StationRecords;
 using Robust.Shared.Utility;
 
-namespace Content.Server.StationRecords;
+namespace Content.Shared.StationRecords;
 
 /// <summary>
 ///     Set of station records for a single station. StationRecordsComponent stores these.
similarity index 70%
rename from Content.Server/StationRecords/Components/StationRecordsComponent.cs
rename to Content.Shared/StationRecords/StationRecordsComponent.cs
index 4ea65522f4b9602cd5a1086324c2dc57248869a1..66c60dddc67803f0e4652cce77006b52217174d7 100644 (file)
@@ -1,8 +1,6 @@
-using Content.Server.StationRecords.Systems;
+namespace Content.Shared.StationRecords;
 
-namespace Content.Server.StationRecords;
-
-[Access(typeof(StationRecordsSystem))]
+[Access(typeof(SharedStationRecordsSystem))]
 [RegisterComponent]
 public sealed partial class StationRecordsComponent : Component
 {
index 246c6a8c7ab85620fdd08e7c1b5b158d76dec84b..db27bd7f74b9b5126471ce2e528a3b0b459e3269 100644 (file)
@@ -13,7 +13,7 @@ public sealed class DnaScrambleOnTriggerSystem : EntitySystem
 {
     [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
-    [Dependency] private readonly SharedIdentitySystem _identity = default!;
+    [Dependency] private readonly IdentitySystem _identity = default!;
     [Dependency] private readonly SharedForensicsSystem _forensics = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly INetManager _net = default!;