]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Gamerule Entities, Take 2 (#15765)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Wed, 26 Apr 2023 00:23:14 +0000 (20:23 -0400)
committerGitHub <noreply@github.com>
Wed, 26 Apr 2023 00:23:14 +0000 (20:23 -0400)
124 files changed:
Content.Client/Suspicion/SuspicionEndTimerSystem.cs [deleted file]
Content.Client/Suspicion/SuspicionGui.xaml [deleted file]
Content.Client/Suspicion/SuspicionGui.xaml.cs [deleted file]
Content.Client/Suspicion/SuspicionRoleComponent.cs [deleted file]
Content.Client/Suspicion/SuspicionRoleSystem.cs [deleted file]
Content.Client/Suspicion/TraitorOverlay.cs [deleted file]
Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs
Content.Server/Dragon/Components/DragonRuleComponent.cs [new file with mode: 0644]
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 [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs
Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs [moved from Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs with 51% similarity]
Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs [deleted file]
Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs [deleted file]
Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs [deleted file]
Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs [deleted file]
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 [deleted file]
Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs [deleted file]
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 [new file with mode: 0644]
Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/GasLeakRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/SentienceTargetComponent.cs
Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs [moved from Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs with 69% similarity]
Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/StationEventComponent.cs [moved from Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs with 58% similarity]
Content.Server/StationEvents/Components/VentClogRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs
Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs [new file with mode: 0644]
Content.Server/StationEvents/EventManagerSystem.cs
Content.Server/StationEvents/Events/AnomalySpawnRule.cs [moved from Content.Server/StationEvents/Events/AnomalySpawn.cs with 61% similarity]
Content.Server/StationEvents/Events/BluespaceArtifact.cs [deleted file]
Content.Server/StationEvents/Events/BluespaceArtifactRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/BluespaceLockerRule.cs [moved from Content.Server/StationEvents/Events/BluespaceLocker.cs with 79% similarity]
Content.Server/StationEvents/Events/BreakerFlipRule.cs [moved from Content.Server/StationEvents/Events/BreakerFlip.cs with 70% similarity]
Content.Server/StationEvents/Events/BureaucraticErrorRule.cs [moved from Content.Server/StationEvents/Events/BureaucraticError.cs with 82% similarity]
Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs [moved from Content.Server/StationEvents/Events/DiseaseOutbreak.cs with 70% similarity]
Content.Server/StationEvents/Events/FalseAlarm.cs [deleted file]
Content.Server/StationEvents/Events/FalseAlarmRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/GasLeak.cs [deleted file]
Content.Server/StationEvents/Events/GasLeakRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/KudzuGrowth.cs [deleted file]
Content.Server/StationEvents/Events/KudzuGrowthRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/LoneOpsSpawn.cs [deleted file]
Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/MeteorSwarmRule.cs [moved from Content.Server/StationEvents/Events/MeteorSwarm.cs with 53% similarity]
Content.Server/StationEvents/Events/MouseMigrationRule.cs [moved from Content.Server/StationEvents/Events/MouseMigration.cs with 66% similarity]
Content.Server/StationEvents/Events/PowerGridCheck.cs [deleted file]
Content.Server/StationEvents/Events/PowerGridCheckRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/RandomSentienceRule.cs [moved from Content.Server/StationEvents/Events/RandomSentience.cs with 67% similarity]
Content.Server/StationEvents/Events/RevenantSpawn.cs [deleted file]
Content.Server/StationEvents/Events/RevenantSpawnRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/SolarFlare.cs [deleted file]
Content.Server/StationEvents/Events/SolarFlareRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/SpiderSpawn.cs [deleted file]
Content.Server/StationEvents/Events/SpiderSpawnRule.cs [new file with mode: 0644]
Content.Server/StationEvents/Events/StationEventSystem.cs
Content.Server/StationEvents/Events/VentClogRule.cs [moved from Content.Server/StationEvents/Events/VentClog.cs with 73% similarity]
Content.Server/StationEvents/Events/VentCritters.cs [deleted file]
Content.Server/StationEvents/Events/VentCrittersRule.cs [new file with mode: 0644]
Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs [deleted file]
Content.Server/Suspicion/Roles/SuspicionRole.cs [deleted file]
Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs [deleted file]
Content.Server/Suspicion/SuspicionItemComponent.cs [deleted file]
Content.Server/Suspicion/SuspicionRoleComponent.cs [deleted file]
Content.Server/Suspicion/SuspicionRoleSystem.cs [deleted file]
Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs [deleted file]
Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs [deleted file]
Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs [deleted file]
Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs [deleted file]
Content.Shared/Suspicion/SuspicionMessages.cs [deleted file]
Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml [deleted file]
Resources/Prototypes/Entities/Structures/Machines/traitordm.yml [deleted file]
Resources/Prototypes/GameRules/events.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml [deleted file]
Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml [deleted file]
Resources/Prototypes/game_presets.yml

diff --git a/Content.Client/Suspicion/SuspicionEndTimerSystem.cs b/Content.Client/Suspicion/SuspicionEndTimerSystem.cs
deleted file mode 100644 (file)
index a843f49..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-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
deleted file mode 100644 (file)
index 8f519b2..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<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
deleted file mode 100644 (file)
index 1314875..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-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
deleted file mode 100644 (file)
index d95ee7e..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-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
deleted file mode 100644 (file)
index 41cd186..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-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
deleted file mode 100644 (file)
index 84f52b9..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-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 4b23af60b2541573865496f04a075e0f6cda98e2..d4f85486bc4aa8aee6bed53cd1f9c53c19c1747a 100644 (file)
@@ -3,12 +3,11 @@ 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
@@ -23,6 +22,7 @@ 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,18 +31,17 @@ 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));
-
-                sGameTicker.StartGameRule(IoCManager.Resolve<IPrototypeManager>().Index<GameRulePrototype>(maxTimeMaxTimeRestartRuleSystem.Prototype));
-                maxTimeMaxTimeRestartRuleSystem.RoundMaxTime = TimeSpan.FromSeconds(3);
-
+                maxTime.RoundMaxTime = TimeSpan.FromSeconds(3);
                 sGameTicker.StartRound();
             });
 
@@ -51,7 +50,7 @@ namespace Content.IntegrationTests.Tests.GameRules
                 Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
             });
 
-            var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundMaxTime.TotalSeconds * 1.1f);
+            var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTime.RoundMaxTime.TotalSeconds * 1.1f);
             await PoolManager.RunTicksSync(pairTracker.Pair, ticks);
 
             await server.WaitAssertion(() =>
@@ -59,7 +58,7 @@ namespace Content.IntegrationTests.Tests.GameRules
                 Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PostRound));
             });
 
-            ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundEndDelay.TotalSeconds * 1.1f);
+            ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTime.RoundEndDelay.TotalSeconds * 1.1f);
             await PoolManager.RunTicksSync(pairTracker.Pair, ticks);
 
             await server.WaitAssertion(() =>
index 9a5ff23fadcdfb93adbcb4857ac4565fa38a6b6f..9fa3fedeea882594f64d2d545510237c70e0d5db 100644 (file)
@@ -1,11 +1,8 @@
-using System;
-using System.Linq;
+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;
 
@@ -26,12 +23,11 @@ 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(protoMan.Index<GameRulePrototype>("Secret"));
+            gameTicker.StartGameRule("Secret");
         });
 
         // Wait three ticks for any random update loops that might happen
@@ -39,9 +35,9 @@ public sealed class SecretStartsTest
 
         await server.WaitAssertion(() =>
         {
-            foreach (var rule in gameTicker.AddedGameRules)
+            foreach (var rule in gameTicker.GetAddedGameRules())
             {
-                Assert.That(gameTicker.StartedGameRules.Contains(rule));
+                Assert.That(gameTicker.GetActiveGameRules().Contains(rule));
             }
 
             // End all rules
index 4621ecbd1e6349758e49fd835441d28e7f3a61ef..6b600224383d8518d5ddb7075be11e1b46e72711 100644 (file)
@@ -1,17 +1,14 @@
-using System;
+using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules;
 using Content.Shared.CCVar;
 using NUnit.Framework;
 using Robust.Shared.Configuration;
 using Robust.Shared.GameObjects;
-using Robust.Shared.Prototypes;
 
 namespace Content.IntegrationTests.Tests.GameRules;
 
-
 [TestFixture]
 public sealed class StartEndGameRulesTest
 {
@@ -28,24 +25,20 @@ public sealed class StartEndGameRulesTest
         });
         var server = pairTracker.Pair.Server;
         await server.WaitIdleAsync();
-        var protoMan = server.ResolveDependency<IPrototypeManager>();
         var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
         var cfg = server.ResolveDependency<IConfigurationManager>();
         Assert.That(cfg.GetCVar(CCVars.DisableGridFill), Is.False);
 
         await server.WaitAssertion(() =>
         {
-            var rules = protoMan.EnumeratePrototypes<GameRulePrototype>().ToList();
+            var rules = gameTicker.GetAllGameRulePrototypes().ToList();
             rules.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
 
             // Start all rules
             foreach (var rule in rules)
             {
-                gameTicker.StartGameRule(rule);
+                gameTicker.StartGameRule(rule.ID);
             }
-
-            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
@@ -55,7 +48,7 @@ public sealed class StartEndGameRulesTest
         {
             // End all rules
             gameTicker.ClearGameRules();
-            Assert.That(!gameTicker.AddedGameRules.Any());
+            Assert.That(!gameTicker.GetAddedGameRules().Any());
         });
 
         await pairTracker.CleanReturnAsync();
diff --git a/Content.Server/Dragon/Components/DragonRuleComponent.cs b/Content.Server/Dragon/Components/DragonRuleComponent.cs
new file mode 100644 (file)
index 0000000..8f358c3
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.Dragon;
+
+[RegisterComponent]
+public sealed class DragonRuleComponent : Component
+{
+
+}
index 4c86634124c3f641204a3f37077b37ebc04cc8a5..0963f8c09b93dff46d22ec28907f50f5ead0dfc0 100644 (file)
@@ -1,6 +1,6 @@
 using System.Linq;
 using Content.Server.GameTicking;
-using Content.Server.StationEvents.Components;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.Dragon;
 using Robust.Server.GameObjects;
 using Robust.Shared.Map.Components;
@@ -10,8 +10,6 @@ namespace Content.Server.Dragon;
 
 public sealed partial class DragonSystem
 {
-    public override string Prototype => "Dragon";
-
     private int RiftsMet(DragonComponent component)
     {
         var finished = 0;
@@ -28,9 +26,11 @@ public sealed partial class DragonSystem
         return finished;
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, DragonRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        var spawnLocations = EntityManager.EntityQuery<MapGridComponent, TransformComponent>().ToList();
+        base.Started(uid, component, gameRule, args);
+
+        var spawnLocations = EntityQuery<MapGridComponent, TransformComponent>().ToList();
 
         if (spawnLocations.Count == 0)
             return;
@@ -39,16 +39,8 @@ 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 d969272ab9041d9bfa1814e22216028d70ebcb3f..f2f0e7ba991cdbc45c9014f5343dcd988e61007f 100644 (file)
@@ -24,7 +24,7 @@ using Content.Shared.Mobs.Components;
 
 namespace Content.Server.Dragon
 {
-    public sealed partial class DragonSystem : GameRuleSystem
+    public sealed partial class DragonSystem : GameRuleSystem<DragonRuleComponent>
     {
         [Dependency] private readonly IMapManager _mapManager = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
index eb44943e76f31bd1b976c5ecef1df97d37f7bb42..3b1bb031571ca5b3a388b16251b383b9f6895450 100644 (file)
@@ -1,9 +1,7 @@
 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;
@@ -11,6 +9,7 @@ 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
@@ -43,7 +42,6 @@ namespace Content.Server.GameTicking
 
             if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
             {
-                var oldPreset = Preset;
                 ClearGameRules();
                 SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset));
                 AddGamePresetRules();
@@ -125,6 +123,7 @@ namespace Content.Server.GameTicking
             return prototype != null;
         }
 
+        [PublicAPI]
         private bool AddGamePresetRules()
         {
             if (DummyTicker || Preset == null)
@@ -132,10 +131,7 @@ namespace Content.Server.GameTicking
 
             foreach (var rule in Preset.Rules)
             {
-                if (!_prototypeManager.TryIndex(rule, out GameRulePrototype? ruleProto))
-                    continue;
-
-                AddGameRule(ruleProto);
+                AddGameRule(rule);
             }
 
             return true;
@@ -144,7 +140,8 @@ namespace Content.Server.GameTicking
         private void StartGamePresetRules()
         {
             // May be touched by the preset during init.
-            foreach (var rule in _addedGameRules.ToArray())
+            var rules = new List<EntityUid>(GetAddedGameRules());
+            foreach (var rule in rules)
             {
                 StartGameRule(rule);
             }
@@ -166,10 +163,12 @@ 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 c0b382af2d5e47db36da98064866eedf8a00af7b..158e6b0085f8e38fd4ff6201426b27eaf0ff0b95 100644 (file)
 using System.Linq;
 using Content.Server.Administration;
-using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 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
+namespace Content.Server.GameTicking;
+
+public sealed partial class GameTicker
 {
-    public sealed partial class GameTicker
-    {
-        // No duplicates.
-        [ViewVariables] private readonly HashSet<GameRulePrototype> _addedGameRules = new();
+    [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new();
 
-        /// <summary>
-        ///     Holds all currently added game rules.
-        /// </summary>
-        public IReadOnlySet<GameRulePrototype> AddedGameRules => _addedGameRules;
+    /// <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;
 
-        [ViewVariables] private readonly HashSet<GameRulePrototype> _startedGameRules = new();
+    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);
+    }
 
-        /// <summary>
-        ///     Holds all currently started game rules.
-        /// </summary>
-        public IReadOnlySet<GameRulePrototype> StartedGameRules => _startedGameRules;
+    private void ShutdownGameRules()
+    {
+        _consoleHost.UnregisterCommand("addgamerule");
+        _consoleHost.UnregisterCommand("endgamerule");
+        _consoleHost.UnregisterCommand("cleargamerules");
+    }
 
-        [ViewVariables] private readonly List<(TimeSpan, GameRulePrototype)> _allPreviousGameRules = 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>
-        ///     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;
+        var ev = new GameRuleAddedEvent(ruleEntity, ruleId);
+        RaiseLocalEvent(ruleEntity, ref ev, true);
+        return ruleEntity;
+    }
 
-        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);
-        }
+    /// <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 _);
+    }
 
-        private void ShutdownGameRules()
-        {
-            _consoleHost.UnregisterCommand("addgamerule");
-            _consoleHost.UnregisterCommand("endgamerule");
-            _consoleHost.UnregisterCommand("cleargamerules");
-        }
+    /// <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);
+    }
 
-        /// <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);
+    /// <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);
 
-            _allPreviousGameRules.Add((RoundDuration(), rule));
-            _sawmill.Info($"Started game rule {rule.ID}");
+        // can't start an already active rule
+        if (ruleData.Active || ruleData.Ended)
+            return false;
 
-            if (_startedGameRules.Add(rule))
-                RaiseLocalEvent(new GameRuleStartedEvent(rule));
-        }
+        if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
+            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;
+        _allPreviousGameRules.Add((RoundDuration(), id));
+        _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
 
-            _addedGameRules.Remove(rule);
-            _sawmill.Info($"Ended game rule {rule.ID}");
+        ruleData.Active = true;
+        var ev = new GameRuleStartedEvent(ruleEntity, id);
+        RaiseLocalEvent(ruleEntity, ref ev, true);
+        return true;
+    }
 
-            if (IsGameRuleStarted(rule))
-                _startedGameRules.Remove(rule);
-            RaiseLocalEvent(new GameRuleEndedEvent(rule));
-        }
+    /// <summary>
+    /// Ends a game rule.
+    /// </summary>
+    [PublicAPI]
+    public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null)
+    {
+        if (!Resolve(ruleEntity, ref ruleData))
+            return false;
 
-        /// <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;
+        // don't end it multiple times
+        if (ruleData.Ended)
+            return false;
 
-            _sawmill.Info($"Added game rule {rule.ID}");
-            RaiseLocalEvent(new GameRuleAddedEvent(rule));
-            return true;
-        }
+        if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
+            return false;
 
-        public bool IsGameRuleAdded(GameRulePrototype rule)
-        {
-            return _addedGameRules.Contains(rule);
-        }
+        ruleData.Active = false;
+        ruleData.Ended = true;
+        _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}");
 
-        public bool IsGameRuleAdded(string rule)
-        {
-            foreach (var ruleProto in _addedGameRules)
-            {
-                if (ruleProto.ID.Equals(rule))
-                    return true;
-            }
+        var ev = new GameRuleEndedEvent(ruleEntity, id);
+        RaiseLocalEvent(ruleEntity, ref ev, true);
+        return true;
+    }
 
-            return false;
-        }
+    public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null)
+    {
+        return Resolve(ruleEntity, ref component) && !component.Ended;
+    }
 
-        public bool IsGameRuleStarted(GameRulePrototype rule)
+    public bool IsGameRuleAdded(string rule)
+    {
+        foreach (var ruleEntity in GetAddedGameRules())
         {
-            return _startedGameRules.Contains(rule);
+            if (MetaData(ruleEntity).EntityPrototype?.ID == rule)
+                return true;
         }
 
-        public bool IsGameRuleStarted(string rule)
-        {
-            foreach (var ruleProto in _startedGameRules)
-            {
-                if (ruleProto.ID.Equals(rule))
-                    return true;
-            }
+        return false;
+    }
 
-            return false;
-        }
+    public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null)
+    {
+        return Resolve(ruleEntity, ref component) && component.Active;
+    }
 
-        public void ClearGameRules()
+    public bool IsGameRuleActive(string rule)
+    {
+        foreach (var ruleEntity in GetActiveGameRules())
         {
-            foreach (var rule in _addedGameRules.ToArray())
-            {
-                EndGameRule(rule);
-            }
+            if (MetaData(ruleEntity).EntityPrototype?.ID == rule)
+                return true;
         }
 
-        #region Command Implementations
+        return false;
+    }
 
-        [AdminCommand(AdminFlags.Fun)]
-        private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
+    public void ClearGameRules()
+    {
+        foreach (var rule in GetAddedGameRules())
         {
-            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);
-            }
+            EndGameRule(rule);
         }
+    }
 
-        private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args)
+    /// <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))
         {
-            var activeIds = _addedGameRules.Select(c => c.ID);
-            return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<GameRulePrototype>().Where(p => !activeIds.Contains(p.Value)),
-                "<rule>");
+            if (IsGameRuleAdded(uid, ruleData))
+                yield return uid;
         }
+    }
 
