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);
}
}