Files

518 lines
18 KiB
Plaintext

//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
//* Init *//
/**
* Initializes, or resets our inventory.
*/
/mob/proc/init_inventory()
if(ispath(inventory))
inventory = new inventory(src)
//* Carry Weight *//
/mob/proc/update_carry_slowdown()
return
/mob/proc/update_item_slowdown()
return
//* Checks / Enumerations *//
/**
* gets the primary item in a slot
* null if not in inventory. inhands don't count as inventory here, use held item procs.
*/
/mob/proc/item_by_slot_id(slot)
return _item_by_slot(slot) // why the needless indirection? so people don't override this for slots!
/**
* get slot of item if it's equipped.
* null if not in inventory. SLOT_HANDS if held.
*/
/mob/proc/slot_id_by_item(obj/item/I)
return is_in_inventory(I) || null // short circuited to that too
// if equipped/unequipped didn't set worn_slot well jokes on you lmfao
/**
* gets the primary item and nested items (e.g. gloves, magboots, accessories) in a slot
* null if not in inventory, otherwise list
* inhands do not count as inventory
*/
/mob/proc/items_by_slot_id(slot)
var/obj/item/I = _item_by_slot(slot)
if(!I)
return list()
I = I.inv_slot_attached()
return islist(I)? I : list(I)
/**
* returns if we have something equipped - the slot if it is, null if not
*
* SLOT_ID_HANDS if in hands
*/
/mob/proc/is_in_inventory(obj/item/I)
return (I?.inv_inside?.owner == src) ? I.worn_slot : null
// we use entirely cached vars for speed.
// if this returns bad data well fuck you, don't break equipped()/unequipped().
/**
* returns if an item is in inventory (equipped) rather than hands
*/
/mob/proc/is_wearing(obj/item/I)
var/slot = is_in_inventory(I)
return slot && (slot != SLOT_ID_HANDS)
/**
* get all equipped items
*
* @params
* include_inhands - include held items too?
* include_restraints - include restraints too?
*/
/mob/proc/get_equipped_items(include_inhands, include_restraints)
return get_held_items() + _get_all_slots(include_restraints)
// todo: below procs needs optimization for when we need the datum anyways, to avoid two lookups
/mob/proc/has_slot(id)
SHOULD_NOT_OVERRIDE(TRUE)
return _item_by_slot(id) != INVENTORY_SLOT_DOES_NOT_EXIST
/mob/proc/semantically_has_slot(id)
return has_slot(id) && _semantic_slot_id_check(id)
/mob/proc/get_inventory_slot_ids(semantic, sorted)
// get all
if(sorted)
. = list()
for(var/id as anything in GLOB.inventory_slot_meta)
if(!semantically_has_slot(id))
continue
. += id
return
else
. = _get_inventory_slot_ids()
// check if we should filter
if(!semantic)
return
. = _get_inventory_slot_ids()
for(var/id in .)
if(!_semantic_slot_id_check(id))
. -= id
//* Equipping *//
/**
* checks if we can equip an item to a slot
*
* Preconditions: The item will either be equipped on us already, or not yet equipped.
*
* @return TRUE/FALSE
*
* @params
* - I - item
* - slot - slot ID
* - flags - inventory operation hint bitfield, see defines
* - user - user trying to equip that thing to us there - can be null
* - denest_to - the old slot we're leaving if called from handle_item_reequip. **extremely** snowflakey
*
* todo: refactor nesting to not require this shit
*/
/mob/proc/can_equip(obj/item/I, slot, flags, mob/user, denest_to)
// let's NOT.
if(I && QDELETED(I))
to_chat(user, SPAN_DANGER("A deleted [I] was checked in can_equip(). Report this entire line to coders immediately. Debug data: [I] ([REF(I)]) slot [slot] flags [flags] user [user]"))
to_chat(user, SPAN_DANGER("can_equip will now attempt to prevent the deleted item from being equipped. There should be no glitches."))
return FALSE
var/datum/inventory_slot/slot_meta = resolve_inventory_slot(slot)
var/self_equip = user == src
if(!slot_meta)
. = FALSE
CRASH("Failed to resolve to slot datm.")
if(slot_meta.inventory_slot_flags & INV_SLOT_IS_ABSTRACT)
// special handling: make educated guess, defaulting to yes
switch(slot_meta.type)
if(/datum/inventory_slot/abstract/hand/left)
return (flags & INV_OP_FORCE) || !get_left_held_item()
if(/datum/inventory_slot/abstract/hand/right)
return (flags & INV_OP_FORCE) || !get_right_held_item()
if(/datum/inventory_slot/abstract/put_in_backpack)
var/obj/item/thing = item_by_slot_id(SLOT_ID_BACK)
return thing?.obj_storage?.can_be_inserted(I, new /datum/event_args/actor(user), TRUE)
if(/datum/inventory_slot/abstract/put_in_belt)
var/obj/item/thing = item_by_slot_id(SLOT_ID_BACK)
return thing?.obj_storage?.can_be_inserted(I, new /datum/event_args/actor(user), TRUE)
if(/datum/inventory_slot/abstract/put_in_hands)
return (flags & INV_OP_FORCE) || !are_usable_hands_full()
return TRUE
if(!inventory_slot_bodypart_check(I, slot, user, flags) && !(flags & INV_OP_FORCE))
return FALSE
var/conflict_result = inventory_slot_conflict_check(I, slot, flags)
var/obj/item/to_wear_over
if((flags & INV_OP_IS_FINAL_CHECK) && conflict_result && (slot != SLOT_ID_HANDS))
// try to fit over
var/obj/item/conflicting = item_by_slot_id(slot)
if(conflicting)
// there's something there
var/can_fit_over = I.equip_worn_over_check(src, slot, user, conflicting, flags)
if(can_fit_over)
conflict_result = CAN_EQUIP_SLOT_CONFLICT_NONE
to_wear_over = conflicting
// ! DANGER: snowflake time
// take it out of the slot
_unequip_slot(slot, flags | INV_OP_NO_LOGIC | INV_OP_NO_UPDATE_ICONS, conflicting)
// recheck
conflict_result = inventory_slot_conflict_check(I, slot)
// put it back in incase something else breaks
_equip_slot(conflicting, slot, flags | INV_OP_NO_LOGIC | INV_OP_NO_UPDATE_ICONS)
switch(conflict_result)
if(CAN_EQUIP_SLOT_CONFLICT_HARD)
if(!(flags & INV_OP_SUPPRESS_WARNING))
to_chat(user, SPAN_WARNING("[self_equip? "You" : "They"] are already [slot_meta.display_plural? "holding too many things" : "wearing something"] [slot_meta.display_preposition] [self_equip? "your" : "their"] [slot_meta.display_name]."))
return FALSE
if(CAN_EQUIP_SLOT_CONFLICT_SOFT)
if(!(flags & INV_OP_FORCE))
if(!(flags & INV_OP_SUPPRESS_WARNING))
to_chat(user, SPAN_WARNING("[self_equip? "You" : "They"] are already [slot_meta.display_plural? "holding too many things" : "wearing something"] [slot_meta.display_preposition] [self_equip? "your" : "their"] [slot_meta.display_name]."))
return FALSE
if(!inventory_slot_semantic_conflict(I, slot, user) && !(flags & INV_OP_FORCE))
if(!(flags & INV_OP_SUPPRESS_WARNING))
to_chat(user, SPAN_WARNING("[I] doesn't fit there."))
return FALSE
var/blocked_by
if((blocked_by = inventory_slot_reachability_conflict(I, slot, user)) && !(flags & (INV_OP_FORCE | INV_OP_IGNORE_REACHABILITY)))
if(!(flags & INV_OP_SUPPRESS_WARNING))
to_chat(user, SPAN_WARNING("\the [blocked_by] is in the way!"))
return FALSE
// lastly, check item's opinion
if(!I.can_equip(src, slot, user, flags))
return FALSE
// we're the final check - side effects ARE allowed
if((flags & INV_OP_IS_FINAL_CHECK) && to_wear_over)
//! Note: this means that can_unequip is NOT called for to wear over.
//! This is intentional, but very, very sonwflakey.
to_wear_over.worn_inside = I
// setting worn inside first disallows equip/unequip from triggering
to_wear_over.forceMove(I)
for(var/datum/actor_hud/inventory/hud in inventory?.huds_using)
hud.remove_item(to_wear_over, slot_meta)
// check we don't have something already (wtf)
if(I.worn_over)
handle_item_denesting(I, denest_to, flags, user)
// set the other way around
I.worn_over = to_wear_over
// tell it we're inserting the old item
I.equip_on_worn_over_insert(src, slot, user, to_wear_over, flags)
// take the old item off our screen
client?.screen -= to_wear_over
to_wear_over.screen_loc = null
// we don't call slot re-equips here because the equip proc does this for us
return TRUE
/**
* equips an item to a slot if possible
*
* @params
* - I - item
* - slot - the slot
* - flags - inventory operation hint bitfield, see defines
* - user - the user doing the action, if any. defaults to ourselves.
*
* @return TRUE/FALSE
*/
/mob/proc/equip_to_slot_if_possible(obj/item/I, slot, flags, mob/user)
return _equip_item(I, flags, slot, user)
/**
* equips an item to a slot if possible
* item is deleted on failure
*
* @params
* - I - item
* - slot - the slot
* - flags - inventory operation hint bitfield, see defines
* - user - the user doing the action, if any. defaults to ourselves.
*
* @return TRUE/FALSE
*/
/mob/proc/equip_to_slot_or_del(obj/item/I, slot, flags, mob/user)
. = equip_to_slot_if_possible(I, slot, flags, user)
if(!.)
qdel(I)
/**
* automatically equips to the best inventory (non storage!) slot we can find for an item, if possible
* this proc is silent for the sub-calls by default to prevent spam.
*
* @params
* - I - item
* - flags - inventory operation hint bitfield, see defines
* - user - the user doing the action, if any. defaults to ourselves.
*
* @return TRUE/FALSE
*/
/mob/proc/equip_to_appropriate_slot(obj/item/I, flags, mob/user)
for(var/slot in GLOB.slot_equipment_priority)
if(equip_to_slot_if_possible(I, slot, flags | INV_OP_SUPPRESS_WARNING, user))
return TRUE
if(!(flags & INV_OP_SUPPRESS_WARNING))
to_chat(user, user == src? SPAN_WARNING("You can't find somewhere to equip [I] to!") : SPAN_WARNING("[src] has nowhere to equip [I] to!"))
return FALSE
/**
* automatically equips to the best inventory (non storage!) slot we can find for an item, if possible
*
* item is deleted on failure.
*
* @params
* - I - item
* - flags - inventory operation hint bitfield, see defines
* - user - the user doing the action, if any. defaults to ourselves.
*
* @return TRUE/FALSE
*/
/mob/proc/equip_to_appropriate_slot_or_del(obj/item/I, flags, mob/user)
if(!equip_to_appropriate_slot(I, flags, user))
qdel(I)
/**
* forcefully equips an item to a slot
* kicks out conflicting items if possible
*
* This CAN fail, so listen to return value
* Why? YOU MIGHT EQUIP TO A MOB WITHOUT A CERTAIN SLOT!
*
* @params
* - I - item
* - slot - slot to equip to
* - flags - inventory operation hint bitfield, see defines
* - user - the user doing the action, if any. defaults to ourselves.
*
* @return TRUE/FALSE
*/
/mob/proc/force_equip_to_slot(obj/item/I, slot, flags, mob/user)
return _equip_item(I, flags | INV_OP_FORCE | INV_OP_CAN_DISPLACE, slot, user)
/**
* forcefully equips an item to a slot
* kicks out conflicting items if possible
* if still failing, item is deleted
*
* this can fail, so listen to return values.
* @params
* - I - item
* - slot - slot to equip to
* - flags - inventory operation hint bitfield, see defines
* - user - the user doing the action, if any. defaults to ourselves.
*
* @return TRUE/FALSE
*/
/mob/proc/force_equip_to_slot_or_del(obj/item/I, slot, flags, mob/user)
if(!force_equip_to_slot(I, slot, flags, user))
qdel(I)
return FALSE
return TRUE
//* Dropping *//
/**
* checks if we can unequip an item
*
* Preconditions: The item is either equipped already, or isn't equipped.
*
* @return TRUE/FALSE
*
* @params
* - I - item
* - slot - slot we're unequipping from - can be null
* - flags - inventory operation hint bitfield, see defines
* - user - stripper - can be null
*/
/mob/proc/can_unequip(obj/item/I, slot, flags, mob/user = src)
// destroyed IS allowed to call these procs
if(I && QDELETED(I) && !QDESTROYING(I))
to_chat(user, SPAN_DANGER("A deleted [I] was checked in can_unequip(). Report this entire line to coders immediately. Debug data: [I] ([REF(I)]) slot [slot] flags [flags] user [user]"))
to_chat(user, SPAN_DANGER("can_unequip will return TRUE to allow you to drop the item, but expect potential glitches!"))
return TRUE
if(!slot)
slot = slot_id_by_item(I)
if(!(flags & INV_OP_FORCE) && HAS_TRAIT(I, TRAIT_ITEM_NODROP))
if(!(flags & INV_OP_SUPPRESS_WARNING))
var/datum/inventory_slot/slot_meta = resolve_inventory_slot(slot)
to_chat(user, SPAN_WARNING("[I] is stubbornly stuck [slot_meta.display_preposition] your [slot_meta.display_name]!"))
return FALSE
var/blocked_by
if((blocked_by = inventory_slot_reachability_conflict(I, slot, user)) && !(flags & (INV_OP_FORCE | INV_OP_IGNORE_REACHABILITY)))
if(!(flags & INV_OP_SUPPRESS_WARNING))
to_chat(user, SPAN_WARNING("\the [blocked_by] is in the way!"))
return FALSE
// lastly, check item's opinion
if(!I.can_unequip(src, slot, user, flags))
return FALSE
return TRUE
// So why do all of these return true if the item is null?
// Semantically, transferring/dropping nothing always works
// This lets us tidy up other pieces of code by not having to typecheck everything.
// However, if you do pass in an invalid object instead of null, the procs will fail or pass
// depending on needed behavior.
// Use the helpers when you can, it's easier for everyone and makes replacing behaviors later easier.
// This logic is actually **stricter** than the previous logic, which was "if item doesn't exist, it always works"
// todo: yeah this is pretty bad huh; this is because new hand crap is properly refactored and we aren't
// so this is just a wrapper to route stuff around while we slowly refactor inventory.
/datum/inventory/proc/drop_item_to_ground(obj/item/I, inv_op_flags, datum/event_args/actor/actor)
if(!actor)
actor = new /datum/event_args/actor(owner)
return owner.drop_item_to_ground(I, inv_op_flags, actor?.performer)
/**
* Like `drop_item_to_ground` but precise, like SS14's.
*/
/datum/inventory/proc/drop_item_to_ground_precisely(obj/item/dropping, inv_op_flags, datum/event_args/actor/actor, turf/target_loc, target_px, target_py)
return owner.drop_item_to_ground_precisely(dropping, inv_op_flags, actor?.performer, target_loc, target_px, target_py)
/**
* drops an item to ground
*
* semantically returns true if the thing is no longer in our inventory after our call, whether or not we dropped it
* if you require better checks, check if something is in inventory first.
*
* if the item is null, this returns true
* if an item is not in us, this returns true
*/
/mob/proc/drop_item_to_ground(obj/item/I, flags, mob/user = src)
if(!I)
return TRUE
// destroyed IS allowed to call these procs
if(I && QDELETED(I) && !QDESTROYING(I))
to_chat(user, SPAN_DANGER("A deleted item [I] was used in drop_item_to_ground(). Report the entire line to coders. Debugging information: [I] ([REF(I)]) flags [flags] user [user]"))
to_chat(user, SPAN_DANGER("Drop item to ground will now proceed, ignoring the bugged state. Errors may ensue."))
else if(!is_in_inventory(I))
return TRUE
return _unequip_item(I, flags | INV_OP_DIRECTLY_DROPPING, drop_location(), user)
/mob/proc/drop_item_to_ground_precisely(obj/item/I, flags, mob/user = src, turf/target_loc, target_px, target_py)
if(!drop_item_to_ground(I, flags, user))
return
// so this is complicated
// unlike ss14 our reachability systems are... kinda not up to par
// the best way to check if we can actually get in there without triggering side effects,
// is to literally simulate the move.
//
// if your shit explodes from the supermatter or something, blame whatever blew it up
// for not overriding CanPass properly.
if(!target_loc)
return
var/turf/our_turf = get_turf(src)
if(!our_turf?.can_potentially_pass(target_loc, I))
return
if(!I.Move(target_loc))
return
if(I.loc != target_loc)
return
I.set_pixel_offsets(target_px, target_py)
/**
* transfers an item somewhere
* newloc MUST EXIST, use transfer_item_to_nullspace otherwise
*
* semantically returns true if we transferred something from our inventory to newloc in the call
*
* if the item is null, this returns true
* if an item is not in us, this returns FALSE
*/
/mob/proc/transfer_item_to_loc(obj/item/I, newloc, flags, mob/user)
if(!I)
return TRUE
ASSERT(newloc)
if(!is_in_inventory(I))
return FALSE
return _unequip_item(I, flags | INV_OP_DIRECTLY_DROPPING, newloc, user)
/**
* transfers an item into nullspace
*
* semantically returns true if we transferred something from our inventory to null in the call
*
* if the item is null, this returns true
* if an item is not in us, this returns false
*/
/mob/proc/transfer_item_to_nullspace(obj/item/I, flags, mob/user)
if(!I)
return TRUE
if(!is_in_inventory(I))
return FALSE
return _unequip_item(I, flags | INV_OP_DIRECTLY_DROPPING, null, user)
/**
* removes an item from inventory. does NOT move it.
* item MUST be qdel'd or moved after this if it returns TRUE!
*
* semantically returns true if the passed item is no longer in our inventory after the call
*
* if the item is null, ths returns true
* if an item is not in us, this returns true
*/
/mob/proc/temporarily_remove_from_inventory(obj/item/I, flags, mob/user)
if(!is_in_inventory(I))
return TRUE
return _unequip_item(I, flags | INV_OP_DIRECTLY_DROPPING, FALSE, user)
//* MasS Operations *//
/**
* drops everything in our inventory
*
* @params
* - include_inhands - include held items too?
* - include_restraints - include restraints too?
* - force - ignore nodrop and all that
*/
/mob/proc/drop_inventory(include_inhands = TRUE, include_restraints = TRUE, force = TRUE)
for(var/obj/item/I as anything in get_equipped_items(include_inhands, include_restraints))
drop_item_to_ground(I, INV_OP_SILENT | INV_OP_FLUFFLESS | (force? INV_OP_FORCE : NONE))
// todo: handle what happens if dropping something requires a logic thing
// e.g. dropping jumpsuit makes it impossible to transfer a belt since it
// de-equipped from the jumpsuit
/mob/proc/transfer_inventory_to_loc(atom/newLoc, include_inhands = TRUE, include_restraints = TRUE, force = TRUE)
for(var/obj/item/I as anything in get_equipped_items(include_inhands, include_restraints))
transfer_item_to_loc(I, newLoc, INV_OP_SILENT | INV_OP_FLUFFLESS | (force? INV_OP_FORCE : NONE))
// todo: handle what happens if dropping something requires a logic thing
// e.g. dropping jumpsuit makes it impossible to transfer a belt since it
// de-equipped from the jumpsuit
/**
* wipe our inventory
*
* @params
* include_inhands - include held items too?
* include_restraints - include restraints too?
*/
/mob/proc/delete_inventory(include_inhands = TRUE, include_restraints = TRUE)
for(var/obj/item/I as anything in get_equipped_items(include_inhands, include_restraints))
qdel(I)