-        [AdminCommand(AdminFlags.Fun)]
-        private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
+    /// <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))
         {
-            if (args.Length == 0)
-                return;
-
-            foreach (var ruleId in args)
-            {
-                if (!_prototypeManager.TryIndex<GameRulePrototype>(ruleId, out var rule))
-                    continue;
-
-                EndGameRule(rule);
-            }
+            if (ruleData.Active)
+                yield return uid;
         }
+    }
 
-        private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args)
+    /// <summary>
+    /// Gets all gamerule prototypes
+    /// </summary>
+    public IEnumerable<EntityPrototype> GetAllGameRulePrototypes()
+    {
+        foreach (var proto in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
         {
-            return CompletionResult.FromHintOptions(_addedGameRules.Select(c => new CompletionOption(c.ID)),
-                "<added rule>");
+            if (proto.Abstract)
+                continue;
+
+            if (proto.HasComponent<GameRuleComponent>())
+                yield return proto;
         }
+    }
+
+    #region Command Implementations
+
+    [AdminCommand(AdminFlags.Fun)]
+    private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
+    {
+        if (args.Length == 0)
+            return;
 
-        [AdminCommand(AdminFlags.Fun)]
-        private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args)
+        foreach (var rule in args)
         {
-            ClearGameRules();
+            var ent = AddGameRule(rule);
+
+            // Start rule if we're already in the middle of a round
+            if(RunLevel == GameRunLevel.InRound)
+                StartGameRule(ent);
         }
+    }
 
-        #endregion
+    private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args)
+    {
+        return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), "<rule>");
     }
 
-    /// <summary>
-    ///     Raised broadcast when a game rule is selected, but not started yet.
-    /// </summary>
-    public sealed class GameRuleAddedEvent
+    [AdminCommand(AdminFlags.Fun)]
+    private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
     {
-        public GameRulePrototype Rule { get; }
+        if (args.Length == 0)
+            return;
 
-        public GameRuleAddedEvent(GameRulePrototype rule)
+        foreach (var rule in args)
         {
-            Rule = rule;
+            if (!EntityUid.TryParse(rule, out var ruleEnt))
+                continue;
+
+            EndGameRule(ruleEnt);
         }
     }
 
-    public sealed class GameRuleStartedEvent
+    private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args)
     {
-        public GameRulePrototype Rule { get; }
+        return CompletionResult.FromHintOptions(GetAddedGameRules().Select(u => u.ToString()), "<added rule>");
+    }
 
-        public GameRuleStartedEvent(GameRulePrototype rule)
-        {
-            Rule = rule;
-        }
+    [AdminCommand(AdminFlags.Fun)]
+    private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args)
+    {
+        ClearGameRules();
     }
 
-    public sealed class GameRuleEndedEvent
+    #endregion
+}
+
+/*
+/// <summary>
+///     Raised broadcast when a game rule is selected, but not started yet.
+/// </summary>
+public sealed class GameRuleAddedEvent
+{
+    public GameRulePrototype Rule { get; }
+
+    public GameRuleAddedEvent(GameRulePrototype rule)
     {
-        public GameRulePrototype Rule { get; }
+        Rule = rule;
+    }
+}
 
-        public GameRuleEndedEvent(GameRulePrototype rule)
-        {
-            Rule = rule;
-        }
+public sealed class GameRuleStartedEvent
+{
+    public GameRulePrototype Rule { get; }
+
+    public GameRuleStartedEvent(GameRulePrototype rule)
+    {
+        Rule = rule;
+    }
+}
+
+public sealed class GameRuleEndedEvent
+{
+    public GameRulePrototype Rule { get; }
+
+    public GameRuleEndedEvent(GameRulePrototype rule)
+    {
+        Rule = rule;
     }
 }
+*/
index e64837ca9fcecabd1abcaabb67369718372f041a..096d508f356b6874ab8933d876755d4aaa81d503 100644 (file)
@@ -456,7 +456,6 @@ namespace Content.Server.GameTicking
             // Clear up any game rules.
             ClearGameRules();
 
-            _addedGameRules.Clear();
             _allPreviousGameRules.Clear();
 
             // Round restart cleanup event, so entity systems can reset.
index e131030251f845bc8c519df3ad56ec6ee1b38b43..ff6a3d17ba4f89c5d64eb3b4d84271845ca384c3 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 string[] Alias { get; } = Array.Empty<string>();
+        public readonly string[] Alias = Array.Empty<string>();
 
         [DataField("name")]
-        public string ModeTitle { get; } = "????";
+        public readonly string ModeTitle = "????";
 
         [DataField("description")]
-        public string Description { get; } = string.Empty;
+        public readonly string Description = string.Empty;
 
         [DataField("showInVote")]
-        public bool ShowInVote { get; } = false;
+        public readonly bool ShowInVote;
 
         [DataField("minPlayers")]
-        public int? MinPlayers { get; } = null;
+        public readonly int? MinPlayers;
 
         [DataField("maxPlayers")]
-        public int? MaxPlayers { get; } = null;
+        public readonly int? MaxPlayers;
 
-        [DataField("rules", customTypeSerializer:typeof(PrototypeIdListSerializer<GameRulePrototype>))]
+        [DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
         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
new file mode 100644 (file)
index 0000000..7a11eb7
--- /dev/null
@@ -0,0 +1,33 @@
+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
new file mode 100644 (file)
index 0000000..640b6cb
--- /dev/null
@@ -0,0 +1,44 @@
+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
new file mode 100644 (file)
index 0000000..ae9657a
--- /dev/null
@@ -0,0 +1,24 @@
+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
new file mode 100644 (file)
index 0000000..45d9d38
--- /dev/null
@@ -0,0 +1,24 @@
+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 198db1f91228d10870da264e0b5a8c5c2ede7b7d..c32a8569cb730ca98b7aa2f7cd12a74cd31f1096 100644 (file)
@@ -6,7 +6,6 @@ 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")]
similarity index 51%
rename from Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs
rename to Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
index 09ccd7ad31b56d83bae83feded71e0a2bbfc48b6..3df94d9eed2789e133ff836e65e51ea4225611d2 100644 (file)
@@ -1,19 +1,22 @@
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.StationEvents.Events;
 using Content.Shared.Dataset;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Roles;
+using Robust.Server.Player;
 using Robust.Shared.Audio;
-using Robust.Shared.Prototypes;
+using Robust.Shared.Map;
 using Robust.Shared.Serialization.TypeSerializers.Implementations;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 using Robust.Shared.Utility;
 
-namespace Content.Server.GameTicking.Rules.Configurations;
+namespace Content.Server.GameTicking.Rules.Components;
 
-public sealed class NukeopsRuleConfiguration : GameRuleConfiguration
+[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))]
+public sealed class NukeopsRuleComponent : Component
 {
-    public override string Id => "Nukeops";
-
+    /// <summary>
+    /// The minimum needed amount of players
+    /// </summary>
     [DataField("minPlayers")]
     public int MinPlayers = 15;
 
@@ -38,15 +41,6 @@ public sealed class NukeopsRuleConfiguration : GameRuleConfiguration
     [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";
 
@@ -82,4 +76,86 @@ public sealed class NukeopsRuleConfiguration : GameRuleConfiguration
 
     [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
 }
diff --git a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs
new file mode 100644 (file)
index 0000000..7cb748c
--- /dev/null
@@ -0,0 +1,15 @@
+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
new file mode 100644 (file)
index 0000000..6f79b89
--- /dev/null
@@ -0,0 +1,7 @@
+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
new file mode 100644 (file)
index 0000000..f6a6726
--- /dev/null
@@ -0,0 +1,11 @@
+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
new file mode 100644 (file)
index 0000000..38f9921
--- /dev/null
@@ -0,0 +1,32 @@
+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
new file mode 100644 (file)
index 0000000..f0b3b0c
--- /dev/null
@@ -0,0 +1,12 @@
+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
deleted file mode 100644 (file)
index 600cc9d..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644 (file)
index 1db383c..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-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
deleted file mode 100644 (file)
index bc60d77..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-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
deleted file mode 100644 (file)
index d4cd3f3..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-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; }
-}
index c4c61480db95bf30e77cd7a75d290c5f96d3fa77..79c3394d11a339fab7d203506995b76d7007da77 100644 (file)
@@ -1,5 +1,5 @@
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.CCVar;
 using Content.Shared.Damage;
 using Content.Shared.Mobs.Components;
@@ -11,44 +11,42 @@ using Robust.Shared.Enums;
 namespace Content.Server.GameTicking.Rules;
 
 /// <summary>
-///     Simple GameRule that will do a free-for-all death match.
-///     Kill everybody else to win.
+/// Manages <see cref="DeathMatchRuleComponent"/>
 /// </summary>
-public sealed class DeathMatchRuleSystem : GameRuleSystem
+public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponent>
 {
-    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;
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
         _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement"));
 
-        _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
     }
 
-    public override void Ended()
+    protected override void Ended(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
     {
-        _deadCheckTimer = null;
-        _restartTimer = null;
+        base.Ended(uid, component, gameRule, args);
+
+        component.DeadCheckTimer = null;
+        component.RestartTimer = null;
 
-        _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
     }
 
     private void OnHealthChanged(DamageChangedEvent _)
@@ -56,7 +54,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
         RunDelayedCheck();
     }
 
-    private void OnPlayerStatusChanged(object? _, SessionStatusEventArgs e)
+    private void OnPlayerStatusChanged(object? ojb, SessionStatusEventArgs e)
     {
         if (e.NewStatus == SessionStatus.Disconnected)
         {
@@ -66,24 +64,27 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
 
     private void RunDelayedCheck()
     {
-        if (!RuleAdded || _deadCheckTimer != null)
-            return;
+        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;
 
-        _deadCheckTimer = DeadCheckDelay;
+            deathMatch.DeadCheckTimer = deathMatch.DeadCheckDelay;
+        }
     }
 
-    public override void Update(float frameTime)
+    protected override void ActiveTick(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, float frameTime)
     {
-        if (!RuleAdded)
-            return;
+        base.ActiveTick(uid, component, gameRule, frameTime);
 
         // 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 (_restartTimer != null)
+        if (component.RestartTimer != null)
         {
-            _restartTimer -= frameTime;
+            component.RestartTimer -= frameTime;
 
-            if (_restartTimer > 0f)
+            if (component.RestartTimer > 0f)
                 return;
 
             GameTicker.EndRound();
@@ -91,20 +92,20 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
             return;
         }
 
-        if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || _deadCheckTimer == null)
+        if (!_cfg.GetCVar(CCVars.GameLobbyEnableWin) || component.DeadCheckTimer == null)
             return;
 
-        _deadCheckTimer -= frameTime;
+        component.DeadCheckTimer -= frameTime;
 
-        if (_deadCheckTimer > 0)
+        if (component.DeadCheckTimer > 0)
             return;
 
-        _deadCheckTimer = null;
+        component.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;
 
@@ -120,9 +121,10 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
 
         _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", RestartDelay)));
-        _restartTimer = RestartDelay;
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",
+            ("seconds", component.RestartDelay)));
+        component.RestartTimer = component.RestartDelay;
     }
 }
index d48eb1333a609786e7529a3a466626b180c361d1..c2b55822c8c7f75aec1a91e07397aced544a7d6a 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,3 +12,4 @@ public sealed class GameRulePrototype : IPrototype
     [DataField("config", required: true)]
     public GameRuleConfiguration Configuration { get; } = default!;
 }
+*/
index 47bd891db271dfe4ec6453876e09e531277e7f09..a55189d0f2e118428f439480c1e5cc12d0485eae 100644 (file)
@@ -1,94 +1,84 @@
-using Content.Server.GameTicking.Rules.Configurations;
-using JetBrains.Annotations;
+using Content.Server.GameTicking.Rules.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
-[PublicAPI]
-public abstract class GameRuleSystem : EntitySystem
+public abstract class GameRuleSystem<T> : EntitySystem where T : Component
 {
     [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<GameRuleAddedEvent>(OnGameRuleAdded);
-
-        SubscribeLocalEvent<GameRuleStartedEvent>(OnGameRuleStarted);
-        SubscribeLocalEvent<GameRuleEndedEvent>(OnGameRuleEnded);
+        SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
+        SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
+        SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
     }
 
-    private void OnGameRuleAdded(GameRuleAddedEvent ev)
+    private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args)
     {
-        if (ev.Rule.Configuration.Id != Prototype)
+        if (!TryComp<GameRuleComponent>(uid, out var ruleData))
             return;
-
-        Configuration = ev.Rule.Configuration;
-        RuleAdded = true;
-
-        Added();
+        Added(uid, component, ruleData, args);
     }
 
-    private void OnGameRuleStarted(GameRuleStartedEvent ev)
+    private void OnGameRuleStarted(EntityUid uid, T component, ref GameRuleStartedEvent args)
     {
-        if (ev.Rule.Configuration.Id != Prototype)
+        if (!TryComp<GameRuleComponent>(uid, out var ruleData))
             return;
-
-        RuleStarted = true;
-
-        Started();
+        Started(uid, component, ruleData, args);
     }
 
-    private void OnGameRuleEnded(GameRuleEndedEvent ev)
+    private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent args)
     {
-        if (ev.Rule.Configuration.Id != Prototype)
+        if (!TryComp<GameRuleComponent>(uid, out var ruleData))
             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)
+    {
 
-        RuleAdded = false;
-        RuleStarted = false;
-        Ended();
     }
 
     /// <summary>
-    ///     Called when the game rule has been added.
-    ///     You should avoid using this in favor of started--they are not the same thing.
+    /// Called when the gamerule begins
     /// </summary>
-    /// <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() { }
+    protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    {
+
+    }
 
     /// <summary>
-    ///     Called when the game rule has been started.
+    /// Called when the gamerule ends
     /// </summary>
-    public abstract void Started();
+    protected virtual void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    {
+
+    }
 
     /// <summary>
-    ///     Called when the game rule has ended.
+    /// Called on an active gamerule entity in the Update function
     /// </summary>
-    public abstract void Ended();
+    protected virtual void ActiveTick(EntityUid uid, T component, GameRuleComponent gameRule, float frameTime)
+    {
+
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        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);
+        }
+    }
 }
index d61b93d45024167ec1ba188eb67500dea544c725..c2e91ba4a5d01da30a11f0715b3074d6789e0eb3 100644 (file)
 using System.Threading;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 using Robust.Server.Player;
 using Timer = Robust.Shared.Timing.Timer;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem
+public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem<InactivityRuleComponent>
 {
     [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 Started()
+    public override void Shutdown()
     {
-        if (Configuration is not InactivityGameRuleConfiguration inactivityConfig)
-            return;
-        InactivityMaxTime = inactivityConfig.InactivityMaxTime;
-        RoundEndDelay = inactivityConfig.RoundEndDelay;
-        _playerManager.PlayerStatusChanged += PlayerStatusChanged;
+        base.Shutdown();
+        _playerManager.PlayerStatusChanged -= PlayerStatusChanged;
     }
 
-    public override void Ended()
+    protected override void Ended(EntityUid uid, InactivityRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
     {
-        _playerManager.PlayerStatusChanged -= PlayerStatusChanged;
+        base.Ended(uid, component, gameRule, args);
 
-        StopTimer();
+        StopTimer(uid, component);
     }
 
-    public void RestartTimer()
+    public void RestartTimer(EntityUid uid, InactivityRuleComponent? component = null)
     {
-        _timerCancel.Cancel();
-        _timerCancel = new CancellationTokenSource();
-        Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token);
+        if (!Resolve(uid, ref component))
+            return;
+
+        component.TimerCancel.Cancel();
+        component.TimerCancel = new CancellationTokenSource();
+        Timer.Spawn(component.InactivityMaxTime, () => TimerFired(uid, component), component.TimerCancel.Token);
     }
 
-    public void StopTimer()
+    public void StopTimer(EntityUid uid, InactivityRuleComponent? component = null)
     {
-        _timerCancel.Cancel();
+        if (!Resolve(uid, ref component))
+            return;
+
+        component.TimerCancel.Cancel();
     }
 
-    private void TimerFired()
+    private void TimerFired(EntityUid uid, InactivityRuleComponent? component = null)
     {
+        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) RoundEndDelay.TotalSeconds)));
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) component.RoundEndDelay.TotalSeconds)));
 
-        Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
+        Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
     }
 
     private void RunLevelChanged(GameRunLevelChangedEvent args)
     {
-        if (!RuleAdded)
-            return;
-
-        switch (args.New)
+        var query = EntityQueryEnumerator<InactivityRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var inactivity, out var gameRule))
         {
-            case GameRunLevel.InRound:
-                RestartTimer();
-                break;
-            case GameRunLevel.PreRoundLobby:
-            case GameRunLevel.PostRound:
-                StopTimer();
-                break;
+            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;
+            }
         }
     }
 
     private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
     {
-        if (GameTicker.RunLevel != GameRunLevel.InRound)
-        {
-            return;
-        }
-
-        if (_playerManager.PlayerCount == 0)
-        {
-            RestartTimer();
-        }
-        else
+        var query = EntityQueryEnumerator<InactivityRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var inactivity, out var gameRule))
         {
-            StopTimer();
+            if (!GameTicker.IsGameRuleActive(uid, gameRule))
+                return;
+
+            if (GameTicker.RunLevel != GameRunLevel.InRound)
+            {
+                return;
+            }
+
+            if (_playerManager.PlayerCount == 0)
+            {
+                RestartTimer(uid, inactivity);
+            }
+            else
+            {
+                StopTimer(uid, inactivity);
+            }
         }
     }
 }
index 6b2a5805c6a439eb0ea65ccd3949c4d18edbf31a..e792a004df5e094de65910d3bc66a3db55f31c32 100644 (file)
@@ -1,21 +1,14 @@
 using System.Threading;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 using Timer = Robust.Shared.Timing.Timer;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
+public sealed class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRuleComponent>
 {
     [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();
@@ -23,58 +16,60 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
         SubscribeLocalEvent<GameRunLevelChangedEvent>(RunLevelChanged);
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        if (Configuration is not MaxTimeRestartRuleConfiguration maxTimeRestartConfig)
-            return;
-
-        RoundMaxTime = maxTimeRestartConfig.RoundMaxTime;
-        RoundEndDelay = maxTimeRestartConfig.RoundEndDelay;
+        base.Started(uid, component, gameRule, args);
 
         if(GameTicker.RunLevel == GameRunLevel.InRound)
-            RestartTimer();
+            RestartTimer(component);
     }
 
-    public override void Ended()
+    protected override void Ended(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
     {
-        StopTimer();
+        base.Ended(uid, component, gameRule, args);
+
+        StopTimer(component);
     }
 
-    public void RestartTimer()
+    public void RestartTimer(MaxTimeRestartRuleComponent component)
     {
-        _timerCancel.Cancel();
-        _timerCancel = new CancellationTokenSource();
-        Timer.Spawn(RoundMaxTime, TimerFired, _timerCancel.Token);
+        component.TimerCancel.Cancel();
+        component.TimerCancel = new CancellationTokenSource();
+        Timer.Spawn(component.RoundMaxTime, () => TimerFired(component), component.TimerCancel.Token);
     }
 
-    public void StopTimer()
+    public void StopTimer(MaxTimeRestartRuleComponent component)
     {
-        _timerCancel.Cancel();
+        component.TimerCancel.Cancel();
     }
 
-    private void TimerFired()
+    private void TimerFired(MaxTimeRestartRuleComponent component)
     {
         GameTicker.EndRound(Loc.GetString("rule-time-has-run-out"));
 
-        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) RoundEndDelay.TotalSeconds)));
+        _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) component.RoundEndDelay.TotalSeconds)));
 
