]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Revert "Gamerule Entities" (#15724)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Mon, 24 Apr 2023 06:21:05 +0000 (16:21 +1000)
committerGitHub <noreply@github.com>
Mon, 24 Apr 2023 06:21:05 +0000 (16:21 +1000)
124 files changed:
Content.Client/Suspicion/SuspicionEndTimerSystem.cs [new file with mode: 0644]
Content.Client/Suspicion/SuspicionGui.xaml [new file with mode: 0644]
Content.Client/Suspicion/SuspicionGui.xaml.cs [new file with mode: 0644]
Content.Client/Suspicion/SuspicionRoleComponent.cs [new file with mode: 0644]
Content.Client/Suspicion/SuspicionRoleSystem.cs [new file with mode: 0644]
Content.Client/Suspicion/TraitorOverlay.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs
Content.Server/Dragon/Components/DragonRuleComponent.cs [deleted file]
Content.Server/Dragon/DragonSystem.Rule.cs
Content.Server/Dragon/DragonSystem.cs
Content.Server/GameTicking/GameTicker.GamePreset.cs
Content.Server/GameTicking/GameTicker.GameRule.cs
Content.Server/GameTicking/GameTicker.RoundFlow.cs
Content.Server/GameTicking/Presets/GamePresetPrototype.cs
Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs
Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs [deleted file]
Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs [moved from Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs with 51% similarity]
Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs [moved from Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs with 69% similarity]
Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs [moved from Content.Server/StationEvents/Components/StationEventComponent.cs with 58% similarity]
Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
Content.Server/GameTicking/Rules/GameRulePrototype.cs
Content.Server/GameTicking/Rules/GameRuleSystem.cs
Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
Content.Server/GameTicking/Rules/PiratesRuleSystem.cs
Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
Content.Server/GameTicking/Rules/SecretRuleSystem.cs
Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs
Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs
Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs
Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs
Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs [deleted file]
Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/GasLeakRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs [deleted file]
Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/SentienceTargetComponent.cs
Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/VentClogRuleComponent.cs [deleted file]
Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs
Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs [deleted file]
Content.Server/StationEvents/EventManagerSystem.cs
Content.Server/StationEvents/Events/AnomalySpawn.cs [moved from Content.Server/StationEvents/Events/AnomalySpawnRule.cs with 61% similarity]
Content.Server/StationEvents/Events/BluespaceArtifact.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/BluespaceArtifactRule.cs [deleted file]
Content.Server/StationEvents/Events/BluespaceLocker.cs [moved from Content.Server/StationEvents/Events/BluespaceLockerRule.cs with 79% similarity]
Content.Server/StationEvents/Events/BreakerFlip.cs [moved from Content.Server/StationEvents/Events/BreakerFlipRule.cs with 70% similarity]
Content.Server/StationEvents/Events/BureaucraticError.cs [moved from Content.Server/StationEvents/Events/BureaucraticErrorRule.cs with 82% similarity]
Content.Server/StationEvents/Events/DiseaseOutbreak.cs [moved from Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs with 70% similarity]
Content.Server/StationEvents/Events/FalseAlarm.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/FalseAlarmRule.cs [deleted file]
Content.Server/StationEvents/Events/GasLeak.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/GasLeakRule.cs [deleted file]
Content.Server/StationEvents/Events/KudzuGrowth.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/KudzuGrowthRule.cs [deleted file]
Content.Server/StationEvents/Events/LoneOpsSpawn.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs [deleted file]
Content.Server/StationEvents/Events/MeteorSwarm.cs [moved from Content.Server/StationEvents/Events/MeteorSwarmRule.cs with 53% similarity]
Content.Server/StationEvents/Events/MouseMigration.cs [moved from Content.Server/StationEvents/Events/MouseMigrationRule.cs with 66% similarity]
Content.Server/StationEvents/Events/PowerGridCheck.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/PowerGridCheckRule.cs [deleted file]
Content.Server/StationEvents/Events/RandomSentience.cs [moved from Content.Server/StationEvents/Events/RandomSentienceRule.cs with 67% similarity]
Content.Server/StationEvents/Events/RevenantSpawn.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/RevenantSpawnRule.cs [deleted file]
Content.Server/StationEvents/Events/SolarFlare.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/SolarFlareRule.cs [deleted file]
Content.Server/StationEvents/Events/SpiderSpawn.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/SpiderSpawnRule.cs [deleted file]
Content.Server/StationEvents/Events/StationEventSystem.cs
Content.Server/StationEvents/Events/VentClog.cs [moved from Content.Server/StationEvents/Events/VentClogRule.cs with 73% similarity]
Content.Server/StationEvents/Events/VentCritters.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/VentCrittersRule.cs [deleted file]
Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs [new file with mode: 0644]
Content.Server/Suspicion/Roles/SuspicionRole.cs [new file with mode: 0644]
Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs [new file with mode: 0644]
Content.Server/Suspicion/SuspicionItemComponent.cs [new file with mode: 0644]
Content.Server/Suspicion/SuspicionRoleComponent.cs [new file with mode: 0644]
Content.Server/Suspicion/SuspicionRoleSystem.cs [new file with mode: 0644]
Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs [new file with mode: 0644]
Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs [new file with mode: 0644]
Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs [new file with mode: 0644]
Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs [new file with mode: 0644]
Content.Shared/Suspicion/SuspicionMessages.cs [new file with mode: 0644]
Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Machines/traitordm.yml [new file with mode: 0644]
Resources/Prototypes/GameRules/events.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml [new file with mode: 0644]
Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml [new file with mode: 0644]
Resources/Prototypes/game_presets.yml

diff --git a/Content.Client/Suspicion/SuspicionEndTimerSystem.cs b/Content.Client/Suspicion/SuspicionEndTimerSystem.cs
new file mode 100644 (file)
index 0000000..a843f49
--- /dev/null
@@ -0,0 +1,23 @@
+using System;
+using Content.Shared.Suspicion;
+using Robust.Shared.GameObjects;
+
+namespace Content.Client.Suspicion
+{
+    public sealed class SuspicionEndTimerSystem : EntitySystem
+    {
+        public TimeSpan? EndTime { get; private set; }
+
+        public override void Initialize()
+        {
+            base.Initialize();
+
+            SubscribeNetworkEvent<SuspicionMessages.SetSuspicionEndTimerMessage>(RxTimerMessage);
+        }
+
+        private void RxTimerMessage(SuspicionMessages.SetSuspicionEndTimerMessage ev)
+        {
+            EndTime = ev.EndTime;
+        }
+    }
+}
diff --git a/Content.Client/Suspicion/SuspicionGui.xaml b/Content.Client/Suspicion/SuspicionGui.xaml
new file mode 100644 (file)
index 0000000..8f519b2
--- /dev/null
@@ -0,0 +1,8 @@
+<sus:SuspicionGui xmlns="https://spacestation14.io"
+                  xmlns:sus="clr-namespace:Content.Client.Suspicion">
+    <BoxContainer Orientation="Vertical" SeparationOverride="0">
+        <Button Name="RoleButton">
+            <Label Name="TimerLabel" HorizontalAlignment="Right" VerticalAlignment="Bottom" />
+        </Button>
+    </BoxContainer>
+</sus:SuspicionGui>
diff --git a/Content.Client/Suspicion/SuspicionGui.xaml.cs b/Content.Client/Suspicion/SuspicionGui.xaml.cs
new file mode 100644 (file)
index 0000000..1314875
--- /dev/null
@@ -0,0 +1,126 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Content.Shared.Popups;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Suspicion
+{
+    [GenerateTypedNameReferences]
+    public sealed partial class SuspicionGui : UIWidget
+    {
+        [Dependency] private readonly IEntityManager _entManager = default!;
+        [Dependency] private readonly IPlayerManager _playerManager = default!;
+        [Dependency] private readonly IGameTiming _timing = default!;
+
+        private string? _previousRoleName;
+        private bool _previousAntagonist;
+
+        public SuspicionGui()
+        {
+            RobustXamlLoader.Load(this);
+            IoCManager.InjectDependencies(this);
+
+            RoleButton.OnPressed += RoleButtonPressed;
+            RoleButton.MinSize = (200, 60);
+        }
+
+        private void RoleButtonPressed(ButtonEventArgs obj)
+        {
+            if (!TryGetComponent(out var role))
+            {
+                return;
+            }
+
+            if (!role.Antagonist ?? false)
+            {
+                return;
+            }
+
+            var allies = string.Join(", ", role.Allies.Select(tuple => tuple.name));
+
+            role.Owner.PopupMessage(
+                Loc.GetString(
+                    "suspicion-ally-count-display",
+                    ("allyCount", role.Allies.Count),
+                    ("allyNames", allies)
+                )
+            );
+        }
+
+        private bool TryGetComponent([NotNullWhen(true)] out SuspicionRoleComponent? suspicion)
+        {
+            suspicion = default;
+            if (_playerManager.LocalPlayer?.ControlledEntity == null)
+            {
+                return false;
+            }
+
+            return _entManager.TryGetComponent(_playerManager.LocalPlayer.ControlledEntity, out suspicion);
+        }
+
+        public void UpdateLabel()
+        {
+            if (!TryGetComponent(out var suspicion))
+            {
+                Visible = false;
+                return;
+            }
+
+            if (suspicion.Role == null || suspicion.Antagonist == null)
+            {
+                Visible = false;
+                return;
+            }
+
+            var endTime = _entManager.System<SuspicionEndTimerSystem>().EndTime;
+            if (endTime == null)
+            {
+                TimerLabel.Visible = false;
+            }
+            else
+            {
+                var diff = endTime.Value - _timing.CurTime;
+                if (diff < TimeSpan.Zero)
+                {
+                    diff = TimeSpan.Zero;
+                }
+                TimerLabel.Visible = true;
+                TimerLabel.Text = $"{diff:mm\\:ss}";
+            }
+
+            if (_previousRoleName == suspicion.Role && _previousAntagonist == suspicion.Antagonist)
+            {
+                return;
+            }
+
+            _previousRoleName = suspicion.Role;
+            _previousAntagonist = suspicion.Antagonist.Value;
+
+            var buttonText = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(_previousRoleName);
+            buttonText = Loc.GetString(buttonText);
+
+            RoleButton.Text = buttonText;
+            RoleButton.ModulateSelfOverride = _previousAntagonist ? Color.Red : Color.LimeGreen;
+
+            Visible = true;
+        }
+
+        protected override void FrameUpdate(FrameEventArgs args)
+        {
+            base.FrameUpdate(args);
+            UpdateLabel();
+        }
+    }
+}
diff --git a/Content.Client/Suspicion/SuspicionRoleComponent.cs b/Content.Client/Suspicion/SuspicionRoleComponent.cs
new file mode 100644 (file)
index 0000000..d95ee7e
--- /dev/null
@@ -0,0 +1,128 @@
+using Content.Shared.Suspicion;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using static Robust.Client.UserInterface.Controls.LayoutContainer;
+
+namespace Content.Client.Suspicion
+{
+    [RegisterComponent]
+    public sealed class SuspicionRoleComponent : SharedSuspicionRoleComponent
+    {
+        [Dependency] private readonly IOverlayManager _overlayManager = default!;
+        [Dependency] private readonly IResourceCache _resourceCache = default!;
+        [Dependency] private readonly IUserInterfaceManager _ui = default!;
+
+        private SuspicionGui? _gui;
+        private string? _role;
+        private bool? _antagonist;
+        private bool _overlayActive;
+
+        public string? Role
+        {
+            get => _role;
+            set
+            {
+                if (_role == value)
+                {
+                    return;
+                }
+
+                _role = value;
+                _gui?.UpdateLabel();
+                Dirty();
+            }
+        }
+
+        public bool? Antagonist
+        {
+            get => _antagonist;
+            set
+            {
+                if (_antagonist == value)
+                {
+                    return;
+                }
+
+                _antagonist = value;
+                _gui?.UpdateLabel();
+
+                if (value ?? false)
+                {
+                    AddTraitorOverlay();
+                }
+
+                Dirty();
+            }
+        }
+
+        [ViewVariables]
+        public List<(string name, EntityUid uid)> Allies { get; } = new();
+
+        private void AddTraitorOverlay()
+        {
+            if (_overlayManager.HasOverlay<TraitorOverlay>())
+            {
+                return;
+            }
+
+            _overlayActive = true;
+            var entManager = IoCManager.Resolve<IEntityManager>();
+            var overlay = new TraitorOverlay(entManager, IoCManager.Resolve<IPlayerManager>(), _resourceCache, entManager.System<EntityLookupSystem>());
+            _overlayManager.AddOverlay(overlay);
+        }
+
+        private void RemoveTraitorOverlay()
+        {
+            if (!_overlayActive)
+            {
+                return;
+            }
+
+            _overlayManager.RemoveOverlay<TraitorOverlay>();
+        }
+
+        public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
+        {
+            base.HandleComponentState(curState, nextState);
+
+            if (curState is not SuspicionRoleComponentState state)
+            {
+                return;
+            }
+
+            Role = state.Role;
+            Antagonist = state.Antagonist;
+            Allies.Clear();
+            Allies.AddRange(state.Allies);
+        }
+
+        public void RemoveUI()
+        {
+            _gui?.Parent?.RemoveChild(_gui);
+            RemoveTraitorOverlay();
+        }
+
+        public void AddUI()
+        {
+            // TODO move this out of the component
+            _gui = _ui.ActiveScreen?.GetOrAddWidget<SuspicionGui>();
+            _gui!.UpdateLabel();
+            SetAnchorAndMarginPreset(_gui, LayoutPreset.BottomLeft);
+
+            if (_antagonist ?? false)
+            {
+                AddTraitorOverlay();
+            }
+        }
+
+        protected override void OnRemove()
+        {
+            base.OnRemove();
+
+            _gui?.Dispose();
+            RemoveTraitorOverlay();
+        }
+    }
+}
diff --git a/Content.Client/Suspicion/SuspicionRoleSystem.cs b/Content.Client/Suspicion/SuspicionRoleSystem.cs
new file mode 100644 (file)
index 0000000..41cd186
--- /dev/null
@@ -0,0 +1,18 @@
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Suspicion
+{
+    sealed class SuspicionRoleSystem : EntitySystem
+    {
+        public override void Initialize()
+        {
+            base.Initialize();
+
+            SubscribeLocalEvent<SuspicionRoleComponent, ComponentAdd>((_, component, _) => component.AddUI());
+            SubscribeLocalEvent<SuspicionRoleComponent, ComponentRemove>((_, component, _) => component.RemoveUI());
+
+            SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>((_, component, _) => component.AddUI());
+            SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>((_, component, _) => component.RemoveUI());
+        }
+    }
+}
diff --git a/Content.Client/Suspicion/TraitorOverlay.cs b/Content.Client/Suspicion/TraitorOverlay.cs
new file mode 100644 (file)
index 0000000..84f52b9
--- /dev/null
@@ -0,0 +1,95 @@
+using Content.Shared.Examine;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Containers;
+using Robust.Shared.Enums;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Client.Suspicion
+{
+    public sealed class TraitorOverlay : Overlay
+    {
+        private readonly IEntityManager _entityManager;
+        private readonly IPlayerManager _playerManager;
+        private readonly EntityLookupSystem _lookup;
+
+        public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+        private readonly Font _font;
+
+        private readonly string _traitorText = Loc.GetString("traitor-overlay-traitor-text");
+
+        public TraitorOverlay(
+            IEntityManager entityManager,
+            IPlayerManager playerManager,
+            IResourceCache resourceCache,
+            EntityLookupSystem lookup)
+        {
+            _playerManager = playerManager;
+            _entityManager = entityManager;
+            _lookup = lookup;
+
+            _font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
+        }
+
+        protected override void Draw(in OverlayDrawArgs args)
+        {
+            var viewport = args.WorldAABB;
+
+            var ent = _playerManager.LocalPlayer?.ControlledEntity;
+            if (_entityManager.TryGetComponent(ent, out SuspicionRoleComponent? sus) != true)
+            {
+                return;
+            }
+
+            foreach (var (_, ally) in sus.Allies)
+            {
+                // Otherwise the entity can not exist yet
+                if (!_entityManager.EntityExists(ally))
+                {
+                    continue;
+                }
+
+                if (!_entityManager.TryGetComponent(ally, out PhysicsComponent? physics))
+                {
+                    continue;
+                }
+
+                var allyXform = _entityManager.GetComponent<TransformComponent>(ally);
+
+                var entPosition = _entityManager.GetComponent<TransformComponent>(ent.Value).MapPosition;
+                var allyPosition = allyXform.MapPosition;
+                if (!ExamineSystemShared.InRangeUnOccluded(entPosition, allyPosition, 15,
+                    entity => entity == ent || entity == ally))
+                {
+                    continue;
+                }
+
+                // if not on the same map, continue
+                if (allyXform.MapID != args.Viewport.Eye!.Position.MapId
+                    || physics.Owner.IsInContainer())
+                {
+                    continue;
+                }
+
+                var (allyWorldPos, allyWorldRot) = allyXform.GetWorldPositionRotation();
+
+                var worldBox = _lookup.GetWorldAABB(ally, allyXform);
+
+                // if not on screen, or too small, continue
+                if (!worldBox.Intersects(in viewport) || worldBox.IsEmpty())
+                {
+                    continue;
+                }
+
+                var screenCoordinates = args.ViewportControl!.WorldToScreen(worldBox.TopLeft + (0, 0.5f));
+                args.ScreenHandle.DrawString(_font, screenCoordinates, _traitorText, Color.OrangeRed);
+            }
+        }
+    }
+}
index d4f85486bc4aa8aee6bed53cd1f9c53c19c1747a..4b23af60b2541573865496f04a075e0f6cda98e2 100644 (file)
@@ -3,11 +3,12 @@ using System.Threading.Tasks;
 using Content.Server.GameTicking;
 using Content.Server.GameTicking.Commands;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.CCVar;
 using NUnit.Framework;
 using Robust.Shared.Configuration;
 using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 
 namespace Content.IntegrationTests.Tests.GameRules
@@ -22,7 +23,6 @@ namespace Content.IntegrationTests.Tests.GameRules
             await using var pairTracker = await PoolManager.GetServerClient();
             var server = pairTracker.Pair.Server;
 
-            var entityManager = server.ResolveDependency<IEntityManager>();
             var configManager = server.ResolveDependency<IConfigurationManager>();
             await server.WaitPost(() =>
             {
@@ -31,17 +31,18 @@ namespace Content.IntegrationTests.Tests.GameRules
                 command.Execute(null, string.Empty, Array.Empty<string>());
             });
 
+
             var sGameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
+            var maxTimeMaxTimeRestartRuleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MaxTimeRestartRuleSystem>();
             var sGameTiming = server.ResolveDependency<IGameTiming>();
 
-
-            sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
-            Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out var maxTime));
-
             await server.WaitAssertion(() =>
             {
                 Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
-                maxTime.RoundMaxTime = TimeSpan.FromSeconds(3);
+
+                sGameTicker.StartGameRule(IoCManager.Resolve<IPrototypeManager>().Index<GameRulePrototype>(maxTimeMaxTimeRestartRuleSystem.Prototype));
+                maxTimeMaxTimeRestartRuleSystem.RoundMaxTime = TimeSpan.FromSeconds(3);
+
                 sGameTicker.StartRound();
             });
 
@@ -50,7 +51,7 @@ namespace Content.IntegrationTests.Tests.GameRules
                 Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
             });
 
-            var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTime.RoundMaxTime.TotalSeconds * 1.1f);
+            var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundMaxTime.TotalSeconds * 1.1f);
             await PoolManager.RunTicksSync(pairTracker.Pair, ticks);
 
             await server.WaitAssertion(() =>
@@ -58,7 +59,7 @@ namespace Content.IntegrationTests.Tests.GameRules
                 Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PostRound));
             });
 
-            ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTime.RoundEndDelay.TotalSeconds * 1.1f);
+            ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundEndDelay.TotalSeconds * 1.1f);
             await PoolManager.RunTicksSync(pairTracker.Pair, ticks);
 
             await server.WaitAssertion(() =>
index 9fa3fedeea882594f64d2d545510237c70e0d5db..9a5ff23fadcdfb93adbcb4857ac4565fa38a6b6f 100644 (file)
@@ -1,8 +1,11 @@
-using System.Linq;
+using System;
+using System.Linq;
 using System.Threading.Tasks;
 using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
 using NUnit.Framework;
 using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
 
 namespace Content.IntegrationTests.Tests.GameRules;
 
@@ -23,11 +26,12 @@ public sealed class SecretStartsTest
 
         var server = pairTracker.Pair.Server;
         await server.WaitIdleAsync();
+        var protoMan = server.ResolveDependency<IPrototypeManager>();
         var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
 
         await server.WaitAssertion(() =>
         {
-            gameTicker.StartGameRule("Secret");
+            gameTicker.StartGameRule(protoMan.Index<GameRulePrototype>("Secret"));
         });
 
         // Wait three ticks for any random update loops that might happen
@@ -35,9 +39,9 @@ public sealed class SecretStartsTest
 
         await server.WaitAssertion(() =>
         {
-            foreach (var rule in gameTicker.GetAddedGameRules())
+            foreach (var rule in gameTicker.AddedGameRules)
             {
-                Assert.That(gameTicker.GetActiveGameRules().Contains(rule));
+                Assert.That(gameTicker.StartedGameRules.Contains(rule));
             }
 
             // End all rules
index e17990315fdecdb6be4e38461b69e244e347e2e3..90164e4dbee3817bafbb5025f9c2fa258ca78b5e 100644 (file)
@@ -2,11 +2,14 @@
 using System.Linq;
 using System.Threading.Tasks;
 using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
 using NUnit.Framework;
 using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
 
 namespace Content.IntegrationTests.Tests.GameRules;
 
+
 [TestFixture]
 public sealed class StartEndGameRulesTest
 {
@@ -23,18 +26,22 @@ public sealed class StartEndGameRulesTest
         });
         var server = pairTracker.Pair.Server;
         await server.WaitIdleAsync();
+        var protoMan = server.ResolveDependency<IPrototypeManager>();
         var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
 
         await server.WaitAssertion(() =>
         {
-            var rules = gameTicker.GetAllGameRulePrototypes().ToList();
+            var rules = protoMan.EnumeratePrototypes<GameRulePrototype>().ToList();
             rules.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
 
             // Start all rules
             foreach (var rule in rules)
             {
-                gameTicker.StartGameRule(rule.ID);
+                gameTicker.StartGameRule(rule);
             }
+
+            Assert.That(gameTicker.AddedGameRules, Has.Count.EqualTo(rules.Count));
+            Assert.That(gameTicker.AddedGameRules, Has.Count.EqualTo(gameTicker.StartedGameRules.Count));
         });
 
         // Wait three ticks for any random update loops that might happen
@@ -44,7 +51,7 @@ public sealed class StartEndGameRulesTest
         {
             // End all rules
             gameTicker.ClearGameRules();
-            Assert.That(!gameTicker.GetAddedGameRules().Any());
+            Assert.That(!gameTicker.AddedGameRules.Any());
         });
 
         await pairTracker.CleanReturnAsync();
diff --git a/Content.Server/Dragon/Components/DragonRuleComponent.cs b/Content.Server/Dragon/Components/DragonRuleComponent.cs
deleted file mode 100644 (file)
index 8f358c3..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Dragon;
-
-[RegisterComponent]
-public sealed class DragonRuleComponent : Component
-{
-
-}
index 0963f8c09b93dff46d22ec28907f50f5ead0dfc0..4c86634124c3f641204a3f37077b37ebc04cc8a5 100644 (file)
@@ -1,6 +1,6 @@
 using System.Linq;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.StationEvents.Components;
 using Content.Shared.Dragon;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
@@ -10,6 +10,8 @@ namespace Content.Server.Dragon;
 
 public sealed partial class DragonSystem
 {
+    public override string Prototype => "Dragon";
+
     private int RiftsMet(DragonComponent component)
     {
         var finished = 0;
@@ -26,11 +28,9 @@ public sealed partial class DragonSystem
         return finished;
     }
 
-    protected override void Started(EntityUid uid, DragonRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
-
-        var spawnLocations = EntityQuery<MapGridComponent, TransformComponent>().ToList();
+        var spawnLocations = EntityManager.EntityQuery<MapGridComponent, TransformComponent>().ToList();
 
         if (spawnLocations.Count == 0)
             return;
@@ -39,8 +39,16 @@ public sealed partial class DragonSystem
         Spawn("MobDragon", location.Item2.MapPosition);
     }
 
+    public override void Ended()
+    {
+        return;
+    }
+
     private void OnRiftRoundEnd(RoundEndTextAppendEvent args)
     {
+        if (!RuleAdded)
+            return;
+
         var dragons = EntityQuery<DragonComponent>(true).ToList();
 
         if (dragons.Count == 0)
index f2f0e7ba991cdbc45c9014f5343dcd988e61007f..d969272ab9041d9bfa1814e22216028d70ebcb3f 100644 (file)
@@ -24,7 +24,7 @@ using Content.Shared.Mobs.Components;
 
 namespace Content.Server.Dragon
 {
-    public sealed partial class DragonSystem : GameRuleSystem<DragonRuleComponent>
+    public sealed partial class DragonSystem : GameRuleSystem
     {
         [Dependency] private readonly IMapManager _mapManager = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
index d2c5005e176d1518063b1de3a097f9c0b1bb26c9..eb44943e76f31bd1b976c5ecef1df97d37f7bb42 100644 (file)
@@ -1,7 +1,9 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
+using Content.Server.GameTicking.Events;
 using Content.Server.GameTicking.Presets;
+using Content.Server.GameTicking.Rules;
 using Content.Server.Ghost.Components;
 using Content.Shared.CCVar;
 using Content.Shared.Damage;
@@ -9,7 +11,6 @@ using Content.Shared.Damage.Prototypes;
 using Content.Shared.Database;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
-using JetBrains.Annotations;
 using Robust.Server.Player;
 
 namespace Content.Server.GameTicking
@@ -42,6 +43,7 @@ namespace Content.Server.GameTicking
 
             if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
             {
+                var oldPreset = Preset;
                 ClearGameRules();
                 SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset));
                 AddGamePresetRules();
@@ -123,7 +125,6 @@ namespace Content.Server.GameTicking
             return prototype != null;
         }
 
-        [PublicAPI]
         private bool AddGamePresetRules()
         {
             if (DummyTicker || Preset == null)
@@ -131,7 +132,10 @@ namespace Content.Server.GameTicking
 
             foreach (var rule in Preset.Rules)
             {
-                AddGameRule(rule);
+                if (!_prototypeManager.TryIndex(rule, out GameRulePrototype? ruleProto))
+                    continue;
+
+                AddGameRule(ruleProto);
             }
 
             return true;
@@ -140,7 +144,7 @@ namespace Content.Server.GameTicking
         private void StartGamePresetRules()
         {
             // May be touched by the preset during init.
-            foreach (var rule in GetAddedGameRules())
+            foreach (var rule in _addedGameRules.ToArray())
             {
                 StartGameRule(rule);
             }
@@ -162,12 +166,10 @@ namespace Content.Server.GameTicking
 
             if (mind.PreventGhosting)
             {
-                if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
-                {
+                if (mind.Session != null)
+                    // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
                     _chatManager.DispatchServerMessage(mind.Session, Loc.GetString("comp-mind-ghosting-prevented"),
                         true);
-                }
-
                 return false;
             }
 
index 158e6b0085f8e38fd4ff6201426b27eaf0ff0b95..c0b382af2d5e47db36da98064866eedf8a00af7b 100644 (file)
 using System.Linq;
 using Content.Server.Administration;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Shared.Administration;
-using Content.Shared.Prototypes;
-using JetBrains.Annotations;
 using Robust.Shared.Console;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
 
-namespace Content.Server.GameTicking;
-
-public sealed partial class GameTicker
+namespace Content.Server.GameTicking
 {
-    [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new();
-
-    /// <summary>
-    ///     A list storing the start times of all game rules that have been started this round.
-    ///     Game rules can be started and stopped at any time, including midround.
-    /// </summary>
-    public IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules;
-
-    private void InitializeGameRules()
-    {
-        // Add game rule command.
-        _consoleHost.RegisterCommand("addgamerule",
-            string.Empty,
-            "addgamerule <rules>",
-            AddGameRuleCommand,
-            AddGameRuleCompletions);
-
-        // End game rule command.
-        _consoleHost.RegisterCommand("endgamerule",
-            string.Empty,
-            "endgamerule <rules>",
-            EndGameRuleCommand,
-            EndGameRuleCompletions);
-
-        // Clear game rules command.
-        _consoleHost.RegisterCommand("cleargamerules",
-            string.Empty,
-            "cleargamerules",
-            ClearGameRulesCommand);
-    }
-
-    private void ShutdownGameRules()
+    public sealed partial class GameTicker
     {
-        _consoleHost.UnregisterCommand("addgamerule");
-        _consoleHost.UnregisterCommand("endgamerule");
-        _consoleHost.UnregisterCommand("cleargamerules");
-    }
+        // No duplicates.
+        [ViewVariables] private readonly HashSet<GameRulePrototype> _addedGameRules = new();
 
-    /// <summary>
-    /// Adds a game rule to the list, but does not
-    /// start it yet, instead waiting until the rule is actually started by other code (usually roundstart)
-    /// </summary>
-    /// <returns>The entity for the added gamerule</returns>
-    public EntityUid AddGameRule(string ruleId)
-    {
-        var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace);
-        _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}");
+        /// <summary>
+        ///     Holds all currently added game rules.
+        /// </summary>
+        public IReadOnlySet<GameRulePrototype> AddedGameRules => _addedGameRules;
 
-        var ev = new GameRuleAddedEvent(ruleEntity, ruleId);
-        RaiseLocalEvent(ruleEntity, ref ev, true);
-        return ruleEntity;
-    }
+        [ViewVariables] private readonly HashSet<GameRulePrototype> _startedGameRules = new();
 
-    /// <summary>
-    /// Game rules can be 'started' separately from being added. 'Starting' them usually
-    /// happens at round start while they can be added and removed before then.
-    /// </summary>
-    public bool StartGameRule(string ruleId)
-    {
-        return StartGameRule(ruleId, out _);
-    }
+        /// <summary>
+        ///     Holds all currently started game rules.
+        /// </summary>
+        public IReadOnlySet<GameRulePrototype> StartedGameRules => _startedGameRules;
 
-    /// <summary>
-    /// Game rules can be 'started' separately from being added. 'Starting' them usually
-    /// happens at round start while they can be added and removed before then.
-    /// </summary>
-    public bool StartGameRule(string ruleId, out EntityUid ruleEntity)
-    {
-        ruleEntity = AddGameRule(ruleId);
-        return StartGameRule(ruleEntity);
-    }
+        [ViewVariables] private readonly List<(TimeSpan, GameRulePrototype)> _allPreviousGameRules = new();
 
-    /// <summary>
-    /// Game rules can be 'started' separately from being added. 'Starting' them usually
-    /// happens at round start while they can be added and removed before then.
-    /// </summary>
-    public bool StartGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null)
-    {
-        if (!Resolve(ruleEntity, ref ruleData))
-            ruleData ??= EnsureComp<GameRuleComponent>(ruleEntity);
+        /// <summary>
+        ///     A list storing the start times of all game rules that have been started this round.
+        ///     Game rules can be started and stopped at any time, including midround.
+        /// </summary>
+        public IReadOnlyList<(TimeSpan, GameRulePrototype)> AllPreviousGameRules => _allPreviousGameRules;
 
-        // can't start an already active rule
-        if (ruleData.Active || ruleData.Ended)
-            return false;
+        private void InitializeGameRules()
+        {
+            // Add game rule command.
+            _consoleHost.RegisterCommand("addgamerule",
+                string.Empty,
+                "addgamerule <rules>",
+                AddGameRuleCommand,
+                AddGameRuleCompletions);
+
+            // End game rule command.
+            _consoleHost.RegisterCommand("endgamerule",
+                string.Empty,
+                "endgamerule <rules>",
+                EndGameRuleCommand,
+                EndGameRuleCompletions);
+
+            // Clear game rules command.
+            _consoleHost.RegisterCommand("cleargamerules",
+                string.Empty,
+                "cleargamerules",
+                ClearGameRulesCommand);
+        }
 
-        if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
-            return false;
+        private void ShutdownGameRules()
+        {
+            _consoleHost.UnregisterCommand("addgamerule");
+            _consoleHost.UnregisterCommand("endgamerule");
+            _consoleHost.UnregisterCommand("cleargamerules");
+        }
 
-        _allPreviousGameRules.Add((RoundDuration(), id));
-        _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
+        /// <summary>
+        ///     Game rules can be 'started' separately from being added. 'Starting' them usually
+        ///     happens at round start while they can be added and removed before then.
+        /// </summary>
+        public void StartGameRule(GameRulePrototype rule)
+        {
+            if (!IsGameRuleAdded(rule))
+                AddGameRule(rule);
 
-        ruleData.Active = true;
-        var ev = new GameRuleStartedEvent(ruleEntity, id);
-        RaiseLocalEvent(ruleEntity, ref ev, true);
-        return true;
-    }
+            _allPreviousGameRules.Add((RoundDuration(), rule));
+            _sawmill.Info($"Started game rule {rule.ID}");
 
-    /// <summary>
-    /// Ends a game rule.
-    /// </summary>
-    [PublicAPI]
-    public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null)
-    {
-        if (!Resolve(ruleEntity, ref ruleData))
-            return false;
+            if (_startedGameRules.Add(rule))
+                RaiseLocalEvent(new GameRuleStartedEvent(rule));
+        }
 
-        // don't end it multiple times
-        if (ruleData.Ended)
-            return false;
+        /// <summary>
+        ///     Ends a game rule.
+        ///     This always includes removing it (from added game rules) so that behavior
+        ///     is not separate from this.
+        /// </summary>
+        /// <param name="rule"></param>
+        public void EndGameRule(GameRulePrototype rule)
+        {
+            if (!IsGameRuleAdded(rule))
+                return;
 
-        if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
-            return false;
+            _addedGameRules.Remove(rule);
+            _sawmill.Info($"Ended game rule {rule.ID}");
 
-        ruleData.Active = false;
-        ruleData.Ended = true;
-        _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}");
+            if (IsGameRuleStarted(rule))
+                _startedGameRules.Remove(rule);
+            RaiseLocalEvent(new GameRuleEndedEvent(rule));
+        }
 
-        var ev = new GameRuleEndedEvent(ruleEntity, id);
-        RaiseLocalEvent(ruleEntity, ref ev, true);
-        return true;
-    }
+        /// <summary>
+        ///     Adds a game rule to the list, but does not
+        ///     start it yet, instead waiting until the rule is actually started by other code (usually roundstart)
+        /// </summary>
+        public bool AddGameRule(GameRulePrototype rule)
+        {
+            if (!_addedGameRules.Add(rule))
+                return false;
 
-    public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null)
-    {
-        return Resolve(ruleEntity, ref component) && !component.Ended;
-    }
+            _sawmill.Info($"Added game rule {rule.ID}");
+            RaiseLocalEvent(new GameRuleAddedEvent(rule));
+            return true;
+        }
 
-    public bool IsGameRuleAdded(string rule)
-    {
-        foreach (var ruleEntity in GetAddedGameRules())
+        public bool IsGameRuleAdded(GameRulePrototype rule)
         {
-            if (MetaData(ruleEntity).EntityPrototype?.ID == rule)
-                return true;
+            return _addedGameRules.Contains(rule);
         }
 
-        return false;
-    }
+        public bool IsGameRuleAdded(string rule)
+        {
+            foreach (var ruleProto in _addedGameRules)
+            {
+                if (ruleProto.ID.Equals(rule))
+                    return true;
+            }
 
-    public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null)
-    {
-        return Resolve(ruleEntity, ref component) && component.Active;
-    }
+            return false;
+        }
 
-    public bool IsGameRuleActive(string rule)
-    {
-        foreach (var ruleEntity in GetActiveGameRules())
+        public bool IsGameRuleStarted(GameRulePrototype rule)
         {
-            if (MetaData(ruleEntity).EntityPrototype?.ID == rule)
-                return true;
+            return _startedGameRules.Contains(rule);
         }
 
-        return false;
-    }
-
-    public void ClearGameRules()
-    {
-        foreach (var rule in GetAddedGameRules())
+        public bool IsGameRuleStarted(string rule)
         {
-            EndGameRule(rule);
+            foreach (var ruleProto in _startedGameRules)
+            {
+                if (ruleProto.ID.Equals(rule))
+                    return true;
+            }
+
+            return false;
         }
-    }
 
