Event refactor (#9589)

* Station event refactor

* Remove clientside `IStationEventManager`

we can just use prototypes

* Basic API idea

* Cruft

* first attempt at epicness

* okay yeah this shit is really clean

* sort out minor stuff

* Convert `BreakerFlip`

* `BureaucraticError` + general cleanup

* `DiseaseOutbreak`

* `FalseAlarm`

* `GasLeak`

* `KudzuGrowth`

* `MeteorSwarm`

* `MouseMigration`

* misc errors

* `PowerGridCheck`

* `RandomSentience`

* `VentClog`

* `VentCritters`

* `ZombieOutbreak`

* Rewrite basic event scheduler

* Minor fixes and logging

* ooooops

* errors + fix

* linter

* completions, `RuleStarted` property, update loop fixes

* Tweaks

* Fix #9462

* Basic scheduler update fix, and fixes #8174

* Add test

* UI cleanup

* really this was just for testing
This commit is contained in:
Kara
2022-07-10 18:48:41 -07:00
committed by GitHub
parent f28cdaaa7c
commit b9a0894d7c
55 changed files with 1095 additions and 1582 deletions

View File

@@ -12,6 +12,5 @@
<Button Name="LoadGamePrototypeButton" Text="{Loc 'load-game-prototype'}"/>
<cc:UICommandButton Name="LoadBlueprintsButton" Command="loadbp" Text="{Loc 'load-blueprints'}" WindowType="{x:Type abt:LoadBlueprintsWindow}"/>
<cc:CommandButton Command="deleteewc Singularity" Name="DeleteSingulos" Text="{Loc 'delete-singularities'}"/>
<cc:UICommandButton Command="events" Text="{Loc 'open-station-events'}" WindowType="{x:Type abt:StationEventsWindow}" />
</GridContainer>
</Control>

View File

@@ -1,13 +0,0 @@
<DefaultWindow
xmlns="https://spacestation14.io" Title="{Loc Events}">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Event}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="EventsOptions" MinSize="100 0" HorizontalExpand="True" Disabled="True" />
</BoxContainer>
<Button Name="PauseButton" Text="{Loc Pause}" Disabled="True" />
<Button Name="ResumeButton" Text="{Loc Resume}" Disabled="True" />
<Button Name="SubmitButton" Text="{Loc Run}" Disabled="True" />
</BoxContainer>
</DefaultWindow>

View File

@@ -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<string>? _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<IClientConsoleHost>().ExecuteCommand("events pause");
}
private static void ResumeButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
{
IoCManager.Resolve<IClientConsoleHost>().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<IClientConsoleHost>().ExecuteCommand($"events run \"{selectedEvent}\"");
}
}
}

View File

@@ -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<IChatManager>().Initialize();
IoCManager.Resolve<IClientPreferencesManager>().Initialize();
IoCManager.Resolve<IStationEventManager>().Initialize();
IoCManager.Resolve<EuiManager>().Initialize();
IoCManager.Resolve<IVoteManager>().Initialize();
IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize();

View File

@@ -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<IStylesheetManager, StylesheetManager>();
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
IoCManager.Register<IClickMapManager, ClickMapManager>();
IoCManager.Register<IStationEventManager, StationEventManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>();

View File

@@ -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))]

View File

@@ -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
{

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
namespace Content.Client.StationEvents.Managers
{
public interface IStationEventManager
{
public IReadOnlyList<string> StationEvents { get; }
public void Initialize();
public event Action OnStationEventsReceived;
public void RequestEvents();
}
}

View File

@@ -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<string> _events = new();
public IReadOnlyList<string> StationEvents => _events;
public event Action? OnStationEventsReceived;
public void Initialize()
{
_netManager.RegisterNetMessage<MsgRequestStationEvents>();
_netManager.RegisterNetMessage<MsgStationEvents>(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<MsgRequestStationEvents>());
}
}
}

View File

