]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Mega Antag Refactor (#25786)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Thu, 25 Apr 2024 01:31:45 +0000 (21:31 -0400)
committerGitHub <noreply@github.com>
Thu, 25 Apr 2024 01:31:45 +0000 (11:31 +1000)
* Mega Antag Refactor

* last minute delta save

* more workshopping

* more shit

* ok tested this for once

* okkkkk sure

* generic delays for starting rules

* well darn

* nukies partially

* ouagh

* ballin' faded and smonkin wed

* obliterated the diff

* Spread my arms and soak up congratulations

* I've got plenty of love, but nothing to show for it

* but there’s too much sunlight
Shining on my laptop monitor, so I
Can’t see anything with any amount of clarity

* ok this junk

* OOK!

* fubar

* most of sloth's review

* oh boy

* eek

* hell yea!

* ASDFJASDJFvsakcvjkzjnhhhyh

99 files changed:
Content.Client/Humanoid/HumanoidAppearanceSystem.cs
Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
Content.Server/Administration/ServerApi.cs
Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
Content.Server/Antag/AntagSelectionPlayerPool.cs [new file with mode: 0644]
Content.Server/Antag/AntagSelectionSystem.API.cs [new file with mode: 0644]
Content.Server/Antag/AntagSelectionSystem.cs
Content.Server/Antag/Components/AntagSelectionComponent.cs [new file with mode: 0644]
Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs [new file with mode: 0644]
Content.Server/Antag/MobReplacementRuleSystem.cs
Content.Server/Destructible/Thresholds/MinMax.cs
Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs [moved from Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs with 84% similarity]
Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Components/EndedGameRuleComponent.cs [moved from Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs with 81% similarity]
Content.Server/GameTicking/Components/GameRuleComponent.cs [moved from Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs with 83% similarity]
Content.Server/GameTicking/GameTicker.GameRule.cs
Content.Server/GameTicking/GameTicker.cs
Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs
Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs
Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs
Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs
Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs
Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs
Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
Content.Server/GameTicking/Rules/GameRuleSystem.cs
Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
Content.Server/GameTicking/Rules/PiratesRuleSystem.cs [deleted file]
Content.Server/GameTicking/Rules/RespawnRuleSystem.cs
Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs
Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
Content.Server/GameTicking/Rules/SecretRuleSystem.cs
Content.Server/GameTicking/Rules/SubGamemodesSystem.cs
Content.Server/GameTicking/Rules/ThiefRuleSystem.cs
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
Content.Server/Objectives/ObjectivesSystem.cs
Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
Content.Server/Preferences/Managers/IServerPreferencesManager.cs
Content.Server/Preferences/Managers/ServerPreferencesManager.cs
Content.Server/RandomMetadata/RandomMetadataSystem.cs
Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs [deleted file]
Content.Server/StationEvents/Events/AnomalySpawnRule.cs
Content.Server/StationEvents/Events/BluespaceArtifactRule.cs
Content.Server/StationEvents/Events/BluespaceLockerRule.cs
Content.Server/StationEvents/Events/BreakerFlipRule.cs
Content.Server/StationEvents/Events/BureaucraticErrorRule.cs
Content.Server/StationEvents/Events/CargoGiftsRule.cs
Content.Server/StationEvents/Events/ClericalErrorRule.cs
Content.Server/StationEvents/Events/FalseAlarmRule.cs
Content.Server/StationEvents/Events/GasLeakRule.cs
Content.Server/StationEvents/Events/ImmovableRodRule.cs
Content.Server/StationEvents/Events/IonStormRule.cs
Content.Server/StationEvents/Events/KudzuGrowthRule.cs
Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs [deleted file]
Content.Server/StationEvents/Events/MassHallucinationsRule.cs
Content.Server/StationEvents/Events/MeteorSwarmRule.cs
Content.Server/StationEvents/Events/NinjaSpawnRule.cs
Content.Server/StationEvents/Events/PowerGridCheckRule.cs
Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs
Content.Server/StationEvents/Events/RandomSentienceRule.cs
Content.Server/StationEvents/Events/RandomSpawnRule.cs
Content.Server/StationEvents/Events/SolarFlareRule.cs
Content.Server/StationEvents/Events/StationEventSystem.cs
Content.Server/StationEvents/Events/VentClogRule.cs
Content.Server/StationEvents/Events/VentCrittersRule.cs
Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
Content.Server/Traitor/Systems/AutoTraitorSystem.cs
Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs
Content.Server/Zombies/PendingZombieComponent.cs
Content.Server/Zombies/ZombieSystem.cs
Content.Shared/Antag/AntagAcceptability.cs
Content.Shared/CCVar/CCVars.cs
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
Content.Shared/Inventory/InventorySystem.Helpers.cs
Content.Shared/NukeOps/NukeOperativeComponent.cs
Content.Shared/Roles/SharedRoleSystem.cs
Content.Shared/Station/SharedStationSpawningSystem.cs
Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl [deleted file]
Resources/Maps/Shuttles/striker.yml
Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml
Resources/Prototypes/GameRules/events.yml
Resources/Prototypes/GameRules/midround.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml
Resources/Prototypes/game_presets.yml

index 5bae35da5ba2ded4f393a590aefbfd2314da2e44..6eb5dd9ec987ffa3243c07a4ebf52021e3bf81c5 100644 (file)
@@ -108,8 +108,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
     ///     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.
     /// </remarks>
-    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;
index 5833db0a1096304ad823d61ce603ae1dbc3aab67..5bada98a3aa518f2c9a7d1c23e50c5e577187a80 100644 (file)
@@ -112,22 +112,38 @@ public sealed class NukeOpsTest
 
         // The game rule exists, and all the stations/shuttles/maps are properly initialized
         var rule = entMan.AllComponents<NukeopsRuleComponent>().Single().Component;
-        Assert.That(entMan.EntityExists(rule.NukieOutpost));
-        Assert.That(entMan.EntityExists(rule.NukieShuttle));
+        var mapRule = entMan.AllComponents<LoadMapRuleComponent>().Single().Component;
+        foreach (var grid in mapRule.MapGrids)
+        {
+            Assert.That(entMan.EntityExists(grid));
+            Assert.That(entMan.HasComponent<MapGridComponent>(grid));
+            Assert.That(entMan.HasComponent<StationMemberComponent>(grid));
+        }
         Assert.That(entMan.EntityExists(rule.TargetStation));
 
-        Assert.That(entMan.HasComponent<MapGridComponent>(rule.NukieOutpost));
-        Assert.That(entMan.HasComponent<MapGridComponent>(rule.NukieShuttle));
-
-        Assert.That(entMan.HasComponent<StationMemberComponent>(rule.NukieOutpost));
         Assert.That(entMan.HasComponent<StationDataComponent>(rule.TargetStation));
 
-        var nukieStation = entMan.GetComponent<StationMemberComponent>(rule.NukieOutpost!.Value);
+        var nukieShuttlEnt = entMan.AllComponents<NukeOpsShuttleComponent>().FirstOrDefault().Uid;
+        Assert.That(entMan.EntityExists(nukieShuttlEnt));
+
+        EntityUid? nukieStationEnt = null;
+        foreach (var grid in mapRule.MapGrids)
+        {
+            if (entMan.HasComponent<StationMemberComponent>(grid))
+            {
+                nukieStationEnt = grid;
+                break;
+            }
+        }
+
+        Assert.That(entMan.EntityExists(nukieStationEnt));
+        var nukieStation = entMan.GetComponent<StationMemberComponent>(nukieStationEnt!.Value);
+
         Assert.That(entMan.EntityExists(nukieStation.Station));
         Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
 
-        Assert.That(server.MapMan.MapExists(rule.NukiePlanet));
-        var nukieMap = mapSys.GetMap(rule.NukiePlanet!.Value);
+        Assert.That(server.MapMan.MapExists(mapRule.Map));
+        var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
 
         var targetStation = entMan.GetComponent<StationDataComponent>(rule.TargetStation!.Value);
         var targetGrid = targetStation.Grids.First();
@@ -135,8 +151,8 @@ public sealed class NukeOpsTest
         Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
 
         Assert.That(entMan.GetComponent<TransformComponent>(player).MapUid, Is.EqualTo(nukieMap));
-        Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieOutpost!.Value).MapUid, Is.EqualTo(nukieMap));
-        Assert.That(entMan.GetComponent<TransformComponent>(rule.NukieShuttle!.Value).MapUid, Is.EqualTo(nukieMap));
+        Assert.That(entMan.GetComponent<TransformComponent>(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
+        Assert.That(entMan.GetComponent<TransformComponent>(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.
@@ -149,8 +165,8 @@ public sealed class NukeOpsTest
         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(rule.NukieOutpost), Is.GreaterThan(EntityLifeStage.Initialized));
-        Assert.That(LifeStage(rule.NukieShuttle), 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.
index 0707bd64c6ffa2bd8e153da9d915aa97dd0adcb1..20a157e33e83cd2264c29fcd7114bae2f046169b 100644 (file)
@@ -1,5 +1,6 @@
 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;
index 0f665a63de03c3c05b2a3b39139de78648b86ba1..5d7ae8efbf418497a9e7035e9cf364e621d61306 100644 (file)
@@ -17,6 +17,7 @@ public sealed class SecretStartsTest
 
         var server = pair.Server;
         await server.WaitIdleAsync();
+        var entMan = server.ResolveDependency<IEntityManager>();
         var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
 
         await server.WaitAssertion(() =>
@@ -32,10 +33,7 @@ public sealed class SecretStartsTest
 
         await server.WaitAssertion(() =>
         {
-            foreach (var rule in gameTicker.GetAddedGameRules())
-            {
-                Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule));
-            }
+            Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule.");
 
             // End all rules
             gameTicker.ClearGameRules();
index 6f10ef9b4791d39dfb6bee7ae1106228e7a23c6d..04fd38598fb2906ca4fbc0e2e06ddeb2347f7c37 100644 (file)
@@ -8,6 +8,7 @@ using System.Text.Json.Nodes;
 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;
index eff97136d06211eccefd2c9282c9ecbb78a6cff7..599485150a40e615543223591bb7866b80c9a523 100644 (file)
@@ -1,23 +1,37 @@
-using Content.Server.GameTicking.Rules;
+using Content.Server.Administration.Commands;
+using Content.Server.Antag;
+using Content.Server.GameTicking.Rules.Components;
 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!;
-    [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!;
+
+    [ValidatePrototypeId<EntityPrototype>]
+    private const string DefaultTraitorRule = "Traitor";
+
+    [ValidatePrototypeId<EntityPrototype>]
+    private const string DefaultNukeOpRule = "LoneOpsSpawn";
+
+    [ValidatePrototypeId<EntityPrototype>]
+    private const string DefaultRevsRule = "Revolutionary";
+
+    [ValidatePrototypeId<EntityPrototype>]
+    private const string DefaultThiefRule = "Thief";
+
+    [ValidatePrototypeId<StartingGearPrototype>]
+    private const string PirateGearId = "PirateGear";
 
     // All antag verbs have names so invokeverb works.
     private void AddAntagVerbs(GetVerbsEvent<Verb> args)
@@ -40,9 +54,7 @@ public sealed partial class AdminVerbSystem
             Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
             Act = () =>
             {
-                // if its a monkey or mouse or something dont give uplink or objectives
-                var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target);
-                _traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman);
+                _antag.ForceMakeAntag<TraitorRuleComponent>(player, DefaultTraitorRule);
             },
             Impact = LogImpact.High,
             Message = Loc.GetString("admin-verb-make-traitor"),
@@ -71,7 +83,7 @@ public sealed partial class AdminVerbSystem
             Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
             Act = () =>
             {
-                _nukeopsRule.MakeLoneNukie(args.Target);
+                _antag.ForceMakeAntag<NukeopsRuleComponent>(player, DefaultNukeOpRule);
             },
             Impact = LogImpact.High,
             Message = Loc.GetString("admin-verb-make-nuclear-operative"),
@@ -85,14 +97,14 @@ public sealed partial class AdminVerbSystem
             Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
             Act = () =>
             {
-                _piratesRule.MakePirate(args.Target);
+                // pirates just get an outfit because they don't really have logic associated with them
+                SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
             },
             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"),
@@ -100,7 +112,7 @@ public sealed partial class AdminVerbSystem
             Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
             Act = () =>
             {
-                _revolutionaryRule.OnHeadRevAdmin(args.Target);
+                _antag.ForceMakeAntag<RevolutionaryRuleComponent>(player, DefaultRevsRule);
             },
             Impact = LogImpact.High,
             Message = Loc.GetString("admin-verb-make-head-rev"),
@@ -114,7 +126,7 @@ public sealed partial class AdminVerbSystem
             Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
             Act = () =>
             {
-                _thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad
+                _antag.ForceMakeAntag<ThiefRuleComponent>(player, DefaultThiefRule);
             },
             Impact = LogImpact.High,
             Message = Loc.GetString("admin-verb-make-thief"),
diff --git a/Content.Server/Antag/AntagSelectionPlayerPool.cs b/Content.Server/Antag/AntagSelectionPlayerPool.cs
new file mode 100644 (file)
index 0000000..054292d
--- /dev/null
@@ -0,0 +1,29 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.Server.Antag;
+
+public sealed class AntagSelectionPlayerPool(params List<ICommonSession>[] sessions)
+{
+    private readonly List<List<ICommonSession>> _orderedPools = sessions.ToList();
+
+    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
new file mode 100644 (file)
index 0000000..f8ec5bc
--- /dev/null
@@ -0,0 +1,302 @@
+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
+{
+    /// <summary>
+    /// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
+    /// </summary>
+    public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> 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;
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool = null)
+    {
+        var count = 0;
+        foreach (var def in ent.Comp.Definitions)
+        {
+            count += GetTargetAntagCount(ent, pool, def);
+        }
+
+        return count;
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public int GetTargetAntagCount(Entity<AntagSelectionComponent> 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);
+    }
+
+    /// <summary>
+    /// Returns identifiable information for all antagonists to be used in a round end summary.
+    /// </summary>
+    /// <returns>
+    /// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
+    /// </returns>
+    public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity<AntagSelectionComponent?> 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<MindComponent>(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;
+    }
+
+    /// <summary>
+    /// Returns all the minds of antagonists.
+    /// </summary>
+    public List<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return new();
+
+        var output = new List<Entity<MindComponent>>();
+        foreach (var (mind, _) in ent.Comp.SelectedMinds)
+        {
+            if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
+                continue;
+
+            output.Add((mind, mindComp));
+        }
+        return output;
+    }
+
+    /// <remarks>
+    /// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
+    /// </remarks>
+    public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return new();
+
+        return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
+    }
+
+    /// <summary>
+    /// Returns all the antagonists for this rule who are currently alive
+    /// </summary>
+    public IEnumerable<EntityUid> GetAliveAntags(Entity<AntagSelectionComponent?> 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);
+        }
+    }
+
+    /// <summary>
+    /// Returns the number of alive antagonists for this rule.
+    /// </summary>
+    public int GetAliveAntagCount(Entity<AntagSelectionComponent?> 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;
+    }
+
+    /// <summary>
+    /// Returns if there are any remaining antagonists alive for this rule.
+    /// </summary>
+    public bool AnyAliveAntags(Entity<AntagSelectionComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return false;
+
+        return GetAliveAntags(ent).Any();
+    }
+
+    /// <summary>
+    /// Checks if all the antagonists for this rule are alive.
+    /// </summary>
+    public bool AllAntagsAlive(Entity<AntagSelectionComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return false;
+
+        return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
+    }
+
+    /// <summary>
+    /// Helper method to send the briefing text and sound to a player entity
+    /// </summary>
+    /// <param name="entity">The entity chosen to be antag</param>
+    /// <param name="briefing">The briefing text to send</param>
+    /// <param name="briefingColor">The color the briefing should be, null for default</param>
+    /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
+    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);
+    }
+
+    /// <summary>
+    /// Helper method to send the briefing text and sound to a list of sessions
+    /// </summary>
+    /// <param name="sessions">The sessions that will be sent the briefing</param>
+    /// <param name="briefing">The briefing text to send</param>
+    /// <param name="briefingColor">The color the briefing should be, null for default</param>
+    /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
+    [PublicAPI]
+    public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+    {
+        foreach (var session in sessions)
+        {
+            SendBriefing(session, briefing, briefingColor, briefingSound);
+        }
+    }
+
+    /// <summary>
+    /// Helper method to send the briefing text and sound to a session
+    /// </summary>
+    /// <param name="session">The player chosen to be an antag</param>
+    /// <param name="data">The briefing data</param>
+    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);
+    }
+
+    /// <summary>
+    /// Helper method to send the briefing text and sound to a session
+    /// </summary>
+    /// <param name="session">The player chosen to be an antag</param>
+    /// <param name="briefing">The briefing text to send</param>
+    /// <param name="briefingColor">The color the briefing should be, null for default</param>
+    /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
+    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);
+        }
+    }
+
+    /// <summary>
+    /// This technically is a gamerule-ent-less way to make an entity an antag.
+    /// You should almost never be using this.
+    /// </summary>
+    public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
+    {
+        var rule = ForceGetGameRuleEnt<T>(defaultRule);
+
+        if (!TryGetNextAvailableDefinition(rule, out var def))
+            def = rule.Comp.Definitions.Last();
+        MakeAntag(rule, player, def.Value);
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public Entity<AntagSelectionComponent> ForceGetGameRuleEnt<T>(string id) where T : Component
+    {
+        var query = EntityQueryEnumerator<T, AntagSelectionComponent>();
+        while (query.MoveNext(out var uid, out _, out var comp))
+        {
+            return (uid, comp);
+        }
+        var ruleEnt = GameTicker.AddGameRule(id);
+        RemComp<LoadMapRuleComponent>(ruleEnt);
+        var antag = Comp<AntagSelectionComponent>(ruleEnt);
+        antag.SelectionsComplete = true; // don't do normal selection.
+        GameTicker.StartGameRule(ruleEnt);
+        return (ruleEnt, antag);
+    }
+}
index b11c562df5ad641d0f74191d295eef226bb2a34a..eb68e077b19639f2469927ee70cc599792e0d281 100644 (file)
+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.GameTicking.Rules.Components;
+using Content.Server.Ghost.Roles;
+using Content.Server.Ghost.Roles.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.Shared.Audio;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
 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 class AntagSelectionSystem : GameRuleSystem<GameRuleComponent>
