diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 9d896bfa8b3..4b79590b39d 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -1477,3 +1477,4 @@
///Called when the ticker sets up the game for start
#define COMSIG_TICKER_ENTER_SETTING_UP "comsig_ticker_enter_setting_up"
+#define COMSIG_GREYSCALE_CONFIG_REFRESHED "greyscale_config_refreshed"
diff --git a/code/controllers/subsystem/greyscale.dm b/code/controllers/subsystem/processing/greyscale.dm
similarity index 79%
rename from code/controllers/subsystem/greyscale.dm
rename to code/controllers/subsystem/processing/greyscale.dm
index b1b445e4830..9b7f1ad3935 100644
--- a/code/controllers/subsystem/greyscale.dm
+++ b/code/controllers/subsystem/processing/greyscale.dm
@@ -1,12 +1,13 @@
-SUBSYSTEM_DEF(greyscale)
+PROCESSING_SUBSYSTEM_DEF(greyscale)
name = "Greyscale"
- flags = SS_NO_FIRE
+ flags = SS_BACKGROUND
init_order = INIT_ORDER_GREYSCALE
+ wait = 3 SECONDS
var/list/datum/greyscale_config/configurations = list()
var/list/datum/greyscale_layer/layer_types = list()
-/datum/controller/subsystem/greyscale/Initialize(start_timeofday)
+/datum/controller/subsystem/processing/greyscale/Initialize(start_timeofday)
for(var/datum/greyscale_layer/fake_type as anything in subtypesof(/datum/greyscale_layer))
layer_types[initial(fake_type.layer_type)] = fake_type
@@ -28,11 +29,11 @@ SUBSYSTEM_DEF(greyscale)
return ..()
-/datum/controller/subsystem/greyscale/proc/RefreshConfigsFromFile()
+/datum/controller/subsystem/processing/greyscale/proc/RefreshConfigsFromFile()
for(var/i in configurations)
configurations[i].Refresh(TRUE)
-/datum/controller/subsystem/greyscale/proc/GetColoredIconByType(type, list/colors)
+/datum/controller/subsystem/processing/greyscale/proc/GetColoredIconByType(type, list/colors)
if(!ispath(type, /datum/greyscale_config))
CRASH("An invalid greyscale configuration was given to `GetColoredIconByType()`: [type]")
type = "[type]"
@@ -42,7 +43,7 @@ SUBSYSTEM_DEF(greyscale)
CRASH("Invalid colors were given to `GetColoredIconByType()`: [colors]")
return configurations[type].Generate(colors)
-/datum/controller/subsystem/greyscale/proc/ParseColorString(color_string)
+/datum/controller/subsystem/processing/greyscale/proc/ParseColorString(color_string)
. = list()
var/list/split_colors = splittext(color_string, "#")
for(var/color in 2 to length(split_colors))
diff --git a/code/datums/greyscale/_greyscale_config.dm b/code/datums/greyscale/_greyscale_config.dm
index 4f8a0c0b671..674cbb3c892 100644
--- a/code/datums/greyscale/_greyscale_config.dm
+++ b/code/datums/greyscale/_greyscale_config.dm
@@ -27,12 +27,24 @@
/// String path to the json file, used for reloading
var/string_json_config
+ /// The md5 file hash for the json configuration. Used to check if the file has changed
+ var/json_config_hash
+
/// String path to the icon file, used for reloading
var/string_icon_file
+ /// The md5 file hash for the icon file. Used to check if the file has changed
+ var/icon_file_hash
+
/// A list of icon states and their layers
var/list/icon_states
+ /// A list of all layers irrespective of nesting
+ var/list/flat_all_layers
+
+ /// A list of types to update in the world whenever a config changes
+ var/list/live_edit_types
+
/// How many colors are expected to be given when building the sprite
var/expected_colors = 0
@@ -51,11 +63,63 @@
if(!name)
stack_trace("Greyscale config object [DebugName()] is missing a name, make sure `name` has been assigned a value.")
+/datum/greyscale_config/Destroy(force, ...)
+ if(!force)
+ return QDEL_HINT_LETMELIVE
+ return ..()
+
+/datum/greyscale_config/process(delta_time)
+ if(!Refresh(loadFromDisk=TRUE))
+ return
+ if(!live_edit_types)
+ return
+ for(var/atom/thing in world)
+ if(live_edit_types[thing.type])
+ thing.update_greyscale()
+
+/datum/greyscale_config/proc/EnableAutoRefresh(live_type)
+ message_admins("Config auto refresh has been enabled for '[live_type]' with configuration [DebugName()]. Expect heavy lag.")
+ if(live_type)
+ if(!live_edit_types)
+ live_edit_types = list()
+ live_edit_types += typecacheof(live_type)
+ START_PROCESSING(SSgreyscale, src)
+
+/datum/greyscale_config/proc/DisableAutoRefresh(live_type, remove_all=FALSE)
+ if(!remove_all && !(live_type in live_edit_types))
+ return
+ message_admins("Config auto refresh has been disabled for '[live_type]' with configuration [DebugName()]")
+ if(remove_all)
+ live_edit_types = null
+ else if(live_type && live_edit_types)
+ live_edit_types -= typecacheof(live_type)
+ if(!length(live_edit_types))
+ live_edit_types = null
+ STOP_PROCESSING(SSgreyscale, src)
+
/// Call this proc to handle all the data extraction from the json configuration. Can be forced to load values from disk instead of memory.
/datum/greyscale_config/proc/Refresh(loadFromDisk=FALSE)
if(loadFromDisk)
+ var/changed = FALSE
+
json_config = file(string_json_config)
+ var/json_hash = md5asfile(json_config)
+ if(json_config_hash != json_hash)
+ json_config_hash = json_hash
+ changed = TRUE
+
icon_file = file(string_icon_file)
+ var/icon_hash = md5asfile(icon_file)
+ if(icon_file_hash != icon_hash)
+ icon_file_hash = icon_hash
+ changed = TRUE
+
+ for(var/datum/greyscale_layer/layer as anything in flat_all_layers)
+ if(layer.DiskRefresh())
+ changed = TRUE
+
+ if(!changed)
+ return FALSE
var/list/raw = json_decode(file2text(json_config))
ReadIconStateConfiguration(raw)
@@ -67,6 +131,10 @@
ReadMetadata()
+ SEND_SIGNAL(src, COMSIG_GREYSCALE_CONFIG_REFRESHED)
+
+ return TRUE
+
/// Called after every config has refreshed, this proc handles data verification that depends on multiple entwined configurations.
/datum/greyscale_config/proc/CrossVerify()
for(var/icon_state in icon_states)
@@ -139,9 +207,11 @@
if(length(state_layers) > MAX_SANE_LAYERS)
stack_trace("[DebugName()] icon state '[state]' has [length(state_layers)] layers which is larger than the max of [MAX_SANE_LAYERS].")
+ flat_all_layers = list()
var/list/color_groups = list()
var/largest_id = 0
for(var/datum/greyscale_layer/layer as anything in all_layers)
+ flat_all_layers += layer
for(var/id in layer.color_ids)
if(!isnum(id))
continue
@@ -155,6 +225,11 @@
expected_colors = length(color_groups)
+/// For saving a dmi to disk, useful for debug mainly
+/datum/greyscale_config/proc/SaveOutput(color_string)
+ var/icon/icon_output = GenerateBundle(color_string)
+ fcopy(icon_output, "tmp/gags_debug_output.dmi")
+
/// Actually create the icon and color it in, handles caching
/datum/greyscale_config/proc/Generate(color_string)
var/key = color_string
@@ -163,13 +238,7 @@
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
@@ -183,13 +252,19 @@
var/list/generated_icons = list()
for(var/icon_state in icon_states)
- var/icon/generated_icon = GenerateLayerGroup(colors, icon_states[icon_state], render_steps)
+ var/list/icon_state_steps
+ if(render_steps)
+ icon_state_steps = render_steps[icon_state] = list()
+ var/icon/generated_icon = GenerateLayerGroup(colors, icon_states[icon_state], icon_state_steps)
// We read a pixel to force the icon to be fully generated before we let it loose into the world
// I hate this
generated_icon.GetPixel(1, 1)
generated_icons[icon_state] = generated_icon
- var/icon/icon_bundle = icon('icons/testing/greyscale_error.dmi')
+ var/icon/icon_bundle = generated_icons[""] || icon('icons/testing/greyscale_error.dmi')
+ icon_bundle.Scale(width, height)
+ generated_icons -= ""
+
for(var/icon_state in generated_icons)
icon_bundle.Insert(generated_icons[icon_state], icon_state)
@@ -214,8 +289,9 @@
// These are so we can see the result of every step of the process in the preview ui
if(render_steps)
var/list/icon_data = list()
- render_steps[icon(layer_icon)] = icon_data
+ render_steps += list(icon_data)
icon_data["config_name"] = name
+ icon_data["step"] = icon(layer_icon)
icon_data["result"] = icon(new_icon)
return new_icon
diff --git a/code/datums/greyscale/layer.dm b/code/datums/greyscale/layer.dm
index 93bb3b24cca..ef6f358e05a 100644
--- a/code/datums/greyscale/layer.dm
+++ b/code/datums/greyscale/layer.dm
@@ -20,6 +20,10 @@
/datum/greyscale_layer/proc/Initialize(icon_file)
return
+/// Override this if you need to do something during a full config refresh from disk, return TRUE if something was changed
+/datum/greyscale_layer/proc/DiskRefresh()
+ return FALSE
+
/// Handles the processing of the json data and conversion to correct value types.
/// Will error on incorrect, missing, or unexpected values.
/datum/greyscale_layer/proc/ReadJsonData(list/json_data)
@@ -117,13 +121,21 @@
optional_values[NAMEOF(src, icon_state)] = /datum/json_reader/text
required_values[NAMEOF(src, reference_type)] = /datum/json_reader/greyscale_config
+/datum/greyscale_layer/reference/DiskRefresh()
+ . = ..()
+ return reference_type.Refresh(loadFromDisk=TRUE)
+
/datum/greyscale_layer/reference/CrossVerify()
. = ..()
if(!reference_type.icon_states[icon_state])
CRASH("[src] expects icon_state '[icon_state]' but referenced configuration '[reference_type]' does not have it.")
/datum/greyscale_layer/reference/InternalGenerate(list/colors, list/render_steps)
+ var/icon/generated_icon
if(render_steps)
- return reference_type.GenerateBundle(colors, render_steps)
+ var/list/reference_data = list()
+ generated_icon = reference_type.GenerateBundle(colors, reference_data)
+ render_steps += reference_data[icon_state]
else
- return reference_type.Generate(colors.Join())
+ generated_icon = reference_type.Generate(colors.Join())
+ return icon(generated_icon, icon_state)
diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm
index c59a9e0a769..aae202b4a32 100644
--- a/code/modules/admin/greyscale_modify_menu.dm
+++ b/code/modules/admin/greyscale_modify_menu.dm
@@ -17,6 +17,9 @@
/// A list of colors currently selected
var/list/split_colors
+ /// The type that the configuration file was assigned at
+ var/config_owner_type
+
/// Collection of data for tgui to use in displaying everything
var/list/sprite_data
/// The sprite dir currently being shown
@@ -38,15 +41,18 @@
src.apply_callback = apply_callback || CALLBACK(src, .proc/DefaultApply)
icon_state = starting_icon_state
+ SetupConfigOwner()
+
var/current_config = "[starting_config]" || "[target?.greyscale_config]"
- config = SSgreyscale.configurations[current_config]
+ var/datum/greyscale_config/new_config = SSgreyscale.configurations[current_config]
if(!(current_config in allowed_configs))
- config = SSgreyscale.configurations["[allowed_configs[pick(allowed_configs)]]"]
+ new_config = SSgreyscale.configurations["[allowed_configs[pick(allowed_configs)]]"]
+ change_config(new_config)
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
+ var/datum/greyscale_config/allowed_config = text2path("[config_string]")
+ config_choices[initial(allowed_config.name)] = config_string
src.allowed_configs = config_choices
ReadColorsFromString(starting_colors || target?.greyscale_colors)
@@ -88,6 +94,7 @@
data["generate_full_preview"] = generate_full_preview
data["unlocked"] = unlocked
data["refreshing"] = refreshing
+ data["monitoring_files"] = !!(config.datum_flags & DF_ISPROCESSING)
data["sprites_dir"] = dir2text(sprite_dir)
data["icon_state"] = icon_state
data["sprites"] = sprite_data
@@ -108,7 +115,7 @@
new_config = allowed_configs[new_config]
new_config = SSgreyscale.configurations[new_config] || new_config
if(!isnull(new_config) && config != new_config)
- config = new_config
+ change_config(new_config)
queue_refresh()
if("load_config_from_string")
@@ -116,7 +123,7 @@
return
var/datum/greyscale_config/new_config = SSgreyscale.configurations[params["config_string"]]
if(!isnull(new_config) && config != new_config)
- config = new_config
+ change_config(new_config)
queue_refresh()
if("toggle_full_preview")
@@ -171,7 +178,7 @@
apply_callback.Invoke(src)
if("refresh_file")
- if(!unlocked)
+ if(!unlocked || !check_rights(R_DEBUG))
return
if(length(GLOB.player_list) > 1)
var/check = alert(
@@ -184,13 +191,36 @@ This is highly likely to cause a lag spike for a few seconds."},
)
if(check != "Yes")
return
- SSgreyscale.RefreshConfigsFromFile()
- queue_refresh()
+ config.Refresh(loadFromDisk=TRUE)
+
+ if("save_dmi")
+ if(!unlocked)
+ return
+ config.SaveOutput(split_colors.Copy(1, config.expected_colors+1))
if("change_dir")
sprite_dir = text2dir(params["new_sprite_dir"])
queue_refresh()
+ if("toggle_mass_refresh")
+ if(!unlocked || !check_rights(R_DEBUG))
+ return
+ if(config.datum_flags & DF_ISPROCESSING)
+ config.DisableAutoRefresh(remove_all=TRUE)
+ return
+ if(length(GLOB.player_list) > 1)
+ var/check = alert(
+ user,
+{"Other players are connected to the server, are you sure you want to automatically refresh all greyscale configurations?\n
+This is highly likely to cause massive amounts of lag as every object in the game will be iterated over every few seconds."},
+ "Auto-Refresh Greyscale Configurations",
+ "Yes",
+ "Cancel"
+ )
+ if(check != "Yes")
+ return
+ config.EnableAutoRefresh(config_owner_type)
+
/datum/greyscale_modify_menu/proc/ReadColorsFromString(colorString)
var/list/raw_colors = splittext(colorString, "#")
split_colors = list()
@@ -203,7 +233,14 @@ This is highly likely to cause a lag spike for a few seconds."},
new_color += num2hex(rand(0, 255), 2)
split_colors[color_index] = new_color
+/datum/greyscale_modify_menu/proc/change_config(datum/greyscale_config/new_config)
+ if(config)
+ UnregisterSignal(config, COMSIG_GREYSCALE_CONFIG_REFRESHED)
+ config = new_config
+ RegisterSignal(config, COMSIG_GREYSCALE_CONFIG_REFRESHED, .proc/queue_refresh)
+
/datum/greyscale_modify_menu/proc/queue_refresh()
+ SIGNAL_HANDLER
refreshing = TRUE
addtimer(CALLBACK(src, .proc/refresh_preview), 1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
@@ -235,25 +272,17 @@ This is highly likely to cause a lag spike for a few seconds."},
finished = image(data["icon"], icon_state=icon_state)
var/list/steps = list()
sprite_data["steps"] = steps
- for(var/step in data["steps"])
+ var/list/icon_state_data = data["steps"][icon_state]
+ for(var/list/step as anything in icon_state_data)
CHECK_TICK
- var/list/step_data = data["steps"][step]
- var/image/layer = image(step)
- var/image/result = step_data["result"]
- // SKYRAT EDIT BEGIN - Bringing back the GAGS coloring menu - ORIGINAL:
- /*
+ var/image/layer = image(step["step"])
+ var/image/result = image(step["result"])
+ // SKYRAT EDIT BEGIN - Bringing back the GAGS coloring menu
steps += list(
list(
"layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE),
"result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE),
- "config_name"=step_data["config_name"]
- )
- )*/
- steps += list(
- list(
- "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE),
- "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE, override_skyrat = TRUE),
- "config_name"=step_data["config_name"]
+ "config_name"=step["config_name"]
)
)
// SKYRAT EDIT END
@@ -268,3 +297,14 @@ This is highly likely to cause a lag spike for a few seconds."},
/datum/greyscale_modify_menu/proc/DefaultApply()
target.set_greyscale(split_colors, config.type)
+
+/// Gets the top level type that first uses the configuration in this type path
+/datum/greyscale_modify_menu/proc/SetupConfigOwner()
+ var/atom/current = target.type
+ var/atom/parent = target.parent_type
+ if(!initial(current.greyscale_config))
+ return
+ while(initial(current.greyscale_config) == initial(parent.greyscale_config))
+ current = parent
+ parent = type2parent(current)
+ config_owner_type = current
diff --git a/tgstation.dme b/tgstation.dme
index 6803e4c32b9..c8218af449a 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -374,7 +374,6 @@
#include "code\controllers\subsystem\explosions.dm"
#include "code\controllers\subsystem\fire_burning.dm"
#include "code\controllers\subsystem\garbage.dm"
-#include "code\controllers\subsystem\greyscale.dm"
#include "code\controllers\subsystem\icon_smooth.dm"
#include "code\controllers\subsystem\id_access.dm"
#include "code\controllers\subsystem\idlenpcpool.dm"
@@ -432,6 +431,7 @@
#include "code\controllers\subsystem\processing\fastprocess.dm"
#include "code\controllers\subsystem\processing\fields.dm"
#include "code\controllers\subsystem\processing\fluids.dm"
+#include "code\controllers\subsystem\processing\greyscale.dm"
#include "code\controllers\subsystem\processing\instruments.dm"
#include "code\controllers\subsystem\processing\networks.dm"
#include "code\controllers\subsystem\processing\obj.dm"
diff --git a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx
index 4866eba822d..e00e34cc5e0 100644
--- a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx
+++ b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx
@@ -26,6 +26,7 @@ type GreyscaleMenuData = {
sprites: SpriteData;
generate_full_preview: boolean;
unlocked: boolean;
+ monitoring_files: boolean;
sprites_dir: string;
icon_state: string;
refreshing: boolean;
@@ -292,21 +293,48 @@ export const GreyscaleModifyMenu = (props, context) => {
- {
- !!data.unlocked
- &&