diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
index 8512107b69d..de51b2fb192 100644
--- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
+++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
@@ -58,7 +58,7 @@ await _pair.Server.WaitPost(() =>
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
- _spawnSys.EquipStartingGear(_entity, _gear);
+ _spawnSys.EquipStartingGear(_entity, _gear, null);
server.EntMan.DeleteEntity(_entity);
}
});
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 867dcbc2692..8087d1833e6 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -118,11 +118,8 @@ private void SetLayerData(
/// This should not be used if the entity is owned by the server. The server will otherwise
/// override this with the appearance data it sends over.
///
- public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
+ public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
{
- if (profile == null)
- return;
-
if (!Resolve(uid, ref humanoid))
{
return;
diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
deleted file mode 100644
index f539daee367..00000000000
--- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
+++ /dev/null
@@ -1,208 +0,0 @@
-/* WD edit
-
-#nullable enable
-using System.Linq;
-using Content.Server.Body.Components;
-using Content.Server.GameTicking;
-using Content.Server.GameTicking.Presets;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Mind;
-using Content.Server.NPC.Systems;
-using Content.Server.Pinpointer;
-using Content.Server.Roles;
-using Content.Server.Shuttles.Components;
-using Content.Server.Station.Components;
-using Content.Shared.CCVar;
-using Content.Shared.Damage;
-using Content.Shared.FixedPoint;
-using Content.Shared.GameTicking;
-using Content.Shared.Hands.Components;
-using Content.Shared.Inventory;
-using Content.Shared.NukeOps;
-using Robust.Server.GameObjects;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Map.Components;
-
-namespace Content.IntegrationTests.Tests.GameRules;
-
-[TestFixture]
-public sealed class NukeOpsTest
-{
- ///
- /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
- ///
- [Test]
- public async Task TryStopNukeOpsFromConstantlyFailing()
- {
- await using var pair = await PoolManager.GetServerClient(new PoolSettings
- {
- Dirty = true,
- DummyTicker = false,
- Connected = true,
- InLobby = true
- });
-
- var server = pair.Server;
- var client = pair.Client;
- var entMan = server.EntMan;
- var mapSys = server.System();
- var ticker = server.System();
- var mindSys = server.System();
- var roleSys = server.System();
- var invSys = server.System();
- var factionSys = server.System();
-
- Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False);
- server.CfgMan.SetCVar(CCVars.GridFill, true);
-
- // Initially in the lobby
- Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
- Assert.That(client.AttachedEntity, Is.Null);
- Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
-
- // There are no grids or maps
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
-
- // And no nukie related components
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
- Assert.That(entMan.Count(), Is.Zero);
-
- // Ready up and start nukeops
- await pair.WaitClientCommand("toggleready True");
- Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
- await pair.WaitCommand("forcepreset Nukeops");
- await pair.RunTicksSync(10);
-
- // Game should have started
- Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
- Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
- Assert.That(client.EntMan.EntityExists(client.AttachedEntity));
- var player = pair.Player!.AttachedEntity!.Value;
- Assert.That(entMan.EntityExists(player));
-
- // Maps now exist
- Assert.That(entMan.Count(), Is.GreaterThan(0));
- Assert.That(entMan.Count(), Is.GreaterThan(0));
- Assert.That(entMan.Count(), Is.EqualTo(2)); // The main station & nukie station
- Assert.That(entMan.Count(), Is.GreaterThan(3)); // Each station has at least 1 grid, plus some shuttles
- Assert.That(entMan.Count(), Is.EqualTo(1));
-
- // And we now have nukie related components
- Assert.That(entMan.Count(), Is.EqualTo(1));
- Assert.That(entMan.Count(), Is.EqualTo(1));
- Assert.That(entMan.Count(), Is.EqualTo(1));
- Assert.That(entMan.Count(), Is.EqualTo(1));
-
- // The player entity should be the nukie commander
- var mind = mindSys.GetMind(player)!.Value;
- Assert.That(entMan.HasComponent(player));
- Assert.That(roleSys.MindIsAntagonist(mind));
- Assert.That(roleSys.MindHasRole(mind));
-
- Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
- Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
-
- var roles = roleSys.MindGetAllRoles(mind);
- var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
- Assert.That(cmdRoles.Count(), Is.EqualTo(1));
-
- // The game rule exists, and all the stations/shuttles/maps are properly initialized
- var rule = entMan.AllComponents().Single().Component;
- var mapRule = entMan.AllComponents().Single().Component;
- foreach (var grid in mapRule.MapGrids)
- {
- Assert.That(entMan.EntityExists(grid));
- Assert.That(entMan.HasComponent(grid));
- Assert.That(entMan.HasComponent(grid));
- }
- Assert.That(entMan.EntityExists(rule.TargetStation));
-
- Assert.That(entMan.HasComponent(rule.TargetStation));
-
- var nukieShuttlEnt = entMan.AllComponents().FirstOrDefault().Uid;
- Assert.That(entMan.EntityExists(nukieShuttlEnt));
-
- EntityUid? nukieStationEnt = null;
- foreach (var grid in mapRule.MapGrids)
- {
- if (entMan.HasComponent(grid))
- {
- nukieStationEnt = grid;
- break;
- }
- }
-
- Assert.That(entMan.EntityExists(nukieStationEnt));
- var nukieStation = entMan.GetComponent(nukieStationEnt!.Value);
-
- Assert.That(entMan.EntityExists(nukieStation.Station));
- Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
-
- Assert.That(server.MapMan.MapExists(mapRule.Map));
- var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
-
- var targetStation = entMan.GetComponent(rule.TargetStation!.Value);
- var targetGrid = targetStation.Grids.First();
- var targetMap = entMan.GetComponent(targetGrid).MapUid!.Value;
- Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
-
- Assert.That(entMan.GetComponent(player).MapUid, Is.EqualTo(nukieMap));
- Assert.That(entMan.GetComponent(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
- Assert.That(entMan.GetComponent(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap));
-
- // The maps are all map-initialized, including the player
- // Yes, this is necessary as this has repeatedly been broken somehow.
- Assert.That(mapSys.IsInitialized(nukieMap));
- Assert.That(mapSys.IsInitialized(targetMap));
- Assert.That(mapSys.IsPaused(nukieMap), Is.False);
- Assert.That(mapSys.IsPaused(targetMap), Is.False);
-
- EntityLifeStage LifeStage(EntityUid? uid) => entMan.GetComponent(uid!.Value).EntityLifeStage;
- Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized));
- Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized));
-
- // Make sure the player has hands. We've had fucking disarmed nukies before.
- Assert.That(entMan.HasComponent(player));
- Assert.That(entMan.GetComponent(player).Hands.Count, Is.GreaterThan(0));
-
- // While we're at it, lets make sure they aren't naked. I don't know how many inventory slots all mobs will be
- // likely to have in the future. But nukies should probably have at least 3 slots with something in them.
- var enumerator = invSys.GetSlotEnumerator(player);
- int total = 0;
- while (enumerator.NextItem(out _))
- {
- total++;
- }
- Assert.That(total, Is.GreaterThan(3));
-
- // Finally lets check the nukie commander passed basic training and figured out how to breathe.
- var totalSeconds = 30;
- var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds);
- int increment = 5;
- var resp = entMan.GetComponent(player);
- var damage = entMan.GetComponent(player);
- for (var tick = 0; tick < totalTicks; tick += increment)
- {
- await pair.RunTicksSync(increment);
- Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
- Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
- }
-
- //ticker.SetGamePreset((GamePresetPrototype?)null); WD edit
- server.CfgMan.SetCVar(CCVars.GridFill, false);
- await pair.CleanReturnAsync();
- }
-}
-
-*/
diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
index ffaff3b8ded..1e3f9c9854f 100644
--- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
@@ -1,6 +1,5 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Commands;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
index 5d7ae8efbf4..0f665a63de0 100644
--- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
@@ -17,7 +17,6 @@ public async Task TestSecretStarts()
var server = pair.Server;
await server.WaitIdleAsync();
- var entMan = server.ResolveDependency();
var gameTicker = server.ResolveDependency().GetEntitySystem();
await server.WaitAssertion(() =>
@@ -33,7 +32,10 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule.");
+ foreach (var rule in gameTicker.GetAddedGameRules())
+ {
+ Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule));
+ }
// End all rules
gameTicker.ClearGameRules();
diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs
index 04fd38598fb..6f10ef9b479 100644
--- a/Content.Server/Administration/ServerApi.cs
+++ b/Content.Server/Administration/ServerApi.cs
@@ -8,7 +8,6 @@
using System.Threading.Tasks;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Maps;
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index df77a3a1a78..9849d2df79c 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -1,37 +1,23 @@
-using Content.Server.Administration.Commands;
-using Content.Server.Antag;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Shared.Database;
+using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
-using Content.Shared.Roles;
using Content.Shared.Verbs;
using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
public sealed partial class AdminVerbSystem
{
- [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
-
- [ValidatePrototypeId]
- private const string DefaultTraitorRule = "Traitor";
-
- [ValidatePrototypeId]
- private const string DefaultNukeOpRule = "LoneOpsSpawn";
-
- [ValidatePrototypeId]
- private const string DefaultRevsRule = "Revolutionary";
-
- [ValidatePrototypeId]
- private const string DefaultThiefRule = "Thief";
-
- [ValidatePrototypeId]
- private const string PirateGearId = "PirateGear";
+ [Dependency] private readonly ThiefRuleSystem _thief = default!;
+ [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
+ [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
+ [Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
+ [Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent args)
@@ -54,7 +40,9 @@ private void AddAntagVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
Act = () =>
{
- _antag.ForceMakeAntag(player, DefaultTraitorRule);
+ // if its a monkey or mouse or something dont give uplink or objectives
+ var isHuman = HasComp(args.Target);
+ _traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-traitor"),
@@ -83,7 +71,7 @@ private void AddAntagVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
Act = () =>
{
- _antag.ForceMakeAntag(player, DefaultNukeOpRule);
+ _nukeopsRule.MakeLoneNukie(args.Target);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-nuclear-operative"),
@@ -97,14 +85,14 @@ private void AddAntagVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
Act = () =>
{
- // pirates just get an outfit because they don't really have logic associated with them
- SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
+ _piratesRule.MakePirate(args.Target);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-pirate"),
};
args.Verbs.Add(pirate);
+ //todo come here at some point dear lort.
Verb headRev = new()
{
Text = Loc.GetString("admin-verb-text-make-head-rev"),
@@ -112,7 +100,7 @@ private void AddAntagVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
Act = () =>
{
- _antag.ForceMakeAntag(player, DefaultRevsRule);
+ _revolutionaryRule.OnHeadRevAdmin(args.Target);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-head-rev"),
@@ -126,7 +114,7 @@ private void AddAntagVerbs(GetVerbsEvent args)
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/ihscombat.rsi"), "icon"),
Act = () =>
{
- _antag.ForceMakeAntag(player, DefaultThiefRule);
+ _thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-thief"),
diff --git a/Content.Server/Antag/AntagSelectionPlayerPool.cs b/Content.Server/Antag/AntagSelectionPlayerPool.cs
deleted file mode 100644
index 87873e96d1a..00000000000
--- a/Content.Server/Antag/AntagSelectionPlayerPool.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-
-namespace Content.Server.Antag;
-
-public sealed class AntagSelectionPlayerPool (List> orderedPools)
-{
- public bool TryPickAndTake(IRobustRandom random, [NotNullWhen(true)] out ICommonSession? session)
- {
- session = null;
-
- foreach (var pool in orderedPools)
- {
- if (pool.Count == 0)
- continue;
-
- session = random.PickAndTake(pool);
- break;
- }
-
- return session != null;
- }
-
- public int Count => orderedPools.Sum(p => p.Count);
-}
diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs
deleted file mode 100644
index 470f98fca1d..00000000000
--- a/Content.Server/Antag/AntagSelectionSystem.API.cs
+++ /dev/null
@@ -1,303 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Content.Server.Antag.Components;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Objectives;
-using Content.Shared.Chat;
-using Content.Shared.Mind;
-using JetBrains.Annotations;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-
-namespace Content.Server.Antag;
-
-public sealed partial class AntagSelectionSystem
-{
- ///
- /// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
- ///
- public bool TryGetNextAvailableDefinition(Entity ent,
- [NotNullWhen(true)] out AntagSelectionDefinition? definition)
- {
- definition = null;
-
- var totalTargetCount = GetTargetAntagCount(ent);
- var mindCount = ent.Comp.SelectedMinds.Count;
- if (mindCount >= totalTargetCount)
- return false;
-
- foreach (var def in ent.Comp.Definitions)
- {
- var target = GetTargetAntagCount(ent, null, def);
-
- if (mindCount < target)
- {
- definition = def;
- return true;
- }
-
- mindCount -= target;
- }
-
- return false;
- }
-
- ///
- /// Gets the number of antagonists that should be present for a given rule based on the provided pool.
- /// A null pool will simply use the player count.
- ///
- public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool = null)
- {
- var count = 0;
- foreach (var def in ent.Comp.Definitions)
- {
- count += GetTargetAntagCount(ent, pool, def);
- }
-
- return count;
- }
-
- ///
- /// Gets the number of antagonists that should be present for a given antag definition based on the provided pool.
- /// A null pool will simply use the player count.
- ///
- public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
- {
- var poolSize = pool?.Count ?? _playerManager.Sessions.Length;
- // factor in other definitions' affect on the count.
- var countOffset = 0;
- foreach (var otherDef in ent.Comp.Definitions)
- {
- countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
- }
- // make sure we don't double-count the current selection
- countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
-
- return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max);
- }
-
- ///
- /// Returns identifiable information for all antagonists to be used in a round end summary.
- ///
- ///
- /// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
- ///
- public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return new List<(EntityUid, SessionData, string)>();
-
- var output = new List<(EntityUid, SessionData, string)>();
- foreach (var (mind, name) in ent.Comp.SelectedMinds)
- {
- if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
- continue;
-
- if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data))
- continue;
-
- output.Add((mind, data, name));
- }
- return output;
- }
-
- ///
- /// Returns all the minds of antagonists.
- ///
- public List> GetAntagMinds(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return new();
-
- var output = new List>();
- foreach (var (mind, _) in ent.Comp.SelectedMinds)
- {
- if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
- continue;
-
- output.Add((mind, mindComp));
- }
- return output;
- }
-
- ///
- /// Helper specifically for
- ///
- public List GetAntagMindEntityUids(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return new();
-
- return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
- }
-
- ///
- /// Returns all the antagonists for this rule who are currently alive
- ///
- public IEnumerable GetAliveAntags(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- yield break;
-
- var minds = GetAntagMinds(ent);
- foreach (var mind in minds)
- {
- if (_mind.IsCharacterDeadIc(mind))
- continue;
-
- if (mind.Comp.OriginalOwnedEntity != null)
- yield return GetEntity(mind.Comp.OriginalOwnedEntity.Value);
- }
- }
-
- ///
- /// Returns the number of alive antagonists for this rule.
- ///
- public int GetAliveAntagCount(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return 0;
-
- var numbah = 0;
- var minds = GetAntagMinds(ent);
- foreach (var mind in minds)
- {
- if (_mind.IsCharacterDeadIc(mind))
- continue;
-
- numbah++;
- }
-
- return numbah;
- }
-
- ///
- /// Returns if there are any remaining antagonists alive for this rule.
- ///
- public bool AnyAliveAntags(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return false;
-
- return GetAliveAntags(ent).Any();
- }
-
- ///
- /// Checks if all the antagonists for this rule are alive.
- ///
- public bool AllAntagsAlive(Entity ent)
- {
- if (!Resolve(ent, ref ent.Comp, false))
- return false;
-
- return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
- }
-
- ///
- /// Helper method to send the briefing text and sound to a player entity
- ///
- /// The entity chosen to be antag
- /// The briefing text to send
- /// The color the briefing should be, null for default
- /// The sound to briefing/greeting sound to play
- public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
- {
- if (!_mind.TryGetMind(entity, out _, out var mindComponent))
- return;
-
- if (mindComponent.Session == null)
- return;
-
- SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
- }
-
- ///
- /// Helper method to send the briefing text and sound to a list of sessions
- ///
- /// The sessions that will be sent the briefing
- /// The briefing text to send
- /// The color the briefing should be, null for default
- /// The sound to briefing/greeting sound to play
- [PublicAPI]
- public void SendBriefing(List sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
- {
- foreach (var session in sessions)
- {
- SendBriefing(session, briefing, briefingColor, briefingSound);
- }
- }
-
- ///
- /// Helper method to send the briefing text and sound to a session
- ///
- /// The player chosen to be an antag
- /// The briefing data
- public void SendBriefing(
- ICommonSession? session,
- BriefingData? data)
- {
- if (session == null || data == null)
- return;
-
- var text = data.Value.Text == null ? string.Empty : Loc.GetString(data.Value.Text);
- SendBriefing(session, text, data.Value.Color, data.Value.Sound);
- }
-
- ///
- /// Helper method to send the briefing text and sound to a session
- ///
- /// The player chosen to be an antag
- /// The briefing text to send
- /// The color the briefing should be, null for default
- /// The sound to briefing/greeting sound to play
- public void SendBriefing(
- ICommonSession? session,
- string briefing,
- Color? briefingColor,
- SoundSpecifier? briefingSound)
- {
- if (session == null)
- return;
-
- _audio.PlayGlobal(briefingSound, session);
- if (!string.IsNullOrEmpty(briefing))
- {
- var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
- _chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel,
- briefingColor);
- }
- }
-
- ///
- /// This technically is a gamerule-ent-less way to make an entity an antag.
- /// You should almost never be using this.
- ///
- public void ForceMakeAntag(ICommonSession? player, string defaultRule) where T : Component
- {
- var rule = ForceGetGameRuleEnt(defaultRule);
-
- if (!TryGetNextAvailableDefinition(rule, out var def))
- def = rule.Comp.Definitions.Last();
-
- MakeAntag(rule, player, def.Value);
- }
-
- ///
- /// Tries to grab one of the weird specific antag gamerule ents or starts a new one.
- /// This is gross code but also most of this is pretty gross to begin with.
- ///
- public Entity ForceGetGameRuleEnt(string id) where T : Component
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var comp))
- {
- return (uid, comp);
- }
- var ruleEnt = GameTicker.AddGameRule(id);
- RemComp(ruleEnt);
- var antag = Comp(ruleEnt);
- antag.SelectionsComplete = true; // don't do normal selection.
- GameTicker.StartGameRule(ruleEnt);
- return (ruleEnt, antag);
- }
-}
diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs
index 6bfb7394f5b..b11c562df5a 100644
--- a/Content.Server/Antag/AntagSelectionSystem.cs
+++ b/Content.Server/Antag/AntagSelectionSystem.cs
@@ -1,461 +1,347 @@
-using System.Linq;
-using Content.Server.Antag.Components;
-using Content.Server.Chat.Managers;
-using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
-using Content.Server.Ghost.Roles;
-using Content.Server.Ghost.Roles.Components;
+using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Preferences.Managers;
-using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
-using Content.Server.Station.Systems;
using Content.Shared.Antag;
-using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Players;
using Content.Shared.Preferences;
+using Content.Shared.Roles;
using Robust.Server.Audio;
-using Robust.Server.GameObjects;
-using Robust.Server.Player;
-using Robust.Shared.Enums;
-using Robust.Shared.Map;
+using Robust.Shared.Audio;
using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using System.Linq;
+using Content.Shared.Chat;
+using Robust.Shared.Enums;
namespace Content.Server.Antag;
-public sealed partial class AntagSelectionSystem : GameRuleSystem
+public sealed class AntagSelectionSystem : GameRuleSystem
{
- [Dependency] private readonly IChatManager _chat = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IServerPreferencesManager _pref = default!;
- [Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly GhostRoleSystem _ghostRole = default!;
+ [Dependency] private readonly IServerPreferencesManager _prefs = default!;
+ [Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly JobSystem _jobs = default!;
- [Dependency] private readonly MindSystem _mind = default!;
- [Dependency] private readonly RoleSystem _role = default!;
- [Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
- [Dependency] private readonly TransformSystem _transform = default!;
-
- // arbitrary random number to give late joining some mild interest.
- public const float LateJoinRandomChance = 0.5f;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+ [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
- ///
- public override void Initialize()
+ #region Eligible Player Selection
+ ///
+ /// Get all players that are eligible for an antag role
+ ///
+ /// All sessions from which to select eligible players
+ /// The prototype to get eligible players for
+ /// Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included
+ /// Should players already selected as antags be eligible
+ /// Should we ignore if the player has enabled this specific role
+ /// A custom condition that each player is tested against, if it returns true the player is excluded from eligibility
+ /// List of all player entities that match the requirements
+ public List GetEligiblePlayers(IEnumerable playerSessions,
+ ProtoId antagPrototype,
+ bool includeAllJobs = false,
+ AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
+ bool ignorePreferences = false,
+ bool allowNonHumanoids = false,
+ Func? customExcludeCondition = null)
{
- base.Initialize();
-
- SubscribeLocalEvent(OnTakeGhostRole);
-
- SubscribeLocalEvent(OnPlayerSpawning);
- SubscribeLocalEvent(OnJobsAssigned);
- SubscribeLocalEvent(OnSpawnComplete);
- }
+ var eligiblePlayers = new List();
- private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args)
- {
- if (args.TookRole)
- return;
-
- if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def)
- return;
-
- if (!Exists(rule) || !TryComp(rule, out var select))
- return;
+ foreach (var player in playerSessions)
+ {
+ if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
+ eligiblePlayers.Add(player.AttachedEntity!.Value);
+ }
- MakeAntag((rule, select), args.Player, def, ignoreSpawner: true);
- args.TookRole = true;
- _ghostRole.UnregisterGhostRole((ent, Comp(ent)));
+ return eligiblePlayers;
}
- private void OnPlayerSpawning(RulePlayerSpawningEvent args)
+ ///
+ /// Get all sessions that are eligible for an antag role, can be run prior to sessions being attached to an entity
+ /// This does not exclude sessions that have already been chosen as antags - that must be handled manually
+ ///
+ /// All sessions from which to select eligible players
+ /// The prototype to get eligible players for
+ /// Should we ignore if the player has enabled this specific role
+ /// List of all player sessions that match the requirements
+ public List GetEligibleSessions(IEnumerable playerSessions, ProtoId antagPrototype, bool ignorePreferences = false)
{
- var pool = args.PlayerPool;
+ var eligibleSessions = new List();
- var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var comp, out _))
+ foreach (var session in playerSessions)
{
- if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
- continue;
-
- if (comp.SelectionsComplete)
- return;
-
- ChooseAntags((uid, comp), pool);
- comp.SelectionsComplete = true;
-
- foreach (var session in comp.SelectedSessions)
- {
- args.PlayerPool.Remove(session);
- GameTicker.PlayerJoinGame(session);
- }
+ if (IsSessionEligible(session, antagPrototype, ignorePreferences))
+ eligibleSessions.Add(session);
}
- }
-
- private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
- {
- var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var comp, out _))
- {
- if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
- continue;
-
- if (comp.SelectionsComplete)
- continue;
- ChooseAntags((uid, comp));
- comp.SelectionsComplete = true;
- }
+ return eligibleSessions;
}
- private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
+ ///
+ /// Test eligibility of the player for a specific antag role
+ ///
+ /// The player session to test
+ /// The prototype to get eligible players for
+ /// Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included
+ /// Should players already selected as antags be eligible
+ /// Should we ignore if the player has enabled this specific role
+ /// A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility
+ /// True if the player session matches the requirements, false otherwise
+ public bool IsPlayerEligible(ICommonSession session,
+ ProtoId antagPrototype,
+ bool includeAllJobs = false,
+ AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
+ bool ignorePreferences = false,
+ bool allowNonHumanoids = false,
+ Func? customExcludeCondition = null)
{
- if (!args.LateJoin)
- return;
-
- // TODO: this really doesn't handle multiple latejoin definitions well
- // eventually this should probably store the players per definition with some kind of unique identifier.
- // something to figure out later.
-
- var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var antag, out _))
- {
- if (!RobustRandom.Prob(LateJoinRandomChance))
- continue;
+ if (!IsSessionEligible(session, antagPrototype, ignorePreferences))
+ return false;
- if (!antag.Definitions.Any(p => p.LateJoinAdditional))
- continue;
+ //Ensure the player has a mind
+ if (session.GetMind() is not { } playerMind)
+ return false;
- if (!TryGetNextAvailableDefinition((uid, antag), out var def))
- continue;
+ //Ensure the player has an attached entity
+ if (session.AttachedEntity is not { } playerEntity)
+ return false;
- if (TryMakeAntag((uid, antag), args.Player, def.Value))
- break;
- }
- }
+ //Ignore latejoined players, ie those on the arrivals station
+ if (HasComp(playerEntity))
+ return false;
- protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
- {
- base.Added(uid, component, gameRule, args);
+ //Exclude jobs that cannot be antag, unless explicitly allowed
+ if (!includeAllJobs && !_jobs.CanBeAntag(session))
+ return false;
- for (var i = 0; i < component.Definitions.Count; i++)
+ //Check if the entity is already an antag
+ switch (acceptableAntags)
{
- var def = component.Definitions[i];
-
- if (def.MinRange != null)
- {
- def.Min = def.MinRange.Value.Next(RobustRandom);
- }
-
- if (def.MaxRange != null)
- {
- def.Max = def.MaxRange.Value.Next(RobustRandom);
- }
+ //If we dont want to select any antag roles
+ case AntagAcceptability.None:
+ {
+ if (_roleSystem.MindIsAntagonist(playerMind))
+ return false;
+ break;
+ }
+ //If we dont want to select exclusive antag roles
+ case AntagAcceptability.NotExclusive:
+ {
+ if (_roleSystem.MindIsExclusiveAntagonist(playerMind))
+ return false;
+ break;
+ }
}
- }
- protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
- {
- base.Started(uid, component, gameRule, args);
+ //Unless explictly allowed, ignore non humanoids (eg pets)
+ if (!allowNonHumanoids && !HasComp(playerEntity))
+ return false;
- if (component.SelectionsComplete)
- return;
+ //If a custom condition was provided, test it and exclude the player if it returns true
+ if (customExcludeCondition != null && customExcludeCondition(playerEntity))
+ return false;
- if (GameTicker.RunLevel != GameRunLevel.InRound)
- return;
- if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
- return;
-
- ChooseAntags((uid, component));
- component.SelectionsComplete = true;
+ return true;
}
///
- /// Chooses antagonists from the current selection of players
+ /// Check if the session is eligible for a role, can be run prior to the session being attached to an entity
///
- public void ChooseAntags(Entity ent)
+ /// Player session to check
+ /// Which antag prototype to check for
+ /// Ignore if the player has enabled this antag
+ /// True if the session matches the requirements, false otherwise
+ public bool IsSessionEligible(ICommonSession session, ProtoId antagPrototype, bool ignorePreferences = false)
{
- var sessions = _playerManager.Sessions.ToList();
- ChooseAntags(ent, sessions);
+ //Exclude disconnected or zombie sessions
+ //No point giving antag roles to them
+ if (session.Status == SessionStatus.Disconnected ||
+ session.Status == SessionStatus.Zombie)
+ return false;
+
+ //Check the player has this antag preference selected
+ //Unless we are ignoring preferences, in which case add them anyway
+ var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(session.UserId).SelectedCharacter;
+ if (!pref.AntagPreferences.Contains(antagPrototype.Id) && !ignorePreferences)
+ return false;
+
+ return true;
}
+ #endregion
///
- /// Chooses antagonists from the given selection of players
+ /// Helper method to calculate the number of antags to select based upon the number of players
///
- public void ChooseAntags(Entity ent, List pool)
+ /// How many players there are on the server
+ /// How many players should there be for an additional antag
+ /// Maximum number of antags allowed
+ /// The number of antags that should be chosen
+ public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags)
{
- foreach (var def in ent.Comp.Definitions)
- {
- ChooseAntags(ent, pool, def);
- }
+ return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags);
}
+ #region Antag Selection
///
- /// Chooses antagonists from the given selection of players for the given antag definition.
+ /// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc
///
- public void ChooseAntags(Entity ent, List pool, AntagSelectionDefinition def)
+ /// Array of lists, which are chosen from in order until the correct number of items are selected
+ /// How many items to select
+ /// Up to the specified count of elements from all provided lists
+ public List ChooseAntags(int count, params List[] eligiblePlayerLists)
{
- var playerPool = GetPlayerPool(ent, pool, def);
- var count = GetTargetAntagCount(ent, playerPool, def);
-
- for (var i = 0; i < count; i++)
+ var chosenPlayers = new List();
+ foreach (var playerList in eligiblePlayerLists)
{
- var session = (ICommonSession?) null;
- if (def.PickPlayer)
+ //Remove all chosen players from this list, to prevent duplicates
+ foreach (var chosenPlayer in chosenPlayers)
{
- if (!playerPool.TryPickAndTake(RobustRandom, out session))
- break;
-
- if (ent.Comp.SelectedSessions.Contains(session))
- continue;
+ playerList.Remove(chosenPlayer);
}
- MakeAntag(ent, session, def);
+ //If we have reached the desired number of players, skip
+ if (chosenPlayers.Count >= count)
+ continue;
+
+ //Pick and choose a random number of players from this list
+ chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
}
+ return chosenPlayers;
}
-
///
- /// Tries to makes a given player into the specified antagonist.
+ /// Helper method to choose antags from a list
///
- public bool TryMakeAntag(Entity ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
+ /// List of eligible players
+ /// How many to choose
+ /// Up to the specified count of elements from the provided list
+ public List ChooseAntags(int count, List eligiblePlayers)
{
- if (!IsSessionValid(ent, session, def) ||
- !IsEntityValid(session?.AttachedEntity, def))
+ var chosenPlayers = new List();
+
+ for (var i = 0; i < count; i++)
{
- return false;
+ if (eligiblePlayers.Count == 0)
+ break;
+
+ chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
}
- MakeAntag(ent, session, def, ignoreSpawner);
- return true;
+ return chosenPlayers;
}
///
- /// Makes a given player into the specified antagonist.
+ /// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
///
- public void MakeAntag(Entity ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
+ /// Array of lists, which are chosen from in order until the correct number of items are selected
+ /// How many items to select
+ /// Up to the specified count of elements from all provided lists
+ public List ChooseAntags(int count, params List[] eligiblePlayerLists)
{
- var antagEnt = (EntityUid?) null;
- var isSpawner = false;
-
- if (session != null)
- {
- ent.Comp.SelectedSessions.Add(session);
-
- // we shouldn't be blocking the entity if they're just a ghost or smth.
- if (!HasComp(session.AttachedEntity))
- antagEnt = session.AttachedEntity;
- }
- else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy.
- {
- antagEnt = Spawn(def.SpawnerPrototype);
- isSpawner = true;
- }
-
- if (!antagEnt.HasValue)
- {
- var getEntEv = new AntagSelectEntityEvent(session, ent);
- RaiseLocalEvent(ent, ref getEntEv, true);
-
- if (!getEntEv.Handled)
- {
- throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
- }
-
- antagEnt = getEntEv.Entity;
- }
-
- if (antagEnt is not { } player)
- return;
-
- var getPosEv = new AntagSelectLocationEvent(session, ent);
- RaiseLocalEvent(ent, ref getPosEv, true);
- if (getPosEv.Handled)
- {
- var playerXform = Transform(player);
- var pos = RobustRandom.Pick(getPosEv.Coordinates);
- _transform.SetMapCoordinates((player, playerXform), pos);
- }
-
- if (isSpawner)
- {
- if (!TryComp(player, out var spawnerComp))
- {
- Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent.");
- return;
- }
-
- spawnerComp.Rule = ent;
- spawnerComp.Definition = def;
- return;
- }
-
- EntityManager.AddComponents(player, def.Components);
- _stationSpawning.EquipStartingGear(player, def.StartingGear);
-
- if (session != null)
+ var chosenPlayers = new List();
+ foreach (var playerList in eligiblePlayerLists)
{
- var curMind = session.GetMind();
- if (curMind == null)
+ //Remove all chosen players from this list, to prevent duplicates
+ foreach (var chosenPlayer in chosenPlayers)
{
- curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
- _mind.SetUserId(curMind.Value, session.UserId);
+ playerList.Remove(chosenPlayer);
}
- _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
- _role.MindAddRoles(curMind.Value, def.MindComponents);
- ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
- }
+ //If we have reached the desired number of players, skip
+ if (chosenPlayers.Count >= count)
+ continue;
- if (def.Briefing is { } briefing)
- {
- SendBriefing(session, briefing);
+ //Pick and choose a random number of players from this list
+ chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
}
-
- var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
- RaiseLocalEvent(ent, ref afterEv, true);
+ return chosenPlayers;
}
-
///
- /// Gets an ordered player pool based on player preferences and the antagonist definition.
+ /// Helper method to choose sessions from a list
///
- public AntagSelectionPlayerPool GetPlayerPool(Entity ent, List sessions, AntagSelectionDefinition def)
+ /// List of eligible sessions
+ /// How many to choose
+ /// Up to the specified count of elements from the provided list
+ public List ChooseAntags(int count, List eligiblePlayers)
{
- var preferredList = new List();
- var secondBestList = new List();
- var unwantedList = new List();
- var invalidList = new List();
- foreach (var session in sessions)
+ var chosenPlayers = new List();
+
+ for (int i = 0; i < count; i++)
{
- if (!IsSessionValid(ent, session, def) ||
- !IsEntityValid(session.AttachedEntity, def))
- {
- invalidList.Add(session);
- continue;
- }
+ if (eligiblePlayers.Count == 0)
+ break;
- var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
- if (def.PrefRoles.Count != 0 && pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
- {
- preferredList.Add(session);
- }
- else if (def.FallbackRoles.Count != 0 && pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
- {
- secondBestList.Add(session);
- }
- else
- {
- unwantedList.Add(session);
- }
+ chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
}
- return new AntagSelectionPlayerPool(new() { preferredList, secondBestList, unwantedList, invalidList });
+ return chosenPlayers;
}
+ #endregion
+ #region Briefings
///
- /// Checks if a given session is valid for an antagonist.
+ /// Helper method to send the briefing text and sound to a list of entities
///
- public bool IsSessionValid(Entity ent, ICommonSession? session, AntagSelectionDefinition def, EntityUid? mind = null)
+ /// The players chosen to be antags
+ /// The briefing text to send
+ /// The color the briefing should be, null for default
+ /// The sound to briefing/greeting sound to play
+ public void SendBriefing(List entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
{
- if (session == null)
- return true;
-
- mind ??= session.GetMind();
-
- if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
- return false;
-
- if (ent.Comp.SelectedSessions.Contains(session))
- return false;
-
- //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
-
- switch (def.MultiAntagSetting)
+ foreach (var entity in entities)
{
- case AntagAcceptability.None:
- {
- if (_role.MindIsAntagonist(mind))
- return false;
- break;
- }
- case AntagAcceptability.NotExclusive:
- {
- if (_role.MindIsExclusiveAntagonist(mind))
- return false;
- break;
- }
+ SendBriefing(entity, briefing, briefingColor, briefingSound);
}
-
- // todo: expand this to allow for more fine antag-selection logic for game rules.
- if (!_jobs.CanBeAntag(session))
- return false;
-
- return true;
}
///
- /// Checks if a given entity (mind/session not included) is valid for a given antagonist.
+ /// Helper method to send the briefing text and sound to a player entity
///
- private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
+ /// The entity chosen to be antag
+ /// The briefing text to send
+ /// The color the briefing should be, null for default
+ /// The sound to briefing/greeting sound to play
+ public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
{
- if (entity == null)
- return false;
+ if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
+ return;
- if (HasComp(entity))
- return false;
+ if (mindComponent.Session == null)
+ return;
- if (!def.AllowNonHumans && !HasComp(entity))
- return false;
+ SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
+ }
- if (def.Whitelist != null)
- {
- if (!def.Whitelist.IsValid(entity.Value, EntityManager))
- return false;
- }
+ ///
+ /// Helper method to send the briefing text and sound to a list of sessions
+ ///
+ ///
+ ///
+ ///
+ ///
- if (def.Blacklist != null)
+ public void SendBriefing(List sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ {
+ foreach (var session in sessions)
{
- if (def.Blacklist.IsValid(entity.Value, EntityManager))
- return false;
+ SendBriefing(session, briefing, briefingColor, briefingSound);
}
-
- return true;
}
-}
-
-///
-/// Event raised on a game rule entity in order to determine what the antagonist entity will be.
-/// Only raised if the selected player's current entity is invalid.
-///
-[ByRefEvent]
-public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule)
-{
- public readonly ICommonSession? Session = Session;
-
- public bool Handled => Entity != null;
-
- public EntityUid? Entity;
-}
-
-///
-/// Event raised on a game rule entity to determine the location for the antagonist.
-///
-[ByRefEvent]
-public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule)
-{
- public readonly ICommonSession? Session = Session;
-
- public bool Handled => Coordinates.Any();
+ ///
+ /// Helper method to send the briefing text and sound to a session
+ ///
+ /// The player chosen to be an antag
+ /// The briefing text to send
+ /// The color the briefing should be, null for default
+ /// The sound to briefing/greeting sound to play
- public List Coordinates = new();
+ public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+ {
+ _audioSystem.PlayGlobal(briefingSound, session);
+ var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing));
+ ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor);
+ }
+ #endregion
}
-
-///
-/// Event raised on a game rule entity after the setup logic for an antag is complete.
-/// Used for applying additional more complex setup logic.
-///
-[ByRefEvent]
-public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity GameRule, AntagSelectionDefinition Def);
diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Server/Antag/Components/AntagSelectionComponent.cs
deleted file mode 100644
index 096be14049a..00000000000
--- a/Content.Server/Antag/Components/AntagSelectionComponent.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using Content.Server.Administration.Systems;
-using Content.Server.Destructible.Thresholds;
-using Content.Shared.Antag;
-using Content.Shared.Roles;
-using Content.Shared.Storage;
-using Content.Shared.Whitelist;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Antag.Components;
-
-[RegisterComponent, Access(typeof(AntagSelectionSystem), typeof(AdminVerbSystem))]
-public sealed partial class AntagSelectionComponent : Component
-{
- ///
- /// Has the primary selection of antagonists finished yet?
- ///
- [DataField]
- public bool SelectionsComplete;
-
- ///
- /// The definitions for the antagonists
- ///
- [DataField]
- public List Definitions = new();
-
- ///
- /// The minds and original names of the players selected to be antagonists.
- ///
- [DataField]
- public List<(EntityUid, string)> SelectedMinds = new();
-
- ///
- /// When the antag selection will occur.
- ///
- [DataField]
- public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
-
- ///
- /// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
- /// Is not serialized.
- ///
- public HashSet SelectedSessions = new();
-}
-
-[DataDefinition]
-public partial struct AntagSelectionDefinition()
-{
- ///
- /// A list of antagonist roles that are used for selecting which players will be antagonists.
- ///
- [DataField]
- public List> PrefRoles = new();
-
- ///
- /// Fallback for . Useful if you need multiple role preferences for a team antagonist.
- ///
- [DataField]
- public List> FallbackRoles = new();
-
- ///
- /// Should we allow people who already have an antagonist role?
- ///
- [DataField]
- public AntagAcceptability MultiAntagSetting = AntagAcceptability.None;
-
- ///
- /// The minimum number of this antag.
- ///
- [DataField]
- public int Min = 1;
-
- ///
- /// The maximum number of this antag.
- ///
- [DataField]
- public int Max = 1;
-
- ///
- /// A range used to randomly select
- ///
- [DataField]
- public MinMax? MinRange;
-
- ///
- /// A range used to randomly select
- ///
- [DataField]
- public MinMax? MaxRange;
-
- ///
- /// a player to antag ratio: used to determine the amount of antags that will be present.
- ///
- [DataField]
- public int PlayerRatio = 10;
-
- ///
- /// Whether or not players should be picked to inhabit this antag or not.
- ///
- [DataField]
- public bool PickPlayer = true;
-
- ///
- /// If true, players that latejoin into a round have a chance of being converted into antagonists.
- ///
- [DataField]
- public bool LateJoinAdditional = false;
-
- //todo: find out how to do this with minimal boilerplate: filler department, maybe?
- //public HashSet> JobBlacklist = new()
-
- ///
- /// Mostly just here for legacy compatibility and reducing boilerplate
- ///
- [DataField]
- public bool AllowNonHumans = false;
-
- ///
- /// A whitelist for selecting which players can become this antag.
- ///
- [DataField]
- public EntityWhitelist? Whitelist;
-
- ///
- /// A blacklist for selecting which players can become this antag.
- ///
- [DataField]
- public EntityWhitelist? Blacklist;
-
- ///
- /// Components added to the player.
- ///
- [DataField]
- public ComponentRegistry Components = new();
-
- ///
- /// Components added to the player's mind.
- ///
- [DataField]
- public ComponentRegistry MindComponents = new();
-
- ///
- /// A set of starting gear that's equipped to the player.
- ///
- [DataField]
- public ProtoId? StartingGear;
-
- ///
- /// A briefing shown to the player.
- ///
- [DataField]
- public BriefingData? Briefing;
-
- ///
- /// A spawner used to defer the selection of this particular definition.
- ///
- ///
- /// Not the cleanest way of doing this code but it's just an odd specific behavior.
- /// Sue me.
- ///
- [DataField]
- public EntProtoId? SpawnerPrototype;
-}
-
-///
-/// Contains data used to generate a briefing.
-///
-[DataDefinition]
-public partial struct BriefingData
-{
- ///
- /// The text shown
- ///
- [DataField]
- public LocId? Text;
-
- ///
- /// The color of the text.
- ///
- [DataField]
- public Color? Color;
-
- ///
- /// The sound played.
- ///
- [DataField]
- public SoundSpecifier? Sound;
-}
diff --git a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs b/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs
deleted file mode 100644
index fcaa4d42672..00000000000
--- a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Content.Server.Antag.Components;
-
-///
-/// Ghost role spawner that creates an antag for the associated gamerule.
-///
-[RegisterComponent, Access(typeof(AntagSelectionSystem))]
-public sealed partial class GhostRoleAntagSpawnerComponent : Component
-{
- [DataField]
- public EntityUid? Rule;
-
- [DataField]
- public AntagSelectionDefinition? Definition;
-}
diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs
index 18837b5a7c8..ba09c84bce4 100644
--- a/Content.Server/Antag/MobReplacementRuleSystem.cs
+++ b/Content.Server/Antag/MobReplacementRuleSystem.cs
@@ -1,16 +1,45 @@
+using System.Numerics;
+using Content.Server.Advertise.Components;
+using Content.Server.Advertise.EntitySystems;
using Content.Server.Antag.Mimic;
-using Content.Server.GameTicking.Components;
+using Content.Server.Chat.Systems;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
+using Content.Server.NPC.Systems;
+using Content.Server.Station.Systems;
+using Content.Server.GameTicking;
using Content.Shared.VendingMachines;
using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Server.GameObjects;
+using Robust.Shared.Physics.Systems;
+using System.Linq;
+using Robust.Shared.Physics;
+using Content.Shared.Movement.Components;
+using Content.Shared.Damage;
+using Content.Server.NPC.HTN;
+using Content.Server.NPC;
+using Content.Shared.Weapons.Melee;
+using Content.Server.Power.Components;
+using Content.Shared.CombatMode;
namespace Content.Server.Antag;
public sealed class MobReplacementRuleSystem : GameRuleSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly GameTicker _gameTicker = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+ [Dependency] private readonly NPCSystem _npc = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly AdvertiseSystem _advertise = default!;
+
protected override void Started(EntityUid uid, MobReplacementRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
@@ -18,21 +47,133 @@ protected override void Started(EntityUid uid, MobReplacementRuleComponent compo
var query = AllEntityQuery();
var spawns = new List<(EntityUid Entity, EntityCoordinates Coordinates)>();
+ var stations = _gameTicker.GetSpawnableStations();
while (query.MoveNext(out var vendingUid, out _, out var xform))
{
- if (!_random.Prob(component.Chance))
+ var ownerStation = _station.GetOwningStation(vendingUid);
+
+ if (ownerStation == null
+ || ownerStation != stations[0])
+ continue;
+
+ // Make sure that we aren't running this on something that is already a mimic
+ if (HasComp(vendingUid))
continue;
spawns.Add((vendingUid, xform.Coordinates));
}
- foreach (var entity in spawns)
+ if (spawns == null)
{
- var coordinates = entity.Coordinates;
- Del(entity.Entity);
+ //WTF THE STATION DOESN'T EXIST! WE MUST BE IN A TEST! QUICK, PUT A MIMIC AT 0,0!!!
+ Spawn(component.Proto, new EntityCoordinates(uid, new Vector2(0, 0)));
+ }
+ else
+ {
+ // This is intentionally not clamped. If a server host wants to replace every vending machine in the entire station with a mimic, who am I to stop them?
+ var k = MathF.MaxMagnitude(component.NumberToReplace, 1);
+ while (k > 0 && spawns != null && spawns.Count > 0)
+ {
+ if (k > 1)
+ {
+ var spawnLocation = _random.PickAndTake(spawns);
+ BuildAMimicWorkshop(spawnLocation.Entity, component);
+ }
+ else
+ {
+ BuildAMimicWorkshop(spawns[0].Entity, component);
+ }
+
+ if (k == MathF.MaxMagnitude(component.NumberToReplace, 1)
+ && component.DoAnnouncement)
+ _chat.DispatchStationAnnouncement(stations[0], Loc.GetString("station-event-rampant-intelligence-announcement"), playDefaultSound: true,
+ colorOverride: Color.Red, sender: "Central Command");
+
+ k--;
+ }
+ }
+ }
+
+ ///
+ /// It's like Build a Bear, but MURDER
+ ///
+ ///
+ public void BuildAMimicWorkshop(EntityUid uid, MobReplacementRuleComponent component)
+ {
+ var metaData = MetaData(uid);
+ var vendorPrototype = metaData.EntityPrototype;
+ var mimicProto = _prototype.Index(component.Proto);
+
+ var vendorComponents = vendorPrototype?.Components.Keys
+ .Where(n => n != "Transform" && n != "MetaData")
+ .Select(name => (name, _componentFactory.GetRegistration(name).Type))
+ .ToList() ?? new List<(string name, Type type)>();
+
+ var mimicComponents = mimicProto?.Components.Keys
+ .Where(n => n != "Transform" && n != "MetaData")
+ .Select(name => (name, _componentFactory.GetRegistration(name).Type))
+ .ToList() ?? new List<(string name, Type type)>();
- Spawn(component.Proto, coordinates);
+ foreach (var name in mimicComponents.Except(vendorComponents))
+ {
+ var newComponent = _componentFactory.GetComponent(name.name);
+ EntityManager.AddComponent(uid, newComponent);
}
+
+ var xform = Transform(uid);
+ if (xform.Anchored)
+ _transform.Unanchor(uid, xform);
+
+ SetupMimicNPC(uid, component);
+
+ if (TryComp(uid, out var vendor)
+ && component.VendorModify)
+ SetupMimicVendor(uid, component, vendor);
+ }
+ ///
+ /// This handles getting the entity ready to be a hostile NPC
+ ///
+ ///
+ ///
+ private void SetupMimicNPC(EntityUid uid, MobReplacementRuleComponent component)
+ {
+ _physics.SetBodyType(uid, BodyType.KinematicController);
+ _npcFaction.AddFaction(uid, "SimpleHostile");
+
+ var melee = EnsureComp(uid);
+ melee.Angle = 0;
+ DamageSpecifier dspec = new()
+ {
+ DamageDict = new()
+ {
+ { "Blunt", component.MimicMeleeDamage }
+ }
+ };
+ melee.Damage = dspec;
+
+ var movementSpeed = EnsureComp(uid);
+ (movementSpeed.BaseSprintSpeed, movementSpeed.BaseWalkSpeed) = (component.MimicMoveSpeed, component.MimicMoveSpeed);
+
+ var htn = EnsureComp(uid);
+ htn.RootTask = new HTNCompoundTask() { Task = component.MimicAIType };
+ htn.Blackboard.SetValue(NPCBlackboard.NavSmash, component.MimicSmashGlass);
+ _npc.WakeNPC(uid, htn);
+ }
+
+ ///
+ /// Handling specific interactions with vending machines
+ ///
+ ///
+ ///
+ ///
+ private void SetupMimicVendor(EntityUid uid, MobReplacementRuleComponent mimicComponent, AdvertiseComponent vendorComponent)
+ {
+ vendorComponent.MinimumWait = 5;
+ vendorComponent.MaximumWait = 15;
+ _advertise.SayAdvertisement(uid, vendorComponent);
+
+ if (TryComp(uid, out var aPC))
+ aPC.NeedsPower = false;
}
}
diff --git a/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs b/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs
index abdc9500202..62d994dac34 100644
--- a/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs
+++ b/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs
@@ -144,7 +144,7 @@ private bool TrySpawnParadoxAnomaly(string rule, [NotNullWhen(true)] out EntityU
if (job.StartingGear != null && _proto.TryIndex(job.StartingGear, out var gear))
{
- _stationSpawning.EquipStartingGear(spawned, gear);
+ _stationSpawning.EquipStartingGear(spawned, gear, profile);
_stationSpawning.EquipIdCard(spawned,
profile.Name,
job,
diff --git a/Content.Server/DeltaV/StationEvents/Events/GlimmerMobSpawnRule.cs b/Content.Server/DeltaV/StationEvents/Events/GlimmerMobSpawnRule.cs
index 6849c508a1f..ec9ec770313 100644
--- a/Content.Server/DeltaV/StationEvents/Events/GlimmerMobSpawnRule.cs
+++ b/Content.Server/DeltaV/StationEvents/Events/GlimmerMobSpawnRule.cs
@@ -1,5 +1,4 @@
using System.Linq;
-using Content.Server.GameTicking.Components;
using Robust.Shared.Random;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.NPC.Components;
diff --git a/Content.Server/DeltaV/StationEvents/Events/PirateRadioSpawnRule.cs b/Content.Server/DeltaV/StationEvents/Events/PirateRadioSpawnRule.cs
index c5d199164b4..ba042d89662 100644
--- a/Content.Server/DeltaV/StationEvents/Events/PirateRadioSpawnRule.cs
+++ b/Content.Server/DeltaV/StationEvents/Events/PirateRadioSpawnRule.cs
@@ -19,7 +19,6 @@
using Content.Shared.Salvage;
using Content.Shared.Random.Helpers;
using System.Linq;
-using Content.Server.GameTicking.Components;
using Content.Shared.CCVar;
namespace Content.Server.StationEvents.Events;
diff --git a/Content.Server/Destructible/Thresholds/MinMax.cs b/Content.Server/Destructible/Thresholds/MinMax.cs
index c44864183ab..b438e7c0e8d 100644
--- a/Content.Server/Destructible/Thresholds/MinMax.cs
+++ b/Content.Server/Destructible/Thresholds/MinMax.cs
@@ -1,6 +1,4 @@
-using Robust.Shared.Random;
-
-namespace Content.Server.Destructible.Thresholds
+namespace Content.Server.Destructible.Thresholds
{
[Serializable]
[DataDefinition]
@@ -11,16 +9,5 @@ public partial struct MinMax
[DataField("max")]
public int Max;
-
- public MinMax(int min, int max)
- {
- Min = min;
- Max = max;
- }
-
- public int Next(IRobustRandom random)
- {
- return random.Next(Min, Max + 1);
- }
}
}
diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index 43fe46bf8ec..fdefaac3784 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -6,9 +6,9 @@
using Content.Server.Chat.Managers;
using Content.Server.Consent;
using Content.Server.Connection;
+using Content.Server.DiscordAuth;
using Content.Server.JoinQueue;
using Content.Server.Database;
-using Content.Server.DiscordAuth;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Server.GhostKick;
diff --git a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs b/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs
deleted file mode 100644
index de4be83627d..00000000000
--- a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.GameTicking.Components;
-
-///
-/// Generic component used to track a gamerule that's start has been delayed.
-///
-[RegisterComponent, AutoGenerateComponentPause]
-public sealed partial class DelayedStartRuleComponent : Component
-{
- ///
- /// The time at which the rule will start properly.
- ///
- [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
- public TimeSpan RuleStartTime;
-}
diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs
index f52a3cb296d..4ebe946af4a 100644
--- a/Content.Server/GameTicking/GameTicker.GameRule.cs
+++ b/Content.Server/GameTicking/GameTicker.GameRule.cs
@@ -1,6 +1,6 @@
using System.Linq;
using Content.Server.Administration;
-using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Prototypes;
@@ -102,22 +102,6 @@ public bool StartGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = nu
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
return false;
- // If we already have it, then we just skip the delay as it has already happened.
- if (!RemComp(ruleEntity) && ruleData.Delay != null)
- {
- var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom));
-
- if (delayTime > TimeSpan.Zero)
- {
- _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
- _adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
-
- var delayed = EnsureComp(ruleEntity);
- delayed.RuleStartTime = _gameTiming.CurTime + (delayTime);
- return true;
- }
- }
-
_allPreviousGameRules.Add((RoundDuration(), id));
_sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
_adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}");
@@ -271,18 +255,6 @@ public IEnumerable GetAllGameRulePrototypes()
}
}
- private void UpdateGameRules()
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var delay, out var rule))
- {
- if (_gameTiming.CurTime < delay.RuleStartTime)
- continue;
-
- StartGameRule(uid, rule);
- }
- }
-
#region Command Implementations
[AdminCommand(AdminFlags.Fun)]
@@ -351,3 +323,38 @@ private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[]
#endregion
}
+
+/*
+///
+/// Raised broadcast when a game rule is selected, but not started yet.
+///
+public sealed class GameRuleAddedEvent
+{
+ public GameRulePrototype Rule { get; }
+
+ public GameRuleAddedEvent(GameRulePrototype rule)
+ {
+ Rule = rule;
+ }
+}
+
+public sealed class GameRuleStartedEvent
+{
+ public GameRulePrototype Rule { get; }
+
+ public GameRuleStartedEvent(GameRulePrototype rule)
+ {
+ Rule = rule;
+ }
+}
+
+public sealed class GameRuleEndedEvent
+{
+ public GameRulePrototype Rule { get; }
+
+ public GameRuleEndedEvent(GameRulePrototype rule)
+ {
+ Rule = rule;
+ }
+}
+*/
diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs
index fa23312268f..efda3df0ca1 100644
--- a/Content.Server/GameTicking/GameTicker.cs
+++ b/Content.Server/GameTicking/GameTicker.cs
@@ -133,7 +133,6 @@ public override void Update(float frameTime)
return;
base.Update(frameTime);
UpdateRoundFlow(frameTime);
- UpdateGameRules();
}
}
}
diff --git a/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs
similarity index 84%
rename from Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs
rename to Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs
index b9e6fa5d4b8..956768bdd99 100644
--- a/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs
@@ -1,4 +1,4 @@
-namespace Content.Server.GameTicking.Components;
+namespace Content.Server.GameTicking.Rules.Components;
///
/// Added to game rules before and removed before .
diff --git a/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs
similarity index 81%
rename from Content.Server/GameTicking/Components/EndedGameRuleComponent.cs
rename to Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs
index 3234bfff3a0..4484abd4d0b 100644
--- a/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs
@@ -1,4 +1,4 @@
-namespace Content.Server.GameTicking.Components;
+namespace Content.Server.GameTicking.Rules.Components;
///
/// Added to game rules before .
diff --git a/Content.Server/GameTicking/Components/GameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs
similarity index 83%
rename from Content.Server/GameTicking/Components/GameRuleComponent.cs
rename to Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs
index 1e6c3f0ab1d..6309b974020 100644
--- a/Content.Server/GameTicking/Components/GameRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs
@@ -1,7 +1,6 @@
-using Content.Server.Destructible.Thresholds;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-namespace Content.Server.GameTicking.Components;
+namespace Content.Server.GameTicking.Rules.Components;
///
/// Component attached to all gamerule entities.
@@ -21,12 +20,6 @@ public sealed partial class GameRuleComponent : Component
///
[DataField]
public int MinPlayers;
-
- ///
- /// A delay for when the rule the is started and when the starting logic actually runs.
- ///
- [DataField]
- public MinMax? Delay;
}
///
diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs
deleted file mode 100644
index 463aecbff54..00000000000
--- a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Server.Maps;
-using Content.Shared.Whitelist;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Server.GameTicking.Rules.Components;
-
-///
-/// This is used for a game rule that loads a map when activated.
-///
-[RegisterComponent]
-public sealed partial class LoadMapRuleComponent : Component
-{
- [DataField]
- public MapId? Map;
-
- [DataField]
- public ProtoId? GameMap ;
-
- [DataField]
- public ResPath? MapPath;
-
- [DataField]
- public List MapGrids = new();
-
- [DataField]
- public EntityWhitelist? SpawnerWhitelist;
-}
diff --git a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs
index fa352eb320b..e6966c1e377 100644
--- a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs
@@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components;
///
/// Stores some configuration used by the ninja system.
-/// Objectives and roundend summary are handled by .
+/// Objectives and roundend summary are handled by .
///
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
public sealed partial class NinjaRuleComponent : Component
diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs
index bb1b7c87460..e02d90c18bf 100644
--- a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Roles;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
namespace Content.Server.GameTicking.Rules.Components;
///
@@ -6,5 +9,11 @@ namespace Content.Server.GameTicking.Rules.Components;
/// TODO: Remove once systems can request spawns from the ghost role system directly.
///
[RegisterComponent]
-public sealed partial class NukeOperativeSpawnerComponent : Component;
+public sealed partial class NukeOperativeSpawnerComponent : Component
+{
+ [DataField("name", required:true)]
+ public string OperativeName = default!;
+ [DataField]
+ public NukeopSpawnPreset SpawnDetails = default!;
+}
diff --git a/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs
index 3d097cd7c79..358b157cdf3 100644
--- a/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs
@@ -6,6 +6,4 @@
[RegisterComponent]
public sealed partial class NukeOpsShuttleComponent : Component
{
- [DataField]
- public EntityUid AssociatedRule;
}
diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
index f64947e286e..8efd61b4694 100644
--- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
@@ -1,9 +1,10 @@
using Content.Server.Maps;
using Content.Server.NPC.Components;
using Content.Server.RoundEnd;
+using Content.Server.StationEvents.Events;
using Content.Shared.Dataset;
using Content.Shared.Roles;
-using Robust.Shared.Audio;
+using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -13,9 +14,18 @@
namespace Content.Server.GameTicking.Rules.Components;
-[RegisterComponent, Access(typeof(NukeopsRuleSystem))]
+[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
public sealed partial class NukeopsRuleComponent : Component
{
+ ///
+ /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
+ ///
+ [DataField]
+ public int PlayersPerOperative = 10;
+
+ [DataField]
+ public int MaxOps = 5;
+
///
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
///
@@ -46,6 +56,12 @@ public sealed partial class NukeopsRuleComponent : Component
[DataField]
public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3);
+ ///
+ /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
+ ///
+ [DataField]
+ public bool SpawnOutpost = true;
+
///
/// Whether or not nukie left their outpost
///
@@ -68,7 +84,7 @@ public sealed partial class NukeopsRuleComponent : Component
/// This amount of TC will be given to each nukie
///
[DataField]
- public int WarTcAmountPerNukie = 40;
+ public int WarTCAmountPerNukie = 40;
///
/// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
@@ -83,22 +99,48 @@ public sealed partial class NukeopsRuleComponent : Component
public int WarDeclarationMinOps = 4;
[DataField]
- public WinType WinType = WinType.Neutral;
+ public EntProtoId SpawnPointProto = "SpawnPointNukies";
[DataField]
- public List WinConditions = new ();
+ public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
[DataField]
- public EntityUid? TargetStation;
+ public string OperationName = "Test Operation";
[DataField]
- public ProtoId Faction = "Syndicate";
+ public ProtoId OutpostMapPrototype = "NukieOutpost";
+
+ [DataField]
+ public WinType WinType = WinType.Neutral;
+
+ [DataField]
+ public List WinConditions = new ();
+
+ public MapId? NukiePlanet;
+
+ // TODO: use components, don't just cache entity UIDs
+ // There have been (and probably still are) bugs where these refer to deleted entities from old rounds.
+ public EntityUid? NukieOutpost;
+ public EntityUid? NukieShuttle;
+ public EntityUid? TargetStation;
///
- /// Path to antagonist alert sound.
+ /// Data to be used in for an operative once the Mind has been added.
///
[DataField]
- public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
+ public Dictionary OperativeMindPendingData = new();
+
+ [DataField(required: true)]
+ public ProtoId 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();
}
///
diff --git a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs
new file mode 100644
index 00000000000..1d03b41d773
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs
@@ -0,0 +1,24 @@
+using Robust.Shared.Audio;
+
+namespace Content.Server.GameTicking.Rules.Components;
+
+[RegisterComponent, Access(typeof(PiratesRuleSystem))]
+public sealed partial class PiratesRuleComponent : Component
+{
+ [ViewVariables]
+ public List Pirates = new();
+ [ViewVariables]
+ public EntityUid PirateShip = EntityUid.Invalid;
+ [ViewVariables]
+ public HashSet InitialItems = new();
+ [ViewVariables]
+ public double InitialShipValue;
+
+ ///
+ /// Path to antagonist alert sound.
+ ///
+ [DataField("pirateAlertSound")]
+ public SoundSpecifier PirateAlertSound = new SoundPathSpecifier(
+ "/Audio/Ambience/Antag/pirate_start.ogg",
+ AudioParams.Default.WithVolume(4));
+}
diff --git a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
index 3b19bbffb6a..2ce3f1f9a66 100644
--- a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
@@ -22,6 +22,43 @@ public sealed partial class RevolutionaryRuleComponent : Component
[DataField]
public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
+ ///
+ /// Stores players minds
+ ///
+ [DataField]
+ public Dictionary HeadRevs = new();
+
+ [DataField]
+ public ProtoId HeadRevPrototypeId = "HeadRev";
+
+ ///
+ /// Min players needed for Revolutionary gamemode to start.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int MinPlayers = 15;
+
+ ///
+ /// Max Head Revs allowed during selection.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int MaxHeadRevs = 3;
+
+ ///
+ /// The amount of Head Revs that will spawn per this amount of players.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int PlayersPerHeadRev = 15;
+
+ ///
+ /// The gear head revolutionaries are given on spawn.
+ ///
+ [DataField]
+ public List StartingGear = new()
+ {
+ "Flash",
+ "ClothingEyesGlassesSunglasses"
+ };
+
///
/// The time it takes after the last head is killed for the shuttle to arrive.
///
diff --git a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs
index 01a078625ae..9dfd6e6627c 100644
--- a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs
@@ -1,11 +1,12 @@
using Content.Shared.Random;
+using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
///
-/// Stores data for .
+/// Stores data for .
///
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
public sealed partial class ThiefRuleComponent : Component
@@ -22,9 +23,42 @@ public sealed partial class ThiefRuleComponent : Component
[DataField]
public float BigObjectiveChance = 0.7f;
+ ///
+ /// Add a Pacified comp to thieves
+ ///
+ [DataField]
+ public bool PacifistThieves = true;
+
+ [DataField]
+ public ProtoId ThiefPrototypeId = "Thief";
+
[DataField]
public float MaxObjectiveDifficulty = 2.5f;
[DataField]
public int MaxStealObjectives = 10;
+
+ ///
+ /// Things that will be given to thieves
+ ///
+ [DataField]
+ public List StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
+
+ ///
+ /// All Thieves created by this rule
+ ///
+ [DataField]
+ public List ThievesMinds = new();
+
+ ///
+ /// Max Thiefs created by rule on roundstart
+ ///
+ [DataField]
+ public int MaxAllowThief = 3;
+
+ ///
+ /// Sound played when making the player a thief via antag control or ghost role
+ ///
+ [DataField]
+ public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg");
}
diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
index dd359969b6f..62619db76a2 100644
--- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
@@ -57,19 +57,4 @@ public enum SelectionState
///
[DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
-
- ///
- /// The amount of codewords that are selected.
- ///
- [DataField]
- public int CodewordCount = 4;
-
- ///
- /// The amount of TC traitors start with.
- ///
- [DataField]
- public int StartingBalance = 20;
-
- [DataField]
- public int MaxDifficulty = 5;
}
diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs
index 59d1940eafe..4fe91e3a5f5 100644
--- a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs
@@ -8,6 +8,12 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed partial class ZombieRuleComponent : Component
{
+ [DataField]
+ public Dictionary InitialInfectedNames = new();
+
+ [DataField]
+ public ProtoId PatientZeroPrototypeId = "InitialInfected";
+
///
/// When the round will next check for round end.
///
@@ -20,9 +26,61 @@ public sealed partial class ZombieRuleComponent : Component
[DataField]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
+ ///
+ /// The time at which the initial infected will be chosen.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan? StartTime;
+
+ ///
+ /// The minimum amount of time after the round starts that the initial infected will be chosen.
+ ///
+ [DataField]
+ public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
+
+ ///
+ /// The maximum amount of time after the round starts that the initial infected will be chosen.
+ ///
+ [DataField]
+ public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
+
+ ///
+ /// The sound that plays when someone becomes an initial infected.
+ /// todo: this should have a unique sound instead of reusing the zombie one.
+ ///
+ [DataField]
+ public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
+
+ ///
+ /// The minimum amount of time initial infected have before they start taking infection damage.
+ ///
+ [DataField]
+ public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
+
+ ///
+ /// The maximum amount of time initial infected have before they start taking damage.
+ ///
+ [DataField]
+ public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
+
+ ///
+ /// How many players for each initial infected.
+ ///
+ [DataField]
+ public int PlayersPerInfected = 10;
+
+ ///
+ /// The maximum number of initial infected.
+ ///
+ [DataField]
+ public int MaxInitialInfected = 6;
+
///
/// After this amount of the crew become zombies, the shuttle will be automatically called.
///
[DataField]
public float ZombieShuttleCallPercentage = 0.7f;
+
+ [DataField]
+ public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
}
diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
index 78b8a8a85c8..82ac755592e 100644
--- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
@@ -1,6 +1,5 @@
using System.Linq;
using Content.Server.Administration.Commands;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Server.Mind;
@@ -34,6 +33,7 @@ public override void Initialize()
SubscribeLocalEvent(OnSpawnComplete);
SubscribeLocalEvent(OnKillReported);
SubscribeLocalEvent(OnPointChanged);
+ SubscribeLocalEvent(OnRoundEndTextAppend);
}
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
@@ -113,17 +113,21 @@ private void OnPointChanged(EntityUid uid, DeathMatchRuleComponent component, re
_roundEnd.EndRound(component.RestartDelay);
}
- protected override void AppendRoundEndText(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
+ private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
{
- if (!TryComp(uid, out var point))
- return;
-
- if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data))
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
{
- args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
- args.AddLine("");
+ if (!GameTicker.IsGameRuleAdded(uid, rule))
+ continue;
+
+ if (dm.Victor != null && _player.TryGetPlayerData(dm.Victor.Value, out var data))
+ {
+ ev.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
+ ev.AddLine("");
+ }
+ ev.AddLine(Loc.GetString("point-scoreboard-header"));
+ ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
}
- args.AddLine(Loc.GetString("point-scoreboard-header"));
- args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
}
}
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
index 27a9edbad71..a60a2bfe22f 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Shared.Collections;
@@ -16,12 +15,29 @@ protected EntityQueryEnumerator Q
return EntityQueryEnumerator();
}
- ///
- /// Queries all gamerules, regardless of if they're active or not.
- ///
- protected EntityQueryEnumerator QueryAllRules()
+ protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
{
- return EntityQueryEnumerator();
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out _, out _, out var gameRule))
+ {
+ var minPlayers = gameRule.MinPlayers;
+ if (!ev.Forced && ev.Players.Length < minPlayers)
+ {
+ ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
+ ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
+ ("presetName", localizedPresetName)));
+ ev.Cancel();
+ continue;
+ }
+
+ if (ev.Players.Length == 0)
+ {
+ ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
+ ev.Cancel();
+ }
+ }
+
+ return !ev.Cancelled;
}
///
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
index c167ae7b6c7..363c2ad7f75 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
@@ -1,6 +1,6 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -22,31 +22,9 @@ public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnStartAttempt);
SubscribeLocalEvent(OnGameRuleAdded);
SubscribeLocalEvent(OnGameRuleStarted);
SubscribeLocalEvent(OnGameRuleEnded);
- SubscribeLocalEvent(OnRoundEndTextAppend);
- }
-
- private void OnStartAttempt(RoundStartAttemptEvent args)
- {
- if (args.Forced || args.Cancelled)
- return;
-
- var query = QueryAllRules();
- while (query.MoveNext(out var uid, out _, out var gameRule))
- {
- var minPlayers = gameRule.MinPlayers;
- if (args.Players.Length >= minPlayers)
- continue;
-
- ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
- ("readyPlayersCount", args.Players.Length),
- ("minimumPlayers", minPlayers),
- ("presetName", ToPrettyString(uid))));
- args.Cancel();
- }
}
private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
@@ -70,12 +48,6 @@ private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent
Ended(uid, component, ruleData, args);
}
- private void OnRoundEndTextAppend(Entity ent, ref RoundEndTextAppendEvent args)
- {
- if (!TryComp(ent, out var ruleData))
- return;
- AppendRoundEndText(ent, ent, ruleData, ref args);
- }
///
/// Called when the gamerule is added
@@ -101,14 +73,6 @@ protected virtual void Ended(EntityUid uid, T component, GameRuleComponent gameR
}
- ///
- /// Called at the end of a round when text needs to be added for a game rule.
- ///
- protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
- {
-
- }
-
///
/// Called on an active gamerule entity in the Update function
///
diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
index 01fa387595c..b775b7af564 100644
--- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
@@ -1,6 +1,5 @@
using System.Threading;
using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Robust.Server.Player;
using Robust.Shared.Player;
diff --git a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
index 3da55e30c9e..01fd97d9a79 100644
--- a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
@@ -1,5 +1,4 @@
using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.KillTracking;
using Content.Shared.Chat;
diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
deleted file mode 100644
index aba9ed9e583..00000000000
--- a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using Content.Server.Antag;
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Spawners.Components;
-using Robust.Server.GameObjects;
-using Robust.Server.Maps;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.GameTicking.Rules;
-
-public sealed class LoadMapRuleSystem : GameRuleSystem
-{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly MapSystem _map = default!;
- [Dependency] private readonly MapLoaderSystem _mapLoader = default!;
- [Dependency] private readonly TransformSystem _transform = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnSelectLocation);
- SubscribeLocalEvent(OnGridSplit);
- }
-
- private void OnGridSplit(ref GridSplitEvent args)
- {
- var rule = QueryActiveRules();
- while (rule.MoveNext(out _, out var mapComp, out _))
- {
- if (!mapComp.MapGrids.Contains(args.Grid))
- continue;
-
- mapComp.MapGrids.AddRange(args.NewGrids);
- break;
- }
- }
-
- protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args)
- {
- if (comp.Map != null)
- return;
-
- _map.CreateMap(out var mapId);
- comp.Map = mapId;
-
- if (comp.GameMap != null)
- {
- var gameMap = _prototypeManager.Index(comp.GameMap.Value);
- comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions()));
- }
- else if (comp.MapPath != null)
- {
- if (_mapLoader.TryLoad(comp.Map.Value, comp.MapPath.Value.ToString(), out var roots, new MapLoadOptions { LoadMap = true }))
- comp.MapGrids.AddRange(roots);
- }
- else
- {
- Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}");
- }
- }
-
- private void OnSelectLocation(Entity ent, ref AntagSelectLocationEvent args)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var xform))
- {
- if (xform.MapID != ent.Comp.Map)
- continue;
-
- if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value))
- continue;
-
- if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager))
- continue;
-
- args.Coordinates.Add(_transform.GetMapCoordinates(xform));
- }
- }
-}
diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
index ee3a025533a..e792a004df5 100644
--- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
@@ -1,6 +1,5 @@
using System.Threading;
using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Timer = Robust.Shared.Timing.Timer;
diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
index d06b9fb899c..46040e29450 100644
--- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
@@ -1,51 +1,77 @@
+using Content.Server.Administration.Commands;
+using Content.Server.Administration.Managers;
using Content.Server.Antag;
using Content.Server.Communications;
using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Ghost.Roles.Components;
+using Content.Server.Ghost.Roles.Events;
using Content.Server.Humanoid;
+using Content.Server.Mind;
+using Content.Server.NPC.Components;
+using Content.Server.NPC.Systems;
using Content.Server.Nuke;
using Content.Server.NukeOps;
using Content.Server.Popups;
using Content.Server.Preferences.Managers;
+using Content.Server.RandomMetadata;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
+using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
+using Content.Shared.CCVar;
+using Content.Shared.Dataset;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Nuke;
using Content.Shared.NukeOps;
using Content.Shared.Preferences;
+using Content.Shared.Roles;
using Content.Shared.Store;
using Content.Shared.Tag;
using Content.Shared.Zombies;
+using Robust.Server.Player;
+using Robust.Shared.Configuration;
using Robust.Shared.Map;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
-using Content.Server.GameTicking.Components;
-using Content.Server.NPC.Components;
-using Content.Server.NPC.Systems;
namespace Content.Server.GameTicking.Rules;
public sealed class NukeopsRuleSystem : GameRuleSystem
{
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
+ [Dependency] private readonly IAdminManager _adminManager = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly RandomMetadataSystem _randomMetadata = default!;
+ [Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
- [Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
+ [Dependency] private readonly SharedRoleSystem _roles = default!;
+ [Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly TagSystem _tag = default!;
+ [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+
+ private ISawmill _sawmill = default!;
[ValidatePrototypeId]
private const string TelecrystalCurrencyPrototype = "Telecrystal";
@@ -53,67 +79,141 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
[ValidatePrototypeId]
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
+ [ValidatePrototypeId]
+ public const string NukeopsId = "Nukeops";
+
+ [ValidatePrototypeId]
+ private const string OperationPrefixDataset = "operationPrefix";
+
+ [ValidatePrototypeId]
+ private const string OperationSuffixDataset = "operationSuffix";
+
public override void Initialize()
{
base.Initialize();
+ _sawmill = _logManager.GetSawmill("NukeOps");
+
+ SubscribeLocalEvent(OnStartAttempt);
+ SubscribeLocalEvent(OnPlayersSpawning);
+ SubscribeLocalEvent(OnRoundEndText);
SubscribeLocalEvent(OnNukeExploded);
SubscribeLocalEvent(OnRunLevelChanged);
SubscribeLocalEvent(OnNukeDisarm);
SubscribeLocalEvent(OnComponentRemove);
SubscribeLocalEvent(OnMobStateChanged);
+ SubscribeLocalEvent(OnPlayersGhostSpawning);
+ SubscribeLocalEvent(OnMindAdded);
SubscribeLocalEvent(OnOperativeZombified);
- SubscribeLocalEvent(OnMapInit);
-
SubscribeLocalEvent(OnShuttleFTLAttempt);
SubscribeLocalEvent(OnWarDeclared);
SubscribeLocalEvent(OnShuttleCallAttempt);
-
- SubscribeLocalEvent(OnAntagSelectEntity);
- SubscribeLocalEvent(OnAfterAntagEntSelected);
}
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
- var eligible = new List>();
- var eligibleQuery = EntityQueryEnumerator();
- while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
+ base.Started(uid, component, gameRule, args);
+
+ if (GameTicker.RunLevel == GameRunLevel.InRound)
+ SpawnOperativesForGhostRoles(uid, component);
+ }
+
+ #region Event Handlers
+
+ private void OnStartAttempt(RoundStartAttemptEvent ev)
+ {
+ TryRoundStartAttempt(ev, Loc.GetString("nukeops-title"));
+ }
+
+ private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
+ {
+ var query = QueryActiveRules();
+ while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
- if (!_npcFaction.IsFactionHostile(component.Faction, eligibleUid, member))
+ if (!SpawnMap((uid, nukeops)))
+ {
+ _sawmill.Info("Failed to load map for nukeops");
continue;
+ }
- eligible.Add((eligibleUid, eligibleComp, member));
- }
+ //Handle there being nobody readied up
+ if (ev.PlayerPool.Count == 0)
+ continue;
- if (eligible.Count == 0)
- return;
+ var commanderEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.CommanderSpawnDetails.AntagRoleProto);
+ var agentEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.AgentSpawnDetails.AntagRoleProto);
+ var operativeEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.OperativeSpawnDetails.AntagRoleProto);
+ //Calculate how large the nukeops team needs to be
+ var nukiesToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, nukeops.PlayersPerOperative, nukeops.MaxOps);
+
+ //Select Nukies
+ //Select Commander, priority : commanderEligible, agentEligible, operativeEligible, all players
+ var selectedCommander = _antagSelection.ChooseAntags(1, commanderEligible, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault();
+ //Select Agent, priority : agentEligible, operativeEligible, all players
+ var selectedAgent = _antagSelection.ChooseAntags(1, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault();
+ //Select Operatives, priority : operativeEligible, all players
+ var selectedOperatives = _antagSelection.ChooseAntags(nukiesToSelect - 2, operativeEligible, ev.PlayerPool);
+
+ //Create the team!
+ //If the session is null, they will be spawned as ghost roles (provided the cvar is set)
+ var operatives = new List { new NukieSpawn(selectedCommander, nukeops.CommanderSpawnDetails) };
+ if (nukiesToSelect > 1)
+ operatives.Add(new NukieSpawn(selectedAgent, nukeops.AgentSpawnDetails));
+
+ for (var i = 0; i < nukiesToSelect - 2; i++)
+ {
+ //Use up all available sessions first, then spawn the rest as ghost roles (if enabled)
+ if (selectedOperatives.Count > i)
+ {
+ operatives.Add(new NukieSpawn(selectedOperatives[i], nukeops.OperativeSpawnDetails));
+ }
+ else
+ {
+ operatives.Add(new NukieSpawn(null, nukeops.OperativeSpawnDetails));
+ }
+ }
- component.TargetStation = RobustRandom.Pick(eligible);
+ SpawnOperatives(operatives, _cfg.GetCVar(CCVars.NukeopsSpawnGhostRoles), nukeops);
+
+ foreach (var nukieSpawn in operatives)
+ {
+ if (nukieSpawn.Session == null)
+ continue;
+
+ GameTicker.PlayerJoinGame(nukieSpawn.Session);
+ }
+ }
}
- #region Event Handlers
- protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
- ref RoundEndTextAppendEvent args)
+ private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
- var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
- args.AddLine(winText);
-
- foreach (var cond in component.WinConditions)
+ var ruleQuery = QueryActiveRules();
+ while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _))
{
- var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
- args.AddLine(text);
- }
+ var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
+ ev.AddLine(winText);
- args.AddLine(Loc.GetString("nukeops-list-start"));
+ foreach (var cond in nukeops.WinConditions)
+ {
+ var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
+ ev.AddLine(text);
+ }
+ }
- var antags =_antag.GetAntagIdentifiers(uid);
+ ev.AddLine(Loc.GetString("nukeops-list-start"));
- foreach (var (_, sessionData, name) in antags)
+ var nukiesQuery = EntityQueryEnumerator();
+ while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer))
{
- args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
+ if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
+ continue;
+
+ ev.AddLine(mind.Session != null
+ ? Loc.GetString("nukeops-list-name-user", ("name", Name(nukeopsUid)), ("user", mind.Session.Name))
+ : Loc.GetString("nukeops-list-name", ("name", Name(nukeopsUid))));
}
}
@@ -124,10 +224,10 @@ private void OnNukeExploded(NukeExplodedEvent ev)
{
if (ev.OwningStation != null)
{
- if (ev.OwningStation == GetOutpost(uid))
+ if (ev.OwningStation == nukeops.NukieOutpost)
{
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
- SetWinType((uid, nukeops), WinType.CrewMajor);
+ SetWinType(uid, WinType.CrewMajor, nukeops);
continue;
}
@@ -142,7 +242,7 @@ private void OnNukeExploded(NukeExplodedEvent ev)
}
nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
- SetWinType((uid, nukeops), WinType.OpsMajor);
+ SetWinType(uid, WinType.OpsMajor, nukeops);
correctStation = true;
}
@@ -163,85 +263,19 @@ private void OnNukeExploded(NukeExplodedEvent ev)
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
{
- if (ev.New is not GameRunLevel.PostRound)
- return;
-
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
- OnRoundEnd((uid, nukeops));
- }
- }
-
- private void OnRoundEnd(Entity ent)
- {
- // If the win condition was set to operative/crew major win, ignore.
- if (ent.Comp.WinType == WinType.OpsMajor || ent.Comp.WinType == WinType.CrewMajor)
- return;
-
- var nukeQuery = AllEntityQuery();
- var centcomms = _emergency.GetCentcommMaps();
-
- while (nukeQuery.MoveNext(out var nuke, out var nukeTransform))
- {
- if (nuke.Status != NukeStatus.ARMED)
- continue;
-
- // UH OH
- if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value))
+ switch (ev.New)
{
- ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
- SetWinType((ent, ent), WinType.OpsMajor);
- return;
+ case GameRunLevel.InRound:
+ OnRoundStart(uid, nukeops);
+ break;
+ case GameRunLevel.PostRound:
+ OnRoundEnd(uid, nukeops);
+ break;
}
-
- if (nukeTransform.GridUid == null || ent.Comp.TargetStation == null)
- continue;
-
- if (!TryComp(ent.Comp.TargetStation.Value, out StationDataComponent? data))
- continue;
-
- foreach (var grid in data.Grids)
- {
- if (grid != nukeTransform.GridUid)
- continue;
-
- ent.Comp.WinConditions.Add(WinCondition.NukeActiveInStation);
- SetWinType(ent, WinType.OpsMajor);
- return;
- }
- }
-
- if (_antag.AllAntagsAlive(ent.Owner))
- {
- SetWinType(ent, WinType.OpsMinor);
- ent.Comp.WinConditions.Add(WinCondition.AllNukiesAlive);
- return;
}
-
- ent.Comp.WinConditions.Add(_antag.AnyAliveAntags(ent.Owner)
- ? WinCondition.SomeNukiesAlive
- : WinCondition.AllNukiesDead);
-
- var diskAtCentCom = false;
- var diskQuery = AllEntityQuery();
- while (diskQuery.MoveNext(out _, out var transform))
- {
- diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value);
-
- // TODO: The target station should be stored, and the nuke disk should store its original station.
- // This is fine for now, because we can assume a single station in base SS14.
- break;
- }
-
- // If the disk is currently at Central Command, the crew wins - just slightly.
- // This also implies that some nuclear operatives have died.
- SetWinType(ent, diskAtCentCom
- ? WinType.CrewMinor
- : WinType.OpsMinor);
- ent.Comp.WinConditions.Add(diskAtCentCom
- ? WinCondition.NukeDiskOnCentCom
- : WinCondition.NukeDiskNotOnCentCom);
}
private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
@@ -260,31 +294,66 @@ private void OnMobStateChanged(EntityUid uid, NukeOperativeComponent component,
CheckRoundShouldEnd();
}
- private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
+ private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args)
{
- RemCompDeferred(uid, component);
+ var spawner = args.Spawner;
+
+ if (!TryComp(spawner, out var nukeOpSpawner))
+ return;
+
+ HumanoidCharacterProfile? profile = null;
+ if (TryComp(args.Spawned, out ActorComponent? actor))
+ profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile;
+
+ // TODO: this is kinda awful for multi-nukies
+ foreach (var nukeops in EntityQuery())
+ {
+ SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.SpawnDetails, profile);
+
+ nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.SpawnDetails.AntagRoleProto);
+ }
}
- private void OnMapInit(Entity ent, ref MapInitEvent args)
+ private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
{
- var map = Transform(ent).MapID;
+ if (!_mind.TryGetMind(uid, out var mindId, out var mind))
+ return;
- var rules = EntityQueryEnumerator();
- while (rules.MoveNext(out var uid, out _, out var mapRule))
+ var query = QueryActiveRules();
+ while (query.MoveNext(out _, out _, out var nukeops, out _))
{
- if (map != mapRule.Map)
- continue;
- ent.Comp.AssociatedRule = uid;
- break;
+ if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost ||
+ nukeops.RoundEndBehavior == RoundEndBehavior.Nothing)
+ {
+ role ??= nukeops.OperativeSpawnDetails.AntagRoleProto;
+ _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = role });
+ nukeops.OperativeMindPendingData.Remove(uid);
+ }
+
+ if (mind.Session is not { } playerSession)
+ return;
+
+ if (GameTicker.RunLevel != GameRunLevel.InRound)
+ return;
+
+ if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value)))
+ {
+ NotifyNukie(playerSession, component, nukeops);
+ }
}
}
+ private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
+ {
+ RemCompDeferred(uid, component);
+ }
+
private void OnShuttleFTLAttempt(ref ConsoleFTLAttemptEvent ev)
{
var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var nukeops, out _))
+ while (query.MoveNext(out _, out _, out var nukeops, out _))
{
- if (ev.Uid != GetShuttle((uid, nukeops)))
+ if (ev.Uid != nukeops.NukieShuttle)
continue;
if (nukeops.WarDeclaredTime != null)
@@ -328,12 +397,12 @@ private void OnWarDeclared(ref WarDeclaredEvent ev)
{
// TODO: this is VERY awful for multi-nukies
var query = QueryActiveRules();
- while (query.MoveNext(out var uid, out _, out var nukeops, out _))
+ while (query.MoveNext(out _, out _, out var nukeops, out _))
{
if (nukeops.WarDeclaredTime != null)
continue;
- if (TryComp(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map)
+ if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet)
continue;
var newStatus = GetWarCondition(nukeops, ev.Status);
@@ -344,7 +413,7 @@ private void OnWarDeclared(ref WarDeclaredEvent ev)
var timeRemain = nukeops.WarNukieArriveDelay + Timing.CurTime;
ev.DeclaratorEntity.Comp.ShuttleDisabledTime = timeRemain;
- DistributeExtraTc((uid, nukeops));
+ DistributeExtraTc(nukeops);
}
}
}
@@ -371,7 +440,7 @@ public WarConditionStatus GetWarCondition(NukeopsRuleComponent nukieRule, WarCon
return WarConditionStatus.YesWar;
}
- private void DistributeExtraTc(Entity nukieRule)
+ private void DistributeExtraTc(NukeopsRuleComponent nukieRule)
{
var enumerator = EntityQueryEnumerator();
while (enumerator.MoveNext(out var uid, out var component))
@@ -379,22 +448,161 @@ private void DistributeExtraTc(Entity nukieRule)
if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype))
continue;
- if (GetOutpost(nukieRule.Owner) is not { } outpost)
+ if (!nukieRule.NukieOutpost.HasValue)
continue;
- if (Transform(uid).MapID != Transform(outpost).MapID) // Will receive bonus TC only on their start outpost
+ if (Transform(uid).MapID != Transform(nukieRule.NukieOutpost.Value).MapID) // Will receive bonus TC only on their start outpost
continue;
- _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.Comp.WarTcAmountPerNukie } }, uid, component);
+ _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTCAmountPerNukie } }, uid, component);
var msg = Loc.GetString("store-currency-war-boost-given", ("target", uid));
_popupSystem.PopupEntity(msg, uid);
}
}
- private void SetWinType(Entity ent, WinType type, bool endRound = true)
+ private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null)
{
- ent.Comp.WinType = type;
+ if (!Resolve(uid, ref component))
+ return;
+
+ // TODO: This needs to try and target a Nanotrasen station. At the very least,
+ // we can only currently guarantee that NT stations are the only station to
+ // exist in the base game.
+
+ var eligible = new List>();
+ var eligibleQuery = EntityQueryEnumerator();
+ while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
+ {
+ if (!_npcFaction.IsFactionHostile(component.Faction, eligibleUid, member))
+ continue;
+
+ eligible.Add((eligibleUid, eligibleComp, member));
+ }
+
+ if (eligible.Count == 0)
+ return;
+
+ component.TargetStation = RobustRandom.Pick(eligible);
+ component.OperationName = _randomMetadata.GetRandomFromSegments([OperationPrefixDataset, OperationSuffixDataset], " ");
+
+ var filter = Filter.Empty();
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out var nukeops, out var actor))
+ {
+ NotifyNukie(actor.PlayerSession, nukeops, component);
+ filter.AddPlayer(actor.PlayerSession);
+ }
+ }
+
+ private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ // If the win condition was set to operative/crew major win, ignore.
+ if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor)
+ return;
+
+ var nukeQuery = AllEntityQuery();
+ var centcomms = _emergency.GetCentcommMaps();
+
+ while (nukeQuery.MoveNext(out var nuke, out var nukeTransform))
+ {
+ if (nuke.Status != NukeStatus.ARMED)
+ continue;
+
+ // UH OH
+ if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value))
+ {
+ component.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
+ SetWinType(uid, WinType.OpsMajor, component);
+ return;
+ }
+
+ if (nukeTransform.GridUid == null || component.TargetStation == null)
+ continue;
+
+ if (!TryComp(component.TargetStation.Value, out StationDataComponent? data))
+ continue;
+
+ foreach (var grid in data.Grids)
+ {
+ if (grid != nukeTransform.GridUid)
+ continue;
+
+ component.WinConditions.Add(WinCondition.NukeActiveInStation);
+ SetWinType(uid, WinType.OpsMajor, component);
+ return;
+ }
+ }
+
+ var allAlive = true;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var nukeopsUid, out _, out var mindContainer, out var mobState))
+ {
+ // mind got deleted somehow so ignore it
+ if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
+ continue;
+
+ // check if player got gibbed or ghosted or something - count as dead
+ if (mind.OwnedEntity != null &&
+ // if the player somehow isn't a mob anymore that also counts as dead
+ // have to be alive, not crit or dead
+ mobState.CurrentState is MobState.Alive)
+ {
+ continue;
+ }
+
+ allAlive = false;
+ break;
+ }
+
+ // If all nuke ops were alive at the end of the round,
+ // the nuke ops win. This is to prevent people from
+ // running away the moment nuke ops appear.
+ if (allAlive)
+ {
+ SetWinType(uid, WinType.OpsMinor, component);
+ component.WinConditions.Add(WinCondition.AllNukiesAlive);
+ return;
+ }
+
+ component.WinConditions.Add(WinCondition.SomeNukiesAlive);
+
+ var diskAtCentCom = false;
+ var diskQuery = AllEntityQuery();
+
+ while (diskQuery.MoveNext(out _, out var transform))
+ {
+ diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value);
+
+ // TODO: The target station should be stored, and the nuke disk should store its original station.
+ // This is fine for now, because we can assume a single station in base SS14.
+ break;
+ }
+
+ // If the disk is currently at Central Command, the crew wins - just slightly.
+ // This also implies that some nuclear operatives have died.
+ if (diskAtCentCom)
+ {
+ SetWinType(uid, WinType.CrewMinor, component);
+ component.WinConditions.Add(WinCondition.NukeDiskOnCentCom);
+ }
+ // Otherwise, the nuke ops win.
+ else
+ {
+ SetWinType(uid, WinType.OpsMinor, component);
+ component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom);
+ }
+ }
+
+ private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null, bool endRound = true)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ component.WinType = type;
if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor))
_roundEndSystem.EndRound();
@@ -405,130 +613,243 @@ private void CheckRoundShouldEnd()
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var nukeops, out _))
{
- CheckRoundShouldEnd((uid, nukeops));
+ if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
+ continue;
+
+ // If there are any nuclear bombs that are active, immediately return. We're not over yet.
+ var armed = false;
+ foreach (var nuke in EntityQuery())
+ {
+ if (nuke.Status == NukeStatus.ARMED)
+ {
+ armed = true;
+ break;
+ }
+ }
+ if (armed)
+ continue;
+
+ MapId? shuttleMapId = Exists(nukeops.NukieShuttle)
+ ? Transform(nukeops.NukieShuttle.Value).MapID
+ : null;
+
+ MapId? targetStationMap = null;
+ if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
+ {
+ var grid = data.Grids.FirstOrNull();
+ targetStationMap = grid != null
+ ? Transform(grid.Value).MapID
+ : null;
+ }
+
+ // Check if there are nuke operatives still alive on the same map as the shuttle,
+ // or on the same map as the station.
+ // If there are, the round can continue.
+ var operatives = EntityQuery(true);
+ var operativesAlive = operatives
+ .Where(ent =>
+ ent.Item3.MapID == shuttleMapId
+ || ent.Item3.MapID == targetStationMap)
+ .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running);
+
+ if (operativesAlive)
+ continue; // There are living operatives than can access the shuttle, or are still on the station's map.
+
+ // Check that there are spawns available and that they can access the shuttle.
+ var spawnsAvailable = EntityQuery(true).Any();
+ if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet)
+ continue; // Ghost spawns can still access the shuttle. Continue the round.
+
+ // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives,
+ // and there are no nuclear operatives on the target station's map.
+ nukeops.WinConditions.Add(spawnsAvailable
+ ? WinCondition.NukiesAbandoned
+ : WinCondition.AllNukiesDead);
+
+ SetWinType(uid, WinType.CrewMajor, nukeops, false);
+ _roundEndSystem.DoRoundEndBehavior(
+ nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
+
+ // prevent it called multiple times
+ nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
+ }
+ }
+
+ private bool SpawnMap(Entity ent)
+ {
+ if (!ent.Comp.SpawnOutpost
+ || ent.Comp.NukiePlanet != null)
+ return true;
+
+ ent.Comp.NukiePlanet = _mapManager.CreateMap();
+ var gameMap = _prototypeManager.Index(ent.Comp.OutpostMapPrototype);
+ ent.Comp.NukieOutpost = GameTicker.LoadGameMap(gameMap, ent.Comp.NukiePlanet.Value, null)[0];
+ var query = EntityQueryEnumerator