-    /// <summary>
-    /// Gets all the gamerule entities which are currently active.
-    /// </summary>
-    public IEnumerable<EntityUid> GetAddedGameRules()
-    {
-        var query = EntityQueryEnumerator<GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var ruleData))
+        public void ClearGameRules()
         {
-            if (IsGameRuleAdded(uid, ruleData))
-                yield return uid;
+            foreach (var rule in _addedGameRules.ToArray())
+            {
+                EndGameRule(rule);
+            }
         }
-    }
 
-    /// <summary>
-    /// Gets all the gamerule entities which are currently active.
-    /// </summary>
-    public IEnumerable<EntityUid> GetActiveGameRules()
-    {
-        var query = EntityQueryEnumerator<GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var ruleData))
+        #region Command Implementations
+
+        [AdminCommand(AdminFlags.Fun)]
+        private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
         {
-            if (ruleData.Active)
-                yield return uid;
+            if (args.Length == 0)
+                return;
+
+            foreach (var ruleId in args)
+            {
+                if (!_prototypeManager.TryIndex<GameRulePrototype>(ruleId, out var rule))
+                    continue;
+
+                AddGameRule(rule);
+
+                // Start rule if we're already in the middle of a round
+                if(RunLevel == GameRunLevel.InRound)
+                    StartGameRule(rule);
+            }
         }
-    }
 
-    /// <summary>
-    /// Gets all gamerule prototypes
-    /// </summary>
-    public IEnumerable<EntityPrototype> GetAllGameRulePrototypes()
-    {
-        foreach (var proto in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
+        private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args)
         {
-            if (proto.Abstract)
-                continue;
-
-            if (proto.HasComponent<GameRuleComponent>())
-                yield return proto;
+            var activeIds = _addedGameRules.Select(c => c.ID);
+            return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<GameRulePrototype>().Where(p => !activeIds.Contains(p.Value)),
+                "<rule>");
         }
-    }
 
-    #region Command Implementations
+        [AdminCommand(AdminFlags.Fun)]
+        private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
+        {
+            if (args.Length == 0)
+                return;
 
-    [AdminCommand(AdminFlags.Fun)]
-    private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
-    {
-        if (args.Length == 0)
-            return;
+            foreach (var ruleId in args)
+            {
+                if (!_prototypeManager.TryIndex<GameRulePrototype>(ruleId, out var rule))
+                    continue;
 
-        foreach (var rule in args)
+                EndGameRule(rule);
+            }
+        }
+
+        private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args)
         {
-            var ent = AddGameRule(rule);
+            return CompletionResult.FromHintOptions(_addedGameRules.Select(c => new CompletionOption(c.ID)),
+                "<added rule>");
+        }
 
-            // Start rule if we're already in the middle of a round
-            if(RunLevel == GameRunLevel.InRound)
-                StartGameRule(ent);
+        [AdminCommand(AdminFlags.Fun)]
+        private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args)
+        {
+            ClearGameRules();
         }
-    }
 
-    private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args)
-    {
-        return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), "<rule>");
+        #endregion
     }
 
-    [AdminCommand(AdminFlags.Fun)]
-    private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
+    /// <summary>
+    ///     Raised broadcast when a game rule is selected, but not started yet.
+    /// </summary>
+    public sealed class GameRuleAddedEvent
     {
-        if (args.Length == 0)
-            return;
+        public GameRulePrototype Rule { get; }
 
-        foreach (var rule in args)
+        public GameRuleAddedEvent(GameRulePrototype rule)
         {
-            if (!EntityUid.TryParse(rule, out var ruleEnt))
-                continue;
-
-            EndGameRule(ruleEnt);
+            Rule = rule;
         }
     }
 
-    private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args)
-    {
-        return CompletionResult.FromHintOptions(GetAddedGameRules().Select(u => u.ToString()), "<added rule>");
-    }
-
-    [AdminCommand(AdminFlags.Fun)]
-    private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args)
+    public sealed class GameRuleStartedEvent
     {
-        ClearGameRules();
-    }
-
-    #endregion
-}
-
-/*
-/// <summary>
-///     Raised broadcast when a game rule is selected, but not started yet.
-/// </summary>
-public sealed class GameRuleAddedEvent
-{
-    public GameRulePrototype Rule { get; }
+        public GameRulePrototype Rule { get; }
 
-    public GameRuleAddedEvent(GameRulePrototype rule)
-    {
-        Rule = rule;
+        public GameRuleStartedEvent(GameRulePrototype rule)
+        {
+            Rule = rule;
+        }
     }
-}
 
-public sealed class GameRuleStartedEvent
-{
-    public GameRulePrototype Rule { get; }
-
-    public GameRuleStartedEvent(GameRulePrototype rule)
+    public sealed class GameRuleEndedEvent
     {
-        Rule = rule;
-    }
-}
-
-public sealed class GameRuleEndedEvent
-{
-    public GameRulePrototype Rule { get; }
+        public GameRulePrototype Rule { get; }
 
-    public GameRuleEndedEvent(GameRulePrototype rule)
-    {
-        Rule = rule;
+        public GameRuleEndedEvent(GameRulePrototype rule)
+        {
+            Rule = rule;
+        }
     }
 }
-*/
index 096d508f356b6874ab8933d876755d4aaa81d503..e64837ca9fcecabd1abcaabb67369718372f041a 100644 (file)
@@ -456,6 +456,7 @@ namespace Content.Server.GameTicking
             // Clear up any game rules.
             ClearGameRules();
 
+            _addedGameRules.Clear();
             _allPreviousGameRules.Clear();
 
             // Round restart cleanup event, so entity systems can reset.
index ff6a3d17ba4f89c5d64eb3b4d84271845ca384c3..e131030251f845bc8c519df3ad56ec6ee1b38b43 100644 (file)
@@ -1,4 +1,4 @@
-
+using Content.Server.GameTicking.Rules;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
@@ -14,24 +14,24 @@ namespace Content.Server.GameTicking.Presets
         public string ID { get; } = default!;
 
         [DataField("alias")]
-        public readonly string[] Alias = Array.Empty<string>();
+        public string[] Alias { get; } = Array.Empty<string>();
 
         [DataField("name")]
-        public readonly string ModeTitle = "????";
+        public string ModeTitle { get; } = "????";
 
         [DataField("description")]
-        public readonly string Description = string.Empty;
+        public string Description { get; } = string.Empty;
 
         [DataField("showInVote")]
-        public readonly bool ShowInVote;
+        public bool ShowInVote { get; } = false;
 
         [DataField("minPlayers")]
-        public readonly int? MinPlayers;
+        public int? MinPlayers { get; } = null;
 
         [DataField("maxPlayers")]
-        public readonly int? MaxPlayers;
+        public int? MaxPlayers { get; } = null;
 
-        [DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
+        [DataField("rules", customTypeSerializer:typeof(PrototypeIdListSerializer<GameRulePrototype>))]
         public IReadOnlyList<string> Rules { get; } = Array.Empty<string>();
     }
 }
diff --git a/Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs
deleted file mode 100644 (file)
index 7a11eb7..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace Content.Server.GameTicking.Rules.Components;
-
-/// <summary>
-///     Simple GameRule that will do a free-for-all death match.
-///     Kill everybody else to win.
-/// </summary>
-[RegisterComponent, Access(typeof(DeathMatchRuleSystem))]
-public sealed class DeathMatchRuleComponent : Component
-{
-    /// <summary>
-    /// How long until the round restarts
-    /// </summary>
-    [DataField("restartDelay"), ViewVariables(VVAccess.ReadWrite)]
-    public float RestartDelay = 10f;
-
-    /// <summary>
-    /// How long after a person dies will the restart be checked
-    /// </summary>
-    [DataField("deadCheckDelay"), ViewVariables(VVAccess.ReadWrite)]
-    public float DeadCheckDelay = 5f;
-
-    /// <summary>
-    /// A timer for checking after a death
-    /// </summary>
-    [DataField("deadCheckTimer"), ViewVariables(VVAccess.ReadWrite)]
-    public float? DeadCheckTimer;
-
-    /// <summary>
-    /// A timer for the restart.
-    /// </summary>
-    [DataField("restartTimer"), ViewVariables(VVAccess.ReadWrite)]
-    public float? RestartTimer;
-}
diff --git a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs
deleted file mode 100644 (file)
index 640b6cb..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-namespace Content.Server.GameTicking.Rules.Components;
-
-/// <summary>
-/// Component attached to all gamerule entities.
-/// Used to both track the entity as well as store basic data
-/// </summary>
-[RegisterComponent]
-public sealed class GameRuleComponent : Component
-{
-    /// <summary>
-    /// Whether or not the rule is active.
-    /// Is enabled after <see cref="GameRuleStartedEvent"/> and disabled after <see cref="GameRuleEndedEvent"/>
-    /// </summary>
-    [DataField("active")]
-    public bool Active;
-
-    /// <summary>
-    /// Whether or not the gamerule finished.
-    /// Used for tracking whether a non-active gamerule has been started before.
-    /// </summary>
-    [DataField("ended")]
-    public bool Ended;
-}
-
-/// <summary>
-/// Raised when a rule is added but hasn't formally begun yet.
-/// Good for announcing station events and other such things.
-/// </summary>
-[ByRefEvent]
-public readonly record struct GameRuleAddedEvent(EntityUid RuleEntity, string RuleId);
-
-/// <summary>
-/// Raised when the rule actually begins.
-/// Player-facing logic should begin here.
-/// </summary>
-[ByRefEvent]
-public readonly record struct GameRuleStartedEvent(EntityUid RuleEntity, string RuleId);
-
-/// <summary>
-/// Raised when the rule ends.
-/// Do cleanup and other such things here.
-/// </summary>
-[ByRefEvent]
-public readonly record struct GameRuleEndedEvent(EntityUid RuleEntity, string RuleId);
diff --git a/Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs
deleted file mode 100644 (file)
index ae9657a..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Threading;
-
-namespace Content.Server.GameTicking.Rules.Components;
-
-/// <summary>
-/// Gamerule that ends the round after a period of inactivity.
-/// </summary>
-[RegisterComponent, Access(typeof(InactivityTimeRestartRuleSystem))]
-public sealed class InactivityRuleComponent : Component
-{
-    /// <summary>
-    /// How long the round must be inactive to restart
-    /// </summary>
-    [DataField("inactivityMaxTime", required: true)]
-    public TimeSpan InactivityMaxTime = TimeSpan.FromMinutes(10);
-
-    /// <summary>
-    /// The delay between announcing round end and the lobby.
-    /// </summary>
-    [DataField("roundEndDelay", required: true)]
-    public TimeSpan RoundEndDelay  = TimeSpan.FromSeconds(10);
-
-    public CancellationTokenSource TimerCancel = new();
-}
diff --git a/Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs
deleted file mode 100644 (file)
index 45d9d38..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Threading;
-
-namespace Content.Server.GameTicking.Rules.Components;
-
-/// <summary>
-/// Configures the <see cref="InactivityTimeRestartRuleSystem"/> game rule.
-/// </summary>
-[RegisterComponent]
-public sealed class MaxTimeRestartRuleComponent : Component
-{
-    /// <summary>
-    /// The max amount of time the round can last
-    /// </summary>
-    [DataField("roundMaxTime", required: true)]
-    public TimeSpan RoundMaxTime = TimeSpan.FromMinutes(5);
-
-    /// <summary>
-    /// The amount of time between the round completing and the lobby appearing.
-    /// </summary>
-    [DataField("roundEndDelay", required: true)]
-    public TimeSpan RoundEndDelay = TimeSpan.FromSeconds(10);
-
-    public CancellationTokenSource TimerCancel = new();
-}
index c32a8569cb730ca98b7aa2f7cd12a74cd31f1096..198db1f91228d10870da264e0b5a8c5c2ede7b7d 100644 (file)
@@ -6,6 +6,7 @@ namespace Content.Server.GameTicking.Rules.Components;
 /// TODO: Remove once systems can request spawns from the ghost role system directly.
 /// </summary>
 [RegisterComponent]
+[Access(typeof(NukeopsRuleSystem))]
 public sealed class NukeOperativeSpawnerComponent : Component
 {
     [DataField("name")]
diff --git a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs
deleted file mode 100644 (file)
index 7cb748c..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Content.Server.GameTicking.Rules.Components;
-
-[RegisterComponent, Access(typeof(PiratesRuleSystem))]
-public sealed class PiratesRuleComponent : Component
-{
-    [ViewVariables]
-    public List<Mind.Mind> Pirates = new();
-    [ViewVariables]
-    public EntityUid PirateShip = EntityUid.Invalid;
-    [ViewVariables]
-    public HashSet<EntityUid> InitialItems = new();
-    [ViewVariables]
-    public double InitialShipValue;
-
-}
diff --git a/Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs
deleted file mode 100644 (file)
index 6f79b89..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.GameTicking.Rules.Components;
-
-[RegisterComponent, Access(typeof(SandboxRuleSystem))]
-public sealed class SandboxRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs
deleted file mode 100644 (file)
index f6a6726..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Content.Server.GameTicking.Rules.Components;
-
-[RegisterComponent, Access(typeof(SecretRuleSystem))]
-public sealed class SecretRuleComponent : Component
-{
-    /// <summary>
-    /// The gamerules that get added by secret.
-    /// </summary>
-    [DataField("additionalGameRules")]
-    public HashSet<EntityUid> AdditionalGameRules = new();
-}
diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
deleted file mode 100644 (file)
index 38f9921..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-using Content.Server.Traitor;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Server.Player;
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.GameTicking.Rules.Components;
-
-[RegisterComponent, Access(typeof(TraitorRuleSystem))]
-public sealed class TraitorRuleComponent : Component
-{
-    public readonly SoundSpecifier AddedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
-    public List<TraitorRole> Traitors = new();
-
-    [DataField("traitorPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
-    public string TraitorPrototypeId = "Traitor";
-
-    public int TotalTraitors => Traitors.Count;
-    public string[] Codewords = new string[3];
-
-    public enum SelectionState
-    {
-        WaitingForSpawn = 0,
-        ReadyToSelect = 1,
-        SelectionMade = 2,
-    }
-
-    public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
-    public TimeSpan AnnounceAt = TimeSpan.Zero;
-    public Dictionary<IPlayerSession, HumanoidCharacterProfile> StartCandidates = new();
-}
diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs
deleted file mode 100644 (file)
index f0b3b0c..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Content.Server.GameTicking.Rules.Components;
-
-
-[RegisterComponent, Access(typeof(ZombieRuleSystem))]
-public sealed class ZombieRuleComponent : Component
-{
-    public Dictionary<string, string> InitialInfectedNames = new();
-
-    public string PatientZeroPrototypeID = "InitialInfected";
-    public string InitialZombieVirusPrototype = "PassiveZombieVirus";
-    public const string ZombifySelfActionPrototype = "TurnUndead";
-}
diff --git a/Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs
new file mode 100644 (file)
index 0000000..600cc9d
--- /dev/null
@@ -0,0 +1,13 @@
+namespace Content.Server.GameTicking.Rules.Configurations;
+
+/// <summary>
+/// Configures a game rule, providing information like what maps to use or how long to run.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public abstract class GameRuleConfiguration
+{
+    /// <summary>
+    /// The game rule this configuration is intended for.
+    /// </summary>
+    public abstract string Id { get; }
+}
diff --git a/Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs
new file mode 100644 (file)
index 0000000..1db383c
--- /dev/null
@@ -0,0 +1,14 @@
+using JetBrains.Annotations;
+
+namespace Content.Server.GameTicking.Rules.Configurations;
+
+/// <summary>
+/// A generic configuration, for game rules that don't have special config data.
+/// </summary>
+[UsedImplicitly]
+public sealed class GenericGameRuleConfiguration : GameRuleConfiguration
+{
+    [DataField("id", required: true)]
+    private string _id = default!;
+    public override string Id => _id;
+}
diff --git a/Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs
new file mode 100644 (file)
index 0000000..bc60d77
--- /dev/null
@@ -0,0 +1,17 @@
+using JetBrains.Annotations;
+
+namespace Content.Server.GameTicking.Rules.Configurations;
+
+/// <summary>
+/// Configures the <see cref="InactivityTimeRestartRuleSystem"/> game rule.
+/// </summary>
+[UsedImplicitly]
+public sealed class InactivityGameRuleConfiguration : GameRuleConfiguration
+{
+    public override string Id => "InactivityTimeRestart"; // The value for this in the system isn't static and can't be made static. RIP.
+
+    [DataField("inactivityMaxTime", required: true)]
+    public TimeSpan InactivityMaxTime { get; }
+    [DataField("roundEndDelay", required: true)]
+    public TimeSpan RoundEndDelay { get; }
+}
diff --git a/Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs
new file mode 100644 (file)
index 0000000..d4cd3f3
--- /dev/null
@@ -0,0 +1,17 @@
+using JetBrains.Annotations;
+
+namespace Content.Server.GameTicking.Rules.Configurations;
+
+/// <summary>
+/// Configures the <see cref="InactivityTimeRestartRuleSystem"/> game rule.
+/// </summary>
+[UsedImplicitly]
+public sealed class MaxTimeRestartRuleConfiguration : GameRuleConfiguration
+{
+    public override string Id => "MaxTimeRestart"; // The value for this in the system isn't static and can't be made static. RIP.
+
+    [DataField("roundMaxTime", required: true)]
+    public TimeSpan RoundMaxTime { get; }
+    [DataField("roundEndDelay", required: true)]
+    public TimeSpan RoundEndDelay { get; }
+}
similarity index 51%
rename from Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
rename to Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs
index 3df94d9eed2789e133ff836e65e51ea4225611d2..09ccd7ad31b56d83bae83feded71e0a2bbfc48b6 100644 (file)
@@ -1,22 +1,19 @@
-using Content.Server.StationEvents.Events;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Shared.Dataset;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Roles;
-using Robust.Server.Player;
 using Robust.Shared.Audio;
-using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 using Robust.Shared.Utility;
 
-namespace Content.Server.GameTicking.Rules.Components;
+namespace Content.Server.GameTicking.Rules.Configurations;
 
-[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
-public sealed class NukeopsRuleComponent : Component
+public sealed class NukeopsRuleConfiguration : GameRuleConfiguration
 {
-    /// <summary>
-    /// The minimum needed amount of players
-    /// </summary>
+    public override string Id => "Nukeops";
+
     [DataField("minPlayers")]
     public int MinPlayers = 15;
 
@@ -41,6 +38,15 @@ public sealed class NukeopsRuleComponent : Component
     [DataField("spawnOutpost")]
     public bool SpawnOutpost = true;
 
+    /// <summary>
+    /// Whether or not loneops can spawn. Set to false if a normal nukeops round is occurring.
+    /// </summary>
+    [DataField("canLoneOpsSpawn")]
+    public bool CanLoneOpsSpawn = true;
+
+    [DataField("randomHumanoidSettings", customTypeSerializer: typeof(PrototypeIdSerializer<RandomHumanoidSettingsPrototype>))]
+    public string RandomHumanoidSettingsPrototype = "NukeOp";
+
     [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
     public string SpawnPointPrototype = "SpawnPointNukies";
 
@@ -76,86 +82,4 @@ public sealed class NukeopsRuleComponent : Component
 
     [DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))]
     public SoundSpecifier? GreetSound = new SoundPathSpecifier("/Audio/Misc/nukeops.ogg");
-
-    [DataField("winType")]
-    public WinType WinType = WinType.Neutral;
-
-    [DataField("winConditions")]
-    public List<WinCondition> WinConditions = new ();
-
-    public MapId? NukiePlanet;
-
-    // TODO: use components, don't just cache entity UIDs
-    // There have been (and probably still are) bugs where these refer to deleted entities from old rounds.
-    public EntityUid? NukieOutpost;
-    public EntityUid? NukieShuttle;
-    public EntityUid? TargetStation;
-
-    /// <summary>
-    ///     Cached starting gear prototypes.
-    /// </summary>
-    [DataField("startingGearPrototypes")]
-    public readonly Dictionary<string, StartingGearPrototype> StartingGearPrototypes = new ();
-
-    /// <summary>
-    ///     Cached operator name prototypes.
-    /// </summary>
-    [DataField("operativeNames")]
-    public readonly Dictionary<string, List<string>> OperativeNames = new();
-
-    /// <summary>
-    ///     Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
-    /// </summary>
-    [DataField("operativeMindPendingData")]
-    public readonly Dictionary<EntityUid, string> OperativeMindPendingData = new();
-
-    /// <summary>
-    ///     Players who played as an operative at some point in the round.
-    ///     Stores the session as well as the entity name
-    /// </summary>
-    /// todo: don't store sessions, dingus
-    [DataField("operativePlayers")]
-    public readonly Dictionary<string, IPlayerSession> OperativePlayers = new();
-}
-
-public enum WinType : byte
-{
-    /// <summary>
-    ///     Operative major win. This means they nuked the station.
-    /// </summary>
-    OpsMajor,
-    /// <summary>
-    ///     Minor win. All nukies were alive at the end of the round.
-    ///     Alternatively, some nukies were alive, but the disk was left behind.
-    /// </summary>
-    OpsMinor,
-    /// <summary>
-    ///     Neutral win. The nuke exploded, but on the wrong station.
-    /// </summary>
-    Neutral,
-    /// <summary>
-    ///     Crew minor win. The nuclear authentication disk escaped on the shuttle,
-    ///     but some nukies were alive.
-    /// </summary>
-    CrewMinor,
-    /// <summary>
-    ///     Crew major win. This means they either killed all nukies,
-    ///     or the bomb exploded too far away from the station, or on the nukie moon.
-    /// </summary>
-    CrewMajor
-}
-
-public enum WinCondition : byte
-{
-    NukeExplodedOnCorrectStation,
-    NukeExplodedOnNukieOutpost,
-    NukeExplodedOnIncorrectLocation,
-    NukeActiveInStation,
-    NukeActiveAtCentCom,
-    NukeDiskOnCentCom,
-    NukeDiskNotOnCentCom,
-    NukiesAbandoned,
-    AllNukiesDead,
-    SomeNukiesAlive,
-    AllNukiesAlive
 }
similarity index 69%
rename from Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs
rename to Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs
index 92a3b433757702cc56e5dae64f79fe4136c47697..f014c562804be47cc71ba890eac934c7d36a6422 100644 (file)
@@ -1,15 +1,25 @@
-using Content.Server.StationEvents.Events;
 using Content.Shared.Radio;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
 
-namespace Content.Server.StationEvents.Components;
+namespace Content.Server.GameTicking.Rules.Configurations;
 
 /// <summary>
 ///     Solar Flare event specific configuration
 /// </summary>
-[RegisterComponent, Access(typeof(SolarFlareRule))]
-public sealed class SolarFlareRuleComponent : Component
+public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfiguration
 {
+    /// <summary>
+    ///     In seconds, most early moment event can end
+    /// </summary>
+    [DataField("minEndAfter")]
+    public int MinEndAfter;
+
+    /// <summary>
+    ///     In seconds, most late moment event can end
+    /// </summary>
+    [DataField("maxEndAfter")]
+    public int MaxEndAfter;
+
     /// <summary>
     ///     If true, only headsets affected, but e.g. handheld radio will still work
     /// </summary>
@@ -33,4 +43,4 @@ public sealed class SolarFlareRuleComponent : Component
     /// </summary>
     [DataField("doorToggleChancePerSecond")]
     public float DoorToggleChancePerSecond;
-}
+}
\ No newline at end of file
similarity index 58%
rename from Content.Server/StationEvents/Components/StationEventComponent.cs
rename to Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs
index e79fd6e86d0dcc52b855926696469b75a0bd3a18..84689dc225962c9f25730c715a56ac935ef8960e 100644 (file)
@@ -1,14 +1,19 @@
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
 
-namespace Content.Server.StationEvents.Components;
+namespace Content.Server.GameTicking.Rules.Configurations;
 
 /// <summary>
-///     Defines basic data for a station event
+///     Defines a configuration for a given station event game rule, since all station events are just
+///     game rules.
 /// </summary>
-[RegisterComponent]
-public sealed class StationEventComponent : Component
+[UsedImplicitly]
+public class StationEventRuleConfiguration : GameRuleConfiguration
 {
+    [DataField("id", required: true)]
+    private string _id = default!;
+    public override string Id => _id;
+
     public const float WeightVeryLow = 0.0f;
     public const float WeightLow = 5.0f;
     public const float WeightNormal = 10.0f;
@@ -43,22 +48,16 @@ public sealed class StationEventComponent : Component
     public int ReoccurrenceDelay = 30;
 
     /// <summary>
-    ///     How long after being added does the event start
+    ///     When in the lifetime to start the event.
     /// </summary>
-    [DataField("startDelay")]
-    public TimeSpan StartDelay = TimeSpan.Zero;
+    [DataField("startAfter")]
+    public float StartAfter;
 
     /// <summary>
-    ///     How long the event lasts.
+    ///     When in the lifetime to end the event..
     /// </summary>
-    [DataField("duration")]
-    public TimeSpan Duration = TimeSpan.FromSeconds(1);
-
-    /// <summary>
-    ///     The max amount of time the event lasts.
-    /// </summary>
-    [DataField("maxDuration")]
-    public TimeSpan? MaxDuration;
+    [DataField("endAfter")]
+    public float EndAfter = float.MaxValue;
 
     /// <summary>
     ///     How many players need to be present on station for the event to run
@@ -74,16 +73,4 @@ public sealed class StationEventComponent : Component
     /// </summary>
     [DataField("maxOccurrences")]
     public int? MaxOccurrences;
-
-    /// <summary>
-    /// When the station event starts.
-    /// </summary>
-    [DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
-    public TimeSpan StartTime;
-
-    /// <summary>
-    /// When the station event starts.
-    /// </summary>
-    [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
-    public TimeSpan EndTime;
 }
index 79c3394d11a339fab7d203506995b76d7007da77..c4c61480db95bf30e77cd7a75d290c5f96d3fa77 100644 (file)
@@ -1,5 +1,5 @@
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Shared.CCVar;
 using Content.Shared.Damage;
 using Content.Shared.Mobs.Components;
@@ -11,42 +11,44 @@ using Robust.Shared.Enums;
 namespace Content.Server.GameTicking.Rules;
 
 /// <summary>
-/// Manages <see cref="DeathMatchRuleComponent"/>
+///     Simple GameRule that will do a free-for-all death match.
+///     Kill everybody else to win.
 /// </summary>
-public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponent>
+public sealed class DeathMatchRuleSystem : GameRuleSystem
 {
+    public override string Prototype => "DeathMatch";
+
     [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IChatManager _chatManager = default!;
     [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
 
+    private const float RestartDelay = 10f;
+    private const float DeadCheckDelay = 5f;
+
+    private float? _deadCheckTimer = null;
+    private float? _restartTimer = null;
+
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<DamageChangedEvent>(OnHealthChanged);
-        _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
-    }
-
-    public override void Shutdown()
-    {
-        base.Shutdown();
-        _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
     }
 
-    protected override void Started(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
         _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement"));
 
+        _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
     }
 
-    protected override void Ended(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    public override void Ended()
     {
-        base.Ended(uid, component, gameRule, args);
-
-        component.DeadCheckTimer = null;
-        component.RestartTimer = null;
+        _deadCheckTimer = null;
+        _restartTimer = null;
 
+        _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
     }
 
     private void OnHealthChanged(DamageChangedEvent _)
@@ -54,7 +56,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
         RunDelayedCheck();
     }
 
-    private void OnPlayerStatusChanged(object? ojb, SessionStatusEventArgs e)
+    private void OnPlayerStatusChanged(object? _, SessionStatusEventArgs e)
     {
         if (e.NewStatus == SessionStatus.Disconnected)
         {
@@ -64,27 +66,24 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
 
     private void RunDelayedCheck()
     {
-        var query = EntityQueryEnumerator<DeathMatchRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var deathMatch, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule) || deathMatch.DeadCheckTimer != null)
-                continue;
+        if (!RuleAdded || _deadCheckTimer != null)
+            return;
 
-            deathMatch.DeadCheckTimer = deathMatch.DeadCheckDelay;
-        }
+        _deadCheckTimer = DeadCheckDelay;
     }
 
-    protected override void ActiveTick(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, float frameTime)
+    public override void Update(float frameTime)
     {
-        base.ActiveTick(uid, component, gameRule, frameTime);
+        if (!RuleAdded)
+            return;
 
         // If the restart timer is active, that means the round is ending soon, no need to check for winners.
         // TODO: We probably want a sane, centralized round end thingie in GameTicker, RoundEndSystem is no good...
-        if (component.RestartTimer != null)
+        if (_restartTimer != null)
         {
-            component.RestartTimer -= frameTime;
+            _restartTimer -= frameTime;
 
-            if (component.RestartTimer > 0f)
+            if (_restartTimer > 0f)
                 return;
 
             GameTicker.EndRound();
@@ -92,20 +91,20 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
             return;
         }
 
-        if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || component.DeadCheckTimer == null)
+        if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || _deadCheckTimer == null)
             return;
 
-        component.DeadCheckTimer -= frameTime;
+        _deadCheckTimer -= frameTime;
 
-        if (component.DeadCheckTimer > 0)
+        if (_deadCheckTimer > 0)
             return;
 
-        component.DeadCheckTimer = null;
+        _deadCheckTimer = null;
 
         IPlayerSession? winner = null;
         foreach (var playerSession in _playerManager.ServerSessions)
         {
-            if (playerSession.AttachedEntity is not { Valid: true } playerEntity
+            if (playerSession.AttachedEntity is not {Valid: true} playerEntity
                 || !TryComp(playerEntity, out MobStateComponent? state))
                 continue;
 
@@ -121,10 +120,9 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
 
         _chatManager.DispatchServerAnnouncement(winner == null
             ? Loc.GetString("rule-death-match-check-winner-stalemate")
-            : Loc.GetString("rule-death-match-check-winner", ("winner", winner)));
+            : Loc.GetString("rule-death-match-check-winner",("winner", winner)));
 
-        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",
-            ("seconds", component.RestartDelay)));
-        component.RestartTimer = component.RestartDelay;
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", RestartDelay)));
+        _restartTimer = RestartDelay;
     }
 }
index c2b55822c8c7f75aec1a91e07397aced544a7d6a..d48eb1333a609786e7529a3a466626b180c361d1 100644 (file)
@@ -1,8 +1,8 @@
-
+using Content.Server.GameTicking.Rules.Configurations;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.GameTicking.Rules;
 
-/*
 [Prototype("gameRule")]
 public sealed class GameRulePrototype : IPrototype
 {
@@ -12,4 +12,3 @@ public sealed class GameRulePrototype : IPrototype
     [DataField("config", required: true)]
     public GameRuleConfiguration Configuration { get; } = default!;
 }
-*/
index a55189d0f2e118428f439480c1e5cc12d0485eae..47bd891db271dfe4ec6453876e09e531277e7f09 100644 (file)
@@ -1,84 +1,94 @@
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
+using JetBrains.Annotations;
 
 namespace Content.Server.GameTicking.Rules;
 
-public abstract class GameRuleSystem<T> : EntitySystem where T : Component
+[PublicAPI]
+public abstract class GameRuleSystem : EntitySystem
 {
     [Dependency] protected GameTicker GameTicker = default!;
 
+    /// <summary>
+    ///     Whether this GameRule is currently added or not.
+    ///     Be sure to check this before doing anything rule-specific.
+    /// </summary>
+    public bool RuleAdded { get; protected set; }
+
+    /// <summary>
+    ///     Whether this game rule has been started after being added.
+    ///     You probably want to check this before doing any update loop stuff.
+    /// </summary>
+    public bool RuleStarted { get; protected set; }
+
+    /// <summary>
+    ///     When the GameRule prototype with this ID is added, this system will be enabled.
+    ///     When it gets removed, this system will be disabled.
+    /// </summary>
+    public new abstract string Prototype { get; }
+
+    /// <summary>
+    ///     Holds the current configuration after the event has been added.
+    ///     This should not be getting accessed before the event is enabled, as usual.
+    /// </summary>
+    public GameRuleConfiguration Configuration = default!;
+
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
-        SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
-        SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
-    }
+        SubscribeLocalEvent<GameRuleAddedEvent>(OnGameRuleAdded);
 
-    private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
-    {
-        if (!TryComp<GameRuleComponent>(uid, out var ruleData))
-            return;
-        Added(uid, component, ruleData, args);
+        SubscribeLocalEvent<GameRuleStartedEvent>(OnGameRuleStarted);
+        SubscribeLocalEvent<GameRuleEndedEvent>(OnGameRuleEnded);
     }
 
-    private void OnGameRuleStarted(EntityUid uid, T component, ref GameRuleStartedEvent args)
+    private void OnGameRuleAdded(GameRuleAddedEvent ev)
     {
-        if (!TryComp<GameRuleComponent>(uid, out var ruleData))
+        if (ev.Rule.Configuration.Id != Prototype)
             return;
-        Started(uid, component, ruleData, args);
+
+        Configuration = ev.Rule.Configuration;
+        RuleAdded = true;
+
+        Added();
     }
 
-    private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent args)
+    private void OnGameRuleStarted(GameRuleStartedEvent ev)
     {
-        if (!TryComp<GameRuleComponent>(uid, out var ruleData))
+        if (ev.Rule.Configuration.Id != Prototype)
             return;
-        Ended(uid, component, ruleData, args);
-    }
 
-    /// <summary>
-    /// Called when the gamerule is added
-    /// </summary>
-    protected virtual void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args)
-    {
+        RuleStarted = true;
 
+        Started();
     }
 
-    /// <summary>
-    /// Called when the gamerule begins
-    /// </summary>
-    protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    private void OnGameRuleEnded(GameRuleEndedEvent ev)
     {
+        if (ev.Rule.Configuration.Id != Prototype)
+            return;
 
+        RuleAdded = false;
+        RuleStarted = false;
+        Ended();
     }
 
     /// <summary>
-    /// Called when the gamerule ends
+    ///     Called when the game rule has been added.
+    ///     You should avoid using this in favor of started--they are not the same thing.
     /// </summary>
-    protected virtual void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args)
-    {
-
-    }
+    /// <remarks>
+    ///     This is virtual because it doesn't actually have to be used, and most of the time shouldn't be.
+    /// </remarks>
+    public virtual void Added() { }
 
     /// <summary>
-    /// Called on an active gamerule entity in the Update function
+    ///     Called when the game rule has been started.
     /// </summary>
-    protected virtual void ActiveTick(EntityUid uid, T component, GameRuleComponent gameRule, float frameTime)
-    {
-
-    }
-
-    public override void Update(float frameTime)
-    {
-        base.Update(frameTime);
+    public abstract void Started();
 
-        var query = EntityQueryEnumerator<T, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var comp1, out var comp2))
-        {
-            if (!GameTicker.IsGameRuleActive(uid, comp2))
-                continue;
-
-            ActiveTick(uid, comp1, comp2, frameTime);
-        }
-    }
+    /// <summary>
+    ///     Called when the game rule has ended.
+    /// </summary>
+    public abstract void Ended();
 }
index c2e91ba4a5d01da30a11f0715b3074d6789e0eb3..d61b93d45024167ec1ba188eb67500dea544c725 100644 (file)
 using System.Threading;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Robust.Server.Player;
 using Timer = Robust.Shared.Timing.Timer;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem<InactivityRuleComponent>
+public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IChatManager _chatManager = default!;
     [Dependency] private readonly IPlayerManager _playerManager = default!;
 
+    public override string Prototype => "InactivityTimeRestart";
+
+    private CancellationTokenSource _timerCancel = new();
+
+    public TimeSpan InactivityMaxTime { get; set; } = TimeSpan.FromMinutes(10);
+    public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
+
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
-        _playerManager.PlayerStatusChanged += PlayerStatusChanged;
     }
 
-    public override void Shutdown()
+    public override void Started()
     {
-        base.Shutdown();
-        _playerManager.PlayerStatusChanged -= PlayerStatusChanged;
+        if (Configuration is not InactivityGameRuleConfiguration inactivityConfig)
+            return;
+        InactivityMaxTime = inactivityConfig.InactivityMaxTime;
+        RoundEndDelay = inactivityConfig.RoundEndDelay;
+        _playerManager.PlayerStatusChanged += PlayerStatusChanged;
     }
 
-    protected override void Ended(EntityUid uid, InactivityRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    public override void Ended()
     {
-        base.Ended(uid, component, gameRule, args);
+        _playerManager.PlayerStatusChanged -= PlayerStatusChanged;
 
-        StopTimer(uid, component);
+        StopTimer();
     }
 
-    public void RestartTimer(EntityUid uid, InactivityRuleComponent? component = null)
+    public void RestartTimer()
     {
-        if (!Resolve(uid, ref component))
-            return;
-
-        component.TimerCancel.Cancel();
-        component.TimerCancel = new CancellationTokenSource();
-        Timer.Spawn(component.InactivityMaxTime, () => TimerFired(uid, component), component.TimerCancel.Token);
+        _timerCancel.Cancel();
+        _timerCancel = new CancellationTokenSource();
+        Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token);
     }
 
