diff --git a/code/datums/greyscale/_greyscale_config.dm b/code/datums/greyscale/_greyscale_config.dm
index 71bd56fbcb1..caf27af0139 100644
--- a/code/datums/greyscale/_greyscale_config.dm
+++ b/code/datums/greyscale/_greyscale_config.dm
@@ -1,4 +1,7 @@
/datum/greyscale_config
+ /// User friendly name used in the debug menu
+ var/name
+
/// Reference to the json config file
var/json_config
@@ -115,18 +118,31 @@
expected_colors = length(color_groups)
/// Actually create the icon and color it in, handles caching
-/datum/greyscale_config/proc/Generate(color_string, list/render_steps)
+/datum/greyscale_config/proc/Generate(color_string)
var/key = color_string
var/icon/new_icon = icon_cache[key]
- if(new_icon && !render_steps)
- var/icon/output = icon(new_icon)
- output.Scale(width, height)
- return output
- var/list/colors = ParseColorString(color_string)
+ if(new_icon)
+ return icon(new_icon)
+
+ var/icon/icon_bundle = GenerateBundle(color_string)
+
+ // This block is done like this because generated icons are unable to be scaled before getting added to the rsc
+ icon_bundle = fcopy_rsc(icon_bundle)
+ icon_bundle = icon(icon_bundle)
+ icon_bundle.Scale(width, height)
+ icon_bundle = fcopy_rsc(icon_bundle)
+
+ icon_cache[key] = icon_bundle
+ var/icon/output = icon(icon_bundle)
+ return output
+
+/// Handles the actual icon manipulation to create the spritesheet
+/datum/greyscale_config/proc/GenerateBundle(list/colors, list/render_steps)
+ if(!istype(colors))
+ colors = ParseColorString(colors)
if(length(colors) != expected_colors)
CRASH("[DebugName()] expected [expected_colors] color arguments but only received [length(colors)]")
- // We could do this all in one loop but it's easier to debug this way
var/list/generated_icons = list()
for(var/icon_state in icon_states)
var/icon/generated_icon = GenerateLayerGroup(colors, icon_states[icon_state], render_steps)
@@ -135,15 +151,11 @@
generated_icon.GetPixel(1, 1)
generated_icons[icon_state] = generated_icon
- var/icon/icon_bundle = new
+ var/icon/icon_bundle = icon('icons/Testing/greyscale_error.dmi')
for(var/icon_state in generated_icons)
icon_bundle.Insert(generated_icons[icon_state], icon_state)
- icon_bundle = fcopy_rsc(icon_bundle)
- icon_cache[key] = icon_bundle
- var/icon/output = icon(icon_bundle)
- output.Scale(width, height)
- return output
+ return icon_bundle
/// Internal recursive proc to handle nested layer groups
/datum/greyscale_config/proc/GenerateLayerGroup(list/colors, list/group, list/render_steps)
@@ -171,7 +183,7 @@
var/list/debug_steps = list()
output["steps"] = debug_steps
- output["icon"] = Generate(colors, debug_steps)
+ output["icon"] = GenerateBundle(colors, debug_steps)
return output
/datum/greyscale_config/proc/ParseColorString(color_string)
diff --git a/code/datums/greyscale/greyscale_configs.dm b/code/datums/greyscale/greyscale_configs.dm
index 45280f210ab..02604792b4f 100644
--- a/code/datums/greyscale/greyscale_configs.dm
+++ b/code/datums/greyscale/greyscale_configs.dm
@@ -1,159 +1,204 @@
/datum/greyscale_config/canister
+ name = "Default Canister"
icon_file = 'icons/obj/atmospherics/canisters.dmi'
json_config = 'code/datums/greyscale/json_configs/canister_default.json'
/datum/greyscale_config/canister/base
+ name = "Base Canister Style"
json_config = 'code/datums/greyscale/json_configs/canister_base.json'
/datum/greyscale_config/canister/post_effects
+ name = "Canister Post-Effects"
json_config = 'code/datums/greyscale/json_configs/canister_post_effects.json'
/datum/greyscale_config/canister/stripe
+ name = "Single Striped Canister"
json_config = 'code/datums/greyscale/json_configs/canister_stripe.json'
/datum/greyscale_config/canister/double_stripe
+ name = "Double Striped Canister"
json_config = 'code/datums/greyscale/json_configs/canister_double_stripe.json'
/datum/greyscale_config/canister/hazard
+ name = "Hazard Striped Canister"
json_config = 'code/datums/greyscale/json_configs/canister_hazard.json'
/datum/greyscale_config/prototype_canister
+ name = "Prototype Canister"
icon_file = 'icons/obj/atmospherics/prototype_canister.dmi'
json_config = 'code/datums/greyscale/json_configs/canister_proto.json'
/datum/greyscale_config/carp
+ name = "Space Carp"
icon_file = 'icons/mob/carp.dmi'
json_config = 'code/datums/greyscale/json_configs/carp.json'
/datum/greyscale_config/carp/disk_mouth
+ name = "Space Carp, Disk in Mouth"
json_config = 'code/datums/greyscale/json_configs/carp_disk_mouth.json'
/datum/greyscale_config/wirecutters
+ name = "Wirecutters"
icon_file = 'icons/obj/tools.dmi'
json_config = 'code/datums/greyscale/json_configs/wirecutters.json'
/datum/greyscale_config/screwdriver
+ name = "Screwdriver"
icon_file = 'icons/obj/tools.dmi'
json_config = 'code/datums/greyscale/json_configs/screwdriver.json'
/datum/greyscale_config/screwdriver_inhand_left
+ name = "Held Screwdriver, Left"
icon_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json'
/datum/greyscale_config/screwdriver_inhand_right
+ name = "Held Screwdriver, Right"
icon_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json'
/datum/greyscale_config/screwdriver_belt
+ name = "Belt Worn Screwdriver"
icon_file = 'icons/obj/clothing/belt_overlays.dmi'
json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json'
/datum/greyscale_config/jumpsuit
+ name = "Jumpsuit"
icon_file = 'icons/obj/clothing/under/color.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit.json'
/datum/greyscale_config/jumpsuit_worn
+ name = "Worn Jumpsuit"
icon_file = 'icons/mob/clothing/under/color.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_worn.json'
/datum/greyscale_config/jumpsuit_inhand_left
+ name = "Held Jumpsuit, Left"
icon_file = 'icons/mob/inhands/clothing_lefthand.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_inhand.json'
/datum/greyscale_config/jumpsuit_inhand_right
+ name = "Held Jumpsuit, Right"
icon_file = 'icons/mob/inhands/clothing_righthand.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_inhand.json'
/datum/greyscale_config/jumpsuit_prison
+ name = "Prison Jumpsuit"
icon_file = 'icons/obj/clothing/under/color.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison.json'
/datum/greyscale_config/jumpsuit_prison_worn
+ name = "Worn Prison Jumpsuit"
icon_file = 'icons/mob/clothing/under/color.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_worn.json'
/datum/greyscale_config/jumpsuit_prison_inhand_left
+ name = "Held Prison Jumpsuit, Left"
icon_file = 'icons/mob/inhands/clothing_lefthand.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_inhand.json'
/datum/greyscale_config/jumpsuit_prison_inhand_right
+ name = "Held Prison Jumpsuit, Right"
icon_file = 'icons/mob/inhands/clothing_righthand.dmi'
json_config = 'code/datums/greyscale/json_configs/jumpsuit_prison_inhand.json'
/datum/greyscale_config/pda
+ name = "PDA"
icon_file = 'icons/obj/pda.dmi'
json_config = 'code/datums/greyscale/json_configs/pda.json'
/datum/greyscale_config/pda/chaplain
+ name = "Chaplain PDA"
json_config = 'code/datums/greyscale/json_configs/pda_chaplain.json'
/datum/greyscale_config/pda/clown
+ name = "Clown PDA"
json_config = 'code/datums/greyscale/json_configs/pda_clown.json'
/datum/greyscale_config/pda/head
+ name = "Head PDA"
json_config = 'code/datums/greyscale/json_configs/pda_head.json'
/datum/greyscale_config/pda/mime
+ name = "Mime PDA"
json_config = 'code/datums/greyscale/json_configs/pda_mime.json'
/datum/greyscale_config/pda/stripe_split
+ name = "Split Stripe PDA"
json_config = 'code/datums/greyscale/json_configs/pda_stripe_split.json'
/datum/greyscale_config/pda/stripe_thick
+ name = "Thick Stripe PDA"
json_config = 'code/datums/greyscale/json_configs/pda_stripe_thick.json'
/datum/greyscale_config/pda/stripe_thick/head
+ name = "Head Thick Stripe PDA"
json_config = 'code/datums/greyscale/json_configs/pda_stripe_thick_head.json'
/datum/greyscale_config/sneakers
+ name = "Sneakers"
icon_file = 'icons/obj/clothing/shoes.dmi'
json_config = 'code/datums/greyscale/json_configs/sneakers.json'
/datum/greyscale_config/sneakers_worn
+ name = "Worn Sneakers"
icon_file = 'icons/mob/clothing/feet.dmi'
json_config = 'code/datums/greyscale/json_configs/sneakers_worn.json'
/datum/greyscale_config/sneakers_orange
+ name = "Orange Sneakers"
icon_file = 'icons/obj/clothing/shoes.dmi'
json_config = 'code/datums/greyscale/json_configs/sneakers_orange.json'
/datum/greyscale_config/sneakers_orange_worn
+ name = "Worn Orange Sneakers"
icon_file = 'icons/mob/clothing/feet.dmi'
json_config = 'code/datums/greyscale/json_configs/sneakers_orange_worn.json'
/datum/greyscale_config/sneakers_wheelys
+ name = "Wheeled Sneakers"
icon_file = 'icons/obj/clothing/shoes.dmi'
json_config = 'code/datums/greyscale/json_configs/sneakers_wheelys.json'
/datum/greyscale_config/sneakers_marisa
+ name = "Marisa Sneakers"
icon_file = 'icons/obj/clothing/shoes.dmi'
json_config = 'code/datums/greyscale/json_configs/sneakers_marisa.json'
/datum/greyscale_config/circuit
+ name = "Circuit Board"
icon_file = 'icons/obj/module.dmi'
json_config = 'code/datums/greyscale/json_configs/circuit.json'
/datum/greyscale_config/sombrero
+ name = "Sombrero"
icon_file = 'icons/obj/clothing/head/sombrero.dmi'
json_config = 'code/datums/greyscale/json_configs/sombrero.json'
/datum/greyscale_config/sombrero/base
+ name = "Base Sombrero Style"
json_config = 'code/datums/greyscale/json_configs/sombrero_base.json'
/datum/greyscale_config/sombrero/lefthand
+ name = "Held Sombrero, Left"
json_config = 'code/datums/greyscale/json_configs/sombrero_lefthand.json'
/datum/greyscale_config/sombrero/base_lefthand
+ name = "Base Held Sombrero Style, Left"
json_config = 'code/datums/greyscale/json_configs/sombrero_base_lefthand.json'
/datum/greyscale_config/sombrero/righthand
+ name = "Held Sombrero, Right"
json_config = 'code/datums/greyscale/json_configs/sombrero_righthand.json'
/datum/greyscale_config/sombrero/base_righthand
+ name = "Base Held Sombrero Style, Right"
json_config = 'code/datums/greyscale/json_configs/sombrero_base_righthand.json'
/datum/greyscale_config/sombrero/worn
+ name = "Worn Sombrero"
json_config = 'code/datums/greyscale/json_configs/sombrero_worn.json'
/datum/greyscale_config/sombrero/base_worn
+ name = "Base Worn Sombrero Style"
json_config = 'code/datums/greyscale/json_configs/sombrero_base_worn.json'
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 4c0d3f13608..c255ca562f5 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -223,7 +223,8 @@
if(loc)
SEND_SIGNAL(loc, COMSIG_ATOM_CREATED, src) /// Sends a signal that the new atom `src`, has been created at `loc`
- update_greyscale()
+ if(greyscale_config && greyscale_colors)
+ update_greyscale()
//atom color stuff
if(color)
@@ -737,7 +738,7 @@
greyscale_colors = colors
if(!greyscale_config)
return
- if(update)
+ if(update && greyscale_config && greyscale_colors)
update_greyscale()
/// Checks if the greyscale config given is different and if so causes a greyscale icon update
@@ -745,13 +746,12 @@
if(greyscale_config == new_config)
return
greyscale_config = new_config
- if(update)
+ if(update && greyscale_config && greyscale_colors)
update_greyscale()
/// Checks if this atom uses the GAS system and if so updates the icon
/atom/proc/update_greyscale()
- if(greyscale_config && greyscale_colors)
- icon = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors)
+ icon = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors)
/**
* An atom we are buckled or is contained within us has tried to move
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index e5c0c4a0018..76a3eabf9a2 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -207,7 +207,6 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
for(var/path in actions_types)
new path(src)
actions_types = null
- update_item_greyscale()
if(force_string)
item_flags |= FORCE_STRING_OVERRIDE
@@ -255,7 +254,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
return
/// Checks if this atom uses the GAS system and if so updates the worn and inhand icons
-/obj/item/proc/update_item_greyscale()
+/obj/item/update_greyscale()
+ . = ..()
if(!greyscale_colors)
return
if(greyscale_config_worn)
@@ -264,7 +264,6 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
lefthand_file = SSgreyscale.GetColoredIconByType(greyscale_config_inhand_left, greyscale_colors)
if(greyscale_config_inhand_right)
righthand_file = SSgreyscale.GetColoredIconByType(greyscale_config_inhand_right, greyscale_colors)
- return
/obj/item/verb/move_to_top()
set name = "Move To Top"
diff --git a/code/game/objects/items/tools/screwdriver.dm b/code/game/objects/items/tools/screwdriver.dm
index 51ca8958907..e813a6f2bfd 100644
--- a/code/game/objects/items/tools/screwdriver.dm
+++ b/code/game/objects/items/tools/screwdriver.dm
@@ -25,6 +25,9 @@
drop_sound = 'sound/items/handling/screwdriver_drop.ogg'
pickup_sound = 'sound/items/handling/screwdriver_pickup.ogg'
sharpness = SHARP_POINTY
+ greyscale_config = /datum/greyscale_config/screwdriver
+ greyscale_config_inhand_left = /datum/greyscale_config/screwdriver_inhand_left
+ greyscale_config_inhand_right = /datum/greyscale_config/screwdriver_inhand_right
/// If the item should be assigned a random color
var/random_color = TRUE
/// List of possible random colors
@@ -46,12 +49,9 @@
/obj/item/screwdriver/Initialize()
if(random_color)
- set_greyscale_config(/datum/greyscale_config/screwdriver)
var/our_color = pick(screwdriver_colors)
set_greyscale_colors(list(screwdriver_colors[our_color]))
inhand_icon_state = null
- lefthand_file = SSgreyscale.GetColoredIconByType(/datum/greyscale_config/screwdriver_inhand_left, greyscale_colors)
- righthand_file = SSgreyscale.GetColoredIconByType(/datum/greyscale_config/screwdriver_inhand_right, greyscale_colors)
colored_belt_appearance = mutable_appearance(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/screwdriver_belt, greyscale_colors))
. = ..()
AddElement(/datum/element/eyestab)
diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm
index 182493e9216..2f7d4df7894 100644
--- a/code/modules/admin/greyscale_modify_menu.dm
+++ b/code/modules/admin/greyscale_modify_menu.dm
@@ -1,31 +1,68 @@
+/// The controller for the ui in charge of all runtime greyscale configuration/debug.
+/// If `Unlock()` is not called the menu is safe for players to use.
/datum/greyscale_modify_menu
+ /// The "owner" object of this menu, is usually the greyscale object being edited but that can be changed for specific uses of this menu
var/atom/target
+ /// The client that opened this menu
var/client/user
+ /// A keyed list of allowed configs in the form niceName:typepath
+ var/list/allowed_configs
+
+ /// A callback to control what happens when the user presses apply. Used mainly for if you want the menu to be used outside of vv.
+ var/datum/callback/apply_callback
+
+ /// The current config being previewed
var/datum/greyscale_config/config
+ /// A list of colors currently selected
var/list/split_colors
+ /// Collection of data for tgui to use in displaying everything
var/list/sprite_data
+ /// The sprite dir currently being shown
var/sprite_dir = SOUTH
+ /// The sprite icon state currently being shown
+ var/icon_state
+ /// Whether the full preview should be generated, with this FALSE only the final sprite is shown instead of all steps.
+ var/generate_full_preview = FALSE
-/datum/greyscale_modify_menu/New(atom/target, client/user)
+ /// Whether the menu is in the middle of refreshing the preview
+ var/refreshing = TRUE
+
+ /// Whether the menu is currently locked down to prevent abuse from players. Currently is only unlocked when opened from vv.
+ var/unlocked = FALSE
+
+/datum/greyscale_modify_menu/New(atom/target, client/user, list/allowed_configs, datum/callback/apply_callback, starting_icon_state="", starting_config, starting_colors)
src.target = target
src.user = user
+ src.apply_callback = apply_callback || CALLBACK(src, .proc/DefaultApply)
+ icon_state = starting_icon_state
- config = SSgreyscale.configurations["[target.greyscale_config]"]
- ReadColorsFromString(target.greyscale_colors)
+ var/current_config = "[starting_config]" || "[target?.greyscale_config]"
+ config = SSgreyscale.configurations[current_config]
+ if(!(current_config in allowed_configs))
+ config = SSgreyscale.configurations["[allowed_configs[pick(allowed_configs)]]"]
+
+ var/list/config_choices = list()
+ for(var/config_string in allowed_configs)
+ var/datum/greyscale_config/config = text2path("[config_string]")
+ config_choices[initial(config.name)] = config_string
+ src.allowed_configs = config_choices
+
+ ReadColorsFromString(starting_colors || target?.greyscale_colors)
+
+ if(target)
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/ui_close)
refresh_preview()
- RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/ui_close)
-
/datum/greyscale_modify_menu/Destroy()
target = null
user = null
return ..()
/datum/greyscale_modify_menu/ui_state(mob/user)
- return GLOB.admin_state
+ return GLOB.always_state
/datum/greyscale_modify_menu/ui_close()
qdel(src)
@@ -38,7 +75,7 @@
/datum/greyscale_modify_menu/ui_data(mob/user)
var/list/data = list()
- data["greyscale_config"] = "[config.type]"
+ data["greyscale_config"] = "[config.name]"
var/list/color_data = list()
data["colors"] = color_data
@@ -48,7 +85,11 @@
"value" = split_colors[i]
))
+ data["generate_full_preview"] = generate_full_preview
+ data["unlocked"] = unlocked
+ data["refreshing"] = refreshing
data["sprites_dir"] = dir2text(sprite_dir)
+ data["icon_state"] = icon_state
data["sprites"] = sprite_data
return data
@@ -63,26 +104,35 @@
"Choose a new greyscale configuration to use",
"Greyscale Modification Menu",
"[config.type]"
- ) as anything in SSgreyscale.configurations
+ ) as anything in allowed_configs
+ new_config = allowed_configs[new_config]
new_config = SSgreyscale.configurations[new_config]
if(!isnull(new_config) && config != new_config)
config = new_config
- refresh_preview()
+ queue_refresh()
if("load_config_from_string")
+ if(!(params["config_string"] in allowed_configs))
+ return
var/datum/greyscale_config/new_config = SSgreyscale.configurations[params["config_string"]]
if(!isnull(new_config) && config != new_config)
config = new_config
- refresh_preview()
+ queue_refresh()
+
+ if("toggle_full_preview")
+ if(!generate_full_preview && !unlocked)
+ return
+ generate_full_preview = !generate_full_preview
+ queue_refresh()
if("recolor")
var/index = text2num(params["color_index"])
split_colors[index] = lowertext(params["new_color"])
- refresh_preview()
+ queue_refresh()
if("recolor_from_string")
ReadColorsFromString(lowertext(params["color_string"]))
- refresh_preview()
+ queue_refresh()
if("pick_color")
var/group = params["color_index"]
@@ -94,20 +144,38 @@
) as color|null
if(new_color)
split_colors[group] = new_color
- refresh_preview()
+ queue_refresh()
+
+ if("select_icon_state")
+ var/new_icon_state = params["new_icon_state"]
+ if(!config.icon_states[new_icon_state])
+ return
+ icon_state = new_icon_state
+ queue_refresh()
if("apply")
- target.set_greyscale_config(config.type, update=FALSE)
- target.greyscale_colors = "" // We do this to force an update, in some cases it will think nothing changed when it should be refreshing
- target.set_greyscale_colors(split_colors)
+ apply_callback.Invoke(src)
if("refresh_file")
+ if(!unlocked)
+ return
+ if(length(GLOB.player_list) > 1)
+ var/check = alert(
+ user,
+{"Other players are connected to the server, are you sure you want to refresh all greyscale configurations?\n
+This is highly likely to cause a lag spike for a few seconds."},
+ "Refresh Greyscale Configurations",
+ "Yes",
+ "Cancel"
+ )
+ if(check != "Yes")
+ return
SSgreyscale.RefreshConfigsFromFile()
- refresh_preview()
+ queue_refresh()
if("change_dir")
sprite_dir = text2dir(params["new_sprite_dir"])
- refresh_preview()
+ queue_refresh()
/datum/greyscale_modify_menu/proc/ReadColorsFromString(colorString)
var/list/raw_colors = splittext(colorString, "#")
@@ -115,22 +183,54 @@
for(var/i in 2 to length(raw_colors))
split_colors += "#[raw_colors[i]]"
+/datum/greyscale_modify_menu/proc/queue_refresh()
+ refreshing = TRUE
+ addtimer(CALLBACK(src, .proc/refresh_preview), 1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
+
/datum/greyscale_modify_menu/proc/refresh_preview()
for(var/i in length(split_colors) + 1 to config.expected_colors)
split_colors += rgb(100, 100, 100)
var/list/used_colors = split_colors.Copy(1, config.expected_colors+1)
- var/list/data = config.GenerateDebug(used_colors.Join())
sprite_data = list()
- var/list/steps = list()
- sprite_data["steps"] = steps
- for(var/step in data["steps"])
- CHECK_TICK
- var/image/layer = image(data["steps"][step])
- var/image/result = image(step)
- steps += list(
- list(
- "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE),
- "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE)
+
+ var/list/generated_icon_states = list()
+ for(var/state in config.icon_states)
+ generated_icon_states += state // We don't want the values from this keyed list
+ sprite_data["icon_states"] = generated_icon_states
+
+ if(!(icon_state in generated_icon_states))
+ icon_state = target.icon_state
+ if(!(icon_state in generated_icon_states))
+ icon_state = pick(generated_icon_states)
+
+ var/image/finished
+ if(!generate_full_preview)
+ finished = image(config.GenerateBundle(used_colors), icon_state=icon_state)
+ else
+ var/list/data = config.GenerateDebug(used_colors.Join())
+ finished = image(data["icon"], icon_state=icon_state)
+ var/list/steps = list()
+ sprite_data["steps"] = steps
+ for(var/step in data["steps"])
+ CHECK_TICK
+ var/image/layer = image(data["steps"][step])
+ var/image/result = image(step)
+ steps += list(
+ list(
+ "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE),
+ "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE)
+ )
)
- )
+
+ sprite_data["finished"] = icon2html(finished, user, dir=sprite_dir, sourceonly=TRUE)
+ refreshing = FALSE
+
+/datum/greyscale_modify_menu/proc/Unlock()
+ allowed_configs = SSgreyscale.configurations
+ unlocked = TRUE
+
+/datum/greyscale_modify_menu/proc/DefaultApply()
+ target.set_greyscale_config(config.type, update=FALSE)
+ target.greyscale_colors = "" // We do this to force an update, in some cases it will think nothing changed when it should be refreshing
+ target.set_greyscale_colors(split_colors)
diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm
index 0d323e8cf07..4baa36c05ab 100644
--- a/code/modules/admin/view_variables/topic_basic.dm
+++ b/code/modules/admin/view_variables/topic_basic.dm
@@ -81,7 +81,8 @@
if(href_list[VV_HK_MODIFY_GREYSCALE])
if(!check_rights(NONE))
return
- var/datum/greyscale_modify_menu/menu = new(target, usr)
+ var/datum/greyscale_modify_menu/menu = new(target, usr, SSgreyscale.configurations)
+ menu.Unlock()
menu.ui_interact(usr)
if(href_list[VV_HK_CALLPROC])
usr.client.callproc_datum(target)
diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm
index ffda3d4fa0c..418883c2af6 100644
--- a/code/modules/vending/_vending.dm
+++ b/code/modules/vending/_vending.dm
@@ -36,6 +36,8 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C
var/custom_premium_price
///Whether spessmen with an ID with an age below AGE_MINOR (20 by default) can buy this item
var/age_restricted = FALSE
+ ///Whether the product can be recolored by the GAGS system
+ var/colorable
/**
* # vending machines
@@ -323,6 +325,7 @@ GLOBAL_LIST_EMPTY(vending_products)
R.custom_price = round(initial(temp.custom_price) * SSeconomy.inflation_value())
R.custom_premium_price = round(initial(temp.custom_premium_price) * SSeconomy.inflation_value())
R.age_restricted = initial(temp.age_restricted)
+ R.colorable = !!(initial(temp.greyscale_config) && initial(temp.greyscale_colors))
recordlist += R
/**
@@ -791,7 +794,12 @@ GLOBAL_LIST_EMPTY(vending_products)
.["user"]["department"] = "No Department"
.["stock"] = list()
for (var/datum/data/vending_product/R in product_records + coin_records + hidden_records)
- .["stock"][R.name] = R.amount
+ var/list/product_data = list(
+ name = R.name,
+ amount = R.amount,
+ colorable = R.colorable,
+ )
+ .["stock"][R.name] = product_data
.["extended_inventory"] = extended_inventory
/obj/machinery/vending/ui_act(action, params)
@@ -800,96 +808,145 @@ GLOBAL_LIST_EMPTY(vending_products)
return
switch(action)
if("vend")
- . = TRUE
- if(!vend_ready)
- return
- if(panel_open)
- to_chat(usr, "The vending machine cannot dispense products while its service panel is open!")
- return
- vend_ready = FALSE //One thing at a time!!
- var/datum/data/vending_product/R = locate(params["ref"])
- var/list/record_to_check = product_records + coin_records
- if(extended_inventory)
- record_to_check = product_records + coin_records + hidden_records
- if(!R || !istype(R) || !R.product_path)
- vend_ready = TRUE
- return
- var/price_to_use = default_price
- if(R.custom_price)
- price_to_use = R.custom_price
- if(R in hidden_records)
- if(!extended_inventory)
- vend_ready = TRUE
- return
- else if (!(R in record_to_check))
- vend_ready = TRUE
- message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!")
- return
- if (R.amount <= 0)
- say("Sold out of [R.name].")
- flick(icon_deny,src)
- vend_ready = TRUE
- return
- if(onstation)
- var/obj/item/card/id/C
- if(isliving(usr))
- var/mob/living/L = usr
- C = L.get_idcard(TRUE)
- if(!C)
- say("No card found.")
- flick(icon_deny,src)
- vend_ready = TRUE
- return
- else if (!C.registered_account)
- say("No account found.")
- flick(icon_deny,src)
- vend_ready = TRUE
- return
- else if(!C.registered_account.account_job)
- say("Departmental accounts have been blacklisted from personal expenses due to embezzlement.")
- flick(icon_deny, src)
- vend_ready = TRUE
- return
- else if(age_restrictions && R.age_restricted && (!C.registered_age || C.registered_age < AGE_MINOR))
- say("You are not of legal age to purchase [R.name].")
- if(!(usr in GLOB.narcd_underages))
- Radio.set_frequency(FREQ_SECURITY)
- Radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [usr] recorded attempting to purchase [R.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY)
- GLOB.narcd_underages += usr
- flick(icon_deny,src)
- vend_ready = TRUE
- return
- var/datum/bank_account/account = C.registered_account
- if(account.account_job && account.account_job.paycheck_department == payment_department)
- price_to_use = max(round(price_to_use * VENDING_DISCOUNT), 1) //No longer free, but signifigantly cheaper.
- if(coin_records.Find(R) || hidden_records.Find(R))
- price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price
- if(price_to_use && !account.adjust_money(-price_to_use))
- say("You do not possess the funds to purchase [R.name].")
- flick(icon_deny,src)
- vend_ready = TRUE
- return
- var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department)
- if(D)
- D.adjust_money(price_to_use)
- SSblackbox.record_feedback("amount", "vending_spent", price_to_use)
- log_econ("[price_to_use] credits were inserted into [src] by [D.account_holder] to buy [R].")
- if(last_shopper != usr || purchase_message_cooldown < world.time)
- say("Thank you for shopping with [src]!")
- purchase_message_cooldown = world.time + 5 SECONDS
- last_shopper = usr
- use_power(5)
- if(icon_vend) //Show the vending animation if needed
- flick(icon_vend,src)
- playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
- var/obj/item/vended_item = new R.product_path(get_turf(src))
- R.amount--
- if(usr.CanReach(src) && usr.put_in_hands(vended_item))
- to_chat(usr, "You take [R.name] out of the slot.")
- else
- to_chat(usr, "[capitalize(R.name)] falls onto the floor!")
- SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]"))
+ . = vend(params)
+ if("select_colors")
+ . = select_colors(params)
+
+/obj/machinery/vending/proc/can_vend(user, silent=FALSE)
+ . = FALSE
+ if(!vend_ready)
+ return
+ if(panel_open)
+ to_chat(user, "The vending machine cannot dispense products while its service panel is open!")
+ return
+ return TRUE
+
+/obj/machinery/vending/proc/select_colors(list/params)
+ . = TRUE
+ if(!can_vend(usr))
+ return
+ var/datum/data/vending_product/product = locate(params["ref"])
+ var/atom/fake_atom = product.product_path
+
+ var/list/allowed_configs = list()
+ var/config = initial(fake_atom.greyscale_config)
+ if(!config)
+ return
+ allowed_configs += "[config]"
+ if(ispath(fake_atom, /obj/item))
+ var/obj/item/item = fake_atom
+ if(initial(item.greyscale_config_worn))
+ allowed_configs += "[initial(item.greyscale_config_worn)]"
+ if(initial(item.greyscale_config_inhand_left))
+ allowed_configs += "[initial(item.greyscale_config_inhand_left)]"
+ if(initial(item.greyscale_config_inhand_right))
+ allowed_configs += "[initial(item.greyscale_config_inhand_right)]"
+
+ var/datum/greyscale_modify_menu/menu = new(
+ src, usr, allowed_configs, CALLBACK(src, .proc/vend_greyscale, params),
+ starting_icon_state=initial(fake_atom.icon_state),
+ starting_config=initial(fake_atom.greyscale_config),
+ starting_colors=initial(fake_atom.greyscale_colors)
+ )
+ menu.ui_interact(usr)
+
+/obj/machinery/vending/proc/vend_greyscale(list/params, datum/greyscale_modify_menu/menu)
+ if(usr != menu.user)
+ return
+ if(!menu.target.can_interact(usr))
+ return
+ vend(params, menu.split_colors)
+
+/obj/machinery/vending/proc/vend(list/params, list/greyscale_colors)
+ . = TRUE
+ if(!can_vend(usr))
+ return
+ vend_ready = FALSE //One thing at a time!!
+ var/datum/data/vending_product/R = locate(params["ref"])
+ var/list/record_to_check = product_records + coin_records
+ if(extended_inventory)
+ record_to_check = product_records + coin_records + hidden_records
+ if(!R || !istype(R) || !R.product_path)
+ vend_ready = TRUE
+ return
+ var/price_to_use = default_price
+ if(R.custom_price)
+ price_to_use = R.custom_price
+ if(R in hidden_records)
+ if(!extended_inventory)
vend_ready = TRUE
+ return
+ else if (!(R in record_to_check))
+ vend_ready = TRUE
+ message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!")
+ return
+ if (R.amount <= 0)
+ say("Sold out of [R.name].")
+ flick(icon_deny,src)
+ vend_ready = TRUE
+ return
+ if(onstation)
+ var/obj/item/card/id/C
+ if(isliving(usr))
+ var/mob/living/L = usr
+ C = L.get_idcard(TRUE)
+ if(!C)
+ say("No card found.")
+ flick(icon_deny,src)
+ vend_ready = TRUE
+ return
+ else if (!C.registered_account)
+ say("No account found.")
+ flick(icon_deny,src)
+ vend_ready = TRUE
+ return
+ else if(!C.registered_account.account_job)
+ say("Departmental accounts have been blacklisted from personal expenses due to embezzlement.")
+ flick(icon_deny, src)
+ vend_ready = TRUE
+ return
+ else if(age_restrictions && R.age_restricted && (!C.registered_age || C.registered_age < AGE_MINOR))
+ say("You are not of legal age to purchase [R.name].")
+ if(!(usr in GLOB.narcd_underages))
+ Radio.set_frequency(FREQ_SECURITY)
+ Radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [usr] recorded attempting to purchase [R.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY)
+ GLOB.narcd_underages += usr
+ flick(icon_deny,src)
+ vend_ready = TRUE
+ return
+ var/datum/bank_account/account = C.registered_account
+ if(account.account_job && account.account_job.paycheck_department == payment_department)
+ price_to_use = max(round(price_to_use * VENDING_DISCOUNT), 1) //No longer free, but signifigantly cheaper.
+ if(coin_records.Find(R) || hidden_records.Find(R))
+ price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price
+ if(price_to_use && !account.adjust_money(-price_to_use))
+ say("You do not possess the funds to purchase [R.name].")
+ flick(icon_deny,src)
+ vend_ready = TRUE
+ return
+ var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department)
+ if(D)
+ D.adjust_money(price_to_use)
+ SSblackbox.record_feedback("amount", "vending_spent", price_to_use)
+ log_econ("[price_to_use] credits were inserted into [src] by [D.account_holder] to buy [R].")
+ if(last_shopper != usr || purchase_message_cooldown < world.time)
+ say("Thank you for shopping with [src]!")
+ purchase_message_cooldown = world.time + 5 SECONDS
+ last_shopper = usr
+ use_power(5)
+ if(icon_vend) //Show the vending animation if needed
+ flick(icon_vend,src)
+ playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
+ var/obj/item/vended_item = new R.product_path(get_turf(src))
+ if(greyscale_colors)
+ vended_item.set_greyscale_colors(greyscale_colors)
+ R.amount--
+ if(usr.CanReach(src) && usr.put_in_hands(vended_item))
+ to_chat(usr, "You take [R.name] out of the slot.")
+ else
+ to_chat(usr, "[capitalize(R.name)] falls onto the floor!")
+ SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]"))
+ vend_ready = TRUE
/obj/machinery/vending/process(delta_time)
if(machine_stat & (BROKEN|NOPOWER))
diff --git a/icons/Testing/greyscale_error.dmi b/icons/Testing/greyscale_error.dmi
new file mode 100644
index 00000000000..aacf6106821
Binary files /dev/null and b/icons/Testing/greyscale_error.dmi differ
diff --git a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx
index 4615e9a7185..f12e2197eeb 100644
--- a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx
+++ b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx
@@ -1,5 +1,5 @@
import { useBackend } from '../backend';
-import { Box, Button, ColorBox, Flex, Icon, Input, LabeledList, Section, Table } from '../components';
+import { Box, Button, ColorBox, Flex, Stack, Icon, Input, LabeledList, Section, Table } from '../components';
import { Window } from '../layouts';
type ColorEntry = {
@@ -8,20 +8,25 @@ type ColorEntry = {
}
type SpriteData = {
- finished: SpriteEntry
- steps: Array
+ icon_states: string[];
+ finished: string;
+ steps: SpriteEntry[];
}
type SpriteEntry = {
- layer: string
- result: string
+ layer: string;
+ result: string;
}
type GreyscaleMenuData = {
greyscale_config: string;
- colors: Array;
+ colors: ColorEntry[];
sprites: SpriteData;
+ generate_full_preview: boolean;
+ unlocked: boolean;
sprites_dir: string;
+ icon_state: string;
+ refreshing: boolean;
}
enum Direction {
@@ -49,9 +54,9 @@ const DirectionAbbreviation : Record = {
const ConfigDisplay = (props, context) => {
const { act, data } = useBackend(context);
return (
-
+
-
+