+public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
 {
-    [Dependency] private readonly IServerPreferencesManager _prefs = default!;
-    [Dependency] private readonly AudioSystem _audioSystem = default!;
+    [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 JobSystem _jobs = default!;
-    [Dependency] private readonly MindSystem _mindSystem = default!;
-    [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
+    [Dependency] private readonly MapSystem _map = 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!;
 
-    #region Eligible Player Selection
-    /// <summary>
-    /// Get all players that are eligible for an antag role
-    /// </summary>
-    /// <param name="playerSessions">All sessions from which to select eligible players</param>
-    /// <param name="antagPrototype">The prototype to get eligible players for</param>
-    /// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
-    /// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
-    /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
-    /// <param name="customExcludeCondition">A custom condition that each player is tested against, if it returns true the player is excluded from eligibility</param>
-    /// <returns>List of all player entities that match the requirements</returns>
-    public List<EntityUid> GetEligiblePlayers(IEnumerable<ICommonSession> playerSessions,
-        ProtoId<AntagPrototype> antagPrototype,
-        bool includeAllJobs = false,
-        AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
-        bool ignorePreferences = false,
-        bool allowNonHumanoids = false,
-        Func<EntityUid?, bool>? customExcludeCondition = null)
+    // arbitrary random number to give late joining some mild interest.
+    public const float LateJoinRandomChance = 0.5f;
+
+    /// <inheritdoc/>
+    public override void Initialize()
     {
-        var eligiblePlayers = new List<EntityUid>();
+        base.Initialize();
 
-        foreach (var player in playerSessions)
-        {
-            if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition))
-                eligiblePlayers.Add(player.AttachedEntity!.Value);
-        }
+        SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
 
-        return eligiblePlayers;
+        SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
+        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
+        SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
     }
 
-    /// <summary>
-    /// 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
-    /// </summary>
-    /// <param name="playerSessions">All sessions from which to select eligible players</param>
-    /// <param name="antagPrototype">The prototype to get eligible players for</param>
-    /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
-    /// <returns>List of all player sessions that match the requirements</returns>
-    public List<ICommonSession> GetEligibleSessions(IEnumerable<ICommonSession> playerSessions, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
+    private void OnTakeGhostRole(Entity<GhostRoleAntagSpawnerComponent> ent, ref TakeGhostRoleEvent args)
     {
-        var eligibleSessions = new List<ICommonSession>();
+        if (args.TookRole)
+            return;
+
+        if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def)
+            return;
 
-        foreach (var session in playerSessions)
+        if (!Exists(rule) || !TryComp<AntagSelectionComponent>(rule, out var select))
+            return;
+
+        MakeAntag((rule, select), args.Player, def, ignoreSpawner: true);
+        args.TookRole = true;
+        _ghostRole.UnregisterGhostRole((ent, Comp<GhostRoleComponent>(ent)));
+    }
+
+    private void OnPlayerSpawning(RulePlayerSpawningEvent args)
+    {
+        var pool = args.PlayerPool;
+
+        var query = QueryActiveRules();
+        while (query.MoveNext(out var uid, out _, out var comp, out _))
         {
-            if (IsSessionEligible(session, antagPrototype, ignorePreferences))
-                eligibleSessions.Add(session);
-        }
+            if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
+                continue;
 
-        return eligibleSessions;
+            if (comp.SelectionsComplete)
+                return;
+
+            ChooseAntags((uid, comp), pool);
+            comp.SelectionsComplete = true;
+
+            foreach (var session in comp.SelectedSessions)
+            {
+                args.PlayerPool.Remove(session);
+                GameTicker.PlayerJoinGame(session);
+            }
+        }
     }
 
-    /// <summary>
-    /// Test eligibility of the player for a specific antag role
-    /// </summary>
-    /// <param name="session">The player session to test</param>
-    /// <param name="antagPrototype">The prototype to get eligible players for</param>
-    /// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
-    /// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
-    /// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
-    /// <param name="customExcludeCondition">A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility</param>
-    /// <returns>True if the player session matches the requirements, false otherwise</returns>
-    public bool IsPlayerEligible(ICommonSession session,
-        ProtoId<AntagPrototype> antagPrototype,
-        bool includeAllJobs = false,
-        AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
-        bool ignorePreferences = false,
-        bool allowNonHumanoids = false,
-        Func<EntityUid?, bool>? customExcludeCondition = null)
+    private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
     {
-        if (!IsSessionEligible(session, antagPrototype, ignorePreferences))
-            return false;
+        var query = QueryActiveRules();
+        while (query.MoveNext(out var uid, out _, out var comp, out _))
+        {
+            if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
+                continue;
 
-        //Ensure the player has a mind
-        if (session.GetMind() is not { } playerMind)
-            return false;
+            if (comp.SelectionsComplete)
+                continue;
 
-        //Ensure the player has an attached entity
-        if (session.AttachedEntity is not { } playerEntity)
-            return false;
+            ChooseAntags((uid, comp));
+            comp.SelectionsComplete = true;
+        }
+    }
 
-        //Ignore latejoined players, ie those on the arrivals station
-        if (HasComp<PendingClockInComponent>(playerEntity))
-            return false;
+    private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
+    {
+        if (!args.LateJoin)
+            return;
 
-        //Exclude jobs that cannot be antag, unless explicitly allowed
-        if (!includeAllJobs && !_jobs.CanBeAntag(session))
-            return false;
+        // 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.
 
-        //Check if the entity is already an antag
-        switch (acceptableAntags)
+        var query = QueryActiveRules();
+        while (query.MoveNext(out var uid, out _, out var antag, out _))
         {
-            //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;
-                }
+            if (!RobustRandom.Prob(LateJoinRandomChance))
+                continue;
+
+            if (!antag.Definitions.Any(p => p.LateJoinAdditional))
+                continue;
+
+            if (!TryGetNextAvailableDefinition((uid, antag), out var def))
+                continue;
+
+            MakeAntag((uid, antag), args.Player, def.Value);
         }
+    }
 
-        //Unless explictly allowed, ignore non humanoids (eg pets)
-        if (!allowNonHumanoids && !HasComp<HumanoidAppearanceComponent>(playerEntity))
-            return false;
+    protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    {
+        base.Added(uid, component, gameRule, args);
 
-        //If a custom condition was provided, test it and exclude the player if it returns true
-        if (customExcludeCondition != null && customExcludeCondition(playerEntity))
-            return false;
+        for (var i = 0; i < component.Definitions.Count; i++)
+        {
+            var def = component.Definitions[i];
 
+            if (def.MinRange != null)
+            {
+                def.Min = def.MinRange.Value.Next(RobustRandom);
+            }
 
-        return true;
+            if (def.MaxRange != null)
+            {
+                def.Max = def.MaxRange.Value.Next(RobustRandom);
+            }
+        }
     }
 
-    /// <summary>
-    /// Check if the session is eligible for a role, can be run prior to the session being attached to an entity
-    /// </summary>
-    /// <param name="session">Player session to check</param>
-    /// <param name="antagPrototype">Which antag prototype to check for</param>
-    /// <param name="ignorePreferences">Ignore if the player has enabled this antag</param>
-    /// <returns>True if the session matches the requirements, false otherwise</returns>
-    public bool IsSessionEligible(ICommonSession session, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
+    protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        //Exclude disconnected or zombie sessions
-        //No point giving antag roles to them
-        if (session.Status == SessionStatus.Disconnected ||
-            session.Status == SessionStatus.Zombie)
-            return false;
+        base.Started(uid, component, gameRule, args);
 
-        //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;
+        if (component.SelectionsComplete)
+            return;
 
-        return true;
+        if (GameTicker.RunLevel != GameRunLevel.InRound)
+            return;
+
+        if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
+            return;
+
+        ChooseAntags((uid, component));
+        component.SelectionsComplete = true;
     }
-    #endregion
 
     /// <summary>
-    /// Helper method to calculate the number of antags to select based upon the number of players
+    /// Chooses antagonists from the current selection of players
     /// </summary>
-    /// <param name="playerCount">How many players there are on the server</param>
-    /// <param name="playersPerAntag">How many players should there be for an additional antag</param>
-    /// <param name="maxAntags">Maximum number of antags allowed</param>
-    /// <returns>The number of antags that should be chosen</returns>
-    public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags)
+    public void ChooseAntags(Entity<AntagSelectionComponent> ent)
     {
-        return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags);
+        var sessions = _playerManager.Sessions.ToList();
+        ChooseAntags(ent, sessions);
     }
 
-    #region Antag Selection
     /// <summary>
-    /// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc
+    /// Chooses antagonists from the given selection of players
     /// </summary>
-    /// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
-    /// <param name="count">How many items to select</param>
-    /// <returns>Up to the specified count of elements from all provided lists</returns>
-    public List<EntityUid> ChooseAntags(int count, params List<EntityUid>[] eligiblePlayerLists)
+    public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool)
     {
-        var chosenPlayers = new List<EntityUid>();
-        foreach (var playerList in eligiblePlayerLists)
+        foreach (var def in ent.Comp.Definitions)
         {
-            //Remove all chosen players from this list, to prevent duplicates
-            foreach (var chosenPlayer in chosenPlayers)
-            {
-                playerList.Remove(chosenPlayer);
-            }
-
-            //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));
+            ChooseAntags(ent, pool, def);
         }
-        return chosenPlayers;
     }
+
     /// <summary>
-    /// Helper method to choose antags from a list
+    /// Chooses antagonists from the given selection of players for the given antag definition.
     /// </summary>
-    /// <param name="eligiblePlayers">List of eligible players</param>
-    /// <param name="count">How many to choose</param>
-    /// <returns>Up to the specified count of elements from the provided list</returns>
-    public List<EntityUid> ChooseAntags(int count, List<EntityUid> eligiblePlayers)
+    public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool, AntagSelectionDefinition def)
     {
-        var chosenPlayers = new List<EntityUid>();
+        var playerPool = GetPlayerPool(ent, pool, def);
+        var count = GetTargetAntagCount(ent, playerPool, def);
 
         for (var i = 0; i < count; i++)
         {
-            if (eligiblePlayers.Count == 0)
-                break;
+            var session = (ICommonSession?) null;
+            if (def.PickPlayer)
+            {
+                if (!playerPool.TryPickAndTake(RobustRandom, out session))
+                    break;
 
-            chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
-        }
+                if (ent.Comp.SelectedSessions.Contains(session))
+                    continue;
+            }
 
-        return chosenPlayers;
+            MakeAntag(ent, session, def);
+        }
     }
 
     /// <summary>
-    /// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
+    /// Makes a given player into the specified antagonist.
     /// </summary>
-    /// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
-    /// <param name="count">How many items to select</param>
-    /// <returns>Up to the specified count of elements from all provided lists</returns>
-    public List<ICommonSession> ChooseAntags(int count, params List<ICommonSession>[] eligiblePlayerLists)
+    public void MakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false)
     {
-        var chosenPlayers = new List<ICommonSession>();
-        foreach (var playerList in 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<GhostComponent>(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)
         {
-            //Remove all chosen players from this list, to prevent duplicates
-            foreach (var chosenPlayer in chosenPlayers)
+            var getEntEv = new AntagSelectEntityEvent(session, ent);
+            RaiseLocalEvent(ent, ref getEntEv, true);
+
+            if (!getEntEv.Handled)
             {
-                playerList.Remove(chosenPlayer);
+                throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
             }
 
-            //If we have reached the desired number of players, skip
-            if (chosenPlayers.Count >= count)
-                continue;
+            antagEnt = getEntEv.Entity;
+        }
+
+        if (antagEnt is not { } player)
+            return;
 
-            //Pick and choose a random number of players from this list
-            chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList));
+        var getPosEv = new AntagSelectLocationEvent(session, ent);
+        RaiseLocalEvent(ent, ref getPosEv, true);
+        if (getPosEv.Handled)
+        {
+            var playerXform = Transform(player);
+            var pos = RobustRandom.Pick(getPosEv.Coordinates);
+            var mapEnt = _map.GetMap(pos.MapId);
+            _transform.SetMapCoordinates((player, playerXform), pos);
         }
-        return chosenPlayers;
-    }
-    /// <summary>
-    /// Helper method to choose sessions from a list
-    /// </summary>
-    /// <param name="eligiblePlayers">List of eligible sessions</param>
-    /// <param name="count">How many to choose</param>
-    /// <returns>Up to the specified count of elements from the provided list</returns>
-    public List<ICommonSession> ChooseAntags(int count, List<ICommonSession> eligiblePlayers)
-    {
-        var chosenPlayers = new List<ICommonSession>();
 
-        for (int i = 0; i < count; i++)
+        if (isSpawner)
         {
-            if (eligiblePlayers.Count == 0)
-                break;
+            if (!TryComp<GhostRoleAntagSpawnerComponent>(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 curMind = session.GetMind();
+            if (curMind == null)
+            {
+                curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
+                _mind.SetUserId(curMind.Value, session.UserId);
+            }
 
-            chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers));
+            EntityManager.AddComponents(curMind.Value, def.MindComponents);
+            _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
+            ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
+        }
+
+        if (def.Briefing is { } briefing)
+        {
+            SendBriefing(session, briefing);
         }
 
-        return chosenPlayers;
+        var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
+        RaiseLocalEvent(ent, ref afterEv, true);
     }
-    #endregion
 
-    #region Briefings
     /// <summary>
-    /// Helper method to send the briefing text and sound to a list of entities
+    /// Gets an ordered player pool based on player preferences and the antagonist definition.
     /// </summary>
-    /// <param name="entities">The players chosen to be antags</param>
-    /// <param name="briefing">The briefing text to send</param>
-    /// <param name="briefingColor">The color the briefing should be, null for default</param>
-    /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
-    public void SendBriefing(List<EntityUid> entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+    public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
     {
-        foreach (var entity in entities)
+        var primaryList = new List<ICommonSession>();
+        var secondaryList = new List<ICommonSession>();
+        var fallbackList = new List<ICommonSession>();
+        var rawList = new List<ICommonSession>();
+        foreach (var session in sessions)
         {
-            SendBriefing(entity, briefing, briefingColor, briefingSound);
+            if (!IsSessionValid(ent, session, def) ||
+                !IsEntityValid(session.AttachedEntity, def))
+            {
+                rawList.Add(session);
+                continue;
+            }
+
+            var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
+            if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
+            {
+                primaryList.Add(session);
+            }
+            else if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
+            {
+                secondaryList.Add(session);
+            }
+            else
+            {
+                fallbackList.Add(session);
+            }
         }
+
+        return new AntagSelectionPlayerPool(primaryList, secondaryList, fallbackList, rawList);
     }
 
     /// <summary>
-    /// Helper method to send the briefing text and sound to a player entity
+    /// Checks if a given session is valid for an antagonist.
     /// </summary>
-    /// <param name="entity">The entity chosen to be antag</param>
-    /// <param name="briefing">The briefing text to send</param>
-    /// <param name="briefingColor">The color the briefing should be, null for default</param>
-    /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
-    public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+    public bool IsSessionValid(Entity<AntagSelectionComponent> ent, ICommonSession session, AntagSelectionDefinition def, EntityUid? mind = null)
     {
-        if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent))
-            return;
+        mind ??= session.GetMind();
 
-        if (mindComponent.Session == null)
-            return;
+        if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
+            return false;
 
-        SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound);
-    }
+        if (ent.Comp.SelectedSessions.Contains(session))
+            return false;
 
-    /// <summary>
-    /// Helper method to send the briefing text and sound to a list of sessions
-    /// </summary>
-    /// <param name="sessions"></param>
-    /// <param name="briefing"></param>
-    /// <param name="briefingColor"></param>
-    /// <param name="briefingSound"></param>
+        //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
 
-    public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
-    {
-        foreach (var session in sessions)
+        switch (def.MultiAntagSetting)
         {
-            SendBriefing(session, briefing, briefingColor, briefingSound);
+            case AntagAcceptability.None:
+            {
+                if (_role.MindIsAntagonist(mind))
+                    return false;
+                break;
+            }
+            case AntagAcceptability.NotExclusive:
+            {
+                if (_role.MindIsExclusiveAntagonist(mind))
+                    return false;
+                break;
+            }
         }
+
+        // todo: expand this to allow for more fine antag-selection logic for game rules.
+        if (!_jobs.CanBeAntag(session))
+            return false;
+
+        return true;
     }
+
     /// <summary>
-    /// Helper method to send the briefing text and sound to a session
+    /// Checks if a given entity (mind/session not included) is valid for a given antagonist.
     /// </summary>
-    /// <param name="session">The player chosen to be an antag</param>
-    /// <param name="briefing">The briefing text to send</param>
-    /// <param name="briefingColor">The color the briefing should be, null for default</param>
-    /// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
-
-    public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
+    private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
     {
-        _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);
+        if (entity == null)
+            return false;
+
+        if (HasComp<PendingClockInComponent>(entity))
+            return false;
+
+        if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity))
+            return false;
+
+        if (def.Whitelist != null)
+        {
+            if (!def.Whitelist.IsValid(entity.Value, EntityManager))
+                return false;
+        }
+
+        if (def.Blacklist != null)
+        {
+            if (def.Blacklist.IsValid(entity.Value, EntityManager))
+                return false;
+        }
+
+        return true;
     }
-    #endregion
 }