-    public void StopTimer(EntityUid uid, InactivityRuleComponent? component = null)
+    public void StopTimer()
     {
-        if (!Resolve(uid, ref component))
-            return;
-
-        component.TimerCancel.Cancel();
+        _timerCancel.Cancel();
     }
 
-    private void TimerFired(EntityUid uid, InactivityRuleComponent? component = null)
+    private void TimerFired()
     {
-        if (!Resolve(uid, ref component))
-            return;
-
         GameTicker.EndRound(Loc.GetString("rule-time-has-run-out"));
 
-        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) component.RoundEndDelay.TotalSeconds)));
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) RoundEndDelay.TotalSeconds)));
 
-        Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
+        Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
     }
 
     private void RunLevelChanged(GameRunLevelChangedEvent args)
     {
-        var query = EntityQueryEnumerator<InactivityRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var inactivity, out var gameRule))
+        if (!RuleAdded)
+            return;
+
+        switch (args.New)
         {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                return;
-
-            switch (args.New)
-            {
-                case GameRunLevel.InRound:
-                    RestartTimer(uid, inactivity);
-                    break;
-                case GameRunLevel.PreRoundLobby:
-                case GameRunLevel.PostRound:
-                    StopTimer(uid, inactivity);
-                    break;
-            }
+            case GameRunLevel.InRound:
+                RestartTimer();
+                break;
+            case GameRunLevel.PreRoundLobby:
+            case GameRunLevel.PostRound:
+                StopTimer();
+                break;
         }
     }
 
     private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
     {
-        var query = EntityQueryEnumerator<InactivityRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var inactivity, out var gameRule))
+        if (GameTicker.RunLevel != GameRunLevel.InRound)
+        {
+            return;
+        }
+
+        if (_playerManager.PlayerCount == 0)
+        {
+            RestartTimer();
+        }
+        else
         {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                return;
-
-            if (GameTicker.RunLevel != GameRunLevel.InRound)
-            {
-                return;
-            }
-
-            if (_playerManager.PlayerCount == 0)
-            {
-                RestartTimer(uid, inactivity);
-            }
-            else
-            {
-                StopTimer(uid, inactivity);
-            }
+            StopTimer();
         }
     }
 }
index e792a004df5e094de65910d3bc66a3db55f31c32..6b2a5805c6a439eb0ea65ccd3949c4d18edbf31a 100644 (file)
@@ -1,14 +1,21 @@
 using System.Threading;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Timer = Robust.Shared.Timing.Timer;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRuleComponent>
+public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IChatManager _chatManager = default!;
 
+    public override string Prototype => "MaxTimeRestart";
+
+    private CancellationTokenSource _timerCancel = new();
+
+    public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromMinutes(5);
+    public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
+
     public override void Initialize()
     {
         base.Initialize();
@@ -16,60 +23,58 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRule
         SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
     }
 
-    protected override void Started(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        if (Configuration is not MaxTimeRestartRuleConfiguration maxTimeRestartConfig)
+            return;
+
+        RoundMaxTime = maxTimeRestartConfig.RoundMaxTime;
+        RoundEndDelay = maxTimeRestartConfig.RoundEndDelay;
 
         if(GameTicker.RunLevel == GameRunLevel.InRound)
-            RestartTimer(component);
+            RestartTimer();
     }
 
-    protected override void Ended(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    public override void Ended()
     {
-        base.Ended(uid, component, gameRule, args);
-
-        StopTimer(component);
+        StopTimer();
     }
 
-    public void RestartTimer(MaxTimeRestartRuleComponent component)
+    public void RestartTimer()
     {
-        component.TimerCancel.Cancel();
-        component.TimerCancel = new CancellationTokenSource();
-        Timer.Spawn(component.RoundMaxTime, () => TimerFired(component), component.TimerCancel.Token);
+        _timerCancel.Cancel();
+        _timerCancel = new CancellationTokenSource();
+        Timer.Spawn(RoundMaxTime, TimerFired, _timerCancel.Token);
     }
 
-    public void StopTimer(MaxTimeRestartRuleComponent component)
+    public void StopTimer()
     {
-        component.TimerCancel.Cancel();
+        _timerCancel.Cancel();
     }
 
-    private void TimerFired(MaxTimeRestartRuleComponent component)
+    private void TimerFired()
     {
         GameTicker.EndRound(Loc.GetString("rule-time-has-run-out"));
 
-        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) component.RoundEndDelay.TotalSeconds)));
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) RoundEndDelay.TotalSeconds)));
 
-        Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
+        Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
     }
 
     private void RunLevelChanged(GameRunLevelChangedEvent args)
     {
-        var query = EntityQueryEnumerator<MaxTimeRestartRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var timer, out var gameRule))
+        if (!RuleAdded)
+            return;
+
+        switch (args.New)
         {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                return;
-
-            switch (args.New)
-            {
-                case GameRunLevel.InRound:
-                    RestartTimer(timer);
-                    break;
-                case GameRunLevel.PreRoundLobby:
-                case GameRunLevel.PostRound:
-                    StopTimer(timer);
-                    break;
-            }
+            case GameRunLevel.InRound:
+                RestartTimer();
+                break;
+            case GameRunLevel.PreRoundLobby:
+            case GameRunLevel.PostRound:
+                StopTimer();
+                break;
         }
     }
 }
index 2d372cf3d56efee91d6fc79a118cb1854555af11..873e3c90dc4e854e3b85fae865b67634788e12e4 100644 (file)
@@ -2,6 +2,7 @@ using System.Linq;
 using Content.Server.Administration.Commands;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Ghost.Roles.Events;
 using Content.Server.Humanoid;
@@ -17,7 +18,6 @@ using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
 using Content.Server.Traitor;
 using Content.Shared.Dataset;
-using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
@@ -35,7 +35,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
+public sealed class NukeopsRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -50,9 +50,104 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
     [Dependency] private readonly StationSystem _stationSystem = default!;
     [Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
     [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+    [Dependency] private readonly GameTicker _ticker = default!;
     [Dependency] private readonly MapLoaderSystem _map = default!;
     [Dependency] private readonly ShuttleSystem _shuttle = default!;
 
+
+    private enum WinType
+    {
+        /// <summary>
+        ///     Operative major win. This means they nuked the station.
+        /// </summary>
+        OpsMajor,
+        /// <summary>
+        ///     Minor win. All nukies were alive at the end of the round.
+        ///     Alternatively, some nukies were alive, but the disk was left behind.
+        /// </summary>
+        OpsMinor,
+        /// <summary>
+        ///     Neutral win. The nuke exploded, but on the wrong station.
+        /// </summary>
+        Neutral,
+        /// <summary>
+        ///     Crew minor win. The nuclear authentication disk escaped on the shuttle,
+        ///     but some nukies were alive.
+        /// </summary>
+        CrewMinor,
+        /// <summary>
+        ///     Crew major win. This means they either killed all nukies,
+        ///     or the bomb exploded too far away from the station, or on the nukie moon.
+        /// </summary>
+        CrewMajor
+    }
+
+    private enum WinCondition
+    {
+        NukeExplodedOnCorrectStation,
+        NukeExplodedOnNukieOutpost,
+        NukeExplodedOnIncorrectLocation,
+        NukeActiveInStation,
+        NukeActiveAtCentCom,
+        NukeDiskOnCentCom,
+        NukeDiskNotOnCentCom,
+        NukiesAbandoned,
+        AllNukiesDead,
+        SomeNukiesAlive,
+        AllNukiesAlive
+    }
+
+    private WinType _winType = WinType.Neutral;
+
+    private WinType RuleWinType
+    {
+        get => _winType;
+        set
+        {
+            _winType = value;
+
+            if (value == WinType.CrewMajor || value == WinType.OpsMajor)
+            {
+                _roundEndSystem.EndRound();
+            }
+        }
+    }
+    private List<WinCondition> _winConditions = new ();
+
+    private MapId? _nukiePlanet;
+
+    // TODO: use components, don't just cache entity UIDs
+    // There have been (and probably still are) bugs where these refer to deleted entities from old rounds.
+    private EntityUid? _nukieOutpost;
+    private EntityUid? _nukieShuttle;
+    private EntityUid? _targetStation;
+
+    public override string Prototype => "Nukeops";
+
+    private NukeopsRuleConfiguration _nukeopsRuleConfig = new();
+
+    /// <summary>
+    ///     Cached starting gear prototypes.
+    /// </summary>
+    private readonly Dictionary<string, StartingGearPrototype> _startingGearPrototypes = new ();
+
+    /// <summary>
+    ///     Cached operator name prototypes.
+    /// </summary>
+    private readonly Dictionary<string, List<string>> _operativeNames = new();
+
+    /// <summary>
+    ///     Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
+    /// </summary>
+    private readonly Dictionary<EntityUid, string> _operativeMindPendingData = new();
+
+    /// <summary>
+    ///     Players who played as an operative at some point in the round.
+    ///     Stores the session as well as the entity name
+    /// </summary>
+    private readonly Dictionary<string, IPlayerSession> _operativePlayers = new();
+
+
     public override void Initialize()
     {
         base.Initialize();
@@ -72,21 +167,14 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
     private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args)
     {
-        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var ruleEnt, out var nukeops, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(ruleEnt, gameRule))
-                continue;
-
-            // If entity has a prior mind attached, add them to the players list.
-            if (!TryComp<MindComponent>(uid, out var mindComponent))
-                continue;
+        // If entity has a prior mind attached, add them to the players list.
+        if (!TryComp<MindComponent>(uid, out var mindComponent) || !RuleAdded)
+            return;
 
-            var session = mindComponent.Mind?.Session;
-            var name = MetaData(uid).EntityName;
-            if (session != null)
-                nukeops.OperativePlayers.Add(name, session);
-        }
+        var session = mindComponent.Mind?.Session;
+        var name = MetaData(uid).EntityName;
+        if (session != null)
+            _operativePlayers.Add(name, session);
     }
 
     private void OnComponentRemove(EntityUid uid, NukeOperativeComponent component, ComponentRemove args)
@@ -96,138 +184,137 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
     private void OnNukeExploded(NukeExplodedEvent ev)
     {
-        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
+       if (!RuleAdded)
+            return;
 
-            if (ev.OwningStation != null)
+        if (ev.OwningStation != null)
+        {
+            if (ev.OwningStation == _nukieOutpost)
             {
-                if (ev.OwningStation == nukeops.NukieOutpost)
-                {
-                    nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
-                    SetWinType(uid, WinType.CrewMajor, nukeops);
-                    continue;
-                }
+                _winConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
+                RuleWinType = WinType.CrewMajor;
+                return;
+            }
 
-                if (TryComp(nukeops.TargetStation, out StationDataComponent? data))
+            if (TryComp(_targetStation, out StationDataComponent? data))
+            {
+                foreach (var grid in data.Grids)
                 {
-                    var correctStation = false;
-                    foreach (var grid in data.Grids)
+                    if (grid != ev.OwningStation)
                     {
-                        if (grid != ev.OwningStation)
-                        {
-                            continue;
-                        }
-
-                        nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
-                        SetWinType(uid, WinType.OpsMajor, nukeops);
-                        correctStation = true;
+                        continue;
                     }
 
-                    if (correctStation)
-                        continue;
+                    _winConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
+                    RuleWinType = WinType.OpsMajor;
+                    return;
                 }
-
-                nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
-            }
-            else
-            {
-                nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
             }
 
-            _roundEndSystem.EndRound();
+            _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
         }
+        else
+        {
+            _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
+        }
+
+        _roundEndSystem.EndRound();
     }
 
     private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
     {
-        var query = EntityQueryEnumerator<NukeopsRuleComponent>();
-        while (query.MoveNext(out var uid, out var nukeops))
+        switch (ev.New)
         {
-            switch (ev.New)
-            {
-                case GameRunLevel.InRound:
-                    OnRoundStart(uid, nukeops);
-                    break;
-                case GameRunLevel.PostRound:
-                    OnRoundEnd(uid, nukeops);
-                    break;
-            }
+            case GameRunLevel.InRound:
+                OnRoundStart();
+                break;
+            case GameRunLevel.PostRound:
+                OnRoundEnd();
+                break;
         }
     }
 
-    /// <summary>
-    /// Loneops can only spawn if there is no nukeops active
-    /// </summary>
-    public bool CheckLoneOpsSpawn()
+    public void LoadLoneOpsConfig()
     {
-        return !EntityQuery<NukeopsRuleComponent>().Any();
+        _nukeopsRuleConfig.SpawnOutpost = false;
+        _nukeopsRuleConfig.EndsRound = false;
     }
 
-    private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null)
+    public bool CheckLoneOpsSpawn()
     {
-        if (!Resolve(uid, ref component))
-            return;
+        return _nukeopsRuleConfig.CanLoneOpsSpawn;
+    }
 
+    private void OnRoundStart()
+    {
         // 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.
 
-        component.TargetStation = _stationSystem.Stations.FirstOrNull();
+        _targetStation = _stationSystem.Stations.FirstOrNull();
 
-        if (component.TargetStation == null)
+        if (_targetStation == null)
         {
             return;
         }
 
         var filter = Filter.Empty();
-        var query = EntityQueryEnumerator<NukeOperativeComponent, ActorComponent>();
-        while (query.MoveNext(out _, out _, out var actor))
+        foreach (var nukie in EntityQuery<NukeOperativeComponent>())
         {
-            _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", component.TargetStation.Value)));
+            if (!TryComp<ActorComponent>(nukie.Owner, out var actor))
+            {
+                continue;
+            }
+
+            _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value)));
             filter.AddPlayer(actor.PlayerSession);
         }
 
-        _audioSystem.PlayGlobal(component.GreetSound, filter, recordReplay: false);
+        _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, filter, recordReplay: false);
     }
 
-    private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null)
+    private void OnRoundEnd()
     {
-        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)
+        if (RuleWinType == WinType.OpsMajor || RuleWinType == WinType.CrewMajor)
+        {
             return;
+        }
 
-        foreach (var (nuke, nukeTransform) in EntityQuery<NukeComponent, TransformComponent>(true))
+        foreach (var (nuke, nukeTransform) in EntityManager.EntityQuery<NukeComponent, TransformComponent>(true))
         {
             if (nuke.Status != NukeStatus.ARMED)
+            {
                 continue;
+            }
 
             // UH OH
             if (nukeTransform.MapID == _emergency.CentComMap)
             {
-                component.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
-                SetWinType(uid, WinType.OpsMajor, component);
+                _winConditions.Add(WinCondition.NukeActiveAtCentCom);
+                RuleWinType = WinType.OpsMajor;
                 return;
             }
 
-            if (nukeTransform.GridUid == null || component.TargetStation == null)
+            if (nukeTransform.GridUid == null || _targetStation == null)
+            {
                 continue;
+            }
 
-            if (!TryComp(component.TargetStation.Value, out StationDataComponent? data))
+            if (!TryComp(_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);
+                _winConditions.Add(WinCondition.NukeActiveInStation);
+                RuleWinType = WinType.OpsMajor;
                 return;
             }
         }
@@ -236,7 +323,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         foreach (var (_, state) in EntityQuery<NukeOperativeComponent, MobStateComponent>())
         {
             if (state.CurrentState is MobState.Alive)
+            {
                 continue;
+            }
 
             allAlive = false;
             break;
@@ -246,12 +335,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         // running away the moment nuke ops appear.
         if (allAlive)
         {
-            SetWinType(uid, WinType.OpsMinor, component);
-            component.WinConditions.Add(WinCondition.AllNukiesAlive);
+            RuleWinType = WinType.OpsMinor;
+            _winConditions.Add(WinCondition.AllNukiesAlive);
             return;
         }
 
-        component.WinConditions.Add(WinCondition.SomeNukiesAlive);
+        _winConditions.Add(WinCondition.SomeNukiesAlive);
 
         var diskAtCentCom = false;
         foreach (var (_, transform) in EntityManager.EntityQuery<NukeDiskComponent, TransformComponent>())
@@ -268,115 +357,98 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         // This also implies that some nuclear operatives have died.
         if (diskAtCentCom)
         {
-            SetWinType(uid, WinType.CrewMinor, component);
-            component.WinConditions.Add(WinCondition.NukeDiskOnCentCom);
+            RuleWinType = WinType.CrewMinor;
+            _winConditions.Add(WinCondition.NukeDiskOnCentCom);
         }
         // Otherwise, the nuke ops win.
         else
         {
-            SetWinType(uid, WinType.OpsMinor, component);
-            component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom);
+            RuleWinType = WinType.OpsMinor;
+            _winConditions.Add(WinCondition.NukeDiskNotOnCentCom);
         }
     }
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
-        foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
-        {
-            var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
+        if (!RuleAdded)
+            return;
 
-            ev.AddLine(winText);
+        var winText = Loc.GetString($"nukeops-{_winType.ToString().ToLower()}");
 
-            foreach (var cond in nukeops.WinConditions)
-            {
-                var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
+        ev.AddLine(winText);
 
-                ev.AddLine(text);
-            }
+        foreach (var cond in _winConditions)
+        {
+            var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
 
-            ev.AddLine(Loc.GetString("nukeops-list-start"));
-            foreach (var (name, session) in nukeops.OperativePlayers)
-            {
-                var listing = Loc.GetString("nukeops-list-name", ("name", name), ("user", session.Name));
-                ev.AddLine(listing);
-            }
+            ev.AddLine(text);
         }
-    }
-
-    private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
-            return;
-
-        component.WinType = type;
 
-        if (type == WinType.CrewMajor || type == WinType.OpsMajor)
-            _roundEndSystem.EndRound();
+        ev.AddLine(Loc.GetString("nukeops-list-start"));
+        foreach (var (name, session) in _operativePlayers)
+        {
+            var listing = Loc.GetString("nukeops-list-name", ("name", name), ("user", session.Name));
+            ev.AddLine(listing);
+        }
     }
 
     private void CheckRoundShouldEnd()
     {
-        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
-
-            if (!nukeops.EndsRound || 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;
+        if (!RuleAdded || !_nukeopsRuleConfig.EndsRound || RuleWinType == WinType.CrewMajor || RuleWinType == WinType.OpsMajor)
+            return;
 
-            MapId? targetStationMap = null;
-            if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
+        // If there are any nuclear bombs that are active, immediately return. We're not over yet.
+        foreach (var nuke in EntityQuery<NukeComponent>())
+        {
+            if (nuke.Status == NukeStatus.ARMED)
             {
-                var grid = data.Grids.FirstOrNull();
-                targetStationMap = grid != null
-                    ? Transform(grid.Value).MapID
-                    : null;
+                return;
             }
+        }
 
-            // 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.
+        MapId? shuttleMapId = EntityManager.EntityExists(_nukieShuttle)
+            ? Transform(_nukieShuttle!.Value).MapID
+            : null;
 
-            // 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);
+        MapId? targetStationMap = null;
+        if (_targetStation != null && TryComp(_targetStation, out StationDataComponent? data))
+        {
+            var grid = data.Grids.FirstOrNull();
+            targetStationMap = grid != null
+                ? Transform(grid.Value).MapID
+                : null;
+        }
 
-            SetWinType(uid, WinType.CrewMajor, nukeops);
+        // 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)
+            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 && shuttleMapId == _nukiePlanet)
+            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.
+        if (spawnsAvailable)
+        {
+            _winConditions.Add(WinCondition.NukiesAbandoned);
         }
+        else
+        {
+            _winConditions.Add(WinCondition.AllNukiesDead);
+        }
+
+        RuleWinType = WinType.CrewMajor;
     }
 
     private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
@@ -392,124 +464,111 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
     private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
     {
-        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
+        if (!RuleAdded)
+            return;
+
+        if (!SpawnMap())
         {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
+            Logger.InfoS("nukies", "Failed to load map for nukeops");
+            return;
+        }
+
+        // Basically copied verbatim from traitor code
+        var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative;
+        var maxOperatives = _nukeopsRuleConfig.MaxOperatives;
 
-            if (!SpawnMap(uid, nukeops))
+        var everyone = new List<IPlayerSession>(ev.PlayerPool);
+        var prefList = new List<IPlayerSession>();
+        var cmdrPrefList = new List<IPlayerSession>();
+        var operatives = new List<IPlayerSession>();
+
+        // The LINQ expression ReSharper keeps suggesting is completely unintelligible so I'm disabling it
+        // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
+        foreach (var player in everyone)
+        {
+            if (!ev.Profiles.ContainsKey(player.UserId))
             {
-                Logger.InfoS("nukies", "Failed to load map for nukeops");
                 continue;
             }
-
-            // Basically copied verbatim from traitor code
-            var playersPerOperative = nukeops.PlayersPerOperative;
-            var maxOperatives = nukeops.MaxOperatives;
-
-            var everyone = new List<IPlayerSession>(ev.PlayerPool);
-            var prefList = new List<IPlayerSession>();
-            var cmdrPrefList = new List<IPlayerSession>();
-            var operatives = new List<IPlayerSession>();
-
-            // The LINQ expression ReSharper keeps suggesting is completely unintelligible so I'm disabling it
-            // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
-            foreach (var player in everyone)
+            var profile = ev.Profiles[player.UserId];
+            if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.OperativeRoleProto))
             {
-                if (!ev.Profiles.ContainsKey(player.UserId))
-                {
-                    continue;
-                }
-
-                var profile = ev.Profiles[player.UserId];
-                if (profile.AntagPreferences.Contains(nukeops.OperativeRoleProto))
-                {
-                    prefList.Add(player);
-                }
-
-                if (profile.AntagPreferences.Contains(nukeops.CommanderRolePrototype))
-                {
-                    cmdrPrefList.Add(player);
-                }
+                prefList.Add(player);
             }
+            if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.CommanderRolePrototype))
+            {
+                cmdrPrefList.Add(player);
+            }
+        }
 
-            var numNukies = MathHelper.Clamp(ev.PlayerPool.Count / playersPerOperative, 1, maxOperatives);
+        var numNukies = MathHelper.Clamp(ev.PlayerPool.Count / playersPerOperative, 1, maxOperatives);
 
-            for (var i = 0; i < numNukies; i++)
+        for (var i = 0; i < numNukies; i++)
+        {
+            IPlayerSession nukeOp;
+            // Only one commander, so we do it at the start
+            if (i == 0)
             {
-                IPlayerSession nukeOp;
-                // Only one commander, so we do it at the start
-                if (i == 0)
-                {
-                    if (cmdrPrefList.Count == 0)
-                    {
-                        if (prefList.Count == 0)
-                        {
-                            if (everyone.Count == 0)
-                            {
-                                Logger.InfoS("preset",
-                                    "Insufficient ready players to fill up with nukeops, stopping the selection");
-                                break;
-                            }
-
-                            nukeOp = _random.PickAndTake(everyone);
-                            Logger.InfoS("preset",
-                                "Insufficient preferred nukeop commanders or nukies, picking at random.");
-                        }
-                        else
-                        {
-                            nukeOp = _random.PickAndTake(prefList);
-                            everyone.Remove(nukeOp);
-                            Logger.InfoS("preset",
-                                "Insufficient preferred nukeop commanders, picking at random from regular op list.");
-                        }
-                    }
-                    else
-                    {
-                        nukeOp = _random.PickAndTake(cmdrPrefList);
-                        everyone.Remove(nukeOp);
-                        prefList.Remove(nukeOp);
-                        Logger.InfoS("preset", "Selected a preferred nukeop commander.");
-                    }
-                }
-                else
+                if (cmdrPrefList.Count == 0)
                 {
                     if (prefList.Count == 0)
                     {
                         if (everyone.Count == 0)
                         {
-                            Logger.InfoS("preset",
-                                "Insufficient ready players to fill up with nukeops, stopping the selection");
+                            Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection");
                             break;
                         }
-
                         nukeOp = _random.PickAndTake(everyone);
-                        Logger.InfoS("preset", "Insufficient preferred nukeops, picking at random.");
+                        Logger.InfoS("preset", "Insufficient preferred nukeop commanders or nukies, picking at random.");
                     }
                     else
                     {
                         nukeOp = _random.PickAndTake(prefList);
                         everyone.Remove(nukeOp);
-                        Logger.InfoS("preset", "Selected a preferred nukeop.");
+                        Logger.InfoS("preset", "Insufficient preferred nukeop commanders, picking at random from regular op list.");
                     }
                 }
-
-                operatives.Add(nukeOp);
+                else
+                {
+                    nukeOp = _random.PickAndTake(cmdrPrefList);
+                    everyone.Remove(nukeOp);
+                    prefList.Remove(nukeOp);
+                    Logger.InfoS("preset", "Selected a preferred nukeop commander.");
+                }
             }
-
-            SpawnOperatives(numNukies, operatives, false, nukeops);
-
-            foreach (var session in operatives)
+            else
             {
-                ev.PlayerPool.Remove(session);
-                GameTicker.PlayerJoinGame(session);
-                var name = session.AttachedEntity == null
-                    ? string.Empty
-                    : MetaData(session.AttachedEntity.Value).EntityName;
-                // TODO: Fix this being able to have duplicates
-                nukeops.OperativePlayers[name] = session;
+                if (prefList.Count == 0)
+                {
+                    if (everyone.Count == 0)
+                    {
+                        Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection");
+                        break;
+                    }
+                    nukeOp = _random.PickAndTake(everyone);
+                    Logger.InfoS("preset", "Insufficient preferred nukeops, picking at random.");
+                }
+                else
+                {
+                    nukeOp = _random.PickAndTake(prefList);
+                    everyone.Remove(nukeOp);
+                    Logger.InfoS("preset", "Selected a preferred nukeop.");
+                }
             }
+            operatives.Add(nukeOp);
+        }
+
+        SpawnOperatives(numNukies, operatives, false);
+
+        foreach(var session in operatives)
+        {
+            ev.PlayerPool.Remove(session);
+            GameTicker.PlayerJoinGame(session);
+            var name = session.AttachedEntity == null
+                ? string.Empty
+                : MetaData(session.AttachedEntity.Value).EntityName;
+            // TODO: Fix this being able to have duplicates
+            _operativePlayers[name] = session;
         }
     }
 
@@ -524,13 +583,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         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.OperativeStartingGear, profile, nukeops);
+        SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile);
 
-            nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype);
-        }
+        _operativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype);
     }
 
     private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
@@ -540,51 +595,60 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
         var mind = mindComponent.Mind;
 
-        foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
+        if (_operativeMindPendingData.TryGetValue(uid, out var role) || !_nukeopsRuleConfig.SpawnOutpost || !_nukeopsRuleConfig.EndsRound)
         {
-            if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost || !nukeops.EndsRound)
-            {
-                role ??= nukeops.OperativeRoleProto;
+            if (role == null)
+                role = _nukeopsRuleConfig.OperativeRoleProto;
 
-                mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(role)));
-                nukeops.OperativeMindPendingData.Remove(uid);
-            }
+            mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(role)));
+            _operativeMindPendingData.Remove(uid);
+        }
 
-            if (!mind.TryGetSession(out var playerSession))
-                return;
-            if (nukeops.OperativePlayers.ContainsValue(playerSession))
-                return;
+        if (!mind.TryGetSession(out var playerSession))
+            return;
+        if (_operativePlayers.ContainsValue(playerSession))
+            return;
 
-            var name = MetaData(uid).EntityName;
+        var name = MetaData(uid).EntityName;
 
-            nukeops.OperativePlayers.Add(name, playerSession);
+        _operativePlayers.Add(name, playerSession);
 
-            if (GameTicker.RunLevel != GameRunLevel.InRound)
-                return;
+        if (_ticker.RunLevel != GameRunLevel.InRound)
+            return;
 
-            _audioSystem.PlayGlobal(nukeops.GreetSound, playerSession);
+        if (_nukeopsRuleConfig.GreetSound != null)
+            _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, playerSession);
 
-            if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value)))
-                _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", nukeops.TargetStation.Value)));
-        }
+        if (_targetStation != null && !string.IsNullOrEmpty(Name(_targetStation.Value)))
+            _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value)));
     }
 
-    private bool SpawnMap(EntityUid uid, NukeopsRuleComponent? component = null)
+    private bool SpawnMap()
     {
-        if (!Resolve(uid, ref component))
-            return false;
-
-        if (component.NukiePlanet != null)
+        if (_nukiePlanet != null)
             return true; // Map is already loaded.
 
-        if (!component.SpawnOutpost)
+        if (!_nukeopsRuleConfig.SpawnOutpost)
             return true;
 
-        var path = component.NukieOutpostMap;
-        var shuttlePath = component.NukieShuttleMap;
+        _nukeopsRuleConfig.CanLoneOpsSpawn = false;
+
+        var path = _nukeopsRuleConfig.NukieOutpostMap;
+        var shuttlePath = _nukeopsRuleConfig.NukieShuttleMap;
+        if (path == null)
+        {
+            Logger.ErrorS("nukies", "No station map specified for nukeops!");
+            return false;
+        }
+
+        if (shuttlePath == null)
+        {
+            Logger.ErrorS("nukies", "No shuttle map specified for nukeops!");
+            return false;
+        }
 
         var mapId = _mapManager.CreateMap();
-        var options = new MapLoadOptions
+        var options = new MapLoadOptions()
         {
             LoadMap = true,
         };
@@ -596,7 +660,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         }
 
         // Assume the first grid is the outpost grid.
-        component.NukieOutpost = outpostGrids[0];
+        _nukieOutpost = outpostGrids[0];
 
         // Listen I just don't want it to overlap.
         if (!_map.TryLoad(mapId, shuttlePath.ToString(), out var grids, new MapLoadOptions {Offset = Vector2.One*1000f}) || !grids.Any())
@@ -617,15 +681,16 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
         if (TryComp<ShuttleComponent>(shuttleId, out var shuttle))
         {
-            _shuttle.TryFTLDock(shuttleId, shuttle, component.NukieOutpost.Value);
+            _shuttle.TryFTLDock(shuttleId, shuttle, _nukieOutpost.Value);
         }
 
-        component.NukiePlanet = mapId;
-        component.NukieShuttle = shuttleId;
+        _nukiePlanet = mapId;
+        _nukieShuttle = shuttleId;
+
         return true;
     }
 
-    private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber, NukeopsRuleComponent component )
+    private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber)
     {
         string name;
         string role;
@@ -635,19 +700,19 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         switch (spawnNumber)
         {
             case 0:
-                name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(component.OperativeNames[component.EliteNames]);
-                role = component.CommanderRolePrototype;
-                gear = component.CommanderStartGearPrototype;
+                name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.EliteNames]);
+                role = _nukeopsRuleConfig.CommanderRolePrototype;
+                gear = _nukeopsRuleConfig.CommanderStartGearPrototype;
                 break;
             case 1:
-                name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]);
-                role = component.OperativeRoleProto;
-                gear = component.MedicStartGearPrototype;
+                name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]);
+                role = _nukeopsRuleConfig.OperativeRoleProto;
+                gear = _nukeopsRuleConfig.MedicStartGearPrototype;
                 break;
             default:
-                name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]);
-                role = component.OperativeRoleProto;
-                gear = component.OperativeStartGearPrototype;
+                name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]);
+                role = _nukeopsRuleConfig.OperativeRoleProto;
+                gear = _nukeopsRuleConfig.OperativeStartGearPrototype;
                 break;
         }
 
@@ -657,38 +722,38 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
     /// <summary>
     ///     Adds missing nuke operative components, equips starting gear and renames the entity.
     /// </summary>
-    private void SetupOperativeEntity(EntityUid mob, string name, string gear, HumanoidCharacterProfile? profile, NukeopsRuleComponent component)
+    private void SetupOperativeEntity(EntityUid mob, string name, string gear, HumanoidCharacterProfile? profile)
     {
         MetaData(mob).EntityName = name;
-        EnsureComp<NukeOperativeComponent>(mob);
+        EntityManager.EnsureComponent<NukeOperativeComponent>(mob);
 
         if (profile != null)
         {
             _humanoidSystem.LoadProfile(mob, profile);
         }
 
-        if (component.StartingGearPrototypes.TryGetValue(gear, out var gearPrototype))
+        if (_startingGearPrototypes.TryGetValue(gear, out var gearPrototype))
             _stationSpawningSystem.EquipStartingGear(mob, gearPrototype, profile);
 
         _faction.RemoveFaction(mob, "NanoTrasen", false);
         _faction.AddFaction(mob, "Syndicate");
     }
 
-    private void SpawnOperatives(int spawnCount, List<IPlayerSession> sessions, bool addSpawnPoints, NukeopsRuleComponent component)
+    private void SpawnOperatives(int spawnCount, List<IPlayerSession> sessions, bool addSpawnPoints)
     {
-        if (component.NukieOutpost == null)
+        if (_nukieOutpost == null)
             return;
 
-        var outpostUid = component.NukieOutpost.Value;
+        var outpostUid = _nukieOutpost.Value;
         var spawns = new List<EntityCoordinates>();
 
         // Forgive me for hardcoding prototypes
         foreach (var (_, meta, xform) in EntityManager.EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
         {
-            if (meta.EntityPrototype?.ID != component.SpawnPointPrototype)
+            if (meta.EntityPrototype?.ID != _nukeopsRuleConfig.SpawnPointPrototype)
                 continue;
 
-            if (xform.ParentUid != component.NukieOutpost)
+            if (xform.ParentUid != _nukieOutpost)
                 continue;
 
             spawns.Add(xform.Coordinates);
@@ -704,19 +769,19 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         // TODO: This should spawn the nukies in regardless and transfer if possible; rest should go to shot roles.
         for(var i = 0; i < spawnCount; i++)
         {
-            var spawnDetails = GetOperativeSpawnDetails(i, component);
+            var spawnDetails = GetOperativeSpawnDetails(i);
             var nukeOpsAntag = _prototypeManager.Index<AntagPrototype>(spawnDetails.Role);
 
             if (sessions.TryGetValue(i, out var session))
             {
                 var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
-                if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
+                if (!_prototypeManager.TryIndex(profile?.Species ?? HumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
                 {
-                    species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
+                    species = _prototypeManager.Index<SpeciesPrototype>(HumanoidAppearanceSystem.DefaultSpecies);
                 }
 
                 var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns));
-                SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component);
+                SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile);
 
                 var newMind = new Mind.Mind(session.UserId)
                 {
@@ -729,7 +794,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
             }
             else if (addSpawnPoints)
             {
-                var spawnPoint = EntityManager.SpawnEntity(component.GhostSpawnPointProto, _random.Pick(spawns));
+                var spawnPoint = EntityManager.SpawnEntity(_nukeopsRuleConfig.GhostSpawnPointProto, _random.Pick(spawns));
                 var ghostRole = EnsureComp<GhostRoleComponent>(spawnPoint);
                 EnsureComp<GhostRoleMobSpawnerComponent>(spawnPoint);
                 ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name);
@@ -743,25 +808,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         }
     }
 
-    private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
+    private void SpawnOperativesForGhostRoles()
     {
-        if (!Resolve(uid, ref component))
-            return;
-
-        if (!SpawnMap(uid, component))
+        if (!SpawnMap())
         {
             Logger.InfoS("nukies", "Failed to load map for nukeops");
             return;
         }
         // Basically copied verbatim from traitor code
-        var playersPerOperative = component.PlayersPerOperative;
-        var maxOperatives = component.MaxOperatives;
+        var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative;
+        var maxOperatives = _nukeopsRuleConfig.MaxOperatives;
 
         var playerPool = _playerSystem.ServerSessions.ToList();
         var numNukies = MathHelper.Clamp(playerPool.Count / playersPerOperative, 1, maxOperatives);
 
         var operatives = new List<IPlayerSession>();
-        SpawnOperatives(numNukies, operatives, true, component);
+        SpawnOperatives(numNukies, operatives, true);
     }
 
     //For admins forcing someone to nukeOps.
@@ -770,66 +832,77 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
         if (!mind.OwnedEntity.HasValue)
             return;
 
-        //ok hardcoded value bad but so is everything else here
-        mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>("Nukeops")));
+        mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(_nukeopsRuleConfig.OperativeRoleProto)));
         SetOutfitCommand.SetOutfit(mind.OwnedEntity.Value, "SyndicateOperativeGearFull", EntityManager);
     }
 
     private void OnStartAttempt(RoundStartAttemptEvent ev)
     {
-        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
-
-            var minPlayers = nukeops.MinPlayers;
-            if (!ev.Forced && ev.Players.Length < minPlayers)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players",
-                    ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-                ev.Cancel();
-                continue;
-            }
-
-            if (ev.Players.Length != 0)
-                continue;
+        if (!RuleAdded || Configuration is not NukeopsRuleConfiguration nukeOpsConfig)
+            return;
 
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
+        _nukeopsRuleConfig = nukeOpsConfig;
+        var minPlayers = nukeOpsConfig.MinPlayers;
+        if (!ev.Forced && ev.Players.Length < minPlayers)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
             ev.Cancel();
