Ghost roles create new minds, better tracking of roles at round end screen (#5175)

* Ghost roles now get new Minds

* Some round start/end button stuff

* Mind tracking for better round end reports

* Make traitor kill objectives use mind CharacterName rather than actual occupied entity ("kill brain" prevention)

* Transition over to EntityUid for mind stuff because that's the only way to do it

* BrainSystem fix for PR rebase
This commit is contained in:
20kdc
2021-11-15 18:14:34 +00:00
committed by GitHub
parent c3a7548545
commit 4cce40bd9f
26 changed files with 229 additions and 68 deletions

View File

@@ -4,9 +4,10 @@
Margin="4"
MinSize="50 50">
<GridContainer
Columns="4">
Columns="3">
<cc:CommandButton Command="startround" Text="{Loc Start Round}" />
<cc:CommandButton Command="endround" Text="{Loc End Round}" />
<cc:CommandButton Command="restartround" Text="{Loc Restart Round}" />
<cc:CommandButton Command="restartroundnow" Text="{Loc administration-ui-round-tab-restart-round-now}" />
</GridContainer>
</Control>

View File

@@ -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));
});

View File

@@ -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);
}

View File

@@ -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<GhostComponent>();

View File

@@ -60,7 +60,7 @@ namespace Content.Server.Administration.Commands
DebugTools.AssertNotNull(mind);
mind!.TransferTo(target);
mind!.TransferTo(target.Uid);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -43,7 +43,7 @@ namespace Content.Server.Body.Systems
if (!EntityManager.HasComponent<IMoverComponent>(newEntity))
EntityManager.AddComponent<SharedDummyInputMoverComponent>(newEntity);
oldMind.Mind?.TransferTo(EntityManager.GetEntity(newEntity));
oldMind.Mind?.TransferTo(newEntity);
}
}
}

View File

@@ -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);
}

View File

@@ -17,6 +17,14 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ticker = EntitySystem.Get<GameTicker>();
if (ticker.RunLevel != GameRunLevel.InRound)
{
shell.WriteLine("This can only be executed while the game is in a round - try restartroundnow");
return;
}
EntitySystem.Get<RoundEndSystem>().EndRound();
}
}

View File

@@ -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

View File

@@ -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<RoundEndMessageEvent.RoundEndPlayerInfo>();
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<MindTrackerSystem>().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()

View File

@@ -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<JobPrototype>(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<GhostComponent>();
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(ghost, false);
data.Mind.TransferTo(mob);
newMind.TransferTo(mob.Uid);
_playersInLobby[player] = LobbyPlayerStatus.Observer;
RaiseNetworkEvent(GetStatusSingle(player, LobbyPlayerStatus.Observer));

View File

@@ -94,7 +94,7 @@ namespace Content.Server.GameTicking.Presets
if (canReturn)
mind.Visit(ghost);
else
mind.TransferTo(ghost);
mind.TransferTo(ghost.Uid);
return true;
}

View File

@@ -0,0 +1,18 @@
using Content.Server.Roles;
using Robust.Shared.Localization;
namespace Content.Server.Ghost
{
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
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)
{
}
}
}

View File

@@ -51,11 +51,8 @@ namespace Content.Server.Ghost.Roles.Components
mob.EnsureComponent<MindComponent>();
var mind = session.ContentData()?.Mind;
DebugTools.AssertNotNull(mind);
mind!.TransferTo(mob);
var ghostRoleSystem = EntitySystem.Get<GhostRoleSystem>();
ghostRoleSystem.GhostRoleInternalCreateMindAndTransfer(session, OwnerUid, mob.Uid, this);
if (++_currentTakeovers < _availableTakeovers)
return true;

View File

@@ -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>();
ghostRoleSystem.GhostRoleInternalCreateMindAndTransfer(session, OwnerUid, OwnerUid, this);
DebugTools.AssertNotNull(sessionMind);
sessionMind!.TransferTo(Owner);
EntitySystem.Get<GhostRoleSystem>().UnregisterGhostRole(this);
ghostRoleSystem.UnregisterGhostRole(this);
return true;
}

