Rework the ChemMaster's output handling (#11207)

* Fix doc comment on FitsInDispenserComponent

It's clearly intended to be a doc comment, but wasn't.

* Allow the ChemMaster to accept canisters and bottles

* Give the ChemMaster an output container slot

* Tweak ChemMaster UI layout

* Make more ChemMaster UI tweaks

* Update ChemMaster SpinBox max handling

* Rework the ChemMaster

* Apply suggestions from code review

Co-authored-by: Flipp Syder <76629141+vulppine@users.noreply.github.com>

* Implement PR feedback

* Switch ChemMaster to a tabbed UI layout

* Rename Amount to Dosage for clarity

* Replace Amount with Dosage in messages

* Clarify dose in UI

Co-authored-by: Flipp Syder <76629141+vulppine@users.noreply.github.com>
This commit is contained in:
Illiux
2022-09-14 17:10:12 -07:00
committed by GitHub
parent f54c1cb2b3
commit 71e46de0fc
9 changed files with 472 additions and 247 deletions

View File

@@ -37,11 +37,19 @@ namespace Content.Client.Chemistry.UI
_window.OnClose += Close;
// Setup static button actions.
_window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedChemMaster.ContainerSlotName));
_window.BufferTransferButton.OnPressed += _ => SendMessage(new ChemMasterSetModeMessage(ChemMasterMode.Transfer));
_window.BufferDiscardButton.OnPressed += _ => SendMessage(new ChemMasterSetModeMessage(ChemMasterMode.Discard));
_window.CreatePillButton.OnPressed += _ => SendMessage(new ChemMasterCreatePillsMessage(((uint)_window.PillAmount.Value), _window.LabelLine));
_window.CreateBottleButton.OnPressed += _ => SendMessage(new ChemMasterCreateBottlesMessage(((uint)_window.BottleAmount.Value), _window.LabelLine));
_window.InputEjectButton.OnPressed += _ => SendMessage(
new ItemSlotButtonPressedEvent(SharedChemMaster.InputSlotName));
_window.OutputEjectButton.OnPressed += _ => SendMessage(
new ItemSlotButtonPressedEvent(SharedChemMaster.OutputSlotName));
_window.BufferTransferButton.OnPressed += _ => SendMessage(
new ChemMasterSetModeMessage(ChemMasterMode.Transfer));
_window.BufferDiscardButton.OnPressed += _ => SendMessage(
new ChemMasterSetModeMessage(ChemMasterMode.Discard));
_window.CreatePillButton.OnPressed += _ => SendMessage(
new ChemMasterCreatePillsMessage(
(uint)_window.PillDosage.Value, (uint)_window.PillNumber.Value, _window.LabelLine));
_window.CreateBottleButton.OnPressed += _ => SendMessage(
new ChemMasterOutputToBottleMessage((uint)_window.BottleDosage.Value, _window.LabelLine));
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
{

View File

@@ -1,76 +1,106 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="400 525"
MinSize="620 670"
Title="{Loc 'chem-master-bound-user-interface-title'}">
<BoxContainer Orientation="Vertical"
Margin="5 5 5 5"
SeparationOverride="10">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-container-label'}" />
<Control HorizontalExpand="True" />
<Button Name="EjectButton"
Access="Public"
Text="{Loc 'chem-master-window-eject-button'}" />
</BoxContainer>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="6"
MinSize="0 200">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<!-- Initially empty, when server sends state data this will have container contents and fill volume.-->
<BoxContainer Name="ContainerInfo"
Orientation="Vertical"
HorizontalExpand="True">
<Label Text="{Loc 'chem-master-window-no-container-loaded-text'}" />
<TabContainer Name="Tabs" Margin="0 0 2 0">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
Margin="5 5 5 5"
SeparationOverride="10">
<!-- Input container info -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-container-label'}" />
<Control HorizontalExpand="True" />
<Button Name="InputEjectButton"
Access="Public"
Text="{Loc 'chem-master-window-eject-button'}" />
</BoxContainer>
</PanelContainer>
<Control MinSize="0 10" />
<!-- Buffer -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-buffer-text'}" />
<Control HorizontalExpand="True" />
<Button Name="BufferTransferButton"
Access="Public"
Text="{Loc 'chem-master-window-transfer-button'}" ToggleMode="True"
StyleClasses="OpenRight" />
<Button Name="BufferDiscardButton"
Access="Public"
Text="{Loc 'chem-master-window-discard-button'}" ToggleMode="True"
StyleClasses="OpenLeft" />
</BoxContainer>
<!-- Buffer info -->
<PanelContainer VerticalExpand="True" SizeFlagsStretchRatio="1" MinSize="0 150">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<!-- Buffer reagent list -->
<BoxContainer Name="BufferInfo"
Orientation="Vertical"
HorizontalExpand="True">
<Label Text="{Loc 'chem-master-window-buffer-empty-text'}" />
<PanelContainer VerticalExpand="True" MinSize="0 200">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<!-- Initially empty, when server sends state data this will have container contents and fill volume.-->
<BoxContainer Name="InputContainerInfo"
Orientation="Vertical"
Margin="4 4 4 4"
HorizontalExpand="True">
<Label Text="{Loc 'chem-master-window-no-container-loaded-text'}" />
</BoxContainer>
</PanelContainer>
<!-- Padding -->
<Control MinSize="0 10" />
<!-- Buffer -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-buffer-text'}" />
<Control HorizontalExpand="True" />
<Button Name="BufferTransferButton"
Access="Public"
Text="{Loc 'chem-master-window-transfer-button'}" ToggleMode="True"
StyleClasses="OpenRight" />
<Button Name="BufferDiscardButton"
Access="Public"
Text="{Loc 'chem-master-window-discard-button'}" ToggleMode="True"
StyleClasses="OpenLeft" />
</BoxContainer>
</PanelContainer>
<!-- Padding -->
<Control MinSize="0 10" />
<PanelContainer VerticalExpand="True" MinSize="100 100">
<!-- Buffer info -->
<PanelContainer VerticalExpand="True" MinSize="0 200">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<!-- Buffer reagent list -->
<BoxContainer Name="BufferInfo"
Orientation="Vertical"
Margin="4 4 4 4"
HorizontalExpand="True">
<Label Text="{Loc 'chem-master-window-buffer-empty-text'}" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
Margin="5 5 5 5"
SeparationOverride="10">
<!-- Output container info -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-container-label'}" />
<Control HorizontalExpand="True" />
<Button Name="OutputEjectButton"
Access="Public"
Text="{Loc 'chem-master-window-eject-button'}" />
</BoxContainer>
<PanelContainer VerticalExpand="True" MinSize="0 200">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<!-- Initially empty, when server sends state data this will have container contents and fill volume.-->
<BoxContainer Name="OutputContainerInfo"
Orientation="Vertical"
Margin="4 4 4 4"
HorizontalExpand="True">
<Label Text="{Loc 'chem-master-window-no-container-loaded-text'}" />
</BoxContainer>
</PanelContainer>
<!-- Padding -->
<Control MinSize="0 10" />
<!-- Packaging -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-packaging-text'}" />
<Control HorizontalExpand="True"/>
<Label Text="{Loc 'chem-master-window-buffer-label'}" />
<Label Name="BufferCurrentVolume" StyleClasses="LabelSecondaryColor" />
</BoxContainer>
<!-- Wrap the packaging info-->
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="6"
MinSize="0 100">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<!-- Packaging Info -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="5">
Margin="4 4 4 4"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="5">
<!-- Label for output -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-current-text-label'}" />
@@ -89,30 +119,38 @@
<Label Text="{Loc 'chem-master-window-pills-label'}" />
<Control HorizontalExpand="True"
MinSize="50 0" />
<SpinBox Name="PillAmount"
Access="Public"
Value="1" />
<Label Text="{Loc 'chem-master-window-pills-number-label'}"
Margin="5 0 0 0"
StyleClasses="LabelSecondaryColor" />
<SpinBox Name="PillNumber"
Access="Public"
Value="0" />
<Label Text="{Loc 'chem-master-window-dose-label'}"
Margin="5 0 0 0"
StyleClasses="LabelSecondaryColor" />
<SpinBox Name="PillDosage"
Access="Public"
Value="1" />
<Button Name="CreatePillButton"
Access="Public"
Text="{Loc 'chem-master-window-create-pill-button'}" />
<Label Text="{Loc 'chem-master-window-max-pills-volume-text'}"
StyleClasses="LabelSecondaryColor" />
Text="{Loc 'chem-master-window-create-button'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-bottles-label'}" />
<Control HorizontalExpand="True"
MinSize="50 0" />
<SpinBox Name="BottleAmount"
<Label Text="{Loc 'chem-master-window-dose-label'}"
Margin="5 0 0 0"
StyleClasses="LabelSecondaryColor" />
<SpinBox Name="BottleDosage"
Access="Public"
Value="1" />
Value="0" />
<Button Name="CreateBottleButton"
Access="Public"
Text="{Loc 'chem-master-window-create-bottle-button'}" />
<Label Text="{Loc 'chem-master-window-max-bottles-volume-text'}"
StyleClasses="LabelSecondaryColor" />
Text="{Loc 'chem-master-window-create-button'}" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</TabContainer>
</DefaultWindow>

View File

@@ -75,8 +75,12 @@ namespace Content.Client.Chemistry.UI
Grid.AddChild(PillTypeButtons[i]);
}
PillAmount.InitDefaultButtons();
BottleAmount.InitDefaultButtons();
PillDosage.InitDefaultButtons();
PillNumber.InitDefaultButtons();
BottleDosage.InitDefaultButtons();
Tabs.SetTabTitle(0, Loc.GetString("chem-master-window-input-tab"));
Tabs.SetTabTitle(1, Loc.GetString("chem-master-window-output-tab"));
}
private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, string id, bool isBuffer, string styleClass)
@@ -98,14 +102,30 @@ namespace Content.Client.Chemistry.UI
if (castState.UpdateLabel)
LabelLine = GenerateLabel(castState);
UpdatePanelInfo(castState);
if (Contents.Children != null)
{
EjectButton.Disabled = !castState.HasContainer();
}
var output = castState.OutputContainerInfo;
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = output is null;
CreateBottleButton.Disabled = output is null || !output.HoldsReagents;
CreatePillButton.Disabled = output is null || output.HoldsReagents;
var remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int();
var holdsReagents = output?.HoldsReagents ?? false;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
PillTypeButtons[castState.SelectedPillType].Pressed = true;
PillAmount.IsValid = x => x > 0 && x <= castState.PillProductionLimit;
BottleAmount.IsValid = x => x > 0 && x <= castState.BottleProductionLimit;
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit;
BottleDosage.IsValid = x => x >= 0 && x <= bottleAmountMax;
if (PillNumber.Value > pillNumberMax)
PillNumber.Value = pillNumberMax;
if (BottleDosage.Value > bottleAmountMax)
BottleDosage.Value = bottleAmountMax;
}
/// <summary>
@@ -134,64 +154,8 @@ namespace Content.Client.Chemistry.UI
BufferTransferButton.Pressed = state.Mode == Shared.Chemistry.ChemMasterMode.Transfer;
BufferDiscardButton.Pressed = state.Mode == Shared.Chemistry.ChemMasterMode.Discard;
ContainerInfo.Children.Clear();
if (!state.HasContainer())
{
ContainerInfo.Children.Add(new Label {Text = Loc.GetString("chem-master-window-no-container-loaded-text") });
}
else
{
ContainerInfo.Children.Add(new BoxContainer // Name of the container and its fill status (Ex: 44/100u)
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label {Text = $"{state.ContainerName}: "},
new Label
{
Text = $"{state.ContainerCurrentVolume}/{state.ContainerMaxVolume}",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
}
}
});
foreach (var reagent in state.ContainerReagents!.OrderBy(
r => {_prototypeManager.TryIndex(r.ReagentId, out ReagentPrototype? p); return p?.LocalizedName;}))
{
// Try to get the prototype for the given reagent. This gives us its name.
_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
if (proto != null)
{
ContainerInfo.Children.Add(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label {Text = $"{name}: "},
new Label
{
Text = $"{reagent.Quantity}u",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
},
// Padding
new Control {HorizontalExpand = true},
MakeReagentButton("1", ChemMasterReagentAmount.U1, reagent.ReagentId, false, StyleBase.ButtonOpenRight),
MakeReagentButton("5", ChemMasterReagentAmount.U5, reagent.ReagentId, false, StyleBase.ButtonOpenBoth),
MakeReagentButton("10", ChemMasterReagentAmount.U10, reagent.ReagentId, false, StyleBase.ButtonOpenBoth),
MakeReagentButton("25", ChemMasterReagentAmount.U25, reagent.ReagentId, false, StyleBase.ButtonOpenBoth),
MakeReagentButton(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, reagent.ReagentId, false, StyleBase.ButtonOpenLeft),
}
});
}
}
}
BuildContainerUI(InputContainerInfo, state.InputContainerInfo, true);
BuildContainerUI(OutputContainerInfo, state.OutputContainerInfo, false);
BufferInfo.Children.Clear();
@@ -212,7 +176,7 @@ namespace Content.Client.Chemistry.UI
bufferHBox.AddChild(bufferLabel);
var bufferVol = new Label
{
Text = $"{state.BufferCurrentVolume}",
Text = $"{state.BufferCurrentVolume}u",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
};
bufferHBox.AddChild(bufferVol);
@@ -251,6 +215,92 @@ namespace Content.Client.Chemistry.UI
}
}
private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons)
{
control.Children.Clear();
if (info is null)
{
control.Children.Add(new Label
{
Text = Loc.GetString("chem-master-window-no-container-loaded-text")
});
}
else
{
// Name of the container and its fill status (Ex: 44/100u)
control.Children.Add(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label {Text = $"{info.DisplayName}: "},
new Label
{
Text = $"{info.CurrentVolume}/{info.MaxVolume}",
StyleClasses = {StyleNano.StyleClassLabelSecondaryColor}
}
}
});
var contents = info.Contents
.Select(lineItem =>
{
if (!info.HoldsReagents)
return (lineItem.Id, lineItem.Id, lineItem.Quantity);
// Try to get the prototype for the given reagent. This gives us its name.
_prototypeManager.TryIndex(lineItem.Id, out ReagentPrototype? proto);
var name = proto?.LocalizedName
?? Loc.GetString("chem-master-window-unknown-reagent-text");
return (name, lineItem.Id, lineItem.Quantity);
})
.OrderBy(r => r.Item1);
foreach (var (name, id, quantity) in contents)
{
var inner = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
new Label { Text = $"{name}: " },
new Label
{
Text = $"{quantity}u",
StyleClasses = { StyleNano.StyleClassLabelSecondaryColor },
}
}
};
if (addReagentButtons)
{
var cs = inner.Children;
// Padding
cs.Add(new Control { HorizontalExpand = true });
cs.Add(MakeReagentButton(
"1", ChemMasterReagentAmount.U1, id, false, StyleBase.ButtonOpenRight));
cs.Add(MakeReagentButton(
"5", ChemMasterReagentAmount.U5, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
"10", ChemMasterReagentAmount.U10, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
"25", ChemMasterReagentAmount.U25, id, false, StyleBase.ButtonOpenBoth));
cs.Add(MakeReagentButton(
Loc.GetString("chem-master-window-buffer-all-amount"),
ChemMasterReagentAmount.All, id, false, StyleBase.ButtonOpenLeft));
}
control.Children.Add(inner);
}
}
}
public String LabelLine
{
get

View File

@@ -18,11 +18,8 @@ namespace Content.Server.Chemistry.Components
[DataField("mode"), ViewVariables(VVAccess.ReadWrite)]
public ChemMasterMode Mode = ChemMasterMode.Transfer;
[DataField("pillProductionLimit", required: true), ViewVariables(VVAccess.ReadWrite)]
public uint PillProductionLimit;
[DataField("bottleProductionLimit", required: true), ViewVariables(VVAccess.ReadWrite)]
public uint BottleProductionLimit;
[DataField("pillDosageLimit", required: true), ViewVariables(VVAccess.ReadWrite)]
public uint PillDosageLimit;
[DataField("clickSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");

View File

@@ -1,16 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Labels.Components;
using Content.Server.Popups;
using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Chemistry.EntitySystems
@@ -28,7 +32,9 @@ namespace Content.Server.Chemistry.EntitySystems
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly StorageSystem _storageSystem = default!;
private const string PillPrototypeId = "Pill";
public override void Initialize()
{
@@ -44,33 +50,26 @@ namespace Content.Server.Chemistry.EntitySystems
SubscribeLocalEvent<ChemMasterComponent, ChemMasterSetPillTypeMessage>(OnSetPillTypeMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterReagentAmountButtonMessage>(OnReagentButtonMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterCreatePillsMessage>(OnCreatePillsMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterCreateBottlesMessage>(OnCreateBottlesMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterOutputToBottleMessage>(OnOutputToBottleMessage);
}
private void UpdateUiState(ChemMasterComponent chemMaster, bool updateLabel = false)
{
if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out var bufferSolution))
return;
var container = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.ContainerSlotName);
var inputContainer = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.InputSlotName);
var outputContainer = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.OutputSlotName);
Solution? containerSolution = null;
if (container.HasValue && container.Value.Valid)
{
TryComp(container, out FitsInDispenserComponent? fits);
_solutionContainerSystem.TryGetSolution(container.Value, fits!.Solution, out containerSolution);
}
var containerName = container is null ? null : Name(container.Value);
var dispenserName = Name(chemMaster.Owner);
var bufferReagents = bufferSolution.Contents;
var bufferCurrentVolume = bufferSolution.CurrentVolume;
var state = new ChemMasterBoundUserInterfaceState(
containerSolution?.CurrentVolume, containerSolution?.MaxVolume, containerName, dispenserName,
containerSolution?.Contents, bufferReagents, chemMaster.Mode, bufferCurrentVolume, chemMaster.PillType,
chemMaster.PillProductionLimit, chemMaster.BottleProductionLimit, updateLabel
);
chemMaster.Mode, dispenserName,
BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer),
bufferReagents, bufferCurrentVolume,
chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel);
_userInterfaceSystem.TrySetUiState(chemMaster.Owner, ChemMasterUiKey.Key, state);
}
@@ -120,7 +119,7 @@ namespace Content.Server.Chemistry.EntitySystems
private void TransferReagents(ChemMasterComponent chemMaster, string reagentId, FixedPoint2 amount, bool fromBuffer)
{
var container = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.ContainerSlotName);
var container = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.InputSlotName);
if (container is null ||
!_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution) ||
!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out var bufferSolution))
@@ -156,7 +155,7 @@ namespace Content.Server.Chemistry.EntitySystems
}
else
{
var container = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.ContainerSlotName);
var container = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.InputSlotName);
if (container is not null &&
_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution))
{
@@ -171,79 +170,172 @@ namespace Content.Server.Chemistry.EntitySystems
private void OnCreatePillsMessage(EntityUid uid, ChemMasterComponent chemMaster, ChemMasterCreatePillsMessage message)
{
// Ensure the amount is valid.
if (message.Amount == 0 || message.Amount > chemMaster.PillProductionLimit)
var user = message.Session.AttachedEntity;
var maybeContainer = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.OutputSlotName);
if (maybeContainer is not { Valid: true } container
|| !TryComp(container, out ServerStorageComponent? storage)
|| storage.Storage is null)
{
return; // output can't fit pills
}
// Ensure the number is valid.
if (message.Number == 0 || message.Number > storage.StorageCapacityMax - storage.StorageUsed)
return;
CreatePillsOrBottles(chemMaster, pills: true, message.Amount, message.Label, message.Session.AttachedEntity);
// Ensure the amount is valid.
if (message.Dosage == 0 || message.Dosage > chemMaster.PillDosageLimit)
return;
var needed = message.Dosage * message.Number;
if (!WithdrawFromBuffer(chemMaster, needed, user, out var withdrawal))
return;
Label(container, message.Label);
for (var i = 0; i < message.Number; i++)
{
var item = Spawn(PillPrototypeId, Transform(container).Coordinates);
DebugTools.Assert(_storageSystem.Insert(container, item, storage));
Label(item, message.Label);
var itemSolution = _solutionContainerSystem.EnsureSolution(item, SharedChemMaster.PillSolutionName);
DebugTools.Assert(_solutionContainerSystem.TryAddSolution(
item, itemSolution, withdrawal.SplitSolution(message.Dosage)));
if (TryComp<SpriteComponent>(item, out var spriteComp))
spriteComp.LayerSetState(0, "pill" + (chemMaster.PillType + 1));
}
UpdateUiState(chemMaster);
ClickSound(chemMaster);
}
private void OnCreateBottlesMessage(EntityUid uid, ChemMasterComponent chemMaster, ChemMasterCreateBottlesMessage message)
private void OnOutputToBottleMessage(
EntityUid uid, ChemMasterComponent chemMaster, ChemMasterOutputToBottleMessage message)
{
var user = message.Session.AttachedEntity;
var maybeContainer = _itemSlotsSystem.GetItem(chemMaster.Owner, SharedChemMaster.OutputSlotName);
if (maybeContainer is not { Valid: true } container
|| !_solutionContainerSystem.TryGetSolution(
container, SharedChemMaster.BottleSolutionName, out var solution))
{
return; // output can't fit reagents
}
// Ensure the amount is valid.
if (message.Amount == 0 || message.Amount > chemMaster.BottleProductionLimit)
if (message.Dosage == 0 || message.Dosage > solution.AvailableVolume)
return;
CreatePillsOrBottles(chemMaster, pills: false, message.Amount, message.Label, message.Session.AttachedEntity);
if (!WithdrawFromBuffer(chemMaster, message.Dosage, user, out var withdrawal))
return;
Label(container, message.Label);
DebugTools.Assert(_solutionContainerSystem.TryAddSolution(
container, solution, withdrawal));
UpdateUiState(chemMaster);
ClickSound(chemMaster);
}
private void CreatePillsOrBottles(ChemMasterComponent chemMaster, bool pills, FixedPoint2 amount, string label, EntityUid? user)
private bool WithdrawFromBuffer(
IComponent chemMaster,
FixedPoint2 neededVolume, EntityUid? user,
[NotNullWhen(returnValue: true)] out Solution? outputSolution)
{
if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out var bufferSolution))
return;
outputSolution = null;
if (!_solutionContainerSystem.TryGetSolution(
chemMaster.Owner, SharedChemMaster.BufferSolutionName, out var solution))
{
return false;
}
var filter = user.HasValue ? Filter.Entities(user.Value) : Filter.Empty();
if (bufferSolution.TotalVolume == 0)
if (solution.TotalVolume == 0)
{
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), filter);
return;
return false;
}
var individualVolume = FixedPoint2.Min(bufferSolution.TotalVolume / amount, FixedPoint2.New(pills ? 50 : 30));
if (individualVolume < FixedPoint2.New(1))
// ReSharper disable once InvertIf
if (neededVolume > solution.CurrentVolume)
{
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), filter);
return false;
}
outputSolution = solution.SplitSolution(neededVolume);
return true;
}
private void Label(EntityUid ent, string label)
{
if (string.IsNullOrEmpty(label))
return;
}
var labelComponent = EnsureComp<LabelComponent>(ent);
for (int i = 0; i < amount; i++)
{
var item = Spawn(pills ? "Pill" : "ChemistryEmptyBottle01", Transform(chemMaster.Owner).Coordinates);
if (label != "")
{
var labelComponent = EnsureComp<LabelComponent>(item);
labelComponent.OriginalName = Name(item);
string val = Name(item) + $" ({label})";
Comp<MetaDataComponent>(item).EntityName = val;
labelComponent.CurrentLabel = label;
}
var solution = bufferSolution.SplitSolution(individualVolume);
var itemSolution = _solutionContainerSystem.EnsureSolution(item,
pills ? SharedChemMaster.PillSolutionName : SharedChemMaster.BottleSolutionName);
_solutionContainerSystem.TryAddSolution(item, itemSolution, solution);
if (pills)
{
if (TryComp<SpriteComponent>(item, out var spriteComp))
spriteComp.LayerSetState(0, "pill" + (chemMaster.PillType + 1));
}
if (user.HasValue)
_handsSystem.PickupOrDrop(user, item);
}
UpdateUiState(chemMaster);
labelComponent.OriginalName = Name(ent);
var val = Name(ent) + $" ({label})";
MetaData(ent).EntityName = val;
labelComponent.CurrentLabel = label;
}
private void ClickSound(ChemMasterComponent chemMaster)
{
_audioSystem.Play(chemMaster.ClickSound, Filter.Pvs(chemMaster.Owner), chemMaster.Owner, AudioParams.Default.WithVolume(-2f));
}
private ContainerInfo? BuildInputContainerInfo(EntityUid? container)
{
if (container is not { Valid: true })
return null;
if (!TryComp(container, out FitsInDispenserComponent? fits)
|| !_solutionContainerSystem.TryGetSolution(container.Value, fits.Solution, out var solution))
{
return null;
}
return BuildContainerInfo(Name(container.Value), solution);
}
private ContainerInfo? BuildOutputContainerInfo(EntityUid? container)
{
if (container is not { Valid: true })
return null;
var name = Name(container.Value);
{
if (_solutionContainerSystem.TryGetSolution(
container.Value, SharedChemMaster.BottleSolutionName, out var solution))
{
return BuildContainerInfo(name, solution);
}
}
if (!TryComp(container, out ServerStorageComponent? storage))
return null;
var pills = storage.Storage?.ContainedEntities.Select(pill =>
{
_solutionContainerSystem.TryGetSolution(pill, SharedChemMaster.PillSolutionName, out var solution);
var quantity = solution?.CurrentVolume ?? FixedPoint2.Zero;
return (Name(pill), quantity);
}).ToList();
return pills is null
? null
: new ContainerInfo(name, false, storage.StorageUsed, storage.StorageCapacityMax, pills);
}
private static ContainerInfo BuildContainerInfo(string name, Solution solution)
{
var reagents = solution.Contents
.Select(reagent => (reagent.ReagentId, reagent.Quantity)).ToList();
return new ContainerInfo(name, true, solution.CurrentVolume, solution.MaxVolume, reagents);
}
}
}

View File

@@ -2,11 +2,13 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Chemistry.Components
{
/// <summary>
/// Allows the entity with this component to be placed in a <c>SharedReagentDispenserComponent</c>.
/// <para>Otherwise it's considered to be too large or the improper shape to fit.</para>
/// <para>Allows us to have obscenely large containers that are harder to abuse in chem dispensers
/// since they can't be placed directly in them.</para>
/// <see cref="Dispenser.SharedReagentDispenserComponent"/>
/// </summary>
[RegisterComponent]
[NetworkedComponent] // only needed for white-lists. Client doesn't actually need Solution data;
public sealed class FitsInDispenserComponent : Component

View File

@@ -11,7 +11,8 @@ namespace Content.Shared.Chemistry
{
public const uint PillTypes = 20;
public const string BufferSolutionName = "buffer";
public const string ContainerSlotName = "beakerSlot";
public const string InputSlotName = "beakerSlot";
public const string OutputSlotName = "outputSlot";
public const string PillSolutionName = "food";
public const string BottleSolutionName = "drink";
}
@@ -56,25 +57,27 @@ namespace Content.Shared.Chemistry
[Serializable, NetSerializable]
public sealed class ChemMasterCreatePillsMessage : BoundUserInterfaceMessage
{
public readonly uint Amount;
public readonly uint Dosage;
public readonly uint Number;
public readonly string Label;
public ChemMasterCreatePillsMessage(uint amount, string label)
public ChemMasterCreatePillsMessage(uint dosage, uint number, string label)
{
Amount = amount;
Dosage = dosage;
Number = number;
Label = label;
}
}
[Serializable, NetSerializable]
public sealed class ChemMasterCreateBottlesMessage : BoundUserInterfaceMessage
public sealed class ChemMasterOutputToBottleMessage : BoundUserInterfaceMessage
{
public readonly uint Amount;
public readonly uint Dosage;
public readonly string Label;
public ChemMasterCreateBottlesMessage(uint amount, string label)
public ChemMasterOutputToBottleMessage(uint dosage, string label)
{
Amount = amount;
Dosage = dosage;
Label = label;
}
}
@@ -105,17 +108,53 @@ namespace Content.Shared.Chemistry
}
}
/// <summary>
/// Information about the capacity and contents of a container for display in the UI
/// </summary>
[Serializable, NetSerializable]
public sealed class ContainerInfo
{
/// <summary>
/// The container name to show to the player
/// </summary>
public readonly string DisplayName;
/// <summary>
/// Whether the container holds reagents or entities
/// </summary>
public readonly bool HoldsReagents;
/// <summary>
/// The currently used volume of the container
/// </summary>
public readonly FixedPoint2 CurrentVolume;
/// <summary>
/// The maximum volume of the container
/// </summary>
public readonly FixedPoint2 MaxVolume;
/// <summary>
/// A list of the reagents/entities and their sizes within the container
/// </summary>
// todo: this causes NetSerializer exceptions if it's an IReadOnlyList (which would be preferred)
public readonly List<(string Id, FixedPoint2 Quantity)> Contents;
public ContainerInfo(
string displayName, bool holdsReagents,
FixedPoint2 currentVolume, FixedPoint2 maxVolume,
List<(string, FixedPoint2)> contents)
{
DisplayName = displayName;
HoldsReagents = holdsReagents;
CurrentVolume = currentVolume;
MaxVolume = maxVolume;
Contents = contents;
}
}
[Serializable, NetSerializable]
public sealed class ChemMasterBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly FixedPoint2? ContainerCurrentVolume;
public readonly FixedPoint2? ContainerMaxVolume;
public readonly string? ContainerName;
public readonly ContainerInfo? InputContainerInfo;
public readonly ContainerInfo? OutputContainerInfo;
/// <summary>
/// A list of the reagents and their amounts within the beaker/reagent container, if applicable.
/// </summary>
public readonly IReadOnlyList<Solution.ReagentQuantity>? ContainerReagents;
/// <summary>
/// A list of the reagents and their amounts within the buffer, if applicable.
/// </summary>
@@ -127,33 +166,26 @@ namespace Content.Shared.Chemistry
public readonly FixedPoint2? BufferCurrentVolume;
public readonly uint SelectedPillType;
public readonly uint PillProductionLimit;
public readonly uint BottleProductionLimit;
public readonly uint PillDosageLimit;
public readonly bool UpdateLabel;
public ChemMasterBoundUserInterfaceState(FixedPoint2? containerCurrentVolume, FixedPoint2? containerMaxVolume, string? containerName,
string dispenserName, IReadOnlyList<Solution.ReagentQuantity>? containerReagents, IReadOnlyList<Solution.ReagentQuantity> bufferReagents, ChemMasterMode mode,
FixedPoint2 bufferCurrentVolume, uint selectedPillType, uint pillProdictionLimit, uint bottleProdictionLimit, bool updateLabel)
public ChemMasterBoundUserInterfaceState(
ChemMasterMode mode, string dispenserName,
ContainerInfo? inputContainerInfo, ContainerInfo? outputContainerInfo,
IReadOnlyList<Solution.ReagentQuantity> bufferReagents, FixedPoint2 bufferCurrentVolume,
uint selectedPillType, uint pillDosageLimit, bool updateLabel)
{
ContainerCurrentVolume = containerCurrentVolume;
ContainerMaxVolume = containerMaxVolume;
ContainerName = containerName;
InputContainerInfo = inputContainerInfo;
OutputContainerInfo = outputContainerInfo;
DispenserName = dispenserName;
ContainerReagents = containerReagents;
BufferReagents = bufferReagents;
Mode = mode;
BufferCurrentVolume = bufferCurrentVolume;
SelectedPillType = selectedPillType;
PillProductionLimit = pillProdictionLimit;
BottleProductionLimit = bottleProdictionLimit;
PillDosageLimit = pillDosageLimit;
UpdateLabel = updateLabel;
}
public bool HasContainer()
{
return ContainerCurrentVolume is not null;
}
}
[Serializable, NetSerializable]