-        Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
+        Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
     }
 
     private void RunLevelChanged(GameRunLevelChangedEvent args)
     {
-        if (!RuleAdded)
-            return;
-
-        switch (args.New)
+        var query = EntityQueryEnumerator<MaxTimeRestartRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var timer, out var gameRule))
         {
-            case GameRunLevel.InRound:
-                RestartTimer();
-                break;
-            case GameRunLevel.PreRoundLobby:
-            case GameRunLevel.PostRound:
-                StopTimer();
-                break;
+            if (!GameTicker.IsGameRuleActive(uid, gameRule))
+                return;
+
+            switch (args.New)
+            {
+                case GameRunLevel.InRound:
+                    RestartTimer(timer);
+                    break;
+                case GameRunLevel.PreRoundLobby:
+                case GameRunLevel.PostRound:
+                    StopTimer(timer);
+                    break;
+            }
         }
     }
 }
index 873e3c90dc4e854e3b85fae865b67634788e12e4..2d372cf3d56efee91d6fc79a118cb1854555af11 100644 (file)
@@ -2,7 +2,6 @@ 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;
@@ -18,6 +17,7 @@ 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
+public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -50,104 +50,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
     [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();
@@ -167,14 +72,21 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
 
     private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args)
     {
-        // If entity has a prior mind attached, add them to the players list.
-        if (!TryComp<MindComponent>(uid, out var mindComponent) || !RuleAdded)
-            return;
+        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;
 
-        var session = mindComponent.Mind?.Session;
-        var name = MetaData(uid).EntityName;
-        if (session != null)
-            _operativePlayers.Add(name, session);
+            var session = mindComponent.Mind?.Session;
+            var name = MetaData(uid).EntityName;
+            if (session != null)
+                nukeops.OperativePlayers.Add(name, session);
+        }
     }
 
     private void OnComponentRemove(EntityUid uid, NukeOperativeComponent component, ComponentRemove args)
@@ -184,137 +96,138 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
 
     private void OnNukeExploded(NukeExplodedEvent ev)
     {
-       if (!RuleAdded)
-            return;
-
-        if (ev.OwningStation != null)
+        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
         {
-            if (ev.OwningStation == _nukieOutpost)
-            {
-                _winConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
-                RuleWinType = WinType.CrewMajor;
-                return;
-            }
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+                continue;
 
-            if (TryComp(_targetStation, out StationDataComponent? data))
+            if (ev.OwningStation != null)
             {
-                foreach (var grid in data.Grids)
+                if (ev.OwningStation == nukeops.NukieOutpost)
                 {
-                    if (grid != ev.OwningStation)
+                    nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost);
+                    SetWinType(uid, WinType.CrewMajor, nukeops);
+                    continue;
+                }
+
+                if (TryComp(nukeops.TargetStation, out StationDataComponent? data))
+                {
+                    var correctStation = false;
+                    foreach (var grid in data.Grids)
                     {
-                        continue;
+                        if (grid != ev.OwningStation)
+                        {
+                            continue;
+                        }
+
+                        nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
+                        SetWinType(uid, WinType.OpsMajor, nukeops);
+                        correctStation = true;
                     }
 
-                    _winConditions.Add(WinCondition.NukeExplodedOnCorrectStation);
-                    RuleWinType = WinType.OpsMajor;
-                    return;
+                    if (correctStation)
+                        continue;
                 }
+
+                nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
+            }
+            else
+            {
+                nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
             }
 
-            _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
+            _roundEndSystem.EndRound();
         }
-        else
-        {
-            _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation);
-        }
-
-        _roundEndSystem.EndRound();
     }
 
     private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
     {
-        switch (ev.New)
+        var query = EntityQueryEnumerator<NukeopsRuleComponent>();
+        while (query.MoveNext(out var uid, out var nukeops))
         {
-            case GameRunLevel.InRound:
-                OnRoundStart();
-                break;
-            case GameRunLevel.PostRound:
-                OnRoundEnd();
-                break;
+            switch (ev.New)
+            {
+                case GameRunLevel.InRound:
+                    OnRoundStart(uid, nukeops);
+                    break;
+                case GameRunLevel.PostRound:
+                    OnRoundEnd(uid, nukeops);
+                    break;
+            }
         }
     }
 
-    public void LoadLoneOpsConfig()
-    {
-        _nukeopsRuleConfig.SpawnOutpost = false;
-        _nukeopsRuleConfig.EndsRound = false;
-    }
-
+    /// <summary>
+    /// Loneops can only spawn if there is no nukeops active
+    /// </summary>
     public bool CheckLoneOpsSpawn()
     {
-        return _nukeopsRuleConfig.CanLoneOpsSpawn;
+        return !EntityQuery<NukeopsRuleComponent>().Any();
     }
 
-    private void OnRoundStart()
+    private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null)
     {
+        if (!Resolve(uid, ref component))
+            return;
+
         // TODO: This needs to try and target a Nanotrasen station. At the very least,
         // we can only currently guarantee that NT stations are the only station to
         // exist in the base game.
 
-        _targetStation = _stationSystem.Stations.FirstOrNull();
+        component.TargetStation = _stationSystem.Stations.FirstOrNull();
 
-        if (_targetStation == null)
+        if (component.TargetStation == null)
         {
             return;
         }
 
         var filter = Filter.Empty();
-        foreach (var nukie in EntityQuery<NukeOperativeComponent>())
+        var query = EntityQueryEnumerator<NukeOperativeComponent, ActorComponent>();
+        while (query.MoveNext(out _, out _, out var actor))
         {
-            if (!TryComp<ActorComponent>(nukie.Owner, out var actor))
-            {
-                continue;
-            }
-
-            _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value)));
+            _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", component.TargetStation.Value)));
             filter.AddPlayer(actor.PlayerSession);
         }
 
-        _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, filter, recordReplay: false);
+        _audioSystem.PlayGlobal(component.GreetSound, filter, recordReplay: false);
     }
 
-    private void OnRoundEnd()
+    private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null)
     {
+        if (!Resolve(uid, ref component))
+            return;
+
         // If the win condition was set to operative/crew major win, ignore.
-        if (RuleWinType == WinType.OpsMajor || RuleWinType == WinType.CrewMajor)
-        {
+        if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor)
             return;
-        }
 
-        foreach (var (nuke, nukeTransform) in EntityManager.EntityQuery<NukeComponent, TransformComponent>(true))
+        foreach (var (nuke, nukeTransform) in EntityQuery<NukeComponent, TransformComponent>(true))
         {
             if (nuke.Status != NukeStatus.ARMED)
-            {
                 continue;
-            }
 
             // UH OH
             if (nukeTransform.MapID == _emergency.CentComMap)
             {
-                _winConditions.Add(WinCondition.NukeActiveAtCentCom);
-                RuleWinType = WinType.OpsMajor;
+                component.WinConditions.Add(WinCondition.NukeActiveAtCentCom);
+                SetWinType(uid, WinType.OpsMajor, component);
                 return;
             }
 
-            if (nukeTransform.GridUid == null || _targetStation == null)
-            {
+            if (nukeTransform.GridUid == null || component.TargetStation == null)
                 continue;
-            }
 
-            if (!TryComp(_targetStation.Value, out StationDataComponent? data))
-            {
+            if (!TryComp(component.TargetStation.Value, out StationDataComponent? data))
                 continue;
-            }
 
             foreach (var grid in data.Grids)
             {
                 if (grid != nukeTransform.GridUid)
-                {
                     continue;
-                }
 
-                _winConditions.Add(WinCondition.NukeActiveInStation);
-                RuleWinType = WinType.OpsMajor;
+                component.WinConditions.Add(WinCondition.NukeActiveInStation);
+                SetWinType(uid, WinType.OpsMajor, component);
                 return;
             }
         }
@@ -323,9 +236,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         foreach (var (_, state) in EntityQuery<NukeOperativeComponent, MobStateComponent>())
         {
             if (state.CurrentState is MobState.Alive)
-            {
                 continue;
-            }
 
             allAlive = false;
             break;
@@ -335,12 +246,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         // running away the moment nuke ops appear.
         if (allAlive)
         {
-            RuleWinType = WinType.OpsMinor;
-            _winConditions.Add(WinCondition.AllNukiesAlive);
+            SetWinType(uid, WinType.OpsMinor, component);
+            component.WinConditions.Add(WinCondition.AllNukiesAlive);
             return;
         }
 
-        _winConditions.Add(WinCondition.SomeNukiesAlive);
+        component.WinConditions.Add(WinCondition.SomeNukiesAlive);
 
         var diskAtCentCom = false;
         foreach (var (_, transform) in EntityManager.EntityQuery<NukeDiskComponent, TransformComponent>())
@@ -357,98 +268,115 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         // This also implies that some nuclear operatives have died.
         if (diskAtCentCom)
         {
-            RuleWinType = WinType.CrewMinor;
-            _winConditions.Add(WinCondition.NukeDiskOnCentCom);
+            SetWinType(uid, WinType.CrewMinor, component);
+            component.WinConditions.Add(WinCondition.NukeDiskOnCentCom);
         }
         // Otherwise, the nuke ops win.
         else
         {
-            RuleWinType = WinType.OpsMinor;
-            _winConditions.Add(WinCondition.NukeDiskNotOnCentCom);
+            SetWinType(uid, WinType.OpsMinor, component);
+            component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom);
         }
     }
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        var winText = Loc.GetString($"nukeops-{_winType.ToString().ToLower()}");
+        foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
+        {
+            var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}");
 
-        ev.AddLine(winText);
+            ev.AddLine(winText);
 
-        foreach (var cond in _winConditions)
-        {
-            var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
+            foreach (var cond in nukeops.WinConditions)
+            {
+                var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}");
 
-            ev.AddLine(text);
-        }
+                ev.AddLine(text);
+            }
 
-        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);
+            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);
+            }
         }
     }
 
-    private void CheckRoundShouldEnd()
+    private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null)
     {
-        if (!RuleAdded || !_nukeopsRuleConfig.EndsRound || RuleWinType == WinType.CrewMajor || RuleWinType == WinType.OpsMajor)
+        if (!Resolve(uid, ref component))
             return;
 
-        // If there are any nuclear bombs that are active, immediately return. We're not over yet.
-        foreach (var nuke in EntityQuery<NukeComponent>())
+        component.WinType = type;
+
+        if (type == WinType.CrewMajor || type == WinType.OpsMajor)
+            _roundEndSystem.EndRound();
+    }
+
+    private void CheckRoundShouldEnd()
+    {
+        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
         {
-            if (nuke.Status == NukeStatus.ARMED)
+            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>())
             {
-                return;
+                if (nuke.Status == NukeStatus.ARMED)
+                {
+                    armed = true;
+                    break;
+                }
             }
-        }
-
-        MapId? shuttleMapId = EntityManager.EntityExists(_nukieShuttle)
-            ? Transform(_nukieShuttle!.Value).MapID
-            : null;
+            if (armed)
+                continue;
 
-        MapId? targetStationMap = null;
-        if (_targetStation != null && TryComp(_targetStation, out StationDataComponent? data))
-        {
-            var grid = data.Grids.FirstOrNull();
-            targetStationMap = grid != null
-                ? Transform(grid.Value).MapID
+            MapId? shuttleMapId = Exists(nukeops.NukieShuttle)
+                ? Transform(nukeops.NukieShuttle.Value).MapID
                 : null;
-        }
 
-        // Check if there are nuke operatives still alive on the same map as the shuttle,
-        // or on the same map as the station.
-        // If there are, the round can continue.
-        var operatives = EntityQuery<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
-        var operativesAlive = operatives
-            .Where(ent =>
-                ent.Item3.MapID == shuttleMapId
-                || ent.Item3.MapID == targetStationMap)
-            .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running);
-
-        if (operativesAlive)
-            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);
-        }
+            MapId? targetStationMap = null;
+            if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data))
+            {
+                var grid = data.Grids.FirstOrNull();
+                targetStationMap = grid != null
+                    ? Transform(grid.Value).MapID
+                    : null;
+            }
+
+            // Check if there are nuke operatives still alive on the same map as the shuttle,
+            // or on the same map as the station.
+            // If there are, the round can continue.
+            var operatives = EntityQuery<NukeOperativeComponent, MobStateComponent, TransformComponent>(true);
+            var operativesAlive = operatives
+                .Where(ent =>
+                    ent.Item3.MapID == shuttleMapId
+                    || ent.Item3.MapID == targetStationMap)
+                .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running);
+
+            if (operativesAlive)
+                continue; // There are living operatives than can access the shuttle, or are still on the station's map.
+
+            // Check that there are spawns available and that they can access the shuttle.
+            var spawnsAvailable = EntityQuery<NukeOperativeSpawnerComponent>(true).Any();
+            if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet)
+                continue; // Ghost spawns can still access the shuttle. Continue the round.
 
-        RuleWinType = WinType.CrewMajor;
+            // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives,
+            // and there are no nuclear operatives on the target station's map.
+            nukeops.WinConditions.Add(spawnsAvailable
+                ? WinCondition.NukiesAbandoned
+                : WinCondition.AllNukiesDead);
+
+            SetWinType(uid, WinType.CrewMajor, nukeops);
+        }
     }
 
     private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
@@ -464,111 +392,124 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
 
     private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        if (!SpawnMap())
+        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
         {
-            Logger.InfoS("nukies", "Failed to load map for nukeops");
-            return;
-        }
-
-        // Basically copied verbatim from traitor code
-        var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative;
-        var maxOperatives = _nukeopsRuleConfig.MaxOperatives;
-
-        var everyone = new List<IPlayerSession>(ev.PlayerPool);
-        var prefList = new List<IPlayerSession>();
-        var cmdrPrefList = new List<IPlayerSession>();
-        var operatives = new List<IPlayerSession>();
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+                continue;
 
-        // 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))
+            if (!SpawnMap(uid, nukeops))
             {
+                Logger.InfoS("nukies", "Failed to load map for nukeops");
                 continue;
             }
-            var profile = ev.Profiles[player.UserId];
-            if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.OperativeRoleProto))
-            {
-                prefList.Add(player);
-            }
-            if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.CommanderRolePrototype))
+
+            // 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)
             {
-                cmdrPrefList.Add(player);
+                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);
+                }
             }
-        }
 
-        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++)
-        {
-            IPlayerSession nukeOp;
-            // Only one commander, so we do it at the start
-            if (i == 0)
+            for (var i = 0; i < numNukies; i++)
             {
-                if (cmdrPrefList.Count == 0)
+                IPlayerSession nukeOp;
+                // Only one commander, so we do it at the start
+                if (i == 0)
                 {
-                    if (prefList.Count == 0)
+                    if (cmdrPrefList.Count == 0)
                     {
-                        if (everyone.Count == 0)
+                        if (prefList.Count == 0)
                         {
-                            Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection");
-                            break;
+                            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.");
                         }
-                        nukeOp = _random.PickAndTake(everyone);
-                        Logger.InfoS("preset", "Insufficient preferred nukeop commanders or nukies, picking at random.");
                     }
                     else
                     {
-                        nukeOp = _random.PickAndTake(prefList);
+                        nukeOp = _random.PickAndTake(cmdrPrefList);
                         everyone.Remove(nukeOp);
-                        Logger.InfoS("preset", "Insufficient preferred nukeop commanders, picking at random from regular op list.");
+                        prefList.Remove(nukeOp);
+                        Logger.InfoS("preset", "Selected a preferred nukeop commander.");
                     }
                 }
                 else
                 {
-                    nukeOp = _random.PickAndTake(cmdrPrefList);
-                    everyone.Remove(nukeOp);
-                    prefList.Remove(nukeOp);
-                    Logger.InfoS("preset", "Selected a preferred nukeop commander.");
-                }
-            }
-            else
-            {
-                if (prefList.Count == 0)
-                {
-                    if (everyone.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 nukeops, picking at random.");
+                    }
+                    else
                     {
-                        Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection");
-                        break;
+                        nukeOp = _random.PickAndTake(prefList);
+                        everyone.Remove(nukeOp);
+                        Logger.InfoS("preset", "Selected a preferred nukeop.");
                     }
-                    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);
             }
-            operatives.Add(nukeOp);
-        }
 
-        SpawnOperatives(numNukies, operatives, false);
+            SpawnOperatives(numNukies, operatives, false, nukeops);
 
-        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;
+            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
+                nukeops.OperativePlayers[name] = session;
+            }
         }
     }
 
@@ -583,9 +524,13 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         if (TryComp(args.Spawned, out ActorComponent? actor))
             profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile;
 
-        SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile);
+        // todo: this is kinda awful for multi-nukies
+        foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
+        {
+            SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile, nukeops);
 
-        _operativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype);
+            nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype);
+        }
     }
 
     private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args)
@@ -595,60 +540,51 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
 
         var mind = mindComponent.Mind;
 
-        if (_operativeMindPendingData.TryGetValue(uid, out var role) || !_nukeopsRuleConfig.SpawnOutpost || !_nukeopsRuleConfig.EndsRound)
+        foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
         {
-            if (role == null)
-                role = _nukeopsRuleConfig.OperativeRoleProto;
+            if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost || !nukeops.EndsRound)
+            {
+                role ??= nukeops.OperativeRoleProto;
 
-            mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(role)));
-            _operativeMindPendingData.Remove(uid);
-        }
+                mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(role)));
+                nukeops.OperativeMindPendingData.Remove(uid);
+            }
 
-        if (!mind.TryGetSession(out var playerSession))
-            return;
-        if (_operativePlayers.ContainsValue(playerSession))
-            return;
+            if (!mind.TryGetSession(out var playerSession))
+                return;
+            if (nukeops.OperativePlayers.ContainsValue(playerSession))
+                return;
 
-        var name = MetaData(uid).EntityName;
+            var name = MetaData(uid).EntityName;
 
-        _operativePlayers.Add(name, playerSession);
+            nukeops.OperativePlayers.Add(name, playerSession);
 
