diff --git a/Content.Client/Administration/UI/Tabs/RoundTab.xaml b/Content.Client/Administration/UI/Tabs/RoundTab.xaml
index 4d629c806f..439441b133 100644
--- a/Content.Client/Administration/UI/Tabs/RoundTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/RoundTab.xaml
@@ -4,9 +4,10 @@
Margin="4"
MinSize="50 50">
+ Columns="3">
+
diff --git a/Content.IntegrationTests/Tests/MindEntityDeletionTest.cs b/Content.IntegrationTests/Tests/MindEntityDeletionTest.cs
index e5e47869af..5e285ae584 100644
--- a/Content.IntegrationTests/Tests/MindEntityDeletionTest.cs
+++ b/Content.IntegrationTests/Tests/MindEntityDeletionTest.cs
@@ -37,9 +37,9 @@ namespace Content.IntegrationTests.Tests
visitEnt = entMgr.SpawnEntity(null, MapCoordinates.Nullspace);
mind = new Mind(player.UserId);
- player.ContentData().Mind = mind;
+ mind.ChangeOwningPlayer(player.UserId);
- mind.TransferTo(playerEnt);
+ mind.TransferTo(playerEnt.Uid);
mind.Visit(visitEnt);
Assert.That(player.AttachedEntity, Is.EqualTo(visitEnt));
@@ -81,9 +81,9 @@ namespace Content.IntegrationTests.Tests
playerEnt = entMgr.SpawnEntity(null, MapCoordinates.Nullspace);
mind = new Mind(player.UserId);
- player.ContentData().Mind = mind;
+ mind.ChangeOwningPlayer(player.UserId);
- mind.TransferTo(playerEnt);
+ mind.TransferTo(playerEnt.Uid);
Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt));
});
@@ -130,9 +130,9 @@ namespace Content.IntegrationTests.Tests
playerEnt = entMgr.SpawnEntity(null, grid.ToCoordinates());
mind = new Mind(player.UserId);
- player.ContentData().Mind = mind;
+ mind.ChangeOwningPlayer(player.UserId);
- mind.TransferTo(playerEnt);
+ mind.TransferTo(playerEnt.Uid);
Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt));
});
diff --git a/Content.Server/Administration/AdminVerbSystem.cs b/Content.Server/Administration/AdminVerbSystem.cs
index 507bd0ddc2..7eeb5f7f5e 100644
--- a/Content.Server/Administration/AdminVerbSystem.cs
+++ b/Content.Server/Administration/AdminVerbSystem.cs
@@ -93,7 +93,7 @@ namespace Content.Server.Administration
// TODO VERB ICON control mob icon
verb.Act = () =>
{
- player.ContentData()?.Mind?.TransferTo(args.Target, ghostCheckOverride: true);
+ player.ContentData()?.Mind?.TransferTo(args.Target.Uid, ghostCheckOverride: true);
};
args.Verbs.Add(verb);
}
diff --git a/Content.Server/Administration/Commands/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs
index f48c7a3372..fb8a72c6d8 100644
--- a/Content.Server/Administration/Commands/AGhost.cs
+++ b/Content.Server/Administration/Commands/AGhost.cs
@@ -58,7 +58,7 @@ namespace Content.Server.Administration.Commands
else
{
ghost.Name = player.Name;
- mind.TransferTo(ghost);
+ mind.TransferTo(ghost.Uid);
}
var comp = ghost.GetComponent();
diff --git a/Content.Server/Administration/Commands/ControlMob.cs b/Content.Server/Administration/Commands/ControlMob.cs
index 36cb8a4e50..3c5d70f1e3 100644
--- a/Content.Server/Administration/Commands/ControlMob.cs
+++ b/Content.Server/Administration/Commands/ControlMob.cs
@@ -60,7 +60,7 @@ namespace Content.Server.Administration.Commands
DebugTools.AssertNotNull(mind);
- mind!.TransferTo(target);
+ mind!.TransferTo(target.Uid);
}
}
}
diff --git a/Content.Server/Administration/Commands/SetMindCommand.cs b/Content.Server/Administration/Commands/SetMindCommand.cs
index 9b24a636b5..51c856b89e 100644
--- a/Content.Server/Administration/Commands/SetMindCommand.cs
+++ b/Content.Server/Administration/Commands/SetMindCommand.cs
@@ -71,9 +71,9 @@ namespace Content.Server.Administration.Commands
{
CharacterName = target.Name
};
- playerCData.Mind = mind;
+ mind.ChangeOwningPlayer(session.UserId);
}
- mind.TransferTo(target);
+ mind.TransferTo(target.Uid);
}
}
}
diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs
index 1a38721eed..6d842eaf3e 100644
--- a/Content.Server/Body/Systems/BrainSystem.cs
+++ b/Content.Server/Body/Systems/BrainSystem.cs
@@ -43,7 +43,7 @@ namespace Content.Server.Body.Systems
if (!EntityManager.HasComponent(newEntity))
EntityManager.AddComponent(newEntity);
- oldMind.Mind?.TransferTo(EntityManager.GetEntity(newEntity));
+ oldMind.Mind?.TransferTo(newEntity);
}
}
}
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index c489cc3454..fa3b2a1cfc 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -40,7 +40,7 @@ namespace Content.Server.Cloning
mindComp.Mind != null)
return;
- mind.TransferTo(entity, ghostCheckOverride: true);
+ mind.TransferTo(entity.Uid, ghostCheckOverride: true);
mind.UnVisit();
ClonesWaitingForMind.Remove(mind);
}
diff --git a/Content.Server/GameTicking/Commands/RestartRoundCommand.cs b/Content.Server/GameTicking/Commands/RestartRoundCommand.cs
index 84c2f68c6e..c5b63cf10d 100644
--- a/Content.Server/GameTicking/Commands/RestartRoundCommand.cs
+++ b/Content.Server/GameTicking/Commands/RestartRoundCommand.cs
@@ -17,6 +17,14 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
+ var ticker = EntitySystem.Get();
+
+ if (ticker.RunLevel != GameRunLevel.InRound)
+ {
+ shell.WriteLine("This can only be executed while the game is in a round - try restartroundnow");
+ return;
+ }
+
EntitySystem.Get().EndRound();
}
}
diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs
index cf182341f9..0d31d8a0f8 100644
--- a/Content.Server/GameTicking/GameTicker.Player.cs
+++ b/Content.Server/GameTicking/GameTicker.Player.cs
@@ -37,7 +37,7 @@ namespace Content.Server.GameTicking
{
// Always make sure the client has player data. Mind gets assigned on spawn.
if (session.Data.ContentDataUncast == null)
- session.Data.ContentDataUncast = new PlayerData(session.UserId);
+ session.Data.ContentDataUncast = new PlayerData(session.UserId, args.Session.Name);
// Make the player actually join the game.
// timer time must be > tick length
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index df953200c7..fe15feb7c8 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Players;
+using Content.Server.Mind;
+using Content.Server.Ghost;
using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using Content.Shared.GameTicking;
@@ -210,29 +212,50 @@ namespace Content.Server.GameTicking
//Generate a list of basic player info to display in the end round summary.
var listOfPlayerInfo = new List();
- foreach (var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name))
+ // Grab the great big book of all the Minds, we'll need them for this.
+ var allMinds = EntitySystem.Get().AllMinds;
+ foreach (var mind in allMinds)
{
- var mind = ply.ContentData()?.Mind;
-
if (mind != null)
{
- _playersInLobby.TryGetValue(ply, out var status);
+ // Some basics assuming things fail
+ var userId = mind.OriginalOwnerUserId;
+ var playerOOCName = userId.ToString();
+ var connected = false;
+ var observer = mind.AllRoles.Any(role => role is ObserverRole);
+ // Continuing
+ if (_playerManager.TryGetSessionById(userId, out var ply))
+ {
+ connected = true;
+ }
+ PlayerData? contentPlayerData = null;
+ if (_playerManager.TryGetPlayerData(userId, out var playerData))
+ {
+ contentPlayerData = playerData.ContentData();
+ }
+ // Finish
var antag = mind.AllRoles.Any(role => role.Antagonist);
var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo()
{
- PlayerOOCName = ply.Name,
- PlayerICName = mind.CurrentEntity?.Name,
+ // Note that contentPlayerData?.Name sticks around after the player is disconnected.
+ // This is as opposed to ply?.Name which doesn't.
+ PlayerOOCName = contentPlayerData?.Name ?? "(IMPOSSIBLE: REGISTERED MIND WITH NO OWNER)",
+ // Character name takes precedence over current entity name
+ PlayerICName = mind.CharacterName ?? mind.CurrentEntity?.Name,
Role = antag
? mind.AllRoles.First(role => role.Antagonist).Name
: mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("game-ticker-unknown-role"),
Antag = antag,
- Observer = status == LobbyPlayerStatus.Observer,
+ Observer = observer,
+ Connected = connected
};
listOfPlayerInfo.Add(playerEndRoundInfo);
}
}
+ // This ordering mechanism isn't great (no ordering of minds) but functions
+ var listOfPlayerInfoFinal = listOfPlayerInfo.OrderBy(pi => pi.PlayerOOCName).ToArray();
- RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, listOfPlayerInfo.Count, listOfPlayerInfo.ToArray()));
+ RaiseNetworkEvent(new RoundEndMessageEvent(gamemodeTitle, roundEndText, roundDuration, listOfPlayerInfoFinal.Length, listOfPlayerInfoFinal));
}
public void RestartRound()
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index d8f11c0495..f8901a95af 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using Content.Server.Access.Components;
using Content.Server.Access.Systems;
using Content.Server.CharacterAppearance.Components;
+using Content.Server.Ghost;
using Content.Server.Ghost.Components;
using Content.Server.Hands.Components;
using Content.Server.Inventory.Components;
@@ -70,17 +71,18 @@ namespace Content.Server.GameTicking
DebugTools.AssertNotNull(data);
data!.WipeMind();
- data.Mind = new Mind.Mind(player.UserId)
+ var newMind = new Mind.Mind(data.UserId)
{
CharacterName = character.Name
};
+ newMind.ChangeOwningPlayer(data.UserId);
// Pick best job best on prefs.
jobId ??= PickBestAvailableJob(character);
var jobPrototype = _prototypeManager.Index(jobId);
- var job = new Job(data.Mind, jobPrototype);
- data.Mind.AddRole(job);
+ var job = new Job(newMind, jobPrototype);
+ newMind.AddRole(job);
if (lateJoin)
{
@@ -92,7 +94,7 @@ namespace Content.Server.GameTicking
}
var mob = SpawnPlayerMob(job, character, lateJoin);
- data.Mind.TransferTo(mob);
+ newMind.TransferTo(mob.Uid);
if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
{
@@ -150,13 +152,15 @@ namespace Content.Server.GameTicking
DebugTools.AssertNotNull(data);
data!.WipeMind();
- data.Mind = new Mind.Mind(player.UserId);
+ var newMind = new Mind.Mind(data.UserId);
+ newMind.ChangeOwningPlayer(data.UserId);
+ newMind.AddRole(new ObserverRole(newMind));
var mob = SpawnObserverMob();
mob.Name = name;
var ghost = mob.GetComponent();
EntitySystem.Get().SetCanReturnToBody(ghost, false);
- data.Mind.TransferTo(mob);
+ newMind.TransferTo(mob.Uid);
_playersInLobby[player] = LobbyPlayerStatus.Observer;
RaiseNetworkEvent(GetStatusSingle(player, LobbyPlayerStatus.Observer));
diff --git a/Content.Server/GameTicking/Presets/GamePreset.cs b/Content.Server/GameTicking/Presets/GamePreset.cs
index 28f2650238..0f5e19d316 100644
--- a/Content.Server/GameTicking/Presets/GamePreset.cs
+++ b/Content.Server/GameTicking/Presets/GamePreset.cs
@@ -94,7 +94,7 @@ namespace Content.Server.GameTicking.Presets
if (canReturn)
mind.Visit(ghost);
else
- mind.TransferTo(ghost);
+ mind.TransferTo(ghost.Uid);
return true;
}
diff --git a/Content.Server/Ghost/ObserverRole.cs b/Content.Server/Ghost/ObserverRole.cs
new file mode 100644
index 0000000000..eb57069f84
--- /dev/null
+++ b/Content.Server/Ghost/ObserverRole.cs
@@ -0,0 +1,18 @@
+using Content.Server.Roles;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Ghost
+{
+ ///
+ /// This is used to mark Observers properly, as they get Minds
+ ///
+ public class ObserverRole : Role
+ {
+ public override string Name => Loc.GetString("observer-role-name");
+ public override bool Antagonist => false;
+
+ public ObserverRole(Mind.Mind mind) : base(mind)
+ {
+ }
+ }
+}
diff --git a/Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs b/Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs
index ce2d82e24e..0dd659db36 100644
--- a/Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs
+++ b/Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs
@@ -51,11 +51,8 @@ namespace Content.Server.Ghost.Roles.Components
mob.EnsureComponent();
- var mind = session.ContentData()?.Mind;
-
- DebugTools.AssertNotNull(mind);
-
- mind!.TransferTo(mob);
+ var ghostRoleSystem = EntitySystem.Get();
+ ghostRoleSystem.GhostRoleInternalCreateMindAndTransfer(session, OwnerUid, mob.Uid, this);
if (++_currentTakeovers < _availableTakeovers)
return true;
diff --git a/Content.Server/Ghost/Roles/Components/GhostTakeoverAvailableComponent.cs b/Content.Server/Ghost/Roles/Components/GhostTakeoverAvailableComponent.cs
index 4601fd0ebb..d154244b6f 100644
--- a/Content.Server/Ghost/Roles/Components/GhostTakeoverAvailableComponent.cs
+++ b/Content.Server/Ghost/Roles/Components/GhostTakeoverAvailableComponent.cs
@@ -27,13 +27,10 @@ namespace Content.Server.Ghost.Roles.Components
if (mind.HasMind)
return false;
- var sessionMind = session.ContentData()?.Mind;
+ var ghostRoleSystem = EntitySystem.Get();
+ ghostRoleSystem.GhostRoleInternalCreateMindAndTransfer(session, OwnerUid, OwnerUid, this);
- DebugTools.AssertNotNull(sessionMind);
-
- sessionMind!.TransferTo(Owner);
-
- EntitySystem.Get().UnregisterGhostRole(this);
+ ghostRoleSystem.UnregisterGhostRole(this);
return true;
}
diff --git a/Content.Server/Ghost/Roles/GhostRoleMarkerRole.cs b/Content.Server/Ghost/Roles/GhostRoleMarkerRole.cs
new file mode 100644
index 0000000000..e9a87f0a8f
--- /dev/null
+++ b/Content.Server/Ghost/Roles/GhostRoleMarkerRole.cs
@@ -0,0 +1,21 @@
+using Content.Server.Roles;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Ghost.Roles
+{
+ ///
+ /// This is used for round end display of ghost roles.
+ /// It may also be used to ensure some ghost roles count as antagonists in future.
+ ///
+ public class GhostRoleMarkerRole : Role
+ {
+ private readonly string _name;
+ public override string Name => _name;
+ public override bool Antagonist => false;
+
+ public GhostRoleMarkerRole(Mind.Mind mind, string name) : base(mind)
+ {
+ _name = name;
+ }
+ }
+}
diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs
index 85594ac420..872008a2c2 100644
--- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs
+++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs
@@ -4,6 +4,7 @@ using Content.Server.EUI;
using Content.Server.Ghost.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.UI;
+using Content.Server.Players;
using Content.Shared.GameTicking;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost;
@@ -14,6 +15,7 @@ using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
+using Robust.Shared.Utility;
using Robust.Shared.Enums;
namespace Content.Server.Ghost.Roles
@@ -153,6 +155,24 @@ namespace Content.Server.Ghost.Roles
CloseEui(player);
}
+ public void GhostRoleInternalCreateMindAndTransfer(IPlayerSession player, EntityUid roleUid, EntityUid mob, GhostRoleComponent? role = null)
+ {
+ if (!Resolve(roleUid, ref role)) return;
+
+ var contentData = player.ContentData();
+
+ DebugTools.AssertNotNull(contentData);
+
+ 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);
+ }
+
public GhostRoleInfo[] GetGhostRolesInfo()
{
var roles = new GhostRoleInfo[_ghostRoles.Count];
diff --git a/Content.Server/Mind/Components/MindComponent.cs b/Content.Server/Mind/Components/MindComponent.cs
index ebc9fe3a63..3334643a01 100644
--- a/Content.Server/Mind/Components/MindComponent.cs
+++ b/Content.Server/Mind/Components/MindComponent.cs
@@ -92,7 +92,7 @@ namespace Content.Server.Mind.Components
EntitySystem.Get().SetCanReturnToBody(ghost, false);
}
- Mind!.TransferTo(visiting);
+ Mind!.TransferTo(visiting.Uid);
}
else if (GhostOnShutdown)
{
@@ -116,7 +116,7 @@ namespace Content.Server.Mind.Components
if (Mind != null)
{
ghost.Name = Mind.CharacterName ?? string.Empty;
- Mind.TransferTo(ghost);
+ Mind.TransferTo(ghost.Uid);
}
});
}
diff --git a/Content.Server/Mind/Mind.cs b/Content.Server/Mind/Mind.cs
index b4986b0808..3aadd4b2ec 100644
--- a/Content.Server/Mind/Mind.cs
+++ b/Content.Server/Mind/Mind.cs
@@ -37,12 +37,14 @@ namespace Content.Server.Mind
private readonly List _objectives = new();
///
- /// Creates the new mind attached to a specific player session.
+ /// Creates the new mind.
+ /// Note: the Mind is NOT initially attached!
+ /// The provided UserId is solely for tracking of intended owner.
///
- /// The session ID of the owning player.
+ /// The session ID of the original owner (may get credited).
public Mind(NetUserId userId)
{
- UserId = userId;
+ OriginalOwnerUserId = userId;
}
// TODO: This session should be able to be changed, probably.
@@ -52,6 +54,13 @@ namespace Content.Server.Mind
[ViewVariables]
public NetUserId? UserId { get; private 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; }
+
[ViewVariables]
public bool IsVisitingEntity => VisitingEntity != null;
@@ -234,12 +243,10 @@ namespace Content.Server.Mind
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.
///
@@ -249,28 +256,31 @@ namespace Content.Server.Mind
///
/// Thrown if is already owned by another mind.
///
- public void TransferTo(IEntity? entity, bool ghostCheckOverride = false)
+ public void TransferTo(EntityUid? entityUid, bool ghostCheckOverride = false)
{
+ var entMan = IoCManager.Resolve();
+ IEntity? entity = (entityUid != null) ? entMan.GetEntity(entityUid.Value) : null;
+
MindComponent? component = null;
var alreadyAttached = false;
- if (entity != null)
+ if (entityUid != null)
{
- if (!entity.TryGetComponent(out component))
+ if (!entMan.TryGetComponent(entityUid.Value, out component))
{
- component = entity.AddComponent();
+ component = entMan.AddComponent(entityUid.Value);
}
- else if (component.HasMind)
+ else if (component!.HasMind)
{
EntitySystem.Get().OnGhostAttempt(component.Mind!, false);
}
- if (entity.TryGetComponent(out ActorComponent? actor))
+ if (entMan.TryGetComponent(entityUid.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));
+ throw new ArgumentException("Visit target already has a session.", nameof(entityUid));
}
alreadyAttached = true;
@@ -298,11 +308,6 @@ namespace Content.Server.Mind
}
}
- public void RemoveOwningPlayer()
- {
- UserId = null;
- }
-
public void ChangeOwningPlayer(NetUserId? newOwner)
{
var playerMgr = IoCManager.Resolve();
@@ -329,7 +334,7 @@ namespace Content.Server.Mind
{
var data = playerMgr.GetPlayerData(UserId.Value).ContentData();
DebugTools.AssertNotNull(data);
- data!.Mind = null;
+ data!.UpdateMindFromMindChangeOwningPlayer(null);
}
UserId = newOwner;
@@ -342,7 +347,7 @@ namespace Content.Server.Mind
// Can I mention how much I love the word yank?
DebugTools.AssertNotNull(newOwnerData);
newOwnerData!.Mind?.ChangeOwningPlayer(null);
- newOwnerData.Mind = this;
+ newOwnerData.UpdateMindFromMindChangeOwningPlayer(this);
}
public void Visit(IEntity entity)
diff --git a/Content.Server/Mind/MindTrackerSystem.cs b/Content.Server/Mind/MindTrackerSystem.cs
new file mode 100644
index 0000000000..15924a190b
--- /dev/null
+++ b/Content.Server/Mind/MindTrackerSystem.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.GameTicking;
+using Content.Server.Mind.Components;
+using Content.Shared.GameTicking;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.ViewVariables;
+using Robust.Shared.Player;
+
+namespace Content.Server.Mind
+{
+ ///
+ /// This is absolutely evil.
+ /// It tracks all mind changes and logs all the Mind objects.
+ /// This is so that when round end comes around, there's a coherent list of all Minds that were in play during the round.
+ /// The Minds themselves contain metadata about their owners.
+ /// Anyway, this is because disconnected people and ghost roles have been breaking round end statistics for way too long.
+ ///
+ public class MindTrackerSystem : EntitySystem
+ {
+ [ViewVariables]
+ public readonly HashSet AllMinds = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(Reset);
+ SubscribeLocalEvent(OnMindAdded);
+ }
+
+ void Reset(RoundRestartCleanupEvent ev)
+ {
+ AllMinds.Clear();
+ }
+
+ void OnMindAdded(EntityUid uid, MindComponent mc, MindAddedMessage args)
+ {
+ var mind = mc.Mind;
+ if (mind != null)
+ AllMinds.Add(mind);
+ }
+ }
+}
+
diff --git a/Content.Server/Objectives/Conditions/KillPersonCondition.cs b/Content.Server/Objectives/Conditions/KillPersonCondition.cs
index 8f37588b31..cbae4d8119 100644
--- a/Content.Server/Objectives/Conditions/KillPersonCondition.cs
+++ b/Content.Server/Objectives/Conditions/KillPersonCondition.cs
@@ -9,7 +9,7 @@ namespace Content.Server.Objectives.Conditions
protected Mind.Mind? Target;
public abstract IObjectiveCondition GetAssigned(Mind.Mind mind);
- public string Title => Loc.GetString("objective-condition-kill-person-title", ("targetName", Target?.OwnedEntity?.Name ?? string.Empty));
+ public string Title => Loc.GetString("objective-condition-kill-person-title", ("targetName", Target?.CharacterName ?? Target?.OwnedEntity?.Name ?? string.Empty));
public string Description => Loc.GetString("objective-condition-kill-person-description");
diff --git a/Content.Server/Players/PlayerData.cs b/Content.Server/Players/PlayerData.cs
index d9e363cd8d..9ffb763799 100644
--- a/Content.Server/Players/PlayerData.cs
+++ b/Content.Server/Players/PlayerData.cs
@@ -16,12 +16,19 @@ namespace Content.Server.Players
[ViewVariables]
public NetUserId UserId { get; }
+ ///
+ /// This is a backup copy of the player name stored on connection.
+ /// This is useful in the event the player disconnects.
+ ///
+ [ViewVariables]
+ public string Name { get; }
+
///
/// The currently occupied mind of the player owning this data.
/// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING.
///
[ViewVariables]
- public Mind.Mind? Mind { get; set; }
+ public Mind.Mind? Mind { get; private set; }
///
/// If true, the player is an admin and they explicitly de-adminned mid-game,
@@ -32,13 +39,22 @@ namespace Content.Server.Players
public void WipeMind()
{
Mind?.TransferTo(null);
- Mind?.RemoveOwningPlayer();
- Mind = null;
+ // This will ensure Mind == null
+ Mind?.ChangeOwningPlayer(null);
}
- public PlayerData(NetUserId userId)
+ ///
+ /// Called from Mind.ChangeOwningPlayer *and nowhere else.*
+ ///
+ public void UpdateMindFromMindChangeOwningPlayer(Mind.Mind? mind)
+ {
+ Mind = mind;
+ }
+
+ public PlayerData(NetUserId userId, string name)
{
UserId = userId;
+ Name = name;
}
}
diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs
index 944246ca72..ee8f1902ba 100644
--- a/Content.Shared/GameTicking/SharedGameTicker.cs
+++ b/Content.Shared/GameTicking/SharedGameTicker.cs
@@ -128,6 +128,7 @@ namespace Content.Shared.GameTicking
public string Role;
public bool Antag;
public bool Observer;
+ public bool Connected;
}
public string GamemodeTitle { get; }
diff --git a/Resources/Locale/en-US/administration/ui/tabs/round-tab.ftl b/Resources/Locale/en-US/administration/ui/tabs/round-tab.ftl
new file mode 100644
index 0000000000..9fd44d7f3a
--- /dev/null
+++ b/Resources/Locale/en-US/administration/ui/tabs/round-tab.ftl
@@ -0,0 +1,2 @@
+administration-ui-round-tab-restart-round-now = Restart NOW
+
diff --git a/Resources/Locale/en-US/ghost/observer-role.ftl b/Resources/Locale/en-US/ghost/observer-role.ftl
new file mode 100644
index 0000000000..acb30b128f
--- /dev/null
+++ b/Resources/Locale/en-US/ghost/observer-role.ftl
@@ -0,0 +1,2 @@
+observer-role-name = Observer
+