diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs index bd79c0f86ba3..2971573ff28e 100644 --- a/Content.IntegrationTests/Pair/TestPair.cs +++ b/Content.IntegrationTests/Pair/TestPair.cs @@ -1,7 +1,12 @@ #nullable enable using System.Collections.Generic; using System.IO; +using System.Linq; using Content.Server.GameTicking; +using Content.Server.Players; +using Content.Shared.Mind; +using Content.Shared.Players; +using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Network; @@ -25,6 +30,9 @@ public sealed partial class TestPair public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!; public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!; + public IPlayerSession? Player => (IPlayerSession?) Server.PlayerMan.Sessions.FirstOrDefault(); + public PlayerData? PlayerData => Player?.Data.ContentData(); + public PoolTestLogHandler ServerLogHandler { get; private set; } = default!; public PoolTestLogHandler ClientLogHandler { get; private set; } = default!; diff --git a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs new file mode 100644 index 000000000000..0c9bfbfc7438 --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs @@ -0,0 +1,55 @@ +#nullable enable +using Robust.Shared.Console; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Minds; + +[TestFixture] +public sealed partial class MindTests +{ + [Test] + public async Task DeleteAllThenGhost() + { + var settings = new PoolSettings + { + Dirty = true, + DummyTicker = false, + Connected = true + }; + await using var pair = await PoolManager.GetServerClient(settings); + + // Client is connected with a valid entity & mind + Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity)); + Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind)); + + // Delete **everything** + var conHost = pair.Server.ResolveDependency(); + await pair.Server.WaitPost(() => conHost.ExecuteCommand("entities delete")); + await pair.RunTicksSync(5); + + Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0)); + Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0)); + + // Create a new map. + int mapId = 1; + await pair.Server.WaitPost(() => conHost.ExecuteCommand($"addmap {mapId}")); + await pair.RunTicksSync(5); + + // Client is not attached to anything + Assert.Null(pair.Client.Player?.ControlledEntity); + Assert.Null(pair.PlayerData?.Mind); + + // Attempt to ghost + var cConHost = pair.Client.ResolveDependency(); + await pair.Client.WaitPost(() => cConHost.ExecuteCommand("ghost")); + await pair.RunTicksSync(10); + + // Client should be attached to a ghost placed on the new map. + Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity)); + Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind)); + var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value); + Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId))); + + await pair.CleanReturnAsync(); + } +} diff --git a/Content.Server/Ghost/Ghost.cs b/Content.Server/Ghost/Ghost.cs index 0462d1bc943e..d04b1197afa9 100644 --- a/Content.Server/Ghost/Ghost.cs +++ b/Content.Server/Ghost/Ghost.cs @@ -27,8 +27,8 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var minds = _entities.System(); if (!minds.TryGetMind(player, out var mindId, out var mind)) { - shell.WriteLine("You have no Mind, you can't ghost."); - return; + mindId = minds.CreateMind(player.UserId); + mind = _entities.GetComponent(mindId); } if (!EntitySystem.Get().OnGhostAttempt(mindId, true, true, mind)) diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index aca5a9d485da..06f97bd3b9f6 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -31,6 +31,25 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnMindContainerTerminating); + SubscribeLocalEvent(OnMindShutdown); + } + + private void OnMindShutdown(EntityUid uid, MindComponent mind, ComponentShutdown args) + { + if (mind.UserId is {} user) + { + UserMinds.Remove(user); + if (_players.GetPlayerData(user).ContentData() is { } oldData) + oldData.Mind = null; + mind.UserId = null; + } + + if (!TryComp(mind.OwnedEntity, out MetaDataComponent? meta) || meta.EntityLifeStage >= EntityLifeStage.Terminating) + return; + + 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) @@ -195,11 +214,11 @@ public override void UnVisit(EntityUid mindId, MindComponent? mind = null) public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostCheckOverride = false, bool createGhost = true, MindComponent? mind = null) { - base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind); - - if (!Resolve(mindId, ref mind)) + if (mind == null && !Resolve(mindId, ref mind)) return; + base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind); + if (entity == mind.OwnedEntity) return; diff --git a/Content.Shared/Mind/MindComponent.cs b/Content.Shared/Mind/MindComponent.cs index d6e30130e7d1..3ea92c3ce729 100644 --- a/Content.Shared/Mind/MindComponent.cs +++ b/Content.Shared/Mind/MindComponent.cs @@ -65,8 +65,7 @@ public sealed partial class MindComponent : Component /// The component currently owned by this mind. /// Can be null. /// - [ViewVariables] - public MindContainerComponent? OwnedComponent { get; internal set; } + [ViewVariables] public MindContainerComponent? OwnedComponent; /// /// The entity currently owned by this mind. diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index 91f68b024543..b7cd30e96219 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -8,13 +8,11 @@ using Content.Shared.Mind.Components; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using Content.Shared.Objectives; using Content.Shared.Objectives.Systems; using Content.Shared.Players; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Players; -using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Mind; @@ -25,6 +23,7 @@ public abstract class SharedMindSystem : EntitySystem [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(); @@ -132,12 +131,11 @@ private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideE 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); - Dirty(mindId, MetaData(mindId)); - return mindId; } @@ -343,7 +341,14 @@ public bool TryGetMind( { mindId = default; mind = null; - return _player.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind); + if (_player.ContentData(player) is not { } data) + return false; + + if (TryGetMind(data, out mindId, out mind)) + return true; + + DebugTools.AssertNull(data.Mind); + return false; } ///