@@ -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;
/// <summary>
/// Tests that all game rules can be added/started/ended at the same time without exceptions.
/// </summary>
[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<GameTicker>();
var protoMan = IoCManager.Resolve<IPrototypeManager>();
var rules = protoMan.EnumeratePrototypes<GameRulePrototype>().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<GameTicker>();
// End all rules
gameTicker.ClearGameRules();
Assert.That(!gameTicker.AddedGameRules.Any());
});
await pairTracker.CleanReturnAsync();
}
}

View File

@@ -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<StationEventSystem>();
var dummyFrameTime = (float) IoCManager.Resolve<IGameTiming>().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();
}
}
}

View File

@@ -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;
}

View File

@@ -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<GameRulePrototype> _addedGameRules = new();
public IEnumerable<GameRulePrototype> AddedGameRules => _addedGameRules;
/// <summary>
/// Holds all currently added game rules.
/// </summary>
public IReadOnlySet<GameRulePrototype> AddedGameRules => _addedGameRules;
[ViewVariables] private readonly HashSet<GameRulePrototype> _startedGameRules = new();
public IEnumerable<GameRulePrototype> StartedGameRules => _startedGameRules;
/// <summary>
/// Holds all currently started game rules.
/// </summary>
public IReadOnlySet<GameRulePrototype> StartedGameRules => _startedGameRules;
[ViewVariables] private readonly List<(TimeSpan, GameRulePrototype)> _allPreviousGameRules = new();
/// <summary>
/// A list storing the start times of all game rules that have been started this round.
/// Game rules can be started and stopped at any time, including midround.
/// </summary>
public IReadOnlyList<(TimeSpan, GameRulePrototype)> AllPreviousGameRules => _allPreviousGameRules;
private void InitializeGameRules()
{
@@ -21,13 +38,15 @@ namespace Content.Server.GameTicking
_consoleHost.RegisterCommand("addgamerule",
string.Empty,
"addgamerule <rules>",
AddGameRuleCommand);
AddGameRuleCommand,
AddGameRuleCompletions);
// End game rule command.
_consoleHost.RegisterCommand("endgamerule",
string.Empty,
"endgamerule <rules>",
EndGameRuleCommand);
EndGameRuleCommand,
EndGameRuleCompletions);
// Clear game rules command.
_consoleHost.RegisterCommand("cleargamerules",
@@ -49,50 +68,55 @@ namespace Content.Server.GameTicking
/// </summary>
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));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="rule"></param>
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));
}
/// <summary>
/// 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)
/// </summary>
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<GameRulePrototype>().Where(p => !activeIds.Contains(p.Value)),
"<rule>");
}
[AdminCommand(AdminFlags.Fun)]
private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
{
@@ -163,6 +194,12 @@ namespace Content.Server.GameTicking
}
}
private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args)
{
return CompletionResult.FromHintOptions(_addedGameRules.Select(c => new CompletionOption(c.ID)),
"<added rule>");
}
[AdminCommand(AdminFlags.Fun)]
private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args)
{

View File

@@ -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();

View File

@@ -0,0 +1,76 @@
using Content.Shared.Sound;
using JetBrains.Annotations;
namespace Content.Server.GameTicking.Rules.Configurations;
/// <summary>
/// Defines a configuration for a given station event game rule, since all station events are just
/// game rules.
/// </summary>
[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;
/// <summary>
/// In minutes, when is the first round time this event can start
/// </summary>
[DataField("earliestStart")]
public int EarliestStart = 5;
/// <summary>
/// In minutes, the amount of time before the same event can occur again
/// </summary>
[DataField("reoccurrenceDelay")]
public int ReoccurrenceDelay = 30;
/// <summary>
/// When in the lifetime to start the event.
/// </summary>
[DataField("startAfter")]
public float StartAfter;
/// <summary>
/// When in the lifetime to end the event..
/// </summary>
[DataField("endAfter")]
public float EndAfter = float.MaxValue;
/// <summary>
/// How many players need to be present on station for the event to run
/// </summary>
/// <remarks>
/// To avoid running deadly events with low-pop
/// </remarks>
[DataField("minimumPlayers")]
public int MinimumPlayers;
/// <summary>
/// How many times this even can occur in a single round
/// </summary>
[DataField("maxOccurrences")]
public int? MaxOccurrences;
}

View File

@@ -34,14 +34,14 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem
SubscribeLocalEvent<DamageChangedEvent>(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.

View File

@@ -9,10 +9,16 @@ public abstract class GameRuleSystem : EntitySystem
[Dependency] protected GameTicker GameTicker = default!;
/// <summary>
/// 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.
/// </summary>
public bool Enabled { get; protected set; } = false;
public bool RuleAdded { get; protected set; }
/// <summary>
/// Whether this game rule has been started after being added.
/// You probably want to check this before doing any update loop stuff.
/// </summary>
public bool RuleStarted { get; protected set; }
/// <summary>
/// When the GameRule prototype with this ID is added, this system will be enabled.
@@ -20,6 +26,12 @@ public abstract class GameRuleSystem : EntitySystem
/// </summary>
public new abstract string Prototype { get; }
/// <summary>
/// Holds the current configuration after the event has been added.
/// This should not be getting accessed before the event is enabled, as usual.
/// </summary>
public GameRuleConfiguration Configuration = default!;
public override void Initialize()
{
base.Initialize();
@@ -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();
}
/// <summary>
/// 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.
/// </summary>
public abstract void Started(GameRuleConfiguration configuration);
/// <remarks>
/// This is virtual because it doesn't actually have to be used, and most of the time shouldn't be.
/// </remarks>
public virtual void Added() { }
/// <summary>
/// Called when the game rule has ended..
/// Called when the game rule has been started.
/// </summary>
public abstract void Ended(GameRuleConfiguration configuration);
public abstract void Started();
/// <summary>
/// Called when the game rule has ended.
/// </summary>
public abstract void Ended();
}

View File

@@ -25,16 +25,16 @@ public sealed class InactivityTimeRestartRuleSystem : GameRuleSystem
SubscribeLocalEvent<GameRunLevelChangedEvent>(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)

View File

@@ -23,10 +23,11 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem
SubscribeLocalEvent<GameRunLevelChangedEvent>(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)

View File

@@ -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() { }
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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();

View File

@@ -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<string>();
@@ -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()
{
}

View File

@@ -49,16 +49,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem
SubscribeLocalEvent<RoundEndTextAppendEvent>(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));

