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

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Content.Client.Construction; using Content.Client.Construction;
using Content.Client.Examine; using Content.Client.Examine;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Mind;
using Content.Server.Mind.Components; using Content.Server.Mind.Components;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.Stack; 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 // Fuck you mind system I want an hour of my life back
// Mind system is a time vampire // Mind system is a time vampire
ServerSession.ContentData()?.WipeMind(); SEntMan.System<MindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
old = cPlayerMan.LocalPlayer.ControlledEntity; old = cPlayerMan.LocalPlayer.ControlledEntity;
Player = SEntMan.SpawnEntity(PlayerPrototype, PlayerCoords); Player = SEntMan.SpawnEntity(PlayerPrototype, PlayerCoords);

View File

@@ -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
{
/// <summary>
/// Test that a ghost gets created when the player entity is deleted.
/// 1. Delete mob
/// 2. Assert is ghost
/// </summary>
[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<IServerEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
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<GhostComponent>(entity));
});
await pairTracker.CleanReturnAsync();
}
/// <summary>
/// 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
/// </summary>
[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<IServerEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var gameTicker = entMan.EntitySysManager.GetEntitySystem<GameTicker>();
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
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<GhostComponent>(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<GhostComponent>(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();
}
/// <summary>
/// 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.
/// </summary>
[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<IServerEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var serverConsole = server.ResolveDependency<IServerConsoleHost>();
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<GhostComponent>(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<GhostComponent>(player.AttachedEntity!.Value));
var mind = player.ContentData()?.Mind;
Assert.NotNull(mind);
Assert.Null(mind.VisitingEntity);
});
await pairTracker.CleanReturnAsync();
}
/// <summary>
/// Test ghost getting deleted while player is connected spawns another ghost
/// 1. become ghost
/// 2. delete ghost
/// 3. new ghost is spawned
/// </summary>
[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<IServerEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var serverConsole = server.ResolveDependency<IServerConsoleHost>();
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<GhostComponent>(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<GhostComponent>(player.AttachedEntity!.Value));
});
await pairTracker.CleanReturnAsync();
}
}

View File

@@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.Players;
using NUnit.Framework; using NUnit.Framework;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
@@ -16,6 +17,11 @@ namespace Content.IntegrationTests.Tests.Minds
[TestFixture] [TestFixture]
public sealed class MindEntityDeletionTest 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] [Test]
public async Task TestDeleteVisiting() public async Task TestDeleteVisiting()
{ {
@@ -50,95 +56,34 @@ namespace Content.IntegrationTests.Tests.Minds
}); });
await PoolManager.RunTicksSync(pairTracker.Pair, 5); await PoolManager.RunTicksSync(pairTracker.Pair, 5);
await server.WaitPost(() => entMan.DeleteEntity(visitEnt));
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 PoolManager.RunTicksSync(pairTracker.Pair, 5); await PoolManager.RunTicksSync(pairTracker.Pair, 5);
await server.WaitPost(() => Assert.IsNull(mind.VisitingEntity);
{ Assert.That(entMan.EntityExists(mind.OwnedEntity));
mapManager.DeleteMap(map.MapId); Assert.That(mind.OwnedEntity, Is.EqualTo(playerEnt));
});
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<IServerEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
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);
});
// 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(); await pairTracker.CleanReturnAsync();
} }
// this is a variant of TestGhostOnDelete that just deletes the whole map.
[Test] [Test]
public async Task TestGhostOnDeleteMap() 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 server = pairTracker.Pair.Server;
var testMap = await PoolManager.CreateTestMap(pairTracker); var testMap = await PoolManager.CreateTestMap(pairTracker);
var coordinates = testMap.GridCoords; var coordinates = testMap.GridCoords;
var entMan = server.ResolveDependency<IServerEntityManager>(); var entMan = server.ResolveDependency<IServerEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>(); var mapManager = server.ResolveDependency<IMapManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var player = playerMan.ServerSessions.Single();
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>(); var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
@@ -149,8 +94,7 @@ namespace Content.IntegrationTests.Tests.Minds
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
playerEnt = entMan.SpawnEntity(null, coordinates); playerEnt = entMan.SpawnEntity(null, coordinates);
mind = player.ContentData()!.Mind!;
mind = mindSystem.CreateMind(null);
mindSystem.TransferTo(mind, playerEnt); mindSystem.TransferTo(mind, playerEnt);
Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt)); Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt));