View File

@@ -0,0 +1,21 @@
using Content.Server.Roles;
using Robust.Shared.Localization;
namespace Content.Server.Ghost.Roles
{
/// <summary>
/// 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.
/// </summary>
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;
}
}
}

View File

@@ -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<MetaDataComponent>(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];

View File

@@ -92,7 +92,7 @@ namespace Content.Server.Mind.Components
EntitySystem.Get<SharedGhostSystem>().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);
}
});
}

View File

@@ -37,12 +37,14 @@ namespace Content.Server.Mind
private readonly List<Objective> _objectives = new();
/// <summary>
/// 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.
/// </summary>
/// <param name="userId">The session ID of the owning player.</param>
/// <param name="userId">The session ID of the original owner (may get credited).</param>
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; }
/// <summary>
/// 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)
/// </summary>
[ViewVariables]
public NetUserId OriginalOwnerUserId { get; }
[ViewVariables]
public bool IsVisitingEntity => VisitingEntity != null;
@@ -234,12 +243,10 @@ namespace Content.Server.Mind
return true;
}
/// <summary>
/// Transfer this mind's control over to a new entity.
/// </summary>
/// <param name="entity">
/// <param name="entityUid">
/// The entity to control.
/// Can be null, in which case it will simply detach the mind from any entity.
/// </param>
@@ -249,28 +256,31 @@ namespace Content.Server.Mind
/// <exception cref="ArgumentException">
/// Thrown if <paramref name="entity"/> is already owned by another mind.
/// </exception>
public void TransferTo(IEntity? entity, bool ghostCheckOverride = false)
public void TransferTo(EntityUid? entityUid, bool ghostCheckOverride = false)
{
var entMan = IoCManager.Resolve<IEntityManager>();
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<MindComponent>(entityUid.Value, out component))
{
component = entity.AddComponent<MindComponent>();
component = entMan.AddComponent<MindComponent>(entityUid.Value);
}
else if (component.HasMind)
else if (component!.HasMind)
{
EntitySystem.Get<GameTicker>().OnGhostAttempt(component.Mind!, false);
}
if (entity.TryGetComponent(out ActorComponent? actor))
if (entMan.TryGetComponent<ActorComponent>(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<IPlayerManager>();
@@ -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)

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class MindTrackerSystem : EntitySystem
{
[ViewVariables]
public readonly HashSet<Mind> AllMinds = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<MindComponent, MindAddedMessage>(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);
}
}
}

View File

@@ -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");

View File

@@ -16,12 +16,19 @@ namespace Content.Server.Players
[ViewVariables]
public NetUserId UserId { get; }
/// <summary>
/// This is a backup copy of the player name stored on connection.
/// This is useful in the event the player disconnects.
/// </summary>
[ViewVariables]
public string Name { get; }
/// <summary>
/// The currently occupied mind of the player owning this data.
/// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING.
/// </summary>
[ViewVariables]
public Mind.Mind? Mind { get; set; }
public Mind.Mind? Mind { get; private set; }
/// <summary>
/// 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)
/// <summary>
/// Called from Mind.ChangeOwningPlayer *and nowhere else.*
/// </summary>
public void UpdateMindFromMindChangeOwningPlayer(Mind.Mind? mind)
{
Mind = mind;
}
public PlayerData(NetUserId userId, string name)
{
UserId = userId;
Name = name;
}
}

View File

@@ -128,6 +128,7 @@ namespace Content.Shared.GameTicking
public string Role;
public bool Antag;
public bool Observer;
public bool Connected;
}
public string GamemodeTitle { get; }

View File

@@ -0,0 +1,2 @@
administration-ui-round-tab-restart-round-now = Restart NOW

View File

@@ -0,0 +1,2 @@
observer-role-name = Observer