CatStation/Content.Shared/Nutrition/EntitySystems/SatiationSystem.Proxy.cs
Centronias 8ad82dc665
Refactor hunger and thirst systems (#34166)
* ready

* oop forgot the HUD stuff

* woooo examinable generalized tooooo

* oop I forgot to rename the files

* bungled the merge, I guess

* this is called hubris

* Verin's comments

* PR comments

how did I use `ProtoId<> Hunger` in more than a few spots but then just say "thirst can stay a literal :^)"?

also changed a few spots to my beloved
```
functionInvocation(
  one
  param
  per
  line
)
```
```

* Satiation effect condition now with ADDED FUNCTIONALITY?? :o

* tfw you trust the markov chain recommendation because long generic type names are tough q-q

* TFW YOU TRUST THE MARKOV CHAIN BUT IT DOESN'T UNDERSTAND THE DIFFERING SEMANTICS OF MAX AND MIN AHHHHH

* tfw you think to yourself "i need to fix the effects", but then forget to and the code doesn't even compile.

* centralize satiation constants

* other small comments

* The enum is dead.
But at what cost?

* oop

* like 12 more ooperses

* green checks are mine by right; I have performed the rites; I have done my part; you will not deny me this succor

* oops I changed the semantics from `<=` to just `<` and didn't update the tests :)

* serialization has bested me :agony:

also PR comments

* never trust the auto merge :agony:

* literally just this

* force engine back to master because idk why it's different

* fix randomness and update master

* submodule fuckery

* fall before my might

* okay I ran the linter, ARE YOU HAPPY??

* cursed ovis, trouble me no more

* protoman stuff

* inb4 linter bonks me

* break non-critical stuff from the main satiation proto

* ![live crundle reaction](https://preview.redd.it/xh68xqcz518g1.png?width=677&format=png&auto=webp&s=79ba10fe49ec3f8306217732321a7c5429bdfb14)

inb4 this breaks something somewhere lmao

* my two greatest PRs COLLIDE

I wonder why robust says it's changed :hmmclueless

* sanity is at an all-time low

* serialization, I guess

* I don't believe you

* field deltas on class \w*satiation\w*components

* PR comments

* 我做泥團子 😔

* doc + kill one property that's trivially accessible after the threshold caching changes

* fixes

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: AJCM-git <ajcm_mail@disroot.org>
2026-08-08 22:44:24 +00:00

260 lines
11 KiB
C#

using System.Linq;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.Prototypes;
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 sealed partial class SatiationSystem
{
/// <summary>
/// Gets <paramref name="entity"/>'s current value of the satiation of <paramref name="type"/>. If this entity does
/// not have that satiation, returns null.
/// </summary>
public float? GetValueOrNull(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
)
{
if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto))
return null;
return CalculateCurrentValue(satiation, proto);
}
/// <summary>
/// Sets <paramref name="entity"/>'s current satiation of <paramref name="type"/> to a value corresponding to
/// <paramref name="satiationValue"/>. If this entity does not have that satiation, or the key does not correspond to
/// any threshold, has no effect.
/// </summary>
public void SetValue(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> 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);
}
/// <summary>
/// Sets <paramref name="entity"/>'s current satiation of <paramref name="type"/> to <paramref name="value"/>. If
/// this entity does not have that satiation, has no effect.
/// </summary>
// [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<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type,
float value
)
{
if (GetAndResolveSatiationOfType(entity, type) is not var (satiation, proto))
return;
SetAuthoritativeValue(entity, satiation, proto, value);
}
/// <summary>
/// Sets <paramref name="entity"/>'s current satiation of <paramref name="type"/> to its current value plus
/// <paramref name="amount"/>. If this entity does not have that satiation, has no effect.
/// </summary>
public void ModifyValue(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type,
float amount
)
{
if (GetValueOrNull(entity, type) is { } currentValue)
{
SetValue(entity, type, currentValue + amount);
}
}
/// <summary>
/// Returns whether or not the current value (plus optional <paramref name="hypotheticalValueDelta"/>) is between
/// the values described by <paramref name="above"/> and <paramref name="below"/>. If <paramref name="entity"/>
/// does not have a <see cref="Satiation"/> of the specified <paramref name="type"/>, 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 <paramref name="type"/>'s <see cref="SatiationPrototype.Thresholds"/>,
/// all values are considered out-of-range.
/// </summary>
public bool IsValueInRange(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> 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;
}
/// <summary>
/// This function returns a value from <see cref="valuesByThreshold"/> based on the current value of
/// <paramref name="entity"/>'s satiation of the given <paramref name="type"/>. The value in
/// <paramref name="valuesByThreshold"/> with the lowest key greater than the current satiation value is returned.
/// </summary>
/// <param name="entity">The entity whose satiation is considered</param>
/// <param name="type">The type of satiation to consider</param>
/// <param name="valuesByThreshold">The values, keyed by <see cref="SatiationValue"/> thresholds</param>
/// <param name="result">The value selected from <paramref name="valuesByThreshold"/></param>
/// <param name="nextLowerThreshold">
/// 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 <see cref="BaseSatiationEffectSystem{TComp,T}"/>'s prediction of value decay.
/// </param>
/// <typeparam name="T">The type of values in <paramref name="valuesByThreshold"/></typeparam>
/// <returns>
/// True if a value was selected. False if <paramref name="valuesByThreshold"/> 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.
/// </returns>
public bool TryGetValueByThreshold<T>(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type,
Dictionary<SatiationValue, T> valuesByThreshold,
out T? result,
out int? nextLowerThreshold
)
{
result = default;
nextLowerThreshold = null;
if (GetValueOrNull(entity, type) is not { } currentValue ||
GetAndResolveSatiationOfType(entity, type) is not var (_, proto))
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;
nextLowerThreshold = null;
return false;
}
if (currentValue > valuesByDescendingThreshold.Current.Item1)
{
// `currentSatiation` is higher than all thresholds, so we don't have a value, but we can return a next
// lower threshold.
result = default;
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;
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;
nextLowerThreshold = null;
return true;
}
/// <summary>
/// Looks up the <see cref="StatusIconPrototype"/> appropriate for the given entity's <see cref="Satiation"/> of the
/// specified <paramref name="type"/>. 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.
/// </summary>
/// <remarks>This should almost definitely never be used by anything other than the client's Overlay system</remarks>
public StatusIconPrototype? GetStatusIconOrNull(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> 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 _);
return ProtoMan.Resolve(iconProtoId, out var icon) ? icon : null;
}
#region Commands
/// <summary>
/// Returns the all of the <see cref="SatiationPrototype.Thresholds">key strings</see> of the given
/// <paramref name="type"/> for <paramref name="entity"/>, or empty if no such type exists.
/// </summary>
/// <remarks>
/// It is expected that <paramref name="type"/> is validated with before calling this. If it fails to resolve, an
/// error will be logged.
/// </remarks>
public IEnumerable<string> GetKeysForType(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
)
{
return GetAndResolveSatiationOfType(entity, type)?.Proto.Thresholds.Keys ?? Enumerable.Empty<string>();
}
/// <summary>
/// Returns the <see cref="SatiationPrototype.MaximumValue"/> of the given <paramref name="type"/> for
/// <paramref name="entity"/>, or null if no such type exists.
/// </summary>
/// <remarks>
/// It is expected that <paramref name="type"/> is validated with before calling this. If it fails to resolve, an
/// error will be logged.
/// </remarks>
public int? GetMaximumValue(
Entity<SatiationComponent> entity,
[ForbidLiteral] ProtoId<SatiationTypePrototype> type
) => GetAndResolveSatiationOfType(entity, type)?.Proto.MaximumValue;
#endregion
}