Skip to content

Commit

Permalink
CPR Remake (#487)
Browse files Browse the repository at this point in the history
# Description

This is a re implementation and complete rewrite of the original
Nyanotrasen CPR feature, this time as its own completely standalone
system. Unlike the original CPR, this system requires no modification of
base game code, and can be toggled on and off via CVars while the server
is running.

Fixes #473
Fixes #49

# Changelog
:cl:
- add: CPR has been added to the game. People with CPR training can now
perform CPR on anyone who is in either crit or dead states.
- add: CPR Training has been added to the game as a new positive trait.
All medical staff start with this trait for free.

---------

Signed-off-by: VMSolidus <[email protected]>
  • Loading branch information
VMSolidus authored Aug 6, 2024
1 parent c2d83e4 commit aed3bab
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 112 deletions.
107 changes: 2 additions & 105 deletions Content.Server/Atmos/Rotting/RottingSystem.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
using Content.Shared.Damage;
using Content.Shared.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Rejuvenate;
using Content.Shared.Damage;
using Robust.Server.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
Expand All @@ -22,83 +16,16 @@ public sealed class RottingSystem : SharedRottingSystem
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<PerishableComponent, MapInitEvent>(OnPerishableMapInit);
SubscribeLocalEvent<PerishableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PerishableComponent, ExaminedEvent>(OnPerishableExamined);

SubscribeLocalEvent<RottingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<RottingComponent, MobStateChangedEvent>(OnRottingMobStateChanged);
SubscribeLocalEvent<RottingComponent, BeingGibbedEvent>(OnGibbed);
SubscribeLocalEvent<RottingComponent, RejuvenateEvent>(OnRejuvenate);

SubscribeLocalEvent<TemperatureComponent, IsRottingEvent>(OnTempIsRotting);
}

private void OnPerishableMapInit(EntityUid uid, PerishableComponent component, MapInitEvent args)
{
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

private void OnMobStateChanged(EntityUid uid, PerishableComponent component, MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead && args.OldMobState != MobState.Dead)
return;

if (HasComp<RottingComponent>(uid))
return;

component.RotAccumulator = TimeSpan.Zero;
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

private void OnShutdown(EntityUid uid, RottingComponent component, ComponentShutdown args)
{
if (TryComp<PerishableComponent>(uid, out var perishable))
{
perishable.RotNextUpdate = TimeSpan.Zero;
}
}

private void OnRottingMobStateChanged(EntityUid uid, RottingComponent component, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
return;
RemCompDeferred(uid, component);
}

public bool IsRotProgressing(EntityUid uid, PerishableComponent? perishable)
{
// things don't perish by default.
if (!Resolve(uid, ref perishable, false))
return false;

// only dead things or inanimate objects can rot
if (TryComp<MobStateComponent>(uid, out var mobState) && !_mobState.IsDead(uid, mobState))
return false;

if (_container.TryGetOuterContainer(uid, Transform(uid), out var container) &&
HasComp<AntiRottingContainerComponent>(container.Owner))
{
return false;
}

var ev = new IsRottingEvent();
RaiseLocalEvent(uid, ref ev);

return !ev.Handled;
}

public bool IsRotten(EntityUid uid, RottingComponent? rotting = null)
{
return Resolve(uid, ref rotting, false);
}

private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEvent args)
{
if (!TryComp<PhysicsComponent>(uid, out var physics))
Expand All @@ -112,36 +39,6 @@ private void OnGibbed(EntityUid uid, RottingComponent component, BeingGibbedEven
tileMix?.AdjustMoles(Gas.Ammonia, molsToDump);
}

private void OnPerishableExamined(Entity<PerishableComponent> perishable, ref ExaminedEvent args)
{
int stage = PerishStage(perishable, MaxStages);
if (stage < 1 || stage > MaxStages)
{
// We dont push an examined string if it hasen't started "perishing" or it's already rotting
return;
}

var isMob = HasComp<MobStateComponent>(perishable);
var description = "perishable-" + stage + (!isMob ? "-nonmob" : string.Empty);
args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(perishable, EntityManager))));
}

/// <summary>
/// Return an integer from 0 to maxStage representing how close to rotting an entity is. Used to
/// generate examine messages for items that are starting to rot.
/// </summary>
public int PerishStage(Entity<PerishableComponent> perishable, int maxStages)
{
if (perishable.Comp.RotAfter.TotalSeconds == 0 || perishable.Comp.RotAccumulator.TotalSeconds == 0)
return 0;
return (int)(1 + maxStages * perishable.Comp.RotAccumulator.TotalSeconds / perishable.Comp.RotAfter.TotalSeconds);
}

