Files
Bubberstation/code/modules/reagents/chemistry/equilibrium.dm
T
7d1d0e1fad [MIRROR] Refactors most spans into span procs (#6315)
* Refactors most spans into span procs

* AA

* a

* AAAAAAAAAAAAAAAAAAAAAA

* Update species.dm

Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
Co-authored-by: Gandalf <jzo123@hotmail.com>
2021-06-16 00:24:49 +01:00

431 lines
21 KiB
Plaintext

/*
* #/datum/equilibrium
*
* A dynamic reaction object that processes the reaction that it is set within it. Relies on a reagents holder to call and operate the functions.
*
* An object/datum to contain the vars for each of the reactions currently ongoing in a holder/reagents datum
* This way all information is kept within one accessable object
* equilibrium is a unique name as reaction is already too close to chemical_reaction
* This is set up this way to reduce holder.dm bloat as well as reduce confusing list overhead
* The crux of the fermimechanics are handled here
* Instant reactions AREN'T handled here. See holder.dm
*/
/datum/equilibrium
///The chemical reaction that is presently being processed
var/datum/chemical_reaction/reaction
///The location/reagents datum the processing is taking place
var/datum/reagents/holder
///How much product we can make multiplied by the input recipe's products/required_reagents numerical values
var/multiplier = INFINITY
///The sum total of each of the product's numerical's values. This is so the addition/deletion is kept at the right values for multiple product reactions
var/product_ratio = 0
///The total possible that this reaction can make presently - used for gui outputs
var/target_vol = 0
///The target volume the reaction is headed towards. This is updated every tick, so isn't the total value for the reaction, it's just a way to ensure we can't make more than is possible.
var/step_target_vol = INFINITY
///How much of the reaction has been made so far. Mostly used for subprocs, but it keeps track across the whole reaction and is added to every step.
var/reacted_vol = 0
///What our last delta_ph was
var/reaction_quality = 1
///If we're done with this reaction so that holder can clear it.
var/to_delete = FALSE
///Result vars, private - do not edit unless in reaction_step()
///How much we're adding
var/delta_t
///How pure our step is
var/delta_ph
///Modifiers from catalysts, do not use negative numbers.
///I should write a better handiler for modifying these
///Speed mod
var/speed_mod = 1
///pH mod
var/h_ion_mod = 1
///Temp mod
var/thermic_mod = 1
///Allow us to deal with lag by "charging" up our reactions to react faster over a period - this means that the reaction doesn't suddenly mass react - which can cause explosions
var/time_deficit
///Used to store specific data needed for a reaction, usually used to keep track of things between explosion calls. CANNOT be used as a part of chemical_recipe - those vars are static lookup tables.
var/data = list()
/*
* Creates and sets up a new equlibrium object
*
* Arguments:
* * input_reaction - the chemical_reaction datum that will be processed
* * input_holder - the reagents datum that the output will be put into
*/
/datum/equilibrium/New(datum/chemical_reaction/input_reaction, datum/reagents/input_holder)
reaction = input_reaction
holder = input_holder
if(!holder || !reaction) //sanity check
stack_trace("A new [type] was set up, with incorrect/null input vars!")
to_delete = TRUE
return
if(!check_inital_conditions()) //If we're outside of the scope of the reaction vars
to_delete = TRUE
return
if(!length(reaction.results)) //Come back to and revise the affected reactions in the next PR, this is a placeholder fix.
holder.instant_react(reaction) //Even if this check fails, there's a backup - look inside of calculate_yield()
to_delete = TRUE
return
LAZYADD(holder.reaction_list, src)
SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] attempts")
/datum/equilibrium/Destroy()
if(reacted_vol < target_vol) //We did NOT finish from reagents - so we can restart this reaction given property changes in the beaker. (i.e. if it stops due to low temp, this will allow it to fast restart when heated up again)
LAZYADD(holder.failed_but_capable_reactions, reaction) //Consider replacing check with calculate_yield()
LAZYREMOVE(holder.reaction_list, src)
holder = null
reaction = null
return ..()
/*
* Check to make sure our input vars are sensible - truncated version of check_reagent_properties()
*
* (as the setup in holder.dm checks for that already - this is a way to reduce calculations on New())
* Don't call this unless you know what you're doing, this is an internal proc
*/
/datum/equilibrium/proc/check_inital_conditions()
//Make sure we have the right multipler for on_reaction()
for(var/single_reagent in reaction.required_reagents)
multiplier = min(multiplier, round((holder.get_reagent_amount(single_reagent) / reaction.required_reagents[single_reagent]), CHEMICAL_QUANTISATION_LEVEL))
if(multiplier == INFINITY)
return FALSE
//Consider purity gating too? - probably not, purity is hard to determine
//To prevent reactions outside of the pH window from starting.
if(!((holder.ph >= (reaction.optimal_ph_min - reaction.determin_ph_range)) && (holder.ph <= (reaction.optimal_ph_max + reaction.determin_ph_range))))
return FALSE
return TRUE
/*
* Check to make sure our input vars are sensible - is the holder overheated? does it have the required reagents? Does it have the required calalysts?
*
* If you're adding more checks for reactions, this is the proc to edit
* otherwise, generally, don't call this directed except internally
*/
/datum/equilibrium/proc/check_reagent_properties()
//Have we exploded from on_reaction?
if(!holder.my_atom || holder.reagent_list.len == 0)
return FALSE
if(!holder)
stack_trace("an equilibrium is missing it's holder.")
return FALSE
if(!reaction)
stack_trace("an equilibrium is missing it's reaction.")
return FALSE
//set up catalyst checks
var/total_matching_catalysts = 0
//Reagents check should be handled in the calculate_yield() from multiplier
//If the product/reactants are too impure
for(var/datum/reagent/reagent as anything in holder.reagent_list)
//this is done this way to reduce processing compared to holder.has_reagent(P)
for(var/datum/reagent/catalyst as anything in reaction.required_catalysts)
if(catalyst == reagent.type)
total_matching_catalysts++
if(istype(reagent, /datum/reagent/catalyst_agent))
var/datum/reagent/catalyst_agent/catalyst_agent = reagent
if(reagent.volume >= catalyst_agent.min_volume)
catalyst_agent.consider_catalyst(src)
if(!(total_matching_catalysts == reaction.required_catalysts.len))
return FALSE
//All good!
return TRUE
/*
* Calculates how much we're aiming to create
*
* Specifically calcuates multiplier, product_ratio, step_target_vol
* Also checks to see if these numbers are sane, returns a TRUE/FALSE
* Generally an internal proc
*/
/datum/equilibrium/proc/calculate_yield()
if(!reaction)
stack_trace("Tried to calculate an equlibrium for reaction [reaction.type], but there was no reaction set for the datum")
return FALSE
multiplier = INFINITY
for(var/reagent in reaction.required_reagents)
multiplier = min(multiplier, round((holder.get_reagent_amount(reagent) / reaction.required_reagents[reagent]), CHEMICAL_QUANTISATION_LEVEL))
if(!length(reaction.results)) //Incase of no reagent product
product_ratio = 1
step_target_vol = INFINITY
for(var/reagent in reaction.required_reagents)
step_target_vol = min(step_target_vol, multiplier * reaction.required_reagents[reagent])
if(step_target_vol == 0 || multiplier == 0)
return FALSE
//Sanity Check
if(step_target_vol == INFINITY || multiplier == INFINITY) //I don't see how this can happen, but I'm not bold enough to let infinities roll around for free
to_delete = TRUE
CRASH("Tried to calculate target vol for [reaction.type] with no products, but could not find required reagents for the reaction. If it got here, something is really broken with the recipe.")
return TRUE
product_ratio = 0
step_target_vol = 0
var/true_reacted_vol //Because volumes can be lost mid reactions
for(var/product in reaction.results)
step_target_vol += (reaction.results[product]*multiplier)
product_ratio += reaction.results[product]
true_reacted_vol += holder.get_reagent_amount(product)
if(step_target_vol == 0 || multiplier == INFINITY)
return FALSE
target_vol = step_target_vol + true_reacted_vol
reacted_vol = true_reacted_vol
return TRUE
/*
* Deals with lag - allows a reaction to speed up to 3x from delta_time
* "Charged" time (time_deficit) discharges by incrementing reactions by doubling them
* If delta_time is greater than 1.5, then we save the extra time for the next ticks
*
* Arguments:
* * delta_time - the time between the last proc in world.time
*/
/datum/equilibrium/proc/deal_with_time(delta_time)
if(delta_time > 1)
time_deficit += delta_time - 1
delta_time = 1 //Lets make sure reactions aren't super speedy and blow people up from a big lag spike
else if (time_deficit)
if(time_deficit < 0.25)
delta_time += time_deficit
time_deficit = 0
else
delta_time += 0.25
time_deficit -= 0.25
return delta_time
/*
* Main method of checking for explosive - or failed states
* Checks overheated() and overly_impure() of a reaction
* This was moved from the start, to the end - after a reaction, so post reaction temperature changes aren't ignored.
* overheated() is first - so double explosions can't happen (i.e. explosions that blow up the holder)
* step_volume_added is how much product (across all products) was added for this single step
*/
/datum/equilibrium/proc/check_fail_states(step_volume_added)
//Are we overheated?
if(reaction.is_cold_recipe)
if(holder.chem_temp < reaction.overheat_temp && reaction.overheat_temp != NO_OVERHEAT) //This is before the process - this is here so that overly_impure and overheated() share the same code location (and therefore vars) for calls.
SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overheated reaction steps")
reaction.overheated(holder, src, step_volume_added)
else
if(holder.chem_temp > reaction.overheat_temp)
SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overheated reaction steps")
reaction.overheated(holder, src, step_volume_added)
//is our product too impure?
for(var/product in reaction.results)
var/datum/reagent/reagent = holder.has_reagent(product)
if(!reagent) //might be missing from overheat exploding
continue
if (reagent.purity < reaction.purity_min)//If purity is below the min, call the proc
SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overly impure reaction steps")
reaction.overly_impure(holder, src, step_volume_added)
//did we explode?
if(!holder.my_atom || holder.reagent_list.len == 0)
return FALSE
return TRUE
/*
* Main reaction processor - Increments the reaction by a timestep
*
* First checks the holder to make sure it can continue
* Then calculates the purity and volume produced.TRUE
* Then adds/removes reagents
* Then alters the holder pH and temperature, and calls reaction_step
* Arguments:
* * delta_time - the time displacement between the last call and the current, 1 is a standard step
* * purity_modifier - how much to modify the step's purity by (0 - 1)
*/
/datum/equilibrium/proc/react_timestep(delta_time, purity_modifier = 1)
if(to_delete)
//This occurs when it explodes
return FALSE
if(!check_reagent_properties()) //this is first because it'll call explosions first
to_delete = TRUE
return
if(!calculate_yield())//So that this can detect if we're missing reagents
to_delete = TRUE
return
delta_time = deal_with_time(delta_time)
delta_t = 0 //how far off optimal temp we care
delta_ph = 0 //How far off the pH we are
var/cached_ph = holder.ph
var/cached_temp = holder.chem_temp
var/purity = 1 //purity of the current step
//Begin checks
//Calculate DeltapH (Deviation of pH from optimal)
//Within mid range
if (cached_ph >= reaction.optimal_ph_min && cached_ph <= reaction.optimal_ph_max)
delta_ph = 1 //100% purity for this step
//Lower range
else if (cached_ph < reaction.optimal_ph_min) //If we're outside of the optimal lower bound
if (cached_ph < (reaction.optimal_ph_min - reaction.determin_ph_range)) //If we're outside of the deterministic bound
delta_ph = 0 //0% purity
else //We're in the deterministic phase
delta_ph = (((cached_ph - (reaction.optimal_ph_min - reaction.determin_ph_range))**reaction.ph_exponent_factor)/((reaction.determin_ph_range**reaction.ph_exponent_factor))) //main pH calculation
//Upper range
else if (cached_ph > reaction.optimal_ph_max) //If we're above of the optimal lower bound
if (cached_ph > (reaction.optimal_ph_max + reaction.determin_ph_range)) //If we're outside of the deterministic bound
delta_ph = 0 //0% purity
else //We're in the deterministic phase
delta_ph = (((- cached_ph + (reaction.optimal_ph_max + reaction.determin_ph_range))**reaction.ph_exponent_factor)/(reaction.determin_ph_range**reaction.ph_exponent_factor))//Reverse - to + to prevent math operation failures.
//This should never proc, but it's a catch incase someone puts in incorrect values
else
stack_trace("[holder.my_atom] attempted to determine FermiChem pH for '[reaction.type]' which had an invalid pH of [cached_ph] for set recipie pH vars. It's likely the recipe vars are wrong.")
//Calculate DeltaT (Deviation of T from optimal)
if(!reaction.is_cold_recipe)
if (cached_temp < reaction.optimal_temp && cached_temp >= reaction.required_temp)
delta_t = (((cached_temp - reaction.required_temp)**reaction.temp_exponent_factor)/((reaction.optimal_temp - reaction.required_temp)**reaction.temp_exponent_factor))
else if (cached_temp >= reaction.optimal_temp)
delta_t = 1
else //too hot
delta_t = 0
to_delete = TRUE
return
else
if (cached_temp > reaction.optimal_temp && cached_temp <= reaction.required_temp)
delta_t = (((reaction.required_temp - cached_temp)**reaction.temp_exponent_factor)/((reaction.required_temp - reaction.optimal_temp)**reaction.temp_exponent_factor))
else if (cached_temp <= reaction.optimal_temp)
delta_t = 1
else //Too cold
delta_t = 0
to_delete = TRUE
return
//Call any special reaction steps BEFORE addition
if(reaction.reaction_step(holder, src, delta_t, delta_ph, step_target_vol) == END_REACTION)
to_delete = TRUE
return
//Catalyst modifier
delta_t *= speed_mod
purity = delta_ph//set purity equal to pH offset
//Then adjust purity of result with beaker reagent purity.
purity *= reactant_purity(reaction)
//Then adjust it from the input modifier
purity *= purity_modifier
//Now we calculate how much to add - this is normalised to the rate up limiter
var/delta_chem_factor = (reaction.rate_up_lim*delta_t)*delta_time//add/remove factor
var/total_step_added = 0
//keep limited
if(delta_chem_factor > step_target_vol)
delta_chem_factor = step_target_vol
else if (delta_chem_factor < CHEMICAL_VOLUME_MINIMUM)
delta_chem_factor = CHEMICAL_VOLUME_MINIMUM
//Normalise to multiproducts
delta_chem_factor /= product_ratio
//delta_chem_factor = round(delta_chem_factor, CHEMICAL_QUANTISATION_LEVEL) // Might not be needed - left here incase testmerge shows that it does. Remove before full commit.
//Calculate how much product to make and how much reactant to remove factors..
for(var/reagent in reaction.required_reagents)
holder.remove_reagent(reagent, (delta_chem_factor * reaction.required_reagents[reagent]), safety = TRUE)
//Apply pH changes
var/pH_adjust
if(reaction.reaction_flags & REACTION_PH_VOL_CONSTANT)
pH_adjust = ((delta_chem_factor * reaction.required_reagents[reagent])/target_vol)*(reaction.H_ion_release*h_ion_mod)
else //Default adds pH independant of volume
pH_adjust = (delta_chem_factor * reaction.required_reagents[reagent])*(reaction.H_ion_release*h_ion_mod)
holder.adjust_specific_reagent_ph(reagent, pH_adjust)
var/step_add
for(var/product in reaction.results)
//create the products
step_add = delta_chem_factor * reaction.results[product]
//If we make purities in real time
if(reaction.reaction_flags & REACTION_REAL_TIME_SPLIT && purity < 1)
var/datum/reagent/product_ref = GLOB.chemical_reagents_list[product]
if(purity < reaction.purity_min && product_ref.failed_chem) //If we're failed
holder.add_reagent(product_ref.failed_chem, step_add, null, cached_temp, (1-purity), override_base_ph = TRUE)
else if(purity < product_ref.inverse_chem_val && product_ref.inverse_chem) //If we're inverse
holder.add_reagent(product_ref.inverse_chem, step_add, null, cached_temp, (1-purity), override_base_ph = TRUE)
else if(product_ref.impure_chem && product_ref.impure_chem) //if we're impure
holder.add_reagent(product*purity, step_add, null, cached_temp, purity, override_base_ph = TRUE)
holder.add_reagent(product_ref.impure_chem*(1-purity), step_add, null, cached_temp, (1-purity), override_base_ph = TRUE)
else //We can get here if the flag is set, but there's no associated impure_chem assigned. In some cases this is desired (i.e. multiver only wants to real time split it's inverse chem)
holder.add_reagent(product, step_add, null, cached_temp, purity, override_base_ph = TRUE)
//Default handiling
else
holder.add_reagent(product, step_add, null, cached_temp, purity, override_base_ph = TRUE)
//Apply pH changes
var/pH_adjust
if(reaction.reaction_flags & REACTION_PH_VOL_CONSTANT)
pH_adjust = (step_add/target_vol)*(reaction.H_ion_release*h_ion_mod)
else
pH_adjust = step_add*(reaction.H_ion_release*h_ion_mod)
holder.adjust_specific_reagent_ph(product, pH_adjust)
reacted_vol += step_add
total_step_added += step_add
#ifdef REAGENTS_TESTING //Kept in so that people who want to write fermireactions can contact me with this log so I can help them
if(GLOB.Debug2) //I want my spans for my sanity
message_admins("<span class='green'>Reaction step active for:[reaction.type]</spans>")
message_admins("<span class='notice'>|Reaction conditions| Temp: [holder.chem_temp], pH: [holder.ph], reactions: [length(holder.reaction_list)], awaiting reactions: [length(holder.failed_but_capable_reactions)], no. reagents:[length(holder.reagent_list)], no. prev reagents: [length(holder.previous_reagent_list)]<spans>")
message_admins("<span class='warning'>Reaction vars: PreReacted:[reacted_vol] of [step_target_vol] of total [target_vol]. delta_t [delta_t], multiplier [multiplier], delta_chem_factor [delta_chem_factor] Pfactor [product_ratio], purity of [purity] from a delta_ph of [delta_ph]. DeltaTime: [delta_time]")
#endif
//Apply thermal output of reaction to beaker
if(reaction.reaction_flags & REACTION_HEAT_ARBITARY)
holder.chem_temp += clamp((reaction.thermic_constant* total_step_added*thermic_mod), 0, CHEMICAL_MAXIMUM_TEMPERATURE) //old method - for every bit added, the whole temperature is adjusted
else //Standard mechanics
var/heat_energy = reaction.thermic_constant * total_step_added * thermic_mod * SPECIFIC_HEAT_DEFAULT
holder.adjust_thermal_energy(heat_energy, 0, CHEMICAL_MAXIMUM_TEMPERATURE) //heat is relative to the beaker conditions
//Give a chance of sounds
if(prob(5))
holder.my_atom.audible_message(span_notice("[icon2html(holder.my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [reaction.mix_message]"))
if(reaction.mix_sound)
playsound(get_turf(holder.my_atom), reaction.mix_sound, 80, TRUE)
//Used for UI output
reaction_quality = purity
//post reaction checks
if(!(check_fail_states(total_step_added)))
to_delete = TRUE
//end reactions faster so plumbing is faster
if((step_add >= step_target_vol) && (length(holder.reaction_list == 1)))//length is so that plumbing is faster - but it doesn't disable competitive reactions. Basically, competitive reactions will likely reach their step target at the start, so this will disable that. We want to avoid that. But equally, we do want to full stop a holder from reacting asap so plumbing isn't waiting an tick to resolve.
to_delete = TRUE
holder.update_total()//do NOT recalculate reactions
/*
* Calculates the total sum normalised purity of ALL reagents in a holder
*
* Currently calculates it irrespective of required reagents at the start, but this should be changed if this is powergamed to required reagents
* It's not currently because overly_impure affects all reagents
*/
/datum/equilibrium/proc/reactant_purity(datum/chemical_reaction/C)
var/list/cached_reagents = holder.reagent_list
var/i = 0
var/cached_purity
for(var/datum/reagent/reagent as anything in holder.reagent_list)
if (reagent in cached_reagents)
cached_purity += reagent.purity
i++
if(!i)//I've never seen it get here with 0, but in case - it gets here when it blows up from overheat
stack_trace("No reactants found mid reaction for [C.type]. Beaker: [holder.my_atom]")
return 0 //we exploded and cleared reagents - but lets not kill the process
return cached_purity/i
///Panic stop a reaction - cleanup should be handled by the next timestep
/datum/equilibrium/proc/force_clear_reactive_agents()
for(var/reagent in reaction.required_reagents)
holder.remove_reagent(reagent, (multiplier * reaction.required_reagents[reagent]), safety = 1)