Files
Bubberstation/code/datums/components/reskinnable_atom.dm
Aliceee2ch a7901ff365 Fixed color matrix not applying in reskinnable item preview (#95919)
## About The Pull Request

#94466 introduced changes for reskinnable items that broke recoloring
for kmaga alt style, this pr supposedly fixes it??

## Changelog

🆑
fix: Fixed color matrix not applying in reskinnable item preview.
/🆑
2026-05-03 16:24:10 +02:00

287 lines
12 KiB
Plaintext

/**
* ### Atom skin singleton datum
*
* Simple datum which holds information about a skin that can be applied to an atom.
*/
/datum/atom_skin
abstract_type = /datum/atom_skin
/// Required, name shown in the radial menu
var/preview_name
/// If true, changing the reskin also changes the base_icon_state of the atom
var/change_base_icon_state = FALSE
/// If true, changing the reskin also changes the inhand_icon_state of the atom
var/change_inhand_icon_state = FALSE
/// If true, changing the reskin also changes the worn_icon_state of the atom
var/change_worn_icon_state = TRUE
/// If true, unset vars are reset to their original values when applying this skin
var/reset_missing = TRUE
/// Optional, name to change the atom to when applied
var/new_name
/// Optional, description to change the atom to when applied
var/new_desc
/// Optional, icon to change the atom to when applied
var/new_icon
/// Optional, icon_state to change the atom to when applied
var/new_icon_state
/// Specifies the icon state for the atom's appearance in hand. Should appear in both new_lefthand_file and new_righthand_file.
var/new_inhand_icon_state
/// Optional, specifies the left hand inhand icon file. Don't forget to set the right hand file as well.
var/new_lefthand_file
/// Optional, specifies the right hand inhand icon file. Don't forget to set the left hand file as well.
var/new_righthand_file
/// Optional, specifies the worn icon file.
var/new_worn_icon
/// When set true, will allow concrete subtypes of abstract subtypes (such as for organizational purposes) to be selectable as reskins in the loadout menu.
var/allow_all_subtypes_in_loadout
/// Mandatory for GAGs items. The path to the greyscale item this is to be applied to.
var/atom/greyscale_item_path
/// Auto populated. The greyscale_config from greyscale_item_path.
VAR_FINAL/greyscale_config
/// Auto populated. The greyscale_colors from greyscale_item_path.
VAR_FINAL/greyscale_colors
/datum/atom_skin/New()
. = ..()
if(isnull(greyscale_item_path))
return
// Populate the fields required for GAGS previews
greyscale_config = greyscale_item_path::greyscale_config
greyscale_colors = greyscale_item_path::greyscale_colors
// The icon isn't 'new'. So we don't make an extra icon in map_icons.
if(greyscale_item_path::post_init_icon_state == new_icon_state)
new_icon_state = null
/// Returns the correct preview icon state for this atom skin, whether it be a map_preview or a normal icon. This is assumed from our var population above.
/datum/atom_skin/proc/get_preview_icon_state()
if(isnull(greyscale_item_path)) // Not a GAGs icon
return new_icon_state
if(isnull(new_icon_state)) // The base GAGS item icon
return "[greyscale_item_path]"
return "[greyscale_item_path]--[new_icon_state]" // A GAGs reskin of the item icon
/**
* Applies all relevant skin changes to the given atom
* Can be overridden to add additional behavior, such as registering signals or altering other vars.
*
* * apply_to: The atom to apply the skin to
*/
/datum/atom_skin/proc/apply(atom/apply_to, mob/user)
SHOULD_CALL_PARENT(TRUE)
if(!HAS_TRAIT(apply_to, TRAIT_WAS_RENAMED))
APPLY_VAR_OR_RESET_INITIAL(apply_to, name, new_name, reset_missing)
APPLY_VAR_OR_RESET_INITIAL(apply_to, desc, new_desc, reset_missing)
if(!apply_to.greyscale_config) // Never want to be resetting a GAGS icon back to initial.
APPLY_VAR_OR_RESET_INITIAL(apply_to, icon, new_icon, reset_missing)
APPLY_VAR_OR_RESET_TO(apply_to, icon_state, new_icon_state, reset_missing, initial(apply_to.post_init_icon_state) || initial(apply_to.icon_state))
if(change_base_icon_state)
APPLY_VAR_OR_RESET_INITIAL(apply_to, base_icon_state, new_icon_state, reset_missing)
if(isitem(apply_to))
var/obj/item/item_apply_to = apply_to
if(!item_apply_to.greyscale_config_worn)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, worn_icon, new_worn_icon, reset_missing)
if(!item_apply_to.greyscale_config_inhand_left)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, lefthand_file, new_lefthand_file, reset_missing)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, righthand_file, new_righthand_file, reset_missing)
if(change_worn_icon_state)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, worn_icon_state, new_icon_state, reset_missing)
if(change_inhand_icon_state || new_inhand_icon_state)
APPLY_VAR_OR_RESET_INITIAL(item_apply_to, inhand_icon_state, new_inhand_icon_state || new_icon_state, reset_missing)
/**
* Resets all changes this skin would have made to the given atom
* Does not verify that the skin was actually applied to the atom beforehand.
* Can be overridden to add additional behavior, such as unregistering signals or altering other vars.
*
* * clear_from: The atom to clear the skin from
*/
/datum/atom_skin/proc/clear_skin(atom/clear_from)
SHOULD_CALL_PARENT(TRUE)
RESET_INITIAL_IF_SET(clear_from, name, new_name)
RESET_INITIAL_IF_SET(clear_from, desc, new_desc)
var/is_greyscale = clear_from.greyscale_config || clear_from.greyscale_colors
if(!is_greyscale)
RESET_INITIAL_IF_SET(clear_from, icon, new_icon)
RESET_TO_IF_SET(clear_from, icon_state, new_icon_state, initial(clear_from.post_init_icon_state) || initial(clear_from.icon_state))
if(change_base_icon_state)
RESET_INITIAL_IF_SET(clear_from, base_icon_state, new_icon_state)
if(isitem(clear_from))
var/obj/item/item_clear_from = clear_from
RESET_INITIAL_IF_SET(item_clear_from, worn_icon, new_worn_icon)
RESET_INITIAL_IF_SET(item_clear_from, inhand_icon_state, change_inhand_icon_state ? new_inhand_icon_state : new_icon_state)
if(!is_greyscale)
RESET_INITIAL_IF_SET(item_clear_from, worn_icon_state, new_icon_state)
RESET_INITIAL_IF_SET(item_clear_from, lefthand_file, new_lefthand_file)
RESET_INITIAL_IF_SET(item_clear_from, righthand_file, new_righthand_file)
/// Gets a preview image for this skin based on the given atom's icon and icon_state
/datum/atom_skin/proc/get_preview_icon(atom/for_atom)
return image(
icon = new_icon || for_atom.icon,
icon_state = new_icon_state || for_atom.icon_state,
)
/**
* ### Reskinnable atoms
*
* Simple component which lets an atom be alt-clicked to open a radial menu to choose a new skin to apply.
*/
/datum/component/reskinable_item
dupe_mode = COMPONENT_DUPE_SELECTIVE
/// Base reskin type to pull options from - all subtypes except those blacklisted are valid options
VAR_PRIVATE/base_reskin_type
/// If TRUE, the reskin option is infinite-use. If FALSE, the component is deleted on use (so you're stuck with that skin).
VAR_PRIVATE/infinite_reskin = FALSE
/// List of subtypes of /datum/atom_skin that are not allowed to be used for this item
VAR_PRIVATE/list/blacklisted_subtypes
/// Currently applied skin preview_name
VAR_PRIVATE/current_skin
/datum/component/reskinable_item/Initialize(base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes)
if(!isatom(parent) || isarea(parent))
return COMPONENT_INCOMPATIBLE
var/atom/atom_parent = parent
#ifdef UNIT_TESTS
if(atom_parent.greyscale_config && (atom_parent.type in GLOB.all_loadout_datums)) // We only care about these when they're in the loadout
var/datum/atom_skin/reskin_type = base_reskin_type
if(reskin_type && isnull(reskin_type::greyscale_item_path)) // greyscale_item_path unset
stack_trace("[type] added to a greyscale item without setting the greyscale_item_path! In [reskin_type], add 'greyscale_item_path = [atom_parent::type]'.")
else if(atom_parent.type != reskin_type::greyscale_item_path) // greyscale_item_path set but doesn't match the item it's being added to
stack_trace("[type] added to an invalid item type, [atom_parent.type]. [reskin_type] is set up to only be added to: [reskin_type::greyscale_item_path]. \
Either fix its greyscale_item_path if this is incorrect, or apply a different skin.")
#endif
src.base_reskin_type = base_reskin_type
src.infinite_reskin = infinite
src.blacklisted_subtypes = blacklisted_subtypes
if(initial_skin)
set_skin_by_name(initial_skin)
var/list/reskin_options = get_skins_by_name()
if(length(reskin_options) <= 1) // Check that we actually have reskin options - if not there's no point to existing past this point.
return COMPONENT_REDUNDANT
atom_parent.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
/datum/component/reskinable_item/RegisterWithParent()
RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(on_click_alt_reskin))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(add_tags))
RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(add_context))
/datum/component/reskinable_item/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_CLICK_ALT)
UnregisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS)
UnregisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM)
/datum/component/reskinable_item/CheckDupeComponent(datum/component/comp, base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes)
// Always absorb added components
src.base_reskin_type = base_reskin_type
src.infinite_reskin = infinite
src.blacklisted_subtypes = blacklisted_subtypes
if(initial_skin)
set_skin_by_name(initial_skin)
var/list/reskin_options = get_skins_by_name()
if(length(reskin_options) <= 1) // Check that we actually have reskin options - if not there's no point to existing past this point.
qdel(src)
return TRUE
/datum/component/reskinable_item/proc/get_skins_by_name()
var/list/reskin_options = list()
for(var/datum/atom_skin/reskin_option as anything in valid_subtypesof(base_reskin_type) - blacklisted_subtypes)
reskin_options[reskin_option::preview_name] = reskin_option
return reskin_options
/datum/component/reskinable_item/proc/set_skin_by_name(input_name, mob/user)
var/list/reskin_options = get_skins_by_name()
var/list/atom_skins = get_atom_skins()
if(current_skin)
var/datum/atom_skin/previous_skin = atom_skins[reskin_options[current_skin]]
previous_skin.clear_skin(parent, user)
if(input_name)
var/datum/atom_skin/reskin_to_apply = atom_skins[reskin_options[input_name]]
reskin_to_apply.apply(parent, user)
current_skin = input_name
var/atom/atom_parent = parent
atom_parent.update_appearance()
if(isitem(parent))
var/obj/item/item_parent = parent
item_parent.update_slot_icon()
SEND_SIGNAL(parent, COMSIG_OBJ_RESKIN, input_name)
/datum/component/reskinable_item/proc/add_context(atom/source, list/context, obj/item/held_item, mob/user)
SIGNAL_HANDLER
context[SCREENTIP_CONTEXT_ALT_LMB] = "Reskin"
return CONTEXTUAL_SCREENTIP_SET
/datum/component/reskinable_item/proc/add_tags(atom/source, mob/user, list/tags)
SIGNAL_HANDLER
tags["reskinnable"] = "This item is able to be reskinned! Alt-Click to do so!"
/// Called when alt clicked and the item has unique reskin options
/datum/component/reskinable_item/proc/on_click_alt_reskin(datum/source, mob/user)
SIGNAL_HANDLER
if(!user.can_perform_action(parent, NEED_DEXTERITY))
return NONE
INVOKE_ASYNC(src, PROC_REF(reskin_obj), user)
return CLICK_ACTION_SUCCESS
/**
* Reskins object based on a user's choice
*
* Arguments:
* * user The mob choosing a reskin option
*/
/datum/component/reskinable_item/proc/reskin_obj(mob/user)
var/atom/atom_parent = parent
var/list/items = list()
var/list/atom_skins = get_atom_skins()
for(var/reskin_name, reskin_typepath in get_skins_by_name())
var/datum/atom_skin/reskin = atom_skins[reskin_typepath]
items[reskin_name] = reskin.get_preview_icon(atom_parent)
sort_list(items)
var/pick = show_radial_menu(user, parent, items, custom_check = CALLBACK(src, PROC_REF(check_reskin_menu), user), radius = 38, require_near = TRUE)
if(!pick || !items[pick])
return
set_skin_by_name(pick, user)
to_chat(user, span_info("[parent] is now skinned as '[pick].'"))
if(!infinite_reskin)
qdel(src)
/**
* Checks if we are allowed to interact with a radial menu for reskins
*
* Arguments:
* * user The mob interacting with the menu
*/
/datum/component/reskinable_item/proc/check_reskin_menu(mob/user)
if(QDELETED(parent))
return FALSE
if(user.incapacitated)
return FALSE
return TRUE