diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index 009a666727..bca658c3d3 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -128,7 +128,8 @@ namespace Content.Client
"UtilityBeltClothingFill",
"ShuttleController",
"HumanInventoryController",
- "UseDelay"
+ "UseDelay",
+ "Pourable"
};
foreach (var ignoreName in registerIgnore)
diff --git a/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs
new file mode 100644
index 0000000000..f2785ca210
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Content.Server.GameObjects.Components.Nutrition;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Interfaces;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Chemistry
+{
+ ///
+ /// Gives an entity click behavior for pouring reagents into
+ /// other entities and being poured into. The entity must have
+ /// a SolutionComponent or DrinkComponent for this to work.
+ /// (DrinkComponent adds a SolutionComponent if one isn't present).
+ ///
+ [RegisterComponent]
+ class PourableComponent : Component, IAttackBy
+ {
+#pragma warning disable 649
+ [Dependency] private readonly IServerNotifyManager _notifyManager;
+ [Dependency] private readonly ILocalizationManager _localizationManager;
+#pragma warning restore 649
+
+ public override string Name => "Pourable";
+
+ private int _transferAmount;
+
+ ///
+ /// The amount of solution to be transferred from this solution when clicking on other solutions with it.
+ ///
+ [ViewVariables]
+ public int TransferAmount
+ {
+ get => _transferAmount;
+ set => _transferAmount = value;
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _transferAmount, "transferAmount", 5);
+ }
+
+ ///
+ /// Called when the owner of this component is clicked on with another entity.
+ /// The owner of this component is the target.
+ /// The entity used to click on this one is the attacker.
+ ///
+ /// Attack event args
+ ///
+ bool IAttackBy.AttackBy(AttackByEventArgs eventArgs)
+ {
+ //Get target and check if it can be poured into
+ if (!Owner.TryGetComponent(out var targetSolution))
+ return false;
+ if (!targetSolution.CanPourIn)
+ return false;
+
+ //Get attack entity and check if it can pour out.
+ var attackEntity = eventArgs.AttackWith;
+ if (!attackEntity.TryGetComponent(out var attackSolution) || !attackSolution.CanPourOut)
+ return false;
+ if (!attackEntity.TryGetComponent(out var attackPourable))
+ return false;
+
+ //Get transfer amount. May be smaller than _transferAmount if not enough room
+ int realTransferAmount = Math.Min(attackPourable.TransferAmount, targetSolution.EmptyVolume);
+ if (realTransferAmount <= 0) //Special message if container is full
+ {
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, eventArgs.User,
+ _localizationManager.GetString("Container is full"));
+ return false;
+ }
+ //Remove transfer amount from attacker
+ if (!attackSolution.TryRemoveSolution(realTransferAmount, out var removedSolution))
+ return false;
+
+ //Add poured solution to this solution
+ if (!targetSolution.TryAddSolution(removedSolution))
+ return false;
+
+ _notifyManager.PopupMessage(Owner.Transform.GridPosition, eventArgs.User,
+ _localizationManager.GetString("Transferred {0}u", removedSolution.TotalVolume));
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
index ffd687b476..20d187a227 100644
--- a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
+++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.Design;
using Content.Server.Chemistry;
+using Content.Server.GameObjects.Components.Nutrition;
using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Interfaces;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
diff --git a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs
index f3209b7b2a..8b7c9728c6 100644
--- a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs
+++ b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs
@@ -80,10 +80,18 @@ namespace Content.Server.GameObjects.Components.Nutrition
else
{
_contents = Owner.AddComponent();
+ //Ensure SolutionComponent supports click transferring if custom one not set
+ _contents.Capabilities = SolutionCaps.PourIn
+ | SolutionCaps.PourOut
+ | SolutionCaps.Injectable;
+
+ var pourable = Owner.AddComponent();
+ pourable.TransferAmount = 5;
}
}
_contents.MaxVolume = _initialContents.TotalVolume;
+ _contents.SolutionChanged += HandleSolutionChangedEvent;
}
protected override void Startup()
@@ -117,7 +125,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
UseDrink(eventArgs.Attacked);
}
- void UseDrink(IEntity user)
+ private void UseDrink(IEntity user)
{
if (user == null)
{
@@ -150,34 +158,53 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
}
+ Finish(user);
+ }
+
+ ///
+ /// Trigger finish behavior in the drink if applicable.
+ /// Depending on the drink this will either delete it,
+ /// or convert it to another entity, like an empty variant.
+ ///
+ /// The entity that is using the drink
+ public void Finish(IEntity user)
+ {
// Drink containers are mostly transient.
if (!_despawnOnFinish || UsesLeft() > 0)
- {
return;
- }
-
+ var gridPos = Owner.Transform.GridPosition;
+ _contents.SolutionChanged -= HandleSolutionChangedEvent;
Owner.Delete();
- if (_finishPrototype != null)
- {
- var finisher = Owner.EntityManager.SpawnEntity(_finishPrototype, Owner.Transform.GridPosition);
- if (user.TryGetComponent(out HandsComponent handsComponent) && finisher.TryGetComponent(out ItemComponent itemComponent))
- {
- if (handsComponent.CanPutInHand(itemComponent))
- {
- handsComponent.PutInHand(itemComponent);
- return;
- }
- }
-
- finisher.Transform.GridPosition = user.Transform.GridPosition;
- if (finisher.TryGetComponent(out DrinkComponent drinkComponent))
- {
- drinkComponent.MaxVolume = MaxVolume;
- }
+ if (_finishPrototype == null || user == null)
return;
+
+ var finisher = Owner.EntityManager.SpawnEntity(_finishPrototype, Owner.Transform.GridPosition);
+ if (user.TryGetComponent(out HandsComponent handsComponent) && finisher.TryGetComponent(out ItemComponent itemComponent))
+ {
+ if (handsComponent.CanPutInHand(itemComponent))
+ {
+ handsComponent.PutInHand(itemComponent);
+ return;
+ }
}
+
+ finisher.Transform.GridPosition = gridPos;
+ if (finisher.TryGetComponent(out DrinkComponent drinkComponent))
+ {
+ drinkComponent.MaxVolume = MaxVolume;
+ }
+ }
+
+ ///
+ /// Updates drink state when the solution is changed by something other
+ /// than this component. Without this some drinks won't properly delete
+ /// themselves without additional clicks/uses after them being emptied.
+ ///
+ private void HandleSolutionChangedEvent()
+ {
+ Finish(null);
}
}
}
diff --git a/Content.Shared/Chemistry/Solution.cs b/Content.Shared/Chemistry/Solution.cs
index 2e996d3f33..2255691a1e 100644
--- a/Content.Shared/Chemistry/Solution.cs
+++ b/Content.Shared/Chemistry/Solution.cs
@@ -125,15 +125,23 @@ namespace Content.Shared.Chemistry
}
}
- public void RemoveSolution(int quantity)
+ ///
+ /// Remove the specified quantity from this solution.
+ ///
+ /// The quantity of this solution to remove
+ /// Out arg. The removed solution. Useful for adding removed solution
+ /// into other solutions. For example, when pouring from one container to another.
+ public void RemoveSolution(int quantity, out Solution removedSolution)
{
- if(quantity <=0)
+ removedSolution = new Solution();
+ if(quantity <= 0)
return;
var ratio = (float)(TotalVolume - quantity) / TotalVolume;
if (ratio <= 0)
{
+ removedSolution = this.Clone(); //Todo: Check if clone necessary
RemoveAllSolution();
return;
}
@@ -148,6 +156,7 @@ namespace Content.Shared.Chemistry
var newQuantity = (int)Math.Floor(oldQuantity * ratio);
_contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity);
+ removedSolution.AddReagent(reagent.ReagentId, oldQuantity - newQuantity);
}
TotalVolume = (int)Math.Floor(TotalVolume * ratio);
diff --git a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs
index 2624d79cef..498a070332 100644
--- a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs
+++ b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs
@@ -42,6 +42,12 @@ namespace Content.Shared.GameObjects.Components.Chemistry
[ViewVariables]
public int CurrentVolume => _containedSolution.TotalVolume;
+ ///
+ /// The volume without reagents remaining in the container.
+ ///
+ [ViewVariables]
+ public int EmptyVolume => MaxVolume - CurrentVolume;
+
///
/// The current blended color of all the reagents in the container.
///
@@ -60,6 +66,15 @@ namespace Content.Shared.GameObjects.Components.Chemistry
public IReadOnlyList ReagentList => _containedSolution.Contents;
+ ///
+ /// Shortcut for Capabilities PourIn flag to avoid binary operators.
+ ///
+ public bool CanPourIn => (Capabilities & SolutionCaps.PourIn) != 0;
+ ///
+ /// Shortcut for Capabilities PourOut flag to avoid binary operators.
+ ///
+ public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0;
+
///
public override string Name => "Solution";
@@ -108,11 +123,20 @@ namespace Content.Shared.GameObjects.Components.Chemistry
return true;
}
- public bool TryRemoveSolution(int quantity)
+ ///
+ /// Attempt to remove the specified quantity from this solution
+ ///
+ /// Quantity of this solution to remove
+ /// Out arg. The removed solution. Useful for adding removed solution
+ /// into other solutions. For example, when pouring from one container to another.
+ /// Whether or not the solution was successfully removed
+ public bool TryRemoveSolution(int quantity, out Solution removedSolution)
{
- if (CurrentVolume == 0) return false;
+ removedSolution = new Solution();
+ if (CurrentVolume == 0)
+ return false;
- _containedSolution.RemoveSolution(quantity);
+ _containedSolution.RemoveSolution(quantity, out removedSolution);
OnSolutionChanged();
return true;
}
diff --git a/Content.Tests/Shared/Chemistry/Solution_Tests.cs b/Content.Tests/Shared/Chemistry/Solution_Tests.cs
index d78dc39a2f..e2c82b7b4d 100644
--- a/Content.Tests/Shared/Chemistry/Solution_Tests.cs
+++ b/Content.Tests/Shared/Chemistry/Solution_Tests.cs
@@ -141,10 +141,14 @@ namespace Content.Tests.Shared.Chemistry
{
var solution = new Solution("water", 700);
- solution.RemoveSolution(500);
+ solution.RemoveSolution(500, out var removedSolution);
+ //Check that edited solution is correct
Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(200));
Assert.That(solution.TotalVolume, Is.EqualTo(200));
+ //Check that removed solution is correct
+ Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(500));
+ Assert.That(removedSolution.TotalVolume, Is.EqualTo(500));
}
[Test]
@@ -152,10 +156,14 @@ namespace Content.Tests.Shared.Chemistry
{
var solution = new Solution("water", 800);
- solution.RemoveSolution(1000);
+ solution.RemoveSolution(1000, out var removedSolution);
+ //Check that edited solution is correct
Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(0));
Assert.That(solution.TotalVolume, Is.EqualTo(0));
+ //Check that removed solution is correct
+ Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(800));
+ Assert.That(removedSolution.TotalVolume, Is.EqualTo(800));
}
[Test]
@@ -165,11 +173,15 @@ namespace Content.Tests.Shared.Chemistry
solution.AddReagent("water", 1000);
solution.AddReagent("fire", 2000);
- solution.RemoveSolution(1500);
+ solution.RemoveSolution(1500, out var removedSolution);
Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(500));
Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(1000));
Assert.That(solution.TotalVolume, Is.EqualTo(1500));
+
+ Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(500));
+ Assert.That(removedSolution.GetReagentQuantity("fire"), Is.EqualTo(1000));
+ Assert.That(removedSolution.TotalVolume, Is.EqualTo(1500));
}
[Test]
@@ -177,10 +189,13 @@ namespace Content.Tests.Shared.Chemistry
{
var solution = new Solution("water", 800);
- solution.RemoveSolution(-200);
+ solution.RemoveSolution(-200, out var removedSolution);
Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(800));
Assert.That(solution.TotalVolume, Is.EqualTo(800));
+
+ Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(0));
+ Assert.That(removedSolution.TotalVolume, Is.EqualTo(0));
}
[Test]
diff --git a/Resources/Prototypes/Entities/items/chemistry.yml b/Resources/Prototypes/Entities/items/chemistry.yml
index bb2a883e50..ced1e17639 100644
--- a/Resources/Prototypes/Entities/items/chemistry.yml
+++ b/Resources/Prototypes/Entities/items/chemistry.yml
@@ -11,6 +11,8 @@
- type: Solution
maxVol: 50
caps: 19
+ - type: Pourable
+ transferAmount: 5
- type: entity
name: Large Beaker
@@ -25,6 +27,8 @@
- type: Solution
maxVol: 100
caps: 19
+ - type: Pourable
+ transferAmount: 5
- type: entity
name: Dropper
@@ -39,3 +43,5 @@
- type: Solution
maxVol: 5
caps: 19
+ - type: Pourable
+ transferAmount: 5