mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-22 04:46:45 +01:00
requires #6644 Entirely replaces how (most) belts perform insertion checks. This pretty much allows belts to hold most things. Armor vests are next. Trashbags will also use the systems we use on main. Closes #7256 and #7435 --------- Co-authored-by: silicons <silicons@silicons.dev>
397 lines
15 KiB
Plaintext
397 lines
15 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2024 Citadel Station Developers *//
|
|
|
|
//* UI Main *//
|
|
|
|
/**
|
|
* @return TRUE if we did something (to interrupt clickchain)
|
|
*/
|
|
/datum/object_system/storage/proc/auto_handle_interacted_open(datum/event_args/actor/actor, force, suppressed)
|
|
if(!force && is_locked(actor.performer))
|
|
actor.chat_feedback(
|
|
msg = SPAN_WARNING("[parent] is locked."),
|
|
target = parent,
|
|
)
|
|
return TRUE
|
|
if(check_on_found_hooks(actor))
|
|
return TRUE
|
|
if(!suppressed && !isnull(actor) && sfx_open)
|
|
// todo: variable sound
|
|
playsound(actor.performer, sfx_open, 50, 1, -5)
|
|
// todo: jiggle animation
|
|
show(actor.initiator)
|
|
return TRUE
|
|
|
|
/datum/object_system/storage/proc/show(mob/viewer)
|
|
if(viewer.active_storage == src)
|
|
refresh_ui(viewer)
|
|
return TRUE
|
|
|
|
viewer.active_storage?.hide(viewer)
|
|
viewer.active_storage = src
|
|
|
|
create_ui(viewer)
|
|
|
|
parent.object_storage_opened(viewer)
|
|
|
|
/**
|
|
* if user not specified, it is 'all'.
|
|
*/
|
|
/datum/object_system/storage/proc/hide(mob/viewer)
|
|
if(isnull(viewer))
|
|
for(var/mob/iterating as anything in ui_by_mob)
|
|
hide(iterating)
|
|
return
|
|
|
|
if(viewer.active_storage != src)
|
|
stack_trace("viewer didn't have active storage set right, wtf?")
|
|
else
|
|
viewer.active_storage = null
|
|
|
|
cleanup_ui(viewer)
|
|
|
|
parent.object_storage_closed(viewer)
|
|
|
|
/datum/object_system/storage/proc/refresh(mob/viewer)
|
|
ui_refresh_queued = FALSE
|
|
if(isnull(viewer))
|
|
for(var/mob/iterating as anything in ui_by_mob)
|
|
refresh_ui(iterating)
|
|
return
|
|
refresh_ui(viewer)
|
|
|
|
|
|
/datum/object_system/storage/proc/reconsider_mob_viewable(mob/user)
|
|
if(isnull(user))
|
|
for(var/mob/viewer as anything in ui_by_mob)
|
|
reconsider_mob_viewable(viewer)
|
|
return
|
|
if(accessible_by_mob(user))
|
|
return
|
|
hide(user)
|
|
|
|
//* Helper Datums *//
|
|
|
|
/**
|
|
* Holds an object to render along with the amount.
|
|
*
|
|
* * This does hard-ref the rendered object for ease of use. Since UI is re-rendered on removals, this should be fine.
|
|
*/
|
|
/datum/storage_numerical_display
|
|
var/obj/item/rendered_object
|
|
var/amount
|
|
|
|
/datum/storage_numerical_display/New(obj/item/sample, amount = 0)
|
|
src.rendered_object = sample
|
|
src.amount = amount
|
|
|
|
/proc/cmp_storage_numerical_displays_name_asc(datum/storage_numerical_display/A, datum/storage_numerical_display/B)
|
|
return sorttext(B.rendered_object.name, A.rendered_object.name) || sorttext(B.rendered_object.type, A.rendered_object.type)
|
|
|
|
//* UI Render Filters *//
|
|
|
|
/**
|
|
* Do not modify the returned appearances; they might be real instances!
|
|
*
|
|
* @return list(/datum/storage_numerical_display instance, ...)
|
|
*/
|
|
/datum/object_system/storage/proc/render_numerical_display(list/obj/item/for_items)
|
|
RETURN_TYPE(/list)
|
|
. = list()
|
|
var/list/types = list()
|
|
for(var/obj/item/iterating in (for_items || real_contents_loc()))
|
|
var/datum/storage_numerical_display/collation
|
|
if(isnull(types[iterating.type]))
|
|
collation = new
|
|
collation.rendered_object = iterating
|
|
collation.amount = 0
|
|
. += collation
|
|
types[iterating.type] = collation
|
|
collation = types[iterating.type]
|
|
++collation.amount
|
|
tim_sort(., /proc/cmp_storage_numerical_displays_name_asc)
|
|
|
|
//* UI Rendering *//
|
|
|
|
// todo: refactor a bit? this is a little messy
|
|
|
|
/datum/object_system/storage/proc/ui_queue_refresh()
|
|
if(ui_refresh_queued)
|
|
return
|
|
ui_refresh_queued = TRUE
|
|
addtimer(CALLBACK(src, PROC_REF(refresh)), 0)
|
|
|
|
/datum/object_system/storage/proc/cleanup_ui(mob/user)
|
|
var/list/objects = ui_by_mob[user]
|
|
user.client?.screen -= objects
|
|
QDEL_LIST(objects)
|
|
ui_by_mob -= user
|
|
|
|
/**
|
|
* we assume that the display style didn't change.
|
|
*/
|
|
/datum/object_system/storage/proc/refresh_ui(mob/user)
|
|
// for now, we just do a full redraw.
|
|
cleanup_ui(user)
|
|
create_ui(user)
|
|
|
|
/datum/object_system/storage/proc/create_ui(mob/user)
|
|
var/uses_volumetric_ui = uses_volumetric_ui()
|
|
var/list/atom/movable/screen/storage/objects
|
|
if(uses_volumetric_ui)
|
|
objects += create_ui_volumetric_mode(user)
|
|
else
|
|
objects += create_ui_slot_mode(user)
|
|
LAZYINITLIST(ui_by_mob)
|
|
ui_by_mob[user] = objects
|
|
user.client?.screen += objects
|
|
|
|
/**
|
|
* this should not rely on uses_numerical_ui()
|
|
*/
|
|
/datum/object_system/storage/proc/uses_volumetric_ui()
|
|
return max_combined_volume && !ui_numerical_mode && !ui_force_slot_mode
|
|
|
|
/**
|
|
* this should not rely on uses_volumetric_ui()
|
|
*/
|
|
/datum/object_system/storage/proc/uses_numerical_ui()
|
|
return ui_numerical_mode
|
|
|
|
/**
|
|
* inspired by CM; allows tinting the background if something is nearly full
|
|
*/
|
|
/datum/object_system/storage/proc/get_ui_drawer_tint()
|
|
return null
|
|
|
|
/datum/object_system/storage/proc/get_ui_predicted_max_items()
|
|
// good ol' widescreen predict length of 9 if max isn't set
|
|
// this is so things don't break when they expect an answer
|
|
return max_items || 9
|
|
|
|
/**
|
|
* * Will not respect random access limits in numerical mode.
|
|
*/
|
|
/datum/object_system/storage/proc/create_ui_slot_mode(mob/user)
|
|
. = list()
|
|
var/atom/movable/screen/storage/closer/closer = new
|
|
. += closer
|
|
var/atom/movable/screen/storage/panel/slot/boxes/boxes = new
|
|
. += boxes
|
|
// todo: clientless support is awful here
|
|
var/list/decoded_view = decode_view_size(user.client?.view || world.view)
|
|
var/view_x = decoded_view[1]
|
|
// clamp to max items if needed
|
|
var/rendering_width = STORAGE_UI_TILES_FOR_SCREEN_VIEW_X(view_x)
|
|
// see if we need to process numerical display
|
|
var/list/datum/storage_numerical_display/numerical_rendered = uses_numerical_ui()? render_numerical_display() : null
|
|
// process indirection
|
|
var/obj/item/accessible = get_accessible_items()
|
|
// predict max items for slot mode
|
|
var/predicted_max_items = get_ui_predicted_max_items()
|
|
if(predicted_max_items)
|
|
rendering_width = min(predicted_max_items, rendering_width)
|
|
// if we have expand when needed, only show 1 more than the actual amount.
|
|
if(ui_expand_when_needed)
|
|
var/actually_needed = max((isnull(numerical_rendered)? length(accessible) : length(numerical_rendered)) + 1, ui_expand_when_needed)
|
|
rendering_width = min(rendering_width, actually_needed)
|
|
// compute count and rows
|
|
var/item_count = isnull(numerical_rendered)? length(accessible) : length(numerical_rendered)
|
|
var/rows_needed = ROUND_UP(item_count / rendering_width) || 1
|
|
// prepare iteration
|
|
var/current_row = 1
|
|
var/current_column = 1
|
|
// render boxes
|
|
boxes.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X],BOTTOM+[STORAGE_UI_START_TILE_Y]:[STORAGE_UI_START_PIXEL_Y] to \
|
|
LEFT+[STORAGE_UI_START_TILE_X + rendering_width - 1]:[STORAGE_UI_START_PIXEL_X],BOTTOM+[STORAGE_UI_START_TILE_Y + rows_needed - 1]:[STORAGE_UI_START_PIXEL_Y]"
|
|
boxes.color = get_ui_drawer_tint()
|
|
// render closer
|
|
closer.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X + rendering_width]:[STORAGE_UI_START_PIXEL_X],BOTTOM+[STORAGE_UI_START_TILE_Y]:[STORAGE_UI_START_PIXEL_Y]"
|
|
// render items
|
|
if(islist(numerical_rendered))
|
|
for(var/datum/storage_numerical_display/display as anything in numerical_rendered)
|
|
var/atom/movable/screen/storage/item/slot/renderer = new(null, display.rendered_object)
|
|
. += renderer
|
|
// render amount
|
|
display.rendered_object.maptext = MAPTEXT("[display.amount]")
|
|
// position
|
|
renderer.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X + current_column - 1]:[STORAGE_UI_START_PIXEL_X],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + current_row - 1]:[STORAGE_UI_START_PIXEL_Y]"
|
|
// advance
|
|
++current_column
|
|
if(current_column > rendering_width)
|
|
current_column = 1
|
|
++current_row
|
|
if(current_row > STORAGE_UI_MAX_ROWS)
|
|
break
|
|
else
|
|
for(var/obj/item/item in accessible)
|
|
var/atom/movable/screen/storage/item/slot/renderer = new(null, item)
|
|
. += renderer
|
|
// position
|
|
renderer.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X + current_column - 1]:[STORAGE_UI_START_PIXEL_X],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + current_row - 1]:[STORAGE_UI_START_PIXEL_Y]"
|
|
// advance
|
|
++current_column
|
|
if(current_column > rendering_width)
|
|
current_column = 1
|
|
++current_row
|
|
if(current_row > STORAGE_UI_MAX_ROWS)
|
|
break
|
|
|
|
/**
|
|
* Volumetric rendering.
|
|
* * Doesn't respect random access limits.
|
|
*/
|
|
/datum/object_system/storage/proc/create_ui_volumetric_mode(mob/user)
|
|
// guard against divide-by-0's
|
|
if(!max_combined_volume)
|
|
return create_ui_slot_mode(user)
|
|
. = list()
|
|
|
|
//? resolve items
|
|
|
|
// resolve indirection
|
|
var/atom/indirection = real_contents_loc()
|
|
|
|
//? resolve view and rendering
|
|
// todo: clientless support is awful here
|
|
|
|
// resolve view
|
|
var/list/decoded_view = decode_view_size(user.client?.view || world.view)
|
|
var/view_x = decoded_view[1]
|
|
// setup initial width limits
|
|
var/rendering_width_limit = STORAGE_UI_TILES_FOR_SCREEN_VIEW_X(view_x)
|
|
var/rendering_width_px_limit = rendering_width_limit * 32
|
|
|
|
// here's the tricky part
|
|
// we don't know our max (need to know how many items in one row which we don't)
|
|
// or our min (ditto)
|
|
// and during the row-forming ops we can actually end up adding more space as needed
|
|
// so:
|
|
|
|
// we keep a running current-width
|
|
var/rendering_width_px = 0
|
|
// we scale up effective max volume if we're overpacked
|
|
var/effective_max_volume = max(max_combined_volume, cached_combined_volume)
|
|
// we have some dynamic tuning for how many pixels things use
|
|
var/effective_pixel_volume_ratio = VOLUMETRIC_STORAGE_STANDARD_PIXEL_RATIO
|
|
// we compute how much pixels we ideally will need to display everything
|
|
var/ideal_width_px = effective_pixel_volume_ratio * effective_max_volume
|
|
// compute what the sane minimum is so small containers don't explode
|
|
// 'ui_expand_volumetric_minimum_items' is used here to make tiny containers not too small
|
|
var/sane_minimum = max((VOLUMETRIC_STORAGE_INFLATE_TO_TILES * WORLD_ICON_SIZE), ui_expand_volumetric_minimum_items * VOLUMETRIC_STORAGE_MINIMUM_PIXELS_PER_ITEM)
|
|
// if too low,
|
|
if(ideal_width_px < sane_minimum)
|
|
// expand our horizons
|
|
effective_pixel_volume_ratio = sane_minimum / effective_max_volume
|
|
ideal_width_px = effective_pixel_volume_ratio * effective_max_volume
|
|
|
|
// -- prepare for iteration --
|
|
|
|
// volume accounted for.
|
|
// when we render things smaller than minimum pixels per item, we effectively 'overrun'.
|
|
// this results in things looking full when they're not.
|
|
// so, we track how much actual volume we rendered
|
|
var/volume_accounted_for = 0
|
|
|
|
// currently used inner-width;
|
|
// * this includes boxes and padding alike
|
|
var/iteration_width = 0
|
|
// current row
|
|
var/current_row = 1
|
|
// safety var
|
|
var/safety = VOLUMETRIC_STORAGE_MAX_ITEMS
|
|
|
|
// -- iterate --
|
|
|
|
for(var/obj/item/item in indirection)
|
|
var/item_volume = item.get_weight_volume()
|
|
// check safety
|
|
if(--safety <= 0)
|
|
to_chat(user, SPAN_WARNING("Some items in this storage have been truncated for performance reasons."))
|
|
break
|
|
var/item_width_px = max(VOLUMETRIC_STORAGE_MINIMUM_PIXELS_PER_ITEM, item_volume * effective_pixel_volume_ratio)
|
|
var/space_left_px = rendering_width_px_limit - iteration_width
|
|
// check row; only wrap around when we're out of px-limit, or, if
|
|
// the current item would be unacceptably squashed by being stuffed in.
|
|
if(space_left_px < (item_width_px * 0.75))
|
|
// check if we're out of rows
|
|
if(current_row >= STORAGE_UI_MAX_ROWS)
|
|
to_chat(user, SPAN_WARNING("Some items in this storage have been truncated for performance reasons."))
|
|
break
|
|
// do final row stuff
|
|
if(iteration_width > 0)
|
|
// subtract last item padding if any items were rendered
|
|
iteration_width -= VOLUMETRIC_STORAGE_ITEM_PADDING
|
|
iteration_width += VOLUMETRIC_STORAGE_EDGE_PADDING
|
|
rendering_width_px = max(rendering_width_px, iteration_width)
|
|
// make another row
|
|
++current_row
|
|
iteration_width = 0
|
|
|
|
// render the item
|
|
var/atom/movable/screen/storage/item/volumetric/renderer = new(null, item)
|
|
// add to emitted screen list
|
|
. += renderer
|
|
// scale it as necessary, to nearest multiple of 2
|
|
var/used_pixels = max(VOLUMETRIC_STORAGE_MINIMUM_PIXELS_PER_ITEM, CEILING(ideal_width_px * (item_volume / effective_max_volume), 2))
|
|
renderer.set_pixel_width(used_pixels)
|
|
// set screen loc
|
|
renderer.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X + VOLUMETRIC_STORAGE_EDGE_PADDING + iteration_width + (used_pixels - VOLUMETRIC_STORAGE_BOX_ICON_SIZE) * 0.5],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + current_row - 1]:[STORAGE_UI_START_PIXEL_Y]"
|
|
// add to iteration tracking
|
|
iteration_width += used_pixels + VOLUMETRIC_STORAGE_ITEM_PADDING
|
|
volume_accounted_for += item_volume
|
|
|
|
// if we only used one row..
|
|
if(current_row == 1)
|
|
// expand it if we still have room left to take into account the unused space.
|
|
if(volume_accounted_for < effective_max_volume)
|
|
iteration_width = min(rendering_width_px_limit, iteration_width + (effective_max_volume - volume_accounted_for) * effective_pixel_volume_ratio + VOLUMETRIC_STORAGE_ITEM_PADDING)
|
|
// do final row stuff
|
|
if(iteration_width > 0)
|
|
// subtract last item padding if any items were rendered
|
|
iteration_width -= VOLUMETRIC_STORAGE_ITEM_PADDING
|
|
iteration_width += VOLUMETRIC_STORAGE_EDGE_PADDING
|
|
rendering_width_px = max(rendering_width_px, iteration_width)
|
|
|
|
// -- render --
|
|
|
|
// now that everything's set up, we can render everything based on the solved sizes.
|
|
var/middle_width = rendering_width_px + VOLUMETRIC_STORAGE_EDGE_PADDING * 2
|
|
// i hate byond i hate byond i hate byond i hate byond; this is because things break if we don't extend by 2 pixels
|
|
// at a time for left/right as we use a dumb transform matrix and screen loc to shift, instead of a scale and shift matrix
|
|
middle_width = CEILING(middle_width, 2)
|
|
// todo: instead of this crap we should instead have the translation matrix do the shift
|
|
var/middle_shift = round(middle_width * 0.5 - VOLUMETRIC_STORAGE_BOX_ICON_SIZE * 0.5)
|
|
// render left
|
|
var/atom/movable/screen/storage/panel/volumetric/left/p_left = new
|
|
. += p_left
|
|
p_left.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X - VOLUMETRIC_STORAGE_BOX_BORDER_SIZE],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y]:[STORAGE_UI_START_PIXEL_Y] to \
|
|
LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X - VOLUMETRIC_STORAGE_BOX_BORDER_SIZE],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + current_row - 1]:[STORAGE_UI_START_PIXEL_Y]"
|
|
// render middle
|
|
var/atom/movable/screen/storage/panel/volumetric/middle/p_box = new
|
|
. += p_box
|
|
p_box.set_pixel_width(middle_width)
|
|
p_box.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X + middle_shift],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y]:[STORAGE_UI_START_PIXEL_Y] to \
|
|
LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X + middle_shift],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + current_row - 1]:[STORAGE_UI_START_PIXEL_Y]"
|
|
p_box.color = get_ui_drawer_tint()
|
|
// render closer on bottom
|
|
var/atom/movable/screen/storage/closer/closer = new
|
|
. += closer
|
|
closer.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X + middle_width],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y]:[STORAGE_UI_START_PIXEL_Y]"
|
|
// render right sides above closer
|
|
if(current_row > 1)
|
|
var/atom/movable/screen/storage/panel/volumetric/right/p_right = new
|
|
. += p_right
|
|
p_right.screen_loc = "LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X + middle_width - WORLD_ICON_SIZE + VOLUMETRIC_STORAGE_BOX_BORDER_SIZE],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + 1]:[STORAGE_UI_START_PIXEL_Y] to \
|
|
LEFT+[STORAGE_UI_START_TILE_X]:[STORAGE_UI_START_PIXEL_X + middle_width - WORLD_ICON_SIZE + VOLUMETRIC_STORAGE_BOX_BORDER_SIZE],\
|
|
BOTTOM+[STORAGE_UI_START_TILE_Y + current_row - 1]:[STORAGE_UI_START_PIXEL_Y]"
|