From 7a100e498a4e9e959d7cc3a96fc22ea1143c19e1 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Fri, 6 Oct 2023 01:17:39 +0530 Subject: [PATCH] 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 :cl: 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 /:cl: --------- Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com> --- .../plumbing/plumbers/reaction_chamber.dm | 129 ++++++++++++------ ...MixingChamber.js => ChemMixingChamber.tsx} | 51 ++++--- ...tionChamber.js => ChemReactionChamber.tsx} | 25 ++-- 3 files changed, 129 insertions(+), 76 deletions(-) rename tgui/packages/tgui/interfaces/{ChemMixingChamber.js => ChemMixingChamber.tsx} (86%) rename tgui/packages/tgui/interfaces/{ChemReactionChamber.js => ChemReactionChamber.tsx} (93%) diff --git a/code/modules/plumbing/plumbers/reaction_chamber.dm b/code/modules/plumbing/plumbers/reaction_chamber.dm index 689d043418b..36320f18184 100644 --- a/code/modules/plumbing/plumbers/reaction_chamber.dm +++ b/code/modules/plumbing/plumbers/reaction_chamber.dm @@ -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 diff --git a/tgui/packages/tgui/interfaces/ChemMixingChamber.js b/tgui/packages/tgui/interfaces/ChemMixingChamber.tsx similarity index 86% rename from tgui/packages/tgui/interfaces/ChemMixingChamber.js rename to tgui/packages/tgui/interfaces/ChemMixingChamber.tsx index eeffd351387..30a41de8080 100644 --- a/tgui/packages/tgui/interfaces/ChemMixingChamber.js +++ b/tgui/packages/tgui/interfaces/ChemMixingChamber.tsx @@ -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(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 ( @@ -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) => { - setReagentName(value)} +