Refactors reagent transfer operations (#92213)

## About The Pull Request
- Fixes #92198
- Fixes #92298

**1) Replaces reagent `on_transfer()` with its corresponding `expose()`
proc variants**
This PR replaces all known implementations of
`/datum/reagent/on_transfer()` with `/datum/reagent/expose_mob()`. We
use `expose_mob()` & not the other `expose()` variants because all known
implementations were targeting living beings so this was the correct
replacement This has 2 benefits
- `expose_mob()` gets called correctly when an impure reagent is
converted to it's inverse variant like for Cryostylane & Cryogeldia.
This isn't the case for `on_transfer()` so we get correct behaviour
which fixes the above bug
- Removing `on_transfer()` makes the proc `/datum/reagents/trans_to()`
much faster performance wise because we aren't calling `update_total()`
per reagent transfered now but only once at the end after all reagents
are transferred

Also there was little to no functional difference between the 2 procs,
`expose()` works correctly in comparison & this won't confuse devs when
deciding which proc to use. One proc to cover all scenarios

**2) Removes unused expose signals** 
`COMSIG_ATOM_AFTER_EXPOSE_REAGENTS` & `COMSIG_REAGENTS_EXPOSE_ATOM` are
not used anywhere in the codebase i.e. no listeners. They can be
discarded as dead code

**3) Fixes wrong transfer amount passed to
`/datum/reagent/intercept_reagents_transfer()` &
`/datum/reagents/expose()`**
The wrong transfer `amount` was passed when it fact it should use
`transfer_amount` which contains the multiplier & proportional
multiplier applied. Also the reagent volumes exposed was computed
incorrectly resulting in the 2nd issue listed above. Blood transferred
to mobs now go to `blood_volume` directly instead of getting added to
the mobs reagent holder as long as it's less than `BLOOD_VOLUME_MAXIMUM`
level

