From 2a9faabcd77c300d867f7e18f9e6fead049d7734 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:14:42 -0600 Subject: [PATCH] Fix wound surgical state (#94858) ## About The Pull Request Fixes #94855 Three things 1. We filter incompatible surgical states when removing the wound. I'm not sure if this contributed to the problem, but it seemed wrong regardless - At that point we don't really care about incompatible states since we're removing everything regardless 2. `COMSIG_BODYPART_UPDATING_SURGERY_STATE` was registered before adding the states, which would cause the act of adding the states to clear the variable. 3. The signal handler was missing the source arg, so it was removing the wrong values. ## Changelog :cl: Melbert fix: Surgical state applied by wounds no longer persist after fix /:cl: --- code/datums/wounds/_wounds.dm | 20 ++-- .../surgery/operations/operation_generic.dm | 4 +- code/modules/unit_tests/surgeries.dm | 100 ++++++++++++++++++ 3 files changed, 110 insertions(+), 14 deletions(-) diff --git a/code/datums/wounds/_wounds.dm b/code/datums/wounds/_wounds.dm index 8eda251d1ae..bed3a0b019c 100644 --- a/code/datums/wounds/_wounds.dm +++ b/code/datums/wounds/_wounds.dm @@ -79,6 +79,7 @@ /// How much we're contributing to this limb's bleed_rate var/blood_flow /// Surgical states we're applying to our limb + /// Note: This var is mutated after it is applied, to only states that were successfully applied var/surgery_states = NONE /// How much having this wound will add to all future check_wounding() rolls on this limb, to allow progression to worse injuries with repeated damage @@ -304,15 +305,6 @@ limb.update_wounds(replaced) if (disabling) limb.remove_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src)) - - if(surgery_states) - for (var/state in exclusive_surgery_states) - if (!(limb.surgery_state & exclusive_surgery_states[state])) - continue - var/actual_state = text2num(state) - if (actual_state & surgery_states) - surgery_states &= ~actual_state - if (surgery_states) limb.remove_surgical_state(surgery_states) @@ -323,11 +315,10 @@ if (limb) RegisterSignal(limb, COMSIG_QDELETING, PROC_REF(source_died)) RegisterSignals(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_UNGAUZED), PROC_REF(gauze_state_changed)) - RegisterSignal(limb, COMSIG_BODYPART_UPDATING_SURGERY_STATE, PROC_REF(on_surgery_state_change)) if (disabling) limb.add_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src)) - if(surgery_states) + if(surgery_states) // first check filters invalid states for (var/state in exclusive_surgery_states) if (!(limb.surgery_state & exclusive_surgery_states[state])) continue @@ -335,8 +326,10 @@ if (actual_state & surgery_states) surgery_states &= ~actual_state - if (surgery_states) + if (surgery_states) // second check applies any remaining valid states limb.add_surgical_state(surgery_states) + // NB: don't check state changes until AFTER we finish apply our own, or we'll just undo ourselves + RegisterSignal(limb, COMSIG_BODYPART_UPDATING_SURGERY_STATE, PROC_REF(on_surgery_state_change)) if (victim) start_limping_if_we_should() // the status effect already handles removing itself @@ -345,8 +338,9 @@ update_inefficiencies(replaced) /// Used to remove states applied or removed by operations from ourselves as to not remove them if we heal mid-surgery -/datum/wound/proc/on_surgery_state_change(old_state, surgery_state, changed_states) +/datum/wound/proc/on_surgery_state_change(datum/source, old_state, current_state, changed_states) SIGNAL_HANDLER + // Any state that changes, adding or removing, should henceforth be untouched by us. Let the surgeon handle it. surgery_states &= ~changed_states /datum/wound/proc/add_or_remove_actionspeed_mod() diff --git a/code/modules/surgery/operations/operation_generic.dm b/code/modules/surgery/operations/operation_generic.dm index 1f08ccd90e4..c670c09c873 100644 --- a/code/modules/surgery/operations/operation_generic.dm +++ b/code/modules/surgery/operations/operation_generic.dm @@ -131,7 +131,9 @@ /datum/surgery_operation/limb/retract_skin/on_success(obj/item/bodypart/limb) . = ..() - limb.add_surgical_state(SURGERY_SKIN_OPEN) + // the limb SHOULD either have unclamped or clamped vessels if we're retracting skin + // if it doesn't, some shenanigans happened (likely due to wounds), so we add unclamped if needed - just to be thorough + limb.add_surgical_state(SURGERY_SKIN_OPEN | (LIMB_HAS_SURGERY_STATE(limb, SURGERY_VESSELS_CLAMPED) ? NONE : SURGERY_VESSELS_UNCLAMPED)) limb.remove_surgical_state(SURGERY_SKIN_CUT) /datum/surgery_operation/limb/retract_skin/abductor diff --git a/code/modules/unit_tests/surgeries.dm b/code/modules/unit_tests/surgeries.dm index deb154742f8..69bc29fb148 100644 --- a/code/modules/unit_tests/surgeries.dm +++ b/code/modules/unit_tests/surgeries.dm @@ -244,3 +244,103 @@ var/obj/item/clothing/under/jumpsuit = test_mob.get_item_by_slot(ITEM_SLOT_ICLOTHING) jumpsuit.adjust_to_alt() TEST_ASSERT(test_mob.is_location_accessible(BODY_ZONE_CHEST), "Chest should be accessible after rolling jumpsuit down") + +/// Tests surgeries which just modify basic surgical states +/datum/unit_test/state_surgeries + +/datum/unit_test/state_surgeries/Run() + var/mob/living/carbon/human/patient = allocate(/mob/living/carbon/human/consistent) + var/mob/living/carbon/human/surgeon = allocate(/mob/living/carbon/human/consistent) + var/obj/item/scalpel/scalpel = allocate(/obj/item/scalpel) + var/obj/item/retractor/retractor = allocate(/obj/item/retractor) + var/obj/item/circular_saw/saw = allocate(/obj/item/circular_saw) + var/obj/item/hemostat/hemostat = allocate(/obj/item/hemostat) + var/obj/item/cautery/cautery = allocate(/obj/item/cautery) + var/obj/item/bodypart/chest/chest = patient.get_bodypart(BODY_ZONE_CHEST) + + var/datum/surgery_operation/limb/incise_skin/isurgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + UNLINT(isurgery.success(chest, surgeon, scalpel, list())) + + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_CUT), "Making an incision did not apply the skin cut surgical state") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_UNCLAMPED), "Making an incision did not apply the vessels unclamped surgical state") + + var/datum/surgery_operation/limb/retract_skin/rsurgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + UNLINT(rsurgery.success(chest, surgeon, retractor, list())) + + TEST_ASSERT(!LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_CUT), "Retracting skin did not remove the skin cut surgical state") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_OPEN), "Retracting skin did not apply the skin open surgical state") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_UNCLAMPED), "Retracting skin removed the vessels unclamped surgical state unexpectedly") + + var/datum/surgery_operation/limb/clamp_bleeders/csurgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + UNLINT(csurgery.success(chest, surgeon, hemostat, list())) + + TEST_ASSERT(!LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_UNCLAMPED), "Clamping bleeders did not remove the vessels unclamped surgical state") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_CLAMPED), "Clamping bleeders did not apply the vessels clamped surgical state") + + var/datum/surgery_operation/limb/saw_bones/ssurgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + UNLINT(ssurgery.success(chest, surgeon, saw, list())) + + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_BONE_SAWED), "Sawing bones did not apply the bone sawed surgical state") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_CLAMPED), "Sawing bones removed the vessels clamped surgical state unexpectedly") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_OPEN), "Sawing bones removed the skin open surgical state unexpectedly") + + var/datum/surgery_operation/limb/incise_organs/osurgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + UNLINT(osurgery.success(chest, surgeon, scalpel, list())) + + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_ORGANS_CUT), "Incising organs did not apply the organs incision surgical state") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_BONE_SAWED), "Incising organs removed the bone sawed surgical state unexpectedly") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_CLAMPED), "Incising organs removed the vessels clamped surgical state unexpectedly") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_OPEN), "Incising organs removed the skin open surgical state unexpectedly") + + var/datum/surgery_operation/limb/close_skin/msurgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + UNLINT(msurgery.success(chest, surgeon, cautery, list())) + + TEST_ASSERT(!LIMB_HAS_ANY_SURGERY_STATE(chest, ALL), "Closing surgery did not remove all surgical states applied during surgery") + +/// Checks wounds apply surgical state and remove surgical state when healed +/datum/unit_test/wound_state + +/datum/unit_test/wound_state/Run() + var/mob/living/carbon/human/patient = allocate(/mob/living/carbon/human/consistent) + var/obj/item/bodypart/chest/chest = patient.get_bodypart(BODY_ZONE_CHEST) + + var/datum/wound/slash/flesh/severe/wound = new() + wound.surgery_states = SURGERY_SKIN_CUT | SURGERY_VESSELS_UNCLAMPED + wound.apply_wound(chest) + + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_CUT|SURGERY_VESSELS_UNCLAMPED), "Wound did not apply correct surgery states to limb") + + qdel(wound) + + TEST_ASSERT(!LIMB_HAS_ANY_SURGERY_STATE(chest, SURGERY_SKIN_CUT|SURGERY_VESSELS_UNCLAMPED), "Wound did not remove surgery states from limb upon healing") + +/// Checks surgical states applied by surgery while wounded don't get removed when the wound heals +/datum/unit_test/wound_state_with_surgery + +/datum/unit_test/wound_state_with_surgery/Run() + var/mob/living/carbon/human/patient = allocate(/mob/living/carbon/human/consistent) + var/obj/item/bodypart/chest/chest = patient.get_bodypart(BODY_ZONE_CHEST) + + var/datum/wound/slash/flesh/severe/wound = new() + wound.surgery_states = SURGERY_SKIN_CUT | SURGERY_VESSELS_UNCLAMPED | SURGERY_BONE_DRILLED + wound.apply_wound(chest) + + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_CUT|SURGERY_VESSELS_UNCLAMPED|SURGERY_BONE_DRILLED), "Wound did not apply correct surgery states to limb") + + var/datum/surgery_operation/limb/retract_skin/surgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + var/mob/living/carbon/human/surgeon = allocate(/mob/living/carbon/human/consistent) + var/obj/item/retractor/retractor = allocate(/obj/item/retractor) + + UNLINT(surgery.success(chest, surgeon, retractor, list())) + + TEST_ASSERT(!LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_CUT), "Surgical state for skin cut was not removed after surgery opened the cut") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_OPEN), "Surgical state for skin open was not applied after surgery opened the cut") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_UNCLAMPED), "Surgical state for unclamped vessels was incorrectly removed by surgery that only opened the skin") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_BONE_DRILLED), "Surgical state for bone drilled was incorrectly removed by surgery that only opened the skin") + + qdel(wound) + + TEST_ASSERT(!LIMB_HAS_ANY_SURGERY_STATE(chest, SURGERY_SKIN_CUT), "Surgical state for skin cut was somehow re-applied upon wound healing") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_SKIN_OPEN), "Surgical state for skin open was incorrectly removed upon wound healing") + TEST_ASSERT(LIMB_HAS_SURGERY_STATE(chest, SURGERY_VESSELS_UNCLAMPED), "Surgical state for unclamped vessels was incorrectly removed upon wound healing") + TEST_ASSERT(!LIMB_HAS_SURGERY_STATE(chest, SURGERY_BONE_DRILLED), "Surgical state for bone drilled was not correctly removed upon wound healing")