View File

@@ -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
/// </remarks>
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)
{

View File

@@ -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;
}

View File

@@ -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
{
/// <summary>
/// The basic event scheduler rule, loosely based off of /tg/ events, which most
/// game presets use.
/// </summary>
[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!;
/// <summary>
/// How long until the next check for an event runs
/// </summary>
/// 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<RoundRestartCleanupEvent>(Reset);
}
public override void Started()
{
if (!_configurationManager.GetCVar(CCVars.EventsEnabled))
RuleAdded = false;
}
public override void Ended() { }
/// <summary>
/// Randomly run a valid event <b>immediately</b>, ignoring earlieststart or whether the event is enabled
/// </summary>
/// <returns></returns>
public string RunRandomEvent()
{
var randomEvent = PickRandomEvent();
if (randomEvent == null
|| !_prototype.TryIndex<GameRulePrototype>(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));
}
/// <summary>
/// Randomly picks a valid event.
/// </summary>
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<GameRulePrototype>(stationEvent.Id, out var proto))
{
return;
}
GameTicker.AddGameRule(proto);
ResetTimer();
_sawmill.Info($"Started event {proto.ID}. Next event in {_timeUntilNextEvent} seconds");
}
/// <summary>
/// Reset the event timer once the event is done.
/// </summary>
private void ResetTimer()
{
// 5 - 15 minutes. TG does 3-10 but that's pretty frequent
_timeUntilNextEvent = _random.Next(300, 900);
}
/// <summary>
/// Pick a random event from the available events at this time, also considering their weightings.
/// </summary>
/// <returns></returns>
private StationEventRuleConfiguration? FindEvent(List<StationEventRuleConfiguration> 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;
}
/// <summary>
/// Gets the events that have met their player count, time-until start, etc.
/// </summary>
/// <param name="ignoreEarliestStart"></param>
/// <returns></returns>
private List<StationEventRuleConfiguration> AvailableEvents(bool ignoreEarliestStart = false)
{
TimeSpan currentTime;
var playerCount = _playerManager.PlayerCount;
// playerCount does a lock so we'll just keep the variable here
if (!ignoreEarliestStart)
{
currentTime = GameTicker.RoundDuration();
}
else
{
currentTime = TimeSpan.Zero;
}
var result = new List<StationEventRuleConfiguration>();
foreach (var stationEvent in AllEvents())
{
if (CanRun(stationEvent, playerCount, currentTime))
{
result.Add(stationEvent);
}
}
return result;
}
private IEnumerable<StationEventRuleConfiguration> AllEvents()
{
return _prototype.EnumeratePrototypes<GameRulePrototype>()
.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;
}
}
}

View File

@@ -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<ApcSystem>();
var allApcs = _entityManager.EntityQuery<ApcComponent>().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<ApcComponent>().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]);
}
}
}

View File

@@ -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<StationSystem>();
var stationJobsSystem = EntitySystem.Get<StationJobsSystem>();
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));
}
}
}
}

View File

@@ -14,11 +14,11 @@ namespace Content.Server.StationEvents.Events;
/// Infects a couple people
/// with a random disease that isn't super deadly
/// </summary>
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";
/// <summary>
/// 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;
/// <summary>
/// Finds 2-5 random, alive entities that can host diseases
/// and gives them a randomly selected disease.
/// They all get the same disease.
/// </summary>
public override void Startup()
public override void Started()
{
base.Startup();
base.Started();
HashSet<EntityUid> stationsToNotify = new();
List<DiseaseCarrierComponent> aliveList = new();
foreach (var (carrier, mobState) in _entityManager.EntityQuery<DiseaseCarrierComponent, MobStateComponent>())
foreach (var (carrier, mobState) in EntityManager.EntityQuery<DiseaseCarrierComponent, MobStateComponent>())
{
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<DiseaseSystem>();
var entSysMgr = IoCManager.Resolve<IEntitySystemManager>();
var stationSystem = entSysMgr.GetEntitySystem<StationSystem>();
var chatSystem = entSysMgr.GetEntitySystem<ChatSystem>();
// 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);
}
}
}

View File

@@ -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<StationEventSystem>();
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);
}
}
}
}

View File

@@ -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;
/// <summary>
/// Give people time to get their internals on.
/// </summary>
protected override float StartAfter => 20f;
/// <summary>
/// Don't know how long the event will be until we calculate the leak amount.
/// </summary>
protected override float EndAfter { get; set; } = float.MaxValue;
/// <summary>
/// Running cooldown of how much time until another leak.
/// </summary>
@@ -53,23 +31,18 @@ namespace Content.Server.StationEvents.Events
/// </summary>
private const float LeakCooldown = 1.0f;
// Event variables
private EntityUid _targetStation;
private EntityUid _targetGrid;
private Vector2i _targetTile;
private EntityCoordinates _targetCoords;
private bool _foundTile;
private Gas _leakGas;
private float _molesPerSecond;
private const int MinimumMolesPerSecond = 20;
private float _endAfter = float.MaxValue;
/// <summary>
/// Don't want to make it too fast to give people time to flee.
@@ -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<AtmosphereSystem>();
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<AtmosphereSystem>();
if (_robustRandom.NextFloat() <= SparkChance)
if (RobustRandom.NextFloat() <= SparkChance)
{
if (!_foundTile ||
_targetGrid == default ||
(!_entityManager.EntityExists(_targetGrid) ? EntityLifeStage.Deleted : _entityManager.GetComponent<MetaDataComponent>(_targetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
!atmosphereSystem.IsSimulatedGrid(_targetGrid))
(!EntityManager.EntityExists(_targetGrid) ? EntityLifeStage.Deleted : EntityManager.GetComponent<MetaDataComponent>(_targetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
!_atmosphere.IsSimulatedGrid(_targetGrid))
{
return;
}
// Don't want it to be so obnoxious as to instantly murder anyone in the area but enough that
// it COULD start potentially start a bigger fire.
atmosphereSystem.HotspotExpose(_targetGrid, _targetTile, 700f, 50f, true);
_atmosphere.HotspotExpose(_targetGrid, _targetTile, 700f, 50f, true);
SoundSystem.Play("/Audio/Effects/sparks4.ogg", Filter.Pvs(_targetCoords), _targetCoords);
}
}

View File

@@ -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.
}
}

View File

@@ -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<IRobustRandom>();
_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<GameTicker>().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<PhysicsComponent>(meteor);
var meteor = EntityManager.SpawnEntity("MeteorLarge", spawnPosition);
var physics = EntityManager.GetComponent<PhysicsComponent>(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<IEntityManager>().EnsureComponent<TimedDespawnComponent>(meteor).Lifetime = 120f;
EnsureComp<TimedDespawnComponent>(meteor).Lifetime = 120f;
}
}
}

View File

@@ -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<string> SpawnedPrototypeChoices = new List<string>() //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<VentCritterSpawnLocationComponent, TransformComponent>().ToList();
_random.Shuffle(spawnLocations);
var spawnLocations = EntityManager.EntityQuery<VentCritterSpawnLocationComponent, TransformComponent>().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);
}
}
}

View File

@@ -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<IRobustRandom>().Next(60, 120);
base.Added();
_endAfter = RobustRandom.Next(60, 120);
}
public override void Startup()
public override void Started()
{
foreach (var component in _entityManager.EntityQuery<ApcPowerReceiverComponent>(true))
foreach (var component in EntityManager.EntityQuery<ApcPowerReceiverComponent>(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<ApcPowerReceiverComponent>(selected, out var powerReceiverComponent))
if (EntityManager.Deleted(selected)) continue;
if (EntityManager.TryGetComponent<ApcPowerReceiverComponent>(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();
}
}
}

View File

@@ -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<EntityUid> stationsToNotify = new();
var targetList = _entityManager.EntityQuery<SentienceTargetComponent>().ToList();
_random.Shuffle(targetList);
var targetList = EntityManager.EntityQuery<SentienceTargetComponent>().ToList();
RobustRandom.Shuffle(targetList);
var toMakeSentient = _random.Next(2, 5);
var toMakeSentient = RobustRandom.Next(2, 5);
var groups = new HashSet<string>();
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<SentienceTargetComponent>(target.Owner);
var comp = _entityManager.AddComponent<GhostTakeoverAvailableComponent>(target.Owner);
comp.RoleName = _entityManager.GetComponent<MetaDataComponent>(target.Owner).EntityName;
MakeSentientCommand.MakeSentient(target.Owner, EntityManager);
EntityManager.RemoveComponent<SentienceTargetComponent>(target.Owner);
var comp = EntityManager.AddComponent<GhostTakeoverAvailableComponent>(target.Owner);
comp.RoleName = EntityManager.GetComponent<MetaDataComponent>(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
);

View File

@@ -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;
/// <summary>
/// If the event has started and is currently running.
/// </summary>
public bool Running { get; set; }
/// <summary>
/// The time when this event last ran.
/// </summary>
public TimeSpan LastRun { get; set; } = TimeSpan.Zero;
/// <summary>
/// Human-readable name for the event.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// The weight this event has in the random-selection process.
/// </summary>
public virtual float Weight => WeightNormal;
/// <summary>
/// What should be said in chat when the event starts (if anything).
/// </summary>
public virtual string? StartAnnouncement { get; set; } = null;
/// <summary>
/// What should be said in chat when the event ends (if anything).
/// </summary>
protected virtual string? EndAnnouncement { get; } = null;
/// <summary>
/// Starting audio of the event.
/// </summary>
public virtual SoundSpecifier? StartAudio { get; set; } = new SoundPathSpecifier("/Audio/Announcements/attention.ogg");
/// <summary>
/// Ending audio of the event.
/// </summary>
public virtual SoundSpecifier? EndAudio { get; } = null;
public virtual AudioParams AudioParams { get; } = AudioParams.Default.WithVolume(-10f);
/// <summary>
/// In minutes, when is the first round time this event can start
/// </summary>
public virtual int EarliestStart { get; } = 5;
/// <summary>
/// In minutes, the amount of time before the same event can occur again
/// </summary>
public virtual int ReoccurrenceDelay { get; } = 30;
/// <summary>
/// When in the lifetime to call Start().
/// </summary>
protected virtual float StartAfter { get; } = 0.0f;
/// <summary>
/// When in the lifetime the event should end.
/// </summary>
protected virtual float EndAfter { get; set; } = 0.0f;
/// <summary>
/// How long has the event existed. Do not change this.
/// </summary>
private float Elapsed { get; set; } = 0.0f;
/// <summary>
/// How many players need to be present on station for the event to run
/// </summary>
/// <remarks>
/// To avoid running deadly events with low-pop
/// </remarks>
public virtual int MinimumPlayers { get; } = 0;
/// <summary>
/// How many times this event has run this round
/// </summary>
public int Occurrences { get; set; } = 0;
/// <summary>
/// How many times this even can occur in a single round
/// </summary>
public virtual int? MaxOccurrences { get; } = null;
/// <summary>
/// Whether or not the event is announced when it is run
/// </summary>
public virtual bool AnnounceEvent { get; } = true;
/// <summary>
/// Has the startup time elapsed?
/// </summary>
protected bool Started { get; set; } = false;
/// <summary>
/// Has this event commenced (announcement may or may not be used)?
/// </summary>
private bool Announced { get; set; } = false;
/// <summary>
/// Called once to setup the event after StartAfter has elapsed.
/// </summary>
public virtual void Startup()
{
Started = true;
Occurrences += 1;
LastRun = EntitySystem.Get<GameTicker>().RoundDuration();
IoCManager.Resolve<IAdminLogManager>()
.Add(LogType.EventStarted, LogImpact.High, $"Event startup: {Name}");
}
/// <summary>
/// Called once as soon as an event is active.
/// Can also be used for some initial setup.
/// </summary>
public virtual void Announce()
{
IoCManager.Resolve<IAdminLogManager>()
.Add(LogType.EventAnnounced, $"Event announce: {Name}");
if (AnnounceEvent && StartAnnouncement != null)
{
var chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
chatSystem.DispatchGlobalAnnouncement(StartAnnouncement, playDefaultSound: false, colorOverride: Color.Gold);
}
if (AnnounceEvent && StartAudio != null)
{
SoundSystem.Play(StartAudio.GetSound(), Filter.Broadcast(), AudioParams);
}
Announced = true;
Running = true;
}
/// <summary>
/// Called once when the station event ends for any reason.
/// </summary>
public virtual void Shutdown()
{
IoCManager.Resolve<IAdminLogManager>()
.Add(LogType.EventStopped, $"Event shutdown: {Name}");
if (AnnounceEvent && EndAnnouncement != null)
{
var chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
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;
}
/// <summary>
/// Called every tick when this event is running.
/// </summary>
/// <param name="frameTime"></param>
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<StationDataComponent>(targetStation).Grids;
if (possibleTargets.Count == 0)
{
targetGrid = EntityUid.Invalid;
return false;
}
targetGrid = robustRandom.Pick(possibleTargets);
if (!entityManager.TryGetComponent<IMapGridComponent>(targetGrid, out var gridComp)
|| !entityManager.TryGetComponent<TransformComponent>(targetGrid, out var transform))
return false;
var grid = gridComp.Grid;
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// An abstract entity system inherited by all station events for their behavior.
/// </summary>
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!;
/// <summary>
/// How long has the event existed. Do not change this.
/// </summary>
protected float Elapsed { get; set; }
public override void Initialize()
{
base.Initialize();
Sawmill = Logger.GetSawmill("stationevents");
}
/// <summary>
/// Called once to setup the event after StartAfter has elapsed, or if an event is forcibly started.
/// </summary>
public override void Started()
{
AdminLogManager.Add(LogType.EventStarted, LogImpact.High, $"Event started: {Configuration.Id}");
}
/// <summary>
/// Called once as soon as an event is added, for announcements.
/// Can also be used for some initial setup.
/// </summary>
public override void Added()
{
AdminLogManager.Add(LogType.EventAnnounced, $"Event added / announced: {Configuration.Id}");
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;
}
/// <summary>
/// Called once when the station event ends for any reason.
/// </summary>
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);
}
}
/// <summary>
/// Called every tick when this event is running.
/// Events are responsible for their own lifetime, so this handles starting and ending after time.
/// </summary>
public override void Update(float frameTime)
{
if (!RuleAdded || Configuration is not StationEventRuleConfiguration data)
return;
Elapsed += frameTime;
if (!RuleStarted && Elapsed >= data.StartAfter)
{
GameTicker.StartGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
}
if (RuleStarted && Elapsed >= data.EndAfter)
{
GameTicker.EndGameRule(PrototypeManager.Index<GameRulePrototype>(Prototype));
}
}
#region Helper Functions
protected void ForceEndSelf()
{
GameTicker.EndGameRule(PrototypeManager.Index<GameRulePrototype>(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<StationDataComponent>(targetStation).Grids;
if (possibleTargets.Count == 0)
{
targetGrid = EntityUid.Invalid;
return false;
}
targetGrid = RobustRandom.Pick(possibleTargets);
if (!TryComp<IMapGridComponent>(targetGrid, out var gridComp))
return false;
var grid = gridComp.Grid;
var atmosphereSystem = Get<AtmosphereSystem>();
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<GameRulePrototype>()
.Where(p => p.Configuration is StationEventRuleConfiguration).ToArray());
}
#endregion
}
}

View File

@@ -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<string> 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<ReagentPrototype>()
var allReagents = PrototypeManager.EnumeratePrototypes<ReagentPrototype>()
.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<GasVentPumpComponent, TransformComponent>())
foreach (var (_, transform) in EntityManager.EntityQuery<GasVentPumpComponent, TransformComponent>())
{
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);
}
}

View File

@@ -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<string> SpawnedPrototypeChoices = new List<string>()
{"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<VentCritterSpawnLocationComponent>().ToList();
_random.Shuffle(spawnLocations);
base.Started();
var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
var spawnLocations = EntityManager.EntityQuery<VentCritterSpawnLocationComponent>().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<TransformComponent>(location.Owner);
var coords = EntityManager.GetComponent<TransformComponent>(location.Owner);
_entityManager.SpawnEntity(spawnChoice, coords.Coordinates);
EntityManager.SpawnEntity(spawnChoice, coords.Coordinates);
}
}
}

View File

@@ -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
/// <summary>
/// Revives several dead entities as zombies
/// </summary>
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";
/// <summary>
/// Finds 1-3 random, dead entities accross the station
/// Finds 1-3 random, dead entities across the station
/// and turns them into zombies.
/// </summary>
public override void Startup()
public override void Started()
{
base.Startup();
HashSet<EntityUid> stationsToNotify = new();
base.Started();
List<MobStateComponent> deadList = new();
foreach (var mobState in _entityManager.EntityQuery<MobStateComponent>())
foreach (var mobState in EntityManager.EntityQuery<MobStateComponent>())
{
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<ZombifyOnDeathSystem>();
// Now we give it to people in the list of dead entities earlier.
var entSysMgr = IoCManager.Resolve<IEntitySystemManager>();
var stationSystem = entSysMgr.GetEntitySystem<StationSystem>();
var chatSystem = entSysMgr.GetEntitySystem<ChatSystem>();
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);
}
}
}

View File

@@ -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<StationEventSystem>();
var resultText = eventName == "random"
? stationSystem.RunRandomEvent()
: stationSystem.RunEvent(eventName);
shell.WriteLine(resultText);
}
private void Running(IConsoleShell shell, IPlayerSession? player)
{
var eventName = EntitySystem.Get<StationEventSystem>().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<StationEventSystem>();
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<StationEventSystem>();
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<StationEventSystem>();
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<StationEventSystem>().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<StationEventSystem>();
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;
}
}
}

View File

@@ -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<StationEvent> StationEvents => _stationEvents;
private readonly List<StationEvent> _stationEvents = new();
private const float MinimumTimeUntilFirstEvent = 300;
/// <summary>
/// How long until the next check for an event runs
/// </summary>
/// Default value is how long until first event is allowed
private float _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
/// <summary>
/// Whether random events can run
/// </summary>
/// 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;
/// <summary>
/// Admins can forcibly run events by passing in the Name
/// </summary>
/// <param name="name">The exact string for Name, without localization</param>
/// <returns></returns>
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));
}
/// <summary>
/// Randomly run a valid event <b>immediately</b>, ignoring earlieststart
/// </summary>
/// <returns></returns>
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));
}
/// <summary>
/// Randomly picks a valid event.
/// </summary>
public StationEvent? PickRandomEvent()
{
var availableEvents = AvailableEvents(true);
return FindEvent(availableEvents);
}
/// <summary>
/// Admins can stop the currently running event (if applicable) and reset the timer
/// </summary>
/// <returns></returns>
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<IReflectionManager>();
var typeFactory = IoCManager.Resolve<IDynamicTypeFactory>();
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<MsgRequestStationEvents>(RxRequest);
_netManager.RegisterNetMessage<MsgStationEvents>();
SubscribeLocalEvent<RoundRestartCleanupEvent>(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<GameTicker>().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();
}
}
/// <summary>
/// Reset the event timer once the event is done.
/// </summary>
private void ResetTimer()
{
// 5 - 15 minutes. TG does 3-10 but that's pretty frequent
_timeUntilNextEvent = _random.Next(300, 900);
}
/// <summary>
/// Pick a random event from the available events at this time, also considering their weightings.
/// </summary>
/// <returns></returns>
private StationEvent? FindEvent(List<StationEvent> 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;
}
/// <summary>
/// Gets the events that have met their player count, time-until start, etc.
/// </summary>
/// <param name="ignoreEarliestStart"></param>
/// <returns></returns>
private List<StationEvent> 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<StationEvent>();
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;
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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<string>();
public override void ReadFromBuffer(NetIncomingMessage buffer)
{
var serializer = IoCManager.Resolve<IRobustSerializer>();
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<IRobustSerializer>();
using (var stream = new MemoryStream())
{
serializer.SerializeDirect(stream, Events);
buffer.WriteVariableInt32((int)stream.Length);
stream.TryGetBuffer(out var segment);
buffer.Write(segment);
}
}
}
}

View File

@@ -1,2 +0,0 @@
station-events-window-not-loaded-text = Not loaded
station-events-window-random-text = Random

View File

@@ -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.

View File

@@ -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/list/pause/resume/stop/run <eventName/random>>
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 <eventName/random>: start a particular event now; <eventName> is case-insensitive and not localized
cmd-events-arg-subcommand = <subcommand>
cmd-events-arg-run-eventName = <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

View File

@@ -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-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}

View File

@@ -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

View File

@@ -65,3 +65,10 @@
config:
!type:GenericGameRuleConfiguration
id: Zombie
# event schedulers
- type: gameRule
id: BasicStationEventScheduler
config:
!type:GenericGameRuleConfiguration
id: BasicStationEventScheduler

View File

@@ -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