+            return;
         }
+
+        if (ev.Players.Length != 0)
+            return;
+
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
+        ev.Cancel();
     }
 
-    protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        RuleWinType = WinType.Neutral;
+        _winConditions.Clear();
+        _nukieOutpost = null;
+        _nukiePlanet = null;
+
+        _startingGearPrototypes.Clear();
+        _operativeNames.Clear();
+        _operativeMindPendingData.Clear();
+        _operativePlayers.Clear();
+
         // TODO: Loot table or something
         foreach (var proto in new[]
                  {
-                     component.CommanderStartGearPrototype,
-                     component.MedicStartGearPrototype,
-                     component.OperativeStartGearPrototype
+                     _nukeopsRuleConfig.CommanderStartGearPrototype,
+                     _nukeopsRuleConfig.MedicStartGearPrototype,
+                     _nukeopsRuleConfig.OperativeStartGearPrototype
                  })
         {
-            component.StartingGearPrototypes.Add(proto, _prototypeManager.Index<StartingGearPrototype>(proto));
+            _startingGearPrototypes.Add(proto, _prototypeManager.Index<StartingGearPrototype>(proto));
         }
 
-        foreach (var proto in new[] { component.EliteNames, component.NormalNames })
+        foreach (var proto in new[] { _nukeopsRuleConfig.EliteNames, _nukeopsRuleConfig.NormalNames })
         {
-            component.OperativeNames.Add(proto, new List<string>(_prototypeManager.Index<DatasetPrototype>(proto).Values));
+            _operativeNames.Add(proto, new List<string>(_prototypeManager.Index<DatasetPrototype>(proto).Values));
         }
 
         // Add pre-existing nuke operatives to the credit list.
-        var query = EntityQuery<NukeOperativeComponent, MindComponent, MetaDataComponent>(true);
-        foreach (var (_, mindComp, metaData) in query)
+        var query = EntityQuery<NukeOperativeComponent, MindComponent>(true);
+        foreach (var (_, mindComp) in query)
         {
             if (mindComp.Mind == null || !mindComp.Mind.TryGetSession(out var session))
                 continue;
-            component.OperativePlayers.Add(metaData.EntityName, session);
+            var name = MetaData(mindComp.Owner).EntityName;
+            _operativePlayers.Add(name, session);
         }
 
         if (GameTicker.RunLevel == GameRunLevel.InRound)
-            SpawnOperativesForGhostRoles(uid, component);
+            SpawnOperativesForGhostRoles();
     }
 
+    public override void Ended()
+    {
+        _nukeopsRuleConfig.EndsRound = true;
+        _nukeopsRuleConfig.SpawnOutpost = true;
+        _nukeopsRuleConfig.CanLoneOpsSpawn = true;
+    }
 }
index 6ff2429c2b87cf59877c5737d539593ee282753b..c5877ab513b1b72c0fb13345546637f37670dff3 100644 (file)
@@ -2,7 +2,6 @@ using System.Linq;
 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;
@@ -26,7 +25,7 @@ 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>
+public sealed class PiratesRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -40,6 +39,17 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
     [Dependency] private readonly MapLoaderSystem _map = default!;
     [Dependency] private readonly NamingSystem _namingSystem = default!;
 
+    [ViewVariables]
+    private List<Mind.Mind> _pirates = new();
+    [ViewVariables]
+    private EntityUid _pirateShip = EntityUid.Invalid;
+    [ViewVariables]
+    private HashSet<EntityUid> _initialItems = new();
+    [ViewVariables]
+    private double _initialShipValue;
+
+    public override string Prototype => "Pirates";
+
     /// <inheritdoc/>
     public override void Initialize()
     {
@@ -47,186 +57,178 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
 
         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();
+        if (!RuleAdded)
+            return;
 
-                var comp1 = pirates;
-                var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
-                {
-                    foreach (var mind in comp1.Pirates)
-                    {
-                        if (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;
+        if (Deleted(_pirateShip))
+        {
+            // Major loss, the ship somehow got annihilated.
+            ev.AddLine(Loc.GetString("pirates-no-ship"));
+        }
+        else
+        {
 
-                    mostValuableThefts.Add((price, uid));
-                    mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
-                    if (mostValuableThefts.Count > 5)
-                        mostValuableThefts.Pop();
-                });
+            List<(double, EntityUid)> mostValuableThefts = new();
 
-                foreach (var mind in pirates.Pirates)
+            var finalValue = _pricingSystem.AppraiseGrid(_pirateShip, uid =>
+            {
+                foreach (var mind in _pirates)
                 {
-                    if (mind.CurrentEntity is not null)
-                        finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
+                    if (mind.CurrentEntity == uid)
+                        return false; // Don't appraise the pirates twice, we count them in separately.
                 }
+                return true;
+            }, (uid, price) =>
+            {
+                if (_initialItems.Contains(uid))
+                    return;
 
-                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}")));
+                mostValuableThefts.Add((price, uid));
+                mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
+                if (mostValuableThefts.Count > 5)
+                    mostValuableThefts.Pop();
+            });
 
-                ev.AddLine("");
-                ev.AddLine(Loc.GetString("pirates-most-valuable"));
+            foreach (var mind in _pirates)
+            {
+                if (mind.CurrentEntity is not null)
+                    finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
+            }
 
-                foreach (var (price, obj) in mostValuableThefts)
-                {
-                    ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
-                }
+            var score = finalValue - _initialShipValue;
 
-                if (mostValuableThefts.Count == 0)
-                    ev.AddLine(Loc.GetString("pirates-stole-nothing"));
-            }
+            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-list-start"));
-            foreach (var pirate in pirates.Pirates)
+            ev.AddLine(Loc.GetString("pirates-most-valuable"));
+
+            foreach (var (price, obj) in mostValuableThefts)
             {
-                ev.AddLine($"- {pirate.CharacterName} ({pirate.Session?.Name})");
+                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 pirates in _pirates)
+        {
+            ev.AddLine($"- {pirates.CharacterName} ({pirates.Session?.Name})");
         }
     }
 
+    public override void Started() { }
+
+    public override void Ended() { }
+
     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 (!RuleAdded)
         {
-            // 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 IPlayerSession[numOps];
-            for (var i = 0; i < numOps; i++)
-            {
-                ops[i] = _random.PickAndTake(ev.PlayerPool);
-            }
+            return;
+        }
 
-            var map = "/Maps/Shuttles/pirate.yml";
-            var xformQuery = GetEntityQuery<TransformComponent>();
+        _pirates.Clear();
+        _initialItems.Clear();
 
-            var aabbs = _stationSystem.Stations.SelectMany(x =>
-                    Comp<StationDataComponent>(x).Grids.Select(x =>
-                        xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB)))
-                .ToArray();
+        // 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 IPlayerSession[numOps];
+        for (var i = 0; i < numOps; i++)
+        {
+            ops[i] = _random.PickAndTake(ev.PlayerPool);
+        }
 
-            var aabb = aabbs[0];
+        var map = "/Maps/Shuttles/pirate.yml";
+        var xformQuery = GetEntityQuery<TransformComponent>();
 
-            for (var i = 1; i < aabbs.Length; i++)
-            {
-                aabb.Union(aabbs[i]);
-            }
+        var aabbs = _stationSystem.Stations.SelectMany(x =>
+            Comp<StationDataComponent>(x).Grids.Select(x => xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB))).ToArray();
 
-            var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
-            {
-                Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f
-            });
+        var aabb = aabbs[0];
 
-            if (!gridId.HasValue)
-            {
-                Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting.");
-                foreach (var session in ops)
-                {
-                    ev.PlayerPool.Add(session);
-                }
+        for (var i = 1; i < aabbs.Length; i++)
+        {
+            aabb.Union(aabbs[i]);
+        }
 
-                return;
-            }
+        var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
+        {
+            Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f
+        });
 
-            pirates.PirateShip = gridId.Value;
+        if (!gridId.HasValue)
+        {
+            Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting.");
+            foreach (var session in ops)
+            {
+                ev.PlayerPool.Add(session);
+            }
+            return;
+        }
 
-            // TODO: Loot table or something
-            var pirateGear = _prototypeManager.Index<StartingGearPrototype>("PirateGear"); // YARRR
+        _pirateShip = gridId.Value;
 
-            var spawns = new List<EntityCoordinates>();
+        // TODO: Loot table or something
+        var pirateGear = _prototypeManager.Index<StartingGearPrototype>("PirateGear"); // YARRR
 
-            // Forgive me for hardcoding prototypes
-            foreach (var (_, meta, xform) in
-                     EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
-            {
-                if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != pirates.PirateShip)
-                    continue;
+        var spawns = new List<EntityCoordinates>();
 
-                spawns.Add(xform.Coordinates);
-            }
+        // Forgive me for hardcoding prototypes
+        foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
+        {
+            if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != _pirateShip) continue;
 
-            if (spawns.Count == 0)
-            {
-                spawns.Add(Transform(pirates.PirateShip).Coordinates);
-                Logger.WarningS("pirates", $"Fell back to default spawn for pirates!");
-            }
+            spawns.Add(xform.Coordinates);
+        }
 
-            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;
+        if (spawns.Count == 0)
+        {
+            spawns.Add(Transform(_pirateShip).Coordinates);
+            Logger.WarningS("pirates", $"Fell back to default spawn for pirates!");
+        }
 
-                var name = _namingSystem.GetName("Human", gender);
+        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 session = ops[i];
-                var newMind = new Mind.Mind(session.UserId)
-                {
-                    CharacterName = name
-                };
-                newMind.ChangeOwningPlayer(session.UserId);
+            var name = _namingSystem.GetName("Human", gender);
 
-                var mob = Spawn("MobHuman", _random.Pick(spawns));
-                MetaData(mob).EntityName = name;
+            var session = ops[i];
+            var newMind = new Mind.Mind(session.UserId)
+            {
+                CharacterName = name
+            };
+            newMind.ChangeOwningPlayer(session.UserId);
 
-                newMind.TransferTo(mob);
-                var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
-                _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
+            var mob = Spawn("MobHuman", _random.Pick(spawns));
+            MetaData(mob).EntityName = name;
 
-                pirates.Pirates.Add(newMind);
+            newMind.TransferTo(mob);
+            var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
+            _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
 
-                GameTicker.PlayerJoinGame(session);
-            }
+            _pirates.Add(newMind);
 
-            pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
-            {
-                pirates.InitialItems.Add(uid);
-                return true;
-            }); // Include the players in the appraisal.
+            GameTicker.PlayerJoinGame(session);
         }
+
+        _initialShipValue = _pricingSystem.AppraiseGrid(_pirateShip, uid =>
+        {
+            _initialItems.Add(uid);
+            return true;
+        }); // Include the players in the appraisal.
     }
 
     //Forcing one player to be a pirate.
@@ -239,26 +241,21 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
 
     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;
+        if (!RuleAdded)
+            return;
 
-            var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
-            if (!ev.Forced && ev.Players.Length < minPlayers)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players",
-                    ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-                ev.Cancel();
-                return;
-            }
+        var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
+        if (!ev.Forced && ev.Players.Length < minPlayers)
+        {
+            _chatManager.DispatchServerAnnouncement(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();
-            }
+        if (ev.Players.Length == 0)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
+            ev.Cancel();
         }
     }
 }
index a26a2d783c79a41a59e49cac32287b1f9a476315..1df78acd8d45505479bcacb43b60f88805cf7b8a 100644 (file)
@@ -1,21 +1,21 @@
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Server.Sandbox;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class SandboxRuleSystem : GameRuleSystem<SandboxRuleComponent>
+public sealed class SandboxRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly SandboxSystem _sandbox = default!;
 
-    protected override void Started(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override string Prototype => "Sandbox";
+
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
         _sandbox.IsSandboxEnabled = true;
     }
 
-    protected override void Ended(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    public override void Ended()
     {
-        base.Ended(uid, component, gameRule, args);
         _sandbox.IsSandboxEnabled = false;
     }
 }
index 70232923173475d017de40cc6fda3a4ab7ad2bba..4878f65ff077bc86b0ec017a9ef2f2a449d208fe 100644 (file)
@@ -1,5 +1,6 @@
+using System.Linq;
 using Content.Server.GameTicking.Presets;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Robust.Shared.Prototypes;
@@ -7,28 +8,25 @@ using Robust.Shared.Random;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
+public sealed class SecretRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly GameTicker _ticker = default!;
 
-    protected override void Started(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override string Prototype => "Secret";
+
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
-        PickRule(component);
+        PickRule();
     }
 
-    protected override void Ended(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    public override void Ended()
     {
-        base.Ended(uid, component, gameRule, args);
-
-        foreach (var rule in component.AdditionalGameRules)
-        {
-            GameTicker.EndGameRule(rule);
-        }
+        // Preset should already handle it.
     }
 
-    private void PickRule(SecretRuleComponent component)
+    private void PickRule()
     {
         // TODO: This doesn't consider what can't start due to minimum player count, but currently there's no way to know anyway.
         // as they use cvars.
@@ -37,8 +35,7 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
 
         foreach (var rule in _prototypeManager.Index<GamePresetPrototype>(preset).Rules)
         {
-            GameTicker.StartGameRule(rule, out var ruleEnt);
-            component.AdditionalGameRules.Add(ruleEnt);
+            _ticker.StartGameRule(_prototypeManager.Index<GameRulePrototype>(rule));
         }
     }
 }
diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
new file mode 100644 (file)
index 0000000..3c82367
--- /dev/null
@@ -0,0 +1,456 @@
+using System.Linq;
+using System.Threading;
+using Content.Server.Chat.Managers;
+using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.Players;
+using Content.Server.Roles;
+using Content.Server.Station.Components;
+using Content.Server.Suspicion;
+using Content.Server.Suspicion.Roles;
+using Content.Server.Traitor.Uplink;
+using Content.Shared.CCVar;
+using Content.Shared.Doors.Systems;
+using Content.Shared.EntityList;
+using Content.Shared.GameTicking;
+using Content.Shared.Maps;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Roles;
+using Content.Shared.Suspicion;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Audio;
+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.Timing;
+using Robust.Shared.Utility;
+using Timer = Robust.Shared.Timing.Timer;
+
+namespace Content.Server.GameTicking.Rules;
+
+/// <summary>
+///     Simple GameRule that will do a TTT-like gamemode with traitors.
+/// </summary>
+public sealed class SuspicionRuleSystem : GameRuleSystem
+{
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly IChatManager _chatManager = default!;
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+    [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
+    [Dependency] private readonly SharedDoorSystem _doorSystem = default!;
+    [Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
+    [Dependency] private readonly UplinkSystem _uplink = default!;
+
+    public override string Prototype => "Suspicion";
+
+    private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(1);
+
+    private readonly HashSet<SuspicionRoleComponent> _traitors = new();
+
+    public IReadOnlyCollection<SuspicionRoleComponent> Traitors => _traitors;
+
+    [DataField("addedSound")] private SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
+
+    private CancellationTokenSource _checkTimerCancel = new();
+    private TimeSpan? _endTime;
+
+    public TimeSpan? EndTime
+    {
+        get => _endTime;
+        set
+        {
+            _endTime = value;
+            SendUpdateToAll();
+        }
+    }
+
+    public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromSeconds(CCVars.SuspicionMaxTimeSeconds.DefaultValue);
+    public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
+
+    private const string TraitorID = "SuspicionTraitor";
+    private const string InnocentID = "SuspicionInnocent";
+    private const string SuspicionLootTable = "SuspicionRule";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersAssigned);
+        SubscribeLocalEvent<RoundStartAttemptEvent>(OnRoundStartAttempt);
+        SubscribeLocalEvent<RefreshLateJoinAllowedEvent>(OnLateJoinRefresh);
+        SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
+
+        SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>(OnPlayerAttached);
+        SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>(OnPlayerDetached);
+        SubscribeLocalEvent<SuspicionRoleComponent, RoleAddedEvent>(OnRoleAdded);
+        SubscribeLocalEvent<SuspicionRoleComponent, RoleRemovedEvent>(OnRoleRemoved);
+    }
+
+    private void OnRoundStartAttempt(RoundStartAttemptEvent ev)
+    {
+        if (!RuleAdded)
+            return;
+
+        var minPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers);
+
+        if (!ev.Forced && ev.Players.Length < minPlayers)
+        {
+            _chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {ev.Players.Length} players readied up out of {minPlayers} needed.");
+            ev.Cancel();
+            return;
+        }
+
+        if (ev.Players.Length == 0)
+        {
+            _chatManager.DispatchServerAnnouncement("No players readied up! Can't start Suspicion.");
+            ev.Cancel();
+        }
+    }
+
+    private void OnPlayersAssigned(RulePlayerJobsAssignedEvent ev)
+    {
+        if (!RuleAdded)
+            return;
+
+        var minTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors);
+        var playersPerTraitor = _cfg.GetCVar(CCVars.SuspicionPlayersPerTraitor);
+        var traitorStartingBalance = _cfg.GetCVar(CCVars.SuspicionStartingBalance);
+
+        var list = new List<IPlayerSession>(ev.Players);
+        var prefList = new List<IPlayerSession>();
+
+        foreach (var player in list)
+        {
+            if (!ev.Profiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached)
+            {
+                continue;
+            }
+            prefList.Add(player);
+
+            attached.EnsureComponent<SuspicionRoleComponent>();
+        }
+
+        // Max is players-1 so there's always at least one innocent.
+        var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor,
+            minTraitors, ev.Players.Length-1);
+
+        var traitors = new List<SuspicionTraitorRole>();
+
+        for (var i = 0; i < numTraitors; i++)
+        {
+            IPlayerSession traitor;
+            if(prefList.Count == 0)
+            {
+                if (list.Count == 0)
+                {
+                    Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection.");
+                    break;
+                }
+                traitor = _random.PickAndTake(list);
+                Logger.InfoS("preset", "Insufficient preferred traitors, picking at random.");
+            }
+            else
+            {
+                traitor = _random.PickAndTake(prefList);
+                list.Remove(traitor);
+                Logger.InfoS("preset", "Selected a preferred traitor.");
+            }
+            var mind = traitor.Data.ContentData()?.Mind;
+            var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorID);
+
+            DebugTools.AssertNotNull(mind?.OwnedEntity);
+
+            var traitorRole = new SuspicionTraitorRole(mind!, antagPrototype);
+            mind!.AddRole(traitorRole);
+            traitors.Add(traitorRole);
+
+            // try to place uplink
+            _uplink.AddUplink(mind.OwnedEntity!.Value, traitorStartingBalance);
+        }
+
+        foreach (var player in list)
+        {
+            var mind = player.Data.ContentData()?.Mind;
+            var antagPrototype = _prototypeManager.Index<AntagPrototype>(InnocentID);
+
+            DebugTools.AssertNotNull(mind);
+
+            mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype));
+        }
+
+        foreach (var traitor in traitors)
+        {
+            traitor.GreetSuspicion(traitors, _chatManager);
+        }
+    }
+
+    public override void Started()
+    {
+        _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
+
+        RoundMaxTime = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.SuspicionMaxTimeSeconds));
+
+        EndTime = _timing.CurTime + RoundMaxTime;
+
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-added-announcement"));
+
+        var filter = Filter.Empty()
+            .AddWhere(session => ((IPlayerSession) session).ContentData()?.Mind?.HasRole<SuspicionTraitorRole>() ?? false);
+
+        SoundSystem.Play(_addedSound.GetSound(), filter, AudioParams.Default);
+
+        _doorSystem.AccessType = SharedDoorSystem.AccessTypes.AllowAllNoExternal;
+
+        var susLoot = _prototypeManager.Index<EntityLootTablePrototype>(SuspicionLootTable);
+
+        foreach (var (_, mapGrid) in EntityManager.EntityQuery<StationMemberComponent, MapGridComponent>(true))
+        {
+            // I'm so sorry.
+            var tiles = mapGrid.GetAllTiles().ToArray();
+            Logger.Info($"TILES: {tiles.Length}");
+
+            var spawn = susLoot.GetSpawns();
+            var count = spawn.Count;
+
+            // Try to scale spawned amount by station size...
+            if (tiles.Length < 1000)
+            {
+                count = Math.Min(count, tiles.Length / 10);
+
+                // Shuffle so we pick items at random.
+                _random.Shuffle(spawn);
+            }
+
+            for (var i = 0; i < count; i++)
+            {
+                var item = spawn[i];
+
+                // Maximum number of attempts for trying to find a suitable empty tile.
+                // We do this because we don't want to hang the server when a devious map has literally no free tiles.
+                const int maxTries = 100;
+
+                for (var j = 0; j < maxTries; j++)
+                {
+                    var tile = _random.Pick(tiles);
+
+                    // Let's not spawn things on top of walls.
+                    if (tile.IsBlockedTurf(false, _lookupSystem) || tile.IsSpace(_tileDefMan))
+                        continue;
+
+                    var uid = Spawn(item, tile.GridPosition(_mapManager));
+
+                    // Keep track of all suspicion-spawned weapons so we can clean them up once the rule ends.
+                    EnsureComp<SuspicionItemComponent>(uid);
+                    break;
+                }
+            }
+        }
+
+        _checkTimerCancel = new CancellationTokenSource();
+        Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
+    }
+
+    public override void Ended()
+    {
+        _doorSystem.AccessType = SharedDoorSystem.AccessTypes.Id;
+        EndTime = null;
+        _traitors.Clear();
+
+        _playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
+
+        // Clean up all items we spawned before...
+        foreach (var item in EntityManager.EntityQuery<SuspicionItemComponent>(true))
+        {
+            Del(item.Owner);
+        }
+
+        _checkTimerCancel.Cancel();
+    }
+
+    private void CheckWinConditions()
+    {
+        if (!RuleAdded || !_cfg.GetCVar(CCVars.GameLobbyEnableWin))
+            return;
+
+        var traitorsAlive = 0;
+        var innocentsAlive = 0;
+
+        foreach (var playerSession in _playerManager.ServerSessions)
+        {
+            if (playerSession.AttachedEntity is not {Valid: true} playerEntity
+                || !TryComp(playerEntity, out MobStateComponent? mobState)
+                || !HasComp<SuspicionRoleComponent>(playerEntity))
+            {
+                continue;
+            }
+
+            if (!_mobStateSystem.IsAlive(playerEntity, mobState))
+            {
+                continue;
+            }
+
+            var mind = playerSession.ContentData()?.Mind;
+
+            if (mind != null && mind.HasRole<SuspicionTraitorRole>())
+                traitorsAlive++;
+            else
+                innocentsAlive++;
+        }
+
+        if (innocentsAlive + traitorsAlive == 0)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-stalemate"));
+            EndRound(Victory.Stalemate);
+        }
+
+        else if (traitorsAlive == 0)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-station-win"));
+            EndRound(Victory.Innocents);
+        }
+        else if (innocentsAlive == 0)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-traitor-win"));
+            EndRound(Victory.Traitors);
+        }
+        else if (_timing.CurTime > _endTime)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-traitor-time-has-run-out"));
+            EndRound(Victory.Innocents);
+        }
+    }
+
+    private enum Victory
+    {
+        Stalemate,
+        Innocents,
+        Traitors
+    }
+
+    private void EndRound(Victory victory)
+    {
+        string text;
+
+        switch (victory)
+        {
+            case Victory.Innocents:
+                text = Loc.GetString("rule-suspicion-end-round-innocents-victory");
+                break;
+            case Victory.Traitors:
+                text = Loc.GetString("rule-suspicion-end-round-traitors-victory");
+                break;
+            default:
+                text = Loc.GetString("rule-suspicion-end-round-nobody-victory");
+                break;
+        }
+
+        GameTicker.EndRound(text);
+
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int) RoundEndDelay.TotalSeconds)));
+        _checkTimerCancel.Cancel();
+
+        Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
+    }
+
+    private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+    {
+        if (e.NewStatus == SessionStatus.InGame)
+        {
+            SendUpdateTimerMessage(e.Session);
+        }
+    }
+
+    private void SendUpdateToAll()
+    {
+        foreach (var player in _playerManager.ServerSessions.Where(p => p.Status == SessionStatus.InGame))
+        {
+            SendUpdateTimerMessage(player);
+        }
+    }
+
+    private void SendUpdateTimerMessage(IPlayerSession player)
+    {
+        var msg = new SuspicionMessages.SetSuspicionEndTimerMessage
+        {
+            EndTime = EndTime
+        };
+
+        EntityManager.EntityNetManager?.SendSystemNetworkMessage(msg, player.ConnectedClient);
+    }
+
+    public void AddTraitor(SuspicionRoleComponent role)
+    {
+        if (!_traitors.Add(role))
+        {
+            return;
+        }
+
+        foreach (var traitor in _traitors)
+        {
+            traitor.AddAlly(role);
+        }
+
+        role.SetAllies(_traitors);
+    }
+
+    public void RemoveTraitor(SuspicionRoleComponent role)
+    {
+        if (!_traitors.Remove(role))
+        {
+            return;
+        }
+
+        foreach (var traitor in _traitors)
+        {
+            traitor.RemoveAlly(role);
+        }
+
+        role.ClearAllies();
+    }
+
+    private void Reset(RoundRestartCleanupEvent ev)
+    {
+        EndTime = null;
+        _traitors.Clear();
+    }
+
+    private void OnPlayerDetached(EntityUid uid, SuspicionRoleComponent component, PlayerDetachedEvent args)
+    {
+        component.SyncRoles();
+    }
+
+    private void OnPlayerAttached(EntityUid uid, SuspicionRoleComponent component, PlayerAttachedEvent args)
+    {
+        component.SyncRoles();
+    }
+
+    private void OnRoleAdded(EntityUid uid, SuspicionRoleComponent component, RoleAddedEvent args)
+    {
+        if (args.Role is not SuspicionRole role) return;
+        component.Role = role;
+    }
+
+    private void OnRoleRemoved(EntityUid uid, SuspicionRoleComponent component, RoleRemovedEvent args)
+    {
+        if (args.Role is not SuspicionRole) return;
+        component.Role = null;
+    }
+
+    private void OnLateJoinRefresh(RefreshLateJoinAllowedEvent ev)
+    {
+        if (!RuleAdded)
+            return;
+
+        ev.Disallow();
+    }
+}
diff --git a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
new file mode 100644 (file)
index 0000000..a634bb3
--- /dev/null
@@ -0,0 +1,276 @@
+using System.Linq;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Chat.Managers;
+using Content.Server.PDA;
+using Content.Server.Players;
+using Content.Server.Spawners.Components;
+using Content.Server.Store.Components;
+using Content.Server.Traitor;
+using Content.Server.Traitor.Uplink;
+using Content.Server.TraitorDeathMatch.Components;
+using Content.Shared.CCVar;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Hands.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.PDA;
+using Content.Shared.Roles;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules;
+
+public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
+{
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly IChatManager _chatManager = default!;
+    [Dependency] private readonly IRobustRandom _robustRandom = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly MaxTimeRestartRuleSystem _restarter = default!;
+    [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+    [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+    [Dependency] private readonly TransformSystem _transformSystem = default!;
+    [Dependency] private readonly UplinkSystem _uplink = default!;
+
+    public override string Prototype => "TraitorDeathMatch";
+
+    public string PDAPrototypeName => "CaptainPDA";
+    public string BeltPrototypeName => "ClothingBeltJanitorFilled";
+    public string BackpackPrototypeName => "ClothingBackpackFilled";
+
+    private bool _safeToEndRound = false;
+
+    private readonly Dictionary<EntityUid, string> _allOriginalNames = new();
+
+    private const string TraitorPrototypeID = "Traitor";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
+        SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawned);
+        SubscribeLocalEvent<GhostAttemptHandleEvent>(OnGhostAttempt);
+    }
+
+    private void OnPlayerSpawned(PlayerSpawnCompleteEvent ev)
+    {
+        if (!RuleAdded)
+            return;
+
+        var session = ev.Player;
+        var startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance);
+
+        // Yup, they're a traitor
+        var mind = session.Data.ContentData()?.Mind;
+        if (mind == null)
+        {
+            Logger.ErrorS("preset", "Failed getting mind for TDM player.");
+            return;
+        }
+
+        var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);
+        var traitorRole = new TraitorRole(mind, antagPrototype);
+        mind.AddRole(traitorRole);
+
+        // Delete anything that may contain "dangerous" role-specific items.
+        // (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.)
+        if (mind.OwnedEntity is {Valid: true} owned)
+        {
+            var victimSlots = new[] {"id", "belt", "back"};
+            foreach (var slot in victimSlots)
+            {
+                if(_inventory.TryUnequip(owned, slot, out var entityUid, true, true))
+                    Del(entityUid.Value);
+            }
+
+            // Replace their items:
+
+            var ownedCoords = Transform(owned).Coordinates;
+
+            //  pda
+            var newPDA = Spawn(PDAPrototypeName, ownedCoords);
+            _inventory.TryEquip(owned, newPDA, "id", true);
+
+            //  belt
+            var newTmp = Spawn(BeltPrototypeName, ownedCoords);
+            _inventory.TryEquip(owned, newTmp, "belt", true);
+
+            //  backpack
+            newTmp = Spawn(BackpackPrototypeName, ownedCoords);
+            _inventory.TryEquip(owned, newTmp, "back", true);
+
+            if (!_uplink.AddUplink(owned, startingBalance))
+                return;
+
+            _allOriginalNames[owned] = Name(owned);
+
+            // The PDA needs to be marked with the correct owner.
+            var pda = Comp<PDAComponent>(newPDA);
+            EntityManager.EntitySysManager.GetEntitySystem<PDASystem>().SetOwner(pda, Name(owned));
+            EntityManager.AddComponent<TraitorDeathMatchReliableOwnerTagComponent>(newPDA).UserId = mind.UserId;
+        }
+
+        // Finally, it would be preferable if they spawned as far away from other players as reasonably possible.
+        if (mind.OwnedEntity != null && FindAnyIsolatedSpawnLocation(mind, out var bestTarget))
+        {
+            Transform(mind.OwnedEntity.Value).Coordinates = bestTarget;
+        }
+        else
+        {
+            // The station is too drained of air to safely continue.
+            if (_safeToEndRound)
+            {
+                _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-death-match-station-is-too-unsafe-announcement"));
+                _restarter.RoundMaxTime = TimeSpan.FromMinutes(1);
+                _restarter.RestartTimer();
+                _safeToEndRound = false;
+            }
+        }
+    }
+
+    private void OnGhostAttempt(GhostAttemptHandleEvent ev)
+    {
+        if (!RuleAdded || ev.Handled)
+            return;
+
+        ev.Handled = true;
+
+        var mind = ev.Mind;
+
+        if (mind.OwnedEntity is {Valid: true} entity && TryComp(entity, out MobStateComponent? mobState))
+        {
+            if (_mobStateSystem.IsCritical(entity, mobState))
+            {
+                // TODO BODY SYSTEM KILL
+                var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100);
+                Get<DamageableSystem>().TryChangeDamage(entity, damage, true);
+            }
+            else if (!_mobStateSystem.IsDead(entity,mobState))
+            {
+                if (HasComp<HandsComponent>(entity))
+                {
+                    ev.Result = false;
+                    return;
+                }
+            }
+        }
+        var session = mind.Session;
+        if (session == null)
+        {
+            ev.Result = false;
+            return;
+        }
+
+        GameTicker.Respawn(session);
+        ev.Result = true;
+    }
+
+    private void OnRoundEndText(RoundEndTextAppendEvent ev)
+    {
+        if (!RuleAdded)
+            return;
+
+        var lines = new List<string>();
+        lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line"));
+
+        foreach (var uplink in EntityManager.EntityQuery<StoreComponent>(true))
+        {
+            var owner = uplink.AccountOwner;
+            if (owner != null && _allOriginalNames.ContainsKey(owner.Value))
+            {
+                var tcbalance = _uplink.GetTCBalance(uplink);
+
+                lines.Add(Loc.GetString("traitor-death-match-end-round-description-entry",
+                    ("originalName", _allOriginalNames[owner.Value]),
+                    ("tcBalance", tcbalance)));
+            }
+        }
+
+        ev.AddLine(string.Join('\n', lines));
+    }
+
+    public override void Started()
+    {
+        _restarter.RoundMaxTime = TimeSpan.FromMinutes(30);
+        _restarter.RestartTimer();
+        _safeToEndRound = true;
+    }
+
+    public override void Ended()
+    {
+    }
+
+    // It would be nice if this function were moved to some generic helpers class.
+    private bool FindAnyIsolatedSpawnLocation(Mind.Mind ignoreMe, out EntityCoordinates bestTarget)
+    {
+        // Collate people to avoid...
+        var existingPlayerPoints = new List<EntityCoordinates>();
+        foreach (var player in _playerManager.ServerSessions)
+        {
+            var avoidMeMind = player.Data.ContentData()?.Mind;
+            if ((avoidMeMind == null) || (avoidMeMind == ignoreMe))
+                continue;
+            var avoidMeEntity = avoidMeMind.OwnedEntity;
+            if (avoidMeEntity == null)
+                continue;
+            if (TryComp(avoidMeEntity.Value, out MobStateComponent? mobState))
+            {
+                // Does have mob state component; if critical or dead, they don't really matter for spawn checks
+                if (_mobStateSystem.IsCritical(avoidMeEntity.Value, mobState) || _mobStateSystem.IsDead(avoidMeEntity.Value, mobState))
+                    continue;
+            }
+            else
+            {
+                // Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid.
+                continue;
+            }
+            existingPlayerPoints.Add(Transform(avoidMeEntity.Value).Coordinates);
+        }
+
+        // Iterate over each possible spawn point, comparing to the existing player points.
+        // On failure, the returned target is the location that we're already at.
+        var bestTargetDistanceFromNearest = -1.0f;
+        // Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably
+        var ents = EntityManager.EntityQuery<SpawnPointComponent>().Select(x => x.Owner).ToList();
+        _robustRandom.Shuffle(ents);
+        var foundATarget = false;
+        bestTarget = EntityCoordinates.Invalid;
+
+        foreach (var entity in ents)
+        {
+            var transform = Transform(entity);
+
+            if (transform.GridUid == null || transform.MapUid == null)
+                continue;
+
+            var position = _transformSystem.GetGridOrMapTilePosition(entity, transform);
+
+            if (!_atmosphereSystem.IsTileMixtureProbablySafe(transform.GridUid.Value, transform.MapUid.Value, position))
+                continue;
+
+            var distanceFromNearest = float.PositiveInfinity;
+            foreach (var existing in existingPlayerPoints)
+            {
+                if (Transform(entity).Coordinates.TryDistance(EntityManager, existing, out var dist))
+                    distanceFromNearest = Math.Min(distanceFromNearest, dist);
+            }
+            if (bestTargetDistanceFromNearest < distanceFromNearest)
+            {
+                bestTarget = Transform(entity).Coordinates;
+                bestTargetDistanceFromNearest = distanceFromNearest;
+                foundATarget = true;
+            }
+        }
+        return foundATarget;
+    }
+
+}
index bb6bb89f2ac94137ced9d0757a4837c7a77821c7..9edfe80d20e7446a2114d9839bd7f72ab64f0376 100644 (file)
@@ -1,6 +1,5 @@
 using System.Linq;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.NPC.Systems;
 using Content.Server.Objectives.Interfaces;
 using Content.Server.PDA.Ringer;
