From d3552dae006e23a0927bcb67efd4ed7a4d053043 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Mon, 24 Apr 2023 16:21:05 +1000 Subject: [PATCH] Revert "Gamerule Entities" (#15724) --- .../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 | 13 +- .../Dragon/Components/DragonRuleComponent.cs | 7 - Content.Server/Dragon/DragonSystem.Rule.cs | 18 +- Content.Server/Dragon/DragonSystem.cs | 2 +- .../GameTicking/GameTicker.GamePreset.cs | 18 +- .../GameTicking/GameTicker.GameRule.cs | 465 +++++----- .../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 + .../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 + .../NukeopsRuleConfiguration.cs} | 106 +-- .../SolarFlareEventRuleConfiguration.cs} | 20 +- .../StationEventRuleConfiguration.cs} | 47 +- .../GameTicking/Rules/DeathMatchRuleSystem.cs | 74 +- .../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 | 355 ++++---- .../GameTicking/Rules/SandboxRuleSystem.cs | 12 +- .../GameTicking/Rules/SecretRuleSystem.cs | 27 +- .../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 | 57 +- .../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/SpiderSpawnRuleComponent.cs | 9 - .../Components/VentClogRuleComponent.cs | 14 - .../VentCritterSpawnLocationComponent.cs | 6 +- .../Components/VentCrittersRuleComponent.cs | 15 - .../StationEvents/EventManagerSystem.cs | 99 +-- .../{AnomalySpawnRule.cs => AnomalySpawn.cs} | 21 +- .../StationEvents/Events/BluespaceArtifact.cs | 49 ++ .../Events/BluespaceArtifactRule.cs | 34 - ...espaceLockerRule.cs => BluespaceLocker.cs} | 14 +- .../{BreakerFlipRule.cs => BreakerFlip.cs} | 18 +- ...raticErrorRule.cs => BureaucraticError.cs} | 10 +- ...easeOutbreakRule.cs => DiseaseOutbreak.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 -- .../{MeteorSwarmRule.cs => MeteorSwarm.cs} | 67 +- ...ouseMigrationRule.cs => MouseMigration.cs} | 16 +- .../StationEvents/Events/PowerGridCheck.cs | 122 +++ .../Events/PowerGridCheckRule.cs | 96 -- ...domSentienceRule.cs => RandomSentience.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 | 344 ++++---- .../Events/{VentClogRule.cs => VentClog.cs} | 23 +- .../StationEvents/Events/VentCritters.cs | 34 + .../StationEvents/Events/VentCrittersRule.cs | 29 - .../RampingStationEventSchedulerSystem.cs | 90 +- .../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 | 116 ++- .../Antags/Suspicion/suspicion_innocent.yml | 6 + .../Antags/Suspicion/suspicion_traitor.yml | 6 + Resources/Prototypes/game_presets.yml | 21 + 124 files changed, 4328 insertions(+), 3083 deletions(-) create mode 100644 Content.Client/Suspicion/SuspicionEndTimerSystem.cs create mode 100644 Content.Client/Suspicion/SuspicionGui.xaml create mode 100644 Content.Client/Suspicion/SuspicionGui.xaml.cs create mode 100644 Content.Client/Suspicion/SuspicionRoleComponent.cs create mode 100644 Content.Client/Suspicion/SuspicionRoleSystem.cs create mode 100644 Content.Client/Suspicion/TraitorOverlay.cs delete mode 100644 Content.Server/Dragon/Components/DragonRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs create mode 100644 Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs create mode 100644 Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs create mode 100644 Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs rename Content.Server/GameTicking/Rules/{Components/NukeopsRuleComponent.cs => Configurations/NukeopsRuleConfiguration.cs} (51%) rename Content.Server/{StationEvents/Components/SolarFlareRuleComponent.cs => GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs} (69%) rename Content.Server/{StationEvents/Components/StationEventComponent.cs => GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs} (58%) create mode 100644 Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs create mode 100644 Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs delete mode 100644 Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs delete mode 100644 Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/GasLeakRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs delete mode 100644 Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/VentClogRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs rename Content.Server/StationEvents/Events/{AnomalySpawnRule.cs => AnomalySpawn.cs} (61%) create mode 100644 Content.Server/StationEvents/Events/BluespaceArtifact.cs delete mode 100644 Content.Server/StationEvents/Events/BluespaceArtifactRule.cs rename Content.Server/StationEvents/Events/{BluespaceLockerRule.cs => BluespaceLocker.cs} (79%) rename Content.Server/StationEvents/Events/{BreakerFlipRule.cs => BreakerFlip.cs} (70%) rename Content.Server/StationEvents/Events/{BureaucraticErrorRule.cs => BureaucraticError.cs} (82%) rename Content.Server/StationEvents/Events/{DiseaseOutbreakRule.cs => DiseaseOutbreak.cs} (70%) create mode 100644 Content.Server/StationEvents/Events/FalseAlarm.cs delete mode 100644 Content.Server/StationEvents/Events/FalseAlarmRule.cs create mode 100644 Content.Server/StationEvents/Events/GasLeak.cs delete mode 100644 Content.Server/StationEvents/Events/GasLeakRule.cs create mode 100644 Content.Server/StationEvents/Events/KudzuGrowth.cs delete mode 100644 Content.Server/StationEvents/Events/KudzuGrowthRule.cs create mode 100644 Content.Server/StationEvents/Events/LoneOpsSpawn.cs delete mode 100644 Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs rename Content.Server/StationEvents/Events/{MeteorSwarmRule.cs => MeteorSwarm.cs} (53%) rename Content.Server/StationEvents/Events/{MouseMigrationRule.cs => MouseMigration.cs} (66%) create mode 100644 Content.Server/StationEvents/Events/PowerGridCheck.cs delete mode 100644 Content.Server/StationEvents/Events/PowerGridCheckRule.cs rename Content.Server/StationEvents/Events/{RandomSentienceRule.cs => RandomSentience.cs} (67%) create mode 100644 Content.Server/StationEvents/Events/RevenantSpawn.cs delete mode 100644 Content.Server/StationEvents/Events/RevenantSpawnRule.cs create mode 100644 Content.Server/StationEvents/Events/SolarFlare.cs delete mode 100644 Content.Server/StationEvents/Events/SolarFlareRule.cs create mode 100644 Content.Server/StationEvents/Events/SpiderSpawn.cs delete mode 100644 Content.Server/StationEvents/Events/SpiderSpawnRule.cs rename Content.Server/StationEvents/Events/{VentClogRule.cs => VentClog.cs} (73%) create mode 100644 Content.Server/StationEvents/Events/VentCritters.cs delete mode 100644 Content.Server/StationEvents/Events/VentCrittersRule.cs create mode 100644 Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs create mode 100644 Content.Server/Suspicion/Roles/SuspicionRole.cs create mode 100644 Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs create mode 100644 Content.Server/Suspicion/SuspicionItemComponent.cs create mode 100644 Content.Server/Suspicion/SuspicionRoleComponent.cs create mode 100644 Content.Server/Suspicion/SuspicionRoleSystem.cs create mode 100644 Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs create mode 100644 Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs create mode 100644 Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs create mode 100644 Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs create mode 100644 Content.Shared/Suspicion/SuspicionMessages.cs create mode 100644 Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml create mode 100644 Resources/Prototypes/Entities/Structures/Machines/traitordm.yml create mode 100644 Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml create mode 100644 Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml diff --git a/Content.Client/Suspicion/SuspicionEndTimerSystem.cs b/Content.Client/Suspicion/SuspicionEndTimerSystem.cs new file mode 100644 index 0000000000..a843f496d9 --- /dev/null +++ b/Content.Client/Suspicion/SuspicionEndTimerSystem.cs @@ -0,0 +1,23 @@ +using System; +using Content.Shared.Suspicion; +using Robust.Shared.GameObjects; + +namespace Content.Client.Suspicion +{ + public sealed class SuspicionEndTimerSystem : EntitySystem + { + public TimeSpan? EndTime { get; private set; } + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(RxTimerMessage); + } + + private void RxTimerMessage(SuspicionMessages.SetSuspicionEndTimerMessage ev) + { + EndTime = ev.EndTime; + } + } +} diff --git a/Content.Client/Suspicion/SuspicionGui.xaml b/Content.Client/Suspicion/SuspicionGui.xaml new file mode 100644 index 0000000000..8f519b2195 --- /dev/null +++ b/Content.Client/Suspicion/SuspicionGui.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/Content.Client/Suspicion/SuspicionGui.xaml.cs b/Content.Client/Suspicion/SuspicionGui.xaml.cs new file mode 100644 index 0000000000..13148754b3 --- /dev/null +++ b/Content.Client/Suspicion/SuspicionGui.xaml.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Content.Shared.Popups; +using Robust.Client.AutoGenerated; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Content.Client.Suspicion +{ + [GenerateTypedNameReferences] + public sealed partial class SuspicionGui : UIWidget + { + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private string? _previousRoleName; + private bool _previousAntagonist; + + public SuspicionGui() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + RoleButton.OnPressed += RoleButtonPressed; + RoleButton.MinSize = (200, 60); + } + + private void RoleButtonPressed(ButtonEventArgs obj) + { + if (!TryGetComponent(out var role)) + { + return; + } + + if (!role.Antagonist ?? false) + { + return; + } + + var allies = string.Join(", ", role.Allies.Select(tuple => tuple.name)); + + role.Owner.PopupMessage( + Loc.GetString( + "suspicion-ally-count-display", + ("allyCount", role.Allies.Count), + ("allyNames", allies) + ) + ); + } + + private bool TryGetComponent([NotNullWhen(true)] out SuspicionRoleComponent? suspicion) + { + suspicion = default; + if (_playerManager.LocalPlayer?.ControlledEntity == null) + { + return false; + } + + return _entManager.TryGetComponent(_playerManager.LocalPlayer.ControlledEntity, out suspicion); + } + + public void UpdateLabel() + { + if (!TryGetComponent(out var suspicion)) + { + Visible = false; + return; + } + + if (suspicion.Role == null || suspicion.Antagonist == null) + { + Visible = false; + return; + } + + var endTime = _entManager.System().EndTime; + if (endTime == null) + { + TimerLabel.Visible = false; + } + else + { + var diff = endTime.Value - _timing.CurTime; + if (diff < TimeSpan.Zero) + { + diff = TimeSpan.Zero; + } + TimerLabel.Visible = true; + TimerLabel.Text = $"{diff:mm\\:ss}"; + } + + if (_previousRoleName == suspicion.Role && _previousAntagonist == suspicion.Antagonist) + { + return; + } + + _previousRoleName = suspicion.Role; + _previousAntagonist = suspicion.Antagonist.Value; + + var buttonText = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(_previousRoleName); + buttonText = Loc.GetString(buttonText); + + RoleButton.Text = buttonText; + RoleButton.ModulateSelfOverride = _previousAntagonist ? Color.Red : Color.LimeGreen; + + Visible = true; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + UpdateLabel(); + } + } +} diff --git a/Content.Client/Suspicion/SuspicionRoleComponent.cs b/Content.Client/Suspicion/SuspicionRoleComponent.cs new file mode 100644 index 0000000000..d95ee7edf9 --- /dev/null +++ b/Content.Client/Suspicion/SuspicionRoleComponent.cs @@ -0,0 +1,128 @@ +using Content.Shared.Suspicion; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using static Robust.Client.UserInterface.Controls.LayoutContainer; + +namespace Content.Client.Suspicion +{ + [RegisterComponent] + public sealed class SuspicionRoleComponent : SharedSuspicionRoleComponent + { + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IUserInterfaceManager _ui = default!; + + private SuspicionGui? _gui; + private string? _role; + private bool? _antagonist; + private bool _overlayActive; + + public string? Role + { + get => _role; + set + { + if (_role == value) + { + return; + } + + _role = value; + _gui?.UpdateLabel(); + Dirty(); + } + } + + public bool? Antagonist + { + get => _antagonist; + set + { + if (_antagonist == value) + { + return; + } + + _antagonist = value; + _gui?.UpdateLabel(); + + if (value ?? false) + { + AddTraitorOverlay(); + } + + Dirty(); + } + } + + [ViewVariables] + public List<(string name, EntityUid uid)> Allies { get; } = new(); + + private void AddTraitorOverlay() + { + if (_overlayManager.HasOverlay()) + { + 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 new file mode 100644 index 0000000000..41cd186029 --- /dev/null +++ b/Content.Client/Suspicion/SuspicionRoleSystem.cs @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000000..84f52b9673 --- /dev/null +++ b/Content.Client/Suspicion/TraitorOverlay.cs @@ -0,0 +1,95 @@ +using Content.Shared.Examine; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; +using Robust.Shared.Containers; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; + +namespace Content.Client.Suspicion +{ + public sealed class TraitorOverlay : Overlay + { + private readonly IEntityManager _entityManager; + private readonly IPlayerManager _playerManager; + private readonly EntityLookupSystem _lookup; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + private readonly Font _font; + + private readonly string _traitorText = Loc.GetString("traitor-overlay-traitor-text"); + + public TraitorOverlay( + IEntityManager entityManager, + IPlayerManager playerManager, + IResourceCache resourceCache, + EntityLookupSystem lookup) + { + _playerManager = playerManager; + _entityManager = entityManager; + _lookup = lookup; + + _font = new VectorFont(resourceCache.GetResource("/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 d4f85486bc..4b23af60b2 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -3,11 +3,12 @@ using System.Threading.Tasks; using Content.Server.GameTicking; using Content.Server.GameTicking.Commands; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; using Content.Shared.CCVar; using NUnit.Framework; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.GameRules @@ -22,7 +23,6 @@ namespace Content.IntegrationTests.Tests.GameRules await using var pairTracker = await PoolManager.GetServerClient(); var server = pairTracker.Pair.Server; - var entityManager = server.ResolveDependency(); var configManager = server.ResolveDependency(); await server.WaitPost(() => { @@ -31,17 +31,18 @@ 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)); - maxTime.RoundMaxTime = TimeSpan.FromSeconds(3); + + sGameTicker.StartGameRule(IoCManager.Resolve().Index(maxTimeMaxTimeRestartRuleSystem.Prototype)); + maxTimeMaxTimeRestartRuleSystem.RoundMaxTime = TimeSpan.FromSeconds(3); + sGameTicker.StartRound(); }); @@ -50,7 +51,7 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); }); - var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTime.RoundMaxTime.TotalSeconds * 1.1f); + var ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundMaxTime.TotalSeconds * 1.1f); await PoolManager.RunTicksSync(pairTracker.Pair, ticks); await server.WaitAssertion(() => @@ -58,7 +59,7 @@ namespace Content.IntegrationTests.Tests.GameRules Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PostRound)); }); - ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTime.RoundEndDelay.TotalSeconds * 1.1f); + ticks = sGameTiming.TickRate * (int) Math.Ceiling(maxTimeMaxTimeRestartRuleSystem.RoundEndDelay.TotalSeconds * 1.1f); await PoolManager.RunTicksSync(pairTracker.Pair, ticks); await server.WaitAssertion(() => diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 9fa3fedeea..9a5ff23fad 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -1,8 +1,11 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; using NUnit.Framework; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.GameRules; @@ -23,11 +26,12 @@ public sealed class SecretStartsTest var server = pairTracker.Pair.Server; await server.WaitIdleAsync(); + var protoMan = server.ResolveDependency(); var gameTicker = server.ResolveDependency().GetEntitySystem(); await server.WaitAssertion(() => { - gameTicker.StartGameRule("Secret"); + gameTicker.StartGameRule(protoMan.Index("Secret")); }); // Wait three ticks for any random update loops that might happen @@ -35,9 +39,9 @@ public sealed class SecretStartsTest await server.WaitAssertion(() => { - foreach (var rule in gameTicker.GetAddedGameRules()) + foreach (var rule in gameTicker.AddedGameRules) { - Assert.That(gameTicker.GetActiveGameRules().Contains(rule)); + Assert.That(gameTicker.StartedGameRules.Contains(rule)); } // End all rules diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs index e17990315f..90164e4dbe 100644 --- a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs @@ -2,11 +2,14 @@ using System.Linq; using System.Threading.Tasks; using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; using NUnit.Framework; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.GameRules; + [TestFixture] public sealed class StartEndGameRulesTest { @@ -23,18 +26,22 @@ public sealed class StartEndGameRulesTest }); var server = pairTracker.Pair.Server; await server.WaitIdleAsync(); + var protoMan = server.ResolveDependency(); var gameTicker = server.ResolveDependency().GetEntitySystem(); await server.WaitAssertion(() => { - var rules = gameTicker.GetAllGameRulePrototypes().ToList(); + var rules = protoMan.EnumeratePrototypes().ToList(); rules.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal)); // Start all rules foreach (var rule in rules) { - gameTicker.StartGameRule(rule.ID); + gameTicker.StartGameRule(rule); } + + Assert.That(gameTicker.AddedGameRules, Has.Count.EqualTo(rules.Count)); + Assert.That(gameTicker.AddedGameRules, Has.Count.EqualTo(gameTicker.StartedGameRules.Count)); }); // Wait three ticks for any random update loops that might happen @@ -44,7 +51,7 @@ public sealed class StartEndGameRulesTest { // End all rules gameTicker.ClearGameRules(); - Assert.That(!gameTicker.GetAddedGameRules().Any()); + Assert.That(!gameTicker.AddedGameRules.Any()); }); await pairTracker.CleanReturnAsync(); diff --git a/Content.Server/Dragon/Components/DragonRuleComponent.cs b/Content.Server/Dragon/Components/DragonRuleComponent.cs deleted file mode 100644 index 8f358c31b0..0000000000 --- a/Content.Server/Dragon/Components/DragonRuleComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 0963f8c09b..4c86634124 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.GameTicking.Rules.Components; +using Content.Server.StationEvents.Components; using Content.Shared.Dragon; using Robust.Server.GameObjects; using Robust.Shared.Map.Components; @@ -10,6 +10,8 @@ namespace Content.Server.Dragon; public sealed partial class DragonSystem { + public override string Prototype => "Dragon"; + private int RiftsMet(DragonComponent component) { var finished = 0; @@ -26,11 +28,9 @@ public sealed partial class DragonSystem return finished; } - protected override void Started(EntityUid uid, DragonRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); - - var spawnLocations = EntityQuery().ToList(); + var spawnLocations = EntityManager.EntityQuery().ToList(); if (spawnLocations.Count == 0) return; @@ -39,8 +39,16 @@ public sealed partial class DragonSystem Spawn("MobDragon", location.Item2.MapPosition); } + public override void Ended() + { + return; + } + private void OnRiftRoundEnd(RoundEndTextAppendEvent args) { + if (!RuleAdded) + return; + var dragons = EntityQuery(true).ToList(); if (dragons.Count == 0) diff --git a/Content.Server/Dragon/DragonSystem.cs b/Content.Server/Dragon/DragonSystem.cs index f2f0e7ba99..d969272ab9 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 d2c5005e17..eb44943e76 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -1,7 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; +using Content.Server.GameTicking.Events; using Content.Server.GameTicking.Presets; +using Content.Server.GameTicking.Rules; using Content.Server.Ghost.Components; using Content.Shared.CCVar; using Content.Shared.Damage; @@ -9,7 +11,6 @@ using Content.Shared.Damage.Prototypes; using Content.Shared.Database; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using JetBrains.Annotations; using Robust.Server.Player; namespace Content.Server.GameTicking @@ -42,6 +43,7 @@ namespace Content.Server.GameTicking if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled)) { + var oldPreset = Preset; ClearGameRules(); SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset)); AddGamePresetRules(); @@ -123,7 +125,6 @@ namespace Content.Server.GameTicking return prototype != null; } - [PublicAPI] private bool AddGamePresetRules() { if (DummyTicker || Preset == null) @@ -131,7 +132,10 @@ namespace Content.Server.GameTicking foreach (var rule in Preset.Rules) { - AddGameRule(rule); + if (!_prototypeManager.TryIndex(rule, out GameRulePrototype? ruleProto)) + continue; + + AddGameRule(ruleProto); } return true; @@ -140,7 +144,7 @@ namespace Content.Server.GameTicking private void StartGamePresetRules() { // May be touched by the preset during init. - foreach (var rule in GetAddedGameRules()) + foreach (var rule in _addedGameRules.ToArray()) { StartGameRule(rule); } @@ -162,12 +166,10 @@ namespace Content.Server.GameTicking if (mind.PreventGhosting) { - if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts - { + if (mind.Session != null) + // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts _chatManager.DispatchServerMessage(mind.Session, Loc.GetString("comp-mind-ghosting-prevented"), true); - } - return false; } diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 158e6b0085..c0b382af2d 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -1,303 +1,244 @@ using System.Linq; using Content.Server.Administration; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Configurations; using Content.Shared.Administration; -using Content.Shared.Prototypes; -using JetBrains.Annotations; using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; -namespace Content.Server.GameTicking; - -public sealed partial class GameTicker +namespace Content.Server.GameTicking { - [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new(); - - /// - /// 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() + public sealed partial class GameTicker { - // Add game rule command. - _consoleHost.RegisterCommand("addgamerule", - string.Empty, - "addgamerule ", - AddGameRuleCommand, - AddGameRuleCompletions); + // No duplicates. + [ViewVariables] private readonly HashSet _addedGameRules = new(); - // End game rule command. - _consoleHost.RegisterCommand("endgamerule", - string.Empty, - "endgamerule ", - EndGameRuleCommand, - EndGameRuleCompletions); + /// + /// Holds all currently added game rules. + /// + public IReadOnlySet AddedGameRules => _addedGameRules; - // Clear game rules command. - _consoleHost.RegisterCommand("cleargamerules", - string.Empty, - "cleargamerules", - ClearGameRulesCommand); - } + [ViewVariables] private readonly HashSet _startedGameRules = new(); - private void ShutdownGameRules() - { - _consoleHost.UnregisterCommand("addgamerule"); - _consoleHost.UnregisterCommand("endgamerule"); - _consoleHost.UnregisterCommand("cleargamerules"); - } + /// + /// Holds all currently started game rules. + /// + public IReadOnlySet StartedGameRules => _startedGameRules; - /// - /// 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) - /// - /// The entity for the added gamerule - public EntityUid AddGameRule(string ruleId) - { - var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace); - _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}"); + [ViewVariables] private readonly List<(TimeSpan, GameRulePrototype)> _allPreviousGameRules = new(); - var ev = new GameRuleAddedEvent(ruleEntity, ruleId); - RaiseLocalEvent(ruleEntity, ref ev, true); - return ruleEntity; - } + /// + /// 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; - /// - /// 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 _); - } + private void InitializeGameRules() + { + // Add game rule command. + _consoleHost.RegisterCommand("addgamerule", + string.Empty, + "addgamerule ", + AddGameRuleCommand, + AddGameRuleCompletions); - /// - /// 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); - } + // End game rule command. + _consoleHost.RegisterCommand("endgamerule", + string.Empty, + "endgamerule ", + EndGameRuleCommand, + EndGameRuleCompletions); - /// - /// 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); + // 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; + } - // can't start an already active rule - if (ruleData.Active || ruleData.Ended) 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; + } - 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)}"); + public void ClearGameRules() + { + foreach (var rule in _addedGameRules.ToArray()) + { + EndGameRule(rule); + } + } - ruleData.Active = true; - var ev = new GameRuleStartedEvent(ruleEntity, id); - RaiseLocalEvent(ruleEntity, ref ev, true); - return true; + #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 } /// - /// Ends a game rule. + /// Raised broadcast when a game rule is selected, but not started yet. /// - [PublicAPI] - public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) + public sealed class GameRuleAddedEvent { - if (!Resolve(ruleEntity, ref ruleData)) - return false; + public GameRulePrototype Rule { get; } - // 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()) + public GameRuleAddedEvent(GameRulePrototype 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); + Rule = rule; } } - /// - /// Gets all the gamerule entities which are currently active. - /// - public IEnumerable GetAddedGameRules() + public sealed class GameRuleStartedEvent { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var ruleData)) + public GameRulePrototype Rule { get; } + + public GameRuleStartedEvent(GameRulePrototype rule) { - if (IsGameRuleAdded(uid, ruleData)) - yield return uid; + Rule = rule; } } - /// - /// Gets all the gamerule entities which are currently active. - /// - public IEnumerable GetActiveGameRules() + public sealed class GameRuleEndedEvent { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var ruleData)) + public GameRulePrototype Rule { get; } + + public GameRuleEndedEvent(GameRulePrototype rule) { - if (ruleData.Active) - yield return uid; + Rule = rule; } } - - /// - /// 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 096d508f35..e64837ca9f 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -456,6 +456,7 @@ namespace Content.Server.GameTicking // Clear up any game rules. ClearGameRules(); + _addedGameRules.Clear(); _allPreviousGameRules.Clear(); // Round restart cleanup event, so entity systems can reset. diff --git a/Content.Server/GameTicking/Presets/GamePresetPrototype.cs b/Content.Server/GameTicking/Presets/GamePresetPrototype.cs index ff6a3d17ba..e131030251 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 readonly string[] Alias = Array.Empty(); + public string[] Alias { get; } = Array.Empty(); [DataField("name")] - public readonly string ModeTitle = "????"; + public string ModeTitle { get; } = "????"; [DataField("description")] - public readonly string Description = string.Empty; + public string Description { get; } = string.Empty; [DataField("showInVote")] - public readonly bool ShowInVote; + public bool ShowInVote { get; } = false; [DataField("minPlayers")] - public readonly int? MinPlayers; + public int? MinPlayers { get; } = null; [DataField("maxPlayers")] - public readonly int? MaxPlayers; + public int? MaxPlayers { get; } = null; - [DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer))] + [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 deleted file mode 100644 index 7a11eb7b56..0000000000 --- a/Content.Server/GameTicking/Rules/Components/DeathMatchRuleComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 640b6cb839..0000000000 --- a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index ae9657a300..0000000000 --- a/Content.Server/GameTicking/Rules/Components/InactivityRuleComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 45d9d38740..0000000000 --- a/Content.Server/GameTicking/Rules/Components/MaxTimeRestartRuleComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 c32a8569cb..198db1f912 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs @@ -6,6 +6,7 @@ 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/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs deleted file mode 100644 index 7cb748c54d..0000000000 --- a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 6f79b892c5..0000000000 --- a/Content.Server/GameTicking/Rules/Components/SandboxRuleComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.GameTicking.Rules.Components; - -[RegisterComponent, Access(typeof(SandboxRuleSystem))] -public sealed class SandboxRuleComponent : Component -{ - -} diff --git a/Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs deleted file mode 100644 index f6a67267a3..0000000000 --- a/Content.Server/GameTicking/Rules/Components/SecretRuleComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 38f992144a..0000000000 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Server.Traitor; -using Content.Shared.Preferences; -using Content.Shared.Roles; -using Robust.Server.Player; -using Robust.Shared.Audio; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.GameTicking.Rules.Components; - -[RegisterComponent, Access(typeof(TraitorRuleSystem))] -public sealed class TraitorRuleComponent : Component -{ - public readonly SoundSpecifier AddedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); - public List 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 deleted file mode 100644 index f0b3b0ca8c..0000000000 --- a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 new file mode 100644 index 0000000000..600cc9d54e --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/GameRuleConfiguration.cs @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..1db383c169 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/GenericGameRuleConfiguration.cs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000000..bc60d77adb --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/InactivityGameRuleConfiguration.cs @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000000..d4cd3f39b0 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/MaxTimeRestartRuleConfiguration.cs @@ -0,0 +1,17 @@ +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/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs similarity index 51% rename from Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs rename to Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs index 3df94d9eed..09ccd7ad31 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Configurations/NukeopsRuleConfiguration.cs @@ -1,22 +1,19 @@ -using Content.Server.StationEvents.Events; +using Content.Server.GameTicking.Rules.Configurations; using Content.Shared.Dataset; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Roles; -using Robust.Server.Player; using Robust.Shared.Audio; -using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Utility; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Rules.Configurations; -[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))] -public sealed class NukeopsRuleComponent : Component +public sealed class NukeopsRuleConfiguration : GameRuleConfiguration { - /// - /// The minimum needed amount of players - /// + public override string Id => "Nukeops"; + [DataField("minPlayers")] public int MinPlayers = 15; @@ -41,6 +38,15 @@ public sealed class NukeopsRuleComponent : Component [DataField("spawnOutpost")] public bool SpawnOutpost = true; + /// + /// 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"; @@ -76,86 +82,4 @@ public sealed class NukeopsRuleComponent : Component [DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))] public SoundSpecifier? GreetSound = new SoundPathSpecifier("/Audio/Misc/nukeops.ogg"); - - [DataField("winType")] - public WinType WinType = WinType.Neutral; - - [DataField("winConditions")] - public List 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/StationEvents/Components/SolarFlareRuleComponent.cs b/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs similarity index 69% rename from Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs rename to Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs index 92a3b43375..f014c56280 100644 --- a/Content.Server/StationEvents/Components/SolarFlareRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs @@ -1,15 +1,25 @@ -using Content.Server.StationEvents.Events; using Content.Shared.Radio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; -namespace Content.Server.StationEvents.Components; +namespace Content.Server.GameTicking.Rules.Configurations; /// /// Solar Flare event specific configuration /// -[RegisterComponent, Access(typeof(SolarFlareRule))] -public sealed class SolarFlareRuleComponent : Component +public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfiguration { + /// + /// 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 /// @@ -33,4 +43,4 @@ public sealed class SolarFlareRuleComponent : Component /// [DataField("doorToggleChancePerSecond")] public float DoorToggleChancePerSecond; -} +} \ No newline at end of file diff --git a/Content.Server/StationEvents/Components/StationEventComponent.cs b/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs similarity index 58% rename from Content.Server/StationEvents/Components/StationEventComponent.cs rename to Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs index e79fd6e86d..84689dc225 100644 --- a/Content.Server/StationEvents/Components/StationEventComponent.cs +++ b/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs @@ -1,14 +1,19 @@ -using Robust.Shared.Audio; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using JetBrains.Annotations; +using Robust.Shared.Audio; -namespace Content.Server.StationEvents.Components; +namespace Content.Server.GameTicking.Rules.Configurations; /// -/// Defines basic data for a station event +/// Defines a configuration for a given station event game rule, since all station events are just +/// game rules. /// -[RegisterComponent] -public sealed class StationEventComponent : Component +[UsedImplicitly] +public class StationEventRuleConfiguration : GameRuleConfiguration { + [DataField("id", required: true)] + private string _id = default!; + public override string Id => _id; + public const float WeightVeryLow = 0.0f; public const float WeightLow = 5.0f; public const float WeightNormal = 10.0f; @@ -43,22 +48,16 @@ public sealed class StationEventComponent : Component public int ReoccurrenceDelay = 30; /// - /// How long after being added does the event start + /// When in the lifetime to start the event. /// - [DataField("startDelay")] - public TimeSpan StartDelay = TimeSpan.Zero; + [DataField("startAfter")] + public float StartAfter; /// - /// How long the event lasts. + /// When in the lifetime to end the event.. /// - [DataField("duration")] - public TimeSpan Duration = TimeSpan.FromSeconds(1); - - /// - /// The max amount of time the event lasts. - /// - [DataField("maxDuration")] - public TimeSpan? MaxDuration; + [DataField("endAfter")] + public float EndAfter = float.MaxValue; /// /// How many players need to be present on station for the event to run @@ -74,16 +73,4 @@ public sealed class StationEventComponent : Component /// [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/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index 79c3394d11..c4c61480db 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.Components; +using Content.Server.GameTicking.Rules.Configurations; using Content.Shared.CCVar; using Content.Shared.Damage; using Content.Shared.Mobs.Components; @@ -11,42 +11,44 @@ using Robust.Shared.Enums; namespace Content.Server.GameTicking.Rules; /// -/// Manages +/// Simple GameRule that will do a free-for-all death match. +/// Kill everybody else to win. /// -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); - _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } - public override void Shutdown() - { - base.Shutdown(); - _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; - } - - protected override void Started(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement")); + _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } - protected override void Ended(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + public override void Ended() { - base.Ended(uid, component, gameRule, args); - - component.DeadCheckTimer = null; - component.RestartTimer = null; + _deadCheckTimer = null; + _restartTimer = null; + _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged; } private void OnHealthChanged(DamageChangedEvent _) @@ -54,7 +56,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem(); - while (query.MoveNext(out var uid, out var deathMatch, out var gameRule)) - { - if (!GameTicker.IsGameRuleActive(uid, gameRule) || deathMatch.DeadCheckTimer != null) - continue; + if (!RuleAdded || _deadCheckTimer != null) + return; - deathMatch.DeadCheckTimer = deathMatch.DeadCheckDelay; - } + _deadCheckTimer = DeadCheckDelay; } - protected override void ActiveTick(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, float frameTime) + public override void Update(float frameTime) { - base.ActiveTick(uid, component, gameRule, frameTime); + if (!RuleAdded) + return; // If the restart timer is active, that means the round is ending soon, no need to check for winners. // TODO: We probably want a sane, centralized round end thingie in GameTicker, RoundEndSystem is no good... - if (component.RestartTimer != null) + if (_restartTimer != null) { - component.RestartTimer -= frameTime; + _restartTimer -= frameTime; - if (component.RestartTimer > 0f) + if (_restartTimer > 0f) return; GameTicker.EndRound(); @@ -92,20 +91,20 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem 0) + if (_deadCheckTimer > 0) return; - component.DeadCheckTimer = null; + _deadCheckTimer = null; IPlayerSession? winner = null; foreach (var playerSession in _playerManager.ServerSessions) { - if (playerSession.AttachedEntity is not { Valid: true } playerEntity + if (playerSession.AttachedEntity is not {Valid: true} playerEntity || !TryComp(playerEntity, out MobStateComponent? state)) continue; @@ -121,10 +120,9 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem : EntitySystem where T : Component +[PublicAPI] +public abstract class GameRuleSystem : EntitySystem { [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(EntityUid uid, T component, ref GameRuleAddedEvent args) + private void OnGameRuleAdded(GameRuleAddedEvent ev) { - if (!TryComp(uid, out var ruleData)) + if (ev.Rule.Configuration.Id != Prototype) return; - Added(uid, component, ruleData, args); + + Configuration = ev.Rule.Configuration; + RuleAdded = true; + + Added(); } - private void OnGameRuleStarted(EntityUid uid, T component, ref GameRuleStartedEvent args) + private void OnGameRuleStarted(GameRuleStartedEvent ev) { - if (!TryComp(uid, out var ruleData)) + if (ev.Rule.Configuration.Id != Prototype) return; - Started(uid, component, ruleData, args); + + RuleStarted = true; + + Started(); } - private void OnGameRuleEnded(EntityUid uid, T component, ref GameRuleEndedEvent args) + private void OnGameRuleEnded(GameRuleEndedEvent ev) { - if (!TryComp(uid, out var ruleData)) + if (ev.Rule.Configuration.Id != Prototype) return; - Ended(uid, component, ruleData, args); + + RuleAdded = false; + RuleStarted = false; + Ended(); } /// - /// Called when the gamerule is added + /// Called when the game rule has been added. + /// You should avoid using this in favor of started--they are not the same thing. /// - protected virtual void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args) - { - - } + /// + /// This is virtual because it doesn't actually have to be used, and most of the time shouldn't be. + /// + public virtual void Added() { } /// - /// Called when the gamerule begins + /// Called when the game rule has been started. /// - protected virtual void Started(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - - } + public abstract void Started(); /// - /// Called when the gamerule ends + /// Called when the game rule has 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); - } - } + public abstract void Ended(); } diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs index c2e91ba4a5..d61b93d450 100644 --- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs @@ -1,109 +1,98 @@ using System.Threading; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules.Configurations; using Robust.Server.Player; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.GameTicking.Rules; -public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem +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 Shutdown() + public override void Ended() { - base.Shutdown(); _playerManager.PlayerStatusChanged -= PlayerStatusChanged; + + StopTimer(); } - protected override void Ended(EntityUid uid, InactivityRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + public void RestartTimer() { - base.Ended(uid, component, gameRule, args); - - StopTimer(uid, component); + _timerCancel.Cancel(); + _timerCancel = new CancellationTokenSource(); + Timer.Spawn(InactivityMaxTime, TimerFired, _timerCancel.Token); } - public void RestartTimer(EntityUid uid, InactivityRuleComponent? component = null) + public void StopTimer() { - if (!Resolve(uid, ref component)) - return; - - component.TimerCancel.Cancel(); - component.TimerCancel = new CancellationTokenSource(); - Timer.Spawn(component.InactivityMaxTime, () => TimerFired(uid, component), component.TimerCancel.Token); + _timerCancel.Cancel(); } - public void StopTimer(EntityUid uid, InactivityRuleComponent? component = null) + private void TimerFired() { - 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) component.RoundEndDelay.TotalSeconds))); + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds",(int) RoundEndDelay.TotalSeconds))); - Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound()); + Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound()); } private void RunLevelChanged(GameRunLevelChangedEvent args) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var inactivity, out var gameRule)) - { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; + if (!RuleAdded) + return; - switch (args.New) - { - case GameRunLevel.InRound: - RestartTimer(uid, inactivity); - break; - case GameRunLevel.PreRoundLobby: - case GameRunLevel.PostRound: - StopTimer(uid, inactivity); - break; - } + switch (args.New) + { + case GameRunLevel.InRound: + RestartTimer(); + break; + case GameRunLevel.PreRoundLobby: + case GameRunLevel.PostRound: + StopTimer(); + break; } } private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var inactivity, out var gameRule)) + if (GameTicker.RunLevel != GameRunLevel.InRound) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; + return; + } - if (GameTicker.RunLevel != GameRunLevel.InRound) - { - return; - } - - if (_playerManager.PlayerCount == 0) - { - RestartTimer(uid, inactivity); - } - else - { - StopTimer(uid, inactivity); - } + if (_playerManager.PlayerCount == 0) + { + RestartTimer(); + } + else + { + StopTimer(); } } } diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs index e792a004df..6b2a5805c6 100644 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -1,14 +1,21 @@ using System.Threading; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules.Configurations; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.GameTicking.Rules; -public sealed class MaxTimeRestartRuleSystem : GameRuleSystem +public sealed class MaxTimeRestartRuleSystem : GameRuleSystem { [Dependency] private readonly IChatManager _chatManager = default!; + public override string Prototype => "MaxTimeRestart"; + + private CancellationTokenSource _timerCancel = new(); + + public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromMinutes(5); + public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10); + public override void Initialize() { base.Initialize(); @@ -16,60 +23,58 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem(RunLevelChanged); } - protected override void Started(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); + if (Configuration is not MaxTimeRestartRuleConfiguration maxTimeRestartConfig) + return; + + RoundMaxTime = maxTimeRestartConfig.RoundMaxTime; + RoundEndDelay = maxTimeRestartConfig.RoundEndDelay; if(GameTicker.RunLevel == GameRunLevel.InRound) - RestartTimer(component); + RestartTimer(); } - protected override void Ended(EntityUid uid, MaxTimeRestartRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + public override void Ended() { - base.Ended(uid, component, gameRule, args); - - StopTimer(component); + StopTimer(); } - public void RestartTimer(MaxTimeRestartRuleComponent component) + public void RestartTimer() { - component.TimerCancel.Cancel(); - component.TimerCancel = new CancellationTokenSource(); - Timer.Spawn(component.RoundMaxTime, () => TimerFired(component), component.TimerCancel.Token); + _timerCancel.Cancel(); + _timerCancel = new CancellationTokenSource(); + Timer.Spawn(RoundMaxTime, TimerFired, _timerCancel.Token); } - public void StopTimer(MaxTimeRestartRuleComponent component) + public void StopTimer() { - component.TimerCancel.Cancel(); + _timerCancel.Cancel(); } - private void TimerFired(MaxTimeRestartRuleComponent component) + private void TimerFired() { GameTicker.EndRound(Loc.GetString("rule-time-has-run-out")); - _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) component.RoundEndDelay.TotalSeconds))); + _chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) RoundEndDelay.TotalSeconds))); - Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound()); + Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound()); } private void RunLevelChanged(GameRunLevelChangedEvent args) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var timer, out var gameRule)) - { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; + if (!RuleAdded) + return; - switch (args.New) - { - case GameRunLevel.InRound: - RestartTimer(timer); - break; - case GameRunLevel.PreRoundLobby: - case GameRunLevel.PostRound: - StopTimer(timer); - break; - } + switch (args.New) + { + case GameRunLevel.InRound: + RestartTimer(); + break; + case GameRunLevel.PreRoundLobby: + case GameRunLevel.PostRound: + StopTimer(); + break; } } } diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 2d372cf3d5..873e3c90dc 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Administration.Commands; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules.Configurations; using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; using Content.Server.Humanoid; @@ -17,7 +18,6 @@ using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.Traitor; using Content.Shared.Dataset; -using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; @@ -35,7 +35,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking.Rules; -public sealed class NukeopsRuleSystem : GameRuleSystem +public sealed class NukeopsRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -50,9 +50,104 @@ public sealed class NukeopsRuleSystem : GameRuleSystem [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(); @@ -72,21 +167,14 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnComponentInit(EntityUid uid, NukeOperativeComponent component, ComponentInit args) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var ruleEnt, out var nukeops, out var gameRule)) - { - if (!GameTicker.IsGameRuleAdded(ruleEnt, gameRule)) - continue; + // If entity has a prior mind attached, add them to the players list. + if (!TryComp(uid, out var mindComponent) || !RuleAdded) + return; - // 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); - } + var session = mindComponent.Mind?.Session; + var name = MetaData(uid).EntityName; + if (session != null) + _operativePlayers.Add(name, session); } private void OnComponentRemove(EntityUid uid, NukeOperativeComponent component, ComponentRemove args) @@ -96,138 +184,137 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnNukeExploded(NukeExplodedEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var nukeops, out var gameRule)) + if (!RuleAdded) + return; + + if (ev.OwningStation != null) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; - - if (ev.OwningStation != null) + if (ev.OwningStation == _nukieOutpost) { - if (ev.OwningStation == nukeops.NukieOutpost) - { - nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost); - SetWinType(uid, WinType.CrewMajor, nukeops); - continue; - } + _winConditions.Add(WinCondition.NukeExplodedOnNukieOutpost); + RuleWinType = WinType.CrewMajor; + return; + } - if (TryComp(nukeops.TargetStation, out StationDataComponent? data)) + if (TryComp(_targetStation, out StationDataComponent? data)) + { + foreach (var grid in data.Grids) { - var correctStation = false; - foreach (var grid in data.Grids) + if (grid != ev.OwningStation) { - if (grid != ev.OwningStation) - { - continue; - } - - nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation); - SetWinType(uid, WinType.OpsMajor, nukeops); - correctStation = true; + continue; } - if (correctStation) - continue; + _winConditions.Add(WinCondition.NukeExplodedOnCorrectStation); + RuleWinType = WinType.OpsMajor; + return; } - - nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation); - } - else - { - nukeops.WinConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation); } - _roundEndSystem.EndRound(); + _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation); } + else + { + _winConditions.Add(WinCondition.NukeExplodedOnIncorrectLocation); + } + + _roundEndSystem.EndRound(); } private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var nukeops)) + switch (ev.New) { - switch (ev.New) - { - case GameRunLevel.InRound: - OnRoundStart(uid, nukeops); - break; - case GameRunLevel.PostRound: - OnRoundEnd(uid, nukeops); - break; - } + case GameRunLevel.InRound: + OnRoundStart(); + break; + case GameRunLevel.PostRound: + OnRoundEnd(); + break; } } - /// - /// Loneops can only spawn if there is no nukeops active - /// - public bool CheckLoneOpsSpawn() + public void LoadLoneOpsConfig() { - return !EntityQuery().Any(); + _nukeopsRuleConfig.SpawnOutpost = false; + _nukeopsRuleConfig.EndsRound = false; } - private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null) + public bool CheckLoneOpsSpawn() { - if (!Resolve(uid, ref component)) - return; + return _nukeopsRuleConfig.CanLoneOpsSpawn; + } + private void OnRoundStart() + { // TODO: This needs to try and target a Nanotrasen station. At the very least, // we can only currently guarantee that NT stations are the only station to // exist in the base game. - component.TargetStation = _stationSystem.Stations.FirstOrNull(); + _targetStation = _stationSystem.Stations.FirstOrNull(); - if (component.TargetStation == null) + if (_targetStation == null) { return; } var filter = Filter.Empty(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out _, out var actor)) + foreach (var nukie in EntityQuery()) { - _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", component.TargetStation.Value))); + if (!TryComp(nukie.Owner, out var actor)) + { + continue; + } + + _chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("nukeops-welcome", ("station", _targetStation.Value))); filter.AddPlayer(actor.PlayerSession); } - _audioSystem.PlayGlobal(component.GreetSound, filter, recordReplay: false); + _audioSystem.PlayGlobal(_nukeopsRuleConfig.GreetSound, filter, recordReplay: false); } - private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null) + private void OnRoundEnd() { - if (!Resolve(uid, ref component)) - return; - // If the win condition was set to operative/crew major win, ignore. - if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor) + if (RuleWinType == WinType.OpsMajor || RuleWinType == WinType.CrewMajor) + { return; + } - foreach (var (nuke, nukeTransform) in EntityQuery(true)) + foreach (var (nuke, nukeTransform) in EntityManager.EntityQuery(true)) { if (nuke.Status != NukeStatus.ARMED) + { continue; + } // UH OH if (nukeTransform.MapID == _emergency.CentComMap) { - component.WinConditions.Add(WinCondition.NukeActiveAtCentCom); - SetWinType(uid, WinType.OpsMajor, component); + _winConditions.Add(WinCondition.NukeActiveAtCentCom); + RuleWinType = WinType.OpsMajor; return; } - if (nukeTransform.GridUid == null || component.TargetStation == null) + if (nukeTransform.GridUid == null || _targetStation == null) + { continue; + } - if (!TryComp(component.TargetStation.Value, out StationDataComponent? data)) + if (!TryComp(_targetStation.Value, out StationDataComponent? data)) + { continue; + } foreach (var grid in data.Grids) { if (grid != nukeTransform.GridUid) + { continue; + } - component.WinConditions.Add(WinCondition.NukeActiveInStation); - SetWinType(uid, WinType.OpsMajor, component); + _winConditions.Add(WinCondition.NukeActiveInStation); + RuleWinType = WinType.OpsMajor; return; } } @@ -236,7 +323,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem foreach (var (_, state) in EntityQuery()) { if (state.CurrentState is MobState.Alive) + { continue; + } allAlive = false; break; @@ -246,12 +335,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // running away the moment nuke ops appear. if (allAlive) { - SetWinType(uid, WinType.OpsMinor, component); - component.WinConditions.Add(WinCondition.AllNukiesAlive); + RuleWinType = WinType.OpsMinor; + _winConditions.Add(WinCondition.AllNukiesAlive); return; } - component.WinConditions.Add(WinCondition.SomeNukiesAlive); + _winConditions.Add(WinCondition.SomeNukiesAlive); var diskAtCentCom = false; foreach (var (_, transform) in EntityManager.EntityQuery()) @@ -268,115 +357,98 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // This also implies that some nuclear operatives have died. if (diskAtCentCom) { - SetWinType(uid, WinType.CrewMinor, component); - component.WinConditions.Add(WinCondition.NukeDiskOnCentCom); + RuleWinType = WinType.CrewMinor; + _winConditions.Add(WinCondition.NukeDiskOnCentCom); } // Otherwise, the nuke ops win. else { - SetWinType(uid, WinType.OpsMinor, component); - component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom); + RuleWinType = WinType.OpsMinor; + _winConditions.Add(WinCondition.NukeDiskNotOnCentCom); } } private void OnRoundEndText(RoundEndTextAppendEvent ev) { - foreach (var nukeops in EntityQuery()) - { - 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)) + if (!RuleAdded) return; - component.WinType = type; + var winText = Loc.GetString($"nukeops-{_winType.ToString().ToLower()}"); - if (type == WinType.CrewMajor || type == WinType.OpsMajor) - _roundEndSystem.EndRound(); + 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); + } } private void CheckRoundShouldEnd() { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var nukeops, out var gameRule)) + 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()) { - 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()) + if (nuke.Status == NukeStatus.ARMED) { - if (nuke.Status == NukeStatus.ARMED) - { - armed = true; - break; - } + return; } - if (armed) - continue; - - MapId? shuttleMapId = Exists(nukeops.NukieShuttle) - ? Transform(nukeops.NukieShuttle.Value).MapID - : null; - - MapId? targetStationMap = null; - if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data)) - { - var grid = data.Grids.FirstOrNull(); - targetStationMap = grid != null - ? Transform(grid.Value).MapID - : null; - } - - // Check if there are nuke operatives still alive on the same map as the shuttle, - // or on the same map as the station. - // If there are, the round can continue. - var operatives = EntityQuery(true); - var operativesAlive = operatives - .Where(ent => - ent.Item3.MapID == shuttleMapId - || ent.Item3.MapID == targetStationMap) - .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running); - - if (operativesAlive) - continue; // There are living operatives than can access the shuttle, or are still on the station's map. - - // Check that there are spawns available and that they can access the shuttle. - var spawnsAvailable = EntityQuery(true).Any(); - if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet) - continue; // Ghost spawns can still access the shuttle. Continue the round. - - // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, - // and there are no nuclear operatives on the target station's map. - nukeops.WinConditions.Add(spawnsAvailable - ? WinCondition.NukiesAbandoned - : WinCondition.AllNukiesDead); - - SetWinType(uid, WinType.CrewMajor, nukeops); } + + 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 + : 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) + 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) @@ -392,124 +464,111 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnPlayersSpawning(RulePlayerSpawningEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var nukeops, out var gameRule)) + if (!RuleAdded) + return; + + if (!SpawnMap()) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; + Logger.InfoS("nukies", "Failed to load map for nukeops"); + return; + } - if (!SpawnMap(uid, nukeops)) + // 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)) { - Logger.InfoS("nukies", "Failed to load map for nukeops"); continue; } - - // Basically copied verbatim from traitor code - var playersPerOperative = nukeops.PlayersPerOperative; - var maxOperatives = nukeops.MaxOperatives; - - var everyone = new List(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) + var profile = ev.Profiles[player.UserId]; + if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.OperativeRoleProto)) { - if (!ev.Profiles.ContainsKey(player.UserId)) - { - continue; - } - - var profile = ev.Profiles[player.UserId]; - if (profile.AntagPreferences.Contains(nukeops.OperativeRoleProto)) - { - prefList.Add(player); - } - - if (profile.AntagPreferences.Contains(nukeops.CommanderRolePrototype)) - { - cmdrPrefList.Add(player); - } + prefList.Add(player); } - - var numNukies = MathHelper.Clamp(ev.PlayerPool.Count / playersPerOperative, 1, maxOperatives); - - for (var i = 0; i < numNukies; i++) + if (profile.AntagPreferences.Contains(_nukeopsRuleConfig.CommanderRolePrototype)) { - 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; - } + cmdrPrefList.Add(player); + } + } - 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 + 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"); + Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection"); break; } - nukeOp = _random.PickAndTake(everyone); - Logger.InfoS("preset", "Insufficient preferred nukeops, picking at random."); + Logger.InfoS("preset", "Insufficient preferred nukeop commanders or nukies, picking at random."); } else { nukeOp = _random.PickAndTake(prefList); everyone.Remove(nukeOp); - Logger.InfoS("preset", "Selected a preferred nukeop."); + Logger.InfoS("preset", "Insufficient preferred nukeop commanders, picking at random from regular op list."); } } - - operatives.Add(nukeOp); + else + { + nukeOp = _random.PickAndTake(cmdrPrefList); + everyone.Remove(nukeOp); + prefList.Remove(nukeOp); + Logger.InfoS("preset", "Selected a preferred nukeop commander."); + } } - - SpawnOperatives(numNukies, operatives, false, nukeops); - - foreach (var session in operatives) + else { - ev.PlayerPool.Remove(session); - GameTicker.PlayerJoinGame(session); - var name = session.AttachedEntity == null - ? string.Empty - : MetaData(session.AttachedEntity.Value).EntityName; - // TODO: Fix this being able to have duplicates - nukeops.OperativePlayers[name] = session; + if (prefList.Count == 0) + { + if (everyone.Count == 0) + { + Logger.InfoS("preset", "Insufficient ready players to fill up with nukeops, stopping the selection"); + break; + } + nukeOp = _random.PickAndTake(everyone); + Logger.InfoS("preset", "Insufficient preferred nukeops, picking at random."); + } + else + { + nukeOp = _random.PickAndTake(prefList); + everyone.Remove(nukeOp); + Logger.InfoS("preset", "Selected a preferred nukeop."); + } } + operatives.Add(nukeOp); + } + + SpawnOperatives(numNukies, operatives, false); + + foreach(var session in operatives) + { + ev.PlayerPool.Remove(session); + GameTicker.PlayerJoinGame(session); + var name = session.AttachedEntity == null + ? string.Empty + : MetaData(session.AttachedEntity.Value).EntityName; + // TODO: Fix this being able to have duplicates + _operativePlayers[name] = session; } } @@ -524,13 +583,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (TryComp(args.Spawned, out ActorComponent? actor)) profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile; - // todo: this is kinda awful for multi-nukies - foreach (var nukeops in EntityQuery()) - { - SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile, nukeops); + SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile); - nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype); - } + _operativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype); } private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args) @@ -540,51 +595,60 @@ public sealed class NukeopsRuleSystem : GameRuleSystem var mind = mindComponent.Mind; - foreach (var nukeops in EntityQuery()) + if (_operativeMindPendingData.TryGetValue(uid, out var role) || !_nukeopsRuleConfig.SpawnOutpost || !_nukeopsRuleConfig.EndsRound) { - if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost || !nukeops.EndsRound) - { - role ??= nukeops.OperativeRoleProto; + if (role == null) + role = _nukeopsRuleConfig.OperativeRoleProto; - mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(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))); + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(role))); + _operativeMindPendingData.Remove(uid); } + + 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(EntityUid uid, NukeopsRuleComponent? component = null) + private bool SpawnMap() { - if (!Resolve(uid, ref component)) - return false; - - if (component.NukiePlanet != null) + if (_nukiePlanet != null) return true; // Map is already loaded. - if (!component.SpawnOutpost) + if (!_nukeopsRuleConfig.SpawnOutpost) return true; - var path = component.NukieOutpostMap; - var shuttlePath = component.NukieShuttleMap; + _nukeopsRuleConfig.CanLoneOpsSpawn = false; + + var path = _nukeopsRuleConfig.NukieOutpostMap; + var shuttlePath = _nukeopsRuleConfig.NukieShuttleMap; + if (path == null) + { + Logger.ErrorS("nukies", "No station map specified for nukeops!"); + return false; + } + + if (shuttlePath == null) + { + Logger.ErrorS("nukies", "No shuttle map specified for nukeops!"); + return false; + } var mapId = _mapManager.CreateMap(); - var options = new MapLoadOptions + var options = new MapLoadOptions() { LoadMap = true, }; @@ -596,7 +660,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } // Assume the first grid is the outpost grid. - component.NukieOutpost = outpostGrids[0]; + _nukieOutpost = outpostGrids[0]; // Listen I just don't want it to overlap. if (!_map.TryLoad(mapId, shuttlePath.ToString(), out var grids, new MapLoadOptions {Offset = Vector2.One*1000f}) || !grids.Any()) @@ -617,15 +681,16 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (TryComp(shuttleId, out var shuttle)) { - _shuttle.TryFTLDock(shuttleId, shuttle, component.NukieOutpost.Value); + _shuttle.TryFTLDock(shuttleId, shuttle, _nukieOutpost.Value); } - component.NukiePlanet = mapId; - component.NukieShuttle = shuttleId; + _nukiePlanet = mapId; + _nukieShuttle = shuttleId; + return true; } - private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber, NukeopsRuleComponent component ) + private (string Name, string Role, string Gear) GetOperativeSpawnDetails(int spawnNumber) { string name; string role; @@ -635,19 +700,19 @@ public sealed class NukeopsRuleSystem : GameRuleSystem switch (spawnNumber) { case 0: - name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(component.OperativeNames[component.EliteNames]); - role = component.CommanderRolePrototype; - gear = component.CommanderStartGearPrototype; + name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.EliteNames]); + role = _nukeopsRuleConfig.CommanderRolePrototype; + gear = _nukeopsRuleConfig.CommanderStartGearPrototype; break; case 1: - name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]); - role = component.OperativeRoleProto; - gear = component.MedicStartGearPrototype; + name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]); + role = _nukeopsRuleConfig.OperativeRoleProto; + gear = _nukeopsRuleConfig.MedicStartGearPrototype; break; default: - name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]); - role = component.OperativeRoleProto; - gear = component.OperativeStartGearPrototype; + name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(_operativeNames[_nukeopsRuleConfig.NormalNames]); + role = _nukeopsRuleConfig.OperativeRoleProto; + gear = _nukeopsRuleConfig.OperativeStartGearPrototype; break; } @@ -657,38 +722,38 @@ public sealed class NukeopsRuleSystem : GameRuleSystem /// /// Adds missing nuke operative components, equips starting gear and renames the entity. /// - private void SetupOperativeEntity(EntityUid mob, string name, string gear, HumanoidCharacterProfile? profile, NukeopsRuleComponent component) + private void SetupOperativeEntity(EntityUid mob, string name, string gear, HumanoidCharacterProfile? profile) { MetaData(mob).EntityName = name; - EnsureComp(mob); + EntityManager.EnsureComponent(mob); if (profile != null) { _humanoidSystem.LoadProfile(mob, profile); } - if (component.StartingGearPrototypes.TryGetValue(gear, out var gearPrototype)) + if (_startingGearPrototypes.TryGetValue(gear, out var gearPrototype)) _stationSpawningSystem.EquipStartingGear(mob, gearPrototype, profile); _faction.RemoveFaction(mob, "NanoTrasen", false); _faction.AddFaction(mob, "Syndicate"); } - private void SpawnOperatives(int spawnCount, List sessions, bool addSpawnPoints, NukeopsRuleComponent component) + private void SpawnOperatives(int spawnCount, List sessions, bool addSpawnPoints) { - if (component.NukieOutpost == null) + if (_nukieOutpost == null) return; - var outpostUid = component.NukieOutpost.Value; + var outpostUid = _nukieOutpost.Value; var spawns = new List(); // Forgive me for hardcoding prototypes foreach (var (_, meta, xform) in EntityManager.EntityQuery(true)) { - if (meta.EntityPrototype?.ID != component.SpawnPointPrototype) + if (meta.EntityPrototype?.ID != _nukeopsRuleConfig.SpawnPointPrototype) continue; - if (xform.ParentUid != component.NukieOutpost) + if (xform.ParentUid != _nukieOutpost) continue; spawns.Add(xform.Coordinates); @@ -704,19 +769,19 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // TODO: This should spawn the nukies in regardless and transfer if possible; rest should go to shot roles. for(var i = 0; i < spawnCount; i++) { - var spawnDetails = GetOperativeSpawnDetails(i, component); + var spawnDetails = GetOperativeSpawnDetails(i); var nukeOpsAntag = _prototypeManager.Index(spawnDetails.Role); if (sessions.TryGetValue(i, out var session)) { var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile; - if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) + if (!_prototypeManager.TryIndex(profile?.Species ?? HumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) { - species = _prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies); + species = _prototypeManager.Index(HumanoidAppearanceSystem.DefaultSpecies); } var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns)); - SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component); + SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile); var newMind = new Mind.Mind(session.UserId) { @@ -729,7 +794,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } else if (addSpawnPoints) { - var spawnPoint = EntityManager.SpawnEntity(component.GhostSpawnPointProto, _random.Pick(spawns)); + var spawnPoint = EntityManager.SpawnEntity(_nukeopsRuleConfig.GhostSpawnPointProto, _random.Pick(spawns)); var ghostRole = EnsureComp(spawnPoint); EnsureComp(spawnPoint); ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name); @@ -743,25 +808,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } } - private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null) + private void SpawnOperativesForGhostRoles() { - if (!Resolve(uid, ref component)) - return; - - if (!SpawnMap(uid, component)) + if (!SpawnMap()) { Logger.InfoS("nukies", "Failed to load map for nukeops"); return; } // Basically copied verbatim from traitor code - var playersPerOperative = component.PlayersPerOperative; - var maxOperatives = component.MaxOperatives; + var playersPerOperative = _nukeopsRuleConfig.PlayersPerOperative; + var maxOperatives = _nukeopsRuleConfig.MaxOperatives; var playerPool = _playerSystem.ServerSessions.ToList(); var numNukies = MathHelper.Clamp(playerPool.Count / playersPerOperative, 1, maxOperatives); var operatives = new List(); - SpawnOperatives(numNukies, operatives, true, component); + SpawnOperatives(numNukies, operatives, true); } //For admins forcing someone to nukeOps. @@ -770,66 +832,77 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!mind.OwnedEntity.HasValue) return; - //ok hardcoded value bad but so is everything else here - mind.AddRole(new TraitorRole(mind, _prototypeManager.Index("Nukeops"))); + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(_nukeopsRuleConfig.OperativeRoleProto))); SetOutfitCommand.SetOutfit(mind.OwnedEntity.Value, "SyndicateOperativeGearFull", EntityManager); } private void OnStartAttempt(RoundStartAttemptEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var nukeops, out var gameRule)) + if (!RuleAdded || Configuration is not NukeopsRuleConfiguration nukeOpsConfig) + return; + + _nukeopsRuleConfig = nukeOpsConfig; + var minPlayers = nukeOpsConfig.MinPlayers; + if (!ev.Forced && ev.Players.Length < 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")); + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); ev.Cancel(); + return; } + + if (ev.Players.Length != 0) + return; + + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); + ev.Cancel(); } - protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); + RuleWinType = WinType.Neutral; + _winConditions.Clear(); + _nukieOutpost = null; + _nukiePlanet = null; + + _startingGearPrototypes.Clear(); + _operativeNames.Clear(); + _operativeMindPendingData.Clear(); + _operativePlayers.Clear(); + // TODO: Loot table or something foreach (var proto in new[] { - component.CommanderStartGearPrototype, - component.MedicStartGearPrototype, - component.OperativeStartGearPrototype + _nukeopsRuleConfig.CommanderStartGearPrototype, + _nukeopsRuleConfig.MedicStartGearPrototype, + _nukeopsRuleConfig.OperativeStartGearPrototype }) { - component.StartingGearPrototypes.Add(proto, _prototypeManager.Index(proto)); + _startingGearPrototypes.Add(proto, _prototypeManager.Index(proto)); } - foreach (var proto in new[] { component.EliteNames, component.NormalNames }) + foreach (var proto in new[] { _nukeopsRuleConfig.EliteNames, _nukeopsRuleConfig.NormalNames }) { - component.OperativeNames.Add(proto, new List(_prototypeManager.Index(proto).Values)); + _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, metaData) in query) + var query = EntityQuery(true); + foreach (var (_, mindComp) in query) { if (mindComp.Mind == null || !mindComp.Mind.TryGetSession(out var session)) continue; - component.OperativePlayers.Add(metaData.EntityName, session); + var name = MetaData(mindComp.Owner).EntityName; + _operativePlayers.Add(name, session); } if (GameTicker.RunLevel == GameRunLevel.InRound) - SpawnOperativesForGhostRoles(uid, component); + SpawnOperativesForGhostRoles(); } + public override void Ended() + { + _nukeopsRuleConfig.EndsRound = true; + _nukeopsRuleConfig.SpawnOutpost = true; + _nukeopsRuleConfig.CanLoneOpsSpawn = true; + } } diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs index 6ff2429c2b..c5877ab513 100644 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs @@ -2,7 +2,6 @@ using System.Linq; using Content.Server.Administration.Commands; using Content.Server.Cargo.Systems; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Preferences.Managers; using Content.Server.Spawners.Components; using Content.Server.Station.Components; @@ -26,7 +25,7 @@ namespace Content.Server.GameTicking.Rules; /// /// 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!; @@ -40,6 +39,17 @@ 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() { @@ -47,186 +57,178 @@ public sealed class PiratesRuleSystem : GameRuleSystem SubscribeLocalEvent(OnPlayerSpawningEvent); SubscribeLocalEvent(OnRoundEndTextEvent); - SubscribeLocalEvent(OnStartAttempt); } private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) + if (!RuleAdded) + return; + + if (Deleted(_pirateShip)) { - if (Deleted(pirates.PirateShip)) + // Major loss, the ship somehow got annihilated. + ev.AddLine(Loc.GetString("pirates-no-ship")); + } + else + { + + List<(double, EntityUid)> mostValuableThefts = new(); + + var finalValue = _pricingSystem.AppraiseGrid(_pirateShip, uid => { - // Major loss, the ship somehow got annihilated. - ev.AddLine(Loc.GetString("pirates-no-ship")); - } - else + 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; - List<(double, EntityUid)> mostValuableThefts = new(); + mostValuableThefts.Add((price, uid)); + mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1)); + if (mostValuableThefts.Count > 5) + mostValuableThefts.Pop(); + }); - 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")); + foreach (var mind in _pirates) + { + if (mind.CurrentEntity is not null) + finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value); } + var score = finalValue - _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-list-start")); - foreach (var pirate in pirates.Pirates) + ev.AddLine(Loc.GetString("pirates-most-valuable")); + + foreach (var (price, obj) in mostValuableThefts) { - ev.AddLine($"- {pirate.CharacterName} ({pirate.Session?.Name})"); + ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}"))); } + + if (mostValuableThefts.Count == 0) + ev.AddLine(Loc.GetString("pirates-stole-nothing")); + } + + ev.AddLine(""); + ev.AddLine(Loc.GetString("pirates-list-start")); + foreach (var pirates in _pirates) + { + ev.AddLine($"- {pirates.CharacterName} ({pirates.Session?.Name})"); } } + public override void Started() { } + + public override void Ended() { } + private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) + // Forgive me for copy-pasting nukies. + if (!RuleAdded) { - // Forgive me for copy-pasting nukies. - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - return; - - pirates.Pirates.Clear(); - pirates.InitialItems.Clear(); - - // Between 1 and : 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) - { - ev.PlayerPool.Add(session); - } - - return; - } - - 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. + return; } + + _pirates.Clear(); + _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) + { + ev.PlayerPool.Add(session); + } + return; + } + + _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 != _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) + { + 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.Add(newMind); + + GameTicker.PlayerJoinGame(session); + } + + _initialShipValue = _pricingSystem.AppraiseGrid(_pirateShip, uid => + { + _initialItems.Add(uid); + return true; + }); // Include the players in the appraisal. } //Forcing one player to be a pirate. @@ -239,26 +241,21 @@ public sealed class PiratesRuleSystem : GameRuleSystem private void OnStartAttempt(RoundStartAttemptEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) + if (!RuleAdded) + return; + + var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers); + if (!ev.Forced && ev.Players.Length < minPlayers) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); + ev.Cancel(); + return; + } - var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers); - if (!ev.Forced && ev.Players.Length < minPlayers) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", - ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); - ev.Cancel(); - return; - } - - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); - ev.Cancel(); - } + if (ev.Players.Length == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); + ev.Cancel(); } } } diff --git a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs index a26a2d783c..1df78acd8d 100644 --- a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs @@ -1,21 +1,21 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules.Configurations; using Content.Server.Sandbox; namespace Content.Server.GameTicking.Rules; -public sealed class SandboxRuleSystem : GameRuleSystem +public sealed class SandboxRuleSystem : GameRuleSystem { [Dependency] private readonly SandboxSystem _sandbox = default!; - protected override void Started(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override string Prototype => "Sandbox"; + + public override void Started() { - base.Started(uid, component, gameRule, args); _sandbox.IsSandboxEnabled = true; } - protected override void Ended(EntityUid uid, SandboxRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + public override void Ended() { - base.Ended(uid, component, gameRule, args); _sandbox.IsSandboxEnabled = false; } } diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs index 7023292317..4878f65ff0 100644 --- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs @@ -1,5 +1,6 @@ +using System.Linq; using Content.Server.GameTicking.Presets; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules.Configurations; using Content.Shared.Random; using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; @@ -7,28 +8,25 @@ using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules; -public sealed class SecretRuleSystem : GameRuleSystem +public sealed class SecretRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GameTicker _ticker = default!; - protected override void Started(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override string Prototype => "Secret"; + + public override void Started() { - base.Started(uid, component, gameRule, args); - PickRule(component); + PickRule(); } - protected override void Ended(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) + public override void Ended() { - base.Ended(uid, component, gameRule, args); - - foreach (var rule in component.AdditionalGameRules) - { - GameTicker.EndGameRule(rule); - } + // Preset should already handle it. } - private void PickRule(SecretRuleComponent component) + private void PickRule() { // TODO: This doesn't consider what can't start due to minimum player count, but currently there's no way to know anyway. // as they use cvars. @@ -37,8 +35,7 @@ public sealed class SecretRuleSystem : GameRuleSystem foreach (var rule in _prototypeManager.Index(preset).Rules) { - GameTicker.StartGameRule(rule, out var ruleEnt); - component.AdditionalGameRules.Add(ruleEnt); + _ticker.StartGameRule(_prototypeManager.Index(rule)); } } } diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs new file mode 100644 index 0000000000..3c82367182 --- /dev/null +++ b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs @@ -0,0 +1,456 @@ +using System.Linq; +using System.Threading; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Rules.Configurations; +using Content.Server.Players; +using Content.Server.Roles; +using Content.Server.Station.Components; +using Content.Server.Suspicion; +using Content.Server.Suspicion.Roles; +using Content.Server.Traitor.Uplink; +using Content.Shared.CCVar; +using Content.Shared.Doors.Systems; +using Content.Shared.EntityList; +using Content.Shared.GameTicking; +using Content.Shared.Maps; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Roles; +using Content.Shared.Suspicion; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Audio; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.GameTicking.Rules; + +/// +/// 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 new file mode 100644 index 0000000000..a634bb34c6 --- /dev/null +++ b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs @@ -0,0 +1,276 @@ +using System.Linq; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Chat.Managers; +using Content.Server.PDA; +using Content.Server.Players; +using Content.Server.Spawners.Components; +using Content.Server.Store.Components; +using Content.Server.Traitor; +using Content.Server.Traitor.Uplink; +using Content.Server.TraitorDeathMatch.Components; +using Content.Shared.CCVar; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Hands.Components; +using Content.Shared.Inventory; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.PDA; +using Content.Shared.Roles; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.GameTicking.Rules; + +public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MaxTimeRestartRuleSystem _restarter = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly UplinkSystem _uplink = default!; + + public override string Prototype => "TraitorDeathMatch"; + + public string PDAPrototypeName => "CaptainPDA"; + public string BeltPrototypeName => "ClothingBeltJanitorFilled"; + public string BackpackPrototypeName => "ClothingBackpackFilled"; + + private bool _safeToEndRound = false; + + private readonly Dictionary _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 bb6bb89f2a..9edfe80d20 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -1,6 +1,5 @@ using System.Linq; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; using Content.Server.NPC.Systems; using Content.Server.Objectives.Interfaces; using Content.Server.PDA.Ringer; @@ -25,7 +24,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking.Rules; -public sealed class TraitorRuleSystem : GameRuleSystem +public sealed class TraitorRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -33,6 +32,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem [Dependency] private readonly IObjectivesManager _objectivesManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly FactionSystem _faction = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly UplinkSystem _uplink = default!; @@ -40,8 +40,30 @@ public sealed class TraitorRuleSystem : GameRuleSystem private ISawmill _sawmill = default!; - private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); - private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors); + public override string Prototype => "Traitor"; + + private readonly SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); + public List 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(); public override void Initialize() { @@ -55,101 +77,101 @@ public sealed class TraitorRuleSystem : GameRuleSystem SubscribeLocalEvent(OnRoundEndText); } - protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime) + public override void Update(float frameTime) { - base.ActiveTick(uid, component, gameRule, frameTime); + base.Update(frameTime); - if (component.SelectionStatus == TraitorRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt) - DoTraitorStart(component); + if (SelectionStatus == SelectionState.ReadyToSelect && _gameTiming.CurTime >= _announceAt) + DoTraitorStart(); + } + + public override void Started(){} + + public override void Ended() + { + Traitors.Clear(); + _startCandidates.Clear(); + SelectionStatus = SelectionState.WaitingForSpawn; } private void OnStartAttempt(RoundStartAttemptEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var traitor, out var gameRule)) + MakeCodewords(); + if (!RuleAdded) + return; + + var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers); + if (!ev.Forced && ev.Players.Length < minPlayers) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; + _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); + ev.Cancel(); + return; + } - 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(); - } + if (ev.Players.Length == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready")); + ev.Cancel(); } } - private void MakeCodewords(TraitorRuleComponent component) + private void MakeCodewords() { var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); var adjectives = _prototypeManager.Index("adjectives").Values; var verbs = _prototypeManager.Index("verbs").Values; var codewordPool = adjectives.Concat(verbs).ToList(); var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count); - component.Codewords = new string[finalCodewordCount]; + Codewords = new string[finalCodewordCount]; for (var i = 0; i < finalCodewordCount; i++) { - component.Codewords[i] = _random.PickAndTake(codewordPool); + Codewords[i] = _random.PickAndTake(codewordPool); } } - private void DoTraitorStart(TraitorRuleComponent component) + private void DoTraitorStart() { - if (!component.StartCandidates.Any()) + if (!_startCandidates.Any()) { _sawmill.Error("Tried to start Traitor mode without any candidates."); return; } - var numTraitors = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerTraitor, 1, MaxTraitors); - var traitorPool = FindPotentialTraitors(component.StartCandidates, component); + var numTraitors = MathHelper.Clamp(_startCandidates.Count / _playersPerTraitor, 1, _maxTraitors); + var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); + + var traitorPool = FindPotentialTraitors(_startCandidates); var selectedTraitors = PickTraitors(numTraitors, traitorPool); foreach (var traitor in selectedTraitors) - { MakeTraitor(traitor); - } - component.SelectionStatus = TraitorRuleComponent.SelectionState.SelectionMade; + SelectionStatus = SelectionState.SelectionMade; } private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var traitor, out var gameRule)) + if (!RuleAdded) + return; + + foreach (var player in ev.Players) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) + if (!ev.Profiles.ContainsKey(player.UserId)) continue; - foreach (var player in ev.Players) - { - if (!ev.Profiles.ContainsKey(player.UserId)) - continue; - traitor.StartCandidates[player] = ev.Profiles[player.UserId]; - } - - 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; + _startCandidates[player] = ev.Profiles[player.UserId]; } + + 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, TraitorRuleComponent component) + public List FindPotentialTraitors(in Dictionary candidates) { var list = new List(); var pendingQuery = GetEntityQuery(); @@ -174,7 +196,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem foreach (var player in list) { var profile = candidates[player]; - if (profile.AntagPreferences.Contains(component.TraitorPrototypeId)) + if (profile.AntagPreferences.Contains(TraitorPrototypeID)) { prefList.Add(player); } @@ -206,14 +228,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem 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) { @@ -240,15 +254,14 @@ 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(traitorRule.TraitorPrototypeId); + var antagPrototype = _prototypeManager.Index(TraitorPrototypeID); var traitorRole = new TraitorRole(mind, antagPrototype); mind.AddRole(traitorRole); - traitorRule.Traitors.Add(traitorRole); - traitorRole.GreetTraitor(traitorRule.Codewords, code); + Traitors.Add(traitorRole); + traitorRole.GreetTraitor(Codewords, code); _faction.RemoveFaction(entity, "NanoTrasen", false); _faction.AddFaction(entity, "Syndicate"); @@ -267,173 +280,147 @@ public sealed class TraitorRuleSystem : GameRuleSystem } //give traitors their codewords and uplink code to keep in their character info menu - traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", traitorRule.Codewords))) + traitorRole.Mind.Briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", Codewords))) + "\n" + Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("", code))); - _audioSystem.PlayGlobal(traitorRule.AddedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default); + _audioSystem.PlayGlobal(_addedSound, Filter.Empty().AddPlayer(traitor), false, AudioParams.Default); return true; } private void HandleLatejoin(PlayerSpawnCompleteEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var traitor, out var gameRule)) + 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) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; + _startCandidates[ev.Player] = ev.Profile; + return; + } - if (traitor.TotalTraitors >= MaxTraitors) - continue; - if (!ev.LateJoin) - continue; - if (!ev.Profile.AntagPreferences.Contains(traitor.TraitorPrototypeId)) - continue; + // the nth player we adjust our probabilities around + int target = ((_playersPerTraitor * TotalTraitors) + 1); - if (ev.JobId == null || !_prototypeManager.TryIndex(ev.JobId, out var job)) - continue; + float chance = (1f / _playersPerTraitor); - if (!job.CanBeAntag) - 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; - // 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); - } + // Now that we've calculated our chance, roll and make them a traitor if we roll under. + // You get one shot. + if (_random.Prob(chance)) + { + MakeTraitor(ev.Player); } } private void OnRoundEndText(RoundEndTextAppendEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var traitor, out var gameRule)) + 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) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; + var name = traitor.Mind.CharacterName; + traitor.Mind.TryGetSession(out var session); + var username = session?.Name; - 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 objectives = traitor.Mind.AllObjectives.ToArray(); + if (objectives.Length == 0) { - 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-with-objectives", - ("user", username)); + result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username)); else - result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", - ("user", username), ("name", name)); + result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name)); } else if (name != null) - result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name)); + result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name)); - foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer)) + 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) { - result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}"); - - foreach (var objective in objectiveGroup) + foreach (var condition in objective.Conditions) { - foreach (var condition in objective.Conditions) + var progress = condition.Progress; + if (progress > 0.99f) { - var progress = condition.Progress; - if (progress > 0.99f) - { - result += "\n- " + Loc.GetString( - "traitor-objective-condition-success", - ("condition", condition.Title), - ("markupColor", "green") - ); - } - else - { - result += "\n- " + Loc.GetString( - "traitor-objective-condition-fail", - ("condition", condition.Title), - ("progress", (int) (progress * 100)), - ("markupColor", "red") - ); - } + result += "\n- " + Loc.GetString( + "traitor-objective-condition-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 List GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind) + public IEnumerable GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind) { - List allTraitors = new(); - foreach (var traitor in EntityQuery()) - { - foreach (var role in GetOtherTraitorsAliveAndConnected(ourMind, traitor)) - { - if (!allTraitors.Contains(role)) - allTraitors.Add(role); - } - } + var traitors = Traitors; + List removeList = new(); - return allTraitors; - } - - public List GetOtherTraitorsAliveAndConnected(Mind.Mind ourMind, TraitorRuleComponent component) - { - return component.Traitors // don't want + return Traitors // don't want + .Where(t => t.Mind is not null) // no mind .Where(t => t.Mind.OwnedEntity is not null) // no entity .Where(t => t.Mind.Session is not null) // player disconnected .Where(t => t.Mind != ourMind) // ourselves .Where(t => _mobStateSystem.IsAlive((EntityUid) t.Mind.OwnedEntity!)) // dead - .Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity).ToList(); // not in original body + .Where(t => t.Mind.CurrentEntity == t.Mind.OwnedEntity); // not in original body } } diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index 4ac83b354e..f51a22b42d 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.GameTicking.Rules.Components; +using Content.Server.Humanoid; using Content.Server.Mind.Components; using Content.Server.Players; using Content.Server.Popups; @@ -29,7 +29,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking.Rules; -public sealed class ZombieRuleSystem : GameRuleSystem +public sealed class ZombieRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -44,6 +44,14 @@ public sealed class ZombieRuleSystem : GameRuleSystem [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(); @@ -59,61 +67,60 @@ public sealed class ZombieRuleSystem : GameRuleSystem private void OnRoundEndText(RoundEndTextAppendEvent ev) { - foreach (var zombie in EntityQuery()) + 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) { - //this is just the general condition thing used for determining the win/lose text - var percent = GetInfectedPercentage(out var livingHumans); + ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial", + ("name", player.Key), + ("username", player.Value))); + } - 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) + //Gets a bunch of the living players and displays them if they're under a threshold. + //InitialInfected is used for the threshold because it scales with the player count well. + if (livingHumans.Count > 0 && livingHumans.Count <= _initialInfectedNames.Count) + { + ev.AddLine(""); + ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", livingHumans.Count))); + foreach (var survivor in livingHumans) { - ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial", - ("name", player.Key), - ("username", player.Value))); - } + 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; - //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))); - } + ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", + ("name", meta.EntityName), + ("username", username))); } } } private void OnJobAssigned(RulePlayerJobsAssignedEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var zombies, out var gameRule)) - { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; - InfectInitialPlayers(zombies); - } + if (!RuleAdded) + return; + + _initialInfectedNames = new(); + + InfectInitialPlayers(); } /// @@ -122,11 +129,15 @@ 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); } @@ -136,59 +147,50 @@ public sealed class ZombieRuleSystem : GameRuleSystem /// depending on this uid, we should care about the round ending private void CheckRoundEnd(EntityUid target) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var zombies, out var gameRule)) - { - if (GameTicker.IsGameRuleActive(uid, gameRule)) - continue; + //we only care about players, not monkeys and such. + if (!HasComp(target)) + return; - //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(); - } + var percent = GetInfectedPercentage(out var num); + if (num.Count == 1) //only one human left. spooky + _popup.PopupEntity(Loc.GetString("zombie-alone"), num[0], num[0]); + if (percent >= 1) //oops, all zombies + _roundEndSystem.EndRound(); } private void OnStartAttempt(RoundStartAttemptEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var zombies, out var gameRule)) + if (!RuleAdded) + return; + + var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers); + if (!ev.Forced && ev.Players.Length < minPlayers) { - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - continue; + _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); + ev.Cancel(); + return; + } - var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers); - if (!ev.Forced && ev.Players.Length < minPlayers) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); - ev.Cancel(); - continue; - } - - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready")); - ev.Cancel(); - } + if (ev.Players.Length == 0) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("zombie-no-one-ready")); + ev.Cancel(); } } - protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); - InfectInitialPlayers(component); + //this technically will run twice with zombies on roundstart, but it doesn't matter because it fails instantly + InfectInitialPlayers(); } + public override void Ended() { } + private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args) { _zombify.ZombifyEntity(uid); - var action = new InstantAction(_prototypeManager.Index(ZombieRuleComponent.ZombifySelfActionPrototype)); + var action = new InstantAction(_prototypeManager.Index(ZombifySelfActionPrototype)); _action.RemoveAction(uid, action); } @@ -226,7 +228,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(ZombieRuleComponent component) + private void InfectInitialPlayers() { var allPlayers = _playerManager.ServerSessions.ToList(); var playerList = new List(); @@ -238,7 +240,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem playerList.Add(player); var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(player.UserId).SelectedCharacter; - if (pref.AntagPreferences.Contains(component.PatientZeroPrototypeID)) + if (pref.AntagPreferences.Contains(PatientZeroPrototypeID)) prefList.Add(player); } } @@ -282,15 +284,15 @@ public sealed class ZombieRuleSystem : GameRuleSystem DebugTools.AssertNotNull(mind.OwnedEntity); - mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(component.PatientZeroPrototypeID))); + mind.AddRole(new TraitorRole(mind, _prototypeManager.Index(PatientZeroPrototypeID))); var inCharacterName = string.Empty; if (mind.OwnedEntity != null) { - _diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, component.InitialZombieVirusPrototype); + _diseaseSystem.TryAddDisease(mind.OwnedEntity.Value, InitialZombieVirusPrototype); inCharacterName = MetaData(mind.OwnedEntity.Value).EntityName; - var action = new InstantAction(_prototypeManager.Index(ZombieRuleComponent.ZombifySelfActionPrototype)); + var action = new InstantAction(_prototypeManager.Index(ZombifySelfActionPrototype)); _action.AddAction(mind.OwnedEntity.Value, action, null); } @@ -301,7 +303,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. - component.InitialInfectedNames.Add(inCharacterName, mind.Session.Name); + _initialInfectedNames.Add(inCharacterName, mind.Session.Name); // I went all the way to ChatManager.cs and all i got was this lousy T-shirt // You got a free T-shirt!?!? diff --git a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs index 0fd1d53a09..d653df357d 100644 --- a/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs +++ b/Content.Server/Objectives/Conditions/RandomTraitorAliveCondition.cs @@ -14,10 +14,9 @@ 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. + + 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 99de8be282..fd19603387 100644 --- a/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs +++ b/Content.Server/Objectives/Conditions/RandomTraitorProgressCondition.cs @@ -13,9 +13,7 @@ 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(); @@ -25,7 +23,7 @@ namespace Content.Server.Objectives.Conditions { foreach (var condition in objective.Conditions) { - if (condition is RandomTraitorProgressCondition) + if (condition.GetType() == typeof(RandomTraitorProgressCondition)) { removeList.Add(traitor); } diff --git a/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs b/Content.Server/Objectives/Requirements/MultipleTraitorsRequirement.cs index 5d4e458297..e833165862 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().GetOtherTraitorsAliveAndConnected(mind).Count >= _requiredTraitors; + return EntitySystem.Get().TotalTraitors >= _requiredTraitors; } } } diff --git a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs index 12a16151c1..a5279ce848 100644 --- a/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs +++ b/Content.Server/Spawners/Components/ConditionalSpawnerComponent.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Rules; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; @@ -8,11 +9,11 @@ namespace Content.Server.Spawners.Components public class ConditionalSpawnerComponent : Component { [ViewVariables(VVAccess.ReadWrite)] - [DataField("prototypes", customTypeSerializer: typeof(PrototypeIdListSerializer))] + [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 341a2c9d44..1eef300972 100644 --- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -1,5 +1,4 @@ using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Spawners.Components; using JetBrains.Annotations; using Robust.Shared.Random; @@ -23,67 +22,65 @@ namespace Content.Server.Spawners.EntitySystems private void OnCondSpawnMapInit(EntityUid uid, ConditionalSpawnerComponent component, MapInitEvent args) { - TrySpawn(uid, component); + TrySpawn(component); } private void OnRandSpawnMapInit(EntityUid uid, RandomSpawnerComponent component, MapInitEvent args) { - Spawn(uid, component); - QueueDel(uid); + Spawn(component); + EntityManager.QueueDeleteEntity(uid); } - private void OnRuleStarted(ref GameRuleStartedEvent args) + private void OnRuleStarted(GameRuleStartedEvent args) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var spawner)) + foreach (var spawner in EntityManager.EntityQuery()) { - RuleStarted(uid, spawner, args); + RuleStarted(spawner, args); } } - public void RuleStarted(EntityUid uid, ConditionalSpawnerComponent component, GameRuleStartedEvent obj) + public void RuleStarted(ConditionalSpawnerComponent component, GameRuleStartedEvent obj) { - if (component.GameRules.Contains(obj.RuleId)) - Spawn(uid, component); + if(component.GameRules.Contains(obj.Rule.ID)) + Spawn(component); } - private void TrySpawn(EntityUid uid, ConditionalSpawnerComponent component) + private void TrySpawn(ConditionalSpawnerComponent component) { if (component.GameRules.Count == 0) { - Spawn(uid, component); + Spawn(component); return; } foreach (var rule in component.GameRules) { - if (!_ticker.IsGameRuleActive(rule)) - continue; - Spawn(uid, component); + if (!_ticker.IsGameRuleStarted(rule)) continue; + Spawn(component); return; } } - private void Spawn(EntityUid uid, ConditionalSpawnerComponent component) + private void Spawn(ConditionalSpawnerComponent component) { if (component.Chance != 1.0f && !_robustRandom.Prob(component.Chance)) return; if (component.Prototypes.Count == 0) { - Logger.Warning($"Prototype list in ConditionalSpawnComponent is empty! Entity: {ToPrettyString(uid)}"); + Logger.Warning($"Prototype list in ConditionalSpawnComponent is empty! Entity: {component.Owner}"); return; } - if (!Deleted(uid)) - EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), Transform(uid).Coordinates); + if (!Deleted(component.Owner)) + EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), Transform(component.Owner).Coordinates); } - private void Spawn(EntityUid uid, RandomSpawnerComponent component) + private void Spawn(RandomSpawnerComponent component) { if (component.RarePrototypes.Count > 0 && (component.RareChance == 1.0f || _robustRandom.Prob(component.RareChance))) { - EntityManager.SpawnEntity(_robustRandom.Pick(component.RarePrototypes), Transform(uid).Coordinates); + EntityManager.SpawnEntity(_robustRandom.Pick(component.RarePrototypes), Transform(component.Owner).Coordinates); return; } @@ -92,18 +89,17 @@ namespace Content.Server.Spawners.EntitySystems if (component.Prototypes.Count == 0) { - Logger.Warning($"Prototype list in RandomSpawnerComponent is empty! Entity: {ToPrettyString(uid)}"); + Logger.Warning($"Prototype list in RandomSpawnerComponent is empty! Entity: {component.Owner}"); return; } - if (Deleted(uid)) - return; + if (Deleted(component.Owner)) return; var offset = component.Offset; var xOffset = _robustRandom.NextFloat(-offset, offset); var yOffset = _robustRandom.NextFloat(-offset, offset); - var coordinates = Transform(uid).Coordinates.Offset(new Vector2(xOffset, yOffset)); + var coordinates = Transform(component.Owner).Coordinates.Offset(new Vector2(xOffset, yOffset)); EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), coordinates); } diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index bc77a9ce47..0304978ed6 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -1,7 +1,12 @@ +using System.Linq; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; +using Content.Server.GameTicking.Rules.Configurations; +using Content.Shared.CCVar; +using Content.Shared.GameTicking; using JetBrains.Annotations; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.StationEvents @@ -11,49 +16,53 @@ namespace Content.Server.StationEvents /// game presets use. /// [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!; - protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule, - GameRuleEndedEvent args) - { - component.TimeUntilNextEvent = BasicStationEventSchedulerComponent.MinimumTimeUntilFirstEvent; - } + 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() + { + _timeUntilNextEvent = MinimumTimeUntilFirstEvent; + } public override void Update(float frameTime) { base.Update(frameTime); - if (!_event.EventsEnabled) + if (!RuleStarted || !_event.EventsEnabled) return; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var eventScheduler, out var gameRule)) + if (_timeUntilNextEvent > 0) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - continue; - - if (eventScheduler.TimeUntilNextEvent > 0) - { - eventScheduler.TimeUntilNextEvent -= frameTime; - return; - } - - _event.RunRandomEvent(); - ResetTimer(eventScheduler); + _timeUntilNextEvent -= frameTime; + return; } + + _event.RunRandomEvent(); + ResetTimer(); } /// /// Reset the event timer once the event is done. /// - private void ResetTimer(BasicStationEventSchedulerComponent component) + private void ResetTimer() { // 5 - 25 minutes. TG does 3-10 but that's pretty frequent - component.TimeUntilNextEvent = _random.Next(300, 1500); + _timeUntilNextEvent = _random.Next(300, 1500); } } } diff --git a/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs b/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs deleted file mode 100644 index 8e8e853260..0000000000 --- a/Content.Server/StationEvents/Components/AnomalySpawnRuleComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Content.Server.StationEvents.Events; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.StationEvents.Components; - -/// -/// 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 deleted file mode 100644 index 1f920b6c7c..0000000000 --- a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(BasicStationEventSchedulerSystem))] -public sealed class BasicStationEventSchedulerComponent : Component -{ - public const float MinimumTimeUntilFirstEvent = 300; - - /// - /// 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 deleted file mode 100644 index fe953c97f8..0000000000 --- a/Content.Server/StationEvents/Components/BluespaceArtifactRuleComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Server.StationEvents.Events; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.StationEvents.Components; - -/// -/// 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 deleted file mode 100644 index 06515f53fc..0000000000 --- a/Content.Server/StationEvents/Components/BluespaceLockerRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(BluespaceLockerRule))] -public sealed class BluespaceLockerRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs b/Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs deleted file mode 100644 index a4acef1345..0000000000 --- a/Content.Server/StationEvents/Components/BreakerFlipRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(BreakerFlipRule))] -public sealed class BreakerFlipRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs b/Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs deleted file mode 100644 index 5132a09e68..0000000000 --- a/Content.Server/StationEvents/Components/BureaucraticErrorRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(BureaucraticErrorRule))] -public sealed class BureaucraticErrorRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs b/Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs deleted file mode 100644 index d82557cf13..0000000000 --- a/Content.Server/StationEvents/Components/DiseaseOutbreakRuleComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(DiseaseOutbreakRule))] -public sealed class DiseaseOutbreakRuleComponent : Component -{ - /// - /// 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 deleted file mode 100644 index 6e3f69ad6c..0000000000 --- a/Content.Server/StationEvents/Components/FalseAlarmRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(FalseAlarmRule))] -public sealed class FalseAlarmRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs b/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs deleted file mode 100644 index 5f2c2f5ff6..0000000000 --- a/Content.Server/StationEvents/Components/GasLeakRuleComponent.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Content.Server.StationEvents.Events; -using Content.Shared.Atmos; -using Robust.Shared.Map; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(GasLeakRule))] -public sealed class GasLeakRuleComponent : Component -{ - public readonly Gas[] LeakableGases = - { - Gas.Miasma, - Gas.Plasma, - Gas.Tritium, - Gas.Frezon, - }; - - /// - /// 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 deleted file mode 100644 index 82cc1ac77d..0000000000 --- a/Content.Server/StationEvents/Components/KudzuGrowthRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(KudzuGrowthRule))] -public sealed class KudzuGrowthRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs deleted file mode 100644 index 535660362f..0000000000 --- a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.StationEvents.Events; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(LoneOpsSpawnRule))] -public sealed class LoneOpsSpawnRuleComponent : Component -{ - [DataField("loneOpsShuttlePath")] - public string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml"; - - [DataField("gameRuleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string GameRuleProto = "Nukeops"; - - [DataField("additionalRule")] - public EntityUid? AdditionalRule; -} diff --git a/Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs b/Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs deleted file mode 100644 index e42eef5b26..0000000000 --- a/Content.Server/StationEvents/Components/MeteorSwarmRuleComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(MeteorSwarmRule))] -public sealed class MeteorSwarmRuleComponent : Component -{ - public float _cooldown; - - /// - /// 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 deleted file mode 100644 index cb6274624c..0000000000 --- a/Content.Server/StationEvents/Components/MouseMigrationRuleComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(MouseMigrationRule))] -public sealed class MouseMigrationRuleComponent : Component -{ - [DataField("spawnedPrototypeChoices")] - public List SpawnedPrototypeChoices = new() //we double up for that ez fake probability - { - "MobMouse", - "MobMouse1", - "MobMouse2", - "MobRatServant" - }; -} diff --git a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs deleted file mode 100644 index 1788a78077..0000000000 --- a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(PowerGridCheckRule))] -public sealed class PowerGridCheckRuleComponent : Component -{ - public CancellationTokenSource? AnnounceCancelToken; - - public readonly List 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 deleted file mode 100644 index dd3f38b5dd..0000000000 --- a/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(RampingStationEventSchedulerSystem))] -public sealed class RampingStationEventSchedulerComponent : Component -{ - [DataField("endTime"), ViewVariables(VVAccess.ReadWrite)] - public float EndTime; - - [DataField("maxChaos"), ViewVariables(VVAccess.ReadWrite)] - public float MaxChaos; - - [DataField("startingChaos"), ViewVariables(VVAccess.ReadWrite)] - public float StartingChaos; - - [DataField("timeUntilNextEvent"), ViewVariables(VVAccess.ReadWrite)] - public float TimeUntilNextEvent; -} diff --git a/Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs b/Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs deleted file mode 100644 index 6c00e9e362..0000000000 --- a/Content.Server/StationEvents/Components/RandomSentienceRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(RandomSentienceRule))] -public sealed class RandomSentienceRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs deleted file mode 100644 index d195eaad7c..0000000000 --- a/Content.Server/StationEvents/Components/RevenantSpawnRuleComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(RevenantSpawnRule))] -public sealed class RevenantSpawnRuleComponent : Component -{ - [DataField("revenantPrototype")] - public string RevenantPrototype = "MobRevenant"; -} diff --git a/Content.Server/StationEvents/Components/SentienceTargetComponent.cs b/Content.Server/StationEvents/Components/SentienceTargetComponent.cs index d22f3e74eb..bf40d50249 100644 --- a/Content.Server/StationEvents/Components/SentienceTargetComponent.cs +++ b/Content.Server/StationEvents/Components/SentienceTargetComponent.cs @@ -1,8 +1,6 @@ -using Content.Server.StationEvents.Events; +namespace Content.Server.StationEvents.Components; -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(RandomSentienceRule))] +[RegisterComponent] public sealed class SentienceTargetComponent : Component { [DataField("flavorKind", required: true)] diff --git a/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs deleted file mode 100644 index 15e01ac8a8..0000000000 --- a/Content.Server/StationEvents/Components/SpiderSpawnRuleComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(SpiderSpawnRule))] -public sealed class SpiderSpawnRuleComponent : Component -{ - -} diff --git a/Content.Server/StationEvents/Components/VentClogRuleComponent.cs b/Content.Server/StationEvents/Components/VentClogRuleComponent.cs deleted file mode 100644 index 79f4993375..0000000000 --- a/Content.Server/StationEvents/Components/VentClogRuleComponent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(VentClogRule))] -public sealed class VentClogRuleComponent : Component -{ - [DataField("safeishVentChemicals")] - public readonly IReadOnlyList 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 f675744929..3dc46642e7 100644 --- a/Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs +++ b/Content.Server/StationEvents/Components/VentCritterSpawnLocationComponent.cs @@ -1,8 +1,6 @@ -using Content.Server.StationEvents.Events; +namespace Content.Server.StationEvents.Components; -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(VentClogRule))] +[RegisterComponent] public sealed class VentCritterSpawnLocationComponent : Component { diff --git a/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs b/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs deleted file mode 100644 index 5332796e84..0000000000 --- a/Content.Server/StationEvents/Components/VentCrittersRuleComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Content.Server.StationEvents.Events; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(VentCrittersRule))] -public sealed class VentCrittersRuleComponent : Component -{ - [DataField("spawnedPrototypeChoices")] - public List SpawnedPrototypeChoices = new() - { - "MobMouse", - "MobMouse1", - "MobMouse2" - }; -} diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 72a5dfac77..6a2b646602 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -1,7 +1,9 @@ using System.Linq; using Content.Server.GameTicking; -using Content.Server.StationEvents.Components; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Configurations; using Content.Shared.CCVar; +using Content.Shared.GameTicking; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; @@ -29,14 +31,6 @@ public sealed class EventManagerSystem : EntitySystem _sawmill = Logger.GetSawmill("events"); _configurationManager.OnValueChanged(CCVars.EventsEnabled, SetEnabled, true); - - SubscribeLocalEvent(OnUnpaused); - } - - private void OnUnpaused(EntityUid uid, StationEventComponent component, ref EntityUnpausedEvent args) - { - component.StartTime += args.PausedTime; - component.EndTime += args.PausedTime; } public override void Shutdown() @@ -52,15 +46,16 @@ public sealed class EventManagerSystem : EntitySystem { var randomEvent = PickRandomEvent(); - if (randomEvent == null) + if (randomEvent == null + || !_prototype.TryIndex(randomEvent.Id, out var proto)) { var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events"); _sawmill.Error(errStr); return errStr; } - var ent = GameTicker.AddGameRule(randomEvent); - var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent))); + GameTicker.AddGameRule(proto); + var str = Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Id)); _sawmill.Info(str); return str; } @@ -68,7 +63,7 @@ public sealed class EventManagerSystem : EntitySystem /// /// Randomly picks a valid event. /// - public string? PickRandomEvent() + public StationEventRuleConfiguration? PickRandomEvent() { var availableEvents = AvailableEvents(); _sawmill.Info($"Picking from {availableEvents.Count} total available events"); @@ -79,7 +74,7 @@ public sealed class EventManagerSystem : EntitySystem /// Pick a random event from the available events at this time, also considering their weightings. /// /// - private string? FindEvent(Dictionary availableEvents) + private StationEventRuleConfiguration? FindEvent(List availableEvents) { if (availableEvents.Count == 0) { @@ -89,20 +84,20 @@ public sealed class EventManagerSystem : EntitySystem var sumOfWeights = 0; - foreach (var stationEvent in availableEvents.Values) + foreach (var stationEvent in availableEvents) { sumOfWeights += (int) stationEvent.Weight; } sumOfWeights = _random.Next(sumOfWeights); - foreach (var (proto, stationEvent) in availableEvents) + foreach (var stationEvent in availableEvents) { sumOfWeights -= (int) stationEvent.Weight; if (sumOfWeights <= 0) { - return proto.ID; + return stationEvent; } } @@ -115,73 +110,67 @@ public sealed class EventManagerSystem : EntitySystem /// /// /// - private Dictionary AvailableEvents(bool ignoreEarliestStart = false) + private List AvailableEvents(bool ignoreEarliestStart = false) { + TimeSpan currentTime; var playerCount = _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - var currentTime = !ignoreEarliestStart - ? GameTicker.RoundDuration() - : TimeSpan.Zero; - - var result = new Dictionary(); - - foreach (var (proto, stationEvent) in AllEvents()) + if (!ignoreEarliestStart) { - if (CanRun(proto, stationEvent, playerCount, currentTime)) + currentTime = GameTicker.RoundDuration(); + } + else + { + currentTime = TimeSpan.Zero; + } + + var result = new List(); + + foreach (var stationEvent in AllEvents()) + { + if (CanRun(stationEvent, playerCount, currentTime)) { - _sawmill.Debug($"Adding event {proto.ID} to possibilities"); - result.Add(proto, stationEvent); + _sawmill.Debug($"Adding event {stationEvent.Id} to possibilities"); + result.Add(stationEvent); } } return result; } - public Dictionary AllEvents() + private IEnumerable AllEvents() { - 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; + return _prototype.EnumeratePrototypes() + .Where(p => p.Configuration is StationEventRuleConfiguration) + .Select(p => (StationEventRuleConfiguration) p.Configuration); } - private int GetOccurrences(EntityPrototype stationEvent) + private int GetOccurrences(StationEventRuleConfiguration stationEvent) { - return GetOccurrences(stationEvent.ID); + return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id); } - private int GetOccurrences(string stationEvent) - { - return GameTicker.AllPreviousGameRules.Count(p => p.Item2 == stationEvent); - } - - public TimeSpan TimeSinceLastEvent(EntityPrototype stationEvent) + public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent) { foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse()) { - if (rule == stationEvent.ID) + if (rule.Configuration is not StationEventRuleConfiguration) + continue; + + if (stationEvent == null || rule.ID == stationEvent.Id) return time; } return TimeSpan.Zero; } - private bool CanRun(EntityPrototype prototype, StationEventComponent stationEvent, int playerCount, TimeSpan currentTime) + private bool CanRun(StationEventRuleConfiguration stationEvent, int playerCount, TimeSpan currentTime) { - if (GameTicker.IsGameRuleActive(prototype.ID)) + if (GameTicker.IsGameRuleStarted(stationEvent.Id)) return false; - if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(prototype) >= stationEvent.MaxOccurrences.Value) + if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(stationEvent) >= stationEvent.MaxOccurrences.Value) { return false; } @@ -196,7 +185,7 @@ public sealed class EventManagerSystem : EntitySystem return false; } - var lastRun = TimeSinceLastEvent(prototype); + var lastRun = TimeSinceLastEvent(stationEvent); if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes) { diff --git a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs b/Content.Server/StationEvents/Events/AnomalySpawn.cs similarity index 61% rename from Content.Server/StationEvents/Events/AnomalySpawnRule.cs rename to Content.Server/StationEvents/Events/AnomalySpawn.cs index 8e7a860d2e..5b7d1c42f5 100644 --- a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs +++ b/Content.Server/StationEvents/Events/AnomalySpawn.cs @@ -1,28 +1,31 @@ using System.Linq; using Content.Server.Anomaly; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; -using Content.Server.StationEvents.Components; using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; -public sealed class AnomalySpawnRule : StationEventSystem +public sealed class AnomalySpawn : StationEventSystem { + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly AnomalySystem _anomaly = default!; - protected override void Added(EntityUid uid, AnomalySpawnRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + public override string Prototype => "AnomalySpawn"; + + public readonly string AnomalySpawnerPrototype = "RandomAnomalySpawner"; + + public override void Added() { - base.Added(uid, component, gameRule, args); + base.Added(); var str = Loc.GetString("anomaly-spawn-event-announcement", - ("sighting", Loc.GetString($"anomaly-spawn-sighting-{RobustRandom.Next(1, 6)}"))); + ("sighting", Loc.GetString($"anomaly-spawn-sighting-{_random.Next(1, 6)}"))); ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5")); } - protected override void Started(EntityUid uid, AnomalySpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); + base.Started(); if (StationSystem.Stations.Count == 0) return; // No stations @@ -42,7 +45,7 @@ public sealed class AnomalySpawnRule : StationEventSystem "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 deleted file mode 100644 index 306b735b84..0000000000 --- a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; -using Robust.Shared.Random; - -namespace Content.Server.StationEvents.Events; - -public sealed class BluespaceArtifactRule : StationEventSystem -{ - 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/BluespaceLockerRule.cs b/Content.Server/StationEvents/Events/BluespaceLocker.cs similarity index 79% rename from Content.Server/StationEvents/Events/BluespaceLockerRule.cs rename to Content.Server/StationEvents/Events/BluespaceLocker.cs index ddf1ba784c..88de303c07 100644 --- a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceLocker.cs @@ -1,25 +1,27 @@ using System.Linq; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Resist; using Content.Server.Station.Components; -using Content.Server.StationEvents.Components; using Content.Server.Storage.Components; using Content.Server.Storage.EntitySystems; using Content.Shared.Access.Components; using Content.Shared.Coordinates; +using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; -public sealed class BluespaceLockerRule : StationEventSystem +public sealed class BluespaceLockerLink : StationEventSystem { + [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly BluespaceLockerSystem _bluespaceLocker = default!; - protected override void Started(EntityUid uid, BluespaceLockerRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override string Prototype => "BluespaceLockerLink"; + + public override void Started() { - base.Started(uid, component, gameRule, args); + base.Started(); var targets = EntityQuery().ToList(); - RobustRandom.Shuffle(targets); + _robustRandom.Shuffle(targets); foreach (var target in targets) { diff --git a/Content.Server/StationEvents/Events/BreakerFlipRule.cs b/Content.Server/StationEvents/Events/BreakerFlip.cs similarity index 70% rename from Content.Server/StationEvents/Events/BreakerFlipRule.cs rename to Content.Server/StationEvents/Events/BreakerFlip.cs index 2920ee331b..2df721b7cc 100644 --- a/Content.Server/StationEvents/Events/BreakerFlipRule.cs +++ b/Content.Server/StationEvents/Events/BreakerFlip.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 BreakerFlipRule : StationEventSystem +public sealed class BreakerFlip : StationEventSystem { [Dependency] private readonly ApcSystem _apcSystem = default!; - protected override void Added(EntityUid uid, BreakerFlipRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + public override string Prototype => "BreakerFlip"; + + public override void Added() { - base.Added(uid, component, gameRule, args); + base.Added(); var str = Loc.GetString("station-event-breaker-flip-announcement", ("data", Loc.GetString(Loc.GetString($"random-sentience-event-data-{RobustRandom.Next(1, 6)}")))); ChatSystem.DispatchGlobalAnnouncement(str, playSound: false, colorOverride: Color.Gold); } - protected override void Started(EntityUid uid, BreakerFlipRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); + base.Started(); if (StationSystem.Stations.Count == 0) return; var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList()); var stationApcs = new List(); - 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/BureaucraticErrorRule.cs b/Content.Server/StationEvents/Events/BureaucraticError.cs similarity index 82% rename from Content.Server/StationEvents/Events/BureaucraticErrorRule.cs rename to Content.Server/StationEvents/Events/BureaucraticError.cs index 0fbf72fd7d..a96eae51b5 100644 --- a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs +++ b/Content.Server/StationEvents/Events/BureaucraticError.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 BureaucraticErrorRule : StationEventSystem +public sealed class BureaucraticError : StationEventSystem { [Dependency] private readonly StationJobsSystem _stationJobs = default!; - protected override void Started(EntityUid uid, BureaucraticErrorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override string Prototype => "BureaucraticError"; + + public override void Started() { - base.Started(uid, component, gameRule, args); + base.Started(); if (StationSystem.Stations.Count == 0) return; // No stations diff --git a/Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs similarity index 70% rename from Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs rename to Content.Server/StationEvents/Events/DiseaseOutbreak.cs index 438cc041df..3c829ddfab 100644 --- a/Content.Server/StationEvents/Events/DiseaseOutbreakRule.cs +++ b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs @@ -1,7 +1,5 @@ using Content.Server.Disease; using Content.Server.Disease.Components; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; using Content.Shared.Disease; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; @@ -12,23 +10,38 @@ namespace Content.Server.StationEvents.Events; /// Infects a couple people /// with a random disease that isn't super deadly /// -public sealed class DiseaseOutbreakRule : StationEventSystem +public sealed class DiseaseOutbreak : 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. /// - protected override void Started(EntityUid uid, DiseaseOutbreakRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); - + base.Started(); HashSet stationsToNotify = new(); List aliveList = new(); - foreach (var (carrier, mobState) in EntityQuery()) + foreach (var (carrier, mobState) in EntityManager.EntityQuery()) { if (!_mobStateSystem.IsDead(mobState.Owner, mobState)) aliveList.Add(carrier); @@ -38,7 +51,7 @@ public sealed class DiseaseOutbreakRule : StationEventSystem "FalseAlarm"; + + public override void Started() + { + base.Started(); + + var ev = GetRandomEventUnweighted(PrototypeManager, RobustRandom); + + if (ev.Configuration is not StationEventRuleConfiguration cfg) + return; + + if (cfg.StartAnnouncement != null) + { + ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(cfg.StartAnnouncement), playSound: false, colorOverride: Color.Gold); + } + + if (cfg.StartAudio != null) + { + SoundSystem.Play(cfg.StartAudio.GetSound(), Filter.Broadcast(), cfg.StartAudio.Params); + } + } + } +} diff --git a/Content.Server/StationEvents/Events/FalseAlarmRule.cs b/Content.Server/StationEvents/Events/FalseAlarmRule.cs deleted file mode 100644 index 05e9435b40..0000000000 --- a/Content.Server/StationEvents/Events/FalseAlarmRule.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Linq; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; -using JetBrains.Annotations; -using Robust.Shared.Player; -using Robust.Shared.Random; - -namespace Content.Server.StationEvents.Events; - -[UsedImplicitly] -public sealed class FalseAlarmRule : StationEventSystem -{ - [Dependency] private readonly EventManagerSystem _event = default!; - - protected override void Started(EntityUid uid, FalseAlarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - var allEv = _event.AllEvents().Select(p => p.Value).ToList(); - var picked = RobustRandom.Pick(allEv); - - if (picked.StartAnnouncement != null) - { - ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(picked.StartAnnouncement), playSound: false, colorOverride: Color.Gold); - } - Audio.PlayGlobal(picked.StartAudio, Filter.Broadcast(), true); - } -} diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs new file mode 100644 index 0000000000..e073ec20f5 --- /dev/null +++ b/Content.Server/StationEvents/Events/GasLeak.cs @@ -0,0 +1,147 @@ +using Content.Server.Atmos.EntitySystems; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Configurations; +using Content.Shared.Atmos; +using Robust.Shared.Audio; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.StationEvents.Events +{ + internal sealed class GasLeak : StationEventSystem + { + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + + public override string Prototype => "GasLeak"; + + private static readonly Gas[] LeakableGases = + { + Gas.Miasma, + Gas.Plasma, + Gas.Tritium, + Gas.Frezon, + }; + + /// + /// 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 deleted file mode 100644 index 95e9286c46..0000000000 --- a/Content.Server/StationEvents/Events/GasLeakRule.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Content.Server.Atmos.EntitySystems; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; -using Robust.Shared.Audio; -using Robust.Shared.Random; -using Robust.Shared.Timing; - -namespace Content.Server.StationEvents.Events -{ - internal sealed class GasLeakRule : StationEventSystem - { - [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 new file mode 100644 index 0000000000..e0b0f5c687 --- /dev/null +++ b/Content.Server/StationEvents/Events/KudzuGrowth.cs @@ -0,0 +1,28 @@ +using Robust.Shared.Map; +using Robust.Shared.Random; + +namespace Content.Server.StationEvents.Events; + +public sealed class KudzuGrowth : StationEventSystem +{ + public override string Prototype => "KudzuGrowth"; + + private EntityUid _targetGrid; + private Vector2i _targetTile; + private EntityCoordinates _targetCoords; + + public override void Started() + { + base.Started(); + + // Pick a place to plant the kudzu. + if (TryFindRandomTile(out _targetTile, out _, out _targetGrid, out _targetCoords)) + { + EntityManager.SpawnEntity("Kudzu", _targetCoords); + Sawmill.Info($"Spawning a Kudzu at {_targetTile} on {_targetGrid}"); + } + + // If the kudzu tile selection fails we just let the announcement happen anyways because it's funny and people + // will be hunting the non-existent, dangerous plant. + } +} diff --git a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs deleted file mode 100644 index 3fa12cd4e9..0000000000 --- a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; - -namespace Content.Server.StationEvents.Events; - -public sealed class KudzuGrowthRule : StationEventSystem -{ - protected override void Started(EntityUid uid, KudzuGrowthRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - // Pick a place to plant the kudzu. - if (!TryFindRandomTile(out var targetTile, out _, out var targetGrid, out var targetCoords)) - return; - Spawn("Kudzu", targetCoords); - Sawmill.Info($"Spawning a Kudzu at {targetTile} on {targetGrid}"); - - } -} diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawn.cs b/Content.Server/StationEvents/Events/LoneOpsSpawn.cs new file mode 100644 index 0000000000..151a592a26 --- /dev/null +++ b/Content.Server/StationEvents/Events/LoneOpsSpawn.cs @@ -0,0 +1,44 @@ +using Robust.Server.GameObjects; +using Robust.Server.Maps; +using Robust.Shared.Map; +using Content.Server.GameTicking; +using Robust.Shared.Prototypes; +using Content.Server.GameTicking.Rules; + +namespace Content.Server.StationEvents.Events; + +public sealed class LoneOpsSpawn : StationEventSystem +{ + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MapLoaderSystem _map = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly NukeopsRuleSystem _nukeopsRuleSystem = default!; + + public override string Prototype => "LoneOpsSpawn"; + public const string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml"; + public const string GameRuleProto = "Nukeops"; + + public override void Started() + { + base.Started(); + + if (!_nukeopsRuleSystem.CheckLoneOpsSpawn()) + return; + + var shuttleMap = _mapManager.CreateMap(); + var options = new MapLoadOptions() + { + LoadMap = true, + }; + + _map.TryLoad(shuttleMap, LoneOpsShuttlePath, out var grids, options); + + if (!_prototypeManager.TryIndex(GameRuleProto, out var ruleProto)) + return; + + _nukeopsRuleSystem.LoadLoneOpsConfig(); + _gameTicker.StartGameRule(ruleProto); + } +} + diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs b/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs deleted file mode 100644 index 57f0a2cbb6..0000000000 --- a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Robust.Server.GameObjects; -using Robust.Server.Maps; -using Robust.Shared.Map; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; - -namespace Content.Server.StationEvents.Events; - -public sealed class LoneOpsSpawnRule : StationEventSystem -{ - [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/MeteorSwarmRule.cs b/Content.Server/StationEvents/Events/MeteorSwarm.cs similarity index 53% rename from Content.Server/StationEvents/Events/MeteorSwarmRule.cs rename to Content.Server/StationEvents/Events/MeteorSwarm.cs index eb0597a92e..aabc9ba2bc 100644 --- a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs +++ b/Content.Server/StationEvents/Events/MeteorSwarm.cs @@ -1,5 +1,4 @@ -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; +using Content.Server.GameTicking; using Content.Shared.Spawners.Components; using Robust.Shared.Map; using Robust.Shared.Physics.Components; @@ -7,37 +6,67 @@ using Robust.Shared.Physics.Systems; namespace Content.Server.StationEvents.Events { - public sealed class MeteorSwarmRule : StationEventSystem + public sealed class MeteorSwarm : StationEventSystem { [Dependency] private readonly SharedPhysicsSystem _physics = default!; - protected override void Started(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); + public override string Prototype => "MeteorSwarm"; + private float _cooldown; + + /// + /// 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() + { + base.Started(); var mod = Math.Sqrt(GetSeverityModifier()); - component._waveCounter = (int) (RobustRandom.Next(component.MinimumWaves, component.MaximumWaves) * mod); + _waveCounter = (int) (RobustRandom.Next(MinimumWaves, MaximumWaves) * mod); } - protected override void ActiveTick(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, float frameTime) + public override void Ended() { - base.ActiveTick(uid, component, gameRule, frameTime); - if (component._waveCounter <= 0) + base.Ended(); + _waveCounter = 0; + _cooldown = 0f; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!RuleStarted) + return; + + if (_waveCounter <= 0) { - ForceEndSelf(uid, gameRule); + ForceEndSelf(); return; } var mod = GetSeverityModifier(); - component._cooldown -= frameTime; + _cooldown -= frameTime; - if (component._cooldown > 0f) + if (_cooldown > 0f) return; - component._waveCounter--; + _waveCounter--; - component._cooldown += (component.MaximumCooldown - component.MinimumCooldown) * RobustRandom.NextFloat() / mod + component.MinimumCooldown; + _cooldown += (MaximumCooldown - MinimumCooldown) * RobustRandom.NextFloat() / mod + MinimumCooldown; Box2? playableArea = null; var mapId = GameTicker.DefaultMap; @@ -50,7 +79,7 @@ namespace Content.Server.StationEvents.Events if (playableArea == null) { - ForceEndSelf(uid, gameRule); + ForceEndSelf(); return; } @@ -59,7 +88,7 @@ namespace Content.Server.StationEvents.Events var center = playableArea.Value.Center; - for (var i = 0; i < component.MeteorsPerWave; i++) + for (var i = 0; i < MeteorsPerWave; i++) { var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau); var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0)); @@ -69,10 +98,10 @@ namespace Content.Server.StationEvents.Events _physics.SetBodyStatus(physics, BodyStatus.InAir); _physics.SetLinearDamping(physics, 0f); _physics.SetAngularDamping(physics, 0f); - _physics.ApplyLinearImpulse(meteor, -offset.Normalized * component.MeteorVelocity * physics.Mass, body: physics); + _physics.ApplyLinearImpulse(meteor, -offset.Normalized * MeteorVelocity * physics.Mass, body: physics); _physics.ApplyAngularImpulse( meteor, - physics.Mass * ((component.MaxAngularVelocity - component.MinAngularVelocity) * RobustRandom.NextFloat() + component.MinAngularVelocity), + physics.Mass * ((MaxAngularVelocity - MinAngularVelocity) * RobustRandom.NextFloat() + MinAngularVelocity), body: physics); EnsureComp(meteor).Lifetime = 120f; diff --git a/Content.Server/StationEvents/Events/MouseMigrationRule.cs b/Content.Server/StationEvents/Events/MouseMigration.cs similarity index 66% rename from Content.Server/StationEvents/Events/MouseMigrationRule.cs rename to Content.Server/StationEvents/Events/MouseMigration.cs index b0c54a7d0e..e2f2a5b9b0 100644 --- a/Content.Server/StationEvents/Events/MouseMigrationRule.cs +++ b/Content.Server/StationEvents/Events/MouseMigration.cs @@ -1,15 +1,19 @@ using System.Linq; -using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; -public sealed class MouseMigrationRule : StationEventSystem +public sealed class MouseMigration : StationEventSystem { - protected override void Started(EntityUid uid, MouseMigrationRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + 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() { - base.Started(uid, component, gameRule, args); + base.Started(); var modifier = GetSeverityModifier(); @@ -19,9 +23,9 @@ public sealed class MouseMigrationRule : StationEventSystem "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 deleted file mode 100644 index 78a77c9a57..0000000000 --- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Content.Server.Power.Components; -using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Utility; -using System.Threading; -using Content.Server.Power.EntitySystems; -using Timer = Robust.Shared.Timing.Timer; -using System.Linq; -using Content.Server.GameTicking.Rules.Components; -using Robust.Shared.Random; -using Content.Server.Station.Components; -using Content.Server.StationEvents.Components; - -namespace Content.Server.StationEvents.Events -{ - [UsedImplicitly] - public sealed class PowerGridCheckRule : StationEventSystem - { - [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/RandomSentienceRule.cs b/Content.Server/StationEvents/Events/RandomSentience.cs similarity index 67% rename from Content.Server/StationEvents/Events/RandomSentienceRule.cs rename to Content.Server/StationEvents/Events/RandomSentience.cs index 8b2128178a..8181e11c0d 100644 --- a/Content.Server/StationEvents/Events/RandomSentienceRule.cs +++ b/Content.Server/StationEvents/Events/RandomSentience.cs @@ -1,20 +1,22 @@ using System.Linq; using Content.Server.Chat.Systems; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Ghost.Roles.Components; using Content.Server.Station.Systems; using Content.Server.StationEvents.Components; namespace Content.Server.StationEvents.Events; -public sealed class RandomSentienceRule : StationEventSystem +public sealed class RandomSentience : StationEventSystem { - protected override void Started(EntityUid uid, RandomSentienceRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override string Prototype => "RandomSentience"; + + public override void Started() { + base.Started(); HashSet stationsToNotify = new(); var mod = GetSeverityModifier(); - var targetList = EntityQuery().ToList(); + var targetList = EntityManager.EntityQuery().ToList(); RobustRandom.Shuffle(targetList); var toMakeSentient = (int) (RobustRandom.Next(2, 5) * Math.Sqrt(mod)); @@ -25,10 +27,10 @@ public sealed class RandomSentienceRule : StationEventSystem(target.Owner); - var ghostRole = EnsureComp(target.Owner); - EnsureComp(target.Owner); - ghostRole.RoleName = MetaData(target.Owner).EntityName; + EntityManager.RemoveComponent(target.Owner); + var ghostRole = AddComp(target.Owner); + AddComp(target.Owner); + ghostRole.RoleName = EntityManager.GetComponent(target.Owner).EntityName; ghostRole.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", ghostRole.RoleName)); groups.Add(Loc.GetString(target.FlavorKind)); } @@ -41,15 +43,18 @@ public sealed class RandomSentienceRule : StationEventSystem 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 new file mode 100644 index 0000000000..3375b2b3c2 --- /dev/null +++ b/Content.Server/StationEvents/Events/RevenantSpawn.cs @@ -0,0 +1,18 @@ +namespace Content.Server.StationEvents.Events; + +public sealed class RevenantSpawn : StationEventSystem +{ + public override string Prototype => "RevenantSpawn"; + private static readonly string RevenantPrototype = "MobRevenant"; + + public override void Started() + { + base.Started(); + + if (TryFindRandomTile(out _, out _, out _, out var coords)) + { + Sawmill.Info($"Spawning revenant at {coords}"); + EntityManager.SpawnEntity(RevenantPrototype, coords); + } + } +} diff --git a/Content.Server/StationEvents/Events/RevenantSpawnRule.cs b/Content.Server/StationEvents/Events/RevenantSpawnRule.cs deleted file mode 100644 index 6e99fb495e..0000000000 --- a/Content.Server/StationEvents/Events/RevenantSpawnRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; - -namespace Content.Server.StationEvents.Events; - -public sealed class RevenantSpawnRule : StationEventSystem -{ - protected override void Started(EntityUid uid, RevenantSpawnRuleComponent component, GameRuleComponent gameRule, - GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - if (TryFindRandomTile(out _, out _, out _, out var coords)) - { - Sawmill.Info($"Spawning revenant at {coords}"); - Spawn(component.RevenantPrototype, coords); - } - } -} diff --git a/Content.Server/StationEvents/Events/SolarFlare.cs b/Content.Server/StationEvents/Events/SolarFlare.cs new file mode 100644 index 0000000000..cf3a2a403b --- /dev/null +++ b/Content.Server/StationEvents/Events/SolarFlare.cs @@ -0,0 +1,78 @@ +using Content.Server.GameTicking.Rules.Configurations; +using Content.Server.Radio.Components; +using Content.Server.Radio; +using Robust.Shared.Random; +using Content.Server.Light.EntitySystems; +using Content.Server.Light.Components; +using Content.Shared.Radio.Components; +using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; + +namespace Content.Server.StationEvents.Events; + +public sealed class SolarFlare : StationEventSystem +{ + [Dependency] private readonly PoweredLightSystem _poweredLight = default!; + [Dependency] private readonly SharedDoorSystem _door = default!; + + public override string Prototype => "SolarFlare"; + + private SolarFlareEventRuleConfiguration _event = default!; + private float _effectTimer = 0; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(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 deleted file mode 100644 index 206a8ca685..0000000000 --- a/Content.Server/StationEvents/Events/SolarFlareRule.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Radio; -using Robust.Shared.Random; -using Content.Server.Light.EntitySystems; -using Content.Server.Light.Components; -using Content.Server.StationEvents.Components; -using Content.Shared.Radio.Components; -using Content.Shared.Doors.Components; -using Content.Shared.Doors.Systems; - -namespace Content.Server.StationEvents.Events; - -public sealed class SolarFlareRule : StationEventSystem -{ - [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 new file mode 100644 index 0000000000..cb58e438ee --- /dev/null +++ b/Content.Server/StationEvents/Events/SpiderSpawn.cs @@ -0,0 +1,32 @@ +using Content.Server.StationEvents.Components; +using Content.Shared.Actions; +using Robust.Shared.Random; +using System.Linq; + +namespace Content.Server.StationEvents.Events; + +public sealed class SpiderSpawn : StationEventSystem +{ + public override string Prototype => "SpiderSpawn"; + + public override void Started() + { + base.Started(); + var spawnLocations = EntityManager.EntityQuery().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 deleted file mode 100644 index ba440f8cde..0000000000 --- a/Content.Server/StationEvents/Events/SpiderSpawnRule.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Server.StationEvents.Components; -using System.Linq; -using Content.Server.GameTicking.Rules.Components; - -namespace Content.Server.StationEvents.Events; - -public sealed class SpiderSpawnRule : StationEventSystem -{ - 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 bcec380987..bbf73f901f 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -1,197 +1,209 @@ +using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Systems; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Rules.Configurations; using Content.Server.Station.Components; using Content.Server.Station.Systems; -using Content.Server.StationEvents.Components; using Content.Shared.Database; +using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; -namespace Content.Server.StationEvents.Events; - -/// -/// An abstract entity system inherited by all station events for their behavior. -/// -public abstract class StationEventSystem : GameRuleSystem where T : Component +namespace Content.Server.StationEvents.Events { - [Dependency] protected readonly IAdminLogManager AdminLogManager = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] protected readonly IMapManager MapManager = default!; - [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; - [Dependency] protected readonly IRobustRandom RobustRandom = default!; - [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] protected readonly ChatSystem ChatSystem = default!; - [Dependency] protected readonly SharedAudioSystem Audio = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] protected readonly StationSystem StationSystem = default!; - - protected ISawmill Sawmill = default!; - - public override void Initialize() + /// + /// An abstract entity system inherited by all station events for their behavior. + /// + public abstract class StationEventSystem : GameRuleSystem { - base.Initialize(); + [Dependency] protected readonly IRobustRandom RobustRandom = default!; + [Dependency] protected readonly IAdminLogManager AdminLogManager = default!; + [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] protected readonly ChatSystem ChatSystem = default!; + [Dependency] protected readonly StationSystem StationSystem = default!; - Sawmill = Logger.GetSawmill("stationevents"); - } + protected ISawmill Sawmill = default!; - /// - protected override void Added(EntityUid uid, T component, GameRuleComponent gameRule, GameRuleAddedEvent args) - { - base.Added(uid, component, gameRule, args); + /// + /// How long has the event existed. Do not change this. + /// + protected float Elapsed { get; set; } - if (!TryComp(uid, out var stationEvent)) - return; - - AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {ToPrettyString(uid)}"); - - if (stationEvent.StartAnnouncement != null) + public override void Initialize() { - ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.StartAnnouncement), playSound: false, colorOverride: Color.Gold); + base.Initialize(); + + Sawmill = Logger.GetSawmill("stationevents"); } - 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) + /// + /// Called once to setup the event after StartAfter has elapsed, or if an event is forcibly started. + /// + public override void Started() { - ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(stationEvent.EndAnnouncement), playSound: false, colorOverride: Color.Gold); + AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {Configuration.Id}"); } - Audio.PlayGlobal(stationEvent.EndAudio, Filter.Broadcast(), true); + /// + /// 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 } /// - /// Called every tick when this event is running. - /// Events are responsible for their own lifetime, so this handles starting and ending after time. + /// 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 override void Update(float frameTime) + public sealed class GetSeverityModifierEvent : EntityEventArgs { - 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); - } - } + /// + /// Should be multiplied/added to rather than set, for commutativity. + /// + public float Modifier = 1.0f; } - - #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 -{ - /// - /// Should be multiplied/added to rather than set, for commutativity. - /// - public float Modifier = 1.0f; } diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClog.cs similarity index 73% rename from Content.Server/StationEvents/Events/VentClogRule.cs rename to Content.Server/StationEvents/Events/VentClog.cs index c209337aec..04e466f66b 100644 --- a/Content.Server/StationEvents/Events/VentClogRule.cs +++ b/Content.Server/StationEvents/Events/VentClog.cs @@ -8,19 +8,23 @@ using Robust.Shared.Random; using System.Linq; using Content.Server.Chemistry.Components; using Content.Server.Fluids.EntitySystems; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; +using Robust.Server.GameObjects; namespace Content.Server.StationEvents.Events; [UsedImplicitly] -public sealed class VentClogRule : StationEventSystem +public sealed class VentClog : StationEventSystem { - [Dependency] private readonly SmokeSystem _smoke = default!; + public override string Prototype => "VentClog"; - protected override void Started(EntityUid uid, VentClogRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public readonly IReadOnlyList SafeishVentChemicals = new[] { - base.Started(uid, component, gameRule, args); + "Water", "Blood", "Slime", "SpaceDrugs", "SpaceCleaner", "Nutriment", "Sugar", "SpaceLube", "Ephedrine", "Ale", "Beer" + }; + + public override void Started() + { + base.Started(); if (StationSystem.Stations.Count == 0) return; @@ -53,14 +57,15 @@ public sealed class VentClogRule : StationEventSystem } else { - solution.AddReagent(RobustRandom.Pick(component.SafeishVentChemicals), 200); + solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 200); } var foamEnt = Spawn("Foam", transform.Coordinates); var smoke = EnsureComp(foamEnt); smoke.SpreadAmount = 20; - _smoke.Start(foamEnt, smoke, solution, 20f); - Audio.PlayPvs(sound, transform.Coordinates); + EntityManager.System().Start(foamEnt, smoke, solution, 20f); + EntityManager.System().PlayPvs(sound, transform.Coordinates); } } + } diff --git a/Content.Server/StationEvents/Events/VentCritters.cs b/Content.Server/StationEvents/Events/VentCritters.cs new file mode 100644 index 0000000000..5d61e10c71 --- /dev/null +++ b/Content.Server/StationEvents/Events/VentCritters.cs @@ -0,0 +1,34 @@ +using Content.Server.StationEvents.Components; +using Content.Shared.Actions; +using Robust.Shared.Random; +using System.Linq; + +namespace Content.Server.StationEvents.Events; + +public sealed class VentCritters : StationEventSystem +{ + public static List 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 deleted file mode 100644 index 696828838d..0000000000 --- a/Content.Server/StationEvents/Events/VentCrittersRule.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Server.StationEvents.Components; -using Robust.Shared.Random; -using System.Linq; -using Content.Server.GameTicking.Rules.Components; - -namespace Content.Server.StationEvents.Events; - -public sealed class VentCrittersRule : StationEventSystem -{ - 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 5c972df52e..d22e2d86dc 100644 --- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs @@ -1,7 +1,5 @@ using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; using Content.Server.StationEvents.Events; using Content.Shared.CCVar; using Robust.Shared.Configuration; @@ -9,20 +7,35 @@ using Robust.Shared.Random; namespace Content.Server.StationEvents; -public sealed class RampingStationEventSchedulerSystem : GameRuleSystem +public sealed class RampingStationEventSchedulerSystem : GameRuleSystem { + public override string Prototype => "RampingStationEventScheduler"; + [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EventManagerSystem _event = default!; [Dependency] private readonly GameTicker _gameTicker = default!; - public float GetChaosModifier(EntityUid uid, RampingStationEventSchedulerComponent component) - { - var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds; - if (roundTime > component.EndTime) - return component.MaxChaos; + [ViewVariables(VVAccess.ReadWrite)] + private float _endTime; + [ViewVariables(VVAccess.ReadWrite)] + private float _maxChaos; + [ViewVariables(VVAccess.ReadWrite)] + private float _startingChaos; + [ViewVariables(VVAccess.ReadWrite)] + private float _timeUntilNextEvent; - return component.MaxChaos / component.EndTime * roundTime + component.StartingChaos; + [ViewVariables] + public float ChaosModifier + { + get + { + var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds; + if (roundTime > _endTime) + return _maxChaos; + + return (_maxChaos / _endTime) * roundTime + _startingChaos; + } } public override void Initialize() @@ -32,65 +45,60 @@ public sealed class RampingStationEventSchedulerSystem : GameRuleSystem(OnGetSeverityModifier); } - protected override void Started(EntityUid uid, RampingStationEventSchedulerComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + public override void Started() { - base.Started(uid, component, gameRule, args); - var avgChaos = _cfg.GetCVar(CCVars.EventsRampingAverageChaos); var avgTime = _cfg.GetCVar(CCVars.EventsRampingAverageEndTime); // Worlds shittiest probability distribution // Got a complaint? Send them to - component.MaxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4); + _maxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4); // This is in minutes, so *60 for seconds (for the chaos calc) - component.EndTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f; - component.StartingChaos = component.MaxChaos / 10; + _endTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f; + _startingChaos = _maxChaos / 10; - PickNextEventTime(uid, component); + PickNextEventTime(); + } + + public override void Ended() + { + _endTime = 0f; + _maxChaos = 0f; + _startingChaos = 0f; + _timeUntilNextEvent = 0f; } public override void Update(float frameTime) { base.Update(frameTime); - if (!_event.EventsEnabled) + if (!RuleStarted || !_event.EventsEnabled) return; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var scheduler, out var gameRule)) + if (_timeUntilNextEvent > 0f) { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; - - if (scheduler.TimeUntilNextEvent > 0f) - { - scheduler.TimeUntilNextEvent -= frameTime; - return; - } - - PickNextEventTime(uid, scheduler); - _event.RunRandomEvent(); + _timeUntilNextEvent -= frameTime; + return; } + + PickNextEventTime(); + _event.RunRandomEvent(); } private void OnGetSeverityModifier(GetSeverityModifierEvent ev) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var scheduler, out var gameRule)) - { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; + if (!RuleStarted) + return; - ev.Modifier *= GetChaosModifier(uid, scheduler); - Logger.Info($"Ramping set modifier to {ev.Modifier}"); - } + ev.Modifier *= ChaosModifier; + Logger.Info($"Ramping set modifier to {ev.Modifier}"); } - private void PickNextEventTime(EntityUid uid, RampingStationEventSchedulerComponent component) + private void PickNextEventTime() { - var mod = GetChaosModifier(uid, component); + var mod = ChaosModifier; // 4-12 minutes baseline. Will get faster over time as the chaos mod increases. - component.TimeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod); + _timeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod); } } diff --git a/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs b/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs new file mode 100644 index 0000000000..7c07cbd8df --- /dev/null +++ b/Content.Server/Suspicion/Roles/SuspicionInnocentRole.cs @@ -0,0 +1,34 @@ +using Content.Server.Chat.Managers; +using Content.Shared.Roles; + +namespace Content.Server.Suspicion.Roles +{ + public sealed class SuspicionInnocentRole : SuspicionRole + { + public AntagPrototype Prototype { get; } + + public SuspicionInnocentRole(Mind.Mind mind, AntagPrototype antagPrototype) : base(mind) + { + Prototype = antagPrototype; + Name = Loc.GetString(antagPrototype.Name); + Antagonist = antagPrototype.Antagonist; + } + + public override string Name { get; } + public string Objective => Loc.GetString(Prototype.Objective); + public override bool Antagonist { get; } + + public override void Greet() + { + base.Greet(); + + var chat = IoCManager.Resolve(); + + if (Mind.TryGetSession(out var session)) + { + chat.DispatchServerMessage(session, $"You're an {Name}!"); + chat.DispatchServerMessage(session, $"Objective: {Objective}"); + } + } + } +} diff --git a/Content.Server/Suspicion/Roles/SuspicionRole.cs b/Content.Server/Suspicion/Roles/SuspicionRole.cs new file mode 100644 index 0000000000..f63cacb6e2 --- /dev/null +++ b/Content.Server/Suspicion/Roles/SuspicionRole.cs @@ -0,0 +1,9 @@ +using Content.Server.Roles; + +namespace Content.Server.Suspicion.Roles +{ + public abstract class SuspicionRole : Role + { + protected SuspicionRole(Mind.Mind mind) : base(mind) { } + } +} diff --git a/Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs b/Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs new file mode 100644 index 0000000000..b42b22da42 --- /dev/null +++ b/Content.Server/Suspicion/Roles/SuspicionTraitorRole.cs @@ -0,0 +1,41 @@ +using System.Linq; +using Content.Server.Chat.Managers; +using Content.Shared.Roles; + +namespace Content.Server.Suspicion.Roles +{ + public sealed class SuspicionTraitorRole : SuspicionRole + { + public AntagPrototype Prototype { get; } + + public SuspicionTraitorRole(Mind.Mind mind, AntagPrototype antagPrototype) : base(mind) + { + Prototype = antagPrototype; + Name = Loc.GetString(antagPrototype.Name); + Antagonist = antagPrototype.Antagonist; + } + + public override string Name { get; } + public string Objective => Loc.GetString(Prototype.Objective); + public override bool Antagonist { get; } + + public void GreetSuspicion(List traitors, IChatManager chatMgr) + { + if (Mind.TryGetSession(out var session)) + { + chatMgr.DispatchServerMessage(session, Loc.GetString("suspicion-role-greeting", ("roleName", Name))); + chatMgr.DispatchServerMessage(session, Loc.GetString("suspicion-objective", ("objectiveText", Objective))); + + var allPartners = string.Join(", ", traitors.Where(p => p != this).Select(p => p.Mind.CharacterName)); + + var partnerText = Loc.GetString( + "suspicion-partners-in-crime", + ("partnersCount", traitors.Count-1), + ("partnerNames", allPartners) + ); + + chatMgr.DispatchServerMessage(session, partnerText); + } + } + } +} diff --git a/Content.Server/Suspicion/SuspicionItemComponent.cs b/Content.Server/Suspicion/SuspicionItemComponent.cs new file mode 100644 index 0000000000..cca978d25f --- /dev/null +++ b/Content.Server/Suspicion/SuspicionItemComponent.cs @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000000..59964254c8 --- /dev/null +++ b/Content.Server/Suspicion/SuspicionRoleComponent.cs @@ -0,0 +1,138 @@ +using System.Linq; +using Content.Server.GameTicking.Rules; +using Content.Server.Mind.Components; +using Content.Server.Roles; +using Content.Server.Suspicion.Roles; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Suspicion; + +namespace Content.Server.Suspicion +{ + [RegisterComponent] + public sealed class SuspicionRoleComponent : SharedSuspicionRoleComponent + { + [Dependency] private readonly IEntityManager _entMan = default!; + private Role? _role; + [ViewVariables] + private readonly HashSet _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 new file mode 100644 index 0000000000..d23269b95c --- /dev/null +++ b/Content.Server/Suspicion/SuspicionRoleSystem.cs @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000000..9ea08c4204 --- /dev/null +++ b/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchRedemptionComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.TraitorDeathMatch.Components +{ + [RegisterComponent] + public sealed class TraitorDeathMatchRedemptionComponent : Component + { + } +} diff --git a/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs b/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs new file mode 100644 index 0000000000..820ccbc425 --- /dev/null +++ b/Content.Server/TraitorDeathMatch/Components/TraitorDeathMatchReliableOwnerTagComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Network; + +namespace Content.Server.TraitorDeathMatch.Components +{ + [RegisterComponent] + public sealed class TraitorDeathMatchReliableOwnerTagComponent : Component + { + [ViewVariables] + public NetUserId? UserId { get; set; } + } +} + diff --git a/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs b/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs new file mode 100644 index 0000000000..9def8fc316 --- /dev/null +++ b/Content.Server/TraitorDeathMatch/TraitorDeathMatchRedemptionSystem.cs @@ -0,0 +1,108 @@ +using Content.Server.Mind.Components; +using Content.Server.TraitorDeathMatch.Components; +using Content.Server.Store.Components; +using Content.Server.Store.Systems; +using Content.Server.Traitor.Uplink; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Content.Shared.Inventory; +using Content.Shared.Popups; + +namespace Content.Server.TraitorDeathMatch; + +public sealed class TraitorDeathMatchRedemptionSystem : EntitySystem +{ + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly UplinkSystem _uplink = default!; + [Dependency] private readonly StoreSystem _store = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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 new file mode 100644 index 0000000000..09d8a6cd59 --- /dev/null +++ b/Content.Shared/Suspicion/SharedSuspicionRoleComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Suspicion +{ + [NetworkedComponent()] + public abstract class SharedSuspicionRoleComponent : Component + { + } + + [Serializable, NetSerializable] + public sealed class SuspicionRoleComponentState : ComponentState + { + public readonly string? Role; + public readonly bool? Antagonist; + public readonly (string name, EntityUid)[] Allies; + + public SuspicionRoleComponentState(string? role, bool? antagonist, (string name, EntityUid)[] allies) + { + Role = role; + Antagonist = antagonist; + Allies = allies; + } + } +} diff --git a/Content.Shared/Suspicion/SuspicionMessages.cs b/Content.Shared/Suspicion/SuspicionMessages.cs new file mode 100644 index 0000000000..7254e2d177 --- /dev/null +++ b/Content.Shared/Suspicion/SuspicionMessages.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Suspicion +{ + public static class SuspicionMessages + { + [Serializable, NetSerializable] + public sealed class SetSuspicionEndTimerMessage : EntityEventArgs + { + public TimeSpan? EndTime; + } + } +} diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml new file mode 100644 index 0000000000..e4c804ad55 --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/Spawners/Conditional/traitordm.yml @@ -0,0 +1,16 @@ +- type: entity + name: PDA Redemption Machine Spawner + id: TraitorDMRedemptionMachineSpawner + parent: MarkerBase + components: + - type: Sprite + layers: + - state: blue + - sprite: Structures/Machines/traitordm.rsi + state: redemption + - type: ConditionalSpawner + prototypes: + - TraitorDMRedemptionMachine + chance: 1.0 + gameRules: + - TraitorDeathMatch diff --git a/Resources/Prototypes/Entities/Structures/Machines/traitordm.yml b/Resources/Prototypes/Entities/Structures/Machines/traitordm.yml new file mode 100644 index 0000000000..7c15a0989d --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Machines/traitordm.yml @@ -0,0 +1,28 @@ +- type: entity + id: TraitorDMRedemptionMachine + parent: BaseMachinePowered + name: traitor deathmatch pda redemption machine + description: Put someone else's PDA into this to get telecrystals. + components: + - type: Sprite + layers: + - sprite: Structures/Machines/traitordm.rsi + state: redemption + - sprite: Structures/Machines/traitordm.rsi + state: redemption-unshaded + shader: unshaded + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.25,-0.25,0.25,0.25" + density: 190 + mask: + - MachineMask + layer: + - MachineLayer + - type: TraitorDeathMatchRedemption + placement: + mode: AlignTileAny diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 5b54e1a60d..8a2c7b71b3 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -1,116 +1,97 @@ -- type: entity +- type: gameRule id: AnomalySpawn - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: AnomalySpawn weight: 10 startAfter: 30 - duration: 35 - - type: AnomalySpawnRule + endAfter: 35 -- type: entity +- type: gameRule id: BluespaceArtifact - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: BluespaceArtifact weight: 5 startAfter: 30 - duration: 35 - - type: BluespaceArtifactRule + endAfter: 35 -- type: entity - id: BluespaceLocker - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent +- type: gameRule + id: BluespaceLockerLink + config: + !type:StationEventRuleConfiguration + id: BluespaceLockerLink weight: 0 reoccurrenceDelay: 5 earliestStart: 1 - duration: 1 - - type: BluespaceLockerRule + endAfter: 1 -- type: entity +- type: gameRule id: BreakerFlip - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: BreakerFlip weight: 10 - duration: 1 + endAfter: 1 minimumPlayers: 15 - - type: BreakerFlipRule -- type: entity +- type: gameRule id: BureaucraticError - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: BureaucraticError startAnnouncement: station-event-bureaucratic-error-announcement minimumPlayers: 25 weight: 5 - duration: 1 - - type: BureaucraticErrorRule + endAfter: 1 -- type: entity +- type: gameRule id: DiseaseOutbreak - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration startAnnouncement: station-event-disease-outbreak-announcement startAudio: path: /Audio/Announcements/outbreak7.ogg params: volume: -4 + id: DiseaseOutbreak weight: 5 - duration: 1 + endAfter: 1 earliestStart: 15 - - type: DiseaseOutbreakRule -- type: entity +- type: gameRule id: Dragon - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: Dragon weight: 10 - duration: 2 + endAfter: 2 earliestStart: 15 minimumPlayers: 15 - - type: DragonRule -- type: entity +- type: gameRule id: RevenantSpawn - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: RevenantSpawn weight: 5 - duration: 1 + endAfter: 1 earliestStart: 45 minimumPlayers: 20 - - type: RevenantSpawnRule -- type: entity +- type: gameRule id: FalseAlarm - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: FalseAlarm weight: 15 - duration: 1 - - type: FalseAlarmRule + endAfter: 1 -- type: entity +- type: gameRule id: GasLeak - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: GasLeak startAnnouncement: station-event-gas-leak-start-announcement startAudio: path: /Audio/Announcements/attention.ogg @@ -119,27 +100,23 @@ minimumPlayers: 5 weight: 5 startAfter: 20 - - type: GasLeakRule -- type: entity +- type: gameRule id: KudzuGrowth - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: KudzuGrowth earliestStart: 15 minimumPlayers: 15 weight: 5 startAfter: 50 - duration: 240 - - type: KudzuGrowthRule + endAfter: 240 -- type: entity +- type: gameRule id: MeteorSwarm - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: MeteorSwarm earliestStart: 30 weight: 5 minimumPlayers: 20 @@ -150,26 +127,22 @@ params: volume: -4 startAfter: 30 - - type: MeteorSwarmRule -- type: entity +- type: gameRule id: MouseMigration - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: MouseMigration earliestStart: 30 minimumPlayers: 35 weight: 5 - duration: 50 - - type: MouseMigrationRule + endAfter: 50 -- type: entity +- type: gameRule id: PowerGridCheck - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: PowerGridCheck weight: 10 startAnnouncement: station-event-power-grid-check-start-announcement endAnnouncement: station-event-power-grid-check-end-announcement @@ -178,36 +151,28 @@ params: volume: -4 startAfter: 12 - duration: 60 - maxDuration: 120 - - type: PowerGridCheckRule -- type: entity +- type: gameRule id: RandomSentience - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: RandomSentience weight: 10 - duration: 1 + endAfter: 1 startAudio: path: /Audio/Announcements/attention.ogg - - type: RandomSentienceRule -- type: entity +- type: gameRule id: SolarFlare - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: !type:SolarFlareEventRuleConfiguration + id: SolarFlare weight: 10 startAnnouncement: station-event-solar-flare-start-announcement endAnnouncement: station-event-solar-flare-end-announcement startAudio: path: /Audio/Announcements/attention.ogg - duration: 120 - maxDuration: 240 - - type: SolarFlareRule + minEndAfter: 120 + maxEndAfter: 240 onlyJamHeadsets: true affectedChannels: - Common @@ -215,12 +180,11 @@ lightBreakChancePerSecond: 0.0003 doorToggleChancePerSecond: 0.001 -- type: entity +- type: gameRule id: VentClog - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: VentClog startAnnouncement: station-event-vent-clog-start-announcement startAudio: path: /Audio/Announcements/attention.ogg @@ -228,55 +192,44 @@ minimumPlayers: 15 weight: 5 startAfter: 50 - duration: 60 - - type: VentClogRule + endAfter: 60 -- type: entity +- type: gameRule id: VentCritters - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration id: VentCritters earliestStart: 15 minimumPlayers: 15 weight: 5 - duration: 60 - - type: VentCrittersRule + endAfter: 60 -- type: entity +- type: gameRule id: SpiderSpawn - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: SpiderSpawn earliestStart: 20 minimumPlayers: 15 weight: 5 - duration: 60 - - type: SpiderSpawnRule + endAfter: 60 -- type: entity +- type: gameRule id: ZombieOutbreak - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: Zombie earliestStart: 50 weight: 2.5 - duration: 1 - - type: ZombieRule + endAfter: 1 -- type: entity +- type: gameRule id: LoneOpsSpawn - parent: BaseGameRule - noSpawn: true - components: - - type: StationEvent + config: + !type:StationEventRuleConfiguration + id: LoneOpsSpawn earliestStart: 55 weight: 5 minimumPlayers: 10 reoccurrenceDelay: 25 - duration: 1 - - type: LoneOpsSpawnRule - - type: NukeopsRule + endAfter: 1 diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 0d493ba658..5a62ca2706 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -1,88 +1,80 @@ -- type: entity - id: BaseGameRule - abstract: true - noSpawn: true - components: - - type: GameRule - -- type: entity +- type: gameRule id: DeathMatch - parent: BaseGameRule - noSpawn: true - components: - - type: DeathMatchRule + config: + !type:GenericGameRuleConfiguration + id: DeathMatch -- type: entity +- type: gameRule id: InactivityTimeRestart - parent: BaseGameRule - noSpawn: true - components: - - type: InactivityRule + config: + !type:InactivityGameRuleConfiguration inactivityMaxTime: 600 roundEndDelay: 10 -- type: entity +- type: gameRule id: MaxTimeRestart - parent: BaseGameRule - noSpawn: true - components: - - type: MaxTimeRestartRule + config: + !type:MaxTimeRestartRuleConfiguration roundMaxTime: 300 roundEndDelay: 10 -- type: entity +- type: gameRule id: Nukeops - parent: BaseGameRule - noSpawn: true - components: - - type: NukeopsRule + config: + !type:NukeopsRuleConfiguration + id: Nukeops -- type: entity +- type: gameRule id: Pirates - parent: BaseGameRule - noSpawn: true - components: - - type: PiratesRule + config: + !type:GenericGameRuleConfiguration + id: Pirates -- type: entity +- type: gameRule + id: Suspicion + config: + !type:GenericGameRuleConfiguration + id: Suspicion + +- type: gameRule id: Traitor - parent: BaseGameRule - noSpawn: true - components: - - type: TraitorRule + config: + !type:GenericGameRuleConfiguration + id: Traitor -- type: entity +- type: gameRule + id: TraitorDeathMatch + config: + !type:GenericGameRuleConfiguration + id: TraitorDeathMatch + +- type: gameRule id: Sandbox - parent: BaseGameRule - noSpawn: true - components: - - type: SandboxRule + config: + !type:GenericGameRuleConfiguration + id: Sandbox -- type: entity +- type: gameRule id: Secret - parent: BaseGameRule - noSpawn: true - components: - - type: SecretRule + config: + !type:GenericGameRuleConfiguration + id: Secret -- type: entity +- type: gameRule id: Zombie - parent: BaseGameRule - noSpawn: true - components: - - type: ZombieRule + config: + !type:GenericGameRuleConfiguration + id: Zombie # event schedulers -- type: entity +- type: gameRule id: BasicStationEventScheduler - parent: BaseGameRule - noSpawn: true - components: - - type: BasicStationEventScheduler + config: + !type:GenericGameRuleConfiguration + id: BasicStationEventScheduler -- type: entity +- type: gameRule id: RampingStationEventScheduler - parent: BaseGameRule - noSpawn: true - components: - - type: RampingStationEventScheduler + config: + !type:GenericGameRuleConfiguration + id: RampingStationEventScheduler diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml new file mode 100644 index 0000000000..9efbb6f6ab --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_innocent.yml @@ -0,0 +1,6 @@ +- type: antag + id: SuspicionInnocent + name: roles-antag-suspicion-innocent-name + antagonist: false + setPreference: false + objective: roles-antag-suspicion-innocent-objective diff --git a/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml new file mode 100644 index 0000000000..27e47cacb2 --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/Suspicion/suspicion_traitor.yml @@ -0,0 +1,6 @@ +- 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 b6e2044761..f56452f20b 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -52,6 +52,16 @@ - Traitor - BasicStationEventScheduler +- type: gamePreset + id: Suspicion + alias: + - suspicion + - sus + name: suspicion-title + description: suspicion-description + rules: + - Suspicion + - type: gamePreset id: Deathmatch alias: @@ -62,6 +72,17 @@ 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: