Files
Bubberstation/code/datums/storage/storage.dm

1208 lines
40 KiB
Plaintext

/**
* Datumized Storage
* Eliminates the need for custom signals specifically for the storage component, and attaches a storage variable (atom_storage) to every atom.
* The parent and real_location variables are both weakrefs, so they must be resolved before they can be used.
* If you're looking to create custom storage type behaviors, check ../subtypes
*/
/datum/storage
/**
* A reference to the atom linked to this storage object
* If the parent goes, we go. Will never be null.
*/
VAR_FINAL/atom/parent
/**
* A reference to the atom where the items are actually stored.
* By default this is parent. Should generally never be null.
* Sometimes it's not the parent, that's what is called "dissassociated storage".
*
* Do NOT set this directly, use set_real_location.
*/
VAR_FINAL/atom/real_location
/// List of all the mobs currently viewing the contents of this storage.
VAR_PRIVATE/list/mob/is_using = list()
///The type of storage interface this datum uses.
var/datum/storage_interface/storage_type = /datum/storage_interface
/// Associated list that keeps track of all storage UI datums per person.
VAR_PRIVATE/list/datum/storage_interface/storage_interfaces = null
/// Typecache of items that can be inserted into this storage.
/// By default, all item types can be inserted (assuming other conditions are met).
/// Do not set directly, use set_holdable
VAR_FINAL/list/obj/item/can_hold
/// Typecache of items that cannot be inserted into this storage.
/// By default, no item types are barred from insertion.
/// Do not set directly, use set_holdable
VAR_FINAL/list/obj/item/cant_hold
/// Typecache of items that can always be inserted into this storage, regardless of size.
///Do not set directly, use set_holdable
VAR_FINAL/list/obj/item/exception_hold
/// For use with an exception typecache:
/// The maximum amount of items of the exception type that can be inserted into this storage.
var/exception_max = INFINITY
/// Determines whether we play a rustle animation when inserting/removing items.
var/animated = TRUE
/// Determines whether we play a rustle sound when inserting/removing items.
var/do_rustle = TRUE
var/rustle_vary = TRUE
/// Path for the item's rustle sound.
var/rustle_sound = SFX_RUSTLE
/// Path for the item's rustle sound when removing items.
var/remove_rustle_sound = null
/// The sound to play when we open/access the storage
var/open_sound
var/open_sound_vary = TRUE
/// The maximum amount of items that can be inserted into this storage.
var/max_slots = 7
/// The largest weight class that can be inserted into this storage, inclusive.
var/max_specific_storage = WEIGHT_CLASS_NORMAL
/// Determines the maximum amount of weight that can be inserted into this storage.
/// Weight is calculated by the sum of all of our content's weight classes.
var/max_total_storage = WEIGHT_CLASS_SMALL * 7
/// Whether the storage is currently locked (inaccessible). See [code/__DEFINES/storage.dm]
var/locked = STORAGE_NOT_LOCKED
/// Whether we open when attack_handed (clicked on with an empty hand).
var/attack_hand_interact = TRUE
/// Whether we allow storage objects of the same size inside.
var/allow_big_nesting = FALSE
/// If TRUE, we can click on items with the storage object to pick them up and insert them.
var/allow_quick_gather = FALSE
/// The mode for collection when allow_quick_gather is enabled. See [code/__DEFINES/storage.dm]
var/collection_mode = COLLECT_EVERYTHING
/// If TRUE, we can use-in-hand the storage object to dump all of its contents.
var/allow_quick_empty = FALSE
/// If we support smartly removing/inserting things from ourselves
var/supports_smart_equip = TRUE
///do we insert items when clicked by them?
var/insert_on_attack = TRUE
/// An additional description shown on double-examine.
/// Is autogenerated to the can_hold list if not set.
var/can_hold_description
/// The preposition used when inserting items into this storage.
/// IE: You put things *in* a bag, but *on* a plate.
var/insert_preposition = "in"
/// If TRUE, chat messages for inserting/removing items will not be shown.
var/silent = FALSE
/// Same as above but only for the user.
/// Useful to cut on chat spam without removing feedback for other players.
var/silent_for_user = FALSE
/// if TRUE, alt-click takes an item out instantly rather than opening up storage.
var/quickdraw = FALSE
/// Instead of displaying multiple items of the same type, display them as numbered contents.
var/numerical_stacking = FALSE
/// Maximum amount of columns a storage object can have
var/screen_max_columns = 7
/// Maximum amount of rows a storage object can have
var/screen_max_rows = INFINITY
/// X-pixel location of the boxes and close button
var/screen_pixel_x = 16
/// Y-pixel location of the boxes and close button
var/screen_pixel_y = 16
/// Where storage starts being rendered, x-screen_loc wise
var/screen_start_x = 4
/// Where storage starts being rendered, y-screen_loc wise
var/screen_start_y = 2
/// Ref to the item action that toggles collectmode.
VAR_PRIVATE/datum/action/item_action/storage_gather_mode/modeswitch_action
/// If TRUE, shows the contents of the storage in open_storage
var/display_contents = TRUE
/// Switch this off if you want to handle click_alt in the parent atom
var/click_alt_open = TRUE
/datum/storage/New(
atom/parent,
max_slots = src.max_slots,
max_specific_storage = src.max_specific_storage,
max_total_storage = src.max_total_storage,
rustle_sound = src.rustle_sound,
remove_rustle_sound = src.remove_rustle_sound,
)
if(!istype(parent))
stack_trace("Storage datum ([type]) created without a [isnull(parent) ? "null parent" : "invalid parent ([parent.type])"]!")
qdel(src)
return
set_parent(parent)
set_real_location(parent)
src.max_slots = max_slots
src.max_specific_storage = max_specific_storage
src.max_total_storage = max_total_storage
src.rustle_sound = rustle_sound
src.remove_rustle_sound = remove_rustle_sound
/datum/storage/Destroy()
for(var/mob/person as anything in is_using)
hide_contents(person)
is_using.Cut()
QDEL_LIST_ASSOC_VAL(storage_interfaces)
parent = null
real_location = null
return ..()
/datum/storage/proc/on_deconstruct()
SIGNAL_HANDLER
remove_all(update_storage = FALSE)
/// Ran on items instantiated inside the storage, basically a chopped down version of handle_enter
/datum/storage/proc/item_init(datum/source, obj/item/inited)
SIGNAL_HANDLER
if(!istype(inited))
return
inited.item_flags |= IN_STORAGE
RegisterSignal(inited, COMSIG_MOUSEDROPPED_ONTO, PROC_REF(mousedrop_receive))
/// Automatically ran on all object insertions: flag marking and view refreshing.
/datum/storage/proc/handle_enter(datum/source, obj/item/arrived)
SIGNAL_HANDLER
if(!istype(arrived))
return
arrived.item_flags |= IN_STORAGE
refresh_views()
arrived.on_enter_storage(src)
RegisterSignal(arrived, COMSIG_MOUSEDROPPED_ONTO, PROC_REF(mousedrop_receive))
SEND_SIGNAL(arrived, COMSIG_ITEM_STORED, src)
parent.update_appearance()
/// Automatically ran on all object removals: flag marking and view refreshing.
/datum/storage/proc/handle_exit(datum/source, obj/item/gone)
SIGNAL_HANDLER
if(!istype(gone))
return
gone.item_flags &= ~IN_STORAGE
remove_and_refresh(gone)
gone.on_exit_storage(src)
UnregisterSignal(gone, COMSIG_MOUSEDROPPED_ONTO)
SEND_SIGNAL(gone, COMSIG_ITEM_UNSTORED, src)
parent.update_appearance()
/// Set the passed atom as the parent
/datum/storage/proc/set_parent(atom/new_parent)
PROTECTED_PROC(TRUE)
ASSERT(isnull(parent))
parent = new_parent
ADD_TRAIT(parent, TRAIT_COMBAT_MODE_SKIP_INTERACTION, REF(src))
// a few of theses should probably be on the real_location rather than the parent
RegisterSignals(parent, list(COMSIG_ATOM_ATTACK_PAW, COMSIG_ATOM_ATTACK_HAND), PROC_REF(on_attack))
RegisterSignal(parent, COMSIG_MOUSEDROP_ONTO, PROC_REF(on_mousedrop_onto))
RegisterSignal(parent, COMSIG_MOUSEDROPPED_ONTO, PROC_REF(on_mousedropped_onto))
RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(on_preattack))
RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(mass_empty))
RegisterSignals(parent, list(COMSIG_ATOM_ATTACK_GHOST, COMSIG_ATOM_ATTACK_HAND_SECONDARY), PROC_REF(open_storage_on_signal))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(close_distance))
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(update_actions))
RegisterSignal(parent, COMSIG_TOPIC, PROC_REF(topic_handle))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(handle_examination))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(handle_extra_examination))
RegisterSignal(parent, COMSIG_OBJ_DECONSTRUCT, PROC_REF(on_deconstruct))
RegisterSignal(parent, COMSIG_ATOM_EMP_ACT, PROC_REF(on_emp_act))
RegisterSignal(parent, COMSIG_ATOM_CONTENTS_WEIGHT_CLASS_CHANGED, PROC_REF(contents_changed_w_class))
RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(on_click_alt))
RegisterSignal(parent, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, PROC_REF(item_init))
/**
* Sets where items are physically being stored in the case it shouldn't be on the parent.
*
* Does not handle moving any existing items, that must be done manually.
*
* Arguments
* * atom/new_real_location - the new real location of the datum
* * should_drop - if TRUE, all the items in the old real location will be dropped.
*/
/datum/storage/proc/set_real_location(atom/new_real_location, should_drop = FALSE)
if(!isnull(real_location))
UnregisterSignal(real_location, list(
COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXITED,
COMSIG_QDELETING,
COMSIG_ATOM_EMP_ACT,
))
real_location.flags_1 &= ~HAS_DISASSOCIATED_STORAGE_1
if(should_drop)
remove_all()
if(isnull(new_real_location))
return
real_location = new_real_location
if(real_location != parent)
real_location.flags_1 |= HAS_DISASSOCIATED_STORAGE_1
RegisterSignal(real_location, COMSIG_ATOM_ENTERED, PROC_REF(handle_enter))
RegisterSignal(real_location, COMSIG_ATOM_EXITED, PROC_REF(handle_exit))
RegisterSignal(real_location, COMSIG_QDELETING, PROC_REF(real_location_deleted))
/// Signal handler for when the real location is deleted.
/datum/storage/proc/real_location_deleted(datum/deleting_real_location)
SIGNAL_HANDLER
set_real_location(null)
/datum/storage/proc/topic_handle(datum/source, user, href_list)
SIGNAL_HANDLER
if(isnull(can_hold_description))
return
if(href_list["show_valid_pocket_items"])
to_chat(user, span_notice("[source] can hold: [can_hold_description]"))
/datum/storage/proc/handle_examination(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(isnull(can_hold_description))
return
examine_list += span_notice("You can examine this further to check what kind of extra items it can hold.")
/datum/storage/proc/handle_extra_examination(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(isnull(can_hold_description))
return
examine_list += span_notice("[source] can hold: [can_hold_description]")
/// Almost 100% of the time the lists passed into set_holdable are reused for each instance
/// Just fucking cache it 4head
/// Yes I could generalize this, but I don't want anyone else using it. in fact, DO NOT COPY THIS
/// If you find yourself needing this pattern, you're likely better off using static typecaches
/// I'm not because I do not trust implementers of the storage component to use them, BUT
/// IF I FIND YOU USING THIS PATTERN IN YOUR CODE I WILL BREAK YOU ACROSS MY KNEES
/// ~Lemon
GLOBAL_LIST_EMPTY(cached_storage_typecaches)
/**
* Sets what type of contents this storage supports
* Arguments
*
* * list/can_hold_list - The list of item types whitelisted in this storage rejecting everything else
* * list/cant_hold_list - The list of item types blacklisted in this storage accepting everything else
* * list/exception_hold_list - The list of items that can exceed `max_specific_storage`. It can only fit `exception_count` of such items
*/
/datum/storage/proc/set_holdable(list/can_hold_list, list/cant_hold_list, list/exception_hold_list)
can_hold = null
if (!isnull(can_hold_list))
if(!islist(can_hold_list))
can_hold_list = list(can_hold_list)
var/unique_key = can_hold_list.Join("-")
if(!GLOB.cached_storage_typecaches[unique_key])
GLOB.cached_storage_typecaches[unique_key] = typecacheof(can_hold_list)
can_hold = GLOB.cached_storage_typecaches[unique_key]
cant_hold = null
if (!isnull(cant_hold_list))
if(!islist(cant_hold_list))
cant_hold_list = list(cant_hold_list)
var/unique_key = cant_hold_list.Join("-")
if(!GLOB.cached_storage_typecaches[unique_key])
GLOB.cached_storage_typecaches[unique_key] = typecacheof(cant_hold_list)
cant_hold = GLOB.cached_storage_typecaches[unique_key]
exception_hold = null
if (!isnull(exception_hold_list))
if(!islist(exception_hold_list))
exception_hold_list = list(exception_hold_list)
var/unique_key = exception_hold_list.Join("-")
if(!GLOB.cached_storage_typecaches[unique_key])
GLOB.cached_storage_typecaches[unique_key] = typecacheof(exception_hold_list)
exception_hold = GLOB.cached_storage_typecaches[unique_key]
can_hold_description = null
if(length(can_hold_list))
var/list/desc = list()
for(var/obj/item/valid_item as anything in can_hold_list)
desc += "\a [initial(valid_item.name)]"
can_hold_description = "\n\t[span_notice("[desc.Join("\n\t")]")]"
/// Updates the action button for toggling collectmode.
/datum/storage/proc/update_actions(atom/source, mob/equipper, slot)
SIGNAL_HANDLER
if(!allow_quick_gather)
QDEL_NULL(modeswitch_action)
return
if(!isnull(modeswitch_action))
return
if(!isitem(parent))
return
var/obj/item/item_parent = parent
modeswitch_action = item_parent.add_item_action(/datum/action/item_action/storage_gather_mode)
RegisterSignal(modeswitch_action, COMSIG_ACTION_TRIGGER, PROC_REF(action_trigger))
RegisterSignal(modeswitch_action, COMSIG_QDELETING, PROC_REF(action_deleted))
/// Refreshes and item to be put back into the real world, out of storage.
/datum/storage/proc/reset_item(obj/item/thing)
thing.layer = initial(thing.layer)
SET_PLANE_IMPLICIT(thing, initial(thing.plane))
thing.mouse_opacity = initial(thing.mouse_opacity)
thing.screen_loc = null
if(thing.maptext)
thing.maptext = ""
/**
* Checks if an item is capable of being inserted into the storage.
*
* Arguments
* * obj/item/to_insert - the item we're checking
* * messages - if TRUE, will print out a message if the item is not valid
* * force - bypass locked storage up to a certain level. See [code/__DEFINES/storage.dm]
*/
/datum/storage/proc/can_insert(obj/item/to_insert, mob/user, messages = TRUE, force = STORAGE_NOT_LOCKED)
if(QDELETED(to_insert) || !istype(to_insert))
return FALSE
//stops you from putting stuff like off-hand thingy inside. Hologram storages can accept only hologram items
if(to_insert.item_flags & ABSTRACT)
return FALSE
if(parent.flags_1 & HOLOGRAM_1)
if(!(to_insert.flags_1 & HOLOGRAM_1))
return FALSE
else if(to_insert.flags_1 & HOLOGRAM_1)
return FALSE
if(locked > force)
if(messages && user)
user.balloon_alert(user, "closed!")
return FALSE
if((to_insert == parent) || (to_insert == real_location))
return FALSE
if(to_insert.w_class > max_specific_storage)
if(!is_type_in_typecache(to_insert, exception_hold))
if(messages && user)
user.balloon_alert(user, "too big!")
return FALSE
if(exception_max <= get_exception_count())
if(messages && user)
user.balloon_alert(user, "no room!")
return FALSE
if(real_location.contents.len >= max_slots)
if(messages && user && !silent_for_user)
user.balloon_alert(user, "no room!")
return FALSE
if(to_insert.w_class + get_total_weight() > max_total_storage)
if(messages && user && !silent_for_user)
user.balloon_alert(user, "no room!")
return FALSE
var/can_hold_it = isnull(can_hold) || is_type_in_typecache(to_insert, can_hold) || is_type_in_typecache(to_insert, exception_hold)
var/cant_hold_it = is_type_in_typecache(to_insert, cant_hold)
var/trait_says_no = HAS_TRAIT(to_insert, TRAIT_NO_STORAGE_INSERT)
if(!can_hold_it || cant_hold_it || trait_says_no)
if(messages && user)
user.balloon_alert(user, "can't hold!")
return FALSE
if(HAS_TRAIT(to_insert, TRAIT_NODROP))
if(messages && user)
user.balloon_alert(user, "stuck on your hand!")
return FALSE
// this is valid if the container our location is being held in is a storage item
var/datum/storage/bigger_fish = parent.loc.atom_storage
if(bigger_fish && bigger_fish.max_specific_storage < max_specific_storage)
if(messages && user)
user.balloon_alert(user, "[LOWER_TEXT(parent.loc.name)] is in the way!")
return FALSE
if(isitem(parent))
var/obj/item/item_parent = parent
var/datum/storage/smaller_fish = to_insert.atom_storage
if(smaller_fish && !allow_big_nesting && to_insert.w_class >= item_parent.w_class)
if(messages && user)
user.balloon_alert(user, "too big!")
return FALSE
return TRUE
/// Returns a count of how many items held due to exception_hold we have
/datum/storage/proc/get_exception_count()
var/count = 0
for(var/obj/item/thing in real_location)
if(thing.w_class > max_specific_storage && is_type_in_typecache(thing, exception_hold))
count += 1
return count
/// Returns a sum of all of our content's weight classes
/datum/storage/proc/get_total_weight()
var/total_weight = 0
for(var/obj/item/thing in real_location)
total_weight += thing.w_class
return total_weight
/**
* Attempts to insert an item into the storage
*
* Arguments
* * obj/item/to_insert - the item we're inserting
* * mob/user - (optional) the user who is inserting the item.
* * override - skip feedback, only do the animation
* * force - bypass locked storage up to a certain level. See [code/__DEFINES/storage.dm]
* * messages - if TRUE, we will create balloon alerts for the user.
*/
/datum/storage/proc/attempt_insert(obj/item/to_insert, mob/user, override = FALSE, force = STORAGE_NOT_LOCKED, messages = TRUE)
SHOULD_NOT_SLEEP(TRUE)
if(!can_insert(to_insert, user, messages = messages, force = force))
return FALSE
if(SEND_SIGNAL(parent, COMSIG_ATOM_PRE_STORED_ITEM, to_insert, user, force, messages) & BLOCK_STORAGE_INSERT)
return FALSE
SEND_SIGNAL(parent, COMSIG_ATOM_STORED_ITEM, to_insert, user, force)
SEND_SIGNAL(src, COMSIG_STORAGE_STORED_ITEM, to_insert, user, force)
to_insert.forceMove(real_location)
item_insertion_feedback(user, to_insert, override)
parent.update_appearance()
if(get(real_location, /mob) != user)
to_insert.do_pickup_animation(real_location, user)
return TRUE
/// Since items inside storages ignore transparency for QOL reasons, we're tracking when things are dropped onto them instead of our UI elements
/datum/storage/proc/mousedrop_receive(atom/dropped_onto, atom/movable/target, mob/user, params)
SIGNAL_HANDLER
if (src != user.active_storage)
return
if (!user.can_perform_action(parent, FORBID_TELEKINESIS_REACH))
return
if (target.loc != real_location) // what even
return
if(numerical_stacking)
return
var/drop_index = real_location.contents.Find(dropped_onto)
real_location.contents -= target
// Use an empty list if we're dropping onto the last item
var/list/to_move = real_location.contents.len >= drop_index ? real_location.contents.Copy(drop_index) : list()
real_location.contents -= to_move
real_location.contents += target
real_location.contents += to_move
refresh_views()
/**
* Inserts every item in a given list, with a progress bar
*
* Arguments
* * mob/user - the user who is inserting the items
* * list/things - the list of items to insert
* * atom/thing_loc - the location of the items (used to make sure an item hasn't moved during pickup)
* * list/rejections - a list used to make sure we only complain once about an invalid insertion
* * datum/progressbar/progress - the progressbar used to show the progress of the insertion
*/
/datum/storage/proc/handle_mass_pickup(mob/user, list/things, atom/thing_loc, list/rejections, datum/progressbar/progress)
for(var/obj/item/thing in things)
things -= thing
if(thing.loc != thing_loc)
continue
if(thing.type in rejections) // To limit bag spamming: any given type only complains once
continue
if(!attempt_insert(thing, user, override = TRUE)) // Note can_be_inserted still makes noise when the answer is no
if(real_location.contents.len >= max_slots)
break
rejections += thing.type // therefore full bags are still a little spammy
continue
if (TICK_CHECK)
progress.update(progress.goal - things.len)
return TRUE
progress.update(progress.goal - things.len)
return FALSE
/**
* Provides visual feedback in chat for an item insertion
*
* Arguments
* * mob/user - the user who is inserting the item
* * obj/item/thing - the item we're inserting
* * override - skip feedback, only do animation check
*/
/datum/storage/proc/item_insertion_feedback(mob/user, obj/item/thing, override = FALSE)
if(animated)
animate_parent()
if(override)
return
if(silent)
return
if(do_rustle && rustle_sound)
playsound(parent, rustle_sound, 50, rustle_vary, -5)
if(!silent_for_user)
to_chat(user, span_notice("You put [thing] [insert_preposition]to [parent]."))
for(var/mob/viewing in oviewers(user))
if(in_range(user, viewing) || (thing?.w_class >= WEIGHT_CLASS_NORMAL))
viewing.show_message(span_notice("[user] puts [thing] [insert_preposition]to [parent]."), MSG_VISUAL)
/**
* Attempts to remove an item from the storage
* Ignores removal do_afters. Only use this if you're doing it as part of a dumping action
*
* Arguments
* * obj/item/thing - the object we're removing
* * atom/remove_to_loc - where we're placing the item
* * silent - if TRUE, we won't play any exit sounds
* * visual_updates - if TRUE we update storage views & animate parent appearance
*/
/datum/storage/proc/attempt_remove(obj/item/thing, atom/remove_to_loc, silent = FALSE, visual_updates = TRUE)
SHOULD_NOT_SLEEP(TRUE)
if(istype(thing) && ismob(parent.loc))
var/mob/mob_parent = parent.loc
thing.dropped(mob_parent, /*silent = */TRUE)
if(remove_to_loc)
reset_item(thing)
thing.forceMove(remove_to_loc)
if(!silent && do_rustle)
if(remove_rustle_sound)
playsound(parent, remove_rustle_sound, 50, TRUE, -5)
else if(rustle_sound)
playsound(parent, rustle_sound, 50, TRUE, -5)
else
thing.moveToNullspace()
if(visual_updates)
if(animated)
animate_parent()
refresh_views()
parent.update_appearance()
SEND_SIGNAL(parent, COMSIG_ATOM_REMOVED_ITEM, thing, remove_to_loc, silent)
SEND_SIGNAL(src, COMSIG_STORAGE_REMOVED_ITEM, thing, remove_to_loc, silent)
return TRUE
/**
* Removes everything inside of our storage
*
* Arguments
* * atom/drop_loc - where we're placing the item
* * update_storage - should we update the parent to show visual effects
*/
/datum/storage/proc/remove_all(atom/drop_loc = parent.drop_location(), update_storage = TRUE)
for(var/obj/item/thing in real_location)
if(!attempt_remove(thing, drop_loc, silent = TRUE, visual_updates = update_storage))
continue
thing.pixel_x = thing.base_pixel_x + rand(-8, 8)
thing.pixel_y = thing.base_pixel_y + rand(-8, 8)
/**
* Allows a mob to attempt to remove a single item from the storage
* Allows for hooks into things like removal delays
*
* Arguments
* * mob/removing - the mob doing the removing
* * obj/item/thing - the object we're removing
* * atom/remove_to_loc - where we're placing the item
* * silent - if TRUE, we won't play any exit sounds
*/
/datum/storage/proc/remove_single(mob/removing, obj/item/thing, atom/remove_to_loc, silent = FALSE)
return attempt_remove(thing, remove_to_loc, silent)
/**
* Removes only a specific type of item from our storage
*
* Arguments
* * type - the type of item to remove
* * amount - how many we should attempt to pick up at one time
* * check_adjacent - if TRUE, we'll check adjacent locations for the item type
* * force - if TRUE, we'll bypass the check_adjacent check all together
* * mob/user - the user who is removing the items
* * list/inserted - (optional) allows consumers to pass a list to be filled with all removed items.
*/
/datum/storage/proc/remove_type(type, atom/destination, amount = INFINITY, check_adjacent = FALSE, force = FALSE, mob/user, list/inserted)
if(!force && check_adjacent)
if(isnull(user) || !destination.IsReachableBy(user) || !parent.IsReachableBy(user))
return FALSE
var/list/taking = typecache_filter_list(real_location.contents, typecacheof(type))
if(taking.len > amount)
taking.len = amount
if(inserted) //duplicated code for performance, don't bother checking retval/checking for list every item.
for(var/i in taking)
if(attempt_remove(i, destination))
inserted |= i
else
for(var/i in taking)
attempt_remove(i, destination)
return TRUE
/// Signal handler for remove_all()
/datum/storage/proc/mass_empty(datum/source, mob/user)
SIGNAL_HANDLER
if(!allow_quick_empty)
return
remove_all(user.drop_location())
/**
* Recursive proc to get absolutely EVERYTHING inside a storage item, including the contents of inner items.
*
* Arguments
* * recursive - whether or not we're checking inside of inner items
*/
/datum/storage/proc/return_inv(recursive = TRUE)
var/list/ret = list()
for(var/atom/found_thing as anything in real_location)
ret |= found_thing
if(recursive && found_thing.atom_storage)
ret |= found_thing.atom_storage.return_inv(recursive = TRUE)
return ret
/**
* Resets an object, removes it from our screen, and refreshes the view.
*
* @param atom/movable/gone the object leaving our storage
*/
/datum/storage/proc/remove_and_refresh(atom/movable/gone)
SIGNAL_HANDLER
for(var/mob/user as anything in is_using)
user.hud_used?.open_containers -= gone
if(!user.client)
continue
var/client/cuser = user.client
cuser.screen -= gone
reset_item(gone)
refresh_views()
/// Signal handler for emp_act to emp all contents
/datum/storage/proc/on_emp_act(datum/source, severity, protection)
SIGNAL_HANDLER
if(protection & EMP_PROTECT_CONTENTS)
return
for(var/obj/item/thing in real_location)
thing.emp_act(severity)
/// Signal handler for preattack from an object.
/datum/storage/proc/on_preattack(datum/source, obj/item/thing, mob/user, list/modifiers)
SIGNAL_HANDLER
if(!istype(thing) || thing == parent.loc || !allow_quick_gather || thing.atom_storage)
return
if(collection_mode == COLLECT_ONE)
if(thing.loc == user)
user.dropItemToGround(thing, silent = TRUE) //this is nessassary to update any inventory slot it is attached to
attempt_insert(thing, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
if(!isturf(thing.loc))
return COMPONENT_CANCEL_ATTACK_CHAIN
INVOKE_ASYNC(src, PROC_REF(collect_on_turf), thing, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/**
* Collects every item of a type on a turf.
*
* @param obj/item/thing the initial object to pick up
* @param mob/user the user who is picking up the items
*/
/datum/storage/proc/collect_on_turf(obj/item/thing, mob/user)
var/atom/holder = thing.loc
var/list/pick_up = holder.contents.Copy()
if(collection_mode == COLLECT_SAME)
pick_up = typecache_filter_list(pick_up, typecacheof(thing.type))
var/amount = length(pick_up)
if(!amount)
parent.balloon_alert(user, "nothing to pick up!")
return
var/datum/progressbar/progress = new(user, amount, thing.loc)
var/list/rejections = list()
while(do_after(user, 1 SECONDS, parent, NONE, FALSE, CALLBACK(src, PROC_REF(handle_mass_pickup), user, pick_up.Copy(), thing.loc, rejections, progress)))
stoplag(1)
progress.end_progress()
// If nothing was actually removed, don't send the pickup message
var/list/current_contents = holder.contents.Copy()
if(length(pick_up | current_contents) == length(current_contents))
return
parent.balloon_alert(user, "picked up")
/// Signal handler for whenever we drag the storage somewhere.
/datum/storage/proc/on_mousedrop_onto(datum/source, atom/over_object, mob/user)
SIGNAL_HANDLER
if(ismecha(user.loc) || user.incapacitated || !user.canUseStorage())
return NONE
if(istype(over_object, /atom/movable/screen/inventory/hand))
if(parent.loc != user || !user.can_perform_action(parent, FORBID_TELEKINESIS_REACH | ALLOW_RESTING))
return NONE
if(isitem(parent))
var/obj/item/item_parent = parent
if(!item_parent.can_mob_unequip(user))
return COMPONENT_CANCEL_MOUSEDROP_ONTO
var/atom/movable/screen/inventory/hand/hand = over_object
user.putItemFromInventoryInHandIfPossible(parent, hand.held_index)
parent.add_fingerprint(user)
return COMPONENT_CANCEL_MOUSEDROP_ONTO
if(over_object == user)
if(!user.can_perform_action(parent, FORBID_TELEKINESIS_REACH | ALLOW_RESTING))
return NONE
if(isliving(parent) && user.pulling == parent)
var/mob/living/as_living = parent
if(as_living.can_be_held)
return
parent.add_fingerprint(user)
INVOKE_ASYNC(src, PROC_REF(open_storage), user)
return COMPONENT_CANCEL_MOUSEDROP_ONTO
if(istype(over_object, /atom/movable/screen) || ismob(over_object))
return NONE
if(!user.can_perform_action(over_object, FORBID_TELEKINESIS_REACH))
return NONE
parent.add_fingerprint(user)
var/atom/dump_loc = over_object.get_dumping_location()
if(isnull(dump_loc))
return NONE
/// Don't dump *onto* objects in the same storage as ourselves
if (over_object.loc == parent.loc && !isnull(parent.loc.atom_storage) && isnull(over_object.atom_storage))
return NONE
INVOKE_ASYNC(src, PROC_REF(dump_content_at), over_object, dump_loc, user)
return COMPONENT_CANCEL_MOUSEDROP_ONTO
/**
* Dumps all of our contents at a specific location.
*
* @param atom/dest_object where to dump to
* @param mob/user the user who is dumping the contents
*/
/datum/storage/proc/dump_content_at(atom/dest_object, dump_loc, mob/user)
if(locked)
user.balloon_alert(user, "closed!")
return
if(!parent.IsReachableBy(user) || !dest_object.IsReachableBy(user))
return
if(SEND_SIGNAL(dest_object, COMSIG_STORAGE_DUMP_CONTENT, src, user) & STORAGE_DUMP_HANDLED)
return
// Storage to storage transfer is instant
if(dest_object.atom_storage)
to_chat(user, span_notice("You dump the contents of [parent] into [dest_object]."))
if(do_rustle && rustle_sound)
playsound(parent, rustle_sound, 50, TRUE, -5)
for(var/obj/item/to_dump in real_location)
dest_object.atom_storage.attempt_insert(to_dump, user)
parent.update_appearance()
SEND_SIGNAL(src, COMSIG_STORAGE_DUMP_POST_TRANSFER, dest_object, user)
return
// Storage to loc transfer requires a do_after
to_chat(user, span_notice("You start dumping out the contents of [parent] onto [dest_object]..."))
if(!do_after(user, 2 SECONDS, target = dest_object))
return
remove_all(dump_loc)
/// Signal handler for whenever something gets mouse-dropped onto us.
/datum/storage/proc/on_mousedropped_onto(datum/source, obj/item/dropping, mob/user)
SIGNAL_HANDLER
if(!istype(dropping))
return
if(dropping != user.get_active_held_item())
return
if(!user.can_perform_action(source, FORBID_TELEKINESIS_REACH))
return
if(dropping.atom_storage) // If it has storage it should be trying to dump, not insert.
return
if(!iscarbon(user) && !isdrone(user))
return
attempt_insert(dropping, user)
return COMPONENT_CANCEL_MOUSEDROPPED_ONTO
/// Called directly from the attack chain if [insert_on_attack] is TRUE.
/// Handles inserting an item into the storage when clicked.
/datum/storage/proc/item_interact_insert(mob/living/user, obj/item/thing)
attempt_insert(thing, user)
return ITEM_INTERACT_SUCCESS
/// Signal handler for whenever we're attacked by a mob.
/datum/storage/proc/on_attack(datum/source, mob/user)
SIGNAL_HANDLER
if(!attack_hand_interact)
return
if(user.active_storage == src && parent.loc == user)
user.active_storage.hide_contents(user)
hide_contents(user)
return COMPONENT_CANCEL_ATTACK_CHAIN
if(ishuman(user))
var/mob/living/carbon/human/hum = user
if(hum.l_store == parent || hum.r_store == parent)
return
if(parent.loc == user)
INVOKE_ASYNC(src, PROC_REF(open_storage), user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/// Generates the numbers on an item in storage to show stacking.
/datum/storage/proc/process_numerical_display()
var/list/toreturn = list()
for(var/obj/item/thing in real_location)
var/total_amnt = 1
if(isstack(thing))
var/obj/item/stack/things = thing
total_amnt = things.amount
if(!toreturn["[thing.type]-[thing.name]"])
toreturn["[thing.type]-[thing.name]"] = new /datum/numbered_display(thing, total_amnt)
else
var/datum/numbered_display/numberdisplay = toreturn["[thing.type]-[thing.name]"]
numberdisplay.number += total_amnt
return toreturn
/// Signal handler to open up the storage when we receive a signal.
/datum/storage/proc/open_storage_on_signal(datum/source, mob/to_show)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(open_storage), to_show)
if(display_contents)
return COMPONENT_NO_AFTERATTACK
/// Alt click on the storage item. Default: Open the storage.
/datum/storage/proc/on_click_alt(datum/source, mob/user)
SIGNAL_HANDLER
if(!click_alt_open)
return
return open_storage_on_signal(source, user) ? CLICK_ACTION_SUCCESS : NONE
/// Opens the storage to the mob, showing them the contents to their UI.
/datum/storage/proc/open_storage(mob/living/to_show)
if(isobserver(to_show))
show_contents(to_show)
return FALSE
if(!isliving(to_show) || !to_show.can_perform_action(parent, ALLOW_RESTING | FORBID_TELEKINESIS_REACH))
return FALSE
//because of the check above, it's safe to assume we're living now.
if(!(to_show.mobility_flags & MOBILITY_STORAGE))
return FALSE
if(locked)
if(!silent)
parent.balloon_alert(to_show, "closed!")
return FALSE
// If we're quickdrawing boys
if(quickdraw && !to_show.get_active_held_item())
var/obj/item/to_remove = locate() in real_location
if(!to_remove)
return TRUE
if (remove_single(to_show, to_remove))
INVOKE_ASYNC(src, PROC_REF(put_in_hands_async), to_show, to_remove)
if(!silent)
to_show.visible_message(
span_warning("[to_show] draws [to_remove] from [parent]!"),
span_notice("You draw [to_remove] from [parent]."),
)
return TRUE
// If nothing else, then we want to open the thing, so do that
if(!show_contents(to_show))
return FALSE
if(animated)
animate_parent()
if(do_rustle && !silent)
playsound(parent, (open_sound ? open_sound : SFX_RUSTLE), 50, open_sound_vary, -5)
return TRUE
/// Async version of putting something into a mobs hand.
/datum/storage/proc/put_in_hands_async(mob/to_show, obj/item/toremove)
if(!to_show.put_in_hands(toremove))
if(!silent)
toremove.balloon_alert(to_show, "fumbled!")
return TRUE
/// Signal handler for whenever a mob walks away with us, close if they can't reach us.
/datum/storage/proc/close_distance(datum/source)
SIGNAL_HANDLER
for(var/mob/user in can_see_contents())
if (!can_be_reached_by(user))
hide_contents(user)
/// Relay for parent.IsReachableBy
/datum/storage/proc/can_be_reached_by(mob/user)
return parent.IsReachableBy(user)
/// Close the storage UI for everyone viewing us.
/datum/storage/proc/close_all()
for(var/mob/user as anything in is_using)
hide_contents(user)
/// Closes the storage UIs of this and everything inside the parent for everyone viewing them.
/datum/storage/proc/close_all_recursive()
close_all()
for(var/atom/movable/movable as anything in parent.get_all_contents())
movable.atom_storage?.close_all()
/// Refresh the views of everyone currently viewing the storage.
/datum/storage/proc/refresh_views()
for (var/mob/user in can_see_contents())
show_contents(user)
/// Checks who is currently capable of viewing our storage (and is.)
/datum/storage/proc/can_see_contents()
var/list/seeing = list()
for (var/mob/user in is_using)
if(user.active_storage == src && user.client)
seeing += user
else
hide_contents(user)
return seeing
/**
* Show our storage to a mob.
*
* Arguments
* * mob/to_show - the mob to show the storage to
*
* Returns
* * FALSE if the show failed
* * TRUE otherwise
*/
/datum/storage/proc/show_contents(mob/to_show)
if(!to_show.client)
return FALSE
// You can only inspect hidden contents if you're an observer
if(!isobserver(to_show) && !display_contents)
return FALSE
if(to_show.active_storage != src && (to_show.stat == CONSCIOUS))
for(var/obj/item/thing in real_location)
if(thing.on_found(to_show))
to_show.active_storage.hide_contents(to_show)
if(to_show.active_storage)
to_show.active_storage.hide_contents(to_show)
to_show.active_storage = src
if(ismovable(real_location))
var/atom/movable/movable_loc = real_location
movable_loc.become_active_storage(src)
LAZYINITLIST(storage_interfaces)
var/ui_style = ui_style2icon(to_show.client?.prefs?.read_preference(/datum/preference/choiced/ui_style))
if (isnull(storage_interfaces[to_show]))
storage_interfaces[to_show] = new storage_type(ui_style, src, to_show)
orient_storage()
is_using |= to_show
to_show.hud_used.open_containers |= storage_interfaces[to_show].list_ui_elements()
to_show.client.screen |= storage_interfaces[to_show].list_ui_elements()
to_show.hud_used.open_containers |= real_location.contents
to_show.client.screen |= real_location.contents
return TRUE
/**
* Hide our storage from a mob.
*
* Arguments
* * mob/to_hide - the mob to hide the storage from
*/
/datum/storage/proc/hide_contents(mob/to_hide)
if(to_hide.active_storage == src)
to_hide.active_storage = null
if(!length(is_using) && ismovable(real_location))
var/atom/movable/movable_loc = real_location
movable_loc.lose_active_storage(src)
if (!length(storage_interfaces) || isnull(storage_interfaces[to_hide]))
return TRUE
is_using -= to_hide
if(to_hide.client)
to_hide.client.screen -= storage_interfaces[to_hide].list_ui_elements()
to_hide.client.screen -= real_location.contents
if(to_hide.hud_used)
to_hide.hud_used.open_containers -= storage_interfaces[to_hide].list_ui_elements()
to_hide.hud_used.open_containers -= real_location.contents
QDEL_NULL(storage_interfaces[to_hide])
storage_interfaces -= to_hide
return TRUE
/datum/storage/proc/action_trigger(datum/source, datum/action/triggered)
SIGNAL_HANDLER
toggle_collection_mode(triggered.owner)
return COMPONENT_ACTION_BLOCK_TRIGGER
/datum/storage/proc/action_deleted(datum/source)
SIGNAL_HANDLER
modeswitch_action = null
/// Updates views of all objects in storage and stretches UI to appropriate size
/datum/storage/proc/orient_storage()
var/adjusted_contents = length(real_location.contents)
var/list/datum/numbered_display/numbered_contents
if(numerical_stacking)
numbered_contents = process_numerical_display()
adjusted_contents = length(numbered_contents)
//if the ammount of contents reaches some multiplier of the final column (and its not the last slot), let the player view an additional row
var/additional_row = (!(adjusted_contents % screen_max_columns) && adjusted_contents < max_slots)
var/columns = clamp(max_slots, 1, screen_max_columns)
var/rows = clamp(CEILING(adjusted_contents / columns, 1) + additional_row, 1, screen_max_rows)
for (var/mob/ui_user as anything in storage_interfaces)
if (isnull(storage_interfaces[ui_user]))
continue
storage_interfaces[ui_user].update_position(
screen_start_x,
screen_pixel_x,
screen_start_y,
screen_pixel_y,
columns,
rows,
ui_user,
real_location,
numbered_contents,
)
/**
* Toggles the collectmode of our storage.
*
* @param mob/to_show the mob toggling us
*/
/datum/storage/proc/toggle_collection_mode(mob/user)
collection_mode = (collection_mode + 1) % 3
switch(collection_mode)
if(COLLECT_SAME)
parent.balloon_alert(user, "will now only pick up a single type")
if(COLLECT_EVERYTHING)
parent.balloon_alert(user, "will now pick up everything")
if(COLLECT_ONE)
parent.balloon_alert(user, "will now pick up one at a time")
/// Gives a spiffy animation to our parent to represent opening and closing.
/datum/storage/proc/animate_parent()
var/matrix/old_matrix = parent.transform
animate(parent, time = 1.5, loop = 0, transform = parent.transform.Scale(1.07, 0.9))
animate(time = 2, transform = old_matrix)
/// Signal proc for [COMSIG_ATOM_CONTENTS_WEIGHT_CLASS_CHANGED] to drop items out of our storage if they're suddenly too heavy.
/datum/storage/proc/contents_changed_w_class(datum/source, obj/item/changed, old_w_class, new_w_class)
SIGNAL_HANDLER
// If old weight already overloaded the storage, don't drop the item out just in case we're inside of a premade box
if(new_w_class <= max_specific_storage && (get_total_weight() <= max_total_storage || get_total_weight() - new_w_class + old_w_class > max_total_storage))
return
if(!attempt_remove(changed, parent.drop_location()))
return
changed.visible_message(span_warning("[changed] falls out of [parent]!"), vision_distance = COMBAT_MESSAGE_RANGE)
///Assign a new value to the locked variable. If it's higher than NOT_LOCKED, close the UIs and update the appearance of the parent.
/datum/storage/proc/set_locked(new_locked)
if(locked == new_locked)
return
locked = new_locked
if(new_locked > STORAGE_NOT_LOCKED)
close_all_recursive()
parent.update_appearance()