Belts & Trashbags (#7443)

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>
This commit is contained in:
silicons
2025-12-28 22:05:06 -05:00
committed by GitHub
co-authored by silicons
parent 7707f9e881
commit 30fbcfe9b4
124 changed files with 1176 additions and 1086 deletions
@@ -0,0 +1,224 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* todo: refactor?
*
* @return TRUE / FALSE; if true, caller should stop clickchain.
*/
/datum/object_system/storage/proc/auto_handle_interacted_insertion(obj/item/inserting, datum/event_args/actor/actor, silent, suppressed)
if(!actor.performer.is_holding(inserting))
// something probably yanked it, don't bother
return FALSE
if(is_locked(actor.performer))
actor.chat_feedback(
msg = SPAN_WARNING("[parent] is locked."),
target = parent,
)
return TRUE
if(!actor.performer.Reachability(indirection || parent))
return TRUE
if(!try_insert(inserting, actor, silent, suppressed))
return TRUE
// sound
// todo: put this in interacted_insert()..?
if(!suppressed && !isnull(actor))
if(sfx_insert)
// todo: variable sound
playsound(actor.performer, sfx_insert, 50, 1, -5)
actor.visible_feedback(
target = parent,
range = MESSAGE_RANGE_INVENTORY_SOFT,
visible = "[actor.performer] puts [inserting] into [parent].",
visible_self = "You put [inserting] into [parent]",
)
return TRUE
/**
* Called by inventory procs; skips some checks of interacted insertion.
*
* todo: refactor?
*
* @return TRUE on success, FALSE on failure.
*/
/datum/object_system/storage/proc/auto_handle_inventory_insertion(obj/item/inserting, datum/event_args/actor/actor, silent, suppressed)
if(is_locked(actor.performer))
actor.chat_feedback(
msg = SPAN_WARNING("[parent] is locked."),
target = parent,
)
return TRUE
if(!actor.performer.Reachability(indirection || parent))
return TRUE
if(!try_insert(inserting, actor, silent, suppressed))
return TRUE
// sound
// todo: put this in interacted_insert()..?
if(!suppressed && !isnull(actor))
if(sfx_insert)
// todo: variable sound
playsound(actor.performer, sfx_insert, 50, 1, -5)
actor.visible_feedback(
target = parent,
range = MESSAGE_RANGE_INVENTORY_SOFT,
visible = "[actor.performer] puts [inserting] into [parent].",
visible_self = "You put [inserting] into [parent]",
)
return TRUE
/datum/object_system/storage/proc/try_insert(obj/item/inserting, datum/event_args/actor/actor, silent, suppressed, no_update)
if(!can_be_inserted(inserting, actor, silent))
return FALSE
// point of no return
if(actor && (inserting.get_worn_mob() == actor.performer && !actor.performer.temporarily_remove_from_inventory(inserting, user = actor.performer)))
if(!silent)
actor?.chat_feedback(
msg = SPAN_WARNING("[inserting] is stuck to your hand / body!"),
target = parent,
)
return FALSE
// point of no return (real)
return insert(inserting, actor, suppressed, no_update)
/datum/object_system/storage/proc/can_be_inserted(obj/item/inserting, datum/event_args/actor/actor, silent)
if(!check_insertion_filters(inserting))
if(!silent)
actor?.chat_feedback(
msg = SPAN_WARNING("[parent] can't hold [inserting]!"),
target = parent,
)
return FALSE
var/why_insufficient_space = why_failed_insertion_limits(inserting)
if(why_insufficient_space)
if(!silent)
actor?.chat_feedback(
msg = SPAN_WARNING("[parent] can't fit [inserting]! ([why_insufficient_space])"),
target = parent,
)
return FALSE
return TRUE
/**
* inserts something
*
* @params
* * inserting - thing being inserted
* * actor - who's inserting it
* * suppressed - suppress sounds
* * no_update - do not update uis
* * no_move - much more than not moving; basically makes an abstract contents operation without otherwise doing normal logic. use with care.
*/
/datum/object_system/storage/proc/insert(obj/item/inserting, datum/event_args/actor/actor, suppressed, no_update, no_move)
physically_insert_item(inserting, no_move)
if(!no_update)
if(update_icon_on_item_change)
update_icon()
refresh()
return TRUE
/**
* handle moving an item in
* we can assume this proc will do potentially literally anything with the item, so.
*
* @return inserted item or null (null doesn't mean insert failed; for example stacks can be merged in)
*/
/datum/object_system/storage/proc/physically_insert_item(obj/item/inserting, no_move, from_hook)
inserting.item_flags |= ITEM_IN_STORAGE
if(!no_move)
inserting.forceMove(real_contents_loc())
inserting.vis_flags |= VIS_INHERIT_LAYER | VIS_INHERIT_PLANE
inserting.reset_pixel_offsets()
inserting.on_enter_storage(src)
if(weight_propagation)
var/inserting_weight = inserting.get_weight()
if(inserting_weight)
weight_cached += inserting_weight
update_containing_weight(inserting_weight)
cached_combined_volume += inserting.get_weight_volume()
cached_combined_weight_class += inserting.get_weight_class()
return inserting
/**
* @return TRUE / FALSE
*/
/datum/object_system/storage/proc/auto_handle_interacted_removal(obj/item/removing, datum/event_args/actor/actor, silent, suppressed, put_in_hands)
if(is_locked(actor.performer))
actor.chat_feedback(
msg = SPAN_WARNING("[parent] is locked."),
target = parent,
)
return TRUE
if(!actor.performer.Reachability(parent))
return TRUE
if(!try_remove(removing, actor.performer, actor, silent, suppressed))
return TRUE
if(put_in_hands)
actor.performer.put_in_hands_or_drop(removing)
else
removing.forceMove(actor.performer.drop_location())
// todo: put this in interacted_insert()..?
if(!suppressed && !isnull(actor))
if(sfx_remove)
// todo: variable sound
playsound(actor.performer, sfx_remove, 50, 1, -5)
actor.visible_feedback(
target = parent,
range = MESSAGE_RANGE_INVENTORY_SOFT,
visible = "[actor.performer] takes [removing] out of [parent].",
visible_self = "You take [removing] out of [parent]",
)
return TRUE
/**
* try to remove item from self
*
* @return item removed; not necessarily the item passed in.
*/
/datum/object_system/storage/proc/try_remove(obj/item/removing, atom/to_where, datum/event_args/actor/actor, silent, suppressed, no_update)
return remove(removing, to_where, actor, suppressed, no_update)
/datum/object_system/storage/proc/can_be_removed(obj/item/removing, atom/to_where, datum/event_args/actor/actor, silent)
return TRUE
/**
* remove item from self
*
* @return item removed; not necessarily the item passed in.
*/
/datum/object_system/storage/proc/remove(obj/item/removing, atom/to_where, datum/event_args/actor/actor, suppressed, no_update, no_move)
. = physically_remove_item(removing, to_where, no_move)
if(isnull(.))
return
if(!no_update)
if(update_icon_on_item_change)
update_icon()
refresh()
/**
* handle moving an item out
* we can assume this proc will do potentially literally anything with the item, so..
*
* @return moved item
*/
/datum/object_system/storage/proc/physically_remove_item(obj/item/removing, atom/to_where, no_move, from_hook)
removing.item_flags &= ~ITEM_IN_STORAGE
if(!no_move)
if(to_where == null)
removing.moveToNullspace()
else
removing.forceMove(to_where)
removing.vis_flags = (removing.vis_flags & ~(VIS_INHERIT_LAYER | VIS_INHERIT_LAYER)) | (initial(removing.vis_flags) & (VIS_INHERIT_LAYER | VIS_INHERIT_PLANE))
removing.on_exit_storage(src)
// we might have set maptext in render_numerical_display
removing.maptext = null
if(weight_propagation)
var/removing_weight = removing.get_weight()
if(removing_weight)
weight_cached -= removing_weight
update_containing_weight(-removing_weight)
cached_combined_volume -= removing.get_weight_volume()
cached_combined_weight_class -= removing.get_weight_class()
return removing
@@ -32,3 +32,34 @@
if(candidate.obj_storage && (candidate.w_class >= parent.w_class) && disallow_equal_weight_class_storage_nesting)
return "can't nest storage"
return null
/**
* generally a bad idea to call, tbh.
*
* uses max single weight class
* uses combined volume
*
* can use type whitelist
* if type_whitelist is FALSE, this just wipes all type whitelists.
*/
/datum/object_system/storage/proc/fit_to_contents(type_whitelist = FALSE, no_shrink = FALSE)
var/scanned_max_single_weight_class = WEIGHT_CLASS_TINY
var/scanned_max_combined_volume = 0
if(!no_shrink)
max_single_weight_class = scanned_max_single_weight_class
max_combined_volume = scanned_max_combined_volume
var/list/types = list()
for(var/obj/item/item in real_contents_loc())
if(type_whitelist)
types |= item.type
scanned_max_single_weight_class = max(max_single_weight_class, item.w_class)
scanned_max_combined_volume += item.get_weight_volume()
max_single_weight_class = max(max_single_weight_class, scanned_max_single_weight_class)
max_combined_volume = max(max_combined_volume, scanned_max_combined_volume)
set_insertion_whitelist(type_whitelist? types : null)
set_insertion_blacklist(null)
set_insertion_allow(null)
+104 -9
View File
@@ -1,6 +1,75 @@
//* 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 *//
/**
@@ -89,6 +158,20 @@
/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
@@ -100,17 +183,20 @@
var/view_x = decoded_view[1]
// clamp to max items if needed
var/rendering_width = STORAGE_UI_TILES_FOR_SCREEN_VIEW_X(view_x)
if(max_items)
rendering_width = min(max_items, rendering_width)
// 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/atom/indirection = real_contents_loc()
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)
rendering_width = min(rendering_width, (isnull(numerical_rendered)? length(indirection.contents) : length(numerical_rendered)) + 1)
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(indirection.contents) : length(numerical_rendered)
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
@@ -118,6 +204,7 @@
// 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
@@ -138,7 +225,7 @@
if(current_row > STORAGE_UI_MAX_ROWS)
break
else
for(var/obj/item/item in indirection)
for(var/obj/item/item in accessible)
var/atom/movable/screen/storage/item/slot/renderer = new(null, item)
. += renderer
// position
@@ -152,6 +239,10 @@
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)
@@ -187,10 +278,13 @@
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 < (VOLUMETRIC_STORAGE_INFLATE_TO_TILES * WORLD_ICON_SIZE))
if(ideal_width_px < sane_minimum)
// expand our horizons
effective_pixel_volume_ratio = (VOLUMETRIC_STORAGE_INFLATE_TO_TILES * WORLD_ICON_SIZE) / effective_max_volume
effective_pixel_volume_ratio = sane_minimum / effective_max_volume
ideal_width_px = effective_pixel_volume_ratio * effective_max_volume
// -- prepare for iteration --
@@ -211,7 +305,7 @@
// -- iterate --
for(var/obj/item/item as anything in indirection)
for(var/obj/item/item in indirection)
var/item_volume = item.get_weight_volume()
// check safety
if(--safety <= 0)
@@ -286,6 +380,7 @@
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
+77 -354
View File
@@ -11,10 +11,16 @@
//* Access *//
/// if set, limit allowable random access to first n items
var/limited_random_access_stack_amount
var/limited_random_access_amount
/// if set, limit allowable random access to first n items with this max weight class
/// * requires [limited_random_access_amount] to be set
var/limited_random_access_total_weight_class = INFINITY
/// if set, limit allowable random access to first n items with this max weight volume
/// * requires [limited_random_access_amount] to be set
var/limited_random_access_total_weight_volume = INFINITY
/// if set, limit allowable random access goes from bottom up, not top down
/// * top down is from end of contents list, bottom up is from start of contents list
var/limited_random_access_stack_bottom_first = FALSE
var/limited_random_access_bottom_first = FALSE
//* Actions *//
@@ -27,7 +33,7 @@
/// use the setter, do not change this manually.
var/weight_propagation = TRUE
/// carry weight in us prior to mitigation
var/weight_cached = 0
var/tmp/weight_cached = 0
/// carry weight mitigation, static. applied after multiplicative
var/weight_subtract = 0
/// carry weight mitigation, multiplicative.
@@ -80,11 +86,11 @@
var/insert_preposition = "in"
/// allow quick empty at all
var/allow_quick_empty = FALSE
var/allow_quick_empty = TRUE
/// allow quick empty via clickdrag
var/allow_quick_empty_via_clickdrag = TRUE
/// allow quick empty via attack self
var/allow_quick_empty_via_attack_self = TRUE
var/allow_quick_empty_via_attack_self = FALSE
/// allow inbound mass transfers
var/allow_inbound_mass_transfer = TRUE
@@ -140,6 +146,8 @@
/// mutex to prevent mass operation spam
var/mass_operation_interaction_mutex = FALSE
/// max items dropped per second for mass dumping
var/mass_operation_dumping_limit_per_second = INFINITY
//* Redirection *//
@@ -194,8 +202,12 @@
/// ui force slot mode.
var/ui_force_slot_mode = FALSE
/// show minimum number of slots necessary, expand as needed
/// currently only works for slot mode
var/ui_expand_when_needed = FALSE
/// * currently only works for slot mode
/// * this should be set to a number to determine how many minimum slots to show despite this
/// setting; the minimum enabled number of '1' just shows a single slot.
var/ui_expand_when_needed = 0
/// used to hint the number of minimum items to assume we have. useful for pill bottles.
var/ui_expand_volumetric_minimum_items
/// ui update queued?
// todo: this is only needed because redirection is halfassed.
var/ui_refresh_queued = FALSE
@@ -222,33 +234,45 @@
//* Access *//
/**
* do not mutate returned list
*/
/datum/object_system/storage/proc/dangerously_accessible_items_immutable(random_access = TRUE)
var/atom/redirection = real_contents_loc()
if(random_access)
if(limited_random_access_stack_amount)
var/contents_length = length(redirection.contents)
if(limited_random_access_stack_bottom_first)
return redirection.contents.Copy(1, min(contents_length + 1, limited_random_access_stack_bottom_first + 1))
else
return redirection.contents.Copy(max(1, contents_length - limited_random_access_stack_amount + 1), contents_length + 1)
return redirection.contents
/**
* you may mutate returned list
*/
/datum/object_system/storage/proc/accessible_items(random_access = TRUE)
/datum/object_system/storage/proc/get_accessible_items(random_access = TRUE)
var/atom/redirection = real_contents_loc()
if(random_access)
if(limited_random_access_stack_amount)
var/contents_length = length(redirection.contents)
if(limited_random_access_stack_bottom_first)
return redirection.contents.Copy(1, min(contents_length + 1, limited_random_access_stack_bottom_first + 1))
if(!length(redirection.contents))
return list()
if(random_access && limited_random_access_amount)
var/list/copied_contents = . = list()
if(limited_random_access_amount)
var/original_contents_length = length(redirection.contents)
var/lra_start
var/lra_end
var/lra_step
if(limited_random_access_bottom_first)
lra_start = 1
lra_end = min(limited_random_access_amount, original_contents_length)
lra_step = 1
else
return redirection.contents.Copy(max(1, contents_length - limited_random_access_stack_amount + 1), contents_length + 1)
return redirection.contents.Copy()
lra_start = original_contents_length
lra_end = max(1, original_contents_length - limited_random_access_amount + 1)
lra_step = -1
var/list/lra_ref = redirection.contents
var/lra_twc = 0
var/lra_twv = 0
for(var/i in lra_start to lra_end step lra_step)
var/obj/item/maybe_item = lra_ref[i]
// it is valid albeit silly to have non-items in a storage object
if(!istype(maybe_item))
continue
lra_twc += maybe_item.w_class
lra_twv += maybe_item.weight_volume
if(lra_twc > limited_random_access_total_weight_class)
break
if(lra_twv > limited_random_access_total_weight_volume)
break
copied_contents += maybe_item
else
. = redirection.contents.Copy()
/**
* Recursively return all inventory in this or nested storage (without indirection)
@@ -305,11 +329,20 @@
//* Caches *//
/datum/object_system/storage/proc/rebuild_caches()
SHOULD_NOT_SLEEP(TRUE)
SHOULD_NOT_OVERRIDE(TRUE)
var/atom/indirection = real_contents_loc()
rebuild_caches_impl(indirection ? indirection.contents : list())
/datum/object_system/storage/proc/rebuild_caches_impl(list/atom/movable/entities)
SHOULD_CALL_PARENT(TRUE)
SHOULD_NOT_SLEEP(TRUE)
cached_combined_volume = 0
cached_combined_weight_class = 0
var/old_weight = weight_cached
weight_cached = 0
for(var/obj/item/item in real_contents_loc())
for(var/obj/item/item in entities)
cached_combined_volume += item.get_weight_volume()
cached_combined_weight_class += item.get_weight_class()
weight_cached += item.get_weight()
@@ -365,6 +398,11 @@
else
rebuild_caches()
//* Examine *//
/datum/object_system/storage/proc/handle_storage_examine(list/examine_list)
// TODO: proper examine. implement on /stack storage too
//* Hooks *//
/**
@@ -433,260 +471,6 @@
return TRUE
return FALSE
//* Insertion & Removal *//
/**
* todo: refactor?
*
* @return TRUE / FALSE; if true, caller should stop clickchain.
*/
/datum/object_system/storage/proc/auto_handle_interacted_insertion(obj/item/inserting, datum/event_args/actor/actor, silent, suppressed)
if(!actor.performer.is_holding(inserting))
// something probably yanked it, don't bother
return FALSE
if(is_locked(actor.performer))
actor.chat_feedback(
msg = SPAN_WARNING("[parent] is locked."),
target = parent,
)
return TRUE
if(!actor.performer.Reachability(indirection || parent))
return TRUE
if(!try_insert(inserting, actor, silent, suppressed))
return TRUE
// sound
// todo: put this in interacted_insert()..?
if(!suppressed && !isnull(actor))
if(sfx_insert)
// todo: variable sound
playsound(actor.performer, sfx_insert, 50, 1, -5)
actor.visible_feedback(
target = parent,
range = MESSAGE_RANGE_INVENTORY_SOFT,
visible = "[actor.performer] puts [inserting] into [parent].",
visible_self = "You put [inserting] into [parent]",
)
return TRUE
/**
* Called by inventory procs; skips some checks of interacted insertion.
*
* todo: refactor?
*
* @return TRUE on success, FALSE on failure.
*/
/datum/object_system/storage/proc/auto_handle_inventory_insertion(obj/item/inserting, datum/event_args/actor/actor, silent, suppressed)
if(is_locked(actor.performer))
actor.chat_feedback(
msg = SPAN_WARNING("[parent] is locked."),
target = parent,
)
return TRUE
if(!actor.performer.Reachability(indirection || parent))
return TRUE
if(!try_insert(inserting, actor, silent, suppressed))
return TRUE
// sound
// todo: put this in interacted_insert()..?
if(!suppressed && !isnull(actor))
if(sfx_insert)
// todo: variable sound
playsound(actor.performer, sfx_insert, 50, 1, -5)
actor.visible_feedback(
target = parent,
range = MESSAGE_RANGE_INVENTORY_SOFT,
visible = "[actor.performer] puts [inserting] into [parent].",
visible_self = "You put [inserting] into [parent]",
)
return TRUE
/datum/object_system/storage/proc/try_insert(obj/item/inserting, datum/event_args/actor/actor, silent, suppressed, no_update)
if(!can_be_inserted(inserting, actor, silent))
return FALSE
// point of no return
if(actor && (inserting.get_worn_mob() == actor.performer && !actor.performer.temporarily_remove_from_inventory(inserting, user = actor.performer)))
if(!silent)
actor?.chat_feedback(
msg = SPAN_WARNING("[inserting] is stuck to your hand / body!"),
target = parent,
)
return FALSE
// point of no return (real)
return insert(inserting, actor, suppressed, no_update)
/datum/object_system/storage/proc/can_be_inserted(obj/item/inserting, datum/event_args/actor/actor, silent)
if(!check_insertion_filters(inserting))
if(!silent)
actor?.chat_feedback(
msg = SPAN_WARNING("[parent] can't hold [inserting]!"),
target = parent,
)
return FALSE
var/why_insufficient_space = why_failed_insertion_limits(inserting)
if(why_insufficient_space)
if(!silent)
actor?.chat_feedback(
msg = SPAN_WARNING("[parent] can't fit [inserting]! ([why_insufficient_space])"),
target = parent,
)
return FALSE
return TRUE
/**
* inserts something
*
* @params
* * inserting - thing being inserted
* * actor - who's inserting it
* * suppressed - suppress sounds
* * no_update - do not update uis
* * no_move - much more than not moving; basically makes an abstract contents operation without otherwise doing normal logic. use with care.
*/
/datum/object_system/storage/proc/insert(obj/item/inserting, datum/event_args/actor/actor, suppressed, no_update, no_move)
physically_insert_item(inserting, no_move)
if(!no_update)
if(update_icon_on_item_change)
update_icon()
refresh()
return TRUE
/**
* handle moving an item in
*
* we can assume this proc will do potentially literally anything with the item, so.
*/
/datum/object_system/storage/proc/physically_insert_item(obj/item/inserting, no_move, from_hook)
inserting.item_flags |= ITEM_IN_STORAGE
if(!no_move)
inserting.forceMove(real_contents_loc())
inserting.vis_flags |= VIS_INHERIT_LAYER | VIS_INHERIT_PLANE
inserting.reset_pixel_offsets()
inserting.on_enter_storage(src)
if(weight_propagation)
var/inserting_weight = inserting.get_weight()
if(inserting_weight)
weight_cached += inserting_weight
update_containing_weight(inserting_weight)
cached_combined_volume += inserting.get_weight_volume()
cached_combined_weight_class += inserting.get_weight_class()
/**
* @return TRUE / FALSE
*/
/datum/object_system/storage/proc/auto_handle_interacted_removal(obj/item/removing, datum/event_args/actor/actor, silent, suppressed, put_in_hands)
if(is_locked(actor.performer))
actor.chat_feedback(
msg = SPAN_WARNING("[parent] is locked."),
target = parent,
)
return TRUE
if(!actor.performer.Reachability(parent))
return TRUE
if(!try_remove(removing, actor.performer, actor, silent, suppressed))
return TRUE
if(put_in_hands)
actor.performer.put_in_hands_or_drop(removing)
else
removing.forceMove(actor.performer.drop_location())
// todo: put this in interacted_insert()..?
if(!suppressed && !isnull(actor))
if(sfx_remove)
// todo: variable sound
playsound(actor.performer, sfx_remove, 50, 1, -5)
actor.visible_feedback(
target = parent,
range = MESSAGE_RANGE_INVENTORY_SOFT,
visible = "[actor.performer] takes [removing] out of [parent].",
visible_self = "You take [removing] out of [parent]",
)
return TRUE
/**
* try to remove item from self
*
* @return item removed; not necessarily the item passed in.
*/
/datum/object_system/storage/proc/try_remove(obj/item/removing, atom/to_where, datum/event_args/actor/actor, silent, suppressed, no_update)
return remove(removing, to_where, actor, suppressed, no_update)
/datum/object_system/storage/proc/can_be_removed(obj/item/removing, atom/to_where, datum/event_args/actor/actor, silent)
return TRUE
/**
* remove item from self
*
* @return item removed; not necessarily the item passed in.
*/
/datum/object_system/storage/proc/remove(obj/item/removing, atom/to_where, datum/event_args/actor/actor, suppressed, no_update, no_move)
. = physically_remove_item(removing, to_where, no_move)
if(isnull(.))
return
if(!no_update)
if(update_icon_on_item_change)
update_icon()
refresh()
/**
* handle moving an item out
*
* we can assume this proc will do potentially literally anything with the item, so..
*/
/datum/object_system/storage/proc/physically_remove_item(obj/item/removing, atom/to_where, no_move, from_hook)
removing.item_flags &= ~ITEM_IN_STORAGE
if(!no_move)
if(to_where == null)
removing.moveToNullspace()
else
removing.forceMove(to_where)
removing.vis_flags = (removing.vis_flags & ~(VIS_INHERIT_LAYER | VIS_INHERIT_LAYER)) | (initial(removing.vis_flags) & (VIS_INHERIT_LAYER | VIS_INHERIT_PLANE))
removing.on_exit_storage(src)
// we might have set maptext in render_numerical_display
removing.maptext = null
if(weight_propagation)
var/removing_weight = removing.get_weight()
if(removing_weight)
weight_cached -= removing_weight
update_containing_weight(-removing_weight)
cached_combined_volume -= removing.get_weight_volume()
cached_combined_weight_class -= removing.get_weight_class()
return removing
//* Limits *//
/**
* generally a bad idea to call, tbh.
*
* uses max single weight class
* uses combined volume
*
* can use type whitelist
* if type_whitelist is FALSE, this just wipes all type whitelists.
*/
/datum/object_system/storage/proc/fit_to_contents(type_whitelist = FALSE, no_shrink = FALSE)
var/scanned_max_single_weight_class = WEIGHT_CLASS_TINY
var/scanned_max_combined_volume = 0
if(!no_shrink)
max_single_weight_class = scanned_max_single_weight_class
max_combined_volume = scanned_max_combined_volume
var/list/types = list()
for(var/obj/item/item in real_contents_loc())
if(type_whitelist)
types |= item.type
scanned_max_single_weight_class = max(max_single_weight_class, item.w_class)
scanned_max_combined_volume += item.get_weight_volume()
max_single_weight_class = max(max_single_weight_class, scanned_max_single_weight_class)
max_combined_volume = max(max_combined_volume, scanned_max_combined_volume)
set_insertion_whitelist(type_whitelist? types : null)
set_insertion_blacklist(null)
set_insertion_allow(null)
//* Locking *//
/**
@@ -1041,9 +825,13 @@
var/atom/indirection = real_contents_loc()
var/i = length(things)
. = TRUE
// we're driven on 0.5 second ticks; we want to pause after half a MODLPS
// might want to adjust this later and use a parameter or something lol
var/pause_after = mass_operation_dumping_limit_per_second * 0.5
var/transferred = 0
while(i > 0)
// stop if overtaxed
if(TICK_CHECK)
if(TICK_CHECK || (transferred > pause_after))
break
var/obj/item/transferring = things[i]
// make sure they're still there
@@ -1065,6 +853,10 @@
if(removed == transferring)
// but only go down if we got rid of the real item
i--
// always count the cycle as transferred,
// if you have invalid objects and it gets in the way of MODLPS
// then that's on you, stop powergaming.
transferred++
things.Cut(i + 1, length(things) + 1)
return . && length(things)
@@ -1083,75 +875,6 @@
for(var/obj/item/I in real_contents_loc())
I.forceMove(where)
//* UI *//
/**
* @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)
//? Action
/datum/action/storage_gather_mode
@@ -0,0 +1,12 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* Default backpack storage.
*/
/datum/object_system/storage/backpack
max_single_weight_class = WEIGHT_CLASS_BULKY
max_combined_volume = STORAGE_VOLUME_BACKPACK
/datum/object_system/storage/backpack/dufflebag
max_combined_volume = STORAGE_VOLUME_DUFFLEBAG
@@ -0,0 +1,174 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* Belt storage
*
* Uses a list of class-size-limits.
*/
/datum/object_system/storage/belt
// belts use slot mode for now, pending a new belt UI
ui_force_slot_mode = TRUE
// belts can be massive so only expand when needed
ui_expand_when_needed = 7
/// max size of small-class belt items
var/max_combined_belt_small = 5
/// max size of medium-class belt items
var/max_combined_belt_medium = 3
/// max size of large-class belt items
var/max_combined_belt_large = 1
var/tmp/cached_combined_belt_small_size = 0
var/tmp/cached_combined_belt_medium_size = 0
var/tmp/cached_combined_belt_large_size = 0
/datum/object_system/storage/belt/get_ui_predicted_max_items()
return min(max_items, max_combined_belt_small + max_combined_belt_medium + max_combined_belt_large)
/datum/object_system/storage/belt/uses_volumetric_ui()
return FALSE
/datum/object_system/storage/belt/handle_storage_examine(list/examine_list)
var/list/used_slots = list(cached_combined_belt_small_size, cached_combined_belt_medium_size, cached_combined_belt_large_size)
var/list/max_slots = list(max_combined_belt_small, max_combined_belt_medium, max_combined_belt_large)
var/list/rendered_loop_types = list()
var/has_overrun
for(var/i in 1 to 3)
if(used_slots[i] > max_slots[i])
has_overrun = TRUE
rendered_loop_types += "<b>[used_slots[i] > max_slots[i] ? SPAN_DANGER("[used_slots[i]]") : used_slots[i]]/[max_slots[i]]</b> \
[lowertext(global.belt_class_names[i + 1])]"
examine_list += SPAN_NOTICE("This is a storage item with <b>belt loops</b>. There are \
[english_list(rendered_loop_types)] loops on it.[has_overrun ? SPAN_WARNING(" Some of the larger loops are \
currently being filled up by excess items of smaller size.") : ""]")
/**
* @return list(small, medium, large, )
*/
/datum/object_system/storage/belt/proc/compute_free_slots() as /list
var/small = 0
var/medium = 0
var/large = 0
// belts are weird
// small items can overflow onto medium, medium can overflow onto large
// the problem arises in that if small is overrunning, you don't know if it's
// overrunning into medium or large; this is because storage ui does not yet store, bespoke,
// which slot an item is in, so the player has no control over it
// thus this standard proc is here to enforce a single order of operations
// right now, we assume that any items always have dibs on their own slot first,
// and overrun 'hops over' if there's not enough.
small = max_combined_belt_small - cached_combined_belt_small_size
medium = max_combined_belt_medium - cached_combined_belt_medium_size
large = max_combined_belt_large - cached_combined_belt_large_size
if(small < 0)
if(medium > 0)
// overrun as much as we can to medium
. = min(medium, -small)
small += .
medium -= .
// overrun rest to large
large += small
small = 0
if(medium < 0)
// overrun all medium to large
large += medium
medium = 0
return list(small, medium, large)
/datum/object_system/storage/belt/get_ui_drawer_tint()
var/list/remaining = compute_free_slots()
var/full_classes = 0
for(var/i in 1 to 3)
if(remaining[i] <= 0)
full_classes += 1
switch(full_classes)
if(0)
return null
if(1, 2)
return "#888800"
if(3)
return "#aa0000"
/datum/object_system/storage/belt/rebuild_caches_impl(list/atom/movable/entities)
..()
cached_combined_belt_small_size = cached_combined_belt_medium_size = cached_combined_belt_large_size = 0
for(var/obj/item/item in entities)
switch(item.belt_storage_class)
if(BELT_CLASS_SMALL)
cached_combined_belt_small_size += item.belt_storage_size
if(BELT_CLASS_MEDIUM)
cached_combined_belt_medium_size += item.belt_storage_size
if(BELT_CLASS_LARGE)
cached_combined_belt_large_size += item.belt_storage_size
/datum/object_system/storage/belt/can_be_inserted(obj/item/inserting, datum/event_args/actor/actor, silent)
if(inserting.belt_storage_class == BELT_CLASS_INVALID)
if(!silent)
actor?.chat_feedback(
SPAN_WARNING("[inserting] can't go in belts."),
target = inserting,
)
return FALSE
return ..()
/datum/object_system/storage/belt/proc/room_left_for_belt_class(belt_class, forbid_overrun)
if(forbid_overrun)
switch(belt_class)
if(BELT_CLASS_SMALL)
. = 1
if(BELT_CLASS_MEDIUM)
. = 2
if(BELT_CLASS_LARGE)
. = 3
return . ? compute_free_slots()[.] : 0
else
var/list/free_slots = compute_free_slots()
switch(belt_class)
if(BELT_CLASS_SMALL)
return free_slots[1] + free_slots[2] + free_slots[3]
if(BELT_CLASS_MEDIUM)
return free_slots[2] + free_slots[3]
if(BELT_CLASS_LARGE)
return free_slots[3]
return 0
/datum/object_system/storage/belt/why_failed_insertion_limits(obj/item/candidate)
if(!room_left_for_belt_class(candidate.belt_storage_class) >= candidate.belt_storage_size)
// incase list index out of bounds
. = "runtime errored"
return "insufficient belt loops of size [lowertext(global.belt_class_names[candidate.belt_storage_class])] or bigger"
return ..()
/datum/object_system/storage/belt/check_insertion_limits(obj/item/candidate)
return room_left_for_belt_class(candidate.belt_storage_class) >= candidate.belt_storage_size && ..()
/datum/object_system/storage/belt/physically_insert_item(obj/item/inserting, no_move, from_hook)
var/obj/item/actually_inserted = . = ..()
if(!istype(actually_inserted))
return
switch(actually_inserted.belt_storage_class)
if(BELT_CLASS_SMALL)
cached_combined_belt_small_size += actually_inserted.belt_storage_size
if(BELT_CLASS_MEDIUM)
cached_combined_belt_medium_size += actually_inserted.belt_storage_size
if(BELT_CLASS_LARGE)
cached_combined_belt_large_size += actually_inserted.belt_storage_size
/datum/object_system/storage/belt/physically_remove_item(obj/item/removing, atom/to_where, no_move, from_hook)
var/obj/item/actually_removed = . = ..()
if(!istype(actually_removed))
return
switch(actually_removed.belt_storage_class)
if(BELT_CLASS_SMALL)
cached_combined_belt_small_size -= actually_removed.belt_storage_size
if(BELT_CLASS_MEDIUM)
cached_combined_belt_medium_size -= actually_removed.belt_storage_size
if(BELT_CLASS_LARGE)
cached_combined_belt_large_size -= actually_removed.belt_storage_size
@@ -0,0 +1,5 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/object_system/storage/hypokit
ui_expand_volumetric_minimum_items = 14
@@ -0,0 +1,10 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/datum/object_system/storage/pill_bottle
max_combined_volume = WEIGHT_VOLUME_TINY * 14
max_single_weight_class = WEIGHT_CLASS_TINY
max_items = 14
ui_expand_volumetric_minimum_items = 14
allow_quick_empty = TRUE
allow_mass_gather = TRUE
@@ -19,10 +19,10 @@
/datum/object_system/storage/stack/uses_numerical_ui()
return TRUE
/datum/object_system/storage/stack/rebuild_caches()
. = ..()
/datum/object_system/storage/stack/rebuild_caches_impl(list/atom/movable/entities)
..()
cached_combined_stack_amount = 0
for(var/obj/item/stack/stack in real_contents_loc())
for(var/obj/item/stack/stack in entities)
cached_combined_stack_amount += stack.amount
/datum/object_system/storage/stack/why_failed_insertion_limits(obj/item/candidate)
@@ -72,7 +72,7 @@
other.add(stack.amount, TRUE)
// this should delete the stack (?!)
stack.use(stack.amount)
return TRUE
return null
inserting = stack
return ..()
@@ -0,0 +1,13 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* Default trash bag storage.
*/
/datum/object_system/storage/trash_bag
mass_operation_dumping_limit_per_second = 8
max_single_weight_class = WEIGHT_CLASS_NORMAL
limited_random_access_amount = 8
limited_random_access_total_weight_volume = WEIGHT_VOLUME_NORMAL * 3
// needed for LRA to work
ui_force_slot_mode = TRUE