+
+/// <summary>
+/// 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.
+/// </summary>
+[ByRefEvent]
+public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
+{
+    public readonly ICommonSession? Session = Session;
+
+    public bool Handled => Entity != null;
+
+    public EntityUid? Entity;
+}
+
+/// <summary>
+/// Event raised on a game rule entity to determine the location for the antagonist.
+/// </summary>
+[ByRefEvent]
+public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
+{
+    public readonly ICommonSession? Session = Session;
+
+    public bool Handled => Coordinates.Any();
+
+    public List<MapCoordinates> Coordinates = new();
+}
+
+/// <summary>
+/// Event raised on a game rule entity after the setup logic for an antag is complete.
+/// Used for applying additional more complex setup logic.
+/// </summary>
+[ByRefEvent]
+public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity<AntagSelectionComponent> GameRule, AntagSelectionDefinition Def);
diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Server/Antag/Components/AntagSelectionComponent.cs
new file mode 100644 (file)
index 0000000..096be14
--- /dev/null
@@ -0,0 +1,189 @@
+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
+{
+    /// <summary>
+    /// Has the primary selection of antagonists finished yet?
+    /// </summary>
+    [DataField]
+    public bool SelectionsComplete;
+
+    /// <summary>
+    /// The definitions for the antagonists
+    /// </summary>
+    [DataField]
+    public List<AntagSelectionDefinition> Definitions = new();
+
+    /// <summary>
+    /// The minds and original names of the players selected to be antagonists.
+    /// </summary>
+    [DataField]
+    public List<(EntityUid, string)> SelectedMinds = new();
+
+    /// <summary>
+    /// When the antag selection will occur.
+    /// </summary>
+    [DataField]
+    public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public HashSet<ICommonSession> SelectedSessions = new();
+}
+
+[DataDefinition]
+public partial struct AntagSelectionDefinition()
+{
+    /// <summary>
+    /// A list of antagonist roles that are used for selecting which players will be antagonists.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<AntagPrototype>> PrefRoles = new();
+
+    /// <summary>
+    /// Fallback for <see cref="PrefRoles"/>. Useful if you need multiple role preferences for a team antagonist.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<AntagPrototype>> FallbackRoles = new();
+
+    /// <summary>
+    /// Should we allow people who already have an antagonist role?
+    /// </summary>
+    [DataField]
+    public AntagAcceptability MultiAntagSetting = AntagAcceptability.None;
+
+    /// <summary>
+    /// The minimum number of this antag.
+    /// </summary>
+    [DataField]
+    public int Min = 1;
+
+    /// <summary>
+    /// The maximum number of this antag.
+    /// </summary>
+    [DataField]
+    public int Max = 1;
+
+    /// <summary>
+    /// A range used to randomly select <see cref="Min"/>
+    /// </summary>
+    [DataField]
+    public MinMax? MinRange;
+
+    /// <summary>
+    /// A range used to randomly select <see cref="Max"/>
+    /// </summary>
+    [DataField]
+    public MinMax? MaxRange;
+
+    /// <summary>
+    /// a player to antag ratio: used to determine the amount of antags that will be present.
+    /// </summary>
+    [DataField]
+    public int PlayerRatio = 10;
+
+    /// <summary>
+    /// Whether or not players should be picked to inhabit this antag or not.
+    /// </summary>
+    [DataField]
+    public bool PickPlayer = true;
+
+    /// <summary>
+    /// If true, players that latejoin into a round have a chance of being converted into antagonists.
+    /// </summary>
+    [DataField]
+    public bool LateJoinAdditional = false;
+
+    //todo: find out how to do this with minimal boilerplate: filler department, maybe?
+    //public HashSet<ProtoId<JobPrototype>> JobBlacklist = new()
+
+    /// <remarks>
+    /// Mostly just here for legacy compatibility and reducing boilerplate
+    /// </remarks>
+    [DataField]
+    public bool AllowNonHumans = false;
+
+    /// <summary>
+    /// A whitelist for selecting which players can become this antag.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// A blacklist for selecting which players can become this antag.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Blacklist;
+
+    /// <summary>
+    /// Components added to the player.
+    /// </summary>
+    [DataField]
+    public ComponentRegistry Components = new();
+
+    /// <summary>
+    /// Components added to the player's mind.
+    /// </summary>
+    [DataField]
+    public ComponentRegistry MindComponents = new();
+
+    /// <summary>
+    /// A set of starting gear that's equipped to the player.
+    /// </summary>
+    [DataField]
+    public ProtoId<StartingGearPrototype>? StartingGear;
+
+    /// <summary>
+    /// A briefing shown to the player.
+    /// </summary>
+    [DataField]
+    public BriefingData? Briefing;
+
+    /// <summary>
+    /// A spawner used to defer the selection of this particular definition.
+    /// </summary>
+    /// <remarks>
+    /// Not the cleanest way of doing this code but it's just an odd specific behavior.
+    /// Sue me.
+    /// </remarks>
+    [DataField]
+    public EntProtoId? SpawnerPrototype;
+}
+
+/// <summary>
+/// Contains data used to generate a briefing.
+/// </summary>
+[DataDefinition]
+public partial struct BriefingData
+{
+    /// <summary>
+    /// The text shown
+    /// </summary>
+    [DataField]
+    public LocId? Text;
+
+    /// <summary>
+    /// The color of the text.
+    /// </summary>
+    [DataField]
+    public Color? Color;
+
+    /// <summary>
+    /// The sound played.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? Sound;
+}
diff --git a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs b/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs
new file mode 100644 (file)
index 0000000..fcaa4d4
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Server.Antag.Components;
+
+/// <summary>
+/// Ghost role spawner that creates an antag for the associated gamerule.
+/// </summary>
+[RegisterComponent, Access(typeof(AntagSelectionSystem))]
+public sealed partial class GhostRoleAntagSpawnerComponent : Component
+{
+    [DataField]
+    public EntityUid? Rule;
+
+    [DataField]
+    public AntagSelectionDefinition? Definition;
+}
index 2446b976e1a6903039f44cce8445bc02b2b0f7bb..18837b5a7c873eae085b5b3b351584226199cf41 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Antag.Mimic;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.VendingMachines;
index b438e7c0e8dd3ba12772d8a0d88aeed4ed79231a..c44864183abe935aeb980531c20f975f0dd131b8 100644 (file)
@@ -1,4 +1,6 @@
-namespace Content.Server.Destructible.Thresholds
+using Robust.Shared.Random;
+
+namespace Content.Server.Destructible.Thresholds
 {
     [Serializable]
     [DataDefinition]
@@ -9,5 +11,16 @@
 
         [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);
+        }
     }
 }
similarity index 84%
rename from Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs
rename to Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs
index 956768bdd99e7fcdf039ad6e204c54bc0e2264fa..b9e6fa5d4b8b06489fd48adccfd29e4e3a06937f 100644 (file)
@@ -1,4 +1,4 @@
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Components;
 
 /// <summary>
 ///     Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
diff --git a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs b/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs
new file mode 100644 (file)
index 0000000..de4be83
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.GameTicking.Components;
+
+/// <summary>
+/// Generic component used to track a gamerule that's start has been delayed.
+/// </summary>
+[RegisterComponent, AutoGenerateComponentPause]
+public sealed partial class DelayedStartRuleComponent : Component
+{
+    /// <summary>
+    /// The time at which the rule will start properly.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+    public TimeSpan RuleStartTime;
+}
similarity index 81%
rename from Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs
rename to Content.Server/GameTicking/Components/EndedGameRuleComponent.cs
index 4484abd4d0b3e057209d9f037c94519275707103..3234bfff3a0e2277bb5e9e2e93449cd7d7efb935 100644 (file)
@@ -1,4 +1,4 @@
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Components;
 
 /// <summary>
 ///     Added to game rules before <see cref="GameRuleEndedEvent"/>.
similarity index 83%
rename from Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs
rename to Content.Server/GameTicking/Components/GameRuleComponent.cs
index 6309b9740208603746e3f8d31e70865ad1a9daba..1e6c3f0ab1df8c70ae0c01e2b7873642049ab5e7 100644 (file)
@@ -1,6 +1,7 @@
+using Content.Server.Destructible.Thresholds;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Components;
 
 /// <summary>
 /// Component attached to all gamerule entities.
@@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component
     /// </summary>
     [DataField]
     public int MinPlayers;
+
+    /// <summary>
+    /// A delay for when the rule the is started and when the starting logic actually runs.
+    /// </summary>
+    [DataField]
+    public MinMax? Delay;
 }
 
 /// <summary>
index 4ebe946af4ae1aa97e0ddf00e34d063409ed8925..f52a3cb296d20bf0d5fc6d5bcda56a6e60529fa3 100644 (file)
@@ -1,6 +1,6 @@
 using System.Linq;
 using Content.Server.Administration;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
 using Content.Shared.Administration;
 using Content.Shared.Database;
 using Content.Shared.Prototypes;
@@ -102,6 +102,22 @@ public sealed partial class GameTicker
         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<DelayedStartRuleComponent>(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<DelayedStartRuleComponent>(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)}");
@@ -255,6 +271,18 @@ public sealed partial class GameTicker
         }
     }
 
+    private void UpdateGameRules()
+    {
+        var query = EntityQueryEnumerator<DelayedStartRuleComponent, GameRuleComponent>();
+        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)]
@@ -323,38 +351,3 @@ public sealed partial class GameTicker
 
     #endregion
 }
-
-/*
-/// <summary>
-///     Raised broadcast when a game rule is selected, but not started yet.
-/// </summary>
-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;
-    }
-}
-*/
index efda3df0ca19a475a4a7413574d953830f1dff46..fa23312268f57ad51aef0fa95417cf4b6bdfc1bb 100644 (file)
@@ -133,6 +133,7 @@ namespace Content.Server.GameTicking
                 return;
             base.Update(frameTime);
             UpdateRoundFlow(frameTime);
+            UpdateGameRules();
         }
     }
 }
diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs
new file mode 100644 (file)
index 0000000..463aecb
--- /dev/null
@@ -0,0 +1,29 @@
+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;
+
+/// <summary>
+/// This is used for a game rule that loads a map when activated.
+/// </summary>
+[RegisterComponent]
+public sealed partial class LoadMapRuleComponent : Component
+{
+    [DataField]
+    public MapId? Map;
+
+    [DataField]
+    public ProtoId<GameMapPrototype>? GameMap ;
+
+    [DataField]
+    public ResPath? MapPath;
+
+    [DataField]
+    public List<EntityUid> MapGrids = new();
+
+    [DataField]
+    public EntityWhitelist? SpawnerWhitelist;
+}
index e6966c1e377cccf2163dd08b036b0e58ec5e7d9c..fa352eb320bda2b09246c3504ce8945d4ae07940 100644 (file)
@@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components;
 
 /// <summary>
 /// Stores some configuration used by the ninja system.
-/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent/">.
+/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent"/>.
 /// </summary>
 [RegisterComponent, Access(typeof(SpaceNinjaSystem))]
 public sealed partial class NinjaRuleComponent : Component
index e02d90c18bf7616bce17a86c16d2625dd8f77574..bb1b7c87460689db4aa8f1ca7609d2a065597dfb 100644 (file)
@@ -1,6 +1,3 @@
-using Content.Shared.Roles;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
 namespace Content.Server.GameTicking.Rules.Components;
 
 /// <summary>
@@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components;
 /// TODO: Remove once systems can request spawns from the ghost role system directly.
 /// </summary>
 [RegisterComponent]
-public sealed partial class NukeOperativeSpawnerComponent : Component
-{
-    [DataField("name", required:true)]
-    public string OperativeName = default!;
+public sealed partial class NukeOperativeSpawnerComponent : Component;
 
-    [DataField]
-    public NukeopSpawnPreset SpawnDetails = default!;
-}
index 358b157cdf3e8cdf417c76d85e9ea26a7aa10e28..3d097cd7c7968a0d9c21ba364929d5d0d4b4f42a 100644 (file)
@@ -6,4 +6,6 @@
 [RegisterComponent]
 public sealed partial class NukeOpsShuttleComponent : Component
 {
+    [DataField]
+    public EntityUid AssociatedRule;
 }
index 8f11e70560f5aaa4ca2cbe88afbd30ad6dca2eaf..e05c3e5db65443b3c0c4094e19a611a67066f30d 100644 (file)
@@ -1,27 +1,16 @@
-using Content.Server.Maps;
 using Content.Server.RoundEnd;
-using Content.Server.StationEvents.Events;
 using Content.Shared.Dataset;
 using Content.Shared.NPC.Prototypes;
 using Content.Shared.Roles;
-using Robust.Shared.Map;
+using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Server.GameTicking.Rules.Components;
 
-[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
+[RegisterComponent, Access(typeof(NukeopsRuleSystem))]
 public sealed partial class NukeopsRuleComponent : Component
 {
-    /// <summary>
-    /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
-    /// </summary>
-    [DataField]
-    public int PlayersPerOperative = 10;
-
-    [DataField]
-    public int MaxOps = 5;
-
     /// <summary>
     /// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
     /// </summary>
@@ -52,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component
     [DataField]
     public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3);
 
-    /// <summary>
-    /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
-    /// </summary>
-    [DataField]
-    public bool SpawnOutpost = true;
-
     /// <summary>
     /// Whether or not nukie left their outpost
     /// </summary>
@@ -80,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component
     ///     This amount of TC will be given to each nukie
     /// </summary>
     [DataField]
-    public int WarTCAmountPerNukie = 40;
+    public int WarTcAmountPerNukie = 40;
 
     /// <summary>
     ///     Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
@@ -94,50 +77,23 @@ public sealed partial class NukeopsRuleComponent : Component
     [DataField]
     public int WarDeclarationMinOps = 4;
 
-    [DataField]
-    public EntProtoId SpawnPointProto = "SpawnPointNukies";
-
-    [DataField]
-    public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
-
-    [DataField]
-    public string OperationName = "Test Operation";
-
-    [DataField]
-    public ProtoId<GameMapPrototype> OutpostMapPrototype = "NukieOutpost";
-
     [DataField]
     public WinType WinType = WinType.Neutral;
 
     [DataField]
     public List<WinCondition> WinConditions = new ();
 
-    // TODO full game save
-    // 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.
-    // Whenever this gets fixed, update NukiesTest.
-    public EntityUid? NukieOutpost;
-    public EntityUid? NukieShuttle;
-    public EntityUid? TargetStation;
-    public MapId? NukiePlanet;
-
-    /// <summary>
-    ///     Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
-    /// </summary>
-    [DataField]
-    public Dictionary<EntityUid, string> OperativeMindPendingData = new();
-
-    [DataField(required: true)]
-    public ProtoId<NpcFactionPrototype> Faction;
-
     [DataField]
-    public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" };
+    public EntityUid? TargetStation;
 
     [DataField]
-    public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" };
+    public ProtoId<NpcFactionPrototype> Faction = "Syndicate";
 
+    /// <summary>
+    ///     Path to antagonist alert sound.
+    /// </summary>
     [DataField]
-    public NukeopSpawnPreset OperativeSpawnDetails = new();
+    public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
 }
 
 /// <summary>
diff --git a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs
deleted file mode 100644 (file)
index 1d03b41..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-using Robust.Shared.Audio;
-
-namespace Content.Server.GameTicking.Rules.Components;
-
-[RegisterComponent, Access(typeof(PiratesRuleSystem))]
-public sealed partial class PiratesRuleComponent : Component
-{
-    [ViewVariables]
-    public List<EntityUid> Pirates = new();
-    [ViewVariables]
-    public EntityUid PirateShip = EntityUid.Invalid;
-    [ViewVariables]
-    public HashSet<EntityUid> InitialItems = new();
-    [ViewVariables]
-    public double InitialShipValue;
-
-    /// <summary>
-    ///     Path to antagonist alert sound.
-    /// </summary>
-    [DataField("pirateAlertSound")]
-    public SoundSpecifier PirateAlertSound = new SoundPathSpecifier(
-        "/Audio/Ambience/Antag/pirate_start.ogg",
-        AudioParams.Default.WithVolume(4));
-}
index 2ce3f1f9a66e2faf63088de022294a0c1678a4f5..3b19bbffb6aee290d13092217405d3274ad9ae24 100644 (file)
@@ -22,43 +22,6 @@ public sealed partial class RevolutionaryRuleComponent : Component
     [DataField]
     public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
 
-    /// <summary>
-    /// Stores players minds
-    /// </summary>
-    [DataField]
-    public Dictionary<string, EntityUid> HeadRevs = new();
-
-    [DataField]
-    public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev";
-
-    /// <summary>
-    /// Min players needed for Revolutionary gamemode to start.
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public int MinPlayers = 15;
-
-    /// <summary>
-    /// Max Head Revs allowed during selection.
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public int MaxHeadRevs = 3;
-
-    /// <summary>
-    /// The amount of Head Revs that will spawn per this amount of players.
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public int PlayersPerHeadRev = 15;
-
-    /// <summary>
-    /// The gear head revolutionaries are given on spawn.
-    /// </summary>
-    [DataField]
-    public List<EntProtoId> StartingGear = new()
-    {
-        "Flash",
-        "ClothingEyesGlassesSunglasses"
-    };
-
     /// <summary>
     /// The time it takes after the last head is killed for the shuttle to arrive.
     /// </summary>
index 9dfd6e6627cc28127cba50ef5328730346139edd..01a078625ae3ff2ed583a03e51bb9d539be33b1a 100644 (file)
@@ -1,12 +1,11 @@
 using Content.Shared.Random;
-using Content.Shared.Roles;
 using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
 
 namespace Content.Server.GameTicking.Rules.Components;
 
 /// <summary>
-/// Stores data for <see cref="ThiefRuleSystem/">.
+/// Stores data for <see cref="ThiefRuleSystem"/>.
 /// </summary>
 [RegisterComponent, Access(typeof(ThiefRuleSystem))]
 public sealed partial class ThiefRuleComponent : Component
@@ -23,42 +22,9 @@ public sealed partial class ThiefRuleComponent : Component
     [DataField]
     public float BigObjectiveChance = 0.7f;
 
-    /// <summary>
-    /// Add a Pacified comp to thieves
-    /// </summary>
-    [DataField]
-    public bool PacifistThieves = true;
-
-    [DataField]
-    public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief";
-
     [DataField]
     public float MaxObjectiveDifficulty = 2.5f;
 
     [DataField]
     public int MaxStealObjectives = 10;
-
-    /// <summary>
-    /// Things that will be given to thieves
-    /// </summary>
-    [DataField]
-    public List<EntProtoId> StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
-
-    /// <summary>
-    /// All Thieves created by this rule
-    /// </summary>
-    [DataField]
-    public List<EntityUid> ThievesMinds = new();
-
-    /// <summary>
-    /// Max Thiefs created by rule on roundstart
-    /// </summary>
-    [DataField]
-    public int MaxAllowThief = 3;
-
-    /// <summary>
-    /// Sound played when making the player a thief via antag control or ghost role
-    /// </summary>
-    [DataField]
-    public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg");
 }
index e904d8a7c2abb3d120acfe399ab6e2f28cb46fe9..ea5c9a830b8fd4d3e04517d263912a718b2c73c8 100644 (file)
@@ -57,4 +57,19 @@ public sealed partial class TraitorRuleComponent : Component
     /// </summary>
     [DataField]
     public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
+
+    /// <summary>
+    /// The amount of codewords that are selected.
+    /// </summary>
+    [DataField]
+    public int CodewordCount = 4;
+
+    /// <summary>
+    /// The amount of TC traitors start with.
+    /// </summary>
+    [DataField]
+    public int StartingBalance = 20;
+
+    [DataField]
+    public int MaxDifficulty = 20;
 }
index 4fe91e3a5f5e13b521bbf6a58737ca168a9ef776..59d1940eafe260630aff0c41f7c17d1f762c6bb2 100644 (file)
@@ -8,12 +8,6 @@ namespace Content.Server.GameTicking.Rules.Components;
 [RegisterComponent, Access(typeof(ZombieRuleSystem))]
 public sealed partial class ZombieRuleComponent : Component
 {
-    [DataField]
-    public Dictionary<string, string> InitialInfectedNames = new();
-
-    [DataField]
-    public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
-
     /// <summary>
     /// When the round will next check for round end.
     /// </summary>
@@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component
     [DataField]
     public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
 
-    /// <summary>
-    /// The time at which the initial infected will be chosen.
-    /// </summary>
-    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan? StartTime;
-
-    /// <summary>
-    /// The minimum amount of time after the round starts that the initial infected will be chosen.
-    /// </summary>
-    [DataField]
-    public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
-
-    /// <summary>
-    /// The maximum amount of time after the round starts that the initial infected will be chosen.
-    /// </summary>
-    [DataField]
-    public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
-
-    /// <summary>
-    /// The sound that plays when someone becomes an initial infected.
-    /// todo: this should have a unique sound instead of reusing the zombie one.
-    /// </summary>
-    [DataField]
-    public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
-
-    /// <summary>
-    /// The minimum amount of time initial infected have before they start taking infection damage.
-    /// </summary>
-    [DataField]
-    public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
-
-    /// <summary>
-    /// The maximum amount of time initial infected have before they start taking damage.
-    /// </summary>
-    [DataField]
-    public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
-
-    /// <summary>
-    /// How many players for each initial infected.
-    /// </summary>
-    [DataField]
-    public int PlayersPerInfected = 10;
-
-    /// <summary>
-    /// The maximum number of initial infected.
-    /// </summary>
-    [DataField]
-    public int MaxInitialInfected = 6;
-
     /// <summary>
     /// After this amount of the crew become zombies, the shuttle will be automatically called.
     /// </summary>
     [DataField]
     public float ZombieShuttleCallPercentage = 0.7f;
-
-    [DataField]
-    public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
 }
