//* This file is explicitly licensed under the MIT license. *// //* Copyright (c) 2024 Citadel Station Developers *// /** * Item rendering system * Procs in here can be called and overridden as needed, but you should know what you're doing * if you choose to do so! * * * IF YOU ONLY CARE ABOUT MAKING A NEW ITEM OR ARE CONVERTING AN ITEM, JUST READ THIS!! * * 0. Do you want the on-mob icons to be *entirely* shared/defaulted? If so, why are you here? * Use the old system, by putting your states into the default icons as defined in * [code/modules/mob/inventory/slot_meta.dm] * and setting inhand_state and worn_state so they're found. * If you want to share *some* icons, read State Generation and * use worn_icon, and inhand_icon to do what you need to do. * * !! If your sprite already exists in default icons, it will override what !! * !! we do here. This is an unfortunate tradeoff of speed we have to make. !! * !! Make sure to set worn_species_default to FALSE to stop this. !! * ?? THIS IS SOMETIMES A GOOD THING. ?? * ?? There's common states available for inhands and some worn stuff. ?? * ?? You just need to set inhand_state and worn_state to use them! ?? * * Otherwise, continue on with the rest of the steps, setting * worn_render_flags = NONE * to instruct the system to ignore default icons entirely. * * ?? A note on bitfields: worn_render_flags ?? * ?? Without getting into the nitty gritty, worn_render_flags control all ?? * ?? behavior and logic. When the guide below says "add" a flag of a certain ?? * ?? name to it, it means boolean OR it. ?? * ?? For noncoders, basically: to combine, say, WORN_RENDER_SLOT_ONE_FOR_ALL ?? * ?? and WORN_RENDER_INHAND_ONE_FOR_ALL, you just put a | between them, like so: ?? * ?? worn_render_flags = WORN_RENDER_SLOT_ONE_FOR_ALL | WORN_RENDER_INHAND_ONE_FOR_ALL ?? * ?? Congratulations, you used a bitfield. ?? * * 1. Have a sensical place for your item's .dmi. * e.g. /obj/item/pickaxe goes in icons/modules/mining/tools/pickaxe.dmi * 2. Put what the item looks like in the world, and in your inventory, as the base state. * e.g. a sprite of a pickaxe as state `pickaxe`. Set your item's icon_state to this, * and its icon to the file you made. * 3. Add inhands if needed with state_left and state_right. * In this case, it'd be * pickaxe_left * pickaxe_right * If you for some reason want it to use a singular state only when inhand, * set worn_inhand_ignored to TRUE, and it'll render as pickaxe_all instead. * 4. Does the item go in multiple slots and require multiple sprites? * If not, add WORN_RENDER_SLOT_ONE_TO_ALL to worn_render_flags, * and it'll render as pickaxe_all always. * pickaxe_all * If so, set states for each slot id, e.g. in this case, * pickaxe_back * pickaxe_belt * A full list of slot ids are in [code/modules/mob/inventory/slot_meta.dm]. * 5. Do you care about bodytypes? If so, set worn_bodytypes to the bodytypes you want * to implement with bitfield notation, aka * worn_bodytypes = BODYTYPE_TESHARI | BODYTYPE_UNATHI * Then, put in states for the bodytypes in question like so: * pickaxe_back_teshari * pickaxe_back_unathi * A full list of available bodytypes and what the string keys for them are * in [code/__DEFINES/inventory/bodytypes.dm]. * The default bodytypes, as well as any bodytypes that aren't explicitly set as implemented, * do not get appended (e.g. if a human or vox puts it on it's still pickaxe_back) * 6. Do you want the base state, in this case 'pickaxe', for on-mob generation * to be different from icon_state? This is useful if you want to switch * your item's icon state or worn state without impacting the others. * If so, set overrides for worn (held in inventory slots) and inhand (held in hands) * worn_state = "pickaxe_onbody" * inhand_state = "pickaxe_onhand" * like so. Generation for the previous parts will become * pickaxe_onbody_back * pickaxe_onbody_belt * or in the example of "ignore slot" * pickaxe_onbody_all * and for inhands * pickaxe_onhand_left * pickaxe_onhand_right * 7. Defaulting: In the case you want to default slot/inhands (usually one but not the other), * simply add to worn_render_flags, depending on which: * WORN_RENDER_SLOT_ALLOW_DEFAULT * WORN_RENDER_INHAND_ALLOW_DEFAULt * 8. Congratulations, you're done! * * * Overall Goal * - Being able to stuff an entire item's assets in one icon * - Otherwise being able to split an entire item's assets into modular folders * - Generic inhands and slot icons go in general onmob item folders * - Not forcing usage of something as asinine as suit.dmi * * * Limitations * - inhand state only supports left and right; no weird index-based for now. this can easily change. * * * State Generation * This is only pertinent to single-icon rendering, which the system falls back to * if it can't find anything else. This is what people should be using for non-reused * item sprites. * * This system also supports worn_icon and inhand_icon if you really want to split it up. * * Pattern: * * state_slot_bodytype, where * state: (inhand_state if inhand, worn_state if not) || icon_state * slot: the inventory slot's `render_key`, or null if worn slot is ignored * if it's a hand, it'll be _left or _right if it's not ignored. * bodytype: if not trampled, or ignored, the bodytype as string (see DEFINES) * if converted to default, or it is default, or is ignored, this is null * * Obviously the _ part of the state is left out if the part doesn't exist * * Examples: * in world/on map/in inventory: * pickaxe * * worn: * pickaxe_belt (implicit default bodytype) * pickaxe_belt_teshari * pickaxe_left * pickaxe_right * pickaxe_right_teshari (overrides on teshari) * * * Coloration * * ! Coloration WIP. For now, color var only. * ! TODO: GAGS, polychromatic overlays with _1, _2, _3, _..., and RED/BLUE, RED/GREEN, GREEN/BLUE matrices. * ! TODO: For most of these, it will require mutating the icon states used. * * * Alignment * * Mobs are usually able to be shifted left/right, but are always aligned so that their bottom pixels * are on the first pixel of their 'real' tile. * * With that in mind, we allow using the x_mob_y_align variable to shift sprites up, to avoid * needing entirely centered sprites. * * That said, x alignment shifting will never happen due to limitations, so x still has to use centering. * * * Why mutable appearances? * Rendering of equipment changes regularly. They're quite literally built to be changed. * Since items always have the same direction as wearer, this means we don't have to use images. * * * Centering * All sprites are centered on the mob regardless of dimensions. * The species/mob in question can then pixel shift the resulting object as fit. * * ? Everything else: read the procs. */ /obj/item //! LEGACY //** These specify item/icon overrides for _slots_ /// Overrides the default item_state for particular slots. var/list/item_state_slots = list() /// Used to specify the icon file to be used when the item is worn. If not set the default icon for that slot will be used. /// If icon_override or sprite_sheets are set they will take precendence over this, assuming they apply to the slot in question. /// Only slot_l_hand/slot_r_hand are implemented at the moment. Others to be implemented as needed. var/list/item_icons /// Used to override hardcoded clothing dmis in human clothing proc. //TODO: Get rid of this crap -Zandario var/icon_override = null //** These specify item/icon overrides for _species_ //TODO Refactor this from the ground up. Too many overrides. -Zandario //! THIS IS NOW BANNED. DO NOT USE THIS OR I WILL PUT A LEMON ON YOUR EYES. ~silicons //! Use the new bodytype system, PLEASE. //! Accessories are currently exempt from the ban, but also accessories need refactored //! God this is fucking asinine /* Species-specific sprites, concept stolen from Paradise//vg/. * ex: * sprite_sheets = list( * BODYTYPE_STRING_TAJARAN = 'icons/cat/are/bad' * ) * If index term exists and icon_override is not set, this sprite sheet will be used. */ var/list/sprite_sheets /// Species-specific sprite sheets for inventory sprites /// Works similarly to worn sprite_sheets, except the alternate sprites are used when the clothing/refit_for_species() proc is called. var/list/sprite_sheets_obj // todo: remove /// worn icon file var/icon/default_worn_icon //! NEW RENDERING SYSTEM (to be used by all new content tm); read comment section at top //? for when base icon is used for render /// icon alignment y shift var/icon_mob_y_align = 0 //? for equipment slots: prioritized over icon, icon_state, icon dimensions /// state to use; icon_state is used if this isn't set var/worn_state /// worn icon used instead of base icon var/icon/worn_icon /// dimensions of our worn icon file if different from icon var/worn_x_dimension = 32 /// dimensions of our worn icon file if different from icon var/worn_y_dimension = 32 /// worn icon alignment y shift var/worn_mob_y_align = 0 //? for hands: prioritized over icon, icon_state, icon dimensions /// state to use; worn_state, then icon_state is used if this isn't set var/inhand_state /// inhand icon used instead of base icon var/icon/inhand_icon /// dimensions of inhand sprites if different from icon var/inhand_x_dimension = 32 /// dimensions of inhand sprites if different from icon var/inhand_y_dimension = 32 /// inhnad icon alignment y shift var/inhand_mob_y_align = 0 /// inhand default domain aka which icon we grab to check for state var/inhand_default_type = INHAND_DEFAULT_ICON_GENERAL //? for belts /// state to use in [icons/mob/clothing/belt.dmi] for belt overlay var/belt_state //? general handling directives /** * bodytypes that *can* get trampled to default if the default icon is not found on species * * for slot defaults, this means using the default icon state. * for new rendering, this means using the fallback state on the slot for a bodytype. */ var/datum/bodytypes/worn_bodytypes_fallback = BODYTYPES_ALL /// bodytypes that are implemented. Anything not in here is converted to default, if slot fallback state is unavailable. var/datum/bodytypes/worn_bodytypes = BODYTYPES(BODYTYPE_DEFAULT) /// bodytypes that just skip rendering (i hate teshari) var/datum/bodytypes/worn_bodytypes_invisible = BODYTYPES_NONE /// worn rendering flags var/worn_render_flags = WORN_RENDER_INHAND_ALLOW_DEFAULT | WORN_RENDER_SLOT_ALLOW_DEFAULT //? support for adminbus /// vv only; slot id to icon; worn_x_dimension and worn_y_dimension will be used in this case. VAR_PRIVATE/list/worn_icon_override /// vv only; slot id to state VAR_PRIVATE/list/worn_state_override /// vv only; set to override layer VAR_PRIVATE/worn_layer_override /obj/item/Initialize(mapload) . = ..() CONSTRUCT_BODYTYPES(worn_bodytypes) CONSTRUCT_BODYTYPES(worn_bodytypes_invisible) CONSTRUCT_BODYTYPES(worn_bodytypes_fallback) /** * update our worn icon if we can */ /obj/item/proc/update_worn_icon() if(!worn_slot) return // acceptable var/mob/M = get_worn_mob() ASSERT(M) // not acceptable if(held_index) M.update_inv_hand(held_index) return switch(worn_slot) if(SLOT_ID_BACK) M.update_inv_back() if(SLOT_ID_BELT) M.update_inv_belt() if(SLOT_ID_GLASSES) M.update_inv_glasses() if(SLOT_ID_GLOVES) M.update_inv_gloves() if(SLOT_ID_HANDCUFFED) M.update_inv_handcuffed() if(SLOT_ID_HANDS) CRASH("why did we go here when we should have short-circuited at the held_index check?") if(SLOT_ID_HEAD) M.update_inv_head() if(SLOT_ID_LEFT_EAR, SLOT_ID_RIGHT_EAR) M.update_inv_ears() if(SLOT_ID_MASK) M.update_inv_wear_mask() if(SLOT_ID_SHOES) M.update_inv_shoes() if(SLOT_ID_SUIT) M.update_inv_wear_suit() if(SLOT_ID_SUIT_STORAGE) M.update_inv_s_store() if(SLOT_ID_UNIFORM) M.update_inv_w_uniform() if(SLOT_ID_WORN_ID) M.update_inv_wear_id() /** * Renders either a list, or a single image or mutable appearance of what we should be applied to a mob with. * * @params * * M - the mob we're rendering * * slot_id_or_hand_index - the slot ID or numerical held index we're in * * bodytype - the effective bodytype */ /obj/item/proc/render_mob_appearance(mob/M, slot_id_or_hand_index, bodytype = BODYTYPE_DEFAULT) // SHOULD_NOT_OVERRIDE(TRUE) // if you think you need to, rethink. // todo: eh reevaluate later // determine if in hands var/inhands = isnum(slot_id_or_hand_index)? slot_id_or_hand_index : null var/datum/inventory_slot/slot_meta // resolve slot if(inhands) slot_meta = resolve_inventory_slot((slot_id_or_hand_index % 2)? /datum/inventory_slot/abstract/hand/left : /datum/inventory_slot/abstract/hand/right) else slot_meta = resolve_inventory_slot(slot_id_or_hand_index) var/list/resolved = resolve_worn_assets(M, slot_meta, inhands, bodytype) var/rendered = list(_render_mob_appearance(M, slot_meta, inhands, bodytype, resolved[WORN_DATA_ICON], resolved[WORN_DATA_STATE], resolved[WORN_DATA_LAYER], resolved [WORN_DATA_SIZE_X], resolved[WORN_DATA_SIZE_Y], resolved[WORN_DATA_ALIGN_Y])) SEND_SIGNAL(M, COMSIG_CARBON_UPDATING_OVERLAY, rendered, CARBON_APPEARANCE_UPDATE_CLOTHING) return rendered[1] /obj/item/proc/_render_mob_appearance(mob/M, datum/inventory_slot/slot_meta, inhands, bodytype, icon_used, state_used, layer_used, dim_x, dim_y, align_y) SHOULD_NOT_OVERRIDE(TRUE) // if you think you need to, rethink. PRIVATE_PROC(TRUE) // if you think you need to call this, rethink. var/list/additional = render_additional(M, icon_used, state_used, layer_used, dim_x, dim_y, align_y, bodytype, inhands, slot_meta) // todo: signal with (args, add) // todo: args' indices should be defines var/no_render = inhands? (worn_render_flags & WORN_RENDER_INHAND_NO_RENDER) : ((worn_render_flags & WORN_RENDER_SLOT_NO_RENDER) || worn_bodytypes_invisible?.contains(bodytype)) var/mutable_appearance/MA // worn_state_guard makes us not render if we'd render the same as in-inventory icon. if(no_render) // don't bother return additional MA = mutable_appearance(icon_used, state_used, layer_used, FLOAT_PLANE) // temporary - until coloration MA.color = color MA = center_appearance(MA, dim_x, dim_y) MA.pixel_y += align_y MA = render_apply_overlays(MA, bodytype, inhands, slot_meta, icon_used) MA = render_apply_blood(MA, bodytype, inhands, slot_meta, icon_used) MA = render_apply_custom(M, MA, bodytype, inhands, slot_meta, icon_used, align_y) return length(additional)? (additional + MA) : MA /** * override to apply custom stuff to rendering; called last * * icon/icon state/layer information is included in the mutable appearance */ /obj/item/proc/render_apply_custom(mob/M, mutable_appearance/MA, bodytype, inhands, datum/inventory_slot/slot_meta, icon_used, align_y) return MA /** * override to determine how we apply blood overlays to rendering * * icon/icon state/layer information is included in the mutable appearance */ /obj/item/proc/render_apply_blood(mutable_appearance/MA, bodytype, inhands, datum/inventory_slot/slot_meta, icon_used) return MA /** * override to apply overlays to our current mutable appearance; called first */ /obj/item/proc/render_apply_overlays(mutable_appearance/MA, bodytype, inhands, datum/inventory_slot/slot_meta, icon_used) if(addblends) var/mutable_appearance/adding = mutable_appearance(icon = MA.icon, icon_state = addblends) adding.blend_mode = BLEND_ADD MA.add_overlay(adding) return MA /** * override to include additional appearances while rendering */ /obj/item/proc/render_additional(mob/M, icon/icon_used, state_used, layer_used, dim_x, dim_y, align_y, bodytype, inhands, datum/inventory_slot/slot_meta) RETURN_TYPE(/list) return list() /** * returns a tuple of (icon, state, layer, size_x, size_y) * * @params * - M - mob putting us on; optional * - slot_meta - inventory slot datum * - inhands - if we're going to inhands * - bodytype - bodytype in question */ /obj/item/proc/resolve_worn_assets(mob/M, datum/inventory_slot/slot_meta, inhands, bodytype) if(istext(slot_meta)) slot_meta = resolve_inventory_slot(slot_meta) var/list/data = new /list(WORN_DATA_LIST_SIZE) //? state ; item_state_slots --> (worn_state | inhand_state) --> item_state --> icon_state data[WORN_DATA_STATE] = resolve_legacy_state(M, slot_meta, inhands, bodytype) //? icon, size //* icon_override if(icon_override) data[WORN_DATA_ICON] = icon_override switch(slot_meta.id) if(SLOT_ID_LEFT_HAND) data[WORN_DATA_STATE] += "_l" if(SLOT_ID_RIGHT_HAND) data[WORN_DATA_STATE] += "_r" if(SLOT_ID_LEFT_EAR) data[WORN_DATA_STATE] += "_l" if(SLOT_ID_RIGHT_EAR) data[WORN_DATA_STATE] += "_l" data[WORN_DATA_SIZE_X] = worn_x_dimension data[WORN_DATA_SIZE_Y] = worn_y_dimension //* species-specific sprite sheets else if(length(sprite_sheets) && !inhands && (sprite_sheets[bodytype_to_string(bodytype)])) data[WORN_DATA_ICON] = sprite_sheets[bodytype_to_string(bodytype)] data[WORN_DATA_SIZE_X] = WORLD_ICON_SIZE data[WORN_DATA_SIZE_Y] = WORLD_ICON_SIZE //* slot-specific sprite sheets else if(item_icons?[slot_meta.id]) data[WORN_DATA_ICON] = item_icons[slot_meta.id] data[WORN_DATA_SIZE_X] = worn_x_dimension data[WORN_DATA_SIZE_Y] = worn_y_dimension //* item default_worn_icon override else if(default_worn_icon && !inhands) // todo: rework data[WORN_DATA_ICON] = default_worn_icon data[WORN_DATA_SIZE_X] = worn_x_dimension data[WORN_DATA_SIZE_Y] = worn_y_dimension //* inventory slot defaults else if(inhands? (worn_render_flags & WORN_RENDER_INHAND_ALLOW_DEFAULT) : (worn_render_flags & WORN_RENDER_SLOT_ALLOW_DEFAULT)) var/list/resolved = slot_meta.resolve_default_assets(bodytype, data[WORN_DATA_STATE], M, src, inhand_default_type) if(!resolved && (bodytype != BODYTYPE_DEFAULT) && worn_bodytypes_fallback?.contains(bodytype)) // attempt 2 - use fallback if available if(!(slot_meta.handle_worn_fallback(bodytype, data))) // attempt 3 - convert to default if specified to convert resolved = slot_meta.resolve_default_assets(BODYTYPE_DEFAULT, data[WORN_DATA_STATE], M, src, inhand_default_type) if(resolved) data[WORN_DATA_ICON] = resolved[1] data[WORN_DATA_SIZE_X] = resolved[2] data[WORN_DATA_SIZE_Y] = resolved[3] //* Now, the actual intended render system. if(!data[WORN_DATA_ICON]) // grab icon based on priority if(!inhands && !worn_bodytypes?.contains(bodytype) && worn_bodytypes_fallback?.contains(bodytype) && slot_meta.handle_worn_fallback(bodytype, data)) // special: if bodytypes isn't in, and species has fallback // .. well don't do anything as handle_sprite_fallback will write to the data list. else if(inhands && inhand_icon) data[WORN_DATA_ICON] = inhand_icon data[WORN_DATA_SIZE_X] = inhand_x_dimension data[WORN_DATA_SIZE_Y] = inhand_y_dimension data[WORN_DATA_ALIGN_Y] = inhand_mob_y_align data[WORN_DATA_STATE] = resolve_worn_state(inhands, (worn_render_flags & WORN_RENDER_SLOT_USE_PLURAL) ?(slot_meta.render_key_plural || slot_meta.render_key) : slot_meta.render_key, bodytype) else if(!inhands && worn_icon) data[WORN_DATA_ICON] = worn_icon data[WORN_DATA_SIZE_X] = worn_x_dimension data[WORN_DATA_SIZE_Y] = worn_y_dimension data[WORN_DATA_ALIGN_Y] = worn_mob_y_align data[WORN_DATA_STATE] = resolve_worn_state(inhands, (worn_render_flags & WORN_RENDER_SLOT_USE_PLURAL)? (slot_meta.render_key_plural || slot_meta.render_key) : slot_meta.render_key, bodytype) else data[WORN_DATA_ICON] = icon data[WORN_DATA_SIZE_X] = icon_x_dimension data[WORN_DATA_SIZE_Y] = icon_y_dimension data[WORN_DATA_ALIGN_Y] = icon_mob_y_align data[WORN_DATA_STATE] = resolve_worn_state(inhands, (worn_render_flags & WORN_RENDER_SLOT_USE_PLURAL)? (slot_meta.render_key_plural || slot_meta.render_key) : slot_meta.render_key, bodytype) //? layer ; worn_layer --> slot defaults for the item in question data[WORN_DATA_LAYER] = worn_layer_override || slot_meta.resolve_default_layer(bodytype, M, src) //* Handle overrides if(LAZYACCESS(worn_icon_override, slot_meta.id)) data[WORN_DATA_ICON] = worn_icon_override[slot_meta.id] // if you fuck this up, Skill Issue. We have to align somehow. data[WORN_DATA_SIZE_X] = icon_x_dimension data[WORN_DATA_SIZE_Y] = icon_y_dimension if(LAZYACCESS(worn_state_override, slot_meta.id)) data[WORN_DATA_STATE] = worn_state_override[slot_meta.id] return data /obj/item/proc/debug_worn_assets(slot_or_id, mob/M = get_worn_mob(), bodytype) var/mob/living/carbon/human/H = ishuman(M)? M : null var/datum/inventory_slot/slot_meta if(isnull(slot_or_id)) slot_or_id = inv_slot_or_index if(isnum(slot_or_id)) slot_meta = resolve_inventory_slot((slot_or_id % 2)? /datum/inventory_slot/abstract/hand/left : /datum/inventory_slot/abstract/hand/right) else slot_meta = resolve_inventory_slot(slot_or_id) if(isnull(bodytype) && H) bodytype = H.species.get_effective_bodytype(H, src, slot_meta) . = resolve_worn_assets(M, slot_meta, isnum(slot_or_id), bodytype) .[WORN_DATA_ICON] = "[.[WORN_DATA_ICON]]" // todo: remove, aka get rid of fucking uniform _s state /obj/item/proc/resolve_legacy_state(mob/M, datum/inventory_slot/slot_meta, inhands, bodytype) return (item_state_slots?[slot_meta.id]) || (inhands? inhand_state : worn_state) || item_state || icon_state /obj/item/proc/resolve_worn_state(inhands, slot_key, bodytype) // PRIVATE_PROC(TRUE) if(inhands) return "[base_worn_state(inhands, slot_key, bodytype)][(worn_render_flags & WORN_RENDER_INHAND_ONE_FOR_ALL)? "_all" : "_[slot_key]"]" return "[base_worn_state(inhands, slot_key, bodytype)][(worn_render_flags & WORN_RENDER_SLOT_ONE_FOR_ALL)? "_all" : "_[slot_key]"][((bodytype != BODYTYPE_DEFAULT) && worn_bodytypes?.contains(bodytype))? "_[bodytype_to_string(bodytype)]" : ""]" /obj/item/proc/base_worn_state(inhands, slot_key, bodytype) if(inhands) return inhand_state || icon_state return worn_state || icon_state