From d920ae7315b0812eb97d655a2e06e28fe086f261 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:21:42 -0500 Subject: [PATCH] Fix telekinesis / ai / mecha door usage + Adds more airlock access unit tests (#91799) ## About The Pull Request Fixes #91795 Fixes #91790 Checking for `MOBILITY_USE` in `allowed` has too broad of an effect, catching mobs which don't have `MOBILITY_USE` but should otherwise be able to use doors. I checked with JFW and this was done to prevent spirits from accessing doors on click. So I just moved it to attack hand. Telekinesis interactions was also fixed by adding a `check_access(null)` check back to `allowed` as it used to have. Not sure why it was ever removed. ## Changelog :cl: Melbert fix: AIs can open windoors fix: Mechas can open airlocks fix: Handcuffed people can open public airlocks fix: Telekinesis can open public airlocks /:cl: --- code/game/machinery/doors/door.dm | 10 +- code/game/machinery/doors/windowdoor.dm | 35 +++++++ code/modules/jobs/access.dm | 2 +- code/modules/unit_tests/door_access.dm | 126 +++++++++++++++++++++++- 4 files changed, 163 insertions(+), 10 deletions(-) diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index ed564e9a483..bb4d3de0b69 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -303,6 +303,12 @@ . = ..() if(.) return + // Stops people without +USE from being able to click-open airlocks + // Explicitly not a generic check - if you make this generic, AIs (and more) won't be able to open doors + if(isliving(user)) + var/mob/living/living_user = user + if(!(living_user.mobility_flags & MOBILITY_USE)) + return if(try_remove_seal(user)) return if(try_safety_unlock(user)) @@ -328,10 +334,6 @@ run_animation(DOOR_DENY_ANIMATION) /obj/machinery/door/allowed(mob/M) - if(isliving(M)) - var/mob/living/living_user = M - if(!(living_user.mobility_flags & MOBILITY_USE)) - return FALSE if(emergency) return TRUE if(unrestricted_side(M)) diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm index ba44acc126b..0c7004dde92 100644 --- a/code/game/machinery/doors/windowdoor.dm +++ b/code/game/machinery/doors/windowdoor.dm @@ -525,3 +525,38 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/brigdoor/security/holding /obj/machinery/door/window/brigdoor/security/holding/right icon_state = "rightsecure" base_state = "rightsecure" + +/* + * Subtype used in unit tests to ensure instant windoor open/close +*/ +/obj/machinery/door/window/instant + +/obj/machinery/door/window/instant/open(forced = DEFAULT_DOOR_CHECKS) + if(!density || operating || !try_to_force_door_open(forced)) + return FALSE + + operating = TRUE + + set_density(FALSE) + air_update_turf(TRUE, FALSE) + update_freelook_sight() + + operating = FALSE + update_appearance() + + return TRUE + +/obj/machinery/door/window/instant/close(forced = DEFAULT_DOOR_CHECKS) + if(density || operating || !try_to_force_door_shut(forced)) + return FALSE + + operating = TRUE + + set_density(TRUE) + air_update_turf(TRUE, TRUE) + update_freelook_sight() + + operating = FALSE + update_appearance() + + return TRUE diff --git a/code/modules/jobs/access.dm b/code/modules/jobs/access.dm index e9b2e832f92..9d2d443fd37 100644 --- a/code/modules/jobs/access.dm +++ b/code/modules/jobs/access.dm @@ -10,7 +10,7 @@ if(result_bitflags & COMPONENT_OBJ_DISALLOW) // override all other checks return FALSE if(isnull(accessor)) //likely a TK user. - return FALSE + return check_access(null) if(isAdminGhostAI(accessor)) //Access can't stop the abuse return TRUE diff --git a/code/modules/unit_tests/door_access.dm b/code/modules/unit_tests/door_access.dm index 86250167c1d..0f518c2449c 100644 --- a/code/modules/unit_tests/door_access.dm +++ b/code/modules/unit_tests/door_access.dm @@ -4,11 +4,13 @@ /datum/unit_test/door_access_check/Run() var/mob/living/carbon/human/subject = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left) var/obj/machinery/door/airlock/instant/door = allocate(/obj/machinery/door/airlock/instant, run_loc_floor_bottom_left, EAST) //special subtype that just flips the density var on open() and close(), akin to a real airlock. + door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE // First, test that someone without any access can open a door that doesn't have any access requirements. Let's test it via using the bumpopen() proc, called when someone bumps into the door. - door.bumpopen(subject) + subject.Bump(door) TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-free airlock!") door.close() // close it here as well + subject.last_bumped = 0 // Alright, now let's test that someone with access can open a door that requires access when only req_access is set. subject.equipOutfit(/datum/outfit/job/assistant/consistent) // set up the outfit here to ensure the last check is pure. @@ -17,26 +19,140 @@ // Test two accesses at once just to make sure the script hasn't changed on us. keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) - door.bumpopen(subject) + subject.Bump(door) TEST_ASSERT_EQUAL(door.density, FALSE, "Subject with valid access failed to open airlock access-locked behind req_access!") door.close() + subject.last_bumped = 0 // Okay, now let's edit the req_access on the door to make sure the subject can't open it with the requirements of req_access (must have all accesses required on keycard to open door). door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS, ACCESS_CARGO) - door.bumpopen(subject) + subject.Bump(door) TEST_ASSERT_EQUAL(door.density, TRUE, "Subject with invalid access succeeded in opening airlock access-locked behind req_access!") door.close() // included for completeness, will early return if the door is already closed. + subject.last_bumped = 0 // Alright, now to test req_one_access. The two systems should be mutually exclusive, so we'll reset the access on the keycard and the door before we continue.. door.req_access = null door.req_one_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) keycard.access = list(ACCESS_MAINT_TUNNELS) - door.bumpopen(subject) + subject.Bump(door) TEST_ASSERT_EQUAL(door.density, FALSE, "Subject with valid access failed to open airlock access-locked behind req_one_access!") door.close() + subject.last_bumped = 0 // Now, let's test req_one_access with an invalid access. The keycard is still on ACCESS_MAINT_TUNNELS from last step. door.req_one_access = list(ACCESS_ENGINEERING, ACCESS_CARGO) - door.bumpopen(subject) + subject.Bump(door) TEST_ASSERT_EQUAL(door.density, TRUE, "Subject with invalid access succeeded in opening airlock access-locked behind req_one_access!") + +/// Tests that the AI can open doors +/datum/unit_test/door_access_ai + +/datum/unit_test/door_access_ai/Run() + var/mob/dead/observer/fake_ghost = allocate(__IMPLIED_TYPE__) // ai must be passed a mob in /new, cringe + var/mob/living/silicon/ai/subject = allocate(__IMPLIED_TYPE__, run_loc_floor_top_right, null, fake_ghost) + var/obj/machinery/door/airlock/instant/door = allocate(__IMPLIED_TYPE__) + door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE + + door.AIShiftClick(subject) + TEST_ASSERT_EQUAL(door.density, FALSE, "AI failed to open access-free airlock!") + +/// Tests that the AI can open windoors +/datum/unit_test/windoor_access_ai + +/datum/unit_test/windoor_access_ai/Run() + var/mob/dead/observer/fake_ghost = allocate(__IMPLIED_TYPE__) // ai must be passed a mob in /new, cringe + var/mob/living/silicon/ai/subject = allocate(__IMPLIED_TYPE__, run_loc_floor_top_right, null, fake_ghost) + var/obj/machinery/door/window/instant/door = allocate(__IMPLIED_TYPE__) + door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE + + door.attack_ai(subject) + TEST_ASSERT_EQUAL(door.density, FALSE, "AI failed to open access-free window door!") + +/// Tests that telekinesis can open airlocks without access set (and cannot open airlocks that have an access set) +/datum/unit_test/door_access_telekinesis + +/datum/unit_test/door_access_telekinesis/Run() + var/mob/living/carbon/human/consistent/subject = allocate(__IMPLIED_TYPE__, run_loc_floor_top_right) + var/obj/machinery/door/airlock/instant/door = allocate(__IMPLIED_TYPE__, run_loc_floor_bottom_left) + door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE + subject.dna.add_mutation(/datum/mutation/telekinesis, list(INNATE_TRAIT)) + subject.equipOutfit(/datum/outfit/job/assistant/consistent) + + var/obj/item/card/id/advanced/keycard = subject.wear_id + keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) + + // Test TK on an access-free airlock + door.attack_tk(subject) + TEST_ASSERT_EQUAL(door.density, FALSE, "Subject with telekinesis failed to open access-free airlock at range!") door.close() + + // Test TK on an access-locked airlock while having valid access - this should fail + door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) + door.attack_tk(subject) + TEST_ASSERT_EQUAL(door.density, TRUE, "Subject with telekinesis managed to open access-locked airlock at range (with access)!") + + // Test TK on an access-locked airlock without having valid access - this should also fail + keycard.access = list() + door.attack_tk(subject) + TEST_ASSERT_EQUAL(door.density, TRUE, "Subject with telekinesis managed to open access-locked airlock at range (with no access)!") + +/// Tests that mechas can bump open airlocks +/datum/unit_test/door_access_mecha + +/datum/unit_test/door_access_mecha/Run() + var/obj/vehicle/sealed/mecha/ripley/subject_mech = allocate(__IMPLIED_TYPE__) + var/mob/living/carbon/human/consistent/subject_pilot = allocate(__IMPLIED_TYPE__, run_loc_floor_top_right) + var/obj/machinery/door/airlock/instant/door = allocate(__IMPLIED_TYPE__) + door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE + subject_pilot.equipOutfit(/datum/outfit/job/assistant/consistent) + subject_mech.accesses = list() + subject_mech.mob_enter(subject_pilot) + + var/obj/item/card/id/advanced/keycard = subject_pilot.wear_id + keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) + + // Test bumping an access-free airlock - this should open + subject_mech.Bump(door) + TEST_ASSERT_EQUAL(door.density, FALSE, "Mecha failed to open access-free airlock!") + door.close() + + // Setting an access on the door, this should open + door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) + subject_mech.Bump(door) + TEST_ASSERT_EQUAL(door.density, FALSE, "Mecha failed to open access-locked airlock with valid access!") + door.close() + + // Now setting a different access on the door, this should not open + door.req_access = list(ACCESS_CARGO) + subject_mech.Bump(door) + TEST_ASSERT_EQUAL(door.density, TRUE, "Mecha opened access-locked airlock with invalid access!") + +/// Checks that hands_blocked mobs cannot open doors unless it's an access-free door. +/datum/unit_test/door_access_handcuffs + +/datum/unit_test/door_access_handcuffs/Run() + var/mob/living/carbon/human/subject = allocate(__IMPLIED_TYPE__, run_loc_floor_bottom_left) + var/obj/machinery/door/airlock/instant/door = allocate(__IMPLIED_TYPE__) + door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE + subject.equipOutfit(/datum/outfit/job/assistant/consistent) + ADD_TRAIT(subject, TRAIT_HANDS_BLOCKED, INNATE_TRAIT) + + var/obj/item/card/id/advanced/keycard = subject.wear_id + keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) + + // Test that you can bump open an access-free airlock with hands blocked. + subject.Bump(door) + TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to bump open access-free airlock while hands blocked!") + door.close() + subject.last_bumped = 0 + + // Attack handing should fail though - because unarmed attack fails while hands are blocked. + subject.UnarmedAttack(door) + TEST_ASSERT_EQUAL(door.density, TRUE, "Subject opened attack-handed open access-free airlock while hands blocked!") + door.close() + + // Now adding an access, this should not open even though we have access. + door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS) + subject.Bump(door) + TEST_ASSERT_EQUAL(door.density, TRUE, "Subject opened access-locked airlock while hands blocked!")