using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.GameTicking; using Content.Server.Ghost.Components; using Content.Server.Mind.Components; using Content.Server.Objectives; using Content.Server.Players; using Content.Server.Roles; using Content.Shared.MobState.Components; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Mind { /// /// A mind represents the IC "mind" of a player. Stores roles currently. /// /// /// Think of it like this: if a player is supposed to have their memories, /// their mind follows along. /// /// Things such as respawning do not follow, because you're a new character. /// Getting borged, cloned, turned into a catbeast, etc... will keep it following you. /// public sealed class Mind { private readonly ISet _roles = new HashSet(); private readonly List _objectives = new(); public string Briefing = String.Empty; /// /// Creates the new mind. /// Note: the Mind is NOT initially attached! /// The provided UserId is solely for tracking of intended owner. /// /// The session ID of the original owner (may get credited). public Mind(NetUserId userId) { OriginalOwnerUserId = userId; } // TODO: This session should be able to be changed, probably. /// /// The session ID of the player owning this mind. /// [ViewVariables] public NetUserId? UserId { get; private set; } /// /// The session ID of the original owner, if any. /// May end up used for round-end information (as the owner may have abandoned Mind since) /// [ViewVariables] public NetUserId OriginalOwnerUserId { get; } [ViewVariables] public bool IsVisitingEntity => VisitingEntity != null; [ViewVariables] public EntityUid? VisitingEntity { get; private set; } [ViewVariables] public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity; [ViewVariables(VVAccess.ReadWrite)] public string? CharacterName { get; set; } /// /// The time of death for this Mind. /// Can be null - will be null if the Mind is not considered "dead". /// [ViewVariables] public TimeSpan? TimeOfDeath { get; set; } = null; /// /// The component currently owned by this mind. /// Can be null. /// [ViewVariables] public MindComponent? OwnedComponent { get; private set; } /// /// The entity currently owned by this mind. /// Can be null. /// [ViewVariables] public EntityUid? OwnedEntity => OwnedComponent?.Owner; /// /// An enumerable over all the roles this mind has. /// [ViewVariables] public IEnumerable AllRoles => _roles; /// /// An enumerable over all the objectives this mind has. /// [ViewVariables] public IEnumerable AllObjectives => _objectives; /// /// The session of the player owning this mind. /// Can be null, in which case the player is currently not logged in. /// [ViewVariables] public IPlayerSession? Session { get { if (!UserId.HasValue) { return null; } var playerMgr = IoCManager.Resolve(); playerMgr.TryGetSessionById(UserId.Value, out var ret); return ret; } } /// /// 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?) /// [ViewVariables] public bool CharacterDeadIC => CharacterDeadPhysically; /// /// 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. /// [ViewVariables] public bool CharacterDeadPhysically { get { // 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.) // This can be null if they're deleted (spike / brain nom) var targetMobState = IoCManager.Resolve().GetComponentOrNull(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 targetMobState.IsDead(); } } /// /// Gives this mind a new role. /// /// The type of the role to give. /// The instance of the role. /// /// Thrown if we already have a role with this type. /// public Role AddRole(Role role) { if (_roles.Contains(role)) { throw new ArgumentException($"We already have this role: {role}"); } _roles.Add(role); role.Greet(); var message = new RoleAddedEvent(role); if (OwnedEntity != null) { IoCManager.Resolve().EventBus.RaiseLocalEvent(OwnedEntity.Value, message); } return role; } /// /// Removes a role from this mind. /// /// The type of the role to remove. /// /// Thrown if we do not have this role. /// public void RemoveRole(Role role) { if (!_roles.Contains(role)) { throw new ArgumentException($"We do not have this role: {role}"); } _roles.Remove(role); var message = new RoleRemovedEvent(role); if (OwnedEntity != null) { IoCManager.Resolve().EventBus.RaiseLocalEvent(OwnedEntity.Value, message); } } public bool HasRole() where T : Role { var t = typeof(T); return _roles.Any(role => role.GetType() == t); } /// /// Gets the current job /// public Job? CurrentJob => _roles.OfType().SingleOrDefault(); /// /// Adds an objective to this mind. /// public bool TryAddObjective(ObjectivePrototype objectivePrototype) { if (!objectivePrototype.CanBeAssigned(this)) return false; var objective = objectivePrototype.GetObjective(this); if (_objectives.Contains(objective)) return false; _objectives.Add(objective); return true; } /// /// Removes an objective to this mind. /// /// Returns true if the removal succeeded. public bool TryRemoveObjective(int index) { if (_objectives.Count >= index) return false; var objective = _objectives[index]; _objectives.Remove(objective); return true; } /// /// Transfer this mind's control over to a new entity. /// /// /// 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 owned by another mind. /// public void TransferTo(EntityUid? entity, bool ghostCheckOverride = false) { var entMan = IoCManager.Resolve(); MindComponent? component = null; var alreadyAttached = false; if (entity != null) { if (!entMan.TryGetComponent(entity.Value, out component)) { component = entMan.AddComponent(entity.Value); } else if (component!.HasMind) { EntitySystem.Get().OnGhostAttempt(component.Mind!, false); } if (entMan.TryGetComponent(entity.Value, out var actor)) { // Happens when transferring to your currently visited entity. if (actor.PlayerSession != Session) { throw new ArgumentException("Visit target already has a session.", nameof(entity)); } alreadyAttached = true; } } var mindSystem = EntitySystem.Get(); if(OwnedComponent != null) mindSystem.InternalEjectMind(OwnedComponent.Owner, OwnedComponent); OwnedComponent = component; if(OwnedComponent != null) mindSystem.InternalAssignMind(OwnedComponent.Owner, this, OwnedComponent); if (VisitingEntity != null && (ghostCheckOverride // to force mind transfer, for example from ControlMobVerb || !entMan.TryGetComponent(VisitingEntity!, out GhostComponent? ghostComponent) // visiting entity is not a Ghost || !ghostComponent.CanReturnToBody)) // it is a ghost, but cannot return to body anyway, so it's okay { VisitingEntity = default; } // Player is CURRENTLY connected. if (Session != null && !alreadyAttached && VisitingEntity == default) { Session.AttachToEntity(entity); Logger.Info($"Session {Session.Name} transferred to entity {entity}."); } } public void ChangeOwningPlayer(NetUserId? newOwner) { var playerMgr = IoCManager.Resolve(); PlayerData? newOwnerData = null; if (newOwner.HasValue) { if (!playerMgr.TryGetPlayerData(newOwner.Value, out var uncast)) { // This restriction is because I'm too lazy to initialize the player data // for a client that hasn't logged in yet. // Go ahead and remove it if you need. throw new ArgumentException("new owner must have previously logged into the server."); } newOwnerData = uncast.ContentData(); } // Make sure to remove control from our old owner if they're logged in. var oldSession = Session; oldSession?.AttachToEntity(null); if (UserId.HasValue) { var data = playerMgr.GetPlayerData(UserId.Value).ContentData(); DebugTools.AssertNotNull(data); data!.UpdateMindFromMindChangeOwningPlayer(null); } UserId = newOwner; if (!newOwner.HasValue) { return; } // Yank new owner out of their old mind too. // Can I mention how much I love the word yank? DebugTools.AssertNotNull(newOwnerData); newOwnerData!.Mind?.ChangeOwningPlayer(null); newOwnerData.UpdateMindFromMindChangeOwningPlayer(this); } public void Visit(EntityUid entity) { Session?.AttachToEntity(entity); VisitingEntity = entity; var comp = IoCManager.Resolve().AddComponent(entity); comp.Mind = this; Logger.Info($"Session {Session?.Name} visiting entity {entity}."); } public void UnVisit() { if (VisitingEntity == null) { return; } Session?.AttachToEntity(OwnedEntity); var oldVisitingEnt = VisitingEntity.Value; // Null this before removing the component to avoid any infinite loops. VisitingEntity = default; DebugTools.AssertNotNull(oldVisitingEnt); var entities = IoCManager.Resolve(); if (entities.HasComponent(oldVisitingEnt)) { entities.RemoveComponent(oldVisitingEnt); } entities.EventBus.RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage()); } public bool TryGetSession([NotNullWhen(true)] out IPlayerSession? session) { return (session = Session) != null; } } }