View File

@@ -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<EntityUid> BecomeGhost(Pair pair, bool visit = false)
{
var entMan = pair.Server.ResolveDependency<IServerEntityManager>();
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
var mindSys = entMan.System<MindSystem>();
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<GhostComponent>(ghostUid));
Assert.That(player.AttachedEntity == ghostUid);
Assert.That(mind.CurrentEntity == ghostUid);
if (!visit)
Assert.Null(mind.VisitingEntity);
return ghostUid;
}
public async Task<EntityUid> VisitGhost(Pair pair, bool visit = false)
{
return await BecomeGhost(pair, visit: true);
}
/// <summary>
/// Get the player's current mind and check that the entities exists.
/// </summary>
public Mind GetMind(Pair pair)
{
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
var entMan = pair.Server.ResolveDependency<IEntityManager>();
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<IClientNetManager>();
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
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<IClientNetManager>();
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
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<IPlayerSession> DisconnectReconnect(Pair pair)
{
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
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;
}
}

View File

@@ -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<IEntityManager>();
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<IEntityManager>();
await PoolManager.RunTicksSync(pair, 5);
var mind = GetMind(pair);
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
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<GhostComponent>(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<IEntityManager>();
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<IEntityManager>();
var mindSys = entMan.System<MindSystem>();
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();
}
}

View File

@@ -28,7 +28,7 @@ using IPlayerManager = Robust.Server.Player.IPlayerManager;
namespace Content.IntegrationTests.Tests.Minds; namespace Content.IntegrationTests.Tests.Minds;
[TestFixture] [TestFixture]
public sealed class MindTests public sealed partial class MindTests
{ {
private const string Prototypes = @" private const string Prototypes = @"
- type: entity - type: entity
@@ -125,7 +125,7 @@ public sealed class MindTests
var mind = mindSystem.CreateMind(null); var mind = mindSystem.CreateMind(null);
mindSystem.TransferTo(mind, entity); mindSystem.TransferTo(mind, entity);
Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind));
var mind2 = mindSystem.CreateMind(null); var mind2 = mindSystem.CreateMind(null);
mindSystem.TransferTo(mind2, entity); mindSystem.TransferTo(mind2, entity);
Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind2)); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind2));
@@ -220,32 +220,44 @@ public sealed class MindTests
[Test] [Test]
public async Task TestOwningPlayerCanBeChanged() 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 server = pairTracker.Pair.Server;
var entMan = server.ResolveDependency<IServerEntityManager>(); var entMan = server.ResolveDependency<IServerEntityManager>();
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
var originalMind = GetMind(pairTracker.Pair);
var userId = originalMind.UserId;
Mind mind = default!;
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
var entity = entMan.SpawnEntity(null, new MapCoordinates()); var entity = entMan.SpawnEntity(null, new MapCoordinates());
var mindComp = entMan.EnsureComponent<MindContainerComponent>(entity); var mindComp = entMan.EnsureComponent<MindContainerComponent>(entity);
entMan.DirtyEntity(entity);
var mind = mindSystem.CreateMind(null); mind = mindSystem.CreateMind(null);
mindSystem.TransferTo(mind, entity); mindSystem.TransferTo(mind, entity);
Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind));
var newUserId = new NetUserId(Guid.NewGuid());
Assert.That(mindComp.HasMind); 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(); await pairTracker.CleanReturnAsync();
} }
@@ -275,26 +287,26 @@ public sealed class MindTests
Assert.That(!mindSystem.HasRole<Job>(mind)); Assert.That(!mindSystem.HasRole<Job>(mind));
var traitorRole = new TraitorRole(mind, new AntagPrototype()); var traitorRole = new TraitorRole(mind, new AntagPrototype());
mindSystem.AddRole(mind, traitorRole); mindSystem.AddRole(mind, traitorRole);
Assert.That(mindSystem.HasRole<TraitorRole>(mind)); Assert.That(mindSystem.HasRole<TraitorRole>(mind));
Assert.That(!mindSystem.HasRole<Job>(mind)); Assert.That(!mindSystem.HasRole<Job>(mind));
var jobRole = new Job(mind, new JobPrototype()); var jobRole = new Job(mind, new JobPrototype());
mindSystem.AddRole(mind, jobRole); mindSystem.AddRole(mind, jobRole);
Assert.That(mindSystem.HasRole<TraitorRole>(mind)); Assert.That(mindSystem.HasRole<TraitorRole>(mind));
Assert.That(mindSystem.HasRole<Job>(mind)); Assert.That(mindSystem.HasRole<Job>(mind));
mindSystem.RemoveRole(mind, traitorRole); mindSystem.RemoveRole(mind, traitorRole);
Assert.That(!mindSystem.HasRole<TraitorRole>(mind)); Assert.That(!mindSystem.HasRole<TraitorRole>(mind));
Assert.That(mindSystem.HasRole<Job>(mind)); Assert.That(mindSystem.HasRole<Job>(mind));
mindSystem.RemoveRole(mind, jobRole); mindSystem.RemoveRole(mind, jobRole);
Assert.That(!mindSystem.HasRole<TraitorRole>(mind)); Assert.That(!mindSystem.HasRole<TraitorRole>(mind));
Assert.That(!mindSystem.HasRole<Job>(mind)); Assert.That(!mindSystem.HasRole<Job>(mind));
}); });
@@ -353,7 +365,7 @@ public sealed class MindTests
MakeSentientCommand.MakeSentient(mob, IoCManager.Resolve<IEntityManager>()); MakeSentientCommand.MakeSentient(mob, IoCManager.Resolve<IEntityManager>());
mobMind = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second"); mobMind = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second");
mindSystem.ChangeOwningPlayer(mobMind, player.UserId); mindSystem.SetUserId(mobMind, player.UserId);
mindSystem.TransferTo(mobMind, mob); mindSystem.TransferTo(mobMind, mob);
}); });

