using Content.Shared.Alert; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Prototypes; using Content.Shared.Rejuvenate; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Nutrition.EntitySystems; /// /// This system manages . It handles the change of satiations in /// and external changes to satiations through accessors like . /// public abstract partial class SatiationSystem : EntitySystem { [Dependency] private IGameTiming _timing = default!; [Dependency] private AlertsSystem _alerts = default!; /// /// The ID of the Hunger satiation type. Provided because it is so commonly used in Content. /// public static readonly ProtoId Hunger = "Hunger"; /// /// The ID of the Thirst satiation type. Provided because it is so commonly used in Content. /// public static readonly ProtoId Thirst = "Thirst"; /// public override void Update(float frameTime) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var component)) { var entity = new Entity(uid, component); foreach (var satiation in component.Satiations.Values) { if (!ProtoMan.Resolve(satiation.Prototype, out var proto)) continue; if (_timing.CurTime >= satiation.NextAlertUpdateTime) { UpdateAlerts(entity, satiation, proto); } if (_timing.CurTime >= satiation.NextChangeRateModUpdateTime) { SetAuthoritativeValue(entity, satiation, proto, CalculateCurrentValue(satiation, proto)); } } } } /// /// Sets starting satiation values. /// [SubscribeLocalEvent] private void OnMapInit(Entity entity, ref MapInitEvent args) { foreach (var (type, satiation) in entity.Comp.Satiations) { AddSatiation(entity.AsNullable(), type, satiation); } } /// /// Clears alerts. /// [SubscribeLocalEvent] private void OnShutdown(Entity entity, ref ComponentShutdown args) { foreach (var satiation in entity.Comp.Satiations.Values) { if (!ProtoMan.Resolve(satiation.Prototype, out var proto)) continue; _alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory); } } /// /// Sets all satiations to their maximums. /// [SubscribeLocalEvent] private void OnRejuvenate(Entity entity, ref RejuvenateEvent args) { foreach (var type in entity.Comp.Satiations.Keys) { SetValue(entity, type, satiationValue: int.MaxValue); } } /// /// This is basically a special-case reimplementation of . /// [SubscribeLocalEvent] private void UpdateAlertsOnSatiationUpdated(Entity entity, ref SatiationUpdateEvent args) { if (entity.Comp.GetOrNull(args.Type) is not { } satiation || !ProtoMan.Resolve(satiation.Prototype, out var proto)) return; UpdateAlerts(entity, satiation, proto); } /// /// This helper resolves and returns the corresponding from /// along with its . /// Returns null if the prototype fails to resolve, or if the component does not have the specified satiation. /// private (Satiation Satiation, SatiationPrototype Proto)? GetAndResolveSatiationOfType( SatiationComponent satiations, [ForbidLiteral] ProtoId type ) { if (satiations.GetOrNull(type) is not { } satiation || !ProtoMan.Resolve(satiation.Prototype, out var proto)) return null; return (satiation, proto); } /// /// Calculates the current value of the given by linearly extrapolating the change of the /// value based on , /// and /// private float CalculateCurrentValue(Satiation satiation, SatiationPrototype proto) { var dt = _timing.CurTime - satiation.LastAuthoritativeChangeTime; var value = satiation.LastAuthoritativeValue + (float)dt.TotalSeconds * satiation.ActualChangeRate; return proto.ClampSatiationWithinThresholds(value); } /// /// Calculates when 's value will reach either or /// , or null if neither will happen based on the current expected linear /// evolution of the satiation value. A null bound is treated as unreachable, so if both bounds are null, this /// this function returns null. /// /// private TimeSpan? EvolvesToBoundAt( Satiation satiation, SatiationPrototype proto, int? upperBound, int? lowerBound ) => satiation.ActualChangeRate switch { > 0 when upperBound is { } t => EvolvesToTargetAt(satiation, proto, t), < 0 when lowerBound is { } t => EvolvesToTargetAt(satiation, proto, t), // Change rate is zero or there's no threshold to decay/grow into: we'll never change without outside modification _ => null, }; /// /// Calculates when 's value will reach , or null if /// the current linear evolution will not reach that value. /// /// private TimeSpan? EvolvesToTargetAt(Satiation satiation, SatiationPrototype proto, int target) { var seconds = (target - CalculateCurrentValue(satiation, proto)) / satiation.ActualChangeRate; if (!seconds.IsValid() || seconds < 0f) return null; return _timing.CurTime + TimeSpan.FromSeconds(seconds); } /// /// The beating heart of this system, this function sets the given 's /// to . This involves /// updating obvious fields on the , but since changes to the value also affect the /// current threshold, we need to consider all of the effects that has as well. /// private void SetAuthoritativeValue( Entity entity, Satiation satiation, SatiationPrototype proto, float value ) { // Update the authoritative value itself. satiation.LastAuthoritativeChangeTime = _timing.CurTime; satiation.LastAuthoritativeValue = proto.ClampSatiationWithinThresholds(value); if (!TryGetValueByThreshold( entity, satiation.SatiationType, proto.ChangeModifiers, out var currentChangeMod, out var nextHigherThreshold, out var nextLowerThreshold )) { currentChangeMod = 1f; } satiation.ActualChangeRate = proto.BaseChangeRate * currentChangeMod; satiation.NextChangeRateModUpdateTime = EvolvesToBoundAt( satiation, proto, nextHigherThreshold, nextLowerThreshold ); var updateEvent = new SatiationUpdateEvent(satiation.SatiationType); RaiseLocalEvent(entity, ref updateEvent); DirtyField(entity.AsNullable(), SatiationComponent.SatiationFieldName); } private void UpdateAlerts( Entity entity, Satiation satiation, SatiationPrototype proto ) { TryGetValueByThreshold( entity, satiation.SatiationType, proto.Alerts, out var result, out var nextHigherThreshold, out var nextLowerThreshold ); if (result is { } alert) { _alerts.ShowAlert(entity.Owner, alert); } else { _alerts.ClearAlertCategory(entity.Owner, proto.AlertCategory); } satiation.NextAlertUpdateTime = EvolvesToBoundAt( satiation, proto, nextHigherThreshold, nextLowerThreshold ); DirtyField(entity.AsNullable(), SatiationComponent.SatiationFieldName); } } /// /// This event is raised on entities with when their satiation of /// is directly set or when the rate of change to that satiation is changed. /// /// This event may be raised even when no change has occurred. [ByRefEvent] public readonly record struct SatiationUpdateEvent(ProtoId Type);