private void OnRejuvenate(EntityUid uid, RottingComponent component, RejuvenateEvent args)
{
RemCompDeferred<RottingComponent>(uid);
}

private void OnTempIsRotting(EntityUid uid, TemperatureComponent component, ref IsRottingEvent args)
{
if (args.Handled)
Expand Down
141 changes: 134 additions & 7 deletions Content.Shared/Atmos/Rotting/SharedRottingSystem.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,85 @@
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Rejuvenate;
using Robust.Shared.Containers;
using Robust.Shared.Timing;

namespace Content.Shared.Atmos.Rotting;

public abstract class SharedRottingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;

public const int MaxStages = 3;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<PerishableComponent, MapInitEvent>(OnPerishableMapInit);
SubscribeLocalEvent<PerishableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PerishableComponent, ExaminedEvent>(OnPerishableExamined);

SubscribeLocalEvent<RottingComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<RottingComponent, MobStateChangedEvent>(OnRottingMobStateChanged);
SubscribeLocalEvent<RottingComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<RottingComponent, ExaminedEvent>(OnExamined);
}

/// <summary>
/// Return the rot stage, usually from 0 to 2 inclusive.
/// </summary>
public int RotStage(EntityUid uid, RottingComponent? comp = null, PerishableComponent? perishable = null)
private void OnPerishableMapInit(EntityUid uid, PerishableComponent component, MapInitEvent args)
{
if (!Resolve(uid, ref comp, ref perishable))
return 0;
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

return (int) (comp.TotalRotTime.TotalSeconds / perishable.RotAfter.TotalSeconds);
private void OnMobStateChanged(EntityUid uid, PerishableComponent component, MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead && args.OldMobState != MobState.Dead)
return;

if (HasComp<RottingComponent>(uid))
return;

component.RotAccumulator = TimeSpan.Zero;
component.RotNextUpdate = _timing.CurTime + component.PerishUpdateRate;
}

private void OnPerishableExamined(Entity<PerishableComponent> perishable, ref ExaminedEvent args)
{
int stage = PerishStage(perishable, MaxStages);
if (stage < 1 || stage > MaxStages)
{
// We dont push an examined string if it hasen't started "perishing" or it's already rotting
return;
}

var isMob = HasComp<MobStateComponent>(perishable);
var description = "perishable-" + stage + (!isMob ? "-nonmob" : string.Empty);
args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(perishable, EntityManager))));
}

private void OnShutdown(EntityUid uid, RottingComponent component, ComponentShutdown args)
{
if (TryComp<PerishableComponent>(uid, out var perishable))
{
perishable.RotNextUpdate = TimeSpan.Zero;
}
}

private void OnRottingMobStateChanged(EntityUid uid, RottingComponent component, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
return;
RemCompDeferred(uid, component);
}

private void OnRejuvenate(EntityUid uid, RottingComponent component, RejuvenateEvent args)
{
RemCompDeferred<RottingComponent>(uid);
}

private void OnExamined(EntityUid uid, RottingComponent component, ExaminedEvent args)
Expand All @@ -41,4 +97,75 @@ private void OnExamined(EntityUid uid, RottingComponent component, ExaminedEvent

args.PushMarkup(Loc.GetString(description, ("target", Identity.Entity(uid, EntityManager))));
}

/// <summary>
/// Return an integer from 0 to maxStage representing how close to rotting an entity is. Used to
/// generate examine messages for items that are starting to rot.
/// </summary>
public int PerishStage(Entity<PerishableComponent> perishable, int maxStages)
{
if (perishable.Comp.RotAfter.TotalSeconds == 0 || perishable.Comp.RotAccumulator.TotalSeconds == 0)
return 0;
return (int)(1 + maxStages * perishable.Comp.RotAccumulator.TotalSeconds / perishable.Comp.RotAfter.TotalSeconds);
}

public bool IsRotProgressing(EntityUid uid, PerishableComponent? perishable)
{
// things don't perish by default.
if (!Resolve(uid, ref perishable, false))
return false;

// only dead things or inanimate objects can rot
if (TryComp<MobStateComponent>(uid, out var mobState) && !_mobState.IsDead(uid, mobState))
return false;

if (_container.TryGetOuterContainer(uid, Transform(uid), out var container) &&
HasComp<AntiRottingContainerComponent>(container.Owner))
{
return false;
}

var ev = new IsRottingEvent();
RaiseLocalEvent(uid, ref ev);

return !ev.Handled;
}

public bool IsRotten(EntityUid uid, RottingComponent? rotting = null)
{
return Resolve(uid, ref rotting, false);
}

