using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.GameTicking; using Content.Shared.Humanoid; using Content.Shared.Interaction.Events; using Content.Shared.Mind.Components; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Objectives.Systems; using Content.Shared.Players; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Players; using Robust.Shared.Utility; namespace Content.Shared.Mind; public abstract class SharedMindSystem : EntitySystem { [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedObjectivesSystem _objectives = default!; [Dependency] private readonly SharedPlayerSystem _player = default!; [Dependency] private readonly MetaDataSystem _metadata = default!; // This is dictionary is required to track the minds of disconnected players that may have had their entity deleted. protected readonly Dictionary UserMinds = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnSuicide); SubscribeLocalEvent(OnVisitingTerminating); SubscribeLocalEvent(OnReset); } public override void Shutdown() { base.Shutdown(); WipeAllMinds(); } private void OnReset(RoundRestartCleanupEvent ev) { WipeAllMinds(); } public virtual void WipeAllMinds() { foreach (var mind in UserMinds.Values) { WipeMind(mind); } DebugTools.Assert(UserMinds.Count == 0); } public EntityUid? GetMind(NetUserId user) { TryGetMind(user, out var mind, out _); return mind; } public virtual bool TryGetMind(NetUserId user, [NotNullWhen(true)] out EntityUid? mindId, [NotNullWhen(true)] out MindComponent? mind) { if (UserMinds.TryGetValue(user, out var mindIdValue) && TryComp(mindIdValue, out mind)) { DebugTools.Assert(mind.UserId == user); mindId = mindIdValue; return true; } mindId = null; mind = null; return false; } private void OnVisitingTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args) { if (component.MindId != null) UnVisit(component.MindId.Value); } private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, ExaminedEvent args) { if (!mindContainer.ShowExamineInfo || !args.IsInDetailsRange) return; var dead = _mobState.IsDead(uid); var hasSession = CompOrNull(mindContainer.Mind)?.Session; if (dead && !mindContainer.HasMind) args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-dead-and-irrecoverable", ("ent", uid))}[/color]"); else if (dead && hasSession == null) 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 (!mindContainer.HasMind) args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-catatonic", ("ent", uid))}[/color]"); else if (hasSession == null) args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]"); } private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args) { if (args.Handled) return; if (TryComp(component.Mind, out MindComponent? mind) && mind.PreventSuicide) { args.BlockSuicideAttempt(true); } } public EntityUid? GetMind(EntityUid uid, MindContainerComponent? mind = null) { if (!Resolve(uid, ref mind)) return null; if (mind.HasMind) return mind.Mind; return null; } public EntityUid CreateMind(NetUserId? userId, string? name = null) { var mindId = Spawn(null, MapCoordinates.Nullspace); _metadata.SetEntityName(mindId, name == null ? "mind" : $"mind ({name})"); var mind = EnsureComp(mindId); mind.CharacterName = name; SetUserId(mindId, userId, mind); return mindId; } /// /// True if the OwnedEntity of this mind is physically dead. /// This specific definition, as opposed to CharacterDeadIC, is used to determine if ghosting should allow return. /// public bool IsCharacterDeadPhysically(MindComponent mind) { // This is written explicitly so that the logic can be understood. // But it's also weird and potentially situational. // Specific considerations when updating this: // + Does being turned into a borg (if/when implemented) count as dead? // *If not, add specific conditions to users of this property where applicable.* // + Is being transformed into a donut 'dead'? // TODO: Consider changing the way ghost roles work. // Mind is an *IC* mind, therefore ghost takeover is IC revival right now. // + Is it necessary to have a reference to a specific 'mind iteration' to cycle when certain events happen? // (If being a borg or AI counts as dead, then this is highly likely, as it's still the same Mind for practical purposes.) if (mind.OwnedEntity == null) return true; // This can be null if they're deleted (spike / brain nom) var targetMobState = EntityManager.GetComponentOrNull(mind.OwnedEntity); // This can be null if it's a brain (this happens very often) // Brains are the result of gibbing so should definitely count as dead if (targetMobState == null) return true; // They might actually be alive. return _mobState.IsDead(mind.OwnedEntity.Value, targetMobState); } public virtual void Visit(EntityUid mindId, EntityUid entity, MindComponent? mind = null) { } /// /// Returns the mind to its original entity. /// public virtual void UnVisit(EntityUid mindId, MindComponent? mind = null) { } /// /// Returns the mind to its original entity. /// public void UnVisit(ICommonSession? player) { if (player == null || !TryGetMind(player, out var mindId, out var mind)) return; UnVisit(mindId, mind); } /// /// Cleans up the VisitingEntity. /// /// protected void RemoveVisitingEntity(MindComponent mind) { if (mind.VisitingEntity == null) return; var oldVisitingEnt = mind.VisitingEntity.Value; // Null this before removing the component to avoid any infinite loops. mind.VisitingEntity = null; if (TryComp(oldVisitingEnt, out VisitingMindComponent? visitComp)) { visitComp.MindId = null; RemCompDeferred(oldVisitingEnt, visitComp); } RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true); } public void WipeMind(ICommonSession player) { var mind = _player.ContentData(player)?.Mind; DebugTools.Assert(GetMind(player.UserId) == mind); WipeMind(mind); } /// /// Detaches a mind from all entities and clears the user ID. /// public void WipeMind(EntityUid? mindId, MindComponent? mind = null) { if (mindId == null || !Resolve(mindId.Value, ref mind, false)) return; TransferTo(mindId.Value, null, mind: mind); SetUserId(mindId.Value, null, mind: mind); } /// /// Transfer this mind's control over to a new entity. /// /// The mind to transfer /// /// The entity to control. /// Can be null, in which case it will simply detach the mind from any entity. /// /// /// If true, skips ghost check for Visiting Entity /// /// /// Thrown if is already controlled by another player. /// public virtual void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostCheckOverride = false, bool createGhost = true, MindComponent? mind = null) { } /// /// Tries to create and add an objective from its prototype id. /// /// Returns true if adding the objective succeeded. public bool TryAddObjective(EntityUid mindId, MindComponent mind, string proto) { var objective = _objectives.TryCreateObjective(mindId, mind, proto); if (objective == null) return false; AddObjective(mindId, mind, objective.Value); return true; } /// /// Adds an objective that already exists, and is assumed to have had its requirements checked. /// public void AddObjective(EntityUid mindId, MindComponent mind, EntityUid objective) { var title = Name(objective); _adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) added to mind of {MindOwnerLoggingString(mind)}"); mind.Objectives.Add(objective); } /// /// Removes an objective from this mind. /// /// Returns true if the removal succeeded. public bool TryRemoveObjective(EntityUid mindId, MindComponent mind, int index) { if (index < 0 || index >= mind.Objectives.Count) return false; var objective = mind.Objectives[index]; var title = Name(objective); _adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}"); mind.Objectives.Remove(objective); Del(objective); return true; } public bool TryGetObjectiveComp(EntityUid uid, [NotNullWhen(true)] out T? objective) where T : IComponent { if (TryGetMind(uid, out var mindId, out var mind) && TryGetObjectiveComp(mindId, out objective, mind)) { return true; } objective = default; return false; } public bool TryGetObjectiveComp(EntityUid mindId, [NotNullWhen(true)] out T? objective, MindComponent? mind = null) where T : IComponent { if (Resolve(mindId, ref mind)) { var query = GetEntityQuery(); foreach (var uid in mind.AllObjectives) { if (query.TryGetComponent(uid, out objective)) { return true; } } } objective = default; return false; } public bool TryGetSession(EntityUid? mindId, [NotNullWhen(true)] out ICommonSession? session) { session = null; return TryComp(mindId, out MindComponent? mind) && (session = mind.Session) != null; } /// /// Gets a mind from uid and/or MindContainerComponent. Used for null checks. /// /// Entity UID that owns the mind. /// The mind id. /// The returned mind. /// Mind component on to get the mind from. /// True if mind found. False if not. public bool TryGetMind( EntityUid uid, out EntityUid mindId, [NotNullWhen(true)] out MindComponent? mind, MindContainerComponent? container = null) { mindId = default; mind = null; if (!Resolve(uid, ref container, false)) return false; if (!container.HasMind) return false; mindId = container.Mind ?? default; return TryComp(mindId, out mind); } public bool TryGetMind( PlayerData player, out EntityUid mindId, [NotNullWhen(true)] out MindComponent? mind) { mindId = player.Mind ?? default; return TryComp(mindId, out mind); } public bool TryGetMind( ICommonSession? player, out EntityUid mindId, [NotNullWhen(true)] out MindComponent? mind) { mindId = default; mind = null; if (_player.ContentData(player) is not { } data) return false; if (TryGetMind(data, out mindId, out mind)) return true; DebugTools.AssertNull(data.Mind); return false; } /// /// Gets a role component from a player's mind. /// /// Whether a role was found public bool TryGetRole(EntityUid user, [NotNullWhen(true)] out T? role) where T : IComponent { role = default; if (!TryComp(user, out var mindContainer) || mindContainer.Mind == null) return false; return TryComp(mindContainer.Mind, out role); } /// /// Sets the Mind's OwnedComponent and OwnedEntity /// /// Mind to set OwnedComponent and OwnedEntity on /// Entity owned by /// MindContainerComponent owned by protected void SetOwnedEntity(MindComponent mind, EntityUid? uid, MindContainerComponent? mindContainerComponent) { if (uid != null) Resolve(uid.Value, ref mindContainerComponent); mind.OwnedEntity = uid; mind.OwnedComponent = mindContainerComponent; } /// /// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the /// entity that any mind is connected to, except as a side effect of the fact that it may change a player's /// attached entity. E.g., ghosts get deleted. /// public virtual void SetUserId(EntityUid mindId, NetUserId? userId, MindComponent? mind = null) { } /// /// True if this Mind is 'sufficiently dead' IC (Objectives, EndText). /// Note that this is *IC logic*, it's not necessarily tied to any specific truth. /// "If administrators decide that zombies are dead, this returns true for zombies." /// (Maybe you were looking for the action blocker system?) /// public bool IsCharacterDeadIc(MindComponent mind) { if (mind.OwnedEntity is { } owned) { var ev = new GetCharactedDeadIcEvent(null); RaiseLocalEvent(owned, ref ev); if (ev.Dead != null) return ev.Dead.Value; } return IsCharacterDeadPhysically(mind); } /// /// A string to represent the mind for logging /// public string MindOwnerLoggingString(MindComponent mind) { if (mind.OwnedEntity != null) return ToPrettyString(mind.OwnedEntity.Value); if (mind.UserId != null) return mind.UserId.Value.ToString(); return "(originally " + mind.OriginalOwnerUserId + ")"; } public string? GetCharacterName(NetUserId userId) { return TryGetMind(userId, out _, out var mind) ? mind.CharacterName : null; } /// /// Returns a list of every living humanoid player's minds, except for a single one which is exluded. /// public List GetAliveHumansExcept(EntityUid exclude) { var mindQuery = EntityQuery(); var allHumans = new List(); // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var mc, out var mobState, out _)) { // the player needs to have a mind and not be the excluded one if (mc.Mind == null || mc.Mind == exclude) continue; // the player has to be alive if (_mobState.IsAlive(uid, mobState)) allHumans.Add(mc.Mind.Value); } return allHumans; } } /// /// Raised on an entity to determine whether or not they are "dead" in IC-logic. /// If not handled, then it will simply check if they are dead physically. /// /// [ByRefEvent] public record struct GetCharactedDeadIcEvent(bool? Dead);