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