-        if (_ticker.RunLevel != GameRunLevel.InRound)
-            return;
+            if (GameTicker.RunLevel != GameRunLevel.InRound)
+                return;
 
-        if (_nukeopsRuleConfig.GreetSound != null)
-            _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, playerSession);
+            _audioSystem.PlayGlobal(nukeops.GreetSound, playerSession);
 
-        if (_targetStation != null && !string.IsNullOrEmpty(Name(_targetStation.Value)))
-            _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value)));
+            if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value)))
+                _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", nukeops.TargetStation.Value)));
+        }
     }
 
-    private bool SpawnMap()
+    private bool SpawnMap(EntityUid uid, NukeopsRuleComponent? component = null)
     {
-        if (_nukiePlanet != null)
+        if (!Resolve(uid, ref component))
+            return false;
+
+        if (component.NukiePlanet != null)
             return true; // Map is already loaded.
 
-        if (!_nukeopsRuleConfig.SpawnOutpost)
+        if (!component.SpawnOutpost)
             return true;
 
-        _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 path = component.NukieOutpostMap;
+        var shuttlePath = component.NukieShuttleMap;
 
         var mapId = _mapManager.CreateMap();
-        var options = new MapLoadOptions()
+        var options = new MapLoadOptions
         {
             LoadMap = true,
         };
@@ -660,7 +596,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         }
 
         // Assume the first grid is the outpost grid.
-        _nukieOutpost = outpostGrids[0];
+        component.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())
@@ -681,16 +617,15 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
 
         if (TryComp<ShuttleComponent>(shuttleId, out var shuttle))
         {
-            _shuttle.TryFTLDock(shuttleId, shuttle, _nukieOutpost.Value);
+            _shuttle.TryFTLDock(shuttleId, shuttle, component.NukieOutpost.Value);
         }
 
-        _nukiePlanet = mapId;
-        _nukieShuttle = shuttleId;
-
+        component.NukiePlanet = mapId;
+        component.NukieShuttle = shuttleId;
         return true;
     }
 
-    private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber)
+    private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber, NukeopsRuleComponent component )
     {
         string name;
         string role;
@@ -700,19 +635,19 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         switch (spawnNumber)
         {
             case 0:
-                name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.EliteNames]);
-                role = _nukeopsRuleConfig.CommanderRolePrototype;
-                gear = _nukeopsRuleConfig.CommanderStartGearPrototype;
+                name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(component.OperativeNames[component.EliteNames]);
+                role = component.CommanderRolePrototype;
+                gear = component.CommanderStartGearPrototype;
                 break;
             case 1:
-                name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]);
-                role = _nukeopsRuleConfig.OperativeRoleProto;
-                gear = _nukeopsRuleConfig.MedicStartGearPrototype;
+                name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]);
+                role = component.OperativeRoleProto;
+                gear = component.MedicStartGearPrototype;
                 break;
             default:
-                name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]);
-                role = _nukeopsRuleConfig.OperativeRoleProto;
-                gear = _nukeopsRuleConfig.OperativeStartGearPrototype;
+                name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]);
+                role = component.OperativeRoleProto;
+                gear = component.OperativeStartGearPrototype;
                 break;
         }
 
@@ -722,38 +657,38 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
     /// <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)
+    private void SetupOperativeEntity(EntityUid mob, string name, string gear, HumanoidCharacterProfile? profile, NukeopsRuleComponent component)
     {
         MetaData(mob).EntityName = name;
-        EntityManager.EnsureComponent<NukeOperativeComponent>(mob);
+        EnsureComp<NukeOperativeComponent>(mob);
 
         if (profile != null)
         {
             _humanoidSystem.LoadProfile(mob, profile);
         }
 
-        if (_startingGearPrototypes.TryGetValue(gear, out var gearPrototype))
+        if (component.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)
+    private void SpawnOperatives(int spawnCount, List<IPlayerSession> sessions, bool addSpawnPoints, NukeopsRuleComponent component)
     {
-        if (_nukieOutpost == null)
+        if (component.NukieOutpost == null)
             return;
 
-        var outpostUid = _nukieOutpost.Value;
+        var outpostUid = component.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 != _nukeopsRuleConfig.SpawnPointPrototype)
+            if (meta.EntityPrototype?.ID != component.SpawnPointPrototype)
                 continue;
 
-            if (xform.ParentUid != _nukieOutpost)
+            if (xform.ParentUid != component.NukieOutpost)
                 continue;
 
             spawns.Add(xform.Coordinates);
@@ -769,19 +704,19 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         // 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);
+            var spawnDetails = GetOperativeSpawnDetails(i, component);
             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 ?? HumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
+                if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species))
                 {
-                    species = _prototypeManager.Index<SpeciesPrototype>(HumanoidAppearanceSystem.DefaultSpecies);
+                    species = _prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
                 }
 
                 var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns));
-                SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile);
+                SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component);
 
                 var newMind = new Mind.Mind(session.UserId)
                 {
@@ -794,7 +729,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
             }
             else if (addSpawnPoints)
             {
-                var spawnPoint = EntityManager.SpawnEntity(_nukeopsRuleConfig.GhostSpawnPointProto, _random.Pick(spawns));
+                var spawnPoint = EntityManager.SpawnEntity(component.GhostSpawnPointProto, _random.Pick(spawns));
                 var ghostRole = EnsureComp<GhostRoleComponent>(spawnPoint);
                 EnsureComp<GhostRoleMobSpawnerComponent>(spawnPoint);
                 ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name);
@@ -808,22 +743,25 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         }
     }
 
-    private void SpawnOperativesForGhostRoles()
+    private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
     {
-        if (!SpawnMap())
+        if (!Resolve(uid, ref component))
+            return;
+
+        if (!SpawnMap(uid, component))
         {
             Logger.InfoS("nukies", "Failed to load map for nukeops");
             return;
         }
         // Basically copied verbatim from traitor code
-        var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative;
-        var maxOperatives = _nukeopsRuleConfig.MaxOperatives;
+        var playersPerOperative = component.PlayersPerOperative;
+        var maxOperatives = component.MaxOperatives;
 
         var playerPool = _playerSystem.ServerSessions.ToList();
         var numNukies = MathHelper.Clamp(playerPool.Count / playersPerOperative, 1, maxOperatives);
 
         var operatives = new List<IPlayerSession>();
-        SpawnOperatives(numNukies, operatives, true);
+        SpawnOperatives(numNukies, operatives, true, component);
     }
 
     //For admins forcing someone to nukeOps.
@@ -832,77 +770,66 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
         if (!mind.OwnedEntity.HasValue)
             return;
 
-        mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(_nukeopsRuleConfig.OperativeRoleProto)));
+        //ok hardcoded value bad but so is everything else here
+        mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>("Nukeops")));
         SetOutfitCommand.SetOutfit(mind.OwnedEntity.Value, "SyndicateOperativeGearFull", EntityManager);
     }
 
     private void OnStartAttempt(RoundStartAttemptEvent ev)
     {
-        if (!RuleAdded || Configuration is not NukeopsRuleConfiguration nukeOpsConfig)
-            return;
-
-        _nukeopsRuleConfig = nukeOpsConfig;
-        var minPlayers = nukeOpsConfig.MinPlayers;
-        if (!ev.Forced && ev.Players.Length < minPlayers)
+        var query = EntityQueryEnumerator<NukeopsRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var nukeops, out var gameRule))
         {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-            ev.Cancel();
-            return;
-        }
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+                continue;
 
-        if (ev.Players.Length != 0)
-            return;
+            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;
 
-        _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
-        ev.Cancel();
+            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
+            ev.Cancel();
+        }
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        RuleWinType = WinType.Neutral;
-        _winConditions.Clear();
-        _nukieOutpost = null;
-        _nukiePlanet = null;
-
-        _startingGearPrototypes.Clear();
-        _operativeNames.Clear();
-        _operativeMindPendingData.Clear();
-        _operativePlayers.Clear();
-
+        base.Started(uid, component, gameRule, args);
         // TODO: Loot table or something
         foreach (var proto in new[]
                  {
-                     _nukeopsRuleConfig.CommanderStartGearPrototype,
-                     _nukeopsRuleConfig.MedicStartGearPrototype,
-                     _nukeopsRuleConfig.OperativeStartGearPrototype
+                     component.CommanderStartGearPrototype,
+                     component.MedicStartGearPrototype,
+                     component.OperativeStartGearPrototype
                  })
         {
-            _startingGearPrototypes.Add(proto, _prototypeManager.Index<StartingGearPrototype>(proto));
+            component.StartingGearPrototypes.Add(proto, _prototypeManager.Index<StartingGearPrototype>(proto));
         }
 
-        foreach (var proto in new[] { _nukeopsRuleConfig.EliteNames, _nukeopsRuleConfig.NormalNames })
+        foreach (var proto in new[] { component.EliteNames, component.NormalNames })
         {
-            _operativeNames.Add(proto, new List<string>(_prototypeManager.Index<DatasetPrototype>(proto).Values));
+            component.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>(true);
-        foreach (var (_, mindComp) in query)
+        var query = EntityQuery<NukeOperativeComponent, MindComponent, MetaDataComponent>(true);
+        foreach (var (_, mindComp, metaData) in query)
         {
             if (mindComp.Mind == null || !mindComp.Mind.TryGetSession(out var session))
                 continue;
-            var name = MetaData(mindComp.Owner).EntityName;
-            _operativePlayers.Add(name, session);
+            component.OperativePlayers.Add(metaData.EntityName, session);
         }
 
         if (GameTicker.RunLevel == GameRunLevel.InRound)
-            SpawnOperativesForGhostRoles();
+            SpawnOperativesForGhostRoles(uid, component);
     }
 
-    public override void Ended()
-    {
-        _nukeopsRuleConfig.EndsRound = true;
-        _nukeopsRuleConfig.SpawnOutpost = true;
-        _nukeopsRuleConfig.CanLoneOpsSpawn = true;
-    }
 }
index c5877ab513b1b72c0fb13345546637f37670dff3..6ff2429c2b87cf59877c5737d539593ee282753b 100644 (file)
@@ -2,6 +2,7 @@ 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;
@@ -25,7 +26,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
+public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -39,17 +40,6 @@ public sealed class PiratesRuleSystem : GameRuleSystem
     [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()
     {
@@ -57,178 +47,186 @@ public sealed class PiratesRuleSystem : GameRuleSystem
 
         SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawningEvent);
         SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextEvent);
+        SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
     }
 
     private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        if (Deleted(_pirateShip))
-        {
-            // Major loss, the ship somehow got annihilated.
-            ev.AddLine(Loc.GetString("pirates-no-ship"));
-        }
-        else
+        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();
+                List<(double, EntityUid)> mostValuableThefts = new();
 
-            var finalValue = _pricingSystem.AppraiseGrid(_pirateShip, uid =>
-            {
-                foreach (var mind in _pirates)
+                var comp1 = pirates;
+                var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
                 {
-                    if (mind.CurrentEntity == uid)
-                        return false; // Don't appraise the pirates twice, we count them in separately.
+                    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;
+
+                    mostValuableThefts.Add((price, uid));
+                    mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
+                    if (mostValuableThefts.Count > 5)
+                        mostValuableThefts.Pop();
+                });
+
+                foreach (var mind in pirates.Pirates)
+                {
+                    if (mind.CurrentEntity is not null)
+                        finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
                 }
-                return true;
-            }, (uid, price) =>
-            {
-                if (_initialItems.Contains(uid))
-                    return;
 
-                mostValuableThefts.Add((price, uid));
-                mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1));
-                if (mostValuableThefts.Count > 5)
-                    mostValuableThefts.Pop();
-            });
+                var score = finalValue - pirates.InitialShipValue;
 
-            foreach (var mind in _pirates)
-            {
-                if (mind.CurrentEntity is not null)
-                    finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value);
-            }
+                ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}")));
+                ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}")));
+
+                ev.AddLine("");
+                ev.AddLine(Loc.GetString("pirates-most-valuable"));
 
-            var score = finalValue - _initialShipValue;
+                foreach (var (price, obj) in mostValuableThefts)
+                {
+                    ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
+                }
 
-            ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}")));
-            ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}")));
+                if (mostValuableThefts.Count == 0)
+                    ev.AddLine(Loc.GetString("pirates-stole-nothing"));
+            }
 
             ev.AddLine("");
-            ev.AddLine(Loc.GetString("pirates-most-valuable"));
-
-            foreach (var (price, obj) in mostValuableThefts)
+            ev.AddLine(Loc.GetString("pirates-list-start"));
+            foreach (var pirate in pirates.Pirates)
             {
-                ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}")));
+                ev.AddLine($"- {pirate.CharacterName} ({pirate.Session?.Name})");
             }
-
-            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)
     {
-        // Forgive me for copy-pasting nukies.
-        if (!RuleAdded)
+        var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var pirates, out var gameRule))
         {
-            return;
-        }
-
-        _pirates.Clear();
-        _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);
-        }
+            // 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);
+            }
 
-        var map = "/Maps/Shuttles/pirate.yml";
-        var xformQuery = GetEntityQuery<TransformComponent>();
+            var map = "/Maps/Shuttles/pirate.yml";
+            var xformQuery = GetEntityQuery<TransformComponent>();
 
-        var aabbs = _stationSystem.Stations.SelectMany(x =>
-            Comp<StationDataComponent>(x).Grids.Select(x => xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB))).ToArray();
+            var aabbs = _stationSystem.Stations.SelectMany(x =>
+                    Comp<StationDataComponent>(x).Grids.Select(x =>
+                        xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB)))
+                .ToArray();
 
-        var aabb = aabbs[0];
+            var aabb = aabbs[0];
 
-        for (var i = 1; i < aabbs.Length; i++)
-        {
-            aabb.Union(aabbs[i]);
-        }
+            for (var i = 1; i < aabbs.Length; i++)
+            {
+                aabb.Union(aabbs[i]);
+            }
 
-        var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
-        {
-            Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f
-        });
+            var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions
+            {
+                Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f
+            });
 
-        if (!gridId.HasValue)
-        {
-            Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting.");
-            foreach (var session in ops)
+            if (!gridId.HasValue)
             {
-                ev.PlayerPool.Add(session);
+                Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting.");
+                foreach (var session in ops)
+                {
+                    ev.PlayerPool.Add(session);
+                }
+
+                return;
             }
-            return;
-        }
 
-        _pirateShip = gridId.Value;
+            pirates.PirateShip = gridId.Value;
 
-        // TODO: Loot table or something
-        var pirateGear = _prototypeManager.Index<StartingGearPrototype>("PirateGear"); // YARRR
+            // TODO: Loot table or something
+            var pirateGear = _prototypeManager.Index<StartingGearPrototype>("PirateGear"); // YARRR
 
-        var spawns = new List<EntityCoordinates>();
+            var spawns = new List<EntityCoordinates>();
 
-        // Forgive me for hardcoding prototypes
-        foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
-        {
-            if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != _pirateShip) continue;
+            // 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;
 
-            spawns.Add(xform.Coordinates);
-        }
+                spawns.Add(xform.Coordinates);
+            }
 
-        if (spawns.Count == 0)
-        {
-            spawns.Add(Transform(_pirateShip).Coordinates);
-            Logger.WarningS("pirates", $"Fell back to default spawn for pirates!");
-        }
+            if (spawns.Count == 0)
+            {
+                spawns.Add(Transform(pirates.PirateShip).Coordinates);
+                Logger.WarningS("pirates", $"Fell back to default spawn for pirates!");
+            }
 
-        for (var i = 0; i < ops.Length; i++)
-        {
-            var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female;
-            var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
+            for (var i = 0; i < ops.Length; i++)
+            {
+                var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female;
+                var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
 
-            var name = _namingSystem.GetName("Human", gender);
+                var name = _namingSystem.GetName("Human", gender);
 
-            var session = ops[i];
-            var newMind = new Mind.Mind(session.UserId)
-            {
-                CharacterName = name
-            };
-            newMind.ChangeOwningPlayer(session.UserId);
+                var session = ops[i];
+                var newMind = new Mind.Mind(session.UserId)
+                {
+                    CharacterName = name
+                };
+                newMind.ChangeOwningPlayer(session.UserId);
 
-            var mob = Spawn("MobHuman", _random.Pick(spawns));
-            MetaData(mob).EntityName = name;
+                var mob = Spawn("MobHuman", _random.Pick(spawns));
+                MetaData(mob).EntityName = name;
 
-            newMind.TransferTo(mob);
-            var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
-            _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
+                newMind.TransferTo(mob);
+                var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
+                _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
 
-            _pirates.Add(newMind);
+                pirates.Pirates.Add(newMind);
 
-            GameTicker.PlayerJoinGame(session);
-        }
+                GameTicker.PlayerJoinGame(session);
+            }
 
-        _initialShipValue = _pricingSystem.AppraiseGrid(_pirateShip, uid =>
-        {
-            _initialItems.Add(uid);
-            return true;
-        }); // Include the players in the appraisal.
+            pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid =>
+            {
+                pirates.InitialItems.Add(uid);
+                return true;
+            }); // Include the players in the appraisal.
+        }
     }
 
     //Forcing one player to be a pirate.
@@ -241,21 +239,26 @@ public sealed class PiratesRuleSystem : GameRuleSystem
 
     private void OnStartAttempt(RoundStartAttemptEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
-        if (!ev.Forced && ev.Players.Length < minPlayers)
+        var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var pirates, out var gameRule))
         {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-            ev.Cancel();
-            return;
-        }
+            if (!GameTicker.IsGameRuleActive(uid, gameRule))
+                return;
 
-        if (ev.Players.Length == 0)
-        {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready"));
-            ev.Cancel();
+            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();
+            }
         }
     }
 }
