CatStation/Content.Shared/Body/Systems/BloodstreamSystem.cs
Prole 502af6955d
Predict Forensics V2.0 (Ready for Review) (#40693)
* What sitting on code does to a branch + no mo owner v2

* Replacing ViewVariables with AutoNetworkedField + Missed One V2

* Removing Virtual Methods & Renaming ForensicsSystem to SharedForensicsSystem (Also made it abstract) V2

* IProduceWidgets Review Changes & some of my own V2

* Lil Cleanup V2

* Part 1 of Review Changes (Only thing left is the BUI stuff and the constructor) V2

* It now WORKS!!! (Still needs the BUI and constuctor changes, ill figure it out later)

* Test (Removal of ServerSide VomitSystem.cs)

* Ok...

* Moved System BloodstreamSystem.cs to SharedBloodstreamSystem.cs & deleted client BloodStreamSystem.cs to make SharedBloodstreamSystem.cs sealed. (No Unregistered errors)

* Prediction

* Fix?

* No mo log error?

* Revert "No mo log error?"

This reverts commit 50c15a3deab6af9dffd7baed45820bae5558e41f.

* Constructor going up

* No ForensicScannerBoundUserInterface.cs (It doesnt wanna scan anymore but it doesnt crash)

* For Conflicts

* Guh

* Yea

* Organization & no ErrorHell (maybe)

* Dirtying

* Dirtying take 2

* Revert "Dirtying take 2"

This reverts commit 5735012e21f43808db3ae010fce268eac24d0583.

* the dirties dont do much here

* For Conflict

* Readded

* Another Conflict Commit Fix

* Conflict Fix Thrice

* Empty Space

* Keep it simple

* Add it back in

* Move BloodstreamSystem stuff thats causing the error back to system. This'll prob fix the conflict

* Conflict fix?

* Trust the fix

* Looked at other prediction PR's, they had better code.

* To not mix up the system in client

* Removing Conflict

* For conflict

* For the conflic fix, will be removed later

* Scuffed Fix

* Removal & Adding the Decouple

* Took some HEAVY pointers from SirWarocks work. The Roadblock has been CLEARED, DOAFTERS WORK NOW!!

* Cleanup & Optimizing, mostly comps.

* More Optimization, most of this is just imported Warock Code, his QOL included. Did not want to remove SolutionDNA's tho, mostly due to a Log.Debug in OnPrint.

* Forgot that the Print Refreshes, more of Warocks work.

* Removing Loc.GetString

* All of BloodStream to Shared, hopefully no errors.

* Sketchy Dirty Removed

* Removed Timer.Spawn, added FrameUpdate cause anything else only updated when the window opened, & a bit of cleanup.

* No mo Mispredict

* GenerateDnaEvent Predicted

* Forgot the user

* I knew it was real, bloodstutter is REAL and now GONE.

* Cant dirty NewEntityBloodData due to potential stutter, so its safer to put it all back into server. again...

* Clearer

* Thank you leahcat, no more identity leak.

* Routine Maintenance

* sealing the class

* it checks for this now?!

* While im here, FileScope Namespaces & removing unused namespaces

* Update Attribute Type (SirWarock Import), it works and is much easier to read.

* Forever Fixing Conflicts

* Readonly Genocide & Partial Maxing

* LGTM, BloodstreamSystem moved to SharedBloodstreamSystem!

* Sohn

* If OG Github back push this commit

* One Billionth Commit Fix

* Removed client/server stuff

* Made implicit

* NewLine & comment on why AutoGenCompState is true

* Updatin the rest of the Popups to use Entity

* I think that's what they meant? It looks cleaner at least.

* Mass Attribute for LocalEvents that are safe to change.

* Putting it back as abstract

* maybe?

* Guhh, rolling back empty client/server of BloodStream

* Removed Client/Server BloodstreamSystems, changed access modifier from protected to private, & renamed SharedBloodstreamSystem to BloodstreamSystem

* From Protected to Private

* Removed comment & made it explicit on what changed to true

* Guh, hopefully thats all of it...

* Forgot

* Review

* Test Push

* Line 316 in SharedForensicsSystem was causing the errors, better to play it safe

* Review Changes

* Update Content.Shared/Forensics/Events.cs

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>

* The only thing SolutionDNAs does is search and clears itself. I see no reason to keep it

* The print.disabled didnt properly disable when cleared. So ill just use the old if check

* Figured out the rest of the SubscribeLocalEvents and networking BloodData cause its fully in Shared

---------

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
2026-08-08 22:48:36 +00:00

671 lines
29 KiB
C#

using Content.Shared.Alert;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.EntityEffects.Effects.Solution;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
using Content.Shared.Gibbing;
using Content.Shared.HealthExaminable;
using Content.Shared.Metabolism;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Rejuvenate;
using Content.Shared.StatusEffectNew;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Body.Systems;
public sealed partial class BloodstreamSystem : EntitySystem
{
public static readonly EntProtoId Bloodloss = "StatusEffectBloodloss";
[Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private SharedPuddleSystem _puddle = default!;
[Dependency] private StatusEffectsSystem _status = default!;
[Dependency] private AlertsSystem _alertsSystem = default!;
[Dependency] private MobStateSystem _mobStateSystem = default!;
[Dependency] private DamageableSystem _damageableSystem = default!;
[Dependency] private MetabolizerSystem _metabolizer = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<BloodstreamComponent>();
while (query.MoveNext(out var uid, out var bloodstream))
{
if (curTime < bloodstream.NextUpdate)
continue;
bloodstream.NextUpdate += bloodstream.AdjustedUpdateInterval;
DirtyField(uid, bloodstream, nameof(BloodstreamComponent.NextUpdate)); // needs to be dirtied on the client so it can be rerolled during prediction
if (!_solutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution))
continue;
// Blood level regulation. Must be alive.
if (!_mobStateSystem.IsDead(uid))
{
TryRegulateBloodLevel(uid, bloodstream.BloodRefreshAmount);
TickBleed((uid, bloodstream));
// deal bloodloss damage if their blood level is below a threshold.
var bloodPercentage = GetBloodLevel(uid);
if (bloodPercentage < bloodstream.BloodlossThreshold)
{
// 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, 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
_status.TrySetStatusEffectDuration(uid, Bloodloss);
}
else
{
// If they're healthy, we'll try and heal some bloodloss instead.
_damageableSystem.TryChangeDamage(uid, bloodstream.BloodlossHealDamage * bloodPercentage, ignoreResistances: true, interruptsDoAfters: false);
_status.TryRemoveStatusEffect(uid, Bloodloss);
}
}
else
{
TickBleed((uid, bloodstream));
}
}
}
[SubscribeLocalEvent]
private void OnMapInit(Entity<BloodstreamComponent> entity, ref MapInitEvent args)
{
entity.Comp.NextUpdate = _timing.CurTime + entity.Comp.AdjustedUpdateInterval;
DirtyField(entity, entity.Comp, nameof(BloodstreamComponent.NextUpdate));
_solutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodSolutionName, out var bloodSolution);
_solutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodTemporarySolutionName, out var tempSolution);
_solutionContainer.EnsureSolution(entity.Owner, entity.Comp.MetabolitesSolutionName, out var metabolitesSolution);
bloodSolution.Comp.Solution.MaxVolume = entity.Comp.BloodReferenceSolution.Volume * entity.Comp.MaxVolumeModifier;
metabolitesSolution.Comp.Solution.MaxVolume = bloodSolution.Comp.Solution.MaxVolume;
tempSolution.Comp.Solution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
entity.Comp.BloodReferenceSolution.SetReagentData(GetEntityBloodData((entity, entity.Comp)));
// Fill blood solution with BLOOD
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
var solution = entity.Comp.BloodReferenceSolution.Clone();
solution.ScaleTo(entity.Comp.BloodReferenceSolution.Volume - bloodSolution.Comp.Solution.Volume);
bloodSolution.Comp.Solution.AddSolution(solution, ProtoMan);
}
// prevent the infamous UdderSystem debug assert, see https://github.com/space-wizards/space-station-14/pull/35314
// TODO: find a better solution than copy pasting this into every shared system that caches solution entities
[SubscribeLocalEvent]
private void OnEntRemoved(Entity<BloodstreamComponent> entity, ref EntRemovedFromContainerMessage args)
{
// Make sure the removed entity was our contained solution and set it to null
if (args.Entity == entity.Comp.BloodSolution?.Owner)
entity.Comp.BloodSolution = null;
if (args.Entity == entity.Comp.TemporarySolution?.Owner)
entity.Comp.TemporarySolution = null;
}
[SubscribeLocalEvent]
private void OnDnaGenerated(Entity<BloodstreamComponent> entity, ref GenerateDnaEvent args)
{
if (_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
{
var data = NewEntityBloodData(entity);
entity.Comp.BloodReferenceSolution.SetReagentData(data);
foreach (var reagent in bloodSolution.Contents)
{
List<ReagentData> reagentData = reagent.Reagent.EnsureReagentData();
reagentData.RemoveAll(x => x is DnaData);
reagentData.AddRange(data);
}
}
else
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
}
[SubscribeLocalEvent]
private void OnReactionAttempt(Entity<BloodstreamComponent> ent, ref ReactionAttemptEvent args)
{
if (args.Cancelled)
return;
foreach (var effect in args.Reaction.Effects)
{
switch (effect)
{
// TODO: Rather than this, ReactionAttempt should allow systems to remove effects from the list before the reaction.
// TODO: I think there's a PR up on the repo for this and if there isn't I'll make one -Princess
case EntityEffects.Effects.EntitySpawning.SpawnEntity: // Prevent entities from spawning in the bloodstream
case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
args.Cancelled = true;
return;
}
}
// The area-reaction effect canceling is part of avoiding smoke-fork-bombs (create two smoke bombs, that when
// ingested by mobs create more smoke). This also used to act as a rapid chemical-purge, because all the
// reagents would get carried away by the smoke/foam. This does still work for the stomach (I guess people vomit
// up the smoke or spawned entities?).
// TODO apply organ damage instead of just blocking the reaction?
// Having cheese-clots form in your veins can't be good for you.
}
[SubscribeLocalEvent]
private void OnReactionAttempt(Entity<BloodstreamComponent> ent, ref SolutionRelayEvent<ReactionAttemptEvent> args)
{
if (args.Solution.Comp.Id != ent.Comp.BloodSolutionName
&& args.Solution.Comp.Id != ent.Comp.BloodTemporarySolutionName)
{
return;
}
OnReactionAttempt(ent, ref args.Event);
}
[SubscribeLocalEvent]
private void OnDamageChanged(Entity<BloodstreamComponent> ent, ref DamageChangedEvent args)
{
// The incoming state from the server raises a DamageChangedEvent as well.
// But the changes to the bloodstream have also been dirtied,
// so we prevent applying them twice.
if (_timing.ApplyingState)
return;
if (args.DamageDelta is null || !args.DamageIncreased)
{
return;
}
// TODO probably cache this or something. humans get hurt a lot
if (!ProtoMan.Resolve(ent.Comp.DamageBleedModifiers, out var modifiers))
return;
// some reagents may deal and heal different damage types in the same tick, which means DamageIncreased will be true
// but we only want to consider the dealt damage when causing bleeding
var damage = DamageSpecifier.GetPositive(args.DamageDelta);
var bloodloss = DamageSpecifier.ApplyModifierSet(damage, modifiers);
if (bloodloss.Empty)
return;
// Does the calculation of how much bleed rate should be added/removed, then applies it
var oldBleedAmount = ent.Comp.BleedAmount;
var total = bloodloss.GetTotal();
var totalFloat = total.Float();
TryModifyBleedAmount(ent.AsNullable(), totalFloat);
// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5
// The crit chance is currently the bleed rate modifier divided by 25.
// Higher damage weapons have a higher chance to crit!
// Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick
var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && SharedRandomExtensions.PredictedProb(_timing, prob, GetNetEntity(ent), GetNetEntity(args.Origin)))
{
TryBleedOut(ent.AsNullable(), total / 5);
_audio.PlayPredicted(ent.Comp.InstantBloodSound, ent, args.Origin);
}
// Heat damage will cauterize, causing the bleed rate to be reduced.
else if (totalFloat <= ent.Comp.BloodHealedSoundThreshold && oldBleedAmount > 0)
{
// Magically, this damage has healed some bleeding, likely
// because it's burn damage that cauterized their wounds.
// We'll play a special sound and popup for feedback.
// Only the burned entity can see the popup.
_popup.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), ent, ent, PopupType.Medium);
_audio.PlayPredicted(ent.Comp.BloodHealedSound, ent, args.Origin);
}
}
/// <summary>
/// Shows text on health examine, based on bleed rate and blood level.
/// </summary>
[SubscribeLocalEvent]
private void OnHealthBeingExamined(Entity<BloodstreamComponent> ent, ref HealthBeingExaminedEvent args)
{
// Shows massively bleeding at 0.75x the max bleed rate.
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.75f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-massive-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding above half the max rate, but less than massively.
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.5f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-strong-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding above 0.25x the max rate, but less than half the max.
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.25f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding below 0.25x the max cap
else if (ent.Comp.BleedAmount > 0)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-slight-bleeding", ("target", ent.Owner)));
}
// If the mob's blood level is below the damage threshhold, the pale message is added.
if (GetBloodLevel(ent.AsNullable()) < ent.Comp.BloodlossThreshold)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
}
}
[SubscribeLocalEvent]
private void OnBeingGibbed(Entity<BloodstreamComponent> ent, ref GibbedBeforeDeletionEvent args)
{
SpillAllSolutions(ent.AsNullable());
}
[SubscribeLocalEvent]
private void OnApplyMetabolicMultiplier(Entity<BloodstreamComponent> ent, ref ApplyMetabolicMultiplierEvent args)
{
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.UpdateIntervalMultiplier));
}
[SubscribeLocalEvent]
private void OnRejuvenate(Entity<BloodstreamComponent> ent, ref RejuvenateEvent args)
{
TryModifyBleedAmount(ent.AsNullable(), -ent.Comp.BleedAmount);
if (_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution))
{
_solutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value);
// TODO: Use Solutions API for this when it exists
TryRegulateBloodLevel(ent.AsNullable(), ent.Comp.BloodReferenceSolution.Volume);
}
}
[SubscribeLocalEvent]
private void OnMetabolismExclusion(Entity<BloodstreamComponent> ent, ref MetabolismExclusionEvent args)
{
// Adding all blood reagents for filtering blood in metabolizer
foreach (var (reagent, _) in ent.Comp.BloodReferenceSolution)
{
args.Reagents.Add(reagent);
}
}
/// <summary>
/// This returns the minimum amount of *usable* blood.
/// For multi reagent bloodstreams, if you have 100 of Reagent Y need 100, and 50 of Reagent X and need 100,
/// this will return 0.5f
/// </summary>
/// <returns>Returns the current blood level as a value from 0 to <see cref="BloodstreamComponent.MaxVolumeModifier"/></returns>
public float GetBloodLevel(Entity<BloodstreamComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp)
|| !_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution)
|| entity.Comp.BloodReferenceSolution.Volume == 0)
{
return 0.0f;
}
var totalBloodLevel = FixedPoint2.New(entity.Comp.MaxVolumeModifier); // Can't go above max volume factor...
foreach (var (reagentId, quantity) in entity.Comp.BloodReferenceSolution.Contents)
{
// Ideally we use a different calculation for blood pressure, this just defines how much *usable* blood you have!
totalBloodLevel = FixedPoint2.Min(totalBloodLevel, bloodSolution.GetTotalPrototypeQuantity(reagentId.Prototype) / quantity);
}
return (float)totalBloodLevel;
}
/// <summary>
/// Setter for the BloodlossThreshold datafield.
/// </summary>
public void SetBloodLossThreshold(Entity<BloodstreamComponent?> ent, float threshold)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.BloodlossThreshold = threshold;
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodlossThreshold));
}
/// <summary>
/// Attempt to transfer a provided solution to internal solution.
/// </summary>
public bool TryAddToBloodstream(Entity<BloodstreamComponent?> ent, Solution solution)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution))
return false;
if (_solutionContainer.TryAddSolution(ent.Comp.BloodSolution.Value, solution))
return true;
return false;
}
/// <summary>
/// Removes a certain amount of all reagents except of a single excluded one from the bloodstream and blood itself.
/// </summary>
/// <returns>
/// Solution of removed chemicals or null if none were removed.
/// </returns>
public Solution? FlushChemicals(Entity<BloodstreamComponent?> ent, FixedPoint2 quantity, ProtoId<ReagentPrototype>? excludedReagent = null )
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
return null;
var flushedSolution = new Solution();
for (var i = bloodSolution.Contents.Count - 1; i >= 0; i--)
{
var (reagentId, _) = bloodSolution.Contents[i];
if (ent.Comp.BloodReferenceSolution.ContainsPrototype(reagentId.Prototype) || reagentId.Prototype == excludedReagent)
continue;
var reagentFlushAmount = _solutionContainer.RemoveReagent(ent.Comp.BloodSolution.Value, reagentId, quantity);
flushedSolution.AddReagent(reagentId, reagentFlushAmount);
}
return flushedSolution.Volume == 0 ? null : flushedSolution;
}
/// <summary>
/// A simple helper that tries to move blood volume up or down by a specified amount.
/// Blood will not go over normal volume for this entity's bloodstream.
/// </summary>
public bool TryModifyBloodLevel(Entity<BloodstreamComponent?> ent, FixedPoint2 amount)
{
var reference = 1f;
if (amount < 0)
{
reference = 0f;
amount *= -1;
}
return TryRegulateBloodLevel(ent, amount, reference);
}
/// <summary>
/// Attempts to bring an entity's blood level to a modified equilibrium volume.
/// </summary>
/// <param name="ent">Entity whose bloodstream we're modifying.</param>
/// <param name="amount">The absolute maximum amount of blood we can add or remove.</param>
/// <param name="referenceFactor">The modifier for an entity's blood equilibrium, try to hit an entity's default blood volume multiplied by this value.</param>
/// <remarks>This CANNOT go above maximum blood volume!</remarks>
/// <returns>False if we were unable to regulate blood level. This may return true even if blood level doesn't change!</returns>
public bool TryRegulateBloodLevel(Entity<BloodstreamComponent?> ent, FixedPoint2 amount, float referenceFactor = 1f)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)
|| amount == 0)
return false;
// TODO: Either make this percentage based regeneration and pre-pass the percentage.
// TODO: Solution regulation API that doesn't result in very minor FixedPoint2 errors (Currently gingerbreadman only regenerates 0.99u instead of 1.00u)
referenceFactor = Math.Clamp(referenceFactor, 0f, ent.Comp.MaxVolumeModifier);
var ratio = (float)amount / (float)ent.Comp.BloodReferenceSolution.Volume;
foreach (var (referenceReagent, referenceQuantity) in ent.Comp.BloodReferenceSolution)
{
var error = referenceQuantity * referenceFactor - bloodSolution.GetTotalPrototypeQuantity(referenceReagent.Prototype);
var adjustedAmount = referenceQuantity * ratio;
if (error > 0)
{
error = FixedPoint2.Min(error, adjustedAmount);
bloodSolution.AddReagent(referenceReagent, error);
}
else if (error < 0)
{
// invert the error since we're removing reagents...
error = FixedPoint2.Min( -error, adjustedAmount);
bloodSolution.RemoveReagent(referenceReagent, error);
}
}
return true;
}
public void TickBleed(Entity<BloodstreamComponent> entity)
{
// Removes blood from the bloodstream based on bleed amount (bleed rate)
// as well as stop their bleeding to a certain extent.
if (entity.Comp.BleedAmount <= 0)
return;
var ev = new BleedModifierEvent(entity.Comp.BleedAmount, entity.Comp.BleedReductionAmount);
RaiseLocalEvent(entity, ref ev);
// Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
TryBleedOut(entity.AsNullable(), ev.BleedAmount);
// Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
TryModifyBleedAmount(entity.AsNullable(), -ev.BleedReductionAmount);
}
/// <summary>
/// Removes blood by spilling out the bloodstream.
/// </summary>
public bool TryBleedOut(Entity<BloodstreamComponent?> ent, FixedPoint2 amount)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)
|| amount <= 0)
{
return false;
}
var leakedBlood = _solutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, amount);
if (!_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
return true;
tempSolution.AddSolution(leakedBlood, ProtoMan);
if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold)
{
_puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false);
tempSolution.RemoveAllSolution();
}
_solutionContainer.UpdateChemicals(ent.Comp.TemporarySolution.Value);
return true;
}
/// <summary>
/// Tries to make an entity bleed more or less.
/// </summary>
public bool TryModifyBleedAmount(Entity<BloodstreamComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false))
return false;
ent.Comp.BleedAmount += amount;
ent.Comp.BleedAmount = Math.Clamp(ent.Comp.BleedAmount, 0, ent.Comp.MaxBleedAmount);
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BleedAmount));
if (ent.Comp.BleedAmount == 0)
_alertsSystem.ClearAlert(ent.Owner, ent.Comp.BleedingAlert);
else
{
var severity = (short)Math.Clamp(Math.Round(ent.Comp.BleedAmount, MidpointRounding.ToZero), 0, 10);
_alertsSystem.ShowAlert(ent.Owner, ent.Comp.BleedingAlert, severity);
}
return true;
}
/// <summary>
/// Spill all bloodstream solutions into a puddle.
/// BLOOD FOR THE BLOOD GOD
/// </summary>
public void SpillAllSolutions(Entity<BloodstreamComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
var tempSol = new Solution();
if (_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
{
tempSol.MaxVolume += bloodSolution.MaxVolume;
tempSol.AddSolution(bloodSolution, ProtoMan);
_solutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value);
}
if (_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
{
tempSol.MaxVolume += tempSolution.MaxVolume;
tempSol.AddSolution(tempSolution, ProtoMan);
_solutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value);
}
_puddle.TrySpillAt(ent, tempSol, out _);
}
/// <summary>
/// Change what someone's blood is made of, on the fly.
/// </summary>
[Obsolete("ChangeBloodReagent is obsolete, please use ChangeBloodReagents.")]
public void ChangeBloodReagent(Entity<BloodstreamComponent?> ent, ProtoId<ReagentPrototype> reagent)
{
ChangeBloodReagents(ent, new([new(reagent, 1)]));
}
/// <summary>
/// Change what someone's blood is made of, on the fly.
/// </summary>
public void ChangeBloodReagents(Entity<BloodstreamComponent?> ent, Solution reagents)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false))
{
return;
}
if (!_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
{
ent.Comp.BloodReferenceSolution = reagents.Clone();
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution));
return;
}
var currentVolume = FixedPoint2.Zero;
foreach (var reagent in ent.Comp.BloodReferenceSolution)
{
currentVolume += bloodSolution.RemoveReagent(reagent.Reagent, quantity: bloodSolution.Volume, ignoreReagentData: true);
}
ent.Comp.BloodReferenceSolution = reagents.Clone();
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution));
if (currentVolume == FixedPoint2.Zero)
return;
var solution = ent.Comp.BloodReferenceSolution.Clone();
solution.ScaleSolution(currentVolume / solution.Volume);
_solutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution);
}
/// <summary>
/// Get the reagent data for blood that a specific entity should have.
/// </summary>
public List<ReagentData> GetEntityBloodData(Entity<BloodstreamComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return NewEntityBloodData(entity);
return entity.Comp.BloodData ?? NewEntityBloodData(entity);
}
/// <summary>
/// Gets new blood data for this entity and caches it in <see cref="BloodstreamComponent.BloodData"/>
/// </summary>
private List<ReagentData> NewEntityBloodData(EntityUid uid)
{
var bloodData = new List<ReagentData>();
var dnaData = new DnaData();
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
dnaData.DNA = donorComp.DNA;
else
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
bloodData.Add(dnaData);
return bloodData;
}
/// <summary>
/// Copies the values in <see cref="BloodstreamComponent"/> of one entity to another.
/// If the component isn't on the target entity, it is added.
/// </summary>
/// <param name="source">The entity who's component to use.</param>
/// <param name="target">The target to clone the component onto.</param>
public void CopyComponent(Entity<BloodstreamComponent?> source, EntityUid target)
{
if (!Resolve(source, ref source.Comp))
return;
// We don't create the component before adding it here.
// This is because it causes the initialization to get a bit fucky together with ChangeBloodReagent.
// The reagents won't actually change unless we do it this way.
// The DNA and stuff is updated by themselves anyway.
// BleedAmount is excluded on purpose. There is no point in cloning how much someone is bleeding at the moment.
var cloneComp = EnsureComp<BloodstreamComponent>(target);
cloneComp.UpdateInterval = source.Comp.UpdateInterval;
cloneComp.UpdateIntervalMultiplier = source.Comp.UpdateIntervalMultiplier;
cloneComp.BleedReductionAmount = source.Comp.BleedReductionAmount;
cloneComp.MaxBleedAmount = source.Comp.MaxBleedAmount;
cloneComp.BloodlossThreshold = source.Comp.BloodlossThreshold;
cloneComp.BloodlossDamage = new DamageSpecifier(source.Comp.BloodlossDamage);
cloneComp.BloodlossHealDamage = new DamageSpecifier(source.Comp.BloodlossHealDamage);
cloneComp.BloodRefreshAmount = source.Comp.BloodRefreshAmount;
cloneComp.BleedPuddleThreshold = source.Comp.BleedPuddleThreshold;
cloneComp.DamageBleedModifiers = source.Comp.DamageBleedModifiers;
cloneComp.MaxVolumeModifier = source.Comp.MaxVolumeModifier;
cloneComp.InstantBloodSound = source.Comp.InstantBloodSound;
cloneComp.BloodHealedSound = source.Comp.BloodHealedSound;
cloneComp.BloodHealedSoundThreshold = source.Comp.BloodHealedSoundThreshold;
Dirty(target, cloneComp);
// ChangeBloodReagents dirties the values it changes by itself.
ChangeBloodReagents((target, cloneComp), source.Comp.BloodReferenceSolution);
_metabolizer.UpdateMetabolicMultiplier(target);
}
}