mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-17 20:47:29 +00:00
## About The Pull Request ### How things work As things currently stand, when a mob breaths several things happen (simplified to focus on the stupid) We assert the existance of all possible breathable gases, and pull partial pressures for them Then we walk through all possible interactions lungs could have with these gases, one by one, and see if they're happening or not As we go we are forced to cleanup potential alerts caused by the previous breath, even if those effects never actually happen At the end we clear out all the unused gas ids, and handle the temperature of the breath. ### What sucks There's I'd say 3 different types of gas reactions. - You can "need" a gas to survive. o2, n2 and plasma all fall into this category - A gas can do something to you while it's in your system. This applies to most gas types - Variation on the previous, some gases do cleanup when they're not in your system, or when there isn't much of them in the first place The main headache here is that second one, constantly cleaning up potential side effects sucks, and fixing it would require a lot of dummy variables There's other suckage too. Needing to constantly check for a gas type even if it isn't there is stupid, and leads to wasted time It's also really annoying to do subtypes in this system. There is what amounts to a hook proc you can override, but you can't override the reaction to a gas type. It also just like, sucks to add new gases. one mega proc smells real stupid. ### Improvements In the interest of speed: - I'd like to build a system that doesn't require manually checking for gas - Reacting to gas "disappearing" should be promoted by the system, instead of being hacky. - I would like to avoid needing to assert the existence of all possible gases, as this is slow on both the assert and the garbage collect. In the interest of dev ergonomics: - It should be easy to define a new gas reaction - It should be easy for subtypes to implement their own gas reactions. The current method of vars on the lung is all tangled up and not really undoable as of now, but I'd like to not require it - It should be possible to fully override how a gas is handled ### What I've Done Lungs have 3 lists of proc paths stored on them Each list handles a different way the lung might want to interact with a gas. There's a list for always processing on a gas (we use this for stuff that's breathed), a list for handling a gas in our breath, and a list for reacting to a gas previously being in our breath, but not any more. Lungs fill out these lists using a helper proc during Initialize() Then, when it comes time to breath, we loop over the gas in the breath and react to it. We also keep track of the previous list of partial pressures, which we calculate for free here, and use that to figure out when to call the loss reactions. This proc pattern allows for overrides, easy reactions to removals, lower indentation code and early returns, and better organization of signal handlers It's also significantly faster. Ballpark 4x faster ### Misc Removes support for breathing co2, and dying from n2 poisoning. They were both unused, and I think it's cringe to clutter these procs even further Added "do we even have oxyloss" checks to most cases of passive breathing. This is a significant save, since redundant adjustoxy's are decently expensive at the volume of calls we have here. Fixes a bug with breathing out if no gas is passed in, assigning a var to another var doesn't perform a copy Rewrote breathe_gas_volume() slightly to insert gas into an immutable mix stored on the lung, rather then one passed in This avoids passing of a gas_mixture around just to fill a hole. I may change my mind on this, since it would be nice to have support for temperature changing from a hot/cold breath. Not gonna be done off bodytemp tho lord no. Uses merge() instead of a hard coded version to move the gas ids over. This is slightly slower with lower gas counts but supports more things in future and is also just easier to read. ## Why It's Good For The Game Faster, easier to work with and read (imo) Profiles: [breath_results_old.txt](https://github.com/tgstation/tgstation/files/11068247/breath_results_old.txt) [breath_results_pre_master.txt](https://github.com/tgstation/tgstation/files/11068248/breath_results_new.txt) [breath_results_new.txt](https://github.com/tgstation/tgstation/files/11068349/breath_results_new.txt) (These profiles were initially missing #73026. Merging this brings the savings from 16% to 12%. Life is pain) --------- Co-authored-by: san7890 <the@san7890.com>
203 lines
12 KiB
Plaintext
203 lines
12 KiB
Plaintext
#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_plasma = test_lungs.safe_plasma_min
|
|
var/max_oxygen = test_lungs.safe_oxygen_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_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_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_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
|