## Changelog
🆑
fix: reagent intercept operations use correct volumes e.g. ph buffers
fix: impure cryostylane now has inverse cryogeldia effects when applied
on mods
fix: exposing reagents now uses correct volumes i.e. injecting blood
into mobs don't increase it exponentially and stops when max levels are
reached. Exposure affects of all reagents are lessened upon continuous
exposure
refactor: refactors how reagent affects are applied on mobs. Report bugs
on github
/🆑
This commit is contained in:
SyncIt21
2025-07-31 20:04:07 +02:00
committed by GitHub
parent e292dc4041
commit 64376e2899
14 changed files with 96 additions and 117 deletions
@@ -3,7 +3,7 @@
/// Holder for a bunch of [/datum/reagent]
/datum/reagents
/// The reagents being held
var/list/datum/reagent/reagent_list = new/list()
var/list/datum/reagent/reagent_list = list()
/// Current volume of all the reagents
var/total_volume = 0
/// Max volume of this holder
@@ -80,7 +80,7 @@
* * added_purity - override to force a purity when added
* * added_ph - override to force a pH when added
* * override_base_ph - ingore the present pH of the reagent, and instead use the default (i.e. if buffers/reactions alter it)
* * list/reagent_added - If not null use this as an holder to store and retrive the reagent datum that was just added without having to locate it after this proc returns. Clear the list to erase old values
* * list/reagent_added - If not null will contain an map of [reagent datum->amount added] which holds the inverse chems added to mobs. Clear the list to erase old values
* * creation_callback - Callback to invoke when the reagent is created
*/
/datum/reagents/proc/add_reagent(
@@ -154,7 +154,7 @@
set_temperature(reagtemp)
if(!isnull(reagent_added))
reagent_added += iter_reagent
reagent_added[iter_reagent] = amount
if(!no_react && !is_reacting) //To reduce the amount of calculations for a reaction the reaction list is only updated on a reagents addition.
handle_reactions()
return amount
@@ -185,7 +185,7 @@
set_temperature(reagtemp)
if(!isnull(reagent_added))
reagent_added += new_reagent
reagent_added[new_reagent] = amount
if(!no_react)
handle_reactions()
return amount
@@ -463,14 +463,9 @@
if(amount <= 0)
return FALSE
//Set up new reagents to inherit the old ongoing reactions
if(!no_react)
transfer_reactions(target_holder)
var/trans_data = null
var/list/r_to_send = methods ? list() : null // Validated list of reagents to be exposed
var/list/transfer_log = list()
var/list/reagents_to_remove = list()
var/part = isnull(target_id) ? (amount / total_volume) : 1
var/transfer_amount
@@ -490,34 +485,29 @@
continue
else
transfer_amount = reagent.volume * part
transfer_amount *= multiplier
if(preserve_data)
trans_data = copy_data(reagent)
if(reagent.intercept_reagents_transfer(target_holder, amount))
if(reagent.intercept_reagents_transfer(target_holder, transfer_amount))
update_total()
target_holder.update_total()
continue
transfered_amount = target_holder.add_reagent(reagent.type, transfer_amount * multiplier, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, reagent_added = r_to_send, creation_callback = CALLBACK(src, PROC_REF(_on_transfer_creation), reagent, target_holder)) //we only handle reaction after every reagent has been transferred.
transfered_amount = target_holder.add_reagent(reagent.type, transfer_amount, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE, reagent_added = r_to_send, creation_callback = CALLBACK(src, PROC_REF(_on_transfer_creation), reagent, target_holder)) //we only handle reaction after every reagent has been transferred.
if(!transfered_amount)
continue
reagents_to_remove[reagent] = transfer_amount
total_transfered_amount += transfered_amount
reagent.volume -= transfered_amount
transfer_log += "[reagent.type] ([transfered_amount]u, [reagent.purity] purity)"
if(!isnull(target_id))
break
update_total()
//expose target to reagent changes
if(methods)
target_holder.expose(isorgan(target_atom) ? target : target_atom, methods, part, show_message, r_to_send)
//remove chemicals that were added above
for(var/datum/reagent/reagent as anything in reagents_to_remove)
transfer_amount = reagents_to_remove[reagent]
if(methods)
reagent.on_transfer(target_atom, methods, transfer_amount)
reagent.volume -= transfer_amount
update_total()
transfer_log += "[reagent.type] ([transfer_amount]u, [reagent.purity] purity)"
target_holder.expose(isorgan(target_atom) ? target : target_atom, methods, 1, show_message, r_to_send)
//combat log
if(transferred_by && target_atom)
@@ -532,8 +522,9 @@
log_combat(transferred_by, log_target, "transferred reagents to", my_atom, "which had [english_list(transfer_log)]")
if(!no_react)
target_holder.handle_reactions()
transfer_reactions(target_holder)
handle_reactions()
target_holder.handle_reactions()
return total_transfered_amount
@@ -559,7 +550,7 @@
multiplier = 1,
preserve_data = TRUE,
no_react = FALSE,
copy_methods
copy_methods = NONE,
)
if(QDELETED(target) || !total_volume)
return
@@ -584,23 +575,21 @@
var/list/cached_reagents = reagent_list
var/part = amount / total_volume
var/transfer_amount
var/transfered_amount = 0
var/total_transfered_amount = 0
var/trans_data = null
var/list/r_to_send = copy_methods ? list() : null
for(var/datum/reagent/reagent as anything in cached_reagents)
transfer_amount = reagent.volume * part * multiplier
if(preserve_data)
trans_data = copy_data(reagent)
transfered_amount = target_holder.add_reagent(reagent.type, transfer_amount, trans_data, chem_temp, reagent.purity, reagent.ph, no_react = TRUE)
if(copy_methods && !no_react)
reagent.on_transfer(target, copy_methods, transfer_amount)
if(!transfered_amount)
continue
total_transfered_amount += transfered_amount
total_transfered_amount += target_holder.add_reagent(reagent.type, transfer_amount, trans_data, chem_temp, reagent.purity, reagent.ph, reagent_added = r_to_send, no_react = TRUE)
//expose target to reagent changes
if(copy_methods)
target_holder.expose(target, copy_methods, 1, FALSE, r_to_send)
if(!no_react)
// pass over previous ongoing reactions before handle_reactions is called
transfer_reactions(target_holder)
target_holder.handle_reactions()
@@ -804,23 +793,27 @@
* Arguments
* - Atom/target: What mob/turf/object is being exposed to reagents? This is your reaction target.
* - Methods: What reaction type is the reagent itself going to call on the reaction target? Types are TOUCH, INGEST, VAPOR, PATCH, INJECT and INHALE.
* - Volume_modifier: What is the reagent volume multiplied by when exposed? Note that this is called on the volume of EVERY reagent in the base body, so factor in your Maximum_Volume if necessary!
* - Volume_modifier: reagent volume modifier applied to the reagents(passed to r_to_expose or this holder) before exposing. Must be between 0->1
* - Show_message: Whether to display anything to mobs when they are exposed.
* - list/datum/reagent/r_to_expose: list of reagents to expose. if null will expose the reagents present in this holder instead
* - list/datum/reagent/r_to_expose: map of[/datum/reagent -> amount] when you to want to expose specific reagents with precise amounts
*/
/datum/reagents/proc/expose(atom/target, methods = TOUCH, volume_modifier = 1, show_message = 1, list/datum/reagent/r_to_expose = null)
if(isnull(target))
return null
return
var/list/target_reagents = isnull(r_to_expose) ? reagent_list : r_to_expose
if(!target_reagents.len)
return null
if(volume_modifier <= 0)
stack_trace("Volume modifier [volume_modifier] must be +ve")
return
var/list/datum/reagent/reagents = list()
for(var/datum/reagent/reagent as anything in target_reagents)
reagents[reagent] = reagent.volume * volume_modifier
if(isnull(r_to_expose))
for(var/datum/reagent/reagent as anything in reagent_list)
reagents[reagent] = reagent.volume * volume_modifier
else
for(var/datum/reagent/reagent as anything in r_to_expose)
reagents[reagent] = r_to_expose[reagent] * volume_modifier
return target.expose_reagents(reagents, src, methods, volume_modifier, show_message)
return target.expose_reagents(reagents, src, methods, show_message)
/**
* Applies heat to this holder
@@ -196,10 +196,6 @@ Primarily used in reagents/reaction_agents
/datum/reagent/proc/intercept_reagents_transfer(datum/reagents/target, amount)
return FALSE
///Called after a reagent is transferred
/datum/reagent/proc/on_transfer(atom/A, methods=TOUCH, trans_volume)
return
/// Called when this reagent is first added to a mob
/datum/reagent/proc/on_mob_add(mob/living/affected_mob, amount)
// Scale the overdose threshold of the chem by the difference between the default and creation purity.
@@ -154,14 +154,14 @@
if(need_mob_update)
return UPDATE_MOB_HEALTH
/datum/reagent/medicine/c2/probital/on_transfer(atom/A, methods=INGEST, trans_volume)
if(!(methods & INGEST) || (!iscarbon(A) && !istype(A, /obj/item/organ/stomach)) )
/datum/reagent/medicine/c2/probital/expose_mob(mob/living/exposed_mob, methods, reac_volume, show_message, touch_protection)
. = ..()
if(!(methods & INGEST) || !iscarbon(exposed_mob))
return
A.reagents.remove_reagent(/datum/reagent/medicine/c2/probital, trans_volume * 0.05)
A.reagents.add_reagent(/datum/reagent/medicine/metafactor, trans_volume * 0.25)
..()
var/datum/reagents/mob_reagents = exposed_mob.reagents
mob_reagents.remove_reagent(/datum/reagent/medicine/c2/probital, reac_volume * 0.05)
mob_reagents.add_reagent(/datum/reagent/medicine/metafactor, reac_volume * 0.25)
/******BURN******/
/*Suffix: -uri*/
@@ -406,19 +406,21 @@
var/conversion_amount
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
/datum/reagent/medicine/c2/syriniver/on_transfer(atom/A, methods=INJECT, trans_volume)
if(!(methods & INJECT) || !iscarbon(A))
/datum/reagent/medicine/c2/syriniver/expose_mob(mob/living/carbon/exposed_mob, methods, trans_volume, show_message, touch_protection)
. = ..()
if(!(methods & INJECT) || !iscarbon(exposed_mob))
return
var/mob/living/carbon/C = A
if(trans_volume >= 0.4) //prevents cheesing with ultralow doses.
C.adjustToxLoss((-3 * min(2, trans_volume) * REM) * normalise_creation_purity(), required_biotype = affected_biotype) //This is to promote iv pole use for that chemotherapy feel.
var/obj/item/organ/liver/L = C.organs_slot[ORGAN_SLOT_LIVER]
exposed_mob.adjustToxLoss((-3 * min(2, trans_volume) * REM) * normalise_creation_purity(), required_biotype = affected_biotype) //This is to promote iv pole use for that chemotherapy feel.
var/obj/item/organ/liver/L = exposed_mob.organs_slot[ORGAN_SLOT_LIVER]
if(!L || L.organ_flags & ORGAN_FAILING)
return
conversion_amount = (trans_volume * (min(100 -C.get_organ_loss(ORGAN_SLOT_LIVER), 80) / 100)*normalise_creation_purity()) //the more damaged the liver the worse we metabolize.
C.reagents.remove_reagent(/datum/reagent/medicine/c2/syriniver, conversion_amount)
C.reagents.add_reagent(/datum/reagent/medicine/c2/musiver, conversion_amount)
..()
conversion_amount = (trans_volume * (min(100 -exposed_mob.get_organ_loss(ORGAN_SLOT_LIVER), 80) / 100)*normalise_creation_purity()) //the more damaged the liver the worse we metabolize.
var/datum/reagents/mob_reagents = exposed_mob.reagents
mob_reagents.remove_reagent(/datum/reagent/medicine/c2/syriniver, conversion_amount)
mob_reagents.add_reagent(/datum/reagent/medicine/c2/musiver, conversion_amount)
/datum/reagent/medicine/c2/syriniver/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
@@ -501,14 +501,16 @@
return ..()
/datum/reagent/consumable/ethanol/goldschlager/on_transfer(atom/atom, methods = TOUCH, trans_volume)
/datum/reagent/consumable/ethanol/goldschlager/expose_mob(mob/living/exposed_mob, methods, reac_volume)
. = ..()
if(!(methods & INGEST))
return ..()
return
var/convert_amount = trans_volume * min(GOLDSCHLAGER_GOLD_RATIO, 1)
atom.reagents.remove_reagent(/datum/reagent/consumable/ethanol/goldschlager, convert_amount)
atom.reagents.add_reagent(/datum/reagent/gold, convert_amount)
return ..()
var/convert_amount = reac_volume * min(GOLDSCHLAGER_GOLD_RATIO, 1)
var/datum/reagents/mob_reagents = exposed_mob.reagents
mob_reagents.remove_reagent(/datum/reagent/consumable/ethanol/goldschlager, convert_amount)
mob_reagents.add_reagent(/datum/reagent/gold, convert_amount)
/datum/reagent/consumable/ethanol/patron
name = "Patron"
@@ -816,11 +816,11 @@ If you have at over 25u in your body you restore more than 20 stamina per cycle,
if(!kronkaine_fiend.stat)
kronkaine_fiend.stop_sound_channel(CHANNEL_HEARTBEAT)
/datum/reagent/drug/kronkaine/on_transfer(atom/kronkaine_receptacle, methods, trans_volume)
/datum/reagent/drug/kronkaine/expose_mob(mob/living/carbon/druggo, methods, trans_volume, show_message, touch_protection)
. = ..()
if(!iscarbon(kronkaine_receptacle))
if(!iscarbon(druggo))
return
var/mob/living/carbon/druggo = kronkaine_receptacle
//The drug is more effective if smoked or injected, restoring more stamina per unit.
var/stamina_heal_per_unit
if(methods & (INJECT|INHALE))
@@ -833,9 +833,7 @@ If you have at over 25u in your body you restore more than 20 stamina per cycle,
to_chat(druggo, span_nicegreen(pick("You feel the cowardice melt away...", "You feel unbothered by the judgements of others.", "My life feels lovely!", "You lower your snout... and suddenly feel more charitable!")))
else
stamina_heal_per_unit = 6
if(druggo.adjustStaminaLoss(-stamina_heal_per_unit * trans_volume))
return UPDATE_MOB_HEALTH
druggo.adjustStaminaLoss(-stamina_heal_per_unit * trans_volume)
/datum/reagent/drug/kronkaine/on_mob_life(mob/living/carbon/kronkaine_fiend, seconds_per_tick, times_fired)
. = ..()
@@ -97,18 +97,13 @@
chemical_flags = REAGENT_DEAD_PROCESS | REAGENT_IGNORE_STASIS | REAGENT_UNAFFECTED_BY_METABOLISM
metabolization_rate = 1 * REM
/datum/reagent/inverse/cryostylane/on_transfer(atom/transfered_thing, methods, trans_volume)
/datum/reagent/inverse/cryostylane/expose_mob(mob/living/carbon/human/human_thing, methods, reac_volume, show_message, touch_protection)
. = ..()
if(methods & INGEST)
if((methods & INGEST) || !ishuman(human_thing))
return
if(!ishuman(transfered_thing))
return
var/mob/living/carbon/human/human_thing = transfered_thing
if(HAS_TRAIT(human_thing, TRAIT_RESISTCOLD))
holder.remove_reagent(type, volume)
holder.del_reagent(type)
return
human_thing.apply_status_effect(/datum/status_effect/frozenstasis/irresistable)
@@ -2697,15 +2697,15 @@
. = ..()
yuck_cycle = 0 // reset vomiting
/datum/reagent/yuck/on_transfer(atom/A, methods=TOUCH, trans_volume)
if((methods & INGEST) || !iscarbon(A))
return ..()
/datum/reagent/yuck/expose_mob(mob/living/carbon/exposed_mob, methods, expose_volume, show_message, touch_protection)
. = ..()
if(!(methods & INGEST) || !iscarbon(exposed_mob))
return
A.reagents.remove_reagent(type, trans_volume)
A.reagents.add_reagent(/datum/reagent/fuel, trans_volume * 0.75)
A.reagents.add_reagent(/datum/reagent/water, trans_volume * 0.25)
return ..()
var/datum/reagents/mob_reagents = exposed_mob.reagents
mob_reagents.remove_reagent(type, expose_volume)
mob_reagents.add_reagent(/datum/reagent/fuel, expose_volume * 0.75)
mob_reagents.add_reagent(/datum/reagent/water, expose_volume * 0.25)
//monkey powder heehoo
/datum/reagent/monkey_powder
@@ -269,13 +269,15 @@
. = ..()
affected_mob.cure_fakedeath(type)
/datum/reagent/toxin/zombiepowder/on_transfer(atom/target_atom, methods, trans_volume)
/datum/reagent/toxin/zombiepowder/expose_mob(mob/living/exposed_mob, methods, reac_volume, show_message, touch_protection)
. = ..()
var/datum/reagent/zombiepowder = target_atom.reagents.has_reagent(/datum/reagent/toxin/zombiepowder)
if(!zombiepowder || !(methods & (INGEST|INHALE)))
if(!(methods & (INGEST|INHALE)))
return
LAZYINITLIST(zombiepowder.data)
zombiepowder.data["method"] |= (INGEST|INHALE)
var/datum/reagent/zombiepowder = exposed_mob.reagents.has_reagent(/datum/reagent/toxin/zombiepowder)
if(zombiepowder)
LAZYINITLIST(zombiepowder.data)
zombiepowder.data["method"] |= (INGEST|INHALE)
/datum/reagent/toxin/zombiepowder/on_mob_life(mob/living/affected_mob, seconds_per_tick, times_fired)
. = ..()
@@ -450,25 +452,19 @@
taste_description = "spores"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
/datum/reagent/toxin/spore/on_transfer(atom/A, methods, trans_volume)
/datum/reagent/toxin/spore/expose_mob(mob/living/spore_lung_victim, methods, reac_volume, show_message, touch_protection)
. = ..()
if(!isliving(A))
return
if(!(methods & INHALE))
return
var/mob/living/spore_lung_victim = A
if(!(spore_lung_victim.mob_biotypes & (MOB_HUMANOID | MOB_BEAST)))
return
if(prob(min(trans_volume * 10, 80)))
if(prob(min(reac_volume * 10, 80)))
to_chat(spore_lung_victim, span_danger("[pick("You have a coughing fit!", "You hack and cough!", "Your lungs burn!")]"))
spore_lung_victim.Stun(1 SECONDS)
spore_lung_victim.emote("cough")
/datum/reagent/toxin/spore/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
. = ..()
affected_mob.damageoverlaytemp = 60
@@ -67,7 +67,7 @@
to_chat(user, span_warning("You cannot directly fill [target]!"))
return ITEM_INTERACT_BLOCKING
if(target.reagents.total_volume >= target.reagents.maximum_volume)
if(target.reagents.holder_full())
to_chat(user, span_notice("[target] is full."))
return ITEM_INTERACT_BLOCKING
@@ -82,7 +82,7 @@
return ITEM_INTERACT_BLOCKING
if(!reagents.total_volume)
return ITEM_INTERACT_BLOCKING
if(living_target.reagents.total_volume >= living_target.reagents.maximum_volume)
if(living_target.reagents.holder_full())
return ITEM_INTERACT_BLOCKING
living_target.visible_message(
span_danger("[user] injects [living_target] with the syringe!"),
@@ -109,7 +109,7 @@
SEND_SIGNAL(target, COMSIG_LIVING_TRY_SYRINGE_WITHDRAW, user)
if(reagents.total_volume >= reagents.maximum_volume)
if(reagents.holder_full())
to_chat(user, span_notice("[src] is full."))
return ITEM_INTERACT_BLOCKING
@@ -123,7 +123,7 @@
)
if(!do_after(user, CHEM_INTERACT_DELAY(3 SECONDS, user), target, extra_checks = CALLBACK(src, PROC_REF(try_syringe), living_target, user)))
return ITEM_INTERACT_BLOCKING
if(reagents.total_volume >= reagents.maximum_volume)
if(reagents.holder_full())
return ITEM_INTERACT_BLOCKING
if(living_target.transfer_blood_to(src, drawn_amount))
user.visible_message(span_notice("[user] takes a blood sample from [living_target]."))