index 82ac755592e6c8dc623914f38f77ecb790da99f3..78b8a8a85c865e6027d6a92d6d3893ddfbd5fcdb 100644 (file)
@@ -1,5 +1,6 @@
 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;
@@ -33,7 +34,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
         SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
         SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
         SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
-        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
     }
 
     private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
@@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
         _roundEnd.EndRound(component.RestartDelay);
     }
 
-    private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
+    protected override void AppendRoundEndText(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
     {
-        var query = EntityQueryEnumerator<DeathMatchRuleComponent, PointManagerComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, rule))
-                continue;
+        if (!TryComp<PointManagerComponent>(uid, out var point))
+            return;
 
-            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());
+        if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data))
+        {
+            args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName)));
+            args.AddLine("");
         }
+        args.AddLine(Loc.GetString("point-scoreboard-header"));
+        args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup());
     }
 }
index a60a2bfe22f2fb18abdb1f1c30288085783562e2..45343334179e2c6628db939ac73ca0a4eb02efb3 100644 (file)
@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
 using Robust.Shared.Collections;
@@ -15,31 +16,6 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
         return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
     }
 
-    protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
-    {
-        var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
-        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;
-    }
-
     /// <summary>
     ///     Utility function for finding a random event-eligible station entity
     /// </summary>
index 363c2ad7f752c9bf0426c02a66766ffd250da349..bcad146c22d2611a53475780263ebf37e6337e8c 100644 (file)
@@ -1,6 +1,6 @@
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
 using Robust.Server.GameObjects;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
@@ -22,9 +22,31 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
     {
         base.Initialize();
 
+        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
         SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
         SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
         SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
+        SubscribeLocalEvent<T, RoundEndTextAppendEvent>(OnRoundEndTextAppend);
+    }
+
+    private void OnStartAttempt(RoundStartAttemptEvent args)
+    {
+        if (args.Forced || args.Cancelled)
+            return;
+
+        var query = QueryActiveRules();
+        while (query.MoveNext(out var uid, out _, 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)
@@ -48,6 +70,12 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
         Ended(uid, component, ruleData, args);
     }
 
+    private void OnRoundEndTextAppend(Entity<T> ent, ref RoundEndTextAppendEvent args)
+    {
+        if (!TryComp<GameRuleComponent>(ent, out var ruleData))
+            return;
+        AppendRoundEndText(ent, ent, ruleData, ref args);
+    }
 
     /// <summary>
     /// Called when the gamerule is added
@@ -73,6 +101,14 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
 
     }
 
+    /// <summary>
+    /// Called at the end of a round when text needs to be added for a game rule.
+    /// </summary>
+    protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
+    {
+
+    }
+
     /// <summary>
     /// Called on an active gamerule entity in the Update function
     /// </summary>
index b775b7af5640e12ea11f6d7f93549826b4e893ad..01fa387595cc1781cb9c0cf5d32e823a66072c22 100644 (file)
@@ -1,5 +1,6 @@
 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;
index 01fd97d9a7962552c8de4ca7e38a7eb54515bd58..3da55e30c9e58bd675feb0dc4f45414f2e25eb63 100644 (file)
@@ -1,4 +1,5 @@
 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
new file mode 100644 (file)
index 0000000..aba9ed9
--- /dev/null
@@ -0,0 +1,80 @@
+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<LoadMapRuleComponent>
+{
+    [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<LoadMapRuleComponent, AntagSelectLocationEvent>(OnSelectLocation);
+        SubscribeLocalEvent<GridSplitEvent>(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<LoadMapRuleComponent> ent, ref AntagSelectLocationEvent args)
+    {
+        var query = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
+        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));
+        }
+    }
+}
index 2522ebb53b580e59068c25214b3c01a3c4234c4e..cae99fee9fc3c8b03e48da0e54b49e6486487849 100644 (file)
@@ -1,5 +1,6 @@
 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;
 
index d521e26396a1ba8163488d07611883f171d7f46b..2f8b9dc9276e2cd83423a78da34498213bfc3adf 100644 (file)
@@ -1,31 +1,20 @@
-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.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.NPC.Components;
@@ -33,45 +22,30 @@ using Content.Shared.NPC.Systems;
 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;
 
 namespace Content.Server.GameTicking.Rules;
 
 public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 {
-    [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<CurrencyPrototype>]
     private const string TelecrystalCurrencyPrototype = "Telecrystal";
@@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
     [ValidatePrototypeId<TagPrototype>]
     private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
 
-    [ValidatePrototypeId<AntagPrototype>]
-    public const string NukeopsId = "Nukeops";
-
-    [ValidatePrototypeId<DatasetPrototype>]
-    private const string OperationPrefixDataset = "operationPrefix";
-
-    [ValidatePrototypeId<DatasetPrototype>]
-    private const string OperationSuffixDataset = "operationSuffix";
-
     public override void Initialize()
     {
         base.Initialize();
 
-        _sawmill = _logManager.GetSawmill("NukeOps");
-
-        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
-        SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayersSpawning);
-        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
         SubscribeLocalEvent<NukeExplodedEvent>(OnNukeExploded);
         SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
         SubscribeLocalEvent<NukeDisarmSuccessEvent>(OnNukeDisarm);
 
         SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
         SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
-        SubscribeLocalEvent<NukeOperativeComponent, GhostRoleSpawnerUsedEvent>(OnPlayersGhostSpawning);
-        SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
         SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
 
+        SubscribeLocalEvent<NukeOpsShuttleComponent, MapInitEvent>(OnMapInit);
+
         SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
         SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
         SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
+
+        SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
+        SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
     }
 
     protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
         GameRuleStartedEvent args)
     {
-        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 _))
+        var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
+        var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
+        while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
         {
-            if (!SpawnMap((uid, nukeops)))
-            {
-                _sawmill.Info("Failed to load map for nukeops");
-                continue;
-            }
-
-            //Handle there being nobody readied up
-            if (ev.PlayerPool.Count == 0)
+            if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member)))
                 continue;
 
-            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<NukieSpawn> { 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));
-                }
-            }
-
-            SpawnOperatives(operatives, _cfg.GetCVar(CCVars.NukeopsSpawnGhostRoles), nukeops);
+            eligible.Add((eligibleUid, eligibleComp, member));
+        }
 
-            foreach (var nukieSpawn in operatives)
-            {
-                if (nukieSpawn.Session == null)
-                    continue;
+        if (eligible.Count == 0)
+            return;
 
-                GameTicker.PlayerJoinGame(nukieSpawn.Session);
-            }
-        }
+        component.TargetStation = RobustRandom.Pick(eligible);
     }
 
-    private void OnRoundEndText(RoundEndTextAppendEvent ev)
+    #region Event Handlers
+    protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
+        ref RoundEndTextAppendEvent args)
     {
-        var ruleQuery = QueryActiveRules();
-        while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _))
-        {
-            var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
-            ev.AddLine(winText);
+        var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
+        args.AddLine(winText);
 
-            foreach (var cond in nukeops.WinConditions)
-            {
-                var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
-                ev.AddLine(text);
-            }
+        foreach (var cond in component.WinConditions)
+        {
+            var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
+            args.AddLine(text);
         }
 
-        ev.AddLine(Loc.GetString("nukeops-list-start"));
+        args.AddLine(Loc.GetString("nukeops-list-start"));
 
-        var nukiesQuery = EntityQueryEnumerator<NukeopsRoleComponent, MindContainerComponent>();
-        while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer))
-        {
-            if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer))
-                continue;
+        var antags =_antag.GetAntagIdentifiers(uid);
 
-            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))));
+        foreach (var (_, sessionData, name) in antags)
+        {
+            args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName)));
         }
     }
 
@@ -224,10 +124,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         {
             if (ev.OwningStation != null)
             {
-                if (ev.OwningStation == nukeops.NukieOutpost)
+                if (ev.OwningStation == GetOutpost(uid))
                 {
                     nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
-                    SetWinType(uid, WinType.CrewMajor, nukeops);
+                    SetWinType((uid, nukeops), WinType.CrewMajor);
                     continue;
                 }
 
@@ -242,7 +142,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
                         }
 
                         nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
-                        SetWinType(uid, WinType.OpsMajor, nukeops);
+                        SetWinType((uid, nukeops), WinType.OpsMajor);
                         correctStation = true;
                     }
 
@@ -263,19 +163,85 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
     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 _))
         {
-            switch (ev.New)
+            OnRoundEnd((uid, nukeops));
+        }
+    }
+
+    private void OnRoundEnd(Entity<NukeopsRuleComponent> 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<NukeComponent, TransformComponent>();
+        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))
             {
-                case GameRunLevel.InRound:
-                    OnRoundStart(uid, nukeops);
-                    break;
-                case GameRunLevel.PostRound:
-                    OnRoundEnd(uid, nukeops);
-                    break;
+                ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
+                SetWinType((ent, ent), WinType.OpsMajor);
+                return;
+            }
+
+            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<NukeDiskComponent, TransformComponent>();
+        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)
@@ -294,66 +260,31 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
             CheckRoundShouldEnd();
     }
 
-    private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args)
+    private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args)
     {
-        var spawner = args.Spawner;
-
-        if (!TryComp<NukeOperativeSpawnerComponent>(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<NukeopsRuleComponent>())
-        {
-            SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.SpawnDetails, profile);
-
-            nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.SpawnDetails.AntagRoleProto);
-        }
+        RemCompDeferred(uid, component);
     }
 
-    private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
+    private void OnMapInit(Entity<NukeOpsShuttleComponent> ent, ref MapInitEvent args)
     {
-        if (!_mind.TryGetMind(uid, out var mindId, out var mind))
-            return;
+        var map = Transform(ent).MapID;
 
-        var query = QueryActiveRules();
-        while (query.MoveNext(out _, out _, out var nukeops, out _))
+        var rules = EntityQueryEnumerator<NukeopsRuleComponent, LoadMapRuleComponent>();
+        while (rules.MoveNext(out var uid, out _, out var mapRule))
         {
-            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);
-            }
+            if (map != mapRule.Map)
+                continue;
+            ent.Comp.AssociatedRule = uid;
+            break;
         }
     }
 
-    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 _, out _, out var nukeops, out _))
+        while (query.MoveNext(out var uid, out _, out var nukeops, out _))
         {
-            if (ev.Uid != nukeops.NukieShuttle)
+            if (ev.Uid != GetShuttle((uid, nukeops)))
                 continue;
 
             if (nukeops.WarDeclaredTime != null)
@@ -397,12 +328,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
     {
         // TODO: this is VERY awful for multi-nukies
         var query = QueryActiveRules();
-        while (query.MoveNext(out _, out _, out var nukeops, out _))
+        while (query.MoveNext(out var uid, out _, out var nukeops, out _))
         {
             if (nukeops.WarDeclaredTime != null)
                 continue;
 
-            if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet)
+            if (TryComp<LoadMapRuleComponent>(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map)
                 continue;
 
             var newStatus = GetWarCondition(nukeops, ev.Status);
@@ -448,161 +379,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
             if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype))
                 continue;
 
-            if (!nukieRule.NukieOutpost.HasValue)
+            if (GetOutpost(uid) is not {} outpost)
                 continue;
 
-            if (Transform(uid).MapID != Transform(nukieRule.NukieOutpost.Value).MapID) // Will receive bonus TC only on their start outpost
+            if (Transform(uid).MapID != Transform(outpost).MapID) // Will receive bonus TC only on their start outpost
                 continue;
 
-            _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.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 OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null)
+    private void SetWinType(Entity<NukeopsRuleComponent> ent, WinType type, bool endRound = true)
     {
-        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<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
-        var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
-        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<NukeOperativeComponent, ActorComponent>();
-        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<NukeComponent, TransformComponent>();
-        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<NukeopsRoleComponent, MindContainerComponent, MobStateComponent>();
-        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<NukeDiskComponent, TransformComponent>();
-
-        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;
+        ent.Comp.WinType = type;
 
         if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor))
             _roundEndSystem.EndRound();
@@ -613,243 +405,130 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         var query = QueryActiveRules();
         while (query.MoveNext(out var uid, out _, out var nukeops, out _))
         {
-            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<NukeComponent>())
-            {
-                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<NukeOperativeComponent, MobStateComponent, TransformComponent>(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<NukeOperativeSpawnerComponent>(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;
+            CheckRoundShouldEnd((uid, nukeops));
         }
     }
 
-    private bool SpawnMap(Entity<NukeopsRuleComponent> ent)
+    private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> 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<NukeOpsShuttleComponent, TransformComponent>();
-        while (query.MoveNext(out var grid, out _, out var shuttleTransform))
-        {
-            if (shuttleTransform.MapID != ent.Comp.NukiePlanet)
-                continue;
-
-            ent.Comp.NukieShuttle = grid;
-            break;
-        }
-
-        return true;
-    }
-
-    /// <summary>
-    ///     Adds missing nuke operative components, equips starting gear and renames the entity.
-    /// </summary>
-    private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile)
-    {
-        _metaData.SetEntityName(mob, name);
-        EnsureComp<NukeOperativeComponent>(mob);
-
-        if (profile != null)
-            _humanoid.LoadProfile(mob, profile);
+        var nukeops = ent.Comp;
 
-        var gear = _prototypeManager.Index(spawnDetails.GearProto);
-        _stationSpawning.EquipStartingGear(mob, gear);
-
-        _npcFaction.RemoveFaction(mob, "NanoTrasen", false);
-        _npcFaction.AddFaction(mob, "Syndicate");
-    }
-
-    private void SpawnOperatives(List<NukieSpawn> sessions, bool spawnGhostRoles, NukeopsRuleComponent component)
-    {
-        if (component.NukieOutpost is not { Valid: true } outpostUid)
+        if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor)
             return;
 
-        var spawns = new List<EntityCoordinates>();
-        foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
-        {
-            if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id)
-                continue;
 
-            if (xform.ParentUid != component.NukieOutpost)
-                continue;
-
-            spawns.Add(xform.Coordinates);
-            break;
-        }
-
-        //Fallback, spawn at the centre of the map
-        if (spawns.Count == 0)
+        // If there are any nuclear bombs that are active, immediately return. We're not over yet.
+        foreach (var nuke in EntityQuery<NukeComponent>())
         {
-            spawns.Add(Transform(outpostUid).Coordinates);
-            _sawmill.Warning($"Fell back to default spawn for nukies!");
+            if (nuke.Status == NukeStatus.ARMED)
+                return;
         }
 
-        //Spawn the team
-        foreach (var nukieSession in sessions)
-        {
-            var name = $"{Loc.GetString(nukieSession.Type.NamePrefix)} {RobustRandom.PickAndTake(_prototypeManager.Index(nukieSession.Type.NameList).Values.ToList())}";
-
-            var nukeOpsAntag = _prototypeManager.Index(nukieSession.Type.AntagRoleProto);
-
-            //If a session is available, spawn mob and transfer mind into it
-            if (nukieSession.Session != null)
-            {
-                var profile = _prefs.GetPreferences(nukieSession.Session.UserId).SelectedCharacter as HumanoidCharacterProfile;
-                if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
-                {
-                    species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
-                }
+        var shuttle = GetShuttle((ent, ent));
 
-                var mob = Spawn(species.Prototype, RobustRandom.Pick(spawns));
-                SetupOperativeEntity(mob, name, nukieSession.Type, profile);
+        MapId? shuttleMapId = Exists(shuttle)
+            ? Transform(shuttle.Value).MapID
+            : null;
 
-                var newMind = _mind.CreateMind(nukieSession.Session.UserId, name);
-                _mind.SetUserId(newMind, nukieSession.Session.UserId);
-                _roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto });
-
-                _mind.TransferTo(newMind, mob);
-            }
-            //Otherwise, spawn as a ghost role
-            else if (spawnGhostRoles)
-            {
-                var spawnPoint = Spawn(component.GhostSpawnPointProto, RobustRandom.Pick(spawns));
-                var ghostRole = EnsureComp<GhostRoleComponent>(spawnPoint);
-                EnsureComp<GhostRoleMobSpawnerComponent>(spawnPoint);
-                ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name);
-                ghostRole.RoleDescription = Loc.GetString(nukeOpsAntag.Objective);
-
-                var nukeOpSpawner = EnsureComp<NukeOperativeSpawnerComponent>(spawnPoint);
-                nukeOpSpawner.OperativeName = name;
-                nukeOpSpawner.SpawnDetails = nukieSession.Type;
-            }
+        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;
         }
-    }
 
-    /// <summary>
-    /// Display a greeting message and play a sound for a nukie
-    /// </summary>
-    private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule)
-    {
-        if (nukeopsRule.TargetStation is not { } station)
-            return;
-
-        _antagSelection.SendBriefing(session, Loc.GetString("nukeops-welcome", ("station", station), ("name", nukeopsRule.OperationName)), Color.Red, nukeop.GreetSoundNotification);
+        // 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<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
+        var operativesAlive = operatives
+            .Where(op =>
+                op.Item3.MapID == shuttleMapId
+                || op.Item3.MapID == targetStationMap)
+            .Any(op => op.Item2.CurrentState == MobState.Alive && op.Item1.Running);
+
+        if (operativesAlive)
+            return; // 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<NukeOperativeSpawnerComponent>(true).Any();
+        if (spawnsAvailable && CompOrNull<LoadMapRuleComponent>(ent)?.Map == shuttleMapId)
+            return; // 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(ent, WinType.CrewMajor, false);
+        _roundEndSystem.DoRoundEndBehavior(
+            nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
+
+        // prevent it called multiple times
+        nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
     }
 
-    /// <summary>
-    /// Spawn nukie ghost roles if this gamerule was started mid round
-    /// </summary>
-    private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
+    // this should really go anywhere else but im tired.
+    private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
     {
-        if (!Resolve(uid, ref component))
+        if (args.Handled)
             return;
 
-        if (!SpawnMap((uid, component)))
+        var profile = args.Session != null
+            ? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile
+            : HumanoidCharacterProfile.RandomWithSpecies();
+        if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
         {
-            _sawmill.Info("Failed to load map for nukeops");
-            return;
+            species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
         }
 
-        var numNukies = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerOperative, component.MaxOps);
+        args.Entity = Spawn(species.Prototype);
+        _humanoid.LoadProfile(args.Entity.Value, profile);
+    }
 
