Files
8eed335b94 Refactors inventory UI to be completely datum and vis_contents-based (#95998)
## About The Pull Request

Completely removes individual inventory UI handling from mob and HUD
code and moves it to slot datums.
``/datum/inventory_slot`` now is responsible for displaying items on the
player UI and does so through vis_contests as opposed to screen_loc -
which ensures that wide items will display properly rather than be
offset to the right.
Centralized, slot_id based handling allows us to significantly simplify
inventory and HUD code (see how many lines were removed) as you no
longer need to individually track all items for both the HUD owner and
the observer, and makes it easier for us to fully datumize inventory
handling in the future.

Also fixes a bug where observers would see players' storage UI and have
the items linger on-screen after its closed.

## Why It's Good For The Game

Makes working with inventories and HUDs easier, fixes visual issues with
wide items, I need this for human rendering refactors.

## Changelog
🆑
refactor: Refactors inventory UI to be completely datum and
vis_contents-based
fix: Observers should no longer see doubled up inventory UIs
/🆑

---------

Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
2026-05-31 14:27:23 -07:00

494 lines
17 KiB
Plaintext

/// Convers HIDEX to ITEM_SLOT_X, should be phased out in favor of using latter everywhere later
/proc/hidden_slots_to_inventory_slots(hidden_slots)
var/obscured = NONE
if(hidden_slots & HIDENECK)
obscured |= ITEM_SLOT_NECK
if(hidden_slots & HIDEMASK)
obscured |= ITEM_SLOT_MASK
if(hidden_slots & HIDEBELT)
obscured |= ITEM_SLOT_BELT
if(hidden_slots & HIDEEYES)
obscured |= ITEM_SLOT_EYES
if(hidden_slots & HIDEEARS)
obscured |= ITEM_SLOT_EARS
if(hidden_slots & HIDEGLOVES)
obscured |= ITEM_SLOT_GLOVES
if(hidden_slots & HIDEJUMPSUIT)
obscured |= ITEM_SLOT_ICLOTHING
if(hidden_slots & HIDESHOES)
obscured |= ITEM_SLOT_FEET
if(hidden_slots & HIDESUITSTORAGE)
obscured |= ITEM_SLOT_SUITSTORE
if(hidden_slots & HIDEHEADGEAR)
obscured |= ITEM_SLOT_HEAD
return obscured
/mob/living/carbon/get_item_by_slot(slot_id)
switch(slot_id)
if(ITEM_SLOT_BACK)
return back
if(ITEM_SLOT_MASK)
return wear_mask
if(ITEM_SLOT_NECK)
return wear_neck
if(ITEM_SLOT_HEAD)
return head
if(ITEM_SLOT_HANDCUFFED)
return handcuffed
if(ITEM_SLOT_LEGCUFFED)
return legcuffed
return ..()
/mob/living/carbon/get_slot_by_item(obj/item/looking_for)
if(looking_for == back)
return ITEM_SLOT_BACK
if(looking_for == wear_mask)
return ITEM_SLOT_MASK
if(looking_for == wear_neck)
return ITEM_SLOT_NECK
if(looking_for == head)
return ITEM_SLOT_HEAD
if(looking_for == handcuffed)
return ITEM_SLOT_HANDCUFFED
if(looking_for == legcuffed)
return ITEM_SLOT_LEGCUFFED
return ..()
/// Returns items which are currently visible on the mob
/mob/living/carbon/proc/get_visible_items()
var/list/visible_items = list()
var/obscured_item_slots = hidden_slots_to_inventory_slots(obscured_slots)
for(var/obj/item/thing in get_equipped_items(INCLUDE_HELD|INCLUDE_PROSTHETICS))
if(!(get_slot_by_item(thing) & obscured_item_slots))
visible_items += thing
return visible_items
/mob/living/carbon/proc/equip_in_one_of_slots(obj/item/equipping, list/slots, qdel_on_fail = TRUE, indirect_action = FALSE)
var/static/list/equip_slots = list(
LOCATION_LPOCKET = ITEM_SLOT_LPOCKET,
LOCATION_RPOCKET = ITEM_SLOT_RPOCKET,
LOCATION_HANDS = ITEM_SLOT_HANDS,
LOCATION_GLOVES = ITEM_SLOT_GLOVES,
LOCATION_EYES = ITEM_SLOT_EYES,
LOCATION_MASK = ITEM_SLOT_MASK,
LOCATION_HEAD = ITEM_SLOT_HEAD,
LOCATION_NECK = ITEM_SLOT_NECK,
LOCATION_ID = ITEM_SLOT_ID,
)
var/static/list/storage_slots = list(
LOCATION_BACKPACK = ITEM_SLOT_BACK,
)
for(var/slot in slots)
if(equip_slots[slot])
if(equip_to_slot_if_possible(equipping, equip_slots[slot], disable_warning = TRUE, indirect_action = indirect_action))
return slot
else if (storage_slots[slot])
if(equip_to_storage(equipping, storage_slots[slot], indirect_action = indirect_action))
return slot
if(qdel_on_fail)
qdel(equipping)
return null
//This is an UNSAFE proc. Use mob_can_equip() before calling this one! Or rather use equip_to_slot_if_possible() or advanced_equip_to_slot_if_possible()
/mob/living/carbon/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
if(!slot)
return
if(!istype(equipping))
return
var/index = get_held_index_of_item(equipping)
if(index)
held_items[index] = null
if(equipping.pulledby)
equipping.pulledby.stop_pulling()
hud_used?.update_inventory_slot(slot)
for(var/mob/dead/observe as anything in observers)
observe.client?.screen -= equipping
equipping.forceMove(src) //This has to come before has_equipped is called.
SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
equipping.appearance_flags |= NO_CLIENT_COLOR
var/not_handled = FALSE
switch(slot)
if(ITEM_SLOT_BACK)
if(back)
return
back = equipping
update_worn_back()
if(ITEM_SLOT_MASK)
if(wear_mask)
return
wear_mask = equipping
update_worn_mask()
if(ITEM_SLOT_HEAD)
if(head)
return
head = equipping
update_worn_head()
if(ITEM_SLOT_NECK)
if(wear_neck)
return
wear_neck = equipping
update_worn_neck(equipping)
if(ITEM_SLOT_HANDCUFFED)
set_handcuffed(equipping)
if(ITEM_SLOT_LEGCUFFED)
legcuffed = equipping
update_worn_legcuffs()
if(ITEM_SLOT_HANDS)
put_in_hands(equipping)
update_held_items()
else
not_handled = TRUE
//Item has been handled at this point and equipped callback can be safely called
//We cannot call it for items that have not been handled as they are not yet correctly
//in a slot (handled further down inheritance chain, probably living/carbon/human/equip_to_slot
if(!not_handled && slot != ITEM_SLOT_HANDS) // put in hands calls equipped on its own, annoyingly
has_equipped(equipping, slot, initial)
return not_handled
/mob/living/carbon/has_equipped(obj/item/item, slot, initial = FALSE)
. = ..()
if(!.)
return
hud_used?.update_locked_slots()
if(!(slot & item.slot_flags)) // Things below only update if slotted in (ie: not held)
return
if(item.hair_mask)
LAZYADD(hair_masks, item.hair_mask)
update_hair()
update_body() // this is solely for lizard frills
add_item_coverage(item)
/mob/living/carbon/has_unequipped(obj/item/item)
. = ..()
if(!.)
return
hud_used?.update_locked_slots()
if(item.hair_mask)
LAZYREMOVE(hair_masks, item.hair_mask)
update_hair()
update_body() // this is solely for lizard frills
remove_item_coverage(item)
/mob/living/carbon/doUnEquip(obj/item/item_dropping, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
. = ..() //Sets the default return value to what the parent returns.
if(!. || !item_dropping) //We don't want to set anything to null if the parent returned 0.
return
if(item_dropping == head)
head = null
if(!QDELETED(src))
update_worn_head()
else if(item_dropping == back)
back = null
if(!QDELETED(src))
update_worn_back()
else if(item_dropping == wear_mask)
wear_mask = null
if(!QDELETED(src))
update_worn_mask()
else if(item_dropping == wear_neck)
wear_neck = null
if(!QDELETED(src))
update_worn_neck(item_dropping)
else if(item_dropping == handcuffed)
set_handcuffed(null)
if(buckled?.buckle_requires_restraints)
buckled.unbuckle_mob(src)
else if(item_dropping == legcuffed)
legcuffed = null
if(!QDELETED(src))
update_worn_legcuffs()
// Not an else-if because we're probably equipped in another slot
if(item_dropping == internal && (QDELETED(src) || QDELETED(item_dropping) || item_dropping.loc != src))
cutoff_internals()
if(!QDELETED(src))
update_mob_action_buttons(UPDATE_BUTTON_STATUS)
/// Adds the passed item's coverage to the mob's coverage related flags
/mob/living/carbon/proc/add_item_coverage(obj/item/item)
var/pre_coverage = obscured_slots
obscured_slots |= item.flags_inv
covered_slots |= item.flags_inv | item.transparent_protection
if(pre_coverage != obscured_slots)
item_coverage_changed(obscured_slots & ~pre_coverage, pre_coverage & ~obscured_slots)
/// Removes the passed item's coverage from the mob's coverage related flags
/mob/living/carbon/proc/remove_item_coverage(obj/item/item)
refresh_obscured() // No way to remove a single item's coverage without recalculating everything
/mob/living/carbon/refresh_obscured()
var/pre_coverage = obscured_slots
obscured_slots = NONE
covered_slots = NONE
for(var/obj/item/other_equipped_item as anything in get_equipped_items())
obscured_slots |= other_equipped_item.flags_inv
covered_slots |= other_equipped_item.flags_inv | other_equipped_item.transparent_protection
if(HAS_TRAIT(src, TRAIT_HUSK) || HAS_TRAIT(src, TRAIT_INVISIBLE_MAN))
obscured_slots |= HIDEHAIR|HIDEFACIALHAIR
if(pre_coverage != obscured_slots)
item_coverage_changed(obscured_slots & ~pre_coverage, pre_coverage & ~obscured_slots)
/**
* Called when a mob's obscured slots change
*
* Args
* * added_slots - slots that were added to obscured_slots
* * removed_slots - slots that were removed from obscured_slots
*/
/mob/living/carbon/proc/item_coverage_changed(added_slots, removed_slots)
SEND_SIGNAL(src, COMSIG_CARBON_ITEM_COVERAGE_CHANGED, added_slots, removed_slots)
update_clothing(hidden_slots_to_inventory_slots(added_slots|removed_slots))
if((added_slots|removed_slots) & HIDESNOUT)
synchronize_bodyshapes()
if((added_slots|removed_slots) & (HIDEHAIR|HIDEFACIALHAIR))
update_hair()
if((added_slots|removed_slots) & HIDEEYES)
update_eyes()
// HIDEJUMPSUIT is for digitigrade legs, HIDEEARS is for lizard frills, HIDEHAIR is for felinid ears and lizard horns, the others should be obvious
// future todo; we should collect a list of all bodypart overlays and what conceals/reveals them dynamically, rather than hardcoding this
if((added_slots|removed_slots) & (HIDEJUMPSUIT|HIDEEARS|HIDEHAIR|HIDESNOUT|HIDEMUTWINGS|HIDEANTENNAE))
update_body()
/// Returns the helmet if an air tank compatible helmet is equipped.
/mob/living/carbon/proc/can_breathe_helmet()
if (astype(head, /obj/item/clothing)?.clothing_flags & HEADINTERNALS)
return head
/// Returns the mask if an air tank compatible mask is equipped.
/mob/living/carbon/proc/can_breathe_mask()
if (astype(wear_mask, /obj/item/clothing)?.clothing_flags & MASKINTERNALS)
return wear_mask
/// Returns the tube if a breathing tube is equipped.
/mob/living/carbon/proc/can_breathe_tube()
return get_organ_slot(ORGAN_SLOT_BREATHING_TUBE)
/// Returns the object that allows us to breathe internals - tube implant, mask or helmet
/mob/living/carbon/proc/can_breathe_internals()
return can_breathe_tube() || can_breathe_mask() || can_breathe_helmet()
/// Returns truthy if air tank is open and mob lacks apparatus, or if the tank moved away from the mob.
/mob/living/carbon/proc/invalid_internals()
return (internal || external) && (!can_breathe_internals() || (internal && internal.loc != src))
/**
* Open the internal air tank without checking for any breathing apparatus.
* Returns TRUE if the air tank was opened successfully.
* Closes any existing tanks before opening another one.
*
* Arguments:
* * tank - The given tank to open and start breathing from.
* * is_external - A boolean which indicates if the air tank must be equipped, or stored elsewhere.
*/
/mob/living/carbon/proc/open_internals(obj/item/tank/target_tank, is_external = FALSE)
if (!target_tank)
return
close_all_airtanks()
if (is_external)
external = target_tank
else
internal = target_tank
target_tank.after_internals_opened(src)
update_mob_action_buttons()
return TRUE
/**
* Opens the given internal air tank if a breathing apparatus is found. Returns TRUE if successful, FALSE otherwise.
* Returns TRUE if the tank was opened successfully.
*
* Arguments:
* * tank - The given tank we will attempt to toggle open and start breathing from.
* * is_external - A boolean which indicates if the air tank must be equipped, or stored elsewhere.
*/
/mob/living/carbon/proc/try_open_internals(obj/item/tank/target_tank, is_external = FALSE)
if (!can_breathe_internals())
return
return open_internals(target_tank, is_external)
/**
* Actually closes the active internal or external air tank.
* Returns TRUE if the tank was opened successfully.
*
* Arguments:
* * is_external - A boolean which indicates if the air tank must be equipped, or stored elsewhere.
*/
/mob/living/carbon/proc/close_internals(is_external = FALSE)
var/obj/item/tank/target_tank = is_external ? external : internal
if (!target_tank)
return
if (is_external)
external = null
else
internal = null
target_tank.after_internals_closed(src)
update_mob_action_buttons()
//To make sure it stops at a timely manner when you turn off internals
breathing_loop.stop()
return TRUE
/// Close the the currently open external (that's EX-ternal) air tank. Returns TRUE if successful.
/mob/living/carbon/proc/close_externals()
return close_internals(TRUE)
/// Quickly/lazily close all airtanks without any returns or notifications.
/mob/living/carbon/proc/close_all_airtanks()
if (external)
close_externals()
if (internal)
close_internals()
/**
* Prepares to open the internal air tank and notifies the mob in chat.
* Handles displaying messages to the user before doing the actual opening.
* Returns TRUE if the tank was opened/closed successfully.
*
* Arguments:
* * tank - The given tank to toggle open and start breathing from.
* * is_external - A boolean which indicates if the air tank must be equipped, or stored elsewhere.
*/
/mob/living/carbon/proc/toggle_open_internals(obj/item/tank/target_tank, is_external = FALSE)
if (!target_tank)
return
if(internal || (is_external && external))
to_chat(src, span_notice("You switch your internals to [target_tank]."))
else
to_chat(src, span_notice("You open [target_tank] valve."))
return open_internals(target_tank, is_external)
/**
* Prepares to close the currently open internal air tank and notifies in chat.
* Handles displaying messages to the user before doing the actual closing.
* Returns TRUE if
*
* Arguments:
* * is_external - A boolean which indicates if the air tank must be equipped, or stored elsewhere.
*/
/mob/living/carbon/proc/toggle_close_internals(is_external = FALSE)
if (!internal && !external)
return
to_chat(src, span_notice("You close [is_external ? external : internal] valve."))
return close_internals(is_external)
/// Prepares emergency disconnect from open air tanks and notifies in chat. Usually called after mob suddenly unequips breathing apparatus.
/mob/living/carbon/proc/cutoff_internals()
if (!external && !internal)
return
to_chat(src, span_notice("Your internals disconnect from [external || internal] and the valve closes."))
close_all_airtanks()
/**
* Toggles the given internal air tank open, or close the currently open one, if a compatible breathing apparatus is found.
* Returns TRUE if the tank was opened successfully.
*
* Arguments:
* * tank - The given tank to toggle open and start breathing from internally.
*/
/mob/living/carbon/proc/toggle_internals(obj/item/tank)
// Carbons can't open their own internals tanks.
return FALSE
/**
* Toggles the given external (that's EX-ternal) air tank open, or close the currently open one, if a compatible breathing apparatus is found.
* Returns TRUE if the tank was opened successfully.
*
* Arguments:
* * tank - The given tank to toggle open and start breathing from externally.
*/
/mob/living/carbon/proc/toggle_externals(obj/item/tank)
// Carbons can't open their own externals tanks.
return FALSE
/mob/living/carbon/proc/get_holding_bodypart_of_item(obj/item/I)
var/index = get_held_index_of_item(I)
return index && hand_bodyparts[index]
///Returns a list of all body_zones covered by clothing
/mob/living/carbon/proc/get_covered_body_zones()
RETURN_TYPE(/list)
SHOULD_NOT_OVERRIDE(TRUE)
return cover_flags2body_zones(get_all_covered_flags())
///Returns a bitfield of all zones covered by clothing
/mob/living/carbon/proc/get_all_covered_flags()
SHOULD_NOT_OVERRIDE(TRUE)
var/covered_flags = NONE
for(var/obj/item/worn_item in get_equipped_items(INCLUDE_ABSTRACT))
covered_flags |= worn_item.body_parts_covered
return covered_flags
/mob/living/carbon/is_location_accessible(location, exluded_equipment_slots = NONE)
switch(location)
// Snowflake checks for these precise zones
if(BODY_ZONE_PRECISE_EYES)
if(is_eyes_covered(~exluded_equipment_slots) || (obscured_slots & (HIDEEYES|HIDEFACE)))
return FALSE
if(BODY_ZONE_PRECISE_MOUTH)
if(is_mouth_covered(~exluded_equipment_slots) || (obscured_slots & HIDEFACE))
return FALSE
var/covered_flags = NONE
for(var/obj/item/worn_item in get_equipped_items(INCLUDE_ABSTRACT))
if(worn_item.slot_flags & exluded_equipment_slots)
continue
if(worn_item.flags_cover & ALLOW_SURGERY_THROUGH)
continue
covered_flags |= worn_item.body_parts_covered
// NB: we have to convert covered_flags via cover_flags2body_zones here
// instead of converting location via body_zones2cover_flags
//
// our coverage might look something like GROIN|LEGS, which would convert to list(BODY_ZONE_GROIN, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
// so if we were checking "is BODY_ZONE_CHEST accessible", we would pass - this is correct!
//
// however, if we convert the location to body zone, we would get CHEST|GROIN
// then we would check (CHEST|GROIN) & (GROIN|LEGS) and return FALSE - which is incorrect, the chest is perfectly accessible!
// checking for ((CHEST|GROIN) & (GROIN|LEGS)) == (CHEST|GROIN) would also be incorrect,
// as it would imply your chest is accessible from lacking groin coverage
return !(location in cover_flags2body_zones(covered_flags))
/// Attempts to equip the given item in a conspicious place.
/// This is used when, for instance, a character spawning with an item
/// in their hands would be a dead giveaway that they are an antagonist.
/// Returns the human readable name of where it placed the item, or null otherwise.
/mob/living/carbon/proc/equip_conspicuous_item(obj/item/item, delete_item_if_failed = TRUE)
var/static/list/pockets = list(
"left pocket" = ITEM_SLOT_LPOCKET,
"right pocket" = ITEM_SLOT_RPOCKET
)
var/placed_in = equip_in_one_of_slots(item, pockets, qdel_on_fail = FALSE, indirect_action = TRUE)
if (!placed_in)
placed_in = equip_to_storage(item, ITEM_SLOT_BACK, indirect_action = TRUE)
if (isnull(placed_in) && delete_item_if_failed)
qdel(item)
return placed_in