Body code cleanup (#24946)

* Fix test

* Kill float accumulators

* Use entity proxy methods

* DataField auto name generation where possible

* Kill comp properties

* Clean up server comps

* Make events record structs

* Clean up shared body code

* Clean up server body code

* Rename organ events to be same names as in med refactor
This commit is contained in:
0x6273
2024-03-28 01:48:37 +01:00
committed by GitHub
parent 527c2c42ed
commit 37b8d78dac
32 changed files with 916 additions and 820 deletions

View File

@@ -34,7 +34,6 @@ namespace Content.Server.Body.Commands
switch (args.Length)
{
case 0:
{
if (player == null)
{
shell.WriteLine("Only a player can run this command without arguments.");
@@ -50,71 +49,68 @@ namespace Content.Server.Body.Commands
entity = player.AttachedEntity.Value;
hand = _entManager.SpawnEntity(DefaultHandPrototype, _entManager.GetComponent<TransformComponent>(entity).Coordinates);
break;
}
case 1:
{
if (NetEntity.TryParse(args[0], out var uidNet) && _entManager.TryGetEntity(uidNet, out var uid))
{
if (NetEntity.TryParse(args[0], out var uidNet) && _entManager.TryGetEntity(uidNet, out var uid))
{
if (!_entManager.EntityExists(uid))
{
shell.WriteLine($"No entity found with uid {uid}");
return;
}
entity = uid.Value;
hand = _entManager.SpawnEntity(DefaultHandPrototype, _entManager.GetComponent<TransformComponent>(entity).Coordinates);
}
else
{
if (player == null)
{
shell.WriteLine("You must specify an entity to add a hand to when using this command from the server terminal.");
return;
}
if (player.AttachedEntity == null)
{
shell.WriteLine("You don't have an entity to add a hand to.");
return;
}
entity = player.AttachedEntity.Value;
hand = _entManager.SpawnEntity(args[0], _entManager.GetComponent<TransformComponent>(entity).Coordinates);
}
break;
}
case 2:
{
if (!NetEntity.TryParse(args[0], out var netEnt) || !_entManager.TryGetEntity(netEnt, out var uid))
{
shell.WriteLine($"{args[0]} is not a valid entity uid.");
return;
}
if (!_entManager.EntityExists(uid))
{
shell.WriteLine($"No entity found with uid {uid}");
shell.WriteLine($"No entity exists with uid {uid}.");
return;
}
entity = uid.Value;
hand = _entManager.SpawnEntity(DefaultHandPrototype, _entManager.GetComponent<TransformComponent>(entity).Coordinates);
}
else
{
if (player == null)
if (!_protoManager.HasIndex<EntityPrototype>(args[1]))
{
shell.WriteLine("You must specify an entity to add a hand to when using this command from the server terminal.");
shell.WriteLine($"No hand entity exists with id {args[1]}.");
return;
}
if (player.AttachedEntity == null)
{
shell.WriteLine("You don't have an entity to add a hand to.");
return;
}
hand = _entManager.SpawnEntity(args[1], _entManager.GetComponent<TransformComponent>(entity).Coordinates);
entity = player.AttachedEntity.Value;
hand = _entManager.SpawnEntity(args[0], _entManager.GetComponent<TransformComponent>(entity).Coordinates);
break;
}
break;
}
case 2:
{
if (!NetEntity.TryParse(args[0], out var netEnt) || !_entManager.TryGetEntity(netEnt, out var uid))
{
shell.WriteLine($"{args[0]} is not a valid entity uid.");
return;
}
if (!_entManager.EntityExists(uid))
{
shell.WriteLine($"No entity exists with uid {uid}.");
return;
}
entity = uid.Value;
if (!_protoManager.HasIndex<EntityPrototype>(args[1]))
{
shell.WriteLine($"No hand entity exists with id {args[1]}.");
return;
}
hand = _entManager.SpawnEntity(args[1], _entManager.GetComponent<TransformComponent>(entity).Coordinates);
break;
}
default:
{
shell.WriteLine(Help);
return;
}
}
if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null)
@@ -139,7 +135,7 @@ namespace Content.Server.Body.Commands
var slotId = part.GetHashCode().ToString();
if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, BodyPartType.Hand,attachAt.Component, part))
if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, BodyPartType.Hand, attachAt.Component, part))
{
shell.WriteError($"Couldn't create a slot with id {slotId} on entity {_entManager.ToPrettyString(entity)}");
return;

View File

@@ -103,11 +103,11 @@ namespace Content.Server.Body.Commands
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (body.RootContainer.ContainedEntity != null)
{
bodySystem.AttachPartToRoot(bodyId,partUid.Value, body ,part);
bodySystem.AttachPartToRoot(bodyId, partUid.Value, body, part);
}
else
{
var (rootPartId,rootPart) = bodySystem.GetRootPartOrNull(bodyId, body)!.Value;
var (rootPartId, rootPart) = bodySystem.GetRootPartOrNull(bodyId, body)!.Value;
if (!bodySystem.TryCreatePartSlotAndAttach(rootPartId, slotId, partUid.Value, part.PartType, rootPart, part))
{
shell.WriteError($"Could not create slot {slotId} on entity {_entManager.ToPrettyString(bodyId)}");

View File

@@ -1,11 +1,7 @@
namespace Content.Server.Body.Components;
public sealed class BeingGibbedEvent : EntityEventArgs
{
public readonly HashSet<EntityUid> GibbedParts;
public BeingGibbedEvent(HashSet<EntityUid> gibbedParts)
{
GibbedParts = gibbedParts;
}
}
/// <summary>
/// Raised when a body gets gibbed, before it is deleted.
/// </summary>
[ByRefEvent]
public readonly record struct BeingGibbedEvent(HashSet<EntityUid> GibbedParts);

View File

@@ -1,11 +1,13 @@
using Content.Server.Body.Systems;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
{
@@ -16,7 +18,17 @@ namespace Content.Server.Body.Components
public static string DefaultBloodSolutionName = "bloodstream";
public static string DefaultBloodTemporarySolutionName = "bloodstreamTemporary";
public float AccumulatedFrametime = 0.0f;
/// <summary>
/// The next time that blood level will be updated and bloodloss damage dealt.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate;
/// <summary>
/// The interval at which this component updates.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
/// <summary>
/// How much is this entity currently bleeding?
@@ -32,7 +44,7 @@ namespace Content.Server.Body.Components
public float BleedAmount;
/// <summary>
/// How much should bleeding should be reduced every update interval?
/// How much should bleeding be reduced every update interval?
/// </summary>
[DataField]
public float BleedReductionAmount = 0.33f;
@@ -63,18 +75,12 @@ namespace Content.Server.Body.Components
[DataField(required: true)]
public DamageSpecifier BloodlossHealDamage = new();
/// <summary>
/// How frequently should this bloodstream update, in seconds?
/// </summary>
[DataField]
public float UpdateInterval = 3.0f;
// TODO shouldn't be hardcoded, should just use some organ simulation like bone marrow or smth.
/// <summary>
/// How much reagent of blood should be restored each update interval?
/// </summary>
[DataField]
public float BloodRefreshAmount = 1.0f;
public FixedPoint2 BloodRefreshAmount = 1.0f;
/// <summary>
/// How much blood needs to be in the temporary solution in order to create a puddle?
@@ -89,8 +95,8 @@ namespace Content.Server.Body.Components
/// <remarks>
/// For example, piercing damage is increased while poison damage is nullified entirely.
/// </remarks>
[DataField(customTypeSerializer:typeof(PrototypeIdSerializer<DamageModifierSetPrototype>))]
public string DamageBleedModifiers = "BloodlossHuman";
[DataField]
public ProtoId<DamageModifierSetPrototype> DamageBleedModifiers = "BloodlossHuman";
/// <summary>
/// The sound to be played when a weapon instantly deals blood loss damage.
@@ -126,7 +132,7 @@ namespace Content.Server.Body.Components
/// Slime-people might use slime as their blood or something like that.
/// </remarks>
[DataField]
public string BloodReagent = "Blood";
public ProtoId<ReagentPrototype> BloodReagent = "Blood";
/// <summary>Name/Key that <see cref="BloodSolution"/> is indexed by.</summary>
[DataField]
@@ -164,6 +170,6 @@ namespace Content.Server.Body.Components
/// Variable that stores the amount of status time added by having a low blood level.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float StatusTime;
public TimeSpan StatusTime;
}
}

View File

@@ -1,4 +1,3 @@
using System.Threading;
namespace Content.Server.Body.Components
{
/// <summary>
@@ -7,14 +6,17 @@ namespace Content.Server.Body.Components
[RegisterComponent]
public sealed partial class InternalsComponent : Component
{
[ViewVariables] public EntityUid? GasTankEntity { get; set; }
[ViewVariables] public EntityUid? BreathToolEntity { get; set; }
[ViewVariables]
public EntityUid? GasTankEntity;
[ViewVariables]
public EntityUid? BreathToolEntity;
/// <summary>
/// Toggle Internals delay (seconds) when the target is not you.
/// Toggle Internals delay when the target is not you.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("delay")]
public float Delay = 3;
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(3);
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Atmos;
using Content.Server.Atmos;
using Content.Server.Body.Systems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
@@ -11,7 +11,7 @@ public sealed partial class LungComponent : Component
{
[DataField]
[Access(typeof(LungSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture Air { get; set; } = new()
public GasMixture Air = new()
{
Volume = 6,
Temperature = Atmospherics.NormalBodyTemperature

View File

@@ -1,8 +1,8 @@
using Content.Server.Body.Systems;
using Content.Server.Body.Systems;
using Content.Shared.Body.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
{
@@ -12,20 +12,24 @@ namespace Content.Server.Body.Components
[RegisterComponent, Access(typeof(MetabolizerSystem))]
public sealed partial class MetabolizerComponent : Component
{
public float AccumulatedFrametime = 0.0f;
/// <summary>
/// The next time that reagents will be metabolized.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate;
/// <summary>
/// How often to metabolize reagents, in seconds.
/// How often to metabolize reagents.
/// </summary>
/// <returns></returns>
[DataField]
public float UpdateFrequency = 1.0f;
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
/// <summary>
/// From which solution will this metabolizer attempt to metabolize chemicals
/// </summary>
[DataField("solution")]
public string SolutionName { get; set; } = BloodstreamComponent.DefaultChemicalsSolutionName;
public string SolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
/// <summary>
/// Does this component use a solution on it's parent entity (the body) or itself
@@ -39,9 +43,9 @@ namespace Content.Server.Body.Components
/// <summary>
/// List of metabolizer types that this organ is. ex. Human, Slime, Felinid, w/e.
/// </summary>
[DataField(customTypeSerializer:typeof(PrototypeIdHashSetSerializer<MetabolizerTypePrototype>))]
[DataField]
[Access(typeof(MetabolizerSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public HashSet<string>? MetabolizerTypes = null;
public HashSet<ProtoId<MetabolizerTypePrototype>>? MetabolizerTypes = null;
/// <summary>
/// Should this metabolizer remove chemicals that have no metabolisms defined?
@@ -72,8 +76,8 @@ namespace Content.Server.Body.Components
[DataDefinition]
public sealed partial class MetabolismGroupEntry
{
[DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<MetabolismGroupPrototype>))]
public string Id = default!;
[DataField(required: true)]
public ProtoId<MetabolismGroupPrototype> Id = default!;
[DataField("rateModifier")]
public FixedPoint2 MetabolismRateModifier = 1.0;

View File

@@ -1,5 +1,6 @@
using Content.Server.Body.Systems;
using Content.Shared.Damage;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
{
@@ -7,36 +8,49 @@ namespace Content.Server.Body.Components
public sealed partial class RespiratorComponent : Component
{
/// <summary>
/// Saturation level. Reduced by CycleDelay each tick.
/// The next time that this body will inhale or exhale.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate;
/// <summary>
/// The interval between updates. Each update is either inhale or exhale,
/// so a full cycle takes twice as long.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
/// <summary>
/// Saturation level. Reduced by UpdateInterval each tick.
/// Can be thought of as 'how many seconds you have until you start suffocating' in this configuration.
/// </summary>
[DataField("saturation")]
[DataField]
public float Saturation = 5.0f;
/// <summary>
/// At what level of saturation will you begin to suffocate?
/// </summary>
[DataField("suffocationThreshold")]
[DataField]
public float SuffocationThreshold;
[DataField("maxSaturation")]
[DataField]
public float MaxSaturation = 5.0f;
[DataField("minSaturation")]
[DataField]
public float MinSaturation = -2.0f;
// TODO HYPEROXIA?
[DataField("damage", required: true)]
[DataField(required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
[DataField("damageRecovery", required: true)]
[DataField(required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier DamageRecovery = default!;
[DataField("gaspPopupCooldown")]
public TimeSpan GaspPopupCooldown { get; private set; } = TimeSpan.FromSeconds(8);
[DataField]
public TimeSpan GaspPopupCooldown = TimeSpan.FromSeconds(8);
[ViewVariables]
public TimeSpan LastGaspPopupTime;
@@ -55,11 +69,6 @@ namespace Content.Server.Body.Components
[ViewVariables]
public RespiratorStatus Status = RespiratorStatus.Inhaling;
[DataField("cycleDelay")]
public float CycleDelay = 2.0f;
public float AccumulatedFrametime;
}
}

View File

@@ -1,21 +1,26 @@
using Content.Server.Body.Systems;
using Content.Server.Body.Systems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Whitelist;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
{
[RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
public sealed partial class StomachComponent : Component
{
public float AccumulatedFrameTime;
/// <summary>
/// The next time that the stomach will try to digest its contents.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate;
/// <summary>
/// How fast should this component update, in seconds?
/// The interval at which this stomach digests its contents.
/// </summary>
[DataField]
public float UpdateInterval = 1.0f;
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
/// <summary>
/// The solution inside of this stomach this transfers reagents to the body.
@@ -30,11 +35,11 @@ namespace Content.Server.Body.Components
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
/// <summary>
/// Time in seconds between reagents being ingested and them being
/// Time between reagents being ingested and them being
/// transferred to <see cref="BloodstreamComponent"/>
/// </summary>
[DataField]
public float DigestionDelay = 20;
public TimeSpan DigestionDelay = TimeSpan.FromSeconds(20);
/// <summary>
/// A whitelist for what special-digestible-required foods this stomach is capable of eating.
@@ -54,15 +59,15 @@ namespace Content.Server.Body.Components
public sealed class ReagentDelta
{
public readonly ReagentQuantity ReagentQuantity;
public float Lifetime { get; private set; }
public TimeSpan Lifetime { get; private set; }
public ReagentDelta(ReagentQuantity reagentQuantity)
{
ReagentQuantity = reagentQuantity;
Lifetime = 0.0f;
Lifetime = TimeSpan.Zero;
}
public void Increment(float delta) => Lifetime += delta;
public void Increment(TimeSpan delta) => Lifetime += delta;
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Body.Systems;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components;
@@ -6,48 +7,58 @@ namespace Content.Server.Body.Components;
[Access(typeof(ThermalRegulatorSystem))]
public sealed partial class ThermalRegulatorComponent : Component
{
/// <summary>
/// The next time that the body will regulate its heat.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate;
/// <summary>
/// The interval at which thermal regulation is processed.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
/// <summary>
/// Heat generated due to metabolism. It's generated via metabolism
/// </summary>
[DataField("metabolismHeat")]
public float MetabolismHeat { get; private set; }
[DataField]
public float MetabolismHeat;
/// <summary>
/// Heat output via radiation.
/// </summary>
[DataField("radiatedHeat")]
public float RadiatedHeat { get; private set; }
[DataField]
public float RadiatedHeat;
/// <summary>
/// Maximum heat regulated via sweat
/// </summary>
[DataField("sweatHeatRegulation")]
public float SweatHeatRegulation { get; private set; }
[DataField]
public float SweatHeatRegulation;
/// <summary>
/// Maximum heat regulated via shivering
/// </summary>
[DataField("shiveringHeatRegulation")]
public float ShiveringHeatRegulation { get; private set; }
[DataField]
public float ShiveringHeatRegulation;
/// <summary>
/// Amount of heat regulation that represents thermal regulation processes not
/// explicitly coded.
/// </summary>
[DataField("implicitHeatRegulation")]
public float ImplicitHeatRegulation { get; private set; }
[DataField]
public float ImplicitHeatRegulation;
/// <summary>
/// Normal body temperature
/// </summary>
[DataField("normalBodyTemperature")]
public float NormalBodyTemperature { get; private set; }
[DataField]
public float NormalBodyTemperature;
/// <summary>
/// Deviation from normal temperature for body to start thermal regulation
/// </summary>
[DataField("thermalRegulationTemperatureThreshold")]
public float ThermalRegulationTemperatureThreshold { get; private set; }
public float AccumulatedFrametime;
[DataField]
public float ThermalRegulationTemperatureThreshold;
}

View File

@@ -13,7 +13,6 @@ using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Drunk;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Rejuvenate;
@@ -21,11 +20,13 @@ using Content.Shared.Speech.EntitySystems;
using Robust.Server.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
public sealed class BloodstreamSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AudioSystem _audio = default!;
@@ -44,6 +45,8 @@ public sealed class BloodstreamSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<BloodstreamComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<BloodstreamComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BloodstreamComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<BloodstreamComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<BloodstreamComponent, HealthBeingExaminedEvent>(OnHealthBeingExamined);
SubscribeLocalEvent<BloodstreamComponent, BeingGibbedEvent>(OnBeingGibbed);
@@ -53,6 +56,16 @@ public sealed class BloodstreamSystem : EntitySystem
SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
}
private void OnMapInit(Entity<BloodstreamComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<BloodstreamComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
private void OnReactionAttempt(Entity<BloodstreamComponent> entity, ref ReactionAttemptEvent args)
{
if (args.Cancelled)
@@ -83,7 +96,9 @@ public sealed class BloodstreamSystem : EntitySystem
if (args.Name != entity.Comp.BloodSolutionName
&& args.Name != entity.Comp.ChemicalSolutionName
&& args.Name != entity.Comp.BloodTemporarySolutionName)
{
return;
}
OnReactionAttempt(entity, ref args.Event);
}
@@ -95,12 +110,10 @@ public sealed class BloodstreamSystem : EntitySystem
var query = EntityQueryEnumerator<BloodstreamComponent>();
while (query.MoveNext(out var uid, out var bloodstream))
{
bloodstream.AccumulatedFrametime += frameTime;
if (bloodstream.AccumulatedFrametime < bloodstream.UpdateInterval)
if (_gameTiming.CurTime < bloodstream.NextUpdate)
continue;
bloodstream.AccumulatedFrametime -= bloodstream.UpdateInterval;
bloodstream.NextUpdate += bloodstream.UpdateInterval;
if (!_solutionContainerSystem.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
continue;
@@ -128,13 +141,17 @@ public sealed class BloodstreamSystem : EntitySystem
// bloodloss damage is based on the base value, and modified by how low your blood level is.
var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
_damageableSystem.TryChangeDamage(uid, amt, false, false);
_damageableSystem.TryChangeDamage(uid, amt,
ignoreResistances: false, interruptsDoAfters: false);
// Apply dizziness as a symptom of bloodloss.
// The effect is applied in a way that it will never be cleared without being healthy.
// Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
_drunkSystem.TryApplyDrunkenness(uid, bloodstream.UpdateInterval*2, false);
_stutteringSystem.DoStutter(uid, TimeSpan.FromSeconds(bloodstream.UpdateInterval*2), false);
_drunkSystem.TryApplyDrunkenness(
uid,
(float) bloodstream.UpdateInterval.TotalSeconds * 2,
applySlur: false);
_stutteringSystem.DoStutter(uid, bloodstream.UpdateInterval * 2, refresh: false);
// storing the drunk and stutter time so we can remove it independently from other effects additions
bloodstream.StatusTime += bloodstream.UpdateInterval * 2;
@@ -142,13 +159,16 @@ public sealed class BloodstreamSystem : EntitySystem
else if (!_mobStateSystem.IsDead(uid))
{
// If they're healthy, we'll try and heal some bloodloss instead.
_damageableSystem.TryChangeDamage(uid, bloodstream.BloodlossHealDamage * bloodPercentage, true, false);
_damageableSystem.TryChangeDamage(
uid,
bloodstream.BloodlossHealDamage * bloodPercentage,
ignoreResistances: true, interruptsDoAfters: false);
// Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level
_drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime);
_stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime);
_drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds);
_stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds);
// Reset the drunk and stutter time to zero
bloodstream.StatusTime = 0;
bloodstream.StatusTime = TimeSpan.Zero;
}
}
}
@@ -167,17 +187,15 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.AddReagent(entity.Comp.BloodReagent, entity.Comp.BloodMaxVolume - bloodSolution.Volume);
}
private void OnDamageChanged(EntityUid uid, BloodstreamComponent component, DamageChangedEvent args)
private void OnDamageChanged(Entity<BloodstreamComponent> ent, ref DamageChangedEvent args)
{
if (args.DamageDelta is null)
return;
// definitely don't make them bleed if they got healed
if (!args.DamageIncreased)
if (args.DamageDelta is null || !args.DamageIncreased)
{
return;
}
// TODO probably cache this or something. humans get hurt a lot
if (!_prototypeManager.TryIndex<DamageModifierSetPrototype>(component.DamageBleedModifiers, out var modifiers))
if (!_prototypeManager.TryIndex<DamageModifierSetPrototype>(ent.Comp.DamageBleedModifiers, out var modifiers))
return;
var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers);
@@ -186,10 +204,10 @@ public sealed class BloodstreamSystem : EntitySystem
return;
// Does the calculation of how much bleed rate should be added/removed, then applies it
var oldBleedAmount = component.BleedAmount;
var oldBleedAmount = ent.Comp.BleedAmount;
var total = bloodloss.GetTotal();
var totalFloat = total.Float();
TryModifyBleedAmount(uid, totalFloat, component);
TryModifyBleedAmount(ent, totalFloat, ent);
/// <summary>
/// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5
@@ -199,8 +217,8 @@ public sealed class BloodstreamSystem : EntitySystem
var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && _robustRandom.Prob(prob))
{
TryModifyBloodLevel(uid, (-total) / 5, component);
_audio.PlayPvs(component.InstantBloodSound, uid);
TryModifyBloodLevel(ent, (-total) / 5, ent);
_audio.PlayPvs(ent.Comp.InstantBloodSound, ent);
}
// Heat damage will cauterize, causing the bleed rate to be reduced.
@@ -210,53 +228,52 @@ public sealed class BloodstreamSystem : EntitySystem
// because it's burn damage that cauterized their wounds.
// We'll play a special sound and popup for feedback.
_audio.PlayPvs(component.BloodHealedSound, uid);
_popupSystem.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), uid,
uid, PopupType.Medium);
_audio.PlayPvs(ent.Comp.BloodHealedSound, ent);
_popupSystem.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), ent,
ent, PopupType.Medium);
}
}
/// <summary>
/// Shows text on health examine, based on bleed rate and blood level.
/// </summary>
private void OnHealthBeingExamined(EntityUid uid, BloodstreamComponent component, HealthBeingExaminedEvent args)
private void OnHealthBeingExamined(Entity<BloodstreamComponent> ent, ref HealthBeingExaminedEvent args)
{
// Shows profusely bleeding at half the max bleed rate.
if (component.BleedAmount > component.MaxBleedAmount / 2)
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
{
args.Message.PushNewline();
args.Message.AddMarkup(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", Identity.Entity(uid, EntityManager))));
args.Message.AddMarkup(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding, but less than profusely.
else if (component.BleedAmount > 0)
else if (ent.Comp.BleedAmount > 0)
{
args.Message.PushNewline();
args.Message.AddMarkup(Loc.GetString("bloodstream-component-bleeding", ("target", Identity.Entity(uid, EntityManager))));
args.Message.AddMarkup(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
}
// If the mob's blood level is below the damage threshhold, the pale message is added.
if (GetBloodLevelPercentage(uid, component) < component.BloodlossThreshold)
if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)
{
args.Message.PushNewline();
args.Message.AddMarkup(Loc.GetString("bloodstream-component-looks-pale", ("target", Identity.Entity(uid, EntityManager))));
args.Message.AddMarkup(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
}
}
private void OnBeingGibbed(EntityUid uid, BloodstreamComponent component, BeingGibbedEvent args)
private void OnBeingGibbed(Entity<BloodstreamComponent> ent, ref BeingGibbedEvent args)
{
SpillAllSolutions(uid, component);
SpillAllSolutions(ent, ent);
}
private void OnApplyMetabolicMultiplier(EntityUid uid, BloodstreamComponent component, ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(
Entity<BloodstreamComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
if (args.Apply)
{
component.UpdateInterval *= args.Multiplier;
ent.Comp.UpdateInterval *= args.Multiplier;
return;
}
component.UpdateInterval /= args.Multiplier;
// Reset the accumulator properly
if (component.AccumulatedFrametime >= component.UpdateInterval)
component.AccumulatedFrametime = component.UpdateInterval;
ent.Comp.UpdateInterval /= args.Multiplier;
}
private void OnRejuvenate(Entity<BloodstreamComponent> entity, ref RejuvenateEvent args)
@@ -275,21 +292,15 @@ public sealed class BloodstreamSystem : EntitySystem
/// </summary>
public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
if (!_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution))
return false;
return _solutionContainerSystem.TryAddSolution(component.ChemicalSolution.Value, solution);
return Resolve(uid, ref component, logMissing: false)
&& _solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution)
&& _solutionContainerSystem.TryAddSolution(component.ChemicalSolution.Value, solution);
}
public bool FlushChemicals(EntityUid uid, string excludedReagentID, FixedPoint2 quantity, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
if (!_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
if (!Resolve(uid, ref component, logMissing: false)
|| !_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
return false;
for (var i = chemSolution.Contents.Count - 1; i >= 0; i--)
@@ -306,11 +317,11 @@ public sealed class BloodstreamSystem : EntitySystem
public float GetBloodLevelPercentage(EntityUid uid, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component))
return 0.0f;
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
if (!Resolve(uid, ref component)
|| !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
{
return 0.0f;
}
return bloodSolution.FillFraction;
}
@@ -328,11 +339,11 @@ public sealed class BloodstreamSystem : EntitySystem
/// </summary>
public bool TryModifyBloodLevel(EntityUid uid, FixedPoint2 amount, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution))
if (!Resolve(uid, ref component, logMissing: false)
|| !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution))
{
return false;
}
if (amount >= 0)
return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, out _);
@@ -356,9 +367,9 @@ public sealed class BloodstreamSystem : EntitySystem
tempSolution.AddSolution(temp, _prototypeManager);
}
if (_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, false))
if (_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false))
{
_forensicsSystem.TransferDna(puddleUid, uid, false);
_forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false);
}
tempSolution.RemoveAllSolution();
@@ -374,7 +385,7 @@ public sealed class BloodstreamSystem : EntitySystem
/// </summary>
public bool TryModifyBleedAmount(EntityUid uid, float amount, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
if (!Resolve(uid, ref component, logMissing: false))
return false;
component.BleedAmount += amount;
@@ -424,7 +435,7 @@ public sealed class BloodstreamSystem : EntitySystem
if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
{
_forensicsSystem.TransferDna(puddleUid, uid, false);
_forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false);
}
}
@@ -433,11 +444,11 @@ public sealed class BloodstreamSystem : EntitySystem
/// </summary>
public void ChangeBloodReagent(EntityUid uid, string reagent, BloodstreamComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
if (reagent == component.BloodReagent)
if (!Resolve(uid, ref component, logMissing: false)
|| reagent == component.BloodReagent)
{
return;
}
if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
{

View File

@@ -1,23 +1,19 @@
using Content.Server.Body.Components;
using Content.Server.GameTicking;
using Content.Server.Humanoid;
using Content.Server.Kitchen.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
using Content.Shared.Humanoid;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Numerics;
using Content.Shared.Gibbing.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Body.Systems;
@@ -28,7 +24,6 @@ public sealed class BodySystem : SharedBodySystem
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -40,7 +35,7 @@ public sealed class BodySystem : SharedBodySystem
SubscribeLocalEvent<BodyComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnRelayMoveInput(EntityUid uid, BodyComponent component, ref MoveInputEvent args)
private void OnRelayMoveInput(Entity<BodyComponent> ent, ref MoveInputEvent args)
{
// If they haven't actually moved then ignore it.
if ((args.Component.HeldMoveButtons &
@@ -49,68 +44,67 @@ public sealed class BodySystem : SharedBodySystem
return;
}
if (_mobState.IsDead(uid) && _mindSystem.TryGetMind(uid, out var mindId, out var mind))
if (_mobState.IsDead(ent) && _mindSystem.TryGetMind(ent, out var mindId, out var mind))
{
mind.TimeOfDeath ??= _gameTiming.RealTime;
_ticker.OnGhostAttempt(mindId, true, mind: mind);
_ticker.OnGhostAttempt(mindId, canReturnGlobal: true, mind: mind);
}
}
private void OnApplyMetabolicMultiplier(EntityUid uid, BodyComponent component,
ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(
Entity<BodyComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
foreach (var organ in GetBodyOrgans(uid, component))
foreach (var organ in GetBodyOrgans(ent, ent))
{
RaiseLocalEvent(organ.Id, args);
RaiseLocalEvent(organ.Id, ref args);
}
}
protected override void AddPart(
EntityUid bodyUid,
EntityUid partUid,
string slotId,
BodyPartComponent component,
BodyComponent? bodyComp = null)
Entity<BodyComponent?> bodyEnt,
Entity<BodyPartComponent> partEnt,
string slotId)
{
// TODO: Predict this probably.
base.AddPart(bodyUid, partUid, slotId, component, bodyComp);
base.AddPart(bodyEnt, partEnt, slotId);
if (TryComp<HumanoidAppearanceComponent>(bodyUid, out var humanoid))
if (TryComp<HumanoidAppearanceComponent>(bodyEnt, out var humanoid))
{
var layer = component.ToHumanoidLayers();
var layer = partEnt.Comp.ToHumanoidLayers();
if (layer != null)
{
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
_humanoidSystem.SetLayersVisibility(bodyUid, layers, true, true, humanoid);
_humanoidSystem.SetLayersVisibility(
bodyEnt, layers, visible: true, permanent: true, humanoid);
}
}
}
protected override void RemovePart(
EntityUid bodyUid,
EntityUid partUid,
string slotId,
BodyPartComponent component,
BodyComponent? bodyComp = null)
Entity<BodyComponent?> bodyEnt,
Entity<BodyPartComponent> partEnt,
string slotId)
{
base.RemovePart(bodyUid, partUid, slotId, component, bodyComp);
base.RemovePart(bodyEnt, partEnt, slotId);
if (!TryComp<HumanoidAppearanceComponent>(bodyUid, out var humanoid))
if (!TryComp<HumanoidAppearanceComponent>(bodyEnt, out var humanoid))
return;
var layer = component.ToHumanoidLayers();
var layer = partEnt.Comp.ToHumanoidLayers();
if (layer == null)
if (layer is null)
return;
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
_humanoidSystem.SetLayersVisibility(bodyUid, layers, false, true, humanoid);
_humanoidSystem.SetLayersVisibility(
bodyEnt, layers, visible: false, permanent: true, humanoid);
}
public override HashSet<EntityUid> GibBody(
EntityUid bodyId,
bool gibOrgans = false,
BodyComponent? body = null ,
BodyComponent? body = null,
bool launchGibs = true,
Vector2? splatDirection = null,
float splatModifier = 1,
@@ -118,19 +112,23 @@ public sealed class BodySystem : SharedBodySystem
SoundSpecifier? gibSoundOverride = null
)
{
if (!Resolve(bodyId, ref body, false))
return new HashSet<EntityUid>();
if (TerminatingOrDeleted(bodyId) || EntityManager.IsQueuedForDeletion(bodyId))
if (!Resolve(bodyId, ref body, logMissing: false)
|| TerminatingOrDeleted(bodyId)
|| EntityManager.IsQueuedForDeletion(bodyId))
{
return new HashSet<EntityUid>();
}
var xform = Transform(bodyId);
if (xform.MapUid == null)
if (xform.MapUid is null)
return new HashSet<EntityUid>();
var gibs = base.GibBody(bodyId, gibOrgans, body, launchGibs: launchGibs,
splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone);
RaiseLocalEvent(bodyId, new BeingGibbedEvent(gibs));
var ev = new BeingGibbedEvent(gibs);
RaiseLocalEvent(bodyId, ref ev);
QueueDel(bodyId);
return gibs;

View File

@@ -16,8 +16,8 @@ namespace Content.Server.Body.Systems
{
base.Initialize();
SubscribeLocalEvent<BrainComponent, AddedToPartInBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, RemovedFromPartInBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
SubscribeLocalEvent<BrainComponent, OrganAddedToBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, OrganRemovedFromBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
SubscribeLocalEvent<BrainComponent, PointAttemptEvent>(OnPointAttempt);
}
@@ -39,7 +39,7 @@ namespace Content.Server.Body.Systems
_mindSystem.TransferTo(mindId, newEntity, mind: mind);
}
private void OnPointAttempt(EntityUid uid, BrainComponent component, PointAttemptEvent args)
private void OnPointAttempt(Entity<BrainComponent> ent, ref PointAttemptEvent args)
{
args.Cancel();
}

View File

@@ -18,12 +18,10 @@ namespace Content.Server.Body.Systems;
public sealed class InternalsSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly GasTankSystem _gasTank = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -40,28 +38,36 @@ public sealed class InternalsSystem : EntitySystem
SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
}
private void OnGetInteractionVerbs(EntityUid uid, InternalsComponent component, GetVerbsEvent<InteractionVerb> args)
private void OnGetInteractionVerbs(
Entity<InternalsComponent> ent,
ref GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
if (!args.CanAccess || !args.CanInteract || args.Hands is null)
return;
var user = args.User;
InteractionVerb verb = new()
{
Act = () =>
{
ToggleInternals(uid, args.User, false, component);
ToggleInternals(ent, user, force: false, ent);
},
Message = Loc.GetString("action-description-internals-toggle"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
Text = Loc.GetString("action-name-internals-toggle"),
};
args.Verbs.Add(verb);
}
public void ToggleInternals(EntityUid uid, EntityUid user, bool force, InternalsComponent? internals = null)
public void ToggleInternals(
EntityUid uid,
EntityUid user,
bool force,
InternalsComponent? internals = null)
{
if (!Resolve(uid, ref internals, false))
if (!Resolve(uid, ref internals, logMissing: false))
return;
// Toggle off if they're on
@@ -73,12 +79,12 @@ public sealed class InternalsSystem : EntitySystem
return;
}
StartToggleInternalsDoAfter(user, uid, internals);
StartToggleInternalsDoAfter(user, (uid, internals));
return;
}
// If they're not on then check if we have a mask to use
if (internals.BreathToolEntity == null)
if (internals.BreathToolEntity is null)
{
_popupSystem.PopupEntity(Loc.GetString("internals-no-breath-tool"), uid, user);
return;
@@ -86,7 +92,7 @@ public sealed class InternalsSystem : EntitySystem
var tank = FindBestGasTank(uid);
if (tank == null)
if (tank is null)
{
_popupSystem.PopupEntity(Loc.GetString("internals-no-tank"), uid, user);
return;
@@ -94,20 +100,20 @@ public sealed class InternalsSystem : EntitySystem
if (!force)
{
StartToggleInternalsDoAfter(user, uid, internals);
StartToggleInternalsDoAfter(user, (uid, internals));
return;
}
_gasTank.ConnectToInternals(tank.Value);
}
private void StartToggleInternalsDoAfter(EntityUid user, EntityUid target, InternalsComponent internals)
private void StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
{
// Is the target not you? If yes, use a do-after to give them time to respond.
var isUser = user == target;
var delay = !isUser ? internals.Delay : 0f;
var isUser = user == targetEnt.Owner;
var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), target, target: target)
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
{
BreakOnDamage = true,
BreakOnMove = true,
@@ -115,66 +121,64 @@ public sealed class InternalsSystem : EntitySystem
});
}
private void OnDoAfter(EntityUid uid, InternalsComponent component, InternalsDoAfterEvent args)
private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
{
if (args.Cancelled || args.Handled)
return;
ToggleInternals(uid, args.User, true, component);
ToggleInternals(ent, args.User, force: true, ent);
args.Handled = true;
}
private void OnInternalsStartup(EntityUid uid, InternalsComponent component, ComponentStartup args)
private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
{
_alerts.ShowAlert(uid, AlertType.Internals, GetSeverity(component));
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
}
private void OnInternalsShutdown(EntityUid uid, InternalsComponent component, ComponentShutdown args)
private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
{
_alerts.ClearAlert(uid, AlertType.Internals);
_alerts.ClearAlert(ent, AlertType.Internals);
}
private void OnInhaleLocation(EntityUid uid, InternalsComponent component, InhaleLocationEvent args)
private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
{
if (AreInternalsWorking(component))
if (AreInternalsWorking(ent))
{
var gasTank = Comp<GasTankComponent>(component.GasTankEntity!.Value);
args.Gas = _gasTank.RemoveAirVolume((component.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume);
var gasTank = Comp<GasTankComponent>(ent.Comp.GasTankEntity!.Value);
args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume);
// TODO: Should listen to gas tank updates instead I guess?
_alerts.ShowAlert(uid, AlertType.Internals, GetSeverity(component));
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
}
}
public void DisconnectBreathTool(Entity<InternalsComponent> ent)
{
var (owner, component) = ent;
var old = component.BreathToolEntity;
component.BreathToolEntity = null;
var old = ent.Comp.BreathToolEntity;
ent.Comp.BreathToolEntity = null;
if (TryComp(old, out BreathToolComponent? breathTool) )
if (TryComp(old, out BreathToolComponent? breathTool))
{
_atmos.DisconnectInternals(breathTool);
DisconnectTank(ent);
}
_alerts.ShowAlert(owner, AlertType.Internals, GetSeverity(component));
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
}
public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
{
var (owner, component) = ent;
if (TryComp(component.BreathToolEntity, out BreathToolComponent? tool))
if (TryComp(ent.Comp.BreathToolEntity, out BreathToolComponent? tool))
{
_atmos.DisconnectInternals(tool);
}
component.BreathToolEntity = toolEntity;
_alerts.ShowAlert(owner, AlertType.Internals, GetSeverity(component));
ent.Comp.BreathToolEntity = toolEntity;
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
}
public void DisconnectTank(InternalsComponent? component)
{
if (component == null)
if (component is null)
return;
if (TryComp(component.GasTankEntity, out GasTankComponent? tank))
@@ -186,46 +190,47 @@ public sealed class InternalsSystem : EntitySystem
public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
{
var component = ent.Comp;
if (component.BreathToolEntity == null)
if (ent.Comp.BreathToolEntity is null)
return false;
if (TryComp(component.GasTankEntity, out GasTankComponent? tank))
_gasTank.DisconnectFromInternals((component.GasTankEntity.Value, tank));
if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
_gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
component.GasTankEntity = tankEntity;
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(component));
ent.Comp.GasTankEntity = tankEntity;
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
return true;
}
public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
return AreInternalsWorking(component);
return Resolve(uid, ref component, logMissing: false)
&& AreInternalsWorking(component);
}
public bool AreInternalsWorking(InternalsComponent component)
{
return TryComp(component.BreathToolEntity, out BreathToolComponent? breathTool) &&
breathTool.IsFunctional &&
TryComp(component.GasTankEntity, out GasTankComponent? _);
return TryComp(component.BreathToolEntity, out BreathToolComponent? breathTool)
&& breathTool.IsFunctional
&& HasComp<GasTankComponent>(component.GasTankEntity);
}
private short GetSeverity(InternalsComponent component)
{
if (component.BreathToolEntity == null || !AreInternalsWorking(component))
if (component.BreathToolEntity is null || !AreInternalsWorking(component))
return 2;
// If pressure in the tank is below low pressure threshhold, flash warning on internals UI
if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank) && gasTank.IsLowPressure)
if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
&& gasTank.IsLowPressure)
{
return 0;
}
return 1;
}
public Entity<GasTankComponent>? FindBestGasTank(Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
public Entity<GasTankComponent>? FindBestGasTank(
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
{
// Prioritise
// 1. back equipped tanks
@@ -233,17 +238,17 @@ public sealed class InternalsSystem : EntitySystem
// 3. in-hand tanks
// 4. pocket/belt tanks
if (!Resolve(user.Owner, ref user.Comp1, ref user.Comp2, ref user.Comp3))
if (!Resolve(user, ref user.Comp1, ref user.Comp2, ref user.Comp3))
return null;
if (_inventory.TryGetSlotEntity(user.Owner, "back", out var backEntity, user.Comp2, user.Comp3) &&
if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
_gasTank.CanConnectToInternals(backGasTank))
{
return (backEntity.Value, backGasTank);
}
if (_inventory.TryGetSlotEntity(user.Owner, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
TryComp<GasTankComponent>(entity, out var gasTank) &&
_gasTank.CanConnectToInternals(gasTank))
{

View File

@@ -1,4 +1,4 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
@@ -26,21 +26,24 @@ public sealed class LungSystem : EntitySystem
SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
}
private void OnGotUnequipped(EntityUid uid, BreathToolComponent component, GotUnequippedEvent args)
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
{
_atmosphereSystem.DisconnectInternals(component);
_atmosphereSystem.DisconnectInternals(ent);
}
private void OnGotEquipped(EntityUid uid, BreathToolComponent component, GotEquippedEvent args)
private void OnGotEquipped(Entity<BreathToolComponent> ent, ref GotEquippedEvent args)
{
if ((args.SlotFlags & ent.Comp.AllowedSlots) == 0)
{
return;
}
if ((args.SlotFlags & component.AllowedSlots) == 0) return;
component.IsFunctional = true;
ent.Comp.IsFunctional = true;
if (TryComp(args.Equipee, out InternalsComponent? internals))
{
component.ConnectedInternalsEntity = args.Equipee;
_internals.ConnectBreathTool((args.Equipee, internals), uid);
ent.Comp.ConnectedInternalsEntity = args.Equipee;
_internals.ConnectBreathTool((args.Equipee, internals), ent);
}
}
@@ -81,7 +84,7 @@ public sealed class LungSystem : EntitySystem
if (moles <= 0)
continue;
var reagent = _atmosphereSystem.GasReagents[i];
if (reagent == null) continue;
if (reagent is null) continue;
var amount = moles * Atmospherics.BreathMolesToReagentMultiplier;
solution.AddReagent(reagent, amount);

View File

@@ -12,11 +12,13 @@ using Content.Shared.Mobs.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems
{
public sealed class MetabolizerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
@@ -34,9 +36,21 @@ namespace Content.Server.Body.Systems
_solutionQuery = GetEntityQuery<SolutionContainerManagerComponent>();
SubscribeLocalEvent<MetabolizerComponent, ComponentInit>(OnMetabolizerInit);
SubscribeLocalEvent<MetabolizerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MetabolizerComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<MetabolizerComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnMapInit(Entity<MetabolizerComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<MetabolizerComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
private void OnMetabolizerInit(Entity<MetabolizerComponent> entity, ref ComponentInit args)
{
if (!entity.Comp.SolutionOnBody)
@@ -49,19 +63,17 @@ namespace Content.Server.Body.Systems
}
}
private void OnApplyMetabolicMultiplier(EntityUid uid, MetabolizerComponent component,
ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(
Entity<MetabolizerComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
if (args.Apply)
{
component.UpdateFrequency *= args.Multiplier;
ent.Comp.UpdateInterval *= args.Multiplier;
return;
}
component.UpdateFrequency /= args.Multiplier;
// Reset the accumulator properly
if (component.AccumulatedFrametime >= component.UpdateFrequency)
component.AccumulatedFrametime = component.UpdateFrequency;
ent.Comp.UpdateInterval /= args.Multiplier;
}
public override void Update(float frameTime)
@@ -78,50 +90,52 @@ namespace Content.Server.Body.Systems
foreach (var (uid, metab) in metabolizers)
{
metab.AccumulatedFrametime += frameTime;
// Only update as frequently as it should
if (metab.AccumulatedFrametime < metab.UpdateFrequency)
if (_gameTiming.CurTime < metab.NextUpdate)
continue;
metab.AccumulatedFrametime -= metab.UpdateFrequency;
TryMetabolize(uid, metab);
metab.NextUpdate += metab.UpdateInterval;
TryMetabolize((uid, metab));
}
}
private void TryMetabolize(EntityUid uid, MetabolizerComponent meta, OrganComponent? organ = null)
private void TryMetabolize(Entity<MetabolizerComponent, OrganComponent?, SolutionContainerManagerComponent?> ent)
{
_organQuery.Resolve(uid, ref organ, false);
_organQuery.Resolve(ent, ref ent.Comp2, logMissing: false);
// First step is get the solution we actually care about
var solutionName = ent.Comp1.SolutionName;
Solution? solution = null;
Entity<SolutionComponent>? soln = default!;
EntityUid? solutionEntityUid = null;
SolutionContainerManagerComponent? manager = null;
if (meta.SolutionOnBody)
if (ent.Comp1.SolutionOnBody)
{
if (organ?.Body is { } body)
if (ent.Comp2?.Body is { } body)
{
if (!_solutionQuery.Resolve(body, ref manager, false))
if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false))
return;
_solutionContainerSystem.TryGetSolution((body, manager), meta.SolutionName, out soln, out solution);
_solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution);
solutionEntityUid = body;
}
}
else
{
if (!_solutionQuery.Resolve(uid, ref manager, false))
if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false))
return;
_solutionContainerSystem.TryGetSolution((uid, manager), meta.SolutionName, out soln, out solution);
solutionEntityUid = uid;
_solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution);
solutionEntityUid = ent;
}
if (solutionEntityUid == null || soln is null || solution is null || solution.Contents.Count == 0)
if (solutionEntityUid is null
|| soln is null
|| solution is null
|| solution.Contents.Count == 0)
{
return;
}
// randomize the reagent list so we don't have any weird quirks
// like alphabetical order or insertion order mattering for processing
@@ -135,9 +149,9 @@ namespace Content.Server.Body.Systems
continue;
var mostToRemove = FixedPoint2.Zero;
if (proto.Metabolisms == null)
if (proto.Metabolisms is null)
{
if (meta.RemoveEmpty)
if (ent.Comp1.RemoveEmpty)
{
solution.RemoveReagent(reagent, FixedPoint2.New(1));
}
@@ -146,15 +160,15 @@ namespace Content.Server.Body.Systems
}
// we're done here entirely if this is true
if (reagents >= meta.MaxReagentsProcessable)
if (reagents >= ent.Comp1.MaxReagentsProcessable)
return;
// loop over all our groups and see which ones apply
if (meta.MetabolismGroups == null)
if (ent.Comp1.MetabolismGroups is null)
continue;
foreach (var group in meta.MetabolismGroups)
foreach (var group in ent.Comp1.MetabolismGroups)
{
if (!proto.Metabolisms.TryGetValue(group.Id, out var entry))
continue;
@@ -169,14 +183,14 @@ namespace Content.Server.Body.Systems
// if it's possible for them to be dead, and they are,
// then we shouldn't process any effects, but should probably
// still remove reagents
if (EntityManager.TryGetComponent<MobStateComponent>(solutionEntityUid.Value, out var state))
if (TryComp<MobStateComponent>(solutionEntityUid.Value, out var state))
{
if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state))
continue;
}
var actualEntity = organ?.Body ?? solutionEntityUid.Value;
var args = new ReagentEffectArgs(actualEntity, uid, solution, proto, mostToRemove,
var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
var args = new ReagentEffectArgs(actualEntity, ent, solution, proto, mostToRemove,
EntityManager, null, scale);
// do all effects, if conditions apply
@@ -187,8 +201,14 @@ namespace Content.Server.Body.Systems
if (effect.ShouldLog)
{
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Metabolism effect {effect.GetType().Name:effect} of reagent {proto.LocalizedName:reagent} applied on entity {actualEntity:entity} at {Transform(actualEntity).Coordinates:coordinates}");
_adminLogger.Add(
LogType.ReagentEffect,
effect.LogImpact,
$"Metabolism effect {effect.GetType().Name:effect}"
+ $" of reagent {proto.LocalizedName:reagent}"
+ $" applied on entity {actualEntity:entity}"
+ $" at {Transform(actualEntity).Coordinates:coordinates}"
);
}
effect.Effect(args);
@@ -209,15 +229,25 @@ namespace Content.Server.Body.Systems
}
}
public sealed class ApplyMetabolicMultiplierEvent : EntityEventArgs
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
float Multiplier,
bool Apply)
{
// The entity whose metabolism is being modified
public EntityUid Uid;
/// <summary>
/// The entity whose metabolism is being modified.
/// </summary>
public readonly EntityUid Uid = Uid;
// What the metabolism's update rate will be multiplied by
public float Multiplier;
/// <summary>
/// What the metabolism's update rate will be multiplied by.
/// </summary>
public readonly float Multiplier = Multiplier;
// Apply this multiplier or ignore / reset it?
public bool Apply;
/// <summary>
/// If true, apply the multiplier. If false, revert it.
/// </summary>
public readonly bool Apply = Apply;
}
}

