]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict Mind State Examine (#42253)
authorScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Sat, 10 Jan 2026 23:02:56 +0000 (00:02 +0100)
committerGitHub <noreply@github.com>
Sat, 10 Jan 2026 23:02:56 +0000 (23:02 +0000)
* init

* review

* i might be stupid

* docs

* datafieldn't

* update comments

Content.Client/SSDIndicator/SSDIndicatorSystem.cs
Content.Shared/Mind/Components/MindContainerComponent.cs
Content.Shared/Mind/Components/MindExaminableComponent.cs [new file with mode: 0644]
Content.Shared/Mind/MindExamineSystem.cs [new file with mode: 0644]
Content.Shared/Mind/SharedMindSystem.cs
Resources/Prototypes/Entities/Mobs/Player/familiars.yml
Resources/Prototypes/Entities/Mobs/Species/base.yml

index e7311953170b7dd930ed059308f38721110d59b7..370bc902c207843c00dab3411256b898ce19f74e 100644 (file)
@@ -32,8 +32,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
             _cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
             !_mobState.IsDead(uid) &&
             !HasComp<ActiveNPCComponent>(uid) &&
-            TryComp<MindContainerComponent>(uid, out var mindContainer) &&
-            mindContainer.ShowExamineInfo)
+            HasComp<MindExaminableComponent>(uid))
         {
             args.StatusIcons.Add(_prototype.Index(component.Icon));
         }
index 760f5026fad8674510dbb7670aaa82310dc0e3d9..dd4948cea731264681de12624a6f9b66e8a9853c 100644 (file)
@@ -14,7 +14,7 @@ public sealed partial class MindContainerComponent : Component
     ///     The mind controlling this mob. Can be null.
     /// </summary>
     [DataField, AutoNetworkedField]
-    public EntityUid? Mind { get; set; }
+    public EntityUid? Mind;
 
     /// <summary>
     ///     True if we have a mind, false otherwise.
@@ -22,19 +22,11 @@ public sealed partial class MindContainerComponent : Component
     [MemberNotNullWhen(true, nameof(Mind))]
     public bool HasMind => Mind != null;
 
-    /// <summary>
-    ///     Whether examining should show information about the mind or not.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("showExamineInfo"), AutoNetworkedField]
-    public bool ShowExamineInfo { get; set; }
-
     /// <summary>
     ///     Whether the mind will be put on a ghost after this component is shutdown.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("ghostOnShutdown")]
-    public bool GhostOnShutdown { get; set; } = true;
+    [DataField]
+    public bool GhostOnShutdown = true;
 }
 
 public abstract class MindEvent : EntityEventArgs