@@ -25,7 +24,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
+public sealed class TraitorRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -33,6 +32,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
     [Dependency] private readonly IObjectivesManager _objectivesManager = default!;
     [Dependency] private readonly IChatManager _chatManager = default!;
     [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly GameTicker _gameTicker = default!;
     [Dependency] private readonly FactionSystem _faction = default!;
     [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
     [Dependency] private readonly UplinkSystem _uplink = default!;
@@ -40,8 +40,30 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
 
     private ISawmill _sawmill = default!;
 
-    private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
-    private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
+    public override string Prototype => "Traitor";
+
+    private readonly SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
+    public List<TraitorRole> Traitors = new();
+
+    private const string TraitorPrototypeID = "Traitor";
+    private const string TraitorUplinkPresetId = "StorePresetUplink";
+
+    public int TotalTraitors => Traitors.Count;
+    public string[] Codewords = new string[3];
+
+    private int _playersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
+    private int _maxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
+
+    public enum SelectionState
+    {
+        WaitingForSpawn = 0,
+        ReadyToSelect = 1,
+        SelectionMade = 2,
+    }
+
+    public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
+    private TimeSpan _announceAt = TimeSpan.Zero;
+    private Dictionary<IPlayerSession, HumanoidCharacterProfile> _startCandidates = new();
 
     public override void Initialize()
     {
@@ -55,101 +77,101 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
     }
 
-    protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
+    public override void Update(float frameTime)
     {
-        base.ActiveTick(uid, component, gameRule, frameTime);
+        base.Update(frameTime);
 
-        if (component.SelectionStatus == TraitorRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt)
-            DoTraitorStart(component);
+        if (SelectionStatus == SelectionState.ReadyToSelect && _gameTiming.CurTime >= _announceAt)
+            DoTraitorStart();
     }
 
-    private void OnStartAttempt(RoundStartAttemptEvent ev)
+    public override void Started(){}
+
+    public override void Ended()
     {
-        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
+        Traitors.Clear();
+        _startCandidates.Clear();
+        SelectionStatus = SelectionState.WaitingForSpawn;
+    }
 
-            MakeCodewords(traitor);
+    private void OnStartAttempt(RoundStartAttemptEvent ev)
+    {
+        MakeCodewords();
+        if (!RuleAdded)
+            return;
 
-            var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
-            if (!ev.Forced && ev.Players.Length < minPlayers)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players",
-                    ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-                ev.Cancel();
-                continue;
-            }
+        var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
+        if (!ev.Forced && ev.Players.Length < minPlayers)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
+            ev.Cancel();
+            return;
+        }
 
-            if (ev.Players.Length == 0)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
-                ev.Cancel();
-            }
+        if (ev.Players.Length == 0)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
+            ev.Cancel();
         }
     }
 
-    private void MakeCodewords(TraitorRuleComponent component)
+    private void MakeCodewords()
     {
         var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
         var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
         var verbs = _prototypeManager.Index<DatasetPrototype>("verbs").Values;
         var codewordPool = adjectives.Concat(verbs).ToList();
         var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
-        component.Codewords = new string[finalCodewordCount];
+        Codewords = new string[finalCodewordCount];
         for (var i = 0; i < finalCodewordCount; i++)
         {
-            component.Codewords[i] = _random.PickAndTake(codewordPool);
+            Codewords[i] = _random.PickAndTake(codewordPool);
         }
     }
 
-    private void DoTraitorStart(TraitorRuleComponent component)
+    private void DoTraitorStart()
     {
-        if (!component.StartCandidates.Any())
+        if (!_startCandidates.Any())
         {
             _sawmill.Error("Tried to start Traitor mode without any candidates.");
             return;
         }
 
-        var numTraitors = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerTraitor, 1, MaxTraitors);
-        var traitorPool = FindPotentialTraitors(component.StartCandidates, component);
+        var numTraitors = MathHelper.Clamp(_startCandidates.Count / _playersPerTraitor, 1, _maxTraitors);
+        var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
+
+        var traitorPool = FindPotentialTraitors(_startCandidates);
         var selectedTraitors = PickTraitors(numTraitors, traitorPool);
 
         foreach (var traitor in selectedTraitors)
-        {
             MakeTraitor(traitor);
-        }
 
-        component.SelectionStatus = TraitorRuleComponent.SelectionState.SelectionMade;
+        SelectionStatus = SelectionState.SelectionMade;
     }
 
     private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
     {
-        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
+        if (!RuleAdded)
+            return;
+
+        foreach (var player in ev.Players)
         {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+            if (!ev.Profiles.ContainsKey(player.UserId))
                 continue;
-            foreach (var player in ev.Players)
-            {
-                if (!ev.Profiles.ContainsKey(player.UserId))
-                    continue;
 
-                traitor.StartCandidates[player] = ev.Profiles[player.UserId];
-            }
+            _startCandidates[player] = ev.Profiles[player.UserId];
+        }
 
-            var delay = TimeSpan.FromSeconds(
-                _cfg.GetCVar(CCVars.TraitorStartDelay) +
-                _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
+        var delay = TimeSpan.FromSeconds(
+            _cfg.GetCVar(CCVars.TraitorStartDelay) +
+            _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance)));
 
-            traitor.AnnounceAt = _gameTiming.CurTime + delay;
+        _announceAt = _gameTiming.CurTime + delay;
 
-            traitor.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToSelect;
-        }
+        SelectionStatus = SelectionState.ReadyToSelect;
     }
 
-    public List<IPlayerSession> FindPotentialTraitors(in Dictionary<IPlayerSession, HumanoidCharacterProfile> candidates, TraitorRuleComponent component)
+    public List<IPlayerSession> FindPotentialTraitors(in Dictionary<IPlayerSession, HumanoidCharacterProfile> candidates)
     {
         var list = new List<IPlayerSession>();
         var pendingQuery = GetEntityQuery<PendingClockInComponent>();
@@ -174,7 +196,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         foreach (var player in list)
         {
             var profile = candidates[player];
-            if (profile.AntagPreferences.Contains(component.TraitorPrototypeId))
+            if (profile.AntagPreferences.Contains(TraitorPrototypeID))
             {
                 prefList.Add(player);
             }
@@ -206,14 +228,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
 
     public bool MakeTraitor(IPlayerSession traitor)
     {
-        var traitorRule = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
-        if (traitorRule == null)
-        {
-            //todo fuck me this shit is awful
-            GameTicker.StartGameRule("traitor", out var ruleEntity);
-            traitorRule = EntityManager.GetComponent<TraitorRuleComponent>(ruleEntity);
-        }
-
         var mind = traitor.Data.ContentData()?.Mind;
         if (mind == null)
         {
@@ -240,15 +254,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         if (pda == null || !_uplink.AddUplink(mind.OwnedEntity.Value, startingBalance))
             return false;
 
-
         // add the ringtone uplink and get its code for greeting
         var code = AddComp<RingerUplinkComponent>(pda.Value).Code;
 
-        var antagPrototype = _prototypeManager.Index<AntagPrototype>(traitorRule.TraitorPrototypeId);
+        var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorPrototypeID);
         var traitorRole = new TraitorRole(mind, antagPrototype);
         mind.AddRole(traitorRole);
-        traitorRule.Traitors.Add(traitorRole);
-        traitorRole.GreetTraitor(traitorRule.Codewords, code);
+        Traitors.Add(traitorRole);
+        traitorRole.GreetTraitor(Codewords, code);
 
         _faction.RemoveFaction(entity, "NanoTrasen", false);
         _faction.AddFaction(entity, "Syndicate");
@@ -267,173 +280,147 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         }
 
         //give traitors their codewords and uplink code to keep in their character info menu
-        traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", traitorRule.Codewords)))
+        traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", Codewords)))
             + "\n" + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("", code)));
 
-        _audioSystem.PlayGlobal(traitorRule.AddedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default);
+        _audioSystem.PlayGlobal(_addedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default);
         return true;
     }
 
     private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
     {
-        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
-
-            if (traitor.TotalTraitors >= MaxTraitors)
-                continue;
-            if (!ev.LateJoin)
-                continue;
-            if (!ev.Profile.AntagPreferences.Contains(traitor.TraitorPrototypeId))
-                continue;
+        if (!RuleAdded)
+            return;
+        if (TotalTraitors >= _maxTraitors)
+            return;
+        if (!ev.LateJoin)
+            return;
+        if (!ev.Profile.AntagPreferences.Contains(TraitorPrototypeID))
+            return;
 
-            if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
-                continue;
 
-            if (!job.CanBeAntag)
-                continue;
+        if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
+            return;
 
-            // Before the announcement is made, late-joiners are considered the same as players who readied.
-            if (traitor.SelectionStatus < TraitorRuleComponent.SelectionState.SelectionMade)
-            {
-                traitor.StartCandidates[ev.Player] = ev.Profile;
-                continue;
-            }
+        if (!job.CanBeAntag)
+            return;
 
-            // the nth player we adjust our probabilities around
-            var target = PlayersPerTraitor * traitor.TotalTraitors + 1;
+        // Before the announcement is made, late-joiners are considered the same as players who readied.
+        if (SelectionStatus < SelectionState.SelectionMade)
+        {
+            _startCandidates[ev.Player] = ev.Profile;
+            return;
+        }
 
-            var chance = 1f / PlayersPerTraitor;
+        // the nth player we adjust our probabilities around
+        int target = ((_playersPerTraitor * TotalTraitors) + 1);
 
-            // 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);
-            }
+        float chance = (1f / _playersPerTraitor);
 
-            if (chance > 1)
-                chance = 1;
+        // 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.Player);
-            }
+        // 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.Player);
         }
     }
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
-        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
-
-            var result = Loc.GetString("traitor-round-end-result", ("traitorCount", traitor.Traitors.Count));
+        if (!RuleAdded)
+            return;
 
-            result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", traitor.Codewords))) +
-                      "\n";
+        var result = Loc.GetString("traitor-round-end-result", ("traitorCount", Traitors.Count));
 
-            foreach (var t in traitor.Traitors)
-            {
-                var name = t.Mind.CharacterName;
-                t.Mind.TryGetSession(out var session);
-                var username = session?.Name;
+        result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", Codewords))) + "\n";
 
-                var objectives = t.Mind.AllObjectives.ToArray();
-                if (objectives.Length == 0)
-                {
-                    if (username != null)
-                    {
-                        if (name == null)
-                            result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username));
-                        else
-                            result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username),
-                                ("name", name));
-                    }
-                    else if (name != null)
-                        result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name));
-
-                    continue;
-                }
+        foreach (var traitor in Traitors)
+        {
+            var name = traitor.Mind.CharacterName;
+            traitor.Mind.TryGetSession(out var session);
+            var username = session?.Name;
 
+            var objectives = traitor.Mind.AllObjectives.ToArray();
+            if (objectives.Length == 0)
+            {
                 if (username != null)
                 {
                     if (name == null)
-                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives",
-                            ("user", username));
+                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username));
                     else
-                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named",
-                            ("user", username), ("name", name));
+                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name));
                 }
                 else if (name != null)
-                    result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name));
+                    result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name));
 
-                foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
-                {
-                    result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}");
+                continue;
+            }
+
+            if (username != null)
+            {
+                if (name == null)
+                    result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives", ("user", username));
+                else
+                    result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", ("user", username), ("name", name));
+            }
+            else if (name != null)
+                result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name));
+
+            foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
+            {
+                result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}");
 
-                    foreach (var objective in objectiveGroup)
+                foreach (var objective in objectiveGroup)
+                {
+                    foreach (var condition in objective.Conditions)
                     {
-                        foreach (var condition in objective.Conditions)
+                        var progress = condition.Progress;
+                        if (progress > 0.99f)
+                        {
+                            result += "\n- " + Loc.GetString(
+                                "traitor-objective-condition-success",
+                                ("condition", condition.Title),
+                                ("markupColor", "green")
+                            );
+                        }
+                        else
                         {
-                            var progress = condition.Progress;
-                            if (progress > 0.99f)
-                            {
-                                result += "\n- " + Loc.GetString(
-                                    "traitor-objective-condition-success",
-                                    ("condition", condition.Title),
-                                    ("markupColor", "green")
-                                );
-                            }
-                            else
-                            {
-                                result += "\n- " + Loc.GetString(
-                                    "traitor-objective-condition-fail",
-                                    ("condition", condition.Title),
-                                    ("progress", (int) (progress * 100)),
-                                    ("markupColor", "red")
-                                );
-                            }
+                            result += "\n- " + Loc.GetString(
+                                "traitor-objective-condition-fail",
+                                ("condition", condition.Title),
+                                ("progress", (int) (progress * 100)),
+                                ("markupColor", "red")
+                            );
                         }
                     }
                 }
             }
-
-            ev.AddLine(result);
         }
+        ev.AddLine(result);
     }
 
-    public List<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind)
+    public IEnumerable<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind)
     {
-        List<TraitorRole> allTraitors = new();
-        foreach (var traitor in EntityQuery<TraitorRuleComponent>())
-        {
-            foreach (var role in GetOtherTraitorsAliveAndConnected(ourMind, traitor))
-            {
-                if (!allTraitors.Contains(role))
-                    allTraitors.Add(role);
-            }
-        }
+        var traitors = Traitors;
+        List<TraitorRole> removeList = new();
 
-        return allTraitors;
-    }
-
-    public List<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind, TraitorRuleComponent component)
-    {
-        return component.Traitors // don't want
+        return Traitors // don't want
+            .Where(t => t.Mind is not null) // no mind
             .Where(t => t.Mind.OwnedEntity is not null) // no entity
             .Where(t => t.Mind.Session is not null) // player disconnected
             .Where(t => t.Mind != ourMind) // ourselves
             .Where(t => _mobStateSystem.IsAlive((EntityUid) t.Mind.OwnedEntity!)) // dead
-            .Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity).ToList(); // not in original body
+            .Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity); // not in original body
     }
 }
index 4ac83b354e61e37ad5333cc9fc9e665ae398f785..f51a22b42d2882e6116780f62547d7ee43bc5005 100644 (file)
@@ -4,7 +4,7 @@ using Content.Server.Actions;
 using Content.Server.Chat.Managers;
 using Content.Server.Disease;
 using Content.Server.Disease.Components;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Humanoid;
 using Content.Server.Mind.Components;
 using Content.Server.Players;
 using Content.Server.Popups;
@@ -29,7 +29,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
+public sealed class ZombieRuleSystem : GameRuleSystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -44,6 +44,14 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
 
+    private Dictionary<string, string> _initialInfectedNames = new();
+
+    public override string Prototype => "Zombie";
+
+    private const string PatientZeroPrototypeID = "InitialInfected";
+    private const string InitialZombieVirusPrototype = "PassiveZombieVirus";
+    private const string ZombifySelfActionPrototype = "TurnUndead";
+
     public override void Initialize()
     {
         base.Initialize();
@@ -59,61 +67,60 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
-        foreach (var zombie in EntityQuery<ZombieRuleComponent>())
-        {
-            //this is just the general condition thing used for determining the win/lose text
-            var percent = GetInfectedPercentage(out var livingHumans);
-
-            if (percent <= 0)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
-            else if (percent <= 0.25)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
-            else if (percent <= 0.5)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
-            else if (percent < 1)
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
-            else
-                ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
+        if (!RuleAdded)
+            return;
 
-            ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count)));
-            foreach (var player in zombie.InitialInfectedNames)
-            {
-                ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
-                    ("name", player.Key),
-                    ("username", player.Value)));
-            }
+        //this is just the general condition thing used for determining the win/lose text
+        var percent = GetInfectedPercentage(out var livingHumans);
+
+        if (percent <= 0)
+            ev.AddLine(Loc.GetString("zombie-round-end-amount-none"));
+        else if (percent <= 0.25)
+            ev.AddLine(Loc.GetString("zombie-round-end-amount-low"));
+        else if (percent <= 0.5)
+            ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
+        else if (percent < 1)
+            ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((percent * 100), 2).ToString(CultureInfo.InvariantCulture))));
+        else
+            ev.AddLine(Loc.GetString("zombie-round-end-amount-all"));
+
+        ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", _initialInfectedNames.Count)));
+        foreach (var player in _initialInfectedNames)
+        {
+            ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial",
+                ("name", player.Key),
+                ("username", player.Value)));
+        }
 
-            //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 (livingHumans.Count > 0 && livingHumans.Count <= zombie.InitialInfectedNames.Count)
+        //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 (livingHumans.Count > 0 && livingHumans.Count <= _initialInfectedNames.Count)
+        {
+            ev.AddLine("");
+            ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", livingHumans.Count)));
+            foreach (var survivor in livingHumans)
             {
-                ev.AddLine("");
-                ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", livingHumans.Count)));
-                foreach (var survivor in livingHumans)
-                {
-                    var meta = MetaData(survivor);
-                    var username = string.Empty;
-                    if (TryComp<MindComponent>(survivor, out var mindcomp))
-                        if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
-                            username = mindcomp.Mind.Session.Name;
-
-                    ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
-                        ("name", meta.EntityName),
-                        ("username", username)));
-                }
+                var meta = MetaData(survivor);
+                var username = string.Empty;
+                if (TryComp<MindComponent>(survivor, out var mindcomp))
+                    if (mindcomp.Mind != null && mindcomp.Mind.Session != null)
+                        username = mindcomp.Mind.Session.Name;
+
+                ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor",
+                    ("name", meta.EntityName),
+                    ("username", username)));
             }
         }
     }
 
     private void OnJobAssigned(RulePlayerJobsAssignedEvent ev)
     {
-        var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var zombies, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
-            InfectInitialPlayers(zombies);
-        }
+        if (!RuleAdded)
+            return;
+
+        _initialInfectedNames = new();
+
+        InfectInitialPlayers();
     }
 
     /// <remarks>
@@ -122,11 +129,15 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
     /// </remarks>
     private void OnMobStateChanged(MobStateChangedEvent ev)
     {
+        if (!RuleAdded)
+            return;
         CheckRoundEnd(ev.Target);
     }
 
     private void OnEntityZombified(EntityZombifiedEvent ev)
     {
+        if (!RuleAdded)
+            return;
         CheckRoundEnd(ev.Target);
     }
 
@@ -136,59 +147,50 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
     /// <param name="target">depending on this uid, we should care about the round ending</param>
     private void CheckRoundEnd(EntityUid target)
     {
-        var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var zombies, out var gameRule))
-        {
-            if (GameTicker.IsGameRuleActive(uid, gameRule))
-                continue;
-
-            //we only care about players, not monkeys and such.
-            if (!HasComp<HumanoidAppearanceComponent>(target))
-                continue;
+        //we only care about players, not monkeys and such.
+        if (!HasComp<HumanoidAppearanceComponent>(target))
+            return;
 
-            var percent = GetInfectedPercentage(out var num);
-            if (num.Count == 1) //only one human left. spooky
-                _popup.PopupEntity(Loc.GetString("zombie-alone"), num[0], num[0]);
-            if (percent >= 1) //oops, all zombies
-                _roundEndSystem.EndRound();
-        }
+        var percent = GetInfectedPercentage(out var num);
+        if (num.Count == 1) //only one human left. spooky
+           _popup.PopupEntity(Loc.GetString("zombie-alone"), num[0], num[0]);
+        if (percent >= 1) //oops, all zombies
+            _roundEndSystem.EndRound();
     }
 
     private void OnStartAttempt(RoundStartAttemptEvent ev)
     {
-        var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var zombies, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
-                continue;
+        if (!RuleAdded)
+            return;
 
-            var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
-            if (!ev.Forced && ev.Players.Length < minPlayers)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-                ev.Cancel();
-                continue;
-            }
+        var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
+        if (!ev.Forced && ev.Players.Length < minPlayers)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
+            ev.Cancel();
+            return;
+        }
 
-            if (ev.Players.Length == 0)
-            {
-                _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
-                ev.Cancel();
-            }
+        if (ev.Players.Length == 0)
+        {
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
+            ev.Cancel();
         }
     }
 
-    protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
-        InfectInitialPlayers(component);
+        //this technically will run twice with zombies on roundstart, but it doesn't matter because it fails instantly
+        InfectInitialPlayers();
     }
 
+    public override void Ended() { }
+
     private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
     {
         _zombify.ZombifyEntity(uid);
 
-        var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
+        var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombifySelfActionPrototype));
         _action.RemoveAction(uid, action);
     }
 
@@ -226,7 +228,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
     ///     allowing this gamemode to be started midround. As such, it doesn't need
     ///     any information besides just running.
     /// </remarks>
-    private void InfectInitialPlayers(ZombieRuleComponent component)
+    private void InfectInitialPlayers()
     {
         var allPlayers = _playerManager.ServerSessions.ToList();
         var playerList = new List<IPlayerSession>();
@@ -238,7 +240,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
                 playerList.Add(player);
 
                 var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
-                if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeID))
+                if (pref.AntagPreferences.Contains(PatientZeroPrototypeID))
                     prefList.Add(player);
             }
         }
@@ -282,15 +284,15 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 
             DebugTools.AssertNotNull(mind.OwnedEntity);
 
-            mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
+            mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(PatientZeroPrototypeID)));
 
             var inCharacterName = string.Empty;
             if (mind.OwnedEntity != null)
             {
-                _diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, component.InitialZombieVirusPrototype);
+                _diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, InitialZombieVirusPrototype);
                 inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName;
 
-                var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
+                var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombifySelfActionPrototype));
                 _action.AddAction(mind.OwnedEntity.Value, action, null);
             }
 
@@ -301,7 +303,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 
                 //gets the names now in case the players leave.
                 //this gets unhappy if people with the same name get chose. Probably shouldn't happen.
-                component.InitialInfectedNames.Add(inCharacterName, mind.Session.Name);
+                _initialInfectedNames.Add(inCharacterName, mind.Session.Name);
 
                 // I went all the way to ChatManager.cs and all i got was this lousy T-shirt
                 // You got a free T-shirt!?!?
index 0fd1d53a098860ef8edc9750b4ae780826e68394..d653df357dfd7be0d7d3c9d4c8c2cf8b9e8b533f 100644 (file)
@@ -14,10 +14,9 @@ namespace Content.Server.Objectives.Conditions
         public IObjectiveCondition GetAssigned(Mind.Mind mind)
         {
             var entityMgr = IoCManager.Resolve<IEntityManager>();
-
             var traitors = entityMgr.EntitySysManager.GetEntitySystem<TraitorRuleSystem>().GetOtherTraitorsAliveAndConnected(mind).ToList();
-            if (traitors.Count == 0)
-                return new EscapeShuttleCondition(); //You were made a traitor by admins, and are the first/only.
+
+            if (traitors.Count == 0) return new EscapeShuttleCondition{}; //You were made a traitor by admins, and are the first/only.
             return new RandomTraitorAliveCondition { _target = IoCManager.Resolve<IRobustRandom>().Pick(traitors).Mind };
         }
 
index 99de8be282ded808b345c8b7d350e17b014afe84..fd19603387eacab6f77e628bc4ba92fe17df66b9 100644 (file)
@@ -13,9 +13,7 @@ namespace Content.Server.Objectives.Conditions
 
         public IObjectiveCondition GetAssigned(Mind.Mind mind)
         {
-            //todo shit of a fuck
             var entityMgr = IoCManager.Resolve<IEntityManager>();
-
             var traitors = entityMgr.EntitySysManager.GetEntitySystem<TraitorRuleSystem>().GetOtherTraitorsAliveAndConnected(mind).ToList();
             List<Traitor.TraitorRole> removeList = new();
 
@@ -25,7 +23,7 @@ namespace Content.Server.Objectives.Conditions
                 {
                     foreach (var condition in objective.Conditions)
                     {
-                        if (condition is RandomTraitorProgressCondition)
+                        if (condition.GetType() == typeof(RandomTraitorProgressCondition))
                         {
                             removeList.Add(traitor);
                         }
index 5d4e458297e01f786f5a1d87c9b394a1d98e0b67..e833165862baa476a742d1171f1b7782c241e14e 100644 (file)
@@ -11,7 +11,7 @@ namespace Content.Server.Objectives.Requirements
 
         public bool CanBeAssigned(Mind.Mind mind)
         {
-            return EntitySystem.Get<TraitorRuleSystem>().GetOtherTraitorsAliveAndConnected(mind).Count >= _requiredTraitors;
+            return EntitySystem.Get<TraitorRuleSystem>().TotalTraitors >= _requiredTraitors;
         }
     }
 }
index 12a16151c1d6a187bce3f3bc1691087afc976e99..a5279ce848d1a55b98577cb0a41167757d1f2acb 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Server.GameTicking.Rules;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
@@ -8,11 +9,11 @@ namespace Content.Server.Spawners.Components
     public class ConditionalSpawnerComponent : Component
     {
         [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("prototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
+        [DataField("prototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
         public List<string> Prototypes { get; set; } = new();
 
         [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("gameRules", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
+        [DataField("gameRules", customTypeSerializer:typeof(PrototypeIdListSerializer<GameRulePrototype>))]
         public readonly List<string> GameRules = new();
 
         [ViewVariables(VVAccess.ReadWrite)]
index 341a2c9d440c212464fc26281ad9f7a318696826..1eef300972f244b37b58407bce3a98baa993062e 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Spawners.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Random;
@@ -23,67 +22,65 @@ namespace Content.Server.Spawners.EntitySystems
 
         private void OnCondSpawnMapInit(EntityUid uid, ConditionalSpawnerComponent component, MapInitEvent args)
         {
-            TrySpawn(uid, component);
+            TrySpawn(component);
         }
 
         private void OnRandSpawnMapInit(EntityUid uid, RandomSpawnerComponent component, MapInitEvent args)
         {
-            Spawn(uid, component);
-            QueueDel(uid);
+            Spawn(component);
+            EntityManager.QueueDeleteEntity(uid);
         }
 
-        private void OnRuleStarted(ref GameRuleStartedEvent args)
+        private void OnRuleStarted(GameRuleStartedEvent args)
         {
-            var query = EntityQueryEnumerator<ConditionalSpawnerComponent>();
-            while (query.MoveNext(out var uid, out var spawner))
+            foreach (var spawner in EntityManager.EntityQuery<ConditionalSpawnerComponent>())
             {
-                RuleStarted(uid, spawner, args);
+                RuleStarted(spawner, args);
             }
         }
 
-        public void RuleStarted(EntityUid uid, ConditionalSpawnerComponent component, GameRuleStartedEvent obj)
+        public void RuleStarted(ConditionalSpawnerComponent component, GameRuleStartedEvent obj)
         {
-            if (component.GameRules.Contains(obj.RuleId))
-                Spawn(uid, component);
+            if(component.GameRules.Contains(obj.Rule.ID))
+                Spawn(component);
         }
 
-        private void TrySpawn(EntityUid uid, ConditionalSpawnerComponent component)
+        private void TrySpawn(ConditionalSpawnerComponent component)
         {
             if (component.GameRules.Count == 0)
             {
-                Spawn(uid, component);
+                Spawn(component);
                 return;
             }
 
             foreach (var rule in component.GameRules)
             {
-                if (!_ticker.IsGameRuleActive(rule))
-                    continue;
-                Spawn(uid, component);
+                if (!_ticker.IsGameRuleStarted(rule)) continue;
+                Spawn(component);
                 return;
             }
         }
 
-        private void Spawn(EntityUid uid, ConditionalSpawnerComponent component)
+        private void Spawn(ConditionalSpawnerComponent component)
         {
             if (component.Chance != 1.0f && !_robustRandom.Prob(component.Chance))
                 return;
 
             if (component.Prototypes.Count == 0)
             {
-                Logger.Warning($"Prototype list in ConditionalSpawnComponent is empty! Entity: {ToPrettyString(uid)}");
+                Logger.Warning($"Prototype list in ConditionalSpawnComponent is empty! Entity: {component.Owner}");
                 return;
             }
 
-            if (!Deleted(uid))
-                EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), Transform(uid).Coordinates);
+            if (!Deleted(component.Owner))
+                EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), Transform(component.Owner).Coordinates);
         }
 
-        private void Spawn(EntityUid uid, RandomSpawnerComponent component)
+        private void Spawn(RandomSpawnerComponent component)
         {
             if (component.RarePrototypes.Count > 0 && (component.RareChance == 1.0f || _robustRandom.Prob(component.RareChance)))
             {
-                EntityManager.SpawnEntity(_robustRandom.Pick(component.RarePrototypes), Transform(uid).Coordinates);
+                EntityManager.SpawnEntity(_robustRandom.Pick(component.RarePrototypes), Transform(component.Owner).Coordinates);
                 return;
             }
 
@@ -92,18 +89,17 @@ namespace Content.Server.Spawners.EntitySystems
 
             if (component.Prototypes.Count == 0)
             {
-                Logger.Warning($"Prototype list in RandomSpawnerComponent is empty! Entity: {ToPrettyString(uid)}");
+                Logger.Warning($"Prototype list in RandomSpawnerComponent is empty! Entity: {component.Owner}");
                 return;
             }
 
-            if (Deleted(uid))
-                return;
+            if (Deleted(component.Owner)) return;
 
             var offset = component.Offset;
             var xOffset = _robustRandom.NextFloat(-offset, offset);
             var yOffset = _robustRandom.NextFloat(-offset, offset);
 
-            var coordinates = Transform(uid).Coordinates.Offset(new Vector2(xOffset, yOffset));
+            var coordinates = Transform(component.Owner).Coordinates.Offset(new Vector2(xOffset, yOffset));
 
             EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), coordinates);
         }
index bc77a9ce47129106d73cabe4f8f138529b4192d4..0304978ed6c94239df9769492df961bf52c352f4 100644 (file)
@@ -1,7 +1,12 @@
+using System.Linq;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Rules.Configurations;
+using Content.Shared.CCVar;
+using Content.Shared.GameTicking;
 using JetBrains.Annotations;
+using Robust.Server.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents
@@ -11,49 +16,53 @@ namespace Content.Server.StationEvents
     ///     game presets use.
     /// </summary>
     [UsedImplicitly]
-    public sealed class BasicStationEventSchedulerSystem : GameRuleSystem<BasicStationEventSchedulerComponent>
+    public sealed class BasicStationEventSchedulerSystem : GameRuleSystem
     {
+        public override string Prototype => "BasicStationEventScheduler";
+
         [Dependency] private readonly IRobustRandom _random = default!;
         [Dependency] private readonly EventManagerSystem _event = default!;
 
-        protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
-            GameRuleEndedEvent args)
+        private const float MinimumTimeUntilFirstEvent = 300;
+
+        /// <summary>
+        /// How long until the next check for an event runs
+        /// </summary>
+        /// Default value is how long until first event is allowed
+        [ViewVariables(VVAccess.ReadWrite)]
+        private float _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
+
+        public override void Started() { }
+
+        public override void Ended()
         {
-            component.TimeUntilNextEvent = BasicStationEventSchedulerComponent.MinimumTimeUntilFirstEvent;
+            _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
         }
 
-
         public override void Update(float frameTime)
         {
             base.Update(frameTime);
 
-            if (!_event.EventsEnabled)
+            if (!RuleStarted || !_event.EventsEnabled)
                 return;
 
-            var query = EntityQueryEnumerator<BasicStationEventSchedulerComponent, GameRuleComponent>();
-            while (query.MoveNext(out var uid, out var eventScheduler, out var gameRule))
+            if (_timeUntilNextEvent > 0)
             {
-                if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                    continue;
-
-                if (eventScheduler.TimeUntilNextEvent > 0)
-                {
-                    eventScheduler.TimeUntilNextEvent -= frameTime;
-                    return;
-                }
-
-                _event.RunRandomEvent();
-                ResetTimer(eventScheduler);
+                _timeUntilNextEvent -= frameTime;
+                return;
             }
+
+            _event.RunRandomEvent();
+            ResetTimer();
         }
 
         /// <summary>
         /// Reset the event timer once the event is done.
         /// </summary>
-        private void ResetTimer(BasicStationEventSchedulerComponent component)
+        private void ResetTimer()
         {
             // 5 - 25 minutes. TG does 3-10 but that's pretty frequent
-            component.TimeUntilNextEvent = _random.Next(300, 1500);
+            _timeUntilNextEvent = _random.Next(300, 1500);
         }
     }
 }
diff --git a/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs b/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs
deleted file mode 100644 (file)
index 8e8e853..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-using Content.Server.StationEvents.Events;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.StationEvents.Components;
-
-/// <summary>
-/// Used an event that spawns an anomaly somewhere random on the map.
-/// </summary>
-[RegisterComponent, Access(typeof(AnomalySpawnRule))]
-public sealed class AnomalySpawnRuleComponent : Component
-{
-    [DataField("anomalySpawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string AnomalySpawnerPrototype = "RandomAnomalySpawner";
-}
diff --git a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs
deleted file mode 100644 (file)
index 1f920b6..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(BasicStationEventSchedulerSystem))]
-public sealed class BasicStationEventSchedulerComponent : Component
-{
-    public const float MinimumTimeUntilFirstEvent = 300;
-
-    /// <summary>
-    /// How long until the next check for an event runs
-    /// </summary>
-    /// Default value is how long until first event is allowed
-    [ViewVariables(VVAccess.ReadWrite)]
-    public float TimeUntilNextEvent = MinimumTimeUntilFirstEvent;
-}
diff --git a/Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs b/Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs
deleted file mode 100644 (file)
index fe953c9..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-using Content.Server.StationEvents.Events;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.StationEvents.Components;
-
-/// <summary>
-/// This is used for an event that spawns an artifact
-/// somewhere random on the station.
-/// </summary>
-[RegisterComponent, Access(typeof(BluespaceArtifactRule))]
-public sealed class BluespaceArtifactRuleComponent : Component
-{
-    [DataField("artifactSpawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string ArtifactSpawnerPrototype = "RandomArtifactSpawner";
-
-    [DataField("artifactFlashPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string ArtifactFlashPrototype = "EffectFlashBluespace";
-
-    [DataField("possibleSightings")]
-    public List<string> PossibleSighting = new()
-    {
-        "bluespace-artifact-sighting-1",
-        "bluespace-artifact-sighting-2",
-        "bluespace-artifact-sighting-3",
-        "bluespace-artifact-sighting-4",
-        "bluespace-artifact-sighting-5",
-        "bluespace-artifact-sighting-6",
-        "bluespace-artifact-sighting-7"
-    };
-}
diff --git a/Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs b/Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs
deleted file mode 100644 (file)
index 06515f5..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(BluespaceLockerRule))]
-public sealed class BluespaceLockerRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs b/Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs
deleted file mode 100644 (file)
index a4acef1..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(BreakerFlipRule))]
-public sealed class BreakerFlipRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs b/Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs
deleted file mode 100644 (file)
index 5132a09..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(BureaucraticErrorRule))]
-public sealed class BureaucraticErrorRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs b/Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs
deleted file mode 100644 (file)
index d82557c..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(DiseaseOutbreakRule))]
-public sealed class DiseaseOutbreakRuleComponent : Component
-{
-    /// <summary>
-    /// Disease prototypes I decided were not too deadly for a random event
-    /// </summary>
-    /// <remarks>
-    /// Fire name
-    /// </remarks>
-    [DataField("notTooSeriousDiseases")]
-    public readonly IReadOnlyList<string> NotTooSeriousDiseases = new[]
-    {
-        "SpaceCold",
-        "VanAusdallsRobovirus",
-        "VentCough",
-        "AMIV",
-        "SpaceFlu",
-        "BirdFlew",
-        "TongueTwister"
-    };
-}
diff --git a/Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs b/Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs
deleted file mode 100644 (file)
index 6e3f69a..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(FalseAlarmRule))]
-public sealed class FalseAlarmRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs b/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs
deleted file mode 100644 (file)
index 5f2c2f5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-using Content.Server.StationEvents.Events;
-using Content.Shared.Atmos;
-using Robust.Shared.Map;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(GasLeakRule))]
-public sealed class GasLeakRuleComponent : Component
-{
-    public readonly Gas[] LeakableGases =
-    {
-        Gas.Miasma,
-        Gas.Plasma,
-        Gas.Tritium,
-        Gas.Frezon,
-    };
-
-    /// <summary>
-    ///     Running cooldown of how much time until another leak.
-    /// </summary>
-    public float TimeUntilLeak;
-
-    /// <summary>
-    ///     How long between more gas being added to the tile.
-    /// </summary>
-    public float LeakCooldown = 1.0f;
-
-    // Event variables
-    public EntityUid TargetStation;
-    public EntityUid TargetGrid;
-    public Vector2i TargetTile;
-    public EntityCoordinates TargetCoords;
-    public bool FoundTile;
-    public Gas LeakGas;
-    public float MolesPerSecond;
-    public readonly int MinimumMolesPerSecond = 20;
-
-    /// <summary>
-    ///     Don't want to make it too fast to give people time to flee.
-    /// </summary>
-    public int MaximumMolesPerSecond = 50;
-
-    public int MinimumGas = 250;
-    public int MaximumGas = 1000;
-    public float SparkChance = 0.05f;
-}
diff --git a/Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs b/Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs
deleted file mode 100644 (file)
index 82cc1ac..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(KudzuGrowthRule))]
-public sealed class KudzuGrowthRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs
deleted file mode 100644 (file)
index 5356603..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 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;
-}
diff --git a/Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs b/Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs
deleted file mode 100644 (file)
index e42eef5..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(MeteorSwarmRule))]
-public sealed class MeteorSwarmRuleComponent : Component
-{
-    public float _cooldown;
-
-    /// <summary>
-    /// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer.
-    /// </summary>
-    public int _waveCounter;
-
-    public int MinimumWaves = 3;
-    public int MaximumWaves = 8;
-
-    public float MinimumCooldown = 10f;
-    public float MaximumCooldown = 60f;
-
-    public int MeteorsPerWave = 5;
-    public float MeteorVelocity = 10f;
-    public float MaxAngularVelocity = 0.25f;
-    public float MinAngularVelocity = -0.25f;
-}
diff --git a/Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs b/Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs
deleted file mode 100644 (file)
index cb62746..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(MouseMigrationRule))]
-public sealed class MouseMigrationRuleComponent : Component
-{
-    [DataField("spawnedPrototypeChoices")]
-    public List<string> SpawnedPrototypeChoices = new() //we double up for that ez fake probability
-    {
-        "MobMouse",
-        "MobMouse1",
-        "MobMouse2",
-        "MobRatServant"
-    };
-}
diff --git a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs
deleted file mode 100644 (file)
index 1788a78..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(PowerGridCheckRule))]
-public sealed class PowerGridCheckRuleComponent : Component
-{
-    public CancellationTokenSource? AnnounceCancelToken;
-
-    public readonly List<EntityUid> Powered = new();
-    public readonly List<EntityUid> Unpowered = new();
-
-    public float SecondsUntilOff = 30.0f;
-
-    public int NumberPerSecond = 0;
-    public float UpdateRate => 1.0f / NumberPerSecond;
-    public float FrameTimeAccumulator = 0.0f;
-}
diff --git a/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs b/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs
deleted file mode 100644 (file)
index dd3f38b..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(RampingStationEventSchedulerSystem))]
-public sealed class RampingStationEventSchedulerComponent : Component
-{
-    [DataField("endTime"), ViewVariables(VVAccess.ReadWrite)]
-    public float EndTime;
-
-    [DataField("maxChaos"), ViewVariables(VVAccess.ReadWrite)]
-    public float MaxChaos;
-
-    [DataField("startingChaos"), ViewVariables(VVAccess.ReadWrite)]
-    public float StartingChaos;
-
-    [DataField("timeUntilNextEvent"), ViewVariables(VVAccess.ReadWrite)]
-    public float TimeUntilNextEvent;
-}
diff --git a/Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs b/Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs
deleted file mode 100644 (file)
index 6c00e9e..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(RandomSentienceRule))]
-public sealed class RandomSentienceRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs
deleted file mode 100644 (file)
index d195eaa..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(RevenantSpawnRule))]
-public sealed class RevenantSpawnRuleComponent : Component
-{
-    [DataField("revenantPrototype")]
-    public string RevenantPrototype = "MobRevenant";
-}
index d22f3e74eb9ef15f120191bb0e00a1662a38be64..bf40d50249aa18cdf0c90baaae1aed191055badf 100644 (file)
@@ -1,8 +1,6 @@
-using Content.Server.StationEvents.Events;
+namespace Content.Server.StationEvents.Components;
 
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(RandomSentienceRule))]
+[RegisterComponent]
 public sealed class SentienceTargetComponent : Component
 {
     [DataField("flavorKind", required: true)]
diff --git a/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs
deleted file mode 100644 (file)
index 15e01ac..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(SpiderSpawnRule))]
-public sealed class SpiderSpawnRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/StationEvents/Components/VentClogRuleComponent.cs b/Content.Server/StationEvents/Components/VentClogRuleComponent.cs
deleted file mode 100644 (file)
index 79f4993..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(VentClogRule))]
-public sealed class VentClogRuleComponent : Component
-{
-    [DataField("safeishVentChemicals")]
-    public readonly IReadOnlyList<string> SafeishVentChemicals = new[]
-    {
-        "Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer"
-    };
-
-}
index f675744929d66ae645ed2a5421a6a8baf56d6902..3dc46642e73a8ac1c7f9d3c4a78ad478e8c2144d 100644 (file)
@@ -1,8 +1,6 @@
-using Content.Server.StationEvents.Events;
+namespace Content.Server.StationEvents.Components;
 
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(VentClogRule))]
+[RegisterComponent]
 public sealed class VentCritterSpawnLocationComponent : Component
 {
 
diff --git a/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs b/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs
deleted file mode 100644 (file)
index 5332796..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-using Content.Server.StationEvents.Events;
-
-namespace Content.Server.StationEvents.Components;
-
-[RegisterComponent, Access(typeof(VentCrittersRule))]
-public sealed class VentCrittersRuleComponent : Component
-{
-    [DataField("spawnedPrototypeChoices")]
-    public List<string> SpawnedPrototypeChoices = new()
-    {
-        "MobMouse",
-        "MobMouse1",
-        "MobMouse2"
-    };
-}
index 72a5dfac7733da24c35a0e04330ab764275a35aa..6a2b646602352dd4dd1326c251ff2a477c4d0961 100644 (file)
@@ -1,7 +1,9 @@
 using System.Linq;
 using Content.Server.GameTicking;
-using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Rules;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Shared.CCVar;
+using Content.Shared.GameTicking;
 using Robust.Server.Player;
 using Robust.Shared.Configuration;
 using Robust.Shared.Prototypes;
@@ -29,14 +31,6 @@ public sealed class EventManagerSystem : EntitySystem
         _sawmill = Logger.GetSawmill("events");
 
         _configurationManager.OnValueChanged(CCVars.EventsEnabled, SetEnabled, true);
-
-        SubscribeLocalEvent<StationEventComponent, EntityUnpausedEvent>(OnUnpaused);
-    }
-
-    private void OnUnpaused(EntityUid uid, StationEventComponent component, ref EntityUnpausedEvent args)
-    {
-        component.StartTime += args.PausedTime;
-        component.EndTime += args.PausedTime;
     }
 
     public override void Shutdown()
