From f9fe79a307dc55eb6e3ecf25019ef388889898ba Mon Sep 17 00:00:00 2001 From: Dani Glore Date: Sat, 18 Mar 2023 20:20:28 -0400 Subject: [PATCH] Organ Unit Tests & Bugfixes (#73026) ## About The Pull Request This PR adds a new unit test for all organs, a new unit test for lungs, and includes improvements for the existing breath and organ_set_bonus tests. Using the tests, I was able to root out bugs in the organs. This PR includes an advanced refactor of several developer-facing functions. This PR certainly represents a "quality pass" for organs which will make them easier to develop from now on. ### Synopsis of changes: 1. Fixed many fundamental bugs in organ code, especially in `Insert()`/`Remove()` and their overrides. 2. Added two new procs to `/obj/item/organ` named `on_insert` and `on_remove`, each being called after `Insert()`/`Remove()`. 3. Added `organ_effects` lazylist to `/obj/item/organ`. Converted `organ_traits` to lazylist. 2x less empty lists per organ. 4. Adding `SHOULD_CALL_PARENT(TRUE)` to `Insert()`/`Remove()` was very beneficial to stability and overall code health. 5. Created unit test `organ_sanity` for all usable organs in the game. Tests insertion and removal. 6. Created unit test `lungs_sanity` for `/obj/item/organ/internal/lungs`. 7. Improved `breath_sanity` unit tests with additional tests and conditions. 8. Improved `organ_set_bonus_sanity` unit tests with better documentation and maintainable code. --- ### Granular bug/fix list: - A lot of organs are overriding `Insert()` to apply unique side-effects, but aren't checking the return value of the parent proc which causes the activation of side-effects even if the insertion technically fails. I noticed the use-case of applying "unique side-effects" is repeated across a lot of organs in the game, and by overriding `Insert()` the potential for bugs is very high; I solved this problem with inversion-of-control by adding two new procs to `/obj/item/organ` named `on_insert` and `on_remove`, each being called after `Insert()` and `Remove()` succeed. - Many organs, such as abductor "glands", cursed heart, demon heart, alien hive-node, alien plasma-vessel, etc, were not returning their parent's `Insert()` proc return value at all, and as a result those organs `Insert()`s were always returning `null`. I have been mopping those bugs up in my last few PRs, and now the unit test reveals it all. Functions such as those in surgery expect a truthy value to be returned from `Insert()` to represent insertion success, and otherwise it force-moves the organ out of the mob. - Fixed abductor "glands" which had a hard-del bug due to their `Remove()` not calling the parent proc. - Fixed cybernetic arm implants which had a hard-del bug due to `Remove()` not resetting their `hand` variable to `null`. - Fixed lungs gas exchange implementation, which was allowing exhaled gases to feedback into the inhaled gases, which caused Humans to inhale much more gas than intended and not exhale expected gases. ### Overview of the `organ_sanity` unit test: - The new `organ_sanity` unit test gathers all "usable" organs in the game and tests to see if their `Insert()` and `Remove()` functions behave as we expect them to. - Some organs, such as the Nightmare Brain, cause the mob's species to change which subsequently swaps out all of their organs; the unit test accounts for these organs via the typecache `species_changing_organs`. - Some organs are not usable in-game and can't be unit tested, so the unit test accounts for them via the typecache `test_organ_blacklist`. ### Overview of the `lungs_sanity` unit test: - This unit test focuses on `/obj/item/organ/internal/lungs` including Plasmaman and Ashwalker lungs. The test focuses on testing the lungs' `check_breath()` proc. - The tests are composed of calling `check_breath` with different gas mixes to test breathing and suffocation. - Includes gas exchange test for inhaled/exhaled gases, such as O2 to CO2. ### Improvements to the `breath_sanity` unit tests: - Added additional tests for suffocation with empty internals, pure Nitrogen internals, and a gas-less turf. - Includes slightly more reliable tests for internals tanks. ## Why It's Good For The Game **Organs and Lungs were mostly untested. Too many refactors have been submitted without the addition of unit tests to prove the code works at all.** Time to stop. _Time to get some help_. Due to how bad the code health is in organs, any time we've tried to work with them some sort of bug caused them to blow up in our faces. I am trying to fix some of that by establishing some standard testing for organs. These tests have revealed and allowed me to fix lot of basic developer errors/oversights, as well as a few severe bugs. ![image](https://user-images.githubusercontent.com/17753498/220251281-07ef598f-355b-43a9-afd6-1de9690831da.png) ## Changelog :cl: A.C.M.O. fix: Fixed lungs gas exchange implementation, so you always inhale and exhale the correct gases. fix: Fixed a large quantity of hard-deletes which were being caused by organs and cybernetic organs. fix: Fixed many organs which were applying side-effects regardless of whether or not the insertion failed. code: Added unit tests for Organs. code: Added unit tests for Lungs. code: Improved unit tests for breathing. code: Improved unit tests for DNA Infuser organs. /:cl: --- code/__DEFINES/traits.dm | 2 + .../dna_infuser/organ_sets/carp_organs.dm | 8 +- .../dna_infuser/organ_sets/fly_organs.dm | 2 +- .../dna_infuser/organ_sets/fox_organs.dm | 4 +- .../dna_infuser/organ_sets/goliath_organs.dm | 4 +- .../dna_infuser/organ_sets/rat_organs.dm | 14 +- code/game/objects/items/body_egg.dm | 21 +- .../antagonists/abductor/equipment/gland.dm | 13 +- .../abductor/equipment/glands/electric.dm | 10 +- .../abductor/equipment/glands/slime.dm | 14 +- .../antagonists/nightmare/nightmare_organs.dm | 30 +-- .../equipment/monster_organs/monster_organ.dm | 8 +- code/modules/mob/living/brain/brain_item.dm | 46 ++-- .../modules/mob/living/carbon/alien/organs.dm | 6 +- .../carbon/human/species_types/monkeys.dm | 6 +- .../carbon/human/species_types/vampire.dm | 4 +- .../simple_animal/hostile/slaughter_demon.dm | 10 +- code/modules/religion/burdened/psyker.dm | 4 +- code/modules/surgery/organs/_organ.dm | 94 ++++++-- code/modules/surgery/organs/appendix.dm | 13 +- code/modules/surgery/organs/augments_arms.dm | 7 +- code/modules/surgery/organs/augments_eyes.dm | 8 +- .../surgery/organs/augments_internal.dm | 6 +- code/modules/surgery/organs/ears.dm | 8 +- .../organs/external/_external_organs.dm | 4 +- .../organs/external/wings/functional_wings.dm | 3 +- .../organs/external/wings/moth_wings.dm | 6 +- code/modules/surgery/organs/eyes.dm | 26 ++- code/modules/surgery/organs/heart.dm | 25 ++- code/modules/surgery/organs/lungs.dm | 145 ++++++------ .../organs/stomach/stomach_ethereal.dm | 22 +- code/modules/surgery/organs/tongue.dm | 2 + code/modules/unit_tests/_unit_tests.dm | 4 +- code/modules/unit_tests/breath.dm | 142 +++++++----- code/modules/unit_tests/lungs.dm | 212 ++++++++++++++++++ code/modules/unit_tests/organ_set_bonus.dm | 74 +++--- code/modules/unit_tests/organs.dm | 83 +++++++ code/modules/unit_tests/unit_test.dm | 12 +- .../wiremod/shell/brain_computer_interface.dm | 3 +- code/modules/zombie/organs.dm | 2 + 40 files changed, 751 insertions(+), 356 deletions(-) create mode 100644 code/modules/unit_tests/lungs.dm create mode 100644 code/modules/unit_tests/organs.dm diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index f037ba138d2..ab4790a6c4b 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -736,6 +736,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define DISEASE_TRAIT "disease" #define SPECIES_TRAIT "species" #define ORGAN_TRAIT "organ" +/// Trait given by organ gained via abductor surgery +#define ABDUCTOR_GLAND_TRAIT "abductor_gland" /// cannot be removed without admin intervention #define ROUNDSTART_TRAIT "roundstart" #define JOB_TRAIT "job" diff --git a/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm b/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm index 9208ae40fdf..bb6ef0288d9 100644 --- a/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm @@ -48,7 +48,7 @@ AddElement(/datum/element/noticable_organ, "has big sharp teeth.", BODY_ZONE_PRECISE_MOUTH) AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/carp) -/obj/item/organ/internal/tongue/carp/Insert(mob/living/carbon/tongue_owner, special, drop_if_replaced) +/obj/item/organ/internal/tongue/carp/on_insert(mob/living/carbon/tongue_owner) . = ..() if(!ishuman(tongue_owner)) return @@ -62,7 +62,7 @@ head.unarmed_damage_high = 15 head.unarmed_stun_threshold = 15 -/obj/item/organ/internal/tongue/carp/Remove(mob/living/carbon/tongue_owner, special) +/obj/item/organ/internal/tongue/carp/on_remove(mob/living/carbon/tongue_owner) . = ..() if(!ishuman(tongue_owner)) return @@ -110,13 +110,13 @@ AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/carp) AddElement(/datum/element/noticable_organ, "seem%PRONOUN_S unable to stay still.") -/obj/item/organ/internal/brain/carp/Insert(mob/living/carbon/brain_owner, special, drop_if_replaced, no_id_transfer) +/obj/item/organ/internal/brain/carp/on_insert(mob/living/carbon/brain_owner) . = ..() cooldown_timer = addtimer(CALLBACK(src, PROC_REF(unsatisfied_nomad)), cooldown_time, TIMER_STOPPABLE|TIMER_OVERRIDE|TIMER_UNIQUE) RegisterSignal(brain_owner, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(satisfied_nomad)) //technically you could get around the mood issue by extracting and reimplanting the brain but it will be far easier to just go one z there and back -/obj/item/organ/internal/brain/carp/Remove(mob/living/carbon/brain_owner, special, no_id_transfer) +/obj/item/organ/internal/brain/carp/on_remove(mob/living/carbon/brain_owner) . = ..() UnregisterSignal(brain_owner, COMSIG_MOVABLE_Z_CHANGED) deltimer(cooldown_timer) diff --git a/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm b/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm index 2f1f57d5091..74d028d22c6 100644 --- a/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm @@ -10,7 +10,7 @@ /datum/status_effect/organ_set_bonus/fly/enable_bonus() . = ..() - if(!ishuman(owner)) + if(!. || !ishuman(owner)) return var/mob/living/carbon/human/new_fly = owner if(isflyperson(new_fly)) diff --git a/code/game/machinery/dna_infuser/organ_sets/fox_organs.dm b/code/game/machinery/dna_infuser/organ_sets/fox_organs.dm index 24dadcb3b7c..1b2f69f4ccc 100644 --- a/code/game/machinery/dna_infuser/organ_sets/fox_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/fox_organs.dm @@ -6,7 +6,7 @@ visual = TRUE damage_multiplier = 2 -/obj/item/organ/internal/ears/fox/Insert(mob/living/carbon/human/ear_owner, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/internal/ears/fox/on_insert(mob/living/carbon/human/ear_owner) . = ..() if(istype(ear_owner) && ear_owner.dna) color = ear_owner.hair_color @@ -14,7 +14,7 @@ ear_owner.dna.update_uf_block(DNA_EARS_BLOCK) ear_owner.update_body() -/obj/item/organ/internal/ears/fox/Remove(mob/living/carbon/human/ear_owner, special = 0) +/obj/item/organ/internal/ears/fox/on_remove(mob/living/carbon/human/ear_owner) . = ..() if(istype(ear_owner) && ear_owner.dna) color = ear_owner.hair_color diff --git a/code/game/machinery/dna_infuser/organ_sets/goliath_organs.dm b/code/game/machinery/dna_infuser/organ_sets/goliath_organs.dm index c23a8a5307e..87fa08d6eb8 100644 --- a/code/game/machinery/dna_infuser/organ_sets/goliath_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/goliath_organs.dm @@ -66,7 +66,7 @@ AddElement(/datum/element/noticable_organ, "arm is just a mass of plate and tendrils.", BODY_ZONE_CHEST) AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/goliath) -/obj/item/organ/internal/brain/goliath/Insert(mob/living/carbon/brain_owner, special, drop_if_replaced, no_id_transfer) +/obj/item/organ/internal/brain/goliath/on_insert(mob/living/carbon/brain_owner) . = ..() if(!ishuman(brain_owner)) return @@ -78,7 +78,7 @@ hammer = new/obj/item/goliath_infuser_hammer brain_owner.put_in_hands(hammer) -/obj/item/organ/internal/brain/goliath/Remove(mob/living/carbon/brain_owner, special, no_id_transfer) +/obj/item/organ/internal/brain/goliath/on_remove(mob/living/carbon/brain_owner) . = ..() UnregisterSignal(brain_owner) if(!ishuman(brain_owner)) diff --git a/code/game/machinery/dna_infuser/organ_sets/rat_organs.dm b/code/game/machinery/dna_infuser/organ_sets/rat_organs.dm index 56307b09a35..5d9c54fa404 100644 --- a/code/game/machinery/dna_infuser/organ_sets/rat_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/rat_organs.dm @@ -50,7 +50,7 @@ AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/rat) AddElement(/datum/element/noticable_organ, "mouth is drooling excessively.", BODY_ZONE_PRECISE_MOUTH) -/obj/item/organ/internal/stomach/rat/Insert(mob/living/carbon/receiver, special, drop_if_replaced) +/obj/item/organ/internal/stomach/rat/on_insert(mob/living/carbon/receiver) . = ..() if(!ishuman(receiver)) return @@ -73,7 +73,7 @@ new_species.disliked_food = NONE new_species.toxic_food = NONE -/obj/item/organ/internal/stomach/rat/Remove(mob/living/carbon/stomach_owner, special) +/obj/item/organ/internal/stomach/rat/on_remove(mob/living/carbon/stomach_owner) . = ..() if(!ishuman(stomach_owner)) return @@ -103,9 +103,9 @@ AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/rat) AddElement(/datum/element/noticable_organ, "hunch%PRONOUN_ES over unnaturally!") -/obj/item/organ/internal/heart/rat/Insert(mob/living/carbon/receiver, special, drop_if_replaced) +/obj/item/organ/internal/heart/rat/on_insert(mob/living/carbon/receiver) . = ..() - if(!ishuman(receiver)) + if(!. || !ishuman(receiver)) return var/mob/living/carbon/human/human_receiver = receiver if(!human_receiver.can_mutate()) @@ -115,7 +115,7 @@ if(human_receiver.physiology) human_receiver.physiology.damage_resistance -= 50 -/obj/item/organ/internal/heart/rat/Remove(mob/living/carbon/heartless, special) +/obj/item/organ/internal/heart/rat/on_remove(mob/living/carbon/heartless, special) . = ..() if(!ishuman(heartless)) return @@ -151,11 +151,11 @@ if(message == "hi?") speech_args[SPEECH_MESSAGE] = "Um... cheesed to meet you?" -/obj/item/organ/internal/tongue/rat/Insert(mob/living/carbon/tongue_owner, special, drop_if_replaced) +/obj/item/organ/internal/tongue/rat/on_insert(mob/living/carbon/tongue_owner, special, drop_if_replaced) . = ..() RegisterSignal(tongue_owner, COMSIG_CARBON_ITEM_GIVEN, PROC_REF(its_on_the_mouse)) -/obj/item/organ/internal/tongue/rat/Remove(mob/living/carbon/tongue_owner, special) +/obj/item/organ/internal/tongue/rat/on_remove(mob/living/carbon/tongue_owner) . = ..() UnregisterSignal(tongue_owner, COMSIG_CARBON_ITEM_GIVEN) diff --git a/code/game/objects/items/body_egg.dm b/code/game/objects/items/body_egg.dm index fd23ebb82a9..6717d263bf0 100644 --- a/code/game/objects/items/body_egg.dm +++ b/code/game/objects/items/body_egg.dm @@ -15,18 +15,19 @@ if(iscarbon(loc)) Insert(loc) -/obj/item/organ/internal/body_egg/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/body_egg/Insert(mob/living/carbon/egg_owner, special = FALSE, drop_if_replaced = TRUE) . = ..() - owner.add_traits(list(TRAIT_XENO_HOST, TRAIT_XENO_IMMUNE), ORGAN_TRAIT) - owner.med_hud_set_status() - INVOKE_ASYNC(src, PROC_REF(AddInfectionImages), owner) + if(!.) + return + egg_owner.add_traits(list(TRAIT_XENO_HOST, TRAIT_XENO_IMMUNE), ORGAN_TRAIT) + egg_owner.med_hud_set_status() + INVOKE_ASYNC(src, PROC_REF(AddInfectionImages), egg_owner) -/obj/item/organ/internal/body_egg/Remove(mob/living/carbon/M, special = FALSE) - if(owner) - owner.remove_traits(list(TRAIT_XENO_HOST, TRAIT_XENO_IMMUNE), ORGAN_TRAIT) - owner.med_hud_set_status() - INVOKE_ASYNC(src, PROC_REF(RemoveInfectionImages), owner) - return ..() +/obj/item/organ/internal/body_egg/Remove(mob/living/carbon/egg_owner, special = FALSE) + . = ..() + egg_owner.remove_traits(list(TRAIT_XENO_HOST, TRAIT_XENO_IMMUNE), ORGAN_TRAIT) + egg_owner.med_hud_set_status() + INVOKE_ASYNC(src, PROC_REF(RemoveInfectionImages), egg_owner) /obj/item/organ/internal/body_egg/on_death(delta_time, times_fired) . = ..() diff --git a/code/modules/antagonists/abductor/equipment/gland.dm b/code/modules/antagonists/abductor/equipment/gland.dm index 63a72824e41..ca279dbb2d0 100644 --- a/code/modules/antagonists/abductor/equipment/gland.dm +++ b/code/modules/antagonists/abductor/equipment/gland.dm @@ -82,21 +82,24 @@ active_mind_control = FALSE return TRUE -/obj/item/organ/internal/heart/gland/Remove(mob/living/carbon/M, special = FALSE) +/obj/item/organ/internal/heart/gland/Remove(mob/living/carbon/gland_owner, special = FALSE) + . = ..() active = FALSE if(initial(uses) == 1) uses = initial(uses) var/datum/atom_hud/abductor/hud = GLOB.huds[DATA_HUD_ABDUCTOR] - hud.remove_atom_from_hud(owner) + hud.remove_atom_from_hud(gland_owner) clear_mind_control() - return ..() -/obj/item/organ/internal/heart/gland/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/gland/Insert(mob/living/carbon/gland_owner, special = FALSE, drop_if_replaced = TRUE) . = ..() + if(!.) + return + if(special != 2 && uses) // Special 2 means abductor surgery Start() var/datum/atom_hud/abductor/hud = GLOB.huds[DATA_HUD_ABDUCTOR] - hud.add_atom_to_hud(owner) + hud.add_atom_to_hud(gland_owner) update_gland_hud() /obj/item/organ/internal/heart/gland/on_life(delta_time, times_fired) diff --git a/code/modules/antagonists/abductor/equipment/glands/electric.dm b/code/modules/antagonists/abductor/equipment/glands/electric.dm index f935b867143..acb1505a71c 100644 --- a/code/modules/antagonists/abductor/equipment/glands/electric.dm +++ b/code/modules/antagonists/abductor/equipment/glands/electric.dm @@ -7,13 +7,13 @@ mind_control_uses = 2 mind_control_duration = 900 -/obj/item/organ/internal/heart/gland/electric/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/gland/electric/on_insert(mob/living/carbon/gland_owner) . = ..() - ADD_TRAIT(owner, TRAIT_SHOCKIMMUNE, "abductor_gland") + ADD_TRAIT(gland_owner, TRAIT_SHOCKIMMUNE, ABDUCTOR_GLAND_TRAIT) -/obj/item/organ/internal/heart/gland/electric/Remove(mob/living/carbon/M, special = FALSE) - REMOVE_TRAIT(owner, TRAIT_SHOCKIMMUNE, "abductor_gland") - return ..() +/obj/item/organ/internal/heart/gland/electric/on_remove(mob/living/carbon/gland_owner) + . = ..() + REMOVE_TRAIT(gland_owner, TRAIT_SHOCKIMMUNE, ABDUCTOR_GLAND_TRAIT) /obj/item/organ/internal/heart/gland/electric/activate() owner.visible_message(span_danger("[owner]'s skin starts emitting electric arcs!"),\ diff --git a/code/modules/antagonists/abductor/equipment/glands/slime.dm b/code/modules/antagonists/abductor/equipment/glands/slime.dm index ec99f4b4cc6..50e83ee3735 100644 --- a/code/modules/antagonists/abductor/equipment/glands/slime.dm +++ b/code/modules/antagonists/abductor/equipment/glands/slime.dm @@ -7,15 +7,15 @@ mind_control_uses = 1 mind_control_duration = 2400 -/obj/item/organ/internal/heart/gland/slime/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/gland/slime/on_insert(mob/living/carbon/gland_owner) . = ..() - owner.faction |= FACTION_SLIME - owner.grant_language(/datum/language/slime, TRUE, TRUE, LANGUAGE_GLAND) + gland_owner.faction |= FACTION_SLIME + gland_owner.grant_language(/datum/language/slime, TRUE, TRUE, LANGUAGE_GLAND) -/obj/item/organ/internal/heart/gland/slime/Remove(mob/living/carbon/M, special = FALSE) - owner.faction -= FACTION_SLIME - owner.remove_language(/datum/language/slime, TRUE, TRUE, LANGUAGE_GLAND) - return ..() +/obj/item/organ/internal/heart/gland/slime/on_remove(mob/living/carbon/gland_owner) + . = ..() + gland_owner.faction -= FACTION_SLIME + gland_owner.remove_language(/datum/language/slime, TRUE, TRUE, LANGUAGE_GLAND) /obj/item/organ/internal/heart/gland/slime/activate() to_chat(owner, span_warning("You feel nauseated!")) diff --git a/code/modules/antagonists/nightmare/nightmare_organs.dm b/code/modules/antagonists/nightmare/nightmare_organs.dm index 18d3c041950..205e66c5dfc 100644 --- a/code/modules/antagonists/nightmare/nightmare_organs.dm +++ b/code/modules/antagonists/nightmare/nightmare_organs.dm @@ -14,23 +14,23 @@ ///Our associated terrorize spell, for antagonist nightmares var/datum/action/cooldown/spell/pointed/terrorize/terrorize_spell -/obj/item/organ/internal/brain/shadow/nightmare/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE, no_id_transfer = FALSE) +/obj/item/organ/internal/brain/shadow/nightmare/on_insert(mob/living/carbon/brain_owner) . = ..() - if(M.dna.species.id != SPECIES_NIGHTMARE) - M.set_species(/datum/species/shadow/nightmare) - visible_message(span_warning("[M] thrashes as [src] takes root in [M.p_their()] body!")) + if(brain_owner.dna.species.id != SPECIES_NIGHTMARE) + brain_owner.set_species(/datum/species/shadow/nightmare) + visible_message(span_warning("[brain_owner] thrashes as [src] takes root in [brain_owner.p_their()] body!")) - our_jaunt = new(M) - our_jaunt.Grant(M) + our_jaunt = new(brain_owner) + our_jaunt.Grant(brain_owner) - if(M.mind?.has_antag_datum(/datum/antagonist/nightmare)) //Only a TRUE NIGHTMARE is worthy of using this ability + if(brain_owner.mind?.has_antag_datum(/datum/antagonist/nightmare)) //Only a TRUE NIGHTMARE is worthy of using this ability terrorize_spell = new(src) - terrorize_spell.Grant(M) + terrorize_spell.Grant(brain_owner) -/obj/item/organ/internal/brain/shadow/nightmare/Remove(mob/living/carbon/M, special = FALSE, no_id_transfer = FALSE) +/obj/item/organ/internal/brain/shadow/nightmare/on_remove(mob/living/carbon/brain_owner) + . = ..() QDEL_NULL(our_jaunt) QDEL_NULL(terrorize_spell) - return ..() /obj/item/organ/internal/heart/nightmare name = "heart of darkness" @@ -65,18 +65,18 @@ user.temporarilyRemoveItemFromInventory(src, TRUE) Insert(user) -/obj/item/organ/internal/heart/nightmare/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/nightmare/on_insert(mob/living/carbon/heart_owner, special) . = ..() if(special != HEART_SPECIAL_SHADOWIFY) blade = new/obj/item/light_eater - M.put_in_hands(blade) + heart_owner.put_in_hands(blade) -/obj/item/organ/internal/heart/nightmare/Remove(mob/living/carbon/M, special = FALSE) +/obj/item/organ/internal/heart/nightmare/on_remove(mob/living/carbon/heart_owner, special) + . = ..() respawn_progress = 0 if(blade && special != HEART_SPECIAL_SHADOWIFY) - M.visible_message(span_warning("\The [blade] disintegrates!")) + heart_owner.visible_message(span_warning("\The [blade] disintegrates!")) QDEL_NULL(blade) - return ..() /obj/item/organ/internal/heart/nightmare/Stop() return 0 diff --git a/code/modules/mining/equipment/monster_organs/monster_organ.dm b/code/modules/mining/equipment/monster_organs/monster_organ.dm index 931dc7a73b6..22837383d31 100644 --- a/code/modules/mining/equipment/monster_organs/monster_organ.dm +++ b/code/modules/mining/equipment/monster_organs/monster_organ.dm @@ -69,18 +69,18 @@ deltimer(decay_timer) return ..() -/obj/item/organ/internal/monster_core/Insert(mob/living/carbon/target_carbon, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/internal/monster_core/Insert(mob/living/carbon/target_carbon, special = FALSE, drop_if_replaced = TRUE) . = ..() - if (!.) + if(!.) return if (inert) - to_chat(owner, span_notice("[src] breaks down as you try to insert it.")) + to_chat(target_carbon, span_notice("[src] breaks down as you try to insert it.")) qdel(src) return FALSE if (!decay_timer) return TRUE preserve(TRUE) - owner.visible_message(span_notice("[src] stabilizes as it's inserted.")) + target_carbon.visible_message(span_notice("[src] stabilizes as it's inserted.")) return TRUE /obj/item/organ/internal/monster_core/Remove(mob/living/carbon/target_carbon, special = 0) diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm index 1fb9a1bb912..6f0ef016b99 100644 --- a/code/modules/mob/living/brain/brain_item.dm +++ b/code/modules/mob/living/brain/brain_item.dm @@ -39,57 +39,59 @@ /// Maximum skillchip slots available. Do not reference this var directly and instead call get_max_skillchip_slots() var/max_skillchip_slots = 5 -/obj/item/organ/internal/brain/Insert(mob/living/carbon/C, special = FALSE, drop_if_replaced = TRUE, no_id_transfer = FALSE) +/obj/item/organ/internal/brain/Insert(mob/living/carbon/brain_owner, special = FALSE, drop_if_replaced = TRUE, no_id_transfer = FALSE) . = ..() + if(!.) + return name = initial(name) - if(C.mind && C.mind.has_antag_datum(/datum/antagonist/changeling) && !no_id_transfer) //congrats, you're trapped in a body you don't control - if(brainmob && !(C.stat == DEAD || (HAS_TRAIT(C, TRAIT_DEATHCOMA)))) + if(brain_owner.mind && brain_owner.mind.has_antag_datum(/datum/antagonist/changeling) && !no_id_transfer) //congrats, you're trapped in a body you don't control + if(brainmob && !(brain_owner.stat == DEAD || (HAS_TRAIT(brain_owner, TRAIT_DEATHCOMA)))) to_chat(brainmob, span_danger("You can't feel your body! You're still just a brain!")) - forceMove(C) - C.update_body_parts() + forceMove(brain_owner) + brain_owner.update_body_parts() return if(brainmob) - if(C.key) - C.ghostize() + if(brain_owner.key) + brain_owner.ghostize() if(brainmob.mind) - brainmob.mind.transfer_to(C) + brainmob.mind.transfer_to(brain_owner) else - C.key = brainmob.key + brain_owner.key = brainmob.key - C.set_suicide(brainmob.suiciding) + brain_owner.set_suicide(brainmob.suiciding) QDEL_NULL(brainmob) else - C.set_suicide(suicided) + brain_owner.set_suicide(suicided) for(var/datum/brain_trauma/trauma as anything in traumas) if(trauma.owner) - if(trauma.owner == owner) + if(trauma.owner == brain_owner) // if we're being special replaced, the trauma is already applied, so this is expected // but if we're not... this is likely a bug, and should be reported if(!special) - stack_trace("A brain trauma ([trauma]) is being re-applied to its owning mob ([owner])!") + stack_trace("A brain trauma ([trauma]) is being re-applied to its owning mob ([brain_owner])!") continue - stack_trace("A brain trauma ([trauma]) is being applied to a new mob ([owner]) when it's owned by someone else ([trauma.owner])!") + stack_trace("A brain trauma ([trauma]) is being applied to a new mob ([brain_owner]) when it's owned by someone else ([trauma.owner])!") continue - trauma.owner = owner + trauma.owner = brain_owner trauma.on_gain() //Update the body's icon so it doesnt appear debrained anymore - C.update_body_parts() + brain_owner.update_body_parts() -/obj/item/organ/internal/brain/Remove(mob/living/carbon/C, special = 0, no_id_transfer = FALSE) +/obj/item/organ/internal/brain/Remove(mob/living/carbon/brain_owner, special = 0, no_id_transfer = FALSE) // Delete skillchips first as parent proc sets owner to null, and skillchips need to know the brain's owner. - if(!QDELETED(C) && length(skillchips)) + if(!QDELETED(brain_owner) && length(skillchips)) if(!special) - to_chat(C, span_notice("You feel your skillchips enable emergency power saving mode, deactivating as your brain leaves your body...")) + to_chat(brain_owner, span_notice("You feel your skillchips enable emergency power saving mode, deactivating as your brain leaves your body...")) for(var/chip in skillchips) var/obj/item/skillchip/skillchip = chip // Run the try_ proc with force = TRUE. @@ -103,9 +105,9 @@ BT.owner = null if((!gc_destroyed || (owner && !owner.gc_destroyed)) && !no_id_transfer) - transfer_identity(C) - C.update_body_parts() - C.clear_mood_event("brain_damage") + transfer_identity(brain_owner) + brain_owner.update_body_parts() + brain_owner.clear_mood_event("brain_damage") /obj/item/organ/internal/brain/proc/transfer_identity(mob/living/L) name = "[L.name]'s [initial(name)]" diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm index b91abbce0ec..cb1f5c257c8 100644 --- a/code/modules/mob/living/carbon/alien/organs.dm +++ b/code/modules/mob/living/carbon/alien/organs.dm @@ -67,14 +67,14 @@ else owner.adjustPlasma(0.1 * plasma_rate * delta_time) -/obj/item/organ/internal/alien/plasmavessel/Insert(mob/living/carbon/organ_owner, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/alien/plasmavessel/on_insert(mob/living/carbon/organ_owner) . = ..() if(isalien(organ_owner)) var/mob/living/carbon/alien/target_alien = organ_owner target_alien.updatePlasmaDisplay() RegisterSignal(organ_owner, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) -/obj/item/organ/internal/alien/plasmavessel/Remove(mob/living/carbon/organ_owner, special = FALSE) +/obj/item/organ/internal/alien/plasmavessel/on_remove(mob/living/carbon/organ_owner) . = ..() if(isalien(organ_owner)) var/mob/living/carbon/alien/organ_owner_alien = organ_owner @@ -97,7 +97,7 @@ /// Indicates if the queen died recently, aliens are heavily weakened while this is active. var/recent_queen_death = FALSE -/obj/item/organ/internal/alien/hivenode/Insert(mob/living/carbon/organ_owner, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/alien/hivenode/on_insert(mob/living/carbon/organ_owner) . = ..() organ_owner.faction |= ROLE_ALIEN ADD_TRAIT(organ_owner, TRAIT_XENO_IMMUNE, ORGAN_TRAIT) diff --git a/code/modules/mob/living/carbon/human/species_types/monkeys.dm b/code/modules/mob/living/carbon/human/species_types/monkeys.dm index f1f6f1808be..4ad0895d6c3 100644 --- a/code/modules/mob/living/carbon/human/species_types/monkeys.dm +++ b/code/modules/mob/living/carbon/human/species_types/monkeys.dm @@ -232,13 +232,13 @@ build_all_button_icons() -/obj/item/organ/internal/brain/primate/Insert(mob/living/carbon/primate, special = FALSE, drop_if_replaced = FALSE, no_id_transfer = FALSE) +/obj/item/organ/internal/brain/primate/on_insert(mob/living/carbon/primate) . = ..() RegisterSignal(primate, COMSIG_MOVABLE_CROSS, PROC_REF(on_crossed), TRUE) -/obj/item/organ/internal/brain/primate/Remove(mob/living/carbon/primate, special = FALSE, no_id_transfer = FALSE) +/obj/item/organ/internal/brain/primate/on_remove(mob/living/carbon/primate) + . = ..() UnregisterSignal(primate, COMSIG_MOVABLE_CROSS) - return ..() /obj/item/organ/internal/brain/primate/proc/on_crossed(datum/source, atom/movable/crossed) SIGNAL_HANDLER diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index a513a340718..c085aa7171e 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -196,11 +196,11 @@ name = "vampire heart" color = "#1C1C1C" -/obj/item/organ/internal/heart/vampire/Insert(mob/living/carbon/receiver, special, drop_if_replaced) +/obj/item/organ/internal/heart/vampire/on_insert(mob/living/carbon/receiver) . = ..() RegisterSignal(receiver, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) -/obj/item/organ/internal/heart/vampire/Remove(mob/living/carbon/heartless, special) +/obj/item/organ/internal/heart/vampire/on_remove(mob/living/carbon/heartless) . = ..() UnregisterSignal(heartless, COMSIG_MOB_GET_STATUS_TAB_ITEMS) diff --git a/code/modules/mob/living/simple_animal/hostile/slaughter_demon.dm b/code/modules/mob/living/simple_animal/hostile/slaughter_demon.dm index 671fbf0ff10..2c19e101e36 100644 --- a/code/modules/mob/living/simple_animal/hostile/slaughter_demon.dm +++ b/code/modules/mob/living/simple_animal/hostile/slaughter_demon.dm @@ -180,15 +180,15 @@ user.temporarilyRemoveItemFromInventory(src, TRUE) src.Insert(user) //Consuming the heart literally replaces your heart with a demon heart. H A R D C O R E -/obj/item/organ/internal/heart/demon/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/demon/on_insert(mob/living/carbon/heart_owner) . = ..() // Gives a non-eat-people crawl to the new owner - var/datum/action/cooldown/spell/jaunt/bloodcrawl/crawl = new(M) - crawl.Grant(M) + var/datum/action/cooldown/spell/jaunt/bloodcrawl/crawl = new(heart_owner) + crawl.Grant(heart_owner) -/obj/item/organ/internal/heart/demon/Remove(mob/living/carbon/M, special = FALSE) +/obj/item/organ/internal/heart/demon/on_remove(mob/living/carbon/heart_owner, special = FALSE) . = ..() - var/datum/action/cooldown/spell/jaunt/bloodcrawl/crawl = locate() in M.actions + var/datum/action/cooldown/spell/jaunt/bloodcrawl/crawl = locate() in heart_owner.actions qdel(crawl) /obj/item/organ/internal/heart/demon/Stop() diff --git a/code/modules/religion/burdened/psyker.dm b/code/modules/religion/burdened/psyker.dm index f99e687448e..b5bc45a9e0b 100644 --- a/code/modules/religion/burdened/psyker.dm +++ b/code/modules/religion/burdened/psyker.dm @@ -10,12 +10,12 @@ organ_traits = list(TRAIT_ADVANCEDTOOLUSER, TRAIT_LITERATE, TRAIT_CAN_STRIP, TRAIT_ANTIMAGIC_NO_SELFBLOCK) w_class = WEIGHT_CLASS_NORMAL -/obj/item/organ/internal/brain/psyker/Insert(mob/living/carbon/inserted_into, special, drop_if_replaced, no_id_transfer) +/obj/item/organ/internal/brain/psyker/on_insert(mob/living/carbon/inserted_into) . = ..() inserted_into.AddComponent(/datum/component/echolocation, blocking_trait = TRAIT_DUMB, echo_group = "psyker", echo_icon = "psyker", color_path = /datum/client_colour/psyker) inserted_into.AddComponent(/datum/component/anti_magic, antimagic_flags = MAGIC_RESISTANCE_MIND) -/obj/item/organ/internal/brain/psyker/Remove(mob/living/carbon/removed_from, special, no_id_transfer) +/obj/item/organ/internal/brain/psyker/on_remove(mob/living/carbon/removed_from) . = ..() qdel(removed_from.GetComponent(/datum/component/echolocation)) qdel(removed_from.GetComponent(/datum/component/anti_magic)) diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm index 033533a10af..2b7c3f447bd 100644 --- a/code/modules/surgery/organs/_organ.dm +++ b/code/modules/surgery/organs/_organ.dm @@ -42,7 +42,9 @@ ///Do we effect the appearance of our mob. Used to save time in preference code var/visual = TRUE /// Traits that are given to the holder of the organ. If you want an effect that changes this, don't add directly to this. Use the add_organ_trait() proc - var/list/organ_traits = list() + var/list/organ_traits + /// Status Effects that are given to the holder of the organ. + var/list/organ_effects // Players can look at prefs before atoms SS init, and without this // they would not be able to see external organs, such as moth wings. @@ -67,6 +69,8 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) * drop_if_replaced - if there's an organ in the slot already, whether we drop it afterwards */ /obj/item/organ/proc/Insert(mob/living/carbon/receiver, special = FALSE, drop_if_replaced = TRUE) + SHOULD_CALL_PARENT(TRUE) + if(!iscarbon(receiver) || owner == receiver) return FALSE @@ -82,17 +86,32 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) receiver.organs_slot[slot] = src owner = receiver - moveToNullspace() - RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_owner_examine)) - update_organ_traits(receiver) - for(var/datum/action/action as anything in actions) - action.Grant(receiver) - - SEND_SIGNAL(src, COMSIG_ORGAN_IMPLANTED, receiver) - SEND_SIGNAL(receiver, COMSIG_CARBON_GAIN_ORGAN, src, special) + // Apply unique side-effects. Return value does not matter. + on_insert(receiver, special) return TRUE +/// Called after the organ is inserted into a mob. +/// Adds Traits, Actions, and Status Effects on the mob in which the organ is impanted. +/// Override this proc to create unique side-effects for inserting your organ. Must be called by overrides. +/obj/item/organ/proc/on_insert(mob/living/carbon/organ_owner, special) + SHOULD_CALL_PARENT(TRUE) + + moveToNullspace() + + for(var/trait in organ_traits) + ADD_TRAIT(organ_owner, trait, REF(src)) + + for(var/datum/action/action as anything in actions) + action.Grant(organ_owner) + + for(var/datum/status_effect/effect as anything in organ_effects) + organ_owner.apply_status_effect(effect, type) + + RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_owner_examine)) + SEND_SIGNAL(src, COMSIG_ORGAN_IMPLANTED, organ_owner) + SEND_SIGNAL(organ_owner, COMSIG_CARBON_GAIN_ORGAN, src, special) + /* * Remove the organ from the select mob. * @@ -100,37 +119,66 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) * * special - "quick swapping" an organ out - when TRUE, the mob will be unaffected by not having that organ for the moment */ /obj/item/organ/proc/Remove(mob/living/carbon/organ_owner, special = FALSE) - - UnregisterSignal(organ_owner, COMSIG_PARENT_EXAMINE) + SHOULD_CALL_PARENT(TRUE) organ_owner.organs -= src if(organ_owner.organs_slot[slot] == src) organ_owner.organs_slot.Remove(slot) owner = null + + // Apply or reset unique side-effects. Return value does not matter. + on_remove(organ_owner, special) + + return TRUE + +/// Called after the organ is removed from a mob. +/// Removes Traits, Actions, and Status Effects on the mob in which the organ was impanted. +/// Override this proc to create unique side-effects for removing your organ. Must be called by overrides. +/obj/item/organ/proc/on_remove(mob/living/carbon/organ_owner, special) + SHOULD_CALL_PARENT(TRUE) + + for(var/trait in organ_traits) + REMOVE_TRAIT(organ_owner, trait, REF(src)) + for(var/datum/action/action as anything in actions) action.Remove(organ_owner) - if(length(organ_traits)) - organ_owner.remove_traits(organ_traits, REF(src)) + + for(var/datum/status_effect/effect as anything in organ_effects) + organ_owner.remove_status_effect(effect, type) + UnregisterSignal(organ_owner, COMSIG_PARENT_EXAMINE) SEND_SIGNAL(src, COMSIG_ORGAN_REMOVED, organ_owner) SEND_SIGNAL(organ_owner, COMSIG_CARBON_LOSE_ORGAN, src, special) -/// Updates the traits of the organ on the specific organ it is called on. Should be called anytime an organ is given a trait while it is already in a body. -/obj/item/organ/proc/update_organ_traits() - if(length(organ_traits)) - owner.add_traits(organ_traits, REF(src)) - -/// Add a trait to an organ that it will give its owner. +/// Add a Trait to an organ that it will give its owner. /obj/item/organ/proc/add_organ_trait(trait) - organ_traits += trait - update_organ_traits() + LAZYADD(organ_traits, trait) + if(isnull(owner)) + return + ADD_TRAIT(owner, trait, REF(src)) -/// Removes a trait from an organ, and by extension, its owner. +/// Removes a Trait from an organ, and by extension, its owner. /obj/item/organ/proc/remove_organ_trait(trait) - organ_traits -= trait + LAZYREMOVE(organ_traits, trait) + if(isnull(owner)) + return REMOVE_TRAIT(owner, trait, REF(src)) +/// Add a Status Effect to an organ that it will give its owner. +/obj/item/organ/proc/add_organ_status(status) + LAZYADD(organ_effects, status) + if(isnull(owner)) + return + owner.apply_status_effect(status, type) + +/// Removes a Status Effect from an organ, and by extension, its owner. +/obj/item/organ/proc/remove_organ_status(status) + LAZYREMOVE(organ_effects, status) + if(isnull(owner)) + return + owner.remove_status_effect(status, type) + /obj/item/organ/proc/on_owner_examine(datum/source, mob/user, list/examine_list) SIGNAL_HANDLER return diff --git a/code/modules/surgery/organs/appendix.dm b/code/modules/surgery/organs/appendix.dm index cbdca6fe6a1..3f98c660cac 100644 --- a/code/modules/surgery/organs/appendix.dm +++ b/code/modules/surgery/organs/appendix.dm @@ -72,17 +72,16 @@ /obj/item/organ/internal/appendix/get_availability(datum/species/owner_species, mob/living/owner_mob) return owner_species.mutantappendix -/obj/item/organ/internal/appendix/Remove(mob/living/carbon/organ_owner, special = FALSE) +/obj/item/organ/internal/appendix/on_remove(mob/living/carbon/organ_owner) + . = ..() REMOVE_TRAIT(organ_owner, TRAIT_DISEASELIKE_SEVERITY_MEDIUM, type) organ_owner.med_hud_set_status() - ..() -/obj/item/organ/internal/appendix/Insert(mob/living/carbon/organ_owner, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/appendix/on_insert(mob/living/carbon/organ_owner) . = ..() - if(.) - if(inflamation_stage) - ADD_TRAIT(organ_owner, TRAIT_DISEASELIKE_SEVERITY_MEDIUM, type) - organ_owner.med_hud_set_status() + if(inflamation_stage) + ADD_TRAIT(organ_owner, TRAIT_DISEASELIKE_SEVERITY_MEDIUM, type) + organ_owner.med_hud_set_status() /obj/item/organ/internal/appendix/get_status_text() if((!(organ_flags & ORGAN_FAILING)) && inflamation_stage) diff --git a/code/modules/surgery/organs/augments_arms.dm b/code/modules/surgery/organs/augments_arms.dm index c211cb6edd0..94911bb39c9 100644 --- a/code/modules/surgery/organs/augments_arms.dm +++ b/code/modules/surgery/organs/augments_arms.dm @@ -73,7 +73,7 @@ to_chat(user, span_notice("You modify [src] to be installed on the [zone == BODY_ZONE_R_ARM ? "right" : "left"] arm.")) update_appearance() -/obj/item/organ/internal/cyberimp/arm/Insert(mob/living/carbon/arm_owner, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/cyberimp/arm/on_insert(mob/living/carbon/arm_owner) . = ..() var/side = zone == BODY_ZONE_R_ARM? RIGHT_HANDS : LEFT_HANDS hand = arm_owner.hand_bodyparts[side] @@ -81,12 +81,13 @@ RegisterSignal(hand, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_item_attack_self)) //If the limb gets an attack-self, open the menu. Only happens when hand is empty RegisterSignal(arm_owner, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(dropkey)) //We're nodrop, but we'll watch for the drop hotkey anyway and then stow if possible. -/obj/item/organ/internal/cyberimp/arm/Remove(mob/living/carbon/arm_owner, special = 0) +/obj/item/organ/internal/cyberimp/arm/on_remove(mob/living/carbon/arm_owner) + . = ..() Retract() if(hand) UnregisterSignal(hand, COMSIG_ITEM_ATTACK_SELF) UnregisterSignal(arm_owner, COMSIG_KB_MOB_DROPITEM_DOWN) - return ..() + hand = null /obj/item/organ/internal/cyberimp/arm/proc/on_item_attack_self() SIGNAL_HANDLER diff --git a/code/modules/surgery/organs/augments_eyes.dm b/code/modules/surgery/organs/augments_eyes.dm index 24e03fd825a..3e9f48e37ab 100644 --- a/code/modules/surgery/organs/augments_eyes.dm +++ b/code/modules/surgery/organs/augments_eyes.dm @@ -15,21 +15,23 @@ var/HUD_type = 0 var/HUD_trait = null -/obj/item/organ/internal/cyberimp/eyes/hud/Insert(mob/living/carbon/eye_owner, special = 0, drop_if_replaced = FALSE) +/obj/item/organ/internal/cyberimp/eyes/hud/Insert(mob/living/carbon/eye_owner, special = FALSE, drop_if_replaced = TRUE) . = ..() + if(!.) + return if(HUD_type) var/datum/atom_hud/hud = GLOB.huds[HUD_type] hud.show_to(eye_owner) if(HUD_trait) ADD_TRAIT(eye_owner, HUD_trait, ORGAN_TRAIT) -/obj/item/organ/internal/cyberimp/eyes/hud/Remove(mob/living/carbon/eye_owner, special = 0) +/obj/item/organ/internal/cyberimp/eyes/hud/Remove(mob/living/carbon/eye_owner, special = FALSE) + . = ..() if(HUD_type) var/datum/atom_hud/hud = GLOB.huds[HUD_type] hud.hide_from(eye_owner) if(HUD_trait) REMOVE_TRAIT(eye_owner, HUD_trait, ORGAN_TRAIT) - return ..() /obj/item/organ/internal/cyberimp/eyes/hud/medical name = "Medical HUD implant" diff --git a/code/modules/surgery/organs/augments_internal.dm b/code/modules/surgery/organs/augments_internal.dm index 6c6143464d4..2cb1e29e4e2 100644 --- a/code/modules/surgery/organs/augments_internal.dm +++ b/code/modules/surgery/organs/augments_internal.dm @@ -114,13 +114,13 @@ var/stun_cap_amount = 40 -/obj/item/organ/internal/cyberimp/brain/anti_stun/Remove(mob/living/carbon/implant_owner, special = FALSE) +/obj/item/organ/internal/cyberimp/brain/anti_stun/on_remove(mob/living/carbon/implant_owner) . = ..() UnregisterSignal(implant_owner, signalCache) -/obj/item/organ/internal/cyberimp/brain/anti_stun/Insert(special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/cyberimp/brain/anti_stun/on_insert(mob/living/carbon/receiver) . = ..() - RegisterSignals(owner, signalCache, PROC_REF(on_signal)) + RegisterSignals(receiver, signalCache, PROC_REF(on_signal)) /obj/item/organ/internal/cyberimp/brain/anti_stun/proc/on_signal(datum/source, amount) SIGNAL_HANDLER diff --git a/code/modules/surgery/organs/ears.dm b/code/modules/surgery/organs/ears.dm index 44eb8b0215b..13e5395cd28 100644 --- a/code/modules/surgery/organs/ears.dm +++ b/code/modules/surgery/organs/ears.dm @@ -68,7 +68,7 @@ visual = TRUE damage_multiplier = 2 -/obj/item/organ/internal/ears/cat/Insert(mob/living/carbon/human/ear_owner, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/internal/ears/cat/on_insert(mob/living/carbon/human/ear_owner) . = ..() if(istype(ear_owner) && ear_owner.dna) color = ear_owner.hair_color @@ -76,7 +76,7 @@ ear_owner.dna.update_uf_block(DNA_EARS_BLOCK) ear_owner.update_body() -/obj/item/organ/internal/ears/cat/Remove(mob/living/carbon/human/ear_owner, special = 0) +/obj/item/organ/internal/ears/cat/on_remove(mob/living/carbon/human/ear_owner) . = ..() if(istype(ear_owner) && ear_owner.dna) color = ear_owner.hair_color @@ -87,13 +87,13 @@ name = "penguin ears" desc = "The source of a penguin's happy feet." -/obj/item/organ/internal/ears/penguin/Insert(mob/living/carbon/human/ear_owner, special = 0, drop_if_replaced = TRUE) +/obj/item/organ/internal/ears/penguin/on_insert(mob/living/carbon/human/ear_owner) . = ..() if(istype(ear_owner)) to_chat(ear_owner, span_notice("You suddenly feel like you've lost your balance.")) ear_owner.AddElement(/datum/element/waddling) -/obj/item/organ/internal/ears/penguin/Remove(mob/living/carbon/human/ear_owner, special = 0) +/obj/item/organ/internal/ears/penguin/on_remove(mob/living/carbon/human/ear_owner) . = ..() if(istype(ear_owner)) to_chat(ear_owner, span_notice("Your sense of balance comes back to you.")) diff --git a/code/modules/surgery/organs/external/_external_organs.dm b/code/modules/surgery/organs/external/_external_organs.dm index 94e4722e663..3f34ae13478 100644 --- a/code/modules/surgery/organs/external/_external_organs.dm +++ b/code/modules/surgery/organs/external/_external_organs.dm @@ -266,13 +266,13 @@ /obj/item/organ/external/antennae/Insert(mob/living/carbon/receiver, special, drop_if_replaced) . = ..() - + if(!.) + return RegisterSignal(receiver, COMSIG_HUMAN_BURNING, PROC_REF(try_burn_antennae)) RegisterSignal(receiver, COMSIG_LIVING_POST_FULLY_HEAL, PROC_REF(heal_antennae)) /obj/item/organ/external/antennae/Remove(mob/living/carbon/organ_owner, special, moving) . = ..() - UnregisterSignal(organ_owner, list(COMSIG_HUMAN_BURNING, COMSIG_LIVING_POST_FULLY_HEAL)) ///check if our antennae can burn off ;_; diff --git a/code/modules/surgery/organs/external/wings/functional_wings.dm b/code/modules/surgery/organs/external/wings/functional_wings.dm index 7a7cf18d8bf..335902b44da 100644 --- a/code/modules/surgery/organs/external/wings/functional_wings.dm +++ b/code/modules/surgery/organs/external/wings/functional_wings.dm @@ -28,8 +28,7 @@ /obj/item/organ/external/wings/functional/Insert(mob/living/carbon/receiver, special, drop_if_replaced) . = ..() - - if(isnull(fly)) + if(. && isnull(fly)) fly = new fly.Grant(receiver) diff --git a/code/modules/surgery/organs/external/wings/moth_wings.dm b/code/modules/surgery/organs/external/wings/moth_wings.dm index 8be02c29272..f13b346ab07 100644 --- a/code/modules/surgery/organs/external/wings/moth_wings.dm +++ b/code/modules/surgery/organs/external/wings/moth_wings.dm @@ -14,16 +14,14 @@ ///Store our old datum here for if our burned wings are healed var/original_sprite_datum -/obj/item/organ/external/wings/moth/Insert(mob/living/carbon/receiver, special, drop_if_replaced) +/obj/item/organ/external/wings/moth/on_insert(mob/living/carbon/receiver) . = ..() - RegisterSignal(receiver, COMSIG_HUMAN_BURNING, PROC_REF(try_burn_wings)) RegisterSignal(receiver, COMSIG_LIVING_POST_FULLY_HEAL, PROC_REF(heal_wings)) RegisterSignal(receiver, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(update_float_move)) -/obj/item/organ/external/wings/moth/Remove(mob/living/carbon/organ_owner, special, moving) +/obj/item/organ/external/wings/moth/on_remove(mob/living/carbon/organ_owner) . = ..() - UnregisterSignal(organ_owner, list(COMSIG_HUMAN_BURNING, COMSIG_LIVING_POST_FULLY_HEAL, COMSIG_MOVABLE_PRE_MOVE)) REMOVE_TRAIT(organ_owner, TRAIT_FREE_FLOAT_MOVEMENT, REF(src)) diff --git a/code/modules/surgery/organs/eyes.dm b/code/modules/surgery/organs/eyes.dm index efe36ba11ec..f3b6d85a31b 100644 --- a/code/modules/surgery/organs/eyes.dm +++ b/code/modules/surgery/organs/eyes.dm @@ -52,6 +52,8 @@ /obj/item/organ/internal/eyes/Insert(mob/living/carbon/eye_owner, special = FALSE, drop_if_replaced = FALSE) . = ..() + if(!.) + return owner.cure_blind(NO_EYES) apply_damaged_eye_effects() refresh() @@ -292,13 +294,13 @@ eye_color_right = "000" sight_flags = SEE_MOBS | SEE_OBJS | SEE_TURFS -/obj/item/organ/internal/eyes/robotic/xray/Insert(mob/living/carbon/eye_owner, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/eyes/robotic/xray/on_insert(mob/living/carbon/eye_owner) . = ..() ADD_TRAIT(eye_owner, TRAIT_XRAY_VISION, ORGAN_TRAIT) -/obj/item/organ/internal/eyes/robotic/xray/Remove(mob/living/carbon/eye_owner, special = FALSE) +/obj/item/organ/internal/eyes/robotic/xray/on_remove(mob/living/carbon/eye_owner) + . = ..() REMOVE_TRAIT(eye_owner, TRAIT_XRAY_VISION, ORGAN_TRAIT) - return ..() /obj/item/organ/internal/eyes/robotic/thermals name = "thermal eyes" @@ -324,7 +326,7 @@ /obj/item/organ/internal/eyes/robotic/flashlight/emp_act(severity) return -/obj/item/organ/internal/eyes/robotic/flashlight/Insert(mob/living/carbon/victim, special = FALSE, drop_if_replaced = FALSE) +/obj/item/organ/internal/eyes/robotic/flashlight/on_insert(mob/living/carbon/victim) . = ..() if(!eye) eye = new /obj/item/flashlight/eyelight() @@ -334,12 +336,12 @@ victim.become_blind(FLASHLIGHT_EYES) -/obj/item/organ/internal/eyes/robotic/flashlight/Remove(mob/living/carbon/victim, special = 0) +/obj/item/organ/internal/eyes/robotic/flashlight/on_remove(mob/living/carbon/victim) + . = ..() eye.on = FALSE eye.update_brightness(victim) eye.forceMove(src) victim.cure_blind(FLASHLIGHT_EYES) - return ..() // Welding shield implant /obj/item/organ/internal/eyes/robotic/shield @@ -438,11 +440,11 @@ return deactivate(silent = TRUE) -/obj/item/organ/internal/eyes/robotic/glow/Insert(mob/living/carbon/eye_owner, special = FALSE, drop_if_replaced = FALSE) +/obj/item/organ/internal/eyes/robotic/glow/on_insert(mob/living/carbon/eye_owner) . = ..() RegisterSignal(eye_owner, COMSIG_ATOM_DIR_CHANGE, PROC_REF(update_visuals)) -/obj/item/organ/internal/eyes/robotic/glow/Remove(mob/living/carbon/eye_owner, special = FALSE) +/obj/item/organ/internal/eyes/robotic/glow/on_remove(mob/living/carbon/eye_owner) . = ..() UnregisterSignal(eye_owner, COMSIG_ATOM_DIR_CHANGE) @@ -582,15 +584,15 @@ high_light_cutoff = list(30, 35, 50) var/obj/item/flashlight/eyelight/adapted/adapt_light -/obj/item/organ/internal/eyes/night_vision/maintenance_adapted/Insert(mob/living/carbon/adapted, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/eyes/night_vision/maintenance_adapted/on_insert(mob/living/carbon/eye_owner) . = ..() //add lighting if(!adapt_light) adapt_light = new /obj/item/flashlight/eyelight/adapted() adapt_light.on = TRUE - adapt_light.forceMove(adapted) - adapt_light.update_brightness(adapted) - ADD_TRAIT(adapted, TRAIT_UNNATURAL_RED_GLOWY_EYES, ORGAN_TRAIT) + adapt_light.forceMove(eye_owner) + adapt_light.update_brightness(eye_owner) + ADD_TRAIT(eye_owner, TRAIT_UNNATURAL_RED_GLOWY_EYES, ORGAN_TRAIT) /obj/item/organ/internal/eyes/night_vision/maintenance_adapted/on_life(delta_time, times_fired) if(!owner.is_blind() && isturf(owner.loc) && owner.has_light_nearby(light_amount=0.5)) //we allow a little more than usual so we can produce light from the adapted eyes diff --git a/code/modules/surgery/organs/heart.dm b/code/modules/surgery/organs/heart.dm index 6ae5ba9998a..bce8140e9a2 100644 --- a/code/modules/surgery/organs/heart.dm +++ b/code/modules/surgery/organs/heart.dm @@ -168,11 +168,10 @@ accursed.add_client_colour(/datum/client_colour/cursed_heart_blood) //bloody screen so real add_colour = FALSE -/obj/item/organ/internal/heart/cursed/Insert(mob/living/carbon/accursed, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/cursed/on_insert(mob/living/carbon/accursed) . = ..() last_pump = world.time // give them time to react - if(owner) - to_chat(owner, span_userdanger("Your heart has been replaced with a cursed one, you have to pump this one manually otherwise you'll die!")) + to_chat(accursed, span_userdanger("Your heart has been replaced with a cursed one, you have to pump this one manually otherwise you'll die!")) /obj/item/organ/internal/heart/cursed/Remove(mob/living/carbon/accursed, special = FALSE) . = ..() @@ -298,16 +297,18 @@ . = ..() add_atom_colour(ethereal_color, FIXED_COLOUR_PRIORITY) -/obj/item/organ/internal/heart/ethereal/Insert(mob/living/carbon/owner, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/heart/ethereal/Insert(mob/living/carbon/heart_owner, special = FALSE, drop_if_replaced = TRUE) . = ..() - RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_change)) - RegisterSignal(owner, COMSIG_LIVING_POST_FULLY_HEAL, PROC_REF(on_owner_fully_heal)) - RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(owner_deleted)) + if(!.) + return + RegisterSignal(heart_owner, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_change)) + RegisterSignal(heart_owner, COMSIG_LIVING_POST_FULLY_HEAL, PROC_REF(on_owner_fully_heal)) + RegisterSignal(heart_owner, COMSIG_PARENT_QDELETING, PROC_REF(owner_deleted)) -/obj/item/organ/internal/heart/ethereal/Remove(mob/living/carbon/owner, special = FALSE) - UnregisterSignal(owner, list(COMSIG_MOB_STATCHANGE, COMSIG_LIVING_POST_FULLY_HEAL, COMSIG_PARENT_QDELETING)) - REMOVE_TRAIT(owner, TRAIT_CORPSELOCKED, SPECIES_TRAIT) - stop_crystalization_process(owner) +/obj/item/organ/internal/heart/ethereal/Remove(mob/living/carbon/heart_owner, special = FALSE) + UnregisterSignal(heart_owner, list(COMSIG_MOB_STATCHANGE, COMSIG_LIVING_POST_FULLY_HEAL, COMSIG_PARENT_QDELETING)) + REMOVE_TRAIT(heart_owner, TRAIT_CORPSELOCKED, SPECIES_TRAIT) + stop_crystalization_process(heart_owner) QDEL_NULL(current_crystal) return ..() @@ -404,7 +405,7 @@ crystalization_process_damage = 0 //Reset damage taken during crystalization if(!succesful) - REMOVE_TRAIT(owner, TRAIT_CORPSELOCKED, SPECIES_TRAIT) + REMOVE_TRAIT(ethereal, TRAIT_CORPSELOCKED, SPECIES_TRAIT) QDEL_NULL(current_crystal) if(crystalize_timer_id) diff --git a/code/modules/surgery/organs/lungs.dm b/code/modules/surgery/organs/lungs.dm index d4416fe1ba2..bc94e982fa6 100644 --- a/code/modules/surgery/organs/lungs.dm +++ b/code/modules/surgery/organs/lungs.dm @@ -6,7 +6,7 @@ slot = ORGAN_SLOT_LUNGS gender = PLURAL w_class = WEIGHT_CLASS_SMALL - + var/respiration_type = NONE // The type(s) of gas this lung needs for respiration healing_factor = STANDARD_ORGAN_HEALING @@ -92,10 +92,10 @@ // assign the respiration_type /obj/item/organ/internal/lungs/Initialize(mapload) . = ..() - + if(safe_co2_min) respiration_type |= RESPIRATION_CO2 - if(safe_nitro_min) + if(safe_nitro_min) respiration_type |= RESPIRATION_N2 if(safe_oxygen_min) respiration_type |= RESPIRATION_OXYGEN @@ -104,12 +104,14 @@ ///Simply exists so that you don't keep any alerts from your previous lack of lungs. /obj/item/organ/internal/lungs/Insert(mob/living/carbon/receiver, special = FALSE, drop_if_replaced = TRUE) + . = ..() + if(!.) + return . receiver.clear_alert(ALERT_NOT_ENOUGH_OXYGEN) receiver.clear_alert(ALERT_NOT_ENOUGH_CO2) receiver.clear_alert(ALERT_NOT_ENOUGH_NITRO) receiver.clear_alert(ALERT_NOT_ENOUGH_PLASMA) receiver.clear_alert(ALERT_NOT_ENOUGH_N2O) - return ..() /** * This proc tests if the lungs can breathe, if they can breathe a given gas mixture, and throws/clears gas alerts. @@ -123,8 +125,6 @@ * * breather: A carbon mob that is using the lungs to breathe. */ /obj/item/organ/internal/lungs/proc/check_breath(datum/gas_mixture/breath, mob/living/carbon/human/breather) - . = TRUE - if(breather.status_flags & GODMODE) breather.failed_last_breath = FALSE breather.clear_alert(ALERT_NOT_ENOUGH_OXYGEN) @@ -133,10 +133,15 @@ if(HAS_TRAIT(breather, TRAIT_NOBREATH)) return - // Breath may be null, so use a fallback "empty breath" for convenience. + /// Immutable "empty breath" used to store waste gases before they are re-added to the main breath. + /// empty_breath is also used as a backup for breath if it was null. + /// During gas exchange each output gas is temporarily transferred into this gas_mixture, and then transferred back into the breath. + /// Two gas_mixtures are used because we don't want exchanges to influence each other. + var/static/datum/gas_mixture/immutable/empty_breath = new(BREATH_VOLUME) + var/datum/gas_mixture/immutable/breath_out = empty_breath + + // If the breath is falsy or "null", we can use the backup empty_breath. if(!breath) - /// Fallback "empty breath" for convenience. - var/static/datum/gas_mixture/immutable/empty_breath = new(BREATH_VOLUME) breath = empty_breath // Ensure gas volumes are present. @@ -146,17 +151,19 @@ // Indicates if there are moles of gas in the breath. var/has_moles = breath.total_moles() != 0 - // The list of gases in the breath. + /// List of gases to be inhaled. var/list/breath_gases = breath.gases + /// List of gases to be exhaled. + var/list/breath_gases_out = breath_out.gases // Indicates if lungs can breathe without gas. var/can_breathe_vacuum = HAS_TRAIT(src, TRAIT_SPACEBREATHING) - // Re-usable var used to remove a limited volume of each gas from the given gas mixture. - var/gas_breathed = 0 // Vars for N2O/healium induced euphoria, stun, and sleep. var/n2o_euphoria = EUPHORIA_LAST_FLAG var/healium_euphoria = EUPHORIA_LAST_FLAG + // Re-usable var used to remove a limited volume of each gas from the given gas mixture. + var/gas_breathed = 0 // Partial pressures in the breath. // Main Gases var/pluoxium_pp = 0 @@ -183,6 +190,7 @@ // Route gases through mask filter if breather is wearing one. if(istype(breather.wear_mask) && (breather.wear_mask.clothing_flags & GAS_FILTERING) && breather.wear_mask.has_filter) breath = breather.wear_mask.consume_filter(breath) + // Idiot-proofing for filter implementation, in case someone swaps the entire gas_mixture. breath_gases = breath.gases // Partial pressures of "main" gases. pluoxium_pp = breath.get_breath_partial_pressure(breath_gases[/datum/gas/pluoxium][MOLES]) @@ -212,7 +220,6 @@ breather.adjustOxyLoss(-5) else // Can't breathe! - . = FALSE breather.failed_last_breath = TRUE // Handle subtypes' breath processing @@ -224,7 +231,7 @@ // Behaves like Oxygen with 8X efficacy, but metabolizes into a reagent. if(pluoxium_pp) // Inhale Pluoxium. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/pluoxium) + breathe_gas_volume(breath, /datum/gas/pluoxium) // Metabolize to reagent. if(pluoxium_pp > gas_stimulation_min) var/existing = breather.reagents.get_reagent_amount(/datum/reagent/pluoxium) @@ -252,17 +259,17 @@ // Inhale insufficient amount of O2, exhale CO2. if(o2_pp) gas_breathed = handle_suffocation(breather, o2_pp, safe_oxygen_min, breath_gases[/datum/gas/oxygen][MOLES]) - breathe_gas_volume(breath_gases, /datum/gas/oxygen, /datum/gas/carbon_dioxide, volume = gas_breathed) + breathe_gas_volume(breath, /datum/gas/oxygen, /datum/gas/carbon_dioxide, breath_out, volume = gas_breathed) else // No amount of O2, just suffocate - gas_breathed = handle_suffocation(breather, o2_pp, safe_oxygen_min, breath_gases[/datum/gas/oxygen][MOLES]) + handle_suffocation(breather, o2_pp, safe_oxygen_min, 0) else // Enough oxygen to breathe. breather.failed_last_breath = FALSE breather.clear_alert(ALERT_NOT_ENOUGH_OXYGEN) // Inhale Oxygen, exhale equivalent amount of CO2. if(o2_pp) - breathe_gas_volume(breath_gases, /datum/gas/oxygen, /datum/gas/carbon_dioxide) + breathe_gas_volume(breath, /datum/gas/oxygen, /datum/gas/carbon_dioxide, breath_out) // Heal mob if not in crit. if(breather.health >= breather.crit_threshold) breather.adjustOxyLoss(-5) @@ -288,17 +295,17 @@ // Inhale insufficient amount of N2, exhale CO2. if(n2_pp) gas_breathed = handle_suffocation(breather, n2_pp, safe_nitro_min, breath_gases[/datum/gas/nitrogen][MOLES]) - breathe_gas_volume(breath_gases, /datum/gas/nitrogen, /datum/gas/carbon_dioxide, volume = gas_breathed) + breathe_gas_volume(breath, /datum/gas/nitrogen, /datum/gas/carbon_dioxide, breath_out, volume = gas_breathed) else - // no amount of N2, just suffocate - gas_breathed = handle_suffocation(breather, n2_pp, safe_nitro_min, breath_gases[/datum/gas/nitrogen][MOLES]) + // No amount of N2, just suffocate + handle_suffocation(breather, n2_pp, safe_nitro_min, 0) else // Enough nitrogen to breathe. breather.failed_last_breath = FALSE breather.clear_alert(ALERT_NOT_ENOUGH_NITRO) // Inhale N2, exhale equivalent amount of CO2. Look ma, sideways breathing! if(n2_pp) - breathe_gas_volume(breath_gases, /datum/gas/nitrogen, /datum/gas/carbon_dioxide) + breathe_gas_volume(breath, /datum/gas/nitrogen, /datum/gas/carbon_dioxide, breath_out) // Heal mob if not in crit. if(breather.health >= breather.crit_threshold) breather.adjustOxyLoss(-5) @@ -336,17 +343,17 @@ // Inhale insufficient amount of CO2, exhale O2. if(co2_pp) gas_breathed = handle_suffocation(breather, co2_pp, safe_co2_min, breath_gases[/datum/gas/carbon_dioxide][MOLES]) - breathe_gas_volume(breath_gases, /datum/gas/carbon_dioxide, /datum/gas/oxygen, volume = gas_breathed) + breathe_gas_volume(breath, /datum/gas/carbon_dioxide, /datum/gas/oxygen, breath_out, volume = gas_breathed) else // No amount of CO2, just suffocate - gas_breathed = handle_suffocation(breather, co2_pp, safe_co2_min, breath_gases[/datum/gas/carbon_dioxide][MOLES]) + handle_suffocation(breather, co2_pp, safe_co2_min, 0) else // Enough CO2 to breathe. breather.failed_last_breath = FALSE breather.clear_alert(ALERT_NOT_ENOUGH_CO2) // Inhale CO2, exhale equivalent amount of O2. Look ma, reverse breathing! if(co2_pp) - breathe_gas_volume(breath_gases, /datum/gas/carbon_dioxide, /datum/gas/oxygen) + breathe_gas_volume(breath, /datum/gas/carbon_dioxide, /datum/gas/oxygen, breath_out) // Heal mob if not in crit. if(breather.health >= breather.crit_threshold) breather.adjustOxyLoss(-5) @@ -372,17 +379,17 @@ // Breathe insufficient amount of Plasma, exhale CO2. if(plasma_pp) gas_breathed = handle_suffocation(breather, plasma_pp, safe_plasma_min, breath_gases[/datum/gas/plasma][MOLES]) - breathe_gas_volume(breath_gases, /datum/gas/plasma, /datum/gas/carbon_dioxide, volume = gas_breathed) + breathe_gas_volume(breath, /datum/gas/plasma, /datum/gas/carbon_dioxide, breath_out, volume = gas_breathed) else // No amount of plasma, just suffocate - gas_breathed = handle_suffocation(breather, plasma_pp, safe_plasma_min, breath_gases[/datum/gas/plasma][MOLES]) + handle_suffocation(breather, plasma_pp, safe_plasma_min, 0) else // Enough Plasma to breathe. breather.failed_last_breath = FALSE breather.clear_alert(ALERT_NOT_ENOUGH_PLASMA) // Inhale Plasma, exhale equivalent amount of CO2. if(plasma_pp) - breathe_gas_volume(breath_gases, /datum/gas/plasma, /datum/gas/carbon_dioxide) + breathe_gas_volume(breath, /datum/gas/plasma, /datum/gas/carbon_dioxide, breath_out) // Heal mob if not in crit. if(breather.health >= breather.crit_threshold) breather.adjustOxyLoss(-5) @@ -402,7 +409,7 @@ //-- FREON --// if(freon_pp) // Inhale Freon. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/freon) + breathe_gas_volume(breath, /datum/gas/freon) if (freon_pp > gas_stimulation_min) breather.reagents.add_reagent(/datum/reagent/freon, 1) if (prob(freon_pp)) @@ -419,7 +426,7 @@ //-- HALON --// if(halon_pp) // Inhale Halon. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/halon) + breathe_gas_volume(breath, /datum/gas/halon) // Metabolize to reagent. if(halon_pp > gas_stimulation_min) breather.adjustOxyLoss(5) @@ -432,7 +439,7 @@ healium_euphoria = EUPHORIA_INACTIVE else // Inhale Healium. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/healium) + breathe_gas_volume(breath, /datum/gas/healium) // Euphoria side-effect. if(healium_pp > gas_stimulation_min) if(prob(15)) @@ -453,22 +460,22 @@ // Activates helium speech when partial pressure gets high enough if(!helium_pp) helium_speech = FALSE - UnregisterSignal(owner, COMSIG_MOB_SAY) + UnregisterSignal(breather, COMSIG_MOB_SAY) else // Inhale Helium. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/helium) + breathe_gas_volume(breath, /datum/gas/helium) // Helium side-effects. if(helium_speech && (helium_pp <= helium_speech_min)) helium_speech = FALSE - UnregisterSignal(owner, COMSIG_MOB_SAY) + UnregisterSignal(breather, COMSIG_MOB_SAY) else if(!helium_speech && (helium_pp > helium_speech_min)) helium_speech = TRUE - RegisterSignal(owner, COMSIG_MOB_SAY, PROC_REF(handle_helium_speech)) + RegisterSignal(breather, COMSIG_MOB_SAY, PROC_REF(handle_helium_speech)) //-- HYPER-NOBILUM --// if(hypernob_pp) // Inhale Hyber-Nobilum. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/hypernoblium) + breathe_gas_volume(breath, /datum/gas/hypernoblium) // Metabolize to reagent. if (hypernob_pp > gas_stimulation_min) var/existing = breather.reagents.get_reagent_amount(/datum/reagent/hypernoblium) @@ -477,46 +484,46 @@ //-- MIASMA --// if(!miasma_pp || !suffers_miasma) // Clear out moods when immune to miasma, or if there's no miasma at all. - owner.clear_mood_event("smell") + breather.clear_mood_event("smell") else // Inhale Miasma. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/miasma) + breathe_gas_volume(breath, /datum/gas/miasma) // Miasma sickness if(prob(0.5 * miasma_pp)) var/datum/disease/advance/miasma_disease = new /datum/disease/advance/random(max_symptoms = min(round(max(miasma_pp / 2, 1), 1), 6), max_level = min(round(max(miasma_pp, 1), 1), 8)) // tl;dr the first argument chooses the smaller of miasma_pp/2 or 6(typical max virus symptoms), the second chooses the smaller of miasma_pp or 8(max virus symptom level) // Each argument has a minimum of 1 and rounds to the nearest value. Feel free to change the pp scaling I couldn't decide on good numbers for it. miasma_disease.name = "Unknown" - miasma_disease.try_infect(owner) + miasma_disease.try_infect(breather) // Miasma side effects switch(miasma_pp) if(0.25 to 5) // At lower pp, give out a little warning - owner.clear_mood_event("smell") + breather.clear_mood_event("smell") if(prob(5)) - to_chat(owner, span_notice("There is an unpleasant smell in the air.")) + to_chat(breather, span_notice("There is an unpleasant smell in the air.")) if(5 to 15) //At somewhat higher pp, warning becomes more obvious if(prob(15)) - to_chat(owner, span_warning("You smell something horribly decayed inside this room.")) - owner.add_mood_event("smell", /datum/mood_event/disgust/bad_smell) + to_chat(breather, span_warning("You smell something horribly decayed inside this room.")) + breather.add_mood_event("smell", /datum/mood_event/disgust/bad_smell) if(15 to 30) //Small chance to vomit. By now, people have internals on anyway if(prob(5)) - to_chat(owner, span_warning("The stench of rotting carcasses is unbearable!")) - owner.add_mood_event("smell", /datum/mood_event/disgust/nauseating_stench) - owner.vomit() + to_chat(breather, span_warning("The stench of rotting carcasses is unbearable!")) + breather.add_mood_event("smell", /datum/mood_event/disgust/nauseating_stench) + breather.vomit() if(30 to INFINITY) //Higher chance to vomit. Let the horror start if(prob(15)) - to_chat(owner, span_warning("The stench of rotting carcasses is unbearable!")) - owner.add_mood_event("smell", /datum/mood_event/disgust/nauseating_stench) - owner.vomit() + to_chat(breather, span_warning("The stench of rotting carcasses is unbearable!")) + breather.add_mood_event("smell", /datum/mood_event/disgust/nauseating_stench) + breather.vomit() else - owner.clear_mood_event("smell") + breather.clear_mood_event("smell") // In a full miasma atmosphere with 101.34 pKa, about 10 disgust per breath, is pretty low compared to threshholds // Then again, this is a purely hypothetical scenario and hardly reachable - owner.adjust_disgust(0.1 * miasma_pp) + breather.adjust_disgust(0.1 * miasma_pp) //-- N2O --// // N2O side-effects. "Too much N2O!" @@ -546,7 +553,7 @@ //-- NITRIUM --// if (nitrium_pp) // Inhale Nitrium. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/nitrium) + breathe_gas_volume(breath, /datum/gas/nitrium) // Random chance to inflict side effects increases with pressure. if((prob(nitrium_pp) && (nitrium_pp > 15))) // Nitrium side-effect. @@ -566,7 +573,7 @@ //-- TRITIUM --// if (trit_pp) // Inhale Tritium. Exhale nothing. - gas_breathed = breathe_gas_volume(breath_gases, /datum/gas/tritium) + gas_breathed = breathe_gas_volume(breath, /datum/gas/tritium) // Tritium side-effects. var/ratio = gas_breathed * 15 breather.adjustToxLoss(clamp(ratio, MIN_TOXIC_GAS_DAMAGE, MAX_TOXIC_GAS_DAMAGE)) @@ -580,7 +587,7 @@ //-- ZAUKER --// if(zauker_pp) // Inhale Zauker. Exhale nothing. - breathe_gas_volume(breath_gases, /datum/gas/zauker) + breathe_gas_volume(breath, /datum/gas/zauker) // Metabolize to reagent. if(zauker_pp > gas_stimulation_min) var/existing = breather.reagents.get_reagent_amount(/datum/reagent/zauker) @@ -588,28 +595,40 @@ // Handle chemical euphoria mood event, caused by gases such as N2O or healium. if (n2o_euphoria == EUPHORIA_ACTIVE || healium_euphoria == EUPHORIA_ACTIVE) - owner.add_mood_event("chemical_euphoria", /datum/mood_event/chemical_euphoria) + breather.add_mood_event("chemical_euphoria", /datum/mood_event/chemical_euphoria) else if (n2o_euphoria == EUPHORIA_INACTIVE && healium_euphoria == EUPHORIA_INACTIVE) - owner.clear_mood_event("chemical_euphoria") + breather.clear_mood_event("chemical_euphoria") // Activate mood on first flag, remove on second, do nothing on third. if(has_moles) handle_breath_temperature(breath, breather) + // Transfer exchanged gases into breath for exhalation. + for(var/gas_type in breath_gases_out) + breath.assert_gas(gas_type) + breath_gases[gas_type][MOLES] += breath_gases_out[gas_type][MOLES] + // Resets immutable gas_mixture to empty. + breath_out.garbage_collect() breath.garbage_collect() + // Returned status code 0 indicates breath failed. + if(!breather.failed_last_breath) + return TRUE + ///override this for breath handling unique to lung subtypes, breath_gas is the list of gas in the breath while gas breathed is just what is being added or removed from that list, just as they are when this is called in check_breath() /obj/item/organ/internal/lungs/proc/handle_gas_override(mob/living/carbon/human/breather, list/breath_gas, gas_breathed) return -/// Remove a volume of gas from the breath. Used to simulate absorbtion and interchange of gas in the lungs. -/// Removes all of the given gas type unless given a volume argument. +/// Remove gas from breath. If output_gas and breath_out arguments are given, transfers the removed gas to breath_out. +/// Removes 100% of the given gas type unless given a volume argument. /// Returns the amount of gas theoretically removed. -/obj/item/organ/internal/lungs/proc/breathe_gas_volume(list/breath_gases, datum/gas/remove_gas, datum/gas/exchange_gas = null, volume = INFINITY) - volume = min(volume, breath_gases[remove_gas][MOLES]) - breath_gases[remove_gas][MOLES] -= volume - if(exchange_gas) - breath_gases[exchange_gas][MOLES] += volume +/obj/item/organ/internal/lungs/proc/breathe_gas_volume(datum/gas_mixture/breath, datum/gas/input_gas = null, datum/gas/output_gas = null, datum/gas_mixture/breath_out = null, volume = INFINITY) + var/gases_in = breath.gases + volume = min(volume, gases_in[input_gas][MOLES]) + gases_in[input_gas][MOLES] -= volume + if(output_gas && breath_out) + breath_out.assert_gas(output_gas) + breath_out.gases[output_gas][MOLES] += volume return volume /// Applies suffocation side-effects to a given Human, scaling based on ratio of required pressure VS "true" pressure. @@ -714,7 +733,7 @@ . = ..() if (breath?.gases[/datum/gas/plasma]) var/plasma_pp = breath.get_breath_partial_pressure(breath.gases[/datum/gas/plasma][MOLES]) - owner.blood_volume += (0.2 * plasma_pp) // 10/s when breathing literally nothing but plasma, which will suffocate you. + breather_slime.blood_volume += (0.2 * plasma_pp) // 10/s when breathing literally nothing but plasma, which will suffocate you. /obj/item/organ/internal/lungs/cybernetic name = "basic cybernetic lungs" diff --git a/code/modules/surgery/organs/stomach/stomach_ethereal.dm b/code/modules/surgery/organs/stomach/stomach_ethereal.dm index d8d74fcf9ab..afbf6031203 100644 --- a/code/modules/surgery/organs/stomach/stomach_ethereal.dm +++ b/code/modules/surgery/organs/stomach/stomach_ethereal.dm @@ -13,20 +13,18 @@ adjust_charge(-ETHEREAL_CHARGE_FACTOR * delta_time) handle_charge(owner, delta_time, times_fired) -/obj/item/organ/internal/stomach/ethereal/Insert(mob/living/carbon/carbon, special = FALSE, drop_if_replaced = TRUE) +/obj/item/organ/internal/stomach/ethereal/on_insert(mob/living/carbon/stomach_owner) . = ..() - RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(charge)) - RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_electrocute)) + RegisterSignal(stomach_owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(charge)) + RegisterSignal(stomach_owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_electrocute)) -/obj/item/organ/internal/stomach/ethereal/Remove(mob/living/carbon/carbon, special = FALSE) - UnregisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT) - UnregisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT) - - owner.clear_mood_event("charge") - carbon.clear_alert(ALERT_ETHEREAL_CHARGE) - carbon.clear_alert(ALERT_ETHEREAL_OVERCHARGE) - - return ..() +/obj/item/organ/internal/stomach/ethereal/on_remove(mob/living/carbon/stomach_owner) + . = ..() + UnregisterSignal(stomach_owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT) + UnregisterSignal(stomach_owner, COMSIG_LIVING_ELECTROCUTE_ACT) + stomach_owner.clear_mood_event("charge") + stomach_owner.clear_alert(ALERT_ETHEREAL_CHARGE) + stomach_owner.clear_alert(ALERT_ETHEREAL_OVERCHARGE) /obj/item/organ/internal/stomach/ethereal/handle_hunger_slowdown(mob/living/carbon/human/human) human.add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/hunger, multiplicative_slowdown = (1.5 * (1 - crystal_charge / 100))) diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm index 5ac5131dbc3..9ef10d5cff4 100644 --- a/code/modules/surgery/organs/tongue.dm +++ b/code/modules/surgery/organs/tongue.dm @@ -85,6 +85,8 @@ /obj/item/organ/internal/tongue/Insert(mob/living/carbon/tongue_owner, special = FALSE, drop_if_replaced = TRUE) . = ..() + if(!.) + return ADD_TRAIT(tongue_owner, TRAIT_SPEAKS_CLEARLY, SPEAKING_FROM_TONGUE) if (modifies_speech) RegisterSignal(tongue_owner, COMSIG_MOB_SAY, PROC_REF(handle_speech)) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 4c94daedaae..2d30546aa8c 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -135,6 +135,7 @@ #include "knockoff_component.dm" #include "lesserform.dm" #include "limbsanity.dm" +#include "lungs.dm" #include "load_map_security.dm" #include "machine_disassembly.dm" #include "mapload_space_verification.dm" @@ -157,8 +158,9 @@ #include "ntnetwork_tests.dm" #include "nuke_cinematic.dm" #include "objectives.dm" -#include "orderable_items.dm" #include "operating_table.dm" +#include "orderable_items.dm" +#include "organs.dm" #include "organ_set_bonus.dm" #include "outfit_sanity.dm" #include "paintings.dm" diff --git a/code/modules/unit_tests/breath.dm b/code/modules/unit_tests/breath.dm index 8451c1725a2..faba1a08e22 100644 --- a/code/modules/unit_tests/breath.dm +++ b/code/modules/unit_tests/breath.dm @@ -1,76 +1,100 @@ -/// Tests to make sure humans can breath in normal situations -/// Built to prevent regression on an issue surrounding QUANTIZE() and BREATH_VOLUME -/// See the comment on BREATH_VOLUME for more details -/datum/unit_test/breath_sanity +/// Tests to ensure humans, plasmamen, and ashwalkers can breath in normal situations. +/// Ensures algorithmic correctness of the "breathe()" and "toggle_internals()" procs. +/// Built to prevent regression on an issue surrounding QUANTIZE() and BREATH_VOLUME. +/// See the comment on BREATH_VOLUME for more details. +/datum/unit_test/breath + abstract_type = /datum/unit_test/breath -/datum/unit_test/breath_sanity/Run() - var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) - var/obj/item/clothing/mask/breath/tube = allocate(/obj/item/clothing/mask/breath) - var/obj/item/tank/internals/emergency_oxygen/source = allocate(/obj/item/tank/internals/emergency_oxygen) - - lab_rat.equip_to_slot_if_possible(tube, ITEM_SLOT_MASK) +/// Equips the given Human with a new instance of the given tank type and a breathing mask. +/// Returns the new equipped tank. +/datum/unit_test/breath/proc/equip_labrat_internals(mob/living/carbon/human/lab_rat, tank_type) + var/obj/item/clothing/mask/breath/mask = allocate(/obj/item/clothing/mask/breath) + var/obj/item/tank/internals/source = allocate(tank_type) + lab_rat.equip_to_slot_if_possible(mask, ITEM_SLOT_MASK) lab_rat.equip_to_slot_if_possible(source, ITEM_SLOT_HANDS) - source.toggle_internals(lab_rat) + return source - lab_rat.breathe() - - TEST_ASSERT(!lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans can't get a full breath from standard o2 tanks") - lab_rat.clear_alert(ALERT_NOT_ENOUGH_OXYGEN) - - //Prep the mob +/datum/unit_test/breath/breath_sanity/Run() + // Breathing from turf. + var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) lab_rat.forceMove(run_loc_floor_bottom_left) - source.toggle_internals(lab_rat) - TEST_ASSERT(!lab_rat.internal, "toggle_internals() failed to toggle internals") - var/turf/open/to_fill = run_loc_floor_bottom_left - //Prep the floor to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS to_fill.air = to_fill.create_gas_mixture() - lab_rat.breathe() + TEST_ASSERT(!lab_rat.failed_last_breath && !lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans can't get a full breath from the standard initial_gas_mix on a turf") - TEST_ASSERT(!lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans can't get a full breath from the standard initial_gas_mix on a turf") - -/// Tests to make sure plasmaman can breath from their internal tanks -/datum/unit_test/breath_sanity_plasmamen - -/datum/unit_test/breath_sanity_plasmamen/Run() - var/mob/living/carbon/human/species/plasma/lab_rat = allocate(/mob/living/carbon/human/species/plasma) - var/obj/item/clothing/mask/breath/tube = allocate(/obj/item/clothing/mask/breath) - var/obj/item/tank/internals/plasmaman/source = allocate(/obj/item/tank/internals/plasmaman) - - lab_rat.equip_to_slot_if_possible(tube, ITEM_SLOT_MASK) - lab_rat.equip_to_slot_if_possible(source, ITEM_SLOT_HANDS) - source.toggle_internals(lab_rat) - + // Breathing from standard internals tank. + lab_rat = allocate(/mob/living/carbon/human/consistent) + var/obj/item/tank/internals/source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen) lab_rat.breathe() + TEST_ASSERT(!lab_rat.failed_last_breath && !lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans can't get a full breath from standard o2 tanks") + if(!isnull(lab_rat.internal)) + TEST_ASSERT(source.toggle_internals(lab_rat) && isnull(lab_rat.internal), "toggle_internals() failed to close internals") - TEST_ASSERT(!lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Plasmamen can't get a full breath from a standard plasma tank") - lab_rat.clear_alert(ALERT_NOT_ENOUGH_PLASMA) - - //Prep the mob - source.toggle_internals(lab_rat) - TEST_ASSERT(!lab_rat.internal, "Plasmaman toggle_internals() failed to toggle internals") - -/// Tests to make sure ashwalkers can breath from the lavaland air -/datum/unit_test/breath_sanity_ashwalker - -/datum/unit_test/breath_sanity_ashwalker/Run() - var/mob/living/carbon/human/species/lizard/ashwalker/lab_rat = allocate(/mob/living/carbon/human/species/lizard/ashwalker) - - //Prep the mob - lab_rat.forceMove(run_loc_floor_bottom_left) - - var/turf/open/to_fill = run_loc_floor_bottom_left - //Prep the floor - to_fill.initial_gas_mix = LAVALAND_DEFAULT_ATMOS - to_fill.air = to_fill.create_gas_mixture() - + // Empty internals suffocation. + lab_rat = allocate(/mob/living/carbon/human/consistent) + source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty) + TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals") lab_rat.breathe() + TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans should suffocate from empty o2 tanks") - TEST_ASSERT(!lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Ashwalkers can't get a full breath from the Lavaland's initial_gas_mix on a turf") + // Nitrogen internals suffocation. + lab_rat = allocate(/mob/living/carbon/human/consistent) + source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty) + source.air_contents.assert_gas(/datum/gas/nitrogen) + source.air_contents.gases[/datum/gas/nitrogen][MOLES] = (10 * ONE_ATMOSPHERE) * source.volume / (R_IDEAL_GAS_EQUATION * T20C) + TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals") + lab_rat.breathe() + TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans should suffocate from pure n2 tanks") -/datum/unit_test/breath_sanity_ashwalker/Destroy() +/datum/unit_test/breath/breath_sanity/Destroy() + //Reset initial_gas_mix to avoid future issues on other tests + var/turf/open/to_fill = run_loc_floor_bottom_left + to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS + return ..() + +/// Tests to make sure plasmaman can breath from their internal tanks +/datum/unit_test/breath/breath_sanity_plasmamen + +/datum/unit_test/breath/breath_sanity_plasmamen/Run() + // Breathing from pure Plasma internals. + var/mob/living/carbon/human/species/plasma/lab_rat = allocate(/mob/living/carbon/human/species/plasma) + var/obj/item/tank/internals/plasmaman/source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/plasmaman) + TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals") + lab_rat.breathe() + TEST_ASSERT(!lab_rat.failed_last_breath && !lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Plasmamen can't get a full breath from a standard plasma tank") + TEST_ASSERT(source.toggle_internals(lab_rat) && !lab_rat.internal, "Plasmaman toggle_internals() failed to toggle internals") + + // Empty internals suffocation. + lab_rat = allocate(/mob/living/carbon/human/species/plasma) + source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty) + TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals") + lab_rat.breathe() + TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Plasmamen should suffocate from empty o2 tanks") + + // Nitrogen internals suffocation. + lab_rat = allocate(/mob/living/carbon/human/species/plasma) + source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty) + source.air_contents.assert_gas(/datum/gas/nitrogen) + source.air_contents.gases[/datum/gas/nitrogen][MOLES] = (10 * ONE_ATMOSPHERE) * source.volume / (R_IDEAL_GAS_EQUATION * T20C) + TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals") + lab_rat.breathe() + TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Humans should suffocate from pure n2 tanks") + +/// Tests to make sure ashwalkers can breathe from the lavaland air. +/datum/unit_test/breath/breath_sanity_ashwalker + +/datum/unit_test/breath/breath_sanity_ashwalker/Run() + var/mob/living/carbon/human/species/lizard/ashwalker/lab_rat = allocate(/mob/living/carbon/human/species/lizard/ashwalker) + lab_rat.forceMove(run_loc_floor_bottom_left) + var/turf/open/to_fill = run_loc_floor_bottom_left + to_fill.initial_gas_mix = LAVALAND_DEFAULT_ATMOS + to_fill.air = to_fill.create_gas_mixture() + lab_rat.breathe() + TEST_ASSERT(!lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Ashwalkers can't get a full breath from the Lavaland's initial_gas_mix on a turf") + +/datum/unit_test/breath/breath_sanity_ashwalker/Destroy() //Reset initial_gas_mix to avoid future issues on other tests var/turf/open/to_fill = run_loc_floor_bottom_left to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS diff --git a/code/modules/unit_tests/lungs.dm b/code/modules/unit_tests/lungs.dm new file mode 100644 index 00000000000..62c1778a8d9 --- /dev/null +++ b/code/modules/unit_tests/lungs.dm @@ -0,0 +1,212 @@ +#define TEST_CHECK_BREATH_MESSAGE(lungs_organ, message) "[lungs_organ.type]/check_breath() [message]" +#define TEST_ALERT_THROW_MESSAGE(lungs_organ, alert_name) TEST_CHECK_BREATH_MESSAGE(lungs_organ, "failed to throw alert [alert_name] when expected.") +#define TEST_ALERT_INHIBIT_MESSAGE(lungs_organ, alert_name) TEST_CHECK_BREATH_MESSAGE(lungs_organ, "threw alert [alert_name] when it wasn't expected.") +#define GET_MOLES(gas_mixture, gas_type) (gas_mixture.gases[gas_type] ? gas_mixture.gases[gas_type][MOLES] : 0) + +/// Tests the standard, plasmaman, and lavaland lungs organ to ensure breathing and suffocation behave as expected. +/// Performs a check on each main (can be life-sustaining) gas, and ensures gas alerts are only thrown when expected. +/datum/unit_test/lungs + abstract_type = /datum/unit_test/lungs + +/datum/unit_test/lungs/lungs_sanity/Run() + // "Standard" form of breathing. + // 2500 Litres of O2/N2 gas mix, ideal for life. + var/datum/gas_mixture/test_mix = create_standard_mix() + var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) + var/obj/item/organ/internal/lungs/test_lungs = allocate(/obj/item/organ/internal/lungs) + // Test one breath of O2/N2 mix. + lungs_test_check_breath("standard gas mixture", lab_rat, test_lungs, test_mix) + + // Suffocation with an empty gas mix. + var/datum/gas_mixture/empty_test_mix = allocate(/datum/gas_mixture) + lab_rat = allocate(/mob/living/carbon/human/consistent) + test_lungs = allocate(/obj/item/organ/internal/lungs) + // Test one breath of nothing. Suffocate due to the breath being empty. + lungs_test_check_breath("empty gas mixture", lab_rat, test_lungs, empty_test_mix, expect_failure = TRUE) + + // Suffocation with null. This does indeed happen normally. + lab_rat = allocate(/mob/living/carbon/human/consistent) + test_lungs = allocate(/obj/item/organ/internal/lungs) + // Test one breath of nothing. Suffocate due to the breath being null. + lungs_test_check_breath("null", lab_rat, test_lungs, null, expect_failure = TRUE) + + // Suffocation with Nitrogen. + var/datum/gas_mixture/nitro_test_mix = create_nitrogen_mix() + lab_rat = allocate(/mob/living/carbon/human/consistent) + test_lungs = allocate(/obj/item/organ/internal/lungs) + // Test one breath of Nitrogen. Suffocate due to the breath being 100% N2. + lungs_test_check_breath("pure Nitrogen", lab_rat, test_lungs, nitro_test_mix, expect_failure = TRUE) + +/// Tests the Plasmaman lungs organ to ensure Plasma breathing and suffocation behave as expected. +/datum/unit_test/lungs/lungs_sanity_plasmaman + +/datum/unit_test/lungs/lungs_sanity_plasmaman/Run() + // 2500 Litres of pure Plasma. + var/datum/gas_mixture/plasma_test_mix = create_plasma_mix() + var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) + var/obj/item/organ/internal/lungs/plasmaman/test_lungs = allocate(/obj/item/organ/internal/lungs/plasmaman) + // Test one breath of Plasma on Plasmaman lungs. + lungs_test_check_breath("pure Plasma", lab_rat, test_lungs, plasma_test_mix) + + // Tests suffocation with Nitrogen. + var/datum/gas_mixture/nitro_test_mix = create_nitrogen_mix() + lab_rat = allocate(/mob/living/carbon/human/consistent) + test_lungs = allocate(/obj/item/organ/internal/lungs/plasmaman) + // Test one breath of Nitrogen on Plasmaman lungs. + lungs_test_check_breath("pure Nitrogen", lab_rat, test_lungs, nitro_test_mix, expect_failure = TRUE) + +/// Tests the lavaland/Ashwalker lungs organ. +/// Ensures they can breathe from the lavaland air mixture properly, and suffocate on inadequate mixture. +/datum/unit_test/lungs/lungs_sanity_ashwalker + +/datum/unit_test/lungs/lungs_sanity_ashwalker/Run() + // Gas mix resembling one cell of lavaland's atmosphere. + var/datum/gas_mixture/lavaland_test_mix = create_lavaland_mix() + var/obj/item/organ/internal/lungs/lavaland/test_lungs = allocate(/obj/item/organ/internal/lungs/lavaland) + var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) + // Test one breath of Lavaland gas mix on Ashwalker lungs. + lungs_test_check_breath("Lavaland air mixture", lab_rat, test_lungs, lavaland_test_mix) + +/// Comprehensive unit test for [/obj/item/organ/internal/lungs/proc/check_breath()] +/// If "expect_failure" is set to TRUE, the test ensures the given Human suffocated. +/// A "test_name" string is required to contextualize test logs. Describe the gas you're testing. +/datum/unit_test/lungs/proc/lungs_test_check_breath(test_name, mob/living/carbon/human/lab_rat, obj/item/organ/internal/lungs/test_lungs, datum/gas_mixture/test_mix, expect_failure = FALSE) + // Setup a small volume of gas which represents one "breath" from test_mix. + var/datum/gas_mixture/test_breath + + if(!isnull(test_mix)) + var/total_moles = test_mix.total_moles() + if(total_moles > 0) + test_breath = test_mix.remove(total_moles * BREATH_PERCENTAGE) + + if(isnull(test_breath)) + test_breath = allocate(/datum/gas_mixture, BREATH_VOLUME) + + // Backup of the breath mixture, to compare against after testing check_breath(). + var/datum/gas_mixture/test_breath_backup = test_breath.copy() + + // Get partial pressures for each "main" gas. + var/oxygen_pp = 0 + var/nitro_pp = 0 + var/co2_pp = 0 + var/plasma_pp = 0 + if(test_breath.total_moles() > 0) + oxygen_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/oxygen)) + nitro_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/nitrogen)) + co2_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/carbon_dioxide)) + plasma_pp = test_breath.get_breath_partial_pressure(GET_MOLES(test_breath, /datum/gas/plasma)) + + // Minimum and maximum gas tolerances for the 4 main life-sustaining gases. + var/min_oxygen = test_lungs.safe_oxygen_min + var/min_nitro = test_lungs.safe_nitro_min + var/min_co2 = test_lungs.safe_co2_min + var/min_plasma = test_lungs.safe_plasma_min + var/max_oxygen = test_lungs.safe_oxygen_max + var/max_nitro = test_lungs.safe_nitro_max + var/max_co2 = test_lungs.safe_co2_max + var/max_plasma = test_lungs.safe_plasma_max + + // Test a single "breath" of air. + var/status_code = test_lungs.check_breath(test_breath, lab_rat) + + // Ensures failed_last_breath is set as we expect, and that check_breath returns a corollary status code. + if(expect_failure) + TEST_ASSERT(!status_code, TEST_CHECK_BREATH_MESSAGE(test_lungs, "returned truthy / status code 1 (success) when it wasn't expected.")) + TEST_ASSERT(lab_rat.failed_last_breath, TEST_CHECK_BREATH_MESSAGE(test_lungs, "should suffocate from [test_name].")) + else + TEST_ASSERT(status_code, TEST_CHECK_BREATH_MESSAGE(test_lungs, "returned falsy / status code 0 (failure) when it wasn't expected.")) + TEST_ASSERT(!lab_rat.failed_last_breath, TEST_CHECK_BREATH_MESSAGE(test_lungs, "can't get a full breath from [test_name].")) + + // Checks each "main" gas to ensure gas alerts are thrown/inhibited when expected. + lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_OXYGEN, min_oxygen, oxygen_pp) + lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_OXYGEN, max_oxygen, oxygen_pp) + + lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_NITRO, min_nitro, nitro_pp) + lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_NITRO, max_nitro, nitro_pp) + + lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_CO2, min_co2, co2_pp) + lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_CO2, max_co2, co2_pp) + + lungs_test_alert_min(lab_rat, test_lungs, ALERT_NOT_ENOUGH_PLASMA, min_plasma, plasma_pp) + lungs_test_alert_max(lab_rat, test_lungs, ALERT_TOO_MUCH_PLASMA, max_plasma, plasma_pp) + + // Track the volumes of O2 and CO2 which are expected to be exhaled. + var/expected_oxygen = GET_MOLES(test_breath_backup, /datum/gas/oxygen) + var/expected_nitro = GET_MOLES(test_breath_backup, /datum/gas/nitrogen) + var/expected_co2 = GET_MOLES(test_breath_backup, /datum/gas/carbon_dioxide) + var/expected_plasma = GET_MOLES(test_breath_backup, /datum/gas/plasma) + + // Setup expectations for main gas exchange tests. + if(min_oxygen) + expected_co2 += expected_oxygen + expected_oxygen = 0 + if(min_nitro) + expected_co2 += GET_MOLES(test_breath_backup, /datum/gas/nitrogen) + expected_nitro = 0 + if(min_co2) + expected_oxygen += GET_MOLES(test_breath_backup, /datum/gas/carbon_dioxide) + expected_co2 -= GET_MOLES(test_breath_backup, /datum/gas/carbon_dioxide) + if(min_plasma) + expected_co2 += GET_MOLES(test_breath_backup, /datum/gas/plasma) + expected_plasma = 0 + + // Validate conversion of inhaled gas to exhaled gas. + if(min_oxygen) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/oxygen), expected_oxygen), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all Oxygen initially present in the breath.")) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert Oxygen into an equivalent volume of CO2.")) + if(min_nitro) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/nitrogen), expected_nitro), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all Nitrogen initially present in the breath.")) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert Nitrogen into an equivalent volume of CO2.")) + if(min_co2) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all CO2 initially present in the breath.")) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/oxygen), expected_oxygen), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert CO2 into an equivalent volume of Oxygen.")) + if(min_plasma) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/plasma), expected_plasma), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should consume all Plasma initially present in the breath.")) + TEST_ASSERT(molar_cmp_equals(GET_MOLES(test_breath, /datum/gas/carbon_dioxide), expected_co2), TEST_CHECK_BREATH_MESSAGE(test_lungs, "should convert Plasma into an equivalent volume of CO2.")) + +/// Tests minimum gas alerts by comparing gas pressure. +/datum/unit_test/lungs/proc/lungs_test_alert_min(mob/living/carbon/human/lab_rat, obj/item/organ/internal/lungs/test_lungs, alert_name, min_pressure, pressure) + var/alert_thrown = lab_rat.has_alert(alert_name) + var/pressure_safe = (pressure >= min_pressure) || (min_pressure == 0) + TEST_ASSERT(!pressure_safe && alert_thrown || pressure_safe, TEST_ALERT_THROW_MESSAGE(test_lungs, alert_name)) + TEST_ASSERT(pressure_safe && !alert_thrown || !pressure_safe, TEST_ALERT_INHIBIT_MESSAGE(test_lungs, alert_name)) + +/// Tests maximum gas alerts by comparing gas pressure. +/datum/unit_test/lungs/proc/lungs_test_alert_max(mob/living/carbon/human/lab_rat, obj/item/organ/internal/lungs/test_lungs, alert_name, max_pressure, pressure) + var/alert_thrown = lab_rat.has_alert(alert_name) + var/pressure_safe = (pressure <= max_pressure) || (max_pressure == 0) + TEST_ASSERT(!pressure_safe && alert_thrown || pressure_safe, TEST_ALERT_THROW_MESSAGE(test_lungs, alert_name)) + TEST_ASSERT(pressure_safe && !alert_thrown || !pressure_safe, TEST_ALERT_INHIBIT_MESSAGE(test_lungs, alert_name)) + +/// Set up a 2500-Litre gas mixture with the given gases and percentages. +/datum/unit_test/lungs/proc/create_gas_mix(list/gas_to_percent) + var/datum/gas_mixture/test_mix = allocate(/datum/gas_mixture, 2500) + test_mix.temperature = T20C + for(var/datum/gas/gas_type as anything in gas_to_percent) + test_mix.add_gas(gas_type) + test_mix.gases[gas_type][MOLES] = (ONE_ATMOSPHERE * 2500 / (R_IDEAL_GAS_EQUATION * T20C) * gas_to_percent[gas_type]) + return test_mix + +/// Set up an O2/N2 gas mix which is "ideal" for organic life. +/datum/unit_test/lungs/proc/create_standard_mix() + return create_gas_mix(list(/datum/gas/oxygen = O2STANDARD, /datum/gas/nitrogen = N2STANDARD)) + +/// Set up a pure Nitrogen gas mix. +/datum/unit_test/lungs/proc/create_nitrogen_mix() + return create_gas_mix(list(/datum/gas/nitrogen = 1)) + +/// Set up an O2/N2 gas mix which is "ideal" for plasmamen. +/datum/unit_test/lungs/proc/create_plasma_mix() + return create_gas_mix(list(/datum/gas/plasma = 1)) + +/// Set up an Lavaland gas mix which is "ideal" for Ashwalker life. +/datum/unit_test/lungs/proc/create_lavaland_mix() + var/datum/gas_mixture/immutable/planetary/lavaland_mix = SSair.planetary[LAVALAND_DEFAULT_ATMOS] + var/datum/gas_mixture/test_mix = allocate(/datum/gas_mixture, 2500) + test_mix.copy_from(lavaland_mix) + return test_mix + +#undef TEST_CHECK_BREATH_MESSAGE +#undef TEST_ALERT_THROW_MESSAGE +#undef TEST_ALERT_INHIBIT_MESSAGE +#undef GET_MOLES diff --git a/code/modules/unit_tests/organ_set_bonus.dm b/code/modules/unit_tests/organ_set_bonus.dm index 0a9a0fa0040..250433028c1 100644 --- a/code/modules/unit_tests/organ_set_bonus.dm +++ b/code/modules/unit_tests/organ_set_bonus.dm @@ -1,5 +1,5 @@ -/// Tests the "organ set bonus" Elements and Status Effects, which are for the DNA Infuser. -/// Ensures the developers properly change IDs to be unique. +/// Checks that all "organ_set_bonus" status effects have unique "id" vars. +/// Required to ensure that the status effects are treated as "unique". /datum/unit_test/organ_set_bonus_id /datum/unit_test/organ_set_bonus_id/Run() @@ -7,79 +7,69 @@ var/list/existing_ids = list() for(var/datum/status_effect/organ_set_bonus/bonus_effect as anything in bonus_effects) var/effect_id = initial(bonus_effect.id) - var/existing_status = (effect_id in existing_ids) - if(existing_status) - TEST_FAIL("The ID of [bonus_effect] was duplicated in another status effect.") - else - existing_ids += effect_id - -/datum/unit_test/organ_set_bonus_sanity/proc/check_status_type(mob/living/carbon/human/lab_rat, datum/status_effect/status_type) - for(var/datum/status_effect/present_effect as anything in lab_rat.status_effects) - if(istype(present_effect, status_type)) - return present_effect + TEST_ASSERT(!(effect_id in existing_ids), "The ID of [bonus_effect] was duplicated in another status effect.") + existing_ids += effect_id +/// Checks that all implantable DNA Infuser organs are set up correctly and without error. /// Tests the "organ set bonus" Elements and Status Effects, which are for the DNA Infuser. -/// Ensures that each Element and Status Effect gets properly added/removed from mobs. +/// This test ensures that the "organ_set_bonus" status effects activate and deactivate when expected. /datum/unit_test/organ_set_bonus_sanity /datum/unit_test/organ_set_bonus_sanity/Run() + /// List of infuser_entry typepaths which contain species-changing organs. + /// Species change swaps out all the organs, making test_organ un-usable after insertion. + var/list/species_changing_entries = typecacheof(list( + /datum/infuser_entry/fly, + )) // Fetch the globally instantiated DNA Infuser entries. for(var/datum/infuser_entry/infuser_entry as anything in GLOB.infuser_entries) var/output_organs = infuser_entry.output_organs - // Human which will reiceve organs. var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) var/list/obj/item/organ/inserted_organs = list() + + // Attempt to insert entire list of mutant organs for the given infusion_entry. for(var/obj/item/organ/organ as anything in output_organs) organ = new organ() - var/implant_ok = organ.Insert(lab_rat, special = TRUE, drop_if_replaced = FALSE) - if(!implant_ok) - TEST_FAIL("The organ \"[organ.type]\" for \"[infuser_entry.type]\" was not inserted in the mob when expected.") - continue + TEST_ASSERT(organ.Insert(lab_rat, special = TRUE, drop_if_replaced = FALSE), "The organ `[organ.type]` for `[infuser_entry.type]` was not inserted in the mob when expected, Insert() returned falsy when TRUE was expected.") inserted_organs += organ - var/total_inserted = length(inserted_organs) - // Search for added Status Effect. - var/datum/status_effect/organ_set_bonus/added_status = check_status_type(lab_rat, /datum/status_effect/organ_set_bonus) + var/datum/status_effect/organ_set_bonus/added_status = locate(/datum/status_effect/organ_set_bonus) in lab_rat.status_effects - // Threshold description implies an organ set bonus. + // If threshold_desc is filled-in, it implies the organ_set_bonus Status Effect should be activated. // Without it, we'll assume there isn't a Status Effect to look for. var/has_threshold = (infuser_entry.threshold_desc != DNA_INFUSION_NO_THRESHOLD) - var/total_organs_needed = added_status ? added_status.organs_needed : 0 + // How many organs the Status Effect requires to be inserted before it will activate. + var/total_organs_needed = added_status?.organs_needed || 0 + // How many organs are available from the infuser entry. var/total_organs = length(infuser_entry.output_organs) + // Quantity of successfully inserted organs. + var/total_inserted = length(inserted_organs) - // Found a Status Effect but no threshold description. - if(!has_threshold && added_status) - TEST_FAIL("The threshold_desc variable for \"[infuser_entry.type]\" was an empty string when a description was expected.") + // If Status Effect exists, ensure it has a matching threshold description and vice versa. + // Otherwise, ensure both are falsy. + TEST_ASSERT((added_status && has_threshold) || (!added_status && !has_threshold), "The threshold_desc variable for `[infuser_entry.type]` was an empty string when a description was expected.") - // Since threshold_desc is filled-in, we expect the organ set bonus. if(has_threshold) - if(!added_status) - TEST_FAIL("The \"/datum/status_effect/organ_set_bonus\" for \"[infuser_entry.type]\" was not added to the mob when expected.") - else if(!total_organs_needed) - TEST_FAIL("The \"needed_organs\" variable for \"[added_status.type]\" was 0 or null, when a positive number was expected.") - else if(total_organs_needed > total_organs) - TEST_FAIL("The \"output_organs\" list for \"[infuser_entry.type]\" had a length of \"[length(infuser_entry.output_organs)]\" when a minimum of at least [total_organs_needed] organs was specified in \"[added_status.type]\".") - else if(!added_status.bonus_active) - TEST_FAIL("The \"[added_status.type]\" bonus was not activated after inserting [total_inserted] of the [total_organs_needed] required organs in the mob, when it was expected.") + TEST_ASSERT(added_status, "The `/datum/status_effect/organ_set_bonus` for `[infuser_entry.type]` was not added to the mob when expected.") + TEST_ASSERT(total_organs_needed, "The `needed_organs` variable for `[added_status.type]` should be a positive number.") + TEST_ASSERT(total_organs_needed <= total_organs, "The `output_organs` list for `[infuser_entry.type]` had a length of `[length(infuser_entry.output_organs)]` when a minimum of at least [total_organs_needed] organs was specified in `[added_status.type]`.") + TEST_ASSERT(added_status.bonus_active, "The `[added_status.type]` bonus was not activated after inserting [total_inserted] of the [total_organs_needed] required organs in the mob, when it was expected.") // Nothing to do. - if(!total_inserted) + if(total_inserted == 0) continue // Bonus of the Fly mutation swaps out all the organs, making it rather permanent. // As a result, the inserted_organs list is un-usable by this point. - if(istype(infuser_entry, /datum/infuser_entry/fly)) + if(species_changing_entries[infuser_entry.type]) continue // Remove all the organs which were just added. - var/total_removed = 0 for(var/obj/item/organ/test_organ as anything in inserted_organs) test_organ.Remove(lab_rat, special = TRUE) - total_removed += 1 - added_status = check_status_type(lab_rat, /datum/status_effect/organ_set_bonus) + var/datum/status_effect/organ_set_bonus/removed_status = (locate(/datum/status_effect/organ_set_bonus) in lab_rat.status_effects) // Search for added Status Effect. - if(added_status && added_status.bonus_active) - TEST_FAIL("The \"[added_status.type]\" bonus was not deactivated after removing [total_removed] of the [total_organs_needed] required organs from the mob, when it was expected.") + TEST_ASSERT(!removed_status || !added_status.bonus_active, "The `[added_status.type]` bonus was not deactivated after removing [total_inserted] of the [total_organs_needed] required organs from the mob, when it was expected.") diff --git a/code/modules/unit_tests/organs.dm b/code/modules/unit_tests/organs.dm new file mode 100644 index 00000000000..478aefcc199 --- /dev/null +++ b/code/modules/unit_tests/organs.dm @@ -0,0 +1,83 @@ +#define TEST_ORGAN_INSERT_MESSAGE(test_organ, message) "`[test_organ.type]/Insert()` [message]" +#define TEST_ORGAN_REMOVE_MESSAGE(test_organ, message) "`[test_organ.type]/Remove()` [message]" + +/// Check organ insertion and removal, for all organ subtypes usable in-game. +/// Ensures algorithmic correctness of the "Insert()" and "Remove()" procs. +/// This test is especially useful because developers frequently override those. +/datum/unit_test/organ_sanity + // List of organ typepaths which cause species change. + // Species change swaps out all the organs, making test_organ un-usable after insertion. + var/static/list/species_changing_organs = typecacheof(list( + /obj/item/organ/internal/brain/shadow/nightmare, + )) + // List of organ typepaths which are not test-able, such as certain class prototypes. + var/static/list/test_organ_blacklist = typecacheof(list( + /obj/item/organ/internal, + /obj/item/organ/external, + /obj/item/organ/external/wings, + /obj/item/organ/internal/cyberimp, + /obj/item/organ/internal/cyberimp/brain, + /obj/item/organ/internal/cyberimp/mouth, + /obj/item/organ/internal/cyberimp/arm, + /obj/item/organ/internal/cyberimp/chest, + /obj/item/organ/internal/cyberimp/eyes, + /obj/item/organ/internal/alien, + )) + +/datum/unit_test/organ_sanity/Run() + for(var/obj/item/organ/organ_type as anything in subtypesof(/obj/item/organ)) + organ_test_insert(organ_type) + +/datum/unit_test/organ_sanity/proc/organ_test_insert(obj/item/organ/organ_type) + // Skip prototypes. + if(test_organ_blacklist[organ_type]) + return + + // Appropriate mob (Human) which will receive organ. + var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent) + var/obj/item/organ/test_organ = new organ_type() + + // Inappropriate mob (Dog) which will hopefully reject organ. + var/mob/living/basic/pet/dog/lab_dog = allocate(/mob/living/basic/pet/dog/corgi) + var/obj/item/organ/reject_organ = new organ_type() + + TEST_ASSERT(test_organ.Insert(lab_rat, special = TRUE, drop_if_replaced = FALSE), TEST_ORGAN_INSERT_MESSAGE(test_organ, "should return TRUE to indicate success.")) + TEST_ASSERT(!reject_organ.Insert(lab_dog, special = TRUE, drop_if_replaced = FALSE), TEST_ORGAN_INSERT_MESSAGE(test_organ, "shouldn't return TRUE when inserting into a basic mob (Corgi).")) + + // Species change swaps out all the organs, making test_organ un-usable by this point. + if(species_changing_organs[test_organ.type]) + return + + TEST_ASSERT(test_organ.owner == lab_rat, TEST_ORGAN_INSERT_MESSAGE(test_organ, "should assign the human to the organ's `owner` var.")) + TEST_ASSERT(test_organ in lab_rat.organs, TEST_ORGAN_INSERT_MESSAGE(test_organ, "should insert the organ into the human's `internal_organs` list.")) + if(test_organ.slot) + TEST_ASSERT(lab_rat.organs_slot[test_organ.slot] == test_organ, TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add the organ to the human's `internal_organs_slot` list.")) + TEST_ASSERT(lab_rat.getorganslot(test_organ.slot) == test_organ, TEST_ORGAN_INSERT_MESSAGE(test_organ, "should make the organ available via human's `getorganslot()` proc.")) + + if(LAZYLEN(test_organ.organ_traits)) + TEST_ASSERT(LAZYLEN(lab_rat.status_traits), TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add Traits to lazylist `human.status_traits`.")) + for(var/test_trait in test_organ.organ_traits) + TEST_ASSERT(HAS_TRAIT(lab_rat, test_trait), TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add Trait `[test_trait]` to lazylist `human.status_traits`")) + + if(LAZYLEN(test_organ.actions)) + TEST_ASSERT(LAZYLEN(lab_rat.actions), TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add Actions to lazylist `human.actions`.")) + for(var/datum/action/test_action as anything in test_organ.actions) + TEST_ASSERT(test_action in lab_rat.actions, TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add Action `[test_action]` to lazylist `human.actions`.")) + + if(LAZYLEN(test_organ.organ_effects)) + TEST_ASSERT(LAZYLEN(lab_rat.status_effects), TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add Status Effects to lazylist `human.status_effects`.")) + for(var/datum/status_effect/test_status as anything in test_organ.organ_effects) + TEST_ASSERT(test_status in lab_rat.status_effects, TEST_ORGAN_INSERT_MESSAGE(test_organ, "should add Status Effect `[test_status]` to lazylist `human.status_effects`.")) + + test_organ.Remove(lab_rat, special = TRUE) + + TEST_ASSERT(test_organ.owner == null, TEST_ORGAN_REMOVE_MESSAGE(test_organ, "should assign the organ's `owner` var to null.")) + TEST_ASSERT(!(test_organ in lab_rat.organs), TEST_ORGAN_REMOVE_MESSAGE(test_organ, "should remove the organ from the human's `internal_organs` list.")) + if(test_organ.slot) + TEST_ASSERT(lab_rat.organs_slot[test_organ.slot] != test_organ, TEST_ORGAN_REMOVE_MESSAGE(test_organ, "should remove the organ from the human's `internal_organs_slot` list.")) + TEST_ASSERT(lab_rat.getorganslot(test_organ.slot) != test_organ, TEST_ORGAN_REMOVE_MESSAGE(test_organ, "should remove the organ from the human's `getorganslot()` proc.")) + + return + +#undef TEST_ORGAN_INSERT_MESSAGE +#undef TEST_ORGAN_REMOVE_MESSAGE diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index 0aab091aaa1..ab2bc776a27 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -46,6 +46,9 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) var/list/allocated var/list/fail_reasons + /// Do not instantiate if type matches this + var/abstract_type = /datum/unit_test + var/static/datum/space_level/reservation /proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b) @@ -74,7 +77,7 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) return ..() /datum/unit_test/proc/Run() - TEST_FAIL("Run() called parent or not implemented") + TEST_FAIL("[type]/Run() called parent or not implemented") /datum/unit_test/proc/Fail(reason = "No reason", file = "OUTDATED_TEST", line = 1) succeeded = FALSE @@ -146,8 +149,11 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) log_world("::[priority] file=[file],line=[line],title=[map_name]: [type]::[annotation_text]") -/proc/RunUnitTest(test_path, list/test_results) - if (ispath(test_path, /datum/unit_test/focus_only)) +/proc/RunUnitTest(datum/unit_test/test_path, list/test_results) + if(ispath(test_path, /datum/unit_test/focus_only)) + return + + if(initial(test_path.abstract_type) == test_path) return var/datum/unit_test/test = new test_path diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm index 7893a755b2c..8623329a6f6 100644 --- a/code/modules/wiremod/shell/brain_computer_interface.dm +++ b/code/modules/wiremod/shell/brain_computer_interface.dm @@ -17,9 +17,8 @@ new /obj/item/circuit_component/bci_core, ), SHELL_CAPACITY_SMALL, starting_circuit = circuit) -/obj/item/organ/internal/cyberimp/bci/Insert(mob/living/carbon/receiver, special, drop_if_replaced) +/obj/item/organ/internal/cyberimp/bci/on_insert(mob/living/carbon/receiver) . = ..() - // Organs are put in nullspace, but this breaks circuit interactions forceMove(receiver) diff --git a/code/modules/zombie/organs.dm b/code/modules/zombie/organs.dm index 4969e07734f..3b64934d278 100644 --- a/code/modules/zombie/organs.dm +++ b/code/modules/zombie/organs.dm @@ -25,6 +25,8 @@ /obj/item/organ/internal/zombie_infection/Insert(mob/living/carbon/M, special = FALSE, drop_if_replaced = TRUE) . = ..() + if(!.) + return . START_PROCESSING(SSobj, src) /obj/item/organ/internal/zombie_infection/Remove(mob/living/carbon/M, special = FALSE)