Files
Bubberstation/code/datums/storage/storage.dm
SkyratBot 561c328f3d [MIRROR] Fixes belts not updating their overlays when transferring their contents to another storage item. [MDB IGNORE] (#19195)
* Fixes belts not updating their overlays when transferring their contents to another storage item. (#73251)

## About The Pull Request

When you drag and drop a belt to another storage item it transfers all
its contents but retains its overlays. This PR calls update appearance
at the end of storage to storage transfers so this shouldn't occur
again.
## Why It's Good For The Game

Bug fix, belts appearing to have things on them which they don't is bad.
## Changelog
🆑
fix: Transferring objects from a belt to another storage object now
removes those objects from the belts visuals.
/🆑

* Fixes belts not updating their overlays when transferring their contents to another storage item.

---------

Co-authored-by: NamelessFairy <40036527+NamelessFairy@users.noreply.github.com>
2023-02-08 02:10:48 +00:00

1114 lines
34 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
/// the actual item we're attached to
var/datum/weakref/parent
/// the actual item we're storing in
var/datum/weakref/real_location
/// if this is set, only items, and their children, will fit
var/list/can_hold
/// if this is set, items, and their children, won't fit
var/list/cant_hold
/// if set, these items will be the exception to the max size of object that can fit.
var/list/exception_hold
/// if set can only contain stuff with this single trait present.
var/list/can_hold_trait
/// whether or not we should have those cute little animations
var/animated = TRUE
var/max_slots = 7
/// max weight class for a single item being inserted
var/max_specific_storage = WEIGHT_CLASS_NORMAL
/// max combined weight classes the storage can hold
var/max_total_storage = 14
/// list of all the mobs currently viewing the contents
var/list/is_using = list()
var/locked = FALSE
/// whether or not we should open when clicked
var/attack_hand_interact = TRUE
/// whether or not we allow storage objects of the same size inside
var/allow_big_nesting = FALSE
/// should we be allowed to pickup an object by clicking it
var/allow_quick_gather = FALSE
/// show we allow emptying all contents by using the storage object in hand
var/allow_quick_empty = FALSE
/// the mode for collection when allow_quick_gather is enabled
var/collection_mode = COLLECT_ONE
/// shows what we can hold in examine text
var/can_hold_description
/// contents shouldn't be emped
var/emp_shielded
/// you put things *in* a bag, but *on* a plate
var/insert_preposition = "in"
/// don't show any chat messages regarding inserting items
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
/// play a rustling sound when interacting with the bag
var/rustle_sound = TRUE
/// alt click takes an item out instead of opening up storage
var/quickdraw = FALSE
/// instead of displaying multiple items of the same type, display them as numbered contents
var/numerical_stacking = FALSE
/// storage display object
var/atom/movable/screen/storage/boxes
/// close button object
var/atom/movable/screen/close/closer
/// maximum amount of columns a storage object can have
var/screen_max_columns = 7
var/screen_max_rows = INFINITY
/// pixel location of the boxes and close button
var/screen_pixel_x = 16
var/screen_pixel_y = 16
/// where storage starts being rendered, screen_loc wise
var/screen_start_x = 4
var/screen_start_y = 2
var/datum/weakref/modeswitch_action_ref
/// If true shows the contents of the storage in open_storage
var/display_contents = TRUE
/datum/storage/New(atom/parent, max_slots, max_specific_storage, max_total_storage, numerical_stacking, allow_quick_gather, allow_quick_empty, collection_mode, attack_hand_interact)
boxes = new(null, src)
closer = new(null, src)
src.parent = WEAKREF(parent)
src.real_location = src.parent
src.max_slots = max_slots || src.max_slots
src.max_specific_storage = max_specific_storage || src.max_specific_storage
src.max_total_storage = max_total_storage || src.max_total_storage
src.numerical_stacking = numerical_stacking || src.numerical_stacking
src.allow_quick_gather = allow_quick_gather || src.allow_quick_gather
src.allow_quick_empty = allow_quick_empty || src.allow_quick_empty
src.collection_mode = collection_mode || src.collection_mode
src.attack_hand_interact = attack_hand_interact || src.attack_hand_interact
var/atom/resolve_parent = src.parent?.resolve()
var/atom/resolve_location = src.real_location?.resolve()
if(!resolve_parent)
stack_trace("storage could not resolve parent weakref")
qdel(src)
return
if(!resolve_location)
stack_trace("storage could not resolve location weakref")
qdel(src)
return
RegisterSignals(resolve_parent, list(COMSIG_ATOM_ATTACK_PAW, COMSIG_ATOM_ATTACK_HAND), PROC_REF(on_attack))
RegisterSignal(resolve_parent, COMSIG_MOUSEDROP_ONTO, PROC_REF(on_mousedrop_onto))
RegisterSignal(resolve_parent, COMSIG_MOUSEDROPPED_ONTO, PROC_REF(on_mousedropped_onto))
RegisterSignal(resolve_parent, COMSIG_ATOM_EMP_ACT, PROC_REF(on_emp_act))
RegisterSignal(resolve_parent, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby))
RegisterSignal(resolve_parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(on_preattack))
RegisterSignal(resolve_parent, COMSIG_OBJ_DECONSTRUCT, PROC_REF(on_deconstruct))
RegisterSignal(resolve_parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(mass_empty))
RegisterSignals(resolve_parent, list(COMSIG_CLICK_ALT, COMSIG_ATOM_ATTACK_GHOST, COMSIG_ATOM_ATTACK_HAND_SECONDARY), PROC_REF(open_storage_on_signal))
RegisterSignal(resolve_parent, COMSIG_PARENT_ATTACKBY_SECONDARY, PROC_REF(open_storage_attackby_secondary))
RegisterSignal(resolve_location, COMSIG_ATOM_ENTERED, PROC_REF(handle_enter))
RegisterSignal(resolve_location, COMSIG_ATOM_EXITED, PROC_REF(handle_exit))
RegisterSignal(resolve_parent, COMSIG_MOVABLE_MOVED, PROC_REF(close_distance))
RegisterSignal(resolve_parent, COMSIG_ITEM_EQUIPPED, PROC_REF(update_actions))
RegisterSignal(resolve_parent, COMSIG_TOPIC, PROC_REF(topic_handle))
orient_to_hud()
/datum/storage/Destroy()
parent = null
real_location = null
for(var/mob/person in is_using)
if(person.active_storage == src)
person.active_storage = null
person.client?.screen -= boxes
person.client?.screen -= closer
QDEL_NULL(boxes)
QDEL_NULL(closer)
is_using.Cut()
return ..()
/datum/storage/proc/on_deconstruct()
SIGNAL_HANDLER
remove_all()
/// 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
var/atom/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
resolve_parent.update_appearance(UPDATE_ICON_STATE)
arrived.item_flags |= IN_STORAGE
refresh_views()
arrived.on_enter_storage(src)
/// 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
var/atom/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
resolve_parent.update_appearance(UPDATE_ICON_STATE)
gone.item_flags &= ~IN_STORAGE
remove_and_refresh(gone)
gone.on_exit_storage(src)
/**
* Sets where items are physically being stored in the case it shouldn't be on the parent.
*
* @param atom/real the new real location of the datum
* @param should_drop if TRUE, all the items in the old real location will be dropped
*/
/datum/storage/proc/set_real_location(atom/real, should_drop = FALSE)
if(!real)
return
var/atom/resolve_location = src.real_location?.resolve()
if(!resolve_location)
return
var/atom/resolve_parent = src.parent?.resolve()
if(!resolve_parent)
return
if(should_drop)
remove_all(get_turf(resolve_parent))
resolve_location.flags_1 &= ~HAS_DISASSOCIATED_STORAGE_1
real.flags_1 |= HAS_DISASSOCIATED_STORAGE_1
UnregisterSignal(resolve_location, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED))
RegisterSignal(real, COMSIG_ATOM_ENTERED, PROC_REF(handle_enter))
RegisterSignal(real, COMSIG_ATOM_EXITED, PROC_REF(handle_exit))
real_location = WEAKREF(real)
/datum/storage/proc/topic_handle(datum/source, user, href_list)
SIGNAL_HANDLER
if(href_list["show_valid_pocket_items"])
handle_show_valid_items(source, user)
/datum/storage/proc/handle_show_valid_items(datum/source, user)
to_chat(user, 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)
/datum/storage/proc/set_holdable(list/can_hold_list = null, list/cant_hold_list = null)
if(!islist(can_hold_list))
can_hold_list = list(can_hold_list)
if(!islist(cant_hold_list))
cant_hold_list = list(cant_hold_list)
can_hold_description = generate_hold_desc(can_hold_list)
if (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]
if (cant_hold_list != null)
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]
/// Generates a description, primarily for clothing storage.
/datum/storage/proc/generate_hold_desc(can_hold_list)
var/list/desc = list()
for(var/valid_type in can_hold_list)
var/obj/item/valid_item = valid_type
desc += "\a [initial(valid_item.name)]"
return "\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
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
if(!istype(resolve_parent) || !allow_quick_gather)
QDEL_NULL(modeswitch_action_ref)
return
var/datum/action/existing = modeswitch_action_ref?.resolve()
if(!QDELETED(existing))
return
var/datum/action/modeswitch_action = resolve_parent.add_item_action(/datum/action/item_action/storage_gather_mode)
RegisterSignal(modeswitch_action, COMSIG_ACTION_TRIGGER, PROC_REF(action_trigger))
modeswitch_action_ref = WEAKREF(modeswitch_action)
/// 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
*
* @param obj/item/to_insert the item we're checking
* @param messages if TRUE, will print out a message if the item is not valid
* @param force bypass locked storage
*/
/datum/storage/proc/can_insert(obj/item/to_insert, mob/user, messages = TRUE, force = FALSE)
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
if(QDELETED(to_insert))
return FALSE
if(!isitem(to_insert))
return FALSE
if(locked && !force)
return FALSE
if((to_insert == resolve_parent) || (to_insert == real_location))
return FALSE
if(to_insert.w_class > max_specific_storage && !is_type_in_typecache(to_insert, exception_hold))
if(messages && user)
to_chat(user, span_warning("\The [to_insert] is too big for \the [resolve_parent]!"))
return FALSE
if(resolve_location.contents.len >= max_slots)
if(messages && user && !silent_for_user)
to_chat(user, span_warning("\The [to_insert] can't fit into \the [resolve_parent]! Make some space!"))
return FALSE
var/total_weight = to_insert.w_class
for(var/obj/item/thing in resolve_location)
total_weight += thing.w_class
if(total_weight > max_total_storage)
if(messages && user && !silent_for_user)
to_chat(user, span_warning("\The [to_insert] can't fit into \the [resolve_parent]! Make some space!"))
return FALSE
if(length(can_hold))
if(!is_type_in_typecache(to_insert, can_hold))
if(messages && user)
to_chat(user, span_warning("\The [resolve_parent] cannot hold \the [to_insert]!"))
return FALSE
if(is_type_in_typecache(to_insert, cant_hold) || HAS_TRAIT(to_insert, TRAIT_NO_STORAGE_INSERT) || (can_hold_trait && !HAS_TRAIT(to_insert, can_hold_trait)))
if(messages && user)
to_chat(user, span_warning("\The [resolve_parent] cannot hold \the [to_insert]!"))
return FALSE
if(HAS_TRAIT(to_insert, TRAIT_NODROP))
if(messages)
to_chat(user, span_warning("\The [to_insert] is stuck on your hand!"))
return FALSE
var/datum/storage/biggerfish = resolve_parent.loc.atom_storage // this is valid if the container our resolve_parent is being held in is a storage item
if(biggerfish && biggerfish.max_specific_storage < max_specific_storage)
if(messages && user)
to_chat(user, span_warning("[to_insert] can't fit in [resolve_parent] while [resolve_parent.loc] is in the way!"))
return FALSE
if(istype(resolve_parent))
var/datum/storage/item_storage = to_insert.atom_storage
if((to_insert.w_class >= resolve_parent.w_class) && item_storage && !allow_big_nesting)
if(messages && user)
to_chat(user, span_warning("[resolve_parent] cannot hold [to_insert] as it's a storage item of the same size!"))
return FALSE
return TRUE
/**
* Attempts to insert an item into the storage
*
* @param datum/source used by the signal handler
* @param obj/item/to_insert the item we're inserting
* @param mob/user the user who is inserting the item
* @param override see item_insertion_feedback()
* @param force bypass locked storage
*/
/datum/storage/proc/attempt_insert(obj/item/to_insert, mob/user, override = FALSE, force = FALSE)
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return FALSE
if(!can_insert(to_insert, user, force = force))
return FALSE
to_insert.item_flags |= IN_STORAGE
to_insert.forceMove(resolve_location)
item_insertion_feedback(user, to_insert, override)
resolve_location.update_appearance()
return TRUE
/**
* Inserts every item in a given list, with a progress bar
*
* @param mob/user the user who is inserting the items
* @param list/things the list of items to insert
* @param atom/thing_loc the location of the items (used to make sure an item hasn't moved during pickup)
* @param list/rejections a list used to make sure we only complain once about an invalid insertion
* @param 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)
var/obj/item/resolve_parent = parent?.resolve()
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_parent || !resolve_location)
return
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, TRUE)) // Note can_be_inserted still makes noise when the answer is no
if(resolve_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
*
* @param mob/user the user who is inserting the item
* @param obj/item/thing the item we're inserting
* @param override skip feedback, only do animation check
*/
/datum/storage/proc/item_insertion_feedback(mob/user, obj/item/thing, override = FALSE)
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
if(animated)
animate_parent()
if(override)
return
if(silent)
return
if(rustle_sound)
playsound(resolve_parent, SFX_RUSTLE, 50, TRUE, -5)
if(!silent_for_user)
to_chat(user, span_notice("You put [thing] [insert_preposition]to [resolve_parent]."))
for(var/mob/viewing in oviewers(user, null))
if(in_range(user, viewing))
viewing.show_message(span_notice("[user] puts [thing] [insert_preposition]to [resolve_parent]."), MSG_VISUAL)
return
if(thing && thing.w_class >= 3)
viewing.show_message(span_notice("[user] puts [thing] [insert_preposition]to [resolve_parent]."), MSG_VISUAL)
return
/**
* Attempts to remove an item from the storage
*
* @param obj/item/thing the object we're removing
* @param atom/newLoc where we're placing the item
* @param silent if TRUE, we won't play any exit sounds
*/
/datum/storage/proc/attempt_remove(obj/item/thing, atom/newLoc, silent = FALSE)
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
if(istype(thing))
if(ismob(resolve_parent.loc))
var/mob/mobparent = resolve_parent.loc
thing.dropped(mobparent, TRUE)
if(newLoc)
reset_item(thing)
thing.forceMove(newLoc)
if(rustle_sound && !silent)
playsound(resolve_parent, SFX_RUSTLE, 50, TRUE, -5)
else
thing.moveToNullspace()
thing.item_flags &= ~IN_STORAGE
if(animated)
animate_parent()
refresh_views()
if(isobj(resolve_parent))
resolve_parent.update_appearance()
return TRUE
/**
* Removes everything inside of our storage
*
* @param atom/target where we're placing the item
*/
/datum/storage/proc/remove_all(atom/target)
var/obj/item/resolve_parent = parent?.resolve()
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_parent || !resolve_location)
return
if(!target)
target = get_turf(resolve_parent)
for(var/obj/item/thing in resolve_location)
if(thing.loc != resolve_location)
continue
if(!attempt_remove(thing, target, silent = TRUE))
continue
thing.pixel_x = thing.base_pixel_x + rand(-8, 8)
thing.pixel_y = thing.base_pixel_y + rand(-8, 8)
/**
* Removes only a specific type of item from our storage
*
* @param type the type of item to remove
* @param amount how many we should attempt to pick up at one time
* @param check_adjacent if TRUE, we'll check adjacent locations for the item type
* @param force if TRUE, we'll bypass the check_adjacent check all together
* @param mob/user the user who is removing the items
* @param list/inserted a list passed to attempt_remove for ultimate removal
*/
/datum/storage/proc/remove_type(type, atom/destination, amount = INFINITY, check_adjacent = FALSE, force = FALSE, mob/user, list/inserted)
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
if(!force)
if(check_adjacent)
if(!user || !user.CanReach(destination) || !user.CanReach(resolve_location))
return FALSE
var/list/taking = typecache_filter_list(resolve_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, atom/location, force)
SIGNAL_HANDLER
if(!allow_quick_empty && !force)
return
remove_all(get_turf(location))
/**
* Recursive proc to get absolutely EVERYTHING inside a storage item, including the contents of inner items.
*
* @param list/interface the list we're adding objects to
* @param recursive whether or not we're checking inside of inner items
*/
/datum/storage/proc/return_inv(list/interface, recursive = TRUE)
if(!islist(interface))
return FALSE
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
var/list/ret = list()
ret |= resolve_location.contents
if(recursive)
for(var/i in ret.Copy())
var/atom/atom = i
atom.atom_storage?.return_inv(ret, TRUE)
interface |= ret
return TRUE
/**
* 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 in is_using)
if(user.client)
var/client/cuser = user.client
cuser.screen -= gone
reset_item(gone)
refresh_views()
/// Signal handler for the emp_act() of all contents
/datum/storage/proc/on_emp_act(datum/source, severity)
SIGNAL_HANDLER
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
if(emp_shielded)
return
for(var/atom/thing in resolve_location)
thing.emp_act(severity)
/// Signal handler for preattack from an object.
/datum/storage/proc/on_preattack(datum/source, obj/item/thing, mob/user, params)
SIGNAL_HANDLER
if(!istype(thing) || !allow_quick_gather || thing.atom_storage)
return
if(collection_mode == COLLECT_ONE)
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/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
var/list/turf_things = thing.loc.contents.Copy()
if(collection_mode == COLLECT_SAME)
turf_things = typecache_filter_list(turf_things, typecacheof(thing.type))
var/amount = length(turf_things)
if(!amount)
resolve_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, resolve_parent, NONE, FALSE, CALLBACK(src, PROC_REF(handle_mass_pickup), user, turf_things, thing.loc, rejections, progress)))
stoplag(1)
progress.end_progress()
resolve_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
var/obj/item/resolve_parent = parent?.resolve()
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_parent || !resolve_location)
return
if(ismecha(user.loc) || user.incapacitated() || !user.canUseStorage())
return
resolve_parent.add_fingerprint(user)
if(istype(over_object, /atom/movable/screen/inventory/hand))
if(resolve_parent.loc != user)
return
var/atom/movable/screen/inventory/hand/hand = over_object
user.putItemFromInventoryInHandIfPossible(resolve_parent, hand.held_index)
else if(ismob(over_object))
if(over_object != user)
return
INVOKE_ASYNC(src, PROC_REF(open_storage), user)
else if(!istype(over_object, /atom/movable/screen))
INVOKE_ASYNC(src, PROC_REF(dump_content_at), over_object, user)
/**
* 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, mob/user)
var/obj/item/resolve_parent = parent.resolve()
var/obj/item/resolve_location = real_location.resolve()
if(locked)
return
if(!user.CanReach(resolve_parent) || !user.CanReach(dest_object))
return
if(SEND_SIGNAL(dest_object, COMSIG_STORAGE_DUMP_CONTENT, resolve_location, 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 [resolve_parent] into [dest_object]."))
if(rustle_sound)
playsound(resolve_parent, SFX_RUSTLE, 50, TRUE, -5)
for(var/obj/item/to_dump in resolve_location)
if(to_dump.loc != resolve_location)
continue
dest_object.atom_storage.attempt_insert(to_dump, user)
resolve_parent.update_appearance()
return
var/atom/dump_loc = dest_object.get_dumping_location()
if(isnull(dump_loc))
return
// Storage to loc transfer requires a do_after
to_chat(user, span_notice("You start dumping out the contents of [resolve_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
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
if(!istype(dropping))
return
if(dropping != user.get_active_held_item())
return
if(dropping.atom_storage) // If it has storage it should be trying to dump, not insert.
return
if(!iscarbon(user) && !isdrone(user))
return
var/mob/living/user_living = user
if(user_living.incapacitated())
return
attempt_insert(dropping, user)
/// Signal handler for whenever we're attacked by an object.
/datum/storage/proc/on_attackby(datum/source, obj/item/thing, mob/user, params)
SIGNAL_HANDLER
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
if(!thing.attackby_storage_insert(src, resolve_parent, user))
return FALSE
if(iscyborg(user))
return TRUE
attempt_insert(thing, user)
return TRUE
/// Signal handler for whenever we're attacked by a mob.
/datum/storage/proc/on_attack(datum/source, mob/user)
SIGNAL_HANDLER
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
if(!attack_hand_interact)
return
if(user.active_storage == src && resolve_parent.loc == user)
user.active_storage.hide_contents(user)
hide_contents(user)
return TRUE
if(ishuman(user))
var/mob/living/carbon/human/hum = user
if(hum.l_store == resolve_parent && !hum.get_active_held_item())
INVOKE_ASYNC(hum, TYPE_PROC_REF(/mob, put_in_hands), resolve_parent)
hum.l_store = null
return
if(hum.r_store == resolve_parent && !hum.get_active_held_item())
INVOKE_ASYNC(hum, TYPE_PROC_REF(/mob, put_in_hands), resolve_parent)
hum.r_store = null
return
if(resolve_parent.loc == user)
INVOKE_ASYNC(src, PROC_REF(open_storage), user)
return TRUE
/// Generates the numbers on an item in storage to show stacking.
/datum/storage/proc/process_numerical_display()
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
var/list/toreturn = list()
for(var/obj/item/thing in resolve_location.contents)
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
/// Updates the storage UI to fit all objects inside storage.
/datum/storage/proc/orient_to_hud()
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
var/adjusted_contents = resolve_location.contents.len
//Numbered contents display
var/list/datum/numbered_display/numbered_contents
if(numerical_stacking)
numbered_contents = process_numerical_display()
adjusted_contents = numbered_contents.len
//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)
orient_item_boxes(rows, columns, numbered_contents)
/// Generates the actual UI objects, their location, and alignments whenever we open storage up.
/datum/storage/proc/orient_item_boxes(rows, cols, list/obj/item/numerical_display_contents)
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
boxes.screen_loc = "[screen_start_x]:[screen_pixel_x],[screen_start_y]:[screen_pixel_y] to [screen_start_x+cols-1]:[screen_pixel_x],[screen_start_y+rows-1]:[screen_pixel_y]"
var/current_x = screen_start_x
var/current_y = screen_start_y
var/turf/our_turf = get_turf(resolve_location)
if(islist(numerical_display_contents))
for(var/type in numerical_display_contents)
var/datum/numbered_display/numberdisplay = numerical_display_contents[type]
var/obj/item/display_sample = numberdisplay.sample_object
display_sample.mouse_opacity = MOUSE_OPACITY_OPAQUE
display_sample.screen_loc = "[current_x]:[screen_pixel_x],[current_y]:[screen_pixel_y]"
display_sample.maptext = MAPTEXT("<font color='white'>[(numberdisplay.number > 1)? "[numberdisplay.number]" : ""]</font>")
SET_PLANE(display_sample, ABOVE_HUD_PLANE, our_turf)
current_x++
if(current_x - screen_start_x >= cols)
current_x = screen_start_x
current_y++
if(current_y - screen_start_y >= rows)
break
else
for(var/obj/item in resolve_location)
item.mouse_opacity = MOUSE_OPACITY_OPAQUE
item.screen_loc = "[current_x]:[screen_pixel_x],[current_y]:[screen_pixel_y]"
item.maptext = ""
item.plane = ABOVE_HUD_PLANE
SET_PLANE(item, ABOVE_HUD_PLANE, our_turf)
current_x++
if(current_x - screen_start_x >= cols)
current_x = screen_start_x
current_y++
if(current_y - screen_start_y >= rows)
break
closer.screen_loc = "[screen_start_x + cols]:[screen_pixel_x],[screen_start_y]:[screen_pixel_y]"
/// Signal handler for when we get attacked with secondary click by an item.
/datum/storage/proc/open_storage_attackby_secondary(datum/source, atom/weapon, mob/user)
SIGNAL_HANDLER
return open_storage_on_signal(source, user)
/// Signal handler to open up the storage when we recieve 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)
return COMPONENT_NO_AFTERATTACK
/// Opens the storage to the mob, showing them the contents to their UI.
/datum/storage/proc/open_storage(mob/to_show)
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return FALSE
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return FALSE
if(isobserver(to_show))
show_contents(to_show)
return FALSE
if(!to_show.CanReach(resolve_parent))
resolve_parent.balloon_alert(to_show, "can't reach!")
return FALSE
if(!isliving(to_show) || to_show.incapacitated())
return FALSE
if(locked)
if(!silent)
resolve_parent.balloon_alert(to_show, "locked!")
return FALSE
if(!quickdraw || to_show.get_active_held_item())
if(display_contents)
show_contents(to_show)
if(animated)
animate_parent()
if(rustle_sound)
playsound(resolve_parent, SFX_RUSTLE, 50, TRUE, -5)
return TRUE
var/obj/item/to_remove = locate() in resolve_location
if(!to_remove)
return TRUE
attempt_remove(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 [resolve_parent]!"), span_notice("You draw [to_remove] from [resolve_parent]."))
return TRUE
/// Async version of putting something into a mobs hand.
/datum/storage/proc/put_in_hands_async(mob/toshow, obj/item/toremove)
if(!toshow.put_in_hands(toremove))
if(!silent)
toremove.balloon_alert(toshow, "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
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
for(var/mob/user in can_see_contents())
if (!user.CanReach(resolve_parent))
hide_contents(user)
/// Close the storage UI for everyone viewing us.
/datum/storage/proc/close_all()
for(var/mob/user in is_using)
hide_contents(user)
/// 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
is_using -= user
return seeing
/**
* Show our storage to a mob.
*
* @param mob/toshow the mob to show the storage to
*/
/datum/storage/proc/show_contents(mob/toshow)
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
if(!toshow.client)
return
if(toshow.active_storage != src && (toshow.stat == CONSCIOUS))
for(var/obj/item/thing in resolve_location)
if(thing.on_found(toshow))
toshow.active_storage.hide_contents(toshow)
if(toshow.active_storage)
toshow.active_storage.hide_contents(toshow)
toshow.active_storage = src
if(ismovable(resolve_location))
var/atom/movable/movable_loc = resolve_location
movable_loc.become_active_storage(src)
orient_to_hud()
is_using |= toshow
toshow.client.screen |= boxes
toshow.client.screen |= closer
toshow.client.screen |= resolve_location.contents
/**
* Hide our storage from a mob.
*
* @param mob/toshow the mob to hide the storage from
*/
/datum/storage/proc/hide_contents(mob/toshow)
var/obj/item/resolve_location = real_location?.resolve()
if(!resolve_location)
return
if(!toshow.client)
return TRUE
if(toshow.active_storage == src)
toshow.active_storage = null
if(!length(is_using) && ismovable(resolve_location))
var/atom/movable/movable_loc = resolve_location
movable_loc.lose_active_storage(src)
is_using -= toshow
toshow.client.screen -= boxes
toshow.client.screen -= closer
toshow.client.screen -= resolve_location.contents
/datum/storage/proc/action_trigger(datum/signal_source, datum/action/source)
SIGNAL_HANDLER
toggle_collection_mode(source.owner)
return TRUE
/**
* Toggles the collectmode of our storage.
*
* @param mob/toshow the mob toggling us
*/
/datum/storage/proc/toggle_collection_mode(mob/user)
var/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
collection_mode = (collection_mode+1)%3
switch(collection_mode)
if(COLLECT_SAME)
resolve_parent.balloon_alert(user, "will now only pick up a single type")
if(COLLECT_EVERYTHING)
resolve_parent.balloon_alert(user, "will now pick up everything")
if(COLLECT_ONE)
resolve_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/obj/item/resolve_parent = parent?.resolve()
if(!resolve_parent)
return
var/matrix/old_matrix = resolve_parent.transform
animate(resolve_parent, time = 1.5, loop = 0, transform = resolve_parent.transform.Scale(1.07, 0.9))
animate(time = 2, transform = old_matrix)