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
This commit is contained in:
@@ -274,35 +274,13 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
}
|
||||
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
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<GhostComponent>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <param name="station">The station they're spawning on</param>
|
||||
/// <param name="jobId">An optional job for them to spawn as</param>
|
||||
/// <param name="silent">Whether or not the player should be greeted upon joining</param>
|
||||
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<MindComponent?>? 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<SpawnPointComponent, TransformComponent>(true))
|
||||
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
if (point.SpawnType != SpawnPointType.Observer)
|
||||
continue;
|
||||
@@ -402,7 +386,7 @@ namespace Content.Server.GameTicking
|
||||
var query = AllEntityQuery<MapGridComponent>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<GhostComponent> _ghostQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
@@ -389,5 +392,59 @@ namespace Content.Server.Ghost
|
||||
|
||||
return ghostBoo.Handled;
|
||||
}
|
||||
|
||||
public EntityUid? SpawnGhost(Entity<MindComponent?> mind, EntityUid targetEntity,
|
||||
bool canReturn = false)
|
||||
{
|
||||
_transformSystem.TryGetMapOrGridCoordinates(targetEntity, out var spawnPosition);
|
||||
return SpawnGhost(mind, spawnPosition, canReturn);
|
||||
}
|
||||
|
||||
public EntityUid? SpawnGhost(Entity<MindComponent?> 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<GhostComponent>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MapGridComponent>(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<GhostComponent>(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)
|
||||
|
||||
Reference in New Issue
Block a user