diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index 7ed60563b9..e6a8347c4a 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -131,7 +131,8 @@ namespace Content.Client
"UseDelay",
"Pourable",
"Paper",
- "Write"
+ "Write",
+ "Bloodstream"
};
foreach (var ignoreName in registerIgnore)
diff --git a/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs
new file mode 100644
index 0000000000..ab1bb415d1
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs
@@ -0,0 +1,80 @@
+using Content.Client.UserInterface;
+using Content.Client.Utility;
+using Robust.Shared.Timing;
+using Content.Shared.GameObjects.Components.Chemistry;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Localization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.GameObjects.Components.Chemistry
+{
+ ///
+ /// Client behavior for injectors & syringes. Used for item status on injectors
+ ///
+ [RegisterComponent]
+ public class InjectorComponent : SharedInjectorComponent, IItemStatus
+ {
+ [ViewVariables] private int CurrentVolume { get; set; }
+ [ViewVariables] private int TotalVolume { get; set; }
+ [ViewVariables] private InjectorToggleMode CurrentMode { get; set; }
+ [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
+
+ //Add/remove item status code
+ Control IItemStatus.MakeControl() => new StatusControl(this);
+ void IItemStatus.DestroyControl(Control control) { }
+
+ //Handle net updates
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ var cast = (InjectorComponentState)curState;
+ if (cast != null)
+ {
+ CurrentVolume = cast.CurrentVolume;
+ TotalVolume = cast.TotalVolume;
+ CurrentMode = cast.CurrentMode;
+ _uiUpdateNeeded = true;
+ }
+ }
+
+ ///
+ /// Item status control for injectors
+ ///
+ private sealed class StatusControl : Control
+ {
+ private readonly InjectorComponent _parent;
+ private readonly RichTextLabel _label;
+
+ public StatusControl(InjectorComponent parent)
+ {
+ _parent = parent;
+ _label = new RichTextLabel { StyleClasses = { NanoStyle.StyleClassItemStatus } };
+ AddChild(_label);
+
+ parent._uiUpdateNeeded = true;
+ }
+
+ protected override void Update(FrameEventArgs args)
+ {
+ base.Update(args);
+ if (!_parent._uiUpdateNeeded)
+ {
+ return;
+ }
+
+ _parent._uiUpdateNeeded = false;
+
+ //Update current volume and injector state
+ var modeStringLocalized = _parent.CurrentMode switch
+ {
+ InjectorToggleMode.Draw => Loc.GetString("Draw"),
+ InjectorToggleMode.Inject => Loc.GetString("Inject"),
+ _ => Loc.GetString("Invalid")
+ };
+ _label.SetMarkup(Loc.GetString("Volume: [color=white]{0}/{1}[/color] | [color=white]{2}[/color]",
+ _parent.CurrentVolume, _parent.TotalVolume, modeStringLocalized));
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs
new file mode 100644
index 0000000000..f4cf53ad14
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs
@@ -0,0 +1,239 @@
+using System;
+using Content.Server.GameObjects.Components.Metabolism;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Interfaces;
+using Content.Shared.Chemistry;
+using Content.Shared.GameObjects.Components.Chemistry;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Chemistry
+{
+ ///
+ /// Server behavior for reagent injectors and syringes. Can optionally support both
+ /// injection and drawing or just injection. Can inject/draw reagents from solution
+ /// containers, and can directly inject into a mobs bloodstream.
+ ///
+ [RegisterComponent]
+ public class InjectorComponent : SharedInjectorComponent, IAfterAttack, IUse
+ {
+#pragma warning disable 649
+ [Dependency] private readonly IServerNotifyManager _notifyManager;
+#pragma warning restore 649
+
+ ///
+ /// Whether or not the injector is able to draw from containers or if it's a single use
+ /// device that can only inject.
+ ///
+ [ViewVariables]
+ private bool _injectOnly;
+
+ ///
+ /// Amount to inject or draw on each usage. If the injector is inject only, it will
+ /// attempt to inject it's entire contents upon use.
+ ///
+ [ViewVariables]
+ private int _transferAmount;
+
+ ///
+ /// Initial storage volume of the injector
+ ///
+ [ViewVariables]
+ private int _initialMaxVolume;
+
+ ///
+ /// The state of the injector. Determines it's attack behavior. Containers must have the
+ /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
+ /// only ever be set to Inject
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ private InjectorToggleMode _toggleState;
+ ///
+ /// Internal solution container
+ ///
+ [ViewVariables]
+ private SolutionComponent _internalContents;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _injectOnly, "injectOnly", false);
+ serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", 15);
+ serializer.DataField(ref _transferAmount, "transferAmount", 5);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ //Create and setup internal storage
+ _internalContents = new SolutionComponent();
+ _internalContents.InitializeFromPrototype();
+ _internalContents.Init();
+ _internalContents.MaxVolume = _initialMaxVolume;
+ _internalContents.Owner = Owner; //Manually set owner to avoid crash when VV'ing this
+ _internalContents.Capabilities |= SolutionCaps.Injector;
+
+ //Set _toggleState based on prototype
+ _toggleState = _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw;
+ }
+
+ ///
+ /// Toggle between draw/inject state if applicable
+ ///
+ private void Toggle()
+ {
+ if (_injectOnly)
+ {
+ return;
+ }
+
+ _toggleState = _toggleState switch
+ {
+ InjectorToggleMode.Inject => InjectorToggleMode.Draw,
+ InjectorToggleMode.Draw => InjectorToggleMode.Inject,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ Dirty();
+ }
+
+ ///
+ /// Called when clicking on entities while holding in active hand
+ ///
+ ///
+ void IAfterAttack.AfterAttack(AfterAttackEventArgs eventArgs)
+ {
+ //Make sure we have the attacking entity
+ if (eventArgs.Attacked == null || !_internalContents.Injector)
+ {
+ return;
+ }
+
+ var targetEntity = eventArgs.Attacked;
+ //Handle injecting/drawing for solutions
+ if (targetEntity.TryGetComponent(out var targetSolution) && targetSolution.Injectable)
+ {
+ if (_toggleState == InjectorToggleMode.Inject)
+ {
+ TryInject(targetSolution, eventArgs.User);
+ }
+ else if (_toggleState == InjectorToggleMode.Draw)
+ {
+ TryDraw(targetSolution, eventArgs.User);
+ }
+ }
+ else //Handle injecting into bloodstream
+ {
+ if (targetEntity.TryGetComponent(out var bloodstream) && _toggleState == InjectorToggleMode.Inject)
+ {
+ TryInjectIntoBloodstream(bloodstream, eventArgs.User);
+ }
+ }
+ }
+
+ ///
+ /// Called when use key is pressed when held in active hand
+ ///
+ ///
+ ///
+ bool IUse.UseEntity(UseEntityEventArgs eventArgs)
+ {
+ Toggle();
+ return true;
+ }
+
+ private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
+ {
+ if (_internalContents.CurrentVolume == 0)
+ {
+ return;
+ }
+
+ //Get transfer amount. May be smaller than _transferAmount if not enough room
+ int realTransferAmount = Math.Min(_transferAmount, targetBloodstream.EmptyVolume);
+ if (realTransferAmount <= 0)
+ {
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
+ Loc.GetString("Container full."));
+ return;
+ }
+
+ //Move units from attackSolution to targetSolution
+ var removedSolution = _internalContents.SplitSolution(realTransferAmount);
+ if (!targetBloodstream.TryTransferSolution(removedSolution))
+ {
+ return;
+ }
+
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
+ Loc.GetString("Injected {0}u", removedSolution.TotalVolume));
+ Dirty();
+ }
+
+ private void TryInject(SolutionComponent targetSolution, IEntity user)
+ {
+ if (_internalContents.CurrentVolume == 0)
+ {
+ return;
+ }
+
+ //Get transfer amount. May be smaller than _transferAmount if not enough room
+ int realTransferAmount = Math.Min(_transferAmount, targetSolution.EmptyVolume);
+ if (realTransferAmount <= 0)
+ {
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
+ Loc.GetString("Container full."));
+ return;
+ }
+
+ //Move units from attackSolution to targetSolution
+ var removedSolution = _internalContents.SplitSolution(realTransferAmount);
+ if (!targetSolution.TryAddSolution(removedSolution))
+ {
+ return;
+ }
+
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
+ Loc.GetString("Injected {0}u", removedSolution.TotalVolume));
+ Dirty();
+ }
+
+ private void TryDraw(SolutionComponent targetSolution, IEntity user)
+ {
+ if (_internalContents.EmptyVolume == 0)
+ {
+ return;
+ }
+
+ //Get transfer amount. May be smaller than _transferAmount if not enough room
+ int realTransferAmount = Math.Min(_transferAmount, targetSolution.CurrentVolume);
+ if (realTransferAmount <= 0)
+ {
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
+ Loc.GetString("Container empty"));
+ return;
+ }
+
+ //Move units from attackSolution to targetSolution
+ var removedSolution = targetSolution.SplitSolution(realTransferAmount);
+ if (!_internalContents.TryAddSolution(removedSolution))
+ {
+ return;
+ }
+
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
+ Loc.GetString("Drew {0}u", removedSolution.TotalVolume));
+ Dirty();
+ }
+
+ public override ComponentState GetComponentState()
+ {
+ return new InjectorComponentState(_internalContents.CurrentVolume, _internalContents.MaxVolume, _toggleState);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
index 20d187a227..3e123fca11 100644
--- a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
+++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
@@ -35,7 +35,11 @@ namespace Content.Server.GameObjects.Components.Chemistry
protected override void Startup()
{
base.Startup();
-
+ Init();
+ }
+
+ public void Init()
+ {
_reactions = _prototypeManager.EnumeratePrototypes();
_audioSystem = _entitySystemManager.GetEntitySystem();
}
diff --git a/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs b/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs
new file mode 100644
index 0000000000..0fa862ce82
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs
@@ -0,0 +1,116 @@
+using System.Linq;
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.Chemistry;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Metabolism
+{
+ ///
+ /// Handles all metabolism for mobs. All delivery methods eventually bring reagents
+ /// to the bloodstream. For example, injectors put reagents directly into the bloodstream,
+ /// and the stomach does with some delay.
+ ///
+ [RegisterComponent]
+ public class BloodstreamComponent : Component
+ {
+#pragma warning disable 649
+ [Dependency] private readonly IPrototypeManager _prototypeManager;
+#pragma warning restore 649
+
+ public override string Name => "Bloodstream";
+
+ ///
+ /// Internal solution for reagent storage
+ ///
+ [ViewVariables]
+ private SolutionComponent _internalSolution;
+
+ ///
+ /// Max volume of internal solution storage
+ ///
+ [ViewVariables]
+ private int _initialMaxVolume;
+
+ ///
+ /// Empty volume of internal solution
+ ///
+ public int EmptyVolume => _internalSolution.EmptyVolume;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _initialMaxVolume, "maxVolume", 250);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ //Create and setup internal solution storage
+ _internalSolution = new SolutionComponent();
+ _internalSolution.InitializeFromPrototype();
+ _internalSolution.Init();
+ _internalSolution.MaxVolume = _initialMaxVolume;
+ _internalSolution.Owner = Owner; //Manually set owner to avoid crash when VV'ing this
+ }
+
+ ///
+ /// Attempt to transfer provided solution to internal solution. Only supports complete transfers
+ ///
+ /// Solution to be transferred
+ /// Whether or not transfer was a success
+ public bool TryTransferSolution(Solution solution)
+ {
+ //For now doesn't support partial transfers
+ if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
+ {
+ return false;
+ }
+
+ _internalSolution.TryAddSolution(solution, false, true);
+ return true;
+ }
+
+ ///
+ /// Loops through each reagent in _internalSolution, and calls the IMetabolizable for each of them./>
+ ///
+ /// The time since the last metabolism tick in seconds.
+ private void Metabolize(float tickTime)
+ {
+ if (_internalSolution.CurrentVolume == 0)
+ {
+ return;
+ }
+
+ //Run metabolism for each reagent, remove metabolized reagents
+ foreach (var reagent in _internalSolution.ReagentList.ToList()) //Using ToList here lets us edit reagents while iterating
+ {
+ if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
+ {
+ continue;
+ }
+
+ //Run metabolism code for each reagent
+ foreach (var metabolizable in proto.Metabolism)
+ {
+ int reagentDelta = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime);
+ _internalSolution.TryRemoveReagent(reagent.ReagentId, reagentDelta);
+ }
+ }
+ }
+
+ ///
+ /// Triggers metabolism of the reagents inside _internalSolution. Called by
+ ///
+ /// The time since the last metabolism tick in seconds.
+ public void OnUpdate(float tickTime)
+ {
+ Metabolize(tickTime);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs b/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs
index 7478d6c038..95353351f9 100644
--- a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs
+++ b/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs
@@ -1,40 +1,74 @@
using System.Collections.Generic;
+using System.Linq;
using Content.Server.GameObjects.Components.Chemistry;
-using Content.Server.GameObjects.EntitySystems;
+using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
-using Robust.Shared.Prototypes;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Nutrition
{
+ ///
+ /// Where reagents go when ingested. Tracks ingested reagents over time, and
+ /// eventually transfers them to once digested.
+ ///
[RegisterComponent]
public class StomachComponent : SharedStomachComponent
{
#pragma warning disable 649
- [Dependency] private readonly IPrototypeManager _prototypeManager;
+ [Dependency] private readonly ILocalizationManager _localizationManager;
#pragma warning restore 649
- [ViewVariables(VVAccess.ReadOnly)]
- private SolutionComponent _stomachContents;
+ ///
+ /// Max volume of internal solution storage
+ ///
public int MaxVolume
{
get => _stomachContents.MaxVolume;
set => _stomachContents.MaxVolume = value;
}
+
+ ///
+ /// Internal solution storage
+ ///
+ [ViewVariables]
+ private SolutionComponent _stomachContents;
+
+ ///
+ /// Initial internal solution storage volume
+ ///
+ [ViewVariables]
private int _initialMaxVolume;
- //Used to track changes to reagent amounts during metabolism
- private readonly Dictionary _reagentDeltas = new Dictionary();
+
+ ///
+ /// Time in seconds between reagents being ingested and them being transferred to
+ ///
+ [ViewVariables]
+ private float _digestionDelay;
+
+ ///
+ /// Used to track how long each reagent has been in the stomach
+ ///
+ private readonly List _reagentDeltas = new List();
+
+ ///
+ /// Reference to bloodstream where digested reagents are transferred to
+ ///
+ private BloodstreamComponent _bloodstream;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
- serializer.DataField(ref _initialMaxVolume, "max_volume", 20);
+ serializer.DataField(ref _initialMaxVolume, "maxVolume", 100);
+ serializer.DataField(ref _digestionDelay, "digestionDelay", 20);
}
+
public override void Initialize()
{
base.Initialize();
@@ -43,6 +77,14 @@ namespace Content.Server.GameObjects.Components.Nutrition
_stomachContents.InitializeFromPrototype();
_stomachContents.MaxVolume = _initialMaxVolume;
_stomachContents.Owner = Owner; //Manually set owner to avoid crash when VV'ing this
+
+ //Ensure bloodstream in present
+ if (!Owner.TryGetComponent(out _bloodstream))
+ {
+ Logger.Warning(_localizationManager.GetString(
+ "StomachComponent entity does not have a BloodstreamComponent, which is required for it to function. Owner entity name: {0}",
+ Owner.Name));
+ }
}
public bool TryTransferSolution(Solution solution)
@@ -52,47 +94,64 @@ namespace Content.Server.GameObjects.Components.Nutrition
{
return false;
}
+
+ //Add solution to _stomachContents
_stomachContents.TryAddSolution(solution, false, true);
+ //Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
+ foreach (var reagent in solution.Contents)
+ {
+ _reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
+ }
+
return true;
}
///
- /// Loops through each reagent in _stomachContents, and calls the IMetabolizable for each of them./>
+ /// Updates digestion status of ingested reagents. Once reagents surpass _digestionDelay
+ /// they are moved to the bloodstream, where they are then metabolized.
///
- /// The time since the last metabolism tick in seconds.
- public void Metabolize(float tickTime)
+ /// The time since the last update in seconds.
+ public void OnUpdate(float tickTime)
{
- if (_stomachContents.CurrentVolume == 0)
- return;
-
- //Run metabolism for each reagent, track quantity changes
- _reagentDeltas.Clear();
- foreach (var reagent in _stomachContents.ReagentList)
+ if (_bloodstream == null)
{
- if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
- continue;
+ return;
+ }
- foreach (var metabolizable in proto.Metabolism)
+ //Add reagents ready for transfer to bloodstream to transferSolution
+ var transferSolution = new Solution();
+ foreach (var delta in _reagentDeltas.ToList()) //Use ToList here to remove entries while iterating
+ {
+ //Increment lifetime of reagents
+ delta.Increment(tickTime);
+ if (delta.Lifetime > _digestionDelay)
{
- _reagentDeltas[reagent.ReagentId] = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime);
+ _stomachContents.TryRemoveReagent(delta.ReagentId, delta.Quantity);
+ transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
+ _reagentDeltas.Remove(delta);
}
}
-
- //Apply changes to quantity afterwards. Can't change the reagent quantities while the iterating the
- //list of reagents, because that would invalidate the iterator and throw an exception.
- foreach (var reagentDelta in _reagentDeltas)
- {
- _stomachContents.TryRemoveReagent(reagentDelta.Key, reagentDelta.Value);
- }
+ //Transfer digested reagents to bloodstream
+ _bloodstream.TryTransferSolution(transferSolution);
}
///
- /// Triggers metabolism of the reagents inside _stomachContents. Called by
+ /// Used to track quantity changes when ingesting & digesting reagents
///
- /// The time since the last metabolism tick in seconds.
- public void OnUpdate(float tickTime)
+ private class ReagentDelta
{
- Metabolize(tickTime);
+ public readonly string ReagentId;
+ public readonly int Quantity;
+ public float Lifetime { get; private set; }
+
+ public ReagentDelta(string reagentId, int quantity)
+ {
+ ReagentId = reagentId;
+ Quantity = quantity;
+ Lifetime = 0.0f;
+ }
+
+ public void Increment(float delta) => Lifetime += delta;
}
}
}
diff --git a/Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs b/Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs
new file mode 100644
index 0000000000..7806e09b25
--- /dev/null
+++ b/Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs
@@ -0,0 +1,35 @@
+using Content.Server.GameObjects.Components.Metabolism;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
+
+namespace Content.Server.GameObjects.EntitySystems
+{
+ ///
+ /// Triggers metabolism updates for
+ ///
+ [UsedImplicitly]
+ public class BloodstreamSystem : EntitySystem
+ {
+ private float _accumulatedFrameTime;
+ public override void Initialize()
+ {
+ EntityQuery = new TypeEntityQuery(typeof(BloodstreamComponent));
+ }
+
+ public override void Update(float frameTime)
+ {
+ //Trigger metabolism updates at most once per second
+ _accumulatedFrameTime += frameTime;
+ if (_accumulatedFrameTime > 1.0f)
+ {
+ foreach (var entity in RelevantEntities)
+ {
+ var comp = entity.GetComponent();
+ comp.OnUpdate(_accumulatedFrameTime);
+ }
+ _accumulatedFrameTime = 0.0f;
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/EntitySystems/StomachSystem.cs b/Content.Server/GameObjects/EntitySystems/StomachSystem.cs
index 4b30bcfffb..9e7e12aac7 100644
--- a/Content.Server/GameObjects/EntitySystems/StomachSystem.cs
+++ b/Content.Server/GameObjects/EntitySystems/StomachSystem.cs
@@ -1,10 +1,13 @@
-using Content.Server.GameObjects.Components.Nutrition;
+using Content.Server.GameObjects.Components.Nutrition;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
+ ///
+ /// Triggers digestion updates on
+ ///
[UsedImplicitly]
public class StomachSystem : EntitySystem
{
@@ -16,8 +19,8 @@ namespace Content.Server.GameObjects.EntitySystems
public override void Update(float frameTime)
{
+ //Update at most once per second
_accumulatedFrameTime += frameTime;
- // TODO: Potential performance improvement (e.g. going through say 1/5th the entities every tick)
if (_accumulatedFrameTime > 1.0f)
{
foreach (var entity in RelevantEntities)
diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs
new file mode 100644
index 0000000000..ebbb4b11e4
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs
@@ -0,0 +1,39 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Chemistry
+{
+ ///
+ /// Shared class for injectors & syringes
+ ///
+ public class SharedInjectorComponent : Component
+ {
+ public override string Name => "Injector";
+ public sealed override uint? NetID => ContentNetIDs.REAGENT_INJECTOR;
+
+ ///
+ /// Component data used for net updates. Used by client for item status ui
+ ///
+ [Serializable, NetSerializable]
+ protected sealed class InjectorComponentState : ComponentState
+ {
+ public int CurrentVolume { get; }
+ public int TotalVolume { get; }
+ public InjectorToggleMode CurrentMode { get; }
+
+ public InjectorComponentState(int currentVolume, int totalVolume, InjectorToggleMode currentMode) : base(ContentNetIDs.REAGENT_INJECTOR)
+ {
+ CurrentVolume = currentVolume;
+ TotalVolume = totalVolume;
+ CurrentMode = currentMode;
+ }
+ }
+
+ protected enum InjectorToggleMode
+ {
+ Inject,
+ Draw
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs
index d1e3b8fca7..426daa0639 100644
--- a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs
+++ b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs
@@ -74,6 +74,14 @@ namespace Content.Shared.GameObjects.Components.Chemistry
/// Shortcut for Capabilities PourOut flag to avoid binary operators.
///
public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0;
+ ///
+ /// Shortcut for Capabilities Injectable flag
+ ///
+ public bool Injectable => (Capabilities & SolutionCaps.Injectable) != 0;
+ ///
+ /// Shortcut for Capabilities Injector flag
+ ///
+ public bool Injector => (Capabilities & SolutionCaps.Injector) != 0;
///
public override string Name => "Solution";
diff --git a/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs b/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs
index 1333b58744..030b1eae50 100644
--- a/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs
+++ b/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs
@@ -1,8 +1,10 @@
-using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects;
namespace Content.Shared.GameObjects.Components.Nutrition
{
-
+ ///
+ /// Shared class for stomach components
+ ///
public class SharedStomachComponent : Component
{
public override string Name => "Stomach";
diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs
index bb3f1c6400..eeecefda41 100644
--- a/Content.Shared/GameObjects/ContentNetIDs.cs
+++ b/Content.Shared/GameObjects/ContentNetIDs.cs
@@ -40,5 +40,6 @@
public const uint STACK = 1035;
public const uint HANDHELD_LIGHT = 1036;
public const uint PAPER = 1037;
+ public const uint REAGENT_INJECTOR = 1038;
}
}
diff --git a/Resources/Prototypes/Entities/items/chemistry.yml b/Resources/Prototypes/Entities/items/chemistry.yml
index ced1e17639..cca0065844 100644
--- a/Resources/Prototypes/Entities/items/chemistry.yml
+++ b/Resources/Prototypes/Entities/items/chemistry.yml
@@ -10,7 +10,7 @@
texture: Objects/Chemistry/chemicals.rsi/beaker.png
- type: Solution
maxVol: 50
- caps: 19
+ caps: 27
- type: Pourable
transferAmount: 5
@@ -26,7 +26,7 @@
texture: Objects/Chemistry/chemicals.rsi/beakerlarge.png
- type: Solution
maxVol: 100
- caps: 19
+ caps: 27
- type: Pourable
transferAmount: 5
@@ -45,3 +45,19 @@
caps: 19
- type: Pourable
transferAmount: 5
+
+- type: entity
+ name: Syringe
+ parent: BaseItem
+ description: Used to draw blood samples from mobs, or to inject them with reagents
+ id: Syringe
+ components:
+ - type: Sprite
+ texture: Objects/Chemistry/chemicals.rsi/syringeproj.png
+ - type: Icon
+ texture: Objects/Chemistry/chemicals.rsi/syringeproj.png
+ - type: Solution
+ maxVol: 15
+ caps: 19
+ - type: Injector
+ injectOnly: false
diff --git a/Resources/Prototypes/Entities/mobs/human.yml b/Resources/Prototypes/Entities/mobs/human.yml
index e7b3c9c2c2..3ae50bfaa9 100644
--- a/Resources/Prototypes/Entities/mobs/human.yml
+++ b/Resources/Prototypes/Entities/mobs/human.yml
@@ -14,6 +14,10 @@
- type: Thirst
# Organs
- type: Stomach
+ maxVolume: 100
+ digestionDelay: 20
+ - type: Bloodstream
+ maxVolume: 250
- type: Inventory
- type: Constructor
diff --git a/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json b/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json
index 7c43141c20..6ff5767f18 100644
--- a/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json
+++ b/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json
@@ -1 +1 @@
-{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/blob/2b969adc2dfd3e9621bf3597c5cbffeb3ac8c9f0/icons/obj/chemical.dmi", "states": [{"name": "beaker", "directions": 1, "delays": [[1.0]]}, {"name": "beakerbluespace", "directions": 1, "delays": [[0.1, 0.1]]}, {"name": "beakerlarge", "directions": 1, "delays": [[1.0]]}, {"name": "beakernoreact", "directions": 1, "delays": [[0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]]}, {"name": "bottle", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-1", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-2", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-3", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-4", "directions": 1, "delays": [[1.0]]}, {"name": "bottle1", "directions": 1, "delays": [[1.0]]}, {"name": "bottle10", "directions": 1, "delays": [[1.0]]}, {"name": "bottle11", "directions": 1, "delays": [[1.0]]}, {"name": "bottle12", "directions": 1, "delays": [[1.0]]}, {"name": "bottle13", "directions": 1, "delays": [[1.0]]}, {"name": "bottle14", "directions": 1, "delays": [[1.0]]}, {"name": "bottle15", "directions": 1, "delays": [[1.0]]}, {"name": "bottle16", "directions": 1, "delays": [[1.0]]}, {"name": "bottle17", "directions": 1, "delays": [[1.0]]}, {"name": "bottle18", "directions": 1, "delays": [[1.0]]}, {"name": "bottle19", "directions": 1, "delays": [[1.0]]}, {"name": "bottle2", "directions": 1, "delays": [[1.0]]}, {"name": "bottle20", "directions": 1, "delays": [[1.0]]}, {"name": "bottle3", "directions": 1, "delays": [[1.0]]}, {"name": "bottle4", "directions": 1, "delays": [[1.0]]}, {"name": "bottle5", "directions": 1, "delays": [[1.0]]}, {"name": "bottle6", "directions": 1, "delays": [[1.0]]}, {"name": "bottle7", "directions": 1, "delays": [[1.0]]}, {"name": "bottle8", "directions": 1, "delays": [[1.0]]}, {"name": "bottle9", "directions": 1, "delays": [[1.0]]}, {"name": "chemg", "directions": 1, "delays": [[1.0]]}, {"name": "chemg_armed", "directions": 1, "delays": [[0.1, 0.2]]}, {"name": "chemg_ass", "directions": 1, "delays": [[1.0]]}, {"name": "chemg_locked", "directions": 1, "delays": [[1.0]]}, {"name": "chempuff", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "dropper", "directions": 1, "delays": [[1.0]]}, {"name": "large_grenade", "directions": 1, "delays": [[1.0]]}, {"name": "large_grenade_armed", "directions": 1, "delays": [[0.1, 0.1]]}, {"name": "large_grenade_ass", "directions": 1, "delays": [[1.0]]}, {"name": "large_grenade_locked", "directions": 1, "delays": [[1.0]]}, {"name": "lid_beaker", "directions": 1, "delays": [[1.0]]}, {"name": "lid_beakerlarge", "directions": 1, "delays": [[1.0]]}, {"name": "lid_beakernoreact", "directions": 1, "delays": [[1.0]]}, {"name": "lid_bottle", "directions": 1, "delays": [[1.0]]}, {"name": "lid_vial", "directions": 1, "delays": [[1.0]]}, {"name": "molten", "directions": 1, "delays": [[1.0]]}, {"name": "pill", "directions": 1, "delays": [[1.0]]}, {"name": "pill1", "directions": 1, "delays": [[1.0]]}, {"name": "pill10", "directions": 1, "delays": [[1.0]]}, {"name": "pill11", "directions": 1, "delays": [[1.0]]}, {"name": "pill12", "directions": 1, "delays": [[1.0]]}, {"name": "pill13", "directions": 1, "delays": [[1.0]]}, {"name": "pill14", "directions": 1, "delays": [[1.0]]}, {"name": "pill15", "directions": 1, "delays": [[1.0]]}, {"name": "pill16", "directions": 1, "delays": [[1.0]]}, {"name": "pill17", "directions": 1, "delays": [[1.0]]}, {"name": "pill18", "directions": 1, "delays": [[1.0]]}, {"name": "pill19", "directions": 1, "delays": [[1.0]]}, {"name": "pill2", "directions": 1, "delays": [[1.0]]}, {"name": "pill20", "directions": 1, "delays": [[1.0]]}, {"name": "pill3", "directions": 1, "delays": [[1.0]]}, {"name": "pill4", "directions": 1, "delays": [[1.0]]}, {"name": "pill5", "directions": 1, "delays": [[1.0]]}, {"name": "pill6", "directions": 1, "delays": [[1.0]]}, {"name": "pill7", "directions": 1, "delays": [[1.0]]}, {"name": "pill8", "directions": 1, "delays": [[1.0]]}, {"name": "pill9", "directions": 1, "delays": [[1.0]]}, {"name": "pill_canister", "directions": 1, "delays": [[1.0]]}, {"name": "syringeproj", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "vial", "directions": 1, "delays": [[1.0]]}, {"name": "weedpuff", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1]]}]}
\ No newline at end of file
+{"version":1,"size":{"x":32,"y":32},"license":"CC-BY-SA-3.0","copyright":"Taken from https://github.com/discordia-space/CEV-Eris/blob/2b969adc2dfd3e9621bf3597c5cbffeb3ac8c9f0/icons/obj/chemical.dmi","states":[{"name":"beaker","directions":1,"delays":[[1]]},{"name":"beakerbluespace","directions":1,"delays":[[0.1,0.1]]},{"name":"beakerlarge","directions":1,"delays":[[1]]},{"name":"beakernoreact","directions":1,"delays":[[0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05]]},{"name":"bottle","directions":1,"delays":[[1]]},{"name":"bottle-1","directions":1,"delays":[[1]]},{"name":"bottle-2","directions":1,"delays":[[1]]},{"name":"bottle-3","directions":1,"delays":[[1]]},{"name":"bottle-4","directions":1,"delays":[[1]]},{"name":"bottle1","directions":1,"delays":[[1]]},{"name":"bottle10","directions":1,"delays":[[1]]},{"name":"bottle11","directions":1,"delays":[[1]]},{"name":"bottle12","directions":1,"delays":[[1]]},{"name":"bottle13","directions":1,"delays":[[1]]},{"name":"bottle14","directions":1,"delays":[[1]]},{"name":"bottle15","directions":1,"delays":[[1]]},{"name":"bottle16","directions":1,"delays":[[1]]},{"name":"bottle17","directions":1,"delays":[[1]]},{"name":"bottle18","directions":1,"delays":[[1]]},{"name":"bottle19","directions":1,"delays":[[1]]},{"name":"bottle2","directions":1,"delays":[[1]]},{"name":"bottle20","directions":1,"delays":[[1]]},{"name":"bottle3","directions":1,"delays":[[1]]},{"name":"bottle4","directions":1,"delays":[[1]]},{"name":"bottle5","directions":1,"delays":[[1]]},{"name":"bottle6","directions":1,"delays":[[1]]},{"name":"bottle7","directions":1,"delays":[[1]]},{"name":"bottle8","directions":1,"delays":[[1]]},{"name":"bottle9","directions":1,"delays":[[1]]},{"name":"chemg","directions":1,"delays":[[1]]},{"name":"chemg_armed","directions":1,"delays":[[0.1,0.2]]},{"name":"chemg_ass","directions":1,"delays":[[1]]},{"name":"chemg_locked","directions":1,"delays":[[1]]},{"name":"chempuff","directions":4,"delays":[[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1]]},{"name":"dropper","directions":1,"delays":[[1]]},{"name":"large_grenade","directions":1,"delays":[[1]]},{"name":"large_grenade_armed","directions":1,"delays":[[0.1,0.1]]},{"name":"large_grenade_ass","directions":1,"delays":[[1]]},{"name":"large_grenade_locked","directions":1,"delays":[[1]]},{"name":"lid_beaker","directions":1,"delays":[[1]]},{"name":"lid_beakerlarge","directions":1,"delays":[[1]]},{"name":"lid_beakernoreact","directions":1,"delays":[[1]]},{"name":"lid_bottle","directions":1,"delays":[[1]]},{"name":"lid_vial","directions":1,"delays":[[1]]},{"name":"molten","directions":1,"delays":[[1]]},{"name":"pill","directions":1,"delays":[[1]]},{"name":"pill1","directions":1,"delays":[[1]]},{"name":"pill10","directions":1,"delays":[[1]]},{"name":"pill11","directions":1,"delays":[[1]]},{"name":"pill12","directions":1,"delays":[[1]]},{"name":"pill13","directions":1,"delays":[[1]]},{"name":"pill14","directions":1,"delays":[[1]]},{"name":"pill15","directions":1,"delays":[[1]]},{"name":"pill16","directions":1,"delays":[[1]]},{"name":"pill17","directions":1,"delays":[[1]]},{"name":"pill18","directions":1,"delays":[[1]]},{"name":"pill19","directions":1,"delays":[[1]]},{"name":"pill2","directions":1,"delays":[[1]]},{"name":"pill20","directions":1,"delays":[[1]]},{"name":"pill3","directions":1,"delays":[[1]]},{"name":"pill4","directions":1,"delays":[[1]]},{"name":"pill5","directions":1,"delays":[[1]]},{"name":"pill6","directions":1,"delays":[[1]]},{"name":"pill7","directions":1,"delays":[[1]]},{"name":"pill8","directions":1,"delays":[[1]]},{"name":"pill9","directions":1,"delays":[[1]]},{"name":"pill_canister","directions":1,"delays":[[1]]},{"name":"syringeproj","directions":1,"delays":[[1]]},{"name":"vial","directions":1,"delays":[[1]]},{"name":"weedpuff","directions":4,"delays":[[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1]]}]}
diff --git a/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png b/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png
index 175d7cdf63..7819a48c66 100644
Binary files a/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png and b/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png differ