Refactor antag rule code (#23445)

* Initial Pass, Rev, Thief

* Zombie initial pass

* Rebase, Traitor

* Nukeops, More overloads

* Revert RevolutionaryRuleComponent

* Use TryRoundStartAttempt, Rewrite nukie spawning

* Comments, Add task scheduler to GameRuleSystem

* Zombie initial testing done

* Sort methods, rework GameRuleTask

* Add CCVar, Initial testing continues

* Might as well get rid of the obsolete logging

* Oops, i dont know how to log apparently

* Suggested formatting fixes

* Suggested changes

* Fix merge issues

* Minor optimisation

* Allowed thief to choose other antags

* Review changes

* Spawn items on floor first, then inserting

* minor tweaks

* Shift as much as possible to ProtoId<>

* Remove unneeded

* Add exclusive antag attribute

* Fix merge issues

* Minor formatting fix

* Convert to struct

* Cleanup

* Review cleanup (need to test a lot)

* Some fixes, (mostly) tested

* oop

* Pass tests (for real)

---------

Co-authored-by: Rainfall <rainfey0+git@gmail.com>
Co-authored-by: AJCM <AJCM@tutanota.com>
This commit is contained in:
Rainfey
2024-02-29 06:25:10 +00:00
committed by GitHub
parent 3966a65c65
commit 4e6c59cfe5
53 changed files with 22454 additions and 22396 deletions

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components;

View File

@@ -14,9 +14,6 @@ public sealed partial class NukeOperativeSpawnerComponent : Component
[DataField("name", required:true)]
public string OperativeName = default!;
[DataField("rolePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<AntagPrototype>), required:true)]
public string OperativeRolePrototype = default!;
[DataField("startingGearPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<StartingGearPrototype>), required:true)]
public string OperativeStartingGear = default!;
[DataField]
public NukeopSpawnPreset SpawnDetails = default!;
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Maps;
using Content.Server.NPC.Components;
using Content.Server.RoundEnd;
using Content.Server.StationEvents.Events;
@@ -16,15 +17,8 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
public sealed partial class NukeopsRuleComponent : Component
{
// TODO Replace with GameRuleComponent.minPlayers
/// <summary>
/// The minimum needed amount of players
/// </summary>
[DataField]
public int MinPlayers = 20;
/// <summary>
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
/// </summary>
[DataField]
public int PlayersPerOperative = 10;
@@ -92,17 +86,11 @@ public sealed partial class NukeopsRuleComponent : Component
[DataField]
public int WarTCAmountPerNukie = 40;
/// <summary>
/// Time allowed for declaration of war
/// </summary>
[DataField("warDeclarationDelay")]
public TimeSpan WarDeclarationDelay = TimeSpan.FromMinutes(6);
/// <summary>
/// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
/// </summary>
[DataField]
public TimeSpan? WarNukieArriveDelay = TimeSpan.FromMinutes(15);
public TimeSpan WarNukieArriveDelay = TimeSpan.FromMinutes(15);
/// <summary>
/// Minimal operatives count for war declaration
@@ -116,38 +104,11 @@ public sealed partial class NukeopsRuleComponent : Component
[DataField]
public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
[DataField]
public ProtoId<AntagPrototype> CommanderRoleProto = "NukeopsCommander";
[DataField]
public ProtoId<AntagPrototype> OperativeRoleProto = "Nukeops";
[DataField]
public ProtoId<AntagPrototype> MedicRoleProto = "NukeopsMedic";
[DataField]
public ProtoId<StartingGearPrototype> CommanderStartGearProto = "SyndicateCommanderGearFull";
[DataField]
public ProtoId<StartingGearPrototype> MedicStartGearProto = "SyndicateOperativeMedicFull";
[DataField]
public ProtoId<StartingGearPrototype> OperativeStartGearProto = "SyndicateOperativeGearFull";
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
public string EliteNames = "SyndicateNamesElite";
[DataField]
public string OperationName = "Test Operation";
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
public string NormalNames = "SyndicateNamesNormal";
[DataField(customTypeSerializer: typeof(ResPathSerializer))]
public ResPath OutpostMap = new("/Maps/nukieplanet.yml");
[DataField(customTypeSerializer: typeof(ResPathSerializer))]
public ResPath ShuttleMap = new("/Maps/infiltrator.yml");
[DataField]
public ProtoId<GameMapPrototype> OutpostMapPrototype = "NukieOutpost";
[DataField]
public WinType WinType = WinType.Neutral;
@@ -163,33 +124,53 @@ public sealed partial class NukeopsRuleComponent : Component
public EntityUid? NukieShuttle;
public EntityUid? TargetStation;
/// <summary>
/// Cached starting gear prototypes.
/// </summary>
[DataField]
public Dictionary<string, StartingGearPrototype> StartingGearPrototypes = new ();
/// <summary>
/// Cached operator name prototypes.
/// </summary>
[DataField]
public Dictionary<string, List<string>> OperativeNames = new();
/// <summary>
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
/// </summary>
[DataField]
public Dictionary<EntityUid, string> OperativeMindPendingData = new();
/// <summary>
/// Players who played as an operative at some point in the round.
/// Stores the mind as well as the entity name
/// </summary>
[DataField]
public Dictionary<string, EntityUid> OperativePlayers = new();
[DataField(required: true)]
public ProtoId<NpcFactionPrototype> Faction = default!;
[DataField]
public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" };
[DataField]
public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" };
[DataField]
public NukeopSpawnPreset OperativeSpawnDetails = new();
}
/// <summary>
/// Stores the presets for each operative type
/// Ie Commander, Agent and Operative
/// </summary>
[DataDefinition, Serializable]
public sealed partial class NukeopSpawnPreset
{
[DataField]
public ProtoId<AntagPrototype> AntagRoleProto = "Nukeops";
/// <summary>
/// The equipment set this operative will be given when spawned
/// </summary>
[DataField]
public ProtoId<StartingGearPrototype> GearProto = "SyndicateOperativeGearFull";
/// <summary>
/// The name prefix, ie "Agent"
/// </summary>
[DataField]
public LocId NamePrefix = "nukeops-role-operator";
/// <summary>
/// The entity name suffix will be chosen from this list randomly
/// </summary>
[DataField]
public ProtoId<DatasetPrototype> NameList = "SyndicateNamesNormal";
}
public enum WinType : byte

View File

@@ -1,5 +1,4 @@
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -32,15 +31,6 @@ public sealed partial class RevolutionaryRuleComponent : Component
[DataField]
public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev";
[DataField]
public ProtoId<AntagPrototype> RevPrototypeId = "Rev";
/// <summary>
/// Sound that plays when you are chosen as Rev. (Placeholder until I find something cool I guess)
/// </summary>
[DataField]
public SoundSpecifier HeadRevStartSound = new SoundPathSpecifier("/Audio/Ambience/Antag/headrev_start.ogg");
/// <summary>
/// Min players needed for Revolutionary gamemode to start.
/// </summary>

View File

@@ -1,8 +1,7 @@
using Content.Shared.Random;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Content.Shared.Roles;
using Robust.Shared.Player;
using Content.Shared.Preferences;
namespace Content.Server.GameTicking.Rules.Components;
@@ -12,6 +11,18 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
public sealed partial class ThiefRuleComponent : Component
{
[DataField]
public ProtoId<WeightedRandomPrototype> BigObjectiveGroup = "ThiefBigObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> SmallObjectiveGroup = "ThiefObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
[DataField]
public float BigObjectiveChance = 0.7f;
/// <summary>
/// Add a Pacified comp to thieves
/// </summary>
@@ -27,8 +38,6 @@ public sealed partial class ThiefRuleComponent : Component
[DataField]
public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief";
public Dictionary<ICommonSession, HumanoidCharacterProfile> StartCandidates = new();
[DataField]
public float MaxObjectiveDifficulty = 2.5f;
@@ -39,7 +48,7 @@ public sealed partial class ThiefRuleComponent : Component
/// Things that will be given to thieves
/// </summary>
[DataField]
public List<EntProtoId> StarterItems = new List<EntProtoId> { "ToolboxThief", "ClothingHandsChameleonThief" }; //TO DO - replace to chameleon thieving gloves whem merg
public List<EntProtoId> StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
/// <summary>
/// All Thieves created by this rule

View File

@@ -1,8 +1,10 @@
using Content.Shared.Preferences;
using Content.Server.NPC.Components;
using Content.Shared.Dataset;
using Content.Shared.Random;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components;
@@ -11,8 +13,23 @@ public sealed partial class TraitorRuleComponent : Component
{
public readonly List<EntityUid> TraitorMinds = new();
[DataField("traitorPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string TraitorPrototypeId = "Traitor";
[DataField]
public ProtoId<AntagPrototype> TraitorPrototypeId = "Traitor";
[DataField]
public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
[DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<WeightedRandomPrototype> ObjectiveGroup = "TraitorObjectiveGroups";
[DataField]
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
[DataField]
public ProtoId<DatasetPrototype> CodewordVerbs = "verbs";
public int TotalTraitors => TraitorMinds.Count;
public string[] Codewords = new string[3];
@@ -20,17 +37,24 @@ public sealed partial class TraitorRuleComponent : Component
public enum SelectionState
{
WaitingForSpawn = 0,
ReadyToSelect = 1,
SelectionMade = 2,
ReadyToStart = 1,
Started = 2,
}
/// <summary>
/// Current state of the rule
/// </summary>
public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
public TimeSpan AnnounceAt = TimeSpan.Zero;
public Dictionary<ICommonSession, HumanoidCharacterProfile> StartCandidates = new();
/// <summary>
/// When should traitors be selected and the announcement made
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? AnnounceAt;
/// <summary>
/// Path to antagonist alert sound.
/// </summary>
[DataField("greetSoundNotification")]
[DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
}

View File

@@ -2,98 +2,85 @@ using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed partial class ZombieRuleComponent : Component
{
[DataField("initialInfectedNames")]
[DataField]
public Dictionary<string, string> InitialInfectedNames = new();
[DataField("patientZeroPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string PatientZeroPrototypeId = "InitialInfected";
/// <summary>
/// Whether or not the initial infected have been chosen.
/// </summary>
[DataField("infectedChosen")]
public bool InfectedChosen;
[DataField]
public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
/// <summary>
/// When the round will next check for round end.
/// </summary>
[DataField("nextRoundEndCheck", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextRoundEndCheck;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? NextRoundEndCheck;
/// <summary>
/// The amount of time between each check for the end of the round.
/// </summary>
[DataField("endCheckDelay")]
[DataField]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
/// <summary>
/// The time at which the initial infected will be chosen.
/// </summary>
[DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? StartTime;
/// <summary>
/// The minimum amount of time after the round starts that the initial infected will be chosen.
/// </summary>
[DataField("minStartDelay")]
[DataField]
public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
/// <summary>
/// The maximum amount of time after the round starts that the initial infected will be chosen.
/// </summary>
[DataField("maxStartDelay")]
[DataField]
public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
/// <summary>
/// The sound that plays when someone becomes an initial infected.
/// todo: this should have a unique sound instead of reusing the zombie one.
/// </summary>
[DataField("initialInfectedSound")]
[DataField]
public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
/// <summary>
/// The minimum amount of time initial infected have before they start taking infection damage.
/// </summary>
[DataField("minInitialInfectedGrace")]
[DataField]
public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
/// <summary>
/// The maximum amount of time initial infected have before they start taking damage.
/// </summary>
[DataField("maxInitialInfectedGrace")]
[DataField]
public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
/// <summary>
/// How many players for each initial infected.
/// </summary>
[DataField("playersPerInfected")]
[DataField]
public int PlayersPerInfected = 10;
/// <summary>
/// The maximum number of initial infected.
/// </summary>
[DataField("maxInitialInfected")]
[DataField]
public int MaxInitialInfected = 6;
/// <summary>
/// After this amount of the crew become zombies, the shuttle will be automatically called.
/// </summary>
[DataField("zombieShuttleCallPercentage")]
[DataField]
public float ZombieShuttleCallPercentage = 0.7f;
/// <summary>
/// Have we called the evac shuttle yet?
/// </summary>
[DataField("shuttleCalled")]
public bool ShuttleCalled;
[ValidatePrototypeId<EntityPrototype>]
public const string ZombifySelfActionPrototype = "ActionTurnUndead";
[DataField]
public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
}

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Shared.Collections;

View File

@@ -1,13 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.GameTicking.Rules;
@@ -16,9 +12,9 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
[Dependency] protected readonly IRobustRandom RobustRandom = default!;
[Dependency] protected readonly IChatManager ChatManager = default!;
[Dependency] protected readonly GameTicker GameTicker = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
// Not protected, just to be used in utility methods
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly MapSystem _map = default!;

File diff suppressed because it is too large Load Diff

View File

@@ -271,11 +271,12 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
}
//Forcing one player to be a pirate.
public void MakePirate(EntityUid mindId, MindComponent mind)
public void MakePirate(EntityUid entity)
{
if (!mind.OwnedEntity.HasValue)
if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind))
return;
SetOutfitCommand.SetOutfit(mind.OwnedEntity.Value, GearId, EntityManager);
SetOutfitCommand.SetOutfit(entity, GearId, EntityManager);
var pirateRule = EntityQuery<PiratesRuleComponent>().FirstOrDefault();
if (pirateRule == null)

View File

@@ -1,7 +1,5 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Antag;
using Content.Server.Chat.Managers;
using Content.Server.EUI;
using Content.Server.Flash;
using Content.Server.GameTicking.Rules.Components;
@@ -13,10 +11,12 @@ using Content.Server.Revolutionary;
using Content.Server.Revolutionary.Components;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Shared.Chat;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mindshield.Components;
@@ -27,9 +27,9 @@ using Content.Shared.Revolutionary.Components;
using Content.Shared.Roles;
using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
namespace Content.Server.GameTicking.Rules;
@@ -39,7 +39,6 @@ namespace Content.Server.GameTicking.Rules;
public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly EuiManager _euiMan = default!;
@@ -50,12 +49,13 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<NpcFactionPrototype>]
public const string RevolutionaryNpcFaction = "Revolutionary";
[ValidatePrototypeId<AntagPrototype>]
public const string RevolutionaryAntagRole = "Rev";
//Used in OnPostFlash, no reference to the rule component is available
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
public readonly ProtoId<NpcFactionPrototype> RevPrototypeId = "Rev";
public override void Initialize()
{
@@ -69,15 +69,20 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
}
//Set miniumum players
protected override void Added(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
gameRule.MinPlayers = component.MinPlayers;
}
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.CommandCheck = _timing.CurTime + component.TimerWait;
}
/// <summary>
/// Checks if the round should end and also checks who has a mindshield.
/// </summary>
protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
@@ -139,63 +144,63 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
}
//Check for enough players to start rule
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = AllEntityQuery<RevolutionaryRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var comp, out var gameRule))
{
_antagSelection.AttemptStartGameRule(ev, uid, comp.MinPlayers, gameRule);
}
TryRoundStartAttempt(ev, Loc.GetString("roles-antag-rev-name"));
}
private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
{
var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out _))
while (query.MoveNext(out var uid, out var activeGameRule, out var comp, out var gameRule))
{
_antagSelection.EligiblePlayers(comp.HeadRevPrototypeId, comp.MaxHeadRevs, comp.PlayersPerHeadRev, comp.HeadRevStartSound,
"head-rev-role-greeting", "#5e9cff", out var chosen);
if (chosen.Any())
GiveHeadRev(chosen, comp.HeadRevPrototypeId, comp);
else
{
_chatManager.SendAdminAnnouncement(Loc.GetString("rev-no-heads"));
}
var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.HeadRevPrototypeId);
if (eligiblePlayers.Count == 0)
continue;
var headRevCount = _antagSelection.CalculateAntagCount(ev.Players.Length, comp.PlayersPerHeadRev, comp.MaxHeadRevs);
var headRevs = _antagSelection.ChooseAntags(headRevCount, eligiblePlayers);
GiveHeadRev(headRevs, comp.HeadRevPrototypeId, comp);
}
}
private void GiveHeadRev(List<EntityUid> chosen, string antagProto, RevolutionaryRuleComponent comp)
private void GiveHeadRev(IEnumerable<EntityUid> chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
{
foreach (var headRev in chosen)
GiveHeadRev(headRev, antagProto, comp);
}
private void GiveHeadRev(EntityUid chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
{
RemComp<CommandStaffComponent>(chosen);
var inCharacterName = MetaData(chosen).EntityName;
if (!_mind.TryGetMind(chosen, out var mind, out _))
return;
if (!_role.MindHasRole<RevolutionaryRoleComponent>(mind))
{
RemComp<CommandStaffComponent>(headRev);
var inCharacterName = MetaData(headRev).EntityName;
if (_mind.TryGetMind(headRev, out var mindId, out var mind))
{
if (!_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = antagProto });
}
if (mind.Session != null)
{
comp.HeadRevs.Add(inCharacterName, mindId);
}
}
_antagSelection.GiveAntagBagGear(headRev, comp.StartingGear);
EnsureComp<RevolutionaryComponent>(headRev);
EnsureComp<HeadRevolutionaryComponent>(headRev);
_role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true);
}
comp.HeadRevs.Add(inCharacterName, mind);
_inventory.SpawnItemsOnEntity(chosen, comp.StartingGear);
var revComp = EnsureComp<RevolutionaryComponent>(chosen);
EnsureComp<HeadRevolutionaryComponent>(chosen);
_antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound);
}
/// <summary>
/// Called when a Head Rev uses a flash in melee to convert somebody else.
/// </summary>
public void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
private void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
{
TryComp<AlwaysRevolutionaryConvertibleComponent>(ev.Target, out var alwaysConvertibleComp);
var alwaysConvertible = alwaysConvertibleComp != null;
var alwaysConvertible = HasComp<AlwaysRevolutionaryConvertibleComponent>(ev.Target);
if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible)
return;
@@ -211,8 +216,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
_npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction);
EnsureComp<RevolutionaryComponent>(ev.Target);
var revComp = EnsureComp<RevolutionaryComponent>(ev.Target);
_stun.TryParalyze(ev.Target, comp.StunTime, true);
if (ev.User != null)
{
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
@@ -223,20 +229,16 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevolutionaryAntagRole });
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevPrototypeId });
}
if (mind?.Session != null)
{
var message = Loc.GetString("rev-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.Channel, Color.Red);
_audioSystem.PlayGlobal("/Audio/Ambience/Antag/headrev_start.ogg", ev.Target);
}
_antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
}
public void OnHeadRevAdmin(EntityUid mindId, MindComponent? mind = null)
public void OnHeadRevAdmin(EntityUid entity)
{
if (!Resolve(mindId, ref mind))
if (HasComp<HeadRevolutionaryComponent>(entity))
return;
var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
@@ -246,24 +248,10 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
}
if (!HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity))
{
if (mind.OwnedEntity != null)
{
var player = new List<EntityUid>
{
mind.OwnedEntity.Value
};
GiveHeadRev(player, RevolutionaryAntagRole, revRule);
}
if (mind.Session != null)
{
var message = Loc.GetString("head-rev-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.Channel, Color.FromHex("#5e9cff"));
}
}
GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule);
}
//TODO: Enemies of the revolution
private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev)
{
if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid)
@@ -283,7 +271,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
commandList.Add(id);
}
return _antagSelection.IsGroupDead(commandList, true);
return IsGroupDead(commandList, true);
}
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
@@ -307,7 +295,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
if (_antagSelection.IsGroupDead(headRevList, false))
if (IsGroupDead(headRevList, false))
{
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
while (rev.MoveNext(out var uid, out _, out var mc))
@@ -338,6 +326,38 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
return false;
}
/// <summary>
/// Will take a group of entities and check if they are all alive or dead
/// </summary>
/// <param name="list">The list of the entities</param>
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them dead. (Won't check when emergency shuttle arrives just in case)</param>
/// <returns></returns>
private bool IsGroupDead(List<EntityUid> list, bool checkOffStation)
{
var dead = 0;
foreach (var entity in list)
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
dead++;
}
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
dead++;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
dead++;
}
}
return dead == list.Count || list.Count == 0;
}
private static readonly string[] Outcomes =
{
// revs survived and heads survived... how

View File

@@ -1,42 +1,29 @@
using Content.Server.Chat.Managers;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Shared.Antag;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Humanoid;
using Content.Server.Antag;
using Robust.Server.Audio;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
{
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<WeightedRandomPrototype>]
const string BigObjectiveGroup = "ThiefBigObjectiveGroups";
[ValidatePrototypeId<WeightedRandomPrototype>]
const string SmallObjectiveGroup = "ThiefObjectiveGroups";
[ValidatePrototypeId<WeightedRandomPrototype>]
const string EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
private const float BigObjectiveChance = 0.7f;
public override void Initialize()
{
base.Initialize();
@@ -49,99 +36,95 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{
var query = EntityQueryEnumerator<ThiefRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var thief, out var gameRule))
var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out _))
{
//Chance to not lauch gamerule
if (_random.Prob(thief.RuleChance))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
//Chance to not launch the game rule
if (!_random.Prob(comp.RuleChance))
continue;
foreach (var player in ev.Players)
{
if (!ev.Profiles.TryGetValue(player.UserId, out var profile))
continue;
//Get all players eligible for this role, allow selecting existing antags
//TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:)
var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true);
thief.StartCandidates[player] = profile;
}
DoThiefStart(thief);
}
//Abort if there are none
if (eligiblePlayers.Count == 0)
continue;
//Calculate number of thieves to choose
var thiefCount = _random.Next(1, comp.MaxAllowThief + 1);
//Select our theives
var thieves = _antagSelection.ChooseAntags(thiefCount, eligiblePlayers);
MakeThief(thieves, comp, comp.PacifistThieves);
}
}
private void DoThiefStart(ThiefRuleComponent component)
public void MakeThief(List<EntityUid> players, ThiefRuleComponent thiefRule, bool addPacified)
{
if (!component.StartCandidates.Any())
foreach (var thief in players)
{
Log.Error("There are no players who can become thieves.");
return;
}
var startThiefCount = Math.Min(component.MaxAllowThief, component.StartCandidates.Count);
var thiefPool = _antagSelection.FindPotentialAntags(component.StartCandidates, component.ThiefPrototypeId);
//TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:)
//Add 1, as Next() is exclusive of maxValue
var numberOfThievesToSelect = _random.Next(1, startThiefCount + 1);
//While we dont have the correct number of thieves, and there are potential thieves remaining
while (component.ThievesMinds.Count < numberOfThievesToSelect && thiefPool.Count > 0)
{
Log.Info($"{numberOfThievesToSelect} thieves required, {component.ThievesMinds.Count} currently chosen, {thiefPool.Count} potentials");
var selectedThieves = _antagSelection.PickAntag(numberOfThievesToSelect - component.ThievesMinds.Count, thiefPool);
foreach (var thief in selectedThieves)
{
MakeThief(component, thief, component.PacifistThieves);
}
MakeThief(thief, thiefRule, addPacified);
}
}
public bool MakeThief(ThiefRuleComponent thiefRule, ICommonSession thief, bool addPacified)
public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified)
{
//checks
if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind))
{
Log.Info("Failed getting mind for picked thief.");
return false;
}
return;
if (HasComp<ThiefRoleComponent>(mindId))
{
Log.Error($"Player {thief.Name} is already a thief.");
return false;
}
if (mind.OwnedEntity is not { } entity)
{
Log.Error("Mind picked for thief did not have an attached entity.");
return false;
}
return;
// Assign thief roles
_roleSystem.MindAddRole(mindId, new ThiefRoleComponent
{
PrototypeId = thiefRule.ThiefPrototypeId
});
PrototypeId = thiefRule.ThiefPrototypeId,
}, silent: true);
//Add Pacified
//To Do: Long-term this should just be using the antag code to add components.
if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove.
{
EnsureComp<PacifiedComponent>(mind.OwnedEntity.Value);
EnsureComp<PacifiedComponent>(thief);
}
// Notificate player about new role assignment
if (_mindSystem.TryGetSession(mindId, out var session))
//Generate objectives
GenerateObjectives(mindId, mind, thiefRule);
//Send briefing here to account for humanoid/animal
_antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound);
// Give starting items
_inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems);
thiefRule.ThievesMinds.Add(mindId);
}
public void AdminMakeThief(EntityUid entity, bool addPacified)
{
var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
if (thiefRule == null)
{
_audio.PlayGlobal(thiefRule.GreetingSound, session);
_chatManager.DispatchServerMessage(session, MakeBriefing(mind.OwnedEntity.Value));
GameTicker.StartGameRule("Thief", out var ruleEntity);
thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
}
if (HasComp<ThiefRoleComponent>(entity))
return;
MakeThief(entity, thiefRule, addPacified);
}
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
{
// Give thieves their objectives
var difficulty = 0f;
if (_random.Prob(BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal)
if (_random.Prob(thiefRule.BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal)
{
var objective = _objectives.GetRandomObjective(mindId, mind, BigObjectiveGroup);
var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.BigObjectiveGroup);
if (objective != null)
{
_mindSystem.AddObjective(mindId, mind, objective.Value);
@@ -151,7 +134,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives
{
var objective = _objectives.GetRandomObjective(mindId, mind, SmallObjectiveGroup);
var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.SmallObjectiveGroup);
if (objective == null)
continue;
@@ -160,27 +143,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
}
//Escape target
var escapeObjective = _objectives.GetRandomObjective(mindId, mind, EscapeObjectiveGroup);
var escapeObjective = _objectives.GetRandomObjective(mindId, mind, thiefRule.EscapeObjectiveGroup);
if (escapeObjective != null)
_mindSystem.AddObjective(mindId, mind, escapeObjective.Value);
// Give starting items
_antagSelection.GiveAntagBagGear(mind.OwnedEntity.Value, thiefRule.StarterItems);
thiefRule.ThievesMinds.Add(mindId);
return true;
}
public void AdminMakeThief(ICommonSession thief, bool addPacified)
{
var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
if (thiefRule == null)
{
GameTicker.StartGameRule("Thief", out var ruleEntity);
thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
}
MakeThief(thiefRule, thief, addPacified);
}
//Add mind briefing

View File

@@ -1,13 +1,10 @@
using System.Linq;
using Content.Server.Antag;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.NPC.Systems;
using Content.Server.Objectives;
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Shuttles.Components;
using Content.Server.Traitor.Uplink;
using Content.Shared.CCVar;
using Content.Shared.Dataset;
@@ -15,17 +12,15 @@ using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Linq;
using System.Text;
namespace Content.Server.GameTicking.Rules;
@@ -35,16 +30,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
@@ -61,46 +55,45 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
//Set min players on game rule
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
}
protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
MakeCodewords(component);
}
protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (component.SelectionStatus == TraitorRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt)
if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime)
{
DoTraitorStart(component);
component.SelectionStatus = TraitorRuleComponent.SelectionState.Started;
}
}
/// <summary>
/// Check for enough players
/// </summary>
/// <param name="ev"></param>
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
MakeCodewords(traitor);
var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.SendAdminAnnouncement(Loc.GetString("traitor-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
ev.Cancel();
}
}
TryRoundStartAttempt(ev, Loc.GetString("traitor-title"));
}
private void MakeCodewords(TraitorRuleComponent component)
{
var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
var verbs = _prototypeManager.Index<DatasetPrototype>("verbs").Values;
var adjectives = _prototypeManager.Index<DatasetPrototype>(component.CodewordAdjectives).Values;
var verbs = _prototypeManager.Index<DatasetPrototype>(component.CodewordVerbs).Values;
var codewordPool = adjectives.Concat(verbs).ToList();
var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
component.Codewords = new string[finalCodewordCount];
@@ -112,125 +105,99 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
private void DoTraitorStart(TraitorRuleComponent component)
{
if (!component.StartCandidates.Any())
{
Log.Error("Tried to start Traitor mode without any candidates.");
var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId);
if (eligiblePlayers.Count == 0)
return;
}
var numTraitors = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerTraitor, 1, MaxTraitors);
var traitorPool = _antagSelection.FindPotentialAntags(component.StartCandidates, component.TraitorPrototypeId);
var selectedTraitors = _antagSelection.PickAntag(numTraitors, traitorPool);
var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors);
foreach (var traitor in selectedTraitors)
{
MakeTraitor(traitor);
}
var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers);
component.SelectionStatus = TraitorRuleComponent.SelectionState.SelectionMade;
MakeTraitor(selectedTraitors, component);
}
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
//Start the timer
var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out var gameRuleComponent))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
foreach (var player in ev.Players)
{
if (!ev.Profiles.ContainsKey(player.UserId))
continue;
traitor.StartCandidates[player] = ev.Profiles[player.UserId];
}
var delay = TimeSpan.FromSeconds(
_cfg.GetCVar(CCVars.TraitorStartDelay) +
_random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
traitor.AnnounceAt = _gameTiming.CurTime + delay;
//Set the delay for choosing traitors
comp.AnnounceAt = _timing.CurTime + delay;
traitor.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToSelect;
comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart;
}
}
public bool MakeTraitor(ICommonSession traitor, bool giveUplink = true, bool giveObjectives = true)
public bool MakeTraitor(List<EntityUid> traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
{
var traitorRule = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
if (traitorRule == null)
foreach (var traitor in traitors)
{
//todo fuck me this shit is awful
//no i wont fuck you, erp is against rules
GameTicker.StartGameRule("Traitor", out var ruleEntity);
traitorRule = Comp<TraitorRuleComponent>(ruleEntity);
MakeCodewords(traitorRule);
MakeTraitor(traitor, component, giveUplink, giveObjectives);
}
return true;
}
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
{
//Grab the mind if it wasnt provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
{
Log.Info("Failed getting mind for picked traitor.");
return false;
}
if (HasComp<TraitorRoleComponent>(mindId))
{
Log.Error($"Player {traitor.Name} is already a traitor.");
Log.Error($"Player {mind.CharacterName} is already a traitor.");
return false;
}
if (mind.OwnedEntity is not { } entity)
{
Log.Error("Mind picked for traitor did not have an attached entity.");
return false;
}
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
// Calculate the amount of currency on the uplink.
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
// Give traitors their codewords and uplink code to keep in their character info menu
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", traitorRule.Codewords)));
Note[]? code = null;
if (giveUplink)
{
// Calculate the amount of currency on the uplink.
var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
// creadth: we need to create uplink for the antag.
// PDA should be in place already
var pda = _uplink.FindUplinkTarget(mind.OwnedEntity!.Value);
if (pda == null || !_uplink.AddUplink(mind.OwnedEntity.Value, startingBalance))
var pda = _uplink.FindUplinkTarget(traitor);
if (pda == null || !_uplink.AddUplink(traitor, startingBalance))
return false;
// Give traitors their codewords and uplink code to keep in their character info menu
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// If giveUplink is false the uplink code part is omitted
briefing = string.Format("{0}\n{1}", briefing,
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp","#"))));
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
}
// Prepare traitor role
var traitorRole = new TraitorRoleComponent
{
PrototypeId = traitorRule.TraitorPrototypeId,
};
_antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
component.TraitorMinds.Add(mindId);
// Assign traitor roles
_roleSystem.MindAddRole(mindId, new TraitorRoleComponent
{
PrototypeId = traitorRule.TraitorPrototypeId
}, mind);
// Assign briefing and greeting sound
PrototypeId = component.TraitorPrototypeId
}, mind, true);
// Assign briefing
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
{
Briefing = briefing
}, mind);
_roleSystem.MindPlaySound(mindId, traitorRule.GreetSoundNotification, mind);
SendTraitorBriefing(mindId, traitorRule.Codewords, code);
traitorRule.TraitorMinds.Add(mindId);
Briefing = briefing.ToString()
}, mind, true);
// Change the faction
_npcFaction.RemoveFaction(entity, "NanoTrasen", false);
_npcFaction.AddFaction(entity, "Syndicate");
_npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(traitor, component.SyndicateFaction);
// Give traitors their objectives
if (giveObjectives)
@@ -241,7 +208,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId, mind, "TraitorObjectiveGroups");
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
if (objective == null)
continue;
@@ -255,54 +222,26 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return true;
}
/// <summary>
/// Send a codewords and uplink codes to traitor chat.
/// </summary>
/// <param name="mind">A mind (player)</param>
/// <param name="codewords">Codewords</param>
/// <param name="code">Uplink codes</param>
private void SendTraitorBriefing(EntityUid mind, string[] codewords, Note[]? code)
{
if (!_mindSystem.TryGetSession(mind, out var session))
return;
_chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-greeting"));
_chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (code != null)
_chatManager.DispatchServerMessage(session, Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", code).Replace("sharp","#"))));
}
private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
{
var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var traitor, out var gameRule))
var query = QueryActiveRules();
while (query.MoveNext(out _, out var comp, out _))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
if (comp.TotalTraitors >= MaxTraitors)
continue;
if (traitor.TotalTraitors >= MaxTraitors)
continue;
if (!ev.LateJoin)
continue;
if (!ev.Profile.AntagPreferences.Contains(traitor.TraitorPrototypeId))
if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId))
continue;
if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
//If its before we have selected traitors, continue
if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started)
continue;
if (!job.CanBeAntag)
continue;
// Before the announcement is made, late-joiners are considered the same as players who readied.
if (traitor.SelectionStatus < TraitorRuleComponent.SelectionState.SelectionMade)
{
traitor.StartCandidates[ev.Player] = ev.Profile;
continue;
}
// the nth player we adjust our probabilities around
var target = PlayersPerTraitor * traitor.TotalTraitors + 1;
var target = PlayersPerTraitor * comp.TotalTraitors + 1;
var chance = 1f / PlayersPerTraitor;
// If we have too many traitors, divide by how many players below target for next traitor we are.
@@ -322,7 +261,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
// You get one shot.
if (_random.Prob(chance))
{
MakeTraitor(ev.Player);
MakeTraitor(ev.Mob, comp);
}
}
}
@@ -338,6 +277,38 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
/// <summary>
/// Start this game rule manually
/// </summary>
public TraitorRuleComponent StartGameRule()
{
var comp = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
if (comp == null)
{
GameTicker.StartGameRule("Traitor", out var ruleEntity);
comp = Comp<TraitorRuleComponent>(ruleEntity);
}
return comp;
}
public void MakeTraitorAdmin(EntityUid entity, bool giveUplink, bool giveObjectives)
{
var traitorRule = StartGameRule();
MakeTraitor(entity, traitorRule, giveUplink, giveObjectives);
}
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode)
{
var sb = new StringBuilder();
sb.AppendLine(Loc.GetString("traitor-role-greeting"));
sb.AppendLine(Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", codewords))));
if (uplinkCode != null)
sb.AppendLine(Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
return sb.ToString();
}
public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
{
List<(EntityUid Id, MindComponent Mind)> allTraitors = new();

View File

@@ -1,13 +1,9 @@
using System.Globalization;
using System.Linq;
using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Antag;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Popups;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
@@ -18,17 +14,14 @@ using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Zombies;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Globalization;
namespace Content.Server.GameTicking.Rules;
@@ -36,10 +29,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly PopupSystem _popup = default!;
@@ -49,8 +39,8 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@@ -61,6 +51,16 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
/// <summary>
/// Set the required minimum players for this gamemode to start
/// </summary>
protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
}
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
foreach (var zombie in EntityQuery<ZombieRuleComponent>())
@@ -113,85 +113,59 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// <summary>
/// The big kahoona function for checking if the round is gonna end
/// </summary>
private void CheckRoundEnd()
private void CheckRoundEnd(ZombieRuleComponent zombieRuleComponent)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var comp, out var gameRule))
var healthy = GetHealthyHumans();
if (healthy.Count == 1) // Only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]);
if (GetInfectedFraction(false) > zombieRuleComponent.ZombieShuttleCallPercentage && !_roundEnd.IsRoundEndRequested())
{
if (!GameTicker.IsGameRuleActive(uid, gameRule))
continue;
var healthy = GetHealthyHumans();
if (healthy.Count == 1) // Only one human left. spooky
_popup.PopupEntity(Loc.GetString("zombie-alone"), healthy[0], healthy[0]);
if (!comp.ShuttleCalled && GetInfectedFraction(false) >= comp.ZombieShuttleCallPercentage)
foreach (var station in _station.GetStations())
{
comp.ShuttleCalled = true;
foreach (var station in _station.GetStations())
{
_chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson);
}
_roundEnd.RequestRoundEnd(null, false);
_chat.DispatchStationAnnouncement(station, Loc.GetString("zombie-shuttle-call"), colorOverride: Color.Crimson);
}
// we include dead for this count because we don't want to end the round
// when everyone gets on the shuttle.
if (GetInfectedFraction() >= 1) // Oops, all zombies
_roundEnd.EndRound();
_roundEnd.RequestRoundEnd(null, false);
}
// we include dead for this count because we don't want to end the round
// when everyone gets on the shuttle.
if (GetInfectedFraction() >= 1) // Oops, all zombies
_roundEnd.EndRound();
}
/// <summary>
/// Check we have enough players to start this game mode, if not - cancel and announce
/// </summary>
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.SendAdminAnnouncement(Loc.GetString("zombie-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length),
("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
ev.Cancel();
}
}
TryRoundStartAttempt(ev, Loc.GetString("zombie-title"));
}
protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.StartTime = _timing.CurTime + _random.Next(component.MinStartDelay, component.MaxStartDelay);
var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay);
component.StartTime = _timing.CurTime + delay;
}
protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (component.InfectedChosen)
if (component.StartTime.HasValue && component.StartTime < _timing.CurTime)
{
if (_timing.CurTime >= component.NextRoundEndCheck)
{
component.NextRoundEndCheck += component.EndCheckDelay;
CheckRoundEnd();
}
return;
InfectInitialPlayers(component);
component.StartTime = null;
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
}
if (component.StartTime == null || _timing.CurTime < component.StartTime)
return;
InfectInitialPlayers(component);
if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime)
{
CheckRoundEnd(component);
component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
}
}
private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
@@ -201,6 +175,12 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
Del(component.Action.Value);
}
/// <summary>
/// Get the fraction of players that are infected, between 0 and 1
/// </summary>
/// <param name="includeOffStation">Include healthy players that are not on the station grid</param>
/// <param name="includeDead">Should dead zombies be included in the count</param>
/// <returns></returns>
private float GetInfectedFraction(bool includeOffStation = true, bool includeDead = false)
{
var players = GetHealthyHumans(includeOffStation);
@@ -264,87 +244,55 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
/// </remarks>
private void InfectInitialPlayers(ZombieRuleComponent component)
{
if (component.InfectedChosen)
return;
component.InfectedChosen = true;
//Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent
var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.PatientZeroPrototypeId, includeAllJobs: true, customExcludeCondition: x => HasComp<ZombieImmuneComponent>(x) || HasComp<InitialInfectedExemptComponent>(x));
//And get all players, excluding ZombieImmune - to fill any leftover initial infected slots
var allPlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.PatientZeroPrototypeId, acceptableAntags: Shared.Antag.AntagAcceptability.All, includeAllJobs: true, ignorePreferences: true, customExcludeCondition: HasComp<ZombieImmuneComponent>);
var allPlayers = _playerManager.Sessions.ToList();
var playerList = new List<ICommonSession>();
var prefList = new List<ICommonSession>();
foreach (var player in allPlayers)
{
if (player.AttachedEntity == null || !HasComp<HumanoidAppearanceComponent>(player.AttachedEntity) ||
HasComp<ZombieImmuneComponent>(player.AttachedEntity) || !_jobs.CanBeAntag(player))
continue;
if (HasComp<InitialInfectedExemptComponent>(player.AttachedEntity))
continue; // used (for example) on ERT
playerList.Add(player);
var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeId))
prefList.Add(player);
}
if (playerList.Count == 0)
//If there are no players to choose, abort
if (allPlayers.Count == 0)
return;
var numInfected = Math.Max(1,
(int) Math.Min(
Math.Floor((double) playerList.Count / component.PlayersPerInfected), component.MaxInitialInfected));
//How many initial infected should we select
var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected);
var totalInfected = 0;
while (totalInfected < numInfected)
//Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players
var initialInfected = _antagSelection.ChooseAntags(initialInfectedCount, eligiblePlayers, allPlayers);
//Make brain craving
MakeZombie(initialInfected, component);
//Send the briefing, play greeting sound
_antagSelection.SendBriefing(initialInfected, Loc.GetString("zombie-patientzero-role-greeting"), Color.Plum, component.InitialInfectedSound);
}
private void MakeZombie(List<EntityUid> entities, ZombieRuleComponent component)
{
foreach (var entity in entities)
{
ICommonSession zombie;
if (prefList.Count == 0)
{
if (playerList.Count == 0)
{
Log.Info("Insufficient number of players. stopping selection.");
break;
}
zombie = _random.Pick(playerList);
Log.Info("Insufficient preferred patient 0, picking at random.");
}
else
{
zombie = _random.Pick(prefList);
Log.Info("Selected a patient 0.");
}
prefList.Remove(zombie);
playerList.Remove(zombie);
if (!_mindSystem.TryGetMind(zombie, out var mindId, out var mind) ||
mind.OwnedEntity is not { } ownedEntity)
{
continue;
}
totalInfected++;
_roles.MindAddRole(mindId, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId });
var pending = EnsureComp<PendingZombieComponent>(ownedEntity);
pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
EnsureComp<ZombifyOnDeathComponent>(ownedEntity);
EnsureComp<IncurableZombieComponent>(ownedEntity);
var inCharacterName = MetaData(ownedEntity).EntityName;
_action.AddAction(ownedEntity, ref pending.Action, ZombieRuleComponent.ZombifySelfActionPrototype, ownedEntity);
var message = Loc.GetString("zombie-patientzero-role-greeting");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
//gets the names now in case the players leave.
//this gets unhappy if people with the same name get chosen. Probably shouldn't happen.
component.InitialInfectedNames.Add(inCharacterName, zombie.Name);
// I went all the way to ChatManager.cs and all i got was this lousy T-shirt
// You got a free T-shirt!?!?
_chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message,
wrappedMessage, default, false, zombie.Channel, Color.Plum);
_audio.PlayGlobal(component.InitialInfectedSound, ownedEntity);
MakeZombie(entity, component);
}
}
private void MakeZombie(EntityUid entity, ZombieRuleComponent component)
{
if (!_mindSystem.TryGetMind(entity, out var mind, out var mindComponent))
return;
//Add the role to the mind silently (to avoid repeating job assignment)
_roles.MindAddRole(mind, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId }, silent: true);
//Add the zombie components and grace period
var pending = EnsureComp<PendingZombieComponent>(entity);
pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
EnsureComp<ZombifyOnDeathComponent>(entity);
EnsureComp<IncurableZombieComponent>(entity);
//Add the zombify action
_action.AddAction(entity, ref pending.Action, component.ZombifySelfActionPrototype, entity);
//Get names for the round end screen, incase they leave mid-round
var inCharacterName = MetaData(entity).EntityName;
var accountName = mindComponent.Session == null ? string.Empty : mindComponent.Session.Name;
component.InitialInfectedNames.Add(inCharacterName, accountName);
}
}