using System.Linq; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Prototypes; using Content.Shared.Random.Helpers; using Content.Shared.StatusIcon; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Nutrition.EntitySystems; // This part provides functions for use in other systems. public abstract partial class SatiationSystem { /// /// Gets 's current value of the satiation of . If this entity does /// not have that satiation, returns null. /// public float? GetValueOrNull( Entity entity, [ForbidLiteral] ProtoId type ) { if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto)) return null; return CalculateCurrentValue(satiation, proto); } /// /// Sets 's current satiation of to a value corresponding to /// . If this entity does not have that satiation, or the key does not correspond to /// any threshold, has no effect. /// public void SetValue( Entity entity, [ForbidLiteral] ProtoId type, [ForbidLiteral] SatiationValue satiationValue ) { if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto) || proto.GetValueOrNull(satiationValue) is not { } value) return; SetAuthoritativeValue(entity, satiation, proto, value); } /// /// Sets 's current satiation of to . If /// this entity does not have that satiation, has no effect. /// // [OverloadResolutionPriority(1)] // If you pass in an int, avoid instantiating a record to hold it. // Requires a newer language version :agony: public void SetValue( Entity entity, [ForbidLiteral] ProtoId type, float value ) { if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto)) return; SetAuthoritativeValue(entity, satiation, proto, value); } /// /// Sets 's current satiation of to its current value plus /// . If this entity does not have that satiation, has no effect. /// public void ModifyValue( Entity entity, [ForbidLiteral] ProtoId type, float amount ) { if (GetValueOrNull(entity, type) is { } currentValue) { SetValue(entity, type, currentValue + amount); } } /// /// Returns whether or not the current value (plus optional ) is between /// the values described by and . If /// does not have a of the specified , returns false. If either /// above- or below-key is null, any value is considered in-range compared to that key. /// If either key is specified but not present in 's , /// all values are considered out-of-range. /// public bool IsValueInRange( Entity entity, [ForbidLiteral] ProtoId type, [ForbidLiteral] SatiationValue? above = null, [ForbidLiteral] SatiationValue? below = null, float hypotheticalValueDelta = 0 ) { if (above is null && below is null) { DebugTools.Assert("Range is unbounded. Is this a programming error?"); return true; } if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto)) return false; // Resolve the bounds to integers we can actually compare against. int? valueAbove = null; if (above is { } a && (valueAbove = proto.GetValueOrNull(a)) is null) return false; // `above` is not null, but we failed to resolve its value. int? valueAtOrBelow = null; if (below is { } b && (valueAtOrBelow = proto.GetValueOrNull(b)) is null) return false; // `atOrBelow` is not null, but we failed to resolve its value. if (valueAbove > valueAtOrBelow) { DebugTools.Assert("Range is empty. Is this a programming error?"); return false; } var currentValue = CalculateCurrentValue(satiation, proto); if (hypotheticalValueDelta is not 0) currentValue = proto.ClampSatiationWithinThresholds(currentValue + hypotheticalValueDelta); var isAboveBottom = valueAbove is null || currentValue > valueAbove; var isAtOrBelowTop = valueAtOrBelow is null || currentValue < valueAtOrBelow; ; return isAboveBottom && isAtOrBelowTop; } /// /// Returns the value for 's current satiation value of the given /// , as informed by . /// /// The entity whose satiation is considered /// The type of satiation to consider /// The values, keyed by thresholds /// The value selected from /// /// The keying-threshold-value of the next threshold above the selected value. Most consumers will not have a use /// for this value, but it is used by 's prediction of value change. /// /// /// The keying-threshold-value of the next threshold below the selected value. Most consumers will not have a use /// for this value, but it is used by 's prediction of value change. /// /// The type of values in /// /// True if a value was selected. False if is empty, if the current value of /// the specified satiation is higher than all thresholds, or the given entity does not have a satiation of the /// given type. /// public bool TryGetValueByThreshold( Entity entity, [ForbidLiteral] ProtoId type, Dictionary valuesByThreshold, out T? result, out int? nextHigherThreshold, out int? nextLowerThreshold ) { if (GetValueOrNull(entity, type) is not { } currentValue || GetAndResolveSatiationOfType(entity, type) is not var (_, proto)) { result = default; nextHigherThreshold = null; nextLowerThreshold = null; return false; } using var valuesByDescendingThreshold = valuesByThreshold // Resolve keys to threshold integers, discarding any keys which cannot be resolved. .Select(it => proto.GetValueOrNull(it.Key) is { } value ? ((int, T)?)(value, it.Value) : null) .OfType<(int, T)>() // Order by descending threshold. .OrderByDescending(it => it.Item1) .GetEnumerator(); if (!valuesByDescendingThreshold.MoveNext()) { // `values` is empty, so there are no values to return. result = default; nextHigherThreshold = null; nextLowerThreshold = null; return false; } if (currentValue > valuesByDescendingThreshold.Current.Item1) { // `currentSatiation` is higher than all thresholds, so we have neither a value nor a higher threshold, but // we can return the next lower threshold. result = default; nextHigherThreshold = null; nextLowerThreshold = valuesByDescendingThreshold.Current.Item1; return false; } var nextHigher = valuesByDescendingThreshold.Current; while (valuesByDescendingThreshold.MoveNext()) { var nextLower = valuesByDescendingThreshold.Current; if (currentValue > nextLower.Item1) { // The current value is below `nextHigher` and above `nextLower`, so `nextHigher` is the correct threshold. result = nextHigher.Item2; nextHigherThreshold = nextHigher.Item1; nextLowerThreshold = nextLower.Item1; return true; } // Loop, setting `nextLower` to the next iteration's `nextHigher` nextHigher = nextLower; } // We've run out of thresholds below. result = nextHigher.Item2; nextHigherThreshold = nextHigher.Item1; nextLowerThreshold = null; return true; } /// /// Calculates when the given 's satiation of the given will /// evolve to either or . Returns null if the given /// entity does not have the given satiation or when the given satiation will not never evolve to one of the given /// values. A null bound is treated as unreachable. /// public TimeSpan? GetTimeToBound( Entity entity, ProtoId type, int? upperBound, int? lowerBound ) => GetAndResolveSatiationOfType(entity, type) is var (satiation, proto) ? EvolvesToBoundAt(satiation, proto, upperBound, lowerBound) : null; /// /// Looks up the appropriate for the given entity's of the /// specified . If the entity does not have the specified satiation type, or if the satiation /// does not have an icon for its current state, returns null. /// /// This should almost definitely never be used by anything other than the client's Overlay system public StatusIconPrototype? GetStatusIconOrNull( Entity entity, [ForbidLiteral] ProtoId type ) { if (entity.Comp.GetOrNull(type) is not { } satiation || !ProtoMan.Resolve(satiation.Prototype, out var proto)) return null; TryGetValueByThreshold(entity, type, proto.Icons, out var iconProtoId, out _, out _); return ProtoMan.Resolve(iconProtoId, out var icon) ? icon : null; } #region Commands /// /// Returns the all of the key strings of the given /// for , or empty if no such type exists. /// /// /// It is expected that is validated with before calling this. If it fails to resolve, an /// error will be logged. /// public IEnumerable GetKeysForType( Entity entity, [ForbidLiteral] ProtoId type ) => GetAndResolveSatiationOfType(entity, type)?.Proto.Thresholds.Keys ?? Enumerable.Empty(); /// /// Returns the of the given for /// , or null if no such type exists. /// /// /// It is expected that is validated with before calling this. If it fails to resolve, an /// error will be logged. /// public int? GetMaximumValue( Entity entity, [ForbidLiteral] ProtoId type ) => GetAndResolveSatiationOfType(entity, type)?.Proto.MaximumValue; #endregion /// /// Adds a new of a given to the entity. /// /// The entity to add the satiation to. /// The type of satiation to add. /// The satiation of that type to be added. public void AddSatiation(Entity entity, ProtoId type, Satiation satiation) { if (!Resolve(entity, ref entity.Comp)) return; if (!ProtoMan.Resolve(satiation.Prototype, out var proto)) return; satiation.SatiationType = type; entity.Comp.Satiations.TryAdd(type, satiation); var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(entity)); var value = rand.NextFloat(proto.StartingValueMinimum, proto.StartingValueMaximum); SetAuthoritativeValue((entity, entity.Comp), satiation, proto, value); DirtyField(entity, SatiationComponent.SatiationFieldName); } /// /// Removes an existing from the entity. /// /// The entity to remove the satiation from. /// The type of satiation to remove. public void RemoveSatiationType(Entity entity, ProtoId type) { if (!Resolve(entity, ref entity.Comp)) return; if (GetAndResolveSatiationOfType(entity.Comp, type) is { } satiation) { _alerts.ClearAlertCategory(entity.Owner, satiation.Proto.AlertCategory); entity.Comp.Satiations.Remove(type); } DirtyField(entity, SatiationComponent.SatiationFieldName); } }