CatStation/Content.Server/GameTicking/Rules/DynamicRuleSystem.cs
Princess Cheeseballs 015a1a44d0
Fix Dynamic Rule (Real) (#46197)
fixe
2026-09-26 22:11:51 +00:00

202 lines
6.8 KiB
C#

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;
/// <summary>
/// Handler for dynamically adding subgamemode rules based on a budget.
/// </summary>
/// <seealso cref="DynamicRuleComponent"/>
public sealed partial class DynamicRuleSystem : GameRuleSystem<DynamicRuleComponent>
{
[Dependency] private IAdminLogManager _adminLog = default!;
[Dependency] private EntityTableSystem _entityTable = default!;
[Dependency] private RoundEndSystem _roundEnd = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private EntityQuery<DynamicRuleCostComponent> _ruleCostQuery;
protected override void Added(Entity<DynamicRuleComponent, GameRuleComponent> 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<DynamicRuleComponent, GameRuleComponent> 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<DynamicRuleComponent> 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));
}
/// <summary>
/// Generates and returns a list of randomly selected,
/// valid rules to spawn based on <see cref="DynamicRuleComponent.Table"/>.
/// </summary>
private IEnumerable<EntProtoId> GetRuleSpawns(Entity<DynamicRuleComponent> entity)
{
UpdateBudget((entity.Owner, entity.Comp));
var ctx = new EntityTableContext(new Dictionary<string, object>
{
{ HasBudgetCondition.BudgetContextKey, entity.Comp.Budget },
});
return _entityTable.GetSpawns(entity.Comp.Table, ctx: ctx);
}
/// <summary>
/// Updates the budget of the provided dynamic rule component based on the amount of time since the last update
/// multiplied by the <see cref="DynamicRuleComponent.BudgetPerSecond"/> value.
/// </summary>
private void UpdateBudget(Entity<DynamicRuleComponent> entity)
{
var duration = (float)(Timing.CurTime - entity.Comp.LastBudgetUpdate).TotalSeconds;
entity.Comp.Budget += duration * entity.Comp.BudgetPerSecond;
entity.Comp.LastBudgetUpdate = Timing.CurTime;
}
/// <summary>
/// Executes this rule, generating new dynamic rules and starting them.
/// </summary>
/// <returns>
/// Returns a list of the rules that were executed.
/// </returns>
private List<EntityUid> Execute(Entity<DynamicRuleComponent> entity)
{
entity.Comp.NextRuleTime =
Timing.CurTime + _random.Next(entity.Comp.MinRuleInterval, entity.Comp.MaxRuleInterval);
var executedRules = new List<EntityUid>();
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<EntityUid> GetDynamicRules()
{
var rules = new List<EntityUid>();
var query = EntityQueryEnumerator<DynamicRuleComponent, GameRuleComponent>();
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<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return null;
UpdateBudget((entity.Owner, entity.Comp));
return entity.Comp.Budget;
}
public float? AdjustBudget(Entity<DynamicRuleComponent?> 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<DynamicRuleComponent?> 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<EntProtoId> DryRun(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return new List<EntProtoId>();
return GetRuleSpawns((entity.Owner, entity.Comp));
}
public IEnumerable<EntityUid> ExecuteNow(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return new List<EntityUid>();
return Execute((entity.Owner, entity.Comp));
}
public IEnumerable<EntityUid> Rules(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return new List<EntityUid>();
return entity.Comp.Rules;
}
#endregion
}