View File

@@ -1,3 +1,4 @@
using Content.Server.Mind;
using Content.Server.Players; using Content.Server.Players;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;
@@ -21,7 +22,9 @@ namespace Content.Server.GameTicking.Commands
} }
var playerMgr = IoCManager.Resolve<IPlayerManager>(); var playerMgr = IoCManager.Resolve<IPlayerManager>();
var ticker = EntitySystem.Get<GameTicker>(); var sysMan = IoCManager.Resolve<EntitySystemManager>();
var ticker = sysMan.GetEntitySystem<GameTicker>();
var mind = sysMan.GetEntitySystem<MindSystem>();
NetUserId userId; NetUserId userId;
if (args.Length == 0) if (args.Length == 0)
@@ -48,7 +51,7 @@ namespace Content.Server.GameTicking.Commands
return; 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"); shell.WriteLine("Player is not currently online, but they will respawn if they come back online");
return; return;
} }

View File

@@ -27,15 +27,28 @@ namespace Content.Server.GameTicking
{ {
var session = args.Session; 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) switch (args.NewStatus)
{ {
case SessionStatus.Connected: case SessionStatus.Connected:
{ {
AddPlayerToDb(args.Session.UserId.UserId); 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) 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. // Make the player actually join the game.
// timer time must be > tick length // timer time must be > tick length
@@ -74,28 +87,28 @@ namespace Content.Server.GameTicking
return; 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(); SpawnWaitDb();
} }
else else
{ {
if (data.Mind.CurrentEntity == null) session.AttachToEntity(data.Mind.CurrentEntity);
{ PlayerJoinGame(session);
SpawnWaitDb();
}
else
{
session.AttachToEntity(data.Mind.CurrentEntity);
PlayerJoinGame(session);
}
} }
break; break;
} }
case SessionStatus.Disconnected: case SessionStatus.Disconnected:
{ {
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name))); _chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
if (mind != null)
mind.Session = null;
_userDb.ClientDisconnected(session); _userDb.ClientDisconnected(session);
break; break;