View File

@@ -35,9 +35,21 @@ public sealed class RespiratorSystem : EntitySystem
// We want to process lung reagents before we inhale new reagents.
UpdatesAfter.Add(typeof(MetabolizerSystem));
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<RespiratorComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -45,17 +57,15 @@ public sealed class RespiratorSystem : EntitySystem
var query = EntityQueryEnumerator<RespiratorComponent, BodyComponent>();
while (query.MoveNext(out var uid, out var respirator, out var body))
{
if (_gameTiming.CurTime < respirator.NextUpdate)
continue;
respirator.NextUpdate += respirator.UpdateInterval;
if (_mobState.IsDead(uid))
{
continue;
}
respirator.AccumulatedFrametime += frameTime;
if (respirator.AccumulatedFrametime < respirator.CycleDelay)
continue;
respirator.AccumulatedFrametime -= respirator.CycleDelay;
UpdateSaturation(uid, -respirator.CycleDelay, respirator);
UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator);
if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
{
@@ -80,30 +90,30 @@ public sealed class RespiratorSystem : EntitySystem
_popupSystem.PopupEntity(Loc.GetString("lung-behavior-gasp"), uid);
}
TakeSuffocationDamage(uid, respirator);
TakeSuffocationDamage((uid, respirator));
respirator.SuffocationCycles += 1;
continue;
}
StopSuffocation(uid, respirator);
StopSuffocation((uid, respirator));
respirator.SuffocationCycles = 0;
}
}
public void Inhale(EntityUid uid, BodyComponent? body = null)
{
if (!Resolve(uid, ref body, false))
if (!Resolve(uid, ref body, logMissing: false))
return;
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(uid, body);
// Inhale gas
var ev = new InhaleLocationEvent();
RaiseLocalEvent(uid, ev);
RaiseLocalEvent(uid, ref ev, broadcast: false);
ev.Gas ??= _atmosSys.GetContainingMixture(uid, false, true);
ev.Gas ??= _atmosSys.GetContainingMixture(uid, excite: true);
if (ev.Gas == null)
if (ev.Gas is null)
{
return;
}
@@ -122,7 +132,7 @@ public sealed class RespiratorSystem : EntitySystem
public void Exhale(EntityUid uid, BodyComponent? body = null)
{
if (!Resolve(uid, ref body, false))
if (!Resolve(uid, ref body, logMissing: false))
return;
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(uid, body);
@@ -130,11 +140,11 @@ public sealed class RespiratorSystem : EntitySystem
// exhale gas
var ev = new ExhaleLocationEvent();
RaiseLocalEvent(uid, ev, false);
RaiseLocalEvent(uid, ref ev, broadcast: false);
if (ev.Gas == null)
if (ev.Gas is null)
{
ev.Gas = _atmosSys.GetContainingMixture(uid, false, true);
ev.Gas = _atmosSys.GetContainingMixture(uid, excite: true);
// Walls and grids without atmos comp return null. I guess it makes sense to not be able to exhale in walls,
// but this also means you cannot exhale on some grids.
@@ -154,37 +164,37 @@ public sealed class RespiratorSystem : EntitySystem
_atmosSys.Merge(ev.Gas, outGas);
}
private void TakeSuffocationDamage(EntityUid uid, RespiratorComponent respirator)
private void TakeSuffocationDamage(Entity<RespiratorComponent> ent)
{
if (respirator.SuffocationCycles == 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(uid):entity} started suffocating");
if (ent.Comp.SuffocationCycles == 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
if (respirator.SuffocationCycles >= respirator.SuffocationCycleThreshold)
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(uid);
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
foreach (var (comp, _) in organs)
{
_alertsSystem.ShowAlert(uid, comp.Alert);
_alertsSystem.ShowAlert(ent, comp.Alert);
}
}
_damageableSys.TryChangeDamage(uid, respirator.Damage, false, false);
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
}
private void StopSuffocation(EntityUid uid, RespiratorComponent respirator)
private void StopSuffocation(Entity<RespiratorComponent> ent)
{
if (respirator.SuffocationCycles >= 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(uid):entity} stopped suffocating");
if (ent.Comp.SuffocationCycles >= 2)
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating");
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(uid);
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
foreach (var (comp, _) in organs)
{
_alertsSystem.ClearAlert(uid, comp.Alert);
_alertsSystem.ClearAlert(ent, comp.Alert);
}
_damageableSys.TryChangeDamage(uid, respirator.DamageRecovery);
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
}
public void UpdateSaturation(EntityUid uid, float amount,
@@ -198,35 +208,29 @@ public sealed class RespiratorSystem : EntitySystem
Math.Clamp(respirator.Saturation, respirator.MinSaturation, respirator.MaxSaturation);
}
private void OnApplyMetabolicMultiplier(EntityUid uid, RespiratorComponent component,
ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(
Entity<RespiratorComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
if (args.Apply)
{
component.CycleDelay *= args.Multiplier;
component.Saturation *= args.Multiplier;
component.MaxSaturation *= args.Multiplier;
component.MinSaturation *= args.Multiplier;
ent.Comp.UpdateInterval *= args.Multiplier;
ent.Comp.Saturation *= args.Multiplier;
ent.Comp.MaxSaturation *= args.Multiplier;
ent.Comp.MinSaturation *= args.Multiplier;
return;
}
// This way we don't have to worry about it breaking if the stasis bed component is destroyed
component.CycleDelay /= args.Multiplier;
component.Saturation /= args.Multiplier;
component.MaxSaturation /= args.Multiplier;
component.MinSaturation /= args.Multiplier;
// Reset the accumulator properly
if (component.AccumulatedFrametime >= component.CycleDelay)
component.AccumulatedFrametime = component.CycleDelay;
ent.Comp.UpdateInterval /= args.Multiplier;
ent.Comp.Saturation /= args.Multiplier;
ent.Comp.MaxSaturation /= args.Multiplier;
ent.Comp.MinSaturation /= args.Multiplier;
}
}
public sealed class InhaleLocationEvent : EntityEventArgs
{
public GasMixture? Gas;
}
[ByRefEvent]
public record struct InhaleLocationEvent(GasMixture? Gas);
public sealed class ExhaleLocationEvent : EntityEventArgs
{
public GasMixture? Gas;
}
[ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas);

View File

@@ -3,32 +3,44 @@ using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Body.Systems
{
public sealed class StomachSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
public const string DefaultSolutionName = "stomach";
public override void Initialize()
{
SubscribeLocalEvent<StomachComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StomachComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<StomachComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnMapInit(Entity<StomachComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<StomachComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<StomachComponent, OrganComponent, SolutionContainerManagerComponent>();
while (query.MoveNext(out var uid, out var stomach, out var organ, out var sol))
{
stomach.AccumulatedFrameTime += frameTime;
if (stomach.AccumulatedFrameTime < stomach.UpdateInterval)
if (_gameTiming.CurTime < stomach.NextUpdate)
continue;
stomach.AccumulatedFrameTime -= stomach.UpdateInterval;
stomach.NextUpdate += stomach.UpdateInterval;
// Get our solutions
if (!_solutionContainerSystem.ResolveSolution((uid, sol), DefaultSolutionName, ref stomach.Solution, out var stomachSolution))
@@ -70,49 +82,44 @@ namespace Content.Server.Body.Systems
}
}
private void OnApplyMetabolicMultiplier(EntityUid uid, StomachComponent component,
ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(
Entity<StomachComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
if (args.Apply)
{
component.UpdateInterval *= args.Multiplier;
ent.Comp.UpdateInterval *= args.Multiplier;
return;
}
// This way we don't have to worry about it breaking if the stasis bed component is destroyed
component.UpdateInterval /= args.Multiplier;
// Reset the accumulator properly
if (component.AccumulatedFrameTime >= component.UpdateInterval)
component.AccumulatedFrameTime = component.UpdateInterval;
ent.Comp.UpdateInterval /= args.Multiplier;
}
public bool CanTransferSolution(EntityUid uid, Solution solution,
public bool CanTransferSolution(
EntityUid uid,
Solution solution,
StomachComponent? stomach = null,
SolutionContainerManagerComponent? solutions = null)
{
if (!Resolve(uid, ref stomach, ref solutions, false))
return false;
if (!_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution))
return false;
// TODO: For now no partial transfers. Potentially change by design
if (!stomachSolution.CanAddSolution(solution))
return false;
return true;
return Resolve(uid, ref stomach, ref solutions, logMissing: false)
&& _solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution)
// TODO: For now no partial transfers. Potentially change by design
&& stomachSolution.CanAddSolution(solution);
}
public bool TryTransferSolution(EntityUid uid, Solution solution,
public bool TryTransferSolution(
EntityUid uid,
Solution solution,
StomachComponent? stomach = null,
SolutionContainerManagerComponent? solutions = null)
{
if (!Resolve(uid, ref stomach, ref solutions, false))
return false;
if (!_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution)
if (!Resolve(uid, ref stomach, ref solutions, logMissing: false)
|| !_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution)
|| !CanTransferSolution(uid, solution, stomach, solutions))
{
return false;
}
_solutionContainerSystem.TryAddSolution(stomach.Solution.Value, solution);
// Add each reagent to ReagentDeltas. Used to track how long each reagent has been in the stomach

View File

@@ -1,73 +1,95 @@
using Content.Server.Body.Components;
using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.ActionBlocker;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
public sealed class ThermalRegulatorSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly TemperatureSystem _tempSys = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSys = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ThermalRegulatorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ThermalRegulatorComponent, EntityUnpausedEvent>(OnUnpaused);
}
private void OnMapInit(Entity<ThermalRegulatorComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<ThermalRegulatorComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ThermalRegulatorComponent>();
while (query.MoveNext(out var uid, out var regulator))
{
regulator.AccumulatedFrametime += frameTime;
if (regulator.AccumulatedFrametime < 1)
if (_gameTiming.CurTime < regulator.NextUpdate)
continue;
regulator.AccumulatedFrametime -= 1;
ProcessThermalRegulation(uid, regulator);
regulator.NextUpdate += regulator.UpdateInterval;
ProcessThermalRegulation((uid, regulator));
}
}
/// <summary>
/// Processes thermal regulation for a mob
/// </summary>
private void ProcessThermalRegulation(EntityUid uid, ThermalRegulatorComponent comp)
private void ProcessThermalRegulation(Entity<ThermalRegulatorComponent, TemperatureComponent?> ent)
{
if (!EntityManager.TryGetComponent(uid, out TemperatureComponent? temperatureComponent)) return;
if (!Resolve(ent, ref ent.Comp2, logMissing: false))
return;
var totalMetabolismTempChange = comp.MetabolismHeat - comp.RadiatedHeat;
var totalMetabolismTempChange = ent.Comp1.MetabolismHeat - ent.Comp1.RadiatedHeat;
// implicit heat regulation
var tempDiff = Math.Abs(temperatureComponent.CurrentTemperature - comp.NormalBodyTemperature);
var heatCapacity = _tempSys.GetHeatCapacity(uid, temperatureComponent);
var tempDiff = Math.Abs(ent.Comp2.CurrentTemperature - ent.Comp1.NormalBodyTemperature);
var heatCapacity = _tempSys.GetHeatCapacity(ent, ent);
var targetHeat = tempDiff * heatCapacity;
if (temperatureComponent.CurrentTemperature > comp.NormalBodyTemperature)
if (ent.Comp2.CurrentTemperature > ent.Comp1.NormalBodyTemperature)
{
totalMetabolismTempChange -= Math.Min(targetHeat, comp.ImplicitHeatRegulation);
totalMetabolismTempChange -= Math.Min(targetHeat, ent.Comp1.ImplicitHeatRegulation);
}
else
{
totalMetabolismTempChange += Math.Min(targetHeat, comp.ImplicitHeatRegulation);
totalMetabolismTempChange += Math.Min(targetHeat, ent.Comp1.ImplicitHeatRegulation);
}
_tempSys.ChangeHeat(uid, totalMetabolismTempChange, true, temperatureComponent);
_tempSys.ChangeHeat(ent, totalMetabolismTempChange, ignoreHeatResistance: true, ent);
// recalc difference and target heat
tempDiff = Math.Abs(temperatureComponent.CurrentTemperature - comp.NormalBodyTemperature);
tempDiff = Math.Abs(ent.Comp2.CurrentTemperature - ent.Comp1.NormalBodyTemperature);
targetHeat = tempDiff * heatCapacity;
// if body temperature is not within comfortable, thermal regulation
// processes starts
if (tempDiff > comp.ThermalRegulationTemperatureThreshold)
if (tempDiff > ent.Comp1.ThermalRegulationTemperatureThreshold)
return;
if (temperatureComponent.CurrentTemperature > comp.NormalBodyTemperature)
if (ent.Comp2.CurrentTemperature > ent.Comp1.NormalBodyTemperature)
{
if (!_actionBlockerSys.CanSweat(uid)) return;
_tempSys.ChangeHeat(uid, -Math.Min(targetHeat, comp.SweatHeatRegulation), true,
temperatureComponent);
if (!_actionBlockerSys.CanSweat(ent))
return;
_tempSys.ChangeHeat(ent, -Math.Min(targetHeat, ent.Comp1.SweatHeatRegulation), ignoreHeatResistance: true, ent);
}
else
{
if (!_actionBlockerSys.CanShiver(uid)) return;
_tempSys.ChangeHeat(uid, Math.Min(targetHeat, comp.ShiveringHeatRegulation), true,
temperatureComponent);
if (!_actionBlockerSys.CanShiver(ent))
return;
_tempSys.ChangeHeat(ent, Math.Min(targetHeat, ent.Comp1.ShiveringHeatRegulation), ignoreHeatResistance: true, ent);
}
}
}