From: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:02:56 +0000 (+0100) Subject: Predict Mind State Examine (#42253) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=5d9371931a00e9c2811f6f0869ec2b23e1099015;p=space-station-14.git Predict Mind State Examine (#42253) * init * review * i might be stupid * docs * datafieldn't * update comments --- diff --git a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs index e731195317..370bc902c2 100644 --- a/Content.Client/SSDIndicator/SSDIndicatorSystem.cs +++ b/Content.Client/SSDIndicator/SSDIndicatorSystem.cs @@ -32,8 +32,7 @@ public sealed class SSDIndicatorSystem : EntitySystem _cfg.GetCVar(CCVars.ICShowSSDIndicator) && !_mobState.IsDead(uid) && !HasComp(uid) && - TryComp(uid, out var mindContainer) && - mindContainer.ShowExamineInfo) + HasComp(uid)) { args.StatusIcons.Add(_prototype.Index(component.Icon)); } diff --git a/Content.Shared/Mind/Components/MindContainerComponent.cs b/Content.Shared/Mind/Components/MindContainerComponent.cs index 760f5026fa..dd4948cea7 100644 --- a/Content.Shared/Mind/Components/MindContainerComponent.cs +++ b/Content.Shared/Mind/Components/MindContainerComponent.cs @@ -14,7 +14,7 @@ public sealed partial class MindContainerComponent : Component /// The mind controlling this mob. Can be null. /// [DataField, AutoNetworkedField] - public EntityUid? Mind { get; set; } + public EntityUid? Mind; /// /// 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; - /// - /// Whether examining should show information about the mind or not. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("showExamineInfo"), AutoNetworkedField] - public bool ShowExamineInfo { get; set; } - /// /// Whether the mind will be put on a ghost after this component is shutdown. /// - [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 index 0000000000..880c0f331d --- /dev/null +++ b/Content.Shared/Mind/Components/MindExaminableComponent.cs @@ -0,0 +1,33 @@ +using Content.Shared.Examine; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Mind.Components; + +/// +/// This component adds an examine text to the owner entity based on the state of their mind. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(MindExamineSystem))] +public sealed partial class MindExaminableComponent : Component +{ + /// + /// The state the mind is currently in. + /// + [DataField, AutoNetworkedField] + public MindState State = MindState.None; +} + +/// +/// The states for when an entity with a mind is examined. +/// +[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 index 0000000000..bae0476d96 --- /dev/null +++ b/Content.Shared/Mind/MindExamineSystem.cs @@ -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(OnExamined); + SubscribeLocalEvent((e, ref _) => RefreshMindStatus(e.AsNullable())); + SubscribeLocalEvent((e, ref _) => RefreshMindStatus(e.AsNullable())); + SubscribeLocalEvent((e, ref _) => RefreshMindStatus(e.AsNullable())); + SubscribeLocalEvent((e, ref _) => RefreshMindStatus(e.AsNullable())); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + } + + private void OnExamined(Entity 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 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); + } +} diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index a7d3357f00..7fa77dafb3 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -50,8 +50,8 @@ public abstract partial class SharedMindSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnSuicide); + SubscribeLocalEvent(OnVisitingTerminating); SubscribeLocalEvent(OnReset); SubscribeLocalEvent(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(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]"); - } - /// /// Checks to see if the user's mind prevents them from suicide /// Handles the suicide event without killing the user if true diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 320a2b9bff..bfa8a591a6 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -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: diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 4df9ddcaac..335b9c7a5d 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -163,7 +163,7 @@ - type: Crawler - type: Dna - type: MindContainer - showExamineInfo: true + - type: MindExaminable - type: CanEnterCryostorage - type: InteractionPopup successChance: 1