index 1df78acd8d45505479bcacb43b60f88805cf7b8a..a26a2d783c79a41a59e49cac32287b1f9a476315 100644 (file)
@@ -1,21 +1,21 @@
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Sandbox;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class SandboxRuleSystem : GameRuleSystem
+public sealed class SandboxRuleSystem : GameRuleSystem<SandboxRuleComponent>
 {
     [Dependency] private readonly SandboxSystem _sandbox = default!;
 
-    public override string Prototype => "Sandbox";
-
-    public override void Started()
+    protected override void Started(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
+        base.Started(uid, component, gameRule, args);
         _sandbox.IsSandboxEnabled = true;
     }
 
-    public override void Ended()
+    protected override void Ended(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
     {
+        base.Ended(uid, component, gameRule, args);
         _sandbox.IsSandboxEnabled = false;
     }
 }
index 4878f65ff077bc86b0ec017a9ef2f2a449d208fe..8ad83ecd5d93e710494cb15c7be190c907149c66 100644 (file)
@@ -1,6 +1,5 @@
-using System.Linq;
 using Content.Server.GameTicking.Presets;
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Robust.Shared.Prototypes;
@@ -8,34 +7,40 @@ using Robust.Shared.Random;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class SecretRuleSystem : GameRuleSystem
+public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly GameTicker _ticker = default!;
 
-    public override string Prototype => "Secret";
-
-    public override void Started()
+    protected override void Started(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        PickRule();
+        base.Started(uid, component, gameRule, args);
+        PickRule(component);
     }
 
-    public override void Ended()
+    protected override void Ended(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
     {
-        // Preset should already handle it.
+        base.Ended(uid, component, gameRule, args);
+
+        foreach (var rule in component.AdditionalGameRules)
+        {
+            GameTicker.EndGameRule(rule);
+        }
     }
 
-    private void PickRule()
+    private void PickRule(SecretRuleComponent component)
     {
         // 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.
         var preset = _prototypeManager.Index<WeightedRandomPrototype>("Secret").Pick(_random);
         Logger.InfoS("gamepreset", $"Selected {preset} for secret.");
 
-        foreach (var rule in _prototypeManager.Index<GamePresetPrototype>(preset).Rules)
+        var rules = _prototypeManager.Index<GamePresetPrototype>(preset).Rules;
+        foreach (var rule in rules)
         {
-            _ticker.StartGameRule(_prototypeManager.Index<GameRulePrototype>(rule));
+            Logger.Debug($"what the fuck, {rule}");
+            GameTicker.StartGameRule(rule, out var ruleEnt);
+            component.AdditionalGameRules.Add(ruleEnt);
         }
     }
 }
diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
deleted file mode 100644 (file)
index 3c82367..0000000
+++ /dev/null
@@ -1,456 +0,0 @@
-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
deleted file mode 100644 (file)
index a634bb3..0000000
+++ /dev/null
@@ -1,276 +0,0 @@
-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 9edfe80d20e7446a2114d9839bd7f72ab64f0376..bb6bb89f2ac94137ced9d0757a4837c7a77821c7 100644 (file)
@@ -1,5 +1,6 @@
 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;
@@ -24,7 +25,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Server.GameTicking.Rules;
 
-public sealed class TraitorRuleSystem : GameRuleSystem
+public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -32,7 +33,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem
     [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,30 +40,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem
 
     private ISawmill _sawmill = default!;
 
-    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();
+    private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
+    private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors);
 
     public override void Initialize()
     {
@@ -77,101 +55,101 @@ public sealed class TraitorRuleSystem : GameRuleSystem
         SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
     }
 
-    public override void Update(float frameTime)
+    protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime)
     {
-        base.Update(frameTime);
-
-        if (SelectionStatus == SelectionState.ReadyToSelect && _gameTiming.CurTime >= _announceAt)
-            DoTraitorStart();
-    }
+        base.ActiveTick(uid, component, gameRule, frameTime);
 
-    public override void Started(){}
-
-    public override void Ended()
-    {
-        Traitors.Clear();
-        _startCandidates.Clear();
-        SelectionStatus = SelectionState.WaitingForSpawn;
+        if (component.SelectionStatus == TraitorRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt)
+            DoTraitorStart(component);
     }
 
     private void OnStartAttempt(RoundStartAttemptEvent ev)
     {
-        MakeCodewords();
-        if (!RuleAdded)
-            return;
-
-        var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers);
-        if (!ev.Forced && ev.Players.Length < minPlayers)
+        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
         {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-            ev.Cancel();
-            return;
-        }
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+                continue;
 
-        if (ev.Players.Length == 0)
-        {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
-            ev.Cancel();
+            MakeCodewords(traitor);
+
+            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;
+            }
+
+            if (ev.Players.Length == 0)
+            {
+                _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready"));
+                ev.Cancel();
+            }
         }
     }
 
-    private void MakeCodewords()
+    private void MakeCodewords(TraitorRuleComponent component)
     {
         var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
         var adjectives = _prototypeManager.Index<DatasetPrototype>("adjectives").Values;
         var verbs = _prototypeManager.Index<DatasetPrototype>("verbs").Values;
         var codewordPool = adjectives.Concat(verbs).ToList();
         var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count);
-        Codewords = new string[finalCodewordCount];
+        component.Codewords = new string[finalCodewordCount];
         for (var i = 0; i < finalCodewordCount; i++)
         {
-            Codewords[i] = _random.PickAndTake(codewordPool);
+            component.Codewords[i] = _random.PickAndTake(codewordPool);
         }
     }
 
-    private void DoTraitorStart()
+    private void DoTraitorStart(TraitorRuleComponent component)
     {
-        if (!_startCandidates.Any())
+        if (!component.StartCandidates.Any())
         {
             _sawmill.Error("Tried to start Traitor mode without any candidates.");
             return;
         }
 
-        var numTraitors = MathHelper.Clamp(_startCandidates.Count / _playersPerTraitor, 1, _maxTraitors);
-        var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount);
-
-        var traitorPool = FindPotentialTraitors(_startCandidates);
+        var numTraitors = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerTraitor, 1, MaxTraitors);
+        var traitorPool = FindPotentialTraitors(component.StartCandidates, component);
         var selectedTraitors = PickTraitors(numTraitors, traitorPool);
 
         foreach (var traitor in selectedTraitors)
+        {
             MakeTraitor(traitor);
+        }
 
-        SelectionStatus = SelectionState.SelectionMade;
+        component.SelectionStatus = TraitorRuleComponent.SelectionState.SelectionMade;
     }
 
     private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        foreach (var player in ev.Players)
+        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
         {
-            if (!ev.Profiles.ContainsKey(player.UserId))
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
                 continue;
+            foreach (var player in ev.Players)
+            {
+                if (!ev.Profiles.ContainsKey(player.UserId))
+                    continue;
 
-            _startCandidates[player] = ev.Profiles[player.UserId];
-        }
+                traitor.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)));
 
-        _announceAt = _gameTiming.CurTime + delay;
+            traitor.AnnounceAt = _gameTiming.CurTime + delay;
 
-        SelectionStatus = SelectionState.ReadyToSelect;
+            traitor.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToSelect;
+        }
     }
 
-    public List<IPlayerSession> FindPotentialTraitors(in Dictionary<IPlayerSession, HumanoidCharacterProfile> candidates)
+    public List<IPlayerSession> FindPotentialTraitors(in Dictionary<IPlayerSession, HumanoidCharacterProfile> candidates, TraitorRuleComponent component)
     {
         var list = new List<IPlayerSession>();
         var pendingQuery = GetEntityQuery<PendingClockInComponent>();
@@ -196,7 +174,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
         foreach (var player in list)
         {
             var profile = candidates[player];
-            if (profile.AntagPreferences.Contains(TraitorPrototypeID))
+            if (profile.AntagPreferences.Contains(component.TraitorPrototypeId))
             {
                 prefList.Add(player);
             }
@@ -228,6 +206,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem
 
     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)
         {
@@ -254,14 +240,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem
         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>(TraitorPrototypeID);
+        var antagPrototype = _prototypeManager.Index<AntagPrototype>(traitorRule.TraitorPrototypeId);
         var traitorRole = new TraitorRole(mind, antagPrototype);
         mind.AddRole(traitorRole);
-        Traitors.Add(traitorRole);
-        traitorRole.GreetTraitor(Codewords, code);
+        traitorRule.Traitors.Add(traitorRole);
+        traitorRole.GreetTraitor(traitorRule.Codewords, code);
 
         _faction.RemoveFaction(entity, "NanoTrasen", false);
         _faction.AddFaction(entity, "Syndicate");
@@ -280,147 +267,173 @@ public sealed class TraitorRuleSystem : GameRuleSystem
         }
 
         //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(", ", Codewords)))
+        traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", traitorRule.Codewords)))
             + "\n" + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("", code)));
 
-        _audioSystem.PlayGlobal(_addedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default);
+        _audioSystem.PlayGlobal(traitorRule.AddedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default);
         return true;
     }
 
     private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
     {
-        if (!RuleAdded)
-            return;
-        if (TotalTraitors >= _maxTraitors)
-            return;
-        if (!ev.LateJoin)
-            return;
-        if (!ev.Profile.AntagPreferences.Contains(TraitorPrototypeID))
-            return;
+        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 (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
-            return;
+            if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
+                continue;
 
-        if (!job.CanBeAntag)
-            return;
+            if (!job.CanBeAntag)
+                continue;
 
-        // 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;
-        }
+            // Before the announcement is made, late-joiners are considered the same as players who readied.
+            if (traitor.SelectionStatus < TraitorRuleComponent.SelectionState.SelectionMade)
+            {
+                traitor.StartCandidates[ev.Player] = ev.Profile;
+                continue;
+            }
 
-        // the nth player we adjust our probabilities around
-        int target = ((_playersPerTraitor * TotalTraitors) + 1);
+            // the nth player we adjust our probabilities around
+            var target = PlayersPerTraitor * traitor.TotalTraitors + 1;
 
-        float chance = (1f / _playersPerTraitor);
+            var chance = 1f / PlayersPerTraitor;
 
-        // If we have too many traitors, divide by how many players below target for next traitor we are.
-        if (ev.JoinOrder < target)
-        {
-            chance /= (target - ev.JoinOrder);
-        } else // Tick up towards 100% chance.
-        {
-            chance *= ((ev.JoinOrder + 1) - target);
-        }
-        if (chance > 1)
-            chance = 1;
+            // 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);
+            }
 
-        // 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);
+            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);
+            }
         }
     }
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        var result = Loc.GetString("traitor-round-end-result", ("traitorCount", Traitors.Count));
+        var query = EntityQueryEnumerator<TraitorRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var traitor, out var gameRule))
+        {
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+                continue;
 
-        result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", Codewords))) + "\n";
+            var result = Loc.GetString("traitor-round-end-result", ("traitorCount", traitor.Traitors.Count));
 
-        foreach (var traitor in Traitors)
-        {
-            var name = traitor.Mind.CharacterName;
-            traitor.Mind.TryGetSession(out var session);
-            var username = session?.Name;
+            result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", traitor.Codewords))) +
+                      "\n";
 
-            var objectives = traitor.Mind.AllObjectives.ToArray();
-            if (objectives.Length == 0)
+            foreach (var t in traitor.Traitors)
             {
+                var name = t.Mind.CharacterName;
+                t.Mind.TryGetSession(out var session);
+                var username = session?.Name;
+
+                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;
+                }
+
                 if (username != null)
                 {
                     if (name == null)
-                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username));
+                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives",
+                            ("user", username));
                     else
-                        result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name));
+                        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-named", ("name", name));
-
-                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}");
+                    result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name));
 
-                foreach (var objective in objectiveGroup)
+                foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
                 {
-                    foreach (var condition in objective.Conditions)
+                    result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}");
+
+                    foreach (var objective in objectiveGroup)
                     {
-                        var progress = condition.Progress;
-                        if (progress > 0.99f)
-                        {
-                            result += "\n- " + Loc.GetString(
-                                "traitor-objective-condition-success",
-                                ("condition", condition.Title),
-                                ("markupColor", "green")
-                            );
-                        }
-                        else
+                        foreach (var condition in objective.Conditions)
                         {
-                            result += "\n- " + Loc.GetString(
-                                "traitor-objective-condition-fail",
-                                ("condition", condition.Title),
-                                ("progress", (int) (progress * 100)),
-                                ("markupColor", "red")
-                            );
+                            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")
+                                );
+                            }
                         }
                     }
                 }
             }
+
+            ev.AddLine(result);
         }
-        ev.AddLine(result);
     }
 
-    public IEnumerable<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind)
+    public List<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind)
     {
-        var traitors = Traitors;
-        List<TraitorRole> removeList = new();
+        List<TraitorRole> allTraitors = new();
+        foreach (var traitor in EntityQuery<TraitorRuleComponent>())
+        {
+            foreach (var role in GetOtherTraitorsAliveAndConnected(ourMind, traitor))
+            {
+                if (!allTraitors.Contains(role))
+                    allTraitors.Add(role);
+            }
+        }
 
-        return Traitors // don't want
-            .Where(t => t.Mind is not null) // no mind
+        return allTraitors;
+    }
+
+    public List<TraitorRole> GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind, TraitorRuleComponent component)
+    {
+        return component.Traitors // don't want
             .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); // not in original body
+            .Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity).ToList(); // not in original body
     }
 }
index f51a22b42d2882e6116780f62547d7ee43bc5005..4ac83b354e61e37ad5333cc9fc9e665ae398f785 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.Humanoid;
+using Content.Server.GameTicking.Rules.Components;
 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
+public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -44,14 +44,6 @@ public sealed class ZombieRuleSystem : GameRuleSystem
     [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();
@@ -67,60 +59,61 @@ public sealed class ZombieRuleSystem : GameRuleSystem
 
     private void OnRoundEndText(RoundEndTextAppendEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        //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)
+        foreach (var zombie in EntityQuery<ZombieRuleComponent>())
         {
-            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"));
 
-        //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(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)));
+            }
+
+            //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)
             {
-                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)));
+                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)));
+                }
             }
         }
     }
 
     private void OnJobAssigned(RulePlayerJobsAssignedEvent ev)
     {
-        if (!RuleAdded)
-            return;
-
-        _initialInfectedNames = new();
-
-        InfectInitialPlayers();
+        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);
+        }
     }
 
     /// <remarks>
@@ -129,15 +122,11 @@ public sealed class ZombieRuleSystem : GameRuleSystem
     /// </remarks>
     private void OnMobStateChanged(MobStateChangedEvent ev)
     {
-        if (!RuleAdded)
-            return;
         CheckRoundEnd(ev.Target);
     }
 
     private void OnEntityZombified(EntityZombifiedEvent ev)
     {
-        if (!RuleAdded)
-            return;
         CheckRoundEnd(ev.Target);
     }
 
@@ -147,50 +136,59 @@ public sealed class ZombieRuleSystem : GameRuleSystem
     /// <param name="target">depending on this uid, we should care about the round ending</param>
     private void CheckRoundEnd(EntityUid target)
     {
-        //we only care about players, not monkeys and such.
-        if (!HasComp<HumanoidAppearanceComponent>(target))
-            return;
+        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;
 
-        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)
     {
-        if (!RuleAdded)
-            return;
-
-        var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
-        if (!ev.Forced && ev.Players.Length < minPlayers)
+        var query = EntityQueryEnumerator<ZombieRuleComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var zombies, out var gameRule))
         {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
-            ev.Cancel();
-            return;
-        }
+            if (!GameTicker.IsGameRuleAdded(uid, gameRule))
+                continue;
 
-        if (ev.Players.Length == 0)
-        {
-            _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
-            ev.Cancel();
+            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;
+            }
+
+            if (ev.Players.Length == 0)
+            {
+                _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready"));
+                ev.Cancel();
+            }
         }
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        //this technically will run twice with zombies on roundstart, but it doesn't matter because it fails instantly
-        InfectInitialPlayers();
+        base.Started(uid, component, gameRule, args);
+        InfectInitialPlayers(component);
     }
 
-    public override void Ended() { }
-
     private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
     {
         _zombify.ZombifyEntity(uid);
 
-        var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombifySelfActionPrototype));
+        var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
         _action.RemoveAction(uid, action);
     }
 
@@ -228,7 +226,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
     ///     allowing this gamemode to be started midround. As such, it doesn't need
     ///     any information besides just running.
     /// </remarks>
-    private void InfectInitialPlayers()
+    private void InfectInitialPlayers(ZombieRuleComponent component)
     {
         var allPlayers = _playerManager.ServerSessions.ToList();
         var playerList = new List<IPlayerSession>();
@@ -240,7 +238,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
                 playerList.Add(player);
 
                 var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter;
-                if (pref.AntagPreferences.Contains(PatientZeroPrototypeID))
+                if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeID))
                     prefList.Add(player);
             }
         }
@@ -284,15 +282,15 @@ public sealed class ZombieRuleSystem : GameRuleSystem
 
             DebugTools.AssertNotNull(mind.OwnedEntity);
 
-            mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(PatientZeroPrototypeID)));
+            mind.AddRole(new TraitorRole(mind, _prototypeManager.Index<AntagPrototype>(component.PatientZeroPrototypeID)));
 
             var inCharacterName = string.Empty;
             if (mind.OwnedEntity != null)
             {
-                _diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, InitialZombieVirusPrototype);
+                _diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, component.InitialZombieVirusPrototype);
                 inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName;
 
-                var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombifySelfActionPrototype));
+                var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(ZombieRuleComponent.ZombifySelfActionPrototype));
                 _action.AddAction(mind.OwnedEntity.Value, action, null);
             }
 
@@ -303,7 +301,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
 
                 //gets the names now in case the players leave.
                 //this gets unhappy if people with the same name get chose. Probably shouldn't happen.
-                _initialInfectedNames.Add(inCharacterName, mind.Session.Name);
+                component.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 d653df357dfd7be0d7d3c9d4c8c2cf8b9e8b533f..0fd1d53a098860ef8edc9750b4ae780826e68394 100644 (file)
@@ -14,9 +14,10 @@ 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.
+            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.
             return new RandomTraitorAliveCondition { _target = IoCManager.Resolve<IRobustRandom>().Pick(traitors).Mind };
         }
 
index fd19603387eacab6f77e628bc4ba92fe17df66b9..99de8be282ded808b345c8b7d350e17b014afe84 100644 (file)
@@ -13,7 +13,9 @@ 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();
 
@@ -23,7 +25,7 @@ namespace Content.Server.Objectives.Conditions
                 {
                     foreach (var condition in objective.Conditions)
                     {
-                        if (condition.GetType() == typeof(RandomTraitorProgressCondition))
+                        if (condition is RandomTraitorProgressCondition)
                         {
                             removeList.Add(traitor);
                         }
index e833165862baa476a742d1171f1b7782c241e14e..5d4e458297e01f786f5a1d87c9b394a1d98e0b67 100644 (file)
@@ -11,7 +11,7 @@ namespace Content.Server.Objectives.Requirements
 
         public bool CanBeAssigned(Mind.Mind mind)
         {
-            return EntitySystem.Get<TraitorRuleSystem>().TotalTraitors >= _requiredTraitors;
+            return EntitySystem.Get<TraitorRuleSystem>().GetOtherTraitorsAliveAndConnected(mind).Count >= _requiredTraitors;
         }
     }
 }
