diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 431e4fbe09a..564092d2f99 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -185,7 +185,7 @@ namespace Content.Server.StationEvents } [CommandImplementation("lsprob")] - public IEnumerable<(string, float)> LsProb([CommandArgument] EntProtoId eventSchedulerProto) + public IEnumerable<(string, double)> LsProb([CommandArgument] EntProtoId eventSchedulerProto) { _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); @@ -196,19 +196,18 @@ namespace Content.Server.StationEvents if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) yield break; - if (!_stationEvent.TryListLimitedEvents(basicScheduler.ScheduledGameRules, out var events)) - yield break; + var sortedEvents + = _stationEvent.ListLimitedEvents(basicScheduler.ScheduledGameRules) + .OrderBy(x => -x.Item2); - var totalWeight = events.Sum(x => x.Value.Weight); // Well this shit definitely isnt correct now, and I see no way to make it correct. - // Its probably *fine* but it wont be accurate if the EntityTableSelector does any subsetting. - foreach (var (proto, comp) in events) // The only solution I see is to do a simulation, and we already have that, so...! + foreach (var eventProb in sortedEvents) { - yield return (proto.ID, comp.Weight * (float)basicScheduler.ScheduledGameRules.Prob / totalWeight); + yield return eventProb; } } [CommandImplementation("lsprobtheoretical")] - public IEnumerable<(string, float)> LsProbTime([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] int playerCount, [CommandArgument] float time) + public IEnumerable<(EntProtoId, double)> LsProbTime([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] int playerCount, [CommandArgument] float time) { _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); @@ -221,24 +220,21 @@ namespace Content.Server.StationEvents var timemins = time * 60; var theoryTime = TimeSpan.Zero + TimeSpan.FromSeconds(timemins); - if (!_stationEvent.TryListLimitedEvents(basicScheduler.ScheduledGameRules, - out var untimedEvents, - currentTime: theoryTime, - playerCount: playerCount)) - yield break; - var events = untimedEvents.Where(pair => pair.Value.EarliestStart <= timemins).ToList(); + var sortedEvents + = _stationEvent.ListLimitedEvents(basicScheduler.ScheduledGameRules, + currentTime: theoryTime, + playerCount: playerCount) + .OrderBy(x => -x.Item2); - var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob. - - foreach (var (proto, comp) in events) + foreach (var eventProb in sortedEvents) { - yield return (proto.ID, comp.Weight * (float)basicScheduler.ScheduledGameRules.Prob / totalWeight); + yield return eventProb; } } [CommandImplementation("prob")] - public float Prob([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] string eventId) + public double Prob([CommandArgument] EntProtoId eventSchedulerProto, [CommandArgument] string eventId) { _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); @@ -249,17 +245,15 @@ namespace Content.Server.StationEvents if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) return 0f; - if (!_stationEvent.TryListLimitedEvents(basicScheduler.ScheduledGameRules, out var events)) - return 0f; - - var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob. - var weight = 0f; - if (events.TryFirstOrNull(p => p.Key.ID == eventId, out var pair)) + foreach (var (proto, prob) in _stationEvent.ListLimitedEvents(basicScheduler.ScheduledGameRules)) { - weight = pair.Value.Value.Weight * (float)basicScheduler.ScheduledGameRules.Prob; + if (eventSchedulerProto != proto) + continue; + + return prob; } - return weight / totalWeight; + return 0f; } } } diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 219624d28de..7a983bc12a2 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -93,16 +93,17 @@ public sealed class EventManagerSystem : EntitySystem return randomLimitedEvent; } - /// - public bool TryListLimitedEvents( + /// + /// Builds a list of all possible events and their probabilities. + /// + public IEnumerable<(EntProtoId, double)> ListLimitedEvents( EntityTableSelector limitedEventsTable, - out Dictionary limitedEvents, TimeSpan? currentTime = null, int? playerCount = null) { var selectedEvents = _entityTable.ListSpawns(limitedEventsTable); - return TryBuildLimitedEvents(selectedEvents, out limitedEvents, currentTime, playerCount); + return ListLimitedEvents(selectedEvents, currentTime, playerCount); } /// @@ -117,6 +118,49 @@ public sealed class EventManagerSystem : EntitySystem return TryBuildLimitedEvents(selectedEvents, out limitedEvents, currentTime, playerCount); } + public IEnumerable<(EntProtoId, double)> ListLimitedEvents( + IEnumerable<(EntProtoId, double)> selectedEvents, + TimeSpan? currentTime = null, + int? playerCount = null) + { + var limitedEvents = new List<(EntProtoId, double)>(); + + playerCount ??= _playerManager.PlayerCount; + + // playerCount does a lock so we'll just keep the variable here + currentTime ??= GameTicker.RoundDuration(); + + var totalWeight = 0f; + + foreach (var (eventId, prob) in selectedEvents) + { + if (!_prototype.Resolve(eventId, out var eventproto)) + continue; + + if (eventproto.Abstract) + continue; + + if (!eventproto.TryGetComponent(out var stationEvent, EntityManager.ComponentFactory)) + continue; + + if (!CanRun(eventproto, stationEvent, playerCount.Value, currentTime.Value)) + continue; + + limitedEvents.Add((eventproto, prob * stationEvent.Weight)); + totalWeight += stationEvent.Weight; + } + + if (!limitedEvents.Any() || totalWeight <= 0) + yield break; + + for (var i = 0; i < limitedEvents.Count; i++) + { + var eventWeight = limitedEvents[i]; + eventWeight.Item2 /= totalWeight; + yield return eventWeight; + } + } + /// /// Builds a dictionary of valid event prototypes from a list of . /// Dictionary output consists of the valid prototype as the key, and the as the value. diff --git a/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs b/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs index 0fb477ca07a..3f1ad671dda 100644 --- a/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs @@ -1,3 +1,4 @@ +using System.Linq; using Robust.Shared.Prototypes; namespace Content.Shared.EntityTable.EntitySelectors; @@ -23,4 +24,26 @@ public sealed partial class AllSelector : EntityTableSelector } } } + + protected override IEnumerable<(EntProtoId spawn, double)> ListSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + foreach (var child in Children) + { + foreach (var (spawn, prob) in child.ListSpawns(entMan, proto, ctx)) + { + yield return (spawn, prob); + } + } + } + + protected override IEnumerable<(EntProtoId spawn, double)> AverageSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + foreach (var child in Children) + { + foreach (var (spawn, prob) in child.AverageSpawns(entMan, proto, ctx)) + { + yield return (spawn, prob); + } + } + } } diff --git a/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs index 98346031365..ee2b0951d09 100644 --- a/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs @@ -27,4 +27,14 @@ public sealed partial class EntSelector : EntityTableSelector yield return Id; } } + + protected override IEnumerable<(EntProtoId spawn, double)> ListSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + yield return (Id, 1f); + } + + protected override IEnumerable<(EntProtoId spawn, double)> AverageSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + yield return (Id, Amount.Average()); + } } diff --git a/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs index 09258e1bb47..fe3aa08bd98 100644 --- a/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.EntityTable.Conditions; using Content.Shared.EntityTable.ValueSelector; using JetBrains.Annotations; @@ -84,17 +85,30 @@ public abstract partial class EntityTableSelector } /// - /// Gets the spawns in a given table, ignoring the requirements for the table. - /// This should only be used for debugging! + /// Gets a list of every spawn in the table, and the odds of that spawn occuring, ignoring conditions. /// - public IEnumerable ListSpawns(System.Random rand, - IEntityManager entMan, + public IEnumerable<(EntProtoId spawn, double prob)> ListSpawns(IEntityManager entMan, IPrototypeManager proto, - EntityTableContext ctx) + EntityTableContext ctx, + float mod = 1f) { - foreach (var spawn in GetSpawnsImplementation(rand, entMan, proto, ctx)) + foreach (var (spawn, prob) in ListSpawnsImplementation(entMan, proto, ctx)) { - yield return spawn; + yield return (spawn, prob * Prob * Rolls.Odds() * mod); + } + } + + /// + /// Gets a list of every spawn in the table, and the average number of occurrences, ignoring conditions. + /// + public IEnumerable<(EntProtoId spawn, double prob)> AverageSpawns(IEntityManager entMan, + IPrototypeManager proto, + EntityTableContext ctx, + float mod = 1f) + { + foreach (var (spawn, prob) in AverageSpawnsImplementation(entMan, proto, ctx)) + { + yield return (spawn, prob * Prob * Rolls.Average() * mod); } } @@ -102,4 +116,12 @@ public abstract partial class EntityTableSelector IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx); + + protected abstract IEnumerable<(EntProtoId spawn, double)> ListSpawnsImplementation(IEntityManager entMan, + IPrototypeManager proto, + EntityTableContext ctx); + + protected abstract IEnumerable<(EntProtoId spawn, double)> AverageSpawnsImplementation(IEntityManager entMan, + IPrototypeManager proto, + EntityTableContext ctx); } diff --git a/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs b/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs index 0d2a451bdc3..d51b467f599 100644 --- a/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; @@ -33,4 +34,32 @@ public sealed partial class GroupSelector : EntityTableSelector return pick.GetSpawns(rand, entMan, proto, ctx); } + + protected override IEnumerable<(EntProtoId spawn, double)> ListSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + var totalWeight = Children.Sum(x => x.Weight); + + foreach (var child in Children) + { + var weightMod = child.Weight / totalWeight; + foreach (var (ent, prob) in child.ListSpawns(entMan, proto, ctx, weightMod)) + { + yield return (ent, prob); + } + } + } + + protected override IEnumerable<(EntProtoId spawn, double)> AverageSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + var totalWeight = Children.Sum(x => x.Weight); + + foreach (var child in Children) + { + var weightMod = child.Weight / totalWeight; + foreach (var (ent, prob) in child.AverageSpawns(entMan, proto, ctx, weightMod)) + { + yield return (ent, prob); + } + } + } } diff --git a/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs b/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs index 4a96c89ccbe..eb672a4249a 100644 --- a/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs @@ -18,4 +18,14 @@ public sealed partial class NestedSelector : EntityTableSelector { return proto.Index(TableId).Table.GetSpawns(rand, entMan, proto, ctx); } + + protected override IEnumerable<(EntProtoId spawn, double)> ListSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + return proto.Index(TableId).Table.ListSpawns(entMan, proto, ctx); + } + + protected override IEnumerable<(EntProtoId spawn, double)> AverageSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + return proto.Index(TableId).Table.AverageSpawns(entMan, proto, ctx); + } } diff --git a/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs b/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs index f41c5424203..c7ae87c9056 100644 --- a/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs +++ b/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs @@ -14,4 +14,14 @@ public sealed partial class NoneSelector : EntityTableSelector { yield break; } + + protected override IEnumerable<(EntProtoId spawn, double)> ListSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + yield break; + } + + protected override IEnumerable<(EntProtoId spawn, double)> AverageSpawnsImplementation(IEntityManager entMan, IPrototypeManager proto, EntityTableContext ctx) + { + yield break; + } } diff --git a/Content.Shared/EntityTable/EntityTableSystem.cs b/Content.Shared/EntityTable/EntityTableSystem.cs index 69ddb0e36e8..a295974775e 100644 --- a/Content.Shared/EntityTable/EntityTableSystem.cs +++ b/Content.Shared/EntityTable/EntityTableSystem.cs @@ -27,15 +27,44 @@ public sealed class EntityTableSystem : EntitySystem return table.GetSpawns(rand, EntityManager, _prototypeManager, ctx); } - // TODO: Have this method be much better for entity tables - public IEnumerable ListSpawns(EntityTableSelector? table, System.Random? rand = null, EntityTableContext? ctx = null) + public IEnumerable<(EntProtoId spawn, double)> ListSpawns(EntityTablePrototype entTableProto, EntityTableContext? ctx = null) + { + return ListSpawns(entTableProto.Table, ctx); + } + + /// + /// Builds a list of all the spawns in an EntityTable as keys, and their modified weights as values. + /// + /// Table we're examining + /// Optional extra context + public IEnumerable<(EntProtoId spawn, double)> ListSpawns(EntityTableSelector? table, EntityTableContext? ctx = null) { if (table == null) - return new List(); + return new List<(EntProtoId spawn, double)>(); - rand ??= _random.GetRandom(); ctx ??= new EntityTableContext(); - return table.ListSpawns(rand, EntityManager, _prototypeManager, ctx); + return table.ListSpawns(EntityManager, _prototypeManager, ctx); + } + + /// + public IEnumerable<(EntProtoId spawn, double)> AverageSpawns(EntityTablePrototype entTableProto, EntityTableContext? ctx = null) + { + return AverageSpawns(entTableProto.Table, ctx); + } + + /// + /// Returns the average expected spawns of a specific entity table. + /// + /// The entity table we want the spawns of + /// Optional EntityTableContext + /// + public IEnumerable<(EntProtoId spawn, double)> AverageSpawns(EntityTableSelector? table, EntityTableContext? ctx = null) + { + if (table == null) + return new List<(EntProtoId spawn, double)>(); + + ctx ??= new EntityTableContext(); + return table.AverageSpawns(EntityManager, _prototypeManager, ctx); } } diff --git a/Content.Shared/EntityTable/ValueSelector/BinomialNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/BinomialNumberSelector.cs index b3ed076e70e..c05d3bfa315 100644 --- a/Content.Shared/EntityTable/ValueSelector/BinomialNumberSelector.cs +++ b/Content.Shared/EntityTable/ValueSelector/BinomialNumberSelector.cs @@ -35,4 +35,20 @@ public sealed partial class BinomialNumberSelector : NumberSelector return count; // get binomialed motherfucker } + + public override float Odds() + { + if (Chance >= 1f) + return 1; + + return 1 - MathF.Pow(1 - Chance, Trials); + } + + public override float Average() + { + if (Chance >= 1f) + return Trials; + + return Trials * Chance; + } } diff --git a/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs index a1a7e4ac582..30c664ac408 100644 --- a/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs +++ b/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs @@ -17,4 +17,15 @@ public sealed partial class ConstantNumberSelector : NumberSelector { return Value; } + + public override float Odds() + { + // You really shouldn't have a constant value of 0 ever. + return 1; + } + + public override float Average() + { + return Value; + } } diff --git a/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs index 763578a3841..f4e3e7b2865 100644 --- a/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs +++ b/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs @@ -10,4 +10,16 @@ namespace Content.Shared.EntityTable.ValueSelector; public abstract partial class NumberSelector { public abstract int Get(System.Random rand); + + /// + /// Odds of occurrence + /// + /// An odds multiplier of at least one occurrence + public abstract float Odds(); + + /// + /// Average number of occurrences + /// + /// The average amount of occurrences + public abstract float Average(); } diff --git a/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs index 2eca71d4009..0e82a2a378c 100644 --- a/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs +++ b/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs @@ -19,4 +19,14 @@ public sealed partial class RangeNumberSelector : NumberSelector // so we add 1 to the second number. return rand.Next(Range.X, Range.Y + 1); } + + public override float Odds() + { + return Range.X == 0 ? 1f / (Range.Y + 1) : 1; + } + + public override float Average() + { + return (Range.X + Range.Y) / 2f; + } }