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 +