diff --git a/Content.Shared/Mind/Components/MindExaminableComponent.cs b/Content.Shared/Mind/Components/MindExaminableComponent.cs
new file mode 100644 (file)
index 0000000..880c0f3
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Shared.Examine;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Mind.Components;
+
+/// <summary>
+/// This component adds an examine text to the owner entity based on the state of their mind.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(MindExamineSystem))]
+public sealed partial class MindExaminableComponent : Component
+{
+    /// <summary>
+    /// The state the mind is currently in.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public MindState State = MindState.None;
+}
+
+/// <summary>
+/// The states for when an entity with a mind is examined.
+/// </summary>
+[Serializable, NetSerializable]
+public enum MindState : byte
+{
+    None, // No text
+    Dead, // Player is dead but still connected
+    Catatonic, // Entity is alive but has no mind attached to it.
+    SSD, // Player disconnected while alive
+    DeadSSD, // Player died and disconnected
+    Irrecoverable // Entity is dead and has no mind attached
+}
diff --git a/Content.Shared/Mind/MindExamineSystem.cs b/Content.Shared/Mind/MindExamineSystem.cs
new file mode 100644 (file)
index 0000000..bae0476
--- /dev/null
@@ -0,0 +1,119 @@
+using Content.Shared.Examine;
+using Content.Shared.Mind.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Systems;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+
+namespace Content.Shared.Mind;
+
+public sealed class MindExamineSystem : EntitySystem
+{
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly ISharedPlayerManager _player = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<MindExaminableComponent, ExaminedEvent>(OnExamined);
+        SubscribeLocalEvent<MindExaminableComponent, ComponentStartup>((e, ref _) => RefreshMindStatus(e.AsNullable()));
+        SubscribeLocalEvent<MindExaminableComponent, MindAddedMessage>((e, ref _) => RefreshMindStatus(e.AsNullable()));
+        SubscribeLocalEvent<MindExaminableComponent, MindRemovedMessage>((e, ref _) => RefreshMindStatus(e.AsNullable()));
+        SubscribeLocalEvent<MindExaminableComponent, MobStateChangedEvent>((e, ref _) => RefreshMindStatus(e.AsNullable()));
+
+        SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
+        SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
+    }
+
+    private void OnExamined(Entity<MindExaminableComponent> ent, ref ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        var message = ent.Comp.State switch
+        {
+            MindState.Irrecoverable => $"[color=mediumpurple]{Loc.GetString("comp-mind-examined-dead-and-irrecoverable", ("ent", ent.Owner))}[/color]",
+            MindState.DeadSSD => $"[color=yellow]{Loc.GetString("comp-mind-examined-dead-and-ssd", ("ent", ent.Owner))}[/color]",
+            MindState.Dead => $"[color=red]{Loc.GetString("comp-mind-examined-dead", ("ent", ent.Owner))}[/color]",
+            MindState.Catatonic => $"[color=mediumpurple]{Loc.GetString("comp-mind-examined-catatonic", ("ent", ent.Owner))}[/color]",
+            MindState.SSD => $"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", ent.Owner))}[/color]",
+            _ => null,
+        };
+
+        if (message != null)
+            args.PushMarkup(message);
+    }
+
+    private void OnPlayerAttached(PlayerAttachedEvent args)
+    {
+        // We use the broadcasted event because we need to access the body of a ghost if it disconnects.
+        // DeadSSD does not check if a player is attached, but if the session is valid (connected).
+        // To properly track that, we subscribe to the broadcast version of this event
+        // and update the mind status of the original entity accordingly.
+        // Otherwise, if you ghost out and THEN disconnect, it would not update your status as it gets raised on your ghost and not your body.
+        if (!_mind.TryGetMind(args.Entity, out _, out var mindComp))
+            return;
+
+        if (mindComp.OwnedEntity is not { } refreshEnt)
+            return;
+
+        RefreshMindStatus(refreshEnt);
+    }
+
+    private void OnPlayerDetached(PlayerDetachedEvent args)
+    {
+        // Same reason as in the subscription above.
+        if (!_mind.TryGetMind(args.Entity, out _, out var mindComp))
+            return;
+
+        if (mindComp.OwnedEntity is not { } refreshEnt)
+            return;
+
+        RefreshMindStatus(refreshEnt);
+    }
+
+
+    public void RefreshMindStatus(Entity<MindExaminableComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return;
+
+        // Only allow the local client to handle this.
+        // This is because the mind is only networked to the owner, and other clients will always be wrong.
+        // So instead, we do this on server and dirty the result to the client.
+        // And since it is stored on the component, the text won't flicker anymore.
+        // Will cause a small jump when examined during networking due to the server update coming in.
+        if (_net.IsClient && _player.LocalEntity != ent)
+            return;
+
+        var dead = _mobState.IsDead(ent);
+        _mind.TryGetMind(ent.Owner, out _, out var mindComp);
+        var hasUserId = mindComp?.UserId;
+        var hasActiveSession = hasUserId != null && _player.ValidSessionId(hasUserId.Value);
+
+        // Scenarios:
+        // 1. Dead + No User ID: Entity is dead and has no mind attached
+        // 2. Dead + Has User ID + No Session: Player died and disconnected
+        // 3. Dead + Has Session: Player is dead but still connected
+        // 4. Alive + No User ID: Entity is alive but has no mind attached to it
+        // 5. Alive + No Session: Player disconnected while alive (SSD)
+
+        if (dead && hasUserId == null)
+            ent.Comp.State = MindState.Irrecoverable;
+        else if (dead && !hasActiveSession)
+            ent.Comp.State = MindState.DeadSSD;
+        else if (dead)
+            ent.Comp.State = MindState.Dead;
+        else if (hasUserId == null)
+            ent.Comp.State = MindState.Catatonic;
+        else if (!hasActiveSession)
+            ent.Comp.State = MindState.SSD;
+        else
+            ent.Comp.State = MindState.None;
+
+        Dirty(ent);
+    }
+}
index a7d3357f00f822e8859d6ecb4b616abd7705d4b2..7fa77dafb3f7ccc2c346e23d2e76665e9b8d102f 100644 (file)
@@ -50,8 +50,8 @@ public abstract partial class SharedMindSystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined);
         SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
