/** * 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("[(numberdisplay.number > 1)? "[numberdisplay.number]" : ""]") 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)