From: ShadowCommander Date: Sat, 11 May 2024 15:03:40 +0000 (-0700) Subject: Fix ghosts getting spawned in nullspace (#27617) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=a985c5e83ead6098783ed2129eed516dbd619586;p=space-station-14.git Fix ghosts getting spawned in nullspace (#27617) * Add tests for ghost spawn position * Make ghosts spawn immediately * Format mind system * Move ghost spawning to GhostSystem * Spawn ghost on grid or map This fixes the ghosts being attached the parent entity instead of the grid. * Move logging out of the ghost system * Make round start observer spawn using GhostSystem * Move GameTicker ghost spawning to GhostSystem Moved the more robust character name selection code over. Moved the TimeOfDeath code over. Added canReturn logic. * Add overrides and default for ghost spawn coordinates * Add warning log to ghost spawn fail * Clean up test * Dont spawn ghost on map delete * Minor changes to the role test * Fix role test failing to spawn ghost It was failing the map check due to using Nullspace * Fix ghost tests when running in parallel Not sure what happened, but it seems to be because they were running simultaneously and overwriting values. * Clean up ghost tests * Test that map deletion does not spawn ghosts * Spawn ghost on the next available map * Disallow spawning on deleted maps * Fix map deletion ghost test * Cleanup --- diff --git a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs index ca97e435a7..150bc951f8 100644 --- a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs +++ b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Linq; +using Content.IntegrationTests.Pair; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Server.Players; @@ -26,7 +27,7 @@ public sealed class GhostRoleTests "; /// - /// This is a simple test that just checks if a player can take a ghost roll and then regain control of their + /// This is a simple test that just checks if a player can take a ghost role and then regain control of their /// original entity without encountering errors. /// [Test] @@ -34,12 +35,15 @@ public sealed class GhostRoleTests { await using var pair = await PoolManager.GetServerClient(new PoolSettings { + Dirty = true, DummyTicker = false, Connected = true }); var server = pair.Server; var client = pair.Client; + var mapData = await pair.CreateTestMap(); + var entMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); var conHost = client.ResolveDependency(); @@ -51,7 +55,7 @@ public sealed class GhostRoleTests EntityUid originalMob = default; await server.WaitPost(() => { - originalMob = entMan.SpawnEntity(null, MapCoordinates.Nullspace); + originalMob = entMan.SpawnEntity(null, mapData.GridCoords); mindSystem.TransferTo(originalMindId, originalMob, true); }); @@ -69,12 +73,12 @@ public sealed class GhostRoleTests Assert.That(entMan.HasComponent(ghost)); Assert.That(ghost, Is.Not.EqualTo(originalMob)); Assert.That(session.ContentData()?.Mind, Is.EqualTo(originalMindId)); - Assert.That(originalMind.OwnedEntity, Is.EqualTo(originalMob)); + Assert.That(originalMind.OwnedEntity, Is.EqualTo(originalMob), $"Original mob: {originalMob}, Ghost: {ghost}"); Assert.That(originalMind.VisitingEntity, Is.EqualTo(ghost)); // Spawn ghost takeover entity. EntityUid ghostRole = default; - await server.WaitPost(() => ghostRole = entMan.SpawnEntity("GhostRoleTestEntity", MapCoordinates.Nullspace)); + await server.WaitPost(() => ghostRole = entMan.SpawnEntity("GhostRoleTestEntity", mapData.GridCoords)); // Take the ghost role await server.WaitPost(() => diff --git a/Content.IntegrationTests/Tests/Minds/GhostTests.cs b/Content.IntegrationTests/Tests/Minds/GhostTests.cs new file mode 100644 index 0000000000..ad9d53a70d --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/GhostTests.cs @@ -0,0 +1,159 @@ +using System.Numerics; +using Content.IntegrationTests.Pair; +using Content.Shared.Ghost; +using Content.Shared.Mind; +using Content.Shared.Players; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.UnitTesting; + +namespace Content.IntegrationTests.Tests.Minds; + +[TestFixture] +public sealed class GhostTests +{ + struct GhostTestData + { + public IEntityManager SEntMan; + public Robust.Server.Player.IPlayerManager SPlayerMan; + public Server.Mind.MindSystem SMindSys; + public SharedTransformSystem STransformSys = default!; + + public TestPair Pair = default!; + + public TestMapData MapData => Pair.TestMap!; + + public RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server; + public RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client; + + /// + /// Initial player coordinates. Note that this does not necessarily correspond to the position of the + /// entity. + /// + public NetCoordinates PlayerCoords = default!; + + public NetEntity Player = default!; + public EntityUid SPlayerEnt = default!; + + public ICommonSession ClientSession = default!; + public ICommonSession ServerSession = default!; + + public GhostTestData() + { + } + } + + private async Task SetupData() + { + var data = new GhostTestData(); + + // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult. + data.Pair = await PoolManager.GetServerClient(new PoolSettings + { + DummyTicker = false, + Connected = true, + Dirty = true + }); + + data.SEntMan = data.Pair.Server.ResolveDependency(); + data.SPlayerMan = data.Pair.Server.ResolveDependency(); + data.SMindSys = data.SEntMan.System(); + data.STransformSys = data.SEntMan.System(); + + // Setup map. + await data.Pair.CreateTestMap(); + data.PlayerCoords = data.SEntMan.GetNetCoordinates(data.MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(data.MapData.MapUid, data.STransformSys, data.SEntMan)); + + if (data.Client.Session == null) + Assert.Fail("No player"); + data.ClientSession = data.Client.Session!; + data.ServerSession = data.SPlayerMan.GetSessionById(data.ClientSession.UserId); + + Entity mind = default!; + await data.Pair.Server.WaitPost(() => + { + data.Player = data.SEntMan.GetNetEntity(data.SEntMan.SpawnEntity(null, data.SEntMan.GetCoordinates(data.PlayerCoords))); + mind = data.SMindSys.CreateMind(data.ServerSession.UserId, "DummyPlayerEntity"); + data.SPlayerEnt = data.SEntMan.GetEntity(data.Player); + data.SMindSys.TransferTo(mind, data.SPlayerEnt, mind: mind.Comp); + data.Server.PlayerMan.SetAttachedEntity(data.ServerSession, data.SPlayerEnt); + }); + + await data.Pair.RunTicksSync(5); + + Assert.Multiple(() => + { + Assert.That(data.ServerSession.ContentData()?.Mind, Is.EqualTo(mind.Owner)); + Assert.That(data.ServerSession.AttachedEntity, Is.EqualTo(data.SPlayerEnt)); + Assert.That(data.ServerSession.AttachedEntity, Is.EqualTo(mind.Comp.CurrentEntity), + "Player is not attached to the mind's current entity."); + Assert.That(data.SEntMan.EntityExists(mind.Comp.OwnedEntity), + "The mind's current entity does not exist"); + Assert.That(mind.Comp.VisitingEntity == null || data.SEntMan.EntityExists(mind.Comp.VisitingEntity), + "The minds visited entity does not exist."); + }); + + Assert.That(data.SPlayerEnt, Is.Not.EqualTo(null)); + + return data; + } + + /// + /// Test that a ghost gets created when the player entity is deleted. + /// 1. Delete mob + /// 2. Assert is ghost + /// + [Test] + public async Task TestGridGhostOnDelete() + { + var data = await SetupData(); + + var oldPosition = data.SEntMan.GetComponent(data.SPlayerEnt).Coordinates; + + Assert.That(!data.SEntMan.HasComponent(data.SPlayerEnt), "Player was initially a ghost?"); + + // Delete entity + await data.Server.WaitPost(() => data.SEntMan.DeleteEntity(data.SPlayerEnt)); + await data.Pair.RunTicksSync(5); + + var ghost = data.ServerSession.AttachedEntity!.Value; + Assert.That(data.SEntMan.HasComponent(ghost), "Player did not become a ghost"); + + // Ensure the position is the same + var ghostPosition = data.SEntMan.GetComponent(ghost).Coordinates; + Assert.That(ghostPosition, Is.EqualTo(oldPosition)); + + await data.Pair.CleanReturnAsync(); + } + + /// + /// Test that a ghost gets created when the player entity is queue deleted. + /// 1. Delete mob + /// 2. Assert is ghost + /// + [Test] + public async Task TestGridGhostOnQueueDelete() + { + var data = await SetupData(); + + var oldPosition = data.SEntMan.GetComponent(data.SPlayerEnt).Coordinates; + + Assert.That(!data.SEntMan.HasComponent(data.SPlayerEnt), "Player was initially a ghost?"); + + // Delete entity + await data.Server.WaitPost(() => data.SEntMan.QueueDeleteEntity(data.SPlayerEnt)); + await data.Pair.RunTicksSync(5); + + var ghost = data.ServerSession.AttachedEntity!.Value; + Assert.That(data.SEntMan.HasComponent(ghost), "Player did not become a ghost"); + + // Ensure the position is the same + var ghostPosition = data.SEntMan.GetComponent(ghost).Coordinates; + Assert.That(ghostPosition, Is.EqualTo(oldPosition)); + + await data.Pair.CleanReturnAsync(); + } + +} diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs index 2ebe750f98..de7739b2ad 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Linq; using Content.Server.GameTicking; using Content.Shared.Ghost; @@ -77,7 +78,7 @@ public sealed partial class MindTests await using var pair = await SetupPair(dirty: true); var server = pair.Server; var testMap = await pair.CreateTestMap(); - var coordinates = testMap.GridCoords; + var testMap2 = await pair.CreateTestMap(); var entMan = server.ResolveDependency(); var mapManager = server.ResolveDependency(); @@ -91,7 +92,7 @@ public sealed partial class MindTests MindComponent mind = default!; await server.WaitAssertion(() => { - playerEnt = entMan.SpawnEntity(null, coordinates); + playerEnt = entMan.SpawnEntity(null, testMap.GridCoords); mindId = player.ContentData()!.Mind!.Value; mind = entMan.GetComponent(mindId); mindSystem.TransferTo(mindId, playerEnt); @@ -100,14 +101,20 @@ public sealed partial class MindTests }); await pair.RunTicksSync(5); - await server.WaitPost(() => mapManager.DeleteMap(testMap.MapId)); + await server.WaitAssertion(() => mapManager.DeleteMap(testMap.MapId)); await pair.RunTicksSync(5); await server.WaitAssertion(() => { #pragma warning disable NUnit2045 // Interdependent assertions. - Assert.That(entMan.EntityExists(mind.CurrentEntity), Is.True); - Assert.That(mind.CurrentEntity, Is.Not.EqualTo(playerEnt)); + // Spawn ghost on the second map + var attachedEntity = player.AttachedEntity; + Assert.That(entMan.EntityExists(attachedEntity), Is.True); + Assert.That(attachedEntity, Is.Not.EqualTo(playerEnt)); + Assert.That(entMan.HasComponent(attachedEntity)); + var transform = entMan.GetComponent(attachedEntity.Value); + Assert.That(transform.MapID, Is.Not.EqualTo(MapId.Nullspace)); + Assert.That(transform.MapID, Is.Not.EqualTo(testMap.MapId)); #pragma warning restore NUnit2045 }); diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index a1946d34a0..fffacb59de 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -274,35 +274,13 @@ namespace Content.Server.GameTicking } } - var xformQuery = GetEntityQuery(); - var coords = _transform.GetMoverCoordinates(position, xformQuery); - - var ghost = Spawn(ObserverPrototypeName, coords); - - // Try setting the ghost entity name to either the character name or the player name. - // If all else fails, it'll default to the default entity prototype name, "observer". - // However, that should rarely happen. - if (!string.IsNullOrWhiteSpace(mind.CharacterName)) - _metaData.SetEntityName(ghost, mind.CharacterName); - else if (!string.IsNullOrWhiteSpace(mind.Session?.Name)) - _metaData.SetEntityName(ghost, mind.Session.Name); - - var ghostComponent = Comp(ghost); - - if (mind.TimeOfDeath.HasValue) - { - _ghost.SetTimeOfDeath(ghost, mind.TimeOfDeath!.Value, ghostComponent); - } + var ghost = _ghost.SpawnGhost((mindId, mind), position, canReturn); + if (ghost == null) + return false; if (playerEntity != null) _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}"); - _ghost.SetCanReturnToBody(ghostComponent, canReturn); - - if (canReturn) - _mind.Visit(mindId, ghost, mind); - else - _mind.TransferTo(mindId, ghost, mind: mind); return true; } diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 869b8b92f7..7478560379 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -8,6 +8,7 @@ using Content.Server.Speech.Components; using Content.Server.Station.Components; using Content.Shared.CCVar; using Content.Shared.Database; +using Content.Shared.Mind; using Content.Shared.Players; using Content.Shared.Preferences; using Content.Shared.Roles; @@ -96,8 +97,7 @@ namespace Content.Server.GameTicking if (job == null) { var playerSession = _playerManager.GetSessionById(netUser); - _chatManager.DispatchServerMessage(playerSession, - Loc.GetString("job-not-available-wait-in-lobby")); + _chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby")); } else { @@ -315,10 +315,7 @@ namespace Content.Server.GameTicking /// The station they're spawning on /// An optional job for them to spawn as /// Whether or not the player should be greeted upon joining - public void MakeJoinGame(ICommonSession player, - EntityUid station, - string? jobId = null, - bool silent = false) + public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false) { if (!_playerGameStatuses.ContainsKey(player.UserId)) return; @@ -351,42 +348,29 @@ namespace Content.Server.GameTicking if (DummyTicker) return; - var mind = player.GetMind(); + Entity? mind = player.GetMind(); if (mind == null) { - mind = _mind.CreateMind(player.UserId); + var name = GetPlayerProfile(player).Name; + var (mindId, mindComp) = _mind.CreateMind(player.UserId, name); + mind = (mindId, mindComp); _mind.SetUserId(mind.Value, player.UserId); _roles.MindAddRole(mind.Value, new ObserverRoleComponent()); } - var name = GetPlayerProfile(player).Name; - var ghost = SpawnObserverMob(); - _metaData.SetEntityName(ghost, name); - _ghost.SetCanReturnToBody(ghost, false); - _mind.TransferTo(mind.Value, ghost); + var ghost = _ghost.SpawnGhost(mind.Value); _adminLogger.Add(LogType.LateJoin, LogImpact.Low, $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}."); } - #region Mob Spawning Helpers - - private EntityUid SpawnObserverMob() - { - var coordinates = GetObserverSpawnPoint(); - return EntityManager.SpawnEntity(ObserverPrototypeName, coordinates); - } - - #endregion - #region Spawn Points public EntityCoordinates GetObserverSpawnPoint() { _possiblePositions.Clear(); - foreach (var (point, transform) in EntityManager - .EntityQuery(true)) + foreach (var (point, transform) in EntityManager.EntityQuery(true)) { if (point.SpawnType != SpawnPointType.Observer) continue; @@ -402,7 +386,7 @@ namespace Content.Server.GameTicking var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var grid)) { - if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused) + if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused || TerminatingOrDeleted(uid)) { continue; } @@ -439,7 +423,9 @@ namespace Content.Server.GameTicking { var mapUid = _mapManager.GetMapEntityId(map); - if (!metaQuery.TryGetComponent(mapUid, out var meta) || meta.EntityPaused) + if (!metaQuery.TryGetComponent(mapUid, out var meta) + || meta.EntityPaused + || TerminatingOrDeleted(mapUid)) { continue; } diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 0be93d2054..ac519b4c2e 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -19,6 +19,7 @@ using Content.Shared.Movement.Systems; using Content.Shared.Storage.Components; using Robust.Server.GameObjects; using Robust.Server.Player; +using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; @@ -42,6 +43,8 @@ namespace Content.Server.Ghost [Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly VisibilitySystem _visibilitySystem = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly IMapManager _mapManager = default!; private EntityQuery _ghostQuery; private EntityQuery _physicsQuery; @@ -389,5 +392,59 @@ namespace Content.Server.Ghost return ghostBoo.Handled; } + + public EntityUid? SpawnGhost(Entity mind, EntityUid targetEntity, + bool canReturn = false) + { + _transformSystem.TryGetMapOrGridCoordinates(targetEntity, out var spawnPosition); + return SpawnGhost(mind, spawnPosition, canReturn); + } + + public EntityUid? SpawnGhost(Entity mind, EntityCoordinates? spawnPosition = null, + bool canReturn = false) + { + if (!Resolve(mind, ref mind.Comp)) + return null; + + // Test if the map is being deleted + var mapUid = spawnPosition?.GetMapUid(EntityManager); + if (mapUid == null || TerminatingOrDeleted(mapUid.Value)) + spawnPosition = null; + + spawnPosition ??= _ticker.GetObserverSpawnPoint(); + + if (!spawnPosition.Value.IsValid(EntityManager)) + { + Log.Warning($"No spawn valid ghost spawn position found for {mind.Comp.CharacterName}" + + " \"{ToPrettyString(mind)}\""); + _minds.TransferTo(mind.Owner, null, createGhost: false, mind: mind.Comp); + return null; + } + + var ghost = SpawnAtPosition(GameTicker.ObserverPrototypeName, spawnPosition.Value); + var ghostComponent = Comp(ghost); + + // Try setting the ghost entity name to either the character name or the player name. + // If all else fails, it'll default to the default entity prototype name, "observer". + // However, that should rarely happen. + if (!string.IsNullOrWhiteSpace(mind.Comp.CharacterName)) + _metaData.SetEntityName(ghost, mind.Comp.CharacterName); + else if (!string.IsNullOrWhiteSpace(mind.Comp.Session?.Name)) + _metaData.SetEntityName(ghost, mind.Comp.Session.Name); + + if (mind.Comp.TimeOfDeath.HasValue) + { + SetTimeOfDeath(ghost, mind.Comp.TimeOfDeath!.Value, ghostComponent); + } + + SetCanReturnToBody(ghostComponent, canReturn); + + if (canReturn) + _minds.Visit(mind.Owner, ghost, mind.Comp); + else + _minds.TransferTo(mind.Owner, ghost, mind: mind.Comp); + Log.Debug($"Spawned ghost \"{ToPrettyString(ghost)}\" for {mind.Comp.CharacterName}."); + return ghost; + } } } diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index dc12836d90..4271d76b44 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.Ghost; using Content.Server.Mind.Commands; using Content.Shared.Database; using Content.Shared.Ghost; @@ -9,10 +10,8 @@ using Content.Shared.Mind.Components; using Content.Shared.Players; using Robust.Server.GameStates; using Robust.Server.Player; -using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Mind; @@ -22,8 +21,7 @@ public sealed class MindSystem : SharedMindSystem [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IPlayerManager _players = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly SharedGhostSystem _ghosts = default!; + [Dependency] private readonly GhostSystem _ghosts = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly PvsOverrideSystem _pvsOverride = default!; @@ -63,8 +61,8 @@ public sealed class MindSystem : SharedMindSystem && !Terminating(visiting)) { TransferTo(mindId, visiting, mind: mind); - if (TryComp(visiting, out GhostComponent? ghost)) - _ghosts.SetCanReturnToBody(ghost, false); + if (TryComp(visiting, out GhostComponent? ghostComp)) + _ghosts.SetCanReturnToBody(ghostComp, false); return; } @@ -74,40 +72,13 @@ public sealed class MindSystem : SharedMindSystem if (!component.GhostOnShutdown || mind.Session == null || _gameTicker.RunLevel == GameRunLevel.PreRoundLobby) return; - 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. - Timer.Spawn(0, () => - { - // Make extra sure the round didn't end between spawning the timer and it being executed. - if (_gameTicker.RunLevel == GameRunLevel.PreRoundLobby) - return; - - // Async this so that we don't throw if the grid we're on is being deleted. - if (!HasComp(gridId)) - spawnPosition = _gameTicker.GetObserverSpawnPoint(); - - // TODO refactor observer spawning. - // please. - if (!spawnPosition.IsValid(EntityManager)) - { - // This should be an error, if it didn't cause tests to start erroring when they delete a player. - Log.Warning($"Entity \"{ToPrettyString(uid)}\" for {mind.CharacterName} was deleted, and no applicable spawn location is available."); - TransferTo(mindId, null, createGhost: false, mind: mind); - return; - } - - var ghost = Spawn(GameTicker.ObserverPrototypeName, spawnPosition); - var ghostComponent = Comp(ghost); - _ghosts.SetCanReturnToBody(ghostComponent, false); - + var ghost = _ghosts.SpawnGhost((mindId, mind), uid); + if (ghost != null) // Log these to make sure they're not causing the GameTicker round restart bugs... Log.Debug($"Entity \"{ToPrettyString(uid)}\" for {mind.CharacterName} was deleted, spawned \"{ToPrettyString(ghost)}\"."); - _metaData.SetEntityName(ghost, mind.CharacterName ?? string.Empty); - TransferTo(mindId, ghost, mind: mind); - }); + else + // This should be an error, if it didn't cause tests to start erroring when they delete a player. + Log.Warning($"Entity \"{ToPrettyString(uid)}\" for {mind.CharacterName} was deleted, and no applicable spawn location is available."); } public override bool TryGetMind(NetUserId user, [NotNullWhen(true)] out EntityUid? mindId, [NotNullWhen(true)] out MindComponent? mind)