@@ -52,15 +46,16 @@ public sealed class EventManagerSystem : EntitySystem
     {
         var randomEvent = PickRandomEvent();
 
-        if (randomEvent == null)
+        if (randomEvent == null
+            || !_prototype.TryIndex<GameRulePrototype>(randomEvent.Id, out var proto))
         {
             var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events");
             _sawmill.Error(errStr);
             return errStr;
         }
 
-        var ent = GameTicker.AddGameRule(randomEvent);
-        var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent)));
+        GameTicker.AddGameRule(proto);
+        var str = Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Id));
         _sawmill.Info(str);
         return str;
     }
@@ -68,7 +63,7 @@ public sealed class EventManagerSystem : EntitySystem
     /// <summary>
     /// Randomly picks a valid event.
     /// </summary>
-    public string? PickRandomEvent()
+    public StationEventRuleConfiguration? PickRandomEvent()
     {
         var availableEvents = AvailableEvents();
         _sawmill.Info($"Picking from {availableEvents.Count} total available events");
@@ -79,7 +74,7 @@ public sealed class EventManagerSystem : EntitySystem
     /// Pick a random event from the available events at this time, also considering their weightings.
     /// </summary>
     /// <returns></returns>
-    private string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
+    private StationEventRuleConfiguration? FindEvent(List<StationEventRuleConfiguration> availableEvents)
     {
         if (availableEvents.Count == 0)
         {
@@ -89,20 +84,20 @@ public sealed class EventManagerSystem : EntitySystem
 
         var sumOfWeights = 0;
 
-        foreach (var stationEvent in availableEvents.Values)
+        foreach (var stationEvent in availableEvents)
         {
             sumOfWeights += (int) stationEvent.Weight;
         }
 
         sumOfWeights = _random.Next(sumOfWeights);
 
-        foreach (var (proto, stationEvent) in availableEvents)
+        foreach (var stationEvent in availableEvents)
         {
             sumOfWeights -= (int) stationEvent.Weight;
 
             if (sumOfWeights <= 0)
             {
-                return proto.ID;
+                return stationEvent;
             }
         }
 
@@ -115,73 +110,67 @@ public sealed class EventManagerSystem : EntitySystem
     /// </summary>
     /// <param name="ignoreEarliestStart"></param>
     /// <returns></returns>
-    private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool ignoreEarliestStart = false)
+    private List<StationEventRuleConfiguration> AvailableEvents(bool ignoreEarliestStart = false)
     {
+        TimeSpan currentTime;
         var playerCount = _playerManager.PlayerCount;
 
         // playerCount does a lock so we'll just keep the variable here
-        var currentTime = !ignoreEarliestStart
-            ? GameTicker.RoundDuration()
-            : TimeSpan.Zero;
+        if (!ignoreEarliestStart)
+        {
+            currentTime = GameTicker.RoundDuration();
+        }
+        else
+        {
+            currentTime = TimeSpan.Zero;
+        }
 
-        var result = new Dictionary<EntityPrototype, StationEventComponent>();
+        var result = new List<StationEventRuleConfiguration>();
 
-        foreach (var (proto, stationEvent) in AllEvents())
+        foreach (var stationEvent in AllEvents())
         {
-            if (CanRun(proto, stationEvent, playerCount, currentTime))
+            if (CanRun(stationEvent, playerCount, currentTime))
             {
-                _sawmill.Debug($"Adding event {proto.ID} to possibilities");
-                result.Add(proto, stationEvent);
+                _sawmill.Debug($"Adding event {stationEvent.Id} to possibilities");
+                result.Add(stationEvent);
             }
         }
 
         return result;
     }
 
-    public Dictionary<EntityPrototype, StationEventComponent> AllEvents()
-    {
-        var allEvents = new Dictionary<EntityPrototype, StationEventComponent>();
-        foreach (var prototype in _prototype.EnumeratePrototypes<EntityPrototype>())
-        {
-            if (prototype.Abstract)
-                continue;
-
-            if (!prototype.TryGetComponent<StationEventComponent>(out var stationEvent))
-                continue;
-
-            allEvents.Add(prototype, stationEvent);
-        }
-
-        return allEvents;
-    }
-
-    private int GetOccurrences(EntityPrototype stationEvent)
+    private IEnumerable<StationEventRuleConfiguration> AllEvents()
     {
-        return GetOccurrences(stationEvent.ID);
+        return _prototype.EnumeratePrototypes<GameRulePrototype>()
+            .Where(p => p.Configuration is StationEventRuleConfiguration)
+            .Select(p => (StationEventRuleConfiguration) p.Configuration);
     }
 
-    private int GetOccurrences(string stationEvent)
+    private int GetOccurrences(StationEventRuleConfiguration stationEvent)
     {
-        return GameTicker.AllPreviousGameRules.Count(p => p.Item2 == stationEvent);
+        return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id);
     }
 
-    public TimeSpan TimeSinceLastEvent(EntityPrototype stationEvent)
+    public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent)
     {
         foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse())
         {
-            if (rule == stationEvent.ID)
+            if (rule.Configuration is not StationEventRuleConfiguration)
+                continue;
+
+            if (stationEvent == null || rule.ID == stationEvent.Id)
                 return time;
         }
 
         return TimeSpan.Zero;
     }
 
-    private bool CanRun(EntityPrototype prototype, StationEventComponent stationEvent, int playerCount, TimeSpan currentTime)
+    private bool CanRun(StationEventRuleConfiguration stationEvent, int playerCount, TimeSpan currentTime)
     {
-        if (GameTicker.IsGameRuleActive(prototype.ID))
+        if (GameTicker.IsGameRuleStarted(stationEvent.Id))
             return false;
 
-        if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(prototype) >= stationEvent.MaxOccurrences.Value)
+        if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(stationEvent) >= stationEvent.MaxOccurrences.Value)
         {
             return false;
         }
@@ -196,7 +185,7 @@ public sealed class EventManagerSystem : EntitySystem
             return false;
         }
 
