diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 0f1690d453..597c063dc7 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Content.Client.Construction; using Content.Client.Examine; using Content.Server.Body.Systems; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.Players; using Content.Server.Stack; @@ -184,7 +185,7 @@ public abstract partial class InteractionTest { // Fuck you mind system I want an hour of my life back // Mind system is a time vampire - ServerSession.ContentData()?.WipeMind(); + SEntMan.System().WipeMind(ServerSession.ContentData()?.Mind); old = cPlayerMan.LocalPlayer.ControlledEntity; Player = SEntMan.SpawnEntity(PlayerPrototype, PlayerCoords); diff --git a/Content.IntegrationTests/Tests/Minds/GhostTests.cs b/Content.IntegrationTests/Tests/Minds/GhostTests.cs new file mode 100644 index 0000000000..5d9ea2b30b --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/GhostTests.cs @@ -0,0 +1,234 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Server.GameTicking; +using Content.Server.Ghost.Components; +using Content.Server.Mind; +using Content.Server.Mind.Components; +using Content.Server.Players; +using NUnit.Framework; +using Robust.Server.Console; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Network; + +namespace Content.IntegrationTests.Tests.Minds; + +[TestFixture] +public sealed class GhostTests +{ + /// + /// Test that a ghost gets created when the player entity is deleted. + /// 1. Delete mob + /// 2. Assert is ghost + /// + [Test] + public async Task TestGhostOnDelete() + { + // Client is needed to spawn session + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + + IPlayerSession player = playerMan.ServerSessions.Single(); + + await server.WaitAssertion(() => + { + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + entMan.DeleteEntity(player.AttachedEntity!.Value); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + // Is player a ghost? + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + var entity = player.AttachedEntity!.Value; + Assert.That(entMan.HasComponent(entity)); + }); + + await pairTracker.CleanReturnAsync(); + } + + /// + /// Test that when the original mob gets deleted, the visited ghost does not get deleted. + /// And that the visited ghost becomes the main mob. + /// 1. Visit ghost + /// 2. Delete original mob + /// 3. Assert is ghost + /// 4. Assert was not deleted + /// 5. Assert is main mob + /// + [Test] + public async Task TestOriginalDeletedWhileGhostingKeepsGhost() + { + // Client is needed to spawn session + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + var gameTicker = entMan.EntitySysManager.GetEntitySystem(); + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + IPlayerSession player = playerMan.ServerSessions.Single(); + + EntityUid originalEntity = default!; + EntityUid ghost = default!; + + await server.WaitAssertion(() => + { + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + originalEntity = player.AttachedEntity!.Value; + + Assert.That(mindSystem.TryGetMind(player.UserId, out var mind), "could not find mind"); + ghost = entMan.SpawnEntity("MobObserver", MapCoordinates.Nullspace); + mindSystem.Visit(mind, ghost); + + Assert.That(player.AttachedEntity, Is.EqualTo(ghost)); + Assert.That(entMan.HasComponent(player.AttachedEntity), "player is not a ghost"); + Assert.That(mind.VisitingEntity, Is.EqualTo(player.AttachedEntity)); + Assert.That(mind.OwnedEntity, Is.EqualTo(originalEntity)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + await server.WaitAssertion(() => entMan.DeleteEntity(originalEntity)); + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + // Is player a ghost? + Assert.That(!entMan.Deleted(ghost), "ghost has been deleted"); + Assert.That(player.AttachedEntity, Is.EqualTo(ghost)); + Assert.That(entMan.HasComponent(player.AttachedEntity)); + + Assert.That(mindSystem.TryGetMind(player.UserId, out var mind), "could not find mind"); + Assert.That(mind.UserId, Is.EqualTo(player.UserId)); + Assert.That(mind.Session, Is.EqualTo(player)); + Assert.IsNull(mind.VisitingEntity); + Assert.That(mind.OwnedEntity, Is.EqualTo(ghost)); + }); + + await pairTracker.CleanReturnAsync(); + } + + /// + /// Test that ghosts can become admin ghosts without issue + /// 1. Become a ghost + /// 2. visit an admin ghost + /// 3. original ghost is deleted, player is an admin ghost. + /// + [Test] + public async Task TestGhostToAghost() + { + // Client is needed to spawn session + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + var serverConsole = server.ResolveDependency(); + + IPlayerSession player = playerMan.ServerSessions.Single(); + + EntityUid ghost = default!; + + await server.WaitAssertion(() => + { + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + entMan.DeleteEntity(player.AttachedEntity!.Value); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + // Is player a ghost? + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + ghost = player.AttachedEntity!.Value; + Assert.That(entMan.HasComponent(ghost)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + serverConsole.ExecuteCommand(player, "aghost"); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + Assert.That(entMan.Deleted(ghost)); + Assert.That(player.AttachedEntity, Is.Not.EqualTo(ghost)); + Assert.That(entMan.HasComponent(player.AttachedEntity!.Value)); + + var mind = player.ContentData()?.Mind; + Assert.NotNull(mind); + Assert.Null(mind.VisitingEntity); + }); + + await pairTracker.CleanReturnAsync(); + } + + /// + /// Test ghost getting deleted while player is connected spawns another ghost + /// 1. become ghost + /// 2. delete ghost + /// 3. new ghost is spawned + /// + [Test] + public async Task TestGhostDeletedSpawnsNewGhost() + { + // Client is needed to spawn session + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + var serverConsole = server.ResolveDependency(); + + IPlayerSession player = playerMan.ServerSessions.Single(); + + EntityUid ghost = default!; + + await server.WaitAssertion(() => + { + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + entMan.DeleteEntity(player.AttachedEntity!.Value); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + // Is player a ghost? + Assert.That(player.AttachedEntity, Is.Not.EqualTo(null)); + ghost = player.AttachedEntity!.Value; + Assert.That(entMan.HasComponent(ghost)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + serverConsole.ExecuteCommand(player, "aghost"); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + Assert.That(entMan.Deleted(ghost)); + Assert.That(player.AttachedEntity, Is.Not.EqualTo(ghost)); + Assert.That(entMan.HasComponent(player.AttachedEntity!.Value)); + }); + + await pairTracker.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs b/Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs index 82baf4810d..49626d8082 100644 --- a/Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs +++ b/Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Content.Server.Mind; +using Content.Server.Players; using NUnit.Framework; using Robust.Server.GameObjects; using Robust.Server.Player; @@ -16,6 +17,11 @@ namespace Content.IntegrationTests.Tests.Minds [TestFixture] public sealed class MindEntityDeletionTest { + // This test will do the following: + // - spawn a player + // - visit some entity + // - delete the entity being visited + // - assert that player returns to original entity [Test] public async Task TestDeleteVisiting() { @@ -50,95 +56,34 @@ namespace Content.IntegrationTests.Tests.Minds }); await PoolManager.RunTicksSync(pairTracker.Pair, 5); - - await server.WaitAssertion(() => - { - entMan.DeleteEntity(visitEnt); - - if (mind.VisitingEntity != null) - { - Assert.Fail("Mind VisitingEntity was not null"); - return; - } - - // This used to throw so make sure it doesn't. - entMan.DeleteEntity(playerEnt); - }); - + await server.WaitPost(() => entMan.DeleteEntity(visitEnt)); await PoolManager.RunTicksSync(pairTracker.Pair, 5); - await server.WaitPost(() => - { - mapManager.DeleteMap(map.MapId); - }); - - await pairTracker.CleanReturnAsync(); - } - - [Test] - public async Task TestGhostOnDelete() - { - // Has to be a non-dummy ticker so we have a proper map. - - await using var pairTracker = await PoolManager.GetServerClient(); - var server = pairTracker.Pair.Server; - - var entMan = server.ResolveDependency(); - var playerMan = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); - - var mindSystem = entMan.EntitySysManager.GetEntitySystem(); - - var map = await PoolManager.CreateTestMap(pairTracker); - - EntityUid playerEnt = default; - Mind mind = default!; - await server.WaitAssertion(() => - { - var player = playerMan.ServerSessions.Single(); - - var pos = new MapCoordinates(Vector2.Zero, map.MapId); - - playerEnt = entMan.SpawnEntity(null, pos); - - mind = mindSystem.CreateMind(player.UserId); - mindSystem.TransferTo(mind, playerEnt); - - Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt)); - }); - - await PoolManager.RunTicksSync(pairTracker.Pair, 5); - - await server.WaitPost(() => - { - entMan.DeleteEntity(playerEnt); - }); - - await PoolManager.RunTicksSync(pairTracker.Pair, 5); - - await server.WaitAssertion(() => - { - Assert.That(entMan.EntityExists(mind.CurrentEntity!.Value), Is.True); - }); - - await server.WaitPost(() => - { - mapManager.DeleteMap(map.MapId); - }); + Assert.IsNull(mind.VisitingEntity); + Assert.That(entMan.EntityExists(mind.OwnedEntity)); + Assert.That(mind.OwnedEntity, Is.EqualTo(playerEnt)); + // This used to throw so make sure it doesn't. + await server.WaitPost(() => entMan.DeleteEntity(mind.OwnedEntity!.Value)); + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitPost(() => mapManager.DeleteMap(map.MapId)); await pairTracker.CleanReturnAsync(); } + // this is a variant of TestGhostOnDelete that just deletes the whole map. [Test] public async Task TestGhostOnDeleteMap() { - await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true }); + await using var pairTracker = await PoolManager.GetServerClient(); var server = pairTracker.Pair.Server; var testMap = await PoolManager.CreateTestMap(pairTracker); var coordinates = testMap.GridCoords; var entMan = server.ResolveDependency(); var mapManager = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + var player = playerMan.ServerSessions.Single(); var mindSystem = entMan.EntitySysManager.GetEntitySystem(); @@ -149,8 +94,7 @@ namespace Content.IntegrationTests.Tests.Minds await server.WaitAssertion(() => { playerEnt = entMan.SpawnEntity(null, coordinates); - - mind = mindSystem.CreateMind(null); + mind = player.ContentData()!.Mind!; mindSystem.TransferTo(mind, playerEnt); Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt)); diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs new file mode 100644 index 0000000000..b4cae0bd88 --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs @@ -0,0 +1,134 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Ghost.Components; +using Content.Server.Mind; +using Content.Server.Players; +using NUnit.Framework; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Network; +using IPlayerManager = Robust.Server.Player.IPlayerManager; + +namespace Content.IntegrationTests.Tests.Minds; + +// This partial class contains misc helper functions for other tests. +[TestFixture] +public sealed partial class MindTests +{ + public async Task BecomeGhost(Pair pair, bool visit = false) + { + var entMan = pair.Server.ResolveDependency(); + var playerMan = pair.Server.ResolveDependency(); + var mindSys = entMan.System(); + EntityUid ghostUid = default; + Mind mind = default!; + + var player = playerMan.ServerSessions.Single(); + await pair.Server.WaitAssertion(() => + { + var oldUid = player.AttachedEntity; + ghostUid = entMan.SpawnEntity("MobObserver", MapCoordinates.Nullspace); + mind = mindSys.GetMind(player.UserId); + Assert.NotNull(mind); + + if (visit) + { + mindSys.Visit(mind, ghostUid); + return; + } + + mindSys.TransferTo(mind, ghostUid); + if (oldUid != null) + entMan.DeleteEntity(oldUid.Value); + + }); + + await PoolManager.RunTicksSync(pair, 5); + Assert.That(entMan.HasComponent(ghostUid)); + Assert.That(player.AttachedEntity == ghostUid); + Assert.That(mind.CurrentEntity == ghostUid); + + if (!visit) + Assert.Null(mind.VisitingEntity); + + return ghostUid; + } + + public async Task VisitGhost(Pair pair, bool visit = false) + { + return await BecomeGhost(pair, visit: true); + } + + /// + /// Get the player's current mind and check that the entities exists. + /// + public Mind GetMind(Pair pair) + { + var playerMan = pair.Server.ResolveDependency(); + var entMan = pair.Server.ResolveDependency(); + var player = playerMan.ServerSessions.SingleOrDefault(); + Assert.NotNull(player); + + var mind = player.ContentData()!.Mind; + Assert.NotNull(mind); + Assert.That(player.AttachedEntity, Is.EqualTo(mind.CurrentEntity)); + Assert.That(entMan.EntityExists(mind.OwnedEntity)); + Assert.That(entMan.EntityExists(mind.CurrentEntity)); + + return mind; + } + + public async Task Disconnect(Pair pair) + { + var netManager = pair.Client.ResolveDependency(); + var playerMan = pair.Server.ResolveDependency(); + var player = playerMan.ServerSessions.Single(); + var mind = player.ContentData()!.Mind; + + await pair.Client.WaitAssertion(() => + { + netManager.ClientDisconnect("Disconnect command used."); + }); + await PoolManager.RunTicksSync(pair, 5); + + Assert.That(player.Status == SessionStatus.Disconnected); + Assert.NotNull(mind.UserId); + Assert.Null(mind.Session); + } + + public async Task Connect(Pair pair, string username) + { + var netManager = pair.Client.ResolveDependency(); + var playerMan = pair.Server.ResolveDependency(); + Assert.That(!playerMan.ServerSessions.Any()); + + await Task.WhenAll(pair.Client.WaitIdleAsync(), pair.Client.WaitIdleAsync()); + pair.Client.SetConnectTarget(pair.Server); + await pair.Client.WaitPost(() => netManager.ClientConnect(null!, 0, username)); + await PoolManager.RunTicksSync(pair, 5); + + var player = playerMan.ServerSessions.Single(); + Assert.That(player.Status == SessionStatus.InGame); + } + + public async Task DisconnectReconnect(Pair pair) + { + var playerMan = pair.Server.ResolveDependency(); + var player = playerMan.ServerSessions.Single(); + var name = player.Name; + var id = player.UserId; + + await Disconnect(pair); + await Connect(pair, name); + + // Session has changed + var newSession = playerMan.ServerSessions.Single(); + Assert.That(newSession != player); + Assert.That(newSession.UserId == id); + + return newSession; + } +} diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs new file mode 100644 index 0000000000..161bcf9dd2 --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs @@ -0,0 +1,157 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Ghost.Components; +using Content.Server.Mind; +using NUnit.Framework; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Minds; + +[TestFixture] +public sealed partial class MindTests +{ + // This test will do the following: + // - attach a player to a ghost (not visiting) + // - disconnect + // - reconnect + // - assert that they spawned in as a new entity + [Test] + public async Task TestGhostsCanReconnect() + { + await using var pairTracker = await PoolManager.GetServerClient(); + var pair = pairTracker.Pair; + + var entMan = pair.Server.ResolveDependency(); + await PoolManager.RunTicksSync(pair, 5); + var mind = GetMind(pair); + + var ghost = await BecomeGhost(pair); + await DisconnectReconnect(pair); + + // Player in control of a NEW entity + var newMind = GetMind(pair); + Assert.That(newMind != mind); + Assert.That(entMan.Deleted(ghost)); + Assert.Null(newMind.VisitingEntity); + + await pairTracker.CleanReturnAsync(); + } + + // This test will do the following: + // - disconnect a player + // - delete their original entity + // - reconnect + // - assert that they spawned in as a new entity + [Test] + public async Task TestDeletedCanReconnect() + { + await using var pairTracker = await PoolManager.GetServerClient(); + var pair = pairTracker.Pair; + + var entMan = pair.Server.ResolveDependency(); + await PoolManager.RunTicksSync(pair, 5); + var mind = GetMind(pair); + + var playerMan = pair.Server.ResolveDependency(); + var player = playerMan.ServerSessions.Single(); + var name = player.Name; + var user = player.UserId; + Assert.NotNull(mind.OwnedEntity); + var entity = mind.OwnedEntity.Value; + + // Player is not a ghost + Assert.That(!entMan.HasComponent(mind.CurrentEntity)); + + // Disconnect + await Disconnect(pair); + + // Delete entity + Assert.That(entMan.EntityExists(entity)); + await pair.Server.WaitPost(() => entMan.DeleteEntity(entity)); + Assert.That(entMan.Deleted(entity)); + Assert.IsNull(mind.OwnedEntity); + + // Reconnect + await Connect(pair, name); + player = playerMan.ServerSessions.Single(); + Assert.That(user == player.UserId); + + // Player is now a new entity + var newMind = GetMind(pair); + Assert.That(newMind != mind); + Assert.Null(mind.UserId); + Assert.Null(mind.CurrentEntity); + Assert.NotNull(newMind.OwnedEntity); + Assert.That(entMan.EntityExists(newMind.OwnedEntity)); + + await pairTracker.CleanReturnAsync(); + } + + // This test will do the following: + // - visit a ghost + // - disconnect + // - reconnect + // - assert that they return to their original entity + [Test] + public async Task TestVisitingGhostReconnect() + { + await using var pairTracker = await PoolManager.GetServerClient(); + var pair = pairTracker.Pair; + + var entMan = pair.Server.ResolveDependency(); + await PoolManager.RunTicksSync(pair, 5); + var mind = GetMind(pair); + + var original = mind.CurrentEntity; + var ghost = await VisitGhost(pair); + await DisconnectReconnect(pair); + + // Player now controls their original mob, mind was preserved + Assert.That(mind == GetMind(pair)); + Assert.That(mind.CurrentEntity == original); + Assert.That(!entMan.Deleted(original)); + Assert.That(entMan.Deleted(ghost)); + + await pairTracker.CleanReturnAsync(); + } + + // This test will do the following: + // - visit a normal (non-ghost) entity, + // - disconnect + // - reconnect + // - assert that they return to the visited entity. + [Test] + public async Task TestVisitingReconnect() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ ExtraPrototypes = Prototypes }); + var pair = pairTracker.Pair; + + var entMan = pair.Server.ResolveDependency(); + var mindSys = entMan.System(); + await PoolManager.RunTicksSync(pair, 5); + var mind = GetMind(pair); + + // Make player visit a new mob + var original = mind.CurrentEntity; + EntityUid visiting = default; + await pair.Server.WaitAssertion(() => + { + visiting = entMan.SpawnEntity("MindTestEntity", MapCoordinates.Nullspace); + mindSys.Visit(mind, visiting); + }); + await PoolManager.RunTicksSync(pair, 5); + + await DisconnectReconnect(pair); + + // Player is back in control of the visited mob, mind was preserved + Assert.That(mind == GetMind(pair)); + Assert.That(!entMan.Deleted(original)); + Assert.That(!entMan.Deleted(visiting)); + Assert.That(mind.CurrentEntity == visiting); + Assert.That(mind.CurrentEntity == visiting); + + await pairTracker.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs index 68c1617554..e5d781c96d 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs @@ -28,7 +28,7 @@ using IPlayerManager = Robust.Server.Player.IPlayerManager; namespace Content.IntegrationTests.Tests.Minds; [TestFixture] -public sealed class MindTests +public sealed partial class MindTests { private const string Prototypes = @" - type: entity @@ -125,7 +125,7 @@ public sealed class MindTests var mind = mindSystem.CreateMind(null); mindSystem.TransferTo(mind, entity); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); - + var mind2 = mindSystem.CreateMind(null); mindSystem.TransferTo(mind2, entity); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind2)); @@ -220,32 +220,44 @@ public sealed class MindTests [Test] public async Task TestOwningPlayerCanBeChanged() { - await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true }); + await using var pairTracker = await PoolManager.GetServerClient(); var server = pairTracker.Pair.Server; var entMan = server.ResolveDependency(); + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + var originalMind = GetMind(pairTracker.Pair); + var userId = originalMind.UserId; + + Mind mind = default!; await server.WaitAssertion(() => { - var mindSystem = entMan.EntitySysManager.GetEntitySystem(); - var entity = entMan.SpawnEntity(null, new MapCoordinates()); var mindComp = entMan.EnsureComponent(entity); + entMan.DirtyEntity(entity); - var mind = mindSystem.CreateMind(null); - + mind = mindSystem.CreateMind(null); mindSystem.TransferTo(mind, entity); - Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); - - var newUserId = new NetUserId(Guid.NewGuid()); Assert.That(mindComp.HasMind); - CatchPlayerDataException(() => - mindSystem.ChangeOwningPlayer(mindComp.Mind!, newUserId)); - - Assert.That(mind.UserId, Is.EqualTo(newUserId)); }); + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + mindSystem.SetUserId(mind, userId); + Assert.That(mind.UserId, Is.EqualTo(userId)); + Assert.That(originalMind.UserId, Is.EqualTo(null)); + + mindSystem.SetUserId(originalMind, userId); + Assert.That(mind.UserId, Is.EqualTo(null)); + Assert.That(originalMind.UserId, Is.EqualTo(userId)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + await pairTracker.CleanReturnAsync(); } @@ -275,26 +287,26 @@ public sealed class MindTests Assert.That(!mindSystem.HasRole(mind)); var traitorRole = new TraitorRole(mind, new AntagPrototype()); - + mindSystem.AddRole(mind, traitorRole); - + Assert.That(mindSystem.HasRole(mind)); Assert.That(!mindSystem.HasRole(mind)); var jobRole = new Job(mind, new JobPrototype()); - + mindSystem.AddRole(mind, jobRole); - + Assert.That(mindSystem.HasRole(mind)); Assert.That(mindSystem.HasRole(mind)); - + mindSystem.RemoveRole(mind, traitorRole); - + Assert.That(!mindSystem.HasRole(mind)); Assert.That(mindSystem.HasRole(mind)); - + mindSystem.RemoveRole(mind, jobRole); - + Assert.That(!mindSystem.HasRole(mind)); Assert.That(!mindSystem.HasRole(mind)); }); @@ -353,7 +365,7 @@ public sealed class MindTests MakeSentientCommand.MakeSentient(mob, IoCManager.Resolve()); mobMind = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second"); - mindSystem.ChangeOwningPlayer(mobMind, player.UserId); + mindSystem.SetUserId(mobMind, player.UserId); mindSystem.TransferTo(mobMind, mob); }); diff --git a/Content.Server/GameTicking/Commands/RespawnCommand.cs b/Content.Server/GameTicking/Commands/RespawnCommand.cs index 3d24443023..df08b05084 100644 --- a/Content.Server/GameTicking/Commands/RespawnCommand.cs +++ b/Content.Server/GameTicking/Commands/RespawnCommand.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Content.Server.Players; using Robust.Server.Player; using Robust.Shared.Console; @@ -21,7 +22,9 @@ namespace Content.Server.GameTicking.Commands } var playerMgr = IoCManager.Resolve(); - var ticker = EntitySystem.Get(); + var sysMan = IoCManager.Resolve(); + var ticker = sysMan.GetEntitySystem(); + var mind = sysMan.GetEntitySystem(); NetUserId userId; if (args.Length == 0) @@ -48,7 +51,7 @@ namespace Content.Server.GameTicking.Commands return; } - data.ContentData()?.WipeMind(); + mind.WipeMind(data.ContentData()?.Mind); shell.WriteLine("Player is not currently online, but they will respawn if they come back online"); return; } diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs index fd4a2e6a9c..31b2eb4773 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -27,15 +27,28 @@ namespace Content.Server.GameTicking { var session = args.Session; + if (_mindSystem.TryGetMind(session.UserId, out var mind)) + { + if (args.OldStatus == SessionStatus.Connecting && args.NewStatus == SessionStatus.Connected) + mind.Session = session; + + DebugTools.Assert(mind.Session == session); + DebugTools.Assert(session.Data.ContentData()?.Mind is not {} dataMind || dataMind == mind); + } + switch (args.NewStatus) { case SessionStatus.Connected: { AddPlayerToDb(args.Session.UserId.UserId); - // Always make sure the client has player data. Mind gets assigned on spawn. + // Always make sure the client has player data. if (session.Data.ContentDataUncast == null) - session.Data.ContentDataUncast = new PlayerData(session.UserId, args.Session.Name); + { + var data = new PlayerData(session.UserId, args.Session.Name); + data.Mind = mind; + session.Data.ContentDataUncast = data; + } // Make the player actually join the game. // timer time must be > tick length @@ -74,28 +87,28 @@ namespace Content.Server.GameTicking return; } + SpawnWaitDb(); + break; + } + if (data.Mind.CurrentEntity == null || Deleted(data.Mind.CurrentEntity)) + { + DebugTools.Assert(data.Mind.CurrentEntity == null, "a mind's current entity has been deleted"); SpawnWaitDb(); } else { - if (data.Mind.CurrentEntity == null) - { - SpawnWaitDb(); - } - else - { - session.AttachToEntity(data.Mind.CurrentEntity); - PlayerJoinGame(session); - } + session.AttachToEntity(data.Mind.CurrentEntity); + PlayerJoinGame(session); } - break; } case SessionStatus.Disconnected: { _chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name))); + if (mind != null) + mind.Session = null; _userDb.ClientDisconnected(session); break; diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 4f21cc2184..6b85f718e0 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -414,13 +414,6 @@ namespace Content.Server.GameTicking PlayerJoinLobby(player); } - // Delete the minds of everybody. - // TODO: Maybe move this into a separate manager? - foreach (var unCastData in _playerManager.GetAllPlayerData()) - { - unCastData.ContentData()?.WipeMind(); - } - // Delete all entities. foreach (var entity in EntityManager.GetEntities().ToArray()) { diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 115c3ace02..73b3ad74d5 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -175,9 +175,8 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(data); - data!.WipeMind(); - var newMind = _mindSystem.CreateMind(data.UserId, character.Name); - _mindSystem.ChangeOwningPlayer(newMind, data.UserId); + var newMind = _mindSystem.CreateMind(data!.UserId, character.Name); + _mindSystem.SetUserId(newMind, data.UserId); var jobPrototype = _prototypeManager.Index(jobId); var job = new Job(newMind, jobPrototype); @@ -244,7 +243,7 @@ namespace Content.Server.GameTicking public void Respawn(IPlayerSession player) { - player.ContentData()?.WipeMind(); + _mindSystem.WipeMind(player); _adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned."); if (LobbyEnabled) @@ -278,9 +277,8 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(data); - data!.WipeMind(); - var newMind = _mindSystem.CreateMind(data.UserId); - _mindSystem.ChangeOwningPlayer(newMind, data.UserId); + var newMind = _mindSystem.CreateMind(data!.UserId); + _mindSystem.SetUserId(newMind, data.UserId); _mindSystem.AddRole(newMind, new ObserverRole(newMind)); var mob = SpawnObserverMob(); diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 9bc2324d4f..b99af24f0e 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -759,7 +759,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns)); SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component); var newMind = _mindSystem.CreateMind(session.UserId, spawnDetails.Name); - _mindSystem.ChangeOwningPlayer(newMind, session.UserId); + _mindSystem.SetUserId(newMind, session.UserId); _mindSystem.AddRole(newMind, new NukeopsRole(newMind, nukeOpsAntag)); _mindSystem.TransferTo(newMind, mob); diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs index e38eeba2e9..5bccab0b9e 100644 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs @@ -208,7 +208,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem var session = ops[i]; var newMind = _mindSystem.CreateMind(session.UserId, name); - _mindSystem.ChangeOwningPlayer(newMind, session.UserId); + _mindSystem.SetUserId(newMind, session.UserId); var mob = Spawn("MobHuman", _random.Pick(spawns)); MetaData(mob).EntityName = name; diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 9e598964c3..6bc2604e60 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -236,8 +236,6 @@ namespace Content.Server.Ghost if (Deleted(uid) || Terminating(uid)) return; - if (EntityManager.TryGetComponent(uid, out var mind)) - _mindSystem.SetGhostOnShutdown(uid, false, mind); QueueDel(uid); } diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs index 70f4673b19..09cb53392b 100644 --- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs @@ -222,7 +222,7 @@ namespace Content.Server.Ghost.Roles EntityManager.GetComponent(mob).EntityName); _mindSystem.AddRole(newMind, new GhostRoleMarkerRole(newMind, role.RoleName)); - _mindSystem.ChangeOwningPlayer(newMind, player.UserId); + _mindSystem.SetUserId(newMind, player.UserId); _mindSystem.TransferTo(newMind, mob); } diff --git a/Content.Server/Mind/Components/MindContainerComponent.cs b/Content.Server/Mind/Components/MindContainerComponent.cs index bf2c8291fc..0333c75ada 100644 --- a/Content.Server/Mind/Components/MindContainerComponent.cs +++ b/Content.Server/Mind/Components/MindContainerComponent.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using YamlDotNet.Core.Tokens; namespace Content.Server.Mind.Components { diff --git a/Content.Server/Mind/Mind.cs b/Content.Server/Mind/Mind.cs index 81f26d55a9..b1a6e228e0 100644 --- a/Content.Server/Mind/Mind.cs +++ b/Content.Server/Mind/Mind.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking; using Content.Server.Mind.Components; using Content.Server.Objectives; using Content.Server.Roles; @@ -30,16 +31,14 @@ namespace Content.Server.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) + public Mind() { - OriginalOwnerUserId = userId; } /// /// The session ID of the player owning this mind. /// - [ViewVariables] + [ViewVariables, Access(typeof(MindSystem))] public NetUserId? UserId { get; internal set; } /// @@ -112,7 +111,7 @@ namespace Content.Server.Mind /// The session of the player owning this mind. /// Can be null, in which case the player is currently not logged in. /// - [ViewVariables] + [ViewVariables, Access(typeof(MindSystem), typeof(GameTicker))] public IPlayerSession? Session { get; internal set; } /// diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index ccb180cd5a..c6b8a65af0 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -10,6 +10,7 @@ using Content.Server.Players; using Content.Server.Roles; using Content.Shared.Database; using Content.Shared.Examine; +using Content.Shared.GameTicking; using Content.Shared.Mobs.Systems; using Content.Shared.Interaction.Events; using Content.Shared.Mobs.Components; @@ -30,29 +31,25 @@ public sealed class MindSystem : EntitySystem [Dependency] private readonly GhostSystem _ghostSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly ActorSystem _actor = default!; + + // This is dictionary is required to track the minds of disconnected players that may have had their entity deleted. + private readonly Dictionary _userMinds = new(); public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnSuicide); - SubscribeLocalEvent(OnTerminating); - SubscribeLocalEvent(OnDetached); + SubscribeLocalEvent(OnMindContainerTerminating); + SubscribeLocalEvent(OnVisitingTerminating); + SubscribeLocalEvent(OnReset); } - private void OnDetached(EntityUid uid, VisitingMindComponent component, PlayerDetachedEvent args) + public override void Shutdown() { - component.Mind = null; - RemCompDeferred(uid, component); - } - - private void OnTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args) - { - if (component.Mind?.Session?.AttachedEntity == uid) - UnVisit(component.Mind); + base.Shutdown(); + WipeAllMinds(); } public void SetGhostOnShutdown(EntityUid uid, bool value, MindContainerComponent? mind = null) @@ -63,6 +60,49 @@ public sealed class MindSystem : EntitySystem mind.GhostOnShutdown = value; } + private void OnReset(RoundRestartCleanupEvent ev) + { + WipeAllMinds(); + } + + public void WipeAllMinds() + { + foreach (var mind in _userMinds.Values) + { + WipeMind(mind); + } + DebugTools.Assert(_userMinds.Count == 0); + + foreach (var unCastData in _playerManager.GetAllPlayerData()) + { + if (unCastData.ContentData()?.Mind is not { } mind) + continue; + + Log.Error("Player mind was missing from MindSystem dictionary."); + WipeMind(mind); + } + } + + public Mind? GetMind(NetUserId user) + { + TryGetMind(user, out var mind); + return mind; + } + + public bool TryGetMind(NetUserId user, [NotNullWhen(true)] out Mind? mind) + { + if (_userMinds.TryGetValue(user, out mind)) + { + DebugTools.Assert(mind.UserId == user); + DebugTools.Assert(_playerManager.GetPlayerData(user).ContentData() is not {} data + || data.Mind == mind); + return true; + } + + DebugTools.Assert(_playerManager.GetPlayerData(user).ContentData()?.Mind == null); + return false; + } + /// /// Don't call this unless you know what the hell you're doing. /// Use instead. @@ -91,29 +131,39 @@ public sealed class MindSystem : EntitySystem mind.Mind = null; } - private void OnShutdown(EntityUid uid, MindContainerComponent mindContainerComp, ComponentShutdown args) + private void OnVisitingTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args) + { + if (component.Mind != null) + UnVisit(component.Mind); + } + + 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.InRound) return; - if (!TryGetMind(uid, out var mind, mindContainerComp)) + if (component.Mind is not { } mind) return; - if (mind.VisitingEntity is {Valid: true} visiting) + // If the player is currently visiting some other entity, simply attach to that entity. + if (mind.VisitingEntity is {Valid: true} visiting + && visiting != uid + && !Deleted(visiting) + && !Terminating(visiting)) { - if (TryComp(visiting, out GhostComponent? ghost)) - { - _ghostSystem.SetCanReturnToBody(ghost, false); - } - TransferTo(mind, visiting); + if (TryComp(visiting, out GhostComponent? ghost)) + _ghostSystem.SetCanReturnToBody(ghost, false); + return; } - else if (mindContainerComp.GhostOnShutdown) + + TransferTo(mind, null); + + if (component.GhostOnShutdown && mind.Session != null) { - // Changing an entities parents while deleting is VERY sus. This WILL throw exceptions. - // TODO: just find the applicable spawn position directly without actually updating the transform's parent. - Transform(uid).AttachToGridOrMap(); + var xform = Transform(uid); + var gridId = xform.GridUid; var spawnPosition = Transform(uid).Coordinates; // Use a regular timer here because the entity has probably been deleted. @@ -124,11 +174,8 @@ public sealed class MindSystem : EntitySystem return; // Async this so that we don't throw if the grid we're on is being deleted. - var gridId = spawnPosition.GetGridUid(EntityManager); - if (!spawnPosition.IsValid(EntityManager) || gridId == EntityUid.Invalid || !_mapManager.GridExists(gridId)) - { + if (!_mapManager.GridExists(gridId)) spawnPosition = _gameTicker.GetObserverSpawnPoint(); - } // TODO refactor observer spawning. // please. @@ -195,9 +242,10 @@ public sealed class MindSystem : EntitySystem public Mind CreateMind(NetUserId? userId, string? name = null) { - var mind = new Mind(userId); + var mind = new Mind(); mind.CharacterName = name; - ChangeOwningPlayer(mind, userId); + SetUserId(mind, userId); + return mind; } @@ -262,7 +310,6 @@ public sealed class MindSystem : EntitySystem if (mind == null || mind.VisitingEntity == null) return; - DebugTools.Assert(mind.VisitingEntity != mind.OwnedEntity); RemoveVisitingEntity(mind); if (mind.Session == null || mind.Session.AttachedEntity == mind.VisitingEntity) @@ -300,6 +347,25 @@ public sealed class MindSystem : EntitySystem RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true); } + public void WipeMind(IPlayerSession player) + { + var mind = player.ContentData()?.Mind; + DebugTools.Assert(GetMind(player.UserId) == mind); + WipeMind(mind); + } + + /// + /// Detaches a mind from all entities and clears the user ID. + /// + public void WipeMind(Mind? mind) + { + if (mind == null) + return; + + TransferTo(mind, null); + SetUserId(mind, null); + } + /// /// Transfer this mind's control over to a new entity. /// @@ -316,12 +382,8 @@ public sealed class MindSystem : EntitySystem /// public void TransferTo(Mind mind, EntityUid? entity, bool ghostCheckOverride = false) { - // Looks like caller just wants us to go back to normal. if (entity == mind.OwnedEntity) - { - UnVisit(mind); return; - } MindContainerComponent? component = null; var alreadyAttached = false; @@ -382,51 +444,6 @@ public sealed class MindSystem : EntitySystem } } - public void ChangeOwningPlayer(Mind mind, NetUserId? newOwner) - { - // Make sure to remove control from our old owner if they're logged in. - var oldSession = mind.Session; - oldSession?.AttachToEntity(null); - - if (mind.UserId.HasValue) - { - if (_playerManager.TryGetPlayerData(mind.UserId.Value, out var oldUncast)) - { - var data = oldUncast.ContentData(); - DebugTools.AssertNotNull(data); - data!.UpdateMindFromMindChangeOwningPlayer(null); - } - else - { - Log.Warning($"Mind UserId {newOwner} is does not exist in PlayerManager"); - } - } - - SetUserId(mind, newOwner); - if (!newOwner.HasValue) - { - return; - } - - if (!_playerManager.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.", nameof(newOwner)); - } - - // PlayerData? newOwnerData = null; - var newOwnerData = uncast.ContentData(); - - // Yank new owner out of their old mind too. - // Can I mention how much I love the word yank? - DebugTools.AssertNotNull(newOwnerData); - if (newOwnerData!.Mind != null) - ChangeOwningPlayer(newOwnerData.Mind, null); - newOwnerData.UpdateMindFromMindChangeOwningPlayer(mind); - } - /// /// Adds an objective to this mind. /// @@ -569,19 +586,56 @@ public sealed class MindSystem : EntitySystem } /// - /// Sets the Mind's UserId and Session + /// 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, but it may change a player's attached entity. /// /// /// - private void SetUserId(Mind mind, NetUserId? userId) + public void SetUserId(Mind mind, NetUserId? userId) { - mind.UserId = userId; - - if (!userId.HasValue) + if (mind.UserId == userId) return; + if (userId != null && !_playerManager.TryGetPlayerData(userId.Value, out _)) + { + Log.Error($"Attempted to set mind user to invalid value {userId}"); + return; + } + + if (mind.Session != null) + { + mind.Session.AttachToEntity(null); + mind.Session = null; + } + + if (mind.UserId != null) + { + _userMinds.Remove(mind.UserId.Value); + if (_playerManager.GetPlayerData(mind.UserId.Value).ContentData() is { } oldData) + oldData.Mind = null; + mind.UserId = null; + } + + if (userId == null) + { + DebugTools.AssertNull(mind.Session); + return; + } + + if (_userMinds.TryGetValue(userId.Value, out var oldMind)) + SetUserId(oldMind, null); + + DebugTools.AssertNull(_playerManager.GetPlayerData(userId.Value).ContentData()?.Mind); + + _userMinds[userId.Value] = mind; + mind.UserId = userId; + _playerManager.TryGetSessionById(userId.Value, out var ret); mind.Session = ret; + + // session may be null, but user data may still exist for disconnected players. + if (_playerManager.GetPlayerData(userId.Value).ContentData() is { } data) + data.Mind = mind; } /// diff --git a/Content.Server/Players/PlayerData.cs b/Content.Server/Players/PlayerData.cs index c8e368cfed..90d59113c1 100644 --- a/Content.Server/Players/PlayerData.cs +++ b/Content.Server/Players/PlayerData.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking; using Content.Server.Mind; using Robust.Server.Player; using Robust.Shared.Network; @@ -27,8 +28,8 @@ namespace Content.Server.Players /// The currently occupied mind of the player owning this data. /// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING. /// - [ViewVariables] - public Mind.Mind? Mind { get; private set; } + [ViewVariables, Access(typeof(MindSystem), typeof(GameTicker))] + public Mind.Mind? Mind { get; set; } /// /// If true, the player is an admin and they explicitly de-adminned mid-game, @@ -36,27 +37,6 @@ namespace Content.Server.Players /// public bool ExplicitlyDeadminned { get; set; } - public void WipeMind() - { - var entityManager = IoCManager.Resolve(); - var mindSystem = entityManager.System(); - - // This will ensure Mind == null - if (Mind == null) - return; - - mindSystem.TransferTo(Mind, null); - mindSystem.ChangeOwningPlayer(Mind, null); - } - - /// - /// Called from Mind.ChangeOwningPlayer *and nowhere else.* - /// - public void UpdateMindFromMindChangeOwningPlayer(Mind.Mind? mind) - { - Mind = mind; - } - public PlayerData(NetUserId userId, string name) { UserId = userId;