Files
Bubberstation/code/datums/outfit.dm
The SharkeningandGitHub f944caa59c Fixes multiple protean issues. (Outfitting, species loss / add, suit assimilation, and you can be a ninja now) (#5947)
## About The Pull Request

Fixes some really long standing Protean issues and minorly refractors
Protean suit assimilation to be on the species datum.

This also fixes antag selection like ninjas not functioning properly.
You can be a protean ninja now.

## Why It's Good For The Game

I think bugs are bad.

## Proof Of Testing

I tested everything I can think of, 

<img width="1199" height="534" alt="image"
src="https://github.com/user-attachments/assets/6906815b-5810-4ec2-bf9a-f928c636b1eb"
/>

## Changelog
🆑
fix: Proteans can now be outfitted properly.
fix: Suit assimilation no longer drops modules on the floor and it
should act more sane.
fix: Losing your species no longer deletes your items in storage or your
assimilated suit.
fix: Gaining the protean species allows you to properly be a Protean.
fix: You can now be mid-round antag spawn as a Protean without being
broken.
fix: As a protean ninja, you can now emag doors and hack borgs.
code: Minorly refactored suit assimilation. Please report any issues.
/🆑
2026-07-21 20:03:45 -07:00

540 lines
17 KiB
Plaintext

/**
* # Outfit datums
*
* This is a clean system of applying outfits to mobs, if you need to equip someone in a uniform
* this is the way to do it cleanly and properly.
*
* You can also specify an outfit datum on a job to have it auto equipped to the mob on join
*
* /mob/living/carbon/human/proc/equipOutfit(outfit) is the mob level proc to equip an outfit
* and you pass it the relevant datum outfit
*
* outfits can also be saved as json blobs downloadable by a client and then can be uploaded
* by that user to recreate the outfit, this is used by admins to allow for custom event outfits
* that can be restored at a later date
*/
/datum/outfit
///Name of the outfit (shows up in the equip admin verb)
var/name = "Naked"
/// Type path of item to go in the idcard slot
var/id = null
/// Type path of ID card trim associated with this outfit.
var/id_trim = null
/// Type path of item to go in uniform slot
var/obj/item/uniform = null
/// Type path of item to go in suit slot
var/obj/item/suit = null
/**
* Type path of item to go in suit storage slot
*
* (make sure it's valid for that suit)
*/
var/obj/item/suit_store = null
/// Type path of item to go in back slot
var/obj/item/back = null
/**
* list of items that should go in the backpack of the user
*
* Format of this list should be: list(path=count,otherpath=count)
*/
var/list/backpack_contents = null
/// Type path of item to go in belt slot
var/obj/item/belt = null
/**
* list of items that should go in the belt of the user
*
* Format of this list should be: list(path=count,otherpath=count)
*/
var/list/belt_contents = null
/// Type path of item to go in ears slot
var/ears = null
/// Type path of item to go in the glasses slot
var/glasses = null
/// Type path of item to go in gloves slot
var/gloves = null
/// Type path of item to go in head slot
var/head = null
/// Type path of item to go in mask slot
var/mask = null
/// Type path of item to go in neck slot
var/neck = null
/// Type path of item to go in shoes slot
var/shoes = null
/// Type path of item for left pocket slot
var/l_pocket = null
/// Type path of item for right pocket slot
var/r_pocket = null
///Type path of item to go in the right hand
var/l_hand = null
//Type path of item to go in left hand
var/r_hand = null
/// Any clothing accessory item
var/accessory = null
/// Internals box. Will be inserted at the start of backpack_contents
var/box
/**
* extra types for chameleon outfit changes, mostly guns
*
* Valid values are a single typepath or list of typepaths
*
* These are all added and returns in the list for get_chamelon_diguise_info proc
*/
var/list/chameleon_extras
/**
* Any implants the mob should start implanted with
*
* Format of this list is (typepath, typepath, typepath)
*/
var/list/implants = null
///ID of the slot containing a gas tank
var/internals_slot = null
/**
* Any skillchips the mob should have in their brain.
*
* Format of this list is (typepath, typepath, typepath)
*/
var/list/skillchips = null
///Should we preload some of this job's items?
var/preload = FALSE
/// Any undershirt. While on humans it is a string, here we use paths to stay consistent with the rest of the equips.
var/datum/sprite_accessory/clothing/undershirt = null
var/datum/sprite_accessory/clothing/underwear = null
var/datum/sprite_accessory/clothing/socks = null
/**
* Called at the start of the equip proc
*
* Override to change the value of the slots depending on client prefs, species and
* other such sources of change
*
* Extra Arguments
* * visuals_only true if this is only for display (in the character setup screen)
*
* If visuals_only is true, you can omit any work that doesn't visually appear on the character sprite
*/
/datum/outfit/proc/pre_equip(mob/living/carbon/human/user, visuals_only = FALSE)
//to be overridden for customization depending on client prefs,species etc
return
/**
* Called after the equip proc has finished
*
* All items are on the mob at this point, use this proc to toggle internals
* fiddle with id bindings and accesses etc
*
* Extra Arguments
* * visuals_only true if this is only for display (in the character setup screen)
*
* If visuals_only is true, you can omit any work that doesn't visually appear on the character sprite
*/
/datum/outfit/proc/post_equip(mob/living/carbon/human/user, visuals_only = FALSE)
//to be overridden for toggling internals, id binding, access etc
return
#define EQUIP_OUTFIT_ITEM(item_path, slot_name) if(##item_path) { \
user.equip_to_slot_or_del(SSwardrobe.provide_type(##item_path, user), ##slot_name, TRUE, indirect_action = TRUE); \
var/obj/item/outfit_item = user.get_item_by_slot(##slot_name); \
if (outfit_item && outfit_item.type == ##item_path) { \
outfit_item.on_outfit_equip(user, visuals_only, ##slot_name); \
} \
}
/**
* Equips all defined types and paths to the mob passed in
*
* Extra Arguments
* * visuals_only true if this is only for display (in the character setup screen)
*
* If visuals_only is true, you can omit any work that doesn't visually appear on the character sprite
*/
/datum/outfit/proc/equip(mob/living/carbon/human/user, visuals_only = FALSE)
pre_equip(user, visuals_only)
//Start with uniform,suit,backpack for additional slots
if(uniform)
EQUIP_OUTFIT_ITEM(uniform, ITEM_SLOT_ICLOTHING)
if(suit)
EQUIP_OUTFIT_ITEM(suit, ITEM_SLOT_OCLOTHING)
if(belt)
EQUIP_OUTFIT_ITEM(belt, ITEM_SLOT_BELT)
if(gloves)
EQUIP_OUTFIT_ITEM(gloves, ITEM_SLOT_GLOVES)
if(shoes)
EQUIP_OUTFIT_ITEM(shoes, ITEM_SLOT_FEET)
if(head)
EQUIP_OUTFIT_ITEM(head, ITEM_SLOT_HEAD)
if(mask)
EQUIP_OUTFIT_ITEM(mask, ITEM_SLOT_MASK)
if(neck)
EQUIP_OUTFIT_ITEM(neck, ITEM_SLOT_NECK)
if(ears)
EQUIP_OUTFIT_ITEM(ears, ITEM_SLOT_EARS)
if(glasses)
EQUIP_OUTFIT_ITEM(glasses, ITEM_SLOT_EYES)
if(back)
EQUIP_OUTFIT_ITEM(back, ITEM_SLOT_BACK)
if(id)
EQUIP_OUTFIT_ITEM(id, ITEM_SLOT_ID)
if(!visuals_only && id_trim && user.wear_id)
var/obj/item/card/id/id_card = user.wear_id
if(!istype(id_card)) //If an ID wasn't found in their ID slot, it's probably something holding their ID like a wallet or PDA
id_card = locate() in user.wear_id
if(istype(id_card)) //Make sure that we actually found an ID to modify, otherwise this runtimes and cancels equipping the outfit
id_card.registered_age = user.age
if(id_trim)
if(!SSid_access.apply_trim_to_card(id_card, id_trim))
WARNING("Unable to apply trim [id_trim] to [id_card] in outfit [name].")
user.update_ID_card()
if(suit_store)
EQUIP_OUTFIT_ITEM(suit_store, ITEM_SLOT_SUITSTORE)
if(undershirt)
user.undershirt = initial(undershirt.name)
if(underwear)
user.underwear = initial(underwear.name)
if(socks)
user.socks = initial(socks.name)
// SKYRAT EDIT ADDITION START - Underwear and bra split
if(bra)
user.bra = initial(bra.name)
// SKYRAT EDIT END
if(accessory)
var/obj/item/clothing/under/U = user.w_uniform
if(U)
U.attach_accessory(SSwardrobe.provide_type(accessory, user))
else if(!visuals_only)
WARNING("Unable to equip accessory [accessory] in outfit [name]. No uniform present!")
if(l_hand)
user.put_in_l_hand(SSwardrobe.provide_type(l_hand, user), visuals_only = visuals_only)
if(r_hand)
user.put_in_r_hand(SSwardrobe.provide_type(r_hand, user), visuals_only = visuals_only)
if(!visuals_only) // Items in pockets or backpack don't show up on mob's icon.
if(l_pocket)
EQUIP_OUTFIT_ITEM(l_pocket, ITEM_SLOT_LPOCKET)
if(r_pocket)
EQUIP_OUTFIT_ITEM(r_pocket, ITEM_SLOT_RPOCKET)
if(box)
if(!backpack_contents)
backpack_contents = list()
backpack_contents.Insert(1, box)
backpack_contents[box] = 1
if(backpack_contents)
for(var/path in backpack_contents)
var/number = backpack_contents[path]
if(!isnum(number))//Default to 1
number = 1
for(var/i in 1 to number)
user.equip_to_storage(SSwardrobe.provide_type(path, user), ITEM_SLOT_BACK, indirect_action = TRUE, del_on_fail = TRUE)
if(belt_contents)
for(var/path in belt_contents)
var/number = belt_contents[path]
if(!isnum(number))//Default to 1
number = 1
for(var/i in 1 to number)
user.equip_to_storage(SSwardrobe.provide_type(path, user), ITEM_SLOT_BELT, indirect_action = TRUE, del_on_fail = TRUE)
SEND_SIGNAL(user.dna.species, COMSIG_OUTFIT_EQUIP, src, visuals_only) // BUBBER EDIT: Proteans. See /datum/species/protean/proc/outfit_handling
post_equip(user, visuals_only)
if(!visuals_only)
apply_fingerprints(user)
if(internals_slot)
if(internals_slot & ITEM_SLOT_HANDS)
var/obj/item/tank/internals/internals = user.is_holding_item_of_type(/obj/item/tank/internals)
if(internals)
user.open_internals(internals)
else
user.open_internals(user.get_item_by_slot(internals_slot))
if(implants)
for(var/implant_type in implants)
var/obj/item/implant/implanter = SSwardrobe.provide_type(implant_type, user)
implanter.implant(user, null, TRUE)
// Insert the skillchips associated with this outfit into the target.
if(skillchips)
for(var/skillchip_path in skillchips)
var/obj/item/skillchip/skillchip_instance = SSwardrobe.provide_type(skillchip_path)
var/implant_msg = user.implant_skillchip(skillchip_instance)
if(implant_msg)
stack_trace("Failed to implant [user] with [skillchip_instance], on job [src]. Failure message: [implant_msg]")
qdel(skillchip_instance)
return
var/activate_msg = skillchip_instance.try_activate_skillchip(TRUE, TRUE)
if(activate_msg)
CRASH("Failed to activate [user]'s [skillchip_instance], on job [src]. Failure message: [activate_msg]")
user.update_body()
return TRUE
#undef EQUIP_OUTFIT_ITEM
/**
* Apply a fingerprint from the passed in human to all items in the outfit
*
* Used for forensics setup when the mob is first equipped at roundstart
* essentially calls add_fingerprint to every defined item on the human
*
*/
/datum/outfit/proc/apply_fingerprints(mob/living/carbon/human/user)
if(!istype(user))
return
if(user.back)
user.back.add_fingerprint(user, ignoregloves = TRUE)
for(var/obj/item/item in user.back.contents)
item.add_fingerprint(user, ignoregloves = TRUE)
if(user.wear_id)
user.wear_id.add_fingerprint(user, ignoregloves = TRUE)
if(user.w_uniform)
user.w_uniform.add_fingerprint(user, ignoregloves = TRUE)
if(user.wear_suit)
user.wear_suit.add_fingerprint(user, ignoregloves = TRUE)
if(user.wear_mask)
user.wear_mask.add_fingerprint(user, ignoregloves = TRUE)
if(user.wear_neck)
user.wear_neck.add_fingerprint(user, ignoregloves = TRUE)
if(user.head)
user.head.add_fingerprint(user, ignoregloves = TRUE)
if(user.shoes)
user.shoes.add_fingerprint(user, ignoregloves = TRUE)
if(user.gloves)
user.gloves.add_fingerprint(user, ignoregloves = TRUE)
if(user.ears)
user.ears.add_fingerprint(user, ignoregloves = TRUE)
if(user.glasses)
user.glasses.add_fingerprint(user, ignoregloves = TRUE)
if(user.belt)
user.belt.add_fingerprint(user, ignoregloves = TRUE)
for(var/obj/item/item in user.belt.contents)
item.add_fingerprint(user, ignoregloves = TRUE)
if(user.s_store)
user.s_store.add_fingerprint(user, ignoregloves = TRUE)
if(user.l_store)
user.l_store.add_fingerprint(user, ignoregloves = TRUE)
if(user.r_store)
user.r_store.add_fingerprint(user, ignoregloves = TRUE)
for(var/obj/item/item in user.held_items)
item.add_fingerprint(user, ignoregloves = TRUE)
return TRUE
/// Return a list of all the types that are required to disguise as this outfit type
/datum/outfit/proc/get_chameleon_disguise_info()
var/list/types = list(uniform, suit, back, belt, gloves, shoes, head, mask, neck, ears, glasses, id, l_pocket, r_pocket, suit_store, r_hand, l_hand)
types += chameleon_extras
types += skillchips
list_clear_nulls(types)
return types
/// Return a list of types to pregenerate for later equipping
/// This should not be things that do unique stuff in Initialize() based off their location, since we'll be storing them for a while
/datum/outfit/proc/get_types_to_preload()
var/list/preload = list()
preload += id
preload += uniform
preload += suit
preload += suit_store
preload += back
//Load in backpack gear and shit
for(var/type_to_load in backpack_contents)
var/num_to_load = backpack_contents[type_to_load]
if(!isnum(num_to_load))
num_to_load = 1
for(var/i in 1 to num_to_load)
preload += type_to_load
//Load in belt gear and shit
for(var/type_to_load in belt_contents)
var/num_to_load = belt_contents[type_to_load]
if(!isnum(num_to_load))
num_to_load = 1
for(var/i in 1 to num_to_load)
preload += type_to_load
preload += belt
preload += ears
preload += glasses
preload += gloves
preload += head
preload += mask
preload += neck
preload += shoes
preload += l_pocket
preload += r_pocket
preload += l_hand
preload += r_hand
preload += accessory
preload += box
for(var/implant_type in implants)
preload += implant_type
for(var/skillpath in skillchips)
preload += skillpath
preload -= typesof(/obj/item/clothing/under/color/random) // SKYRAT EDIT - Don't preload random jumpsuit spawners that delete themselves
return preload
/// Return a json list of this outfit
/datum/outfit/proc/get_json_data()
. = list()
.["outfit_type"] = type
.["name"] = name
.["uniform"] = uniform
.["suit"] = suit
.["back"] = back
.["belt"] = belt
.["gloves"] = gloves
.["shoes"] = shoes
.["head"] = head
.["mask"] = mask
.["neck"] = neck
.["ears"] = ears
.["glasses"] = glasses
.["id"] = id
.["id_trim"] = id_trim
.["l_pocket"] = l_pocket
.["r_pocket"] = r_pocket
.["suit_store"] = suit_store
.["r_hand"] = r_hand
.["l_hand"] = l_hand
.["internals_slot"] = internals_slot
.["backpack_contents"] = backpack_contents
.["belt_contents"] = belt_contents
.["box"] = box
.["implants"] = implants
.["accessory"] = accessory
/// Copy most vars from another outfit to this one
/datum/outfit/proc/copy_from(datum/outfit/target)
name = target.name
uniform = target.uniform
suit = target.suit
back = target.back
belt = target.belt
gloves = target.gloves
shoes = target.shoes
head = target.head
mask = target.mask
neck = target.neck
ears = target.ears
glasses = target.glasses
id = target.id
id_trim = target.id_trim
l_pocket = target.l_pocket
r_pocket = target.r_pocket
suit_store = target.suit_store
r_hand = target.r_hand
l_hand = target.l_hand
internals_slot = target.internals_slot
backpack_contents = target.backpack_contents
belt_contents = target.belt_contents
box = target.box
implants = target.implants
accessory = target.accessory
/// Prompt the passed in mob client to download this outfit as a json blob
/datum/outfit/proc/save_to_file(mob/admin)
var/stored_data = get_json_data()
var/json = json_encode(stored_data)
//Kinda annoying but as far as i can tell you need to make actual file.
var/f = file("data/TempOutfitUpload")
fdel(f)
WRITE_FILE(f,json)
admin << ftp(f,"[name].json")
/// Create an outfit datum from a list of json data
/datum/outfit/proc/load_from(list/outfit_data)
//This could probably use more strict validation
name = outfit_data["name"]
uniform = text2path(outfit_data["uniform"])
suit = text2path(outfit_data["suit"])
back = text2path(outfit_data["back"])
belt = text2path(outfit_data["belt"])
gloves = text2path(outfit_data["gloves"])
shoes = text2path(outfit_data["shoes"])
head = text2path(outfit_data["head"])
mask = text2path(outfit_data["mask"])
neck = text2path(outfit_data["neck"])
ears = text2path(outfit_data["ears"])
glasses = text2path(outfit_data["glasses"])
id = text2path(outfit_data["id"])
id_trim = text2path(outfit_data["id_trim"])
l_pocket = text2path(outfit_data["l_pocket"])
r_pocket = text2path(outfit_data["r_pocket"])
suit_store = text2path(outfit_data["suit_store"])
r_hand = text2path(outfit_data["r_hand"])
l_hand = text2path(outfit_data["l_hand"])
internals_slot = outfit_data["internals_slot"]
var/list/backpack = outfit_data["backpack_contents"]
backpack_contents = list()
for(var/item in backpack)
var/itype = text2path(item)
if(itype)
backpack_contents[itype] = backpack[item]
var/list/beltpack = outfit_data["belt_contents"]
belt_contents = list()
for(var/item in beltpack)
var/itype = text2path(item)
if(itype)
belt_contents[itype] = beltpack[item]
box = text2path(outfit_data["box"])
var/list/impl = outfit_data["implants"]
implants = list()
for(var/I in impl)
var/imptype = text2path(I)
if(imptype)
implants += imptype
accessory = text2path(outfit_data["accessory"])
return TRUE
/datum/outfit/vv_get_dropdown()
. = ..()
VV_DROPDOWN_OPTION("", "--- /outfit ---")
VV_DROPDOWN_OPTION(VV_HK_TO_OUTFIT_EDITOR, "Outfit Editor")
/datum/outfit/vv_do_topic(list/href_list)
. = ..()
if(!.)
return
if(href_list[VV_HK_TO_OUTFIT_EDITOR])
usr.client.open_outfit_editor(src)