diff --git a/Content.Client/Mind/MindSystem.cs b/Content.Client/Mind/MindSystem.cs index 87d9e9ddbe..cc43c349e4 100644 --- a/Content.Client/Mind/MindSystem.cs +++ b/Content.Client/Mind/MindSystem.cs @@ -4,4 +4,24 @@ namespace Content.Client.Mind; public sealed class MindSystem : SharedMindSystem { + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, MindComponent component, ref AfterAutoHandleStateEvent args) + { + // Because minds are generally not networked, there might be weird situations were a client thinks multiple + // users share a mind? E.g., if an admin periodical gets sent all minds via some PVS override, but doesn't get + // sent intermediate states? Not sure if this is actually possible, but better to be safe. + foreach (var (user, mind) in UserMinds) + { + if (mind == uid) + UserMinds.Remove(user); + } + + if (component.UserId != null) + UserMinds[component.UserId.Value] = uid; + } } diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs index fb2fef43ed..3ad61bcdf0 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs @@ -67,13 +67,12 @@ public sealed partial class MindTests var entity = entMan.SpawnEntity(null, new MapCoordinates()); var mindComp = entMan.EnsureComponent(entity); - var mindId = mindSystem.CreateMind(null); - var mind = entMan.GetComponent(mindId); + var mind = mindSystem.CreateMind(null); - Assert.That(mind.UserId, Is.EqualTo(null)); + Assert.That(mind.Comp.UserId, Is.EqualTo(null)); - mindSystem.TransferTo(mindId, entity, mind: mind); - Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mindId)); + mindSystem.TransferTo(mind, entity, mind: mind); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind.Owner)); }); await pair.CleanReturnAsync(); @@ -94,11 +93,11 @@ public sealed partial class MindTests var entity = entMan.SpawnEntity(null, new MapCoordinates()); var mindComp = entMan.EnsureComponent(entity); - var mindId = mindSystem.CreateMind(null); + var mindId = mindSystem.CreateMind(null).Owner; mindSystem.TransferTo(mindId, entity); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mindId)); - var mind2 = mindSystem.CreateMind(null); + var mind2 = mindSystem.CreateMind(null).Owner; mindSystem.TransferTo(mind2, entity); Assert.Multiple(() => { @@ -184,7 +183,7 @@ public sealed partial class MindTests var mindComp = entMan.EnsureComponent(entity); entMan.EnsureComponent(targetEntity); - var mind = mindSystem.CreateMind(null); + var mind = mindSystem.CreateMind(null).Owner; mindSystem.TransferTo(mind, entity); @@ -276,7 +275,7 @@ public sealed partial class MindTests var entity = entMan.SpawnEntity(null, new MapCoordinates()); var mindComp = entMan.EnsureComponent(entity); - var mindId = mindSystem.CreateMind(null); + var mindId = mindSystem.CreateMind(null).Owner; var mind = entMan.EnsureComponent(mindId); Assert.That(mind.UserId, Is.EqualTo(null)); @@ -334,7 +333,7 @@ public sealed partial class MindTests public async Task TestPlayerCanGhost() { // Client is needed to spawn session - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true }); + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false }); var server = pair.Server; var entMan = server.ResolveDependency(); diff --git a/Content.Server/Administration/Commands/ControlMob.cs b/Content.Server/Administration/Commands/ControlMob.cs index 2d205e44d3..8fc74c61d4 100644 --- a/Content.Server/Administration/Commands/ControlMob.cs +++ b/Content.Server/Administration/Commands/ControlMob.cs @@ -1,5 +1,5 @@ +using Content.Server.Mind; using Content.Shared.Administration; -using Content.Shared.Mind; using Robust.Server.Player; using Robust.Shared.Console; @@ -42,14 +42,7 @@ namespace Content.Server.Administration.Commands return; } - var mindSystem = _entities.System(); - if (!mindSystem.TryGetMind(target, out var mindId, out var mind)) - { - shell.WriteLine(Loc.GetString("shell-entity-is-not-mob")); - return; - } - - mindSystem.TransferTo(mindId, target, mind: mind); + _entities.System().ControlMob(player.UserId, target); } } } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index c7e23374a3..0f0c562356 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Disposal.Tube; using Content.Server.Disposal.Tube.Components; using Content.Server.EUI; using Content.Server.Ghost.Roles; +using Content.Server.Mind; using Content.Server.Mind.Commands; using Content.Server.Prayer; using Content.Server.Xenoarchaeology.XenoArtifacts; @@ -18,7 +19,6 @@ using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.GameTicking; using Content.Shared.Inventory; -using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Popups; using Content.Shared.Verbs; @@ -56,7 +56,7 @@ namespace Content.Server.Administration.Systems [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly PrayerSystem _prayerSystem = default!; [Dependency] private readonly EuiManager _eui = default!; - [Dependency] private readonly SharedMindSystem _mindSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly ToolshedManager _toolshed = default!; [Dependency] private readonly RejuvenateSystem _rejuvenate = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; @@ -277,12 +277,7 @@ namespace Content.Server.Administration.Systems // TODO VERB ICON control mob icon Act = () => { - MakeSentientCommand.MakeSentient(args.Target, EntityManager); - - if (!_minds.TryGetMind(player, out var mindId, out var mind)) - return; - - _mindSystem.TransferTo(mindId, args.Target, ghostCheckOverride: true, mind: mind); + _mindSystem.ControlMob(args.User, args.Target); }, Impact = LogImpact.High, ConfirmationPopup = true @@ -358,7 +353,7 @@ namespace Content.Server.Administration.Systems var message = ExamineSystemShared.InRangeUnOccluded(args.User, args.Target) ? Loc.GetString("in-range-unoccluded-verb-on-activate-not-occluded") : Loc.GetString("in-range-unoccluded-verb-on-activate-occluded"); - + _popup.PopupEntity(message, args.Target, args.User); } }; diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index df2aafa901..a10196a43e 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -360,8 +360,7 @@ namespace Content.Server.GameTicking else if (mind.CurrentEntity != null && TryName(mind.CurrentEntity.Value, out var icName)) playerIcName = icName; - var entity = mind.OriginalOwnedEntity; - if (Exists(entity)) + if (TryGetEntity(mind.OriginalOwnedEntity, out var entity)) _pvsOverride.AddGlobalOverride(entity.Value, recursive: true); var roles = _roles.MindGetAllRoles(mindId); diff --git a/Content.Server/Mind/Commands/RenameCommand.cs b/Content.Server/Mind/Commands/RenameCommand.cs index 5674da4ffd..2d65adc508 100644 --- a/Content.Server/Mind/Commands/RenameCommand.cs +++ b/Content.Server/Mind/Commands/RenameCommand.cs @@ -54,6 +54,7 @@ public sealed class RenameCommand : IConsoleCommand { // Mind mind.CharacterName = name; + _entManager.Dirty(mindId, mind); } // Id Cards diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index 373007fd1b..d2721db7b6 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Server.Administration.Logs; using Content.Server.GameTicking; +using Content.Server.Mind.Commands; using Content.Shared.Database; using Content.Shared.Ghost; using Content.Shared.Mind; @@ -46,20 +47,14 @@ public sealed class MindSystem : SharedMindSystem mind.UserId = null; } - if (!TryComp(mind.OwnedEntity, out MetaDataComponent? meta) || meta.EntityLifeStage >= EntityLifeStage.Terminating) - return; + if (mind.OwnedEntity != null && !TerminatingOrDeleted(mind.OwnedEntity.Value)) + TransferTo(uid, null, mind: mind, createGhost: false); - RaiseLocalEvent(mind.OwnedEntity.Value, new MindRemovedMessage(uid, mind), true); mind.OwnedEntity = null; - mind.OwnedComponent = null; } private void OnMindContainerTerminating(EntityUid uid, MindContainerComponent component, ref EntityTerminatingEvent args) { - // Let's not create ghosts if not in the middle of the round. - if (_gameTicker.RunLevel == GameRunLevel.PreRoundLobby) - return; - if (!TryGetMind(uid, out var mindId, out var mind, component)) return; @@ -77,6 +72,11 @@ public sealed class MindSystem : SharedMindSystem TransferTo(mindId, null, createGhost: false, mind: mind); + // Let's not create ghosts if not in the middle of the round. + if (_gameTicker.RunLevel == GameRunLevel.PreRoundLobby) + return; + + // I just love convoluted entity shutdown logic that results in more entities being spawned. if (component.GhostOnShutdown && mind.Session != null) { var xform = Transform(uid); @@ -198,7 +198,7 @@ public sealed class MindSystem : SharedMindSystem if (mind.VisitingEntity == null) return; - RemoveVisitingEntity(mind); + RemoveVisitingEntity(mindId, mind); if (mind.Session == null || mind.Session.AttachedEntity == mind.VisitingEntity) return; @@ -219,11 +219,10 @@ public sealed class MindSystem : SharedMindSystem if (mind == null && !Resolve(mindId, ref mind)) return; - base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind); - if (entity == mind.OwnedEntity) return; + Dirty(mindId, mind); MindContainerComponent? component = null; var alreadyAttached = false; @@ -247,27 +246,33 @@ public sealed class MindSystem : SharedMindSystem } else if (createGhost) { + // TODO remove this option. + // Transfer-to-null should just detach a mind. + // If people want to create a ghost, that should be done explicitly via some TransferToGhost() method, not + // not implicitly via optional arguments. + var position = Deleted(mind.OwnedEntity) ? _gameTicker.GetObserverSpawnPoint().ToMap(EntityManager, _transform) : Transform(mind.OwnedEntity.Value).MapPosition; entity = Spawn("MobObserver", position); + component = EnsureComp(entity.Value); var ghostComponent = Comp(entity.Value); _ghosts.SetCanReturnToBody(ghostComponent, false); } - var oldComp = mind.OwnedComponent; var oldEntity = mind.OwnedEntity; - if (oldComp != null && oldEntity != null) + if (TryComp(oldEntity, out MindContainerComponent? oldContainer)) { - if (oldComp.Mind != null) - _pvsOverride.ClearOverride(oldComp.Mind.Value); - oldComp.Mind = null; - RaiseLocalEvent(oldEntity.Value, new MindRemovedMessage(oldEntity.Value, mind), true); + oldContainer.Mind = null; + mind.OwnedEntity = null; + Entity mindEnt = (mindId, mind); + Entity containerEnt = (oldEntity.Value, oldContainer); + RaiseLocalEvent(oldEntity.Value, new MindRemovedMessage(mindEnt, containerEnt)); + RaiseLocalEvent(mindId, new MindGotRemovedEvent(mindEnt, containerEnt)); + Dirty(oldEntity.Value, oldContainer); } - SetOwnedEntity(mind, entity, component); - // Don't do the full deletion cleanup if we're transferring to our VisitingEntity if (alreadyAttached) { @@ -281,7 +286,7 @@ public sealed class MindSystem : SharedMindSystem || !TryComp(mind.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 { - RemoveVisitingEntity(mind); + RemoveVisitingEntity(mindId, mind); } // Player is CURRENTLY connected. @@ -292,11 +297,16 @@ public sealed class MindSystem : SharedMindSystem Log.Info($"Session {session.Name} transferred to entity {entity}."); } - if (mind.OwnedComponent != null) + if (entity != null) { - mind.OwnedComponent.Mind = mindId; - RaiseLocalEvent(mind.OwnedEntity!.Value, new MindAddedMessage(), true); - mind.OriginalOwnedEntity ??= mind.OwnedEntity; + component!.Mind = mindId; + mind.OwnedEntity = entity; + mind.OriginalOwnedEntity ??= GetNetEntity(mind.OwnedEntity); + Entity mindEnt = (mindId, mind); + Entity containerEnt = (entity.Value, component); + RaiseLocalEvent(entity.Value, new MindAddedMessage(mindEnt, containerEnt)); + RaiseLocalEvent(mindId, new MindGotAddedEvent(mindEnt, containerEnt)); + Dirty(entity.Value, component); } } @@ -313,6 +323,7 @@ public sealed class MindSystem : SharedMindSystem if (mind.UserId == userId) return; + Dirty(mindId, mind); _pvsOverride.ClearOverride(mindId); if (userId != null && !_players.TryGetPlayerData(userId.Value, out _)) { @@ -363,4 +374,27 @@ public sealed class MindSystem : SharedMindSystem if (_players.GetPlayerData(userId.Value).ContentData() is { } data) data.Mind = mindId; } + + public void ControlMob(EntityUid user, EntityUid target) + { + if (TryComp(user, out ActorComponent? actor)) + ControlMob(actor.PlayerSession.UserId, target); + } + + public void ControlMob(NetUserId user, EntityUid target) + { + var (mindId, mind) = GetOrCreateMind(user); + + if (mind.CurrentEntity == target) + return; + + if (mind.OwnedEntity == target) + { + UnVisit(mindId, mind); + return; + } + + MakeSentientCommand.MakeSentient(target, EntityManager); + TransferTo(mindId, target, ghostCheckOverride: true, mind: mind); + } } diff --git a/Content.Server/Silicons/Laws/SiliconLawSystem.cs b/Content.Server/Silicons/Laws/SiliconLawSystem.cs index 1d5c2e35e8..beb760ec8f 100644 --- a/Content.Server/Silicons/Laws/SiliconLawSystem.cs +++ b/Content.Server/Silicons/Laws/SiliconLawSystem.cs @@ -170,7 +170,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem if (component.AntagonistRole == null) return; - _roles.MindTryRemoveRole(args.OldMindId); + _roles.MindTryRemoveRole(args.Mind); } private void EnsureEmaggedRole(EntityUid uid, EmagSiliconLawComponent component) diff --git a/Content.Shared/Mind/Components/MindContainerComponent.cs b/Content.Shared/Mind/Components/MindContainerComponent.cs index ca0f14d994..62b26cbd35 100644 --- a/Content.Shared/Mind/Components/MindContainerComponent.cs +++ b/Content.Shared/Mind/Components/MindContainerComponent.cs @@ -1,24 +1,25 @@ using System.Diagnostics.CodeAnalysis; +using Robust.Shared.GameStates; namespace Content.Shared.Mind.Components { /// - /// Stores a on a mob. + /// This component indicates that this entity may have mind, which is simply an entity with a . + /// The mind entity is not actually stored in a "container", but is simply stored in nullspace. /// - [RegisterComponent, Access(typeof(SharedMindSystem))] + [RegisterComponent, Access(typeof(SharedMindSystem)), NetworkedComponent, AutoGenerateComponentState] public sealed partial class MindContainerComponent : Component { /// /// The mind controlling this mob. Can be null. /// - [ViewVariables] + [DataField, AutoNetworkedField] [Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends public EntityUid? Mind { get; set; } /// /// True if we have a mind, false otherwise. /// - [ViewVariables] [MemberNotNullWhen(true, nameof(Mind))] public bool HasMind => Mind != null; @@ -26,7 +27,7 @@ namespace Content.Shared.Mind.Components /// Whether examining should show information about the mind or not. /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("showExamineInfo")] + [DataField("showExamineInfo"), AutoNetworkedField] public bool ShowExamineInfo { get; set; } /// @@ -38,19 +39,59 @@ namespace Content.Shared.Mind.Components public bool GhostOnShutdown { get; set; } = true; } - public sealed class MindRemovedMessage : EntityEventArgs + public abstract class MindEvent : EntityEventArgs { - public EntityUid OldMindId; - public MindComponent OldMind; + public readonly Entity Mind; + public readonly Entity Container; - public MindRemovedMessage(EntityUid oldMindId, MindComponent oldMind) + public MindEvent(Entity mind, Entity container) { - OldMindId = oldMindId; - OldMind = oldMind; + Mind = mind; + Container = container; } } - public sealed class MindAddedMessage : EntityEventArgs + /// + /// Event raised directed at a mind-container when a mind gets removed. + /// + public sealed class MindRemovedMessage : MindEvent { + public MindRemovedMessage(Entity mind, Entity container) + : base(mind, container) + { + } + } + + /// + /// Event raised directed at a mind when it gets removed from a mind-container. + /// + public sealed class MindGotRemovedEvent : MindEvent + { + public MindGotRemovedEvent(Entity mind, Entity container) + : base(mind, container) + { + } + } + + /// + /// Event raised directed at a mind-container when a mind gets added. + /// + public sealed class MindAddedMessage : MindEvent + { + public MindAddedMessage(Entity mind, Entity container) + : base(mind, container) + { + } + } + + /// + /// Event raised directed at a mind when it gets added to a mind-container. + /// + public sealed class MindGotAddedEvent : MindEvent + { + public MindGotAddedEvent(Entity mind, Entity container) + : base(mind, container) + { + } } } diff --git a/Content.Shared/Mind/MindComponent.cs b/Content.Shared/Mind/MindComponent.cs index 3ea92c3ce7..465db6a3d8 100644 --- a/Content.Shared/Mind/MindComponent.cs +++ b/Content.Shared/Mind/MindComponent.cs @@ -1,84 +1,86 @@ using Content.Shared.GameTicking; using Content.Shared.Mind.Components; +using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Players; namespace Content.Shared.Mind { /// - /// This is added as a component to mind entities, not to player entities. - /// for the one that is added to players. - /// A mind represents the IC "mind" of a player. - /// Roles are attached as components to its owning entity. + /// This component stores information about a player/mob mind. The component will be attached to a mind-entity + /// which is stored in null-space. The entity that is currently "possessed" by the mind will have a + /// . /// /// + /// Roles are attached as components on the mind-entity entity. /// 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. + /// + /// Minds are stored in null-space, and are thus generally not set to players unless that player is the owner + /// of the mind. As a result it should be safe to network "secret" information like roles & objectives /// - [RegisterComponent] + [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class MindComponent : Component { - internal readonly List Objectives = new(); + [DataField, AutoNetworkedField] + public List Objectives = new(); /// /// The session ID of the player owning this mind. /// - [ViewVariables, Access(typeof(SharedMindSystem))] + [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))] public NetUserId? UserId { get; 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, Access(typeof(SharedMindSystem))] + [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))] public NetUserId? OriginalOwnerUserId { get; set; } /// - /// Entity UID for the first entity that this mind controlled. Used for round end. + /// The first entity that this mind controlled. Used for round end information. /// Might be relevant if the player has ghosted since. /// - [ViewVariables] public EntityUid? OriginalOwnedEntity; + [DataField, AutoNetworkedField] + public NetEntity? OriginalOwnedEntity; + // This is a net entity, because this field currently ddoes not get set to null when this entity is deleted. + // This is a lazy way to ensure that people check that the entity still exists. + // TODO MIND Fix this properly by adding an OriginalMindContainerComponent or something like that. [ViewVariables] public bool IsVisitingEntity => VisitingEntity != null; - [ViewVariables, Access(typeof(SharedMindSystem))] + [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))] public EntityUid? VisitingEntity { get; set; } [ViewVariables] public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity; - [ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField, 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] + [DataField] public TimeSpan? TimeOfDeath { get; set; } - /// - /// The component currently owned by this mind. - /// Can be null. - /// - [ViewVariables] public MindContainerComponent? OwnedComponent; - /// /// The entity currently owned by this mind. /// Can be null. /// - [ViewVariables, Access(typeof(SharedMindSystem))] + [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))] public EntityUid? OwnedEntity { get; set; } - // TODO move objectives out of mind component /// /// An enumerable over all the objective entities this mind has. /// - [ViewVariables] + [ViewVariables, Obsolete("Use Objectives field")] public IEnumerable AllObjectives => Objectives; /// @@ -100,6 +102,7 @@ namespace Content.Shared.Mind /// Can be null, in which case the player is currently not logged in. /// [ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))] + // TODO remove this after moving IPlayerManager functions to shared public ICommonSession? Session { get; set; } } } diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index cc4ac4af23..6fc1c01dc2 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.Examine; @@ -25,7 +26,7 @@ public abstract class SharedMindSystem : EntitySystem [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. + [ViewVariables] protected readonly Dictionary UserMinds = new(); public override void Initialize() @@ -36,6 +37,7 @@ public abstract class SharedMindSystem : EntitySystem SubscribeLocalEvent(OnSuicide); SubscribeLocalEvent(OnVisitingTerminating); SubscribeLocalEvent(OnReset); + SubscribeLocalEvent(OnMindStartup); } public override void Shutdown() @@ -44,6 +46,29 @@ public abstract class SharedMindSystem : EntitySystem WipeAllMinds(); } + private void OnMindStartup(EntityUid uid, MindComponent component, ComponentStartup args) + { + if (component.UserId == null) + return; + + if (UserMinds.TryAdd(component.UserId.Value, uid)) + return; + + var existing = UserMinds[component.UserId.Value]; + if (existing == uid) + return; + + if (!Exists(existing)) + { + Log.Error($"Found deleted entity in mind dictionary while initializing mind {ToPrettyString(uid)}"); + UserMinds[component.UserId.Value] = uid; + return; + } + + Log.Error($"Encountered a user {component.UserId} that is already assigned to a mind while initializing mind {ToPrettyString(uid)}. Ignoring user field."); + component.UserId = null; + } + private void OnReset(RoundRestartCleanupEvent ev) { WipeAllMinds(); @@ -51,12 +76,22 @@ public abstract class SharedMindSystem : EntitySystem public virtual void WipeAllMinds() { - foreach (var mind in UserMinds.Values) + Log.Info($"Wiping all minds"); + foreach (var mind in UserMinds.Values.ToArray()) { WipeMind(mind); } - DebugTools.Assert(UserMinds.Count == 0); + if (UserMinds.Count == 0) + return; + + foreach (var mind in UserMinds.Values) + { + if (Exists(mind)) + Log.Error($"Failed to wipe mind: {ToPrettyString(mind)}"); + } + + UserMinds.Clear(); } public EntityUid? GetMind(NetUserId user) @@ -80,6 +115,26 @@ public abstract class SharedMindSystem : EntitySystem return false; } + public bool TryGetMind(NetUserId user, [NotNullWhen(true)] out Entity? mind) + { + if (!TryGetMind(user, out var mindId, out var mindComp)) + { + mind = null; + return false; + } + + mind = (mindId.Value, mindComp); + return true; + } + + public Entity GetOrCreateMind(NetUserId user) + { + if (!TryGetMind(user, out var mind)) + mind = CreateMind(user); + + return mind.Value; + } + private void OnVisitingTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args) { if (component.MindId != null) @@ -128,7 +183,7 @@ public abstract class SharedMindSystem : EntitySystem return null; } - public EntityUid CreateMind(NetUserId? userId, string? name = null) + public Entity CreateMind(NetUserId? userId, string? name = null) { var mindId = Spawn(null, MapCoordinates.Nullspace); _metadata.SetEntityName(mindId, name == null ? "mind" : $"mind ({name})"); @@ -136,7 +191,7 @@ public abstract class SharedMindSystem : EntitySystem mind.CharacterName = name; SetUserId(mindId, userId, mind); - return mindId; + return (mindId, mind); } /// @@ -195,7 +250,7 @@ public abstract class SharedMindSystem : EntitySystem /// Cleans up the VisitingEntity. /// /// - protected void RemoveVisitingEntity(MindComponent mind) + protected void RemoveVisitingEntity(EntityUid mindId, MindComponent mind) { if (mind.VisitingEntity == null) return; @@ -210,6 +265,7 @@ public abstract class SharedMindSystem : EntitySystem RemCompDeferred(oldVisitingEnt, visitComp); } + Dirty(mindId, mind); RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true); } @@ -228,7 +284,7 @@ public abstract class SharedMindSystem : EntitySystem if (mindId == null || !Resolve(mindId.Value, ref mind, false)) return; - TransferTo(mindId.Value, null, mind: mind); + TransferTo(mindId.Value, null, createGhost:false, mind: mind); SetUserId(mindId.Value, null, mind: mind); } @@ -391,21 +447,6 @@ public abstract class SharedMindSystem : EntitySystem 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