#nullable enable using System.Linq; using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles.Components; using Content.Shared.Ghost; using Content.Shared.Mind; using Content.Shared.Players; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Minds; [TestFixture] public sealed class GhostRoleTests { private const string GhostRoleProtoId = "GhostRoleTestEntity"; private const string TestMobProtoId = "GhostRoleTestMob"; [TestPrototypes] private const string Prototypes = $""" - type: entity id: {GhostRoleProtoId} components: - type: MindContainer - type: GhostRole - type: GhostTakeoverAvailable - type: MobState - type: entity id: {TestMobProtoId} components: - type: MobState # MobState is required for correct determination of if the player can return to body or not """; /// /// 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. /// [TestCase(true)] [TestCase(false)] public async Task TakeRoleAndReturn(bool adminGhost) { var ghostCommand = adminGhost ? "aghost" : "ghost"; 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(); var mindSystem = entMan.System(); var session = sPlayerMan.Sessions.Single(); var originalPlayerMindId = session.ContentData()!.Mind!.Value; // Check that there are no ghosts Assert.That(entMan.Count(), Is.Zero); // Spawn player entity & attach EntityUid originalPlayerMob = default; await server.WaitPost(() => { originalPlayerMob = entMan.SpawnEntity(TestMobProtoId, mapData.GridCoords); mindSystem.TransferTo(originalPlayerMindId, originalPlayerMob, true); }); await pair.RunTicksSync(10); var originalPlayerMind = entMan.GetComponent(originalPlayerMindId); Assert.Multiple(() => { // Check player got attached. Assert.That(session.AttachedEntity, Is.EqualTo(originalPlayerMob)); Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob)); Assert.That(originalPlayerMind.VisitingEntity, Is.Null); Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); // Check that there are still no ghosts Assert.That(entMan.Count(), Is.Zero); }); // Use the ghost command conHost.ExecuteCommand(ghostCommand); await pair.RunTicksSync(10); var ghostOne = session.AttachedEntity; Assert.Multiple(() => { // Assert that the ghost is a new entity with a new mind Assert.That(entMan.HasComponent(ghostOne)); Assert.That(ghostOne, Is.Not.EqualTo(originalPlayerMob)); Assert.That(session.ContentData()?.Mind, Is.EqualTo(originalPlayerMindId)); if (adminGhost) { // aghost, so the player mob should still own the mind, but the mind is visiting the ghost. Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob)); Assert.That(originalPlayerMind.VisitingEntity, Is.EqualTo(ghostOne)); Assert.That(originalPlayerMind.UserId, Is.EqualTo(session.UserId)); } else { // player ghost, can't return. The mind is owned by the ghost, and is not visiting. Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(ghostOne)); Assert.That(originalPlayerMind.VisitingEntity, Is.Null); } // Check that we're tracking the original owner for round end screen Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); // Check that there is only one ghost Assert.That(entMan.Count(), Is.EqualTo(1)); }); // Spawn ghost takeover entity. EntityUid ghostRole = default; await server.WaitPost(() => ghostRole = entMan.SpawnEntity(GhostRoleProtoId, mapData.GridCoords)); // Take the ghost role await server.WaitPost(() => { var id = entMan.GetComponent(ghostRole).Identifier; entMan.EntitySysManager.GetEntitySystem().Takeover(session, id); }); // Check player got attached to ghost role. await pair.RunTicksSync(10); var ghostRoleMindId = session.ContentData()!.Mind!.Value; var ghostRoleMind = entMan.GetComponent(ghostRoleMindId); Assert.Multiple(() => { // Check that the ghost role mind is new Assert.That(ghostRoleMindId, Is.Not.EqualTo(originalPlayerMindId)); // Check that the session and mind are properly attached to the ghost role Assert.That(session.AttachedEntity, Is.EqualTo(ghostRole)); Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostRole)); Assert.That(ghostRoleMind.VisitingEntity, Is.Null); // Original mind should be unaffected, but the ghost will have deleted itself. if (adminGhost) { // aghost case, the original player mob should still own the mind, and that mind is not visiting. Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob)); } else { // player ghost case, the original mind is disconnected and not owned by an entity. // This mind cannot be returned to Assert.That(originalPlayerMind.OwnedEntity, Is.Null); } // In either case the original player mind is not visiting anything, not connected to any user. Assert.That(originalPlayerMind.VisitingEntity, Is.Null); Assert.That(originalPlayerMind.UserId, Is.Null); // Now the original owner of both minds should permanently be set to this session. Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); Assert.That(ghostRoleMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); // Make sure that the ghost was deleted Assert.That(entMan.Deleted(ghostOne)); // Check that there is are no lingereing ghosts Assert.That(entMan.Count(), Is.Zero); }); // Ghost again. conHost.ExecuteCommand(ghostCommand); await pair.RunTicksSync(10); var ghostTwo = session.AttachedEntity; Assert.Multiple(() => { // Check that the new ghost is a new entity Assert.That(entMan.HasComponent(ghostTwo)); Assert.That(ghostTwo, Is.Not.EqualTo(originalPlayerMob)); Assert.That(ghostTwo, Is.Not.EqualTo(ghostRole)); Assert.That(session.ContentData()?.Mind, Is.EqualTo(ghostRoleMindId)); if(adminGhost) { // aghost case, the ghost role mind should be owned by the ghost role entity, // the ghost role mind is visiting the new ghost Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostRole)); Assert.That(ghostRoleMind.VisitingEntity, Is.EqualTo(ghostTwo)); } else { // player ghost, can't return. The mind is owned by the ghost, and is not visiting. Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostTwo)); Assert.That(ghostRoleMind.VisitingEntity, Is.Null); } // Check that the original mind is still not attached to a user Assert.That(originalPlayerMind.UserId, Is.Null); // Check that original owners of other minds are still tracked Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); Assert.That(ghostRoleMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); // Check that there is exactly one ghost Assert.That(entMan.Count(), Is.EqualTo(1)); }); if (!adminGhost) { // End of the normal player ghost role test await pair.CleanReturnAsync(); return; } // Next, control the original entity again: await server.WaitPost(() => mindSystem.SetUserId(originalPlayerMindId, session.UserId)); await pair.RunTicksSync(10); Assert.Multiple(() => { // Check that we are attached Assert.That(session.AttachedEntity, Is.EqualTo(originalPlayerMob)); // Check the ownership of the original mind Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob)); Assert.That(originalPlayerMind.VisitingEntity, Is.Null); Assert.That(originalPlayerMind.UserId, Is.EqualTo(session.UserId)); // Check that the ghost-role mind is unaffected Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostRole)); Assert.That(ghostRoleMind.VisitingEntity, Is.Null); // Check that the second ghost is deleted Assert.That(entMan.Deleted(ghostTwo)); // Check that the original owners of the previous minds are still tracked Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); Assert.That(ghostRoleMind.OriginalOwnerUserId, Is.EqualTo(session.UserId)); // Check that there is are no lingereing ghosts Assert.That(entMan.Count(), Is.Zero); }); await pair.CleanReturnAsync(); } }