]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Mind tweaks & fixes (#21203)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Tue, 24 Oct 2023 14:23:56 +0000 (01:23 +1100)
committerGitHub <noreply@github.com>
Tue, 24 Oct 2023 14:23:56 +0000 (10:23 -0400)
Content.Client/Mind/MindSystem.cs
Content.IntegrationTests/Tests/Minds/MindTests.cs
Content.Server/Administration/Commands/ControlMob.cs
Content.Server/Administration/Systems/AdminVerbSystem.cs
Content.Server/GameTicking/GameTicker.RoundFlow.cs
Content.Server/Mind/Commands/RenameCommand.cs
Content.Server/Mind/MindSystem.cs
Content.Server/Silicons/Laws/SiliconLawSystem.cs
Content.Shared/Mind/Components/MindContainerComponent.cs
Content.Shared/Mind/MindComponent.cs
Content.Shared/Mind/SharedMindSystem.cs

index 87d9e9ddbe01c0286eeaf189e260919aabdde16d..cc43c349e47e0d376d80a23bcb4e0c0167180c9e 100644 (file)
@@ -4,4 +4,24 @@ namespace Content.Client.Mind;
 
 public sealed class MindSystem : SharedMindSystem
 {
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<MindComponent, AfterAutoHandleStateEvent>(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;
+    }
 }
index fb2fef43edba27d565eec592623f04fb0e7d518a..3ad61bcdf05ddbf04f795cec7f788c26c72665d3 100644 (file)
@@ -67,13 +67,12 @@ public sealed partial class MindTests
             var entity = entMan.SpawnEntity(null, new MapCoordinates());
             var mindComp = entMan.EnsureComponent<MindContainerComponent>(entity);
 
-            var mindId = mindSystem.CreateMind(null);
-            var mind = entMan.GetComponent<MindComponent>(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<MindContainerComponent>(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<MindContainerComponent>(entity);
             entMan.EnsureComponent<MindContainerComponent>(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<MindContainerComponent>(entity);
 
-            var mindId = mindSystem.CreateMind(null);
+            var mindId = mindSystem.CreateMind(null).Owner;
             var mind = entMan.EnsureComponent<MindComponent>(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<IServerEntityManager>();
index 2d205e44d3b911d938327fa16347511f4d2fe060..8fc74c61d468f8f3de4032aef432e2a607b18d93 100644 (file)
@@ -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<SharedMindSystem>();
-            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<MindSystem>().ControlMob(player.UserId, target);
         }
     }
 }
index c7e23374a3c8fbd2fa263e70d7e2fdb241e0e461..0f0c562356260afe098f52ba180dbec28597b874 100644 (file)
@@ -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);
                     }
                 };
index df2aafa90119fc809250e6c5182758d4361df2ac..a10196a43e1ef6402ce2624221b928b8c097a7bb 100644 (file)
@@ -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);
index 5674da4ffd34051cae6dfca5cec15f210dc70391..2d65adc5082284dd6afb35ff00e0cd8a1d8d85c9 100644 (file)
@@ -54,6 +54,7 @@ public sealed class RenameCommand : IConsoleCommand
         {
             // Mind
             mind.CharacterName = name;
+            _entManager.Dirty(mindId, mind);
         }
 
         // Id Cards
index 373007fd1b5a2ba0ca7ed389a31556c0f85bcfda..d2721db7b627637e180ce60ca3d9c8d1b769c4c3 100644 (file)
@@ -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<MindContainerComponent>(entity.Value);
             var ghostComponent = Comp<GhostComponent>(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<MindComponent> mindEnt = (mindId, mind);
+            Entity<MindContainerComponent> 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<MindComponent> mindEnt = (mindId, mind);
+            Entity<MindContainerComponent> 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);
+    }
 }
index 1d5c2e35e8b74425920ee49a7a79204d3d77261d..beb760ec8fd9190a133508f87d6a0f4916e351a1 100644 (file)
@@ -170,7 +170,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
         if (component.AntagonistRole == null)
             return;
 
-        _roles.MindTryRemoveRole<SubvertedSiliconRoleComponent>(args.OldMindId);
+        _roles.MindTryRemoveRole<SubvertedSiliconRoleComponent>(args.Mind);
     }
 
     private void EnsureEmaggedRole(EntityUid uid, EmagSiliconLawComponent component)