public void ReduceAccumulator(EntityUid uid, TimeSpan time)
{
if (!TryComp<PerishableComponent>(uid, out var perishable))
return;

if (!TryComp<RottingComponent>(uid, out var rotting))
{
perishable.RotAccumulator -= time;
return;
}
var total = (rotting.TotalRotTime + perishable.RotAccumulator) - time;

if (total < perishable.RotAfter)
{
RemCompDeferred(uid, rotting);
perishable.RotAccumulator = total;
}

else
rotting.TotalRotTime = total - perishable.RotAfter;
}

/// <summary>
/// Return the rot stage, usually from 0 to 2 inclusive.
/// </summary>
public int RotStage(EntityUid uid, RottingComponent? comp = null, PerishableComponent? perishable = null)
{
if (!Resolve(uid, ref comp, ref perishable))
return 0;

return (int) (comp.TotalRotTime.TotalSeconds / perishable.RotAfter.TotalSeconds);
}
}
49 changes: 49 additions & 0 deletions Content.Shared/CCVar/CCVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2291,6 +2291,55 @@ public static readonly CVarDef<float>
/// </summary>
public static readonly CVarDef<float> StationGoalsChance =
CVarDef.Create("game.station_goals_chance", 0.1f, CVar.SERVERONLY);


#region CPR System
/// <summary>
/// Controls whether the entire CPR system runs. When false, nobody can perform CPR. You should probably remove the trait too
/// if you are wishing to permanently disable the system on your server.
/// </summary>
public static readonly CVarDef<bool> EnableCPR =
CVarDef.Create("cpr.enable", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// Toggles whether or not CPR reduces rot timers(As an abstraction of delaying brain death, the IRL actual purpose of CPR)
/// </summary>
public static readonly CVarDef<bool> CPRReducesRot =
CVarDef.Create("cpr.reduces_rot", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// Toggles whether or not CPR heals airloss, included for completeness sake. I'm not going to stop you if your intention is to make CPR do nothing.
/// I guess it might be funny to troll your players with? I won't judge.
/// </summary>
public static readonly CVarDef<bool> CPRHealsAirloss =
CVarDef.Create("cpr.heals_airloss", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// The chance for a patient to be resuscitated when CPR is successfully performed.
/// Setting this above 0 isn't very realistic, but people who see CPR in movies and TV will expect CPR to work this way.
/// </summary>
public static readonly CVarDef<float> CPRResuscitationChance =
CVarDef.Create("cpr.resuscitation_chance", 0.05f, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// By default, CPR reduces rot timers by an amount of seconds equal to the time spent performing CPR. This is an optional multiplier that can increase or decrease the amount
/// of rot reduction. Set it to 2 for if you want 3 seconds of CPR to reduce 6 seconds of rot.
/// </summary>
/// <remarks>
/// If you're wondering why there isn't a CVar for setting the duration of the doafter, that's because it's not actually possible to have a timespan in cvar form
/// Curiously, it's also not possible for **shared** systems to set variable timespans. Which is where this system lives.
/// </remarks>
public static readonly CVarDef<float> CPRRotReductionMultiplier =
CVarDef.Create("cpr.rot_reduction_multiplier", 1f, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// By default, CPR heals airloss by 1 point for every second spent performing CPR. Just like above, this directly multiplies the healing amount.
/// Set it to 2 to get 6 points of airloss healing for every 3 seconds of CPR.
/// </summary>
public static readonly CVarDef<float> CPRAirlossReductionMultiplier =
CVarDef.Create("cpr.airloss_reduction_multiplier", 1f, CVar.REPLICATED | CVar.SERVER);

#endregion

#region Contests System

Expand Down
33 changes: 33 additions & 0 deletions Content.Shared/Medical/CPR/Components/CPRTrainingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Robust.Shared.GameStates;
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.Serialization;

namespace Content.Shared.Medical.CPR
{
[RegisterComponent, NetworkedComponent]
public sealed partial class CPRTrainingComponent : Component
{
[DataField]
public SoundSpecifier CPRSound = new SoundPathSpecifier("/Audio/Effects/CPR.ogg");

/// <summary>
/// How long the doafter for CPR takes
/// </summary>
[DataField]
public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3);

[DataField]
public int AirlossHeal = 6;

[DataField]
public float CrackRibsModifier = 1f;
public EntityUid? CPRPlayingStream;
}

[Serializable, NetSerializable]
public sealed partial class CPRDoAfterEvent : SimpleDoAfterEvent
{

}
}
Loading

0 comments on commit aed3bab

Please sign in to comment.