-        //Dont continue if we have no nukies to spawn
-        if (numNukies == 0)
+    private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
+    {
+        if (ent.Comp.TargetStation is not { } station)
             return;
 
-        //Fill the ranks, commander first, then agent, then operatives
-        //TODO: Possible alternative team compositions? Like multiple commanders or agents
-        var operatives = new List<NukieSpawn>();
-        if (numNukies >= 1)
-            operatives.Add(new NukieSpawn(null, component.CommanderSpawnDetails));
-        if (numNukies >= 2)
-            operatives.Add(new NukieSpawn(null, component.AgentSpawnDetails));
-        if (numNukies >= 3)
-        {
-            for (var i = 2; i < numNukies; i++)
-            {
-                operatives.Add(new NukieSpawn(null, component.OperativeSpawnDetails));
-            }
-        }
-
-        SpawnOperatives(operatives, true, component);
+        _antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
+                ("station", station),
+                ("name", Name(ent))),
+            Color.Red,
+            ent.Comp.GreetSoundNotification);
     }
 
-    //For admins forcing someone to nukeOps.
-    public void MakeLoneNukie(EntityUid entity)
+    /// <remarks>
+    /// Is this method the shitty glue holding together the last of my sanity? yes.
+    /// Do i have a better solution? not presently.
+    /// </remarks>
+    private EntityUid? GetOutpost(Entity<LoadMapRuleComponent?> ent)
     {
-        if (!_mind.TryGetMind(entity, out var mindId, out var mindComponent))
-            return;
+        if (!Resolve(ent, ref ent.Comp, false))
+            return null;
 
-        //ok hardcoded value bad but so is everything else here
-        _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = NukeopsId }, mindComponent);
-        SetOutfitCommand.SetOutfit(entity, "SyndicateOperativeGearFull", EntityManager);
+        return ent.Comp.MapGrids.FirstOrNull();
     }
 
-    private sealed class NukieSpawn
+    /// <remarks>
+    /// Is this method the shitty glue holding together the last of my sanity? yes.
+    /// Do i have a better solution? not presently.
+    /// </remarks>
+    private EntityUid? GetShuttle(Entity<NukeopsRuleComponent?> ent)
     {
-        public ICommonSession? Session { get; private set; }
-        public NukeopSpawnPreset Type { get; private set; }
+        if (!Resolve(ent, ref ent.Comp, false))
+            return null;
 
-        public NukieSpawn(ICommonSession? session, NukeopSpawnPreset type)
+        var query = EntityQueryEnumerator<NukeOpsShuttleComponent>();
+        while (query.MoveNext(out var uid, out var comp))
         {
-            Session = session;
-            Type = type;
+            if (comp.AssociatedRule == ent.Owner)
+                return uid;
         }
+
+        return null;
     }
 }
diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs
deleted file mode 100644 (file)
index 0a749d2..0000000
+++ /dev/null
@@ -1,321 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Server.Administration.Commands;
-using Content.Server.Cargo.Systems;
-using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Preferences.Managers;
-using Content.Server.Spawners.Components;
-using Content.Server.Station.Components;
-using Content.Server.Station.Systems;
-using Content.Shared.CCVar;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Mind;
-using Content.Shared.NPC.Prototypes;
-using Content.Shared.NPC.Systems;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Server.GameObjects;
-using Robust.Server.Maps;
-using Robust.Server.Player;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Configuration;
-using Robust.Shared.Enums;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.GameTicking.Rules;
-
-/// <summary>
-/// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion.
-/// </summary>
-public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
-{
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly IConfigurationManager _cfg = default!;
-    [Dependency] private readonly IChatManager _chatManager = default!;
-    [Dependency] private readonly IMapManager _mapManager = default!;
-    [Dependency] private readonly IServerPreferencesManager _prefs = default!;
-    [Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
-    [Dependency] private readonly PricingSystem _pricingSystem = default!;
-    [Dependency] private readonly MapLoaderSystem _map = default!;
-    [Dependency] private readonly NamingSystem _namingSystem = default!;
-    [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
-    [Dependency] private readonly SharedMindSystem _mindSystem = default!;
-    [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
-    [Dependency] private readonly MetaDataSystem _metaData = default!;
-
-    [ValidatePrototypeId<EntityPrototype>]
-    private const string GameRuleId = "Pirates";
-
-    [ValidatePrototypeId<EntityPrototype>]
-    private const string MobId = "MobHuman";
-
-    [ValidatePrototypeId<SpeciesPrototype>]
-    private const string SpeciesId = "Human";
-
-    [ValidatePrototypeId<NpcFactionPrototype>]
-    private const string PirateFactionId = "Syndicate";
-
-    [ValidatePrototypeId<NpcFactionPrototype>]
-    private const string EnemyFactionId = "NanoTrasen";
-
-    [ValidatePrototypeId<StartingGearPrototype>]
-    private const string GearId = "PirateGear";
-
-    [ValidatePrototypeId<EntityPrototype>]
-    private const string SpawnPointId = "SpawnPointPirates";
-
-    /// <inheritdoc/>
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawningEvent);
-        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextEvent);
-        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
-    }
-
-    private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev)
-    {
-        var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var pirates, out var gameRule))
-        {
-            if (Deleted(pirates.PirateShip))
-            {
-                // Major loss, the ship somehow got annihilated.
-                ev.AddLine(Loc.GetString("pirates-no-ship"));
-            }
-            else
-            {
-                List<(double, EntityUid)> mostValuableThefts = new();
-
-                var comp1 = pirates;
-                var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
-                {
-                    foreach (var mindId in comp1.Pirates)
-                    {
-                        if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity == uid)
-                            return false; // Don't appraise the pirates twice, we count them in separately.
-                    }
-
-                    return true;
-                }, (uid, price) =>
-                {
-                    if (comp1.InitialItems.Contains(uid))
-                        return;
-
-                    mostValuableThefts.Add((price, uid));
-                    mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
-                    if (mostValuableThefts.Count > 5)
-                        mostValuableThefts.Pop();
-                });
-
-                foreach (var mindId in pirates.Pirates)
-                {
-                    if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity is not null)
-                        finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
-                }
-
-                var score = finalValue - pirates.InitialShipValue;
-
-                ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}")));
-                ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}")));
-
-                ev.AddLine("");
-                ev.AddLine(Loc.GetString("pirates-most-valuable"));
-
-                foreach (var (price, obj) in mostValuableThefts)
-                {
-                    ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
-                }
-
-                if (mostValuableThefts.Count == 0)
-                    ev.AddLine(Loc.GetString("pirates-stole-nothing"));
-            }
-
-            ev.AddLine("");
-            ev.AddLine(Loc.GetString("pirates-list-start"));
-            foreach (var pirate in pirates.Pirates)
-            {
-                if (TryComp(pirate, out MindComponent? mind))
-                {
-                    ev.AddLine($"- {mind.CharacterName} ({mind.Session?.Name})");
-                }
-            }
-        }
-    }
-
-    private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev)
-    {
-        var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var pirates, out var gameRule))
-        {
-            // Forgive me for copy-pasting nukies.
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                return;
-
-            pirates.Pirates.Clear();
-            pirates.InitialItems.Clear();
-
-            // Between 1 and <max pirate count>: needs at least n players per op.
-            var numOps = Math.Max(1,
-                (int) Math.Min(
-                    Math.Floor((double) ev.PlayerPool.Count / _cfg.GetCVar(CCVars.PiratesPlayersPerOp)),
-                    _cfg.GetCVar(CCVars.PiratesMaxOps)));
-            var ops = new ICommonSession[numOps];
-            for (var i = 0; i < numOps; i++)
-            {
-                ops[i] = _random.PickAndTake(ev.PlayerPool);
-            }
-
-            var map = "/Maps/Shuttles/pirate.yml";
-            var xformQuery = GetEntityQuery<TransformComponent>();
-
-            var aabbs = EntityQuery<StationDataComponent>().SelectMany(x =>
-                    x.Grids.Select(x =>
-                        xformQuery.GetComponent(x).WorldMatrix.TransformBox(Comp<MapGridComponent>(x).LocalAABB)))
-                .ToArray();
-
-            var aabb = aabbs[0];
-
-            for (var i = 1; i < aabbs.Length; i++)
-            {
-                aabb.Union(aabbs[i]);
-            }
-
-            // (Not commented?)
-            var a = MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f;
-
-            var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
-            {
-                Offset = aabb.Center + new Vector2(a, a),
-                LoadMap = false,
-            });
-
-            if (!gridId.HasValue)
-            {
-                Log.Error($"Gridid was null when loading \"{map}\", aborting.");
-                foreach (var session in ops)
-                {
-                    ev.PlayerPool.Add(session);
-                }
-
-                return;
-            }
-
-            pirates.PirateShip = gridId.Value;
-
-            // TODO: Loot table or something
-            var pirateGear = _prototypeManager.Index<StartingGearPrototype>(GearId); // YARRR
-
-            var spawns = new List<EntityCoordinates>();
-
-            // Forgive me for hardcoding prototypes
-            foreach (var (_, meta, xform) in
-                     EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
-            {
-                if (meta.EntityPrototype?.ID != SpawnPointId || xform.ParentUid != pirates.PirateShip)
-                    continue;
-
-                spawns.Add(xform.Coordinates);
-            }
-
-            if (spawns.Count == 0)
-            {
-                spawns.Add(Transform(pirates.PirateShip).Coordinates);
-                Log.Warning($"Fell back to default spawn for pirates!");
-            }
-
-            for (var i = 0; i < ops.Length; i++)
-            {
-                var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female;
-                var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
-
-                var name = _namingSystem.GetName(SpeciesId, gender);
-
-                var session = ops[i];
-                var newMind = _mindSystem.CreateMind(session.UserId, name);
-                _mindSystem.SetUserId(newMind, session.UserId);
-
-                var mob = Spawn(MobId, _random.Pick(spawns));
-                _metaData.SetEntityName(mob, name);
-
-                _mindSystem.TransferTo(newMind, mob);
-                var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
-                _stationSpawningSystem.EquipStartingGear(mob, pirateGear);
-
-                _npcFaction.RemoveFaction(mob, EnemyFactionId, false);
-                _npcFaction.AddFaction(mob, PirateFactionId);
-
-                pirates.Pirates.Add(newMind);
-
-                // Notificate every player about a pirate antagonist role with sound
-                _audioSystem.PlayGlobal(pirates.PirateAlertSound, session);
-
-                GameTicker.PlayerJoinGame(session);
-            }
-
-            pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
-            {
-                pirates.InitialItems.Add(uid);
-                return true;
-            }); // Include the players in the appraisal.
-        }
-    }
-
-    //Forcing one player to be a pirate.
-    public void MakePirate(EntityUid entity)
-    {
-        if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind))
-            return;
-
-        SetOutfitCommand.SetOutfit(entity, GearId, EntityManager);
-
-        var pirateRule = EntityQuery<PiratesRuleComponent>().FirstOrDefault();
-        if (pirateRule == null)
-        {
-            //todo fuck me this shit is awful
-            GameTicker.StartGameRule(GameRuleId, out var ruleEntity);
-            pirateRule = Comp<PiratesRuleComponent>(ruleEntity);
-        }
-
-        // Notificate every player about a pirate antagonist role with sound
-        if (mind.Session != null)
-        {
-            _audioSystem.PlayGlobal(pirateRule.PirateAlertSound, mind.Session);
-        }
-    }
-
-    private void OnStartAttempt(RoundStartAttemptEvent ev)
-    {
-        var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var pirates, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                return;
-
-            var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
-            if (!ev.Forced && ev.Players.Length < minPlayers)
-            {
-                _chatManager.SendAdminAnnouncement(Loc.GetString("nukeops-not-enough-ready-players",
-                    ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-                ev.Cancel();
-                return;
-            }
-
-            if (ev.Players.Length == 0)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
-                ev.Cancel();
-            }
-        }
-    }
-}
index b11c28fb2b0ee47a6226d4f4d57359422a674db8..5215da96aa8df550ad7b679fa9ebb7083822141c 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Systems;
 using Content.Shared.Chat;
index ba9fb2ccbccfd273b89b4417c29b5db1f7083ee7..e89d4614ffdfbf992fe8182d5f235d00a421ac30 100644 (file)
@@ -14,7 +14,6 @@ using Content.Server.Station.Systems;
 using Content.Shared.Database;
 using Content.Shared.Humanoid;
 using Content.Shared.IdentityManagement;
-using Content.Shared.Inventory;
 using Content.Shared.Mind;
 using Content.Shared.Mind.Components;
 using Content.Shared.Mindshield.Components;
@@ -24,12 +23,11 @@ using Content.Shared.Mobs.Systems;
 using Content.Shared.NPC.Prototypes;
 using Content.Shared.NPC.Systems;
 using Content.Shared.Revolutionary.Components;
-using Content.Shared.Roles;
 using Content.Shared.Stunnable;
 using Content.Shared.Zombies;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
-using System.Linq;
+using Content.Server.GameTicking.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
@@ -40,7 +38,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
 {
     [Dependency] private readonly IAdminLogManager _adminLogManager = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
-    [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly EuiManager _euiMan = default!;
     [Dependency] private readonly MindSystem _mind = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
@@ -51,7 +49,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
     [Dependency] private readonly RoundEndSystem _roundEnd = default!;
     [Dependency] private readonly StationSystem _stationSystem = default!;
     [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
-    [Dependency] private readonly InventorySystem _inventory = default!;
 
     //Used in OnPostFlash, no reference to the rule component is available
     public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
@@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
     public override void Initialize()
     {
         base.Initialize();
-        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
-        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
         SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
         SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
-        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
         SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
         SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
     }
 
-    //Set miniumum players
-    protected override void Added(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
-    {
-        base.Added(uid, component, gameRule, args);
-
-        gameRule.MinPlayers = component.MinPlayers;
-    }
-
     protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
         base.Started(uid, component, gameRule, args);
@@ -98,40 +84,29 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
         }
     }
 
-    private void OnRoundEndText(RoundEndTextAppendEvent ev)
+    protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
+        ref RoundEndTextAppendEvent args)
     {
+        base.AppendRoundEndText(uid, component, gameRule, ref args);
+
         var revsLost = CheckRevsLose();
         var commandLost = CheckCommandLose();
-        var query = AllEntityQuery<RevolutionaryRuleComponent>();
-        while (query.MoveNext(out var headrev))
+        // This is (revsLost, commandsLost) concatted together
+        // (moony wrote this comment idk what it means)
+        var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
+        args.AddLine(Loc.GetString(Outcomes[index]));
+
+        var sessionData = _antag.GetAntagIdentifiers(uid);
+        args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
+        foreach (var (mind, data, name) in sessionData)
         {
-            // This is (revsLost, commandsLost) concatted together
-            // (moony wrote this comment idk what it means)
-            var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
-            ev.AddLine(Loc.GetString(Outcomes[index]));
-
-            ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count)));
-            foreach (var player in headrev.HeadRevs)
-            {
-                // TODO: when role entities are a thing this has to change
-                var count = CompOrNull<RevolutionaryRoleComponent>(player.Value)?.ConvertedCount ?? 0;
-
-                _mind.TryGetSession(player.Value, out var session);
-                var username = session?.Name;
-                if (username != null)
-                {
-                    ev.AddLine(Loc.GetString("rev-headrev-name-user",
-                    ("name", player.Key),
-                    ("username", username), ("count", count)));
-                }
-                else
-                {
-                    ev.AddLine(Loc.GetString("rev-headrev-name",
-                    ("name", player.Key), ("count", count)));
-                }
+            var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
+            args.AddLine(Loc.GetString("rev-headrev-name-user",
+                ("name", name),
+                ("username", data.UserName),
+                ("count", count)));
 
-                // TODO: someone suggested listing all alive? revs maybe implement at some point
-            }
+            // TODO: someone suggested listing all alive? revs maybe implement at some point
         }
     }
 
@@ -144,57 +119,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
         args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
     }
 
-    //Check for enough players to start rule
-    private void OnStartAttempt(RoundStartAttemptEvent ev)
-    {
-        TryRoundStartAttempt(ev, Loc.GetString("roles-antag-rev-name"));
-    }
-
-    private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
-    {
-        var query = QueryActiveRules();
-        while (query.MoveNext(out var uid, out var activeGameRule, out var comp, out var gameRule))
-        {
-            var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.HeadRevPrototypeId);
-
-            if (eligiblePlayers.Count == 0)
-                continue;
-
-            var headRevCount = _antagSelection.CalculateAntagCount(ev.Players.Length, comp.PlayersPerHeadRev, comp.MaxHeadRevs);
-
-            var headRevs = _antagSelection.ChooseAntags(headRevCount, eligiblePlayers);
-
-            GiveHeadRev(headRevs, comp.HeadRevPrototypeId, comp);
-        }
-    }
-
-    private void GiveHeadRev(IEnumerable<EntityUid> chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
-    {
-        foreach (var headRev in chosen)
-            GiveHeadRev(headRev, antagProto, comp);
-    }
-    private void GiveHeadRev(EntityUid chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
-    {
-        RemComp<CommandStaffComponent>(chosen);
-
-        var inCharacterName = MetaData(chosen).EntityName;
-
-        if (!_mind.TryGetMind(chosen, out var mind, out _))
-            return;
-
-        if (!_role.MindHasRole<RevolutionaryRoleComponent>(mind))
-        {
-            _role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true);
-        }
-
-        comp.HeadRevs.Add(inCharacterName, mind);
-        _inventory.SpawnItemsOnEntity(chosen, comp.StartingGear);
-        var revComp = EnsureComp<RevolutionaryComponent>(chosen);
-        EnsureComp<HeadRevolutionaryComponent>(chosen);
-
-        _antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound);
-    }
-
     /// <summary>
     /// Called when a Head Rev uses a flash in melee to convert somebody else.
     /// </summary>
@@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
         }
 
         if (mind?.Session != null)
-            _antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
-    }
-
-    public void OnHeadRevAdmin(EntityUid entity)
-    {
-        if (HasComp<HeadRevolutionaryComponent>(entity))
-            return;
-
-        var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
-        if (revRule == null)
-        {
-            GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
-            revRule = Comp<RevolutionaryRuleComponent>(ruleEnt);
-        }
-
-        GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule);
+            _antag.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
     }
 
     //TODO: Enemies of the revolution
@@ -308,7 +217,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
                 _popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
                 _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
 
-                if (!_mind.TryGetMind(uid, out var mindId, out var mind, mc))
+                if (!_mind.TryGetMind(uid, out var mindId, out _, mc))
                     continue;
 
                 // remove their antag role
