From 2451cfd69ddc9b424cf9874fbb31405e221dcfb6 Mon Sep 17 00:00:00 2001 From: Kara Date: Mon, 6 May 2024 09:33:30 +0200 Subject: [PATCH] Round event frequency simulation command (#27718) --- .../BasicStationEventSchedulerSystem.cs | 59 ++++++++++++++++++- .../StationEvents/EventManagerSystem.cs | 17 +++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 11ae43b5b3..80ac21dd92 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration; +using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; @@ -21,6 +22,9 @@ namespace Content.Server.StationEvents [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EventManagerSystem _event = default!; + public const float MinEventTime = 60 * 5; // DeltaV - Was 3 minutes + public const float MaxEventTime = 60 * 25; // DeltaV - Was 10 minutes + protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) { @@ -57,7 +61,7 @@ namespace Content.Server.StationEvents /// private void ResetTimer(BasicStationEventSchedulerComponent component) { - component.TimeUntilNextEvent = _random.Next(5 * 60, 25 * 60); // DeltaV - Was between 3 and 10 minutes + component.TimeUntilNextEvent = _random.NextFloat(MinEventTime, MaxEventTime); } } @@ -65,6 +69,59 @@ namespace Content.Server.StationEvents public sealed class StationEventCommand : ToolshedCommand { private EventManagerSystem? _stationEvent; + private BasicStationEventSchedulerSystem? _basicScheduler; + private IRobustRandom? _random; + + /// + /// Estimates the expected number of times an event will run over the course of X rounds, taking into account weights and + /// how many events are expected to run over a given timeframe for a given playercount by repeatedly simulating rounds. + /// Effectively /100 (if you put 100 rounds) = probability an event will run per round. + /// + /// + /// This isn't perfect. Code path eventually goes into , which requires + /// state from . As a result, you should probably just run this locally and not doing + /// a real round (it won't pollute the state, but it will get contaminated by previously ran events in the actual round) + /// and things like `MaxOccurrences` and `ReoccurrenceDelay` won't be respected. + /// + /// I consider these to not be that relevant to the analysis here though (and I don't want most uses of them + /// to even exist) so I think it's fine. + /// + [CommandImplementation("simulate")] + public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev) + { + _stationEvent ??= GetSys(); + _basicScheduler ??= GetSys(); + _random ??= IoCManager.Resolve(); + + var occurrences = new Dictionary(); + + foreach (var ev in _stationEvent.AllEvents()) + { + occurrences.Add(ev.Key.ID, 0); + } + + for (var i = 0; i < rounds; i++) + { + var curTime = TimeSpan.Zero; + var randomEndTime = _random.NextGaussian(roundEndMean, roundEndStdDev) * 60; // *60 = minutes to seconds + if (randomEndTime <= 0) + continue; + + while (curTime.TotalSeconds < randomEndTime) + { + // sim an event + curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime)); + var available = _stationEvent.AvailableEvents(false, playerCount, curTime); + var ev = _stationEvent.FindEvent(available); + if (ev == null) + continue; + + occurrences[ev] += 1; + } + } + + return occurrences.Select(p => (p.Key, (float) p.Value)).OrderByDescending(p => p.Item2); + } [CommandImplementation("lsprob")] public IEnumerable<(string, float)> LsProb() diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 261e8ca46d..d96d3fa729 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -63,7 +63,7 @@ public sealed class EventManagerSystem : EntitySystem /// Pick a random event from the available events at this time, also considering their weightings. /// /// - private string? FindEvent(Dictionary availableEvents) + public string? FindEvent(Dictionary availableEvents) { if (availableEvents.Count == 0) { @@ -97,16 +97,20 @@ public sealed class EventManagerSystem : EntitySystem /// /// Gets the events that have met their player count, time-until start, etc. /// - /// + /// Override for player count, if using this to simulate events rather than in an actual round. + /// Override for round time, if using this to simulate events rather than in an actual round. /// - private Dictionary AvailableEvents(bool ignoreEarliestStart = false) + public Dictionary AvailableEvents( + bool ignoreEarliestStart = false, + int? playerCountOverride = null, + TimeSpan? currentTimeOverride = null) { - var playerCount = _playerManager.PlayerCount; + var playerCount = playerCountOverride ?? _playerManager.PlayerCount; // playerCount does a lock so we'll just keep the variable here - var currentTime = !ignoreEarliestStart + var currentTime = currentTimeOverride ?? (!ignoreEarliestStart ? GameTicker.RoundDuration() - : TimeSpan.Zero; + : TimeSpan.Zero); var result = new Dictionary(); @@ -114,7 +118,6 @@ public sealed class EventManagerSystem : EntitySystem { if (CanRun(proto, stationEvent, playerCount, currentTime)) { - Log.Debug($"Adding event {proto.ID} to possibilities"); result.Add(proto, stationEvent); } }