View File

@@ -414,13 +414,6 @@ namespace Content.Server.GameTicking
PlayerJoinLobby(player); 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. // Delete all entities.
foreach (var entity in EntityManager.GetEntities().ToArray()) foreach (var entity in EntityManager.GetEntities().ToArray())
{ {

View File

@@ -175,9 +175,8 @@ namespace Content.Server.GameTicking
DebugTools.AssertNotNull(data); DebugTools.AssertNotNull(data);
data!.WipeMind(); var newMind = _mindSystem.CreateMind(data!.UserId, character.Name);
var newMind = _mindSystem.CreateMind(data.UserId, character.Name); _mindSystem.SetUserId(newMind, data.UserId);
_mindSystem.ChangeOwningPlayer(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId); var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
var job = new Job(newMind, jobPrototype); var job = new Job(newMind, jobPrototype);
@@ -244,7 +243,7 @@ namespace Content.Server.GameTicking
public void Respawn(IPlayerSession player) public void Respawn(IPlayerSession player)
{ {
player.ContentData()?.WipeMind(); _mindSystem.WipeMind(player);
_adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned."); _adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned.");
if (LobbyEnabled) if (LobbyEnabled)
@@ -278,9 +277,8 @@ namespace Content.Server.GameTicking
DebugTools.AssertNotNull(data); DebugTools.AssertNotNull(data);
data!.WipeMind(); var newMind = _mindSystem.CreateMind(data!.UserId);
var newMind = _mindSystem.CreateMind(data.UserId); _mindSystem.SetUserId(newMind, data.UserId);
_mindSystem.ChangeOwningPlayer(newMind, data.UserId);
_mindSystem.AddRole(newMind, new ObserverRole(newMind)); _mindSystem.AddRole(newMind, new ObserverRole(newMind));
var mob = SpawnObserverMob(); var mob = SpawnObserverMob();

View File

@@ -759,7 +759,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns)); var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns));
SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component); SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component);
var newMind = _mindSystem.CreateMind(session.UserId, spawnDetails.Name); 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.AddRole(newMind, new NukeopsRole(newMind, nukeOpsAntag));
_mindSystem.TransferTo(newMind, mob); _mindSystem.TransferTo(newMind, mob);

View File

@@ -208,7 +208,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
var session = ops[i]; var session = ops[i];
var newMind = _mindSystem.CreateMind(session.UserId, name); var newMind = _mindSystem.CreateMind(session.UserId, name);
_mindSystem.ChangeOwningPlayer(newMind, session.UserId); _mindSystem.SetUserId(newMind, session.UserId);
var mob = Spawn("MobHuman", _random.Pick(spawns)); var mob = Spawn("MobHuman", _random.Pick(spawns));
MetaData(mob).EntityName = name; MetaData(mob).EntityName = name;

View File

@@ -236,8 +236,6 @@ namespace Content.Server.Ghost
if (Deleted(uid) || Terminating(uid)) if (Deleted(uid) || Terminating(uid))
return; return;
if (EntityManager.TryGetComponent<MindContainerComponent?>(uid, out var mind))
_mindSystem.SetGhostOnShutdown(uid, false, mind);
QueueDel(uid); QueueDel(uid);
} }

View File