+
         SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
         SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
         SubscribeLocalEvent<MindComponent, ComponentStartup>(OnMindStartup);
@@ -164,39 +164,6 @@ public abstract partial class SharedMindSystem : EntitySystem
             UnVisit(component.MindId.Value);
     }
 
-    private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, ExaminedEvent args)
-    {
-        if (!mindContainer.ShowExamineInfo || !args.IsInDetailsRange)
-            return;
-
-        // TODO: Move this out of the SharedMindSystem into its own comp and predict it
-        if (_net.IsClient)
-            return;
-
-        var dead = _mobState.IsDead(uid);
-        var mind = CompOrNull<MindComponent>(mindContainer.Mind);
-        var hasUserId = mind?.UserId;
-        var hasActiveSession = hasUserId != null && _playerManager.ValidSessionId(hasUserId.Value);
-
-        // Scenarios:
-        // 1. Dead + No User ID: Entity is permanently dead with no player ever attached
-        // 2. Dead + Has User ID + No Session: Player died and disconnected
-        // 3. Dead + Has Session: Player is dead but still connected
-        // 4. Alive + No User ID: Entity was never controlled by a player
-        // 5. Alive + No Session: Player disconnected while alive (SSD)
-
-        if (dead && hasUserId == null)
-            args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-dead-and-irrecoverable", ("ent", uid))}[/color]");
-        else if (dead && !hasActiveSession)
-            args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-dead-and-ssd", ("ent", uid))}[/color]");
-        else if (dead)
-            args.PushMarkup($"[color=red]{Loc.GetString("comp-mind-examined-dead", ("ent", uid))}[/color]");
-        else if (hasUserId == null)
-            args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-catatonic", ("ent", uid))}[/color]");
-        else if (!hasActiveSession)
-            args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]");
-    }
-
     /// <summary>
     /// Checks to see if the user's mind prevents them from suicide
     /// Handles the suicide event without killing the user if true
index 320a2b9bffe8d7303ad8df06fa0dd076f67fe1be..bfa8a591a68832abd7e00e511b115ac79341759e 100644 (file)
@@ -26,7 +26,7 @@
     tags:
     - Chapel
   - type: MindContainer
-    showExamineInfo: true
+  - type: MindExaminable
   - type: NpcFactionMember
     factions:
     - PetsNT
@@ -87,7 +87,7 @@
     tags:
     - Chapel
   - type: MindContainer
-    showExamineInfo: true
+  - type: MindExaminable
   - type: Familiar
   - type: Vocal
     sounds:
index 4df9ddcaacced8fb47254daf2d11cae802a16abc..335b9c7a5de136a7ec71b370ed7aa635f284f597 100644 (file)
   - type: Crawler
   - type: Dna
   - type: MindContainer
-    showExamineInfo: true
+  - type: MindExaminable
   - type: CanEnterCryostorage
   - type: InteractionPopup
     successChance: 1