using Content.Server.Administration.Logs; using Content.Server.RoundEnd; using Content.Shared.Database; using Content.Shared.EntityTable; using Content.Shared.EntityTable.Conditions; using Content.Shared.GameTicking.Components; using Content.Shared.GameTicking.Rules; using Content.Shared.GameTicking.Rules.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules; /// /// Handler for dynamically adding subgamemode rules based on a budget. /// /// public sealed partial class DynamicRuleSystem : GameRuleSystem { [Dependency] private IAdminLogManager _adminLog = default!; [Dependency] private EntityTableSystem _entityTable = default!; [Dependency] private RoundEndSystem _roundEnd = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private EntityQuery _ruleCostQuery; protected override void Added(Entity ent, ref GameRuleAddedEvent args) { base.Added(ent, ref args); var dynamic = ent.Comp1; dynamic.Budget = _random.Next(dynamic.StartingBudgetMin, dynamic.StartingBudgetMax); dynamic.NextRuleTime = Timing.CurTime + _random.Next(dynamic.MinRuleInterval, dynamic.MaxRuleInterval); } protected override void Started(Entity ent, ref GameRuleStartedEvent args) { base.Started(ent, ref args); // Since we don't know how long until this rule is activated, we need to // set the last budget update to now so it doesn't immediately give the component a bunch of points. ent.Comp1.LastBudgetUpdate = Timing.CurTime; Execute(ent); } // TODO: We may not actually want to do this protected override void Ended(Entity rule, ref GameRuleEndedEvent args) { base.Ended(rule, ref args); foreach (var gameRule in rule.Comp.Rules) { GameTicker.EndGameRule(gameRule); } } protected override void ActiveTick(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, float frameTime) { base.ActiveTick(uid, component, gameRule, frameTime); if (Timing.CurTime < component.NextRuleTime) return; // only spawn antags if we aren't evacuating. if (!_roundEnd.IsRoundEndRequested() || _roundEnd.CanCallOrRecall()) Execute((uid, component)); } /// /// Generates and returns a list of randomly selected, /// valid rules to spawn based on . /// private IEnumerable GetRuleSpawns(Entity entity) { UpdateBudget((entity.Owner, entity.Comp)); var ctx = new EntityTableContext(new Dictionary { { HasBudgetCondition.BudgetContextKey, entity.Comp.Budget }, }); return _entityTable.GetSpawns(entity.Comp.Table, ctx: ctx); } /// /// Updates the budget of the provided dynamic rule component based on the amount of time since the last update /// multiplied by the value. /// private void UpdateBudget(Entity entity) { var duration = (float)(Timing.CurTime - entity.Comp.LastBudgetUpdate).TotalSeconds; entity.Comp.Budget += duration * entity.Comp.BudgetPerSecond; entity.Comp.LastBudgetUpdate = Timing.CurTime; } /// /// Executes this rule, generating new dynamic rules and starting them. /// /// /// Returns a list of the rules that were executed. /// private List Execute(Entity entity) { entity.Comp.NextRuleTime = Timing.CurTime + _random.Next(entity.Comp.MinRuleInterval, entity.Comp.MaxRuleInterval); var executedRules = new List(); foreach (var rule in GetRuleSpawns(entity)) { if (!GameTicker.StartGameRule(rule, out var ruleUid)) continue; executedRules.Add(ruleUid.Value); if (_ruleCostQuery.TryComp(ruleUid, out var cost)) { entity.Comp.Budget -= cost.Cost; _adminLog.Add(LogType.EventRan, LogImpact.High, $"{ToPrettyString(entity)} ran rule {ToPrettyString(ruleUid)} with cost {cost.Cost} on budget {entity.Comp.Budget}."); } else { _adminLog.Add(LogType.EventRan, LogImpact.High, $"{ToPrettyString(entity)} ran rule {ToPrettyString(ruleUid)} which had no cost."); } } entity.Comp.Rules.AddRange(executedRules); return executedRules; } #region Command Methods public List GetDynamicRules() { var rules = new List(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _, out var comp)) { if (!GameTicker.IsGameRuleActive((uid, comp))) continue; rules.Add(uid); } return rules; } public float? GetRuleBudget(Entity entity) { if (!Resolve(entity, ref entity.Comp)) return null; UpdateBudget((entity.Owner, entity.Comp)); return entity.Comp.Budget; } public float? AdjustBudget(Entity entity, float amount) { if (!Resolve(entity, ref entity.Comp)) return null; UpdateBudget((entity.Owner, entity.Comp)); entity.Comp.Budget += amount; return entity.Comp.Budget; } public float? SetBudget(Entity entity, float amount) { if (!Resolve(entity, ref entity.Comp)) return null; entity.Comp.LastBudgetUpdate = Timing.CurTime; entity.Comp.Budget = amount; return entity.Comp.Budget; } public IEnumerable DryRun(Entity entity) { if (!Resolve(entity, ref entity.Comp)) return new List(); return GetRuleSpawns((entity.Owner, entity.Comp)); } public IEnumerable ExecuteNow(Entity entity) { if (!Resolve(entity, ref entity.Comp)) return new List(); return Execute((entity.Owner, entity.Comp)); } public IEnumerable Rules(Entity entity) { if (!Resolve(entity, ref entity.Comp)) return new List(); return entity.Comp.Rules; } #endregion }