Predict Mind State Examine (#42253)

* init

* review

* i might be stupid

* docs

* datafieldn't

* update comments
This commit is contained in:
ScarKy0
2026-01-11 00:02:56 +01:00
committed by GitHub
parent c3d7652620
commit 5d9371931a
7 changed files with 160 additions and 50 deletions

View 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));
}

View 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

View File

@@ -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
}

View File

@@ -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);
}
}

View 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

View 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:

View File

@@ -163,7 +163,7 @@
- type: Crawler
- type: Dna
- type: MindContainer
showExamineInfo: true
- type: MindExaminable
- type: CanEnterCryostorage
- type: InteractionPopup
successChance: 1