-        var lastRun = TimeSinceLastEvent(prototype);
+        var lastRun = TimeSinceLastEvent(stationEvent);
         if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes <
             stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes)
         {
similarity index 61%
rename from Content.Server/StationEvents/Events/AnomalySpawnRule.cs
rename to Content.Server/StationEvents/Events/AnomalySpawn.cs
index 8e7a860d2e3e7a8560a7f1cf8202212f5b772267..5b7d1c42f5fcb61cec298ece29f9aa60deee9cb9 100644 (file)
@@ -1,28 +1,31 @@
 using System.Linq;
 using Content.Server.Anomaly;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
-using Content.Server.StationEvents.Components;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents.Events;
 
-public sealed class AnomalySpawnRule : StationEventSystem<AnomalySpawnRuleComponent>
+public sealed class AnomalySpawn : StationEventSystem
 {
+    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly AnomalySystem _anomaly = default!;
 
-    protected override void Added(EntityUid uid, AnomalySpawnRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    public override string Prototype => "AnomalySpawn";
+
+    public readonly string AnomalySpawnerPrototype = "RandomAnomalySpawner";
+
+    public override void Added()
     {
-        base.Added(uid, component, gameRule, args);
+        base.Added();
 
         var str = Loc.GetString("anomaly-spawn-event-announcement",
-            ("sighting", Loc.GetString($"anomaly-spawn-sighting-{RobustRandom.Next(1, 6)}")));
+            ("sighting", Loc.GetString($"anomaly-spawn-sighting-{_random.Next(1, 6)}")));
         ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5"));
     }
 
-    protected override void Started(EntityUid uid, AnomalySpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        base.Started();
 
         if (StationSystem.Stations.Count == 0)
             return; // No stations
@@ -42,7 +45,7 @@ public sealed class AnomalySpawnRule : StationEventSystem<AnomalySpawnRuleCompon
         var amountToSpawn = Math.Max(1, (int) MathF.Round(GetSeverityModifier() / 2));
         for (var i = 0; i < amountToSpawn; i++)
         {
-            _anomaly.SpawnOnRandomGridLocation(grid.Value, component.AnomalySpawnerPrototype);
+            _anomaly.SpawnOnRandomGridLocation(grid.Value, AnomalySpawnerPrototype);
         }
     }
 }
diff --git a/Content.Server/StationEvents/Events/BluespaceArtifact.cs b/Content.Server/StationEvents/Events/BluespaceArtifact.cs
new file mode 100644 (file)
index 0000000..407cca0
--- /dev/null
@@ -0,0 +1,49 @@
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents.Events;
+
+public sealed class BluespaceArtifact : StationEventSystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    public override string Prototype => "BluespaceArtifact";
+
+    public readonly string ArtifactSpawnerPrototype = "RandomArtifactSpawner";
+    public readonly string ArtifactFlashPrototype = "EffectFlashBluespace";
+
+    public readonly List<string> PossibleSighting = new()
+    {
+        "bluespace-artifact-sighting-1",
+        "bluespace-artifact-sighting-2",
+        "bluespace-artifact-sighting-3",
+        "bluespace-artifact-sighting-4",
+        "bluespace-artifact-sighting-5",
+        "bluespace-artifact-sighting-6",
+        "bluespace-artifact-sighting-7"
+    };
+
+    public override void Added()
+    {
+        base.Added();
+
+        var str = Loc.GetString("bluespace-artifact-event-announcement",
+            ("sighting", Loc.GetString(_random.Pick(PossibleSighting))));
+        ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5"));
+    }
+
+    public override void Started()
+    {
+        base.Started();
+        var amountToSpawn = Math.Max(1, (int) MathF.Round(GetSeverityModifier() / 1.5f));
+        for (var i = 0; i < amountToSpawn; i++)
+        {
+            if (!TryFindRandomTile(out _, out _, out _, out var coords))
+                return;
+
+            EntityManager.SpawnEntity(ArtifactSpawnerPrototype, coords);
+            EntityManager.SpawnEntity(ArtifactFlashPrototype, coords);
+
+            Sawmill.Info($"Spawning random artifact at {coords}");
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs
deleted file mode 100644 (file)
index 306b735..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-using Robust.Shared.Random;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class BluespaceArtifactRule : StationEventSystem<BluespaceArtifactRuleComponent>
-{
-    protected override void Added(EntityUid uid, BluespaceArtifactRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
-    {
-        base.Added(uid, component, gameRule, args);
-
-        var str = Loc.GetString("bluespace-artifact-event-announcement",
-            ("sighting", Loc.GetString(RobustRandom.Pick(component.PossibleSighting))));
-        ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5"));
-    }
-
-    protected override void Started(EntityUid uid, BluespaceArtifactRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        var amountToSpawn = Math.Max(1, (int) MathF.Round(GetSeverityModifier() / 1.5f));
-        for (var i = 0; i < amountToSpawn; i++)
-        {
-            if (!TryFindRandomTile(out _, out _, out _, out var coords))
-                return;
-
-            Spawn(component.ArtifactSpawnerPrototype, coords);
-            Spawn(component.ArtifactFlashPrototype, coords);
-
-            Sawmill.Info($"Spawning random artifact at {coords}");
-        }
-    }
-}
similarity index 79%
rename from Content.Server/StationEvents/Events/BluespaceLockerRule.cs
rename to Content.Server/StationEvents/Events/BluespaceLocker.cs
index ddf1ba784cb3a00128ef0f90907d0295af720b3c..88de303c0700e57f0a49dace2581735914dfcdd5 100644 (file)
@@ -1,25 +1,27 @@
 using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Resist;
 using Content.Server.Station.Components;
-using Content.Server.StationEvents.Components;
 using Content.Server.Storage.Components;
 using Content.Server.Storage.EntitySystems;
 using Content.Shared.Access.Components;
 using Content.Shared.Coordinates;
+using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents.Events;
 
-public sealed class BluespaceLockerRule : StationEventSystem<BluespaceLockerRuleComponent>
+public sealed class BluespaceLockerLink : StationEventSystem
 {
+    [Dependency] private readonly IRobustRandom _robustRandom = default!;
     [Dependency] private readonly BluespaceLockerSystem _bluespaceLocker = default!;
 
-    protected override void Started(EntityUid uid, BluespaceLockerRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override string Prototype => "BluespaceLockerLink";
+
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        base.Started();
 
         var targets = EntityQuery<EntityStorageComponent, ResistLockerComponent>().ToList();
-        RobustRandom.Shuffle(targets);
+        _robustRandom.Shuffle(targets);
 
         foreach (var target in targets)
         {
similarity index 70%
rename from Content.Server/StationEvents/Events/BreakerFlipRule.cs
rename to Content.Server/StationEvents/Events/BreakerFlip.cs
index 2920ee331bd1973c289e40469590cb68dbfa0c0e..2df721b7cc17d36283f39025e2a6c35b747d45da 100644 (file)
@@ -1,44 +1,44 @@
 using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Station.Components;
-using Content.Server.StationEvents.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents.Events;
 
 [UsedImplicitly]
-public sealed class BreakerFlipRule : StationEventSystem<BreakerFlipRuleComponent>
+public sealed class BreakerFlip : StationEventSystem
 {
     [Dependency] private readonly ApcSystem _apcSystem = default!;
 
-    protected override void Added(EntityUid uid, BreakerFlipRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    public override string Prototype => "BreakerFlip";
+
+    public override void Added()
     {
-        base.Added(uid, component, gameRule, args);
+        base.Added();
 
         var str = Loc.GetString("station-event-breaker-flip-announcement", ("data", Loc.GetString(Loc.GetString($"random-sentience-event-data-{RobustRandom.Next(1, 6)}"))));
         ChatSystem.DispatchGlobalAnnouncement(str, playSound: false, colorOverride: Color.Gold);
     }
 
-    protected override void Started(EntityUid uid, BreakerFlipRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        base.Started();
 
         if (StationSystem.Stations.Count == 0)
             return;
         var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList());
 
         var stationApcs = new List<ApcComponent>();
-        foreach (var (apc, transform) in EntityQuery<ApcComponent, TransformComponent>())
+        foreach (var (apc, transform) in EntityQuery<ApcComponent, TransformComponent>()) 
         {
             if (apc.MainBreakerEnabled && CompOrNull<StationMemberComponent>(transform.GridUid)?.Station == chosenStation)
             {
                 stationApcs.Add(apc);
             }
         }
-
+        
         var toDisable = Math.Min(RobustRandom.Next(3, 7), stationApcs.Count);
         if (toDisable == 0)
             return;
similarity index 82%
rename from Content.Server/StationEvents/Events/BureaucraticErrorRule.cs
rename to Content.Server/StationEvents/Events/BureaucraticError.cs
index 0fbf72fd7d0d70a85223f002c785c013a4edcf7d..a96eae51b533265f8458eb5375b0798a62e524bd 100644 (file)
@@ -1,20 +1,20 @@
 using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Systems;
-using Content.Server.StationEvents.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents.Events;
 
 [UsedImplicitly]
-public sealed class BureaucraticErrorRule : StationEventSystem<BureaucraticErrorRuleComponent>
+public sealed class BureaucraticError : StationEventSystem
 {
     [Dependency] private readonly StationJobsSystem _stationJobs = default!;
 
-    protected override void Started(EntityUid uid, BureaucraticErrorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override string Prototype => "BureaucraticError";
+
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        base.Started();
 
         if (StationSystem.Stations.Count == 0)
             return; // No stations
similarity index 70%
rename from Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs
rename to Content.Server/StationEvents/Events/DiseaseOutbreak.cs
index 438cc041df10d1a85069e10d415f8f71994bb07e..3c829ddfabb62bed4648f527d862823d0e006b51 100644 (file)
@@ -1,7 +1,5 @@
 using Content.Server.Disease;
 using Content.Server.Disease.Components;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
 using Content.Shared.Disease;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
@@ -12,23 +10,38 @@ namespace Content.Server.StationEvents.Events;
 /// Infects a couple people
 /// with a random disease that isn't super deadly
 /// </summary>
-public sealed class DiseaseOutbreakRule : StationEventSystem<DiseaseOutbreakRuleComponent>
+public sealed class DiseaseOutbreak : StationEventSystem
 {
     [Dependency] private readonly DiseaseSystem _diseaseSystem = default!;
     [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
 
+    public override string Prototype => "DiseaseOutbreak";
+
+    /// <summary>
+    /// Disease prototypes I decided were not too deadly for a random event
+    /// </summary>
+    public readonly IReadOnlyList<string> NotTooSeriousDiseases = new[]
+    {
+        "SpaceCold",
+        "VanAusdallsRobovirus",
+        "VentCough",
+        "AMIV",
+        "SpaceFlu",
+        "BirdFlew",
+        "TongueTwister"
+    };
+
     /// <summary>
     /// Finds 2-5 random, alive entities that can host diseases
     /// and gives them a randomly selected disease.
     /// They all get the same disease.
     /// </summary>
-    protected override void Started(EntityUid uid, DiseaseOutbreakRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
-
+        base.Started();
         HashSet<EntityUid> stationsToNotify = new();
         List<DiseaseCarrierComponent> aliveList = new();
-        foreach (var (carrier, mobState) in EntityQuery<DiseaseCarrierComponent, MobStateComponent>())
+        foreach (var (carrier, mobState) in EntityManager.EntityQuery<DiseaseCarrierComponent, MobStateComponent>())
         {
             if (!_mobStateSystem.IsDead(mobState.Owner, mobState))
                 aliveList.Add(carrier);
@@ -38,7 +51,7 @@ public sealed class DiseaseOutbreakRule : StationEventSystem<DiseaseOutbreakRule
         // We're going to filter the above out to only alive mobs. Might change after future mobstate rework
         var toInfect = RobustRandom.Next(2, 5);
 
-        var diseaseName = RobustRandom.Pick(component.NotTooSeriousDiseases);
+        var diseaseName = RobustRandom.Pick(NotTooSeriousDiseases);
 
         if (!PrototypeManager.TryIndex(diseaseName, out DiseasePrototype? disease))
             return;
diff --git a/Content.Server/StationEvents/Events/FalseAlarm.cs b/Content.Server/StationEvents/Events/FalseAlarm.cs
new file mode 100644 (file)
index 0000000..701f1c2
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Server.GameTicking.Rules.Configurations;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+
+namespace Content.Server.StationEvents.Events
+{
+    [UsedImplicitly]
+    public sealed class FalseAlarm : StationEventSystem
+    {
+        public override string Prototype => "FalseAlarm";
+
+        public override void Started()
+        {
+            base.Started();
+
+            var ev = GetRandomEventUnweighted(PrototypeManager, RobustRandom);
+
+            if (ev.Configuration is not StationEventRuleConfiguration cfg)
+                return;
+
+            if (cfg.StartAnnouncement != null)
+            {
+                ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(cfg.StartAnnouncement), playSound: false, colorOverride: Color.Gold);
+            }
+
+            if (cfg.StartAudio != null)
+            {
+                SoundSystem.Play(cfg.StartAudio.GetSound(), Filter.Broadcast(), cfg.StartAudio.Params);
+            }
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/FalseAlarmRule.cs b/Content.Server/StationEvents/Events/FalseAlarmRule.cs
deleted file mode 100644 (file)
index 05e9435..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-using JetBrains.Annotations;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-
-namespace Content.Server.StationEvents.Events;
-
-[UsedImplicitly]
-public sealed class FalseAlarmRule : StationEventSystem<FalseAlarmRuleComponent>
-{
-    [Dependency] private readonly EventManagerSystem _event = default!;
-
-    protected override void Started(EntityUid uid, FalseAlarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        var allEv = _event.AllEvents().Select(p => p.Value).ToList();
-        var picked = RobustRandom.Pick(allEv);
-
-        if (picked.StartAnnouncement != null)
-        {
-            ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(picked.StartAnnouncement), playSound: false, colorOverride: Color.Gold);
-        }
-        Audio.PlayGlobal(picked.StartAudio, Filter.Broadcast(), true);
-    }
-}
diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs
new file mode 100644 (file)
index 0000000..e073ec2
--- /dev/null
@@ -0,0 +1,147 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.GameTicking.Rules;
+using Content.Server.GameTicking.Rules.Configurations;
+using Content.Shared.Atmos;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents.Events
+{
+    internal sealed class GasLeak : StationEventSystem
+    {
+        [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+
+        public override string Prototype => "GasLeak";
+
+        private static readonly Gas[] LeakableGases =
+        {
+            Gas.Miasma,
+            Gas.Plasma,
+            Gas.Tritium,
+            Gas.Frezon,
+        };
+
+        /// <summary>
+        ///     Running cooldown of how much time until another leak.
+        /// </summary>
+        private float _timeUntilLeak;
+
+        /// <summary>
+        ///     How long between more gas being added to the tile.
+        /// </summary>
+        private const float LeakCooldown = 1.0f;
+
+
+        // Event variables
+
+        private EntityUid _targetStation;
+        private EntityUid _targetGrid;
+        private Vector2i _targetTile;
+        private EntityCoordinates _targetCoords;
+        private bool _foundTile;
+        private Gas _leakGas;
+        private float _molesPerSecond;
+        private const int MinimumMolesPerSecond = 20;
+        private float _endAfter = float.MaxValue;
+
+        /// <summary>
+        ///     Don't want to make it too fast to give people time to flee.
+        /// </summary>
+        private const int MaximumMolesPerSecond = 50;
+
+        private const int MinimumGas = 250;
+        private const int MaximumGas = 1000;
+        private const float SparkChance = 0.05f;
+
+        public override void Started()
+        {
+            base.Started();
+
+            var mod = MathF.Sqrt(GetSeverityModifier());
+
+            // Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there.
+            if (TryFindRandomTile(out _targetTile, out _targetStation, out _targetGrid, out _targetCoords))
+            {
+                _foundTile = true;
+
+                _leakGas = RobustRandom.Pick(LeakableGases);
+                // Was 50-50 on using normal distribution.
+                var totalGas = RobustRandom.Next(MinimumGas, MaximumGas) * mod;
+                var startAfter = ((StationEventRuleConfiguration) Configuration).StartAfter;
+                _molesPerSecond = RobustRandom.Next(MinimumMolesPerSecond, MaximumMolesPerSecond);
+                _endAfter = totalGas / _molesPerSecond + startAfter;
+                Sawmill.Info($"Leaking {totalGas} of {_leakGas} over {_endAfter - startAfter} seconds at {_targetTile}");
+            }
+
+            // Look technically if you wanted to guarantee a leak you'd do this in announcement but having the announcement
+            // there just to fuck with people even if there is no valid tile is funny.
+        }
+
+        public override void Update(float frameTime)
+        {
+            base.Update(frameTime);
+
+            if (!RuleStarted)
+                return;
+
+            if (Elapsed > _endAfter)
+            {
+                ForceEndSelf();
+                return;
+            }
+
+            _timeUntilLeak -= frameTime;
+
+            if (_timeUntilLeak > 0f) return;
+            _timeUntilLeak += LeakCooldown;
+
+            if (!_foundTile ||
+                _targetGrid == default ||
+                EntityManager.Deleted(_targetGrid) ||
+                !_atmosphere.IsSimulatedGrid(_targetGrid))
+            {
+                ForceEndSelf();
+                return;
+            }
+
+            var environment = _atmosphere.GetTileMixture(_targetGrid, null, _targetTile, true);
+
+            environment?.AdjustMoles(_leakGas, LeakCooldown * _molesPerSecond);
+        }
+
+        public override void Ended()
+        {
+            base.Ended();
+
+            Spark();
+
+            _foundTile = false;
+            _targetGrid = default;
+            _targetTile = default;
+            _targetCoords = default;
+            _leakGas = Gas.Oxygen;
+            _endAfter = float.MaxValue;
+        }
+
+        private void Spark()
+        {
+            if (RobustRandom.NextFloat() <= SparkChance)
+            {
+                if (!_foundTile ||
+                    _targetGrid == default ||
+                    (!EntityManager.EntityExists(_targetGrid) ? EntityLifeStage.Deleted : EntityManager.GetComponent<MetaDataComponent>(_targetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
+                    !_atmosphere.IsSimulatedGrid(_targetGrid))
+                {
+                    return;
+                }
+
+                // Don't want it to be so obnoxious as to instantly murder anyone in the area but enough that
+                // it COULD start potentially start a bigger fire.
+                _atmosphere.HotspotExpose(_targetGrid, _targetTile, 700f, 50f, null, true);
+                SoundSystem.Play("/Audio/Effects/sparks4.ogg", Filter.Pvs(_targetCoords), _targetCoords);
+            }
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/GasLeakRule.cs b/Content.Server/StationEvents/Events/GasLeakRule.cs
deleted file mode 100644 (file)
index 95e9286..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
-
-namespace Content.Server.StationEvents.Events
-{
-    internal sealed class GasLeakRule : StationEventSystem<GasLeakRuleComponent>
-    {
-        [Dependency] private readonly IGameTiming _timing = default!;
-        [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
-
-        protected override void Started(EntityUid uid, GasLeakRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-        {
-            base.Started(uid, component, gameRule, args);
-
-            if (!TryComp<StationEventComponent>(uid, out var stationEvent))
-                return;
-
-            var mod = MathF.Sqrt(GetSeverityModifier());
-
-            // Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there.
-            if (TryFindRandomTile(out component.TargetTile, out component.TargetStation, out component.TargetGrid, out component.TargetCoords))
-            {
-                component.FoundTile = true;
-
-                component.LeakGas = RobustRandom.Pick(component.LeakableGases);
-                // Was 50-50 on using normal distribution.
-                var totalGas = RobustRandom.Next(component.MinimumGas, component.MaximumGas) * mod;
-                var startAfter = stationEvent.StartDelay;
-                component.MolesPerSecond = RobustRandom.Next(component.MinimumMolesPerSecond, component.MaximumMolesPerSecond);
-
-                stationEvent.EndTime = _timing.CurTime + TimeSpan.FromSeconds(totalGas / component.MolesPerSecond + startAfter.TotalSeconds);
-            }
-
-            // Look technically if you wanted to guarantee a leak you'd do this in announcement but having the announcement
-            // there just to fuck with people even if there is no valid tile is funny.
-        }
-
-        protected override void ActiveTick(EntityUid uid, GasLeakRuleComponent component, GameRuleComponent gameRule, float frameTime)
-        {
-            base.ActiveTick(uid, component, gameRule, frameTime);
-            component.TimeUntilLeak -= frameTime;
-
-            if (component.TimeUntilLeak > 0f)
-                return;
-            component.TimeUntilLeak += component.LeakCooldown;
-
-            if (!component.FoundTile ||
-                component.TargetGrid == default ||
-                Deleted(component.TargetGrid) ||
-                !_atmosphere.IsSimulatedGrid(component.TargetGrid))
-            {
-                ForceEndSelf(uid, gameRule);
-                return;
-            }
-
-            var environment = _atmosphere.GetTileMixture(component.TargetGrid, null, component.TargetTile, true);
-
-            environment?.AdjustMoles(component.LeakGas, component.LeakCooldown * component.MolesPerSecond);
-        }
-
-        protected override void Ended(EntityUid uid, GasLeakRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
-        {
-            base.Ended(uid, component, gameRule, args);
-            Spark(uid, component);
-        }
-
-        private void Spark(EntityUid uid, GasLeakRuleComponent component)
-        {
-            if (RobustRandom.NextFloat() <= component.SparkChance)
-            {
-                if (!component.FoundTile ||
-                    component.TargetGrid == default ||
-                    (!Exists(component.TargetGrid) ? EntityLifeStage.Deleted : MetaData(component.TargetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
-                    !_atmosphere.IsSimulatedGrid(component.TargetGrid))
-                {
-                    return;
-                }
-
-                // Don't want it to be so obnoxious as to instantly murder anyone in the area but enough that
-                // it COULD start potentially start a bigger fire.
-                _atmosphere.HotspotExpose(component.TargetGrid, component.TargetTile, 700f, 50f, null, true);
-                Audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"), component.TargetCoords);
-            }
-        }
-    }
-}
diff --git a/Content.Server/StationEvents/Events/KudzuGrowth.cs b/Content.Server/StationEvents/Events/KudzuGrowth.cs
new file mode 100644 (file)
index 0000000..e0b0f5c
--- /dev/null
@@ -0,0 +1,28 @@
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents.Events;
+
+public sealed class KudzuGrowth : StationEventSystem
+{
+    public override string Prototype => "KudzuGrowth";
+
+    private EntityUid _targetGrid;
+    private Vector2i _targetTile;
+    private EntityCoordinates _targetCoords;
+
+    public override void Started()
+    {
+        base.Started();
+
+        // Pick a place to plant the kudzu.
+        if (TryFindRandomTile(out _targetTile, out _, out _targetGrid, out _targetCoords))
+        {
+            EntityManager.SpawnEntity("Kudzu", _targetCoords);
+            Sawmill.Info($"Spawning a Kudzu at {_targetTile} on {_targetGrid}");
+        }
+
+        // If the kudzu tile selection fails we just let the announcement happen anyways because it's funny and people
+        // will be hunting the non-existent, dangerous plant.
+    }
+}
diff --git a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs
deleted file mode 100644 (file)
index 3fa12cd..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class KudzuGrowthRule : StationEventSystem<KudzuGrowthRuleComponent>
-{
-    protected override void Started(EntityUid uid, KudzuGrowthRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        // Pick a place to plant the kudzu.
-        if (!TryFindRandomTile(out var targetTile, out _, out var targetGrid, out var targetCoords))
-            return;
-        Spawn("Kudzu", targetCoords);
-        Sawmill.Info($"Spawning a Kudzu at {targetTile} on {targetGrid}");
-
-    }
-}
diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawn.cs b/Content.Server/StationEvents/Events/LoneOpsSpawn.cs
new file mode 100644 (file)
index 0000000..151a592
--- /dev/null
@@ -0,0 +1,44 @@
+using Robust.Server.GameObjects;
+using Robust.Server.Maps;
+using Robust.Shared.Map;
+using Content.Server.GameTicking;
+using Robust.Shared.Prototypes;
+using Content.Server.GameTicking.Rules;
+
+namespace Content.Server.StationEvents.Events;
+
+public sealed class LoneOpsSpawn : StationEventSystem
+{
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly MapLoaderSystem _map = default!;
+    [Dependency] private readonly GameTicker _gameTicker = default!;
+    [Dependency] private readonly NukeopsRuleSystem _nukeopsRuleSystem = default!;
+
+    public override string Prototype => "LoneOpsSpawn";
+    public const string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml";
+    public const string GameRuleProto = "Nukeops";
+
+    public override void Started()
+    {
+        base.Started();
+
+        if (!_nukeopsRuleSystem.CheckLoneOpsSpawn())
+            return;
+
+        var shuttleMap = _mapManager.CreateMap();
+        var options = new MapLoadOptions()
+        {
+            LoadMap = true,
+        };
+
+        _map.TryLoad(shuttleMap, LoneOpsShuttlePath, out var grids, options);
+
+        if (!_prototypeManager.TryIndex<GameRulePrototype>(GameRuleProto, out var ruleProto))
+            return;
+
+        _nukeopsRuleSystem.LoadLoneOpsConfig();
+        _gameTicker.StartGameRule(ruleProto);
+    }
+}
+
diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs b/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs
deleted file mode 100644 (file)
index 57f0a2c..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-using Robust.Server.GameObjects;
-using Robust.Server.Maps;
-using Robust.Shared.Map;
-using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class LoneOpsSpawnRule : StationEventSystem<LoneOpsSpawnRuleComponent>
-{
-    [Dependency] private readonly IMapManager _mapManager = default!;
-    [Dependency] private readonly MapLoaderSystem _map = default!;
-    [Dependency] private readonly GameTicker _gameTicker = default!;
-    [Dependency] private readonly NukeopsRuleSystem _nukeopsRuleSystem = default!;
-
-    protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        if (!_nukeopsRuleSystem.CheckLoneOpsSpawn())
-            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 = EntityManager.GetComponent<NukeopsRuleComponent>(nukeopsEntity);
-        nukeopsComp.SpawnOutpost = false;
-        nukeopsComp.EndsRound = false;
-        _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);
-    }
-}
-
similarity index 53%
rename from Content.Server/StationEvents/Events/MeteorSwarmRule.cs
rename to Content.Server/StationEvents/Events/MeteorSwarm.cs
index eb0597a92ef5596087766170a71997658e595e04..aabc9ba2bc48624c2857bb9ae7d411b0d0765324 100644 (file)
@@ -1,5 +1,4 @@
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking;
 using Content.Shared.Spawners.Components;
 using Robust.Shared.Map;
 using Robust.Shared.Physics.Components;
@@ -7,37 +6,67 @@ using Robust.Shared.Physics.Systems;
 
 namespace Content.Server.StationEvents.Events
 {
-    public sealed class MeteorSwarmRule : StationEventSystem<MeteorSwarmRuleComponent>
+    public sealed class MeteorSwarm : StationEventSystem
     {
         [Dependency] private readonly SharedPhysicsSystem _physics = default!;
 
-        protected override void Started(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-        {
-            base.Started(uid, component, gameRule, args);
+        public override string Prototype => "MeteorSwarm";
+
+        private float _cooldown;
+
+        /// <summary>
+        /// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer.
+        /// </summary>
+        private int _waveCounter;
+
+        private const int MinimumWaves = 3;
+        private const int MaximumWaves = 8;
 
+        private const float MinimumCooldown = 10f;
+        private const float MaximumCooldown = 60f;
+
+        private const int MeteorsPerWave = 5;
+        private const float MeteorVelocity = 10f;
+        private const float MaxAngularVelocity = 0.25f;
+        private const float MinAngularVelocity = -0.25f;
+
+        public override void Started()
+        {
+            base.Started();
             var mod = Math.Sqrt(GetSeverityModifier());
-            component._waveCounter = (int) (RobustRandom.Next(component.MinimumWaves, component.MaximumWaves) * mod);
+            _waveCounter = (int) (RobustRandom.Next(MinimumWaves, MaximumWaves) * mod);
         }
 
-        protected override void ActiveTick(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, float frameTime)
+        public override void Ended()
         {
-            base.ActiveTick(uid, component, gameRule, frameTime);
-            if (component._waveCounter <= 0)
+            base.Ended();
+            _waveCounter = 0;
+            _cooldown = 0f;
+        }
+
+        public override void Update(float frameTime)
+        {
+            base.Update(frameTime);
+
+            if (!RuleStarted)
+                return;
+
+            if (_waveCounter <= 0)
             {
-                ForceEndSelf(uid, gameRule);
+                ForceEndSelf();
                 return;
             }
 
             var mod = GetSeverityModifier();
 
-            component._cooldown -= frameTime;
+            _cooldown -= frameTime;
 
-            if (component._cooldown > 0f)
+            if (_cooldown > 0f)
                 return;
 
-            component._waveCounter--;
+            _waveCounter--;
 
-            component._cooldown += (component.MaximumCooldown - component.MinimumCooldown) * RobustRandom.NextFloat() / mod + component.MinimumCooldown;
+            _cooldown += (MaximumCooldown - MinimumCooldown) * RobustRandom.NextFloat() / mod + MinimumCooldown;
 
             Box2? playableArea = null;
             var mapId = GameTicker.DefaultMap;
@@ -50,7 +79,7 @@ namespace Content.Server.StationEvents.Events
 
             if (playableArea == null)
             {
-                ForceEndSelf(uid, gameRule);
+                ForceEndSelf();
                 return;
             }
 
@@ -59,7 +88,7 @@ namespace Content.Server.StationEvents.Events
 
             var center = playableArea.Value.Center;
 
-            for (var i = 0; i < component.MeteorsPerWave; i++)
+            for (var i = 0; i < MeteorsPerWave; i++)
             {
                 var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
                 var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
@@ -69,10 +98,10 @@ namespace Content.Server.StationEvents.Events
                 _physics.SetBodyStatus(physics, BodyStatus.InAir);
                 _physics.SetLinearDamping(physics, 0f);
                 _physics.SetAngularDamping(physics, 0f);
-                _physics.ApplyLinearImpulse(meteor, -offset.Normalized * component.MeteorVelocity * physics.Mass, body: physics);
+                _physics.ApplyLinearImpulse(meteor, -offset.Normalized * MeteorVelocity * physics.Mass, body: physics);
                 _physics.ApplyAngularImpulse(
                     meteor,
-                    physics.Mass * ((component.MaxAngularVelocity - component.MinAngularVelocity) * RobustRandom.NextFloat() + component.MinAngularVelocity),
+                    physics.Mass * ((MaxAngularVelocity - MinAngularVelocity) * RobustRandom.NextFloat() + MinAngularVelocity),
                     body: physics);
 
                 EnsureComp<TimedDespawnComponent>(meteor).Lifetime = 120f;
similarity index 66%
rename from Content.Server/StationEvents/Events/MouseMigrationRule.cs
rename to Content.Server/StationEvents/Events/MouseMigration.cs
index b0c54a7d0ea5d2f7d02e6ca7ec9db6ad92041690..e2f2a5b9b0905be24613c56533808bc4de4d4f3b 100644 (file)
@@ -1,15 +1,19 @@
 using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents.Events;
 
-public sealed class MouseMigrationRule : StationEventSystem<MouseMigrationRuleComponent>
+public sealed class MouseMigration : StationEventSystem
 {
-    protected override void Started(EntityUid uid, MouseMigrationRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public static List<string> SpawnedPrototypeChoices = new List<string>() //we double up for that ez fake probability
+        {"MobMouse", "MobMouse1", "MobMouse2", "MobRatServant"};
+
+    public override string Prototype => "MouseMigration";
+
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
+        base.Started();
 
         var modifier = GetSeverityModifier();
 
@@ -19,9 +23,9 @@ public sealed class MouseMigrationRule : StationEventSystem<MouseMigrationRuleCo
         // sqrt so we dont get insane values for ramping events
         var spawnAmount = (int) (RobustRandom.Next(7, 15) * Math.Sqrt(modifier)); // A small colony of critters.
 
-        for (var i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
+        for (int i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
         {
-            var spawnChoice = RobustRandom.Pick(component.SpawnedPrototypeChoices);
+            var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
             if (RobustRandom.Prob(Math.Min(0.01f * modifier, 1.0f)) || i == 0) //small chance for multiple, but always at least 1
                 spawnChoice = "SpawnPointGhostRatKing";
 
diff --git a/Content.Server/StationEvents/Events/PowerGridCheck.cs b/Content.Server/StationEvents/Events/PowerGridCheck.cs
new file mode 100644 (file)
index 0000000..b1f21af
--- /dev/null
@@ -0,0 +1,122 @@
+using Content.Server.Power.Components;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+using System.Threading;
+using Content.Server.Power.EntitySystems;
+using Timer = Robust.Shared.Timing.Timer;
+using System.Linq;
+using Robust.Shared.Random;
+using Content.Server.Station.Components;
+
+namespace Content.Server.StationEvents.Events
+{
+    [UsedImplicitly]
+    public sealed class PowerGridCheck : StationEventSystem
+    {
+        [Dependency] private readonly ApcSystem _apcSystem = default!;
+        [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+
+        public override string Prototype => "PowerGridCheck";
+
+        private CancellationTokenSource? _announceCancelToken;
+
+        private readonly List<EntityUid> _powered = new();
+        private readonly List<EntityUid> _unpowered = new();
+
+        private const float SecondsUntilOff = 30.0f;
+
+        private int _numberPerSecond = 0;
+        private float UpdateRate => 1.0f / _numberPerSecond;
+        private float _frameTimeAccumulator = 0.0f;
+        private float _endAfter = 0.0f;
+
+        public override void Added()
+        {
+            base.Added();
+            _endAfter = RobustRandom.Next(60, 120);
+        }
+
+        public override void Started()
+        {
+            if (StationSystem.Stations.Count == 0)
+                return;
+            var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList());
+
+            foreach (var (apc, transform) in EntityQuery<ApcComponent, TransformComponent>(true))
+            {
+                if (apc.MainBreakerEnabled && CompOrNull<StationMemberComponent>(transform.GridUid)?.Station == chosenStation)
+                    _powered.Add(apc.Owner);
+            }
+
+            RobustRandom.Shuffle(_powered);
+
+            _numberPerSecond = Math.Max(1, (int)(_powered.Count / SecondsUntilOff)); // Number of APCs to turn off every second. At least one.
+
+            base.Started();
+        }
+
+        public override void Update(float frameTime)
+        {
+            base.Update(frameTime);
+
+            if (!RuleStarted)
+                return;
+
+            if (Elapsed > _endAfter)
+            {
+                ForceEndSelf();
+                return;
+            }
+
+            var updates = 0;
+            _frameTimeAccumulator += frameTime;
+            if (_frameTimeAccumulator > UpdateRate)
+            {
+                updates = (int) (_frameTimeAccumulator / UpdateRate);
+                _frameTimeAccumulator -= UpdateRate * updates;
+            }
+
+            for (var i = 0; i < updates; i++)
+            {
+                if (_powered.Count == 0)
+                    break;
+
+                var selected = _powered.Pop();
+                if (EntityManager.Deleted(selected)) continue;
+                if (EntityManager.TryGetComponent<ApcComponent>(selected, out var apcComponent))
+                {
+                    if (apcComponent.MainBreakerEnabled)
+                        _apcSystem.ApcToggleBreaker(selected, apcComponent);
+                }
+                _unpowered.Add(selected);
+            }
+        }
+
+        public override void Ended()
+        {
+            foreach (var entity in _unpowered)
+            {
+                if (EntityManager.Deleted(entity)) continue;
+
+                if (EntityManager.TryGetComponent(entity, out ApcComponent? apcComponent))
+                {
+                    if(!apcComponent.MainBreakerEnabled)
+                        _apcSystem.ApcToggleBreaker(entity, apcComponent);
+                }
+            }
+
+            // Can't use the default EndAudio
+            _announceCancelToken?.Cancel();
+            _announceCancelToken = new CancellationTokenSource();
+            Timer.Spawn(3000, () =>
+            {
+                _audioSystem.PlayGlobal("/Audio/Announcements/power_on.ogg", Filter.Broadcast(), true, AudioParams.Default.WithVolume(-4f));
+            }, _announceCancelToken.Token);
+            _unpowered.Clear();
+
+            base.Ended();
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
deleted file mode 100644 (file)
index 78a77c9..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-using Content.Server.Power.Components;
-using JetBrains.Annotations;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-using System.Threading;
-using Content.Server.Power.EntitySystems;
-using Timer = Robust.Shared.Timing.Timer;
-using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
-using Robust.Shared.Random;
-using Content.Server.Station.Components;
-using Content.Server.StationEvents.Components;
-
-namespace Content.Server.StationEvents.Events
-{
-    [UsedImplicitly]
-    public sealed class PowerGridCheckRule : StationEventSystem<PowerGridCheckRuleComponent>
-    {
-        [Dependency] private readonly ApcSystem _apcSystem = default!;
-
-        protected override void Started(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-        {
-            base.Started(uid, component, gameRule, args);
-
-            if (StationSystem.Stations.Count == 0)
-                return;
-            var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList());
-
-            foreach (var (apc, transform) in EntityQuery<ApcComponent, TransformComponent>(true))
-            {
-                if (apc.MainBreakerEnabled && CompOrNull<StationMemberComponent>(transform.GridUid)?.Station == chosenStation)
-                    component.Powered.Add(apc.Owner);
-            }
-
-            RobustRandom.Shuffle(component.Powered);
-
-            component.NumberPerSecond = Math.Max(1, (int)(component.Powered.Count / component.SecondsUntilOff)); // Number of APCs to turn off every second. At least one.
-        }
-
-        protected override void Ended(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
-        {
-            base.Ended(uid, component, gameRule, args);
-
-            foreach (var entity in component.Unpowered)
-            {
-                if (Deleted(entity))
-                    continue;
-
-                if (TryComp(entity, out ApcComponent? apcComponent))
-                {
-                    if(!apcComponent.MainBreakerEnabled)
-                        _apcSystem.ApcToggleBreaker(entity, apcComponent);
-                }
-            }
-
-            // Can't use the default EndAudio
-            component.AnnounceCancelToken?.Cancel();
-            component.AnnounceCancelToken = new CancellationTokenSource();
-            Timer.Spawn(3000, () =>
-            {
-                Audio.PlayGlobal("/Audio/Announcements/power_on.ogg", Filter.Broadcast(), true, AudioParams.Default.WithVolume(-4f));
-            }, component.AnnounceCancelToken.Token);
-            component.Unpowered.Clear();
-        }
-
-        protected override void ActiveTick(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, float frameTime)
-        {
-            base.ActiveTick(uid, component, gameRule, frameTime);
-
-            var updates = 0;
-            component.FrameTimeAccumulator += frameTime;
-            if (component.FrameTimeAccumulator > component.UpdateRate)
-            {
-                updates = (int) (component.FrameTimeAccumulator / component.UpdateRate);
-                component.FrameTimeAccumulator -= component.UpdateRate * updates;
-            }
-
-            for (var i = 0; i < updates; i++)
-            {
-                if (component.Powered.Count == 0)
-                    break;
-
-                var selected = component.Powered.Pop();
-                if (Deleted(selected))
-                    continue;
-                if (TryComp<ApcComponent>(selected, out var apcComponent))
-                {
-                    if (apcComponent.MainBreakerEnabled)
-                        _apcSystem.ApcToggleBreaker(selected, apcComponent);
-                }
-                component.Unpowered.Add(selected);
-            }
-        }
-    }
-}
similarity index 67%
rename from Content.Server/StationEvents/Events/RandomSentienceRule.cs
rename to Content.Server/StationEvents/Events/RandomSentience.cs
index 8b2128178adf2da238277c663e77f8724caffbd7..8181e11c0d7270d45f0b7cd7d4db992a75b90988 100644 (file)
@@ -1,20 +1,22 @@
 using System.Linq;
 using Content.Server.Chat.Systems;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Ghost.Roles.Components;
 using Content.Server.Station.Systems;
 using Content.Server.StationEvents.Components;
 
 namespace Content.Server.StationEvents.Events;
 
-public sealed class RandomSentienceRule : StationEventSystem<RandomSentienceRuleComponent>
+public sealed class RandomSentience : StationEventSystem
 {
-    protected override void Started(EntityUid uid, RandomSentienceRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override string Prototype => "RandomSentience";
+
+    public override void Started()
     {
+        base.Started();
         HashSet<EntityUid> stationsToNotify = new();
 
         var mod = GetSeverityModifier();
-        var targetList = EntityQuery<SentienceTargetComponent>().ToList();
+        var targetList = EntityManager.EntityQuery<SentienceTargetComponent>().ToList();
         RobustRandom.Shuffle(targetList);
 
         var toMakeSentient = (int) (RobustRandom.Next(2, 5) * Math.Sqrt(mod));
@@ -25,10 +27,10 @@ public sealed class RandomSentienceRule : StationEventSystem<RandomSentienceRule
             if (toMakeSentient-- == 0)
                 break;
 
-            RemComp<SentienceTargetComponent>(target.Owner);
-            var ghostRole = EnsureComp<GhostRoleComponent>(target.Owner);
-            EnsureComp<GhostTakeoverAvailableComponent>(target.Owner);
-            ghostRole.RoleName = MetaData(target.Owner).EntityName;
+            EntityManager.RemoveComponent<SentienceTargetComponent>(target.Owner);
+            var ghostRole = AddComp<GhostRoleComponent>(target.Owner);
+            AddComp<GhostTakeoverAvailableComponent>(target.Owner);
+            ghostRole.RoleName = EntityManager.GetComponent<MetaDataComponent>(target.Owner).EntityName;
             ghostRole.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", ghostRole.RoleName));
             groups.Add(Loc.GetString(target.FlavorKind));
         }
@@ -41,15 +43,18 @@ public sealed class RandomSentienceRule : StationEventSystem<RandomSentienceRule
         var kind2 = groupList.Count > 1 ? groupList[1] : "???";
         var kind3 = groupList.Count > 2 ? groupList[2] : "???";
 
+        var entSysMgr = IoCManager.Resolve<IEntitySystemManager>();
+        var stationSystem = entSysMgr.GetEntitySystem<StationSystem>();
+        var chatSystem = entSysMgr.GetEntitySystem<ChatSystem>();
         foreach (var target in targetList)
         {
-            var station = StationSystem.GetOwningStation(target.Owner);
+            var station = stationSystem.GetOwningStation(target.Owner);
             if(station == null) continue;
             stationsToNotify.Add((EntityUid) station);
         }
         foreach (var station in stationsToNotify)
         {
-            ChatSystem.DispatchStationAnnouncement(
+            chatSystem.DispatchStationAnnouncement(
                 station,
                 Loc.GetString("station-event-random-sentience-announcement",
                     ("kind1", kind1), ("kind2", kind2), ("kind3", kind3), ("amount", groupList.Count),
diff --git a/Content.Server/StationEvents/Events/RevenantSpawn.cs b/Content.Server/StationEvents/Events/RevenantSpawn.cs
new file mode 100644 (file)
index 0000000..3375b2b
--- /dev/null
@@ -0,0 +1,18 @@
+namespace Content.Server.StationEvents.Events;
+
+public sealed class RevenantSpawn : StationEventSystem
+{
+    public override string Prototype => "RevenantSpawn";
+    private static readonly string RevenantPrototype = "MobRevenant";
+
+    public override void Started()
+    {
+        base.Started();
+
+        if (TryFindRandomTile(out _, out _, out _, out var coords))
+        {
+            Sawmill.Info($"Spawning revenant at {coords}");
+            EntityManager.SpawnEntity(RevenantPrototype, coords);
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/RevenantSpawnRule.cs b/Content.Server/StationEvents/Events/RevenantSpawnRule.cs
deleted file mode 100644 (file)
index 6e99fb4..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class RevenantSpawnRule : StationEventSystem<RevenantSpawnRuleComponent>
-{
-    protected override void Started(EntityUid uid, RevenantSpawnRuleComponent component, GameRuleComponent gameRule,
-        GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        if (TryFindRandomTile(out _, out _, out _, out var coords))
-        {
-            Sawmill.Info($"Spawning revenant at {coords}");
-            Spawn(component.RevenantPrototype, coords);
-        }
-    }
-}
diff --git a/Content.Server/StationEvents/Events/SolarFlare.cs b/Content.Server/StationEvents/Events/SolarFlare.cs
new file mode 100644 (file)
index 0000000..cf3a2a4
--- /dev/null
@@ -0,0 +1,78 @@
+using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.Radio.Components;
+using Content.Server.Radio;
+using Robust.Shared.Random;
+using Content.Server.Light.EntitySystems;
+using Content.Server.Light.Components;
+using Content.Shared.Radio.Components;
+using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
+
+namespace Content.Server.StationEvents.Events;
+
+public sealed class SolarFlare : StationEventSystem
+{
+    [Dependency] private readonly PoweredLightSystem _poweredLight = default!;
+    [Dependency] private readonly SharedDoorSystem _door = default!;
+
+    public override string Prototype => "SolarFlare";
+
+    private SolarFlareEventRuleConfiguration _event = default!;
+    private float _effectTimer = 0;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<RadioReceiveAttemptEvent>(OnRadioSendAttempt);
+    }
+
+    public override void Added()
+    {
+        base.Added();
+
+        if (Configuration is not SolarFlareEventRuleConfiguration ev)
+            return;
+
+        _event = ev;
+        _event.EndAfter = RobustRandom.Next(ev.MinEndAfter, ev.MaxEndAfter);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        if (!RuleStarted)
+            return;
+
+        _effectTimer -= frameTime;
+        if (_effectTimer < 0)
+        {
+            _effectTimer += 1;
+            var lightQuery = EntityQueryEnumerator<PoweredLightComponent>();
+            while (lightQuery.MoveNext(out var uid, out var light))
+            {
+                if (RobustRandom.Prob(_event.LightBreakChancePerSecond))
+                    _poweredLight.TryDestroyBulb(uid, light);
+            }
+            var airlockQuery = EntityQueryEnumerator<AirlockComponent, DoorComponent>();
+            while (airlockQuery.MoveNext(out var uid, out var airlock, out var door))
+            {
+                if (airlock.AutoClose && RobustRandom.Prob(_event.DoorToggleChancePerSecond))
+                    _door.TryToggleDoor(uid, door);
+            }
+        }
+
+        if (Elapsed > _event.EndAfter)
+        {
+            ForceEndSelf();
+            return;
+        }
+    }
+
+    private void OnRadioSendAttempt(ref RadioReceiveAttemptEvent args)
+    {
+        if (RuleStarted && _event.AffectedChannels.Contains(args.Channel.ID))
+            if (!_event.OnlyJamHeadsets || (HasComp<HeadsetComponent>(args.RadioReceiver) || HasComp<HeadsetComponent>(args.RadioSource)))
+                args.Cancelled = true;
+    }
+}
diff --git a/Content.Server/StationEvents/Events/SolarFlareRule.cs b/Content.Server/StationEvents/Events/SolarFlareRule.cs
deleted file mode 100644 (file)
index 206a8ca..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Radio;
-using Robust.Shared.Random;
-using Content.Server.Light.EntitySystems;
-using Content.Server.Light.Components;
-using Content.Server.StationEvents.Components;
-using Content.Shared.Radio.Components;
-using Content.Shared.Doors.Components;
-using Content.Shared.Doors.Systems;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class SolarFlareRule : StationEventSystem<SolarFlareRuleComponent>
-{
-    [Dependency] private readonly PoweredLightSystem _poweredLight = default!;
-    [Dependency] private readonly SharedDoorSystem _door = default!;
-
-    private float _effectTimer = 0;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-        SubscribeLocalEvent<RadioReceiveAttemptEvent>(OnRadioSendAttempt);
-    }
-
-    protected override void ActiveTick(EntityUid uid, SolarFlareRuleComponent component, GameRuleComponent gameRule, float frameTime)
-    {
-        base.ActiveTick(uid, component, gameRule, frameTime);
-
-        _effectTimer -= frameTime;
-        if (_effectTimer < 0)
-        {
-            _effectTimer += 1;
-            var lightQuery = EntityQueryEnumerator<PoweredLightComponent>();
-            while (lightQuery.MoveNext(out var lightEnt, out var light))
-            {
-                if (RobustRandom.Prob(component.LightBreakChancePerSecond))
-                    _poweredLight.TryDestroyBulb(lightEnt, light);
-            }
-            var airlockQuery = EntityQueryEnumerator<AirlockComponent, DoorComponent>();
-            while (airlockQuery.MoveNext(out var airlockEnt, out var airlock, out var door))
-            {
-                if (airlock.AutoClose && RobustRandom.Prob(component.DoorToggleChancePerSecond))
-                    _door.TryToggleDoor(airlockEnt, door);
-            }
-        }
-    }
-
-    private void OnRadioSendAttempt(ref RadioReceiveAttemptEvent args)
-    {
-        var query = EntityQueryEnumerator<SolarFlareRuleComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var flare, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                continue;
-
-            if (!flare.AffectedChannels.Contains(args.Channel.ID))
-                continue;
-
-            if (!flare.OnlyJamHeadsets || (HasComp<HeadsetComponent>(args.RadioReceiver) || HasComp<HeadsetComponent>(args.RadioSource)))
-                args.Cancelled = true;
-        }
-    }
-}
diff --git a/Content.Server/StationEvents/Events/SpiderSpawn.cs b/Content.Server/StationEvents/Events/SpiderSpawn.cs
new file mode 100644 (file)
index 0000000..cb58e43
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Server.StationEvents.Components;
+using Content.Shared.Actions;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.StationEvents.Events;
+
+public sealed class SpiderSpawn : StationEventSystem
+{
+    public override string Prototype => "SpiderSpawn";
+
+    public override void Started()
+    {
+        base.Started();
+        var spawnLocations = EntityManager.EntityQuery<VentCritterSpawnLocationComponent>().ToList();
+        RobustRandom.Shuffle(spawnLocations);
+
+        var mod = Math.Sqrt(GetSeverityModifier());
+
+        var spawnAmount = (int) (RobustRandom.Next(4, 8) * mod);
+        Sawmill.Info($"Spawning {spawnAmount} of spiders");
+        foreach (var location in spawnLocations)
+        {
+            if (spawnAmount-- == 0)
+                break;
+
+            var coords = EntityManager.GetComponent<TransformComponent>(location.Owner);
+
+            EntityManager.SpawnEntity("MobGiantSpiderAngry", coords.Coordinates);
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/SpiderSpawnRule.cs b/Content.Server/StationEvents/Events/SpiderSpawnRule.cs
deleted file mode 100644 (file)
index ba440f8..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Server.StationEvents.Components;
-using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class SpiderSpawnRule : StationEventSystem<SpiderSpawnRuleComponent>
-{
-    protected override void Started(EntityUid uid, SpiderSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-        var spawnLocations = EntityQuery<VentCritterSpawnLocationComponent>().ToList();
-        RobustRandom.Shuffle(spawnLocations);
-
-        var mod = Math.Sqrt(GetSeverityModifier());
-
-        var spawnAmount = (int) (RobustRandom.Next(4, 8) * mod);
-        Sawmill.Info($"Spawning {spawnAmount} of spiders");
-        foreach (var location in spawnLocations)
-        {
-            if (spawnAmount-- == 0)
-                break;
-
-            var xform = Transform(location.Owner);
-            Spawn("MobGiantSpiderAngry", xform.Coordinates);
-        }
-    }
-}
index bcec380987f5e3ab1676b75d192fe55d8e3d9356..bbf73f901f5c3d29e283b1dd11ea2eb22b993ed3 100644 (file)
+using System.Linq;
 using Content.Server.Administration.Logs;
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Chat.Systems;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.GameTicking.Rules.Configurations;
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
-using Content.Server.StationEvents.Components;
 using Content.Shared.Database;
+using Robust.Shared.Audio;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
-using Robust.Shared.Timing;
 
-namespace Content.Server.StationEvents.Events;
-
-/// <summary>
-///     An abstract entity system inherited by all station events for their behavior.
-/// </summary>
-public abstract class StationEventSystem<T> : GameRuleSystem<T> where T : Component
+namespace Content.Server.StationEvents.Events
 {
-    [Dependency] protected readonly IAdminLogManager AdminLogManager = default!;
-    [Dependency] private readonly IGameTiming _timing = default!;
-    [Dependency] protected readonly IMapManager MapManager = default!;
-    [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
-    [Dependency] protected readonly IRobustRandom RobustRandom = default!;
-    [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
-    [Dependency] protected readonly ChatSystem ChatSystem = default!;
-    [Dependency] protected readonly SharedAudioSystem Audio = default!;
-    [Dependency] private readonly SharedTransformSystem _transform = default!;
-    [Dependency] protected readonly StationSystem StationSystem = default!;
-
-    protected ISawmill Sawmill = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        Sawmill = Logger.GetSawmill("stationevents");
-    }
-
-    /// <inheritdoc/>
-    protected override void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    /// <summary>
+    ///     An abstract entity system inherited by all station events for their behavior.
+    /// </summary>
+    public abstract class StationEventSystem : GameRuleSystem
     {
-        base.Added(uid, component, gameRule, args);
-
-        if (!TryComp<StationEventComponent>(uid, out var stationEvent))
-            return;
+        [Dependency] protected readonly IRobustRandom RobustRandom = default!;
+        [Dependency] protected readonly IAdminLogManager AdminLogManager = default!;
+        [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
+        [Dependency] protected readonly IMapManager MapManager = default!;
+        [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+        [Dependency] protected readonly ChatSystem ChatSystem = default!;
+        [Dependency] protected readonly StationSystem StationSystem = default!;
+
+        protected ISawmill Sawmill = default!;
+
+        /// <summary>
+        ///     How long has the event existed. Do not change this.
+        /// </summary>
+        protected float Elapsed { get; set; }
+
+        public override void Initialize()
+        {
+            base.Initialize();
 
-        AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {ToPrettyString(uid)}");
+            Sawmill = Logger.GetSawmill("stationevents");
+        }
 
-        if (stationEvent.StartAnnouncement != null)
+        /// <summary>
+        ///     Called once to setup the event after StartAfter has elapsed, or if an event is forcibly started.
+        /// </summary>
+        public override void Started()
         {
-            ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.StartAnnouncement), playSound: false, colorOverride: Color.Gold);
+            AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {Configuration.Id}");
         }
 
-        Audio.PlayGlobal(stationEvent.StartAudio, Filter.Broadcast(), true);
-        stationEvent.StartTime = _timing.CurTime + stationEvent.StartDelay;
-    }
+        /// <summary>
+        ///     Called once as soon as an event is added, for announcements.
+        ///     Can also be used for some initial setup.
+        /// </summary>
+        public override void Added()
+        {
+            AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {Configuration.Id}");
 
-    /// <inheritdoc/>
-    protected override void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
+            if (Configuration is not StationEventRuleConfiguration ev)
+                return;
 
-        if (!TryComp<StationEventComponent>(uid, out var stationEvent))
-            return;
+            if (ev.StartAnnouncement != null)
+            {
+                ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.StartAnnouncement), playSound: false, colorOverride: Color.Gold);
+            }
 
-        AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {ToPrettyString(uid)}");
-        var duration = stationEvent.MaxDuration == null
-            ? stationEvent.Duration
-            : TimeSpan.FromSeconds(RobustRandom.NextDouble(stationEvent.Duration.TotalSeconds,
-                stationEvent.MaxDuration.Value.TotalSeconds));
-        stationEvent.EndTime = _timing.CurTime + duration;
-    }
+            if (ev.StartAudio != null)
+            {
+                SoundSystem.Play(ev.StartAudio.GetSound(), Filter.Broadcast(), ev.StartAudio.Params);
+            }
 
-    /// <inheritdoc/>
-    protected override void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args)
-    {
-        base.Ended(uid, component, gameRule, args);
+            Elapsed = 0;
+        }
 
-        if (!TryComp<StationEventComponent>(uid, out var stationEvent))
-            return;
+        /// <summary>
+        ///     Called once when the station event ends for any reason.
+        /// </summary>
+        public override void Ended()
+        {
+            AdminLogManager.Add(LogType.EventStopped, $"Event ended: {Configuration.Id}");
 
-        AdminLogManager.Add(LogType.EventStopped, $"Event ended: {ToPrettyString(uid)}");
+            if (Configuration is not StationEventRuleConfiguration ev)
+                return;
 
-        if (stationEvent.EndAnnouncement != null)
-        {
-            ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.EndAnnouncement), playSound: false, colorOverride: Color.Gold);
-        }
+            if (ev.EndAnnouncement != null)
+            {
+                ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.EndAnnouncement), playSound: false, colorOverride: Color.Gold);
+            }
 
-        Audio.PlayGlobal(stationEvent.EndAudio, Filter.Broadcast(), true);
-    }
+            if (ev.EndAudio != null)
+            {
+                SoundSystem.Play(ev.EndAudio.GetSound(), Filter.Broadcast(), ev.EndAudio.Params);
+            }
+        }
 
-    /// <summary>
-    ///     Called every tick when this event is running.
-    ///     Events are responsible for their own lifetime, so this handles starting and ending after time.
-    /// </summary>
-    /// <inheritdoc/>
-    public override void Update(float frameTime)
-    {
-        var query = EntityQueryEnumerator<StationEventComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var stationEvent, out var ruleData))
+        /// <summary>
+        ///     Called every tick when this event is running.
+        ///     Events are responsible for their own lifetime, so this handles starting and ending after time.
+        /// </summary>
+        public override void Update(float frameTime)
         {
-            if (!GameTicker.IsGameRuleAdded(uid, ruleData))
-                continue;
+            if (!RuleAdded || Configuration is not StationEventRuleConfiguration data)
+                return;
+
+            Elapsed += frameTime;
 
-            if (!GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.StartTime)
+            if (!RuleStarted && Elapsed >= data.StartAfter)
             {
-                GameTicker.StartGameRule(uid, ruleData);
+                GameTicker.StartGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
             }
-            else if (GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.EndTime)
+
+            if (RuleStarted && Elapsed >= data.EndAfter)
             {
-                GameTicker.EndGameRule(uid, ruleData);
+                GameTicker.EndGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
             }
         }
-    }
-
-    #region Helper Functions
-
-    protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null)
-    {
-        GameTicker.EndGameRule(uid, component);
-    }
 
-    protected bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords)
-    {
-        tile = default;
+        #region Helper Functions
 
-        targetCoords = EntityCoordinates.Invalid;
-        if (StationSystem.Stations.Count == 0)
+        protected void ForceEndSelf()
         {
-            targetStation = EntityUid.Invalid;
-            targetGrid = EntityUid.Invalid;
-            return false;
+            GameTicker.EndGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
         }
-        targetStation = RobustRandom.Pick(StationSystem.Stations);
-        var possibleTargets = Comp<StationDataComponent>(targetStation).Grids;
-        if (possibleTargets.Count == 0)
+
+        protected bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords)
         {
-            targetGrid = EntityUid.Invalid;
-            return false;
-        }
+            tile = default;
 
-        targetGrid = RobustRandom.Pick(possibleTargets);
+            targetCoords = EntityCoordinates.Invalid;
+            if (StationSystem.Stations.Count == 0)
+            {
+                targetStation = EntityUid.Invalid;
+                targetGrid = EntityUid.Invalid;
+                return false;
+            }
+            targetStation = RobustRandom.Pick(StationSystem.Stations);
+            var possibleTargets = Comp<StationDataComponent>(targetStation).Grids;
+            if (possibleTargets.Count == 0)
+            {
+                targetGrid = EntityUid.Invalid;
+                return false;
+            }
 
-        if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
-            return false;
+            targetGrid = RobustRandom.Pick(possibleTargets);
 
-        var found = false;
-        var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(targetGrid);
-        var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
+            if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
+                return false;
 
-        for (var i = 0; i < 10; i++)
-        {
-            var randomX = RobustRandom.Next((int) gridBounds.Left, (int) gridBounds.Right);
-            var randomY = RobustRandom.Next((int) gridBounds.Bottom, (int) gridBounds.Top);
+            var found = false;
+            var (gridPos, _, gridMatrix) = Transform(targetGrid).GetWorldPositionRotationMatrix();
+            var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
 
-            tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y);
-            if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile,
-                    mapGridComp: gridComp)
-                || _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp))
+            for (var i = 0; i < 10; i++)
             {
-                continue;
+                var randomX = RobustRandom.Next((int) gridBounds.Left, (int) gridBounds.Right);
+                var randomY = RobustRandom.Next((int) gridBounds.Bottom, (int) gridBounds.Top);
+
+                tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y);
+                if (_atmosphere.IsTileSpace(gridComp.Owner, Transform(targetGrid).MapUid, tile,
+                        mapGridComp: gridComp)
+                    || _atmosphere.IsTileAirBlocked(gridComp.Owner, tile, mapGridComp: gridComp))
+                {
+                    continue;
+                }
+
+                found = true;
+                targetCoords = gridComp.GridTileToLocal(tile);
+                break;
             }
 
-            found = true;
-            targetCoords = gridComp.GridTileToLocal(tile);
-            break;
+            if (!found) return false;
+
+            return true;
         }
 
-        return found;
-    }
-    public float GetSeverityModifier()
-    {
-        var ev = new GetSeverityModifierEvent();
-        RaiseLocalEvent(ev);
-        return ev.Modifier;
-    }
+        public static GameRulePrototype GetRandomEventUnweighted(IPrototypeManager? prototypeManager = null, IRobustRandom? random = null)
+        {
+            IoCManager.Resolve(ref prototypeManager, ref random);
 
-    #endregion
-}
+            return random.Pick(prototypeManager.EnumeratePrototypes<GameRulePrototype>()
+                .Where(p => p.Configuration is StationEventRuleConfiguration).ToArray());
+        }
+
+        public float GetSeverityModifier()
+        {
+            var ev = new GetSeverityModifierEvent();
+            RaiseLocalEvent(ev);
+            return ev.Modifier;
+        }
+
+        #endregion
+    }
 
-/// <summary>
-///     Raised broadcast to determine what the severity modifier should be for an event, some positive number that can be multiplied with various things.
-///     Handled by usually other game rules (like the ramping scheduler).
-///     Most events should try and make use of this if possible.
-/// </summary>
-public sealed class GetSeverityModifierEvent : EntityEventArgs
-{
     /// <summary>
-    ///     Should be multiplied/added to rather than set, for commutativity.
+    ///     Raised broadcast to determine what the severity modifier should be for an event, some positive number that can be multiplied with various things.
+    ///     Handled by usually other game rules (like the ramping scheduler).
+    ///     Most events should try and make use of this if possible.
     /// </summary>
-    public float Modifier = 1.0f;
+    public sealed class GetSeverityModifierEvent : EntityEventArgs
+    {
+        /// <summary>
+        ///     Should be multiplied/added to rather than set, for commutativity.
+        /// </summary>
+        public float Modifier = 1.0f;
+    }
 }
similarity index 73%
rename from Content.Server/StationEvents/Events/VentClogRule.cs
rename to Content.Server/StationEvents/Events/VentClog.cs
index c209337aec51f7189f290724d372000b0742ff6e..04e466f66b5c77dd3b660d110354a4cda5168ef2 100644 (file)
@@ -8,19 +8,23 @@ using Robust.Shared.Random;
 using System.Linq;
 using Content.Server.Chemistry.Components;
 using Content.Server.Fluids.EntitySystems;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
+using Robust.Server.GameObjects;
 
 namespace Content.Server.StationEvents.Events;
 
 [UsedImplicitly]
-public sealed class VentClogRule : StationEventSystem<VentClogRuleComponent>
+public sealed class VentClog : StationEventSystem
 {
-    [Dependency] private readonly SmokeSystem _smoke = default!;
+    public override string Prototype => "VentClog";
 
-    protected override void Started(EntityUid uid, VentClogRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public readonly IReadOnlyList<string> SafeishVentChemicals = new[]
     {
-        base.Started(uid, component, gameRule, args);
+        "Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer"
+    };
+
+    public override void Started()
+    {
+        base.Started();
 
         if (StationSystem.Stations.Count == 0)
             return;
@@ -53,14 +57,15 @@ public sealed class VentClogRule : StationEventSystem<VentClogRuleComponent>
             }
             else
             {
-                solution.AddReagent(RobustRandom.Pick(component.SafeishVentChemicals), 200);
+                solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 200);
             }
 
             var foamEnt = Spawn("Foam", transform.Coordinates);
             var smoke = EnsureComp<SmokeComponent>(foamEnt);
             smoke.SpreadAmount = 20;
-            _smoke.Start(foamEnt, smoke, solution, 20f);
-            Audio.PlayPvs(sound, transform.Coordinates);
+            EntityManager.System<SmokeSystem>().Start(foamEnt, smoke, solution, 20f);
+            EntityManager.System<AudioSystem>().PlayPvs(sound, transform.Coordinates);
         }
     }
+
 }
diff --git a/Content.Server/StationEvents/Events/VentCritters.cs b/Content.Server/StationEvents/Events/VentCritters.cs
new file mode 100644 (file)
index 0000000..5d61e10
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Server.StationEvents.Components;
+using Content.Shared.Actions;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.StationEvents.Events;
+
+public sealed class VentCritters : StationEventSystem
+{
+    public static List<string> SpawnedPrototypeChoices = new List<string>()
+        {"MobMouse", "MobMouse1", "MobMouse2"};
+
+    public override string Prototype => "VentCritters";
+
+    public override void Started()
+    {
+        base.Started();
+        var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
+        var spawnLocations = EntityManager.EntityQuery<VentCritterSpawnLocationComponent>().ToList();
+        RobustRandom.Shuffle(spawnLocations);
+
+        var spawnAmount = (int) (RobustRandom.Next(4, 12)); // A small colony of critters.
+        Sawmill.Info($"Spawning {spawnAmount} of {spawnChoice}");
+        foreach (var location in spawnLocations)
+        {
+            if (spawnAmount-- == 0)
+                break;
+
+            var coords = EntityManager.GetComponent<TransformComponent>(location.Owner);
+
+            EntityManager.SpawnEntity(spawnChoice, coords.Coordinates);
+        }
+    }
+}
diff --git a/Content.Server/StationEvents/Events/VentCrittersRule.cs b/Content.Server/StationEvents/Events/VentCrittersRule.cs
deleted file mode 100644 (file)
index 6968288..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Server.StationEvents.Components;
-using Robust.Shared.Random;
-using System.Linq;
-using Content.Server.GameTicking.Rules.Components;
-
-namespace Content.Server.StationEvents.Events;
-
-public sealed class VentCrittersRule : StationEventSystem<VentCrittersRuleComponent>
-{
-    protected override void Started(EntityUid uid, VentCrittersRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
-    {
-        base.Started(uid, component, gameRule, args);
-
-        var spawnChoice = RobustRandom.Pick(component.SpawnedPrototypeChoices);
-        var spawnLocations = EntityManager.EntityQuery<VentCritterSpawnLocationComponent>().ToList();
-        RobustRandom.Shuffle(spawnLocations);
-
-        var spawnAmount = RobustRandom.Next(4, 12); // A small colony of critters.
-        Sawmill.Info($"Spawning {spawnAmount} of {spawnChoice}");
-        foreach (var location in spawnLocations)
-        {
-            if (spawnAmount-- == 0)
-                break;
-
-            var coords = Transform(location.Owner);
-            Spawn(spawnChoice, coords.Coordinates);
-        }
-    }
-}
index 5c972df52e1b96db6f2b1149b1480ab3c0bc5680..d22e2d86dc7c5673cfc5226034030731a7d0f4f4 100644 (file)
@@ -1,7 +1,5 @@
 using Content.Server.GameTicking;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.StationEvents.Components;
 using Content.Server.StationEvents.Events;
 using Content.Shared.CCVar;
 using Robust.Shared.Configuration;
@@ -9,20 +7,35 @@ using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents;
 
-public sealed class RampingStationEventSchedulerSystem : GameRuleSystem<RampingStationEventSchedulerComponent>
+public sealed class RampingStationEventSchedulerSystem : GameRuleSystem
 {
+    public override string Prototype => "RampingStationEventScheduler";
+
     [Dependency] private readonly IConfigurationManager _cfg = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly EventManagerSystem _event = default!;
     [Dependency] private readonly GameTicker _gameTicker = default!;
 
-    public float GetChaosModifier(EntityUid uid, RampingStationEventSchedulerComponent component)
+    [ViewVariables(VVAccess.ReadWrite)]
+    private float _endTime;
+    [ViewVariables(VVAccess.ReadWrite)]
+    private float _maxChaos;
+    [ViewVariables(VVAccess.ReadWrite)]
+    private float _startingChaos;
+    [ViewVariables(VVAccess.ReadWrite)]
+    private float _timeUntilNextEvent;
+
+    [ViewVariables]
+    public float ChaosModifier
     {
-        var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds;
-        if (roundTime > component.EndTime)
-            return component.MaxChaos;
+        get
+        {
+            var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds;
+            if (roundTime > _endTime)
+                return _maxChaos;
 
-        return component.MaxChaos / component.EndTime * roundTime + component.StartingChaos;
+            return (_maxChaos / _endTime) * roundTime + _startingChaos;
+        }
     }
 
     public override void Initialize()
@@ -32,65 +45,60 @@ public sealed class RampingStationEventSchedulerSystem : GameRuleSystem<RampingS
         SubscribeLocalEvent<GetSeverityModifierEvent>(OnGetSeverityModifier);
     }
 
-    protected override void Started(EntityUid uid, RampingStationEventSchedulerComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    public override void Started()
     {
-        base.Started(uid, component, gameRule, args);
-
         var avgChaos = _cfg.GetCVar(CCVars.EventsRampingAverageChaos);
         var avgTime = _cfg.GetCVar(CCVars.EventsRampingAverageEndTime);
 
         // Worlds shittiest probability distribution
         // Got a complaint? Send them to
-        component.MaxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4);
+        _maxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4);
         // This is in minutes, so *60 for seconds (for the chaos calc)
-        component.EndTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f;
-        component.StartingChaos = component.MaxChaos / 10;
+        _endTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f;
+        _startingChaos = _maxChaos / 10;
 
-        PickNextEventTime(uid, component);
+        PickNextEventTime();
+    }
+
+    public override void Ended()
+    {
+        _endTime = 0f;
+        _maxChaos = 0f;
+        _startingChaos = 0f;
+        _timeUntilNextEvent = 0f;
     }
 
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
 
-        if (!_event.EventsEnabled)
+        if (!RuleStarted || !_event.EventsEnabled)
             return;
 
-        var query = EntityQueryEnumerator<RampingStationEventSchedulerComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var scheduler, out var gameRule))
+        if (_timeUntilNextEvent > 0f)
         {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                return;
-
-            if (scheduler.TimeUntilNextEvent > 0f)
-            {
-                scheduler.TimeUntilNextEvent -= frameTime;
-                return;
-            }
-
-            PickNextEventTime(uid, scheduler);
-            _event.RunRandomEvent();
+            _timeUntilNextEvent -= frameTime;
+            return;
         }
+
+        PickNextEventTime();
+        _event.RunRandomEvent();
     }
 
     private void OnGetSeverityModifier(GetSeverityModifierEvent ev)
     {
-        var query = EntityQueryEnumerator<RampingStationEventSchedulerComponent, GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var scheduler, out var gameRule))
-        {
-            if (!GameTicker.IsGameRuleActive(uid, gameRule))
-                return;
+        if (!RuleStarted)
+            return;
 
-            ev.Modifier *= GetChaosModifier(uid, scheduler);
-            Logger.Info($"Ramping set modifier to {ev.Modifier}");
-        }
+        ev.Modifier *= ChaosModifier;
+        Logger.Info($"Ramping set modifier to {ev.Modifier}");
     }
 
-    private void PickNextEventTime(EntityUid uid, RampingStationEventSchedulerComponent component)
+    private void PickNextEventTime()
     {
-        var mod = GetChaosModifier(uid, component);
+        var mod = ChaosModifier;
 
         // 4-12 minutes baseline. Will get faster over time as the chaos mod increases.
-        component.TimeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod);
+        _timeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod);
     }
 }
diff --git a/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs b/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs
new file mode 100644 (file)
index 0000000..7c07cbd
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Server.Chat.Managers;
+using Content.Shared.Roles;
+
+namespace Content.Server.Suspicion.Roles
+{
+    public sealed class SuspicionInnocentRole : SuspicionRole
+    {
+        public AntagPrototype Prototype { get; }
+
+        public SuspicionInnocentRole(Mind.Mind mind, AntagPrototype antagPrototype) : base(mind)
+        {
+            Prototype = antagPrototype;
+            Name = Loc.GetString(antagPrototype.Name);
+            Antagonist = antagPrototype.Antagonist;
+        }
+
+        public override string Name { get; }
+        public string Objective => Loc.GetString(Prototype.Objective);
+        public override bool Antagonist { get; }
+
+        public override void Greet()
+        {
+            base.Greet();
+
+            var chat = IoCManager.Resolve<IChatManager>();
+
+            if (Mind.TryGetSession(out var session))
+            {
+                chat.DispatchServerMessage(session, $"You're an {Name}!");
+                chat.DispatchServerMessage(session, $"Objective: {Objective}");
+            }
+        }
+    }
+}
diff --git a/Content.Server/Suspicion/Roles/SuspicionRole.cs b/Content.Server/Suspicion/Roles/SuspicionRole.cs
new file mode 100644 (file)
index 0000000..f63cacb
--- /dev/null
@@ -0,0 +1,9 @@
+using Content.Server.Roles;
+
+namespace Content.Server.Suspicion.Roles
+{
+    public abstract class SuspicionRole : Role
+    {
+        protected SuspicionRole(Mind.Mind mind) : base(mind) { }
+    }
+}
diff --git a/Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs b/Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs
new file mode 100644 (file)
index 0000000..b42b22d
--- /dev/null
@@ -0,0 +1,41 @@
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Shared.Roles;
+
+namespace Content.Server.Suspicion.Roles
+{
+    public sealed class SuspicionTraitorRole : SuspicionRole
+    {
+        public AntagPrototype Prototype { get; }
+
+        public SuspicionTraitorRole(Mind.Mind mind, AntagPrototype antagPrototype) : base(mind)
+        {
+            Prototype = antagPrototype;
+            Name = Loc.GetString(antagPrototype.Name);
+            Antagonist = antagPrototype.Antagonist;
+        }
+
+        public override string Name { get; }
+        public string Objective => Loc.GetString(Prototype.Objective);
+        public override bool Antagonist { get; }
+
+        public void GreetSuspicion(List<SuspicionTraitorRole> traitors, IChatManager chatMgr)
+        {
+            if (Mind.TryGetSession(out var session))
+            {
+                chatMgr.DispatchServerMessage(session, Loc.GetString("suspicion-role-greeting", ("roleName", Name)));
+                chatMgr.DispatchServerMessage(session, Loc.GetString("suspicion-objective", ("objectiveText", Objective)));
+
+                var allPartners = string.Join(", ", traitors.Where(p => p != this).Select(p => p.Mind.CharacterName));
+
+                var partnerText = Loc.GetString(
+                    "suspicion-partners-in-crime",
+                    ("partnersCount", traitors.Count-1),
+                    ("partnerNames", allPartners)
+                );
+
+                chatMgr.DispatchServerMessage(session, partnerText);
+            }
+        }
+    }
+}
diff --git a/Content.Server/Suspicion/SuspicionItemComponent.cs b/Content.Server/Suspicion/SuspicionItemComponent.cs
new file mode 100644 (file)
index 0000000..cca978d
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Server.Suspicion;
+
+/// <summary>
+///     Tag component meant for bookkeeping items spawned by the suspicion rule.
+/// </summary>
+[RegisterComponent]
+public sealed class SuspicionItemComponent : Component
+{
+}
diff --git a/Content.Server/Suspicion/SuspicionRoleComponent.cs b/Content.Server/Suspicion/SuspicionRoleComponent.cs
new file mode 100644 (file)
index 0000000..5996425
--- /dev/null
@@ -0,0 +1,138 @@
+using System.Linq;
+using Content.Server.GameTicking.Rules;
+using Content.Server.Mind.Components;
+using Content.Server.Roles;
+using Content.Server.Suspicion.Roles;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Suspicion;
+
+namespace Content.Server.Suspicion
+{
+    [RegisterComponent]
+    public sealed class SuspicionRoleComponent : SharedSuspicionRoleComponent
+    {
+        [Dependency] private readonly IEntityManager _entMan = default!;
+        private Role? _role;
+        [ViewVariables]
+        private readonly HashSet<SuspicionRoleComponent> _allies = new();
+
+        [ViewVariables]
+        public Role? Role
+        {
+            get => _role;
+            set
+            {
+                if (_role == value)
+                {
+                    return;
+                }
+
+                _role = value;
+
+                Dirty();
+
+                var sus = EntitySystem.Get<SuspicionRuleSystem>();
+
+                if (value == null || !value.Antagonist)
+                {
+                    ClearAllies();
+                    sus.RemoveTraitor(this);
+                }
+                else if (value.Antagonist)
+                {
+                    SetAllies(sus.Traitors);
+                    sus.AddTraitor(this);
+                }
+            }
+        }
+
+        [ViewVariables] public bool KnowsAllies => IsTraitor();
+
+        public bool IsDead()
+        {
+            return _entMan.TryGetComponent(Owner, out MobStateComponent? state) &&
+                   _entMan.EntitySysManager.GetEntitySystem<MobStateSystem>().IsDead(Owner, state);
+        }
+
+        public bool IsInnocent()
+        {
+            return !IsTraitor();
+        }
+
+        public bool IsTraitor()
+        {
+            return Role?.Antagonist ?? false;
+        }
+
+        public void SyncRoles()
+        {
+            if (!_entMan.TryGetComponent(Owner, out MindComponent? mind) ||
+                !mind.HasMind)
+            {
+                return;
+            }
+
+            Role = mind.Mind!.AllRoles.First(role => role is SuspicionRole);
+        }
+
+        public void AddAlly(SuspicionRoleComponent ally)
+        {
+            if (ally == this)
+            {
+                return;
+            }
+
+            _allies.Add(ally);
+        }
+
+        public bool RemoveAlly(SuspicionRoleComponent ally)
+        {
+            if (_allies.Remove(ally))
+            {
+                Dirty();
+
+                return true;
+            }
+
+            return false;
+        }
+
+        public void SetAllies(IEnumerable<SuspicionRoleComponent> allies)
+        {
+            _allies.Clear();
+
+            _allies.UnionWith(allies.Where(a => a != this));
+
+            Dirty();
+        }
+
+        public void ClearAllies()
+        {
+            _allies.Clear();
+
+            Dirty();
+        }
+        public override ComponentState GetComponentState()
+        {
+            if (Role == null)
+            {
+                return new SuspicionRoleComponentState(null, null, Array.Empty<(string, EntityUid)>());
+            }
+
+            var allies = new List<(string name, EntityUid)>();
+
+            foreach (var role in _allies)
+            {
+                if (role.Role?.Mind.CharacterName == null)
+                {
+                    continue;
+                }
+
+                allies.Add((role.Role!.Mind.CharacterName, role.Owner));
+            }
+
+            return new SuspicionRoleComponentState(Role?.Name, Role?.Antagonist, allies.ToArray());
+        }
+    }
+}
diff --git a/Content.Server/Suspicion/SuspicionRoleSystem.cs b/Content.Server/Suspicion/SuspicionRoleSystem.cs
new file mode 100644 (file)
index 0000000..d23269b
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Shared.Examine;
+
+namespace Content.Server.Suspicion
+{
+    public sealed class SuspicionRoleSystem : EntitySystem
+    {
+        public override void Initialize()
+        {
+            base.Initialize();
+            SubscribeLocalEvent<SuspicionRoleComponent, ExaminedEvent>(OnExamined);
+        }
+        private void OnExamined(EntityUid uid, SuspicionRoleComponent component, ExaminedEvent args)
+        {
+           if (!component.IsDead())
+            {
+                return;
+            }
+
+            var traitor = component.IsTraitor();
+            var color = traitor ? "red" : "green";
+            var role = traitor ? "suspicion-role-component-role-traitor" : "suspicion-role-component-role-innocent";
+            var article = traitor ? "generic-article-a" : "generic-article-an";
+
+            var tooltip = Loc.GetString("suspicion-role-component-on-examine-tooltip",
+                                        ("article", Loc.GetString(article)),
+                                        ("colorName", color),
+                                        ("role",Loc.GetString(role)));
+
+            args.PushMarkup(tooltip);
+        }
+    }
+}
diff --git a/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs b/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs
new file mode 100644 (file)
index 0000000..9ea08c4
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.TraitorDeathMatch.Components
+{
+    [RegisterComponent]
+    public sealed class TraitorDeathMatchRedemptionComponent : Component
+    {
+    }
+}
diff --git a/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs b/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs
new file mode 100644 (file)
index 0000000..820ccbc
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.Network;
+
+namespace Content.Server.TraitorDeathMatch.Components
+{
+    [RegisterComponent]
+    public sealed class TraitorDeathMatchReliableOwnerTagComponent : Component
+    {
+        [ViewVariables]
+        public NetUserId? UserId { get; set; }
+    }
+}
+
diff --git a/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs b/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs
new file mode 100644 (file)
index 0000000..9def8fc
--- /dev/null
@@ -0,0 +1,108 @@
+using Content.Server.Mind.Components;
+using Content.Server.TraitorDeathMatch.Components;
+using Content.Server.Store.Components;
+using Content.Server.Store.Systems;
+using Content.Server.Traitor.Uplink;
+using Content.Shared.FixedPoint;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+
+namespace Content.Server.TraitorDeathMatch;
+
+public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem
+{
+    [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly UplinkSystem _uplink = default!;
+    [Dependency] private readonly StoreSystem _store = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<TraitorDeathMatchRedemptionComponent, InteractUsingEvent>(OnInteractUsing);
+    }
+
+    private void OnInteractUsing(EntityUid uid, TraitorDeathMatchRedemptionComponent component, InteractUsingEvent args)
+    {
+        if (!EntityManager.TryGetComponent<MindComponent>(args.User, out var userMindComponent))
+        {
+            _popup.PopupEntity(Loc.GetString(
+                "traitor-death-match-redemption-component-interact-using-main-message",
+                ("secondMessage",
+                    Loc.GetString("traitor-death-match-redemption-component-interact-using-no-mind-message"))), uid, args.User);
+            return;
+        }
+
+        var userMind = userMindComponent.Mind;
+        if (userMind == null)
+        {
+            _popup.PopupEntity(Loc.GetString(
+                "traitor-death-match-redemption-component-interact-using-main-message",
+                ("secondMessage",
+                    Loc.GetString("traitor-death-match-redemption-component-interact-using-no-user-mind-message"))), uid, args.User);
+            return;
+        }
+
+        if (!EntityManager.TryGetComponent<StoreComponent>(args.Used, out var victimUplink))
+        {
+            _popup.PopupEntity(Loc.GetString(
+                "traitor-death-match-redemption-component-interact-using-main-message",
+                ("secondMessage",
+                    Loc.GetString("traitor-death-match-redemption-component-interact-using-no-pda-message"))), uid, args.User);
+            return;
+        }
+
+        if (!EntityManager.TryGetComponent<TraitorDeathMatchReliableOwnerTagComponent>(args.Used,
+                out var victimPDAuid))
+        {
+            _popup.PopupEntity(Loc.GetString(
+                "traitor-death-match-redemption-component-interact-using-main-message",
+                ("secondMessage",
+                    Loc.GetString("traitor-death-match-redemption-component-interact-using-no-pda-owner-message"))), uid, args.User);
+            return;
+        }
+
+        if (victimPDAuid.UserId == userMind.UserId)
+        {
+            _popup.PopupEntity(Loc.GetString(
+                "traitor-death-match-redemption-component-interact-using-main-message",
+                ("secondMessage",
+                    Loc.GetString(
+                        "traitor-death-match-redemption-component-interact-using-pda-different-user-message"))), uid, args.User);
+            return;
+        }
+
+        StoreComponent? userUplink = null;
+
+        if (_inventory.TryGetSlotEntity(args.User, "id", out var pdaUid) &&
+            EntityManager.TryGetComponent<StoreComponent>(pdaUid, out var userUplinkComponent))
+            userUplink = userUplinkComponent;
+
+        if (userUplink == null)
+        {
+            _popup.PopupEntity(Loc.GetString(
+                "traitor-death-match-redemption-component-interact-using-main-message",
+                ("secondMessage",
+                    Loc.GetString(
+                        "traitor-death-match-redemption-component-interact-using-no-pda-in-pocket-message"))), uid, args.User);
+            return;
+        }
+
+
+        // We have finally determined both PDA components. FINALLY.
+
+        // 4 is the per-PDA bonus amount
+        var transferAmount = _uplink.GetTCBalance(victimUplink) + 4;
+        victimUplink.Balance.Clear();
+        _store.TryAddCurrency(new Dictionary<string, FixedPoint2>() { {"Telecrystal", FixedPoint2.New(transferAmount)}}, userUplink.Owner, userUplink);
+
+        EntityManager.DeleteEntity(victimUplink.Owner);
+
+        _popup.PopupEntity(Loc.GetString("traitor-death-match-redemption-component-interact-using-success-message",
+                ("tcAmount", transferAmount)), uid, args.User);
+
+        args.Handled = true;
+    }
+}
diff --git a/Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs b/Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs
new file mode 100644 (file)
index 0000000..09d8a6c
--- /dev/null
@@ -0,0 +1,25 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Suspicion
+{
+    [NetworkedComponent()]
+    public abstract class SharedSuspicionRoleComponent : Component
+    {
+    }
+
+    [Serializable, NetSerializable]
+    public sealed class SuspicionRoleComponentState : ComponentState
+    {
+        public readonly string? Role;
+        public readonly bool? Antagonist;
+        public readonly (string name, EntityUid)[] Allies;
+
+        public SuspicionRoleComponentState(string? role, bool? antagonist, (string name, EntityUid)[] allies)
+        {
+            Role = role;
+            Antagonist = antagonist;
+            Allies = allies;
+        }
+    }
+}
diff --git a/Content.Shared/Suspicion/SuspicionMessages.cs b/Content.Shared/Suspicion/SuspicionMessages.cs
new file mode 100644 (file)
index 0000000..7254e2d
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Suspicion
+{
+    public static class SuspicionMessages
+    {
+        [Serializable, NetSerializable]
+        public sealed class SetSuspicionEndTimerMessage : EntityEventArgs
+        {
+            public TimeSpan? EndTime;
+        }
+    }
+}
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml
new file mode 100644 (file)
index 0000000..e4c804a
--- /dev/null
@@ -0,0 +1,16 @@
+- type: entity
+  name: PDA Redemption Machine Spawner
+  id: TraitorDMRedemptionMachineSpawner
+  parent: MarkerBase
+  components:
+    - type: Sprite
+      layers:
+        - state: blue
+        - sprite: Structures/Machines/traitordm.rsi
+          state: redemption
+    - type: ConditionalSpawner
+      prototypes:
+        - TraitorDMRedemptionMachine
+      chance: 1.0
+      gameRules:
+        - TraitorDeathMatch
diff --git a/Resources/Prototypes/Entities/Structures/Machines/traitordm.yml b/Resources/Prototypes/Entities/Structures/Machines/traitordm.yml
new file mode 100644 (file)
index 0000000..7c15a09
--- /dev/null
@@ -0,0 +1,28 @@
+- type: entity
+  id: TraitorDMRedemptionMachine
+  parent: BaseMachinePowered
+  name: traitor deathmatch pda redemption machine
+  description: Put someone else's PDA into this to get telecrystals.
+  components:
+  - type: Sprite
+    layers:
+      - sprite: Structures/Machines/traitordm.rsi
+        state: redemption
+      - sprite: Structures/Machines/traitordm.rsi
+        state: redemption-unshaded
+        shader: unshaded
+  - type: Physics
+    bodyType: Static
+  - type: Fixtures
+    fixtures:
+    - shape:
+        !type:PhysShapeAabb
+        bounds: "-0.25,-0.25,0.25,0.25"
+      density: 190
+      mask:
+      - MachineMask
+      layer:
+      - MachineLayer
+  - type: TraitorDeathMatchRedemption
+  placement:
+    mode: AlignTileAny
index 5b54e1a60ddc436563925e7fa4a9f984bafdd84a..8a2c7b71b3733ceb83ff85e61cff4fbbd2e35815 100644 (file)
-- type: entity
+- type: gameRule
   id: AnomalySpawn
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: AnomalySpawn
     weight: 10
     startAfter: 30
-    duration: 35
-  - type: AnomalySpawnRule
+    endAfter: 35
 
-- type: entity
+- type: gameRule
   id: BluespaceArtifact
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: BluespaceArtifact
     weight: 5
     startAfter: 30
-    duration: 35
-  - type: BluespaceArtifactRule
+    endAfter: 35
 
-- type: entity
-  id: BluespaceLocker
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+- type: gameRule
+  id: BluespaceLockerLink
+  config:
+    !type:StationEventRuleConfiguration
+    id: BluespaceLockerLink
     weight: 0
     reoccurrenceDelay: 5
     earliestStart: 1
-    duration: 1
-  - type: BluespaceLockerRule
+    endAfter: 1
 
-- type: entity
+- type: gameRule
   id: BreakerFlip
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: BreakerFlip
     weight: 10
-    duration: 1
+    endAfter: 1
     minimumPlayers: 15
-  - type: BreakerFlipRule
 
-- type: entity
+- type: gameRule
   id: BureaucraticError
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: BureaucraticError
     startAnnouncement: station-event-bureaucratic-error-announcement
     minimumPlayers: 25
     weight: 5
-    duration: 1
-  - type: BureaucraticErrorRule
+    endAfter: 1
 
-- type: entity
+- type: gameRule
   id: DiseaseOutbreak
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
     startAnnouncement: station-event-disease-outbreak-announcement
     startAudio:
       path: /Audio/Announcements/outbreak7.ogg
       params:
         volume: -4
+    id: DiseaseOutbreak
     weight: 5
-    duration: 1
+    endAfter: 1
     earliestStart: 15
-  - type: DiseaseOutbreakRule
 
-- type: entity
+- type: gameRule
   id: Dragon
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: Dragon
     weight: 10
-    duration: 2
+    endAfter: 2
     earliestStart: 15
     minimumPlayers: 15
-  - type: DragonRule
 
-- type: entity
+- type: gameRule
   id: RevenantSpawn
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: RevenantSpawn
     weight: 5
-    duration: 1
+    endAfter: 1
     earliestStart: 45
     minimumPlayers: 20
-  - type: RevenantSpawnRule
 
-- type: entity
+- type: gameRule
   id: FalseAlarm
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: FalseAlarm
     weight: 15
-    duration: 1
-  - type: FalseAlarmRule
+    endAfter: 1
 
-- type: entity
+- type: gameRule
   id: GasLeak
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: GasLeak
     startAnnouncement: station-event-gas-leak-start-announcement
     startAudio:
       path: /Audio/Announcements/attention.ogg
     minimumPlayers: 5
     weight: 5
     startAfter: 20
-  - type: GasLeakRule
 
-- type: entity
+- type: gameRule
   id: KudzuGrowth
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: KudzuGrowth
     earliestStart: 15
     minimumPlayers: 15
     weight: 5
     startAfter: 50
-    duration: 240
-  - type: KudzuGrowthRule
+    endAfter: 240
 
-- type: entity
+- type: gameRule
   id: MeteorSwarm
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: MeteorSwarm
     earliestStart: 30
     weight: 5
     minimumPlayers: 20
       params:
         volume: -4
     startAfter: 30
-  - type: MeteorSwarmRule
 
-- type: entity
+- type: gameRule
   id: MouseMigration
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: MouseMigration
     earliestStart: 30
     minimumPlayers: 35
     weight: 5
-    duration: 50
-  - type: MouseMigrationRule
+    endAfter: 50
 
-- type: entity
+- type: gameRule
   id: PowerGridCheck
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: PowerGridCheck
     weight: 10
     startAnnouncement: station-event-power-grid-check-start-announcement
     endAnnouncement: station-event-power-grid-check-end-announcement
       params:
        volume: -4
     startAfter: 12
-    duration: 60
-    maxDuration: 120
-  - type: PowerGridCheckRule
 
-- type: entity
+- type: gameRule
   id: RandomSentience
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: RandomSentience
     weight: 10
-    duration: 1
+    endAfter: 1
     startAudio:
       path: /Audio/Announcements/attention.ogg
-  - type: RandomSentienceRule
 
-- type: entity
+- type: gameRule
   id: SolarFlare
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config: !type:SolarFlareEventRuleConfiguration
+    id: SolarFlare
     weight: 10
     startAnnouncement: station-event-solar-flare-start-announcement
     endAnnouncement: station-event-solar-flare-end-announcement
     startAudio:
       path: /Audio/Announcements/attention.ogg
-    duration: 120
-    maxDuration: 240
-  - type: SolarFlareRule
+    minEndAfter: 120
+    maxEndAfter: 240
     onlyJamHeadsets: true
     affectedChannels:
     - Common
     lightBreakChancePerSecond: 0.0003
     doorToggleChancePerSecond: 0.001
 
-- type: entity
+- type: gameRule
   id: VentClog
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: VentClog
     startAnnouncement: station-event-vent-clog-start-announcement
     startAudio:
       path: /Audio/Announcements/attention.ogg
     minimumPlayers: 15
     weight: 5
     startAfter: 50
-    duration: 60
-  - type: VentClogRule
+    endAfter: 60
 
-- type: entity
+- type: gameRule
   id: VentCritters
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
     id: VentCritters
     earliestStart: 15
     minimumPlayers: 15
     weight: 5
-    duration: 60
-  - type: VentCrittersRule
+    endAfter: 60
 
-- type: entity
+- type: gameRule
   id: SpiderSpawn
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: SpiderSpawn
     earliestStart: 20
     minimumPlayers: 15
     weight: 5
-    duration: 60
-  - type: SpiderSpawnRule
+    endAfter: 60
 
-- type: entity
+- type: gameRule
   id: ZombieOutbreak
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: Zombie
     earliestStart: 50
     weight: 2.5
-    duration: 1
-  - type: ZombieRule
+    endAfter: 1
 
-- type: entity
+- type: gameRule
   id: LoneOpsSpawn
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: StationEvent
+  config:
+    !type:StationEventRuleConfiguration
+    id: LoneOpsSpawn
     earliestStart: 55
     weight: 5
     minimumPlayers: 10
     reoccurrenceDelay: 25
-    duration: 1
-  - type: LoneOpsSpawnRule
-  - type: NukeopsRule
+    endAfter: 1
index 0d493ba658dd11a6fc1563df86c07c8b57042723..5a62ca270619b50d8d5acdd625b135d60236eaf2 100644 (file)
@@ -1,88 +1,80 @@
-- type: entity
-  id: BaseGameRule
-  abstract: true
-  noSpawn: true
-  components:
-  - type: GameRule
-
-- type: entity
+- type: gameRule
   id: DeathMatch
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: DeathMatchRule
+  config:
+    !type:GenericGameRuleConfiguration
+    id: DeathMatch
 
-- type: entity
+- type: gameRule
   id: InactivityTimeRestart
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: InactivityRule
+  config:
+    !type:InactivityGameRuleConfiguration
     inactivityMaxTime: 600
     roundEndDelay: 10
 
-- type: entity
+- type: gameRule
   id: MaxTimeRestart
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: MaxTimeRestartRule
+  config:
+    !type:MaxTimeRestartRuleConfiguration
     roundMaxTime: 300
     roundEndDelay: 10
 
-- type: entity
+- type: gameRule
   id: Nukeops
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: NukeopsRule
+  config:
+    !type:NukeopsRuleConfiguration
+    id: Nukeops
 
-- type: entity
+- type: gameRule
   id: Pirates
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: PiratesRule
+  config:
+    !type:GenericGameRuleConfiguration
+    id: Pirates
+
+- type: gameRule
+  id: Suspicion
+  config:
+    !type:GenericGameRuleConfiguration
+    id: Suspicion
 
-- type: entity
+- type: gameRule
   id: Traitor
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: TraitorRule
+  config:
+    !type:GenericGameRuleConfiguration
+    id: Traitor
+
+- type: gameRule
+  id: TraitorDeathMatch
+  config:
+    !type:GenericGameRuleConfiguration
+    id: TraitorDeathMatch
 
-- type: entity
+- type: gameRule
   id: Sandbox
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: SandboxRule
+  config:
+    !type:GenericGameRuleConfiguration
+    id: Sandbox
 
-- type: entity
+- type: gameRule
   id: Secret
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: SecretRule
+  config:
+    !type:GenericGameRuleConfiguration
+    id: Secret
 
-- type: entity
+- type: gameRule
   id: Zombie
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: ZombieRule
+  config:
+    !type:GenericGameRuleConfiguration
+    id: Zombie
 
 # event schedulers
-- type: entity
+- type: gameRule
   id: BasicStationEventScheduler
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: BasicStationEventScheduler
+  config:
+    !type:GenericGameRuleConfiguration
+    id: BasicStationEventScheduler
 
-- type: entity
+- type: gameRule
   id: RampingStationEventScheduler
-  parent: BaseGameRule
-  noSpawn: true
-  components:
-  - type: RampingStationEventScheduler
+  config:
+    !type:GenericGameRuleConfiguration
+    id: RampingStationEventScheduler
diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml
new file mode 100644 (file)
index 0000000..9efbb6f
--- /dev/null
@@ -0,0 +1,6 @@
+- type: antag
+  id: SuspicionInnocent
+  name: roles-antag-suspicion-innocent-name
+  antagonist: false
+  setPreference: false
+  objective: roles-antag-suspicion-innocent-objective
diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml
new file mode 100644 (file)
index 0000000..27e47ca
--- /dev/null
@@ -0,0 +1,6 @@
+- type: antag
+  id: SuspicionTraitor
+  name: roles-antag-suspicion-suspect-name
+  antagonist: true
+  setPreference: true
+  objective: roles-antag-suspicion-suspect-objective
index b6e2044761c48827f53aca4eb3984b6ffc4063ef..f56452f20be03a6613dde13de1afb988616600f1 100644 (file)
     - Traitor
     - BasicStationEventScheduler
 
+- type: gamePreset
+  id: Suspicion
+  alias:
+    - suspicion
+    - sus
+  name: suspicion-title
+  description: suspicion-description
+  rules:
+    - Suspicion
+
 - type: gamePreset
   id: Deathmatch
   alias:
   rules:
     - DeathMatch
 
+- type: gamePreset
+  id: TraitorDeathMatch
+  alias:
+    - traitordm
+    - traitordeathmatch
+  name: traitor-death-match-title
+  description: traitor-death-match-description
+  rules:
+    - TraitorDeathMatch
+    - MaxTimeRestart
+
 - type: gamePreset
   id: Nukeops
   alias: