Plumbing chemical chamber UI and operation improvements (#78619)

## About The Pull Request
1. Fixes #58119

The word "Fixes" actually means the reaction chamber does it's job but
"**does not guarantee**" the ph & temperature of the final solution is
exactly within the ranges you specify especially for instantaneous
reactions. There were 2 problems with the reaction chamber.

a) **The ph chemistry of the reaction chamber was completely flipped
upside down.**
      Let's look at the code

https://github.com/tgstation/tgstation/blob/cfe13015203480dafaf3fce1589ad6045e708442/code/modules/plumbing/plumbers/reaction_chamber.dm#L152-L155

This was adding basic buffer when the solution was getting to basic &
acid buffer when the solution was getting too acidic

      The correct mode of operation is as follows
- If the ph is getting way too **acidic** i.e. it's ph **decreases** and
becomes **lower** than the acidic limit then we need to add some **base
buffer** so as to **increase** it's value and bring it back **above**
the acidic limit
- If the ph is getting way to **basic** i.e. it's ph **increases** and
becomes **higher** than the alkaline limit then we need to add some
**acidic buffer** so as to **decrease** it's value and bring it back
**below** the alkaline limit

    b) **The reaction chamber did it's work only half of the time**
   Looking at the code

https://github.com/tgstation/tgstation/blob/cfe13015203480dafaf3fce1589ad6045e708442/code/modules/plumbing/plumbers/reaction_chamber.dm#L152

The reaction chamber would only balance the ph of the solution **ONLY**
when an reaction was taking place i.e. only when `reagents.is_reacting`
is true. It would not attempt to balance the ph of the reagents as and
when they were coming in i.e when `emptying` is FALSE. This means if an
reaction took only like 0.5 to 1 second to occur the ph would be
balanced only during that very short interval of time and so it would
not make much of difference.

So the 2 problems were the chamber was doing its job wrong and would do
it only half the time. Again re-emphasizing even with these patches the
chamber "**does not guarantee**" the final solution ph will be within
those ranges. This is because the nature of reactions is unpredictable.
Some reactions can take place so fast and have huge drastic ph changes
that the reaction chamber would be ineffective regardless of how much
base/acid it used to balance the reaction. The only thing is that it
does it's job correctly and too the best of its ability with no
guarentees.

3. The plumbing mixing chamber(parent type of the plumbing chemical
reaction chamber) which you make via research plumbing RCD, UI now also
uses an TGUI input list to accept chemicals so no more manual typing.

4. Both these types have their UI files converted Typescript cause why
not

## Changelog
🆑
fix: plumbing reaction chamber now balances the ph of it's solution
correctly to the best of it's ability so no guarantees
code: converted plumbing reaction chamber & mixing chamber UI files to
Typescript
refactor: plumbing mixing chamber now also accepts an TGUI input list to
input it's chemicals
/🆑

---------

Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
This commit is contained in:
SyncIt21
2023-10-05 21:47:39 +02:00
committed by GitHub
co-authored by Time-Green
parent 3ed187e86b
commit 7a100e498a
3 changed files with 129 additions and 76 deletions
@@ -1,5 +1,8 @@
///a reaction chamber for plumbing. pretty much everything can react, but this one keeps the reagents separated and only reacts under your given terms
/// coefficient to convert temperature to joules. same lvl as acclimator
#define HEATER_COEFFICIENT 0.05
/obj/machinery/plumbing/reaction_chamber
name = "mixing chamber"
desc = "Keeps chemicals separated until given conditions are met."
@@ -19,9 +22,6 @@
///towards which temperature do we build (except during draining)?
var/target_temperature = 300
///cool/heat power
var/heater_coefficient = 0.05 //same lvl as acclimator
/obj/machinery/plumbing/reaction_chamber/Initialize(mapload, bolt, layer)
. = ..()
@@ -35,30 +35,48 @@
/// Handles properly detaching signal hooks.
/obj/machinery/plumbing/reaction_chamber/proc/on_reagents_del(datum/reagents/reagents)
SIGNAL_HANDLER
UnregisterSignal(reagents, list(COMSIG_REAGENTS_REM_REAGENT, COMSIG_REAGENTS_DEL_REAGENT, COMSIG_REAGENTS_CLEAR_REAGENTS, COMSIG_REAGENTS_REACTED, COMSIG_QDELETING))
return NONE
/// Handles stopping the emptying process when the chamber empties.
/obj/machinery/plumbing/reaction_chamber/proc/on_reagent_change(datum/reagents/holder, ...)
SIGNAL_HANDLER
if(holder.total_volume == 0 && emptying) //we were emptying, but now we aren't
if(!holder.total_volume && emptying) //we were emptying, but now we aren't
emptying = FALSE
holder.flags |= NO_REACT
return NONE
/obj/machinery/plumbing/reaction_chamber/process(seconds_per_tick)
if(!emptying || reagents.is_reacting) //suspend heating/cooling during emptying phase
reagents.adjust_thermal_energy((target_temperature - reagents.chem_temp) * heater_coefficient * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater
//half the power for getting reagents in
var/power_usage = active_power_usage * 0.5
if(!emptying || reagents.is_reacting)
//do reactions and stuff
reagents.handle_reactions()
use_power(active_power_usage * seconds_per_tick)
//adjust temperature of final solution
var/temp_diff = target_temperature - reagents.chem_temp
if(abs(temp_diff) > 0.01) //if we are not close enough keep going
reagents.adjust_thermal_energy(temp_diff * HEATER_COEFFICIENT * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater
//do other stuff with final solution
handle_reagents(seconds_per_tick)
//full power for doing reactions
power_usage *= 2
use_power(power_usage * seconds_per_tick)
///For subtypes that want to do additional reagent handling
/obj/machinery/plumbing/reaction_chamber/proc/handle_reagents(seconds_per_tick)
return
/obj/machinery/plumbing/reaction_chamber/power_change()
. = ..()
if(use_power != NO_POWER_USE)
icon_state = initial(icon_state) + "_on"
else
icon_state = initial(icon_state)
icon_state = initial(icon_state) + "[use_power != NO_POWER_USE ? "_on" : ""]"
/obj/machinery/plumbing/reaction_chamber/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -67,31 +85,29 @@
ui.open()
/obj/machinery/plumbing/reaction_chamber/ui_data(mob/user)
var/list/data = list()
. = list()
var/list/reagents_data = list()
for(var/datum/reagent/required_reagent as anything in required_reagents) //make a list where the key is text, because that looks alot better in the ui than a typepath
var/list/reagent_data = list()
reagent_data["name"] = initial(required_reagent.name)
reagent_data["required_reagent"] = required_reagents[required_reagent]
reagent_data["volume"] = required_reagents[required_reagent]
reagents_data += list(reagent_data)
data["reagents"] = reagents_data
data["emptying"] = emptying
data["temperature"] = round(reagents.chem_temp, 0.1)
data["targetTemp"] = target_temperature
data["isReacting"] = reagents.is_reacting
return data
.["reagents"] = reagents_data
.["emptying"] = emptying
.["temperature"] = round(reagents.chem_temp, 0.1)
.["targetTemp"] = target_temperature
.["isReacting"] = reagents.is_reacting
/obj/machinery/plumbing/reaction_chamber/ui_act(action, params)
/obj/machinery/plumbing/reaction_chamber/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return TRUE
. = FALSE
switch(action)
if("add")
var/selected_reagent = tgui_input_list(usr, "Select reagent", "Reagent", GLOB.chemical_name_list)
var/selected_reagent = tgui_input_list(ui.user, "Select reagent", "Reagent", GLOB.chemical_name_list)
if(!selected_reagent)
return TRUE
@@ -104,32 +120,41 @@
if(input_amount)
required_reagents[input_reagent] = input_amount
. = TRUE
return TRUE
if("remove")
var/reagent = get_chem_id(params["chem"])
if(reagent)
required_reagents.Remove(reagent)
. = TRUE
return TRUE
if("temperature")
var/target = text2num(params["target"])
if(target != null)
target_temperature=clamp(target, 0, 1000)
.=TRUE
target_temperature = clamp(target, 0, 1000)
return TRUE
var/result = handle_ui_act(action, params, ui, state)
if(isnull(result))
result = FALSE
return result
/// For custom handling of ui actions from inside a subtype
/obj/machinery/plumbing/reaction_chamber/proc/handle_ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
return null
///Chemistry version of reaction chamber that allows for acid and base buffers to be used while reacting
/obj/machinery/plumbing/reaction_chamber/chem
name = "reaction chamber"
///If above this pH, we start dumping buffer into it
var/acidic_limit = 9
///If below this pH, we start dumping buffer into it
var/alkaline_limit = 5
var/acidic_limit = 5
///If above this pH, we start dumping acid into it
var/alkaline_limit = 9
///Beaker that holds the acidic buffer. I don't want to deal with snowflaking so it's just a separate thing. It's a small (50u) beaker
///beaker that holds the acidic buffer(50u)
var/obj/item/reagent_containers/cup/beaker/acidic_beaker
///beaker that holds the alkaline buffer.
///beaker that holds the alkaline buffer(50u).
var/obj/item/reagent_containers/cup/beaker/alkaline_beaker
/obj/machinery/plumbing/reaction_chamber/chem/Initialize(mapload, bolt, layer)
@@ -147,13 +172,27 @@
QDEL_NULL(alkaline_beaker)
return ..()
/obj/machinery/plumbing/reaction_chamber/chem/process(seconds_per_tick)
//add acidic/alkaine buffer if over/under limit
if(reagents.is_reacting && reagents.ph < alkaline_limit)
alkaline_beaker.reagents.trans_to(reagents, 1 * seconds_per_tick)
if(reagents.is_reacting && reagents.ph > acidic_limit)
acidic_beaker.reagents.trans_to(reagents, 1 * seconds_per_tick)
..()
/obj/machinery/plumbing/reaction_chamber/chem/handle_reagents(seconds_per_tick)
while(reagents.ph < acidic_limit || reagents.ph > alkaline_limit)
if(machine_stat & NOPOWER)
return
/**
* figure out which buffer to transfer to restore balance
* if solution is getting too basic(high ph) add some acid to lower it's value
* else if solution is getting too acidic(low ph) add some base to increase it's value
*/
var/datum/reagents/buffer = reagents.ph > alkaline_limit ? acidic_beaker.reagents : alkaline_beaker.reagents
if(!buffer.total_volume)
return
//transfer buffer and handle reactions, not a proven math but looks logical
var/transfer_amount = FLOOR((reagents.ph > alkaline_limit ? (reagents.ph - alkaline_limit) : (acidic_limit - reagents.ph)) * seconds_per_tick, CHEMICAL_QUANTISATION_LEVEL)
if(transfer_amount <= CHEMICAL_QUANTISATION_LEVEL || !buffer.trans_to(reagents, transfer_amount))
return
//some power for accurate ph balancing
use_power(active_power_usage * 0.2 * seconds_per_tick)
/obj/machinery/plumbing/reaction_chamber/chem/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -167,16 +206,16 @@
.["reagentAcidic"] = acidic_limit
.["reagentAlkaline"] = alkaline_limit
/obj/machinery/plumbing/reaction_chamber/chem/ui_act(action, params)
. = ..()
if (.)
return
/obj/machinery/plumbing/reaction_chamber/chem/handle_ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = TRUE
switch(action)
if("acidic")
acidic_limit = round(text2num(params["target"]))
acidic_limit = clamp(round(text2num(params["target"])), 0, alkaline_limit)
if("alkaline")
alkaline_limit = round(text2num(params["target"]))
alkaline_limit = clamp(round(text2num(params["target"])), acidic_limit + 0.01, 14)
else
return FALSE
return TRUE
#undef HEATER_COEFFICIENT
@@ -1,10 +1,24 @@
import { useBackend, useLocalState } from '../backend';
import { AnimatedNumber, Box, Button, Input, NumberInput, Section, Stack } from '../components';
import { AnimatedNumber, Box, Button, NumberInput, Section, Stack } from '../components';
import { Window } from '../layouts';
import { round, toFixed } from 'common/math';
import { BooleanLike } from 'common/react';
type Reagent = {
name: string;
volume: number;
};
export type MixingData = {
reagents: Reagent[];
emptying: BooleanLike;
temperature: number;
targetTemp: number;
isReacting: BooleanLike;
};
export const ChemMixingChamber = (props, context) => {
const { act, data } = useBackend(context);
const { act, data } = useBackend<MixingData>(context);
const [reagentName, setReagentName] = useLocalState(
context,
@@ -17,7 +31,7 @@ export const ChemMixingChamber = (props, context) => {
1
);
const { emptying, temperature, ph, targetTemp, isReacting } = data;
const { emptying, temperature, targetTemp, isReacting } = data;
const reagents = data.reagents || [];
return (
<Window width={290} height={400}>
@@ -35,7 +49,7 @@ export const ChemMixingChamber = (props, context) => {
unit="K"
step={10}
stepPixelSize={3}
value={round(targetTemp)}
value={round(targetTemp, 0.1)}
minValue={0}
maxValue={1000}
onDrag={(e, value) =>
@@ -88,11 +102,15 @@ export const ChemMixingChamber = (props, context) => {
<Stack.Item>
<Stack fill>
<Stack.Item grow>
<Input
fluid
value=""
placeholder="Reagent Name"
onInput={(e, value) => setReagentName(value)}
<Button
content="Add Reagent"
color="good"
icon="plus"
onClick={() =>
act('add', {
amount: reagentQuantity,
})
}
/>
</Stack.Item>
<Stack.Item>
@@ -107,29 +125,18 @@ export const ChemMixingChamber = (props, context) => {
/>
<Box inline mr={1} />
</Stack.Item>
<Stack.Item>
<Button
icon="plus"
onClick={() =>
act('add', {
chem: reagentName,
amount: reagentQuantity,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack vertical>
{reagents.map((reagent) => (
<Stack.Item key={reagent}>
<Stack.Item key={reagent.name}>
<Stack fill>
<Stack.Item mt={0.25} textColor="label">
{reagent.name + ':'}
</Stack.Item>
<Stack.Item mt={0.25} grow>
{reagent.required_reagent}
{reagent.volume}
</Stack.Item>
<Stack.Item>
<Button
@@ -2,9 +2,16 @@ import { useBackend, useLocalState } from '../backend';
import { AnimatedNumber, Box, Button, LabeledList, NumberInput, Section, RoundGauge, Stack } from '../components';
import { Window } from '../layouts';
import { round, toFixed } from 'common/math';
import { MixingData } from './ChemMixingChamber';
type ReactingData = MixingData & {
ph: number;
reagentAcidic: number;
reagentAlkaline: number;
};
export const ChemReactionChamber = (props, context) => {
const { act, data } = useBackend(context);
const { act, data } = useBackend<ReactingData>(context);
const [reagentQuantity, setReagentQuantity] = useLocalState(
context,
@@ -38,7 +45,7 @@ export const ChemReactionChamber = (props, context) => {
unit="K"
step={10}
stepPixelSize={3}
value={round(targetTemp)}
value={round(targetTemp, 0.1)}
minValue={0}
maxValue={1000}
onDrag={(e, value) =>
@@ -96,7 +103,7 @@ export const ChemReactionChamber = (props, context) => {
<Button
color="transparent"
icon="question"
tooltip={multiline`
tooltip={`
In chemistry, pH is a scale used to specify
the acidity or basicity of an aqueous solution.
Acidic solutions are measured to have lower
@@ -138,8 +145,8 @@ export const ChemReactionChamber = (props, context) => {
<LabeledList.Item label="Acidic pH limit">
<NumberInput
value={reagentAcidic}
minValue={-1000}
maxValue={1000}
minValue={0}
maxValue={14}
step={1}
stepPixelSize={3}
width="39px"
@@ -153,8 +160,8 @@ export const ChemReactionChamber = (props, context) => {
<LabeledList.Item label="Alkaline pH limit">
<NumberInput
value={reagentAlkaline}
minValue={-1000}
maxValue={1000}
minValue={0}
maxValue={14}
step={1}
stepPixelSize={3}
width="39px"
@@ -199,13 +206,13 @@ export const ChemReactionChamber = (props, context) => {
<Stack.Item>
<Stack vertical>
{reagents.map((reagent) => (
<Stack.Item key={reagent}>
<Stack.Item key={reagent.name}>
<Stack fill>
<Stack.Item mt={0.25} textColor="label">
{reagent.name + ':'}
</Stack.Item>
<Stack.Item mt={0.25} grow>
{reagent.required_reagent}
{reagent.volume}
</Stack.Item>
<Stack.Item>
<Button