index a5279ce848d1a55b98577cb0a41167757d1f2acb..12a16151c1d6a187bce3f3bc1691087afc976e99 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Server.GameTicking.Rules;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
@@ -9,11 +8,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<GameRulePrototype>))]
+        [DataField("gameRules", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
         public readonly List<string> GameRules = new();
 
         [ViewVariables(VVAccess.ReadWrite)]
index 1eef300972f244b37b58407bce3a98baa993062e..341a2c9d440c212464fc26281ad9f7a318696826 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Spawners.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Random;
@@ -22,65 +23,67 @@ namespace Content.Server.Spawners.EntitySystems
 
         private void OnCondSpawnMapInit(EntityUid uid, ConditionalSpawnerComponent component, MapInitEvent args)
         {
-            TrySpawn(component);
+            TrySpawn(uid, component);
         }
 
         private void OnRandSpawnMapInit(EntityUid uid, RandomSpawnerComponent component, MapInitEvent args)
         {
-            Spawn(component);
-            EntityManager.QueueDeleteEntity(uid);
+            Spawn(uid, component);
+            QueueDel(uid);
         }
 
-        private void OnRuleStarted(GameRuleStartedEvent args)
+        private void OnRuleStarted(ref GameRuleStartedEvent args)
         {
-            foreach (var spawner in EntityManager.EntityQuery<ConditionalSpawnerComponent>())
+            var query = EntityQueryEnumerator<ConditionalSpawnerComponent>();
+            while (query.MoveNext(out var uid, out var spawner))
             {
-                RuleStarted(spawner, args);
+                RuleStarted(uid, spawner, args);
             }
         }
 
-        public void RuleStarted(ConditionalSpawnerComponent component, GameRuleStartedEvent obj)
+        public void RuleStarted(EntityUid uid, ConditionalSpawnerComponent component, GameRuleStartedEvent obj)
         {
-            if(component.GameRules.Contains(obj.Rule.ID))
-                Spawn(component);
+            if (component.GameRules.Contains(obj.RuleId))
+                Spawn(uid, component);
         }
 
-        private void TrySpawn(ConditionalSpawnerComponent component)
+        private void TrySpawn(EntityUid uid, ConditionalSpawnerComponent component)
         {
             if (component.GameRules.Count == 0)
             {
-                Spawn(component);
+                Spawn(uid, component);
                 return;
             }
 
             foreach (var rule in component.GameRules)
             {
-                if (!_ticker.IsGameRuleStarted(rule)) continue;
-                Spawn(component);
+                if (!_ticker.IsGameRuleActive(rule))
+                    continue;
+                Spawn(uid, component);
                 return;
             }
         }
 
-        private void Spawn(ConditionalSpawnerComponent component)
+        private void Spawn(EntityUid uid, 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: {component.Owner}");
+                Logger.Warning($"Prototype list in ConditionalSpawnComponent is empty! Entity: {ToPrettyString(uid)}");
                 return;
             }
 
-            if (!Deleted(component.Owner))
-                EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), Transform(component.Owner).Coordinates);
+            if (!Deleted(uid))
+                EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), Transform(uid).Coordinates);
         }
 
-        private void Spawn(RandomSpawnerComponent component)
+        private void Spawn(EntityUid uid, RandomSpawnerComponent component)
         {
             if (component.RarePrototypes.Count > 0 && (component.RareChance == 1.0f || _robustRandom.Prob(component.RareChance)))
             {
-                EntityManager.SpawnEntity(_robustRandom.Pick(component.RarePrototypes), Transform(component.Owner).Coordinates);
+                EntityManager.SpawnEntity(_robustRandom.Pick(component.RarePrototypes), Transform(uid).Coordinates);
                 return;
             }
 
@@ -89,17 +92,18 @@ namespace Content.Server.Spawners.EntitySystems
 
             if (component.Prototypes.Count == 0)
             {
-                Logger.Warning($"Prototype list in RandomSpawnerComponent is empty! Entity: {component.Owner}");
+                Logger.Warning($"Prototype list in RandomSpawnerComponent is empty! Entity: {ToPrettyString(uid)}");
                 return;
             }
 
-            if (Deleted(component.Owner)) return;
+            if (Deleted(uid))
+                return;
 
             var offset = component.Offset;
             var xOffset = _robustRandom.NextFloat(-offset, offset);
             var yOffset = _robustRandom.NextFloat(-offset, offset);
 
-            var coordinates = Transform(component.Owner).Coordinates.Offset(new Vector2(xOffset, yOffset));
+            var coordinates = Transform(uid).Coordinates.Offset(new Vector2(xOffset, yOffset));
 
             EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), coordinates);
         }
index 0304978ed6c94239df9769492df961bf52c352f4..bc77a9ce47129106d73cabe4f8f138529b4192d4 100644 (file)
@@ -1,12 +1,7 @@
-using System.Linq;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Configurations;
-using Content.Shared.CCVar;
-using Content.Shared.GameTicking;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.StationEvents.Components;
 using JetBrains.Annotations;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents
@@ -16,53 +11,49 @@ namespace Content.Server.StationEvents
     ///     game presets use.
     /// </summary>
     [UsedImplicitly]
-    public sealed class BasicStationEventSchedulerSystem : GameRuleSystem
+    public sealed class BasicStationEventSchedulerSystem : GameRuleSystem<BasicStationEventSchedulerComponent>
     {
-        public override string Prototype => "BasicStationEventScheduler";
-
         [Dependency] private readonly IRobustRandom _random = default!;
         [Dependency] private readonly EventManagerSystem _event = default!;
 
-        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()
+        protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
+            GameRuleEndedEvent args)
         {
-            _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
+            component.TimeUntilNextEvent = BasicStationEventSchedulerComponent.MinimumTimeUntilFirstEvent;
         }
 
+
         public override void Update(float frameTime)
         {
             base.Update(frameTime);
 
-            if (!RuleStarted || !_event.EventsEnabled)
+            if (!_event.EventsEnabled)
                 return;
 
-            if (_timeUntilNextEvent > 0)
+            var query = EntityQueryEnumerator<BasicStationEventSchedulerComponent, GameRuleComponent>();
+            while (query.MoveNext(out var uid, out var eventScheduler, out var gameRule))
             {
-                _timeUntilNextEvent -= frameTime;
-                return;
-            }
+                if (!GameTicker.IsGameRuleActive(uid, gameRule))
+                    continue;
 
-            _event.RunRandomEvent();
-            ResetTimer();
+                if (eventScheduler.TimeUntilNextEvent > 0)
+                {
+                    eventScheduler.TimeUntilNextEvent -= frameTime;
+                    return;
+                }
+
+                _event.RunRandomEvent();
+                ResetTimer(eventScheduler);
+            }
         }
 
         /// <summary>
         /// Reset the event timer once the event is done.
         /// </summary>
-        private void ResetTimer()
+        private void ResetTimer(BasicStationEventSchedulerComponent component)
         {
             // 5 - 25 minutes. TG does 3-10 but that's pretty frequent
-            _timeUntilNextEvent = _random.Next(300, 1500);
+            component.TimeUntilNextEvent = _random.Next(300, 1500);
         }
     }
 }
diff --git a/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs b/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs
new file mode 100644 (file)
index 0000000..8e8e853
--- /dev/null
@@ -0,0 +1,15 @@
+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
new file mode 100644 (file)
index 0000000..1f920b6
--- /dev/null
@@ -0,0 +1,14 @@
+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
new file mode 100644 (file)
index 0000000..fe953c9
--- /dev/null
@@ -0,0 +1,31 @@
+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
new file mode 100644 (file)
index 0000000..06515f5
--- /dev/null
@@ -0,0 +1,9 @@
+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
new file mode 100644 (file)
index 0000000..a4acef1
--- /dev/null
@@ -0,0 +1,9 @@
+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
new file mode 100644 (file)
index 0000000..5132a09
--- /dev/null
@@ -0,0 +1,9 @@
+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
new file mode 100644 (file)
index 0000000..d82557c
--- /dev/null
@@ -0,0 +1,25 @@
+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
new file mode 100644 (file)
index 0000000..6e3f69a
--- /dev/null
@@ -0,0 +1,9 @@
+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
new file mode 100644 (file)
index 0000000..5f2c2f5
--- /dev/null
@@ -0,0 +1,46 @@
+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
new file mode 100644 (file)
index 0000000..82cc1ac
--- /dev/null
@@ -0,0 +1,9 @@
+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
new file mode 100644 (file)
index 0000000..5356603
--- /dev/null
@@ -0,0 +1,18 @@
+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
new file mode 100644 (file)
index 0000000..e42eef5
--- /dev/null
@@ -0,0 +1,25 @@
+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
new file mode 100644 (file)
index 0000000..cb62746
--- /dev/null
@@ -0,0 +1,16 @@
+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
new file mode 100644 (file)
index 0000000..1788a78
--- /dev/null
@@ -0,0 +1,19 @@
+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
new file mode 100644 (file)
index 0000000..dd3f38b
--- /dev/null
@@ -0,0 +1,17 @@
+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
new file mode 100644 (file)
index 0000000..6c00e9e
--- /dev/null
@@ -0,0 +1,9 @@
+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
new file mode 100644 (file)
index 0000000..d195eaa
--- /dev/null
@@ -0,0 +1,10 @@
+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 bf40d50249aa18cdf0c90baaae1aed191055badf..d22f3e74eb9ef15f120191bb0e00a1662a38be64 100644 (file)
@@ -1,6 +1,8 @@
-namespace Content.Server.StationEvents.Components;
+using Content.Server.StationEvents.Events;
 
-[RegisterComponent]
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(RandomSentienceRule))]
 public sealed class SentienceTargetComponent : Component
 {
     [DataField("flavorKind", required: true)]
similarity index 69%
rename from Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs
rename to Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs
index f014c562804be47cc71ba890eac934c7d36a6422..92a3b433757702cc56e5dae64f79fe4136c47697 100644 (file)
@@ -1,25 +1,15 @@
+using Content.Server.StationEvents.Events;
 using Content.Shared.Radio;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
 
-namespace Content.Server.GameTicking.Rules.Configurations;
+namespace Content.Server.StationEvents.Components;
 
 /// <summary>
 ///     Solar Flare event specific configuration
 /// </summary>
-public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfiguration
+[RegisterComponent, Access(typeof(SolarFlareRule))]
+public sealed class SolarFlareRuleComponent : Component
 {
-    /// <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>
@@ -43,4 +33,4 @@ public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfigura
     /// </summary>
     [DataField("doorToggleChancePerSecond")]
     public float DoorToggleChancePerSecond;
-}
\ No newline at end of file
+}
diff --git a/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs
new file mode 100644 (file)
index 0000000..15e01ac
--- /dev/null
@@ -0,0 +1,9 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(SpiderSpawnRule))]
+public sealed class SpiderSpawnRuleComponent : Component
+{
+
+}
similarity index 58%
rename from Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs
rename to Content.Server/StationEvents/Components/StationEventComponent.cs
index 84689dc225962c9f25730c715a56ac935ef8960e..e79fd6e86d0dcc52b855926696469b75a0bd3a18 100644 (file)
@@ -1,19 +1,14 @@
-using JetBrains.Annotations;
-using Robust.Shared.Audio;
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Server.GameTicking.Rules.Configurations;
+namespace Content.Server.StationEvents.Components;
 
 /// <summary>
-///     Defines a configuration for a given station event game rule, since all station events are just
-///     game rules.
+///     Defines basic data for a station event
 /// </summary>
-[UsedImplicitly]
-public class StationEventRuleConfiguration : GameRuleConfiguration
+[RegisterComponent]
+public sealed class StationEventComponent : Component
 {
-    [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;
@@ -48,16 +43,22 @@ public class StationEventRuleConfiguration : GameRuleConfiguration
     public int ReoccurrenceDelay = 30;
 
     /// <summary>
-    ///     When in the lifetime to start the event.
+    ///     How long after being added does the event start
     /// </summary>
-    [DataField("startAfter")]
-    public float StartAfter;
+    [DataField("startDelay")]
+    public TimeSpan StartDelay = TimeSpan.Zero;
 
     /// <summary>
-    ///     When in the lifetime to end the event..
+    ///     How long the event lasts.
     /// </summary>
-    [DataField("endAfter")]
-    public float EndAfter = float.MaxValue;
+    [DataField("duration")]
+    public TimeSpan Duration = TimeSpan.FromSeconds(1);
+
+    /// <summary>
+    ///     The max amount of time the event lasts.
+    /// </summary>
+    [DataField("maxDuration")]
+    public TimeSpan? MaxDuration;
 
     /// <summary>
     ///     How many players need to be present on station for the event to run
@@ -73,4 +74,16 @@ public class StationEventRuleConfiguration : GameRuleConfiguration
     /// </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;
 }
diff --git a/Content.Server/StationEvents/Components/VentClogRuleComponent.cs b/Content.Server/StationEvents/Components/VentClogRuleComponent.cs
new file mode 100644 (file)
index 0000000..79f4993
--- /dev/null
@@ -0,0 +1,14 @@
+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 3dc46642e73a8ac1c7f9d3c4a78ad478e8c2144d..f675744929d66ae645ed2a5421a6a8baf56d6902 100644 (file)
@@ -1,6 +1,8 @@
-namespace Content.Server.StationEvents.Components;
+using Content.Server.StationEvents.Events;
 
-[RegisterComponent]
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(VentClogRule))]
 public sealed class VentCritterSpawnLocationComponent : Component
 {
 
diff --git a/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs b/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs
new file mode 100644 (file)
index 0000000..5332796
--- /dev/null
@@ -0,0 +1,15 @@
+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 6a2b646602352dd4dd1326c251ff2a477c4d0961..72a5dfac7733da24c35a0e04330ab764275a35aa 100644 (file)
@@ -1,9 +1,7 @@
 using System.Linq;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.StationEvents.Components;
 using Content.Shared.CCVar;
-using Content.Shared.GameTicking;
 using Robust.Server.Player;
 using Robust.Shared.Configuration;
 using Robust.Shared.Prototypes;
@@ -31,6 +29,14 @@ 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()
@@ -46,16 +52,15 @@ public sealed class EventManagerSystem : EntitySystem
     {
         var randomEvent = PickRandomEvent();
 
-        if (randomEvent == null
-            || !_prototype.TryIndex<GameRulePrototype>(randomEvent.Id, out var proto))
+        if (randomEvent == null)
         {
             var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events");
             _sawmill.Error(errStr);
             return errStr;
         }
 
-        GameTicker.AddGameRule(proto);
-        var str = Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Id));
+        var ent = GameTicker.AddGameRule(randomEvent);
+        var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent)));
         _sawmill.Info(str);
         return str;
     }
@@ -63,7 +68,7 @@ public sealed class EventManagerSystem : EntitySystem
     /// <summary>
     /// Randomly picks a valid event.
     /// </summary>
-    public StationEventRuleConfiguration? PickRandomEvent()
+    public string? PickRandomEvent()
     {
         var availableEvents = AvailableEvents();
         _sawmill.Info($"Picking from {availableEvents.Count} total available events");
@@ -74,7 +79,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 StationEventRuleConfiguration? FindEvent(List<StationEventRuleConfiguration> availableEvents)
+    private string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
     {
         if (availableEvents.Count == 0)
         {
@@ -84,20 +89,20 @@ public sealed class EventManagerSystem : EntitySystem
 
         var sumOfWeights = 0;
 
-        foreach (var stationEvent in availableEvents)
+        foreach (var stationEvent in availableEvents.Values)
         {
             sumOfWeights += (int) stationEvent.Weight;
         }
 
         sumOfWeights = _random.Next(sumOfWeights);
 
-        foreach (var stationEvent in availableEvents)
+        foreach (var (proto, stationEvent) in availableEvents)
         {
             sumOfWeights -= (int) stationEvent.Weight;
 
             if (sumOfWeights <= 0)
             {
-                return stationEvent;
+                return proto.ID;
             }
         }
 
@@ -110,67 +115,73 @@ public sealed class EventManagerSystem : EntitySystem
     /// </summary>
     /// <param name="ignoreEarliestStart"></param>
     /// <returns></returns>
-    private List<StationEventRuleConfiguration> AvailableEvents(bool ignoreEarliestStart = false)
+    private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool ignoreEarliestStart = false)
     {
-        TimeSpan currentTime;
         var playerCount = _playerManager.PlayerCount;
 
         // playerCount does a lock so we'll just keep the variable here
-        if (!ignoreEarliestStart)
-        {
-            currentTime = GameTicker.RoundDuration();
-        }
-        else
-        {
-            currentTime = TimeSpan.Zero;
-        }
+        var currentTime = !ignoreEarliestStart
+            ? GameTicker.RoundDuration()
+            : TimeSpan.Zero;
 
-        var result = new List<StationEventRuleConfiguration>();
+        var result = new Dictionary<EntityPrototype, StationEventComponent>();
 
-        foreach (var stationEvent in AllEvents())
+        foreach (var (proto, stationEvent) in AllEvents())
         {
-            if (CanRun(stationEvent, playerCount, currentTime))
+            if (CanRun(proto, stationEvent, playerCount, currentTime))
             {
-                _sawmill.Debug($"Adding event {stationEvent.Id} to possibilities");
-                result.Add(stationEvent);
+                _sawmill.Debug($"Adding event {proto.ID} to possibilities");
+                result.Add(proto, stationEvent);
             }
         }
 
         return result;
     }
 
-    private IEnumerable<StationEventRuleConfiguration> AllEvents()
+    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)
     {
-        return _prototype.EnumeratePrototypes<GameRulePrototype>()
-            .Where(p => p.Configuration is StationEventRuleConfiguration)
-            .Select(p => (StationEventRuleConfiguration) p.Configuration);
+        return GetOccurrences(stationEvent.ID);
     }
 
-    private int GetOccurrences(StationEventRuleConfiguration stationEvent)
+    private int GetOccurrences(string stationEvent)
     {
-        return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id);
+        return GameTicker.AllPreviousGameRules.Count(p => p.Item2 == stationEvent);
     }
 
-    public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent)
+    public TimeSpan TimeSinceLastEvent(EntityPrototype stationEvent)
     {
         foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse())
         {
-            if (rule.Configuration is not StationEventRuleConfiguration)
-                continue;
-
-            if (stationEvent == null || rule.ID == stationEvent.Id)
+            if (rule == stationEvent.ID)
                 return time;
         }
 
         return TimeSpan.Zero;
     }
 
-    private bool CanRun(StationEventRuleConfiguration stationEvent, int playerCount, TimeSpan currentTime)
+    private bool CanRun(EntityPrototype prototype, StationEventComponent stationEvent, int playerCount, TimeSpan currentTime)
     {
-        if (GameTicker.IsGameRuleStarted(stationEvent.Id))
+        if (GameTicker.IsGameRuleActive(prototype.ID))
             return false;
 
-        if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(stationEvent) >= stationEvent.MaxOccurrences.Value)
+        if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(prototype) >= stationEvent.MaxOccurrences.Value)
         {
             return false;
         }
@@ -185,7 +196,7 @@ public sealed class EventManagerSystem : EntitySystem
             return false;
         }
 
