Files
SmArtKar 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

519 lines
17 KiB
Plaintext

/atom/movable/screen/movable/action_button
var/datum/action/linked_action
var/datum/hud/our_hud
var/actiontooltipstyle = ""
screen_loc = null
mouse_over_pointer = MOUSE_HAND_POINTER
/// The icon state of our active overlay, used to prevent re-applying identical overlays
var/active_overlay_icon_state
/// The icon state of our active underlay, used to prevent re-applying identical underlays
var/active_underlay_icon_state
/// The overlay we have overtop our button
var/mutable_appearance/button_overlay
/// Where we are currently placed on the hud. SCRN_OBJ_DEFAULT asks the linked action what it thinks
var/location = SCRN_OBJ_DEFAULT
/// A unique bitflag, combined with the name of our linked action this lets us persistently remember any user changes to our position
var/id
/// A weakref of the last thing we hovered over
/// God I hate how dragging works
var/datum/weakref/last_hovored_ref
/// overlay for keybind maptext
var/mutable_appearance/keybind_maptext
/// if observers can trigger this action at any time
var/allow_observer_click = FALSE
/atom/movable/screen/movable/action_button/Destroy()
if(our_hud)
var/mob/viewer = our_hud.mymob
our_hud.hide_action(src)
viewer?.client?.screen -= src
linked_action.viewers -= our_hud
viewer.update_action_buttons()
our_hud.screen_objects -= hud_key
our_hud.screen_groups?[hud_group_key] -= src
our_hud = null
linked_action = null
return ..()
/atom/movable/screen/movable/action_button/proc/can_use(mob/user)
if(isobserver(user))
var/mob/dead/observer/dead_mob = user
if(allow_observer_click)
return TRUE
if(dead_mob.observetarget) // Observers can only click on action buttons if they're not observing something
return FALSE
if(linked_action)
if(linked_action.viewers[user.hud_used])
return TRUE
return FALSE
return TRUE
/atom/movable/screen/movable/action_button/Click(location,control,params)
if(!can_use(usr))
return FALSE
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, ALT_CLICK))
linked_action?.begin_creating_bind(src, usr)
return TRUE
if(LAZYACCESS(modifiers, SHIFT_CLICK))
var/datum/hud/our_hud = usr.hud_used
our_hud.position_action(src, SCRN_OBJ_DEFAULT)
return TRUE
if(usr.next_click > world.time)
return
usr.next_click = world.time + 1
var/trigger_flags
if(LAZYACCESS(modifiers, RIGHT_CLICK))
trigger_flags |= TRIGGER_SECONDARY_ACTION
linked_action.Trigger(usr, trigger_flags = trigger_flags)
return TRUE
// Entered and Exited won't fire while you're dragging something, because you're still "holding" it
// Very much byond logic, but I want nice behavior, so we fake it with drag
/atom/movable/screen/movable/action_button/MouseDrag(atom/over_object, src_location, over_location, src_control, over_control, params)
. = ..()
if(!can_use(usr))
return
if(IS_WEAKREF_OF(over_object, last_hovored_ref))
return
var/atom/old_object
if(last_hovored_ref)
old_object = last_hovored_ref?.resolve()
else // If there's no current ref, we assume it was us. We also treat this as our "first go" location
old_object = src
var/datum/hud/our_hud = usr.hud_used
our_hud?.generate_landings(src)
if(old_object)
old_object.MouseExited(over_location, over_control, params)
last_hovored_ref = WEAKREF(over_object)
over_object?.MouseEntered(over_location, over_control, params)
/atom/movable/screen/movable/action_button/MouseEntered(location, control, params)
. = ..()
if(!QDELETED(src))
openToolTip(usr, src, params, title = name, content = desc, theme = actiontooltipstyle)
/atom/movable/screen/movable/action_button/MouseExited(location, control, params)
closeToolTip(usr)
return ..()
/atom/movable/screen/movable/action_button/mouse_drop_dragged(atom/over_object, mob/user, src_location, over_location, params)
last_hovored_ref = null
if(!can_use(usr))
return
var/datum/hud/our_hud = usr.hud_used
if(over_object == src)
our_hud.hide_landings()
return
if(istype(over_object, /atom/movable/screen/action_landing))
var/atom/movable/screen/action_landing/reserve = over_object
reserve.hit_by(src)
save_position()
our_hud.hide_landings()
return
if(istype(over_object, /atom/movable/screen/button_palette) || istype(over_object, /atom/movable/screen/palette_scroll))
our_hud.position_action(src, SCRN_OBJ_IN_PALETTE)
save_position()
our_hud.hide_landings()
return
if(istype(over_object, /atom/movable/screen/movable/action_button))
var/atom/movable/screen/movable/action_button/button = over_object
our_hud.position_action_relative(src, button)
save_position()
our_hud.hide_landings()
return
. = ..()
our_hud.position_action(src, screen_loc)
save_position()
our_hud.hide_landings()
/atom/movable/screen/movable/action_button/proc/save_position()
var/mob/user = our_hud.mymob
if(!user?.client)
return
var/position_info = ""
switch(location)
if(SCRN_OBJ_FLOATING)
position_info = screen_loc
if(SCRN_OBJ_IN_LIST)
position_info = SCRN_OBJ_IN_LIST
if(SCRN_OBJ_IN_PALETTE)
position_info = SCRN_OBJ_IN_PALETTE
user.client.prefs.action_buttons_screen_locs["[name]_[id]"] = position_info
/atom/movable/screen/movable/action_button/proc/load_position()
var/mob/user = our_hud.mymob
if(!user)
return
var/position_info = user.client?.prefs?.action_buttons_screen_locs["[name]_[id]"] || SCRN_OBJ_DEFAULT
user.hud_used.position_action(src, position_info)
/atom/movable/screen/movable/action_button/proc/dump_save()
var/mob/user = our_hud.mymob
if(!user?.client)
return
user.client.prefs.action_buttons_screen_locs -= "[name]_[id]"
/atom/movable/screen/movable/action_button/proc/update_keybind_maptext(key)
cut_overlay(keybind_maptext)
if(!key)
return
keybind_maptext = new
keybind_maptext.maptext = MAPTEXT("<span style='text-align: right'>[key]</span>")
keybind_maptext.transform = keybind_maptext.transform.Translate(-4, length(key) > 1 ? -6 : 2) //with modifiers, its placed lower so cooldown is visible
add_overlay(keybind_maptext)
/**
* This is a silly proc used in hud code code to determine what icon and icon state we should be using
* for hud elements (such as action buttons) that don't have their own icon and icon state set.
*
* It returns a list, which is pretty much just a struct of info
*/
/datum/hud/proc/get_action_buttons_icons()
. = list()
.["bg_icon"] = ui_style
.["bg_state"] = "template"
.["bg_state_active"] = "template_active"
/**
* Updates all action buttons this mob has.
*
* Arguments:
* * update_flags - Which flags of the action should we update
* * force - Force buttons update even if the given button icon state has not changed
*/
/mob/proc/update_mob_action_buttons(update_flags = ALL, force = FALSE)
for(var/datum/action/current_action as anything in actions)
current_action.build_all_button_icons(update_flags, force)
/**
* This proc handles adding all of the mob's actions to their screen
*
* If you just need to update existing buttons, use [/mob/proc/update_mob_action_buttons]!
*
* Arguments:
* * update_flags - reload_screen - bool, if TRUE, this proc will add the button to the screen of the passed mob as well
*/
/mob/proc/update_action_buttons(reload_screen = FALSE)
if(!hud_used || !client)
return
if(hud_used.hud_version != HUD_STYLE_STANDARD)
return
for(var/datum/action/action as anything in actions)
var/atom/movable/screen/movable/action_button/button = action.viewers[hud_used]
action.build_all_button_icons()
if(reload_screen)
client.screen += button
if(reload_screen)
hud_used.update_our_owner()
// This holds the logic for the palette buttons
hud_used.palette_actions.refresh_actions()
/**
* Show (most) of the another mob's action buttons to this mob
*
* Used for observers viewing another mob's screen
*/
/mob/proc/show_other_mob_action_buttons(mob/take_from)
if(!hud_used || !client)
return
for(var/datum/action/action as anything in take_from.actions)
if(!action.show_to_observers || !action.owner_has_control)
continue
action.GiveAction(src)
RegisterSignal(take_from, COMSIG_MOB_GRANTED_ACTION, PROC_REF(on_observing_action_granted))
RegisterSignal(take_from, COMSIG_MOB_REMOVED_ACTION, PROC_REF(on_observing_action_removed))
/**
* Hide another mob's action buttons from this mob
*
* Used for observers viewing another mob's screen
*/
/mob/proc/hide_other_mob_action_buttons(mob/take_from)
for(var/datum/action/action as anything in take_from.actions)
action.HideFrom(src)
UnregisterSignal(take_from, list(COMSIG_MOB_GRANTED_ACTION, COMSIG_MOB_REMOVED_ACTION))
/// Signal proc for [COMSIG_MOB_GRANTED_ACTION] - If we're viewing another mob's action buttons,
/// we need to update with any newly added buttons granted to the mob.
/mob/proc/on_observing_action_granted(mob/living/source, datum/action/action)
SIGNAL_HANDLER
if(!action.show_to_observers || !action.owner_has_control)
return
action.GiveAction(src)
/// Signal proc for [COMSIG_MOB_REMOVED_ACTION] - If we're viewing another mob's action buttons,
/// we need to update with any removed buttons from the mob.
/mob/proc/on_observing_action_removed(mob/living/source, datum/action/action)
SIGNAL_HANDLER
action.HideFrom(src)
/atom/movable/screen/button_palette
desc = "<b>Drag</b> buttons to move them<br><b>Shift-click</b> any button to reset it<br><b>Alt-click any button</b> to begin binding it to a key<br><b>Alt-click this</b> to reset all buttons"
icon = 'icons/hud/64x16_actions.dmi'
icon_state = "screen_gen_palette"
screen_loc = ui_action_palette
mouse_over_pointer = MOUSE_HAND_POINTER
var/datum/hud/our_hud
var/expanded = FALSE
/// Id of any currently running timers that set our color matrix
var/color_timer_id
/atom/movable/screen/button_palette/Destroy()
if (our_hud)
our_hud.mymob?.canon_client?.screen -= src
our_hud.screen_objects -= hud_key
our_hud.screen_groups?[hud_group_key] -= src
our_hud = null
return ..()
/atom/movable/screen/button_palette/Initialize(mapload, datum/hud/hud_owner)
. = ..()
update_appearance()
set_hud(hud_owner)
/atom/movable/screen/button_palette/proc/set_hud(datum/hud/our_hud)
src.our_hud = our_hud
refresh_owner()
disable_landing() // If our hud already has elements, don't force hide us
/atom/movable/screen/button_palette/update_name(updates)
. = ..()
if(expanded)
name = "Hide Buttons"
else
name = "Show Buttons"
/atom/movable/screen/button_palette/proc/refresh_owner()
var/mob/viewer = our_hud.mymob
if(viewer.client)
viewer.client.screen |= src
var/list/settings = our_hud.get_action_buttons_icons()
var/ui_icon = "[settings["bg_icon"]]"
var/list/ui_segments = splittext(ui_icon, ".")
var/list/ui_paths = splittext(ui_segments[1], "/")
var/ui_name = ui_paths[length(ui_paths)]
icon_state = "[ui_name]_palette"
/atom/movable/screen/button_palette/proc/activate_landing()
// Reveal ourselves to the user
invisibility = INVISIBILITY_NONE
/atom/movable/screen/button_palette/proc/disable_landing()
// If we have no elements in the palette, hide your ugly self please
if (!length(our_hud.palette_actions?.actions) && !length(our_hud.floating_actions))
invisibility = INVISIBILITY_ABSTRACT
/atom/movable/screen/button_palette/proc/update_state()
if (length(our_hud.floating_actions))
activate_landing()
else
disable_landing()
/atom/movable/screen/button_palette/MouseEntered(location, control, params)
. = ..()
if(QDELETED(src))
return
show_tooltip(params)
/atom/movable/screen/button_palette/MouseExited()
closeToolTip(usr)
return ..()
/atom/movable/screen/button_palette/proc/show_tooltip(params)
openToolTip(usr, src, params, title = name, content = desc)
GLOBAL_LIST_INIT(palette_added_matrix, list(0.4,0.5,0.2,0, 0,1.4,0,0, 0,0.4,0.6,0, 0,0,0,1, 0,0,0,0))
GLOBAL_LIST_INIT(palette_removed_matrix, list(1.4,0,0,0, 0.7,0.4,0,0, 0.4,0,0.6,0, 0,0,0,1, 0,0,0,0))
/atom/movable/screen/button_palette/proc/play_item_added()
color_for_now(GLOB.palette_added_matrix)
/atom/movable/screen/button_palette/proc/play_item_removed()
color_for_now(GLOB.palette_removed_matrix)
/atom/movable/screen/button_palette/proc/color_for_now(list/color)
if(color_timer_id)
return
add_atom_colour(color, TEMPORARY_COLOUR_PRIORITY) //We unfortunately cannot animate matrix colors. Curse you lummy it would be ~~non~~trivial to interpolate between the two valuessssssssss
color_timer_id = addtimer(CALLBACK(src, PROC_REF(remove_color), color), 2 SECONDS)
/atom/movable/screen/button_palette/proc/remove_color(list/to_remove)
color_timer_id = null
remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, to_remove)
/atom/movable/screen/button_palette/proc/can_use(mob/user)
if (isobserver(user))
var/mob/dead/observer/O = user
return !O.observetarget
return TRUE
/atom/movable/screen/button_palette/Click(location, control, params)
if(!can_use(usr))
return
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, ALT_CLICK))
for(var/datum/action/action as anything in usr.actions) // Reset action positions to default
for(var/datum/hud/hud as anything in action.viewers)
var/atom/movable/screen/movable/action_button/button = action.viewers[hud]
hud.position_action(button, SCRN_OBJ_DEFAULT)
to_chat(usr, span_notice("Action button positions have been reset."))
return TRUE
set_expanded(!expanded)
/atom/movable/screen/button_palette/proc/clicked_while_open(datum/source, atom/target, atom/location, control, params, mob/user)
if(istype(target, /atom/movable/screen/movable/action_button) || istype(target, /atom/movable/screen/palette_scroll) || target == src) // If you're clicking on an action button, or us, you can live
return
set_expanded(FALSE)
if(source)
UnregisterSignal(source, COMSIG_CLIENT_CLICK)
/atom/movable/screen/button_palette/proc/set_expanded(new_expanded)
var/datum/action_group/our_group = our_hud.palette_actions
if(!length(our_group.actions)) //Looks dumb, trust me lad
new_expanded = FALSE
if(expanded == new_expanded)
return
expanded = new_expanded
our_group.refresh_actions()
update_appearance()
if(!usr.client)
return
if(expanded)
RegisterSignal(usr.client, COMSIG_CLIENT_CLICK, PROC_REF(clicked_while_open))
else
UnregisterSignal(usr.client, COMSIG_CLIENT_CLICK)
closeToolTip(usr) //Our tooltips are now invalid, can't seem to update them in one frame, so here, just close them
/atom/movable/screen/palette_scroll
icon = 'icons/hud/screen_gen.dmi'
screen_loc = ui_palette_scroll
mouse_over_pointer = MOUSE_HAND_POINTER
/// How should we move the palette's actions?
/// Positive scrolls down the list, negative scrolls back
var/scroll_direction = 0
var/datum/hud/our_hud
/atom/movable/screen/palette_scroll/Initialize(mapload, datum/hud/hud_owner)
. = ..()
set_hud(hud_owner)
/atom/movable/screen/palette_scroll/Destroy()
if (our_hud)
our_hud.mymob?.canon_client?.screen -= src
our_hud.screen_objects -= hud_key
our_hud.screen_groups?[hud_group_key] -= src
our_hud = null
return ..()
/atom/movable/screen/palette_scroll/proc/can_use(mob/user)
if (isobserver(user))
var/mob/dead/observer/O = user
return !O.observetarget
return TRUE
/atom/movable/screen/palette_scroll/proc/set_hud(datum/hud/our_hud)
src.our_hud = our_hud
refresh_owner()
/atom/movable/screen/palette_scroll/proc/refresh_owner()
var/mob/viewer = our_hud.mymob
if(viewer.client)
viewer.client.screen |= src
var/list/settings = our_hud.get_action_buttons_icons()
icon = settings["bg_icon"]
/atom/movable/screen/palette_scroll/Click(location, control, params)
if(!can_use(usr))
return
our_hud.palette_actions.scroll(scroll_direction)
/atom/movable/screen/palette_scroll/MouseEntered(location, control, params)
. = ..()
if(QDELETED(src))
return
openToolTip(usr, src, params, title = name, content = desc)
/atom/movable/screen/palette_scroll/MouseExited()
closeToolTip(usr)
return ..()
/atom/movable/screen/palette_scroll/down
name = "Scroll Down"
desc = "<b>Click</b> on this to scroll the actions above down"
icon_state = "scroll_down"
scroll_direction = 1
/atom/movable/screen/palette_scroll/up
name = "Scroll Up"
desc = "<b>Click</b> on this to scroll the actions above up"
icon_state = "scroll_up"
scroll_direction = -1
/// Exists so you have a place to put your buttons when you move them around
/atom/movable/screen/action_landing
name = "Button Space"
desc = "<b>Drag and drop</b> a button into this spot<br>to add it to the group"
icon = 'icons/hud/screen_gen.dmi'
icon_state = "reserved"
// We want our whole 32x32 space to be clickable, so dropping's forgiving
mouse_opacity = MOUSE_OPACITY_OPAQUE
var/datum/action_group/owner
/atom/movable/screen/action_landing/Destroy()
if(owner)
owner.landing = null
owner?.owner?.mymob?.canon_client?.screen -= src
owner.refresh_actions()
owner = null
return ..()
/atom/movable/screen/action_landing/proc/set_owner(datum/action_group/owner)
src.owner = owner
refresh_owner()
/atom/movable/screen/action_landing/proc/refresh_owner()
var/datum/hud/our_hud = owner.owner
var/mob/viewer = our_hud.mymob
if(viewer.client)
viewer.client.screen |= src
var/list/settings = our_hud.get_action_buttons_icons()
icon = settings["bg_icon"]
/// Reacts to having a button dropped on it
/atom/movable/screen/action_landing/proc/hit_by(atom/movable/screen/movable/action_button/button)
var/datum/hud/our_hud = owner.owner
our_hud.position_action(button, owner.location)