diff --git a/baystation12.dme b/baystation12.dme index 2d7b5948f0..633f6c9a61 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -99,6 +99,7 @@ #include "code\controllers\voting.dm" #include "code\controllers\Processes\air.dm" #include "code\controllers\Processes\alarm.dm" +#include "code\controllers\Processes\chemistry.dm" #include "code\controllers\Processes\disease.dm" #include "code\controllers\Processes\emergencyShuttle.dm" #include "code\controllers\Processes\event.dm" diff --git a/code/controllers/Processes/chemistry.dm b/code/controllers/Processes/chemistry.dm new file mode 100644 index 0000000000..169bd15ee5 --- /dev/null +++ b/code/controllers/Processes/chemistry.dm @@ -0,0 +1,33 @@ +var/datum/controller/process/chemistry/chemistryProcess + +/datum/controller/process/chemistry + var/tmp/datum/updateQueue/updateQueueInstance + var/list/active_holders + var/list/chemical_reactions + var/list/chemical_reagents + +/datum/controller/process/chemistry/setup() + name = "chemistry" + schedule_interval = 20 // every 2 seconds + updateQueueInstance = new + chemistryProcess = src + active_holders = list() + chemical_reactions = chemical_reactions_list + chemical_reagents = chemical_reagents_list + +/datum/controller/process/chemistry/getStatName() + return ..()+"([active_holders.len])" + +/datum/controller/process/chemistry/doWork() + for(var/datum/reagents/holder in active_holders) + if(!holder.process_reactions()) + active_holders -= holder + scheck() + +/datum/controller/process/chemistry/proc/mark_for_update(var/datum/reagents/holder) + if(holder in active_holders) + return + + //Process once, right away. If we still need to continue then add to the active_holders list and continue later + if(holder.process_reactions()) + active_holders += holder diff --git a/code/controllers/verbs.dm b/code/controllers/verbs.dm index ddfd99f5d6..d849bc836b 100644 --- a/code/controllers/verbs.dm +++ b/code/controllers/verbs.dm @@ -55,7 +55,7 @@ usr.client.debug_variables(antag) message_admins("Admin [key_name_admin(usr)] is debugging the [antag.role_text] template.") -/client/proc/debug_controller(controller in list("Master","Ticker","Ticker Process","Air","Jobs","Sun","Radio","Supply","Shuttles","Emergency Shuttle","Configuration","pAI", "Cameras", "Transfer Controller", "Gas Data","Event","Plants","Alarm","Nano")) +/client/proc/debug_controller(controller in list("Master","Ticker","Ticker Process","Air","Jobs","Sun","Radio","Supply","Shuttles","Emergency Shuttle","Configuration","pAI", "Cameras", "Transfer Controller", "Gas Data","Event","Plants","Alarm","Nano","Chemistry")) set category = "Debug" set name = "Debug Controller" set desc = "Debug the various periodic loop controllers for the game (be careful!)" @@ -119,5 +119,8 @@ if("Nano") debug_variables(nanomanager) feedback_add_details("admin_verb", "DNano") + if("Chemistry") + debug_variables(chemistryProcess) + feedback_add_details("admin_verb", "DChem") message_admins("Admin [key_name_admin(usr)] is debugging the [controller] controller.") return diff --git a/code/modules/reagents/Chemistry-Holder.dm b/code/modules/reagents/Chemistry-Holder.dm index dfd735d2f7..4ad6e7337c 100644 --- a/code/modules/reagents/Chemistry-Holder.dm +++ b/code/modules/reagents/Chemistry-Holder.dm @@ -1,3 +1,5 @@ +#define PROCESS_REACTION_ITER 5 //when processing a reaction, iterate this many times + /datum/reagents var/list/datum/reagent/reagent_list = list() var/total_volume = 0 @@ -11,6 +13,9 @@ /datum/reagents/Destroy() ..() + if(chemistryProcess) + chemistryProcess.active_holders -= src + for(var/datum/reagent/R in reagent_list) qdel(R) reagent_list.Cut() @@ -70,17 +75,22 @@ my_atom.reagents = null /datum/reagents/proc/handle_reactions() + if(chemistryProcess) + chemistryProcess.mark_for_update(src) + +//returns 1 if the holder should continue reactiong, 0 otherwise. +/datum/reagents/proc/process_reactions() if(!my_atom) // No reactions in temporary holders - return + return 0 if(!my_atom.loc) //No reactions inside GC'd containers - return + return 0 if(my_atom.flags & NOREACT) // No reactions here - return + return 0 + var/reaction_occured + var/list/effect_reactions = list() var/list/eligible_reactions = list() - - var/reaction_occured = 0 - do + for(var/i in 1 to PROCESS_REACTION_ITER) reaction_occured = 0 //need to rebuild this to account for chain reactions @@ -88,17 +98,20 @@ eligible_reactions |= chemical_reactions_list[R.id] for(var/datum/chemical_reaction/C in eligible_reactions) - if(!C.can_happen(src)) - continue - - if(C.process(src)) + if(C.can_happen(src) && C.process(src)) + effect_reactions |= C reaction_occured = 1 eligible_reactions.Cut() - while(reaction_occured) + if(!reaction_occured) + break + + for(var/datum/chemical_reaction/C in effect_reactions) + C.post_reaction(src) + update_total() - return + return reaction_occured /* Holder-to-chemical */ diff --git a/code/modules/reagents/Chemistry-Recipes.dm b/code/modules/reagents/Chemistry-Recipes.dm index 1414d959e5..8615b73452 100644 --- a/code/modules/reagents/Chemistry-Recipes.dm +++ b/code/modules/reagents/Chemistry-Recipes.dm @@ -11,11 +11,24 @@ for(var/path in paths) var/datum/chemical_reaction/D = new path() - if(D.required_reagents && D.required_reagents.len) var/reagent_id = D.required_reagents[1] + if(!chemical_reactions_list[reagent_id]) + chemical_reactions_list[reagent_id] = list() chemical_reactions_list[reagent_id] += D +//helper that ensures the reaction rate holds after iterating +//Ex. REACTION_RATE(0.3) means that 30% of the reagents will react each chemistry tick (~2 seconds by default). +#define REACTION_RATE(rate) (1.0 - (1.0-rate)**(1.0/PROCESS_REACTION_ITER)) + +//helper to define reaction rate in terms of half-life +//Ex. +//HALF_LIFE(0) -> Reaction completes immediately (default chems) +//HALF_LIFE(1) -> Half of the reagents react immediately, the rest after 1 chemistry ticks +//HALF_LIFE(2) -> Half of the reagents are consumed after 2 chemistry ticks. +//HALF_LIFE(3) -> Half of the reagents are consumed after 3 chemistry ticks. +#define HALF_LIFE(ticks) (ticks? 1.0 - (0.5)**(1.0/(ticks*PROCESS_REACTION_ITER)) : 1.0) + /datum/chemical_reaction var/name = null var/id = null @@ -23,8 +36,19 @@ var/list/required_reagents = list() var/list/catalysts = list() var/list/inhibitors = list() - var/result_amount = 0 + + //how far the reaction proceeds each time it is processed. Used with either REACTION_RATE or HALF_LIFE macros. + var/reaction_rate = HALF_LIFE(0) + + //if less than 1, the reaction will be inhibited if the ratio of products/reagents is too high. + //0.5 = 50% yield -> reaction will only proceed halfway until products are removed. + var/yield = 1.0 + + //If limits on reaction rate would leave less than this amount of any reagent (adjusted by the reaction ratios), + //the reaction goes to completion. This is to prevent reactions from going on forever with tiny reagent amounts. + var/min_reaction = 2 + var/mix_message = "The solution begins to bubble." var/reaction_sound = 'sound/effects/bubbles.ogg' @@ -48,27 +72,69 @@ return 1 +/datum/chemical_reaction/proc/calc_reaction_progress(var/datum/reagents/holder, var/reaction_limit) + var/progress = reaction_limit * reaction_rate //simple exponential progression + + //calculate yield + if(1-yield > 0.001) //if yield ratio is big enough just assume it goes to completion + /* + Determine the max amount of product by applying the yield condition: + (max_product/result_amount) / reaction_limit == yield/(1-yield) + + We make use of the fact that: + reaction_limit = (holder.get_reagent_amount(reactant) / required_reagents[reactant]) of the limiting reagent. + */ + var/yield_ratio = yield/(1-yield) + var/max_product = yield_ratio * reaction_limit * result_amount //rearrange to obtain max_product + var/yield_limit = max(0, max_product - holder.get_reagent_amount(result))/result_amount + + progress = min(progress, yield_limit) //apply yield limit + + //apply min reaction progress - wasn't sure if this should go before or after applying yield + //I guess people can just have their miniscule reactions go to completion regardless of yield. + for(var/reactant in required_reagents) + var/remainder = holder.get_reagent_amount(reactant) - progress*required_reagents[reactant] + if(remainder <= min_reaction*required_reagents[reactant]) + progress = reaction_limit + break + + return progress + /datum/chemical_reaction/proc/process(var/datum/reagents/holder) //determine how far the reaction can proceed var/list/reaction_limits = list() for(var/reactant in required_reagents) reaction_limits += holder.get_reagent_amount(reactant) / required_reagents[reactant] + + //determine how far the reaction proceeds var/reaction_limit = min(reaction_limits) + var/progress_limit = calc_reaction_progress(holder, reaction_limit) + + var/reaction_progress = min(reaction_limit, progress_limit) //no matter what, the reaction progress cannot exceed the stoichiometric limit. //need to obtain the new reagent's data before anything is altered - var/data = send_data(holder, reaction_limit) + var/data = send_data(holder, reaction_progress) //remove the reactants for(var/reactant in required_reagents) - var/amt_used = required_reagents[reactant] * reaction_limit + var/amt_used = required_reagents[reactant] * reaction_progress holder.remove_reagent(reactant, amt_used, safety = 1) //add the product - var/amt_produced = result_amount * reaction_limit + var/amt_produced = result_amount * reaction_progress if(result) - holder.add_reagent(result, amt_produced, data) + holder.add_reagent(result, amt_produced, data, safety = 1) - //produces messages and sounds + on_reaction(holder, amt_produced) + + return reaction_progress + +//called when a reaction processes +/datum/chemical_reaction/proc/on_reaction(var/datum/reagents/holder, var/created_volume) + return + +//called after processing reactions, if they occurred +/datum/chemical_reaction/proc/post_reaction(var/datum/reagents/holder) var/atom/container = holder.my_atom if(mix_message && container && !ismob(container)) var/turf/T = get_turf(container) @@ -76,14 +142,6 @@ for(var/mob/M in seen) M.show_message("\icon[container] [mix_message]", 1) playsound(T, reaction_sound, 80, 1) - - on_reaction(holder, amt_produced) - - return reaction_limit - -//called after a reaction occurs -/datum/chemical_reaction/proc/on_reaction(var/datum/reagents/holder, var/created_volume) - return //obtains any special data that will be provided to the reaction products //this is called just before reactants are removed.