Predict Mind State Examine (#42253)
* init * review * i might be stupid * docs * datafieldn't * update comments
This commit is contained in:
parent
580ee0b13f
commit
e9b4130ec9
|
|
@ -33,8 +33,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
|
||||||
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
|
_cfg.GetCVar(CCVars.ICShowSSDIndicator) &&
|
||||||
!_mobState.IsDead(uid) &&
|
!_mobState.IsDead(uid) &&
|
||||||
!HasComp<ActiveNPCComponent>(uid) &&
|
!HasComp<ActiveNPCComponent>(uid) &&
|
||||||
TryComp<MindContainerComponent>(uid, out var mindContainer) &&
|
HasComp<MindExaminableComponent>(uid))
|
||||||
mindContainer.ShowExamineInfo)
|
|
||||||
{
|
{
|
||||||
// Begin DeltaV Addition
|
// Begin DeltaV Addition
|
||||||
var ev = new ShowSSDIndicatorEvent();
|
var ev = new ShowSSDIndicatorEvent();
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ public sealed partial class MindContainerComponent : Component
|
||||||
/// The mind controlling this mob. Can be null.
|
/// The mind controlling this mob. Can be null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public EntityUid? Mind { get; set; }
|
public EntityUid? Mind;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DeltaV: The first mind to control this mob. Will only be null if the mob never had a mind at all.
|
/// DeltaV: The first mind to control this mob. Will only be null if the mob never had a mind at all.
|
||||||
|
|
@ -28,19 +28,11 @@ public sealed partial class MindContainerComponent : Component
|
||||||
[MemberNotNullWhen(true, nameof(Mind))]
|
[MemberNotNullWhen(true, nameof(Mind))]
|
||||||
public bool HasMind => Mind != null;
|
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>
|
/// <summary>
|
||||||
/// Whether the mind will be put on a ghost after this component is shutdown.
|
/// Whether the mind will be put on a ghost after this component is shutdown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
[DataField("ghostOnShutdown")]
|
public bool GhostOnShutdown = true;
|
||||||
public bool GhostOnShutdown { get; set; } = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class MindEvent : EntityEventArgs
|
public abstract class MindEvent : EntityEventArgs
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
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;
|
||||||
|
using Content.Shared._DV.Mind; // DeltaV
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Begin DeltaV Addition
|
||||||
|
var ev = new ShowSSDIndicatorEvent();
|
||||||
|
RaiseLocalEvent(ent, ref ev);
|
||||||
|
if (ev.Hidden)
|
||||||
|
return;
|
||||||
|
// End DeltaV Addition
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,8 +52,8 @@ public abstract partial class SharedMindSystem : EntitySystem
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined);
|
|
||||||
SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
|
SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
|
||||||
|
|
||||||
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
|
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
|
||||||
SubscribeLocalEvent<MindComponent, ComponentStartup>(OnMindStartup);
|
SubscribeLocalEvent<MindComponent, ComponentStartup>(OnMindStartup);
|
||||||
|
|
@ -166,46 +166,6 @@ public abstract partial class SharedMindSystem : EntitySystem
|
||||||
UnVisit(component.MindId.Value);
|
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;
|
|
||||||
|
|
||||||
// Begin DeltaV Addition
|
|
||||||
var ev = new ShowSSDIndicatorEvent();
|
|
||||||
RaiseLocalEvent(uid, ref ev);
|
|
||||||
if (ev.Hidden)
|
|
||||||
return;
|
|
||||||
// End DeltaV Addition
|
|
||||||
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Checks to see if the user's mind prevents them from suicide
|
/// Checks to see if the user's mind prevents them from suicide
|
||||||
/// Handles the suicide event without killing the user if true
|
/// Handles the suicide event without killing the user if true
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
damageContainer: BiologicalMetaphysical
|
damageContainer: BiologicalMetaphysical
|
||||||
damageModifierSet: ManifestedSpirit
|
damageModifierSet: ManifestedSpirit
|
||||||
- type: MindContainer
|
- type: MindContainer
|
||||||
showExamineInfo: true
|
- type: MindExaminable
|
||||||
- type: NpcFactionMember
|
- type: NpcFactionMember
|
||||||
factions:
|
factions:
|
||||||
- PetsNT
|
- PetsNT
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
tags:
|
tags:
|
||||||
- Chapel
|
- Chapel
|
||||||
- type: MindContainer
|
- type: MindContainer
|
||||||
showExamineInfo: true
|
- type: MindExaminable
|
||||||
- type: Familiar
|
- type: Familiar
|
||||||
- type: Dispellable
|
- type: Dispellable
|
||||||
- type: Psionic #Nyano - Summary: makes psionic on creation.
|
- type: Psionic #Nyano - Summary: makes psionic on creation.
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
- type: Crawler
|
- type: Crawler
|
||||||
- type: Dna
|
- type: Dna
|
||||||
- type: MindContainer
|
- type: MindContainer
|
||||||
showExamineInfo: true
|
- type: MindExaminable
|
||||||
- type: CanEnterCryostorage
|
- type: CanEnterCryostorage
|
||||||
- type: InteractionPopup
|
- type: InteractionPopup
|
||||||
successChance: 1
|
successChance: 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue