1
0

BasicStationEventSchedulerSystem.cs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. using System.Linq;
  2. using Content.Server.Administration;
  3. using Content.Server.GameTicking;
  4. using Content.Server.GameTicking.Rules;
  5. using Content.Server.StationEvents.Components;
  6. using Content.Shared.Administration;
  7. using Content.Shared.EntityTable;
  8. using Content.Shared.GameTicking.Components;
  9. using JetBrains.Annotations;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Random;
  12. using Robust.Shared.Toolshed;
  13. using Robust.Shared.Utility;
  14. namespace Content.Server.StationEvents
  15. {
  16. /// <summary>
  17. /// The basic event scheduler rule, loosely based off of /tg/ events, which most
  18. /// game presets use.
  19. /// </summary>
  20. [UsedImplicitly]
  21. public sealed class BasicStationEventSchedulerSystem : GameRuleSystem<BasicStationEventSchedulerComponent>
  22. {
  23. [Dependency] private readonly IRobustRandom _random = default!;
  24. [Dependency] private readonly EventManagerSystem _event = default!;
  25. protected override void Started(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
  26. GameRuleStartedEvent args)
  27. {
  28. // A little starting variance so schedulers dont all proc at once.
  29. component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + 120);
  30. }
  31. protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
  32. GameRuleEndedEvent args)
  33. {
  34. component.TimeUntilNextEvent = component.MinimumTimeUntilFirstEvent;
  35. }
  36. public override void Update(float frameTime)
  37. {
  38. base.Update(frameTime);
  39. if (!_event.EventsEnabled)
  40. return;
  41. var query = EntityQueryEnumerator<BasicStationEventSchedulerComponent, GameRuleComponent>();
  42. while (query.MoveNext(out var uid, out var eventScheduler, out var gameRule))
  43. {
  44. if (!GameTicker.IsGameRuleActive(uid, gameRule))
  45. continue;
  46. if (eventScheduler.TimeUntilNextEvent > 0)
  47. {
  48. eventScheduler.TimeUntilNextEvent -= frameTime;
  49. continue;
  50. }
  51. _event.RunRandomEvent(eventScheduler.ScheduledGameRules);
  52. ResetTimer(eventScheduler);
  53. }
  54. }
  55. /// <summary>
  56. /// Reset the event timer once the event is done.
  57. /// </summary>
  58. private void ResetTimer(BasicStationEventSchedulerComponent component)
  59. {
  60. component.TimeUntilNextEvent = component.MinMaxEventTiming.Next(_random);
  61. }
  62. }
  63. [ToolshedCommand, AdminCommand(AdminFlags.Debug)]
  64. public sealed class StationEventCommand : ToolshedCommand
  65. {
  66. private EventManagerSystem? _stationEvent;
  67. private EntityTableSystem? _entityTable;
  68. private IComponentFactory? _compFac;
  69. private IRobustRandom? _random;
  70. /// <summary>
  71. /// Estimates the expected number of times an event will run over the course of X rounds, taking into account weights and
  72. /// how many events are expected to run over a given timeframe for a given playercount by repeatedly simulating rounds.
  73. /// Effectively /100 (if you put 100 rounds) = probability an event will run per round.
  74. /// </summary>
  75. /// <remarks>
  76. /// This isn't perfect. Code path eventually goes into <see cref="EventManagerSystem.CanRun"/>, which requires
  77. /// state from <see cref="GameTicker"/>. As a result, you should probably just run this locally and not doing
  78. /// a real round (it won't pollute the state, but it will get contaminated by previously ran events in the actual round)
  79. /// and things like `MaxOccurrences` and `ReoccurrenceDelay` won't be respected.
  80. ///
  81. /// I consider these to not be that relevant to the analysis here though (and I don't want most uses of them
  82. /// to even exist) so I think it's fine.
  83. /// </remarks>
  84. [CommandImplementation("simulate")]
  85. public IEnumerable<(string, float)> Simulate(EntityPrototype eventScheduler, int rounds, int playerCount, float roundEndMean, float roundEndStdDev)
  86. {
  87. _stationEvent ??= GetSys<EventManagerSystem>();
  88. _entityTable ??= GetSys<EntityTableSystem>();
  89. _compFac ??= IoCManager.Resolve<IComponentFactory>();
  90. _random ??= IoCManager.Resolve<IRobustRandom>();
  91. var occurrences = new Dictionary<string, int>();
  92. foreach (var ev in _stationEvent.AllEvents())
  93. {
  94. occurrences.Add(ev.Key.ID, 0);
  95. }
  96. if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
  97. {
  98. return occurrences.Select(p => (p.Key, (float)p.Value)).OrderByDescending(p => p.Item2);
  99. }
  100. var compMinMax = basicScheduler.MinMaxEventTiming; // we gotta do this since we cant execute on comp w/o an ent.
  101. for (var i = 0; i < rounds; i++)
  102. {
  103. var curTime = TimeSpan.Zero;
  104. var randomEndTime = _random.NextGaussian(roundEndMean, roundEndStdDev) * 60; // *60 = minutes to seconds
  105. if (randomEndTime <= 0)
  106. continue;
  107. while (curTime.TotalSeconds < randomEndTime)
  108. {
  109. // sim an event
  110. curTime += TimeSpan.FromSeconds(compMinMax.Next(_random));
  111. if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var selectedEvents))
  112. {
  113. continue; // doesnt break because maybe the time is preventing events being available.
  114. }
  115. var available = _stationEvent.AvailableEvents(false, playerCount, curTime);
  116. var plausibleEvents = new Dictionary<EntityPrototype, StationEventComponent>(available.Intersect(selectedEvents)); // C# makes me sad
  117. var ev = _stationEvent.FindEvent(plausibleEvents);
  118. if (ev == null)
  119. continue;
  120. occurrences[ev] += 1;
  121. }
  122. }
  123. return occurrences.Select(p => (p.Key, (float) p.Value)).OrderByDescending(p => p.Item2);
  124. }
  125. [CommandImplementation("lsprob")]
  126. public IEnumerable<(string, float)> LsProb(EntityPrototype eventScheduler)
  127. {
  128. _compFac ??= IoCManager.Resolve<IComponentFactory>();
  129. _stationEvent ??= GetSys<EventManagerSystem>();
  130. if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
  131. yield break;
  132. if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events))
  133. yield break;
  134. var totalWeight = events.Sum(x => x.Value.Weight); // Well this shit definitely isnt correct now, and I see no way to make it correct.
  135. // Its probably *fine* but it wont be accurate if the EntityTableSelector does any subsetting.
  136. foreach (var (proto, comp) in events) // The only solution I see is to do a simulation, and we already have that, so...!
  137. {
  138. yield return (proto.ID, comp.Weight / totalWeight);
  139. }
  140. }
  141. [CommandImplementation("lsprobtime")]
  142. public IEnumerable<(string, float)> LsProbTime(EntityPrototype eventScheduler, float time)
  143. {
  144. _compFac ??= IoCManager.Resolve<IComponentFactory>();
  145. _stationEvent ??= GetSys<EventManagerSystem>();
  146. if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
  147. yield break;
  148. if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var untimedEvents))
  149. yield break;
  150. var events = untimedEvents.Where(pair => pair.Value.EarliestStart <= time).ToList();
  151. var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob.
  152. foreach (var (proto, comp) in events)
  153. {
  154. yield return (proto.ID, comp.Weight / totalWeight);
  155. }
  156. }
  157. [CommandImplementation("prob")]
  158. public float Prob(EntityPrototype eventScheduler, string eventId)
  159. {
  160. _compFac ??= IoCManager.Resolve<IComponentFactory>();
  161. _stationEvent ??= GetSys<EventManagerSystem>();
  162. if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
  163. return 0f;
  164. if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events))
  165. return 0f;
  166. var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob.
  167. var weight = 0f;
  168. if (events.TryFirstOrNull(p => p.Key.ID == eventId, out var pair))
  169. {
  170. weight = pair.Value.Value.Weight;
  171. }
  172. return weight / totalWeight;
  173. }
  174. }
  175. }