diff --git a/baystation12.dme b/baystation12.dme index 3d52f63fb55..a608d74fee1 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -101,6 +101,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 00000000000..169bd15ee5e --- /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 ddfd99f5d6c..d849bc836b8 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 b4749d06d3c..84c8affc945 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 @@ -5,45 +7,14 @@ var/atom/my_atom = null /datum/reagents/New(var/max = 100) + ..() maximum_volume = max - //I dislike having these here but map-objects are initialised before world/New() is called. >_> - if(!chemical_reagents_list) - //Chemical Reagents - Initialises all /datum/reagent into a list indexed by reagent id - var/paths = typesof(/datum/reagent) - /datum/reagent - chemical_reagents_list = list() - for(var/path in paths) - var/datum/reagent/D = new path() - if(!D.name) - continue - chemical_reagents_list[D.id] = D - - if(!chemical_reactions_list) - //Chemical Reactions - Initialises all /datum/chemical_reaction into a list - // It is filtered into multiple lists within a list. - // For example: - // chemical_reaction_list["phoron"] is a list of all reactions relating to phoron - - var/paths = typesof(/datum/chemical_reaction) - /datum/chemical_reaction - chemical_reactions_list = list() - - for(var/path in paths) - - var/datum/chemical_reaction/D = new path() - var/list/reaction_ids = list() - - if(D.required_reagents && D.required_reagents.len) - for(var/reaction in D.required_reagents) - reaction_ids += reaction - - // Create filters based on each reagent id in the required reagents list - for(var/id in reaction_ids) - if(!chemical_reactions_list[id]) - chemical_reactions_list[id] = list() - chemical_reactions_list[id] += D - break // Don't bother adding ourselves to other reagent ids, it is redundant. /datum/reagents/Destroy() ..() + if(chemistryProcess) + chemistryProcess.active_holders -= src + for(var/datum/reagent/R in reagent_list) qdel(R) reagent_list.Cut() @@ -103,55 +74,43 @@ 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 0 if(my_atom.flags & NOREACT) // No reactions here - return - - var/reaction_occured = 0 - do + return 0 + + var/reaction_occured + var/list/effect_reactions = list() + var/list/eligible_reactions = list() + for(var/i in 1 to PROCESS_REACTION_ITER) reaction_occured = 0 + + //need to rebuild this to account for chain reactions for(var/datum/reagent/R in reagent_list) - for(var/datum/chemical_reaction/C in chemical_reactions_list[R.id]) - var/reagents_suitable = 1 - for(var/B in C.required_reagents) - if(!has_reagent(B)) - reagents_suitable = 0 - for(var/B in C.catalysts) - if(!has_reagent(B, C.catalysts[B])) - reagents_suitable = 0 - for(var/B in C.inhibitors) - if(has_reagent(B, C.inhibitors[B])) - reagents_suitable = 0 - - if(!reagents_suitable || !C.can_happen(src)) - continue - - var/use = -1 - for(var/B in C.required_reagents) - if(use == -1) - use = get_reagent_amount(B) / C.required_reagents[B] - else - use = min(use, get_reagent_amount(B) / C.required_reagents[B]) - - var/newdata = C.send_data(src) // We need to get it before reagents are removed. See blood paint. - for(var/B in C.required_reagents) - remove_reagent(B, use * C.required_reagents[B], safety = 1) - - if(C.result) - add_reagent(C.result, C.result_amount * use, newdata) - - if(!ismob(my_atom) && C.mix_message) - var/list/seen = viewers(4, get_turf(my_atom)) - for(var/mob/M in seen) - M << "\icon[my_atom] [C.mix_message]" - playsound(get_turf(my_atom), 'sound/effects/bubbles.ogg', 80, 1) - - C.on_reaction(src, C.result_amount * use) + eligible_reactions |= chemical_reactions_list[R.id] + + for(var/datum/chemical_reaction/C in eligible_reactions) + if(C.can_happen(src) && C.process(src)) + effect_reactions |= C reaction_occured = 1 - while(reaction_occured) + + eligible_reactions.Cut() + + 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 */ @@ -223,6 +182,24 @@ return 0 return 0 +/datum/reagents/proc/has_any_reagent(var/list/check_reagents) + for(var/datum/reagent/current in reagent_list) + if(current.id in check_reagents) + if(current.volume >= check_reagents[current.id]) + return 1 + else + return 0 + return 0 + +/datum/reagents/proc/has_all_reagents(var/list/check_reagents) + //this only works if check_reagents has no duplicate entries... hopefully okay since it expects an associative list + var/missing = check_reagents.len + for(var/datum/reagent/current in reagent_list) + if(current.id in check_reagents) + if(current.volume >= check_reagents[current.id]) + missing-- + return !missing + /datum/reagents/proc/clear_reagents() for(var/datum/reagent/current in reagent_list) del_reagent(current.id) diff --git a/code/modules/reagents/Chemistry-Reagents.dm b/code/modules/reagents/Chemistry-Reagents.dm index 2d8ece18c12..80658f48645 100644 --- a/code/modules/reagents/Chemistry-Reagents.dm +++ b/code/modules/reagents/Chemistry-Reagents.dm @@ -1,3 +1,15 @@ + +//Chemical Reagents - Initialises all /datum/reagent into a list indexed by reagent id +/proc/initialize_chemical_reagents() + var/paths = typesof(/datum/reagent) - /datum/reagent + chemical_reagents_list = list() + for(var/path in paths) + var/datum/reagent/D = new path() + if(!D.name) + continue + chemical_reagents_list[D.id] = D + + /datum/reagent var/name = "Reagent" var/id = "reagent" diff --git a/code/modules/reagents/Chemistry-Recipes.dm b/code/modules/reagents/Chemistry-Recipes.dm index e400cee3604..ce9ea05717a 100644 --- a/code/modules/reagents/Chemistry-Recipes.dm +++ b/code/modules/reagents/Chemistry-Recipes.dm @@ -1,3 +1,34 @@ + +//Chemical Reactions - Initialises all /datum/chemical_reaction into a list +// It is filtered into multiple lists within a list. +// For example: +// chemical_reaction_list["phoron"] is a list of all reactions relating to phoron +// Note that entries in the list are NOT duplicated. So if a reaction pertains to +// more than one chemical it will still only appear in only one of the sublists. +/proc/initialize_chemical_reactions() + var/paths = typesof(/datum/chemical_reaction) - /datum/chemical_reaction + chemical_reactions_list = list() + + 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 over the following 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 @@ -5,17 +36,111 @@ 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' /datum/chemical_reaction/proc/can_happen(var/datum/reagents/holder) + //check that all the required reagents are present + if(!holder.has_all_reagents(required_reagents)) + return 0 + + //check that all the required catalysts are present in the required amount + if(!holder.has_all_reagents(catalysts)) + return 0 + + //check that none of the inhibitors are present in the required amount + if(holder.has_any_reagent(inhibitors)) + return 0 + 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_progress) + + //remove the reactants + for(var/reactant in required_reagents) + 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_progress + if(result) + holder.add_reagent(result, amt_produced, data, safety = 1) + + 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 -/datum/chemical_reaction/proc/send_data(var/datum/reagents/T) +//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) + var/list/seen = viewers(4, T) + for(var/mob/M in seen) + M.show_message("\icon[container] [mix_message]", 1) + playsound(T, reaction_sound, 80, 1) + +//obtains any special data that will be provided to the reaction products +//this is called just before reactants are removed. +/datum/chemical_reaction/proc/send_data(var/datum/reagents/holder, var/reaction_limit) return null /* Common reactions */ @@ -53,7 +178,7 @@ id = "oxycodone" result = "oxycodone" required_reagents = list("ethanol" = 1, "tramadol" = 1) - catalysts = list("phoron" = 1) + catalysts = list("phoron" = 5) result_amount = 1 /datum/chemical_reaction/sterilizine @@ -145,7 +270,7 @@ id = "peridaxon" result = "peridaxon" required_reagents = list("bicaridine" = 2, "clonexadone" = 2) - catalysts = list("phoron" = 1) + catalysts = list("phoron" = 5) result_amount = 2 /datum/chemical_reaction/virus_food @@ -160,7 +285,7 @@ id = "leporazine" result = "leporazine" required_reagents = list("silicon" = 1, "copper" = 1) - catalysts = list("phoron" = 1) + catalysts = list("phoron" = 5) result_amount = 2 /datum/chemical_reaction/cryptobiolin @@ -187,9 +312,9 @@ /datum/chemical_reaction/dexalin name = "Dexalin" id = "dexalin" - result = "dexalin" + result = "dexalin" required_reagents = list("acetone" = 2, "phoron" = 0.1) - catalysts = list("phoron" = 1) + catalysts = list("phoron" = 1) inhibitors = list("water" = 1) // Messes with cryox result_amount = 1 @@ -241,7 +366,7 @@ id = "clonexadone" result = "clonexadone" required_reagents = list("cryoxadone" = 1, "sodium" = 1, "phoron" = 0.1) - catalysts = list("phoron" = 1) + catalysts = list("phoron" = 5) result_amount = 2 /datum/chemical_reaction/spaceacillin @@ -369,7 +494,7 @@ id = "condensedcapsaicin" result = "condensedcapsaicin" required_reagents = list("capsaicin" = 2) - catalysts = list("phoron" = 1) + catalysts = list("phoron" = 5) result_amount = 1 /datum/chemical_reaction/coolant @@ -861,7 +986,7 @@ if(holder.my_atom && istype(holder.my_atom, required)) var/obj/item/slime_extract/T = holder.my_atom if(T.Uses > 0) - return 1 + return ..() return 0 /datum/chemical_reaction/slime/on_reaction(var/datum/reagents/holder) @@ -1191,7 +1316,7 @@ id = "tofu" result = null required_reagents = list("soymilk" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 1 /datum/chemical_reaction/tofu/on_reaction(var/datum/reagents/holder, var/created_volume) @@ -1245,7 +1370,7 @@ id = "cheesewheel" result = null required_reagents = list("milk" = 40) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 1 /datum/chemical_reaction/cheesewheel/on_reaction(var/datum/reagents/holder, var/created_volume) @@ -1356,7 +1481,7 @@ id = "moonshine" result = "moonshine" required_reagents = list("nutriment" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/grenadine @@ -1364,7 +1489,7 @@ id = "grenadine" result = "grenadine" required_reagents = list("berryjuice" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/wine @@ -1372,7 +1497,7 @@ id = "wine" result = "wine" required_reagents = list("grapejuice" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/pwine @@ -1380,7 +1505,7 @@ id = "pwine" result = "pwine" required_reagents = list("poisonberryjuice" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/melonliquor @@ -1388,7 +1513,7 @@ id = "melonliquor" result = "melonliquor" required_reagents = list("watermelonjuice" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/bluecuracao @@ -1396,7 +1521,7 @@ id = "bluecuracao" result = "bluecuracao" required_reagents = list("orangejuice" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/spacebeer @@ -1404,7 +1529,7 @@ id = "spacebeer" result = "beer" required_reagents = list("cornoil" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/vodka @@ -1412,7 +1537,7 @@ id = "vodka" result = "vodka" required_reagents = list("potato" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/sake @@ -1420,7 +1545,7 @@ id = "sake" result = "sake" required_reagents = list("rice" = 10) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 10 /datum/chemical_reaction/kahlua @@ -1428,7 +1553,7 @@ id = "kahlua" result = "kahlua" required_reagents = list("coffee" = 5, "sugar" = 5) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 5 /datum/chemical_reaction/gin_tonic @@ -1716,7 +1841,7 @@ id = "mead" result = "mead" required_reagents = list("sugar" = 1, "water" = 1) - catalysts = list("enzyme" = 1) + catalysts = list("enzyme" = 5) result_amount = 2 /datum/chemical_reaction/iced_beer diff --git a/code/world.dm b/code/world.dm index 6f736b87445..e74268f9ffc 100644 --- a/code/world.dm +++ b/code/world.dm @@ -1,3 +1,14 @@ + +/* + The initialization of the game happens roughly like this: + + 1. All global variables are initialized (including the global_init instance). + 2. The map is initialized, and map objects are created. + 3. world/New() runs, creating the process scheduler (and the old master controller) and spawning their setup. + 4. processScheduler/setup() runs, creating all the processes. game_controller/setup() runs, calling initialize() on all movable atoms in the world. + 5. The gameticker is created. + +*/ var/global/datum/global_init/init = new () /* @@ -7,8 +18,11 @@ var/global/datum/global_init/init = new () makeDatumRefLists() load_configuration() + + initialize_chemical_reagents() + initialize_chemical_reactions() - qdel(src) + qdel(src) //we're done /world