index ca0f14d994cc08f29ddd84cdb066aa45f6365b98..62b26cbd35357521239fa4cdcf22d2efeca18793 100644 (file)
@@ -1,24 +1,25 @@
 using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.GameStates;
 
 namespace Content.Shared.Mind.Components
 {
     /// <summary>
-    ///     Stores a <see cref="MindComponent"/> on a mob.
+    /// This component indicates that this entity may have mind, which is simply an entity with a <see cref="MindComponent"/>.
+    /// The mind entity is not actually stored in a "container", but is simply stored in nullspace.
     /// </summary>
-    [RegisterComponent, Access(typeof(SharedMindSystem))]
+    [RegisterComponent, Access(typeof(SharedMindSystem)), NetworkedComponent, AutoGenerateComponentState]
     public sealed partial class MindContainerComponent : Component
     {
         /// <summary>
         ///     The mind controlling this mob. Can be null.
         /// </summary>
-        [ViewVariables]
+        [DataField, AutoNetworkedField]
         [Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
         public EntityUid? Mind { get; set; }
 
         /// <summary>
         ///     True if we have a mind, false otherwise.
         /// </summary>
-        [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.
         /// </summary>
         [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("showExamineInfo")]
+        [DataField("showExamineInfo"), AutoNetworkedField]
         public bool ShowExamineInfo { get; set; }
 
         /// <summary>
@@ -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<MindComponent> Mind;
+        public readonly Entity<MindContainerComponent> Container;
 
-        public MindRemovedMessage(EntityUid oldMindId, MindComponent oldMind)
+        public MindEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
         {
-            OldMindId = oldMindId;
-            OldMind = oldMind;
+            Mind = mind;
+            Container = container;
         }
     }
 
-    public sealed class MindAddedMessage : EntityEventArgs
+    /// <summary>
+    /// Event raised directed at a mind-container when a mind gets removed.
+    /// </summary>
+    public sealed class MindRemovedMessage : MindEvent
     {
+        public MindRemovedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
+            : base(mind, container)
+        {
+        }
+    }
+
+    /// <summary>
+    /// Event raised directed at a mind when it gets removed from a mind-container.
+    /// </summary>
+    public sealed class MindGotRemovedEvent : MindEvent
+    {
+        public MindGotRemovedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
+            : base(mind, container)
+        {
+        }
+    }
+
+    /// <summary>
+    /// Event raised directed at a mind-container when a mind gets added.
+    /// </summary>
+    public sealed class MindAddedMessage : MindEvent
+    {
+        public MindAddedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
+            : base(mind, container)
+        {
+        }
+    }
+
+    /// <summary>
+    /// Event raised directed at a mind when it gets added to a mind-container.
+    /// </summary>
+    public sealed class MindGotAddedEvent : MindEvent
+    {
+        public MindGotAddedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
+            : base(mind, container)
+        {
+        }
     }
 }
index 3ea92c3ce7296c427c6c6f382b94e9dc53dda475..465db6a3d886e2bd4045597983612eb5f5a36e66 100644 (file)
@@ -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
 {
     /// <summary>
-    ///     This is added as a component to mind entities, not to player entities.
-    ///     <see cref="MindContainerComponent"/> 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
+    ///     <see cref="MindContainerComponent"/>.
     /// </summary>
     /// <remarks>
+    ///     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
     /// </remarks>
-    [RegisterComponent]
+    [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
     public sealed partial class MindComponent : Component
     {
-        internal readonly List<EntityUid> Objectives = new();
+        [DataField, AutoNetworkedField]
+        public List<EntityUid> Objectives = new();
 
         /// <summary>
         ///     The session ID of the player owning this mind.
         /// </summary>
-        [ViewVariables, Access(typeof(SharedMindSystem))]
+        [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
         public NetUserId? UserId { get; set; }
 
         /// <summary>
         ///     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)
         /// </summary>
-        [ViewVariables, Access(typeof(SharedMindSystem))]
+        [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
         public NetUserId? OriginalOwnerUserId { get; set; }
 
         /// <summary>
-        ///     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.
         /// </summary>
-        [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; }
 
         /// <summary>
         ///     The time of death for this Mind.
         ///     Can be null - will be null if the Mind is not considered "dead".
         /// </summary>
-        [ViewVariables]
+        [DataField]
         public TimeSpan? TimeOfDeath { get; set; }
 
-        /// <summary>
-        ///     The component currently owned by this mind.
-        ///     Can be null.
-        /// </summary>
-        [ViewVariables] public MindContainerComponent? OwnedComponent;
-
         /// <summary>
         ///     The entity currently owned by this mind.
         ///     Can be null.
         /// </summary>
-        [ViewVariables, Access(typeof(SharedMindSystem))]
+        [DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
         public EntityUid? OwnedEntity { get; set; }
 
-        // TODO move objectives out of mind component
         /// <summary>
         ///     An enumerable over all the objective entities this mind has.
         /// </summary>
-        [ViewVariables]
+        [ViewVariables, Obsolete("Use Objectives field")]
         public IEnumerable<EntityUid> AllObjectives => Objectives;
 
         /// <summary>
@@ -100,6 +102,7 @@ namespace Content.Shared.Mind
         ///     Can be null, in which case the player is currently not logged in.
         /// </summary>
         [ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
+        // TODO remove this after moving IPlayerManager functions to shared
         public ICommonSession? Session { get; set; }
     }
 }
index cc4ac4af23675c5ff2ed38b689d6de3df9c86bb1..6fc1c01dc23d1ed6aeadcab598bd6a09f9a0a9c8 100644 (file)
@@ -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<NetUserId, EntityUid> UserMinds = new();
 
     public override void Initialize()
@@ -36,6 +37,7 @@ public abstract class SharedMindSystem : EntitySystem
         SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
         SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
         SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
+        SubscribeLocalEvent<MindComponent, ComponentStartup>(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<MindComponent>? mind)
+    {
+        if (!TryGetMind(user, out var mindId, out var mindComp))
+        {
+            mind = null;
+            return false;
+        }
+
+        mind = (mindId.Value, mindComp);
+        return true;
+    }
+
+    public Entity<MindComponent> 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<MindComponent> 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);
     }
 
     /// <summary>
@@ -195,7 +250,7 @@ public abstract class SharedMindSystem : EntitySystem
     /// Cleans up the VisitingEntity.
     /// </summary>
     /// <param name="mind"></param>
-    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);
     }
 
-    /// <summary>
-    /// Sets the Mind's OwnedComponent and OwnedEntity
-    /// </summary>
-    /// <param name="mind">Mind to set OwnedComponent and OwnedEntity on</param>
-    /// <param name="uid">Entity owned by <paramref name="mind"/></param>
-    /// <param name="mindContainerComponent">MindContainerComponent owned by <paramref name="mind"/></param>
-    protected void SetOwnedEntity(MindComponent mind, EntityUid? uid, MindContainerComponent? mindContainerComponent)
-    {
-        if (uid != null)
-            Resolve(uid.Value, ref mindContainerComponent);
-
-        mind.OwnedEntity = uid;
-        mind.OwnedComponent = mindContainerComponent;
-    }
-
     /// <summary>
     /// 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