-        var lastRun = TimeSinceLastEvent(stationEvent);
+        var lastRun = TimeSinceLastEvent(prototype);
         if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes <
             stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes)
         {
similarity index 61%
rename from Content.Server/StationEvents/Events/AnomalySpawn.cs
rename to Content.Server/StationEvents/Events/AnomalySpawnRule.cs
index 5b7d1c42f5fcb61cec298ece29f9aa60deee9cb9..8e7a860d2e3e7a8560a7f1cf8202212f5b772267 100644 (file)
@@ -1,31 +1,28 @@
 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 AnomalySpawn : StationEventSystem
+public sealed class AnomalySpawnRule : StationEventSystem<AnomalySpawnRuleComponent>
 {
-    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly AnomalySystem _anomaly = default!;
 
-    public override string Prototype => "AnomalySpawn";
-
-    public readonly string AnomalySpawnerPrototype = "RandomAnomalySpawner";
-
-    public override void Added()
+    protected override void Added(EntityUid uid, AnomalySpawnRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
     {
-        base.Added();
+        base.Added(uid, component, gameRule, args);
 
         var str = Loc.GetString("anomaly-spawn-event-announcement",
-            ("sighting", Loc.GetString($"anomaly-spawn-sighting-{_random.Next(1, 6)}")));
+            ("sighting", Loc.GetString($"anomaly-spawn-sighting-{RobustRandom.Next(1, 6)}")));
         ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5"));
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, AnomalySpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
 
         if (StationSystem.Stations.Count == 0)
             return; // No stations
@@ -45,7 +42,7 @@ public sealed class AnomalySpawn : StationEventSystem
         var amountToSpawn = Math.Max(1, (int) MathF.Round(GetSeverityModifier() / 2));
         for (var i = 0; i < amountToSpawn; i++)
         {
-            _anomaly.SpawnOnRandomGridLocation(grid.Value, AnomalySpawnerPrototype);
+            _anomaly.SpawnOnRandomGridLocation(grid.Value, component.AnomalySpawnerPrototype);
         }
     }
 }
diff --git a/Content.Server/StationEvents/Events/BluespaceArtifact.cs b/Content.Server/StationEvents/Events/BluespaceArtifact.cs
deleted file mode 100644 (file)
index 407cca0..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..306b735
--- /dev/null
@@ -0,0 +1,34 @@
+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/BluespaceLocker.cs
rename to Content.Server/StationEvents/Events/BluespaceLockerRule.cs
index 88de303c0700e57f0a49dace2581735914dfcdd5..ddf1ba784cb3a00128ef0f90907d0295af720b3c 100644 (file)
@@ -1,27 +1,25 @@
 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 BluespaceLockerLink : StationEventSystem
+public sealed class BluespaceLockerRule : StationEventSystem<BluespaceLockerRuleComponent>
 {
-    [Dependency] private readonly IRobustRandom _robustRandom = default!;
     [Dependency] private readonly BluespaceLockerSystem _bluespaceLocker = default!;
 
-    public override string Prototype => "BluespaceLockerLink";
-
-    public override void Started()
+    protected override void Started(EntityUid uid, BluespaceLockerRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
 
         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/BreakerFlip.cs
rename to Content.Server/StationEvents/Events/BreakerFlipRule.cs
index 2df721b7cc17d36283f39025e2a6c35b747d45da..2920ee331bd1973c289e40469590cb68dbfa0c0e 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 BreakerFlip : StationEventSystem
+public sealed class BreakerFlipRule : StationEventSystem<BreakerFlipRuleComponent>
 {
     [Dependency] private readonly ApcSystem _apcSystem = default!;
 
-    public override string Prototype => "BreakerFlip";
-
-    public override void Added()
+    protected override void Added(EntityUid uid, BreakerFlipRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
     {
-        base.Added();
+        base.Added(uid, component, gameRule, args);
 
         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);
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, BreakerFlipRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
 
         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/BureaucraticError.cs
rename to Content.Server/StationEvents/Events/BureaucraticErrorRule.cs
index a96eae51b533265f8458eb5375b0798a62e524bd..0fbf72fd7d0d70a85223f002c785c013a4edcf7d 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 BureaucraticError : StationEventSystem
+public sealed class BureaucraticErrorRule : StationEventSystem<BureaucraticErrorRuleComponent>
 {
     [Dependency] private readonly StationJobsSystem _stationJobs = default!;
 
-    public override string Prototype => "BureaucraticError";
-
-    public override void Started()
+    protected override void Started(EntityUid uid, BureaucraticErrorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
 
         if (StationSystem.Stations.Count == 0)
             return; // No stations
similarity index 70%
rename from Content.Server/StationEvents/Events/DiseaseOutbreak.cs
rename to Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs
index 3c829ddfabb62bed4648f527d862823d0e006b51..438cc041df10d1a85069e10d415f8f71994bb07e 100644 (file)
@@ -1,5 +1,7 @@
 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;
@@ -10,38 +12,23 @@ namespace Content.Server.StationEvents.Events;
 /// Infects a couple people
 /// with a random disease that isn't super deadly
 /// </summary>
-public sealed class DiseaseOutbreak : StationEventSystem
+public sealed class DiseaseOutbreakRule : StationEventSystem<DiseaseOutbreakRuleComponent>
 {
     [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>
-    public override void Started()
+    protected override void Started(EntityUid uid, DiseaseOutbreakRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
+
         HashSet<EntityUid> stationsToNotify = new();
         List<DiseaseCarrierComponent> aliveList = new();
-        foreach (var (carrier, mobState) in EntityManager.EntityQuery<DiseaseCarrierComponent, MobStateComponent>())
+        foreach (var (carrier, mobState) in EntityQuery<DiseaseCarrierComponent, MobStateComponent>())
         {
             if (!_mobStateSystem.IsDead(mobState.Owner, mobState))
                 aliveList.Add(carrier);
@@ -51,7 +38,7 @@ public sealed class DiseaseOutbreak : StationEventSystem
         // 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(NotTooSeriousDiseases);
+        var diseaseName = RobustRandom.Pick(component.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
deleted file mode 100644 (file)
index 701f1c2..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..05e9435
--- /dev/null
@@ -0,0 +1,28 @@
+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
deleted file mode 100644 (file)
index e073ec2..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..95e9286
--- /dev/null
@@ -0,0 +1,90 @@
+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
deleted file mode 100644 (file)
index e0b0f5c..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..3fa12cd
--- /dev/null
@@ -0,0 +1,19 @@
+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
deleted file mode 100644 (file)
index 151a592..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..57f0a2c
--- /dev/null
@@ -0,0 +1,49 @@
+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/MeteorSwarm.cs
rename to Content.Server/StationEvents/Events/MeteorSwarmRule.cs
index aabc9ba2bc48624c2857bb9ae7d411b0d0765324..eb0597a92ef5596087766170a71997658e595e04 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.StationEvents.Components;
 using Content.Shared.Spawners.Components;
 using Robust.Shared.Map;
 using Robust.Shared.Physics.Components;
@@ -6,67 +7,37 @@ using Robust.Shared.Physics.Systems;
 
 namespace Content.Server.StationEvents.Events
 {
-    public sealed class MeteorSwarm : StationEventSystem
+    public sealed class MeteorSwarmRule : StationEventSystem<MeteorSwarmRuleComponent>
     {
         [Dependency] private readonly SharedPhysicsSystem _physics = default!;
 
-        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()
+        protected override void Started(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
         {
-            base.Started();
-            var mod = Math.Sqrt(GetSeverityModifier());
-            _waveCounter = (int) (RobustRandom.Next(MinimumWaves, MaximumWaves) * mod);
-        }
+            base.Started(uid, component, gameRule, args);
 
-        public override void Ended()
-        {
-            base.Ended();
-            _waveCounter = 0;
-            _cooldown = 0f;
+            var mod = Math.Sqrt(GetSeverityModifier());
+            component._waveCounter = (int) (RobustRandom.Next(component.MinimumWaves, component.MaximumWaves) * mod);
         }
 
-        public override void Update(float frameTime)
+        protected override void ActiveTick(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, float frameTime)
         {
-            base.Update(frameTime);
-
-            if (!RuleStarted)
-                return;
-
-            if (_waveCounter <= 0)
+            base.ActiveTick(uid, component, gameRule, frameTime);
+            if (component._waveCounter <= 0)
             {
-                ForceEndSelf();
+                ForceEndSelf(uid, gameRule);
                 return;
             }
 
             var mod = GetSeverityModifier();
 
-            _cooldown -= frameTime;
+            component._cooldown -= frameTime;
 
-            if (_cooldown > 0f)
+            if (component._cooldown > 0f)
                 return;
 
-            _waveCounter--;
+            component._waveCounter--;
 
-            _cooldown += (MaximumCooldown - MinimumCooldown) * RobustRandom.NextFloat() / mod + MinimumCooldown;
+            component._cooldown += (component.MaximumCooldown - component.MinimumCooldown) * RobustRandom.NextFloat() / mod + component.MinimumCooldown;
 
             Box2? playableArea = null;
             var mapId = GameTicker.DefaultMap;
@@ -79,7 +50,7 @@ namespace Content.Server.StationEvents.Events
 
             if (playableArea == null)
             {
-                ForceEndSelf();
+                ForceEndSelf(uid, gameRule);
                 return;
             }
 
@@ -88,7 +59,7 @@ namespace Content.Server.StationEvents.Events
 
             var center = playableArea.Value.Center;
 
-            for (var i = 0; i < MeteorsPerWave; i++)
+            for (var i = 0; i < component.MeteorsPerWave; i++)
             {
                 var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
                 var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
@@ -98,10 +69,10 @@ namespace Content.Server.StationEvents.Events
                 _physics.SetBodyStatus(physics, BodyStatus.InAir);
                 _physics.SetLinearDamping(physics, 0f);
                 _physics.SetAngularDamping(physics, 0f);
-                _physics.ApplyLinearImpulse(meteor, -offset.Normalized * MeteorVelocity * physics.Mass, body: physics);
+                _physics.ApplyLinearImpulse(meteor, -offset.Normalized * component.MeteorVelocity * physics.Mass, body: physics);
                 _physics.ApplyAngularImpulse(
                     meteor,
-                    physics.Mass * ((MaxAngularVelocity - MinAngularVelocity) * RobustRandom.NextFloat() + MinAngularVelocity),
+                    physics.Mass * ((component.MaxAngularVelocity - component.MinAngularVelocity) * RobustRandom.NextFloat() + component.MinAngularVelocity),
                     body: physics);
 
                 EnsureComp<TimedDespawnComponent>(meteor).Lifetime = 120f;
similarity index 66%
rename from Content.Server/StationEvents/Events/MouseMigration.cs
rename to Content.Server/StationEvents/Events/MouseMigrationRule.cs
index e2f2a5b9b0905be24613c56533808bc4de4d4f3b..b0c54a7d0ea5d2f7d02e6ca7ec9db6ad92041690 100644 (file)
@@ -1,19 +1,15 @@
 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 MouseMigration : StationEventSystem
+public sealed class MouseMigrationRule : StationEventSystem<MouseMigrationRuleComponent>
 {
-    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()
+    protected override void Started(EntityUid uid, MouseMigrationRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
 
         var modifier = GetSeverityModifier();
 
@@ -23,9 +19,9 @@ public sealed class MouseMigration : StationEventSystem
         // 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 (int i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
+        for (var i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
         {
-            var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
+            var spawnChoice = RobustRandom.Pick(component.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
deleted file mode 100644 (file)
index b1f21af..0000000
+++ /dev/null
@@ -1,122 +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 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
new file mode 100644 (file)
index 0000000..78a77c9
--- /dev/null
@@ -0,0 +1,96 @@
+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/RandomSentience.cs
rename to Content.Server/StationEvents/Events/RandomSentienceRule.cs
index 8181e11c0d7270d45f0b7cd7d4db992a75b90988..8b2128178adf2da238277c663e77f8724caffbd7 100644 (file)
@@ -1,22 +1,20 @@
 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 RandomSentience : StationEventSystem
+public sealed class RandomSentienceRule : StationEventSystem<RandomSentienceRuleComponent>
 {
-    public override string Prototype => "RandomSentience";
-
-    public override void Started()
+    protected override void Started(EntityUid uid, RandomSentienceRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        base.Started();
         HashSet<EntityUid> stationsToNotify = new();
 
         var mod = GetSeverityModifier();
-        var targetList = EntityManager.EntityQuery<SentienceTargetComponent>().ToList();
+        var targetList = EntityQuery<SentienceTargetComponent>().ToList();
         RobustRandom.Shuffle(targetList);
 
         var toMakeSentient = (int) (RobustRandom.Next(2, 5) * Math.Sqrt(mod));
@@ -27,10 +25,10 @@ public sealed class RandomSentience : StationEventSystem
             if (toMakeSentient-- == 0)
                 break;
 
-            EntityManager.RemoveComponent<SentienceTargetComponent>(target.Owner);
-            var ghostRole = AddComp<GhostRoleComponent>(target.Owner);
-            AddComp<GhostTakeoverAvailableComponent>(target.Owner);
-            ghostRole.RoleName = EntityManager.GetComponent<MetaDataComponent>(target.Owner).EntityName;
+            RemComp<SentienceTargetComponent>(target.Owner);
+            var ghostRole = EnsureComp<GhostRoleComponent>(target.Owner);
+            EnsureComp<GhostTakeoverAvailableComponent>(target.Owner);
+            ghostRole.RoleName = MetaData(target.Owner).EntityName;
             ghostRole.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", ghostRole.RoleName));
             groups.Add(Loc.GetString(target.FlavorKind));
         }
@@ -43,18 +41,15 @@ public sealed class RandomSentience : StationEventSystem
         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
deleted file mode 100644 (file)
index 3375b2b..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..6e99fb4
--- /dev/null
@@ -0,0 +1,19 @@
+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
deleted file mode 100644 (file)
index cf3a2a4..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..206a8ca
--- /dev/null
@@ -0,0 +1,64 @@
+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
deleted file mode 100644 (file)
index cb58e43..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..ba440f8
--- /dev/null
@@ -0,0 +1,28 @@
+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 bbf73f901f5c3d29e283b1dd11ea2eb22b993ed3..bcec380987f5e3ab1676b75d192fe55d8e3d9356 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.Configurations;
+using Content.Server.GameTicking.Rules.Components;
 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
+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
 {
-    /// <summary>
-    ///     An abstract entity system inherited by all station events for their behavior.
-    /// </summary>
-    public abstract class StationEventSystem : GameRuleSystem
+    [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()
     {
-        [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();
+        base.Initialize();
 
-            Sawmill = Logger.GetSawmill("stationevents");
-        }
+        Sawmill = Logger.GetSawmill("stationevents");
+    }
+
+    /// <inheritdoc/>
+    protected override void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    {
+        base.Added(uid, component, gameRule, args);
 
-        /// <summary>
-        ///     Called once to setup the event after StartAfter has elapsed, or if an event is forcibly started.
-        /// </summary>
-        public override void Started()
+        if (!TryComp<StationEventComponent>(uid, out var stationEvent))
+            return;
+
+        AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {ToPrettyString(uid)}");
+
+        if (stationEvent.StartAnnouncement != null)
         {
-            AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {Configuration.Id}");
+            ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.StartAnnouncement), playSound: false, colorOverride: Color.Gold);
         }
 
-        /// <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}");
+        Audio.PlayGlobal(stationEvent.StartAudio, Filter.Broadcast(), true);
+        stationEvent.StartTime = _timing.CurTime + stationEvent.StartDelay;
+    }
 
-            if (Configuration is not StationEventRuleConfiguration ev)
-                return;
+    /// <inheritdoc/>
+    protected override void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    {
+        base.Started(uid, component, gameRule, args);
 
-            if (ev.StartAnnouncement != null)
-            {
-                ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.StartAnnouncement), playSound: false, colorOverride: Color.Gold);
-            }
+        if (!TryComp<StationEventComponent>(uid, out var stationEvent))
+            return;
 
-            if (ev.StartAudio != null)
-            {
-                SoundSystem.Play(ev.StartAudio.GetSound(), Filter.Broadcast(), ev.StartAudio.Params);
-            }
-
-            Elapsed = 0;
-        }
+        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;
+    }
 
-        /// <summary>
-        ///     Called once when the station event ends for any reason.
-        /// </summary>
-        public override void Ended()
-        {
-            AdminLogManager.Add(LogType.EventStopped, $"Event ended: {Configuration.Id}");
+    /// <inheritdoc/>
+    protected override void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+    {
+        base.Ended(uid, component, gameRule, args);
 
-            if (Configuration is not StationEventRuleConfiguration ev)
-                return;
+        if (!TryComp<StationEventComponent>(uid, out var stationEvent))
+            return;
 
-            if (ev.EndAnnouncement != null)
-            {
-                ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.EndAnnouncement), playSound: false, colorOverride: Color.Gold);
-            }
+        AdminLogManager.Add(LogType.EventStopped, $"Event ended: {ToPrettyString(uid)}");
 
-            if (ev.EndAudio != null)
-            {
-                SoundSystem.Play(ev.EndAudio.GetSound(), Filter.Broadcast(), ev.EndAudio.Params);
-            }
+        if (stationEvent.EndAnnouncement != null)
+        {
+            ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.EndAnnouncement), playSound: false, colorOverride: Color.Gold);
         }
 
-        /// <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 (!RuleAdded || Configuration is not StationEventRuleConfiguration data)
-                return;
+        Audio.PlayGlobal(stationEvent.EndAudio, Filter.Broadcast(), true);
+    }
 
-            Elapsed += frameTime;
+    /// <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))
+        {
+            if (!GameTicker.IsGameRuleAdded(uid, ruleData))
+                continue;
 
-            if (!RuleStarted && Elapsed >= data.StartAfter)
+            if (!GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.StartTime)
             {
-                GameTicker.StartGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
+                GameTicker.StartGameRule(uid, ruleData);
             }
-
-            if (RuleStarted && Elapsed >= data.EndAfter)
+            else if (GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.EndTime)
             {
-                GameTicker.EndGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
+                GameTicker.EndGameRule(uid, ruleData);
             }
         }
+    }
+
+    #region Helper Functions
+
+    protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null)
+    {
+        GameTicker.EndGameRule(uid, component);
+    }
 
-        #region Helper Functions
+    protected bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords)
+    {
+        tile = default;
 
-        protected void ForceEndSelf()
+        targetCoords = EntityCoordinates.Invalid;
+        if (StationSystem.Stations.Count == 0)
         {
-            GameTicker.EndGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
+            targetStation = EntityUid.Invalid;
+            targetGrid = EntityUid.Invalid;
+            return false;
         }
-
-        protected bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords)
+        targetStation = RobustRandom.Pick(StationSystem.Stations);
+        var possibleTargets = Comp<StationDataComponent>(targetStation).Grids;
+        if (possibleTargets.Count == 0)
         {
-            tile = default;
+            targetGrid = EntityUid.Invalid;
+            return false;
+        }
 
-            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;
-            }
+        targetGrid = RobustRandom.Pick(possibleTargets);
 
-            targetGrid = RobustRandom.Pick(possibleTargets);
+        if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
+            return false;
 
-            if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
-                return false;
+        var found = false;
+        var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(targetGrid);
+        var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
 
-            var found = false;
-            var (gridPos, _, gridMatrix) = Transform(targetGrid).GetWorldPositionRotationMatrix();
-            var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
+        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);
 
-            for (var i = 0; i < 10; i++)
+            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))
             {
-                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;
+                continue;
             }
 
-            if (!found) return false;
-
-            return true;
-        }
-
-        public static GameRulePrototype GetRandomEventUnweighted(IPrototypeManager? prototypeManager = null, IRobustRandom? random = null)
-        {
-            IoCManager.Resolve(ref prototypeManager, ref random);
-
-            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;
+            found = true;
+            targetCoords = gridComp.GridTileToLocal(tile);
+            break;
         }
 
-        #endregion
+        return found;
     }
+    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>
-    ///     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.
+    ///     Should be multiplied/added to rather than set, for commutativity.
     /// </summary>
-    public sealed class GetSeverityModifierEvent : EntityEventArgs
-    {
-        /// <summary>
-        ///     Should be multiplied/added to rather than set, for commutativity.
-        /// </summary>
-        public float Modifier = 1.0f;
-    }
+    public float Modifier = 1.0f;
 }
similarity index 73%
rename from Content.Server/StationEvents/Events/VentClog.cs
rename to Content.Server/StationEvents/Events/VentClogRule.cs
index 04e466f66b5c77dd3b660d110354a4cda5168ef2..c209337aec51f7189f290724d372000b0742ff6e 100644 (file)
@@ -8,23 +8,19 @@ using Robust.Shared.Random;
 using System.Linq;
 using Content.Server.Chemistry.Components;
 using Content.Server.Fluids.EntitySystems;
-using Robust.Server.GameObjects;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.StationEvents.Components;
 
 namespace Content.Server.StationEvents.Events;
 
 [UsedImplicitly]
-public sealed class VentClog : StationEventSystem
+public sealed class VentClogRule : StationEventSystem<VentClogRuleComponent>
 {
-    public override string Prototype => "VentClog";
+    [Dependency] private readonly SmokeSystem _smoke = default!;
 
-    public readonly IReadOnlyList<string> SafeishVentChemicals = new[]
+    protected override void Started(EntityUid uid, VentClogRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
-        "Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer"
-    };
-
-    public override void Started()
-    {
-        base.Started();
+        base.Started(uid, component, gameRule, args);
 
         if (StationSystem.Stations.Count == 0)
             return;
@@ -57,15 +53,14 @@ public sealed class VentClog : StationEventSystem
             }
             else
             {
-                solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 200);
+                solution.AddReagent(RobustRandom.Pick(component.SafeishVentChemicals), 200);
             }
 
             var foamEnt = Spawn("Foam", transform.Coordinates);
             var smoke = EnsureComp<SmokeComponent>(foamEnt);
             smoke.SpreadAmount = 20;
-            EntityManager.System<SmokeSystem>().Start(foamEnt, smoke, solution, 20f);
-            EntityManager.System<AudioSystem>().PlayPvs(sound, transform.Coordinates);
+            _smoke.Start(foamEnt, smoke, solution, 20f);
+            Audio.PlayPvs(sound, transform.Coordinates);
         }
     }
-
 }
diff --git a/Content.Server/StationEvents/Events/VentCritters.cs b/Content.Server/StationEvents/Events/VentCritters.cs
deleted file mode 100644 (file)
index 5d61e10..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-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
new file mode 100644 (file)
index 0000000..6968288
--- /dev/null
@@ -0,0 +1,29 @@
+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 d22e2d86dc7c5673cfc5226034030731a7d0f4f4..5c972df52e1b96db6f2b1149b1480ab3c0bc5680 100644 (file)
@@ -1,5 +1,7 @@
 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;
@@ -7,35 +9,20 @@ using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents;
 
-public sealed class RampingStationEventSchedulerSystem : GameRuleSystem
+public sealed class RampingStationEventSchedulerSystem : GameRuleSystem<RampingStationEventSchedulerComponent>
 {
-    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!;
 
-    [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
+    public float GetChaosModifier(EntityUid uid, RampingStationEventSchedulerComponent component)
     {
-        get
-        {
-            var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds;
-            if (roundTime > _endTime)
-                return _maxChaos;
+        var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds;
+        if (roundTime > component.EndTime)
+            return component.MaxChaos;
 
-            return (_maxChaos / _endTime) * roundTime + _startingChaos;
-        }
+        return component.MaxChaos / component.EndTime * roundTime + component.StartingChaos;
     }
 
     public override void Initialize()
@@ -45,60 +32,65 @@ public sealed class RampingStationEventSchedulerSystem : GameRuleSystem
         SubscribeLocalEvent<GetSeverityModifierEvent>(OnGetSeverityModifier);
     }
 
-    public override void Started()
+    protected override void Started(EntityUid uid, RampingStationEventSchedulerComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
     {
+        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
-        _maxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4);
+        component.MaxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4);
         // This is in minutes, so *60 for seconds (for the chaos calc)
-        _endTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f;
-        _startingChaos = _maxChaos / 10;
+        component.EndTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f;
+        component.StartingChaos = component.MaxChaos / 10;
 
-        PickNextEventTime();
-    }
-
-    public override void Ended()
-    {
-        _endTime = 0f;
-        _maxChaos = 0f;
-        _startingChaos = 0f;
-        _timeUntilNextEvent = 0f;
+        PickNextEventTime(uid, component);
     }
 
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
 
-        if (!RuleStarted || !_event.EventsEnabled)
+        if (!_event.EventsEnabled)
             return;
 
-        if (_timeUntilNextEvent > 0f)
+        var query = EntityQueryEnumerator<RampingStationEventSchedulerComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var scheduler, out var gameRule))
         {
-            _timeUntilNextEvent -= frameTime;
-            return;
-        }
+            if (!GameTicker.IsGameRuleActive(uid, gameRule))
+                return;
 
-        PickNextEventTime();
-        _event.RunRandomEvent();
+            if (scheduler.TimeUntilNextEvent > 0f)
+            {
+                scheduler.TimeUntilNextEvent -= frameTime;
+                return;
+            }
+
+            PickNextEventTime(uid, scheduler);
+            _event.RunRandomEvent();
+        }
     }
 
     private void OnGetSeverityModifier(GetSeverityModifierEvent ev)
     {
-        if (!RuleStarted)
-            return;
+        var query = EntityQueryEnumerator<RampingStationEventSchedulerComponent, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out var scheduler, out var gameRule))
+        {
+            if (!GameTicker.IsGameRuleActive(uid, gameRule))
+                return;
 
-        ev.Modifier *= ChaosModifier;
-        Logger.Info($"Ramping set modifier to {ev.Modifier}");
+            ev.Modifier *= GetChaosModifier(uid, scheduler);
+            Logger.Info($"Ramping set modifier to {ev.Modifier}");
+        }
     }
 
