Fix some Mind ECS bugs (#17480)

This commit is contained in:
Leon Friedrich
2023-06-20 16:29:26 +12:00
committed by GitHub
parent 41244b74aa
commit 9fc4fc6ac2
18 changed files with 767 additions and 248 deletions

View File

@@ -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<NetUserId, Mind> _userMinds = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MindContainerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnTerminating);
SubscribeLocalEvent<VisitingMindComponent, PlayerDetachedEvent>(OnDetached);
SubscribeLocalEvent<MindContainerComponent, EntityTerminatingEvent>(OnMindContainerTerminating);
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
SubscribeLocalEvent<RoundRestartCleanupEvent>(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;
}
/// <summary>
/// Don't call this unless you know what the hell you're doing.
/// Use <see cref="MindSystem.TransferTo(Mind,System.Nullable{Robust.Shared.GameObjects.EntityUid},bool)"/> 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);
}
/// <summary>
/// Detaches a mind from all entities and clears the user ID.
/// </summary>
public void WipeMind(Mind? mind)
{
if (mind == null)
return;
TransferTo(mind, null);
SetUserId(mind, null);
}
/// <summary>
/// Transfer this mind's control over to a new entity.
/// </summary>
@@ -316,12 +382,8 @@ public sealed class MindSystem : EntitySystem
/// </exception>
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);
}
/// <summary>
/// Adds an objective to this mind.
/// </summary>
@@ -569,19 +586,56 @@ public sealed class MindSystem : EntitySystem
}
/// <summary>
/// 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.
/// </summary>
/// <param name="mind"></param>
/// <param name="userId"></param>
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;
}
/// <summary>