#nullable enable
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using Content.Server.GameTicking;
+using Content.Server.Players;
+using Content.Shared.Mind;
+using Content.Shared.Players;
+using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
+ public IPlayerSession? Player => (IPlayerSession?) Server.PlayerMan.Sessions.FirstOrDefault();
+ public PlayerData? PlayerData => Player?.Data.ContentData();
+
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
--- /dev/null
+#nullable enable
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Tests.Minds;
+
+[TestFixture]
+public sealed partial class MindTests
+{
+ [Test]
+ public async Task DeleteAllThenGhost()
+ {
+ var settings = new PoolSettings
+ {
+ Dirty = true,
+ DummyTicker = false,
+ Connected = true
+ };
+ await using var pair = await PoolManager.GetServerClient(settings);
+
+ // Client is connected with a valid entity & mind
+ Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
+ Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
+
+ // Delete **everything**
+ var conHost = pair.Server.ResolveDependency<IConsoleHost>();
+ await pair.Server.WaitPost(() => conHost.ExecuteCommand("entities delete"));
+ await pair.RunTicksSync(5);
+
+ Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
+ Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
+
+ // Create a new map.
+ int mapId = 1;
+ await pair.Server.WaitPost(() => conHost.ExecuteCommand($"addmap {mapId}"));
+ await pair.RunTicksSync(5);
+
+ // Client is not attached to anything
+ Assert.Null(pair.Client.Player?.ControlledEntity);
+ Assert.Null(pair.PlayerData?.Mind);
+
+ // Attempt to ghost
+ var cConHost = pair.Client.ResolveDependency<IConsoleHost>();
+ await pair.Client.WaitPost(() => cConHost.ExecuteCommand("ghost"));
+ await pair.RunTicksSync(10);
+
+ // Client should be attached to a ghost placed on the new map.
+ Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
+ Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
+ var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
+ Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
+
+ await pair.CleanReturnAsync();
+ }
+}
var minds = _entities.System<SharedMindSystem>();
if (!minds.TryGetMind(player, out var mindId, out var mind))
{
- shell.WriteLine("You have no Mind, you can't ghost.");
- return;
+ mindId = minds.CreateMind(player.UserId);
+ mind = _entities.GetComponent<MindComponent>(mindId);
}
if (!EntitySystem.Get<GameTicker>().OnGhostAttempt(mindId, true, true, mind))
base.Initialize();
SubscribeLocalEvent<MindContainerComponent, EntityTerminatingEvent>(OnMindContainerTerminating);
+ SubscribeLocalEvent<MindComponent, ComponentShutdown>(OnMindShutdown);
+ }
+
+ private void OnMindShutdown(EntityUid uid, MindComponent mind, ComponentShutdown args)
+ {
+ if (mind.UserId is {} user)
+ {
+ UserMinds.Remove(user);
+ if (_players.GetPlayerData(user).ContentData() is { } oldData)
+ oldData.Mind = null;
+ mind.UserId = null;
+ }
+
+ if (!TryComp(mind.OwnedEntity, out MetaDataComponent? meta) || meta.EntityLifeStage >= EntityLifeStage.Terminating)
+ return;
+
+ RaiseLocalEvent(mind.OwnedEntity.Value, new MindRemovedMessage(uid, mind), true);
+ mind.OwnedEntity = null;
+ mind.OwnedComponent = null;
}
private void OnMindContainerTerminating(EntityUid uid, MindContainerComponent component, ref EntityTerminatingEvent args)
public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostCheckOverride = false, bool createGhost = true,
MindComponent? mind = null)
{
- base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind);
-
- if (!Resolve(mindId, ref mind))
+ if (mind == null && !Resolve(mindId, ref mind))
return;
+ base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind);
+
if (entity == mind.OwnedEntity)
return;
/// The component currently owned by this mind.
/// Can be null.
/// </summary>
- [ViewVariables]
- public MindContainerComponent? OwnedComponent { get; internal set; }
+ [ViewVariables] public MindContainerComponent? OwnedComponent;
/// <summary>
/// The entity currently owned by this mind.
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
-using Content.Shared.Objectives;
using Content.Shared.Objectives.Systems;
using Content.Shared.Players;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Players;
-using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Mind;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedPlayerSystem _player = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
// This is dictionary is required to track the minds of disconnected players that may have had their entity deleted.
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
public EntityUid CreateMind(NetUserId? userId, string? name = null)
{
var mindId = Spawn(null, MapCoordinates.Nullspace);
+ _metadata.SetEntityName(mindId, name == null ? "mind" : $"mind ({name})");
var mind = EnsureComp<MindComponent>(mindId);
mind.CharacterName = name;
SetUserId(mindId, userId, mind);
- Dirty(mindId, MetaData(mindId));
-
return mindId;
}
{
mindId = default;
mind = null;
- return _player.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind);
+ if (_player.ContentData(player) is not { } data)
+ return false;
+
+ if (TryGetMind(data, out mindId, out mind))
+ return true;
+
+ DebugTools.AssertNull(data.Mind);
+ return false;
}
/// <summary>