View File

@@ -8,6 +8,8 @@ chem-master-bound-user-interface-title = ChemMaster 4000
## UI
chem-master-window-input-tab = Input
chem-master-window-output-tab = Output
chem-master-window-container-label = Container
chem-master-window-eject-button = Eject
chem-master-window-no-container-loaded-text = No container loaded.
@@ -22,9 +24,8 @@ chem-master-window-packaging-text = Packaging
chem-master-current-text-label = Label:
chem-master-window-pills-label = Pills:
chem-master-window-pill-type-label = Pill type:
chem-master-window-max-pills-volume-text = max 50u/each
chem-master-window-max-bottles-volume-text = max 30u/each
chem-master-window-create-pill-button = Create
chem-master-window-create-bottle-button = Create
chem-master-window-pills-number-label = Count:
chem-master-window-dose-label = Dose (u):
chem-master-window-create-button = Create
chem-master-window-bottles-label = Bottles:
chem-master-window-unknown-reagent-text = Unknown reagent

View File

@@ -18,8 +18,7 @@
sprite: Structures/Machines/mixer.rsi
state: mixer_loaded
- type: ChemMaster
pillProductionLimit: 10
bottleProductionLimit: 10
pillDosageLimit: 50
- type: Physics
bodyType: Static
- type: Fixtures
@@ -61,15 +60,21 @@
- type: ContainerContainer
containers:
beakerSlot: !type:ContainerSlot
outputSlot: !type:ContainerSlot
machine_board: !type:Container
machine_parts: !type:Container
- type: ItemSlots
slots:
beakerSlot:
whitelistFailPopup: chem-master-component-cannot-put-entity-message
whitelist:
components:
- FitsInDispenser
outputSlot:
whitelistFailPopup: chem-master-component-cannot-put-entity-message
whitelist:
tags:
- Bottle
- PillCanister
- type: SolutionContainerManager
solutions:
buffer: {}