diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index f89045c378a..364bda112d6 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -283,6 +283,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_KISS_OF_DEATH "kiss_of_death" /// Used on limbs in the process of turning a human into a plasmaman while in plasma lava #define TRAIT_PLASMABURNT "plasma_burnt" +/// This mob should never close UI even if it doesn't have a client +#define TRAIT_PRESERVE_UI_WITHOUT_CLIENT "preserve_ui_without_client" #define TRAIT_NOBLEED "nobleed" //This carbon doesn't bleed /// This atom can ignore the "is on a turf" check for simple AI datum attacks, allowing them to attack from bags or lockers as long as any other conditions are met diff --git a/code/datums/elements/strippable.dm b/code/datums/elements/strippable.dm index b2d53de019b..edc4714d52f 100644 --- a/code/datums/elements/strippable.dm +++ b/code/datums/elements/strippable.dm @@ -463,19 +463,18 @@ /datum/strip_menu/ui_host(mob/user) return owner +/datum/strip_menu/ui_state(mob/user) + return GLOB.always_state + /datum/strip_menu/ui_status(mob/user, datum/ui_state/state) - . = ..() - - if (isliving(user)) - var/mob/living/living_user = user - - if ( - . == UI_UPDATE \ - && user.stat == CONSCIOUS \ - && living_user.body_position == LYING_DOWN \ - && user.Adjacent(owner) - ) - return UI_INTERACTIVE + return min( + ui_status_only_living(user, owner), + ui_status_user_is_adjacent(user, owner), + max( + ui_status_user_is_conscious_and_lying_down(user), + ui_status_user_is_abled(user, owner), + ), + ) /// Creates an assoc list of keys to /datum/strippable_item /proc/create_strippable_list(types) diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm index 913c47e2a31..4a665f828be 100644 --- a/code/datums/mocking/client.dm +++ b/code/datums/mocking/client.dm @@ -2,3 +2,6 @@ /datum/client_interface /// Player preferences datum for the client var/datum/preferences/prefs + + /// The view of the client, similar to /client/var/view. + var/view = "15x15" diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm index 73c053254d8..2c8c5ce10a4 100644 --- a/code/modules/tgui/states.dm +++ b/code/modules/tgui/states.dm @@ -61,7 +61,7 @@ */ /mob/proc/shared_ui_interaction(src_object) // Close UIs if mindless. - if(!client) + if(!client && !HAS_TRAIT(src, TRAIT_PRESERVE_UI_WITHOUT_CLIENT)) return UI_CLOSE // Disable UIs if unconcious. else if(stat) diff --git a/code/modules/tgui/status_composers.dm b/code/modules/tgui/status_composers.dm new file mode 100644 index 00000000000..3e59f3f70f7 --- /dev/null +++ b/code/modules/tgui/status_composers.dm @@ -0,0 +1,98 @@ +/// The sane defaults for a UI such as a computer or a machine. +/proc/default_ui_state(mob/user, atom/source) + return min( + ui_status_user_is_abled(user, source), + ui_status_user_is_advanced_tool_user(user), + ui_status_only_living(user), + max( + ui_status_user_is_adjacent(user, source), + ui_status_silicon_has_access(user, source), + ) + ) + +/// Returns a UI status such that users adjacent to source will be able to interact, +/// far away users will be able to see, and anyone farther won't see anything. +/// Dead users will receive updates no matter what, though you likely want to add +/// a [`ui_status_only_living`] check for finer observer interactions. +/proc/ui_status_user_is_adjacent(mob/user, atom/source) + if (isliving(user)) + var/mob/living/living_user = user + return living_user.shared_living_ui_distance(source) + else + return UI_UPDATE + +/// Returns a UI status such that the dead will be able to watch, but not interact. +/proc/ui_status_only_living(mob/user, source) + if (isliving(user)) + return UI_INTERACTIVE + + if(isobserver(user)) + // If they turn on ghost AI control, admins can always interact. + if(isAdminGhostAI(user)) + return UI_INTERACTIVE + + // Regular ghosts can always at least view if in range. + var/datum/client_interface/client = GET_CLIENT(user) + if(client) + var/clientviewlist = getviewsize(client.view) + if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2])) + return UI_UPDATE + + return UI_CLOSE + +/// Returns a UI status such that users with debilitating conditions, such as +/// being dead or not having power for silicons, will not be able to interact. +/// Being dead will disable UI, being incapacitated will continue updating it, +/// and anything else will make it interactive. +/proc/ui_status_user_is_abled(mob/user, atom/source) + return user.shared_ui_interaction(source) + +/// Returns a UI status such that advanced tool users will be able to update, +/// but everyone else can only watch. +/proc/ui_status_user_is_advanced_tool_user(mob/user) + return ISADVANCEDTOOLUSER(user) ? UI_INTERACTIVE : UI_UPDATE + +/// Returns a UI status such that silicons will be able to interact with whatever +/// they would have access to if this was a machine. For example, AIs can +/// interact if there's cameras with wireless control is enabled. +/proc/ui_status_silicon_has_access(mob/user, atom/source) + if (!issilicon(user)) + return UI_CLOSE + var/mob/living/silicon/silicon_user = user + return silicon_user.get_ui_access(source) + +/// Returns a UI status representing this silicon's capability to access +/// the given source. Called by `ui_status_silicon_has_access`. +/mob/living/silicon/proc/get_ui_access(atom/source) + return UI_CLOSE + +/mob/living/silicon/robot/get_ui_access(atom/source) + // Robots can interact with anything they can see. + var/list/clientviewlist = getviewsize(client.view) + if(get_dist(src, source) <= min(clientviewlist[1],clientviewlist[2])) + return UI_INTERACTIVE + return UI_DISABLED // Otherwise they can keep the UI open. + +/mob/living/silicon/ai/get_ui_access(atom/source) + // The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled. + if(!control_disabled && can_see(source)) + return UI_INTERACTIVE + return UI_CLOSE + +/mob/living/silicon/pai/get_ui_access(atom/source) + // pAIs can only use themselves and the owner's radio. + if((source == src || source == radio) && !stat) + return UI_INTERACTIVE + else + return UI_CLOSE + +/// Returns UI_INTERACTIVE if the user is conscious and lying down. +/// Returns UI_UPDATE otherwise. +/proc/ui_status_user_is_conscious_and_lying_down(mob/user) + if (!isliving(user)) + return UI_UPDATE + + var/mob/living/living_user = user + return (living_user.body_position == LYING_DOWN && living_user.stat == CONSCIOUS) \ + ? UI_INTERACTIVE \ + : UI_UPDATE diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index a8198785f44..47b0a6bf64e 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -37,6 +37,9 @@ #define UNIT_TEST_FAILED 1 #define UNIT_TEST_SKIPPED 2 +/// A trait source when adding traits through unit tests +#define TRAIT_SOURCE_UNIT_TESTS "unit_tests" + #include "anchored_mobs.dm" #include "bespoke_id.dm" #include "binary_insert.dm" @@ -79,6 +82,7 @@ #include "spawn_mobs.dm" #include "species_whitelists.dm" #include "stomach.dm" +#include "strippable.dm" #include "subsystem_init.dm" #include "surgeries.dm" #include "teleporters.dm" diff --git a/code/modules/unit_tests/strippable.dm b/code/modules/unit_tests/strippable.dm new file mode 100644 index 00000000000..ce31972fc4a --- /dev/null +++ b/code/modules/unit_tests/strippable.dm @@ -0,0 +1,51 @@ +/datum/unit_test/strip_menu_ui_status/Run() + // We just need something that doesn't have strippable by default, so we can add it ourselves. + var/obj/target = allocate(/obj/item/pen, run_loc_floor_bottom_left) + var/datum/element/strippable/strippable = target.AddElement(/datum/element/strippable, list()) + + var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human, run_loc_floor_bottom_left) + ADD_TRAIT(user, TRAIT_PRESERVE_UI_WITHOUT_CLIENT, TRAIT_SOURCE_UNIT_TESTS) + + var/datum/strip_menu/strip_menu = allocate(/datum/strip_menu, target, strippable) + + var/ui_state = strip_menu.ui_state(user) + + TEST_ASSERT_EQUAL(strip_menu.ui_status(user, ui_state), UI_INTERACTIVE, "Perfect conditions were not interactive.") + + user.set_body_position(LYING_DOWN) + // Necessary for tying shoes. + TEST_ASSERT_EQUAL(strip_menu.ui_status(user, ui_state), UI_INTERACTIVE, "Lying down was not interactive.") + + user.forceMove(locate(run_loc_floor_bottom_left.x + 2, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) + TEST_ASSERT_EQUAL(strip_menu.ui_status(user, ui_state), UI_UPDATE, "Being too far away while lying down was not update-only.") + + user.set_body_position(STANDING_UP) + TEST_ASSERT_EQUAL(strip_menu.ui_status(user, ui_state), UI_UPDATE, "Being too far away while standing up was not update-only.") + + user.set_body_position(LYING_DOWN) + user.forceMove(target.loc) + user.death() + TEST_ASSERT_EQUAL(strip_menu.ui_status(user, ui_state), UI_UPDATE, "Being within range but dead was not update-only.") + + var/mob/dead/observer/observer = allocate(/mob/dead/observer) + // observers set their own turf, so we can't just pass it into allocate + observer.forceMove(run_loc_floor_bottom_left) + + // A mocked client is needed for providing view + var/datum/client_interface/mock_client = new + observer.mock_client = mock_client + ADD_TRAIT(observer, TRAIT_PRESERVE_UI_WITHOUT_CLIENT, TRAIT_SOURCE_UNIT_TESTS) + TEST_ASSERT_EQUAL(strip_menu.ui_status(observer, ui_state), UI_UPDATE, "Being within range but an observer was not update-only.") + + var/mob/living/silicon/robot/borg = allocate(/mob/living/silicon/robot, run_loc_floor_bottom_left) + ADD_TRAIT(borg, TRAIT_PRESERVE_UI_WITHOUT_CLIENT, TRAIT_SOURCE_UNIT_TESTS) + TEST_ASSERT_EQUAL(strip_menu.ui_status(borg, ui_state), UI_INTERACTIVE, "Being within range as a borg was not interactive.") + + // Borgs can normally access tgui's regardless of position if it's within view range. + // This makes sense for machinery, but not for this abstract UI. + borg.forceMove(locate(run_loc_floor_bottom_left.x + 2, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) + TEST_ASSERT_EQUAL(strip_menu.ui_status(borg, ui_state), UI_UPDATE, "Being too far away as a borg was not update-only.") + + var/mob/living/carbon/alien/rouny = allocate(/mob/living/carbon/alien, run_loc_floor_bottom_left) + ADD_TRAIT(rouny, TRAIT_PRESERVE_UI_WITHOUT_CLIENT, TRAIT_SOURCE_UNIT_TESTS) + TEST_ASSERT_EQUAL(strip_menu.ui_status(rouny, ui_state), UI_INTERACTIVE, "Being within range as a xeno was not interactive.") diff --git a/tgstation.dme b/tgstation.dme index 1c7e5d700c5..d3dede29e50 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3370,6 +3370,7 @@ #include "code\modules\tgs\includes.dm" #include "code\modules\tgui\external.dm" #include "code\modules\tgui\states.dm" +#include "code\modules\tgui\status_composers.dm" #include "code\modules\tgui\tgui.dm" #include "code\modules\tgui\tgui_alert.dm" #include "code\modules\tgui\tgui_input_list.dm"