From 59349b1b9bf87533835c83437c52e228c41fa5d4 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Tue, 25 Apr 2023 20:23:14 -0400 Subject: [PATCH] Gamerule Entities, Take 2 (#15765) --- .../Suspicion/SuspicionEndTimerSystem.cs | 23 - Content.Client/Suspicion/SuspicionGui.xaml | 8 - Content.Client/Suspicion/SuspicionGui.xaml.cs | 126 --- .../Suspicion/SuspicionRoleComponent.cs | 128 --- .../Suspicion/SuspicionRoleSystem.cs | 18 - Content.Client/Suspicion/TraitorOverlay.cs | 95 -- .../Tests/GameRules/RuleMaxTimeRestartTest.cs | 19 +- .../Tests/GameRules/SecretStartsTest.cs | 12 +- .../Tests/GameRules/StartEndGameRulesTest.cs | 15 +- .../Dragon/Components/DragonRuleComponent.cs | 7 + Content.Server/Dragon/DragonSystem.Rule.cs | 18 +- Content.Server/Dragon/DragonSystem.cs | 2 +- .../GameTicking/GameTicker.GamePreset.cs | 19 +- .../GameTicking/GameTicker.GameRule.cs | 487 ++++++----- .../GameTicking/GameTicker.RoundFlow.cs | 1 - .../Presets/GamePresetPrototype.cs | 16 +- .../Components/DeathMatchRuleComponent.cs | 33 + .../Rules/Components/GameRuleComponent.cs | 44 + .../Components/InactivityRuleComponent.cs | 24 + .../Components/MaxTimeRestartRuleComponent.cs | 24 + .../NukeOperativeSpawnerComponent.cs | 1 - .../NukeopsRuleComponent.cs} | 106 ++- .../Rules/Components/PiratesRuleComponent.cs | 15 + .../Rules/Components/SandboxRuleComponent.cs | 7 + .../Rules/Components/SecretRuleComponent.cs | 11 + .../Rules/Components/TraitorRuleComponent.cs | 32 + .../Rules/Components/ZombieRuleComponent.cs | 12 + .../Configurations/GameRuleConfiguration.cs | 13 - .../GenericGameRuleConfiguration.cs | 14 - .../InactivityGameRuleConfiguration.cs | 17 - .../MaxTimeRestartRuleConfiguration.cs | 17 - .../GameTicking/Rules/DeathMatchRuleSystem.cs | 82 +- .../GameTicking/Rules/GameRulePrototype.cs | 5 +- .../GameTicking/Rules/GameRuleSystem.cs | 112 ++- .../Rules/InactivityTimeRestartRuleSystem.cs | 111 +-- .../Rules/MaxTimeRestartRuleSystem.cs | 71 +- .../GameTicking/Rules/NukeopsRuleSystem.cs | 821 ++++++++---------- .../GameTicking/Rules/PiratesRuleSystem.cs | 331 +++---- .../GameTicking/Rules/SandboxRuleSystem.cs | 12 +- .../GameTicking/Rules/SecretRuleSystem.cs | 31 +- .../GameTicking/Rules/SuspicionRuleSystem.cs | 456 ---------- .../Rules/TraitorDeathMatchRuleSystem.cs | 276 ------ .../GameTicking/Rules/TraitorRuleSystem.cs | 375 ++++---- .../GameTicking/Rules/ZombieRuleSystem.cs | 180 ++-- .../Conditions/RandomTraitorAliveCondition.cs | 5 +- .../RandomTraitorProgressCondition.cs | 4 +- .../MultipleTraitorsRequirement.cs | 2 +- .../Components/ConditionalSpawnerComponent.cs | 5 +- .../EntitySystems/ConditionalSpawnerSystem.cs | 48 +- .../BasicStationEventSchedulerSystem.cs | 55 +- .../Components/AnomalySpawnRuleComponent.cs | 15 + .../BasicStationEventSchedulerComponent.cs | 14 + .../BluespaceArtifactRuleComponent.cs | 31 + .../BluespaceLockerRuleComponent.cs | 9 + .../Components/BreakerFlipRuleComponent.cs | 9 + .../BureaucraticErrorRuleComponent.cs | 9 + .../DiseaseOutbreakRuleComponent.cs | 25 + .../Components/FalseAlarmRuleComponent.cs | 9 + .../Components/GasLeakRuleComponent.cs | 46 + .../Components/KudzuGrowthRuleComponent.cs | 9 + .../Components/LoneOpsSpawnRuleComponent.cs | 18 + .../Components/MeteorSwarmRuleComponent.cs | 25 + .../Components/MouseMigrationRuleComponent.cs | 16 + .../Components/PowerGridCheckRuleComponent.cs | 19 + .../RampingStationEventSchedulerComponent.cs | 17 + .../RandomSentienceRuleComponent.cs | 9 + .../Components/RevenantSpawnRuleComponent.cs | 10 + .../Components/SentienceTargetComponent.cs | 6 +- .../Components/SolarFlareRuleComponent.cs} | 20 +- .../Components/SpiderSpawnRuleComponent.cs | 9 + .../Components/StationEventComponent.cs} | 47 +- .../Components/VentClogRuleComponent.cs | 14 + .../VentCritterSpawnLocationComponent.cs | 6 +- .../Components/VentCrittersRuleComponent.cs | 15 + .../StationEvents/EventManagerSystem.cs | 95 +- .../{AnomalySpawn.cs => AnomalySpawnRule.cs} | 21 +- .../StationEvents/Events/BluespaceArtifact.cs | 49 -- .../Events/BluespaceArtifactRule.cs | 34 + ...espaceLocker.cs => BluespaceLockerRule.cs} | 14 +- .../{BreakerFlip.cs => BreakerFlipRule.cs} | 18 +- ...raticError.cs => BureaucraticErrorRule.cs} | 10 +- ...easeOutbreak.cs => DiseaseOutbreakRule.cs} | 29 +- .../StationEvents/Events/FalseAlarm.cs | 33 - .../StationEvents/Events/FalseAlarmRule.cs | 28 + .../StationEvents/Events/GasLeak.cs | 147 ---- .../StationEvents/Events/GasLeakRule.cs | 90 ++ .../StationEvents/Events/KudzuGrowth.cs | 28 - .../StationEvents/Events/KudzuGrowthRule.cs | 19 + .../StationEvents/Events/LoneOpsSpawn.cs | 44 - .../StationEvents/Events/LoneOpsSpawnRule.cs | 49 ++ .../{MeteorSwarm.cs => MeteorSwarmRule.cs} | 67 +- ...ouseMigration.cs => MouseMigrationRule.cs} | 16 +- .../StationEvents/Events/PowerGridCheck.cs | 122 --- .../Events/PowerGridCheckRule.cs | 96 ++ ...domSentience.cs => RandomSentienceRule.cs} | 25 +- .../StationEvents/Events/RevenantSpawn.cs | 18 - .../StationEvents/Events/RevenantSpawnRule.cs | 19 + .../StationEvents/Events/SolarFlare.cs | 78 -- .../StationEvents/Events/SolarFlareRule.cs | 64 ++ .../StationEvents/Events/SpiderSpawn.cs | 32 - .../StationEvents/Events/SpiderSpawnRule.cs | 28 + .../Events/StationEventSystem.cs | 372 ++++---- .../Events/{VentClog.cs => VentClogRule.cs} | 23 +- .../StationEvents/Events/VentCritters.cs | 34 - .../StationEvents/Events/VentCrittersRule.cs | 29 + .../RampingStationEventSchedulerSystem.cs | 88 +- .../Suspicion/Roles/SuspicionInnocentRole.cs | 34 - .../Suspicion/Roles/SuspicionRole.cs | 9 - .../Suspicion/Roles/SuspicionTraitorRole.cs | 41 - .../Suspicion/SuspicionItemComponent.cs | 9 - .../Suspicion/SuspicionRoleComponent.cs | 138 --- .../Suspicion/SuspicionRoleSystem.cs | 32 - .../TraitorDeathMatchRedemptionComponent.cs | 7 - ...itorDeathMatchReliableOwnerTagComponent.cs | 12 - .../TraitorDeathMatchRedemptionSystem.cs | 108 --- .../Suspicion/SharedSuspicionRoleComponent.cs | 25 - Content.Shared/Suspicion/SuspicionMessages.cs | 13 - .../Spawners/Conditional/traitordm.yml | 16 - .../Structures/Machines/traitordm.yml | 28 - Resources/Prototypes/GameRules/events.yml | 251 +++--- Resources/Prototypes/GameRules/roundstart.yml | 118 +-- .../Antags/Suspicion/suspicion_innocent.yml | 6 - .../Antags/Suspicion/suspicion_traitor.yml | 6 - Resources/Prototypes/game_presets.yml | 21 - 124 files changed, 3102 insertions(+), 4344 deletions(-) delete mode 100644 Content.Client/Suspicion/SuspicionEndTimerSystem.cs delete mode 100644 Content.Client/Suspicion/SuspicionGui.xaml delete mode 100644 Content.Client/Suspicion/SuspicionGui.xaml.cs delete mode 100644 Content.Client/Suspicion/SuspicionRoleComponent.cs delete mode 100644 Content.Client/Suspicion/SuspicionRoleSystem.cs delete mode 100644 Content.Client/Suspicion/TraitorOverlay.cs create mode 100644 Content.Server/Dragon/Components/DragonRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs rename Content.Server/GameTicking/Rules/{Configurations/NukeopsRuleConfiguration.cs => Components/NukeopsRuleComponent.cs} (51%) create mode 100644 Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs delete mode 100644 Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs delete mode 100644 Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs delete mode 100644 Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs delete mode 100644 Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs delete mode 100644 Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs create mode 100644 Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs create mode 100644 Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/GasLeakRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs create mode 100644 Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs rename Content.Server/{GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs => StationEvents/Components/SolarFlareRuleComponent.cs} (69%) create mode 100644 Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs rename Content.Server/{GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs => StationEvents/Components/StationEventComponent.cs} (58%) create mode 100644 Content.Server/StationEvents/Components/VentClogRuleComponent.cs create mode 100644 Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs rename Content.Server/StationEvents/Events/{AnomalySpawn.cs => AnomalySpawnRule.cs} (61%) delete mode 100644 Content.Server/StationEvents/Events/BluespaceArtifact.cs create mode 100644 Content.Server/StationEvents/Events/BluespaceArtifactRule.cs rename Content.Server/StationEvents/Events/{BluespaceLocker.cs => BluespaceLockerRule.cs} (79%) rename Content.Server/StationEvents/Events/{BreakerFlip.cs => BreakerFlipRule.cs} (70%) rename Content.Server/StationEvents/Events/{BureaucraticError.cs => BureaucraticErrorRule.cs} (82%) rename Content.Server/StationEvents/Events/{DiseaseOutbreak.cs => DiseaseOutbreakRule.cs} (70%) delete mode 100644 Content.Server/StationEvents/Events/FalseAlarm.cs create mode 100644 Content.Server/StationEvents/Events/FalseAlarmRule.cs delete mode 100644 Content.Server/StationEvents/Events/GasLeak.cs create mode 100644 Content.Server/StationEvents/Events/GasLeakRule.cs delete mode 100644 Content.Server/StationEvents/Events/KudzuGrowth.cs create mode 100644 Content.Server/StationEvents/Events/KudzuGrowthRule.cs delete mode 100644 Content.Server/StationEvents/Events/LoneOpsSpawn.cs create mode 100644 Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs rename Content.Server/StationEvents/Events/{MeteorSwarm.cs => MeteorSwarmRule.cs} (53%) rename Content.Server/StationEvents/Events/{MouseMigration.cs => MouseMigrationRule.cs} (66%) delete mode 100644 Content.Server/StationEvents/Events/PowerGridCheck.cs create mode 100644 Content.Server/StationEvents/Events/PowerGridCheckRule.cs rename Content.Server/StationEvents/Events/{RandomSentience.cs => RandomSentienceRule.cs} (67%) delete mode 100644 Content.Server/StationEvents/Events/RevenantSpawn.cs create mode 100644 Content.Server/StationEvents/Events/RevenantSpawnRule.cs delete mode 100644 Content.Server/StationEvents/Events/SolarFlare.cs create mode 100644 Content.Server/StationEvents/Events/SolarFlareRule.cs delete mode 100644 Content.Server/StationEvents/Events/SpiderSpawn.cs create mode 100644 Content.Server/StationEvents/Events/SpiderSpawnRule.cs rename Content.Server/StationEvents/Events/{VentClog.cs => VentClogRule.cs} (73%) delete mode 100644 Content.Server/StationEvents/Events/VentCritters.cs create mode 100644 Content.Server/StationEvents/Events/VentCrittersRule.cs delete mode 100644 Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs delete mode 100644 Content.Server/Suspicion/Roles/SuspicionRole.cs delete mode 100644 Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs delete mode 100644 Content.Server/Suspicion/SuspicionItemComponent.cs delete mode 100644 Content.Server/Suspicion/SuspicionRoleComponent.cs delete mode 100644 Content.Server/Suspicion/SuspicionRoleSystem.cs delete mode 100644 Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs delete mode 100644 Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs delete mode 100644 Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs delete mode 100644 Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs delete mode 100644 Content.Shared/Suspicion/SuspicionMessages.cs delete mode 100644 Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml delete mode 100644 Resources/Prototypes/Entities/Structures/Machines/traitordm.yml delete mode 100644 Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml delete mode 100644 Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml diff --git a/Content.Client/Suspicion/SuspicionEndTimerSystem.cs b/Content.Client/Suspicion/SuspicionEndTimerSystem.cs deleted file mode 100644 index a843f496d9..0000000000 --- a/Content.Client/Suspicion/SuspicionEndTimerSystem.cs +++ /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(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 index 8f519b2195..0000000000 --- a/Content.Client/Suspicion/SuspicionGui.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Content.Client/Suspicion/SuspicionGui.xaml.cs b/Content.Client/Suspicion/SuspicionGui.xaml.cs deleted file mode 100644 index 13148754b3..0000000000 --- a/Content.Client/Suspicion/SuspicionGui.xaml.cs +++ /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().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 index d95ee7edf9..0000000000 --- a/Content.Client/Suspicion/SuspicionRoleComponent.cs +++ /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()) - { - return; - } - - _overlayActive = true; - var entManager = IoCManager.Resolve(); - var overlay = new TraitorOverlay(entManager, IoCManager.Resolve(), _resourceCache, entManager.System()); - _overlayManager.AddOverlay(overlay); - } - - private void RemoveTraitorOverlay() - { - if (!_overlayActive) - { - return; - } - - _overlayManager.RemoveOverlay(); - } - - 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(); - _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 index 41cd186029..0000000000 --- a/Content.Client/Suspicion/SuspicionRoleSystem.cs +++ /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((_, component, _) => component.AddUI()); - SubscribeLocalEvent((_, component, _) => component.RemoveUI()); - - SubscribeLocalEvent((_, component, _) => component.AddUI()); - SubscribeLocalEvent((_, component, _) => component.RemoveUI()); - } - } -} diff --git a/Content.Client/Suspicion/TraitorOverlay.cs b/Content.Client/Suspicion/TraitorOverlay.cs deleted file mode 100644 index 84f52b9673..0000000000 --- a/Content.Client/Suspicion/TraitorOverlay.cs +++ /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("/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(ally); - - var entPosition = _entityManager.GetComponent(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); - } - } - } -} diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 4b23af60b2..d4f85486bc 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -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(); var configManager = server.ResolveDependency(); await server.WaitPost(() => { @@ -31,18 +31,17 @@ namespace Content.IntegrationTests.Tests.GameRules command.Execute(null, string.Empty, Array.Empty()); }); - var sGameTicker = server.ResolveDependency().GetEntitySystem(); - var maxTimeMaxTimeRestartRuleSystem = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); + + sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity); + Assert.That(entityManager.TryGetComponent(ruleEntity, out var maxTime)); + await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); - - sGameTicker.StartGameRule(IoCManager.Resolve().Index(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(() => diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 9a5ff23fad..9fa3fedeea 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -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(); var gameTicker = server.ResolveDependency().GetEntitySystem(); await server.WaitAssertion(() => { - gameTicker.StartGameRule(protoMan.Index("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 diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs index 4621ecbd1e..6b60022438 100644 --- a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs @@ -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(); var gameTicker = server.ResolveDependency().GetEntitySystem(); var cfg = server.ResolveDependency(); Assert.That(cfg.GetCVar(CCVars.DisableGridFill), Is.False); await server.WaitAssertion(() => { - var rules = protoMan.EnumeratePrototypes().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 index 0000000000..8f358c31b0 --- /dev/null +++ b/Content.Server/Dragon/Components/DragonRuleComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Dragon; + +[RegisterComponent] +public sealed class DragonRuleComponent : Component +{ + +} diff --git a/Content.Server/Dragon/DragonSystem.Rule.cs b/Content.Server/Dragon/DragonSystem.Rule.cs index 4c86634124..0963f8c09b 100644 --- a/Content.Server/Dragon/DragonSystem.Rule.cs +++ b/Content.Server/Dragon/DragonSystem.Rule.cs @@ -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().ToList(); + base.Started(uid, component, gameRule, args); + + var spawnLocations = EntityQuery().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(true).ToList(); if (dragons.Count == 0) diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index d969272ab9..f2f0e7ba99 100644 --- a/Content.Server/Dragon/DragonSystem.cs +++ b/Content.Server/Dragon/DragonSystem.cs @@ -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 { [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IRobustRandom _random = default!; diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index eb44943e76..3b1bb03157 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -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(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; } diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index c0b382af2d..158e6b0085 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -1,244 +1,303 @@ 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 + [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new(); + + /// + /// 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. + /// + public IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules; + + private void InitializeGameRules() { - // No duplicates. - [ViewVariables] private readonly HashSet _addedGameRules = new(); + // Add game rule command. + _consoleHost.RegisterCommand("addgamerule", + string.Empty, + "addgamerule ", + AddGameRuleCommand, + AddGameRuleCompletions); - /// - /// Holds all currently added game rules. - /// - public IReadOnlySet AddedGameRules => _addedGameRules; + // End game rule command. + _consoleHost.RegisterCommand("endgamerule", + string.Empty, + "endgamerule ", + EndGameRuleCommand, + EndGameRuleCompletions); - [ViewVariables] private readonly HashSet _startedGameRules = new(); + // Clear game rules command. + _consoleHost.RegisterCommand("cleargamerules", + string.Empty, + "cleargamerules", + ClearGameRulesCommand); + } - /// - /// Holds all currently started game rules. - /// - public IReadOnlySet StartedGameRules => _startedGameRules; - - [ViewVariables] private readonly List<(TimeSpan, GameRulePrototype)> _allPreviousGameRules = new(); - - /// - /// 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. - /// - public IReadOnlyList<(TimeSpan, GameRulePrototype)> AllPreviousGameRules => _allPreviousGameRules; - - private void InitializeGameRules() - { - // Add game rule command. - _consoleHost.RegisterCommand("addgamerule", - string.Empty, - "addgamerule ", - AddGameRuleCommand, - AddGameRuleCompletions); - - // End game rule command. - _consoleHost.RegisterCommand("endgamerule", - string.Empty, - "endgamerule ", - EndGameRuleCommand, - EndGameRuleCompletions); - - // Clear game rules command. - _consoleHost.RegisterCommand("cleargamerules", - string.Empty, - "cleargamerules", - ClearGameRulesCommand); - } - - private void ShutdownGameRules() - { - _consoleHost.UnregisterCommand("addgamerule"); - _consoleHost.UnregisterCommand("endgamerule"); - _consoleHost.UnregisterCommand("cleargamerules"); - } - - /// - /// 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. - /// - public void StartGameRule(GameRulePrototype rule) - { - if (!IsGameRuleAdded(rule)) - AddGameRule(rule); - - _allPreviousGameRules.Add((RoundDuration(), rule)); - _sawmill.Info($"Started game rule {rule.ID}"); - - if (_startedGameRules.Add(rule)) - RaiseLocalEvent(new GameRuleStartedEvent(rule)); - } - - /// - /// Ends a game rule. - /// This always includes removing it (from added game rules) so that behavior - /// is not separate from this. - /// - /// - public void EndGameRule(GameRulePrototype rule) - { - if (!IsGameRuleAdded(rule)) - return; - - _addedGameRules.Remove(rule); - _sawmill.Info($"Ended game rule {rule.ID}"); - - if (IsGameRuleStarted(rule)) - _startedGameRules.Remove(rule); - RaiseLocalEvent(new GameRuleEndedEvent(rule)); - } - - /// - /// 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) - /// - public bool AddGameRule(GameRulePrototype rule) - { - if (!_addedGameRules.Add(rule)) - return false; - - _sawmill.Info($"Added game rule {rule.ID}"); - RaiseLocalEvent(new GameRuleAddedEvent(rule)); - return true; - } - - public bool IsGameRuleAdded(GameRulePrototype rule) - { - return _addedGameRules.Contains(rule); - } - - public bool IsGameRuleAdded(string rule) - { - foreach (var ruleProto in _addedGameRules) - { - if (ruleProto.ID.Equals(rule)) - return true; - } - - return false; - } - - public bool IsGameRuleStarted(GameRulePrototype rule) - { - return _startedGameRules.Contains(rule); - } - - public bool IsGameRuleStarted(string rule) - { - foreach (var ruleProto in _startedGameRules) - { - if (ruleProto.ID.Equals(rule)) - return true; - } - - return false; - } - - public void ClearGameRules() - { - foreach (var rule in _addedGameRules.ToArray()) - { - EndGameRule(rule); - } - } - - #region Command Implementations - - [AdminCommand(AdminFlags.Fun)] - private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length == 0) - return; - - foreach (var ruleId in args) - { - if (!_prototypeManager.TryIndex(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); - } - } - - private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args) - { - var activeIds = _addedGameRules.Select(c => c.ID); - return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs().Where(p => !activeIds.Contains(p.Value)), - ""); - } - - [AdminCommand(AdminFlags.Fun)] - private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args) - { - if (args.Length == 0) - return; - - foreach (var ruleId in args) - { - if (!_prototypeManager.TryIndex(ruleId, out var rule)) - continue; - - EndGameRule(rule); - } - } - - private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args) - { - return CompletionResult.FromHintOptions(_addedGameRules.Select(c => new CompletionOption(c.ID)), - ""); - } - - [AdminCommand(AdminFlags.Fun)] - private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args) - { - ClearGameRules(); - } - - #endregion + private void ShutdownGameRules() + { + _consoleHost.UnregisterCommand("addgamerule"); + _consoleHost.UnregisterCommand("endgamerule"); + _consoleHost.UnregisterCommand("cleargamerules"); } /// - /// Raised broadcast when a game rule is selected, but not started yet. + /// 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) /// - public sealed class GameRuleAddedEvent + /// The entity for the added gamerule + public EntityUid AddGameRule(string ruleId) { - public GameRulePrototype Rule { get; } + var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace); + _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}"); - public GameRuleAddedEvent(GameRulePrototype rule) + var ev = new GameRuleAddedEvent(ruleEntity, ruleId); + RaiseLocalEvent(ruleEntity, ref ev, true); + return ruleEntity; + } + + /// + /// 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. + /// + public bool StartGameRule(string ruleId) + { + return StartGameRule(ruleId, out _); + } + + /// + /// 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. + /// + public bool StartGameRule(string ruleId, out EntityUid ruleEntity) + { + ruleEntity = AddGameRule(ruleId); + return StartGameRule(ruleEntity); + } + + /// + /// 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. + /// + public bool StartGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) + { + if (!Resolve(ruleEntity, ref ruleData)) + ruleData ??= EnsureComp(ruleEntity); + + // can't start an already active rule + if (ruleData.Active || ruleData.Ended) + return false; + + if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up + return false; + + _allPreviousGameRules.Add((RoundDuration(), id)); + _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}"); + + ruleData.Active = true; + var ev = new GameRuleStartedEvent(ruleEntity, id); + RaiseLocalEvent(ruleEntity, ref ev, true); + return true; + } + + /// + /// Ends a game rule. + /// + [PublicAPI] + public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) + { + if (!Resolve(ruleEntity, ref ruleData)) + return false; + + // don't end it multiple times + if (ruleData.Ended) + return false; + + if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up + return false; + + ruleData.Active = false; + ruleData.Ended = true; + _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}"); + + var ev = new GameRuleEndedEvent(ruleEntity, id); + RaiseLocalEvent(ruleEntity, ref ev, true); + return true; + } + + public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null) + { + return Resolve(ruleEntity, ref component) && !component.Ended; + } + + public bool IsGameRuleAdded(string rule) + { + foreach (var ruleEntity in GetAddedGameRules()) { - Rule = rule; + if (MetaData(ruleEntity).EntityPrototype?.ID == rule) + return true; + } + + return false; + } + + public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null) + { + return Resolve(ruleEntity, ref component) && component.Active; + } + + public bool IsGameRuleActive(string rule) + { + foreach (var ruleEntity in GetActiveGameRules()) + { + if (MetaData(ruleEntity).EntityPrototype?.ID == rule) + return true; + } + + return false; + } + + public void ClearGameRules() + { + foreach (var rule in GetAddedGameRules()) + { + EndGameRule(rule); } } - public sealed class GameRuleStartedEvent + /// + /// Gets all the gamerule entities which are currently active. + /// + public IEnumerable GetAddedGameRules() { - public GameRulePrototype Rule { get; } - - public GameRuleStartedEvent(GameRulePrototype rule) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var ruleData)) { - Rule = rule; + if (IsGameRuleAdded(uid, ruleData)) + yield return uid; } } - public sealed class GameRuleEndedEvent + /// + /// Gets all the gamerule entities which are currently active. + /// + public IEnumerable GetActiveGameRules() { - public GameRulePrototype Rule { get; } - - public GameRuleEndedEvent(GameRulePrototype rule) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var ruleData)) { - Rule = rule; + if (ruleData.Active) + yield return uid; } } + + /// + /// Gets all gamerule prototypes + /// + public IEnumerable GetAllGameRulePrototypes() + { + foreach (var proto in _prototypeManager.EnumeratePrototypes()) + { + if (proto.Abstract) + continue; + + if (proto.HasComponent()) + yield return proto; + } + } + + #region Command Implementations + + [AdminCommand(AdminFlags.Fun)] + private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length == 0) + return; + + foreach (var rule in args) + { + var ent = AddGameRule(rule); + + // Start rule if we're already in the middle of a round + if(RunLevel == GameRunLevel.InRound) + StartGameRule(ent); + } + } + + private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args) + { + return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), ""); + } + + [AdminCommand(AdminFlags.Fun)] + private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length == 0) + return; + + foreach (var rule in args) + { + if (!EntityUid.TryParse(rule, out var ruleEnt)) + continue; + + EndGameRule(ruleEnt); + } + } + + private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args) + { + return CompletionResult.FromHintOptions(GetAddedGameRules().Select(u => u.ToString()), ""); + } + + [AdminCommand(AdminFlags.Fun)] + private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args) + { + ClearGameRules(); + } + + #endregion +} + +/* +/// +/// Raised broadcast when a game rule is selected, but not started yet. +/// +public sealed class GameRuleAddedEvent +{ + public GameRulePrototype Rule { get; } + + public GameRuleAddedEvent(GameRulePrototype rule) + { + Rule = rule; + } } + +public sealed class GameRuleStartedEvent +{ + public GameRulePrototype Rule { get; } + + public GameRuleStartedEvent(GameRulePrototype rule) + { + Rule = rule; + } +} + +public sealed class GameRuleEndedEvent +{ + public GameRulePrototype Rule { get; } + + public GameRuleEndedEvent(GameRulePrototype rule) + { + Rule = rule; + } +} +*/ diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index e64837ca9f..096d508f35 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -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. diff --git a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs index e131030251..ff6a3d17ba 100644 --- a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs +++ b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs @@ -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(); + public readonly string[] Alias = Array.Empty(); [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))] + [DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer))] public IReadOnlyList Rules { get; } = Array.Empty(); } } diff --git a/Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs new file mode 100644 index 0000000000..7a11eb7b56 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs @@ -0,0 +1,33 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Simple GameRule that will do a free-for-all death match. +/// Kill everybody else to win. +/// +[RegisterComponent, Access(typeof(DeathMatchRuleSystem))] +public sealed class DeathMatchRuleComponent : Component +{ + /// + /// How long until the round restarts + /// + [DataField("restartDelay"), ViewVariables(VVAccess.ReadWrite)] + public float RestartDelay = 10f; + + /// + /// How long after a person dies will the restart be checked + /// + [DataField("deadCheckDelay"), ViewVariables(VVAccess.ReadWrite)] + public float DeadCheckDelay = 5f; + + /// + /// A timer for checking after a death + /// + [DataField("deadCheckTimer"), ViewVariables(VVAccess.ReadWrite)] + public float? DeadCheckTimer; + + /// + /// A timer for the restart. + /// + [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 index 0000000000..640b6cb839 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs @@ -0,0 +1,44 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Component attached to all gamerule entities. +/// Used to both track the entity as well as store basic data +/// +[RegisterComponent] +public sealed class GameRuleComponent : Component +{ + /// + /// Whether or not the rule is active. + /// Is enabled after and disabled after + /// + [DataField("active")] + public bool Active; + + /// + /// Whether or not the gamerule finished. + /// Used for tracking whether a non-active gamerule has been started before. + /// + [DataField("ended")] + public bool Ended; +} + +/// +/// Raised when a rule is added but hasn't formally begun yet. +/// Good for announcing station events and other such things. +/// +[ByRefEvent] +public readonly record struct GameRuleAddedEvent(EntityUid RuleEntity, string RuleId); + +/// +/// Raised when the rule actually begins. +/// Player-facing logic should begin here. +/// +[ByRefEvent] +public readonly record struct GameRuleStartedEvent(EntityUid RuleEntity, string RuleId); + +/// +/// Raised when the rule ends. +/// Do cleanup and other such things here. +/// +[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 index 0000000000..ae9657a300 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs @@ -0,0 +1,24 @@ +using System.Threading; + +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Gamerule that ends the round after a period of inactivity. +/// +[RegisterComponent, Access(typeof(InactivityTimeRestartRuleSystem))] +public sealed class InactivityRuleComponent : Component +{ + /// + /// How long the round must be inactive to restart + /// + [DataField("inactivityMaxTime", required: true)] + public TimeSpan InactivityMaxTime = TimeSpan.FromMinutes(10); + + /// + /// The delay between announcing round end and the lobby. + /// + [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 index 0000000000..45d9d38740 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs @@ -0,0 +1,24 @@ +using System.Threading; + +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Configures the game rule. +/// +[RegisterComponent] +public sealed class MaxTimeRestartRuleComponent : Component +{ + /// + /// The max amount of time the round can last + /// + [DataField("roundMaxTime", required: true)] + public TimeSpan RoundMaxTime = TimeSpan.FromMinutes(5); + + /// + /// The amount of time between the round completing and the lobby appearing. + /// + [DataField("roundEndDelay", required: true)] + public TimeSpan RoundEndDelay = TimeSpan.FromSeconds(10); + + public CancellationTokenSource TimerCancel = new(); +} diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs index 198db1f912..c32a8569cb 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs @@ -6,7 +6,6 @@ namespace Content.Server.GameTicking.Rules.Components; /// TODO: Remove once systems can request spawns from the ghost role system directly. /// [RegisterComponent] -[Access(typeof(NukeopsRuleSystem))] public sealed class NukeOperativeSpawnerComponent : Component { [DataField("name")] diff --git a/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs similarity index 51% rename from Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs rename to Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 09ccd7ad31..3df94d9eed 100644 --- a/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -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"; - + /// + /// The minimum needed amount of players + /// [DataField("minPlayers")] public int MinPlayers = 15; @@ -38,15 +41,6 @@ public sealed class NukeopsRuleConfiguration : GameRuleConfiguration [DataField("spawnOutpost")] public bool SpawnOutpost = true; - /// - /// Whether or not loneops can spawn. Set to false if a normal nukeops round is occurring. - /// - [DataField("canLoneOpsSpawn")] - public bool CanLoneOpsSpawn = true; - - [DataField("randomHumanoidSettings", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string RandomHumanoidSettingsPrototype = "NukeOp"; - [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] 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 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; + + /// + /// Cached starting gear prototypes. + /// + [DataField("startingGearPrototypes")] + public readonly Dictionary StartingGearPrototypes = new (); + + /// + /// Cached operator name prototypes. + /// + [DataField("operativeNames")] + public readonly Dictionary> OperativeNames = new(); + + /// + /// Data to be used in for an operative once the Mind has been added. + /// + [DataField("operativeMindPendingData")] + public readonly Dictionary OperativeMindPendingData = new(); + + /// + /// Players who played as an operative at some point in the round. + /// Stores the session as well as the entity name + /// + /// todo: don't store sessions, dingus + [DataField("operativePlayers")] + public readonly Dictionary OperativePlayers = new(); +} + +public enum WinType : byte +{ + /// + /// Operative major win. This means they nuked the station. + /// + OpsMajor, + /// + /// Minor win. All nukies were alive at the end of the round. + /// Alternatively, some nukies were alive, but the disk was left behind. + /// + OpsMinor, + /// + /// Neutral win. The nuke exploded, but on the wrong station. + /// + Neutral, + /// + /// Crew minor win. The nuclear authentication disk escaped on the shuttle, + /// but some nukies were alive. + /// + CrewMinor, + /// + /// 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. + /// + 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 index 0000000000..7cb748c54d --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs @@ -0,0 +1,15 @@ +namespace Content.Server.GameTicking.Rules.Components; + +[RegisterComponent, Access(typeof(PiratesRuleSystem))] +public sealed class PiratesRuleComponent : Component +{ + [ViewVariables] + public List Pirates = new(); + [ViewVariables] + public EntityUid PirateShip = EntityUid.Invalid; + [ViewVariables] + public HashSet 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 index 0000000000..6f79b892c5 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs @@ -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 index 0000000000..f6a67267a3 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.GameTicking.Rules.Components; + +[RegisterComponent, Access(typeof(SecretRuleSystem))] +public sealed class SecretRuleComponent : Component +{ + /// + /// The gamerules that get added by secret. + /// + [DataField("additionalGameRules")] + public HashSet AdditionalGameRules = new(); +} diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs new file mode 100644 index 0000000000..38f992144a --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -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 Traitors = new(); + + [DataField("traitorPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer))] + 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 StartCandidates = new(); +} diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs new file mode 100644 index 0000000000..f0b3b0ca8c --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs @@ -0,0 +1,12 @@ +namespace Content.Server.GameTicking.Rules.Components; + + +[RegisterComponent, Access(typeof(ZombieRuleSystem))] +public sealed class ZombieRuleComponent : Component +{ + public Dictionary 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 index 600cc9d54e..0000000000 --- a/Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Content.Server.GameTicking.Rules.Configurations; - -/// -/// Configures a game rule, providing information like what maps to use or how long to run. -/// -[ImplicitDataDefinitionForInheritors] -public abstract class GameRuleConfiguration -{ - /// - /// The game rule this configuration is intended for. - /// - 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 index 1db383c169..0000000000 --- a/Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JetBrains.Annotations; - -namespace Content.Server.GameTicking.Rules.Configurations; - -/// -/// A generic configuration, for game rules that don't have special config data. -/// -[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 index bc60d77adb..0000000000 --- a/Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JetBrains.Annotations; - -namespace Content.Server.GameTicking.Rules.Configurations; - -/// -/// Configures the game rule. -/// -[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 index d4cd3f39b0..0000000000 --- a/Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JetBrains.Annotations; - -namespace Content.Server.GameTicking.Rules.Configurations; - -/// -/// Configures the game rule. -/// -[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; } -} diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index c4c61480db..79c3394d11 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -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,52 +11,50 @@ using Robust.Shared.Enums; namespace Content.Server.GameTicking.Rules; /// -/// Simple GameRule that will do a free-for-all death match. -/// Kill everybody else to win. +/// Manages /// -public sealed class DeathMatchRuleSystem : GameRuleSystem +public sealed class DeathMatchRuleSystem : GameRuleSystem { - public override string Prototype => "DeathMatch"; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; - private const float RestartDelay = 10f; - private const float DeadCheckDelay = 5f; - - private float? _deadCheckTimer = null; - private float? _restartTimer = null; - public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnHealthChanged); - } - - public override void Started() - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement")); - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } - public override void Ended() + public override void Shutdown() { - _deadCheckTimer = null; - _restartTimer = null; - + base.Shutdown(); _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; } + protected override void Started(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement")); + + } + + protected override void Ended(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + { + base.Ended(uid, component, gameRule, args); + + component.DeadCheckTimer = null; + component.RestartTimer = null; + + } + private void OnHealthChanged(DamageChangedEvent _) { 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(); + 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; } } diff --git a/Content.Server/GameTicking/Rules/GameRulePrototype.cs b/Content.Server/GameTicking/Rules/GameRulePrototype.cs index d48eb1333a..c2b55822c8 100644 --- a/Content.Server/GameTicking/Rules/GameRulePrototype.cs +++ b/Content.Server/GameTicking/Rules/GameRulePrototype.cs @@ -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!; } +*/ diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs index 47bd891db2..a55189d0f2 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -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 : EntitySystem where T : Component { [Dependency] protected GameTicker GameTicker = default!; - /// - /// Whether this GameRule is currently added or not. - /// Be sure to check this before doing anything rule-specific. - /// - public bool RuleAdded { get; protected set; } - - /// - /// Whether this game rule has been started after being added. - /// You probably want to check this before doing any update loop stuff. - /// - public bool RuleStarted { get; protected set; } - - /// - /// When the GameRule prototype with this ID is added, this system will be enabled. - /// When it gets removed, this system will be disabled. - /// - public new abstract string Prototype { get; } - - /// - /// Holds the current configuration after the event has been added. - /// This should not be getting accessed before the event is enabled, as usual. - /// - public GameRuleConfiguration Configuration = default!; - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnGameRuleAdded); - - SubscribeLocalEvent(OnGameRuleStarted); - SubscribeLocalEvent(OnGameRuleEnded); + SubscribeLocalEvent(OnGameRuleAdded); + SubscribeLocalEvent(OnGameRuleStarted); + SubscribeLocalEvent(OnGameRuleEnded); } - private void OnGameRuleAdded(GameRuleAddedEvent ev) + private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args) { - if (ev.Rule.Configuration.Id != Prototype) + if (!TryComp(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(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(uid, out var ruleData)) return; - - RuleAdded = false; - RuleStarted = false; - Ended(); + Ended(uid, component, ruleData, args); } /// - /// 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 is added /// - /// - /// This is virtual because it doesn't actually have to be used, and most of the time shouldn't be. - /// - public virtual void Added() { } + protected virtual void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + + } /// - /// Called when the game rule has been started. + /// Called when the gamerule begins /// - public abstract void Started(); + protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + + } /// - /// Called when the game rule has ended. + /// Called when the gamerule ends /// - public abstract void Ended(); + protected virtual void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args) + { + + } + + /// + /// Called on an active gamerule entity in the Update function + /// + protected virtual void ActiveTick(EntityUid uid, T component, GameRuleComponent gameRule, float frameTime) + { + + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp1, out var comp2)) + { + if (!GameTicker.IsGameRuleActive(uid, comp2)) + continue; + + ActiveTick(uid, comp1, comp2, frameTime); + } + } } diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs index d61b93d450..c2e91ba4a5 100644 --- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs @@ -1,98 +1,109 @@ 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 { [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(RunLevelChanged); - } - - public override void Started() - { - if (Configuration is not InactivityGameRuleConfiguration inactivityConfig) - return; - InactivityMaxTime = inactivityConfig.InactivityMaxTime; - RoundEndDelay = inactivityConfig.RoundEndDelay; _playerManager.PlayerStatusChanged += PlayerStatusChanged; } - public override void Ended() + public override void Shutdown() { + base.Shutdown(); _playerManager.PlayerStatusChanged -= PlayerStatusChanged; - - StopTimer(); } - public void RestartTimer() + protected override void Ended(EntityUid uid, InactivityRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) { - _timerCancel.Cancel(); - _timerCancel = new CancellationTokenSource(); - Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token); + base.Ended(uid, component, gameRule, args); + + StopTimer(uid, component); } - public void StopTimer() + public void RestartTimer(EntityUid uid, InactivityRuleComponent? component = null) { - _timerCancel.Cancel(); + if (!Resolve(uid, ref component)) + return; + + component.TimerCancel.Cancel(); + component.TimerCancel = new CancellationTokenSource(); + Timer.Spawn(component.InactivityMaxTime, () => TimerFired(uid, component), component.TimerCancel.Token); } - private void TimerFired() + public void StopTimer(EntityUid uid, InactivityRuleComponent? component = null) { + if (!Resolve(uid, ref component)) + return; + + component.TimerCancel.Cancel(); + } + + 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(); + 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) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var inactivity, out var gameRule)) { - return; - } + if (!GameTicker.IsGameRuleActive(uid, gameRule)) + return; - if (_playerManager.PlayerCount == 0) - { - RestartTimer(); - } - else - { - StopTimer(); + if (GameTicker.RunLevel != GameRunLevel.InRound) + { + return; + } + + if (_playerManager.PlayerCount == 0) + { + RestartTimer(uid, inactivity); + } + else + { + StopTimer(uid, inactivity); + } } } } diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs index 6b2a5805c6..e792a004df 100644 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -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 { [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(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(); + 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; + } } } } diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 873e3c90dc..2d372cf3d5 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -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 { [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 - { - /// - /// Operative major win. This means they nuked the station. - /// - OpsMajor, - /// - /// Minor win. All nukies were alive at the end of the round. - /// Alternatively, some nukies were alive, but the disk was left behind. - /// - OpsMinor, - /// - /// Neutral win. The nuke exploded, but on the wrong station. - /// - Neutral, - /// - /// Crew minor win. The nuclear authentication disk escaped on the shuttle, - /// but some nukies were alive. - /// - CrewMinor, - /// - /// 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. - /// - 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 _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(); - - /// - /// Cached starting gear prototypes. - /// - private readonly Dictionary _startingGearPrototypes = new (); - - /// - /// Cached operator name prototypes. - /// - private readonly Dictionary> _operativeNames = new(); - - /// - /// Data to be used in for an operative once the Mind has been added. - /// - private readonly Dictionary _operativeMindPendingData = new(); - - /// - /// Players who played as an operative at some point in the round. - /// Stores the session as well as the entity name - /// - private readonly Dictionary _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(uid, out var mindComponent) || !RuleAdded) - return; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var ruleEnt, out var nukeops, out var gameRule)) + { + if (!GameTicker.IsGameRuleAdded(ruleEnt, gameRule)) + continue; - var session = mindComponent.Mind?.Session; - var name = MetaData(uid).EntityName; - if (session != null) - _operativePlayers.Add(name, session); + // If entity has a prior mind attached, add them to the players list. + if (!TryComp(uid, out var mindComponent)) + continue; + + 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(); + 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(); + 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; - } - + /// + /// Loneops can only spawn if there is no nukeops active + /// public bool CheckLoneOpsSpawn() { - return _nukeopsRuleConfig.CanLoneOpsSpawn; + return !EntityQuery().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()) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out _, out var actor)) { - if (!TryComp(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 the win condition was set to operative/crew major win, ignore. - if (RuleWinType == WinType.OpsMajor || RuleWinType == WinType.CrewMajor) - { + if (!Resolve(uid, ref component)) return; - } - foreach (var (nuke, nukeTransform) in EntityManager.EntityQuery(true)) + // If the win condition was set to operative/crew major win, ignore. + if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor) + return; + + foreach (var (nuke, nukeTransform) in EntityQuery(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()) { 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()) @@ -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) + foreach (var nukeops in EntityQuery()) + { + var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}"); + + ev.AddLine(winText); + + foreach (var cond in nukeops.WinConditions) + { + var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); + + ev.AddLine(text); + } + + 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 SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null) + { + if (!Resolve(uid, ref component)) return; - var winText = Loc.GetString($"nukeops-{_winType.ToString().ToLower()}"); + component.WinType = type; - ev.AddLine(winText); - - foreach (var cond in _winConditions) - { - var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); - - 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); - } + if (type == WinType.CrewMajor || type == WinType.OpsMajor) + _roundEndSystem.EndRound(); } private void CheckRoundShouldEnd() { - if (!RuleAdded || !_nukeopsRuleConfig.EndsRound || RuleWinType == WinType.CrewMajor || RuleWinType == WinType.OpsMajor) - return; - - // If there are any nuclear bombs that are active, immediately return. We're not over yet. - foreach (var nuke in EntityQuery()) + var query = EntityQueryEnumerator(); + 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()) { - return; + if (nuke.Status == NukeStatus.ARMED) + { + armed = true; + break; + } } - } + if (armed) + continue; - MapId? shuttleMapId = EntityManager.EntityExists(_nukieShuttle) - ? Transform(_nukieShuttle!.Value).MapID - : null; - - 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; + + MapId? targetStationMap = null; + if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data)) + { + var grid = data.Grids.FirstOrNull(); + targetStationMap = grid != null + ? Transform(grid.Value).MapID + : null; + } + + // Check if there are nuke operatives still alive on the same map as the shuttle, + // or on the same map as the station. + // If there are, the round can continue. + var operatives = EntityQuery(true); + var operativesAlive = operatives + .Where(ent => + ent.Item3.MapID == shuttleMapId + || ent.Item3.MapID == targetStationMap) + .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running); + + if (operativesAlive) + continue; // There are living operatives than can access the shuttle, or are still on the station's map. + + // Check that there are spawns available and that they can access the shuttle. + var spawnsAvailable = EntityQuery(true).Any(); + if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet) + continue; // Ghost spawns can still access the shuttle. Continue the round. + + // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, + // and there are no nuclear operatives on the target station's map. + nukeops.WinConditions.Add(spawnsAvailable + ? WinCondition.NukiesAbandoned + : WinCondition.AllNukiesDead); + + SetWinType(uid, WinType.CrewMajor, nukeops); } - - // Check if there are nuke operatives still alive on the same map as the shuttle, - // or on the same map as the station. - // If there are, the round can continue. - var operatives = EntityQuery(true); - var operativesAlive = operatives - .Where(ent => - ent.Item3.MapID == shuttleMapId - || ent.Item3.MapID == targetStationMap) - .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running); - - if (operativesAlive) - 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(true).Any(); - if (spawnsAvailable && shuttleMapId == _nukiePlanet) - return; // Ghost spawns can still access the shuttle. Continue the round. - - // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, - // and there are no nuclear operatives on the target station's map. - if (spawnsAvailable) - { - _winConditions.Add(WinCondition.NukiesAbandoned); - } - else - { - _winConditions.Add(WinCondition.AllNukiesDead); - } - - RuleWinType = WinType.CrewMajor; } private void OnNukeDisarm(NukeDisarmSuccessEvent ev) @@ -464,111 +392,124 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnPlayersSpawning(RulePlayerSpawningEvent ev) { - if (!RuleAdded) - return; - - if (!SpawnMap()) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var nukeops, out var gameRule)) { - Logger.InfoS("nukies", "Failed to load map for nukeops"); - return; - } + if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + continue; - // Basically copied verbatim from traitor code - var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative; - var maxOperatives = _nukeopsRuleConfig.MaxOperatives; - - var everyone = new List(ev.PlayerPool); - var prefList = new List(); - var cmdrPrefList = new List(); - var operatives = new List(); - - // 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)) - { - cmdrPrefList.Add(player); - } - } - var numNukies = MathHelper.Clamp(ev.PlayerPool.Count / playersPerOperative, 1, maxOperatives); + // Basically copied verbatim from traitor code + var playersPerOperative = nukeops.PlayersPerOperative; + var maxOperatives = nukeops.MaxOperatives; - for (var i = 0; i < numNukies; i++) - { - IPlayerSession nukeOp; - // Only one commander, so we do it at the start - if (i == 0) + var everyone = new List(ev.PlayerPool); + var prefList = new List(); + var cmdrPrefList = new List(); + var operatives = new List(); + + // The LINQ expression ReSharper keeps suggesting is completely unintelligible so I'm disabling it + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (var player in everyone) { - if (cmdrPrefList.Count == 0) + 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); + + for (var i = 0; i < numNukies; i++) + { + IPlayerSession nukeOp; + // Only one commander, so we do it at the start + if (i == 0) + { + if (cmdrPrefList.Count == 0) + { + if (prefList.Count == 0) + { + if (everyone.Count == 0) + { + Logger.InfoS("preset", + "Insufficient ready players to fill up with nukeops, stopping the selection"); + break; + } + + nukeOp = _random.PickAndTake(everyone); + Logger.InfoS("preset", + "Insufficient preferred nukeop commanders or nukies, picking at random."); + } + else + { + nukeOp = _random.PickAndTake(prefList); + everyone.Remove(nukeOp); + Logger.InfoS("preset", + "Insufficient preferred nukeop commanders, picking at random from regular op list."); + } + } + else + { + nukeOp = _random.PickAndTake(cmdrPrefList); + everyone.Remove(nukeOp); + prefList.Remove(nukeOp); + Logger.InfoS("preset", "Selected a preferred nukeop commander."); + } + } + else { if (prefList.Count == 0) { if (everyone.Count == 0) { - Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection"); + Logger.InfoS("preset", + "Insufficient ready players to fill up with nukeops, stopping the selection"); break; } + nukeOp = _random.PickAndTake(everyone); - Logger.InfoS("preset", "Insufficient preferred nukeop commanders or nukies, picking at random."); + Logger.InfoS("preset", "Insufficient preferred nukeops, 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."); + Logger.InfoS("preset", "Selected a preferred nukeop."); } } - else - { - nukeOp = _random.PickAndTake(cmdrPrefList); - everyone.Remove(nukeOp); - prefList.Remove(nukeOp); - Logger.InfoS("preset", "Selected a preferred nukeop commander."); - } + + operatives.Add(nukeOp); } - else + + SpawnOperatives(numNukies, operatives, false, nukeops); + + foreach (var session in operatives) { - if (prefList.Count == 0) - { - if (everyone.Count == 0) - { - Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection"); - break; - } - nukeOp = _random.PickAndTake(everyone); - Logger.InfoS("preset", "Insufficient preferred nukeops, picking at random."); - } - else - { - nukeOp = _random.PickAndTake(prefList); - everyone.Remove(nukeOp); - Logger.InfoS("preset", "Selected a preferred nukeop."); - } + 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; } - operatives.Add(nukeOp); - } - - SpawnOperatives(numNukies, operatives, false); - - foreach(var session in operatives) - { - ev.PlayerPool.Remove(session); - GameTicker.PlayerJoinGame(session); - var name = session.AttachedEntity == null - ? string.Empty - : MetaData(session.AttachedEntity.Value).EntityName; - // TODO: Fix this being able to have duplicates - _operativePlayers[name] = session; } } @@ -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()) + { + 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()) { - 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(role))); - _operativeMindPendingData.Remove(uid); + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(role))); + nukeops.OperativeMindPendingData.Remove(uid); + } + + if (!mind.TryGetSession(out var playerSession)) + return; + if (nukeops.OperativePlayers.ContainsValue(playerSession)) + return; + + var name = MetaData(uid).EntityName; + + nukeops.OperativePlayers.Add(name, playerSession); + + if (GameTicker.RunLevel != GameRunLevel.InRound) + return; + + _audioSystem.PlayGlobal(nukeops.GreetSound, playerSession); + + if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value))) + _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", nukeops.TargetStation.Value))); } - - if (!mind.TryGetSession(out var playerSession)) - return; - if (_operativePlayers.ContainsValue(playerSession)) - return; - - var name = MetaData(uid).EntityName; - - _operativePlayers.Add(name, playerSession); - - if (_ticker.RunLevel != GameRunLevel.InRound) - return; - - if (_nukeopsRuleConfig.GreetSound != null) - _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, playerSession); - - if (_targetStation != null && !string.IsNullOrEmpty(Name(_targetStation.Value))) - _chatManager.DispatchServerMessage(playerSession, Loc.GetString("nukeops-welcome", ("station", _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(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 /// /// Adds missing nuke operative components, equips starting gear and renames the entity. /// - 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(mob); + EnsureComp(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 sessions, bool addSpawnPoints) + private void SpawnOperatives(int spawnCount, List 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(); // Forgive me for hardcoding prototypes foreach (var (_, meta, xform) in EntityManager.EntityQuery(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(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(HumanoidAppearanceSystem.DefaultSpecies); + species = _prototypeManager.Index(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(spawnPoint); EnsureComp(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(); - 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(_nukeopsRuleConfig.OperativeRoleProto))); + //ok hardcoded value bad but so is everything else here + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index("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(); + 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))); + if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + continue; + + var minPlayers = nukeops.MinPlayers; + if (!ev.Forced && ev.Players.Length < minPlayers) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", + ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); + ev.Cancel(); + continue; + } + + if (ev.Players.Length != 0) + continue; + + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); ev.Cancel(); - return; } - - if (ev.Players.Length != 0) - return; - - _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(proto)); + component.StartingGearPrototypes.Add(proto, _prototypeManager.Index(proto)); } - foreach (var proto in new[] { _nukeopsRuleConfig.EliteNames, _nukeopsRuleConfig.NormalNames }) + foreach (var proto in new[] { component.EliteNames, component.NormalNames }) { - _operativeNames.Add(proto, new List(_prototypeManager.Index(proto).Values)); + component.OperativeNames.Add(proto, new List(_prototypeManager.Index(proto).Values)); } // Add pre-existing nuke operatives to the credit list. - var query = EntityQuery(true); - foreach (var (_, mindComp) in query) + var query = EntityQuery(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; - } } diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs index c5877ab513..6ff2429c2b 100644 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs @@ -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; /// /// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion. /// -public sealed class PiratesRuleSystem : GameRuleSystem +public sealed class PiratesRuleSystem : GameRuleSystem { [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 _pirates = new(); - [ViewVariables] - private EntityUid _pirateShip = EntityUid.Invalid; - [ViewVariables] - private HashSet _initialItems = new(); - [ViewVariables] - private double _initialShipValue; - - public override string Prototype => "Pirates"; - /// public override void Initialize() { @@ -57,178 +47,186 @@ public sealed class PiratesRuleSystem : GameRuleSystem SubscribeLocalEvent(OnPlayerSpawningEvent); SubscribeLocalEvent(OnRoundEndTextEvent); + SubscribeLocalEvent(OnStartAttempt); } private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev) { - if (!RuleAdded) - return; - - if (Deleted(_pirateShip)) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var pirates, out var gameRule)) { - // Major loss, the ship somehow got annihilated. - ev.AddLine(Loc.GetString("pirates-no-ship")); - } - else - { - - List<(double, EntityUid)> mostValuableThefts = new(); - - var finalValue = _pricingSystem.AppraiseGrid(_pirateShip, uid => + if (Deleted(pirates.PirateShip)) { - foreach (var mind in _pirates) - { - if (mind.CurrentEntity == uid) - return false; // Don't appraise the pirates twice, we count them in separately. - } - return true; - }, (uid, price) => - { - if (_initialItems.Contains(uid)) - return; - - mostValuableThefts.Add((price, uid)); - mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1)); - if (mostValuableThefts.Count > 5) - mostValuableThefts.Pop(); - }); - - foreach (var mind in _pirates) - { - if (mind.CurrentEntity is not null) - finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value); + // Major loss, the ship somehow got annihilated. + ev.AddLine(Loc.GetString("pirates-no-ship")); } + else + { - var score = finalValue - _initialShipValue; + List<(double, EntityUid)> mostValuableThefts = new(); - ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}"))); - ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}"))); + var comp1 = pirates; + var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid => + { + foreach (var mind in comp1.Pirates) + { + if (mind.CurrentEntity == uid) + return false; // Don't appraise the pirates twice, we count them in separately. + } + + return true; + }, (uid, price) => + { + if (comp1.InitialItems.Contains(uid)) + return; + + 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); + } + + var score = finalValue - pirates.InitialShipValue; + + ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}"))); + ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}"))); + + ev.AddLine(""); + ev.AddLine(Loc.GetString("pirates-most-valuable")); + + foreach (var (price, obj) in mostValuableThefts) + { + ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}"))); + } + + if (mostValuableThefts.Count == 0) + ev.AddLine(Loc.GetString("pirates-stole-nothing")); + } ev.AddLine(""); - ev.AddLine(Loc.GetString("pirates-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(); + while (query.MoveNext(out var uid, out var pirates, out var gameRule)) { - return; - } + // Forgive me for copy-pasting nukies. + if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + return; - _pirates.Clear(); - _initialItems.Clear(); + pirates.Pirates.Clear(); + pirates.InitialItems.Clear(); - // Between 1 and : 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(); - - var aabbs = _stationSystem.Stations.SelectMany(x => - Comp(x).Grids.Select(x => xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB))).ToArray(); - - var aabb = aabbs[0]; - - 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 - }); - - if (!gridId.HasValue) - { - Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting."); - foreach (var session in ops) + // Between 1 and : 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++) { - ev.PlayerPool.Add(session); + ops[i] = _random.PickAndTake(ev.PlayerPool); } - return; - } - _pirateShip = gridId.Value; + var map = "/Maps/Shuttles/pirate.yml"; + var xformQuery = GetEntityQuery(); - // TODO: Loot table or something - var pirateGear = _prototypeManager.Index("PirateGear"); // YARRR + var aabbs = _stationSystem.Stations.SelectMany(x => + Comp(x).Grids.Select(x => + xformQuery.GetComponent(x).WorldMatrix.TransformBox(_mapManager.GetGridComp(x).LocalAABB))) + .ToArray(); - var spawns = new List(); + var aabb = aabbs[0]; - // Forgive me for hardcoding prototypes - foreach (var (_, meta, xform) in EntityQuery(true)) - { - if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != _pirateShip) continue; - - spawns.Add(xform.Coordinates); - } - - if (spawns.Count == 0) - { - spawns.Add(Transform(_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; - - var name = _namingSystem.GetName("Human", gender); - - var session = ops[i]; - var newMind = new Mind.Mind(session.UserId) + for (var i = 1; i < aabbs.Length; i++) { - CharacterName = name - }; - newMind.ChangeOwningPlayer(session.UserId); + aabb.Union(aabbs[i]); + } - var mob = Spawn("MobHuman", _random.Pick(spawns)); - MetaData(mob).EntityName = name; + var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions + { + Offset = aabb.Center + MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f + }); - newMind.TransferTo(mob); - var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile; - _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile); + if (!gridId.HasValue) + { + Logger.ErrorS("pirates", $"Gridid was null when loading \"{map}\", aborting."); + foreach (var session in ops) + { + ev.PlayerPool.Add(session); + } - _pirates.Add(newMind); + return; + } - GameTicker.PlayerJoinGame(session); + pirates.PirateShip = gridId.Value; + + // TODO: Loot table or something + var pirateGear = _prototypeManager.Index("PirateGear"); // YARRR + + var spawns = new List(); + + // Forgive me for hardcoding prototypes + foreach (var (_, meta, xform) in + EntityQuery(true)) + { + if (meta.EntityPrototype?.ID != "SpawnPointPirates" || xform.ParentUid != pirates.PirateShip) + continue; + + spawns.Add(xform.Coordinates); + } + + 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; + + var name = _namingSystem.GetName("Human", gender); + + 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; + + newMind.TransferTo(mob); + var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile; + _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile); + + pirates.Pirates.Add(newMind); + + GameTicker.PlayerJoinGame(session); + } + + pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid => + { + pirates.InitialItems.Add(uid); + return true; + }); // Include the players in the appraisal. } - - _initialShipValue = _pricingSystem.AppraiseGrid(_pirateShip, uid => - { - _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(); + 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(); + } } } } diff --git a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs index 1df78acd8d..a26a2d783c 100644 --- a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs @@ -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 { [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; } } diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs index 4878f65ff0..8ad83ecd5d 100644 --- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs @@ -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 { [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("Secret").Pick(_random); Logger.InfoS("gamepreset", $"Selected {preset} for secret."); - foreach (var rule in _prototypeManager.Index(preset).Rules) + var rules = _prototypeManager.Index(preset).Rules; + foreach (var rule in rules) { - _ticker.StartGameRule(_prototypeManager.Index(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 index 3c82367182..0000000000 --- a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs +++ /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; - -/// -/// Simple GameRule that will do a TTT-like gamemode with traitors. -/// -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 _traitors = new(); - - public IReadOnlyCollection 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(OnPlayersAssigned); - SubscribeLocalEvent(OnRoundStartAttempt); - SubscribeLocalEvent(OnLateJoinRefresh); - SubscribeLocalEvent(Reset); - - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); - SubscribeLocalEvent(OnRoleAdded); - SubscribeLocalEvent(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(ev.Players); - var prefList = new List(); - - foreach (var player in list) - { - if (!ev.Profiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached) - { - continue; - } - prefList.Add(player); - - attached.EnsureComponent(); - } - - // 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(); - - 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(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(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() ?? false); - - SoundSystem.Play(_addedSound.GetSound(), filter, AudioParams.Default); - - _doorSystem.AccessType = SharedDoorSystem.AccessTypes.AllowAllNoExternal; - - var susLoot = _prototypeManager.Index(SuspicionLootTable); - - foreach (var (_, mapGrid) in EntityManager.EntityQuery(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(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(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(playerEntity)) - { - continue; - } - - if (!_mobStateSystem.IsAlive(playerEntity, mobState)) - { - continue; - } - - var mind = playerSession.ContentData()?.Mind; - - if (mind != null && mind.HasRole()) - 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 index a634bb34c6..0000000000 --- a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs +++ /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 _allOriginalNames = new(); - - private const string TraitorPrototypeID = "Traitor"; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnRoundEndText); - SubscribeLocalEvent(OnPlayerSpawned); - SubscribeLocalEvent(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(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(newPDA); - EntityManager.EntitySysManager.GetEntitySystem().SetOwner(pda, Name(owned)); - EntityManager.AddComponent(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("Asphyxiation"), 100); - Get().TryChangeDamage(entity, damage, true); - } - else if (!_mobStateSystem.IsDead(entity,mobState)) - { - if (HasComp(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(); - lines.Add(Loc.GetString("traitor-death-match-end-round-description-first-line")); - - foreach (var uplink in EntityManager.EntityQuery(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(); - 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().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; - } - -} diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 9edfe80d20..bb6bb89f2a 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -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 { [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 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 _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(OnRoundEndText); } - public override void Update(float frameTime) + protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime) { - base.Update(frameTime); + base.ActiveTick(uid, component, gameRule, frameTime); - if (SelectionStatus == SelectionState.ReadyToSelect && _gameTiming.CurTime >= _announceAt) - DoTraitorStart(); - } - - 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(); + 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("adjectives").Values; var verbs = _prototypeManager.Index("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(); + 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))); + + traitor.AnnounceAt = _gameTiming.CurTime + delay; + + traitor.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToSelect; } - - var delay = TimeSpan.FromSeconds( - _cfg.GetCVar(CCVars.TraitorStartDelay) + - _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance))); - - _announceAt = _gameTiming.CurTime + delay; - - SelectionStatus = SelectionState.ReadyToSelect; } - public List FindPotentialTraitors(in Dictionary candidates) + public List FindPotentialTraitors(in Dictionary candidates, TraitorRuleComponent component) { var list = new List(); var pendingQuery = GetEntityQuery(); @@ -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().FirstOrDefault(); + if (traitorRule == null) + { + //todo fuck me this shit is awful + GameTicker.StartGameRule("traitor", out var ruleEntity); + traitorRule = EntityManager.GetComponent(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(pda.Value).Code; - var antagPrototype = _prototypeManager.Index(TraitorPrototypeID); + var antagPrototype = _prototypeManager.Index(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; - - - if (ev.JobId == null || !_prototypeManager.TryIndex(ev.JobId, out var job)) - return; - - if (!job.CanBeAntag) - return; - - // Before the announcement is made, late-joiners are considered the same as players who readied. - if (SelectionStatus < SelectionState.SelectionMade) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var traitor, out var gameRule)) { - _startCandidates[ev.Player] = ev.Profile; - return; - } + if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + continue; - // the nth player we adjust our probabilities around - int target = ((_playersPerTraitor * TotalTraitors) + 1); + if (traitor.TotalTraitors >= MaxTraitors) + continue; + if (!ev.LateJoin) + continue; + if (!ev.Profile.AntagPreferences.Contains(traitor.TraitorPrototypeId)) + continue; - float chance = (1f / _playersPerTraitor); + if (ev.JobId == null || !_prototypeManager.TryIndex(ev.JobId, out var job)) + continue; - // 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 (!job.CanBeAntag) + continue; - // 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); + // Before the announcement is made, late-joiners are considered the same as players who readied. + if (traitor.SelectionStatus < TraitorRuleComponent.SelectionState.SelectionMade) + { + traitor.StartCandidates[ev.Player] = ev.Profile; + continue; + } + + // the nth player we adjust our probabilities around + var target = PlayersPerTraitor * traitor.TotalTraitors + 1; + + var chance = 1f / PlayersPerTraitor; + + // If we have too many traitors, divide by how many players below target for next traitor we are. + if (ev.JoinOrder < target) + { + chance /= (target - ev.JoinOrder); + } + else // Tick up towards 100% chance. + { + chance *= ((ev.JoinOrder + 1) - target); + } + + if (chance > 1) + chance = 1; + + // Now that we've calculated our chance, roll and make them a traitor if we roll under. + // You get one shot. + if (_random.Prob(chance)) + { + MakeTraitor(ev.Player); + } } } private void OnRoundEndText(RoundEndTextAppendEvent ev) { - if (!RuleAdded) - return; - - var result = Loc.GetString("traitor-round-end-result", ("traitorCount", Traitors.Count)); - - result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", Codewords))) + "\n"; - - foreach (var traitor in Traitors) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var traitor, out var gameRule)) { - var name = traitor.Mind.CharacterName; - traitor.Mind.TryGetSession(out var session); - var username = session?.Name; + if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + continue; - var objectives = traitor.Mind.AllObjectives.ToArray(); - if (objectives.Length == 0) + var result = Loc.GetString("traitor-round-end-result", ("traitorCount", traitor.Traitors.Count)); + + result += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", traitor.Codewords))) + + "\n"; + + 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)); + result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-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}"); - - 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) + foreach (var condition in objective.Conditions) { - 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") - ); + 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 GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind) + public List GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind) { - var traitors = Traitors; - List removeList = new(); + List allTraitors = new(); + foreach (var traitor in EntityQuery()) + { + 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 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 } } diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index f51a22b42d..4ac83b354e 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -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 { [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 _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()) { - 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); - //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) + 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", zombie.InitialInfectedNames.Count))); + foreach (var player in zombie.InitialInfectedNames) { - var meta = MetaData(survivor); - var username = string.Empty; - if (TryComp(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-initial", + ("name", player.Key), + ("username", player.Value))); + } - ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", - ("name", meta.EntityName), - ("username", username))); + //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) + { + 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(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(); + while (query.MoveNext(out var uid, out var zombies, out var gameRule)) + { + if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + continue; + InfectInitialPlayers(zombies); + } } /// @@ -129,15 +122,11 @@ public sealed class ZombieRuleSystem : GameRuleSystem /// 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 /// depending on this uid, we should care about the round ending private void CheckRoundEnd(EntityUid target) { - //we only care about players, not monkeys and such. - if (!HasComp(target)) - return; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var zombies, out var gameRule)) + { + if (GameTicker.IsGameRuleActive(uid, gameRule)) + 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(); + //we only care about players, not monkeys and such. + if (!HasComp(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(); + } } private void OnStartAttempt(RoundStartAttemptEvent ev) { - if (!RuleAdded) - return; - - var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers); - if (!ev.Forced && ev.Players.Length < minPlayers) + var query = EntityQueryEnumerator(); + 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(ZombifySelfActionPrototype)); + var action = new InstantAction(_prototypeManager.Index(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. /// - private void InfectInitialPlayers() + private void InfectInitialPlayers(ZombieRuleComponent component) { var allPlayers = _playerManager.ServerSessions.ToList(); var playerList = new List(); @@ -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(PatientZeroPrototypeID))); + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(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(ZombifySelfActionPrototype)); + var action = new InstantAction(_prototypeManager.Index(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!?!? diff --git a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs index d653df357d..0fd1d53a09 100644 --- a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs +++ b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs @@ -14,9 +14,10 @@ namespace Content.Server.Objectives.Conditions public IObjectiveCondition GetAssigned(Mind.Mind mind) { var entityMgr = IoCManager.Resolve(); - var traitors = entityMgr.EntitySysManager.GetEntitySystem().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().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().Pick(traitors).Mind }; } diff --git a/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs index fd19603387..99de8be282 100644 --- a/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs +++ b/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs @@ -13,7 +13,9 @@ namespace Content.Server.Objectives.Conditions public IObjectiveCondition GetAssigned(Mind.Mind mind) { + //todo shit of a fuck var entityMgr = IoCManager.Resolve(); + var traitors = entityMgr.EntitySysManager.GetEntitySystem().GetOtherTraitorsAliveAndConnected(mind).ToList(); List 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); } diff --git a/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs b/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs index e833165862..5d4e458297 100644 --- a/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs +++ b/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs @@ -11,7 +11,7 @@ namespace Content.Server.Objectives.Requirements public bool CanBeAssigned(Mind.Mind mind) { - return EntitySystem.Get().TotalTraitors >= _requiredTraitors; + return EntitySystem.Get().GetOtherTraitorsAliveAndConnected(mind).Count >= _requiredTraitors; } } } diff --git a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs index a5279ce848..12a16151c1 100644 --- a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs +++ b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs @@ -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))] + [DataField("prototypes", customTypeSerializer: typeof(PrototypeIdListSerializer))] public List Prototypes { get; set; } = new(); [ViewVariables(VVAccess.ReadWrite)] - [DataField("gameRules", customTypeSerializer:typeof(PrototypeIdListSerializer))] + [DataField("gameRules", customTypeSerializer: typeof(PrototypeIdListSerializer))] public readonly List GameRules = new(); [ViewVariables(VVAccess.ReadWrite)] diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs index 1eef300972..341a2c9d44 100644 --- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -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()) + var query = EntityQueryEnumerator(); + 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); } diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 0304978ed6..bc77a9ce47 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -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. /// [UsedImplicitly] - public sealed class BasicStationEventSchedulerSystem : GameRuleSystem + public sealed class BasicStationEventSchedulerSystem : GameRuleSystem { - public override string Prototype => "BasicStationEventScheduler"; - [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EventManagerSystem _event = default!; - private const float MinimumTimeUntilFirstEvent = 300; - - /// - /// How long until the next check for an event runs - /// - /// 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(); + 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); + } } /// /// Reset the event timer once the event is done. /// - 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 index 0000000000..8e8e853260 --- /dev/null +++ b/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs @@ -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; + +/// +/// Used an event that spawns an anomaly somewhere random on the map. +/// +[RegisterComponent, Access(typeof(AnomalySpawnRule))] +public sealed class AnomalySpawnRuleComponent : Component +{ + [DataField("anomalySpawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string AnomalySpawnerPrototype = "RandomAnomalySpawner"; +} diff --git a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs new file mode 100644 index 0000000000..1f920b6c7c --- /dev/null +++ b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.StationEvents.Components; + +[RegisterComponent, Access(typeof(BasicStationEventSchedulerSystem))] +public sealed class BasicStationEventSchedulerComponent : Component +{ + public const float MinimumTimeUntilFirstEvent = 300; + + /// + /// How long until the next check for an event runs + /// + /// 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 index 0000000000..fe953c97f8 --- /dev/null +++ b/Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs @@ -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; + +/// +/// This is used for an event that spawns an artifact +/// somewhere random on the station. +/// +[RegisterComponent, Access(typeof(BluespaceArtifactRule))] +public sealed class BluespaceArtifactRuleComponent : Component +{ + [DataField("artifactSpawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ArtifactSpawnerPrototype = "RandomArtifactSpawner"; + + [DataField("artifactFlashPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ArtifactFlashPrototype = "EffectFlashBluespace"; + + [DataField("possibleSightings")] + public List 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 index 0000000000..06515f53fc --- /dev/null +++ b/Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs @@ -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 index 0000000000..a4acef1345 --- /dev/null +++ b/Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs @@ -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 index 0000000000..5132a09e68 --- /dev/null +++ b/Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs @@ -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 index 0000000000..d82557cf13 --- /dev/null +++ b/Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs @@ -0,0 +1,25 @@ +using Content.Server.StationEvents.Events; + +namespace Content.Server.StationEvents.Components; + +[RegisterComponent, Access(typeof(DiseaseOutbreakRule))] +public sealed class DiseaseOutbreakRuleComponent : Component +{ + /// + /// Disease prototypes I decided were not too deadly for a random event + /// + /// + /// Fire name + /// + [DataField("notTooSeriousDiseases")] + public readonly IReadOnlyList 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 index 0000000000..6e3f69ad6c --- /dev/null +++ b/Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs @@ -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 index 0000000000..5f2c2f5ff6 --- /dev/null +++ b/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs @@ -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, + }; + + /// + /// Running cooldown of how much time until another leak. + /// + public float TimeUntilLeak; + + /// + /// How long between more gas being added to the tile. + /// + 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; + + /// + /// Don't want to make it too fast to give people time to flee. + /// + 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 index 0000000000..82cc1ac77d --- /dev/null +++ b/Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs @@ -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 index 0000000000..535660362f --- /dev/null +++ b/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs @@ -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))] + 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 index 0000000000..e42eef5b26 --- /dev/null +++ b/Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs @@ -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; + + /// + /// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer. + /// + 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 index 0000000000..cb6274624c --- /dev/null +++ b/Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs @@ -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 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 index 0000000000..1788a78077 --- /dev/null +++ b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs @@ -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 Powered = new(); + public readonly List 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 index 0000000000..dd3f38b5dd --- /dev/null +++ b/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs @@ -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 index 0000000000..6c00e9e362 --- /dev/null +++ b/Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs @@ -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 index 0000000000..d195eaad7c --- /dev/null +++ b/Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs @@ -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"; +} diff --git a/Content.Server/StationEvents/Components/SentienceTargetComponent.cs b/Content.Server/StationEvents/Components/SentienceTargetComponent.cs index bf40d50249..d22f3e74eb 100644 --- a/Content.Server/StationEvents/Components/SentienceTargetComponent.cs +++ b/Content.Server/StationEvents/Components/SentienceTargetComponent.cs @@ -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)] diff --git a/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs b/Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs similarity index 69% rename from Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs rename to Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs index f014c56280..92a3b43375 100644 --- a/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs +++ b/Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs @@ -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; /// /// Solar Flare event specific configuration /// -public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfiguration +[RegisterComponent, Access(typeof(SolarFlareRule))] +public sealed class SolarFlareRuleComponent : Component { - /// - /// In seconds, most early moment event can end - /// - [DataField("minEndAfter")] - public int MinEndAfter; - - /// - /// In seconds, most late moment event can end - /// - [DataField("maxEndAfter")] - public int MaxEndAfter; - /// /// If true, only headsets affected, but e.g. handheld radio will still work /// @@ -43,4 +33,4 @@ public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfigura /// [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 index 0000000000..15e01ac8a8 --- /dev/null +++ b/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs @@ -0,0 +1,9 @@ +using Content.Server.StationEvents.Events; + +namespace Content.Server.StationEvents.Components; + +[RegisterComponent, Access(typeof(SpiderSpawnRule))] +public sealed class SpiderSpawnRuleComponent : Component +{ + +} diff --git a/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs b/Content.Server/StationEvents/Components/StationEventComponent.cs similarity index 58% rename from Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs rename to Content.Server/StationEvents/Components/StationEventComponent.cs index 84689dc225..e79fd6e86d 100644 --- a/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs +++ b/Content.Server/StationEvents/Components/StationEventComponent.cs @@ -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; /// -/// 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 /// -[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; /// - /// When in the lifetime to start the event. + /// How long after being added does the event start /// - [DataField("startAfter")] - public float StartAfter; + [DataField("startDelay")] + public TimeSpan StartDelay = TimeSpan.Zero; /// - /// When in the lifetime to end the event.. + /// How long the event lasts. /// - [DataField("endAfter")] - public float EndAfter = float.MaxValue; + [DataField("duration")] + public TimeSpan Duration = TimeSpan.FromSeconds(1); + + /// + /// The max amount of time the event lasts. + /// + [DataField("maxDuration")] + public TimeSpan? MaxDuration; /// /// How many players need to be present on station for the event to run @@ -73,4 +74,16 @@ public class StationEventRuleConfiguration : GameRuleConfiguration /// [DataField("maxOccurrences")] public int? MaxOccurrences; + + /// + /// When the station event starts. + /// + [DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan StartTime; + + /// + /// When the station event starts. + /// + [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 index 0000000000..79f4993375 --- /dev/null +++ b/Content.Server/StationEvents/Components/VentClogRuleComponent.cs @@ -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 SafeishVentChemicals = new[] + { + "Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer" + }; + +} diff --git a/Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs b/Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs index 3dc46642e7..f675744929 100644 --- a/Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs +++ b/Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs @@ -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 index 0000000000..5332796e84 --- /dev/null +++ b/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs @@ -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 SpawnedPrototypeChoices = new() + { + "MobMouse", + "MobMouse1", + "MobMouse2" + }; +} diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 6a2b646602..72a5dfac77 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -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(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(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 /// /// Randomly picks a valid event. /// - 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. /// /// - private StationEventRuleConfiguration? FindEvent(List availableEvents) + private string? FindEvent(Dictionary 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 /// /// /// - private List AvailableEvents(bool ignoreEarliestStart = false) + private Dictionary 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(); + var result = new Dictionary(); - 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 AllEvents() + public Dictionary AllEvents() { - return _prototype.EnumeratePrototypes() - .Where(p => p.Configuration is StationEventRuleConfiguration) - .Select(p => (StationEventRuleConfiguration) p.Configuration); + var allEvents = new Dictionary(); + foreach (var prototype in _prototype.EnumeratePrototypes()) + { + if (prototype.Abstract) + continue; + + if (!prototype.TryGetComponent(out var stationEvent)) + continue; + + allEvents.Add(prototype, stationEvent); + } + + return allEvents; } - private int GetOccurrences(StationEventRuleConfiguration stationEvent) + private int GetOccurrences(EntityPrototype stationEvent) { - return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id); + return GetOccurrences(stationEvent.ID); } - public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent) + private int GetOccurrences(string stationEvent) + { + return GameTicker.AllPreviousGameRules.Count(p => p.Item2 == 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) { diff --git a/Content.Server/StationEvents/Events/AnomalySpawn.cs b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs similarity index 61% rename from Content.Server/StationEvents/Events/AnomalySpawn.cs rename to Content.Server/StationEvents/Events/AnomalySpawnRule.cs index 5b7d1c42f5..8e7a860d2e 100644 --- a/Content.Server/StationEvents/Events/AnomalySpawn.cs +++ b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs @@ -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 { - [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 index 407cca0f47..0000000000 --- a/Content.Server/StationEvents/Events/BluespaceArtifact.cs +++ /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 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 index 0000000000..306b735b84 --- /dev/null +++ b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs @@ -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 +{ + 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}"); + } + } +} diff --git a/Content.Server/StationEvents/Events/BluespaceLocker.cs b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs similarity index 79% rename from Content.Server/StationEvents/Events/BluespaceLocker.cs rename to Content.Server/StationEvents/Events/BluespaceLockerRule.cs index 88de303c07..ddf1ba784c 100644 --- a/Content.Server/StationEvents/Events/BluespaceLocker.cs +++ b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs @@ -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 { - [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().ToList(); - _robustRandom.Shuffle(targets); + RobustRandom.Shuffle(targets); foreach (var target in targets) { diff --git a/Content.Server/StationEvents/Events/BreakerFlip.cs b/Content.Server/StationEvents/Events/BreakerFlipRule.cs similarity index 70% rename from Content.Server/StationEvents/Events/BreakerFlip.cs rename to Content.Server/StationEvents/Events/BreakerFlipRule.cs index 2df721b7cc..2920ee331b 100644 --- a/Content.Server/StationEvents/Events/BreakerFlip.cs +++ b/Content.Server/StationEvents/Events/BreakerFlipRule.cs @@ -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 { [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(); - foreach (var (apc, transform) in EntityQuery()) + foreach (var (apc, transform) in EntityQuery()) { if (apc.MainBreakerEnabled && CompOrNull(transform.GridUid)?.Station == chosenStation) { stationApcs.Add(apc); } } - + var toDisable = Math.Min(RobustRandom.Next(3, 7), stationApcs.Count); if (toDisable == 0) return; diff --git a/Content.Server/StationEvents/Events/BureaucraticError.cs b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs similarity index 82% rename from Content.Server/StationEvents/Events/BureaucraticError.cs rename to Content.Server/StationEvents/Events/BureaucraticErrorRule.cs index a96eae51b5..0fbf72fd7d 100644 --- a/Content.Server/StationEvents/Events/BureaucraticError.cs +++ b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs @@ -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 { [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 diff --git a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs b/Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs similarity index 70% rename from Content.Server/StationEvents/Events/DiseaseOutbreak.cs rename to Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs index 3c829ddfab..438cc041df 100644 --- a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs +++ b/Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs @@ -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 /// -public sealed class DiseaseOutbreak : StationEventSystem +public sealed class DiseaseOutbreakRule : StationEventSystem { [Dependency] private readonly DiseaseSystem _diseaseSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - public override string Prototype => "DiseaseOutbreak"; - - /// - /// Disease prototypes I decided were not too deadly for a random event - /// - public readonly IReadOnlyList NotTooSeriousDiseases = new[] - { - "SpaceCold", - "VanAusdallsRobovirus", - "VentCough", - "AMIV", - "SpaceFlu", - "BirdFlew", - "TongueTwister" - }; - /// /// Finds 2-5 random, alive entities that can host diseases /// and gives them a randomly selected disease. /// They all get the same disease. /// - 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 stationsToNotify = new(); List aliveList = new(); - foreach (var (carrier, mobState) in EntityManager.EntityQuery()) + foreach (var (carrier, mobState) in EntityQuery()) { 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 index 701f1c2d0d..0000000000 --- a/Content.Server/StationEvents/Events/FalseAlarm.cs +++ /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 index 0000000000..05e9435b40 --- /dev/null +++ b/Content.Server/StationEvents/Events/FalseAlarmRule.cs @@ -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 +{ + [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 index e073ec20f5..0000000000 --- a/Content.Server/StationEvents/Events/GasLeak.cs +++ /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, - }; - - /// - /// Running cooldown of how much time until another leak. - /// - private float _timeUntilLeak; - - /// - /// How long between more gas being added to the tile. - /// - 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; - - /// - /// Don't want to make it too fast to give people time to flee. - /// - 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(_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 index 0000000000..95e9286c46 --- /dev/null +++ b/Content.Server/StationEvents/Events/GasLeakRule.cs @@ -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 + { + [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(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 index e0b0f5c687..0000000000 --- a/Content.Server/StationEvents/Events/KudzuGrowth.cs +++ /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 index 0000000000..3fa12cd4e9 --- /dev/null +++ b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs @@ -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 +{ + 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 index 151a592a26..0000000000 --- a/Content.Server/StationEvents/Events/LoneOpsSpawn.cs +++ /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(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 index 0000000000..57f0a2cbb6 --- /dev/null +++ b/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs @@ -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 +{ + [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(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); + } +} + diff --git a/Content.Server/StationEvents/Events/MeteorSwarm.cs b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs similarity index 53% rename from Content.Server/StationEvents/Events/MeteorSwarm.cs rename to Content.Server/StationEvents/Events/MeteorSwarmRule.cs index aabc9ba2bc..eb0597a92e 100644 --- a/Content.Server/StationEvents/Events/MeteorSwarm.cs +++ b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs @@ -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 { [Dependency] private readonly SharedPhysicsSystem _physics = default!; - public override string Prototype => "MeteorSwarm"; - - private float _cooldown; - - /// - /// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer. - /// - 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(); + base.Started(uid, component, gameRule, args); + var mod = Math.Sqrt(GetSeverityModifier()); - _waveCounter = (int) (RobustRandom.Next(MinimumWaves, MaximumWaves) * mod); + component._waveCounter = (int) (RobustRandom.Next(component.MinimumWaves, component.MaximumWaves) * mod); } - public override void Ended() + protected override void ActiveTick(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, float frameTime) { - base.Ended(); - _waveCounter = 0; - _cooldown = 0f; - } - - public override void Update(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(meteor).Lifetime = 120f; diff --git a/Content.Server/StationEvents/Events/MouseMigration.cs b/Content.Server/StationEvents/Events/MouseMigrationRule.cs similarity index 66% rename from Content.Server/StationEvents/Events/MouseMigration.cs rename to Content.Server/StationEvents/Events/MouseMigrationRule.cs index e2f2a5b9b0..b0c54a7d0e 100644 --- a/Content.Server/StationEvents/Events/MouseMigration.cs +++ b/Content.Server/StationEvents/Events/MouseMigrationRule.cs @@ -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 { - public static List SpawnedPrototypeChoices = new List() //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 index b1f21af12c..0000000000 --- a/Content.Server/StationEvents/Events/PowerGridCheck.cs +++ /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 _powered = new(); - private readonly List _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(true)) - { - if (apc.MainBreakerEnabled && CompOrNull(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(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 index 0000000000..78a77c9a57 --- /dev/null +++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs @@ -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 + { + [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(true)) + { + if (apc.MainBreakerEnabled && CompOrNull(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(selected, out var apcComponent)) + { + if (apcComponent.MainBreakerEnabled) + _apcSystem.ApcToggleBreaker(selected, apcComponent); + } + component.Unpowered.Add(selected); + } + } + } +} diff --git a/Content.Server/StationEvents/Events/RandomSentience.cs b/Content.Server/StationEvents/Events/RandomSentienceRule.cs similarity index 67% rename from Content.Server/StationEvents/Events/RandomSentience.cs rename to Content.Server/StationEvents/Events/RandomSentienceRule.cs index 8181e11c0d..8b2128178a 100644 --- a/Content.Server/StationEvents/Events/RandomSentience.cs +++ b/Content.Server/StationEvents/Events/RandomSentienceRule.cs @@ -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 { - public override string Prototype => "RandomSentience"; - - public override void Started() + protected override void Started(EntityUid uid, RandomSentienceRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { - base.Started(); HashSet stationsToNotify = new(); var mod = GetSeverityModifier(); - var targetList = EntityManager.EntityQuery().ToList(); + var targetList = EntityQuery().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(target.Owner); - var ghostRole = AddComp(target.Owner); - AddComp(target.Owner); - ghostRole.RoleName = EntityManager.GetComponent(target.Owner).EntityName; + RemComp(target.Owner); + var ghostRole = EnsureComp(target.Owner); + EnsureComp(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(); - var stationSystem = entSysMgr.GetEntitySystem(); - var chatSystem = entSysMgr.GetEntitySystem(); 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 index 3375b2b3c2..0000000000 --- a/Content.Server/StationEvents/Events/RevenantSpawn.cs +++ /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 index 0000000000..6e99fb495e --- /dev/null +++ b/Content.Server/StationEvents/Events/RevenantSpawnRule.cs @@ -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 +{ + 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 index cf3a2a403b..0000000000 --- a/Content.Server/StationEvents/Events/SolarFlare.cs +++ /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(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(); - while (lightQuery.MoveNext(out var uid, out var light)) - { - if (RobustRandom.Prob(_event.LightBreakChancePerSecond)) - _poweredLight.TryDestroyBulb(uid, light); - } - var airlockQuery = EntityQueryEnumerator(); - 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(args.RadioReceiver) || HasComp(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 index 0000000000..206a8ca685 --- /dev/null +++ b/Content.Server/StationEvents/Events/SolarFlareRule.cs @@ -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 +{ + [Dependency] private readonly PoweredLightSystem _poweredLight = default!; + [Dependency] private readonly SharedDoorSystem _door = default!; + + private float _effectTimer = 0; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(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(); + while (lightQuery.MoveNext(out var lightEnt, out var light)) + { + if (RobustRandom.Prob(component.LightBreakChancePerSecond)) + _poweredLight.TryDestroyBulb(lightEnt, light); + } + var airlockQuery = EntityQueryEnumerator(); + 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(); + 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(args.RadioReceiver) || HasComp(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 index cb58e438ee..0000000000 --- a/Content.Server/StationEvents/Events/SpiderSpawn.cs +++ /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().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(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 index 0000000000..ba440f8cde --- /dev/null +++ b/Content.Server/StationEvents/Events/SpiderSpawnRule.cs @@ -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 +{ + protected override void Started(EntityUid uid, SpiderSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + var spawnLocations = EntityQuery().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); + } + } +} diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index bbf73f901f..bcec380987 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -1,209 +1,197 @@ -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; + +/// +/// An abstract entity system inherited by all station events for their behavior. +/// +public abstract class StationEventSystem : GameRuleSystem where T : Component +{ + [Dependency] protected readonly IAdminLogManager AdminLogManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; + [Dependency] protected readonly IRobustRandom RobustRandom = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] protected readonly ChatSystem ChatSystem = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] protected readonly StationSystem StationSystem = default!; + + protected ISawmill Sawmill = default!; + + public override void Initialize() + { + base.Initialize(); + + Sawmill = Logger.GetSawmill("stationevents"); + } + + /// + protected override void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + base.Added(uid, component, gameRule, args); + + if (!TryComp(uid, out var stationEvent)) + return; + + AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {ToPrettyString(uid)}"); + + if (stationEvent.StartAnnouncement != null) + { + ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.StartAnnouncement), playSound: false, colorOverride: Color.Gold); + } + + Audio.PlayGlobal(stationEvent.StartAudio, Filter.Broadcast(), true); + stationEvent.StartTime = _timing.CurTime + stationEvent.StartDelay; + } + + /// + protected override void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + if (!TryComp(uid, out var stationEvent)) + return; + + 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; + } + + /// + protected override void Ended(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleEndedEvent args) + { + base.Ended(uid, component, gameRule, args); + + if (!TryComp(uid, out var stationEvent)) + return; + + AdminLogManager.Add(LogType.EventStopped, $"Event ended: {ToPrettyString(uid)}"); + + if (stationEvent.EndAnnouncement != null) + { + ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.EndAnnouncement), playSound: false, colorOverride: Color.Gold); + } + + Audio.PlayGlobal(stationEvent.EndAudio, Filter.Broadcast(), true); + } + + /// + /// Called every tick when this event is running. + /// Events are responsible for their own lifetime, so this handles starting and ending after time. + /// + /// + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var stationEvent, out var ruleData)) + { + if (!GameTicker.IsGameRuleAdded(uid, ruleData)) + continue; + + if (!GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.StartTime) + { + GameTicker.StartGameRule(uid, ruleData); + } + else if (GameTicker.IsGameRuleActive(uid, ruleData) && _timing.CurTime >= stationEvent.EndTime) + { + GameTicker.EndGameRule(uid, ruleData); + } + } + } + + #region Helper Functions + + protected void ForceEndSelf(EntityUid uid, GameRuleComponent? component = null) + { + GameTicker.EndGameRule(uid, component); + } + + protected bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords) + { + tile = default; + + targetCoords = EntityCoordinates.Invalid; + if (StationSystem.Stations.Count == 0) + { + targetStation = EntityUid.Invalid; + targetGrid = EntityUid.Invalid; + return false; + } + targetStation = RobustRandom.Pick(StationSystem.Stations); + var possibleTargets = Comp(targetStation).Grids; + if (possibleTargets.Count == 0) + { + targetGrid = EntityUid.Invalid; + return false; + } + + targetGrid = RobustRandom.Pick(possibleTargets); + + if (!TryComp(targetGrid, out var gridComp)) + return false; + + var found = false; + var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(targetGrid); + 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); + + 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)) + { + continue; + } + + found = true; + targetCoords = gridComp.GridTileToLocal(tile); + break; + } + + return found; + } + public float GetSeverityModifier() + { + var ev = new GetSeverityModifierEvent(); + RaiseLocalEvent(ev); + return ev.Modifier; + } + + #endregion +} + +/// +/// 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. +/// +public sealed class GetSeverityModifierEvent : EntityEventArgs { /// - /// An abstract entity system inherited by all station events for their behavior. + /// Should be multiplied/added to rather than set, for commutativity. /// - public abstract class StationEventSystem : GameRuleSystem - { - [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!; - - /// - /// How long has the event existed. Do not change this. - /// - protected float Elapsed { get; set; } - - public override void Initialize() - { - base.Initialize(); - - Sawmill = Logger.GetSawmill("stationevents"); - } - - /// - /// Called once to setup the event after StartAfter has elapsed, or if an event is forcibly started. - /// - public override void Started() - { - AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {Configuration.Id}"); - } - - /// - /// Called once as soon as an event is added, for announcements. - /// Can also be used for some initial setup. - /// - public override void Added() - { - AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {Configuration.Id}"); - - if (Configuration is not StationEventRuleConfiguration ev) - return; - - if (ev.StartAnnouncement != null) - { - ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.StartAnnouncement), playSound: false, colorOverride: Color.Gold); - } - - if (ev.StartAudio != null) - { - SoundSystem.Play(ev.StartAudio.GetSound(), Filter.Broadcast(), ev.StartAudio.Params); - } - - Elapsed = 0; - } - - /// - /// Called once when the station event ends for any reason. - /// - public override void Ended() - { - AdminLogManager.Add(LogType.EventStopped, $"Event ended: {Configuration.Id}"); - - if (Configuration is not StationEventRuleConfiguration ev) - return; - - if (ev.EndAnnouncement != null) - { - ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.EndAnnouncement), playSound: false, colorOverride: Color.Gold); - } - - if (ev.EndAudio != null) - { - SoundSystem.Play(ev.EndAudio.GetSound(), Filter.Broadcast(), ev.EndAudio.Params); - } - } - - /// - /// Called every tick when this event is running. - /// Events are responsible for their own lifetime, so this handles starting and ending after time. - /// - public override void Update(float frameTime) - { - if (!RuleAdded || Configuration is not StationEventRuleConfiguration data) - return; - - Elapsed += frameTime; - - if (!RuleStarted && Elapsed >= data.StartAfter) - { - GameTicker.StartGameRule(PrototypeManager.Index(Prototype)); - } - - if (RuleStarted && Elapsed >= data.EndAfter) - { - GameTicker.EndGameRule(PrototypeManager.Index(Prototype)); - } - } - - #region Helper Functions - - protected void ForceEndSelf() - { - GameTicker.EndGameRule(PrototypeManager.Index(Prototype)); - } - - protected bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords) - { - tile = default; - - targetCoords = EntityCoordinates.Invalid; - if (StationSystem.Stations.Count == 0) - { - targetStation = EntityUid.Invalid; - targetGrid = EntityUid.Invalid; - return false; - } - targetStation = RobustRandom.Pick(StationSystem.Stations); - var possibleTargets = Comp(targetStation).Grids; - if (possibleTargets.Count == 0) - { - targetGrid = EntityUid.Invalid; - return false; - } - - targetGrid = RobustRandom.Pick(possibleTargets); - - if (!TryComp(targetGrid, out var gridComp)) - return false; - - 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); - - 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; - } - - 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() - .Where(p => p.Configuration is StationEventRuleConfiguration).ToArray()); - } - - public float GetSeverityModifier() - { - var ev = new GetSeverityModifierEvent(); - RaiseLocalEvent(ev); - return ev.Modifier; - } - - #endregion - } - - /// - /// 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. - /// - public sealed class GetSeverityModifierEvent : EntityEventArgs - { - /// - /// Should be multiplied/added to rather than set, for commutativity. - /// - public float Modifier = 1.0f; - } + public float Modifier = 1.0f; } diff --git a/Content.Server/StationEvents/Events/VentClog.cs b/Content.Server/StationEvents/Events/VentClogRule.cs similarity index 73% rename from Content.Server/StationEvents/Events/VentClog.cs rename to Content.Server/StationEvents/Events/VentClogRule.cs index 04e466f66b..c209337aec 100644 --- a/Content.Server/StationEvents/Events/VentClog.cs +++ b/Content.Server/StationEvents/Events/VentClogRule.cs @@ -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 { - public override string Prototype => "VentClog"; + [Dependency] private readonly SmokeSystem _smoke = default!; - public readonly IReadOnlyList 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(foamEnt); smoke.SpreadAmount = 20; - EntityManager.System().Start(foamEnt, smoke, solution, 20f); - EntityManager.System().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 index 5d61e10c71..0000000000 --- a/Content.Server/StationEvents/Events/VentCritters.cs +++ /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 SpawnedPrototypeChoices = new List() - {"MobMouse", "MobMouse1", "MobMouse2"}; - - public override string Prototype => "VentCritters"; - - public override void Started() - { - base.Started(); - var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices); - var spawnLocations = EntityManager.EntityQuery().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(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 index 0000000000..696828838d --- /dev/null +++ b/Content.Server/StationEvents/Events/VentCrittersRule.cs @@ -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 +{ + 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().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); + } + } +} diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs index d22e2d86dc..5c972df52e 100644 --- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs @@ -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 { - 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(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(); + 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(); + 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 index 7c07cbd8df..0000000000 --- a/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs +++ /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(); - - 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 index f63cacb6e2..0000000000 --- a/Content.Server/Suspicion/Roles/SuspicionRole.cs +++ /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 index b42b22da42..0000000000 --- a/Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs +++ /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 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 index cca978d25f..0000000000 --- a/Content.Server/Suspicion/SuspicionItemComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Content.Server.Suspicion; - -/// -/// Tag component meant for bookkeeping items spawned by the suspicion rule. -/// -[RegisterComponent] -public sealed class SuspicionItemComponent : Component -{ -} diff --git a/Content.Server/Suspicion/SuspicionRoleComponent.cs b/Content.Server/Suspicion/SuspicionRoleComponent.cs deleted file mode 100644 index 59964254c8..0000000000 --- a/Content.Server/Suspicion/SuspicionRoleComponent.cs +++ /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 _allies = new(); - - [ViewVariables] - public Role? Role - { - get => _role; - set - { - if (_role == value) - { - return; - } - - _role = value; - - Dirty(); - - var sus = EntitySystem.Get(); - - 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().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 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 index d23269b95c..0000000000 --- a/Content.Server/Suspicion/SuspicionRoleSystem.cs +++ /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(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 index 9ea08c4204..0000000000 --- a/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs +++ /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 index 820ccbc425..0000000000 --- a/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs +++ /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 index 9def8fc316..0000000000 --- a/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs +++ /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(OnInteractUsing); - } - - private void OnInteractUsing(EntityUid uid, TraitorDeathMatchRedemptionComponent component, InteractUsingEvent args) - { - if (!EntityManager.TryGetComponent(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(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(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(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() { {"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 index 09d8a6cd59..0000000000 --- a/Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs +++ /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 index 7254e2d177..0000000000 --- a/Content.Shared/Suspicion/SuspicionMessages.cs +++ /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 index e4c804ad55..0000000000 --- a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml +++ /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 index 7c15a0989d..0000000000 --- a/Resources/Prototypes/Entities/Structures/Machines/traitordm.yml +++ /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 diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 8a2c7b71b3..5b54e1a60d 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -1,97 +1,116 @@ -- 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 @@ -100,23 +119,27 @@ 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 @@ -127,22 +150,26 @@ 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 @@ -151,28 +178,36 @@ 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 @@ -180,11 +215,12 @@ 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 @@ -192,44 +228,55 @@ 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 diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 5a62ca2706..0d493ba658 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -1,80 +1,88 @@ -- type: gameRule - id: DeathMatch - config: - !type:GenericGameRuleConfiguration - id: DeathMatch +- type: entity + id: BaseGameRule + abstract: true + noSpawn: true + components: + - type: GameRule -- type: gameRule +- type: entity + id: DeathMatch + parent: BaseGameRule + noSpawn: true + components: + - type: DeathMatchRule + +- 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 + parent: BaseGameRule + noSpawn: true + components: + - type: PiratesRule -- type: gameRule - id: Suspicion - config: - !type:GenericGameRuleConfiguration - id: Suspicion - -- type: gameRule +- type: entity id: Traitor - config: - !type:GenericGameRuleConfiguration - id: Traitor + parent: BaseGameRule + noSpawn: true + components: + - type: TraitorRule -- type: gameRule - id: TraitorDeathMatch - config: - !type:GenericGameRuleConfiguration - id: TraitorDeathMatch - -- 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 index 9efbb6f6ab..0000000000 --- a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml +++ /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 index 27e47cacb2..0000000000 --- a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml +++ /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 diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index f56452f20b..b6e2044761 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -52,16 +52,6 @@ - Traitor - BasicStationEventScheduler -- type: gamePreset - id: Suspicion - alias: - - suspicion - - sus - name: suspicion-title - description: suspicion-description - rules: - - Suspicion - - type: gamePreset id: Deathmatch alias: @@ -72,17 +62,6 @@ 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: