diff --git a/Content.Client/Administration/UI/Tabs/AdminbusTab/AdminbusTab.xaml b/Content.Client/Administration/UI/Tabs/AdminbusTab/AdminbusTab.xaml
index 9e6ef34cad..8dda6b4e3c 100644
--- a/Content.Client/Administration/UI/Tabs/AdminbusTab/AdminbusTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminbusTab/AdminbusTab.xaml
@@ -12,6 +12,5 @@
-
diff --git a/Content.Client/Administration/UI/Tabs/AdminbusTab/StationEventsWindow.xaml b/Content.Client/Administration/UI/Tabs/AdminbusTab/StationEventsWindow.xaml
deleted file mode 100644
index 0ba2a38c4a..0000000000
--- a/Content.Client/Administration/UI/Tabs/AdminbusTab/StationEventsWindow.xaml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Administration/UI/Tabs/AdminbusTab/StationEventsWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/AdminbusTab/StationEventsWindow.xaml.cs
deleted file mode 100644
index 62048c45ab..0000000000
--- a/Content.Client/Administration/UI/Tabs/AdminbusTab/StationEventsWindow.xaml.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Content.Client.StationEvents.Managers;
-using JetBrains.Annotations;
-using Robust.Client.AutoGenerated;
-using Robust.Client.Console;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-
-namespace Content.Client.Administration.UI.Tabs.AdminbusTab
-{
- [GenerateTypedNameReferences]
- [UsedImplicitly]
- public sealed partial class StationEventsWindow : DefaultWindow
- {
- private List? _data;
-
- [Dependency]
- private readonly IStationEventManager _eventManager = default!;
-
- public StationEventsWindow()
- {
- IoCManager.InjectDependencies(this);
-
- MinSize = SetSize = (300, 200);
- RobustXamlLoader.Load(this);
- }
-
- protected override void EnteredTree()
- {
- _eventManager.OnStationEventsReceived += OnStationEventsReceived;
- _eventManager.RequestEvents();
-
- EventsOptions.AddItem(Loc.GetString("station-events-window-not-loaded-text"));
- }
-
- private void OnStationEventsReceived()
- {
- // fill events dropdown
- _data = _eventManager.StationEvents.ToList();
- EventsOptions.Clear();
- foreach (var stationEvent in _data)
- {
- EventsOptions.AddItem(stationEvent);
- }
- EventsOptions.AddItem(Loc.GetString("station-events-window-random-text"));
-
- // Enable all UI elements
- EventsOptions.Disabled = false;
- PauseButton.Disabled = false;
- ResumeButton.Disabled = false;
- SubmitButton.Disabled = false;
-
- // Subscribe to UI events
- EventsOptions.OnItemSelected += eventArgs => EventsOptions.SelectId(eventArgs.Id);
- PauseButton.OnPressed += PauseButtonOnOnPressed;
- ResumeButton.OnPressed += ResumeButtonOnOnPressed;
- SubmitButton.OnPressed += SubmitButtonOnOnPressed;
- }
-
- private static void PauseButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
- {
- IoCManager.Resolve().ExecuteCommand("events pause");
- }
-
- private static void ResumeButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
- {
- IoCManager.Resolve().ExecuteCommand("events resume");
- }
-
- private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
- {
- if (_data == null)
- return;
-
- // random is always last option
- var id = EventsOptions.SelectedId;
- var selectedEvent = id < _data.Count ? _data[id] : "random";
-
- IoCManager.Resolve().ExecuteCommand($"events run \"{selectedEvent}\"");
- }
- }
-}
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index a26fc40c02..066eafb053 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -17,11 +17,10 @@ using Content.Client.MobState.Overlays;
using Content.Client.Parallax;
using Content.Client.Parallax.Managers;
using Content.Client.Preferences;
+using Content.Client.Radiation;
using Content.Client.Sandbox;
using Content.Client.Screenshot;
using Content.Client.Singularity;
-using Content.Client.StationEvents;
-using Content.Client.StationEvents.Managers;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
@@ -192,7 +191,6 @@ namespace Content.Client.Entry
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
- IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize();
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 148a325d83..db2db9b101 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -13,7 +13,6 @@ using Content.Client.Module;
using Content.Client.Parallax.Managers;
using Content.Client.Preferences;
using Content.Client.Screenshot;
-using Content.Client.StationEvents.Managers;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
@@ -37,7 +36,6 @@ namespace Content.Client.IoC
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
- IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
IoCManager.Register();
diff --git a/Content.Client/StationEvents/RadiationPulseComponent.cs b/Content.Client/Radiation/RadiationPulseComponent.cs
similarity index 92%
rename from Content.Client/StationEvents/RadiationPulseComponent.cs
rename to Content.Client/Radiation/RadiationPulseComponent.cs
index fe8ce7e622..b260c41147 100644
--- a/Content.Client/StationEvents/RadiationPulseComponent.cs
+++ b/Content.Client/Radiation/RadiationPulseComponent.cs
@@ -1,8 +1,6 @@
-using System;
using Content.Shared.Radiation;
-using Robust.Shared.GameObjects;
-namespace Content.Client.StationEvents
+namespace Content.Client.Radiation
{
[RegisterComponent]
[ComponentReference(typeof(SharedRadiationPulseComponent))]
diff --git a/Content.Client/StationEvents/RadiationPulseOverlay.cs b/Content.Client/Radiation/RadiationPulseOverlay.cs
similarity index 97%
rename from Content.Client/StationEvents/RadiationPulseOverlay.cs
rename to Content.Client/Radiation/RadiationPulseOverlay.cs
index d80a17ab67..cc00348586 100644
--- a/Content.Client/StationEvents/RadiationPulseOverlay.cs
+++ b/Content.Client/Radiation/RadiationPulseOverlay.cs
@@ -1,16 +1,11 @@
-using System;
-using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
using Robust.Shared.Map;
-using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
-namespace Content.Client.StationEvents
+namespace Content.Client.Radiation
{
public sealed class RadiationPulseOverlay : Overlay
{
diff --git a/Content.Client/StationEvents/Managers/IStationEventManager.cs b/Content.Client/StationEvents/Managers/IStationEventManager.cs
deleted file mode 100644
index 84139de958..0000000000
--- a/Content.Client/StationEvents/Managers/IStationEventManager.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Content.Client.StationEvents.Managers
-{
- public interface IStationEventManager
- {
- public IReadOnlyList StationEvents { get; }
- public void Initialize();
- public event Action OnStationEventsReceived;
- public void RequestEvents();
- }
-}
diff --git a/Content.Client/StationEvents/Managers/StationEventManager.cs b/Content.Client/StationEvents/Managers/StationEventManager.cs
deleted file mode 100644
index d43dda09f3..0000000000
--- a/Content.Client/StationEvents/Managers/StationEventManager.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Content.Shared.StationEvents;
-using Robust.Shared.IoC;
-using Robust.Shared.Network;
-
-namespace Content.Client.StationEvents.Managers
-{
- internal sealed class StationEventManager : IStationEventManager
- {
- [Dependency] private readonly IClientNetManager _netManager = default!;
-
- private readonly List _events = new();
- public IReadOnlyList StationEvents => _events;
- public event Action? OnStationEventsReceived;
-
- public void Initialize()
- {
- _netManager.RegisterNetMessage();
- _netManager.RegisterNetMessage(RxStationEvents);
- _netManager.Disconnect += OnNetManagerOnDisconnect;
- }
-
- private void OnNetManagerOnDisconnect(object? sender, NetDisconnectedArgs msg)
- {
- _events.Clear();
- }
-
- private void RxStationEvents(MsgStationEvents msg)
- {
- _events.Clear();
- _events.AddRange(msg.Events);
- OnStationEventsReceived?.Invoke();
- }
-
- public void RequestEvents()
- {
- _netManager.ClientSendMessage(_netManager.CreateNetMessage());
- }
- }
-}
diff --git a/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs
new file mode 100644
index 0000000000..2e9f31f6bb
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameRules/StartEndGameRulesTest.cs
@@ -0,0 +1,54 @@
+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.IoC;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.GameRules;
+
+///
+/// Tests that all game rules can be added/started/ended at the same time without exceptions.
+///
+[TestFixture]
+public sealed class StartEndGameRulesTest
+{
+ [Test]
+ public async Task Test()
+ {
+ await using var pairTracker = await PoolManager.GetServerClient();
+ var server = pairTracker.Pair.Server;
+
+ await server.WaitAssertion(() =>
+ {
+ var gameTicker = EntitySystem.Get();
+ var protoMan = IoCManager.Resolve();
+
+ var rules = protoMan.EnumeratePrototypes().ToArray();
+
+ // Start all rules
+ foreach (var rule in rules)
+ {
+ gameTicker.StartGameRule(rule);
+ }
+
+ Assert.That(gameTicker.AddedGameRules.Count == rules.Length);
+ });
+
+ // Wait three ticks for any random update loops that might happen
+ await server.WaitRunTicks(3);
+
+ await server.WaitAssertion(() =>
+ {
+ var gameTicker = EntitySystem.Get();
+
+ // End all rules
+ gameTicker.ClearGameRules();
+ Assert.That(!gameTicker.AddedGameRules.Any());
+ });
+
+ await pairTracker.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs b/Content.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs
deleted file mode 100644
index ffd4e5f183..0000000000
--- a/Content.IntegrationTests/Tests/StationEvents/StationEventsSystemTest.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System.Threading.Tasks;
-using Content.Server.StationEvents;
-using Content.Shared.GameTicking;
-using NUnit.Framework;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Timing;
-
-namespace Content.IntegrationTests.Tests.StationEvents
-{
- [TestFixture]
- public sealed class StationEventsSystemTest
- {
- [Test]
- public async Task Test()
- {
- await using var pairTracker = await PoolManager.GetServerClient();
- var server = pairTracker.Pair.Server;
-
- await server.WaitAssertion(() =>
- {
- // Idle each event
- var stationEventsSystem = EntitySystem.Get();
- var dummyFrameTime = (float) IoCManager.Resolve().TickPeriod.TotalSeconds;
-
- foreach (var stationEvent in stationEventsSystem.StationEvents)
- {
- stationEvent.Announce();
- stationEvent.Update(dummyFrameTime);
- stationEvent.Startup();
- stationEvent.Update(dummyFrameTime);
- stationEvent.Running = false;
- stationEvent.Shutdown();
- // Due to timings some events might startup twice when in reality they wouldn't.
- Assert.That(stationEvent.Occurrences > 0);
- }
-
- stationEventsSystem.Reset(new RoundRestartCleanupEvent());
-
- foreach (var stationEvent in stationEventsSystem.StationEvents)
- {
- Assert.That(stationEvent.Occurrences, Is.EqualTo(0));
- }
- });
-
- await pairTracker.CleanReturnAsync();
- }
- }
-}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
index ee9e57ffda..8eaac8ebfb 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
@@ -124,6 +124,9 @@ public partial class AtmosphereSystem
public bool IsTileAirBlocked(EntityUid gridUid, Vector2i tile, AtmosDirection directions = AtmosDirection.All, IMapGridComponent? mapGridComp = null)
{
var ev = new IsTileAirBlockedMethodEvent(gridUid, tile, directions, mapGridComp);
+ RaiseLocalEvent(gridUid, ref ev);
+
+ // If nothing handled the event, it'll default to true.
return ev.Result;
}
diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs
index 0f18d08090..c0b382af2d 100644
--- a/Content.Server/GameTicking/GameTicker.GameRule.cs
+++ b/Content.Server/GameTicking/GameTicker.GameRule.cs
@@ -1,6 +1,7 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking.Rules;
+using Content.Server.GameTicking.Rules.Configurations;
using Content.Shared.Administration;
using Robust.Shared.Console;
@@ -10,10 +11,26 @@ namespace Content.Server.GameTicking
{
// No duplicates.
[ViewVariables] private readonly HashSet _addedGameRules = new();
- public IEnumerable AddedGameRules => _addedGameRules;
+
+ ///
+ /// Holds all currently added game rules.
+ ///
+ public IReadOnlySet AddedGameRules => _addedGameRules;
[ViewVariables] private readonly HashSet _startedGameRules = new();
- public IEnumerable StartedGameRules => _startedGameRules;
+
+ ///
+ /// Holds all currently started game rules.
+ ///
+ public IReadOnlySet StartedGameRules => _startedGameRules;
+
+ [ViewVariables] private readonly List<(TimeSpan, GameRulePrototype)> _allPreviousGameRules = new();
+
+ ///
+ /// A list storing the start times of all game rules that have been started this round.
+ /// Game rules can be started and stopped at any time, including midround.
+ ///
+ public IReadOnlyList<(TimeSpan, GameRulePrototype)> AllPreviousGameRules => _allPreviousGameRules;
private void InitializeGameRules()
{
@@ -21,13 +38,15 @@ namespace Content.Server.GameTicking
_consoleHost.RegisterCommand("addgamerule",
string.Empty,
"addgamerule ",
- AddGameRuleCommand);
+ AddGameRuleCommand,
+ AddGameRuleCompletions);
// End game rule command.
_consoleHost.RegisterCommand("endgamerule",
string.Empty,
"endgamerule ",
- EndGameRuleCommand);
+ EndGameRuleCommand,
+ EndGameRuleCompletions);
// Clear game rules command.
_consoleHost.RegisterCommand("cleargamerules",
@@ -49,50 +68,55 @@ namespace Content.Server.GameTicking
///
public void StartGameRule(GameRulePrototype rule)
{
- if (!GameRuleAdded(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 (removing it from added game rules) so that behavior
+ /// This always includes removing it (from added game rules) so that behavior
/// is not separate from this.
///
///
public void EndGameRule(GameRulePrototype rule)
{
- if (!GameRuleAdded(rule))
+ if (!IsGameRuleAdded(rule))
return;
_addedGameRules.Remove(rule);
+ _sawmill.Info($"Ended game rule {rule.ID}");
- if (GameRuleStarted(rule))
+ 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 roundstart.
+ /// 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 GameRuleAdded(GameRulePrototype rule)
+ public bool IsGameRuleAdded(GameRulePrototype rule)
{
return _addedGameRules.Contains(rule);
}
- public bool GameRuleAdded(string rule)
+ public bool IsGameRuleAdded(string rule)
{
foreach (var ruleProto in _addedGameRules)
{
@@ -103,12 +127,12 @@ namespace Content.Server.GameTicking
return false;
}
- public bool GameRuleStarted(GameRulePrototype rule)
+ public bool IsGameRuleStarted(GameRulePrototype rule)
{
return _startedGameRules.Contains(rule);
}
- public bool GameRuleStarted(string rule)
+ public bool IsGameRuleStarted(string rule)
{
foreach (var ruleProto in _startedGameRules)
{
@@ -142,12 +166,19 @@ namespace Content.Server.GameTicking
AddGameRule(rule);
- // Start rule if we're already in the middle of a round.
+ // 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)
{
@@ -163,12 +194,18 @@ namespace Content.Server.GameTicking
}
}
+ 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
}
diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
index 789d67d1fb..63832b877f 100644
--- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs
+++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs
@@ -376,7 +376,7 @@ namespace Content.Server.GameTicking
ReqWindowAttentionAll();
}
}
-
+
///
/// Cleanup that has to run to clear up anything from the previous round.
/// Stuff like wiping the previous map clean.
@@ -425,6 +425,7 @@ namespace Content.Server.GameTicking
ClearGameRules();
_addedGameRules.Clear();
+ _allPreviousGameRules.Clear();
// Round restart cleanup event, so entity systems can reset.
var ev = new RoundRestartCleanupEvent();
diff --git a/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs
new file mode 100644
index 0000000000..7a73804acb
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs
@@ -0,0 +1,76 @@
+using Content.Shared.Sound;
+using JetBrains.Annotations;
+
+namespace Content.Server.GameTicking.Rules.Configurations;
+
+///
+/// Defines a configuration for a given station event game rule, since all station events are just
+/// game rules.
+///
+[UsedImplicitly]
+public sealed 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;
+ public const float WeightHigh = 15.0f;
+ public const float WeightVeryHigh = 20.0f;
+
+ [DataField("weight")]
+ public float Weight = WeightNormal;
+
+ [DataField("startAnnouncement")]
+ public string? StartAnnouncement;
+
+ [DataField("endAnnouncement")]
+ public string? EndAnnouncement;
+
+ [DataField("startAudio")]
+ public SoundSpecifier? StartAudio;
+
+ [DataField("endAudio")]
+ public SoundSpecifier? EndAudio;
+
+ ///
+ /// In minutes, when is the first round time this event can start
+ ///
+ [DataField("earliestStart")]
+ public int EarliestStart = 5;
+
+ ///
+ /// In minutes, the amount of time before the same event can occur again
+ ///
+ [DataField("reoccurrenceDelay")]
+ public int ReoccurrenceDelay = 30;
+
+ ///
+ /// When in the lifetime to start the event.
+ ///
+ [DataField("startAfter")]
+ public float StartAfter;
+
+ ///
+ /// When in the lifetime to end the event..
+ ///
+ [DataField("endAfter")]
+ public float EndAfter = float.MaxValue;
+
+ ///
+ /// How many players need to be present on station for the event to run
+ ///
+ ///
+ /// To avoid running deadly events with low-pop
+ ///
+ [DataField("minimumPlayers")]
+ public int MinimumPlayers;
+
+ ///
+ /// How many times this even can occur in a single round
+ ///
+ [DataField("maxOccurrences")]
+ public int? MaxOccurrences;
+}
diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
index 78ed1e25f0..a0d1fa94c0 100644
--- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
@@ -34,14 +34,14 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
SubscribeLocalEvent(OnHealthChanged);
}
- public override void Started(GameRuleConfiguration _)
+ public override void Started()
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement"));
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
_deadCheckTimer = null;
_restartTimer = null;
@@ -64,7 +64,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
private void RunDelayedCheck()
{
- if (!Enabled || _deadCheckTimer != null)
+ if (!RuleAdded || _deadCheckTimer != null)
return;
_deadCheckTimer = DeadCheckDelay;
@@ -72,7 +72,7 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
public override void Update(float frameTime)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
// If the restart timer is active, that means the round is ending soon, no need to check for winners.
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
index eba6a386b2..47bd891db2 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
@@ -9,10 +9,16 @@ public abstract class GameRuleSystem : EntitySystem
[Dependency] protected GameTicker GameTicker = default!;
///
- /// Whether this GameRule is currently enabled or not.
+ /// Whether this GameRule is currently added or not.
/// Be sure to check this before doing anything rule-specific.
///
- public bool Enabled { get; protected set; } = false;
+ 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.
@@ -20,6 +26,12 @@ public abstract class GameRuleSystem : EntitySystem
///
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();
@@ -35,7 +47,10 @@ public abstract class GameRuleSystem : EntitySystem
if (ev.Rule.Configuration.Id != Prototype)
return;
- Enabled = true;
+ Configuration = ev.Rule.Configuration;
+ RuleAdded = true;
+
+ Added();
}
private void OnGameRuleStarted(GameRuleStartedEvent ev)
@@ -43,7 +58,9 @@ public abstract class GameRuleSystem : EntitySystem
if (ev.Rule.Configuration.Id != Prototype)
return;
- Started(ev.Rule.Configuration);
+ RuleStarted = true;
+
+ Started();
}
private void OnGameRuleEnded(GameRuleEndedEvent ev)
@@ -51,17 +68,27 @@ public abstract class GameRuleSystem : EntitySystem
if (ev.Rule.Configuration.Id != Prototype)
return;
- Enabled = false;
- Ended(ev.Rule.Configuration);
+ RuleAdded = false;
+ RuleStarted = false;
+ Ended();
}
///
- /// Called when the game rule has been started..
+ /// Called when the game rule has been added.
+ /// You should avoid using this in favor of started--they are not the same thing.
///
- public abstract void Started(GameRuleConfiguration configuration);
+ ///
+ /// 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 game rule has ended..
+ /// Called when the game rule has been started.
///
- public abstract void Ended(GameRuleConfiguration configuration);
+ public abstract void Started();
+
+ ///
+ /// Called when the game rule has ended.
+ ///
+ public abstract void Ended();
}
diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
index 4c76ddd0e1..d61b93d450 100644
--- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
@@ -25,16 +25,16 @@ public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem
SubscribeLocalEvent(RunLevelChanged);
}
- public override void Started(GameRuleConfiguration config)
+ public override void Started()
{
- if (config is not InactivityGameRuleConfiguration inactivityConfig)
+ if (Configuration is not InactivityGameRuleConfiguration inactivityConfig)
return;
InactivityMaxTime = inactivityConfig.InactivityMaxTime;
RoundEndDelay = inactivityConfig.RoundEndDelay;
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
_playerManager.PlayerStatusChanged -= PlayerStatusChanged;
@@ -64,7 +64,7 @@ public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
switch (args.New)
diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
index 0574bc08c7..6b2a5805c6 100644
--- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
@@ -23,10 +23,11 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
SubscribeLocalEvent(RunLevelChanged);
}
- public override void Started(GameRuleConfiguration config)
+ public override void Started()
{
- if (config is not MaxTimeRestartRuleConfiguration maxTimeRestartConfig)
+ if (Configuration is not MaxTimeRestartRuleConfiguration maxTimeRestartConfig)
return;
+
RoundMaxTime = maxTimeRestartConfig.RoundMaxTime;
RoundEndDelay = maxTimeRestartConfig.RoundEndDelay;
@@ -34,7 +35,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
RestartTimer();
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
StopTimer();
}
@@ -62,7 +63,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
private void RunLevelChanged(GameRunLevelChangedEvent args)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
switch (args.New)
diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
index 3c3cbb7002..d3b41455b1 100644
--- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
@@ -56,7 +56,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
private void OnNukeExploded(NukeExplodedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
_opsWon = true;
@@ -65,7 +65,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
ev.AddLine(_opsWon ? Loc.GetString("nukeops-ops-won") : Loc.GetString("nukeops-crew-won"));
@@ -78,7 +78,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
private void OnMobStateChanged(MobStateChangedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
if (!_aliveNukeops.TryFirstOrNull(x => x.Key.OwnedEntity == ev.Entity, out var op)) return;
@@ -93,7 +93,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
_aliveNukeops.Clear();
@@ -292,7 +292,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.NukeopsMinPlayers);
@@ -311,11 +311,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
}
}
-
- public override void Started(GameRuleConfiguration _)
+ public override void Started()
{
_opsWon = false;
}
- public override void Ended(GameRuleConfiguration _) { }
+ public override void Ended() { }
}
diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs
index b6021674a1..00494e88b2 100644
--- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs
@@ -57,7 +57,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem
private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
if (Deleted(_pirateShip))
@@ -120,14 +120,14 @@ public sealed class PiratesRuleSystem : GameRuleSystem
}
}
- public override void Started(GameRuleConfiguration _) { }
+ public override void Started() { }
- public override void Ended(GameRuleConfiguration _) { }
+ public override void Ended() { }
private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev)
{
// Forgive me for copy-pasting nukies.
- if (!Enabled)
+ if (!RuleAdded)
{
return;
}
@@ -225,7 +225,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers);
diff --git a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
index 418f9ebf5c..1df78acd8d 100644
--- a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
@@ -9,12 +9,12 @@ public sealed class SandboxRuleSystem : GameRuleSystem
public override string Prototype => "Sandbox";
- public override void Started(GameRuleConfiguration _)
+ public override void Started()
{
_sandbox.IsSandboxEnabled = true;
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
_sandbox.IsSandboxEnabled = false;
}
diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
index 838e8c0212..d8e2a3830f 100644
--- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
@@ -16,12 +16,12 @@ public sealed class SecretRuleSystem : GameRuleSystem
public override string Prototype => "Secret";
- public override void Started(GameRuleConfiguration _)
+ public override void Started()
{
PickRule();
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
// noop
// Preset should already handle it.
diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
index d2a07e377c..d6b1388a3b 100644
--- a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs
@@ -97,7 +97,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
private void OnRoundStartAttempt(RoundStartAttemptEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers);
@@ -119,7 +119,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
private void OnPlayersAssigned(RulePlayerJobsAssignedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var minTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors);
@@ -203,7 +203,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
}
}
- public override void Started(GameRuleConfiguration _)
+ public override void Started()
{
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
@@ -269,7 +269,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
_doorSystem.AccessType = SharedDoorSystem.AccessTypes.Id;
EndTime = null;
@@ -288,7 +288,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
private void CheckWinConditions()
{
- if (!Enabled || !_cfg.GetCVar(CCVars.GameLobbyEnableWin))
+ if (!RuleAdded || !_cfg.GetCVar(CCVars.GameLobbyEnableWin))
return;
var traitorsAlive = 0;
@@ -457,7 +457,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
private void OnLateJoinRefresh(RefreshLateJoinAllowedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
ev.Disallow();
diff --git a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
index 038e6c6293..92b6913fc1 100644
--- a/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/TraitorDeathMatchRuleSystem.cs
@@ -63,7 +63,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
private void OnPlayerSpawned(PlayerSpawnCompleteEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var session = ev.Player;
@@ -144,7 +144,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
private void OnGhostAttempt(GhostAttemptHandleEvent ev)
{
- if (!Enabled || ev.Handled)
+ if (!RuleAdded || ev.Handled)
return;
ev.Handled = true;
@@ -181,7 +181,7 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var lines = new List();
@@ -200,14 +200,14 @@ public sealed class TraitorDeathMatchRuleSystem : GameRuleSystem
ev.AddLine(string.Join('\n', lines));
}
- public override void Started(GameRuleConfiguration _)
+ public override void Started()
{
_restarter.RoundMaxTime = TimeSpan.FromMinutes(30);
_restarter.RestartTimer();
_safeToEndRound = true;
}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
}
diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
index ee90cd4a45..73d28ec244 100644
--- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
@@ -49,16 +49,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem
SubscribeLocalEvent(OnRoundEndText);
}
- public override void Started(GameRuleConfiguration _) {}
+ public override void Started() {}
- public override void Ended(GameRuleConfiguration _)
+ public override void Ended()
{
_traitors.Clear();
}
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
// If the current preset doesn't explicitly contain the traitor game rule, just carry on and remove self.
@@ -86,7 +86,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var playersPerTraitor = _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor);
@@ -197,7 +197,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var result = Loc.GetString("traitor-round-end-result", ("traitorCount", _traitors.Count));
diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
index f86ad355ef..aed7788b09 100644
--- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
@@ -65,7 +65,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
//this is just the general condition thing used for determining the win/lose text
@@ -113,7 +113,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
private void OnJobAssigned(RulePlayerJobsAssignedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
_initialInfectedNames = new();
@@ -127,14 +127,14 @@ public sealed class ZombieRuleSystem : GameRuleSystem
///
private void OnMobStateChanged(MobStateChangedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
CheckRoundEnd(ev.Entity);
}
private void OnEntityZombified(EntityZombifiedEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
CheckRoundEnd(ev.Target);
}
@@ -158,7 +158,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
- if (!Enabled)
+ if (!RuleAdded)
return;
var minPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers);
@@ -177,13 +177,13 @@ public sealed class ZombieRuleSystem : GameRuleSystem
}
}
- public override void Started(GameRuleConfiguration configuration)
+ public override void Started()
{
//this technically will run twice with zombies on roundstart, but it doesn't matter because it fails instantly
InfectInitialPlayers();
}
- public override void Ended(GameRuleConfiguration configuration) { }
+ public override void Ended() { }
private void OnZombifySelf(EntityUid uid, ZombifyOnDeathComponent component, ZombifySelfActionEvent args)
{
diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
index 613b26cedf..1eef300972 100644
--- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
+++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
@@ -55,7 +55,7 @@ namespace Content.Server.Spawners.EntitySystems
foreach (var rule in component.GameRules)
{
- if (!_ticker.GameRuleStarted(rule)) continue;
+ if (!_ticker.IsGameRuleStarted(rule)) continue;
Spawn(component);
return;
}
diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
new file mode 100644
index 0000000000..a6d101f6d4
--- /dev/null
+++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
@@ -0,0 +1,247 @@
+using System.Linq;
+using Content.Server.GameTicking.Rules;
+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;
+using Robust.Shared.Timing;
+
+namespace Content.Server.StationEvents
+{
+ ///
+ /// The basic event scheduler rule, loosely based off of /tg/ events, which most
+ /// game presets use.
+ ///
+ [UsedImplicitly]
+ public sealed class BasicStationEventSchedulerSystem : GameRuleSystem
+ {
+ public override string Prototype => "BasicStationEventScheduler";
+
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ private const float MinimumTimeUntilFirstEvent = 300;
+ private ISawmill _sawmill = default!;
+
+ ///
+ /// How long until the next check for an event runs
+ ///
+ /// Default value is how long until first event is allowed
+ private float _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = Logger.GetSawmill("basicevents");
+
+ // Can't just check debug / release for a default given mappers need to use release mode
+ // As such we'll always pause it by default.
+ _configurationManager.OnValueChanged(CCVars.EventsEnabled, value => RuleAdded = value, true);
+
+ SubscribeLocalEvent(Reset);
+ }
+
+ public override void Started()
+ {
+ if (!_configurationManager.GetCVar(CCVars.EventsEnabled))
+ RuleAdded = false;
+ }
+ public override void Ended() { }
+
+ ///
+ /// Randomly run a valid event immediately, ignoring earlieststart or whether the event is enabled
+ ///
+ ///
+ public string RunRandomEvent()
+ {
+ var randomEvent = PickRandomEvent();
+
+ if (randomEvent == null
+ || !_prototype.TryIndex(randomEvent.Id, out var proto))
+ {
+ return Loc.GetString("station-event-system-run-random-event-no-valid-events");
+ }
+
+ GameTicker.AddGameRule(proto);
+ return Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Id));
+ }
+
+ ///
+ /// Randomly picks a valid event.
+ ///
+ public StationEventRuleConfiguration? PickRandomEvent()
+ {
+ var availableEvents = AvailableEvents(true);
+ return FindEvent(availableEvents);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!RuleStarted)
+ return;
+
+ if (_timeUntilNextEvent > 0)
+ {
+ _timeUntilNextEvent -= frameTime;
+ return;
+ }
+
+ // No point hammering this trying to find events if none are available
+ var stationEvent = FindEvent(AvailableEvents());
+ if (stationEvent == null
+ || !_prototype.TryIndex(stationEvent.Id, out var proto))
+ {
+ return;
+ }
+
+ GameTicker.AddGameRule(proto);
+ ResetTimer();
+ _sawmill.Info($"Started event {proto.ID}. Next event in {_timeUntilNextEvent} seconds");
+ }
+
+ ///
+ /// Reset the event timer once the event is done.
+ ///
+ private void ResetTimer()
+ {
+ // 5 - 15 minutes. TG does 3-10 but that's pretty frequent
+ _timeUntilNextEvent = _random.Next(300, 900);
+ }
+
+ ///
+ /// Pick a random event from the available events at this time, also considering their weightings.
+ ///
+ ///
+ private StationEventRuleConfiguration? FindEvent(List availableEvents)
+ {
+ if (availableEvents.Count == 0)
+ {
+ return null;
+ }
+
+ var sumOfWeights = 0;
+
+ foreach (var stationEvent in availableEvents)
+ {
+ sumOfWeights += (int) stationEvent.Weight;
+ }
+
+ sumOfWeights = _random.Next(sumOfWeights);
+
+ foreach (var stationEvent in availableEvents)
+ {
+ sumOfWeights -= (int) stationEvent.Weight;
+
+ if (sumOfWeights <= 0)
+ {
+ return stationEvent;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the events that have met their player count, time-until start, etc.
+ ///
+ ///
+ ///
+ private List AvailableEvents(bool ignoreEarliestStart = false)
+ {
+ TimeSpan currentTime;
+ var playerCount = _playerManager.PlayerCount;
+
+ // playerCount does a lock so we'll just keep the variable here
+ if (!ignoreEarliestStart)
+ {
+ currentTime = GameTicker.RoundDuration();
+ }
+ else
+ {
+ currentTime = TimeSpan.Zero;
+ }
+
+ var result = new List();
+
+ foreach (var stationEvent in AllEvents())
+ {
+ if (CanRun(stationEvent, playerCount, currentTime))
+ {
+ result.Add(stationEvent);
+ }
+ }
+
+ return result;
+ }
+
+ private IEnumerable AllEvents()
+ {
+ return _prototype.EnumeratePrototypes()
+ .Where(p => p.Configuration is StationEventRuleConfiguration)
+ .Select(p => (StationEventRuleConfiguration) p.Configuration);
+ }
+
+ private int GetOccurrences(StationEventRuleConfiguration stationEvent)
+ {
+ return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id);
+ }
+
+ public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent)
+ {
+ foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse())
+ {
+ if (rule.Configuration is not StationEventRuleConfiguration)
+ continue;
+
+ if (stationEvent == null || rule.ID == stationEvent.Id)
+ return time;
+ }
+
+ return TimeSpan.Zero;
+ }
+
+ private bool CanRun(StationEventRuleConfiguration stationEvent, int playerCount, TimeSpan currentTime)
+ {
+ if (GameTicker.IsGameRuleStarted(stationEvent.Id))
+ return false;
+
+ if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(stationEvent) >= stationEvent.MaxOccurrences.Value)
+ {
+ return false;
+ }
+
+ if (playerCount < stationEvent.MinimumPlayers)
+ {
+ return false;
+ }
+
+ if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart)
+ {
+ return false;
+ }
+
+ var lastRun = TimeSinceLastEvent(stationEvent);
+ if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes <
+ stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void Reset(RoundRestartCleanupEvent ev)
+ {
+ _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
+ }
+ }
+}
diff --git a/Content.Server/StationEvents/Events/BreakerFlip.cs b/Content.Server/StationEvents/Events/BreakerFlip.cs
index f115682fde..50b3fe8588 100644
--- a/Content.Server/StationEvents/Events/BreakerFlip.cs
+++ b/Content.Server/StationEvents/Events/BreakerFlip.cs
@@ -7,34 +7,34 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
[UsedImplicitly]
-public sealed class BreakerFlip : StationEvent
+public sealed class BreakerFlip : StationEventSystem
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ApcSystem _apcSystem = default!;
- public override string Name => "BreakerFlip";
- public override string? StartAnnouncement =>
- Loc.GetString("station-event-breaker-flip-announcement", ("data", Loc.GetString(Loc.GetString($"random-sentience-event-data-{_random.Next(1, 6)}"))));
- public override float Weight => WeightNormal;
- protected override float EndAfter => 1.0f;
- public override int? MaxOccurrences => 5;
- public override int MinimumPlayers => 15;
+ public override string Prototype => "BreakerFlip";
- public override void Startup()
+ public override void Added()
{
- base.Startup();
+ base.Added();
- var apcSys = EntitySystem.Get();
- var allApcs = _entityManager.EntityQuery().ToList();
- var toDisable = Math.Min(_random.Next(3, 7), allApcs.Count);
+ 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, playDefaultSound: false, colorOverride: Color.Gold);
+ }
+
+ public override void Started()
+ {
+ base.Started();
+
+ var allApcs = EntityQuery().ToList();
+ var toDisable = Math.Min(RobustRandom.Next(3, 7), allApcs.Count);
if (toDisable == 0)
return;
- _random.Shuffle(allApcs);
+ RobustRandom.Shuffle(allApcs);
for (var i = 0; i < toDisable; i++)
{
- apcSys.ApcToggleBreaker(allApcs[i].Owner, allApcs[i]);
+ _apcSystem.ApcToggleBreaker(allApcs[i].Owner, allApcs[i]);
}
}
}
diff --git a/Content.Server/StationEvents/Events/BureaucraticError.cs b/Content.Server/StationEvents/Events/BureaucraticError.cs
index e9f9ed0b10..d765d387b4 100644
--- a/Content.Server/StationEvents/Events/BureaucraticError.cs
+++ b/Content.Server/StationEvents/Events/BureaucraticError.cs
@@ -6,55 +6,44 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
[UsedImplicitly]
-public sealed class BureaucraticError : StationEvent
+public sealed class BureaucraticError : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- public override string StartAnnouncement =>
- Loc.GetString("station-event-bureaucratic-error-announcement");
- public override string Name => "BureaucraticError";
+ [Dependency] private readonly StationJobsSystem _stationJobs = default!;
- public override int MinimumPlayers => 25;
+ public override string Prototype => "BureaucraticError";
- public override float Weight => WeightLow;
-
- public override int? MaxOccurrences => 2;
-
- protected override float EndAfter => 1f;
-
- public override void Startup()
+ public override void Started()
{
- base.Startup();
- var stationSystem = EntitySystem.Get();
- var stationJobsSystem = EntitySystem.Get();
- if (stationSystem.Stations.Count == 0) return; // No stations
- var chosenStation = _random.Pick(stationSystem.Stations.ToList());
- var jobList = stationJobsSystem.GetJobs(chosenStation).Keys.ToList();
+ base.Started();
+
+ if (StationSystem.Stations.Count == 0) return; // No stations
+ var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList());
+ var jobList = _stationJobs.GetJobs(chosenStation).Keys.ToList();
// Low chance to completely change up the late-join landscape by closing all positions except infinite slots.
// Lower chance than the /tg/ equivalent of this event.
- if (_random.Prob(0.25f))
+ if (RobustRandom.Prob(0.25f))
{
- var chosenJob = _random.PickAndTake(jobList);
- stationJobsSystem.MakeJobUnlimited(chosenStation, chosenJob); // INFINITE chaos.
+ var chosenJob = RobustRandom.PickAndTake(jobList);
+ _stationJobs.MakeJobUnlimited(chosenStation, chosenJob); // INFINITE chaos.
foreach (var job in jobList)
{
- if (stationJobsSystem.IsJobUnlimited(chosenStation, job))
+ if (_stationJobs.IsJobUnlimited(chosenStation, job))
continue;
- stationJobsSystem.TrySetJobSlot(chosenStation, job, 0);
+ _stationJobs.TrySetJobSlot(chosenStation, job, 0);
}
}
else
{
// Changing every role is maybe a bit too chaotic so instead change 20-30% of them.
- for (var i = 0; i < _random.Next((int)(jobList.Count * 0.20), (int)(jobList.Count * 0.30)); i++)
+ for (var i = 0; i < RobustRandom.Next((int)(jobList.Count * 0.20), (int)(jobList.Count * 0.30)); i++)
{
- var chosenJob = _random.PickAndTake(jobList);
- if (stationJobsSystem.IsJobUnlimited(chosenStation, chosenJob))
+ var chosenJob = RobustRandom.PickAndTake(jobList);
+ if (_stationJobs.IsJobUnlimited(chosenStation, chosenJob))
continue;
- stationJobsSystem.TryAdjustJobSlot(chosenStation, chosenJob, _random.Next(-3, 6));
+ _stationJobs.TryAdjustJobSlot(chosenStation, chosenJob, RobustRandom.Next(-3, 6));
}
}
}
-
}
diff --git a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs
index f5d0ec523f..ab54914dcd 100644
--- a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs
+++ b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs
@@ -14,11 +14,11 @@ namespace Content.Server.StationEvents.Events;
/// Infects a couple people
/// with a random disease that isn't super deadly
///
-public sealed class DiseaseOutbreak : StationEvent
+public sealed class DiseaseOutbreak : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly DiseaseSystem _diseaseSystem = default!;
+
+ public override string Prototype => "DiseaseOutbreak";
///
/// Disease prototypes I decided were not too deadly for a random event
@@ -33,62 +33,43 @@ public sealed class DiseaseOutbreak : StationEvent
"BirdFlew",
"TongueTwister"
};
- public override string Name => "DiseaseOutbreak";
- public override float Weight => WeightNormal;
-
- public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/outbreak7.ogg");
- protected override float EndAfter => 1.0f;
-
- public override bool AnnounceEvent => false;
///
/// Finds 2-5 random, alive entities that can host diseases
/// and gives them a randomly selected disease.
/// They all get the same disease.
///
- public override void Startup()
+ public override void Started()
{
- base.Startup();
+ base.Started();
HashSet stationsToNotify = new();
List aliveList = new();
- foreach (var (carrier, mobState) in _entityManager.EntityQuery())
+ foreach (var (carrier, mobState) in EntityManager.EntityQuery())
{
if (!mobState.IsDead())
aliveList.Add(carrier);
}
- _random.Shuffle(aliveList);
- /// We're going to filter the above out to only alive mobs. Might change after future mobstate rework
+ RobustRandom.Shuffle(aliveList);
- var toInfect = _random.Next(2, 5);
+ // We're going to filter the above out to only alive mobs. Might change after future mobstate rework
+ var toInfect = RobustRandom.Next(2, 5);
- var diseaseName = _random.Pick(NotTooSeriousDiseases);
+ var diseaseName = RobustRandom.Pick(NotTooSeriousDiseases);
- if (!_prototypeManager.TryIndex(diseaseName, out DiseasePrototype? disease))
+ if (!PrototypeManager.TryIndex(diseaseName, out DiseasePrototype? disease))
return;
- var diseaseSystem = EntitySystem.Get();
- var entSysMgr = IoCManager.Resolve();
- var stationSystem = entSysMgr.GetEntitySystem();
- var chatSystem = entSysMgr.GetEntitySystem();
// Now we give it to people in the list of living disease carriers earlier
foreach (var target in aliveList)
{
if (toInfect-- == 0)
break;
- diseaseSystem.TryAddDisease(target.Owner, disease, target);
+ _diseaseSystem.TryAddDisease(target.Owner, disease, target);
- var station = stationSystem.GetOwningStation(target.Owner);
+ var station = StationSystem.GetOwningStation(target.Owner);
if(station == null) continue;
stationsToNotify.Add((EntityUid) station);
}
-
- if (!AnnounceEvent)
- return;
- foreach (var station in stationsToNotify)
- {
- chatSystem.DispatchStationAnnouncement(station, Loc.GetString("station-event-disease-outbreak-announcement"),
- playDefaultSound: false, colorOverride: Color.YellowGreen);
- }
}
}
diff --git a/Content.Server/StationEvents/Events/FalseAlarm.cs b/Content.Server/StationEvents/Events/FalseAlarm.cs
index f3e22c88e6..a42697a865 100644
--- a/Content.Server/StationEvents/Events/FalseAlarm.cs
+++ b/Content.Server/StationEvents/Events/FalseAlarm.cs
@@ -1,27 +1,33 @@
-using JetBrains.Annotations;
+using Content.Server.GameTicking.Rules.Configurations;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
namespace Content.Server.StationEvents.Events
{
[UsedImplicitly]
- public sealed class FalseAlarm : StationEvent
+ public sealed class FalseAlarm : StationEventSystem
{
- public override string Name => "FalseAlarm";
- public override float Weight => WeightHigh;
- protected override float EndAfter => 1.0f;
- public override int? MaxOccurrences => 5;
+ public override string Prototype => "FalseAlarm";
- public override void Announce()
+ public override void Started()
{
- var stationEventSystem = EntitySystem.Get();
- var randomEvent = stationEventSystem.PickRandomEvent();
+ base.Started();
- if (randomEvent != null)
+ var ev = GetRandomEventUnweighted(PrototypeManager, RobustRandom);
+
+ if (ev.Configuration is not StationEventRuleConfiguration cfg)
+ return;
+
+ if (cfg.StartAnnouncement != null)
{
- StartAnnouncement = randomEvent.StartAnnouncement;
- StartAudio = randomEvent.StartAudio;
+ ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(cfg.StartAnnouncement), playDefaultSound: false, colorOverride: Color.Gold);
}
- base.Announce();
+ if (cfg.StartAudio != null)
+ {
+ SoundSystem.Play(cfg.StartAudio.GetSound(), Filter.Broadcast(), cfg.StartAudio.Params);
+ }
}
}
}
diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs
index a6b70cfdbe..e90ca6f20d 100644
--- a/Content.Server/StationEvents/Events/GasLeak.cs
+++ b/Content.Server/StationEvents/Events/GasLeak.cs
@@ -1,4 +1,6 @@
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;
@@ -7,17 +9,11 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events
{
- internal sealed class GasLeak : StationEvent
+ internal sealed class GasLeak : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- public override string Name => "GasLeak";
-
- public override string StartAnnouncement => Loc.GetString("station-event-gas-leak-start-announcement");
-
-
- protected override string EndAnnouncement => Loc.GetString("station-event-gas-leak-end-announcement");
+ public override string Prototype => "GasLeak";
private static readonly Gas[] LeakableGases = {
Gas.Miasma,
@@ -25,24 +21,6 @@ namespace Content.Server.StationEvents.Events
Gas.Tritium,
};
- public override int EarliestStart => 10;
-
- public override int MinimumPlayers => 5;
-
- public override float Weight => WeightLow;
-
- public override int? MaxOccurrences => 1;
-
- ///
- /// Give people time to get their internals on.
- ///
- protected override float StartAfter => 20f;
-
- ///
- /// Don't know how long the event will be until we calculate the leak amount.
- ///
- protected override float EndAfter { get; set; } = float.MaxValue;
-
///
/// Running cooldown of how much time until another leak.
///
@@ -53,23 +31,18 @@ namespace Content.Server.StationEvents.Events
///
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.
@@ -77,26 +50,25 @@ namespace Content.Server.StationEvents.Events
private const int MaximumMolesPerSecond = 50;
private const int MinimumGas = 250;
-
private const int MaximumGas = 1000;
-
private const float SparkChance = 0.05f;
- public override void Startup()
+ public override void Started()
{
- base.Startup();
+ base.Started();
// 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);
+ _leakGas = RobustRandom.Pick(LeakableGases);
// Was 50-50 on using normal distribution.
- var totalGas = (float) _robustRandom.Next(MinimumGas, MaximumGas);
- _molesPerSecond = _robustRandom.Next(MinimumMolesPerSecond, MaximumMolesPerSecond);
- EndAfter = totalGas / _molesPerSecond + StartAfter;
- Logger.InfoS("stationevents", $"Leaking {totalGas} of {_leakGas} over {EndAfter - StartAfter} seconds at {_targetTile}");
+ var totalGas = (float) RobustRandom.Next(MinimumGas, MaximumGas);
+ 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
@@ -107,32 +79,37 @@ namespace Content.Server.StationEvents.Events
{
base.Update(frameTime);
- if (!Started || !Running) return;
+ if (!RuleStarted)
+ return;
+
+ if (Elapsed > _endAfter)
+ {
+ ForceEndSelf();
+ return;
+ }
_timeUntilLeak -= frameTime;
if (_timeUntilLeak > 0f) return;
_timeUntilLeak += LeakCooldown;
- var atmosphereSystem = _entityManager.EntitySysManager.GetEntitySystem();
-
if (!_foundTile ||
_targetGrid == default ||
- _entityManager.Deleted(_targetGrid) ||
- !atmosphereSystem.IsSimulatedGrid(_targetGrid))
+ EntityManager.Deleted(_targetGrid) ||
+ !_atmosphere.IsSimulatedGrid(_targetGrid))
{
- Running = false;
+ ForceEndSelf();
return;
}
- var environment = atmosphereSystem.GetTileMixture(_targetGrid, null, _targetTile, true);
+ var environment = _atmosphere.GetTileMixture(_targetGrid, null, _targetTile, true);
environment?.AdjustMoles(_leakGas, LeakCooldown * _molesPerSecond);
}
- public override void Shutdown()
+ public override void Ended()
{
- base.Shutdown();
+ base.Ended();
Spark();
@@ -141,25 +118,24 @@ namespace Content.Server.StationEvents.Events
_targetTile = default;
_targetCoords = default;
_leakGas = Gas.Oxygen;
- EndAfter = float.MaxValue;
+ _endAfter = float.MaxValue;
}
private void Spark()
{
- var atmosphereSystem = EntitySystem.Get();
- if (_robustRandom.NextFloat() <= SparkChance)
+ if (RobustRandom.NextFloat() <= SparkChance)
{
if (!_foundTile ||
_targetGrid == default ||
- (!_entityManager.EntityExists(_targetGrid) ? EntityLifeStage.Deleted : _entityManager.GetComponent(_targetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
- !atmosphereSystem.IsSimulatedGrid(_targetGrid))
+ (!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.
- atmosphereSystem.HotspotExpose(_targetGrid, _targetTile, 700f, 50f, true);
+ _atmosphere.HotspotExpose(_targetGrid, _targetTile, 700f, 50f, true);
SoundSystem.Play("/Audio/Effects/sparks4.ogg", Filter.Pvs(_targetCoords), _targetCoords);
}
}
diff --git a/Content.Server/StationEvents/Events/KudzuGrowth.cs b/Content.Server/StationEvents/Events/KudzuGrowth.cs
index 11153e2a88..e0b0f5c687 100644
--- a/Content.Server/StationEvents/Events/KudzuGrowth.cs
+++ b/Content.Server/StationEvents/Events/KudzuGrowth.cs
@@ -3,51 +3,26 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
-public sealed class KudzuGrowth : StationEvent
+public sealed class KudzuGrowth : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
-
- public override string Name => "KudzuGrowth";
-
- public override string? StartAnnouncement =>
- Loc.GetString("station-event-kudzu-growth-start-announcement");
-
- public override int EarliestStart => 15;
-
- public override int MinimumPlayers => 15;
-
- public override float Weight => WeightLow;
-
- public override int? MaxOccurrences => 2;
-
- // Get players to scatter a bit looking for it.
- protected override float StartAfter => 50f;
-
- // Give crew at least 9 minutes to either have it gone, or to suffer another event. Kudzu is not actually required to be dead for another event to roll.
- protected override float EndAfter => 60*4;
-
- public override bool AnnounceEvent => false;
+ public override string Prototype => "KudzuGrowth";
private EntityUid _targetGrid;
-
private Vector2i _targetTile;
-
private EntityCoordinates _targetCoords;
- public override void Startup()
+ public override void Started()
{
- base.Startup();
+ base.Started();
// Pick a place to plant the kudzu.
- if (TryFindRandomTile(out _targetTile, out _, out _targetGrid, out _targetCoords, _robustRandom, _entityManager))
+ if (TryFindRandomTile(out _targetTile, out _, out _targetGrid, out _targetCoords))
{
- _entityManager.SpawnEntity("Kudzu", _targetCoords);
- Logger.InfoS("stationevents", $"Spawning a Kudzu at {_targetTile} on {_targetGrid}");
+ 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/MeteorSwarm.cs b/Content.Server/StationEvents/Events/MeteorSwarm.cs
index b5167856c6..cacb2b2fe4 100644
--- a/Content.Server/StationEvents/Events/MeteorSwarm.cs
+++ b/Content.Server/StationEvents/Events/MeteorSwarm.cs
@@ -1,4 +1,5 @@
using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
using Content.Server.Projectiles.Components;
using Content.Shared.Sound;
using Content.Shared.Spawners.Components;
@@ -7,26 +8,9 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events
{
- public sealed class MeteorSwarm : StationEvent
+ public sealed class MeteorSwarm : StationEventSystem
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
-
- public override string Name => "MeteorSwarm";
-
- public override int EarliestStart => 30;
- public override float Weight => WeightLow;
- public override int? MaxOccurrences => 2;
- public override int MinimumPlayers => 20;
-
- public override string StartAnnouncement => Loc.GetString("station-event-meteor-swarm-start-announcement");
- protected override string EndAnnouncement => Loc.GetString("station-event-meteor-swarm-ebd-announcement");
-
- public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/meteors.ogg");
-
- protected override float StartAfter => 30f;
- protected override float EndAfter => float.MaxValue;
+ public override string Prototype => "MeteorSwarm";
private float _cooldown;
@@ -46,53 +30,53 @@ namespace Content.Server.StationEvents.Events
private const float MaxAngularVelocity = 0.25f;
private const float MinAngularVelocity = -0.25f;
- public override void Startup()
+ public override void Started()
{
- base.Startup();
- var robustRandom = IoCManager.Resolve();
- _waveCounter = robustRandom.Next(MinimumWaves, MaximumWaves);
+ base.Started();
+ _waveCounter = RobustRandom.Next(MinimumWaves, MaximumWaves);
}
- public override void Shutdown()
+ public override void Ended()
{
- base.Shutdown();
+ base.Ended();
_waveCounter = 0;
_cooldown = 0f;
- EndAfter = float.MaxValue;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
- if (!Started) return;
+ if (!RuleStarted)
+ return;
if (_waveCounter <= 0)
{
- Running = false;
+ ForceEndSelf();
return;
}
+
_cooldown -= frameTime;
if (_cooldown > 0f) return;
_waveCounter--;
- _cooldown += (MaximumCooldown - MinimumCooldown) * _robustRandom.NextFloat() + MinimumCooldown;
+ _cooldown += (MaximumCooldown - MinimumCooldown) * RobustRandom.NextFloat() + MinimumCooldown;
Box2? playableArea = null;
- var mapId = EntitySystem.Get().DefaultMap;
+ var mapId = GameTicker.DefaultMap;
- foreach (var grid in _mapManager.GetAllGrids())
+ foreach (var grid in MapManager.GetAllGrids())
{
- if (grid.ParentMapId != mapId || !_entityManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? gridBody)) continue;
+ if (grid.ParentMapId != mapId || !EntityManager.TryGetComponent(grid.GridEntityId, out PhysicsComponent? gridBody)) continue;
var aabb = gridBody.GetWorldAABB();
playableArea = playableArea?.Union(aabb) ?? aabb;
}
if (playableArea == null)
{
- EndAfter = float.MinValue;
+ ForceEndSelf();
return;
}
@@ -103,21 +87,21 @@ namespace Content.Server.StationEvents.Events
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));
+ var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
+ var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
var spawnPosition = new MapCoordinates(center + offset, mapId);
- var meteor = _entityManager.SpawnEntity("MeteorLarge", spawnPosition);
- var physics = _entityManager.GetComponent(meteor);
+ var meteor = EntityManager.SpawnEntity("MeteorLarge", spawnPosition);
+ var physics = EntityManager.GetComponent(meteor);
physics.BodyStatus = BodyStatus.InAir;
physics.LinearDamping = 0f;
physics.AngularDamping = 0f;
physics.ApplyLinearImpulse(-offset.Normalized * MeteorVelocity * physics.Mass);
physics.ApplyAngularImpulse(
// Get a random angular velocity.
- physics.Mass * ((MaxAngularVelocity - MinAngularVelocity) * _robustRandom.NextFloat() +
+ physics.Mass * ((MaxAngularVelocity - MinAngularVelocity) * RobustRandom.NextFloat() +
MinAngularVelocity));
// TODO: God this disgusts me but projectile needs a refactor.
- IoCManager.Resolve().EnsureComponent(meteor).Lifetime = 120f;
+ EnsureComp(meteor).Lifetime = 120f;
}
}
}
diff --git a/Content.Server/StationEvents/Events/MouseMigration.cs b/Content.Server/StationEvents/Events/MouseMigration.cs
index f285b6652b..b6c4405065 100644
--- a/Content.Server/StationEvents/Events/MouseMigration.cs
+++ b/Content.Server/StationEvents/Events/MouseMigration.cs
@@ -4,49 +4,31 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
-public sealed class MouseMigration : StationEvent
+public sealed class MouseMigration : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
-
public static List SpawnedPrototypeChoices = new List() //we double up for that ez fake probability
{"MobMouse", "MobMouse1", "MobMouse2", "MobRatServant"};
- public override string Name => "MouseMigration";
+ public override string Prototype => "MouseMigration";
- public override string? StartAnnouncement =>
- Loc.GetString("station-event-mouse-migration-announcement");
-
- public override int EarliestStart => 30;
-
- public override int MinimumPlayers => 35; //this just ensures that it doesn't spawn on lowpop maps.
-
- public override float Weight => WeightLow;
-
- public override int? MaxOccurrences => 1;
-
- public override bool AnnounceEvent => false;
-
- protected override float StartAfter => 30f;
-
- protected override float EndAfter => 60;
-
- public override void Startup()
+ public override void Started()
{
- base.Startup();
+ base.Started();
- var spawnLocations = _entityManager.EntityQuery().ToList();
- _random.Shuffle(spawnLocations);
+ var spawnLocations = EntityManager.EntityQuery().ToList();
+ RobustRandom.Shuffle(spawnLocations);
- var spawnAmount = _random.Next(7, 15); // A small colony of critters.
+ var spawnAmount = RobustRandom.Next(7, 15); // A small colony of critters.
for (int i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
{
- var spawnChoice = _random.Pick(SpawnedPrototypeChoices);
- if (_random.Prob(0.01f) || i == 0) //small chance for multiple, but always at least 1
+ var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
+ if (RobustRandom.Prob(0.01f) || i == 0) //small chance for multiple, but always at least 1
spawnChoice = "SpawnPointGhostRatKing";
- _entityManager.SpawnEntity(spawnChoice, spawnLocations[i].Item2.Coordinates);
+ var coords = spawnLocations[i].Item2.Coordinates;
+ Sawmill.Info($"Spawning mouse {spawnChoice} at {coords}");
+ EntityManager.SpawnEntity(spawnChoice, coords);
}
}
}
diff --git a/Content.Server/StationEvents/Events/PowerGridCheck.cs b/Content.Server/StationEvents/Events/PowerGridCheck.cs
index 1076bd7faf..da38312573 100644
--- a/Content.Server/StationEvents/Events/PowerGridCheck.cs
+++ b/Content.Server/StationEvents/Events/PowerGridCheck.cs
@@ -11,21 +11,9 @@ using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.StationEvents.Events
{
[UsedImplicitly]
- public sealed class PowerGridCheck : StationEvent
+ public sealed class PowerGridCheck : StationEventSystem
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
-
- public override string Name => "PowerGridCheck";
- public override float Weight => WeightNormal;
- public override int? MaxOccurrences => 3;
- public override string StartAnnouncement => Loc.GetString("station-event-power-grid-check-start-announcement");
- protected override string EndAnnouncement => Loc.GetString("station-event-power-grid-check-end-announcement");
- public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/power_off.ogg");
-
- // If you need EndAudio it's down below. Not set here because we can't play it at the normal time without spamming sounds.
-
- protected override float StartAfter => 12.0f;
+ public override string Prototype => "PowerGridCheck";
private CancellationTokenSource? _announceCancelToken;
@@ -37,34 +25,44 @@ namespace Content.Server.StationEvents.Events
private int _numberPerSecond = 0;
private float UpdateRate => 1.0f / _numberPerSecond;
private float _frameTimeAccumulator = 0.0f;
+ private float _endAfter = 0.0f;
- public override void Announce()
+ public override void Added()
{
- base.Announce();
- EndAfter = IoCManager.Resolve().Next(60, 120);
+ base.Added();
+ _endAfter = RobustRandom.Next(60, 120);
}
- public override void Startup()
+ public override void Started()
{
- foreach (var component in _entityManager.EntityQuery(true))
+ foreach (var component in EntityManager.EntityQuery(true))
{
if (!component.PowerDisabled)
_powered.Add(component.Owner);
}
- _random.Shuffle(_powered);
+ RobustRandom.Shuffle(_powered);
_numberPerSecond = Math.Max(1, (int)(_powered.Count / SecondsUntilOff)); // Number of APCs to turn off every second. At least one.
- base.Startup();
+ base.Started();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
- _frameTimeAccumulator += frameTime;
+
+ if (!RuleStarted)
+ return;
+
+ if (Elapsed > _endAfter)
+ {
+ ForceEndSelf();
+ return;
+ }
var updates = 0;
+ _frameTimeAccumulator += frameTime;
if (_frameTimeAccumulator > UpdateRate)
{
updates = (int) (_frameTimeAccumulator / UpdateRate);
@@ -77,8 +75,8 @@ namespace Content.Server.StationEvents.Events
break;
var selected = _powered.Pop();
- if (_entityManager.Deleted(selected)) continue;
- if (_entityManager.TryGetComponent(selected, out var powerReceiverComponent))
+ if (EntityManager.Deleted(selected)) continue;
+ if (EntityManager.TryGetComponent(selected, out var powerReceiverComponent))
{
powerReceiverComponent.PowerDisabled = true;
}
@@ -86,13 +84,13 @@ namespace Content.Server.StationEvents.Events
}
}
- public override void Shutdown()
+ public override void Ended()
{
foreach (var entity in _unpowered)
{
- if (_entityManager.Deleted(entity)) continue;
+ if (EntityManager.Deleted(entity)) continue;
- if (_entityManager.TryGetComponent(entity, out ApcPowerReceiverComponent? powerReceiverComponent))
+ if (EntityManager.TryGetComponent(entity, out ApcPowerReceiverComponent? powerReceiverComponent))
{
powerReceiverComponent.PowerDisabled = false;
}
@@ -103,11 +101,11 @@ namespace Content.Server.StationEvents.Events
_announceCancelToken = new CancellationTokenSource();
Timer.Spawn(3000, () =>
{
- SoundSystem.Play("/Audio/Announcements/power_on.ogg", Filter.Broadcast(), AudioParams);
+ SoundSystem.Play("/Audio/Announcements/power_on.ogg", Filter.Broadcast(), AudioParams.Default);
}, _announceCancelToken.Token);
_unpowered.Clear();
- base.Shutdown();
+ base.Ended();
}
}
}
diff --git a/Content.Server/StationEvents/Events/RandomSentience.cs b/Content.Server/StationEvents/Events/RandomSentience.cs
index 1ed371651c..3e955bc5f7 100644
--- a/Content.Server/StationEvents/Events/RandomSentience.cs
+++ b/Content.Server/StationEvents/Events/RandomSentience.cs
@@ -9,26 +9,19 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
-public sealed class RandomSentience : StationEvent
+public sealed class RandomSentience : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
+ public override string Prototype => "RandomSentience";
- public override string Name => "RandomSentience";
-
- public override float Weight => WeightNormal;
-
- protected override float EndAfter => 1.0f;
-
- public override void Startup()
+ public override void Started()
{
- base.Startup();
+ base.Started();
HashSet stationsToNotify = new();
- var targetList = _entityManager.EntityQuery().ToList();
- _random.Shuffle(targetList);
+ var targetList = EntityManager.EntityQuery().ToList();
+ RobustRandom.Shuffle(targetList);
- var toMakeSentient = _random.Next(2, 5);
+ var toMakeSentient = RobustRandom.Next(2, 5);
var groups = new HashSet();
foreach (var target in targetList)
@@ -36,10 +29,10 @@ public sealed class RandomSentience : StationEvent
if (toMakeSentient-- == 0)
break;
- MakeSentientCommand.MakeSentient(target.Owner, _entityManager);
- _entityManager.RemoveComponent(target.Owner);
- var comp = _entityManager.AddComponent(target.Owner);
- comp.RoleName = _entityManager.GetComponent(target.Owner).EntityName;
+ MakeSentientCommand.MakeSentient(target.Owner, EntityManager);
+ EntityManager.RemoveComponent(target.Owner);
+ var comp = EntityManager.AddComponent(target.Owner);
+ comp.RoleName = EntityManager.GetComponent(target.Owner).EntityName;
comp.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", comp.RoleName));
groups.Add(target.FlavorKind);
}
@@ -67,8 +60,8 @@ public sealed class RandomSentience : StationEvent
(EntityUid) station,
Loc.GetString("station-event-random-sentience-announcement",
("kind1", kind1), ("kind2", kind2), ("kind3", kind3), ("amount", groupList.Count),
- ("data", Loc.GetString($"random-sentience-event-data-{_random.Next(1, 6)}")),
- ("strength", Loc.GetString($"random-sentience-event-strength-{_random.Next(1, 8)}"))),
+ ("data", Loc.GetString($"random-sentience-event-data-{RobustRandom.Next(1, 6)}")),
+ ("strength", Loc.GetString($"random-sentience-event-strength-{RobustRandom.Next(1, 8)}"))),
playDefaultSound: false,
colorOverride: Color.Gold
);
diff --git a/Content.Server/StationEvents/Events/StationEvent.cs b/Content.Server/StationEvents/Events/StationEvent.cs
deleted file mode 100644
index 65d8a919a3..0000000000
--- a/Content.Server/StationEvents/Events/StationEvent.cs
+++ /dev/null
@@ -1,259 +0,0 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Chat;
-using Content.Server.Chat.Managers;
-using Content.Server.Chat.Systems;
-using Content.Server.GameTicking;
-using Content.Server.Station.Components;
-using Content.Server.Station.Systems;
-using Content.Shared.Database;
-using Content.Shared.Sound;
-using Robust.Shared.Audio;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-
-namespace Content.Server.StationEvents.Events
-{
- public abstract class StationEvent
- {
- public const float WeightVeryLow = 0.0f;
- public const float WeightLow = 5.0f;
- public const float WeightNormal = 10.0f;
- public const float WeightHigh = 15.0f;
- public const float WeightVeryHigh = 20.0f;
-
- ///
- /// If the event has started and is currently running.
- ///
- public bool Running { get; set; }
-
- ///
- /// The time when this event last ran.
- ///
- public TimeSpan LastRun { get; set; } = TimeSpan.Zero;
-
- ///
- /// Human-readable name for the event.
- ///
- public abstract string Name { get; }
-
- ///
- /// The weight this event has in the random-selection process.
- ///
- public virtual float Weight => WeightNormal;
-
- ///
- /// What should be said in chat when the event starts (if anything).
- ///
- public virtual string? StartAnnouncement { get; set; } = null;
-
- ///
- /// What should be said in chat when the event ends (if anything).
- ///
- protected virtual string? EndAnnouncement { get; } = null;
-
- ///
- /// Starting audio of the event.
- ///
- public virtual SoundSpecifier? StartAudio { get; set; } = new SoundPathSpecifier("/Audio/Announcements/attention.ogg");
-
- ///
- /// Ending audio of the event.
- ///
- public virtual SoundSpecifier? EndAudio { get; } = null;
-
- public virtual AudioParams AudioParams { get; } = AudioParams.Default.WithVolume(-10f);
-
- ///
- /// In minutes, when is the first round time this event can start
- ///
- public virtual int EarliestStart { get; } = 5;
-
- ///
- /// In minutes, the amount of time before the same event can occur again
- ///
- public virtual int ReoccurrenceDelay { get; } = 30;
-
- ///
- /// When in the lifetime to call Start().
- ///
- protected virtual float StartAfter { get; } = 0.0f;
-
- ///
- /// When in the lifetime the event should end.
- ///
- protected virtual float EndAfter { get; set; } = 0.0f;
-
- ///
- /// How long has the event existed. Do not change this.
- ///
- private float Elapsed { get; set; } = 0.0f;
-
- ///
- /// How many players need to be present on station for the event to run
- ///
- ///
- /// To avoid running deadly events with low-pop
- ///
- public virtual int MinimumPlayers { get; } = 0;
-
- ///
- /// How many times this event has run this round
- ///
- public int Occurrences { get; set; } = 0;
-
- ///
- /// How many times this even can occur in a single round
- ///
- public virtual int? MaxOccurrences { get; } = null;
-
- ///
- /// Whether or not the event is announced when it is run
- ///
- public virtual bool AnnounceEvent { get; } = true;
-
- ///
- /// Has the startup time elapsed?
- ///
- protected bool Started { get; set; } = false;
-
- ///
- /// Has this event commenced (announcement may or may not be used)?
- ///
- private bool Announced { get; set; } = false;
-
- ///
- /// Called once to setup the event after StartAfter has elapsed.
- ///
- public virtual void Startup()
- {
- Started = true;
- Occurrences += 1;
- LastRun = EntitySystem.Get().RoundDuration();
-
- IoCManager.Resolve()
- .Add(LogType.EventStarted, LogImpact.High, $"Event startup: {Name}");
- }
-
- ///
- /// Called once as soon as an event is active.
- /// Can also be used for some initial setup.
- ///
- public virtual void Announce()
- {
- IoCManager.Resolve()
- .Add(LogType.EventAnnounced, $"Event announce: {Name}");
-
- if (AnnounceEvent && StartAnnouncement != null)
- {
- var chatSystem = IoCManager.Resolve().GetEntitySystem();
- chatSystem.DispatchGlobalAnnouncement(StartAnnouncement, playDefaultSound: false, colorOverride: Color.Gold);
- }
-
- if (AnnounceEvent && StartAudio != null)
- {
- SoundSystem.Play(StartAudio.GetSound(), Filter.Broadcast(), AudioParams);
- }
-
- Announced = true;
- Running = true;
- }
-
- ///
- /// Called once when the station event ends for any reason.
- ///
- public virtual void Shutdown()
- {
- IoCManager.Resolve()
- .Add(LogType.EventStopped, $"Event shutdown: {Name}");
-
- if (AnnounceEvent && EndAnnouncement != null)
- {
- var chatSystem = IoCManager.Resolve().GetEntitySystem();
- chatSystem.DispatchGlobalAnnouncement(EndAnnouncement, playDefaultSound: false, colorOverride: Color.Gold);
- }
-
- if (AnnounceEvent && EndAudio != null)
- {
- SoundSystem.Play(EndAudio.GetSound(), Filter.Broadcast(), AudioParams);
- }
-
- Started = false;
- Announced = false;
- Elapsed = 0;
- }
-
- ///
- /// Called every tick when this event is running.
- ///
- ///
- public virtual void Update(float frameTime)
- {
- Elapsed += frameTime;
-
- if (!Started && Elapsed >= StartAfter)
- {
- Startup();
- }
-
- if (EndAfter <= Elapsed)
- {
- Running = false;
- }
- }
-
-
- public static bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords, IRobustRandom? robustRandom = null, IEntityManager? entityManager = null, IMapManager? mapManager = null, StationSystem? stationSystem = null)
- {
- tile = default;
- IoCManager.Resolve(ref robustRandom, ref entityManager, ref mapManager);
- entityManager.EntitySysManager.Resolve(ref stationSystem);
-
- targetCoords = EntityCoordinates.Invalid;
- if (stationSystem.Stations.Count == 0)
- {
- targetStation = EntityUid.Invalid;
- targetGrid = EntityUid.Invalid;
- return false;
- }
- targetStation = robustRandom.Pick(stationSystem.Stations);
- var possibleTargets = entityManager.GetComponent(targetStation).Grids;
- if (possibleTargets.Count == 0)
- {
- targetGrid = EntityUid.Invalid;
- return false;
- }
-
- targetGrid = robustRandom.Pick(possibleTargets);
-
- if (!entityManager.TryGetComponent(targetGrid, out var gridComp)
- || !entityManager.TryGetComponent(targetGrid, out var transform))
- return false;
- var grid = gridComp.Grid;
-
- var atmosphereSystem = EntitySystem.Get();
- var found = false;
- var gridBounds = grid.WorldAABB;
- var gridPos = grid.WorldPosition;
-
- 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 (atmosphereSystem.IsTileSpace(grid.GridEntityId, transform.MapUid, tile, mapGridComp:gridComp)
- || atmosphereSystem.IsTileAirBlocked(grid.GridEntityId, tile, mapGridComp:gridComp))
- continue;
- found = true;
- targetCoords = grid.GridTileToLocal(tile);
- break;
- }
-
- if (!found) return false;
-
- return true;
- }
- }
-}
diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs
new file mode 100644
index 0000000000..23926d6e93
--- /dev/null
+++ b/Content.Server/StationEvents/Events/StationEventSystem.cs
@@ -0,0 +1,184 @@
+using System.Linq;
+using Content.Server.Administration.Logs;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.GameTicking.Rules;
+using Content.Server.GameTicking.Rules.Configurations;
+using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
+using Content.Shared.Database;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents.Events
+{
+ ///
+ /// An abstract entity system inherited by all station events for their behavior.
+ ///
+ public abstract class StationEventSystem : GameRuleSystem
+ {
+ [Dependency] protected readonly IRobustRandom RobustRandom = default!;
+ [Dependency] protected readonly IAdminLogManager AdminLogManager = default!;
+ [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
+ [Dependency] protected readonly IMapManager MapManager = default!;
+ [Dependency] protected readonly ChatSystem ChatSystem = default!;
+ [Dependency] protected readonly StationSystem StationSystem = default!;
+
+ protected ISawmill Sawmill = default!;
+
+ ///
+ /// How long has the event existed. Do not change this.
+ ///
+ protected float Elapsed { get; set; }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ Sawmill = Logger.GetSawmill("stationevents");
+ }
+
+ ///
+ /// Called once to setup the event after StartAfter has elapsed, or if an event is forcibly started.
+ ///
+ public override void Started()
+ {
+ AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {Configuration.Id}");
+ }
+
+ ///
+ /// Called once as soon as an event is added, for announcements.
+ /// Can also be used for some initial setup.
+ ///
+ public override void Added()
+ {
+ AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {Configuration.Id}");
+
+ if (Configuration is not StationEventRuleConfiguration ev)
+ return;
+
+ if (ev.StartAnnouncement != null)
+ {
+ ChatSystem.DispatchGlobalAnnouncement(Loc.GetString(ev.StartAnnouncement), playDefaultSound: 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), playDefaultSound: 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 grid = gridComp.Grid;
+
+ var atmosphereSystem = Get();
+ var found = false;
+ var gridBounds = grid.WorldAABB;
+ var gridPos = grid.WorldPosition;
+
+ 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 (atmosphereSystem.IsTileSpace(grid.GridEntityId, Transform(targetGrid).MapUid, tile, mapGridComp:gridComp)
+ || atmosphereSystem.IsTileAirBlocked(grid.GridEntityId, tile, mapGridComp:gridComp)) continue;
+ found = true;
+ targetCoords = grid.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());
+ }
+
+ #endregion
+ }
+}
diff --git a/Content.Server/StationEvents/Events/VentClog.cs b/Content.Server/StationEvents/Events/VentClog.cs
index e9be75fa35..822d4b22fc 100644
--- a/Content.Server/StationEvents/Events/VentClog.cs
+++ b/Content.Server/StationEvents/Events/VentClog.cs
@@ -11,29 +11,9 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
[UsedImplicitly]
-public sealed class VentClog : StationEvent
+public sealed class VentClog : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
- public override string Name => "VentClog";
-
- public override string? StartAnnouncement =>
- Loc.GetString("station-event-vent-clog-start-announcement");
-
- public override int EarliestStart => 15;
-
- public override int MinimumPlayers => 15;
-
- public override float Weight => WeightLow;
-
- public override int? MaxOccurrences => 2;
-
- // Give players time to reach cover.
- protected override float StartAfter => 50f;
-
- protected override float EndAfter => 51.0f; // This can, surprisingly, cause the event to end before it starts.
+ public override string Prototype => "VentClog";
public readonly IReadOnlyList SafeishVentChemicals = new[]
{
@@ -41,33 +21,36 @@ public sealed class VentClog : StationEvent
"Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel", "VentCrud"
};
- public override void Startup()
+ public override void Started()
{
- base.Startup();
+ base.Started();
// TODO: "safe random" for chems. Right now this includes admin chemicals.
- var allReagents = _prototypeManager.EnumeratePrototypes()
+ var allReagents = PrototypeManager.EnumeratePrototypes()
.Where(x => !x.Abstract)
.Select(x => x.ID).ToList();
// This is gross, but not much can be done until event refactor, which needs Dynamic.
var sound = new SoundPathSpecifier("/Audio/Effects/extinguish.ogg");
- foreach (var (_, transform) in _entityManager.EntityQuery())
+ foreach (var (_, transform) in EntityManager.EntityQuery())
{
var solution = new Solution();
- if (_random.Prob(0.05f))
+ if (!RobustRandom.Prob(0.33f))
+ continue;
+
+ if (RobustRandom.Prob(0.05f))
{
- solution.AddReagent(_random.Pick(allReagents), 100);
+ solution.AddReagent(RobustRandom.Pick(allReagents), 100);
}
else
{
- solution.AddReagent(_random.Pick(SafeishVentChemicals), 100);
+ solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 100);
}
- FoamAreaReactionEffect.SpawnFoam("Foam", transform.Coordinates, solution, _random.Next(2, 6), 20, 1,
- 1, sound, _entityManager);
+ FoamAreaReactionEffect.SpawnFoam("Foam", transform.Coordinates, solution, RobustRandom.Next(2, 6), 20, 1,
+ 1, sound, EntityManager);
}
}
diff --git a/Content.Server/StationEvents/Events/VentCritters.cs b/Content.Server/StationEvents/Events/VentCritters.cs
index 9788aab126..041b304d2b 100644
--- a/Content.Server/StationEvents/Events/VentCritters.cs
+++ b/Content.Server/StationEvents/Events/VentCritters.cs
@@ -5,51 +5,30 @@ using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
-public sealed class VentCritters : StationEvent
+public sealed class VentCritters : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
-
public static List SpawnedPrototypeChoices = new List()
{"MobGiantSpiderAngry", "MobMouse", "MobMouse1", "MobMouse2"};
- public override string Name => "VentCritters";
+ public override string Prototype => "VentCritters";
- public override string? StartAnnouncement =>
- Loc.GetString("station-event-vent-spiders-start-announcement", ("data", Loc.GetString(Loc.GetString($"random-sentience-event-data-{_random.Next(1, 6)}"))));
-
- public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/aliens.ogg");
-
- public override int EarliestStart => 15;
-
- public override int MinimumPlayers => 15;
-
- public override float Weight => WeightLow;
-
- public override int? MaxOccurrences => 2;
-
- protected override float StartAfter => 30f;
-
- protected override float EndAfter => 60;
-
- public override bool AnnounceEvent => false;
-
- public override void Startup()
+ public override void Started()
{
- base.Startup();
- var spawnChoice = _random.Pick(SpawnedPrototypeChoices);
- var spawnLocations = _entityManager.EntityQuery().ToList();
- _random.Shuffle(spawnLocations);
+ base.Started();
+ var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
+ var spawnLocations = EntityManager.EntityQuery().ToList();
+ RobustRandom.Shuffle(spawnLocations);
- var spawnAmount = _random.Next(4, 12); // A small colony of critters.
+ 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 = _entityManager.GetComponent(location.Owner);
+ var coords = EntityManager.GetComponent(location.Owner);
- _entityManager.SpawnEntity(spawnChoice, coords.Coordinates);
+ EntityManager.SpawnEntity(spawnChoice, coords.Coordinates);
}
}
}
diff --git a/Content.Server/StationEvents/Events/ZombieOutbreak.cs b/Content.Server/StationEvents/Events/ZombieOutbreak.cs
index c25a45ea78..2b495b402f 100644
--- a/Content.Server/StationEvents/Events/ZombieOutbreak.cs
+++ b/Content.Server/StationEvents/Events/ZombieOutbreak.cs
@@ -1,10 +1,4 @@
-using Content.Server.Chat;
-using Robust.Shared.Random;
-using Content.Server.Chat.Managers;
-using Content.Server.Chat.Systems;
-using Content.Server.Station.Systems;
using Content.Shared.MobState.Components;
-using Content.Shared.Sound;
using Content.Server.Zombies;
namespace Content.Server.StationEvents.Events
@@ -12,62 +6,35 @@ namespace Content.Server.StationEvents.Events
///
/// Revives several dead entities as zombies
///
- public sealed class ZombieOutbreak : StationEvent
+ public sealed class ZombieOutbreak : StationEventSystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
- public override string Name => "ZombieOutbreak";
- public override int EarliestStart => 50;
- public override float Weight => WeightLow / 2;
- public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/bloblarm.ogg");
- protected override float EndAfter => 1.0f;
- public override int? MaxOccurrences => 1;
- public override bool AnnounceEvent => false;
+ public override string Prototype => "ZombieOutbreak";
///
- /// Finds 1-3 random, dead entities accross the station
+ /// Finds 1-3 random, dead entities across the station
/// and turns them into zombies.
///
- public override void Startup()
+ public override void Started()
{
- base.Startup();
- HashSet stationsToNotify = new();
+ base.Started();
List deadList = new();
- foreach (var mobState in _entityManager.EntityQuery())
+ foreach (var mobState in EntityManager.EntityQuery())
{
if (mobState.IsDead() || mobState.IsCritical())
deadList.Add(mobState);
}
- _random.Shuffle(deadList);
+ RobustRandom.Shuffle(deadList);
- var toInfect = _random.Next(1, 3);
-
- var zombifysys = _entityManager.EntitySysManager.GetEntitySystem();
-
- // Now we give it to people in the list of dead entities earlier.
- var entSysMgr = IoCManager.Resolve();
- var stationSystem = entSysMgr.GetEntitySystem();
- var chatSystem = entSysMgr.GetEntitySystem();
+ var toInfect = RobustRandom.Next(1, 3);
foreach (var target in deadList)
{
if (toInfect-- == 0)
break;
- zombifysys.ZombifyEntity(target.Owner);
-
- var station = stationSystem.GetOwningStation(target.Owner);
- if(station == null) continue;
- stationsToNotify.Add((EntityUid) station);
- }
-
- if (!AnnounceEvent)
- return;
- foreach (var station in stationsToNotify)
- {
- chatSystem.DispatchStationAnnouncement(station, Loc.GetString("station-event-zombie-outbreak-announcement"),
- playDefaultSound: false, colorOverride: Color.DarkMagenta);
+ _zombify.ZombifyEntity(target.Owner);
}
}
}
diff --git a/Content.Server/StationEvents/StationEventCommand.cs b/Content.Server/StationEvents/StationEventCommand.cs
deleted file mode 100644
index faf8f6e83f..0000000000
--- a/Content.Server/StationEvents/StationEventCommand.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using Content.Server.Administration;
-using Content.Shared.Administration;
-using Robust.Server.Player;
-using Robust.Shared.Console;
-using System.Linq;
-using System.Text;
-
-namespace Content.Server.StationEvents
-{
- [AdminCommand(AdminFlags.Round)]
- public sealed class StationEventCommand : IConsoleCommand
- {
- public string Command => "events";
- public string Description => Loc.GetString("cmd-events-desc");
- public string Help => Loc.GetString("cmd-events-help");
-
- public void Execute(IConsoleShell shell, string argStr, string[] args)
- {
- var player = shell.Player as IPlayerSession;
- if (!args.Any())
- {
- shell.WriteLine(Loc.GetString("shell-wrong-arguments-number") + $"\n{Help}");
- return;
- }
-
- switch (args.First())
- {
- case "list":
- List(shell, player);
- break;
- case "running":
- Running(shell, player);
- break;
- // Didn't use a "toggle" so it's explicit
- case "pause":
- Pause(shell, player);
- break;
- case "resume":
- Resume(shell, player);
- break;
- case "stop":
- Stop(shell, player);
- break;
- case "run":
- if (args.Length != 2)
- {
- shell.WriteLine(Loc.GetString("shell-wrong-arguments-number-need-specific",
- ("properAmount", 2),
- ("currentAmount", args.Length))
- + $"\n{Help}");
- break;
- }
-
- Run(shell, player, args[1]);
- break;
- default:
- shell.WriteLine(Loc.GetString($"shell-invalid-command-specific.", ("commandName", "events")) + $"\n{Help}");
- break;
- }
- }
-
- private void Run(IConsoleShell shell, IPlayerSession? player, string eventName)
- {
- var stationSystem = EntitySystem.Get();
-
- var resultText = eventName == "random"
- ? stationSystem.RunRandomEvent()
- : stationSystem.RunEvent(eventName);
-
- shell.WriteLine(resultText);
- }
-
- private void Running(IConsoleShell shell, IPlayerSession? player)
- {
- var eventName = EntitySystem.Get().CurrentEvent?.Name;
- if (!string.IsNullOrEmpty(eventName))
- {
- shell.WriteLine(eventName);
- }
- else
- {
- shell.WriteLine(Loc.GetString("cmd-events-none-running"));
- }
- }
-
- private void List(IConsoleShell shell, IPlayerSession? player)
- {
- var events = EntitySystem.Get();
- var sb = new StringBuilder();
-
- sb.AppendLine(Loc.GetString("cmd-events-list-random"));
-
- foreach (var stationEvents in events.StationEvents)
- {
- sb.AppendLine(stationEvents.Name);
- }
-
- shell.WriteLine(sb.ToString());
- }
-
- private void Pause(IConsoleShell shell, IPlayerSession? player)
- {
- var stationEventSystem = EntitySystem.Get();
-
- if (!stationEventSystem.Enabled)
- {
- shell.WriteLine(Loc.GetString("cmd-events-already-paused"));
- }
- else
- {
- stationEventSystem.Enabled = false;
- shell.WriteLine(Loc.GetString("cmd-events-paused"));
- }
- }
-
- private void Resume(IConsoleShell shell, IPlayerSession? player)
- {
- var stationEventSystem = EntitySystem.Get();
-
- if (stationEventSystem.Enabled)
- {
- shell.WriteLine(Loc.GetString("cmd-events-already-running"));
- }
- else
- {
- stationEventSystem.Enabled = true;
- shell.WriteLine(Loc.GetString("cmd-events-resumed"));
- }
- }
-
- private void Stop(IConsoleShell shell, IPlayerSession? player)
- {
- var resultText = EntitySystem.Get().StopEvent();
- shell.WriteLine(resultText);
- }
-
- public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
- {
- if (args.Length == 1)
- {
- var options = new[]
- {
- "list",
- "running",
- "pause",
- "resume",
- "stop",
- "run"
- };
-
- return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-events-arg-subcommand"));
- }
-
- var command = args[0];
-
- if (args.Length != 2)
- return CompletionResult.Empty;
-
- if (command == "run")
- {
- var system = EntitySystem.Get();
- var options = new[] { "random" }.Concat(
- system.StationEvents.Select(e => e.Name).OrderBy(e => e));
-
- return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-events-arg-run-eventName"));
- }
-
- return CompletionResult.Empty;
- }
- }
-}
diff --git a/Content.Server/StationEvents/StationEventSystem.cs b/Content.Server/StationEvents/StationEventSystem.cs
deleted file mode 100644
index f2ee3329ec..0000000000
--- a/Content.Server/StationEvents/StationEventSystem.cs
+++ /dev/null
@@ -1,371 +0,0 @@
-using System.Linq;
-using System.Text;
-using Content.Server.Administration.Logs;
-using Content.Server.GameTicking;
-using Content.Server.StationEvents.Events;
-using Content.Shared.CCVar;
-using Content.Shared.Database;
-using Content.Shared.GameTicking;
-using Content.Shared.StationEvents;
-using JetBrains.Annotations;
-using Robust.Server.Console;
-using Robust.Server.Player;
-using Robust.Shared.Configuration;
-using Robust.Shared.Network;
-using Robust.Shared.Random;
-using Robust.Shared.Reflection;
-
-namespace Content.Server.StationEvents
-{
- [UsedImplicitly]
- // Somewhat based off of TG's implementation of events
- public sealed class StationEventSystem : EntitySystem
- {
- [Dependency] private readonly IConfigurationManager _configurationManager = default!;
- [Dependency] private readonly IServerNetManager _netManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IConGroupController _conGroupController = default!;
- [Dependency] private readonly GameTicker _gameTicker = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
-
- [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-
- public StationEvent? CurrentEvent { get; private set; }
- public IReadOnlyCollection StationEvents => _stationEvents;
-
- private readonly List _stationEvents = new();
-
- 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
- private float _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
-
- ///
- /// Whether random events can run
- ///
- /// If disabled while an event is running (even if admin run) it will disable it
- public bool Enabled
- {
- get => _enabled;
- set
- {
- if (_enabled == value)
- {
- return;
- }
-
- _enabled = value;
- CurrentEvent?.Shutdown();
- CurrentEvent = null;
- }
- }
-
- private bool _enabled = true;
-
- ///
- /// Admins can forcibly run events by passing in the Name
- ///
- /// The exact string for Name, without localization
- ///
- public string RunEvent(string name)
- {
- _adminLogger.Add(LogType.EventRan, LogImpact.High, $"Event run: {name}");
-
- // Could use a dictionary but it's such a minor thing, eh.
- // Wasn't sure on whether to localize this given it's a command
- var upperName = name.ToUpperInvariant();
-
- foreach (var stationEvent in _stationEvents)
- {
- if (stationEvent.Name.ToUpperInvariant() != upperName)
- {
- continue;
- }
-
- CurrentEvent?.Shutdown();
- CurrentEvent = stationEvent;
- stationEvent.Announce();
- return Loc.GetString("station-event-system-run-event", ("eventName", stationEvent.Name));
- }
-
- // I had string interpolation but lord it made it hard to read
- return Loc.GetString("station-event-system-run-event-no-event-name", ("eventName", name));
- }
-
- ///
- /// Randomly run a valid event immediately, ignoring earlieststart
- ///
- ///
- public string RunRandomEvent()
- {
- var randomEvent = PickRandomEvent();
-
- if (randomEvent == null)
- {
- return Loc.GetString("station-event-system-run-random-event-no-valid-events");
- }
-
- CurrentEvent?.Shutdown();
- CurrentEvent = randomEvent;
- CurrentEvent.Startup();
-
- return Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Name));
- }
-
- ///
- /// Randomly picks a valid event.
- ///
- public StationEvent? PickRandomEvent()
- {
- var availableEvents = AvailableEvents(true);
- return FindEvent(availableEvents);
- }
-
- ///
- /// Admins can stop the currently running event (if applicable) and reset the timer
- ///
- ///
- public string StopEvent()
- {
- string resultText;
-
- if (CurrentEvent == null)
- {
- resultText = Loc.GetString("station-event-system-stop-event-no-running-event");
- }
- else
- {
- resultText = Loc.GetString("station-event-system-stop-event", ("eventName", CurrentEvent.Name));
- CurrentEvent.Shutdown();
- CurrentEvent = null;
- }
-
- ResetTimer();
- return resultText;
- }
-
- public override void Initialize()
- {
- base.Initialize();
- var reflectionManager = IoCManager.Resolve();
- var typeFactory = IoCManager.Resolve();
-
- foreach (var type in reflectionManager.GetAllChildren(typeof(StationEvent)))
- {
- if (type.IsAbstract) continue;
-
- var stationEvent = (StationEvent) typeFactory.CreateInstance(type);
- IoCManager.InjectDependencies(stationEvent);
- _stationEvents.Add(stationEvent);
- }
-
- // Can't just check debug / release for a default given mappers need to use release mode
- // As such we'll always pause it by default.
- _configurationManager.OnValueChanged(CCVars.EventsEnabled, value => Enabled = value, true);
-
- _netManager.RegisterNetMessage(RxRequest);
- _netManager.RegisterNetMessage();
-
- SubscribeLocalEvent(Reset);
- }
-
- private void RxRequest(MsgRequestStationEvents msg)
- {
- if (_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var player))
- SendEvents(player);
- }
-
- private void SendEvents(IPlayerSession player)
- {
- if (!_conGroupController.CanCommand(player, "events"))
- return;
-
- var newMsg = new MsgStationEvents();
- newMsg.Events = StationEvents.Select(e => e.Name).ToArray();
- _netManager.ServerSendMessage(newMsg, player.ConnectedClient);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- if (!Enabled && CurrentEvent == null)
- {
- return;
- }
-
- // Stop events from happening in lobby and force active event to end if the round ends
- if (Get().RunLevel != GameRunLevel.InRound)
- {
- if (CurrentEvent != null)
- {
- Enabled = false;
- }
-
- return;
- }
-
- // Keep running the current event
- if (CurrentEvent != null)
- {
- CurrentEvent.Update(frameTime);
-
- // Shutdown the event and set the timer for the next event
- if (!CurrentEvent.Running)
- {
- CurrentEvent.Shutdown();
- CurrentEvent = null;
- ResetTimer();
- }
-
- return;
- }
-
- // Make sure we only count down when no event is running.
- if (_timeUntilNextEvent > 0 && CurrentEvent == null)
- {
- _timeUntilNextEvent -= frameTime;
- return;
- }
-
- // No point hammering this trying to find events if none are available
- var stationEvent = FindEvent(AvailableEvents());
- if (stationEvent == null)
- {
- ResetTimer();
- }
- else
- {
- CurrentEvent = stationEvent;
- CurrentEvent.Announce();
- }
- }
-
- ///
- /// Reset the event timer once the event is done.
- ///
- private void ResetTimer()
- {
- // 5 - 15 minutes. TG does 3-10 but that's pretty frequent
- _timeUntilNextEvent = _random.Next(300, 900);
- }
-
- ///
- /// Pick a random event from the available events at this time, also considering their weightings.
- ///
- ///
- private StationEvent? FindEvent(List availableEvents)
- {
- if (availableEvents.Count == 0)
- {
- return null;
- }
-
- var sumOfWeights = 0;
-
- foreach (var stationEvent in availableEvents)
- {
- sumOfWeights += (int) stationEvent.Weight;
- }
-
- sumOfWeights = _random.Next(sumOfWeights);
-
- foreach (var stationEvent in availableEvents)
- {
- sumOfWeights -= (int) stationEvent.Weight;
-
- if (sumOfWeights <= 0)
- {
- return stationEvent;
- }
- }
-
- return null;
- }
-
- ///
- /// Gets the events that have met their player count, time-until start, etc.
- ///
- ///
- ///
- private List AvailableEvents(bool ignoreEarliestStart = false)
- {
- TimeSpan currentTime;
- var playerCount = _playerManager.PlayerCount;
-
- // playerCount does a lock so we'll just keep the variable here
- if (!ignoreEarliestStart)
- {
- currentTime = _gameTicker.RoundDuration();
- }
- else
- {
- currentTime = TimeSpan.Zero;
- }
-
- var result = new List();
-
- foreach (var stationEvent in _stationEvents)
- {
- if (CanRun(stationEvent, playerCount, currentTime))
- {
- result.Add(stationEvent);
- }
- }
-
- return result;
- }
-
- private bool CanRun(StationEvent stationEvent, int playerCount, TimeSpan currentTime)
- {
- if (stationEvent.MaxOccurrences.HasValue && stationEvent.Occurrences >= stationEvent.MaxOccurrences.Value)
- {
- return false;
- }
-
- if (playerCount < stationEvent.MinimumPlayers)
- {
- return false;
- }
-
- if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart)
- {
- return false;
- }
-
- if (stationEvent.LastRun != TimeSpan.Zero && currentTime.TotalMinutes <
- stationEvent.ReoccurrenceDelay + stationEvent.LastRun.TotalMinutes)
- {
- return false;
- }
-
- return true;
- }
-
- public override void Shutdown()
- {
- CurrentEvent?.Shutdown();
- base.Shutdown();
- }
-
- public void Reset(RoundRestartCleanupEvent ev)
- {
- if (CurrentEvent?.Running == true)
- {
- CurrentEvent.Shutdown();
- CurrentEvent = null;
- }
-
- foreach (var stationEvent in _stationEvents)
- {
- stationEvent.Occurrences = 0;
- stationEvent.LastRun = TimeSpan.Zero;
- }
-
- _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
- }
- }
-}
diff --git a/Content.Shared/StationEvents/MsgRequestStationEvents.cs b/Content.Shared/StationEvents/MsgRequestStationEvents.cs
deleted file mode 100644
index 09345c692e..0000000000
--- a/Content.Shared/StationEvents/MsgRequestStationEvents.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Lidgren.Network;
-using Robust.Shared.Network;
-
-namespace Content.Shared.StationEvents
-{
- public sealed class MsgRequestStationEvents : NetMessage
- {
- public override MsgGroups MsgGroup => MsgGroups.Command;
-
- public override void ReadFromBuffer(NetIncomingMessage buffer)
- {
- }
-
- public override void WriteToBuffer(NetOutgoingMessage buffer)
- {
- }
- }
-}
diff --git a/Content.Shared/StationEvents/MsgStationEvents.cs b/Content.Shared/StationEvents/MsgStationEvents.cs
deleted file mode 100644
index f6e17a4ffc..0000000000
--- a/Content.Shared/StationEvents/MsgStationEvents.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.IO;
-using Lidgren.Network;
-using Robust.Shared.Network;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.StationEvents
-{
- public sealed class MsgStationEvents : NetMessage
- {
- public override MsgGroups MsgGroup => MsgGroups.Command;
-
- public string[] Events = Array.Empty();
-
- public override void ReadFromBuffer(NetIncomingMessage buffer)
- {
- var serializer = IoCManager.Resolve();
- var length = buffer.ReadVariableInt32();
- using var stream = buffer.ReadAlignedMemory(length);
- serializer.DeserializeDirect(stream, out Events);
- }
-
- public override void WriteToBuffer(NetOutgoingMessage buffer)
- {
- var serializer = IoCManager.Resolve();
- using (var stream = new MemoryStream())
- {
- serializer.SerializeDirect(stream, Events);
- buffer.WriteVariableInt32((int)stream.Length);
- stream.TryGetBuffer(out var segment);
- buffer.Write(segment);
- }
- }
- }
-}
diff --git a/Resources/Locale/en-US/administration/ui/tabs/adminbus-tab/station-events-window.ftl b/Resources/Locale/en-US/administration/ui/tabs/adminbus-tab/station-events-window.ftl
deleted file mode 100644
index 863ede5794..0000000000
--- a/Resources/Locale/en-US/administration/ui/tabs/adminbus-tab/station-events-window.ftl
+++ /dev/null
@@ -1,2 +0,0 @@
-station-events-window-not-loaded-text = Not loaded
-station-events-window-random-text = Random
\ No newline at end of file
diff --git a/Resources/Locale/en-US/station-events/events/meteor-swarm.ftl b/Resources/Locale/en-US/station-events/events/meteor-swarm.ftl
index 8a6e11dffa..6a96c56048 100644
--- a/Resources/Locale/en-US/station-events/events/meteor-swarm.ftl
+++ b/Resources/Locale/en-US/station-events/events/meteor-swarm.ftl
@@ -1,2 +1,2 @@
station-event-meteor-swarm-start-announcement = Meteors are on a collision course with the station. Brace for impact.
-station-event-meteor-swarm-ebd-announcement = The meteor swarm has passed. Please return to your stations.
+station-event-meteor-swarm-end-announcement = The meteor swarm has passed. Please return to your stations.
diff --git a/Resources/Locale/en-US/station-events/station-event-command.ftl b/Resources/Locale/en-US/station-events/station-event-command.ftl
deleted file mode 100644
index cc4ede9eb9..0000000000
--- a/Resources/Locale/en-US/station-events/station-event-command.ftl
+++ /dev/null
@@ -1,19 +0,0 @@
-### Localization for events console commands
-
-## 'events' command
-cmd-events-desc = Provides admin control to station events
-cmd-events-help = events >
- running: return the current running event
- list: return all event names that can be run
- pause: stop all random events from running and any one currently running
- resume: allow random events to run again
- run : start a particular event now; is case-insensitive and not localized
-cmd-events-arg-subcommand =
-cmd-events-arg-run-eventName =
-
-cmd-events-none-running = No station event running
-cmd-events-list-random = Random
-cmd-events-paused = Station events paused
-cmd-events-already-paused = Station events are already paused
-cmd-events-resumed = Station events resumed
-cmd-events-already-running = Station events are already running
diff --git a/Resources/Locale/en-US/station-events/station-event-system.ftl b/Resources/Locale/en-US/station-events/station-event-system.ftl
index a8b6d41846..2e7843326e 100644
--- a/Resources/Locale/en-US/station-events/station-event-system.ftl
+++ b/Resources/Locale/en-US/station-events/station-event-system.ftl
@@ -1,7 +1,4 @@
-## StationEventSystem
+## BasicStationEventSchedulerSystem
-station-event-system-run-event = Running event {$eventName}
-station-event-system-run-event-no-event-name = No event named: {$eventName}
+station-event-system-run-event = Running event {$eventName}
station-event-system-run-random-event-no-valid-events = No valid events available
-station-event-system-stop-event-no-running-event = No event running currently
-station-event-system-stop-event = Stopped event {$eventName}
\ No newline at end of file
diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml
new file mode 100644
index 0000000000..90799de888
--- /dev/null
+++ b/Resources/Prototypes/GameRules/events.yml
@@ -0,0 +1,149 @@
+- type: gameRule
+ id: BreakerFlip
+ config:
+ !type:StationEventRuleConfiguration
+ id: BreakerFlip
+ weight: 10
+ endAfter: 1
+ maxOccurrences: 5
+ minimumPlayers: 15
+
+- type: gameRule
+ id: BureaucraticError
+ config:
+ !type:StationEventRuleConfiguration
+ id: BureaucraticError
+ startAnnouncement: station-event-bureaucratic-error-announcement
+ minimumPlayers: 25
+ weight: 5
+ maxOccurrences: 2
+ endAfter: 1
+
+- type: gameRule
+ id: DiseaseOutbreak
+ config:
+ !type:StationEventRuleConfiguration
+ id: DiseaseOutbreak
+ weight: 10
+ endAfter: 1
+
+- type: gameRule
+ id: FalseAlarm
+ config:
+ !type:StationEventRuleConfiguration
+ id: FalseAlarm
+ weight: 15
+ endAfter: 1
+ maxOccurrences: 5
+
+- type: gameRule
+ id: GasLeak
+ config:
+ !type:StationEventRuleConfiguration
+ id: GasLeak
+ startAnnouncement: station-event-gas-leak-start-announcement
+ startAudio:
+ path: /Audio/Announcements/attention.ogg
+ endAnnouncement: station-event-gas-leak-end-announcement
+ earliestStart: 10
+ minimumPlayers: 5
+ weight: 5
+ maxOccurrences: 1
+ startAfter: 20
+
+- type: gameRule
+ id: KudzuGrowth
+ config:
+ !type:StationEventRuleConfiguration
+ id: KudzuGrowth
+ earliestStart: 15
+ minimumPlayers: 15
+ weight: 5
+ maxOccurrences: 2
+ startAfter: 50
+ endAfter: 240
+
+- type: gameRule
+ id: MeteorSwarm
+ config:
+ !type:StationEventRuleConfiguration
+ id: MeteorSwarm
+ earliestStart: 30
+ weight: 5
+ maxOccurrences: 2
+ minimumPlayers: 20
+ startAnnouncement: station-event-meteor-swarm-start-announcement
+ endAnnouncement: station-event-meteor-swarm-end-announcement
+ startAudio:
+ path: /Audio/Announcements/meteors.ogg
+ startAfter: 30
+
+- type: gameRule
+ id: MouseMigration
+ config:
+ !type:StationEventRuleConfiguration
+ id: MouseMigration
+ earliestStart: 30
+ minimumPlayers: 35
+ weightLow: 5
+ maxOccurrences: 1
+ endAfter: 50
+
+- type: gameRule
+ id: PowerGridCheck
+ config:
+ !type:StationEventRuleConfiguration
+ id: PowerGridCheck
+ weight: 10
+ maxOccurrences: 3
+ startAnnouncement: station-event-power-grid-check-start-announcement
+ endAnnouncement: station-event-power-grid-check-end-announcement
+ startAudio:
+ path: /Audio/Announcements/power_off.ogg
+ startAfter: 12
+
+- type: gameRule
+ id: RandomSentience
+ config:
+ !type:StationEventRuleConfiguration
+ id: RandomSentience
+ weight: 10
+ endAfter: 1
+ startAudio:
+ path: /Audio/Announcements/attention.ogg
+
+- type: gameRule
+ id: VentClog
+ config:
+ !type:StationEventRuleConfiguration
+ id: VentClog
+ startAnnouncement: station-event-vent-clog-start-announcement
+ startAudio:
+ path: /Audio/Announcements/attention.ogg
+ earliestStart: 15
+ minimumPlayers: 15
+ weight: 5
+ maxOccurrences: 2
+ startAfter: 50
+ endAfter: 60
+
+- type: gameRule
+ id: VentCritters
+ config:
+ !type:StationEventRuleConfiguration
+ id: VentCritters
+ earliestStart: 15
+ minimumPlayers: 15
+ weight: 5
+ maxOccurrences: 2
+ endAfter: 60
+
+- type: gameRule
+ id: ZombieOutbreak
+ config:
+ !type:StationEventRuleConfiguration
+ id: ZombieOutbreak
+ earliestStart: 50
+ weight: 2.5
+ endAfter: 1
+ maxOccurrences: 1
diff --git a/Resources/Prototypes/game_rules.yml b/Resources/Prototypes/GameRules/roundstart.yml
similarity index 88%
rename from Resources/Prototypes/game_rules.yml
rename to Resources/Prototypes/GameRules/roundstart.yml
index 849f5715c7..d1f636dcc0 100644
--- a/Resources/Prototypes/game_rules.yml
+++ b/Resources/Prototypes/GameRules/roundstart.yml
@@ -65,3 +65,10 @@
config:
!type:GenericGameRuleConfiguration
id: Zombie
+
+# event schedulers
+- type: gameRule
+ id: BasicStationEventScheduler
+ config:
+ !type:GenericGameRuleConfiguration
+ id: BasicStationEventScheduler
diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml
index 78418357ce..afc0369517 100644
--- a/Resources/Prototypes/game_presets.yml
+++ b/Resources/Prototypes/game_presets.yml
@@ -6,6 +6,8 @@
name: extended-title
showInVote: false #2boring2vote
description: extended-description
+ rules:
+ - BasicStationEventScheduler
- type: gamePreset
id: Secret
@@ -38,6 +40,7 @@
showInVote: false
rules:
- Traitor
+ - BasicStationEventScheduler
- type: gamePreset
id: Suspicion
@@ -79,6 +82,7 @@
showInVote: false
rules:
- Nukeops
+ - BasicStationEventScheduler
- type: gamePreset
id: Zombie
@@ -93,6 +97,7 @@
showInVote: false
rules:
- Zombie
+ - BasicStationEventScheduler
- type: gamePreset
id: Pirates
@@ -103,3 +108,4 @@
showInVote: false
rules:
- Pirates
+ - BasicStationEventScheduler