index 7755f684be2284598a5d620e545fcc3b74b5263c..f09ed3ebc3cd9be1e0a83eb3a59bc92ffa40630f 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Components;
index a26a2d783c79a41a59e49cac32287b1f9a476315..c60670a3ad7ab0de7cd149a2bf11eb4091b31129 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Sandbox;
 
index fa5f17b4f371cc7e628cac4406a411f2363886d0..d5adb8fdb78dbf8462991addc629bba1db1e79da 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Administration.Logs;
+using Content.Server.GameTicking.Components;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking.Presets;
 using Content.Server.GameTicking.Rules.Components;
index 42e7e82335c441227d992b870e5cbe6ee7a5f9d2..4486ee40fbbd2a7a67e13f4ba67dddd77298bf83 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.Storage;
 
index 32f9040f89f990cfe29747c1bdc9c221224de122..b778f7c6455d3735ab58d22eb3cb424f1ab8055a 100644 (file)
@@ -3,118 +3,37 @@ using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Mind;
 using Content.Server.Objectives;
 using Content.Server.Roles;
-using Content.Shared.Antag;
-using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Humanoid;
-using Content.Shared.Inventory;
 using Content.Shared.Mind;
 using Content.Shared.Objectives.Components;
-using Content.Shared.Roles;
 using Robust.Shared.Random;
-using System.Linq;
 
 namespace Content.Server.GameTicking.Rules;
 
 public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
 {
     [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
     [Dependency] private readonly MindSystem _mindSystem = default!;
-    [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly ObjectivesSystem _objectives = default!;
-    [Dependency] private readonly InventorySystem _inventory = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
+        SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
 
         SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
         SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
     }
 
-    private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
+    private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
     {
-        var query = QueryActiveRules();
-        while (query.MoveNext(out var uid, out _, out var comp, out var gameRule))
-        {
-            //Get all players eligible for this role, allow selecting existing antags
-            //TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:)
-            var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true);
-
-            //Abort if there are none
-            if (eligiblePlayers.Count == 0)
-            {
-                Log.Warning($"No eligible thieves found, ending game rule {ToPrettyString(uid):rule}");
-                GameTicker.EndGameRule(uid, gameRule);
-                continue;
-            }
-
-            //Calculate number of thieves to choose
-            var thiefCount = _random.Next(1, comp.MaxAllowThief + 1);
-
-            //Select our theives
-            var thieves = _antagSelection.ChooseAntags(thiefCount, eligiblePlayers);
-
-            MakeThief(thieves, comp, comp.PacifistThieves);
-        }
-    }
-
-    public void MakeThief(List<EntityUid> players, ThiefRuleComponent thiefRule, bool addPacified)
-    {
-        foreach (var thief in players)
-        {
-            MakeThief(thief, thiefRule, addPacified);
-        }
-    }
-
-    public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified)
-    {
-        if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind))
+        if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
             return;
 
-        if (HasComp<ThiefRoleComponent>(mindId))
-            return;
-
-        // Assign thief roles
-        _roleSystem.MindAddRole(mindId, new ThiefRoleComponent
-        {
-            PrototypeId = thiefRule.ThiefPrototypeId,
-        }, silent: true);
-
-        //Add Pacified  
-        //To Do: Long-term this should just be using the antag code to add components.
-        if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove.
-        {
-            EnsureComp<PacifiedComponent>(thief);
-        }
-
         //Generate objectives
-        GenerateObjectives(mindId, mind, thiefRule);
-
-        //Send briefing here to account for humanoid/animal
-        _antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound);
-
-        // Give starting items
-        _inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems);
-
-        thiefRule.ThievesMinds.Add(mindId);
-    }
-
-    public void AdminMakeThief(EntityUid entity, bool addPacified)
-    {
-        var thiefRule = EntityQuery<ThiefRuleComponent>().FirstOrDefault();
-        if (thiefRule == null)
-        {
-            GameTicker.StartGameRule("Thief", out var ruleEntity);
-            thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
-        }
-
-        if (HasComp<ThiefRoleComponent>(entity))
-            return;
-
-        MakeThief(entity, thiefRule, addPacified);
+        GenerateObjectives(mindId, mind, ent);
     }
 
     private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
@@ -160,8 +79,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
     private string MakeBriefing(EntityUid thief)
     {
         var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
-        var briefing = "\n";
-        briefing = isHuman
+        var briefing = isHuman
             ? Loc.GetString("thief-role-greeting-human")
             : Loc.GetString("thief-role-greeting-animal");
 
@@ -169,9 +87,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
         return briefing;
     }
 
-    private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> thiefs, ref ObjectivesTextGetInfoEvent args)
+    private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
     {
-        args.Minds = thiefs.Comp.ThievesMinds;
+        args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
         args.AgentName = Loc.GetString("thief-round-end-agent-name");
     }
 }
index 769d7e0a5b53130cc412985c26998a46a6b4f7ab..b6bcd5ee1e8908ba1a06d61a3741aa2b29636011 100644 (file)
@@ -5,97 +5,61 @@ using Content.Server.Objectives;
 using Content.Server.PDA.Ringer;
 using Content.Server.Roles;
 using Content.Server.Traitor.Uplink;
-using Content.Shared.CCVar;
-using Content.Shared.Dataset;
 using Content.Shared.Mind;
-using Content.Shared.Mobs.Systems;
 using Content.Shared.NPC.Systems;
 using Content.Shared.Objectives.Components;
 using Content.Shared.PDA;
 using Content.Shared.Roles;
 using Content.Shared.Roles.Jobs;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
-using Robust.Shared.Timing;
 using System.Linq;
 using System.Text;
+using Content.Server.GameTicking.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
 public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
 {
-    [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly IConfigurationManager _cfg = default!;
-    [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
-    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly UplinkSystem _uplink = default!;
     [Dependency] private readonly MindSystem _mindSystem = default!;
     [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
     [Dependency] private readonly SharedJobSystem _jobs = default!;
     [Dependency] private readonly ObjectivesSystem _objectives = default!;
-    [Dependency] private readonly IGameTiming _timing = default!;
 
-    private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
-    private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
+    public const int MaxPicks = 20;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
-        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
-        SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
+        SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
 
         SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
         SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
     }
 
-    //Set min players on game rule
     protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
     {
         base.Added(uid, component, gameRule, args);
-
-        gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
-    }
-
-    protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
         MakeCodewords(component);
     }
 
-    protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
-    {
-        base.ActiveTick(uid, component, gameRule, frameTime);
-
-        if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime)
-        {
-            DoTraitorStart(component);
-            component.SelectionStatus = TraitorRuleComponent.SelectionState.Started;
-        }
-    }
-
-    /// <summary>
-    /// Check for enough players
-    /// </summary>
-    /// <param name="ev"></param>
-    private void OnStartAttempt(RoundStartAttemptEvent ev)
+    private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
     {
-        TryRoundStartAttempt(ev, Loc.GetString("traitor-title"));
+        MakeTraitor(args.EntityUid, ent);
     }
 
     private void MakeCodewords(TraitorRuleComponent component)
     {
-        var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
-        var adjectives = _prototypeManager.Index<DatasetPrototype>(component.CodewordAdjectives).Values;
-        var verbs = _prototypeManager.Index<DatasetPrototype>(component.CodewordVerbs).Values;
+        var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values;
+        var verbs = _prototypeManager.Index(component.CodewordVerbs).Values;
         var codewordPool = adjectives.Concat(verbs).ToList();
-        var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
+        var finalCodewordCount = Math.Min(component.CodewordCount, codewordPool.Count);
         component.Codewords = new string[finalCodewordCount];
         for (var i = 0; i < finalCodewordCount; i++)
         {
@@ -103,66 +67,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         }
     }
 
-    private void DoTraitorStart(TraitorRuleComponent component)
-    {
-        var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId);
-
-        if (eligiblePlayers.Count == 0)
-            return;
-
-        var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors);
-
-        var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers);
-
-        MakeTraitor(selectedTraitors, component);
-    }
-
-    private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
-    {
-        //Start the timer
-        var query = QueryActiveRules();
-        while (query.MoveNext(out _, out var comp, out var gameRuleComponent))
-        {
-            var delay = TimeSpan.FromSeconds(
-                _cfg.GetCVar(CCVars.TraitorStartDelay) +
-                _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
-
-            //Set the delay for choosing traitors
-            comp.AnnounceAt = _timing.CurTime + delay;
-
-            comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart;
-        }
-    }
-
-    public bool MakeTraitor(List<EntityUid> traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
-    {
-        foreach (var traitor in traitors)
-        {
-            MakeTraitor(traitor, component, giveUplink, giveObjectives);
-        }
-
-        return true;
-    }
-
     public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
     {
         //Grab the mind if it wasnt provided
         if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
             return false;
 
-        if (HasComp<TraitorRoleComponent>(mindId))
-        {
-            Log.Error($"Player {mind.CharacterName} is already a traitor.");
-            return false;
-        }
-
         var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
 
         Note[]? code = null;
         if (giveUplink)
         {
             // Calculate the amount of currency on the uplink.
-            var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance);
+            var startingBalance = component.StartingBalance;
             if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
                 startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
 
@@ -180,19 +97,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
                 Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
         }
 
-        _antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
+        _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification);
 
         component.TraitorMinds.Add(mindId);
 
-        // Assign traitor roles
-        _roleSystem.MindAddRole(mindId, new TraitorRoleComponent
-        {
-            PrototypeId = component.TraitorPrototypeId
-        }, mind, true);
         // Assign briefing
         _roleSystem.MindAddRole(mindId, new RoleBriefingComponent
         {
-            Briefing = briefing.ToString()
+            Briefing = briefing
         }, mind, true);
 
         // Change the faction
@@ -202,11 +114,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         // Give traitors their objectives
         if (giveObjectives)
         {
-            var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
-            var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
             var difficulty = 0f;
-            Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty");
-            for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
+            for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
             {
                 var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
                 if (objective == null)
@@ -222,53 +131,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         return true;
     }
 
-    private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
-    {
-        var query = QueryActiveRules();
-        while (query.MoveNext(out _, out var comp, out _))
-        {
-            if (comp.TotalTraitors >= MaxTraitors)
-                continue;
-
-            if (!ev.LateJoin)
-                continue;
-
-            if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId))
-                continue;
-
-            //If its before we have selected traitors, continue
-            if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started)
-                continue;
-
-            // the nth player we adjust our probabilities around
-            var target = PlayersPerTraitor * comp.TotalTraitors + 1;
-            var chance = 1f / PlayersPerTraitor;
-
-            // If we have too many traitors, divide by how many players below target for next traitor we are.
-            if (ev.JoinOrder < target)
-            {
-                chance /= (target - ev.JoinOrder);
-            }
-            else // Tick up towards 100% chance.
-            {
-                chance *= ((ev.JoinOrder + 1) - target);
-            }
-
-            if (chance > 1)
-                chance = 1;
-
-            // Now that we've calculated our chance, roll and make them a traitor if we roll under.
-            // You get one shot.
-            if (_random.Prob(chance))
-            {
-                MakeTraitor(ev.Mob, comp);
-            }
-        }
-    }
-
     private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
     {
-        args.Minds = comp.TraitorMinds;
+        args.Minds = _antag.GetAntagMindEntityUids(uid);
         args.AgentName = Loc.GetString("traitor-round-end-agent-name");
     }
 
@@ -277,27 +142,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
     }
 
-    /// <summary>
-    /// Start this game rule manually
-    /// </summary>
-    public TraitorRuleComponent StartGameRule()
-    {
-        var comp = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
-        if (comp == null)
-        {
-            GameTicker.StartGameRule("Traitor", out var ruleEntity);
-            comp = Comp<TraitorRuleComponent>(ruleEntity);
-        }
-
-        return comp;
-    }
-
-    public void MakeTraitorAdmin(EntityUid entity, bool giveUplink, bool giveObjectives)
-    {
-        var traitorRule = StartGameRule();
-        MakeTraitor(entity, traitorRule, giveUplink, giveObjectives);
-    }
-
     private string GenerateBriefing(string[] codewords, Note[]? uplinkCode)
     {
         var sb = new StringBuilder();
@@ -312,9 +156,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
     public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
     {
         List<(EntityUid Id, MindComponent Mind)> allTraitors = new();
-        foreach (var traitor in EntityQuery<TraitorRuleComponent>())
+
+        var query = EntityQueryEnumerator<TraitorRuleComponent>();
+        while (query.MoveNext(out var uid, out var traitor))
         {
-            foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, traitor))
+            foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, (uid, traitor)))
             {
                 if (!allTraitors.Contains(role))
                     allTraitors.Add(role);
@@ -324,20 +170,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         return allTraitors;
     }
 
-    private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, TraitorRuleComponent component)
+    private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, Entity<TraitorRuleComponent> rule)
     {
         var traitors = new List<(EntityUid Id, MindComponent Mind)>();
-        foreach (var traitor in component.TraitorMinds)
+        foreach (var mind in _antag.GetAntagMinds(rule.Owner))
         {
-            if (TryComp(traitor, out MindComponent? mind) &&
-                mind.OwnedEntity != null &&
-                mind.Session != null &&
-                mind != ourMind &&
-                _mobStateSystem.IsAlive(mind.OwnedEntity.Value) &&
-                mind.CurrentEntity == mind.OwnedEntity)
-            {
-                traitors.Add((traitor, mind));
-            }
+            if (mind.Comp == ourMind)
+                continue;
+
+            traitors.Add((mind, mind));
         }
 
         return traitors;
index 54e8bcf8b708ffddfcd1ed220153ff7a2f57b31b..f22c20840838ba85959f04de76508c58c8f84f1f 100644 (file)
-using Content.Server.Actions;
 using Content.Server.Antag;
 using Content.Server.Chat.Systems;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Popups;
-using Content.Server.Roles;
 using Content.Server.RoundEnd;
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
 using Content.Server.Zombies;
-using Content.Shared.CCVar;
 using Content.Shared.Humanoid;
 using Content.Shared.Mind;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
-using Content.Shared.Roles;
 using Content.Shared.Zombies;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
 using Robust.Shared.Player;
-using Robust.Shared.Random;
 using Robust.Shared.Timing;
 using System.Globalization;
+using Content.Server.GameTicking.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
 public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 {
-    [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly IConfigurationManager _cfg = default!;
-    [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly ChatSystem _chat = default!;
     [Dependency] private readonly RoundEndSystem _roundEnd = default!;
     [Dependency] private readonly PopupSystem _popup = default!;
-    [Dependency] private readonly ActionsSystem _action = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly ZombieSystem _zombie = default!;
     [Dependency] private readonly SharedMindSystem _mindSystem = default!;
-    [Dependency] private readonly SharedRoleSystem _roles = default!;
     [Dependency] private readonly StationSystem _station = default!;
-    [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
-        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
         SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
     }
 
-    /// <summary>
-    /// Set the required minimum players for this gamemode to start
-    /// </summary>
-    protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
+        ref RoundEndTextAppendEvent args)
     {
-        base.Added(uid, component, gameRule, args);
-
-        gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
-    }
-
-    private void OnRoundEndText(RoundEndTextAppendEvent ev)
-    {
-        foreach (var zombie in EntityQuery<ZombieRuleComponent>())
+        base.AppendRoundEndText(uid, component, gameRule, ref args);
+
+        // This is just the general condition thing used for determining the win/lose text
+        var fraction = GetInfectedFraction(true, true);
+
+        if (fraction <= 0)
+            args.AddLine(Loc.GetString("zombie-round-end-amount-none"));
+        else if (fraction <= 0.25)
+            args.AddLine(Loc.GetString("zombie-round-end-amount-low"));
+        else if (fraction <= 0.5)
+            args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
+        else if (fraction < 1)
+            args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
+        else
+            args.AddLine(Loc.GetString("zombie-round-end-amount-all"));
+
+        var antags = _antag.GetAntagIdentifiers(uid);
+        args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count)));
+        foreach (var (_, data, entName) in antags)
         {
-            // This is just the general condition thing used for determining the win/lose text
-            var fraction = GetInfectedFraction(true, true);
-
-            if (fraction <= 0)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
-            else if (fraction <= 0.25)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
-            else if (fraction <= 0.5)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
-            else if (fraction < 1)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture))));
-            else
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
+            args.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
+                ("name", entName),
+                ("username", data.UserName)));
+        }
 
-            ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count)));
-            foreach (var player in zombie.InitialInfectedNames)
+        var healthy = GetHealthyHumans();
+        // Gets a bunch of the living players and displays them if they're under a threshold.
+        // InitialInfected is used for the threshold because it scales with the player count well.
+        if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count)
+            return;
+        args.AddLine("");
+        args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
+        foreach (var survivor in healthy)
+        {
+            var meta = MetaData(survivor);
+            var username = string.Empty;
+            if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
             {
-                ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
-                    ("name", player.Key),
-                    ("username", player.Value)));
+                username = mind.Session.Name;
             }
 
-            var healthy = GetHealthyHumans();
-            // Gets a bunch of the living players and displays them if they're under a threshold.
-            // InitialInfected is used for the threshold because it scales with the player count well.
-            if (healthy.Count <= 0 || healthy.Count > 2 * zombie.InitialInfectedNames.Count)
-                continue;
-            ev.AddLine("");
-            ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count)));
-            foreach (var survivor in healthy)
-            {
-                var meta = MetaData(survivor);
-                var username = string.Empty;
-                if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null)
-                {
-                    username = mind.Session.Name;
-                }
-
-                ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
-                    ("name", meta.EntityName),
-                    ("username", username)));
-            }
+            args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
+                ("name", meta.EntityName),
+                ("username", username)));
         }
     }
 
@@ -134,38 +112,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
             _roundEnd.EndRound();
     }
 
-    /// <summary>
-    /// Check we have enough players to start this game mode, if not - cancel and announce
-    /// </summary>
-    private void OnStartAttempt(RoundStartAttemptEvent ev)
-    {
-        TryRoundStartAttempt(ev, Loc.GetString("zombie-title"));
-    }
-
     protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
         base.Started(uid, component, gameRule, args);
 
-        var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay);
-        component.StartTime = _timing.CurTime + delay;
+        component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
     }
 
     protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime)
     {
         base.ActiveTick(uid, component, gameRule, frameTime);
-
-        if (component.StartTime.HasValue && component.StartTime < _timing.CurTime)
-        {
-            InfectInitialPlayers(component);
-            component.StartTime = null;
-            component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
-        }
-
-        if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime)
-        {
-            CheckRoundEnd(component);
-            component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
-        }
+        if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime)
+            return;
+        CheckRoundEnd(component);
+        component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay;
     }
 
     private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args)
@@ -232,81 +192,4 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
         }
         return healthy;
     }
-
-    /// <summary>
-    ///     Infects the first players with the passive zombie virus.
-    ///     Also records their names for the end of round screen.
-    /// </summary>
-    /// <remarks>
-    ///     The reason this code is written separately is to facilitate
-    ///     allowing this gamemode to be started midround. As such, it doesn't need
-    ///     any information besides just running.
-    /// </remarks>
-    private void InfectInitialPlayers(ZombieRuleComponent component)
-    {
-        //Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent and roles with CanBeAntag = False
-        var eligiblePlayers = _antagSelection.GetEligiblePlayers(
-            _playerManager.Sessions,
-            component.PatientZeroPrototypeId,
-            includeAllJobs: false,
-            customExcludeCondition: player => HasComp<ZombieImmuneComponent>(player) || HasComp<InitialInfectedExemptComponent>(player)
-            );
-
-        //And get all players, excluding ZombieImmune and roles with CanBeAntag = False - to fill any leftover initial infected slots
-        var allPlayers = _antagSelection.GetEligiblePlayers(
-            _playerManager.Sessions,
-            component.PatientZeroPrototypeId,
-            acceptableAntags: Shared.Antag.AntagAcceptability.All,
-            includeAllJobs: false ,
-            ignorePreferences: true,
-            customExcludeCondition: HasComp<ZombieImmuneComponent>
-            );
-
-        //If there are no players to choose, abort
-        if (allPlayers.Count == 0)
-            return;
-
-        //How many initial infected should we select
-        var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected);
-
-        //Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players
-        var initialInfected = _antagSelection.ChooseAntags(initialInfectedCount, eligiblePlayers, allPlayers);
-
-        //Make brain craving
-        MakeZombie(initialInfected, component);
-
-        //Send the briefing, play greeting sound
-        _antagSelection.SendBriefing(initialInfected, Loc.GetString("zombie-patientzero-role-greeting"), Color.Plum, component.InitialInfectedSound);
-    }
-
-    private void MakeZombie(List<EntityUid> entities, ZombieRuleComponent component)
-    {
-        foreach (var entity in entities)
-        {
-            MakeZombie(entity, component);
-        }
-    }
-    private void MakeZombie(EntityUid entity, ZombieRuleComponent component)
-    {
-        if (!_mindSystem.TryGetMind(entity, out var mind, out var mindComponent))
-            return;
-
-        //Add the role to the mind silently (to avoid repeating job assignment)
-        _roles.MindAddRole(mind, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId }, silent: true);
-        EnsureComp<InitialInfectedComponent>(entity);
-
-        //Add the zombie components and grace period
-        var pending = EnsureComp<PendingZombieComponent>(entity);
-        pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
-        EnsureComp<ZombifyOnDeathComponent>(entity);
-        EnsureComp<IncurableZombieComponent>(entity);
-
-        //Add the zombify action
-        _action.AddAction(entity, ref pending.Action, component.ZombifySelfActionPrototype, entity);
-
-        //Get names for the round end screen, incase they leave mid-round
-        var inCharacterName = MetaData(entity).EntityName;
-        var accountName = mindComponent.Session == null ? string.Empty : mindComponent.Session.Name;
-        component.InitialInfectedNames.Add(inCharacterName, accountName);
-    }
 }
index 20205b8b72fac05a70b1dafc86e95304d63d17fb..47fe4eb5f889ca85c908ebbe0b79d1a4c275cf56 100644 (file)
@@ -1,10 +1,7 @@
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Mind;
 using Content.Server.Shuttles.Systems;
 using Content.Shared.Cuffs.Components;
 using Content.Shared.Mind;
-using Content.Shared.Mobs.Systems;
 using Content.Shared.Objectives.Components;
 using Content.Shared.Objectives.Systems;
 using Content.Shared.Random;
@@ -12,7 +9,9 @@ using Content.Shared.Random.Helpers;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using System.Linq;
+using Content.Server.GameTicking.Components;
 using System.Text;
+using Robust.Server.Player;
 
 namespace Content.Server.Objectives;
 
@@ -20,8 +19,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
 {
     [Dependency] private readonly GameTicker _gameTicker = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly MindSystem _mind = default!;
     [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
 
     public override void Initialize()
@@ -179,7 +178,9 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
                                        .ThenByDescending(x => x.completedObjectives);
 
         foreach (var (summary, _, _) in sortedAgents)
+        {
             result.AppendLine(summary);
+        }
     }
 
     public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
@@ -244,8 +245,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
             return null;
 
         var name = mind.CharacterName;
-        _mind.TryGetSession(mindId, out var session);
-        var username = session?.Name;
+        var username = (string?) null;
+
+        if (mind.OriginalOwnerUserId != null &&
+            _player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
+        {
+            username = sessionData.UserName;
+        }
+
 
         if (username != null)
         {
index 107d09c89806fdf609c1faa851d0f7582aacfebd..0e20f007d7179589e1d5c26fea58ec8597238172 100644 (file)
@@ -17,6 +17,7 @@ using Robust.Shared.Player;
 using Robust.Shared.Utility;
 using System.Linq;
 using System.Diagnostics.CodeAnalysis;
+using Content.Server.GameTicking.Components;
 
 namespace Content.Server.Power.EntitySystems;
 
@@ -723,8 +724,8 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori
         }
     }
 
-    // Designates a supplied entity as a 'collection master'. Other entities which share this 
-    // entities collection name and are attached on the same load network are assigned this entity 
+    // Designates a supplied entity as a 'collection master'. Other entities which share this
+    // entities collection name and are attached on the same load network are assigned this entity
     // as the master that represents them on the console UI. This way you can have one device
     // represent multiple connected devices
     private void AssignEntityAsCollectionMaster
index a36b053717f7acc13eab989558ff51631554e542..1808592ef5a949b9fa138817ccec1819d96866ee 100644 (file)
@@ -16,6 +16,7 @@ namespace Content.Server.Preferences.Managers
 
         bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);
         PlayerPreferences GetPreferences(NetUserId userId);
+        PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
         IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
         bool HavePreferencesLoaded(ICommonSession session);
     }
index 0f8cb83f10c80c9b9135b9c53d896f828cac14ef..a1eb8aad82be38b16f0cf3c10f3d8e9fd8656524 100644 (file)
@@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers
             return prefs;
         }
 
+        /// <summary>
+        /// Retrieves preferences for the given username from storage or returns null.
+        /// Creates and saves default preferences if they are not found, then returns them.
+        /// </summary>
+        public PlayerPreferences? GetPreferencesOrNull(NetUserId? userId)
+        {
+            if (userId == null)
+                return null;
+
+            if (_cachedPlayerPrefs.TryGetValue(userId.Value, out var pref))
+                return pref.Prefs;
+            return null;
+        }
+
         private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
         {
             var prefs = await _db.GetPlayerPreferencesAsync(userId);
index c088d57fd96a16b684892915436a8cae397370b9..0c254c52ac09e49737eb755ca07c0220cff56c71 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Shared.Dataset;
+using Content.Shared.Dataset;
 using JetBrains.Annotations;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
@@ -47,9 +47,12 @@ public sealed class RandomMetadataSystem : EntitySystem
         var outputSegments = new List<string>();
         foreach (var segment in segments)
         {
-            outputSegments.Add(_prototype.TryIndex<DatasetPrototype>(segment, out var proto)
-                ? Loc.GetString(_random.Pick(proto.Values))
-                : Loc.GetString(segment));
+            if (_prototype.TryIndex<DatasetPrototype>(segment, out var proto))
+                outputSegments.Add(_random.Pick(proto.Values));
+            else if (Loc.TryGetString(segment, out var localizedSegment))
+                outputSegments.Add(localizedSegment);
+            else
+                outputSegments.Add(segment);
         }
         return string.Join(separator, outputSegments);
     }
index 506fd61d55967db754a72e776259d839a9d05988..75f861879890969b7678a3422d10759a21c45b11 100644 (file)
@@ -1,5 +1,6 @@
 using System.Numerics;
 using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Spawners.Components;
 using JetBrains.Annotations;
index 36d30f50eee6cd86a6db30eb1268bd8eb352292e..e7e0957239f884d57d30731469b8212d9583e693 100644 (file)
@@ -1,5 +1,6 @@
 using System.Linq;
 using Content.Server.Administration;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
diff --git a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs
deleted file mode 100644 (file)
index 92911e0..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Server.StationEvents.Events;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(LoneOpsSpawnRule))]
-public sealed partial class LoneOpsSpawnRuleComponent : Component
-{
-    [DataField("loneOpsShuttlePath")]
-    public string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml";
-
-    [DataField("gameRuleProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string GameRuleProto = "Nukeops";
-
-    [DataField("additionalRule")]
-    public EntityUid? AdditionalRule;
-}
index 48a3b900c44307b65784b3c0eb322409f06daf77..96633834ee10789ee5f8ece87bdc82e3891f5a22 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Anomaly;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
index 0eed77f15436305cb988dfab10fb6dbd0f8018c1..b3ed10999ed34f175120adc01d581328df6b5bb5 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Robust.Shared.Random;
 
index 709b750334e9b460e5c90fe18a476190cc677ac8..eef9850e739dee48f6618bc217b7f403ca326a40 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Resist;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
index 494779fe3503168062ed390b722e2fd911c78dcd..16d3fd8c95dfe37b95a0e490b855f44fa42eb436 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Station.Components;
index 249a14a9b8a090b51340edd74313562ac8e7a167..ccfb8aee58e24615d39d4963361fba6d64515814 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
index 0c8c9b6dc5598f2a7b40a6a215989386b2b60cfd..c27cd302784aef6e9069b4dc2ed25b412bcd10b0 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Server.Cargo.Components;
 using Content.Server.Cargo.Systems;
 using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
index dd4473952cb4dd196e974f77fc3885c127468eba..854ee685b33d575f8172be8689de2316da37a0cd 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.StationRecords;
 using Content.Server.StationRecords.Systems;
index 05e9435b40a3445dd670c5047bc4ba4b0e24dd61..e5317a5449f15ab9c15c409e95e06573f502b8cf 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using JetBrains.Annotations;
index 68544e416c3011a020e95f93c6e7f069c4f24a0b..1221612171d9e1c175d4011f5b8bee59d2862933 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Atmos.EntitySystems;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Robust.Shared.Audio;
index 1b8fb6be1f85d5ebaa466cb347ca5166a1a58c2a..cacb839cd3989dd6b8060656b034f89b94080d90 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.ImmovableRod;
 using Content.Server.StationEvents.Components;
index cd3cd63ae860ec9e0634ca765268c47350c63293..8361cc6048a3ce1371b6652cea68f9c902ed3c86 100644 (file)
@@ -1,5 +1,5 @@
+using Content.Server.GameTicking.Components;
 using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Silicons.Laws;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
index 3fa12cd4e9ff7d9120ca2dc3702b6b2d53660176..5b56e03846f98391f076eb483de9ba549eead824 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 
diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs b/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs
deleted file mode 100644 (file)
index 4b15e59..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-using Robust.Server.GameObjects;
-using Robust.Server.Maps;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-using Content.Server.RoundEnd;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleComponent>
-{
-    [Dependency] private readonly MapLoaderSystem _map = default!;
-
-    protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        // Loneops can only spawn if there is no nukeops active
-        if (GameTicker.IsGameRuleAdded<NukeopsRuleComponent>())
-        {
-            ForceEndSelf(uid, gameRule);
-            return;
-        }
-
-        var shuttleMap = MapManager.CreateMap();
-        var options = new MapLoadOptions
-        {
-            LoadMap = true,
-        };
-
-        _map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options);
-
-        var nukeopsEntity = GameTicker.AddGameRule(component.GameRuleProto);
-        component.AdditionalRule = nukeopsEntity;
-        var nukeopsComp = Comp<NukeopsRuleComponent>(nukeopsEntity);
-        nukeopsComp.SpawnOutpost = false;
-        nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing;
-        GameTicker.StartGameRule(nukeopsEntity);
-    }
-
-    protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
-    {
-        base.Ended(uid, component, gameRule, args);
-
-        if (component.AdditionalRule != null)
-            GameTicker.EndGameRule(component.AdditionalRule.Value);
-    }
-}
index 722a489541f639ce0a13e80615bda16dd99afdd1..d6f609bee1d1dc81e31a64d13149cbe2a2609023 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.Traits.Assorted;
index ad56479b379decf453c2421563ae4b7d332b5fd3..455011259dca6c6d6f72d4a0381600194afb9f67 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Robust.Shared.Map;
index 8ad5c8602e3d825c456487dd3841f0f21908db04..d9d68a386cf54877ac674eefb272de53a4aad4a7 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Ninja.Systems;
 using Content.Server.Station.Components;
index 5503438df8adc441f3764e44fbdf358f4ae4059c..d547fc9446193f3f472603f91cd55ef37573c1be 100644 (file)
@@ -1,4 +1,5 @@
 using System.Threading;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
index c3cd719cc4c98843a035d8d946cc21f323e0d7f6..87d50fc8b2a4818150d052c644a35c347f404ca1 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.Storage.Components;
index 4b7606d01f9c63da588781d6bec31da313d263b7..06bb470602ba2802b8e8c5f0207f950165984dc4 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.StationEvents.Components;
index c514acc62361d4ea2cfb9e70b6f6f25dd8fc7661..77744d44e4652fe9fe565e2a66fe3ae6b3b549b4 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 
index a4ec74b43bae781adb304a68075c500d771551f8..0370b4ee61d08182500ea7f9634c1ce1f1e8d4db 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Radio;
 using Robust.Shared.Random;
index 7f05f8940d9f2af677b131dd05364ebea9e4bdb8..cbdae9e9e36a17d06300f46f655d799f9e7b0fa9 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Administration.Logs;
 using Content.Server.Chat.Systems;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Systems;
index e263a5f4f69efb95215feb20b40afa8ffcef2108..867f41dcccfad00cdf74bee3e067f538fe18f168 100644 (file)
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
 using Robust.Shared.Random;
 using System.Linq;
 using Content.Server.Fluids.EntitySystems;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 
index cdcf2bf6ff282ad40134cbe817bbbaa768efedb0..c2605039bce24b341300776e5457259b90a327f2 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
index ef3b5cf18a7ebf09558860ab9b28a5cfaf6776fb..6c1ad4f4891118c01c8f7964445430464e44b00a 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
index 15deae25529f365c3b21cdcacfbf93cb73a1fb67..e9307effbc645ce673abe14297a284cb74f865e3 100644 (file)
@@ -1,6 +1,7 @@
-using Content.Server.GameTicking.Rules;
+using Content.Server.Antag;
 using Content.Server.Traitor.Components;
 using Content.Shared.Mind.Components;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Traitor.Systems;
 
@@ -9,7 +10,10 @@ namespace Content.Server.Traitor.Systems;
 /// </summary>
 public sealed class AutoTraitorSystem : EntitySystem
 {
-    [Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
+    [Dependency] private readonly AntagSelectionSystem _antag = default!;
+
+    [ValidatePrototypeId<EntityPrototype>]
+    private const string DefaultTraitorRule = "Traitor";
 
     public override void Initialize()
     {
@@ -20,44 +24,6 @@ public sealed class AutoTraitorSystem : EntitySystem
 
     private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
     {
-        TryMakeTraitor(uid, comp);
-    }
-
-    /// <summary>
-    /// Sets the GiveUplink field.
-    /// </summary>
-    public void SetGiveUplink(EntityUid uid, bool giveUplink, AutoTraitorComponent? comp = null)
-    {
-        if (!Resolve(uid, ref comp))
-            return;
-
-        comp.GiveUplink = giveUplink;
-    }
-
-    /// <summary>
-    /// Sets the GiveObjectives field.
-    /// </summary>
-    public void SetGiveObjectives(EntityUid uid, bool giveObjectives, AutoTraitorComponent? comp = null)
-    {
-        if (!Resolve(uid, ref comp))
-            return;
-
-        comp.GiveObjectives = giveObjectives;
-    }
-
-    /// <summary>
-    /// Checks if there is a mind, then makes it a traitor using the options.
-    /// </summary>
-    public bool TryMakeTraitor(EntityUid uid, AutoTraitorComponent? comp = null)
-    {
-        if (!Resolve(uid, ref comp))
-            return false;
-
-        //Start the rule if it has not already been started
-        var traitorRuleComponent = _traitorRule.StartGameRule();
-        _traitorRule.MakeTraitor(uid, traitorRuleComponent, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives);
-        // prevent spamming anything if it fails
-        RemComp<AutoTraitorComponent>(uid);
-        return true;
+        _antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
     }
 }
index cdaed3f928e9044f464cfd9bd5472b6bd0f2ceb6..79192f6b496f9d8e9be1376aa41f9f883801126f 100644 (file)
@@ -83,12 +83,9 @@ namespace Content.Server.Traitor.Uplink.Commands
                 uplinkEntity = eUid;
             }
 
-            // Get TC count
-            var tcCount = _cfgManager.GetCVar(CCVars.TraitorStartingBalance);
-            Logger.Debug(_entManager.ToPrettyString(user));
             // Finally add uplink
             var uplinkSys = _entManager.System<UplinkSystem>();
-            if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity))
+            if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity))
             {
                 shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
             }
index 10b62c05dc7da9eaba73d36939e5892df802aee0..98eae74f06fb889e1562aadf0be7b9c76a3422de 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Damage;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Server.Zombies;
@@ -35,6 +36,21 @@ public sealed partial class PendingZombieComponent : Component
     [DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)]
     public TimeSpan GracePeriod = TimeSpan.Zero;
 
+    /// <summary>
+    /// The minimum amount of time initial infected have before they start taking infection damage.
+    /// </summary>
+    [DataField]
+    public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
+
+    /// <summary>
+    /// The maximum amount of time initial infected have before they start taking damage.
+    /// </summary>
+    [DataField]
+    public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
+
+    [DataField]
+    public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
+
     /// <summary>
     /// The chance each second that a warning will be shown.
     /// </summary>
index 080bef44e7a9bc4c842b6676cd9488b60a5c9e4d..09c8fa26db6aae921c62fb2926771dc01a149a72 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Server.Actions;
 using Content.Server.Body.Systems;
 using Content.Server.Chat;
 using Content.Server.Chat.Systems;
@@ -30,6 +31,7 @@ namespace Content.Server.Zombies
         [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
         [Dependency] private readonly DamageableSystem _damageable = default!;
         [Dependency] private readonly ChatSystem _chat = default!;
+        [Dependency] private readonly ActionsSystem _actions = default!;
         [Dependency] private readonly AutoEmoteSystem _autoEmote = default!;
         [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
         [Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -74,6 +76,8 @@ namespace Content.Server.Zombies
             }
 
             component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
+            component.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
+            _actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype);
         }
 
         public override void Update(float frameTime)
index 98abe713ebea34f3c80c6c74490b94be58375077..02d0b5f58fee933da2516d9758d5c781cf182ebc 100644 (file)
@@ -20,3 +20,8 @@ public enum AntagAcceptability
     All
 }
 
+public enum AntagSelectionTime : byte
+{
+    PrePlayerSpawn,
+    PostPlayerSpawn
+}
index 4a7b4408466bda9a87044a38a7d58727141c05eb..2954cfecce5969f1e5c17b666645a05d9de21ca9 100644 (file)
@@ -403,91 +403,6 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
             CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
 
-
-        /*
-         * Suspicion
-         */
-
-        public static readonly CVarDef<int> SuspicionMinPlayers =
-            CVarDef.Create("suspicion.min_players", 5);
-
-        public static readonly CVarDef<int> SuspicionMinTraitors =
-            CVarDef.Create("suspicion.min_traitors", 2);
-
-        public static readonly CVarDef<int> SuspicionPlayersPerTraitor =
-            CVarDef.Create("suspicion.players_per_traitor", 6);
-
-        public static readonly CVarDef<int> SuspicionStartingBalance =
-            CVarDef.Create("suspicion.starting_balance", 20);
-
-        public static readonly CVarDef<int> SuspicionMaxTimeSeconds =
-            CVarDef.Create("suspicion.max_time_seconds", 300);
-
-        /*
-         * Traitor
-         */
-
-        public static readonly CVarDef<int> TraitorMinPlayers =
-            CVarDef.Create("traitor.min_players", 5);
-
-        public static readonly CVarDef<int> TraitorMaxTraitors =
-            CVarDef.Create("traitor.max_traitors", 12); // Assuming average server maxes somewhere from like 50-80 people
-
-        public static readonly CVarDef<int> TraitorPlayersPerTraitor =
-            CVarDef.Create("traitor.players_per_traitor", 10);
-
-        public static readonly CVarDef<int> TraitorCodewordCount =
-            CVarDef.Create("traitor.codeword_count", 4);
-
-        public static readonly CVarDef<int> TraitorStartingBalance =
-            CVarDef.Create("traitor.starting_balance", 20);
-
-        public static readonly CVarDef<int> TraitorMaxDifficulty =
-            CVarDef.Create("traitor.max_difficulty", 5);
-
-        public static readonly CVarDef<int> TraitorMaxPicks =
-            CVarDef.Create("traitor.max_picks", 20);
-
-        public static readonly CVarDef<float> TraitorStartDelay =
-            CVarDef.Create("traitor.start_delay", 4f * 60f);
-
-        public static readonly CVarDef<float> TraitorStartDelayVariance =
-            CVarDef.Create("traitor.start_delay_variance", 3f * 60f);
-
-        /*
-         * TraitorDeathMatch
-         */
-
-        public static readonly CVarDef<int> TraitorDeathMatchStartingBalance =
-            CVarDef.Create("traitordm.starting_balance", 20);
-
-        /*
-         * Zombie
-         */
-
-        public static readonly CVarDef<int> ZombieMinPlayers =
-            CVarDef.Create("zombie.min_players", 20);
-
-        /*
-         * Pirates
-         */
-
-        public static readonly CVarDef<int> PiratesMinPlayers =
-            CVarDef.Create("pirates.min_players", 25);
-
-        public static readonly CVarDef<int> PiratesMaxOps =
-            CVarDef.Create("pirates.max_pirates", 6);
-
-        public static readonly CVarDef<int> PiratesPlayersPerOp =
-            CVarDef.Create("pirates.players_per_pirate", 5);
-
-        /*
-         * Nukeops
-         */
-
-        public static readonly CVarDef<bool> NukeopsSpawnGhostRoles =
-            CVarDef.Create("nukeops.spawn_ghost_roles", false);
-
         /*
          * Tips
          */
index 2a846d7fe2d2112dfdf6aa4cc3ca288ee1c3587a..ffb78dcf1fcf0790238ac59ab6472f21abd7bc68 100644 (file)
@@ -267,8 +267,11 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
     /// <param name="uid">The mob's entity UID.</param>
     /// <param name="profile">The character profile to load.</param>
     /// <param name="humanoid">Humanoid component of the entity</param>
-    public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
+    public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
     {
+        if (profile == null)
+            return;
+
         if (!Resolve(uid, ref humanoid))
         {
             return;
index 811387d375014250ec3687a69cd27f46417beccc..7e325abe21662ce642c1d531c0d4046d95507bd2 100644 (file)
@@ -1,8 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
 using Content.Shared.Hands.Components;
 using Content.Shared.Storage.EntitySystems;
-using Robust.Shared.Containers;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Inventory;
@@ -96,7 +94,7 @@ public partial class InventorySystem
     /// </summary>
     /// <param name="entity">The entity that you want to spawn an item on</param>
     /// <param name="items">A list of prototype IDs that you want to spawn in the bag.</param>
-    public void SpawnItemsOnEntity(EntityUid entity, List<EntProtoId> items)
+    public void SpawnItemsOnEntity(EntityUid entity, List<string> items)
     {
         foreach (var item in items)
         {
index cdbefece9d6b1031c48723a4a93ab18955b32f78..d19f0ae3e9d670d4c3e2d4a16429a775f74e9521 100644 (file)
@@ -13,14 +13,9 @@ namespace Content.Shared.NukeOps;
 [RegisterComponent, NetworkedComponent]
 public sealed partial class NukeOperativeComponent : Component
 {
-    /// <summary>
-    ///     Path to antagonist alert sound.
-    /// </summary>
-    [DataField("greetSoundNotification")]
-    public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
 
     /// <summary>
-    ///     
+    ///
     /// </summary>
     [DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer<StatusIconPrototype>))]
     public string SyndStatusIcon = "SyndicateFaction";
index c25ac1968d519b23e9dc7c3ef5c7c65959b8ef7a..1a732eb750b863e4d73cd50ee270b7b32b851a8e 100644 (file)
@@ -62,6 +62,32 @@ public abstract class SharedRoleSystem : EntitySystem
         _antagTypes.Add(typeof(T));
     }
 
+    public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false)
+    {
+        if (!Resolve(mindId, ref mind))
+            return;
+
+        if (HasComp(mindId, component.GetType()))
+        {
+            throw new ArgumentException($"We already have this role: {component}");
+        }
+
+        EntityManager.AddComponent(mindId, component);
+        var antagonist = IsAntagonistRole(component.GetType());
+
+        var mindEv = new MindRoleAddedEvent(silent);
+        RaiseLocalEvent(mindId, ref mindEv);
+
+        var message = new RoleAddedEvent(mindId, mind, antagonist, silent);
+        if (mind.OwnedEntity != null)
+        {
+            RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
+        }
+
+        _adminLogger.Add(LogType.Mind, LogImpact.Low,
+            $"'Role {component}' added to mind of {_minds.MindOwnerLoggingString(mind)}");
+    }
+
     /// <summary>
     ///     Gives this mind a new role.
     /// </summary>
@@ -180,6 +206,11 @@ public abstract class SharedRoleSystem : EntitySystem
         return _antagTypes.Contains(typeof(T));
     }
 
+    public bool IsAntagonistRole(Type component)
+    {
+        return _antagTypes.Contains(component);
+    }
+
     /// <summary>
     /// Play a sound for the mind, if it has a session attached.
     /// Use this for role greeting sounds.
index 49ef8509db236637d4371d7627815da6e267e5c0..363fb3f91e6e851c7427c124e9e14f1041acb284 100644 (file)
@@ -1,16 +1,17 @@
 using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Inventory;
-using Content.Shared.Preferences;
 using Content.Shared.Roles;
 using Content.Shared.Storage;
 using Content.Shared.Storage.EntitySystems;
 using Robust.Shared.Collections;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Station;
 
 public abstract class SharedStationSpawningSystem : EntitySystem
 {
+    [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
     [Dependency] protected readonly InventorySystem InventorySystem = default!;
     [Dependency] private   readonly SharedHandsSystem _handsSystem = default!;
     [Dependency] private   readonly SharedStorageSystem _storage = default!;
@@ -21,8 +22,22 @@ public abstract class SharedStationSpawningSystem : EntitySystem
     /// </summary>
     /// <param name="entity">Entity to load out.</param>
     /// <param name="startingGear">Starting gear to use.</param>
-    public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear)
+    public void EquipStartingGear(EntityUid entity, ProtoId<StartingGearPrototype>? startingGear)
     {
+        PrototypeManager.TryIndex(startingGear, out var gearProto);
+        EquipStartingGear(entity, gearProto);
+    }
+
+    /// <summary>
+    /// Equips starting gear onto the given entity.
+    /// </summary>
+    /// <param name="entity">Entity to load out.</param>
+    /// <param name="startingGear">Starting gear to use.</param>
+    public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear)
+    {
+        if (startingGear == null)
+            return;
+
         if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
         {
             foreach (var slot in slotDefinitions)
diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl
deleted file mode 100644 (file)
index 941643d..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-pirates-title = Privateers
-pirates-description = A group of privateers has approached your lowly station. Hostile or not, their sole goal is to end the round with as many knicknacks on their ship as they can get.
-
-pirates-no-ship = Through unknown circumstances, the privateer's ship was completely and utterly destroyed. No score.
-pirates-final-score = The privateers successfully obtained {$score} spesos worth
-pirates-final-score-2 = of knicknacks, with a total of {$finalPrice} spesos.
-pirates-list-start = The privateers were:
-pirates-most-valuable = The most valuable stolen items were:
-pirates-stolen-item-entry = {$entity} ({$credits} spesos)
-pirates-stole-nothing = - The pirates stole absolutely nothing at all. Point and laugh.
index 6a450f526695242852ea2548b08de6b06c6f6a64..88b113d7fdbb3c3cd392ec41e22fa6fba0eefb48 100644 (file)
@@ -1771,7 +1771,7 @@ entities:
     - type: Transform
       pos: 0.5436061,-7.5129323
       parent: 325
-- proto: SpawnPointLoneNukeOperative
+- proto: SpawnPointNukies
   entities:
   - uid: 322
     components:
index ddbccfe686c90420f4017e2c0363a4fb4f393085..727b55eb4ef747aa724ee92968bea314c11228f6 100644 (file)
@@ -84,8 +84,7 @@
     name: ghost-role-information-loneop-name
     description: ghost-role-information-loneop-description
     rules: ghost-role-information-loneop-rules
-  - type: GhostRoleMobSpawner
-    prototype: MobHumanLoneNuclearOperative
+  - type: GhostRoleAntagSpawner
   - type: Sprite
     sprite: Markers/jobs.rsi
     layers:
index 32cfd69cb02d66f15d4af823b93db58ad39f0303..45519e840d7279ecd3ff0e84ee9d6731c1007249 100644 (file)
     weight: 3
     duration: 1
   - type: ZombieRule
-    minStartDelay: 0 #let them know immediately
-    maxStartDelay: 10
-    maxInitialInfected: 3 #fewer zombies
-    minInitialInfectedGrace: 300 #less time to prepare
-    maxInitialInfectedGrace: 450
+  - type: AntagSelection
+    definitions:
+    - prefRoles: [ InitialInfected ]
+      max: 3
+      playerRatio: 10
+      blacklist:
+        components:
+        - ZombieImmune
+        - InitialInfectedExempt
+      briefing:
+        text: zombie-patientzero-role-greeting
+        color: Plum
+        sound: "/Audio/Ambience/Antag/zombie_start.ogg"
+      components:
+      - type: PendingZombie #less time to prepare than normal
+        minInitialInfectedGrace: 300
+        maxInitialInfectedGrace: 450
+      - type: ZombifyOnDeath
+      - type: IncurableZombie
+      mindComponents:
+      - type: InitialInfectedRole
+        prototype: InitialInfected
 
 - type: entity
   id: LoneOpsSpawn
     minimumPlayers: 20
     reoccurrenceDelay: 30
     duration: 1
-  - type: LoneOpsSpawnRule
+  - type: LoadMapRule
+    mapPath: /Maps/Shuttles/striker.yml
+  - type: NukeopsRule
+    roundEndBehavior: Nothing
+  - type: AntagSelection
+    definitions:
+    - spawnerPrototype: SpawnPointLoneNukeOperative
+      min: 1
+      max: 1
+      pickPlayer: false
+      startingGear: SyndicateLoneOperativeGearFull
+      components:
+      - type: NukeOperative
+      - type: RandomMetadata
+        nameSegments:
+        - SyndicateNamesPrefix
+        - SyndicateNamesNormal
+      - type: NpcFactionMember
+        factions:
+        - Syndicate
+      mindComponents:
+      - type: NukeopsRole
+        prototype: Nukeops
 
 - type: entity
   id: MassHallucinations
index 37fc4b44cde277893e7cf081b1cd288b47d2b992..bb870f6007e68b933bf2d54c03d1354e9b5bf997 100644 (file)
   id: Thief
   components:
   - type: ThiefRule
+  - type: AntagSelection
+    definitions:
+    - prefRoles: [ Thief ]
+      maxRange:
+        min: 1
+        max: 3
+      playerRatio: 1
+      allowNonHumans: true
+      multiAntagSetting: All
+      startingGear: ThiefGear
+      components:
+      - type: Pacified
+      mindComponents:
+      - type: ThiefRole
+        prototype: Thief
+      briefing:
+        sound: "/Audio/Misc/thief_greeting.ogg"
 
 - type: entity
   noSpawn: true
index 6d2b1f29d1e44f30b666904e33fa020cd7b3e15d..8218e1bdd167ecf42e45e05013332e439a3d5f14 100644 (file)
   components:
   - type: GameRule
     minPlayers: 20
+  - type: RandomMetadata #this generates the random operation name cuz it's cool.
+    nameSegments:
+    - operationPrefix
+    - operationSuffix
   - type: NukeopsRule
-    faction: Syndicate
-
-- type: entity
-  id: Pirates
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: PiratesRule
+  - type: LoadMapRule
+    gameMap: NukieOutpost
+  - type: AntagSelection
+    selectionTime: PrePlayerSpawn
+    definitions:
+    - prefRoles: [ NukeopsCommander ]
+      fallbackRoles: [ Nukeops, NukeopsMedic ]
+      max: 1
+      playerRatio: 10
+      startingGear: SyndicateCommanderGearFull
+      components:
+      - type: NukeOperative
+      - type: RandomMetadata
+        nameSegments:
+        - nukeops-role-commander
+        - SyndicateNamesElite
+      - type: NpcFactionMember
+        factions:
+        - Syndicate
+      mindComponents:
+      - type: NukeopsRole
+        prototype: NukeopsCommander
+    - prefRoles: [ NukeopsMedic ]
+      fallbackRoles: [ Nukeops, NukeopsCommander ]
+      max: 1
+      playerRatio: 10
+      startingGear: SyndicateOperativeMedicFull
+      components:
+      - type: NukeOperative
+      - type: RandomMetadata
+        nameSegments:
+        - nukeops-role-agent
+        - SyndicateNamesNormal
+      - type: NpcFactionMember
+        factions:
+        - Syndicate
+      mindComponents:
+      - type: NukeopsRole
+        prototype: NukeopsMedic
+    - prefRoles: [ Nukeops ]
+      fallbackRoles: [ NukeopsCommander, NukeopsMedic ]
+      min: 0
+      max: 3
+      playerRatio: 10
+      startingGear: SyndicateOperativeGearFull
+      components:
+      - type: NukeOperative
+      - type: RandomMetadata
+        nameSegments:
+        - nukeops-role-operator
+        - SyndicateNamesNormal
+      - type: NpcFactionMember
+        factions:
+        - Syndicate
+      mindComponents:
+      - type: NukeopsRole
+        prototype: Nukeops
 
 - type: entity
   id: Traitor
   parent: BaseGameRule
   noSpawn: true
   components:
+  - type: GameRule
+    minPlayers: 5
+    delay:
+      min: 240
+      max: 420
   - type: TraitorRule
+  - type: AntagSelection
+    definitions:
+    - prefRoles: [ Traitor ]
+      max: 12
+      playerRatio: 10
+      lateJoinAdditional: true
+      mindComponents:
+      - type: TraitorRole
+        prototype: Traitor
 
 - type: entity
   id: Revolutionary
   parent: BaseGameRule
   noSpawn: true
   components:
+  - type: GameRule
+    minPlayers: 15
   - type: RevolutionaryRule
+  - type: AntagSelection
+    definitions:
+    - prefRoles: [ HeadRev ]
+      max: 3
+      playerRatio: 15
+      briefing:
+        text: head-rev-role-greeting
+        color: CornflowerBlue
+        sound: "/Audio/Ambience/Antag/headrev_start.ogg"
+      startingGear: HeadRevGear
+      components:
+      - type: Revolutionary
+      - type: HeadRevolutionary
+      mindComponents:
+      - type: RevolutionaryRole
+        prototype: HeadRev
 
 - type: entity
   id: Sandbox
   parent: BaseGameRule
   noSpawn: true
   components:
+  - type: GameRule
+    minPlayers: 20
+    delay:
+      min: 600
+      max: 900
   - type: ZombieRule
+  - type: AntagSelection
+    definitions:
+    - prefRoles: [ InitialInfected ]
+      max: 6
+      playerRatio: 10
+      blacklist:
+        components:
+        - ZombieImmune
+        - InitialInfectedExempt
+      briefing:
+        text: zombie-patientzero-role-greeting
+        color: Plum
+        sound: "/Audio/Ambience/Antag/zombie_start.ogg"
+      components:
+      - type: PendingZombie
+      - type: ZombifyOnDeath
+      - type: IncurableZombie
+      mindComponents:
+      - type: InitialInfectedRole
+        prototype: InitialInfected
 
 # event schedulers
 - type: entity
     - id: BasicTrashVariationPass
     - id: SolidWallRustingVariationPass
     - id: ReinforcedWallRustingVariationPass
-    - id: CutWireVariationPass
     - id: BasicPuddleMessVariationPass
       prob: 0.99
       orGroup: puddleMess
index 70d74b932a062aac6f3f7cfdff0561332e07f368..946100fb96c77a037b451dbef7d3b88dc4db5971 100644 (file)
 #Head Rev Gear
 - type: startingGear
   id: HeadRevGear
-  equipment:
-    pocket2: Flash
+  storage:
+    back:
+    - Flash
+    - ClothingEyesGlassesSunglasses
+
+#Thief Gear
+- type: startingGear
+  id: ThiefGear
+  storage:
+    back:
+    - ToolboxThief
+    - ClothingHandsChameleonThief
 
 #Gladiator with spear
 - type: startingGear
index a5b20a3db68e8297c570c0d42651d17cf18b5e68..3bc6ae6208c01958d10c9e14ff000cbc3324c3cd 100644 (file)
   - Zombie
   - BasicStationEventScheduler
   - BasicRoundstartVariation
-
-- type: gamePreset
-  id: Pirates
-  alias:
-    - pirates
-  name: pirates-title
-  description: pirates-description
-  showInVote: false
-  rules:
-    - Pirates
-    - BasicStationEventScheduler
-    - BasicRoundstartVariation