@@ -222,7 +222,7 @@ namespace Content.Server.Ghost.Roles
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName); EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
_mindSystem.AddRole(newMind, new GhostRoleMarkerRole(newMind, role.RoleName)); _mindSystem.AddRole(newMind, new GhostRoleMarkerRole(newMind, role.RoleName));
_mindSystem.ChangeOwningPlayer(newMind, player.UserId); _mindSystem.SetUserId(newMind, player.UserId);
_mindSystem.TransferTo(newMind, mob); _mindSystem.TransferTo(newMind, mob);
} }

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using YamlDotNet.Core.Tokens;
namespace Content.Server.Mind.Components namespace Content.Server.Mind.Components
{ {

View File

@@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Mind.Components; using Content.Server.Mind.Components;
using Content.Server.Objectives; using Content.Server.Objectives;
using Content.Server.Roles; using Content.Server.Roles;
@@ -30,16 +31,14 @@ namespace Content.Server.Mind
/// Note: the Mind is NOT initially attached! /// Note: the Mind is NOT initially attached!
/// The provided UserId is solely for tracking of intended owner. /// The provided UserId is solely for tracking of intended owner.
/// </summary> /// </summary>
/// <param name="userId">The session ID of the original owner (may get credited).</param> public Mind()
public Mind(NetUserId? userId)
{ {
OriginalOwnerUserId = userId;
} }
/// <summary> /// <summary>
/// The session ID of the player owning this mind. /// The session ID of the player owning this mind.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables, Access(typeof(MindSystem))]
public NetUserId? UserId { get; internal set; } public NetUserId? UserId { get; internal set; }
/// <summary> /// <summary>
@@ -112,7 +111,7 @@ namespace Content.Server.Mind
/// The session of the player owning this mind. /// The session of the player owning this mind.
/// Can be null, in which case the player is currently not logged in. /// Can be null, in which case the player is currently not logged in.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables, Access(typeof(MindSystem), typeof(GameTicker))]
public IPlayerSession? Session { get; internal set; } public IPlayerSession? Session { get; internal set; }
/// <summary> /// <summary>

View File

@@ -10,6 +10,7 @@ using Content.Server.Players;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
@@ -30,29 +31,25 @@ public sealed class MindSystem : EntitySystem
[Dependency] private readonly GhostSystem _ghostSystem = default!; [Dependency] private readonly GhostSystem _ghostSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPlayerManager _playerManager = 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<MindContainerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide); SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnTerminating); SubscribeLocalEvent<MindContainerComponent, EntityTerminatingEvent>(OnMindContainerTerminating);
SubscribeLocalEvent<VisitingMindComponent, PlayerDetachedEvent>(OnDetached); SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
} }
private void OnDetached(EntityUid uid, VisitingMindComponent component, PlayerDetachedEvent args) public override void Shutdown()
{ {
component.Mind = null; base.Shutdown();
RemCompDeferred(uid, component); WipeAllMinds();
}
private void OnTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args)
{
if (component.Mind?.Session?.AttachedEntity == uid)
UnVisit(component.Mind);
} }
public void SetGhostOnShutdown(EntityUid uid, bool value, MindContainerComponent? mind = null) public void SetGhostOnShutdown(EntityUid uid, bool value, MindContainerComponent? mind = null)
@@ -63,6 +60,49 @@ public sealed class MindSystem : EntitySystem
mind.GhostOnShutdown = value; 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> /// <summary>
/// Don't call this unless you know what the hell you're doing. /// 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. /// 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; 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. // Let's not create ghosts if not in the middle of the round.
if (_gameTicker.RunLevel != GameRunLevel.InRound) if (_gameTicker.RunLevel != GameRunLevel.InRound)
return; return;
if (!TryGetMind(uid, out var mind, mindContainerComp)) if (component.Mind is not { } mind)
return; 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); 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. var xform = Transform(uid);
// TODO: just find the applicable spawn position directly without actually updating the transform's parent. var gridId = xform.GridUid;
Transform(uid).AttachToGridOrMap();
var spawnPosition = Transform(uid).Coordinates; var spawnPosition = Transform(uid).Coordinates;
// Use a regular timer here because the entity has probably been deleted. // Use a regular timer here because the entity has probably been deleted.
@@ -124,11 +174,8 @@ public sealed class MindSystem : EntitySystem
return; return;
// Async this so that we don't throw if the grid we're on is being deleted. // Async this so that we don't throw if the grid we're on is being deleted.
var gridId = spawnPosition.GetGridUid(EntityManager); if (!_mapManager.GridExists(gridId))
if (!spawnPosition.IsValid(EntityManager) || gridId == EntityUid.Invalid || !_mapManager.GridExists(gridId))
{
spawnPosition = _gameTicker.GetObserverSpawnPoint(); spawnPosition = _gameTicker.GetObserverSpawnPoint();
}
// TODO refactor observer spawning. // TODO refactor observer spawning.
// please. // please.
@@ -195,9 +242,10 @@ public sealed class MindSystem : EntitySystem
public Mind CreateMind(NetUserId? userId, string? name = null) public Mind CreateMind(NetUserId? userId, string? name = null)
{ {
var mind = new Mind(userId); var mind = new Mind();
mind.CharacterName = name; mind.CharacterName = name;
ChangeOwningPlayer(mind, userId); SetUserId(mind, userId);
return mind; return mind;
} }
@@ -262,7 +310,6 @@ public sealed class MindSystem : EntitySystem
if (mind == null || mind.VisitingEntity == null) if (mind == null || mind.VisitingEntity == null)
return; return;
DebugTools.Assert(mind.VisitingEntity != mind.OwnedEntity);
RemoveVisitingEntity(mind); RemoveVisitingEntity(mind);
if (mind.Session == null || mind.Session.AttachedEntity == mind.VisitingEntity) if (mind.Session == null || mind.Session.AttachedEntity == mind.VisitingEntity)
@@ -300,6 +347,25 @@ public sealed class MindSystem : EntitySystem
RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true); 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> /// <summary>
/// Transfer this mind's control over to a new entity. /// Transfer this mind's control over to a new entity.
/// </summary> /// </summary>
@@ -316,12 +382,8 @@ public sealed class MindSystem : EntitySystem
/// </exception> /// </exception>
public void TransferTo(Mind mind, EntityUid? entity, bool ghostCheckOverride = false) 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) if (entity == mind.OwnedEntity)
{
UnVisit(mind);
return; return;
}
MindContainerComponent? component = null; MindContainerComponent? component = null;
var alreadyAttached = false; 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> /// <summary>
/// Adds an objective to this mind. /// Adds an objective to this mind.
/// </summary> /// </summary>
@@ -569,19 +586,56 @@ public sealed class MindSystem : EntitySystem
} }
/// <summary> /// <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> /// </summary>
/// <param name="mind"></param> /// <param name="mind"></param>
/// <param name="userId"></param> /// <param name="userId"></param>
private void SetUserId(Mind mind, NetUserId? userId) public void SetUserId(Mind mind, NetUserId? userId)
{ {
mind.UserId = userId; if (mind.UserId == userId)
if (!userId.HasValue)
return; 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); _playerManager.TryGetSessionById(userId.Value, out var ret);
mind.Session = 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> /// <summary>

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking;
using Content.Server.Mind; using Content.Server.Mind;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -27,8 +28,8 @@ namespace Content.Server.Players
/// The currently occupied mind of the player owning this data. /// The currently occupied mind of the player owning this data.
/// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING. /// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables, Access(typeof(MindSystem), typeof(GameTicker))]
public Mind.Mind? Mind { get; private set; } public Mind.Mind? Mind { get; set; }
/// <summary> /// <summary>
/// If true, the player is an admin and they explicitly de-adminned mid-game, /// If true, the player is an admin and they explicitly de-adminned mid-game,
@@ -36,27 +37,6 @@ namespace Content.Server.Players
/// </summary> /// </summary>
public bool ExplicitlyDeadminned { get; set; } public bool ExplicitlyDeadminned { get; set; }
public void WipeMind()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mindSystem = entityManager.System<MindSystem>();
// This will ensure Mind == null
if (Mind == null)
return;
mindSystem.TransferTo(Mind, null);
mindSystem.ChangeOwningPlayer(Mind, null);
}
/// <summary>
/// Called from Mind.ChangeOwningPlayer *and nowhere else.*
/// </summary>
public void UpdateMindFromMindChangeOwningPlayer(Mind.Mind? mind)
{
Mind = mind;
}
public PlayerData(NetUserId userId, string name) public PlayerData(NetUserId userId, string name)
{ {
UserId = userId; UserId = userId;