diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 6b8760bf59..0f1690d453 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -38,7 +38,7 @@ namespace Content.IntegrationTests.Tests.Interaction; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public abstract partial class InteractionTest { - protected virtual string PlayerPrototype => "MobInteractionTestObserver"; + protected virtual string PlayerPrototype => "InteractionTestMob"; protected PairTracker PairTracker = default!; protected TestMapData MapData = default!; @@ -115,38 +115,27 @@ public abstract partial class InteractionTest public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds; - [SetUp] - public virtual async Task Setup() - { - const string TestPrototypes = @" + + // Simple mob that has one hand and can perform misc interactions. + public const string TestPrototypes = @" - type: entity - id: MobInteractionTestObserver - name: observer - noSpawn: true - save: false - description: Boo! + id: InteractionTestMob components: - - type: Access - groups: - - AllAccess - type: Body prototype: Aghost - type: DoAfter - - type: Ghost - canInteract: true - type: Hands - - type: Mind + - type: MindContainer - type: Stripping - type: Tag tags: - CanPilot - - BypassInteractionRangeChecks - - type: Thieving - stripTimeReduction: 9999 - stealthy: true - type: UserInterface "; + [SetUp] + public virtual async Task Setup() + { PairTracker = await PoolManager.GetServerClient(new PoolSettings{ExtraPrototypes = TestPrototypes}); // server dependencies diff --git a/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs new file mode 100644 index 0000000000..bd6ce81d1a --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/GhostRoleTests.cs @@ -0,0 +1,103 @@ +#nullable enable +using System.Threading.Tasks; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; +using Content.Server.Mind; +using Content.Server.Players; +using NUnit.Framework; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Minds; + +[TestFixture] +public sealed class GhostRoleTests +{ + private const string Prototypes = @" +- type: entity + id: GhostRoleTestEntity_Player + components: + - type: MindContainer + +- type: entity + id: GhostRoleTestEntity_Role + components: + - type: MindContainer + - type: GhostRole + - type: GhostTakeoverAvailable +"; + + /// + /// This is a simple test that just checks if a player can take a ghost roll and then regain control of their + /// original entity without encountering errors. + /// + [Test] + public async Task TakeRoleAndReturn() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings {ExtraPrototypes = Prototypes}); + var server = pairTracker.Pair.Server; + var client = pairTracker.Pair.Client; + + var entMan = server.ResolveDependency(); + var sPlayerMan = server.ResolveDependency(); + var conHost = client.ResolveDependency(); + var cPlayerMan = client.ResolveDependency(); + var mindSystem = entMan.System(); + + // Get player data + if (cPlayerMan.LocalPlayer?.Session == null) + Assert.Fail("No player"); + + var clientSession = cPlayerMan.LocalPlayer!.Session!; + var session = sPlayerMan.GetSessionByUserId(clientSession.UserId); + + // Spawn player entity & attach + EntityUid originalMob = default; + await server.WaitPost(() => + { + originalMob = entMan.SpawnEntity("GhostRoleTestEntity_Player", MapCoordinates.Nullspace); + mindSystem.TransferTo(session.ContentData()!.Mind!, originalMob, true); + }); + + // Check player got attached. + await PoolManager.RunTicksSync(pairTracker.Pair, 10); + Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(originalMob)); + + // Use the ghost command + conHost.ExecuteCommand("ghost"); + await PoolManager.RunTicksSync(pairTracker.Pair, 10); + Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.Not.EqualTo(originalMob)); + + // Spawn ghost takeover entity. + EntityUid ghostRole = default; + await server.WaitPost(() => ghostRole = entMan.SpawnEntity("GhostRoleTestEntity_Role", MapCoordinates.Nullspace)); + + // 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 PoolManager.RunTicksSync(pairTracker.Pair, 10); + Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(ghostRole)); + + // Ghost again. + conHost.ExecuteCommand("ghost"); + await PoolManager.RunTicksSync(pairTracker.Pair, 10); + Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.Not.EqualTo(originalMob)); + Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.Not.EqualTo(ghostRole)); + + // Next, control the original entity again: + await server.WaitPost(() => + { + mindSystem.TransferTo(session.ContentData()!.Mind!, originalMob, true); + }); + await PoolManager.RunTicksSync(pairTracker.Pair, 10); + Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(originalMob)); + + await pairTracker.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/MindEntityDeletionTest.cs b/Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs similarity index 84% rename from Content.IntegrationTests/Tests/MindEntityDeletionTest.cs rename to Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs index 93c5fe41f1..82baf4810d 100644 --- a/Content.IntegrationTests/Tests/MindEntityDeletionTest.cs +++ b/Content.IntegrationTests/Tests/Minds/MindEntityDeletionTest.cs @@ -1,7 +1,7 @@ +#nullable enable using System.Linq; using System.Threading.Tasks; using Content.Server.Mind; -using Content.Shared.Coordinates; using NUnit.Framework; using Robust.Server.GameObjects; using Robust.Server.Player; @@ -10,7 +10,7 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -namespace Content.IntegrationTests.Tests +namespace Content.IntegrationTests.Tests.Minds { // Tests various scenarios of deleting the entity that a player's mind is connected to. [TestFixture] @@ -26,9 +26,11 @@ namespace Content.IntegrationTests.Tests var playerMan = server.ResolveDependency(); var mapManager = server.ResolveDependency(); + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + EntityUid playerEnt = default; EntityUid visitEnt = default; - Mind mind = null; + Mind mind = default!; var map = await PoolManager.CreateTestMap(pairTracker); await server.WaitAssertion(() => @@ -39,11 +41,9 @@ namespace Content.IntegrationTests.Tests playerEnt = entMan.SpawnEntity(null, pos); visitEnt = entMan.SpawnEntity(null, pos); - mind = new Mind(player.UserId); - mind.ChangeOwningPlayer(player.UserId); - - mind.TransferTo(playerEnt); - mind.Visit(visitEnt); + mind = mindSystem.CreateMind(player.UserId); + mindSystem.TransferTo(mind, playerEnt); + mindSystem.Visit(mind, visitEnt); Assert.That(player.AttachedEntity, Is.EqualTo(visitEnt)); Assert.That(mind.VisitingEntity, Is.EqualTo(visitEnt)); @@ -54,11 +54,6 @@ namespace Content.IntegrationTests.Tests await server.WaitAssertion(() => { entMan.DeleteEntity(visitEnt); - if (mind == null) - { - Assert.Fail("Mind was null"); - return; - } if (mind.VisitingEntity != null) { @@ -91,10 +86,13 @@ namespace Content.IntegrationTests.Tests var entMan = server.ResolveDependency(); var playerMan = server.ResolveDependency(); var mapManager = server.ResolveDependency(); + + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + var map = await PoolManager.CreateTestMap(pairTracker); EntityUid playerEnt = default; - Mind mind = null; + Mind mind = default!; await server.WaitAssertion(() => { var player = playerMan.ServerSessions.Single(); @@ -103,10 +101,8 @@ namespace Content.IntegrationTests.Tests playerEnt = entMan.SpawnEntity(null, pos); - mind = new Mind(player.UserId); - mind.ChangeOwningPlayer(player.UserId); - - mind.TransferTo(playerEnt); + mind = mindSystem.CreateMind(player.UserId); + mindSystem.TransferTo(mind, playerEnt); Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt)); }); @@ -136,27 +132,26 @@ namespace Content.IntegrationTests.Tests [Test] public async Task TestGhostOnDeleteMap() { - await using var pairTracker = await PoolManager.GetServerClient(); + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true }); var server = pairTracker.Pair.Server; var testMap = await PoolManager.CreateTestMap(pairTracker); var coordinates = testMap.GridCoords; var entMan = server.ResolveDependency(); - var playerMan = server.ResolveDependency(); var mapManager = server.ResolveDependency(); + + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + var map = await PoolManager.CreateTestMap(pairTracker); EntityUid playerEnt = default; - Mind mind = null; + Mind mind = default!; await server.WaitAssertion(() => { - var player = playerMan.ServerSessions.Single(); - playerEnt = entMan.SpawnEntity(null, coordinates); - mind = new Mind(player.UserId); - mind.ChangeOwningPlayer(player.UserId); - mind.TransferTo(playerEnt); + mind = mindSystem.CreateMind(null); + mindSystem.TransferTo(mind, playerEnt); Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt)); }); diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.cs b/Content.IntegrationTests/Tests/Minds/MindTests.cs new file mode 100644 index 0000000000..68c1617554 --- /dev/null +++ b/Content.IntegrationTests/Tests/Minds/MindTests.cs @@ -0,0 +1,456 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Ghost; +using Content.Server.Ghost.Roles; +using Content.Server.Mind; +using Content.Server.Mind.Commands; +using Content.Server.Mind.Components; +using Content.Server.Players; +using Content.Server.Roles; +using Content.Server.Traitor; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Roles; +using NUnit.Framework; +using Robust.Server.Console; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using IPlayerManager = Robust.Server.Player.IPlayerManager; + +namespace Content.IntegrationTests.Tests.Minds; + +[TestFixture] +public sealed class MindTests +{ + private const string Prototypes = @" +- type: entity + id: MindTestEntity + components: + - type: MindContainer + +- type: entity + parent: MindTestEntity + id: MindTestEntityDamageable + components: + - type: Damageable + damageContainer: Biological + - type: Body + prototype: Human + requiredLegs: 2 + - type: MobState + - type: MobThresholds + thresholds: + 0: Alive + 100: Critical + 200: Dead + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Blunt + damage: 400 + behaviors: + - !type:GibBehavior { } +"; + + /// + /// Exception handling for PlayerData and NetUserId invalid due to testing. + /// Can be removed when Players can be mocked. + /// + /// + private void CatchPlayerDataException(Action func) + { + try + { + func(); + } + catch (ArgumentException e) + { + // Prevent exiting due to PlayerData not being initialized. + if (e.Message == "New owner must have previously logged into the server. (Parameter 'newOwner')") + return; + throw; + } + } + + [Test] + public async Task TestCreateAndTransferMindToNewEntity() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true }); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + var entity = entMan.SpawnEntity(null, new MapCoordinates()); + var mindComp = entMan.EnsureComponent(entity); + + var mind = mindSystem.CreateMind(null); + + Assert.That(mind.UserId, Is.EqualTo(null)); + + mindSystem.TransferTo(mind, entity); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestReplaceMind() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true }); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + var entity = entMan.SpawnEntity(null, new MapCoordinates()); + var mindComp = entMan.EnsureComponent(entity); + + var mind = mindSystem.CreateMind(null); + mindSystem.TransferTo(mind, entity); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + + var mind2 = mindSystem.CreateMind(null); + mindSystem.TransferTo(mind2, entity); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind2)); + Assert.That(mind.OwnedEntity != entity); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestEntityDeadWhenGibbed() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true, ExtraPrototypes = Prototypes }); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var protoMan = server.ResolveDependency(); + + EntityUid entity = default!; + MindContainerComponent mindContainerComp = default!; + Mind mind = default!; + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + var damageableSystem = entMan.EntitySysManager.GetEntitySystem(); + + await server.WaitAssertion(() => + { + entity = entMan.SpawnEntity("MindTestEntityDamageable", new MapCoordinates()); + mindContainerComp = entMan.EnsureComponent(entity); + + mind = mindSystem.CreateMind(null); + + mindSystem.TransferTo(mind, entity); + Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mind)); + Assert.That(!mindSystem.IsCharacterDeadPhysically(mind)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + var damageable = entMan.GetComponent(entity); + if (!protoMan.TryIndex("Blunt", out var prototype)) + { + return; + } + + damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401))); + Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mind)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + Assert.That(mindSystem.IsCharacterDeadPhysically(mind)); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestMindTransfersToOtherEntity() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true }); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + var entity = entMan.SpawnEntity(null, new MapCoordinates()); + var targetEntity = entMan.SpawnEntity(null, new MapCoordinates()); + var mindComp = entMan.EnsureComponent(entity); + entMan.EnsureComponent(targetEntity); + + var mind = mindSystem.CreateMind(null); + + mindSystem.TransferTo(mind, entity); + + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + + mindSystem.TransferTo(mind, targetEntity); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(null)); + Assert.That(mindSystem.GetMind(targetEntity), Is.EqualTo(mind)); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestOwningPlayerCanBeChanged() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true }); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + var entity = entMan.SpawnEntity(null, new MapCoordinates()); + var mindComp = entMan.EnsureComponent(entity); + + var mind = mindSystem.CreateMind(null); + + mindSystem.TransferTo(mind, entity); + + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + + var newUserId = new NetUserId(Guid.NewGuid()); + Assert.That(mindComp.HasMind); + CatchPlayerDataException(() => + mindSystem.ChangeOwningPlayer(mindComp.Mind!, newUserId)); + + Assert.That(mind.UserId, Is.EqualTo(newUserId)); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestAddRemoveHasRoles() + { + await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true }); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + + await server.WaitAssertion(() => + { + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + var entity = entMan.SpawnEntity(null, new MapCoordinates()); + var mindComp = entMan.EnsureComponent(entity); + + var mind = mindSystem.CreateMind(null); + + Assert.That(mind.UserId, Is.EqualTo(null)); + + mindSystem.TransferTo(mind, entity); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + + Assert.That(!mindSystem.HasRole(mind)); + Assert.That(!mindSystem.HasRole(mind)); + + var traitorRole = new TraitorRole(mind, new AntagPrototype()); + + mindSystem.AddRole(mind, traitorRole); + + Assert.That(mindSystem.HasRole(mind)); + Assert.That(!mindSystem.HasRole(mind)); + + var jobRole = new Job(mind, new JobPrototype()); + + mindSystem.AddRole(mind, jobRole); + + Assert.That(mindSystem.HasRole(mind)); + Assert.That(mindSystem.HasRole(mind)); + + mindSystem.RemoveRole(mind, traitorRole); + + Assert.That(!mindSystem.HasRole(mind)); + Assert.That(mindSystem.HasRole(mind)); + + mindSystem.RemoveRole(mind, jobRole); + + Assert.That(!mindSystem.HasRole(mind)); + Assert.That(!mindSystem.HasRole(mind)); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestPlayerCanGhost() + { + // Client is needed to spawn session + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + var ghostSystem = entMan.EntitySysManager.GetEntitySystem(); + + EntityUid entity = default!; + Mind mind = default!; + IPlayerSession player = playerMan.ServerSessions.Single(); + + await server.WaitAssertion(() => + { + entity = entMan.SpawnEntity(null, new MapCoordinates()); + var mindComp = entMan.EnsureComponent(entity); + + mind = mindSystem.CreateMind(player.UserId, "Mindy McThinker"); + + Assert.That(mind.UserId, Is.EqualTo(player.UserId)); + + mindSystem.TransferTo(mind, entity); + Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + entMan.DeleteEntity(entity); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + EntityUid mob = default!; + Mind mobMind = default!; + + await server.WaitAssertion(() => + { + Assert.That(mind.OwnedEntity != null); + + mob = entMan.SpawnEntity(null, new MapCoordinates()); + + MakeSentientCommand.MakeSentient(mob, IoCManager.Resolve()); + mobMind = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second"); + + mindSystem.ChangeOwningPlayer(mobMind, player.UserId); + mindSystem.TransferTo(mobMind, mob); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 5); + + await server.WaitAssertion(() => + { + var m = player.ContentData()?.Mind; + Assert.That(m, Is.Not.EqualTo(null)); + Assert.That(m!.OwnedEntity, Is.EqualTo(mob)); + Assert.That(m, Is.Not.EqualTo(mind)); + }); + + await pairTracker.CleanReturnAsync(); + } + + [Test] + public async Task TestPlayerCanReturnFromGhostWhenDead() + { + // TODO Implement + } + + [Test] + public async Task TestGhostDoesNotInfiniteLoop() + { + // Client is needed to spawn session + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Pair.Server; + + var entMan = server.ResolveDependency(); + var playerMan = server.ResolveDependency(); + var serverConsole = server.ResolveDependency(); + + var mindSystem = entMan.EntitySysManager.GetEntitySystem(); + + EntityUid entity = default!; + EntityUid mouse = default!; + EntityUid ghost = default!; + Mind mind = default!; + IPlayerSession player = playerMan.ServerSessions.Single(); + + await server.WaitAssertion(() => + { + // entity = entMan.SpawnEntity(null, new MapCoordinates()); + // var mindComp = entMan.EnsureComponent(entity); + + // mind = mindSystem.CreateMind(player.UserId, "Mindy McThinker"); + // + // Assert.That(mind.UserId, Is.EqualTo(player.UserId)); + // + // mindSystem.TransferTo(mind, entity); + // Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind)); + + var data = player.ContentData(); + + Assert.That(data?.Mind, Is.Not.EqualTo(null)); + mind = data!.Mind!; + + Assert.That(mind.OwnedEntity != null); + + mouse = entMan.SpawnEntity("MobMouse", new MapCoordinates()); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 120); + + await server.WaitAssertion(() => + { + serverConsole.ExecuteCommand(player, "aghost"); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 120); + + await server.WaitAssertion(() => + { + entMan.EntitySysManager.GetEntitySystem().Takeover(player, 0); + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 120); + + await server.WaitAssertion(() => + { + var data = player.ContentData()!; + Assert.That(data.Mind!.OwnedEntity == mouse); + + serverConsole.ExecuteCommand(player, "aghost"); + Assert.That(player.AttachedEntity != null); + ghost = player.AttachedEntity!.Value; + }); + + await PoolManager.RunTicksSync(pairTracker.Pair, 60); + + await server.WaitAssertion(() => + { + Assert.That(player.AttachedEntity != null); + Assert.That(ghost == player.AttachedEntity!.Value); + }); + + await pairTracker.CleanReturnAsync(); + } +} diff --git a/Content.Server/AME/Components/AMEControllerComponent.cs b/Content.Server/AME/Components/AMEControllerComponent.cs index 1a398a5eb5..4285ff5fbd 100644 --- a/Content.Server/AME/Components/AMEControllerComponent.cs +++ b/Content.Server/AME/Components/AMEControllerComponent.cs @@ -176,20 +176,20 @@ namespace Content.Server.AME.Components } // Logging - _entities.TryGetComponent(player, out MindComponent? mindComponent); - if (mindComponent != null) + _entities.TryGetComponent(player, out MindContainerComponent? mindContainerComponent); + if (mindContainerComponent != null) { var humanReadableState = _injecting ? "Inject" : "Not inject"; if (msg.Button == UiButton.IncreaseFuel || msg.Button == UiButton.DecreaseFuel) - _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{_entities.ToPrettyString(mindComponent.Owner):player} has set the AME to inject {InjectionAmount} while set to {humanReadableState}"); + _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{_entities.ToPrettyString(mindContainerComponent.Owner):player} has set the AME to inject {InjectionAmount} while set to {humanReadableState}"); if (msg.Button == UiButton.ToggleInjection) - _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{_entities.ToPrettyString(mindComponent.Owner):player} has set the AME to {humanReadableState}"); + _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{_entities.ToPrettyString(mindContainerComponent.Owner):player} has set the AME to {humanReadableState}"); // Admin alert if (GetCoreCount() * 2 == InjectionAmount - 2 && msg.Button == UiButton.IncreaseFuel) - _chat.SendAdminAlert(player, $"increased AME over safe limit to {InjectionAmount}", mindComponent); + _chat.SendAdminAlert(player, $"increased AME over safe limit to {InjectionAmount}", mindContainerComponent); } GetAMENodeGroup()?.UpdateCoreVisuals(); diff --git a/Content.Server/Administration/Commands/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs index 037c289cbd..97b1c48ae1 100644 --- a/Content.Server/Administration/Commands/AGhost.cs +++ b/Content.Server/Administration/Commands/AGhost.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking; using Content.Server.Ghost.Components; +using Content.Server.Mind; using Content.Server.Players; using Content.Shared.Administration; using Content.Shared.Ghost; @@ -33,11 +34,15 @@ namespace Content.Server.Administration.Commands shell.WriteLine("You can't ghost here!"); return; } + + var mindSystem = _entities.System(); - if (mind.VisitingEntity != default && _entities.HasComponent(mind.VisitingEntity)) + if (mind.VisitingEntity != default && _entities.TryGetComponent(mind.VisitingEntity, out var oldGhostComponent)) { - player.ContentData()!.Mind?.UnVisit(); - return; + mindSystem.UnVisit(mind); + // If already an admin ghost, then return to body. + if (oldGhostComponent.CanGhostInteract) + return; } var canReturn = mind.CurrentEntity != null @@ -56,12 +61,12 @@ namespace Content.Server.Administration.Commands else if (!string.IsNullOrWhiteSpace(mind.Session?.Name)) _entities.GetComponent(ghost).EntityName = mind.Session.Name; - mind.Visit(ghost); + mindSystem.Visit(mind, ghost); } else { _entities.GetComponent(ghost).EntityName = player.Name; - mind.TransferTo(ghost); + mindSystem.TransferTo(mind, ghost); } var comp = _entities.GetComponent(ghost); diff --git a/Content.Server/Administration/Commands/ControlMob.cs b/Content.Server/Administration/Commands/ControlMob.cs index 4346ca7f17..18fb3bbb65 100644 --- a/Content.Server/Administration/Commands/ControlMob.cs +++ b/Content.Server/Administration/Commands/ControlMob.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.Players; using Content.Shared.Administration; @@ -44,7 +45,7 @@ namespace Content.Server.Administration.Commands return; } - if (!_entities.HasComponent(target)) + if (!_entities.HasComponent(target)) { shell.WriteLine(Loc.GetString("shell-entity-is-not-mob")); return; @@ -54,7 +55,8 @@ namespace Content.Server.Administration.Commands DebugTools.AssertNotNull(mind); - mind!.TransferTo(target); + var mindSystem = _entities.System(); + mindSystem.TransferTo(mind!, target); } } } diff --git a/Content.Server/Administration/Commands/SetMindCommand.cs b/Content.Server/Administration/Commands/SetMindCommand.cs index 5c728f6599..6ef9c7eede 100644 --- a/Content.Server/Administration/Commands/SetMindCommand.cs +++ b/Content.Server/Administration/Commands/SetMindCommand.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.Players; using Content.Shared.Administration; @@ -9,9 +10,10 @@ namespace Content.Server.Administration.Commands [AdminCommand(AdminFlags.Admin)] sealed class SetMindCommand : IConsoleCommand { + public string Command => "setmind"; - public string Description => Loc.GetString("set-mind-command-description", ("requiredComponent", nameof(MindComponent))); + public string Description => Loc.GetString("set-mind-command-description", ("requiredComponent", nameof(MindContainerComponent))); public string Help => Loc.GetString("set-mind-command-help-text", ("command", Command)); @@ -39,7 +41,7 @@ namespace Content.Server.Administration.Commands return; } - if (!entityManager.HasComponent(eUid)) + if (!entityManager.HasComponent(eUid)) { shell.WriteLine(Loc.GetString("set-mind-command-target-has-no-mind-message")); return; @@ -59,16 +61,16 @@ namespace Content.Server.Administration.Commands return; } + var mindSystem = entityManager.System(); + var mind = playerCData.Mind; if (mind == null) { - mind = new Mind.Mind(session.UserId) - { - CharacterName = entityManager.GetComponent(eUid).EntityName - }; - mind.ChangeOwningPlayer(session.UserId); + mind = mindSystem.CreateMind(session.UserId); + mind.CharacterName = entityManager.GetComponent(eUid).EntityName; } - mind.TransferTo(eUid); + + mindSystem.TransferTo(mind, eUid); } } } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index 7b6ca14ea1..c7e66dc77b 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -28,7 +28,7 @@ public sealed partial class AdminVerbSystem if (!_adminManager.HasAdminFlag(player, AdminFlags.Fun)) return; - var targetHasMind = TryComp(args.Target, out MindComponent? targetMindComp); + var targetHasMind = TryComp(args.Target, out MindContainerComponent? targetMindComp); if (!targetHasMind || targetMindComp == null) return; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 5b3c1efc8b..ce5d479851 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -8,6 +8,7 @@ using Content.Server.Disposal.Tube; using Content.Server.Disposal.Tube.Components; using Content.Server.EUI; using Content.Server.Ghost.Roles; +using Content.Server.Mind; using Content.Server.Mind.Commands; using Content.Server.Mind.Components; using Content.Server.Players; @@ -53,6 +54,7 @@ namespace Content.Server.Administration.Systems [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly PrayerSystem _prayerSystem = default!; [Dependency] private readonly EuiManager _eui = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; private readonly Dictionary _openSolutionUis = new(); @@ -245,7 +247,12 @@ namespace Content.Server.Administration.Systems Act = () => { MakeSentientCommand.MakeSentient(args.Target, EntityManager); - player.ContentData()?.Mind?.TransferTo(args.Target, ghostCheckOverride: true); + + var mind = player.ContentData()?.Mind; + if (mind == null) + return; + + _mindSystem.TransferTo(mind, args.Target, ghostCheckOverride: true); }, Impact = LogImpact.High, ConfirmationPopup = true @@ -279,7 +286,7 @@ namespace Content.Server.Administration.Systems // Make Sentient verb if (_groupController.CanCommand(player, "makesentient") && args.User != args.Target && - !EntityManager.HasComponent(args.Target)) + !EntityManager.HasComponent(args.Target)) { Verb verb = new() { @@ -342,7 +349,7 @@ namespace Content.Server.Administration.Systems // Make ghost role verb if (_groupController.CanCommand(player, "makeghostrole") && - !(EntityManager.GetComponentOrNull(args.Target)?.HasMind ?? false)) + !(EntityManager.GetComponentOrNull(args.Target)?.HasMind ?? false)) { Verb verb = new(); verb.Text = Loc.GetString("make-ghost-role-verb-get-data-text"); diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 0d2ec47c20..57603c35dc 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -4,6 +4,7 @@ using Content.Server.Body.Components; using Content.Server.GameTicking; using Content.Server.Humanoid; using Content.Server.Kitchen.Components; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Part; @@ -29,6 +30,7 @@ public sealed class BodySystem : SharedBodySystem [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public override void Initialize() { @@ -41,16 +43,14 @@ public sealed class BodySystem : SharedBodySystem private void OnRelayMoveInput(EntityUid uid, BodyComponent component, ref MoveInputEvent args) { - if (_mobState.IsDead(uid) && - EntityManager.TryGetComponent(uid, out var mind) && - mind.HasMind) + if (_mobState.IsDead(uid) && _mindSystem.TryGetMind(uid, out var mind)) { - if (!mind.Mind!.TimeOfDeath.HasValue) + if (!mind.TimeOfDeath.HasValue) { - mind.Mind.TimeOfDeath = _gameTiming.RealTime; + mind.TimeOfDeath = _gameTiming.RealTime; } - _ticker.OnGhostAttempt(mind.Mind!, true); + _ticker.OnGhostAttempt(mind, true); } } diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs index 97a76a64d1..0a8dd5dc6a 100644 --- a/Content.Server/Body/Systems/BrainSystem.cs +++ b/Content.Server/Body/Systems/BrainSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Body.Components; using Content.Server.Ghost.Components; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Events; @@ -13,6 +14,7 @@ namespace Content.Server.Body.Systems public sealed class BrainSystem : EntitySystem { [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public override void Initialize() { @@ -38,8 +40,8 @@ namespace Content.Server.Body.Systems private void HandleMind(EntityUid newEntity, EntityUid oldEntity) { - EntityManager.EnsureComponent(newEntity); - var oldMind = EntityManager.EnsureComponent(oldEntity); + EntityManager.EnsureComponent(newEntity); + var oldMind = EntityManager.EnsureComponent(oldEntity); EnsureComp(newEntity); if (HasComp(newEntity)) @@ -55,7 +57,10 @@ namespace Content.Server.Body.Systems _movementSpeed.ChangeBaseSpeed(newEntity, 0, 0 , 0, move); } - oldMind.Mind?.TransferTo(newEntity); + if (!_mindSystem.TryGetMind(oldEntity, out var mind, oldMind)) + return; + + _mindSystem.TransferTo(mind, newEntity); } } } diff --git a/Content.Server/Bql/QuerySelectors.cs b/Content.Server/Bql/QuerySelectors.cs index a3b943bc9d..829dee7e38 100644 --- a/Content.Server/Bql/QuerySelectors.cs +++ b/Content.Server/Bql/QuerySelectors.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Chemistry.Components.SolutionManager; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.Power.Components; using Content.Shared.Tag; @@ -21,7 +22,7 @@ namespace Content.Server.Bql { return input.Where(e => { - if (entityManager.TryGetComponent(e, out var mind)) + if (entityManager.TryGetComponent(e, out var mind)) return (mind.Mind?.VisitingEntity == e) ^ isInverted; return isInverted; @@ -32,7 +33,7 @@ namespace Content.Server.Bql { return DoSelection( - entityManager.EntityQuery().Select(x => x.Owner), + entityManager.EntityQuery().Select(x => x.Owner), arguments, isInverted, entityManager); } } @@ -68,14 +69,16 @@ namespace Content.Server.Bql public override IEnumerable DoSelection(IEnumerable input, IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) { + var mindSystem = entityManager.System(); return input.Where(e => - (entityManager.TryGetComponent(e, out var mind) && - !(mind.Mind?.CharacterDeadPhysically ?? false)) ^ isInverted); + entityManager.TryGetComponent(e, out var mind) + && mind.Mind != null + && !mindSystem.IsCharacterDeadPhysically(mind.Mind)); } public override IEnumerable DoInitialSelection(IReadOnlyList arguments, bool isInverted, IEntityManager entityManager) { - return DoSelection(entityManager.EntityQuery().Select(x => x.Owner), arguments, + return DoSelection(entityManager.EntityQuery().Select(x => x.Owner), arguments, isInverted, entityManager); } } diff --git a/Content.Server/CharacterInfo/CharacterInfoSystem.cs b/Content.Server/CharacterInfo/CharacterInfoSystem.cs index 282722ceae..bd26a4836b 100644 --- a/Content.Server/CharacterInfo/CharacterInfoSystem.cs +++ b/Content.Server/CharacterInfo/CharacterInfoSystem.cs @@ -26,9 +26,9 @@ public sealed class CharacterInfoSystem : EntitySystem var conditions = new Dictionary>(); var jobTitle = "No Profession"; var briefing = "!!ERROR: No Briefing!!"; //should never show on the UI unless there's a bug - if (EntityManager.TryGetComponent(entity, out MindComponent? mindComponent) && mindComponent.Mind != null) + if (EntityManager.TryGetComponent(entity, out MindContainerComponent? mindContainerComponent) && mindContainerComponent.Mind != null) { - var mind = mindComponent.Mind; + var mind = mindContainerComponent.Mind; // Get objectives foreach (var objective in mind.AllObjectives) diff --git a/Content.Server/Chat/Commands/SuicideCommand.cs b/Content.Server/Chat/Commands/SuicideCommand.cs index 2f08881cb0..16c699b0a9 100644 --- a/Content.Server/Chat/Commands/SuicideCommand.cs +++ b/Content.Server/Chat/Commands/SuicideCommand.cs @@ -29,7 +29,7 @@ namespace Content.Server.Chat.Commands var mind = player.ContentData()?.Mind; // This check also proves mind not-null for at the end when the mob is ghosted. - if (mind?.OwnedComponent?.Owner is not { Valid: true } victim) + if (mind?.OwnedEntity is not { Valid: true } victim) { shell.WriteLine("You don't have a mind!"); return; diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index 56f516942b..ee55f5a91c 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -116,20 +116,19 @@ namespace Content.Server.Chat.Managers ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients); } - public void SendAdminAlert(EntityUid player, string message, MindComponent? mindComponent = null) + public void SendAdminAlert(EntityUid player, string message, MindContainerComponent? mindContainerComponent = null) { - if((mindComponent == null && !_entityManager.TryGetComponent(player, out mindComponent)) - || mindComponent.Mind == null) + if ((mindContainerComponent == null && !_entityManager.TryGetComponent(player, out mindContainerComponent)) || !mindContainerComponent.HasMind) { SendAdminAlert(message); return; } var adminSystem = _entityManager.System(); - var antag = mindComponent.Mind!.UserId != null - && (adminSystem.GetCachedPlayerInfo(mindComponent.Mind!.UserId.Value)?.Antag ?? false); + var antag = mindContainerComponent.Mind!.UserId != null + && (adminSystem.GetCachedPlayerInfo(mindContainerComponent.Mind!.UserId.Value)?.Antag ?? false); - SendAdminAlert($"{mindComponent.Mind!.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}"); + SendAdminAlert($"{mindContainerComponent.Mind!.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}"); } public void SendHookOOC(string sender, string message) diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs index 6d74f76422..f3e0e8e2cf 100644 --- a/Content.Server/Chat/Managers/IChatManager.cs +++ b/Content.Server/Chat/Managers/IChatManager.cs @@ -24,7 +24,7 @@ namespace Content.Server.Chat.Managers void SendHookOOC(string sender, string message); void SendAdminAnnouncement(string message); void SendAdminAlert(string message); - void SendAdminAlert(EntityUid player, string message, MindComponent? mindComponent = null); + void SendAdminAlert(EntityUid player, string message, MindContainerComponent? mindContainerComponent = null); void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0); diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 3e1a264369..e780a5858f 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -560,9 +560,9 @@ public sealed partial class ChatSystem : SharedChatSystem if (player == null) return true; - var mindComponent = player.ContentData()?.Mind; + var mindContainerComponent = player.ContentData()?.Mind; - if (mindComponent == null) + if (mindContainerComponent == null) { shell?.WriteError("You don't have a mind!"); return false; diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index 5919ae3edb..899477cfa5 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -17,6 +17,12 @@ public sealed class MakeSentient : ReagentEffect var entityManager = args.EntityManager; var uid = args.SolutionEntity; + // This makes it so it doesn't affect things that are already sentient + if (entityManager.HasComponent(uid)) + { + return; + } + // This piece of code makes things able to speak "normally". One thing of note is that monkeys have a unique accent and won't be affected by this. entityManager.RemoveComponent(uid); @@ -24,7 +30,7 @@ public sealed class MakeSentient : ReagentEffect entityManager.RemoveComponent(uid); // This makes it so it doesn't add a ghost role to things that are already sentient - if (entityManager.HasComponent(uid)) + if (entityManager.HasComponent(uid)) { return; } diff --git a/Content.Server/Cloning/CloningConsoleSystem.cs b/Content.Server/Cloning/CloningConsoleSystem.cs index ceb642173f..0391cf541b 100644 --- a/Content.Server/Cloning/CloningConsoleSystem.cs +++ b/Content.Server/Cloning/CloningConsoleSystem.cs @@ -164,7 +164,7 @@ namespace Content.Server.Cloning if (body is null) return; - if (!TryComp(body, out var mindComp)) + if (!TryComp(body, out var mindComp)) return; var mind = mindComp.Mind; @@ -214,7 +214,7 @@ namespace Content.Server.Cloning { scanBodyInfo = MetaData(scanBody.Value).EntityName; - TryComp(scanBody, out var mindComp); + TryComp(scanBody, out var mindComp); if (!_mobStateSystem.IsDead(scanBody.Value)) { diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index f5bde07203..993099f997 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -20,6 +20,7 @@ using Content.Server.Materials; using Content.Server.Jobs; using Content.Shared.DeviceLinking.Events; using Content.Shared.Emag.Components; +using Content.Server.Mind; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Zombies; @@ -37,6 +38,7 @@ using Content.Shared.Doors.Components; using Content.Shared.Emag.Systems; using Robust.Shared.Audio; using System.Runtime.InteropServices; +using Content.Server.MachineLinking.Events; using Content.Server.Popups; using Content.Server.Traits.Assorted; @@ -63,6 +65,7 @@ namespace Content.Server.Cloning [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly MaterialStorageSystem _material = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public readonly Dictionary ClonesWaitingForMind = new(); public const float EasyModeCloningCost = 0.7f; @@ -107,12 +110,12 @@ namespace Content.Server.Cloning { if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) || !EntityManager.EntityExists(entity) || - !TryComp(entity, out var mindComp) || + !TryComp(entity, out var mindComp) || mindComp.Mind != null) return; - mind.TransferTo(entity, ghostCheckOverride: true); - mind.UnVisit(); + _mindSystem.TransferTo(mind, entity, ghostCheckOverride: true); + _mindSystem.UnVisit(mind); ClonesWaitingForMind.Remove(mind); } @@ -167,7 +170,7 @@ namespace Content.Server.Cloning { if (EntityManager.EntityExists(clone) && !_mobStateSystem.IsDead(clone) && - TryComp(clone, out var cloneMindComp) && + TryComp(clone, out var cloneMindComp) && (cloneMindComp.Mind == null || cloneMindComp.Mind == mind)) return false; // Mind already has clone diff --git a/Content.Server/Drone/DroneSystem.cs b/Content.Server/Drone/DroneSystem.cs index 00bde35a58..1cffb79ae7 100644 --- a/Content.Server/Drone/DroneSystem.cs +++ b/Content.Server/Drone/DroneSystem.cs @@ -72,7 +72,7 @@ namespace Content.Server.Drone private void OnExamined(EntityUid uid, DroneComponent component, ExaminedEvent args) { - if (TryComp(uid, out var mind) && mind.HasMind) + if (TryComp(uid, out var mind) && mind.HasMind) { args.PushMarkup(Loc.GetString("drone-active")); } @@ -132,7 +132,7 @@ namespace Content.Server.Drone foreach (var entity in _lookup.GetEntitiesInRange(xform.MapPosition, component.InteractionBlockRange)) { // Return true if the entity is/was controlled by a player and is not a drone or ghost. - if (HasComp(entity) && !HasComp(entity) && !HasComp(entity)) + if (HasComp(entity) && !HasComp(entity) && !HasComp(entity)) { // Filter out dead ghost roles. Dead normal players are intended to block. if ((TryComp(entity, out var entityMobState) && HasComp(entity) && _mobStateSystem.IsDead(entity, entityMobState))) diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs index 9d7617d1b8..e8d25d8190 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs @@ -399,7 +399,7 @@ public sealed partial class ExplosionSystem : EntitySystem { // no damage-dict multiplication required. _damageableSystem.TryChangeDamage(uid, damage, ignoreResistances: true, damageable: damageable); - if (HasComp(uid) || HasComp(uid)) + if (HasComp(uid) || HasComp(uid)) { var damageStr = string.Join(", ", damage.DamageDict.Select(entry => $"{entry.Key}: {entry.Value}")); _adminLogger.Add(LogType.Explosion, LogImpact.Medium, @@ -410,7 +410,7 @@ public sealed partial class ExplosionSystem : EntitySystem { var appliedDamage = damage * ev.DamageCoefficient; _damageableSystem.TryChangeDamage(uid, appliedDamage, ignoreResistances: true, damageable: damageable); - if (HasComp(uid) || HasComp(uid)) + if (HasComp(uid) || HasComp(uid)) { var damageStr = string.Join(", ", appliedDamage.DamageDict.Select(entry => $"{entry.Key}: {entry.Value}")); _adminLogger.Add(LogType.Explosion, LogImpact.Medium, diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index d3ab70a4f6..aa380e2cbd 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Content.Server.GameTicking.Presets; using Content.Server.Ghost.Components; +using Content.Server.Mind; using Content.Shared.CCVar; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; @@ -19,6 +20,7 @@ namespace Content.Server.GameTicking public const float PresetFailedCooldownIncrease = 30f; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public GamePresetPrototype? Preset { get; private set; } @@ -188,10 +190,10 @@ namespace Content.Server.GameTicking if (mind.VisitingEntity != default) { - mind.UnVisit(); + _mindSystem.UnVisit(mind); } - var position = playerEntity is {Valid: true} + var position = Exists(playerEntity) ? Transform(playerEntity.Value).Coordinates : GetObserverSpawnPoint(); @@ -206,7 +208,7 @@ namespace Content.Server.GameTicking // + If we're in a mob that is critical, and we're supposed to be able to return if possible, // we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK. // (If the mob survives, that's a bug. Ghosting is kept regardless.) - var canReturn = canReturnGlobal && mind.CharacterDeadPhysically; + var canReturn = canReturnGlobal && _mindSystem.IsCharacterDeadPhysically(mind); if (canReturnGlobal && TryComp(playerEntity, out MobStateComponent? mobState)) { @@ -248,9 +250,9 @@ namespace Content.Server.GameTicking _ghosts.SetCanReturnToBody(ghostComponent, canReturn); if (canReturn) - mind.Visit(ghost); + _mindSystem.Visit(mind, ghost); else - mind.TransferTo(ghost); + _mindSystem.TransferTo(mind, ghost); return true; } diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs index 10edb7d096..fd4a2e6a9c 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -8,6 +8,7 @@ using Robust.Server.Player; using Robust.Shared.Enums; using Robust.Shared.Timing; using Robust.Shared.Utility; +using PlayerData = Content.Server.Players.PlayerData; namespace Content.Server.GameTicking { diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index b7a814878e..4f21cc2184 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -19,6 +19,7 @@ using Robust.Shared.Utility; using System.Linq; using Content.Shared.Database; using Robust.Shared.Asynchronous; +using PlayerData = Content.Server.Players.PlayerData; namespace Content.Server.GameTicking { @@ -315,12 +316,12 @@ namespace Content.Server.GameTicking var connected = false; var observer = mind.AllRoles.Any(role => role is ObserverRole); // Continuing - if (_playerManager.TryGetSessionById(userId, out var ply)) + if (userId != null && _playerManager.ValidSessionId(userId.Value)) { connected = true; } PlayerData? contentPlayerData = null; - if (_playerManager.TryGetPlayerData(userId, out var playerData)) + if (userId != null && _playerManager.TryGetPlayerData(userId.Value, out var playerData)) { contentPlayerData = playerData.ContentData(); } diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index ca8186d0b9..115c3ace02 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -176,15 +176,12 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(data); data!.WipeMind(); - var newMind = new Mind.Mind(data.UserId) - { - CharacterName = character.Name - }; - newMind.ChangeOwningPlayer(data.UserId); + var newMind = _mindSystem.CreateMind(data.UserId, character.Name); + _mindSystem.ChangeOwningPlayer(newMind, data.UserId); var jobPrototype = _prototypeManager.Index(jobId); var job = new Job(newMind, jobPrototype); - newMind.AddRole(job); + _mindSystem.AddRole(newMind, job); _playTimeTrackings.PlayerRolesChanged(player); @@ -193,7 +190,7 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(mobMaybe); var mob = mobMaybe!.Value; - newMind.TransferTo(mob); + _mindSystem.TransferTo(newMind, mob); if (lateJoin) { @@ -282,15 +279,15 @@ namespace Content.Server.GameTicking DebugTools.AssertNotNull(data); data!.WipeMind(); - var newMind = new Mind.Mind(data.UserId); - newMind.ChangeOwningPlayer(data.UserId); - newMind.AddRole(new ObserverRole(newMind)); + var newMind = _mindSystem.CreateMind(data.UserId); + _mindSystem.ChangeOwningPlayer(newMind, data.UserId); + _mindSystem.AddRole(newMind, new ObserverRole(newMind)); var mob = SpawnObserverMob(); EntityManager.GetComponent(mob).EntityName = name; var ghost = EntityManager.GetComponent(mob); EntitySystem.Get().SetCanReturnToBody(ghost, false); - newMind.TransferTo(mob); + _mindSystem.TransferTo(newMind, mob); _playerGameStatuses[player.UserId] = PlayerGameStatus.JoinedGame; RaiseNetworkEvent(GetStatusSingle(player, PlayerGameStatus.JoinedGame)); diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index ca7a9854fc..9bc2324d4f 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -5,6 +5,8 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; using Content.Server.Humanoid; +using Content.Server.Humanoid.Systems; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.NPC.Components; using Content.Server.NPC.Systems; @@ -54,6 +56,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly MapLoaderSystem _map = default!; [Dependency] private readonly ShuttleSystem _shuttle = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public override void Initialize() { @@ -81,7 +84,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem continue; // If entity has a prior mind attached, add them to the players list. - if (!TryComp(uid, out var mindComponent)) + if (!TryComp(uid, out var mindComponent)) continue; var session = mindComponent.Mind?.Session; @@ -571,22 +574,21 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args) { - if (!TryComp(uid, out var mindComponent) || mindComponent.Mind == null) + if (!TryComp(uid, out var mindContainerComponent) || mindContainerComponent.Mind == null) return; - var mind = mindComponent.Mind; + var mind = mindContainerComponent.Mind; foreach (var nukeops in EntityQuery()) { if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost || !nukeops.EndsRound) { role ??= nukeops.OperativeRoleProto; - - mind.AddRole(new NukeopsRole(mind, _prototypeManager.Index(role))); + _mindSystem.AddRole(mind, new NukeopsRole(mind, _prototypeManager.Index(role))); nukeops.OperativeMindPendingData.Remove(uid); } - if (!mind.TryGetSession(out var playerSession)) + if (!_mindSystem.TryGetSession(mind, out var playerSession)) return; if (nukeops.OperativePlayers.ContainsValue(playerSession)) return; @@ -756,15 +758,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns)); SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component); + var newMind = _mindSystem.CreateMind(session.UserId, spawnDetails.Name); + _mindSystem.ChangeOwningPlayer(newMind, session.UserId); + _mindSystem.AddRole(newMind, new NukeopsRole(newMind, nukeOpsAntag)); - var newMind = new Mind.Mind(session.UserId) - { - CharacterName = spawnDetails.Name - }; - newMind.ChangeOwningPlayer(session.UserId); - newMind.AddRole(new NukeopsRole(newMind, nukeOpsAntag)); - - newMind.TransferTo(mob); + _mindSystem.TransferTo(newMind, mob); } else if (addSpawnPoints) { @@ -810,7 +808,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem return; //ok hardcoded value bad but so is everything else here - mind.AddRole(new NukeopsRole(mind, _prototypeManager.Index("Nukeops"))); + _mindSystem.AddRole(mind, new NukeopsRole(mind, _prototypeManager.Index("Nukeops"))); SetOutfitCommand.SetOutfit(mind.OwnedEntity.Value, "SyndicateOperativeGearFull", EntityManager); } @@ -859,10 +857,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } // Add pre-existing nuke operatives to the credit list. - var query = EntityQuery(true); + var query = EntityQuery(true); foreach (var (_, mindComp, metaData) in query) { - if (mindComp.Mind == null || !mindComp.Mind.TryGetSession(out var session)) + if (!mindComp.HasMind || !_mindSystem.TryGetSession(mindComp.Mind, out var session)) continue; component.OperativePlayers.Add(metaData.EntityName, session); } diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs index 2af25b3a96..e38eeba2e9 100644 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Administration.Commands; using Content.Server.Cargo.Systems; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; +using Content.Server.Mind; using Content.Server.Preferences.Managers; using Content.Server.Spawners.Components; using Content.Server.Station.Components; @@ -40,6 +41,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem [Dependency] private readonly PricingSystem _pricingSystem = default!; [Dependency] private readonly MapLoaderSystem _map = default!; [Dependency] private readonly NamingSystem _namingSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; /// @@ -205,16 +207,13 @@ public sealed class PiratesRuleSystem : GameRuleSystem var name = _namingSystem.GetName("Human", gender); var session = ops[i]; - var newMind = new Mind.Mind(session.UserId) - { - CharacterName = name - }; - newMind.ChangeOwningPlayer(session.UserId); + var newMind = _mindSystem.CreateMind(session.UserId, name); + _mindSystem.ChangeOwningPlayer(newMind, session.UserId); var mob = Spawn("MobHuman", _random.Pick(spawns)); MetaData(mob).EntityName = name; - newMind.TransferTo(mob); + _mindSystem.TransferTo(newMind, mob); var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile; _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile); diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index e23ac9fcc9..5ba752dd12 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; using Content.Server.NPC.Systems; +using Content.Server.Mind; using Content.Server.Objectives.Interfaces; using Content.Server.PDA.Ringer; using Content.Server.Players; @@ -34,6 +35,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly UplinkSystem _uplink = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; private ISawmill _sawmill = default!; @@ -250,11 +252,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("", code)))); // Assign traitor roles - mind.AddRole(traitorRole); + _mindSystem.AddRole(mind, traitorRole); SendTraitorBriefing(mind, traitorRule.Codewords, code); traitorRule.Traitors.Add(traitorRole); - if (mind.TryGetSession(out var session)) + if (_mindSystem.TryGetSession(mind, out var session)) { // Notificate player about new role assignment _audioSystem.PlayGlobal(traitorRule.GreetSoundNotification, session); @@ -271,9 +273,10 @@ public sealed class TraitorRuleSystem : GameRuleSystem for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++) { var objective = _objectivesManager.GetRandomObjective(traitorRole.Mind, "TraitorObjectiveGroups"); + if (objective == null) continue; - if (traitorRole.Mind.TryAddObjective(objective)) + if (_mindSystem.TryAddObjective(traitorRole.Mind, objective)) difficulty += objective.Difficulty; } @@ -288,7 +291,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem /// Uplink codes private void SendTraitorBriefing(Mind.Mind mind, string[] codewords, Note[] code) { - if (mind.TryGetSession(out var session)) + if (_mindSystem.TryGetSession(mind, out var session)) { _chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-greeting")); _chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords)))); @@ -367,7 +370,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem foreach (var t in traitor.Traitors) { var name = t.Mind.CharacterName; - t.Mind.TryGetSession(out var session); + _mindSystem.TryGetSession(t.Mind, out var session); var username = session?.Name; var objectives = t.Mind.AllObjectives.ToArray(); diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index 0164adaaea..9ee6487993 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -3,6 +3,7 @@ using System.Linq; using Content.Server.Actions; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.Players; using Content.Server.Popups; @@ -41,6 +42,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly ZombifyOnDeathSystem _zombify = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public override void Initialize() { @@ -92,7 +94,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem { var meta = MetaData(survivor); var username = string.Empty; - if (TryComp(survivor, out var mindcomp)) + if (TryComp(survivor, out var mindcomp)) if (mindcomp.Mind != null && mindcomp.Mind.Session != null) username = mindcomp.Mind.Session.Name; @@ -286,8 +288,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem } DebugTools.AssertNotNull(mind.OwnedEntity); - - mind.AddRole(new ZombieRole(mind, _prototypeManager.Index(component.PatientZeroPrototypeID))); + _mindSystem.AddRole(mind, new ZombieRole(mind, _prototypeManager.Index(component.PatientZeroPrototypeID))); var inCharacterName = string.Empty; // Create some variation between the times of each zombie, relative to the time of the group as a whole. diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 5213ec83c0..9e598964c3 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -92,13 +92,13 @@ namespace Content.Server.Ghost if (EntityManager.HasComponent(uid)) return; - if (!EntityManager.TryGetComponent(uid, out var mind) || !mind.HasMind || mind.Mind!.IsVisitingEntity) + if (!EntityManager.TryGetComponent(uid, out var mind) || !mind.HasMind || mind.Mind.IsVisitingEntity) return; if (component.MustBeDead && (_mobState.IsAlive(uid) || _mobState.IsCritical(uid))) return; - _ticker.OnGhostAttempt(mind.Mind!, component.CanReturn); + _ticker.OnGhostAttempt(mind.Mind, component.CanReturn); } private void OnGhostStartup(EntityUid uid, GhostComponent component, ComponentStartup args) @@ -172,7 +172,7 @@ namespace Content.Server.Ghost private void OnPlayerDetached(EntityUid uid, GhostComponent component, PlayerDetachedEvent args) { - QueueDel(uid); + DeleteEntity(uid); } private void OnGhostWarpsRequest(GhostWarpsRequestEvent msg, EntitySessionEventArgs args) @@ -199,7 +199,7 @@ namespace Content.Server.Ghost return; } - actor.PlayerSession.ContentData()!.Mind?.UnVisit(); + _mindSystem.UnVisit(actor.PlayerSession.ContentData()!.Mind); } private void OnGhostWarpToTargetRequest(GhostWarpToTargetRequestEvent msg, EntitySessionEventArgs args) @@ -236,9 +236,9 @@ namespace Content.Server.Ghost if (Deleted(uid) || Terminating(uid)) return; - if (EntityManager.TryGetComponent(uid, out var mind)) + if (EntityManager.TryGetComponent(uid, out var mind)) _mindSystem.SetGhostOnShutdown(uid, false, mind); - EntityManager.DeleteEntity(uid); + QueueDel(uid); } private IEnumerable GetLocationWarps() @@ -260,7 +260,7 @@ namespace Content.Server.Ghost { if (attached == except) continue; - TryComp(attached, out var mind); + TryComp(attached, out var mind); string playerInfo = $"{EntityManager.GetComponent(attached).EntityName} ({mind?.Mind?.CurrentJob?.Name ?? "Unknown"})"; diff --git a/Content.Server/Ghost/ReturnToBodyEui.cs b/Content.Server/Ghost/ReturnToBodyEui.cs index b3903f8434..c2d5a96d3f 100644 --- a/Content.Server/Ghost/ReturnToBodyEui.cs +++ b/Content.Server/Ghost/ReturnToBodyEui.cs @@ -1,4 +1,5 @@ using Content.Server.EUI; +using Content.Server.Mind; using Content.Server.Players; using Content.Shared.Eui; using Content.Shared.Ghost; @@ -7,6 +8,8 @@ namespace Content.Server.Ghost; public sealed class ReturnToBodyEui : BaseEui { + [Dependency] private readonly MindSystem _mindSystem = default!; + private readonly Mind.Mind _mind; public ReturnToBodyEui(Mind.Mind mind) @@ -25,8 +28,8 @@ public sealed class ReturnToBodyEui : BaseEui return; } - if (_mind.TryGetSession(out var session)) - session.ContentData()!.Mind?.UnVisit(); + if (_mindSystem.TryGetSession(_mind, out var session)) + _mindSystem.UnVisit(session.ContentData()!.Mind); Close(); } } diff --git a/Content.Server/Ghost/Roles/Components/GhostRoleComponent.cs b/Content.Server/Ghost/Roles/Components/GhostRoleComponent.cs index 0b5a90ff50..96d0f02b79 100644 --- a/Content.Server/Ghost/Roles/Components/GhostRoleComponent.cs +++ b/Content.Server/Ghost/Roles/Components/GhostRoleComponent.cs @@ -6,7 +6,7 @@ namespace Content.Server.Ghost.Roles.Components [Access(typeof(GhostRoleSystem))] public sealed class GhostRoleComponent : Component { - [DataField("name")] public string _roleName = "Unknown"; + [DataField("name")] private string _roleName = "Unknown"; [DataField("description")] private string _roleDescription = "Unknown"; diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs index bdb0d97186..70f4673b19 100644 --- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; using Content.Server.Ghost.Roles.UI; using Content.Server.Mind.Commands; +using Content.Server.Mind; using Content.Server.Mind.Components; using Content.Server.Players; using Content.Shared.Administration; @@ -33,6 +34,7 @@ namespace Content.Server.Ghost.Roles [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly FollowerSystem _followerSystem = default!; [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; private uint _nextRoleIdentifier; private bool _needsUpdateGhostRoleCount = true; @@ -214,18 +216,14 @@ namespace Content.Server.Ghost.Roles { if (!Resolve(roleUid, ref role)) return; - var contentData = player.ContentData(); + DebugTools.AssertNotNull(player.ContentData()); - DebugTools.AssertNotNull(contentData); + var newMind = _mindSystem.CreateMind(player.UserId, + EntityManager.GetComponent(mob).EntityName); + _mindSystem.AddRole(newMind, new GhostRoleMarkerRole(newMind, role.RoleName)); - var newMind = new Mind.Mind(player.UserId) - { - CharacterName = EntityManager.GetComponent(mob).EntityName - }; - newMind.AddRole(new GhostRoleMarkerRole(newMind, role.RoleName)); - - newMind.ChangeOwningPlayer(player.UserId); - newMind.TransferTo(mob); + _mindSystem.ChangeOwningPlayer(newMind, player.UserId); + _mindSystem.TransferTo(newMind, mob); } public GhostRoleInfo[] GetGhostRolesInfo() @@ -343,7 +341,7 @@ namespace Content.Server.Ghost.Roles if (ghostRole.MakeSentient) MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech); - mob.EnsureComponent(); + mob.EnsureComponent(); GhostRoleInternalCreateMindAndTransfer(args.Player, uid, mob, ghostRole); @@ -379,7 +377,7 @@ namespace Content.Server.Ghost.Roles ghostRole.Taken = true; - var mind = EnsureComp(uid); + var mind = EnsureComp(uid); if (mind.HasMind) { diff --git a/Content.Server/Ghost/Roles/MakeGhostRoleCommand.cs b/Content.Server/Ghost/Roles/MakeGhostRoleCommand.cs index e6ed131550..cbbec64f11 100644 --- a/Content.Server/Ghost/Roles/MakeGhostRoleCommand.cs +++ b/Content.Server/Ghost/Roles/MakeGhostRoleCommand.cs @@ -35,7 +35,7 @@ namespace Content.Server.Ghost.Roles return; } - if (entityManager.TryGetComponent(uid, out MindComponent? mind) && + if (entityManager.TryGetComponent(uid, out MindContainerComponent? mind) && mind.HasMind) { shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a mind."); diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index 57e8a257dd..a5b188b373 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -243,7 +243,7 @@ namespace Content.Server.Medical.BiomassReclaimer // Reject souled bodies in easy mode. if (_configManager.GetCVar(CCVars.BiomassEasyMode) && HasComp(dragged) && - TryComp(dragged, out var mindComp)) + TryComp(dragged, out var mindComp)) { if (mindComp.Mind?.UserId != null && _playerManager.TryGetSessionById(mindComp.Mind.UserId.Value, out _)) return false; diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index 06a4e93736..85cdec5590 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -222,7 +222,7 @@ public sealed class DefibrillatorSystem : EntitySystem _mobState.ChangeMobState(target, MobState.Critical, mob, uid); _mobThreshold.SetAllowRevives(target, false, thresholds); - if (TryComp(target, out var mindComp) && + if (TryComp(target, out var mindComp) && mindComp.Mind?.Session is { } playerSession) { session = playerSession; diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index d7f7f7087b..22ca8ebde0 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -44,7 +44,7 @@ namespace Content.Server.Mind.Commands public static void MakeSentient(EntityUid uid, IEntityManager entityManager, bool allowMovement = true, bool allowSpeech = true) { - entityManager.EnsureComponent(uid); + entityManager.EnsureComponent(uid); if (allowMovement) { entityManager.EnsureComponent(uid); diff --git a/Content.Server/Mind/Commands/MindInfoCommand.cs b/Content.Server/Mind/Commands/MindInfoCommand.cs index fc2b0ff001..749135d260 100644 --- a/Content.Server/Mind/Commands/MindInfoCommand.cs +++ b/Content.Server/Mind/Commands/MindInfoCommand.cs @@ -40,7 +40,7 @@ namespace Content.Server.Mind.Commands } var builder = new StringBuilder(); - builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.UserId, mind.OwnedComponent?.Owner); + builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.UserId, mind.OwnedEntity); foreach (var role in mind.AllRoles) { builder.AppendFormat("{0} ", role.Name); diff --git a/Content.Server/Mind/Commands/RenameCommand.cs b/Content.Server/Mind/Commands/RenameCommand.cs index 3c28cd191a..bd22dd4c7d 100644 --- a/Content.Server/Mind/Commands/RenameCommand.cs +++ b/Content.Server/Mind/Commands/RenameCommand.cs @@ -48,7 +48,7 @@ public sealed class RenameCommand : IConsoleCommand var entSysMan = IoCManager.Resolve(); - if (entMan.TryGetComponent(entityUid, out MindComponent? mind) && mind.Mind != null) + if (entMan.TryGetComponent(entityUid, out MindContainerComponent? mind) && mind.Mind != null) { // Mind mind.Mind.CharacterName = name; diff --git a/Content.Server/Mind/Components/MindComponent.cs b/Content.Server/Mind/Components/MindContainerComponent.cs similarity index 88% rename from Content.Server/Mind/Components/MindComponent.cs rename to Content.Server/Mind/Components/MindContainerComponent.cs index cdc2f67d0f..bf2c8291fc 100644 --- a/Content.Server/Mind/Components/MindComponent.cs +++ b/Content.Server/Mind/Components/MindContainerComponent.cs @@ -1,10 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using YamlDotNet.Core.Tokens; + namespace Content.Server.Mind.Components { /// /// Stores a on a mob. /// [RegisterComponent, Access(typeof(MindSystem))] - public sealed class MindComponent : Component + public sealed class MindContainerComponent : Component { /// /// The mind controlling this mob. Can be null. @@ -17,6 +20,7 @@ namespace Content.Server.Mind.Components /// True if we have a mind, false otherwise. /// [ViewVariables] + [MemberNotNullWhen(true, nameof(Mind))] public bool HasMind => Mind != null; /// diff --git a/Content.Server/Mind/Components/VisitingMindComponent.cs b/Content.Server/Mind/Components/VisitingMindComponent.cs index e7b81c0cc7..9b2e0d66e5 100644 --- a/Content.Server/Mind/Components/VisitingMindComponent.cs +++ b/Content.Server/Mind/Components/VisitingMindComponent.cs @@ -3,14 +3,8 @@ namespace Content.Server.Mind.Components [RegisterComponent] public sealed class VisitingMindComponent : Component { - [ViewVariables] public Mind Mind { get; set; } = default!; - - protected override void OnRemove() - { - base.OnRemove(); - - Mind?.UnVisit(); - } + [ViewVariables] + public Mind? Mind; } public sealed class MindUnvisitedMessage : EntityEventArgs diff --git a/Content.Server/Mind/Mind.cs b/Content.Server/Mind/Mind.cs index 3af6daf4eb..81f26d55a9 100644 --- a/Content.Server/Mind/Mind.cs +++ b/Content.Server/Mind/Mind.cs @@ -1,19 +1,9 @@ -using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.GameTicking; -using Content.Server.Ghost.Components; using Content.Server.Mind.Components; using Content.Server.Objectives; -using Content.Server.Players; using Content.Server.Roles; -using Content.Shared.Database; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Network; -using Robust.Shared.Utility; namespace Content.Server.Mind { @@ -29,16 +19,9 @@ namespace Content.Server.Mind /// public sealed class Mind { - private readonly MobStateSystem _mobStateSystem = default!; - private readonly GameTicker _gameTickerSystem = default!; - private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; + internal readonly ISet Roles = new HashSet(); - private readonly ISet _roles = new HashSet(); - - private readonly List _objectives = new(); + internal readonly List Objectives = new(); public string Briefing = String.Empty; @@ -48,36 +31,32 @@ namespace Content.Server.Mind /// The provided UserId is solely for tracking of intended owner. /// /// The session ID of the original owner (may get credited). - public Mind(NetUserId userId) + public Mind(NetUserId? userId) { OriginalOwnerUserId = userId; - IoCManager.InjectDependencies(this); - _entityManager.EntitySysManager.Resolve(ref _mobStateSystem); - _entityManager.EntitySysManager.Resolve(ref _gameTickerSystem); - _entityManager.EntitySysManager.Resolve(ref _mindSystem); } - // TODO: This session should be able to be changed, probably. /// /// The session ID of the player owning this mind. /// [ViewVariables] - public NetUserId? UserId { get; private set; } + public NetUserId? UserId { get; internal set; } /// /// The session ID of the original owner, if any. /// May end up used for round-end information (as the owner may have abandoned Mind since) /// [ViewVariables] - public NetUserId OriginalOwnerUserId { get; } + public NetUserId? OriginalOwnerUserId { get; } [ViewVariables] public bool IsVisitingEntity => VisitingEntity != null; [ViewVariables] - public EntityUid? VisitingEntity { get; private set; } + public EntityUid? VisitingEntity { get; set; } - [ViewVariables] public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity; + [ViewVariables] + public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity; [ViewVariables(VVAccess.ReadWrite)] public string? CharacterName { get; set; } @@ -87,33 +66,33 @@ namespace Content.Server.Mind /// Can be null - will be null if the Mind is not considered "dead". /// [ViewVariables] - public TimeSpan? TimeOfDeath { get; set; } = null; + public TimeSpan? TimeOfDeath { get; set; } /// /// The component currently owned by this mind. /// Can be null. /// [ViewVariables] - public MindComponent? OwnedComponent { get; private set; } + public MindContainerComponent? OwnedComponent { get; internal set; } /// /// The entity currently owned by this mind. /// Can be null. /// [ViewVariables] - public EntityUid? OwnedEntity => OwnedComponent?.Owner; + public EntityUid? OwnedEntity { get; internal set; } /// /// An enumerable over all the roles this mind has. /// [ViewVariables] - public IEnumerable AllRoles => _roles; + public IEnumerable AllRoles => Roles; /// /// An enumerable over all the objectives this mind has. /// [ViewVariables] - public IEnumerable AllObjectives => _objectives; + public IEnumerable AllObjectives => Objectives; /// /// Prevents user from ghosting out @@ -134,341 +113,11 @@ namespace Content.Server.Mind /// Can be null, in which case the player is currently not logged in. /// [ViewVariables] - public IPlayerSession? Session - { - get - { - if (!UserId.HasValue) - { - return null; - } - _playerManager.TryGetSessionById(UserId.Value, out var ret); - return ret; - } - } - - /// - /// True if this Mind is 'sufficiently dead' IC (objectives, endtext). - /// Note that this is *IC logic*, it's not necessarily tied to any specific truth. - /// "If administrators decide that zombies are dead, this returns true for zombies." - /// (Maybe you were looking for the action blocker system?) - /// - [ViewVariables] - public bool CharacterDeadIC => CharacterDeadPhysically; - - /// - /// True if the OwnedEntity of this mind is physically dead. - /// This specific definition, as opposed to CharacterDeadIC, is used to determine if ghosting should allow return. - /// - [ViewVariables] - public bool CharacterDeadPhysically - { - get - { - // This is written explicitly so that the logic can be understood. - // But it's also weird and potentially situational. - // Specific considerations when updating this: - // + Does being turned into a borg (if/when implemented) count as dead? - // *If not, add specific conditions to users of this property where applicable.* - // + Is being transformed into a donut 'dead'? - // TODO: Consider changing the way ghost roles work. - // Mind is an *IC* mind, therefore ghost takeover is IC revival right now. - // + Is it necessary to have a reference to a specific 'mind iteration' to cycle when certain events happen? - // (If being a borg or AI counts as dead, then this is highly likely, as it's still the same Mind for practical purposes.) - - // This can be null if they're deleted (spike / brain nom) - var targetMobState = _entityManager.GetComponentOrNull(OwnedEntity); - // This can be null if it's a brain (this happens very often) - // Brains are the result of gibbing so should definitely count as dead - if (targetMobState == null) - return true; - // They might actually be alive. - return _mobStateSystem.IsDead(OwnedEntity!.Value, targetMobState); - } - } - - /// - /// A string to represent the mind for logging - /// - private string MindOwnerLoggingString - { - get - { - if (OwnedEntity != null) - return _entityManager.ToPrettyString(OwnedEntity.Value); - if (UserId != null) - return UserId.Value.ToString(); - return "(originally " + OriginalOwnerUserId + ")"; - } - } - - /// - /// Gives this mind a new role. - /// - /// The type of the role to give. - /// The instance of the role. - /// - /// Thrown if we already have a role with this type. - /// - public Role AddRole(Role role) - { - if (_roles.Contains(role)) - { - throw new ArgumentException($"We already have this role: {role}"); - } - - _roles.Add(role); - role.Greet(); - - var message = new RoleAddedEvent(this, role); - if (OwnedEntity != null) - { - _entityManager.EventBus.RaiseLocalEvent(OwnedEntity.Value, message, true); - } - _adminLogger.Add(LogType.Mind, LogImpact.Low, - $"'{role.Name}' added to mind of {MindOwnerLoggingString}"); - - return role; - } - - /// - /// Removes a role from this mind. - /// - /// The type of the role to remove. - /// - /// Thrown if we do not have this role. - /// - public void RemoveRole(Role role) - { - if (!_roles.Contains(role)) - { - throw new ArgumentException($"We do not have this role: {role}"); - } - - _roles.Remove(role); - - var message = new RoleRemovedEvent(this, role); - - if (OwnedEntity != null) - { - _entityManager.EventBus.RaiseLocalEvent(OwnedEntity.Value, message, true); - } - _adminLogger.Add(LogType.Mind, LogImpact.Low, - $"'{role.Name}' removed from mind of {MindOwnerLoggingString}"); - } - - public bool HasRole() where T : Role - { - return _roles.Any(role => role is T); - } + public IPlayerSession? Session { get; internal set; } /// /// Gets the current job /// - public Job? CurrentJob => _roles.OfType().SingleOrDefault(); - - /// - /// Adds an objective to this mind. - /// - public bool TryAddObjective(ObjectivePrototype objectivePrototype) - { - if (!objectivePrototype.CanBeAssigned(this)) - return false; - var objective = objectivePrototype.GetObjective(this); - if (_objectives.Contains(objective)) - return false; - - foreach (var condition in objective.Conditions) - _adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' added to mind of {MindOwnerLoggingString}"); - - - _objectives.Add(objective); - return true; - } - - /// - /// Removes an objective from this mind. - /// - /// Returns true if the removal succeeded. - public bool TryRemoveObjective(int index) - { - if (index < 0 || index >= _objectives.Count) return false; - - var objective = _objectives[index]; - - foreach (var condition in objective.Conditions) - _adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' removed from the mind of {MindOwnerLoggingString}"); - - _objectives.Remove(objective); - return true; - } - - /// - /// Transfer this mind's control over to a new entity. - /// - /// - /// The entity to control. - /// Can be null, in which case it will simply detach the mind from any entity. - /// - /// - /// If true, skips ghost check for Visiting Entity - /// - /// - /// Thrown if is already owned by another mind. - /// - public void TransferTo(EntityUid? entity, bool ghostCheckOverride = false) - { - // Looks like caller just wants us to go back to normal. - if (entity == OwnedEntity) - { - UnVisit(); - return; - } - - MindComponent? component = null; - var alreadyAttached = false; - - if (entity != null) - { - if (!_entityManager.TryGetComponent(entity.Value, out component)) - { - component = _entityManager.AddComponent(entity.Value); - } - else if (component.HasMind) - { - _gameTickerSystem.OnGhostAttempt(component.Mind!, false); - } - - if (_entityManager.TryGetComponent(entity.Value, out var actor)) - { - // Happens when transferring to your currently visited entity. - if (actor.PlayerSession != Session) - { - throw new ArgumentException("Visit target already has a session.", nameof(entity)); - } - - alreadyAttached = true; - } - } - - if(OwnedComponent != null) - _mindSystem.InternalEjectMind(OwnedComponent.Owner, OwnedComponent); - - OwnedComponent = component; - if(OwnedComponent != null) - _mindSystem.InternalAssignMind(OwnedComponent.Owner, this, OwnedComponent); - - // Don't do the full deletion cleanup if we're transferring to our visitingentity - if (alreadyAttached) - { - // Set VisitingEntity null first so the removal of VisitingMind doesn't get through Unvisit() and delete what we're visiting. - // Yes this control flow sucks. - VisitingEntity = null; - _entityManager.RemoveComponent(entity!.Value); - } - else if (VisitingEntity != null - && (ghostCheckOverride // to force mind transfer, for example from ControlMobVerb - || !_entityManager.TryGetComponent(VisitingEntity!, out GhostComponent? ghostComponent) // visiting entity is not a Ghost - || !ghostComponent.CanReturnToBody)) // it is a ghost, but cannot return to body anyway, so it's okay - { - RemoveVisitingEntity(); - } - - // Player is CURRENTLY connected. - if (Session != null && !alreadyAttached && VisitingEntity == null) - { - Session.AttachToEntity(entity); - Logger.Info($"Session {Session.Name} transferred to entity {entity}."); - } - } - - public void ChangeOwningPlayer(NetUserId? newOwner) - { - var playerMgr = IoCManager.Resolve(); - PlayerData? newOwnerData = null; - - if (newOwner.HasValue) - { - if (!playerMgr.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."); - } - - newOwnerData = uncast.ContentData(); - } - - // Make sure to remove control from our old owner if they're logged in. - var oldSession = Session; - oldSession?.AttachToEntity(null); - - if (UserId.HasValue) - { - var data = playerMgr.GetPlayerData(UserId.Value).ContentData(); - DebugTools.AssertNotNull(data); - data!.UpdateMindFromMindChangeOwningPlayer(null); - } - - UserId = newOwner; - if (!newOwner.HasValue) - { - return; - } - - // Yank new owner out of their old mind too. - // Can I mention how much I love the word yank? - DebugTools.AssertNotNull(newOwnerData); - newOwnerData!.Mind?.ChangeOwningPlayer(null); - newOwnerData.UpdateMindFromMindChangeOwningPlayer(this); - } - - public void Visit(EntityUid entity) - { - Session?.AttachToEntity(entity); - VisitingEntity = entity; - - var comp = _entityManager.AddComponent(entity); - comp.Mind = this; - - Logger.Info($"Session {Session?.Name} visiting entity {entity}."); - } - - /// - /// Returns the mind to its original entity. - /// - public void UnVisit() - { - var currentEntity = Session?.AttachedEntity; - Session?.AttachToEntity(OwnedEntity); - RemoveVisitingEntity(); - - if (Session != null && OwnedEntity != null && currentEntity != OwnedEntity) - _adminLogger.Add(LogType.Mind, LogImpact.Low, - $"{Session.Name} returned to {_entityManager.ToPrettyString(OwnedEntity.Value)}"); - } - - /// - /// Cleans up the VisitingEntity. - /// - private void RemoveVisitingEntity() - { - if (VisitingEntity == null) - return; - - var oldVisitingEnt = VisitingEntity.Value; - // Null this before removing the component to avoid any infinite loops. - VisitingEntity = null; - - DebugTools.AssertNotNull(oldVisitingEnt); - _entityManager.RemoveComponent(oldVisitingEnt); - _entityManager.EventBus.RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true); - } - - public bool TryGetSession([NotNullWhen(true)] out IPlayerSession? session) - { - return (session = Session) != null; - } + public Job? CurrentJob => Roles.OfType().SingleOrDefault(); } } diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index 5d017418be..ccb180cd5a 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -1,13 +1,24 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.Administration.Logs; using Content.Server.GameTicking; using Content.Server.Ghost; using Content.Server.Ghost.Components; using Content.Server.Mind.Components; +using Content.Server.Objectives; +using Content.Server.Players; +using Content.Server.Roles; +using Content.Shared.Database; using Content.Shared.Examine; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Interaction.Events; +using Content.Shared.Mobs.Components; +using Robust.Server.GameObjects; +using Robust.Server.Player; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Mind; @@ -17,17 +28,34 @@ public sealed class MindSystem : EntitySystem [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly GhostSystem _ghostSystem = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly ActorSystem _actor = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnSuicide); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnSuicide); + SubscribeLocalEvent(OnTerminating); + SubscribeLocalEvent(OnDetached); } - public void SetGhostOnShutdown(EntityUid uid, bool value, MindComponent? mind = null) + private void OnDetached(EntityUid uid, VisitingMindComponent component, PlayerDetachedEvent args) + { + 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); + } + + public void SetGhostOnShutdown(EntityUid uid, bool value, MindContainerComponent? mind = null) { if (!Resolve(uid, ref mind)) return; @@ -37,10 +65,10 @@ public sealed class MindSystem : EntitySystem /// /// Don't call this unless you know what the hell you're doing. - /// Use instead. + /// Use instead. /// If that doesn't cover it, make something to cover it. /// - public void InternalAssignMind(EntityUid uid, Mind value, MindComponent? mind = null) + private void InternalAssignMind(EntityUid uid, Mind value, MindContainerComponent? mind = null) { if (!Resolve(uid, ref mind)) return; @@ -51,10 +79,10 @@ public sealed class MindSystem : EntitySystem /// /// Don't call this unless you know what the hell you're doing. - /// Use instead. + /// Use instead. /// If that doesn't cover it, make something to cover it. /// - public void InternalEjectMind(EntityUid uid, MindComponent? mind = null) + private void InternalEjectMind(EntityUid uid, MindContainerComponent? mind = null) { if (!Resolve(uid, ref mind, false)) return; @@ -63,109 +91,519 @@ public sealed class MindSystem : EntitySystem mind.Mind = null; } - private void OnShutdown(EntityUid uid, MindComponent mind, ComponentShutdown args) + private void OnShutdown(EntityUid uid, MindContainerComponent mindContainerComp, ComponentShutdown args) { // Let's not create ghosts if not in the middle of the round. if (_gameTicker.RunLevel != GameRunLevel.InRound) return; - if (mind.HasMind) + if (!TryGetMind(uid, out var mind, mindContainerComp)) + return; + + if (mind.VisitingEntity is {Valid: true} visiting) { - if (mind.Mind?.VisitingEntity is {Valid: true} visiting) + if (TryComp(visiting, out GhostComponent? ghost)) { - if (TryComp(visiting, out GhostComponent? ghost)) + _ghostSystem.SetCanReturnToBody(ghost, false); + } + + TransferTo(mind, visiting); + } + else if (mindContainerComp.GhostOnShutdown) + { + // 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 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.InRound) + 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)) { - _ghostSystem.SetCanReturnToBody(ghost, false); + spawnPosition = _gameTicker.GetObserverSpawnPoint(); } - mind.Mind!.TransferTo(visiting); - } - else if (mind.GhostOnShutdown) - { - // Changing an entities parents while deleting is VERY sus. This WILL throw exceptions. - // TODO: just find the applicable spawn position dirctly without actually updating the transform's parent. - Transform(uid).AttachToGridOrMap(); - var spawnPosition = Transform(uid).Coordinates; - - // Use a regular timer here because the entity has probably been deleted. - Timer.Spawn(0, () => + // TODO refactor observer spawning. + // please. + if (!spawnPosition.IsValid(EntityManager)) { - // Make extra sure the round didn't end between spawning the timer and it being executed. - if (_gameTicker.RunLevel != GameRunLevel.InRound) - return; + // 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(mind, null); + 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)) - { - spawnPosition = _gameTicker.GetObserverSpawnPoint(); - } + var ghost = Spawn("MobObserver", spawnPosition); + var ghostComponent = Comp(ghost); + _ghostSystem.SetCanReturnToBody(ghostComponent, false); - // 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. - Logger.WarningS("mind", $"Entity \"{ToPrettyString(uid)}\" for {mind.Mind?.CharacterName} was deleted, and no applicable spawn location is available."); - mind.Mind?.TransferTo(null); - return; - } + // 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)}\"."); - var ghost = Spawn("MobObserver", spawnPosition); - var ghostComponent = Comp(ghost); - _ghostSystem.SetCanReturnToBody(ghostComponent, false); - - // Log these to make sure they're not causing the GameTicker round restart bugs... - Logger.DebugS("mind", $"Entity \"{ToPrettyString(uid)}\" for {mind.Mind?.CharacterName} was deleted, spawned \"{ToPrettyString(ghost)}\"."); - - if (mind.Mind == null) - return; - - var val = mind.Mind.CharacterName ?? string.Empty; - MetaData(ghost).EntityName = val; - mind.Mind.TransferTo(ghost); - }); - } + var val = mind.CharacterName ?? string.Empty; + MetaData(ghost).EntityName = val; + TransferTo(mind, ghost); + }); } } - private void OnExamined(EntityUid uid, MindComponent mind, ExaminedEvent args) + private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, ExaminedEvent args) { - if (!mind.ShowExamineInfo || !args.IsInDetailsRange) - { + if (!mindContainer.ShowExamineInfo || !args.IsInDetailsRange) return; - } - var dead = TryComp(uid, out var state) && _mobStateSystem.IsDead(uid, state); + var dead = _mobStateSystem.IsDead(uid); + var hasSession = mindContainer.Mind?.Session; - if (dead) - { - if (mind.Mind?.Session == null) { - // Player has no session attached and dead - args.PushMarkup($"[color=yellow]{Loc.GetString("mind-component-no-mind-and-dead-text", ("ent", uid))}[/color]"); - } else { - // Player is dead with session - args.PushMarkup($"[color=red]{Loc.GetString("comp-mind-examined-dead", ("ent", uid))}[/color]"); - } - } - else if (!mind.HasMind) - { + if (dead && hasSession == null) + args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-dead-and-ssd", ("ent", uid))}[/color]"); + else if (dead) + args.PushMarkup($"[color=red]{Loc.GetString("comp-mind-examined-dead", ("ent", uid))}[/color]"); + else if (!mindContainer.HasMind) args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-catatonic", ("ent", uid))}[/color]"); - } - else if (mind.Mind?.Session == null) - { + else if (hasSession == null) args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]"); - } } - private void OnSuicide(EntityUid uid, MindComponent component, SuicideEvent args) + private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args) { if (args.Handled) return; - if (component.HasMind && component.Mind!.PreventSuicide) + if (component.HasMind && component.Mind.PreventSuicide) { args.BlockSuicideAttempt(true); } } + + public Mind? GetMind(EntityUid uid, MindContainerComponent? mind = null) + { + if (!Resolve(uid, ref mind)) + return null; + + if (mind.HasMind) + return mind.Mind; + return null; + } + + public Mind CreateMind(NetUserId? userId, string? name = null) + { + var mind = new Mind(userId); + mind.CharacterName = name; + ChangeOwningPlayer(mind, userId); + return mind; + } + + /// + /// True if the OwnedEntity of this mind is physically dead. + /// This specific definition, as opposed to CharacterDeadIC, is used to determine if ghosting should allow return. + /// + public bool IsCharacterDeadPhysically(Mind mind) + { + // This is written explicitly so that the logic can be understood. + // But it's also weird and potentially situational. + // Specific considerations when updating this: + // + Does being turned into a borg (if/when implemented) count as dead? + // *If not, add specific conditions to users of this property where applicable.* + // + Is being transformed into a donut 'dead'? + // TODO: Consider changing the way ghost roles work. + // Mind is an *IC* mind, therefore ghost takeover is IC revival right now. + // + Is it necessary to have a reference to a specific 'mind iteration' to cycle when certain events happen? + // (If being a borg or AI counts as dead, then this is highly likely, as it's still the same Mind for practical purposes.) + + if (mind.OwnedEntity == null) + return true; + + // This can be null if they're deleted (spike / brain nom) + var targetMobState = EntityManager.GetComponentOrNull(mind.OwnedEntity); + // This can be null if it's a brain (this happens very often) + // Brains are the result of gibbing so should definitely count as dead + if (targetMobState == null) + return true; + // They might actually be alive. + return _mobStateSystem.IsDead(mind.OwnedEntity.Value, targetMobState); + } + + public void Visit(Mind mind, EntityUid entity) + { + if (mind.VisitingEntity != null) + { + Log.Error($"Attempted to visit an entity ({ToPrettyString(entity)}) while already visiting another ({ToPrettyString(mind.VisitingEntity.Value)})."); + return; + } + + if (HasComp(entity)) + { + Log.Error($"Attempted to visit an entity that already has a visiting mind. Entity: {ToPrettyString(entity)}"); + return; + } + + mind.Session?.AttachToEntity(entity); + mind.VisitingEntity = entity; + + // EnsureComp instead of AddComp to deal with deferred deletions. + var comp = EnsureComp(entity); + comp.Mind = mind; + Log.Info($"Session {mind.Session?.Name} visiting entity {entity}."); + } + + /// + /// Returns the mind to its original entity. + /// + public void UnVisit(Mind? mind) + { + if (mind == null || mind.VisitingEntity == null) + return; + + DebugTools.Assert(mind.VisitingEntity != mind.OwnedEntity); + RemoveVisitingEntity(mind); + + if (mind.Session == null || mind.Session.AttachedEntity == mind.VisitingEntity) + return; + + var owned = mind.OwnedEntity; + mind.Session.AttachToEntity(owned); + + if (owned.HasValue) + { + _adminLogger.Add(LogType.Mind, LogImpact.Low, + $"{mind.Session.Name} returned to {ToPrettyString(owned.Value)}"); + } + } + + /// + /// Cleans up the VisitingEntity. + /// + /// + private void RemoveVisitingEntity(Mind mind) + { + if (mind.VisitingEntity == null) + return; + + var oldVisitingEnt = mind.VisitingEntity.Value; + // Null this before removing the component to avoid any infinite loops. + mind.VisitingEntity = null; + + if (TryComp(oldVisitingEnt, out VisitingMindComponent? visitComp)) + { + visitComp.Mind = null; + RemCompDeferred(oldVisitingEnt, visitComp); + } + + RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true); + } + + /// + /// Transfer this mind's control over to a new entity. + /// + /// The mind to transfer + /// + /// The entity to control. + /// Can be null, in which case it will simply detach the mind from any entity. + /// + /// + /// If true, skips ghost check for Visiting Entity + /// + /// + /// Thrown if is already owned by another mind. + /// + 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; + + if (entity != null) + { + if (!TryComp(entity.Value, out component)) + { + component = AddComp(entity.Value); + } + else if (component.HasMind) + { + _gameTicker.OnGhostAttempt(component.Mind, false); + } + + if (TryComp(entity.Value, out var actor)) + { + // Happens when transferring to your currently visited entity. + if (actor.PlayerSession != mind.Session) + { + throw new ArgumentException("Visit target already has a session.", nameof(entity)); + } + + alreadyAttached = true; + } + } + + var oldComp = mind.OwnedComponent; + var oldEntity = mind.OwnedEntity; + if(oldComp != null && oldEntity != null) + InternalEjectMind(oldEntity.Value, oldComp); + + SetOwnedEntity(mind, entity, component); + if (mind.OwnedComponent != null) + InternalAssignMind(mind.OwnedEntity!.Value, mind, mind.OwnedComponent); + + // Don't do the full deletion cleanup if we're transferring to our VisitingEntity + if (alreadyAttached) + { + // Set VisitingEntity null first so the removal of VisitingMind doesn't get through Unvisit() and delete what we're visiting. + // Yes this control flow sucks. + mind.VisitingEntity = null; + RemComp(entity!.Value); + } + else if (mind.VisitingEntity != null + && (ghostCheckOverride // to force mind transfer, for example from ControlMobVerb + || !TryComp(mind.VisitingEntity!, out GhostComponent? ghostComponent) // visiting entity is not a Ghost + || !ghostComponent.CanReturnToBody)) // it is a ghost, but cannot return to body anyway, so it's okay + { + RemoveVisitingEntity(mind); + } + + // Player is CURRENTLY connected. + if (mind.Session != null && !alreadyAttached && mind.VisitingEntity == null) + { + mind.Session.AttachToEntity(entity); + Log.Info($"Session {mind.Session.Name} transferred to entity {entity}."); + } + } + + 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); + } + + /// + /// Adds an objective to this mind. + /// + public bool TryAddObjective(Mind mind, ObjectivePrototype objectivePrototype) + { + if (!objectivePrototype.CanBeAssigned(mind)) + return false; + var objective = objectivePrototype.GetObjective(mind); + if (mind.Objectives.Contains(objective)) + return false; + + foreach (var condition in objective.Conditions) + { + _adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' added to mind of {MindOwnerLoggingString(mind)}"); + } + + + mind.Objectives.Add(objective); + return true; + } + + /// + /// Removes an objective to this mind. + /// + /// Returns true if the removal succeeded. + public bool TryRemoveObjective(Mind mind, int index) + { + if (mind.Objectives.Count >= index) return false; + + var objective = mind.Objectives[index]; + + foreach (var condition in objective.Conditions) + { + _adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' removed from the mind of {MindOwnerLoggingString(mind)}"); + } + + mind.Objectives.Remove(objective); + return true; + } + + /// + /// Gives this mind a new role. + /// + /// The mind to add the role to. + /// The type of the role to give. + /// The instance of the role. + /// + /// Thrown if we already have a role with this type. + /// + public void AddRole(Mind mind, Role role) + { + if (mind.Roles.Contains(role)) + { + throw new ArgumentException($"We already have this role: {role}"); + } + + mind.Roles.Add(role); + role.Greet(); + + var message = new RoleAddedEvent(mind, role); + if (mind.OwnedEntity != null) + { + RaiseLocalEvent(mind.OwnedEntity.Value, message, true); + } + + _adminLogger.Add(LogType.Mind, LogImpact.Low, + $"'{role.Name}' added to mind of {MindOwnerLoggingString(mind)}"); + } + + /// + /// Removes a role from this mind. + /// + /// The mind to remove the role from. + /// The type of the role to remove. + /// + /// Thrown if we do not have this role. + /// + public void RemoveRole(Mind mind, Role role) + { + if (!mind.Roles.Contains(role)) + { + throw new ArgumentException($"We do not have this role: {role}"); + } + + mind.Roles.Remove(role); + + var message = new RoleRemovedEvent(mind, role); + + if (mind.OwnedEntity != null) + { + RaiseLocalEvent(mind.OwnedEntity.Value, message, true); + } + _adminLogger.Add(LogType.Mind, LogImpact.Low, + $"'{role.Name}' removed from mind of {MindOwnerLoggingString(mind)}"); + } + + public bool HasRole(Mind mind) where T : Role + { + return mind.Roles.Any(role => role is T); + } + + public bool TryGetSession(Mind mind, [NotNullWhen(true)] out IPlayerSession? session) + { + return (session = mind.Session) != null; + } + + /// + /// Gets a mind from uid and/or MindContainerComponent. Used for null checks. + /// + /// Entity UID that owns the mind. + /// The returned mind. + /// Mind component on to get the mind from. + /// True if mind found. False if not. + public bool TryGetMind(EntityUid uid, [NotNullWhen(true)] out Mind? mind, MindContainerComponent? mindContainerComponent = null) + { + mind = null; + if (!Resolve(uid, ref mindContainerComponent)) + return false; + + if (!mindContainerComponent.HasMind) + return false; + + mind = mindContainerComponent.Mind; + return true; + } + + /// + /// Sets the Mind's OwnedComponent and OwnedEntity + /// + /// Mind to set OwnedComponent and OwnedEntity on + /// Entity owned by + /// MindContainerComponent owned by + private void SetOwnedEntity(Mind mind, EntityUid? uid, MindContainerComponent? mindContainerComponent) + { + if (uid != null) + Resolve(uid.Value, ref mindContainerComponent); + + mind.OwnedEntity = uid; + mind.OwnedComponent = mindContainerComponent; + } + + /// + /// Sets the Mind's UserId and Session + /// + /// + /// + private void SetUserId(Mind mind, NetUserId? userId) + { + mind.UserId = userId; + + if (!userId.HasValue) + return; + + _playerManager.TryGetSessionById(userId.Value, out var ret); + mind.Session = ret; + } + + /// + /// True if this Mind is 'sufficiently dead' IC (Objectives, EndText). + /// Note that this is *IC logic*, it's not necessarily tied to any specific truth. + /// "If administrators decide that zombies are dead, this returns true for zombies." + /// (Maybe you were looking for the action blocker system?) + /// + public bool IsCharacterDeadIc(Mind mind) + { + return IsCharacterDeadPhysically(mind); + } + + /// + /// A string to represent the mind for logging + /// + private string MindOwnerLoggingString(Mind mind) + { + if (mind.OwnedEntity != null) + return ToPrettyString(mind.OwnedEntity.Value); + if (mind.UserId != null) + return mind.UserId.Value.ToString(); + return "(originally " + mind.OriginalOwnerUserId + ")"; + } } diff --git a/Content.Server/Mind/MindTrackerSystem.cs b/Content.Server/Mind/MindTrackerSystem.cs index 49f2400b08..2ab76ce9af 100644 --- a/Content.Server/Mind/MindTrackerSystem.cs +++ b/Content.Server/Mind/MindTrackerSystem.cs @@ -20,7 +20,7 @@ namespace Content.Server.Mind base.Initialize(); SubscribeLocalEvent(Reset); - SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnMindAdded); } void Reset(RoundRestartCleanupEvent ev) @@ -28,7 +28,7 @@ namespace Content.Server.Mind AllMinds.Clear(); } - void OnMindAdded(EntityUid uid, MindComponent mc, MindAddedMessage args) + void OnMindAdded(EntityUid uid, MindContainerComponent mc, MindAddedMessage args) { var mind = mc.Mind; if (mind != null) diff --git a/Content.Server/Mind/TransferMindOnGibSystem.cs b/Content.Server/Mind/TransferMindOnGibSystem.cs index 1aed10c324..d499d6a4dd 100644 --- a/Content.Server/Mind/TransferMindOnGibSystem.cs +++ b/Content.Server/Mind/TransferMindOnGibSystem.cs @@ -15,6 +15,7 @@ public sealed class TransferMindOnGibSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; /// public override void Initialize() @@ -24,7 +25,7 @@ public sealed class TransferMindOnGibSystem : EntitySystem private void OnGib(EntityUid uid, TransferMindOnGibComponent component, BeingGibbedEvent args) { - if (!TryComp(uid, out var mindcomp) || mindcomp.Mind == null) + if (!TryComp(uid, out var mindcomp) || mindcomp.Mind == null) return; var validParts = args.GibbedParts.Where(p => _tag.HasTag(p, component.TargetTag)).ToHashSet(); @@ -32,6 +33,6 @@ public sealed class TransferMindOnGibSystem : EntitySystem return; var ent = _random.Pick(validParts); - mindcomp.Mind.TransferTo(ent); + _mindSystem.TransferTo(mindcomp.Mind, ent); } } diff --git a/Content.Server/Objectives/Commands/AddObjectiveCommand.cs b/Content.Server/Objectives/Commands/AddObjectiveCommand.cs index ea4372b1aa..7353763b63 100644 --- a/Content.Server/Objectives/Commands/AddObjectiveCommand.cs +++ b/Content.Server/Objectives/Commands/AddObjectiveCommand.cs @@ -1,4 +1,5 @@ using Content.Server.Administration; +using Content.Server.Mind; using Content.Server.Players; using Content.Shared.Administration; using Robust.Server.Player; @@ -10,6 +11,8 @@ namespace Content.Server.Objectives.Commands [AdminCommand(AdminFlags.Admin)] public sealed class AddObjectiveCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _entityManager = default!; + public string Command => "addobjective"; public string Description => "Adds an objective to the player's mind."; public string Help => "addobjective "; @@ -42,8 +45,10 @@ namespace Content.Server.Objectives.Commands shell.WriteLine($"Can't find matching ObjectivePrototype {objectivePrototype}"); return; } + + var mindSystem = _entityManager.System(); - if (!mind.TryAddObjective(objectivePrototype)) + if (!mindSystem.TryAddObjective(mind, objectivePrototype)) { shell.WriteLine("Objective requirements dont allow that objective to be added."); } diff --git a/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs b/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs index 96979ab6fa..c0846a1e43 100644 --- a/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs +++ b/Content.Server/Objectives/Commands/RemoveObjectiveCommand.cs @@ -1,4 +1,5 @@ using Content.Server.Administration; +using Content.Server.Mind; using Content.Server.Players; using Content.Shared.Administration; using Robust.Server.Player; @@ -9,6 +10,8 @@ namespace Content.Server.Objectives.Commands [AdminCommand(AdminFlags.Admin)] public sealed class RemoveObjectiveCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _entityManager = default!; + public string Command => "rmobjective"; public string Description => "Removes an objective from the player's mind."; public string Help => "rmobjective "; @@ -32,7 +35,8 @@ namespace Content.Server.Objectives.Commands if (int.TryParse(args[1], out var i)) { - shell.WriteLine(mind.TryRemoveObjective(i) + var mindSystem = _entityManager.System(); + shell.WriteLine(mindSystem.TryRemoveObjective(mind, i) ? "Objective successfully removed!" : "Objective removing failed. Maybe the index is out of bounds? Check lsobjectives!"); } diff --git a/Content.Server/Objectives/Conditions/DieCondition.cs b/Content.Server/Objectives/Conditions/DieCondition.cs index cc5ddd9abf..5a63147ad4 100644 --- a/Content.Server/Objectives/Conditions/DieCondition.cs +++ b/Content.Server/Objectives/Conditions/DieCondition.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Content.Server.Objectives.Interfaces; using JetBrains.Annotations; using Robust.Shared.Utility; @@ -21,7 +22,15 @@ namespace Content.Server.Objectives.Conditions public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Mobs/Ghosts/ghost_human.rsi"), "icon"); - public float Progress => (_mind?.CharacterDeadIC ?? true) ? 1f : 0f; + public float Progress + { + get + { + var entityManager = IoCManager.Resolve(); + var mindSystem = entityManager.System(); + return _mind == null || mindSystem.IsCharacterDeadIc(_mind) ? 1f : 0f; + } + } public float Difficulty => 0.5f; diff --git a/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs b/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs index ff475e706b..a41d891ff9 100644 --- a/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs +++ b/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Content.Server.Objectives.Interfaces; using Content.Server.Shuttles.Components; using Content.Server.Station.Components; @@ -47,13 +48,14 @@ namespace Content.Server.Objectives.Conditions { get { var entMan = IoCManager.Resolve(); + var mindSystem = entMan.System(); if (_mind?.OwnedEntity == null || !entMan.TryGetComponent(_mind.OwnedEntity, out var xform)) return 0f; var shuttleContainsAgent = false; - var agentIsAlive = !_mind.CharacterDeadIC; + var agentIsAlive = !mindSystem.IsCharacterDeadIc(_mind); var agentIsEscaping = true; if (entMan.TryGetComponent(_mind.OwnedEntity, out var cuffed) diff --git a/Content.Server/Objectives/Conditions/KillPersonCondition.cs b/Content.Server/Objectives/Conditions/KillPersonCondition.cs index a3a057961b..cdfd25174e 100644 --- a/Content.Server/Objectives/Conditions/KillPersonCondition.cs +++ b/Content.Server/Objectives/Conditions/KillPersonCondition.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Content.Server.Objectives.Interfaces; using Content.Shared.Mobs.Systems; using Robust.Shared.Utility; @@ -32,7 +33,15 @@ namespace Content.Server.Objectives.Conditions public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Weapons/Guns/Pistols/viper.rsi"), "icon"); - public float Progress => (Target?.CharacterDeadIC ?? true) ? 1f : 0f; + public float Progress + { + get + { + var entityManager = IoCManager.Resolve(); + var mindSystem = entityManager.System(); + return Target == null || mindSystem.IsCharacterDeadIc(Target) ? 1f : 0f; + } + } public float Difficulty => 2f; diff --git a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs b/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs index 5cdef407dd..d84b75fff3 100644 --- a/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs +++ b/Content.Server/Objectives/Conditions/KillRandomPersonCondition.cs @@ -13,7 +13,7 @@ namespace Content.Server.Objectives.Conditions { public override IObjectiveCondition GetAssigned(Mind.Mind mind) { - var allHumans = EntityManager.EntityQuery(true).Where(mc => + var allHumans = EntityManager.EntityQuery(true).Where(mc => { var entity = mc.Mind?.OwnedEntity; diff --git a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs index 0fd1d53a09..ddd6f95b5a 100644 --- a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs +++ b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs @@ -3,6 +3,7 @@ using Content.Server.Objectives.Interfaces; using Robust.Shared.Random; using Robust.Shared.Utility; using Content.Server.GameTicking.Rules; +using Content.Server.Mind; namespace Content.Server.Objectives.Conditions { @@ -42,7 +43,15 @@ namespace Content.Server.Objectives.Conditions public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Misc/bureaucracy.rsi"), "folder-white"); - public float Progress => (!_target?.CharacterDeadIC ?? true) ? 1f : 0f; + public float Progress + { + get + { + var entityManager = IoCManager.Resolve(); + var mindSystem = entityManager.System(); + return _target == null || mindSystem.IsCharacterDeadIc(_target) ? 1f : 0f; + } + } public float Difficulty => 1.75f; diff --git a/Content.Server/Objectives/Requirements/TraitorRequirement.cs b/Content.Server/Objectives/Requirements/TraitorRequirement.cs index 1a7cd816ff..5ddcaa673d 100644 --- a/Content.Server/Objectives/Requirements/TraitorRequirement.cs +++ b/Content.Server/Objectives/Requirements/TraitorRequirement.cs @@ -1,6 +1,5 @@ -using Content.Server.Objectives.Interfaces; -using Content.Server.Roles; -using Content.Server.Traitor; +using Content.Server.Mind; +using Content.Server.Objectives.Interfaces; using JetBrains.Annotations; using TraitorRole = Content.Server.Roles.TraitorRole; @@ -12,7 +11,9 @@ namespace Content.Server.Objectives.Requirements { public bool CanBeAssigned(Mind.Mind mind) { - return mind.HasRole(); + var entityManager = IoCManager.Resolve(); + var mindSystem = entityManager.System(); + return mindSystem.HasRole(mind); } } } diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs index 016a569dd9..69496afe5d 100644 --- a/Content.Server/PAI/PAISystem.cs +++ b/Content.Server/PAI/PAISystem.cs @@ -32,7 +32,7 @@ namespace Content.Server.PAI { if (args.IsInDetailsRange) { - if (EntityManager.TryGetComponent(uid, out var mind) && mind.HasMind) + if (EntityManager.TryGetComponent(uid, out var mind) && mind.HasMind) { args.PushMarkup(Loc.GetString("pai-system-pai-installed")); } @@ -57,7 +57,7 @@ namespace Content.Server.PAI args.Handled = true; // Check for pAI activation - if (EntityManager.TryGetComponent(uid, out var mind) && mind.HasMind) + if (EntityManager.TryGetComponent(uid, out var mind) && mind.HasMind) { _popupSystem.PopupEntity(Loc.GetString("pai-system-pai-installed"), uid, args.User, PopupType.Large); return; @@ -136,7 +136,7 @@ namespace Content.Server.PAI if (!args.CanAccess || !args.CanInteract) return; - if (EntityManager.TryGetComponent(uid, out var mind) && mind.HasMind) + if (EntityManager.TryGetComponent(uid, out var mind) && mind.HasMind) { ActivationVerb verb = new(); verb.Text = Loc.GetString("pai-system-wipe-device-verb-text"); @@ -146,9 +146,9 @@ namespace Content.Server.PAI // Wiping device :( // The shutdown of the Mind should cause automatic reset of the pAI during OnMindRemoved // EDIT: But it doesn't!!!! Wtf? Do stuff manually - if (EntityManager.HasComponent(uid)) + if (EntityManager.HasComponent(uid)) { - EntityManager.RemoveComponent(uid); + EntityManager.RemoveComponent(uid); _popupSystem.PopupEntity(Loc.GetString("pai-system-wiped-device"), uid, args.User, PopupType.Large); PAITurningOff(uid); } diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index b76f20d501..eaf7b96ecc 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -4,6 +4,7 @@ using Content.Server.DeviceNetwork.Components; using Content.Server.Instruments; using Content.Server.Light.EntitySystems; using Content.Server.Light.Events; +using Content.Server.Mind; using Content.Server.PDA.Ringer; using Content.Server.Station.Systems; using Content.Server.Store.Components; @@ -25,6 +26,7 @@ namespace Content.Server.PDA [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; public override void Initialize() { diff --git a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs index 0af4bbe474..8d6ddac4a9 100644 --- a/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs +++ b/Content.Server/ParticleAccelerator/EntitySystems/ParticleAcceleratorSystem.ControlBox.cs @@ -69,8 +69,8 @@ public sealed partial class ParticleAcceleratorSystem if (comp.Enabled || !comp.CanBeEnabled) return; - if (HasComp(user?.AttachedEntity)) - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{EntityManager.ToPrettyString((EntityUid) user!.AttachedEntity):player} has turned {EntityManager.ToPrettyString(uid)} on"); + if (user?.AttachedEntity is {} player) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} has turned {ToPrettyString(uid)} on"); comp.Enabled = true; UpdatePowerDraw(uid, comp); @@ -89,8 +89,8 @@ public sealed partial class ParticleAcceleratorSystem if (!comp.Enabled) return; - if (HasComp(user?.AttachedEntity)) - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{EntityManager.ToPrettyString((EntityUid) user!.AttachedEntity):player} has turned {EntityManager.ToPrettyString(uid)} off"); + if (user?.AttachedEntity is {} player) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} has turned {ToPrettyString(uid)} off"); comp.Enabled = false; UpdatePowerDraw(uid, comp); @@ -146,7 +146,7 @@ public sealed partial class ParticleAcceleratorSystem if (strength == comp.SelectedStrength) return; - if (HasComp(user?.AttachedEntity)) + if (user?.AttachedEntity is {} player) { var impact = strength switch { @@ -158,7 +158,7 @@ public sealed partial class ParticleAcceleratorSystem or _ => LogImpact.Extreme, }; - _adminLogger.Add(LogType.Action, impact, $"{EntityManager.ToPrettyString(user!.AttachedEntity!.Value):player} has set the strength of {EntityManager.ToPrettyString(uid)} to {strength}"); + _adminLogger.Add(LogType.Action, impact, $"{ToPrettyString(player):player} has set the strength of {ToPrettyString(uid)} to {strength}"); } comp.SelectedStrength = strength; diff --git a/Content.Server/Players/PlayerData.cs b/Content.Server/Players/PlayerData.cs index f811dfee76..c8e368cfed 100644 --- a/Content.Server/Players/PlayerData.cs +++ b/Content.Server/Players/PlayerData.cs @@ -1,3 +1,4 @@ +using Content.Server.Mind; using Robust.Server.Player; using Robust.Shared.Network; @@ -37,9 +38,15 @@ namespace Content.Server.Players public void WipeMind() { - Mind?.TransferTo(null); + var entityManager = IoCManager.Resolve(); + var mindSystem = entityManager.System(); + // This will ensure Mind == null - Mind?.ChangeOwningPlayer(null); + if (Mind == null) + return; + + mindSystem.TransferTo(Mind, null); + mindSystem.ChangeOwningPlayer(Mind, null); } /// diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index f0368aedce..7b7e2a0a8c 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Actions; using Content.Server.Humanoid; using Content.Server.Inventory; +using Content.Server.Mind; using Content.Server.Mind.Commands; using Content.Server.Mind.Components; using Content.Server.Nutrition; @@ -41,6 +42,7 @@ namespace Content.Server.Polymorph.Systems [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; private ISawmill _sawmill = default!; @@ -230,8 +232,8 @@ namespace Content.Server.Polymorph.Systems _humanoid.CloneAppearance(uid, child); } - if (TryComp(uid, out var mind) && mind.Mind != null) - mind.Mind.TransferTo(child); + if (_mindSystem.TryGetMind(uid, out var mind)) + _mindSystem.TransferTo(mind, child); //Ensures a map to banish the entity to EnsurePausesdMap(); @@ -304,10 +306,8 @@ namespace Content.Server.Polymorph.Systems } } - if (TryComp(uid, out var mind) && mind.Mind != null) - { - mind.Mind.TransferTo(parent); - } + if (_mindSystem.TryGetMind(uid, out var mind)) + _mindSystem.TransferTo(mind, parent); // if an item polymorph was picked up, put it back down after reverting Transform(parent).AttachToGridOrMap(); diff --git a/Content.Server/Revenant/EntitySystems/EssenceSystem.cs b/Content.Server/Revenant/EntitySystems/EssenceSystem.cs index a466573344..5f95da95e8 100644 --- a/Content.Server/Revenant/EntitySystems/EssenceSystem.cs +++ b/Content.Server/Revenant/EntitySystems/EssenceSystem.cs @@ -67,7 +67,7 @@ public sealed class EssenceSystem : EntitySystem switch (mob.CurrentState) { case MobState.Alive: - if (TryComp(uid, out var mind) && mind.Mind != null) + if (TryComp(uid, out var mind) && mind.Mind != null) component.EssenceAmount = _random.NextFloat(75f, 100f); else component.EssenceAmount = _random.NextFloat(45f, 70f); diff --git a/Content.Server/Roles/AddRoleCommand.cs b/Content.Server/Roles/AddRoleCommand.cs index 2240d091da..b172873fdf 100644 --- a/Content.Server/Roles/AddRoleCommand.cs +++ b/Content.Server/Roles/AddRoleCommand.cs @@ -6,12 +6,15 @@ using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.Prototypes; using System.Linq; +using Content.Server.Mind; namespace Content.Server.Roles { [AdminCommand(AdminFlags.Admin)] public sealed class AddRoleCommand : IConsoleCommand { + [Dependency] private readonly EntityManager _entityManager = default!; + public string Command => "addrole"; public string Description => "Adds a role to a player's mind."; @@ -54,7 +57,8 @@ namespace Content.Server.Roles } var role = new Job(mind, jobPrototype); - mind.AddRole(role); + var mindSystem = _entityManager.System(); + mindSystem.AddRole(mind, role); } } } diff --git a/Content.Server/Roles/Job.cs b/Content.Server/Roles/Job.cs index cd81dea484..c5c7403600 100644 --- a/Content.Server/Roles/Job.cs +++ b/Content.Server/Roles/Job.cs @@ -2,6 +2,7 @@ using Content.Server.Chat.Managers; using Content.Server.Chat.Systems; using Content.Shared.Roles; using System.Globalization; +using Content.Server.Mind; namespace Content.Server.Roles { @@ -35,8 +36,11 @@ namespace Content.Server.Roles public override void Greet() { base.Greet(); + + var entityManager = IoCManager.Resolve(); + var mindSystem = entityManager.System(); - if (Mind.TryGetSession(out var session)) + if (mindSystem.TryGetSession(Mind, out var session)) { var chatMgr = IoCManager.Resolve(); chatMgr.DispatchServerMessage(session, Loc.GetString("job-greet-introduce-job-name", diff --git a/Content.Server/Roles/RemoveRoleCommand.cs b/Content.Server/Roles/RemoveRoleCommand.cs index ae0a0f8f4a..00b6492404 100644 --- a/Content.Server/Roles/RemoveRoleCommand.cs +++ b/Content.Server/Roles/RemoveRoleCommand.cs @@ -1,4 +1,5 @@ using Content.Server.Administration; +using Content.Server.Mind; using Content.Server.Players; using Content.Shared.Administration; using Content.Shared.Roles; @@ -12,6 +13,7 @@ namespace Content.Server.Roles public sealed class RemoveRoleCommand : IConsoleCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; public string Command => "rmrole"; @@ -43,7 +45,8 @@ namespace Content.Server.Roles } var role = new Job(mind, _prototypeManager.Index(args[1])); - mind.RemoveRole(role); + var mindSystem = _entityManager.System(); + mindSystem.RemoveRole(mind, role); } } } diff --git a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs index 28e77941dd..45854a88af 100644 --- a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs @@ -132,7 +132,7 @@ public sealed class EventHorizonSystem : SharedEventHorizonSystem var eventHorizonOwner = eventHorizon.Owner; if (!EntityManager.IsQueuedForDeletion(uid) && // I saw it log twice a few times for some reason? - (HasComp(uid) || + (HasComp(uid) || _tagSystem.HasTag(uid, "HighRiskItem") || HasComp(uid))) _adminLogger.Add(LogType.EntityDelete, LogImpact.Extreme, $"{ToPrettyString(uid)} entered the event horizon of {ToPrettyString(eventHorizonOwner)} and was deleted"); diff --git a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs index 1e561a3606..9e82571c99 100644 --- a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs +++ b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs @@ -13,7 +13,7 @@ public sealed class MassHallucinationsRule : StationEventSystem(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out var ent, out _)) { if (!HasComp(ent)) diff --git a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs index 5050b6dd01..6f57da1658 100644 --- a/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs +++ b/Content.Server/Storage/EntitySystems/BluespaceLockerSystem.cs @@ -86,7 +86,7 @@ public sealed class BluespaceLockerSystem : EntitySystem if (component.BehaviorProperties.TransportEntities || component.BehaviorProperties.TransportSentient) foreach (var entity in target.Value.storageComponent.Contents.ContainedEntities.ToArray()) { - if (EntityManager.HasComponent(entity)) + if (EntityManager.HasComponent(entity)) { if (!component.BehaviorProperties.TransportSentient) continue; @@ -297,7 +297,7 @@ public sealed class BluespaceLockerSystem : EntitySystem if (component.BehaviorProperties.TransportEntities || component.BehaviorProperties.TransportSentient) foreach (var entity in entityStorageComponent.Contents.ContainedEntities.ToArray()) { - if (EntityManager.HasComponent(entity)) + if (EntityManager.HasComponent(entity)) { if (!component.BehaviorProperties.TransportSentient) continue; diff --git a/Content.Server/Store/Conditions/BuyerAntagCondition.cs b/Content.Server/Store/Conditions/BuyerAntagCondition.cs index fcf02b73c0..6fb346d2c3 100644 --- a/Content.Server/Store/Conditions/BuyerAntagCondition.cs +++ b/Content.Server/Store/Conditions/BuyerAntagCondition.cs @@ -29,7 +29,7 @@ public sealed class BuyerAntagCondition : ListingCondition { var ent = args.EntityManager; - if (!ent.TryGetComponent(args.Buyer, out var mind) || mind.Mind == null) + if (!ent.TryGetComponent(args.Buyer, out var mind) || mind.Mind == null) return true; if (Blacklist != null) diff --git a/Content.Server/Store/Conditions/BuyerJobCondition.cs b/Content.Server/Store/Conditions/BuyerJobCondition.cs index d867a65d33..b34b6a320b 100644 --- a/Content.Server/Store/Conditions/BuyerJobCondition.cs +++ b/Content.Server/Store/Conditions/BuyerJobCondition.cs @@ -28,7 +28,7 @@ public sealed class BuyerJobCondition : ListingCondition { var ent = args.EntityManager; - if (!ent.TryGetComponent(args.Buyer, out var mind) || mind.Mind == null) + if (!ent.TryGetComponent(args.Buyer, out var mind) || mind.Mind == null) return true; //this is for things like surplus crate if (Blacklist != null) diff --git a/Content.Server/Teleportation/PortalSystem.cs b/Content.Server/Teleportation/PortalSystem.cs index 1730a48ac0..dabcefa9b9 100644 --- a/Content.Server/Teleportation/PortalSystem.cs +++ b/Content.Server/Teleportation/PortalSystem.cs @@ -14,7 +14,7 @@ public sealed class PortalSystem : SharedPortalSystem protected override void LogTeleport(EntityUid portal, EntityUid subject, EntityCoordinates source, EntityCoordinates target) { - if (HasComp(subject)) + if (HasComp(subject)) _adminLogger.Add(LogType.Teleport, LogImpact.Low, $"{ToPrettyString(subject):player} teleported via {ToPrettyString(portal)} from {source} to {target}"); } } diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs index 84c12cd858..e05c76e013 100644 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs @@ -121,7 +121,7 @@ public sealed class WorldControllerSystem : EntitySystem } } - var mindEnum = EntityQueryEnumerator(); + var mindEnum = EntityQueryEnumerator(); var ghostQuery = GetEntityQuery(); // Mindful entities get special privilege as they're always a player and we don't want the illusion being broken around them. @@ -275,4 +275,3 @@ public readonly record struct WorldChunkLoadedEvent(EntityUid Chunk, Vector2i Co [ByRefEvent] [PublicAPI] public readonly record struct WorldChunkUnloadedEvent(EntityUid Chunk, Vector2i Coords); - diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs index 9c1fb81e99..a846164025 100644 --- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs +++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs @@ -21,6 +21,7 @@ using Content.Shared.CombatMode; using Content.Shared.Damage; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; +using Content.Server.Mind; using Content.Shared.Humanoid; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; @@ -59,6 +60,7 @@ namespace Content.Server.Zombies [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; public override void Initialize() @@ -205,11 +207,12 @@ namespace Content.Server.Zombies _identity.QueueIdentityUpdate(target); //He's gotta have a mind - var mindcomp = EnsureComp(target); - if (mindcomp.Mind != null && mindcomp.Mind.TryGetSession(out var session)) + var mindComp = EnsureComp(target); + if (_mindSystem.TryGetMind(target, out var mind, mindComp) && _mindSystem.TryGetSession(mind, out var session)) { //Zombie role for player manifest - mindcomp.Mind.AddRole(new ZombieRole(mindcomp.Mind, _proto.Index(zombiecomp.ZombieRoleId))); + _mindSystem.AddRole(mind, new ZombieRole(mind, _proto.Index(zombiecomp.ZombieRoleId))); + //Greeting message for new bebe zombers _chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting")); @@ -217,7 +220,7 @@ namespace Content.Server.Zombies _audioSystem.PlayGlobal(zombiecomp.GreetSoundNotification, session); } - if (!HasComp(target) && !mindcomp.HasMind) //this specific component gives build test trouble so pop off, ig + if (!HasComp(target) && !mindComp.HasMind) //this specific component gives build test trouble so pop off, ig { //yet more hardcoding. Visit zombie.ftl for more information. var ghostRole = EnsureComp(target); diff --git a/Resources/Locale/en-US/mind/components/mind-component.ftl b/Resources/Locale/en-US/mind/components/mind-component.ftl index 0711958b70..7374dcd163 100644 --- a/Resources/Locale/en-US/mind/components/mind-component.ftl +++ b/Resources/Locale/en-US/mind/components/mind-component.ftl @@ -1,13 +1,10 @@ -# MindComponent localization +# MindContainerComponent localization comp-mind-ghosting-prevented = You are not able to ghost right now. ## Messages displayed when a body is examined and in a certain state + comp-mind-examined-catatonic = { CAPITALIZE(SUBJECT($ent)) } { CONJUGATE-BE($ent) } totally catatonic. The stresses of life in deep-space must have been too much for { OBJECT($ent) }. Any recovery is unlikely. comp-mind-examined-dead = { CAPITALIZE(POSS-ADJ($ent)) } soul has departed. comp-mind-examined-ssd = { CAPITALIZE(SUBJECT($ent)) } { CONJUGATE-HAVE($ent) } a blank, absent-minded stare and appears completely unresponsive to anything. { CAPITALIZE(SUBJECT($ent)) } may snap out of it soon. - - -mind-component-no-mind-and-alive-text = { CAPITALIZE(POSS-ADJ($ent)) } is totally catatonic. The stresses of life in deep-space must have been too much for them. Any recovery is unlikely. -mind-component-no-mind-and-dead-text = { CAPITALIZE(POSS-ADJ($ent)) } soul has departed and moved on. Any recovery is unlikely. -mind-component-mind-and-no-session-text = { CAPITALIZE(POSS-ADJ($ent)) } { CONJUGATE-HAVE($ent) } a blank, absent-minded stare and appears completely unresponsive to anything. { CAPITALIZE(POSS-ADJ($ent)) } may snap out of it soon. +comp-mind-examined-dead-and-ssd = { CAPITALIZE(POSS-ADJ($ent)) } soul has departed and moved on. Any recovery is unlikely. diff --git a/Resources/Maps/barratry.yml b/Resources/Maps/barratry.yml index f98af4777a..02c254a84a 100644 --- a/Resources/Maps/barratry.yml +++ b/Resources/Maps/barratry.yml @@ -86536,7 +86536,7 @@ entities: - baseSprintSpeed: 2.5 type: MovementSpeedModifier - type: GhostTakeoverAvailable - - type: Mind + - type: MindContainer - lerpTarget: 0 type: InputMover - type: MobMover diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index ead6519761..19cdba22f4 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -3,7 +3,7 @@ name: revenant description: A spooky ghostie. components: - - type: Mind + - type: MindContainer - type: InputMover - type: MobMover - type: Input diff --git a/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml index 65ab527d73..8d9906b72d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/arachnid.yml @@ -11,7 +11,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/diona.yml b/Resources/Prototypes/Entities/Mobs/Player/diona.yml index 79dfcdcf4b..97c97d0214 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/diona.yml @@ -11,7 +11,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml index 0f9b19ce85..3367fab0e9 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml @@ -10,7 +10,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 219b274a5e..c41a3349a9 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -22,7 +22,7 @@ - type: Access tags: - Chapel - - type: Mind + - type: MindContainer showExamineInfo: true - type: Faction factions: @@ -77,7 +77,7 @@ - type: Access tags: - Chapel - - type: Mind + - type: MindContainer showExamineInfo: true - type: Familiar - type: Vocal diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index c373aee051..9b87414993 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -10,7 +10,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index c973535f4a..7dc497f5ab 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -7,7 +7,7 @@ components: - type: ContentEye maxZoom: 1.44,1.44 - - type: Mind + - type: MindContainer - type: Clickable - type: InteractionOutline - type: Physics diff --git a/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml index 254bd0ff09..c33e09ca44 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml @@ -10,7 +10,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml b/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml index 1ed3442530..48b81bd02e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml @@ -8,7 +8,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/slime.yml b/Resources/Prototypes/Entities/Mobs/Player/slime.yml index d1fa5a268f..03cda8bca8 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/slime.yml @@ -9,7 +9,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" diff --git a/Resources/Prototypes/Entities/Mobs/Player/vox.yml b/Resources/Prototypes/Entities/Mobs/Player/vox.yml index 6862c3d92e..7764c8302e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/vox.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/vox.yml @@ -10,7 +10,7 @@ interactSuccessString: hugging-success-generic interactSuccessSound: /Audio/Effects/thudswoosh.ogg messagePerceivedByOthers: hugging-success-generic-others - - type: Mind + - type: MindContainer showExamineInfo: true - type: Input context: "human" @@ -31,4 +31,3 @@ damageRecovery: types: Asphyxiation: -1.0 -