-    private void PickNextEventTime()
+    private void PickNextEventTime(EntityUid uid, RampingStationEventSchedulerComponent component)
     {
-        var mod = ChaosModifier;
+        var mod = GetChaosModifier(uid, component);
 
         // 4-12 minutes baseline. Will get faster over time as the chaos mod increases.
-        _timeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod);
+        component.TimeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod);
     }
 }
diff --git a/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs b/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs
deleted file mode 100644 (file)
index 7c07cbd..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-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
deleted file mode 100644 (file)
index f63cacb..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644 (file)
index b42b22d..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-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
deleted file mode 100644 (file)
index cca978d..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644 (file)
index 5996425..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-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
deleted file mode 100644 (file)
index d23269b..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644 (file)
index 9ea08c4..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644 (file)
index 820ccbc..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644 (file)
index 9def8fc..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-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
deleted file mode 100644 (file)
index 09d8a6c..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644 (file)
index 7254e2d..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644 (file)
index e4c804a..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-- 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
deleted file mode 100644 (file)
index 7c15a09..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-- 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 8a2c7b71b3733ceb83ff85e61cff4fbbd2e35815..5b54e1a60ddc436563925e7fa4a9f984bafdd84a 100644 (file)
-- type: gameRule
+- type: entity
   id: AnomalySpawn
-  config:
-    !type:StationEventRuleConfiguration
-    id: AnomalySpawn
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 10
     startAfter: 30
-    endAfter: 35
+    duration: 35
+  - type: AnomalySpawnRule
 
-- type: gameRule
+- type: entity
   id: BluespaceArtifact
-  config:
-    !type:StationEventRuleConfiguration
-    id: BluespaceArtifact
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 5
     startAfter: 30
-    endAfter: 35
+    duration: 35
+  - type: BluespaceArtifactRule
 
-- type: gameRule
-  id: BluespaceLockerLink
-  config:
-    !type:StationEventRuleConfiguration
-    id: BluespaceLockerLink
+- type: entity
+  id: BluespaceLocker
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 0
     reoccurrenceDelay: 5
     earliestStart: 1
-    endAfter: 1
+    duration: 1
+  - type: BluespaceLockerRule
 
-- type: gameRule
+- type: entity
   id: BreakerFlip
-  config:
-    !type:StationEventRuleConfiguration
-    id: BreakerFlip
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 10
-    endAfter: 1
+    duration: 1
     minimumPlayers: 15
+  - type: BreakerFlipRule
 
-- type: gameRule
+- type: entity
   id: BureaucraticError
-  config:
-    !type:StationEventRuleConfiguration
-    id: BureaucraticError
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     startAnnouncement: station-event-bureaucratic-error-announcement
     minimumPlayers: 25
     weight: 5
-    endAfter: 1
+    duration: 1
+  - type: BureaucraticErrorRule
 
-- type: gameRule
+- type: entity
   id: DiseaseOutbreak
-  config:
-    !type:StationEventRuleConfiguration
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     startAnnouncement: station-event-disease-outbreak-announcement
     startAudio:
       path: /Audio/Announcements/outbreak7.ogg
       params:
         volume: -4
-    id: DiseaseOutbreak
     weight: 5
-    endAfter: 1
+    duration: 1
     earliestStart: 15
+  - type: DiseaseOutbreakRule
 
-- type: gameRule
+- type: entity
   id: Dragon
-  config:
-    !type:StationEventRuleConfiguration
-    id: Dragon
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 10
-    endAfter: 2
+    duration: 2
     earliestStart: 15
     minimumPlayers: 15
+  - type: DragonRule
 
-- type: gameRule
+- type: entity
   id: RevenantSpawn
-  config:
-    !type:StationEventRuleConfiguration
-    id: RevenantSpawn
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 5
-    endAfter: 1
+    duration: 1
     earliestStart: 45
     minimumPlayers: 20
+  - type: RevenantSpawnRule
 
-- type: gameRule
+- type: entity
   id: FalseAlarm
-  config:
-    !type:StationEventRuleConfiguration
-    id: FalseAlarm
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 15
-    endAfter: 1
+    duration: 1
+  - type: FalseAlarmRule
 
-- type: gameRule
+- type: entity
   id: GasLeak
-  config:
-    !type:StationEventRuleConfiguration
-    id: GasLeak
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     startAnnouncement: station-event-gas-leak-start-announcement
     startAudio:
       path: /Audio/Announcements/attention.ogg
     minimumPlayers: 5
     weight: 5
     startAfter: 20
+  - type: GasLeakRule
 
-- type: gameRule
+- type: entity
   id: KudzuGrowth
-  config:
-    !type:StationEventRuleConfiguration
-    id: KudzuGrowth
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     earliestStart: 15
     minimumPlayers: 15
     weight: 5
     startAfter: 50
-    endAfter: 240
+    duration: 240
+  - type: KudzuGrowthRule
 
-- type: gameRule
+- type: entity
   id: MeteorSwarm
-  config:
-    !type:StationEventRuleConfiguration
-    id: MeteorSwarm
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     earliestStart: 30
     weight: 5
     minimumPlayers: 20
       params:
         volume: -4
     startAfter: 30
+  - type: MeteorSwarmRule
 
-- type: gameRule
+- type: entity
   id: MouseMigration
-  config:
-    !type:StationEventRuleConfiguration
-    id: MouseMigration
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     earliestStart: 30
     minimumPlayers: 35
     weight: 5
-    endAfter: 50
+    duration: 50
+  - type: MouseMigrationRule
 
-- type: gameRule
+- type: entity
   id: PowerGridCheck
-  config:
-    !type:StationEventRuleConfiguration
-    id: PowerGridCheck
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     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: gameRule
+- type: entity
   id: RandomSentience
-  config:
-    !type:StationEventRuleConfiguration
-    id: RandomSentience
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 10
-    endAfter: 1
+    duration: 1
     startAudio:
       path: /Audio/Announcements/attention.ogg
+  - type: RandomSentienceRule
 
-- type: gameRule
+- type: entity
   id: SolarFlare
-  config: !type:SolarFlareEventRuleConfiguration
-    id: SolarFlare
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     weight: 10
     startAnnouncement: station-event-solar-flare-start-announcement
     endAnnouncement: station-event-solar-flare-end-announcement
     startAudio:
       path: /Audio/Announcements/attention.ogg
-    minEndAfter: 120
-    maxEndAfter: 240
+    duration: 120
+    maxDuration: 240
+  - type: SolarFlareRule
     onlyJamHeadsets: true
     affectedChannels:
     - Common
     lightBreakChancePerSecond: 0.0003
     doorToggleChancePerSecond: 0.001
 
-- type: gameRule
+- type: entity
   id: VentClog
-  config:
-    !type:StationEventRuleConfiguration
-    id: VentClog
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     startAnnouncement: station-event-vent-clog-start-announcement
     startAudio:
       path: /Audio/Announcements/attention.ogg
     minimumPlayers: 15
     weight: 5
     startAfter: 50
-    endAfter: 60
+    duration: 60
+  - type: VentClogRule
 
-- type: gameRule
+- type: entity
   id: VentCritters
-  config:
-    !type:StationEventRuleConfiguration
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     id: VentCritters
     earliestStart: 15
     minimumPlayers: 15
     weight: 5
-    endAfter: 60
+    duration: 60
+  - type: VentCrittersRule
 
-- type: gameRule
+- type: entity
   id: SpiderSpawn
-  config:
-    !type:StationEventRuleConfiguration
-    id: SpiderSpawn
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     earliestStart: 20
     minimumPlayers: 15
     weight: 5
-    endAfter: 60
+    duration: 60
+  - type: SpiderSpawnRule
 
-- type: gameRule
+- type: entity
   id: ZombieOutbreak
-  config:
-    !type:StationEventRuleConfiguration
-    id: Zombie
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     earliestStart: 50
     weight: 2.5
-    endAfter: 1
+    duration: 1
+  - type: ZombieRule
 
-- type: gameRule
+- type: entity
   id: LoneOpsSpawn
-  config:
-    !type:StationEventRuleConfiguration
-    id: LoneOpsSpawn
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: StationEvent
     earliestStart: 55
     weight: 5
     minimumPlayers: 10
     reoccurrenceDelay: 25
-    endAfter: 1
+    duration: 1
+  - type: LoneOpsSpawnRule
+  - type: NukeopsRule
index 5a62ca270619b50d8d5acdd625b135d60236eaf2..0d493ba658dd11a6fc1563df86c07c8b57042723 100644 (file)
@@ -1,80 +1,88 @@
-- type: gameRule
+- type: entity
+  id: BaseGameRule
+  abstract: true
+  noSpawn: true
+  components:
+  - type: GameRule
+
+- type: entity
   id: DeathMatch
-  config:
-    !type:GenericGameRuleConfiguration
-    id: DeathMatch
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: DeathMatchRule
 
-- type: gameRule
+- type: entity
   id: InactivityTimeRestart
-  config:
-    !type:InactivityGameRuleConfiguration
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: InactivityRule
     inactivityMaxTime: 600
     roundEndDelay: 10
 
-- type: gameRule
+- type: entity
   id: MaxTimeRestart
-  config:
-    !type:MaxTimeRestartRuleConfiguration
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: MaxTimeRestartRule
     roundMaxTime: 300
     roundEndDelay: 10
 
-- type: gameRule
+- type: entity
   id: Nukeops
-  config:
-    !type:NukeopsRuleConfiguration
-    id: Nukeops
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: NukeopsRule
 
-- type: gameRule
+- type: entity
   id: Pirates
-  config:
-    !type:GenericGameRuleConfiguration
-    id: Pirates
-
-- type: gameRule
-  id: Suspicion
-  config:
-    !type:GenericGameRuleConfiguration
-    id: Suspicion
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: PiratesRule
 
-- type: gameRule
+- type: entity
   id: Traitor
-  config:
-    !type:GenericGameRuleConfiguration
-    id: Traitor
-
-- type: gameRule
-  id: TraitorDeathMatch
-  config:
-    !type:GenericGameRuleConfiguration
-    id: TraitorDeathMatch
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: TraitorRule
 
-- type: gameRule
+- type: entity
   id: Sandbox
-  config:
-    !type:GenericGameRuleConfiguration
-    id: Sandbox
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: SandboxRule
 
-- type: gameRule
+- type: entity
   id: Secret
-  config:
-    !type:GenericGameRuleConfiguration
-    id: Secret
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: SecretRule
 
-- type: gameRule
+- type: entity
   id: Zombie
-  config:
-    !type:GenericGameRuleConfiguration
-    id: Zombie
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: ZombieRule
 
 # event schedulers
-- type: gameRule
+- type: entity
   id: BasicStationEventScheduler
-  config:
-    !type:GenericGameRuleConfiguration
-    id: BasicStationEventScheduler
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: BasicStationEventScheduler
 
-- type: gameRule
+- type: entity
   id: RampingStationEventScheduler
-  config:
-    !type:GenericGameRuleConfiguration
-    id: RampingStationEventScheduler
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: RampingStationEventScheduler
diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml
deleted file mode 100644 (file)
index 9efbb6f..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-- 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
deleted file mode 100644 (file)
index 27e47ca..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-- type: antag
-  id: SuspicionTraitor
-  name: roles-antag-suspicion-suspect-name
-  antagonist: true
-  setPreference: true
-  objective: roles-antag-suspicion-suspect-objective
index f56452f20be03a6613dde13de1afb988616600f1..b6e2044761c48827f53aca4eb3984b6ffc4063ef 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: