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

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