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 - &&