diff --git a/code/__DEFINES/dcs/signals/signals_greyscale.dm b/code/__DEFINES/dcs/signals/signals_greyscale.dm new file mode 100644 index 000000000000..b3192760cd7e --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_greyscale.dm @@ -0,0 +1 @@ +#define COMSIG_GREYSCALE_CONFIG_REFRESHED "greyscale_config_refreshed" diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 49bad31a60bc..b94816c172e5 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -110,6 +110,7 @@ #define INIT_ORDER_INPUT 85 #define INIT_ORDER_SOUNDS 83 #define INIT_ORDER_INSTRUMENTS 82 +#define INIT_ORDER_GREYSCALE 81 #define INIT_ORDER_VIS 80 #define INIT_ORDER_SECURITY_LEVEL 79 // We need to load before events so that it has a security level to choose from. #define INIT_ORDER_ACHIEVEMENTS 77 diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index a04f02bd6a8e..97104a56dde4 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -86,6 +86,7 @@ // /atom #define VV_HK_MODIFY_TRANSFORM "atom_transform" +#define VV_HK_MODIFY_GREYSCALE "modify_greyscale" #define VV_HK_ADD_REAGENT "addreagent" #define VV_HK_TRIGGER_EMP "empulse" #define VV_HK_TRIGGER_EXPLOSION "explode" diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 579126257fb1..cced9c3c5956 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1249,3 +1249,10 @@ GLOBAL_DATUM_INIT(dummySave, /savefile, new("tmp/dummySave.sav")) //Cache of ico color[2] = color[1] * cm[4] + color[2] * cm[5] + color[3] * cm[6] + cm[11] * 255 color[3] = color[1] * cm[7] + color[2] * cm[8] + color[3] * cm[9] + cm[12] * 255 return rgb(color[1], color[2], color[3]) + +/// Returns a list containing the width and height of an icon file +/proc/get_icon_dimensions(icon_path) + if (isnull(GLOB.icon_dimensions[icon_path])) + var/icon/my_icon = icon(icon_path) + GLOB.icon_dimensions[icon_path] = list("width" = my_icon.Width(), "height" = my_icon.Height()) + return GLOB.icon_dimensions[icon_path] diff --git a/code/_globalvars/lists/icons.dm b/code/_globalvars/lists/icons.dm new file mode 100644 index 000000000000..ff60e6bc8d92 --- /dev/null +++ b/code/_globalvars/lists/icons.dm @@ -0,0 +1,2 @@ +/// Cache of the width and height of icon files, to avoid repeating the same expensive operation +GLOBAL_LIST_EMPTY(icon_dimensions) diff --git a/code/_globalvars/regexes.dm b/code/_globalvars/regexes.dm index bd252b68ce43..934296fe1bcb 100644 --- a/code/_globalvars/regexes.dm +++ b/code/_globalvars/regexes.dm @@ -5,3 +5,19 @@ GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i")) GLOBAL_DATUM_INIT(is_alphanumeric, /regex, regex("\[a-z0-9]+", "i")) GLOBAL_DATUM_INIT(is_punctuation, /regex, regex("\[.!?]+", "i")) +GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) +GLOBAL_DATUM_INIT(is_alpha_color, /regex, regex("^#\[0-9a-fA-F]{8}$")) + +//finds text strings recognized as links on discord. Mainly used to stop embedding. +GLOBAL_DATUM_INIT(has_discord_embeddable_links, /regex, regex("(https?://\[^\\s|<\]{2,})")) + +//All < and > characters +GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g")) + +//All characters between < a > inclusive of the bracket +GLOBAL_DATUM_INIT(html_tags, /regex, regex(@"<.*?>", "g")) + +//All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, >, .. +GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g")) +GLOBAL_PROTECT(filename_forbidden_chars) +// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise. diff --git a/code/controllers/subsystem/greyscale.dm b/code/controllers/subsystem/greyscale.dm new file mode 100644 index 000000000000..460968c1ac40 --- /dev/null +++ b/code/controllers/subsystem/greyscale.dm @@ -0,0 +1,50 @@ +PROCESSING_SUBSYSTEM_DEF(greyscale) + name = "Greyscale" + 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/processing/greyscale/Initialize() + for(var/datum/greyscale_layer/fake_type as anything in subtypesof(/datum/greyscale_layer)) + layer_types[initial(fake_type.layer_type)] = fake_type + + for(var/greyscale_type in subtypesof(/datum/greyscale_config)) + var/datum/greyscale_config/config = new greyscale_type() + configurations["[greyscale_type]"] = config + + // We do this after all the types have been loaded into the listing so reference layers don't care about init order + for(var/greyscale_type in configurations) + CHECK_TICK + var/datum/greyscale_config/config = configurations[greyscale_type] + config.Refresh() + + // This final verification step is for things that need other greyscale configurations to be finished loading + for(var/greyscale_type as anything in configurations) + CHECK_TICK + var/datum/greyscale_config/config = configurations[greyscale_type] + config.CrossVerify() + + return ..() + +/datum/controller/subsystem/processing/greyscale/proc/RefreshConfigsFromFile() + for(var/i in configurations) + configurations[i].Refresh(TRUE) + +/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]" + if(istype(colors)) // It's the color list format + colors = colors.Join() + else if(!istext(colors)) + CRASH("Invalid colors were given to `GetColoredIconByType()`: [colors]") + return configurations[type].Generate(colors) + +/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)) + . += "#[split_colors[color]]" diff --git a/code/datums/components/gags_recolorable.dm b/code/datums/components/gags_recolorable.dm new file mode 100644 index 000000000000..862eb3f42aca --- /dev/null +++ b/code/datums/components/gags_recolorable.dm @@ -0,0 +1,73 @@ +/datum/component/gags_recolorable + +/datum/component/gags_recolorable/RegisterWithParent() + RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby)) + +/datum/component/gags_recolorable/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_PARENT_ATTACKBY) + +/datum/component/gags_recolorable/proc/on_attackby(datum/source, obj/item/attacking_item, mob/user) + SIGNAL_HANDLER + + if(!isatom(parent)) + return + + if(!istype(attacking_item, /obj/item/toy/crayon/spraycan)) + return + var/obj/item/toy/crayon/spraycan/can = attacking_item + + if(can.is_capped || can.check_empty()) + return + + INVOKE_ASYNC(src, .proc/open_ui, user, can) + return COMPONENT_NO_AFTERATTACK + +/datum/component/gags_recolorable/proc/open_ui(mob/user, obj/item/toy/crayon/spraycan/can) + var/atom/atom_parent = parent + var/list/allowed_configs = list() + var/config = initial(atom_parent.greyscale_config) + if(!config) + return + allowed_configs += "[config]" + if(ispath(atom_parent, /obj/item)) + var/obj/item/item = atom_parent + 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/spray_paint/menu = new( + atom_parent, user, allowed_configs, CALLBACK(src, .proc/recolor, user, can), + starting_icon_state = initial(atom_parent.icon_state), + starting_config = initial(atom_parent.greyscale_config), + starting_colors = atom_parent.greyscale_colors, + used_spraycan = can + ) + menu.ui_interact(user) + +/datum/component/gags_recolorable/proc/recolor(mob/user, obj/item/toy/crayon/spraycan/can, datum/greyscale_modify_menu/menu) + if(!isatom(parent)) + return + var/atom/atom_parent = parent + + if(can.is_capped || can.check_empty(user)) + menu.ui_close() + return + + can.use_charges() + if(can.pre_noise) + atom_parent.audible_message(span_hear("You hear spraying.")) + playsound(atom_parent.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) + + atom_parent.set_greyscale(menu.split_colors) + + // If the item is a piece of clothing and is being worn, make sure it updates on the player + if(!isclothing(atom_parent)) + return + if(!ishuman(atom_parent.loc)) + return + var/obj/item/clothing/clothing_parent = atom_parent + var/mob/living/carbon/human/wearer = atom_parent.loc + wearer.update_clothing(clothing_parent.slot_flags) diff --git a/code/datums/greyscale/README.md b/code/datums/greyscale/README.md new file mode 100644 index 000000000000..ca4e28bf843e --- /dev/null +++ b/code/datums/greyscale/README.md @@ -0,0 +1,112 @@ +# Greyscale Auto-Generated Sprites (GAGS) + +If you're wanting to add easy recolors for your sprite then this is the system for you. Features include: + +- Multiple color layers so your sprite can be generated from more than one color. +- Mixed greyscale and colored sprite layers; You can choose to only greyscale a part of the sprite or have premade filters applied to layers. +- Blend modes; Instead of just putting layers of sprites on top of eachother you can use the more advanced blend modes. +- Reusable configurations; You can reference greyscale sprites from within the configuration of another, allowing you to have a bunch of styles with minimal additional configuration. + +## Other Documents + +- [Basic follow along guide on hackmd](https://hackmd.io/@tgstation/GAGS-Walkthrough) + +## Broad overview + +There are three main parts to GAGS that you'll need to be aware of when adding a new greyscale sprite: + +- The json configuration + +All configuration files can be found in [code/datums/greyscale/json_configs](./json_configs) and is where you control how your icons are combined together along with the colors specified from in code. + +- The dmi file + +It contains the sprites that will be used as the basis for the rest of the generation process. You can only have one dmi file specified per configuration but if you want to split up your sprites you can reference other configurations instead. + +- The configuration type + +This is simply some pointers in the code linking together your dmi and the json configuration. + +## Json Configuration File + +The json is made up of some metadata and a list of layers used while creating the sprite. Inner lists are processed as their own chunk before being applied elsewhere, this is useful when you start using more advanced blend modes. Most of the time though you're just going to want a list of icons overlaid on top of eachother. + +```json +{ + "icon_state_name": [ + { + "type": "reference", + "reference_type": "/datum/greyscale_config/some_other_config", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + [ + { + "type": "icon_state", + "icon_state": "highlights", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "reference", + "reference_type": "/datum/greyscale_config/sparkle_effect", + "blend_mode": "add" + } + ] + ] +} +``` + +In this example, we start off by creating a sprite specified by a different configuration. The "type" key is required to specify the kind of layer you defining. Once that is done, the next two layers are grouped together, so they will be generated into one sprite before being applied to any sprites outside their group. You can think of it as an order of operations. + +The first of the two in the inner group is an "icon_state", this means that the icon will be retrieved from the associated dmi file using the "icon_state" key. + +The last layer is another reference type. Note that you don't need to give colors to every layer if the layer does not need any colors applied to it. + +"blend_mode" and "color_ids" are special, all layer types have them. The blend mode is what controls how that layer's finished product gets merged together with the rest of the sprite. The color ids control what colors are passed in to the layer. + +Once it is done generating it will be placed in an icon file with the icon state of "icon_state_name". You can use any name you like here. + +## Dmi File + +There are no special requirements from the dmi file to work with this system. You just need to specify the icon file in code and the icon_state in the json configuration. + +## Dm Code + +While the amount of dm code required to make a greyscale sprite was minimized as much as possible, some small amount is required anyway if you want anything to use it. + +As an example: +```c +/datum/greyscale_config/canister + icon_file = 'icons/obj/atmospherics/canisters/default.dmi' + json_config = 'code/datums/greyscale/json_configs/canister_default.json' +``` +And that's all you need to make it usable by other code: + +```c +/obj/machinery/portable_atmospherics/canister + ... + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#ee4242" +``` + +More configurations can be found in [code/datums/greyscale/greyscale_configs.dm](./greyscale_configs.dm) + +If you want your item to be colorable in a vending machine (or other places if there's ever any support added for that), you should do it like this: + +```c +/obj/item/clothing/head/beret + ... + flags_1 = IS_PLAYER_COLORABLE_1 +``` +However, **be extremely careful**, as this *requires* that you put *all* of the object's `flags_1` flags in that statement all over again. It's ugly, I know, but there's no +better way to do this with BYOND just yet. You can put multiple flags like this (not real flags): +```c +/obj/item/clothing/head/beret + ... + flags_1 = IS_PLAYER_COLORABLE_1 | THIS_IS_A_FAKE_FLAG | THIS_IS_ANOTHER_FAKE_FLAG +``` + +## Debugging + +If you're making a new greyscale sprite you sometimes want to be able to see how layers got generated or maybe you're just tweaking some colors. Rather than rebooting the server with every change there is a greyscale modification menu that can be found in the vv dropdown menu for the greyscale object. Here you can change colors, preview the results, and reload everything from their files. diff --git a/code/datums/greyscale/_greyscale_config.dm b/code/datums/greyscale/_greyscale_config.dm new file mode 100644 index 000000000000..6549e08d03eb --- /dev/null +++ b/code/datums/greyscale/_greyscale_config.dm @@ -0,0 +1,308 @@ +#define MAX_SANE_LAYERS 50 + +/// A datum tying together a greyscale configuration and dmi file. Required for using GAGS and handles the code interactions. +/datum/greyscale_config + /// User friendly name used in the debug menu + var/name + + /// Reference to the json config file + var/json_config + + /// Reference to the dmi file for this config + var/icon_file + + /// An optional var to set that tells the material system what material this configuration is for. + /// Use a typepath here, not an instance. + var/datum/material/material_skin + + /////////////////////////////////////////////////////////////////////////////////////////// + // Do not set any further vars, the json file specified above is what generates the object + + /// Spritesheet width of the icon_file + var/width + + /// Spritesheet height of the icon_file + var/height + + /// 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 + + /// Generated icons keyed by their color arguments + var/list/icon_cache + +// There's more sanity checking here than normal because this is designed for spriters to work with +// Sensible error messages that tell you exactly what's wrong is the best way to make this easy to use +/datum/greyscale_config/New() + if(!json_config) + stack_trace("Greyscale config object [DebugName()] is missing a json configuration, make sure `json_config` has been assigned a value.") + string_json_config = "[json_config]" + if(findtext(string_json_config, "code/datums/greyscale/json_configs/") != 1) + stack_trace("All greyscale json configuration files should be located within 'code/datums/greyscale/json_configs/'") + if(!icon_file) + stack_trace("Greyscale config object [DebugName()] is missing an icon file, make sure `icon_file` has been assigned a value.") + string_icon_file = "[icon_file]" + 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(seconds_per_tick) + 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) + + if(!length(icon_states)) + CRASH("The json configuration [DebugName()] doesn't have any icon states.") + + icon_cache = list() + + 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) + var/list/verification_targets = icon_states[icon_state] + verification_targets = verification_targets.Copy() + while(length(verification_targets)) + var/datum/greyscale_layer/layer = verification_targets[length(verification_targets)] + verification_targets.len-- + if(islist(layer)) + verification_targets += layer + continue + layer.CrossVerify() + +/// Gets the name used for debug purposes +/datum/greyscale_config/proc/DebugName() + var/display_name = name || "MISSING_NAME" + return "[display_name] ([icon_file]|[json_config])" + +/// Takes the json icon state configuration and puts it into a more processed format. +/datum/greyscale_config/proc/ReadIconStateConfiguration(list/data) + icon_states = list() + for(var/state in data) + var/list/raw_layers = data[state] + if(!length(raw_layers)) + stack_trace("The json configuration [DebugName()] for icon state '[state]' is missing any layers.") + continue + if(icon_states[state]) + stack_trace("The json configuration [DebugName()] has a duplicate icon state '[state]' and is being overriden.") + icon_states[state] = ReadLayersFromJson(raw_layers) + +/// Takes the json layers configuration and puts it into a more processed format +/datum/greyscale_config/proc/ReadLayersFromJson(list/data) + var/list/output = ReadLayerGroup(data) + return output[1] + +/datum/greyscale_config/proc/ReadLayerGroup(list/data) + if(!islist(data[1])) + var/layer_type = SSgreyscale.layer_types[data["type"]] + if(!layer_type) + CRASH("An unknown layer type was specified in the json of greyscale configuration [DebugName()]: [data["type"]]") + return new layer_type(icon_file, data.Copy()) // We don't want anything in there touching our version of the data + var/list/output = list() + for(var/list/group as anything in data) + output += ReadLayerGroup(group) + if(length(output)) // Adding lists to lists unwraps the top level so here we are + output = list(output) + return output + +/// Reads layer configurations to take out some useful overall information +/datum/greyscale_config/proc/ReadMetadata() + var/list/icon_dimensions = get_icon_dimensions(icon_file) + height = icon_dimensions["width"] + width = icon_dimensions["height"] + + var/list/datum/greyscale_layer/all_layers = list() + for(var/state in icon_states) + var/list/to_process = list(icon_states[state]) + var/list/state_layers = list() + + while(length(to_process)) + var/current = to_process[length(to_process)] + to_process.len-- + if(islist(current)) + to_process += current + else + state_layers += current + + all_layers += state_layers + + 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 + largest_id = max(id, largest_id) + color_groups["[id]"] = TRUE + + for(var/i in 1 to largest_id) + if(color_groups["[i]"]) + continue + stack_trace("Color Ids are required to be sequential and start from 1. [DebugName()] has a max id of [largest_id] but is missing [i].") + + 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, icon/last_external_icon) + var/key = color_string + var/icon/new_icon = icon_cache[key] + if(new_icon) + return icon(new_icon) + + var/icon/icon_bundle = GenerateBundle(color_string, last_external_icon=last_external_icon) + 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, icon/last_external_icon) + if(!istype(colors)) + colors = SSgreyscale.ParseColorString(colors) + if(length(colors) != expected_colors) + CRASH("[DebugName()] expected [expected_colors] color arguments but only received [length(colors)]") + + var/list/generated_icons = list() + for(var/icon_state in icon_states) + 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, last_external_icon) + // 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 = 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) + + return icon_bundle + +/// Internal recursive proc to handle nested layer groups +/datum/greyscale_config/proc/GenerateLayerGroup(list/colors, list/group, list/render_steps, icon/last_external_icon) + var/icon/new_icon + for(var/datum/greyscale_layer/layer as anything in group) + var/icon/layer_icon + if(islist(layer)) + layer_icon = GenerateLayerGroup(colors, layer, render_steps, new_icon || last_external_icon) + layer = layer[1] // When there are multiple layers in a group like this we use the first one's blend mode + else + layer_icon = layer.Generate(colors, render_steps, new_icon || last_external_icon) + + if(!new_icon) + new_icon = layer_icon + else + new_icon.Blend(layer_icon, layer.blend_mode) + + // 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 += list(icon_data) + icon_data["config_name"] = name + icon_data["step"] = icon(layer_icon) + icon_data["result"] = icon(new_icon) + return new_icon + +/datum/greyscale_config/proc/GenerateDebug(colors) + var/list/output = list() + var/list/debug_steps = list() + output["steps"] = debug_steps + + output["icon"] = GenerateBundle(colors, debug_steps) + return output + +#undef MAX_SANE_LAYERS diff --git a/code/datums/greyscale/config_types/greyscale_configs.dm b/code/datums/greyscale/config_types/greyscale_configs.dm new file mode 100644 index 000000000000..acb6aeb3b1b9 --- /dev/null +++ b/code/datums/greyscale/config_types/greyscale_configs.dm @@ -0,0 +1,33 @@ +/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/triple_stripe + name = "Triple Striped Canister" + json_config = 'code/datums/greyscale/json_configs/canister_triple_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' diff --git a/code/datums/greyscale/config_types/material_effects.dm b/code/datums/greyscale/config_types/material_effects.dm new file mode 100644 index 000000000000..0ed32375a277 --- /dev/null +++ b/code/datums/greyscale/config_types/material_effects.dm @@ -0,0 +1,4 @@ +/datum/greyscale_config/shimmer + name = "Shimmer Effect" + icon_file = 'icons/effects/shimmer.dmi' + json_config = 'code/datums/greyscale/json_configs/material_effects/shimmer.json' diff --git a/code/datums/greyscale/json_configs/canister_base.json b/code/datums/greyscale/json_configs/canister_base.json new file mode 100644 index 000000000000..4528c6225c63 --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_base.json @@ -0,0 +1,20 @@ +{ + "": [ + { + "type": "icon_state", + "icon_state": "base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "add_shader", + "blend_mode": "add" + }, + { + "type": "icon_state", + "icon_state": "multi_shader", + "blend_mode": "multiply" + } + ] +} diff --git a/code/datums/greyscale/json_configs/canister_default.json b/code/datums/greyscale/json_configs/canister_default.json new file mode 100644 index 000000000000..b965e9ec3fd1 --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_default.json @@ -0,0 +1,24 @@ +{ + "": [ + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/post_effects", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "window-base": [ + { + "type": "icon_state", + "icon_state": "window-base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_configs/canister_double_stripe.json b/code/datums/greyscale/json_configs/canister_double_stripe.json new file mode 100644 index 000000000000..33dd115d3ea7 --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_double_stripe.json @@ -0,0 +1,37 @@ +{ + "": [ + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + [ + { + "type": "icon_state", + "icon_state": "double_stripe", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "icon_state", + "icon_state": "double_stripe_shader", + "blend_mode": "subtract" + } + ], + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/post_effects", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "window-base": [ + { + "type": "icon_state", + "icon_state": "window-base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_configs/canister_hazard.json b/code/datums/greyscale/json_configs/canister_hazard.json new file mode 100644 index 000000000000..81c193622312 --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_hazard.json @@ -0,0 +1,30 @@ +{ + "": [ + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "hazard_stripes", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/post_effects", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "window-base": [ + { + "type": "icon_state", + "icon_state": "window-base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_configs/canister_post_effects.json b/code/datums/greyscale/json_configs/canister_post_effects.json new file mode 100644 index 000000000000..6756c3221f9c --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_post_effects.json @@ -0,0 +1,15 @@ +{ + "": [ + { + "type": "icon_state", + "icon_state": "outline", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "lights", + "blend_mode": "overlay" + } + ] +} diff --git a/code/datums/greyscale/json_configs/canister_proto.json b/code/datums/greyscale/json_configs/canister_proto.json new file mode 100644 index 000000000000..09f19b80e9b4 --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_proto.json @@ -0,0 +1,39 @@ +{ + "": [ + { + "type": "icon_state", + "icon_state": "can_base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "can_shader", + "blend_mode": "multiply" + }, + { + "type": "icon_state", + "icon_state": "stand", + "blend_mode": "overlay" + }, + { + "type": "icon_state", + "icon_state": "decals", + "blend_mode": "overlay" + }, + [ + { + "type": "icon_state", + "icon_state": "light_base", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "icon_state", + "icon_state": "light", + "blend_mode": "overlay", + "color_ids": [ 3 ] + } + ] + ] +} diff --git a/code/datums/greyscale/json_configs/canister_stripe.json b/code/datums/greyscale/json_configs/canister_stripe.json new file mode 100644 index 000000000000..9e5f087c0aef --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_stripe.json @@ -0,0 +1,30 @@ +{ + "": [ + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "stripe", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/post_effects", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "window-base": [ + { + "type": "icon_state", + "icon_state": "window-base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_configs/canister_triple_stripe.json b/code/datums/greyscale/json_configs/canister_triple_stripe.json new file mode 100644 index 000000000000..3fbf5d47ac6e --- /dev/null +++ b/code/datums/greyscale/json_configs/canister_triple_stripe.json @@ -0,0 +1,43 @@ +{ + "": [ + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + [ + { + "type": "icon_state", + "icon_state": "double_stripe", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "icon_state", + "icon_state": "double_stripe_shader", + "blend_mode": "subtract" + } + ], + { + "type": "icon_state", + "icon_state": "stripe", + "blend_mode": "overlay", + "color_ids": [ 3 ] + }, + { + "type": "reference", + "reference_type": "/datum/greyscale_config/canister/post_effects", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "window-base": [ + { + "type": "icon_state", + "icon_state": "window-base", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_configs/material_effects/shimmer.json b/code/datums/greyscale/json_configs/material_effects/shimmer.json new file mode 100644 index 000000000000..64e2168e5c2e --- /dev/null +++ b/code/datums/greyscale/json_configs/material_effects/shimmer.json @@ -0,0 +1,10 @@ +{ + "": [ + { + "type": "icon_state", + "icon_state": "animation", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ] +} diff --git a/code/datums/greyscale/json_reader.dm b/code/datums/greyscale/json_reader.dm new file mode 100644 index 000000000000..ffe143c28fc7 --- /dev/null +++ b/code/datums/greyscale/json_reader.dm @@ -0,0 +1,75 @@ +/// Takes a json list and extracts a single value. +/// Subtypes represent different conversions of that value. +/datum/json_reader + +/// Takes a value read directly from json and verifies/converts as needed to a result +/datum/json_reader/proc/ReadJson(value) + return + +/datum/json_reader/text/ReadJson(value) + if(!istext(value)) + CRASH("Text value expected but got '[value]'") + return value + +/datum/json_reader/number/ReadJson(value) + var/newvalue = text2num(value) + if(!isnum(newvalue)) + CRASH("Number expected but got [newvalue]") + return newvalue + +/datum/json_reader/number_color_list/ReadJson(list/value) + if(!istype(value)) + CRASH("Expected a list but got [value]") + var/list/new_values = list() + for(var/number_string in value) + var/new_value = text2num(number_string) + if(!isnum(new_value)) + if(!istext(number_string) || number_string[1] != "#") + stack_trace("Expected list to only contain numbers or colors but got '[number_string]'") + continue + new_value = number_string + new_values += new_value + return new_values + +/datum/json_reader/color_matrix/ReadJson(list/value) + if(!istype(value)) + CRASH("Expected a list but got [value]") + if(length(value) > 5 || length(value) < 4) + CRASH("Color matrix must contain 4 or 5 rows") + var/list/new_values = list() + for(var/list/row in value) + var/list/interpreted_row = list() + if(!istype(row) || length(row) != 4) + stack_trace("Expected list to contain further row lists with exactly 4 entries") + interpreted_row = list(0, 0, 0, 0) + continue + for(var/number in row) + if(!isnum(number)) + stack_trace("Each color matrix row must only contain numbers") + interpreted_row += 0 + else + interpreted_row += number + new_values += interpreted_row + return new_values + +/datum/json_reader/blend_mode + var/static/list/blend_modes = list( + "add" = ICON_ADD, + "subtract" = ICON_SUBTRACT, + "multiply" = ICON_MULTIPLY, + "or" = ICON_OR, + "overlay" = ICON_OVERLAY, + "underlay" = ICON_UNDERLAY, + ) + +/datum/json_reader/blend_mode/ReadJson(value) + var/new_value = blend_modes[lowertext(value)] + if(isnull(new_value)) + CRASH("Blend mode expected but got '[value]'") + return new_value + +/datum/json_reader/greyscale_config/ReadJson(value) + var/newvalue = SSgreyscale.configurations[value] + if(!newvalue) + CRASH("Greyscale configuration type expected but got '[value]'") + return newvalue diff --git a/code/datums/greyscale/layer.dm b/code/datums/greyscale/layer.dm new file mode 100644 index 000000000000..06a001c1ad83 --- /dev/null +++ b/code/datums/greyscale/layer.dm @@ -0,0 +1,156 @@ +/datum/greyscale_layer + var/layer_type + var/list/color_ids + var/blend_mode + + var/static/list/json_readers + +/datum/greyscale_layer/New(icon_file, list/json_data) + if(!json_readers) + json_readers = list() + for(var/path in subtypesof(/datum/json_reader)) + json_readers[path] = new path + + json_data -= "type" // This is used to look us up and doesn't need to be verified like the rest of the data + + ReadJsonData(json_data) + Initialize(icon_file) + +/// Override this to do initial set up +/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) + var/list/required_values = list() + var/list/optional_values = list() + GetExpectedValues(required_values, optional_values) + for(var/keyname in json_data) + if(required_values[keyname] && optional_values[keyname]) + stack_trace("Key '[keyname]' found in both required and optional lists. Make sure keys are only in one or the other.") + continue + if(!required_values[keyname] && !optional_values[keyname]) + stack_trace("Unknown key found in json for [src]: '[keyname]'") + continue + if(!(keyname in vars)) + stack_trace("[src] expects a value from '[keyname]' but has no var to hold the output.") + continue + var/datum/json_reader/reader = required_values[keyname] || optional_values[keyname] + reader = json_readers[reader] + if(!reader) + stack_trace("[src] has an invalid json reader type '[required_values[keyname]]' for key '[keyname]'.") + continue + vars[keyname] = reader.ReadJson(json_data[keyname]) + + // Final check to make sure we got everything we needed + for(var/keyname in required_values) + if(isnull(json_data[keyname])) + stack_trace("[src] is missing required json data key '[keyname]'.") + +/// Gathers information from the layer about what variables are expected in the json. +/// Override and add to the two argument lists if you want extra information in your layer. +/// The lists are formatted like keyname:keytype_define. +/// The key name is assigned to the var named the same on the layer type. +/datum/greyscale_layer/proc/GetExpectedValues(list/required_values, list/optional_values) + optional_values[NAMEOF(src, color_ids)] = /datum/json_reader/number_color_list + required_values[NAMEOF(src, blend_mode)] = /datum/json_reader/blend_mode + +/// Use this proc for extra verification needed by a particular layer, gets run after all greyscale configs have finished reading their json files. +/datum/greyscale_layer/proc/CrossVerify() + return + +/// Used to actualy create the layer using the given colors +/// Do not override, use InternalGenerate instead +/datum/greyscale_layer/proc/Generate(list/colors, list/render_steps, icon/new_icon) + var/list/processed_colors = list() + for(var/i in color_ids) + if(isnum(i)) + processed_colors += colors[i] + else + processed_colors += i + var/icon/copy_of_new_icon = icon(new_icon) // Layers shouldn't be modifying it directly, this is just for them to reference + return InternalGenerate(processed_colors, render_steps, copy_of_new_icon) + +/// Override this to implement layers. +/// The colors var will only contain colors that this layer is configured to use. +/datum/greyscale_layer/proc/InternalGenerate(list/colors, list/render_steps, icon/new_icon) + +//////////////////////////////////////////////////////// +// Subtypes + +/// The most basic greyscale layer; a layer which is created from a single icon_state in the given icon file +/datum/greyscale_layer/icon_state + layer_type = "icon_state" + var/icon_state + var/icon/icon + var/color_id + +/datum/greyscale_layer/icon_state/Initialize(icon_file) + . = ..() + var/list/icon_states = icon_states(icon_file) + if(!(icon_state in icon_states)) + CRASH("Configured icon state \[[icon_state]\] was not found in [icon_file]. Double check your json configuration.") + icon = new(icon_file, icon_state) + + if(length(color_ids) > 1) + CRASH("Icon state layers can not have more than one color id") + +/datum/greyscale_layer/icon_state/GetExpectedValues(list/required_values, list/optional_values) + . = ..() + required_values[NAMEOF(src, icon_state)] = /datum/json_reader/text + +/datum/greyscale_layer/icon_state/InternalGenerate(list/colors, list/render_steps, icon/new_icon) + . = ..() + var/icon/generated_icon = icon(icon) + if(length(colors)) + generated_icon.Blend(colors[1], ICON_MULTIPLY) + return generated_icon + +/// A layer to modify the previous layer's colors with a color matrix +/datum/greyscale_layer/color_matrix + layer_type = "color_matrix" + var/list/color_matrix + +/datum/greyscale_layer/color_matrix/GetExpectedValues(list/required_values, list/optional_values) + . = ..() + required_values[NAMEOF(src, color_matrix)] = /datum/json_reader/color_matrix + +/datum/greyscale_layer/color_matrix/InternalGenerate(list/colors, list/render_steps, icon/new_icon) + . = ..() + new_icon.MapColors(arglist(color_matrix)) + return new_icon + +/// A layer created by using another greyscale icon's configuration +/datum/greyscale_layer/reference + layer_type = "reference" + var/icon_state = "" + var/datum/greyscale_config/reference_type + +/datum/greyscale_layer/reference/GetExpectedValues(list/required_values, list/optional_values) + . = ..() + 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, icon/new_icon) + var/icon/generated_icon + if(render_steps) + var/list/reference_data = list() + generated_icon = reference_type.GenerateBundle(colors, reference_data, new_icon) + render_steps += reference_data[icon_state] + else + generated_icon = reference_type.Generate(colors.Join(), new_icon) + return icon(generated_icon, icon_state) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 220cd8849940..0e19e7d40041 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -109,6 +109,11 @@ ///Used for changing icon states for different base sprites. var/base_icon_state + ///The config type to use for greyscaled sprites. Both this and greyscale_colors must be assigned to work. + var/greyscale_config + ///A string of hex format colors to be used by greyscale sprites, ex: "#0054aa#badcff" + var/greyscale_colors + ///Mobs that are currently do_after'ing this atom, to be cleared from on Destroy() var/list/targeted_by @@ -184,6 +189,9 @@ if(loc) SEND_SIGNAL(loc, COMSIG_ATOM_CREATED, src) /// Sends a signal that the new atom `src`, has been created at `loc` + if(greyscale_config && greyscale_colors) //we'll check again at item/init for inhand/belt/worn configs. + update_greyscale() + //atom color stuff if(color) add_atom_colour(color, FIXED_COLOUR_PRIORITY) @@ -624,6 +632,31 @@ //SHOULD_CALL_PARENT(TRUE) return SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_ICON_STATE) +/// Handles updates to greyscale value updates. +/// The colors argument can be either a list or the full color string. +/// Child procs should call parent last so the update happens after all changes. +/atom/proc/set_greyscale(list/colors, new_config) + SHOULD_CALL_PARENT(TRUE) + if(istype(colors)) + colors = colors.Join("") + if(!isnull(colors) && greyscale_colors != colors) // If you want to disable greyscale stuff then give a blank string + greyscale_colors = colors + + if(!isnull(new_config) && greyscale_config != new_config) + greyscale_config = new_config + + update_greyscale() + +/// Checks if this atom uses the GAGS system and if so updates the icon +/atom/proc/update_greyscale() + SHOULD_CALL_PARENT(TRUE) + if(greyscale_colors && greyscale_config) + icon = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) + if(!smooth) // This is a bitfield but we're just checking that some sort of smoothing is happening + return + update_atom_colour() + queue_smooth(src) + /** * Builds a list of overlays for the atom, this will not apply them. * If you need to update overlays, use [update_icon(UPDATE_OVERLAYS)], @@ -1023,6 +1056,8 @@ // VV_DROPDOWN_OPTION(VV_HK_RADIATE, "Radiate") VV_DROPDOWN_OPTION(VV_HK_EDIT_FILTERS, "Edit Filters") // VV_DROPDOWN_OPTION(VV_HK_ADD_AI, "Add AI controller") + if(greyscale_colors) + VV_DROPDOWN_OPTION(VV_HK_MODIFY_GREYSCALE, "Modify greyscale colors") /atom/vv_do_topic(list/href_list) . = ..() diff --git a/code/modules/admin/greyscale_modify_menu.dm b/code/modules/admin/greyscale_modify_menu.dm new file mode 100644 index 000000000000..d25445bc4017 --- /dev/null +++ b/code/modules/admin/greyscale_modify_menu.dm @@ -0,0 +1,356 @@ +/// 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/datum/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 + + /// 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 + 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 + + /// 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. + * It also enables the user to modify the alpha channel of the colors. + */ + var/unlocked = FALSE + +/datum/greyscale_modify_menu/New(datum/target, client/user, list/allowed_configs, datum/callback/apply_callback, starting_icon_state = "", starting_config, starting_colors, unlocked = FALSE) + src.target = target + var/atom/atom_target + if(isatom(target)) + atom_target = target + src.user = user + src.apply_callback = apply_callback + if(!apply_callback) + if(atom_target) + src.apply_callback = CALLBACK(src, PROC_REF(DefaultApply)) + else + stack_trace("A geyscale modify menu was instantiated with a non-atom target and no specified apply callback (DefaultApply won't do).") + + icon_state = starting_icon_state + + SetupConfigOwner() + + + var/current_config = "[starting_config]" || "[atom_target?.greyscale_config]" + var/datum/greyscale_config/new_config = SSgreyscale.configurations[current_config] + if(!(current_config in allowed_configs)) + new_config = SSgreyscale.configurations["[allowed_configs[pick(allowed_configs)]]"] + change_config(new_config) + + if(unlocked) + Unlock() + else + var/list/config_choices = list() + for(var/config_string in allowed_configs) + 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 || atom_target?.greyscale_colors) + + if(target) + RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(ui_close)) + + refresh_preview() + +/datum/greyscale_modify_menu/Destroy() + target = null + user = null + apply_callback = null + config = null + return ..() + +/datum/greyscale_modify_menu/ui_state(mob/user) + return unlocked ? GLOB.always_state : GLOB.greyscale_menu_state + +/datum/greyscale_modify_menu/ui_close() + qdel(src) + +/datum/greyscale_modify_menu/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "GreyscaleModifyMenu") + ui.open() + +/datum/greyscale_modify_menu/ui_data(mob/user) + var/list/data = list() + data["greyscale_config"] = "[config.name]" + + var/list/color_data = list() + data["colors"] = color_data + for(var/i in 1 to config.expected_colors) + color_data += list(list( + "index" = i, + "value" = split_colors[i] + )) + + 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 + return data + +/datum/greyscale_modify_menu/ui_act(action, params) + . = ..() + if(.) + return + switch(action) + if("select_config") + var/datum/greyscale_config/new_config = input( + usr, + "Choose a new greyscale configuration to use", + "Greyscale Modification Menu", + "[config.type]" + ) as anything in allowed_configs + new_config = allowed_configs[new_config] + new_config = SSgreyscale.configurations[new_config] || new_config + if(!isnull(new_config) && config != new_config) + change_config(new_config) + 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) + change_config(new_config) + 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"]) + var/new_color = lowertext(params["new_color"]) + if(split_colors[index] != new_color && (findtext(new_color, GLOB.is_color) || (unlocked && findtext(new_color, GLOB.is_alpha_color)))) + split_colors[index] = new_color + queue_refresh() + + if("recolor_from_string") + var/full_color_string = lowertext(params["color_string"]) + if(full_color_string != split_colors.Join() && ReadColorsFromString(full_color_string)) + queue_refresh() + + if("pick_color") + var/group = params["color_index"] + var/new_color = input( + usr, + "Choose color for greyscale color group [group]:", + "Greyscale Modification Menu", + split_colors[group] + ) as color|null + if(new_color) + split_colors[group] = new_color + queue_refresh() + + if("random_color") + var/group = text2num(params["color_index"]) + randomize_color(group) + queue_refresh() + + if("random_all_colors") + for(var/i in 1 to length(split_colors)) + randomize_color(i) + 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") + apply_callback.Invoke(src) + + if("refresh_file") + if(!unlocked || !check_rights(R_DEBUG)) + 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 + 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/new_split_colors = list() + var/list/colors = splittext(colorString, "#") + for(var/index in 2 to min(length(colors), config.expected_colors + 1)) + var/color = "#[colors[index]]" + if(!findtext(color, GLOB.is_color) && (!unlocked || !findtext(color, GLOB.is_alpha_color))) + return FALSE + new_split_colors += color + split_colors = new_split_colors + return TRUE + +/datum/greyscale_modify_menu/proc/randomize_color(color_index) + var/new_color = "#" + for(var/i in 1 to 3) + 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_REF(queue_refresh)) + +/datum/greyscale_modify_menu/proc/queue_refresh() + SIGNAL_HANDLER + refreshing = TRUE + addtimer(CALLBACK(src, PROC_REF(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) + LAZYADD(split_colors, rgb(100, 100, 100)) + var/list/used_colors = split_colors.Copy(1, config.expected_colors+1) + + sprite_data = list() + + 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)) + if(isatom(target)) + var/atom/atom_target = target + icon_state = atom_target.icon_state + if(!(icon_state in generated_icon_states)) + icon_state = pick(generated_icon_states) + else + icon_state = pick(generated_icon_states) + + var/image/finished + var/time_spent = TICK_USAGE + if(!generate_full_preview) + finished = image(config.GenerateBundle(used_colors), icon_state=icon_state) + time_spent = TICK_USAGE - time_spent + else + var/list/data = config.GenerateDebug(used_colors.Join()) + time_spent = TICK_USAGE - time_spent + finished = image(data["icon"], icon_state=icon_state) + var/list/steps = list() + sprite_data["steps"] = steps + var/list/icon_state_data = data["steps"][icon_state] + for(var/list/step as anything in icon_state_data) + CHECK_TICK + var/image/layer = image(step["step"]) + var/image/result = image(step["result"]) + steps += list( + list( + "layer"=icon2html(layer, user, dir=sprite_dir, sourceonly=TRUE), + "result"=icon2html(result, user, dir=sprite_dir, sourceonly=TRUE), + "config_name"=step["config_name"] + ) + ) + + sprite_data["time_spent"] = TICK_DELTA_TO_MS(time_spent) + 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() + var/atom/atom_target = target + atom_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() + if(!isatom(target)) + return + + 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 + +/// Used for spray painting items in the gags_recolorable component +/datum/greyscale_modify_menu/spray_paint + var/obj/item/toy/crayon/spraycan/spraycan = null + +/datum/greyscale_modify_menu/spray_paint/New(atom/target, client/user, list/allowed_configs, datum/callback/apply_callback, starting_icon_state, starting_config, starting_colors, obj/item/toy/crayon/spraycan/used_spraycan) + ..() + spraycan = used_spraycan + +/datum/greyscale_modify_menu/spray_paint/ui_status(mob/user, datum/ui_state/state) + return min( + ui_status_only_living(user, target), + ui_status_user_is_abled(user, target), + ui_status_user_strictly_adjacent(user, target), + user.is_holding(spraycan)? UI_INTERACTIVE : UI_CLOSE + ) diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm index 043c50173d12..8151f8ad70e4 100644 --- a/code/modules/admin/view_variables/topic_basic.dm +++ b/code/modules/admin/view_variables/topic_basic.dm @@ -87,5 +87,11 @@ target._AddElement(lst) log_admin("[key_name(usr)] has added [result] [datumname] to [key_name(target)].") message_admins("[key_name_admin(usr)] has added [result] [datumname] to [key_name_admin(target)].") + if(href_list[VV_HK_MODIFY_GREYSCALE]) + if(!check_rights(NONE)) + return + 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/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm index f7bdc173f3b7..ce645d908352 100644 --- a/code/modules/atmospherics/machinery/portable/canister.dm +++ b/code/modules/atmospherics/machinery/portable/canister.dm @@ -1,9 +1,36 @@ #define CAN_DEFAULT_RELEASE_PRESSURE (ONE_ATMOSPHERE) +///List of all the gases, used in labelling the canisters +GLOBAL_LIST_INIT(gas_id_to_canister, init_gas_id_to_canister()) + +/proc/init_gas_id_to_canister() + return sortList(list( + "n2" = /obj/machinery/portable_atmospherics/canister/nitrogen, + "o2" = /obj/machinery/portable_atmospherics/canister/oxygen, + "co2" = /obj/machinery/portable_atmospherics/canister/carbon_dioxide, + "plasma" = /obj/machinery/portable_atmospherics/canister/toxins, + "n2o" = /obj/machinery/portable_atmospherics/canister/nitrous_oxide, + "no2" = /obj/machinery/portable_atmospherics/canister/nitryl, + "bz" = /obj/machinery/portable_atmospherics/canister/bz, + "air" = /obj/machinery/portable_atmospherics/canister/air, + "water vapor" = /obj/machinery/portable_atmospherics/canister/water_vapor, + "tritium" = /obj/machinery/portable_atmospherics/canister/tritium, + "hyper-noblium" = /obj/machinery/portable_atmospherics/canister/nob, + "stimulum" = /obj/machinery/portable_atmospherics/canister/stimulum, + "pluoxium" = /obj/machinery/portable_atmospherics/canister/pluoxium, + "caution" = /obj/machinery/portable_atmospherics/canister, + "miasma" = /obj/machinery/portable_atmospherics/canister/miasma, + "methane" = /obj/machinery/portable_atmospherics/canister/methane, + "methyl bromide" = /obj/machinery/portable_atmospherics/canister/methyl_bromide + )) + /obj/machinery/portable_atmospherics/canister name = "canister" desc = "A canister for the storage of gas." - icon_state = "yellow" + icon = 'icons/obj/atmospherics/canisters.dmi' + icon_state = "#mapme" + greyscale_config = /datum/greyscale_config/canister/hazard + greyscale_colors = "#ffff00#000000" density = TRUE volume = 1000 armor = list(MELEE = 50, BULLET = 50, LASER = 50, ENERGY = 100, BOMB = 10, BIO = 100, RAD = 100, FIRE = 80, ACID = 50) @@ -42,26 +69,9 @@ // var/mode = CANISTER_TIER_1 req_access = list() + var/icon/canister_overlay_file = 'icons/obj/atmospherics/canisters.dmi' + var/update = 0 - var/static/list/label2types = list( - "n2" = /obj/machinery/portable_atmospherics/canister/nitrogen, - "o2" = /obj/machinery/portable_atmospherics/canister/oxygen, - "co2" = /obj/machinery/portable_atmospherics/canister/carbon_dioxide, - "plasma" = /obj/machinery/portable_atmospherics/canister/toxins, - "n2o" = /obj/machinery/portable_atmospherics/canister/nitrous_oxide, - "no2" = /obj/machinery/portable_atmospherics/canister/nitryl, - "bz" = /obj/machinery/portable_atmospherics/canister/bz, - "air" = /obj/machinery/portable_atmospherics/canister/air, - "water vapor" = /obj/machinery/portable_atmospherics/canister/water_vapor, - "tritium" = /obj/machinery/portable_atmospherics/canister/tritium, - "hyper-noblium" = /obj/machinery/portable_atmospherics/canister/nob, - "stimulum" = /obj/machinery/portable_atmospherics/canister/stimulum, - "pluoxium" = /obj/machinery/portable_atmospherics/canister/pluoxium, - "caution" = /obj/machinery/portable_atmospherics/canister, - "miasma" = /obj/machinery/portable_atmospherics/canister/miasma, - "methane" = /obj/machinery/portable_atmospherics/canister/methane, - "methyl bromide" = /obj/machinery/portable_atmospherics/canister/methyl_bromide - ) /obj/machinery/portable_atmospherics/canister/interact(mob/user) if(!allowed(user)) @@ -70,102 +80,190 @@ return ..() -/obj/machinery/portable_atmospherics/canister/nitrogen - name = "n2 canister" - desc = "Nitrogen. Reportedly useful for something." - icon_state = "red" - gas_type = GAS_N2 +/obj/machinery/portable_atmospherics/canister/air + name = "Air canister" + desc = "Pre-mixed air." + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#c6c0b5" + +/* +/obj/machinery/portable_atmospherics/canister/antinoblium + name = "Antinoblium canister" + desc = "Antinoblium, we still don't know what it does, but it sells for a lot" + gas_type = /datum/gas/antinoblium + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#9b5d7f#368bff" +*/ -/obj/machinery/portable_atmospherics/canister/oxygen - name = "o2 canister" - desc = "Oxygen. Necessary for human life." - icon_state = "blue" - gas_type = GAS_O2 +/obj/machinery/portable_atmospherics/canister/bz + name = "\improper BZ canister" + desc = "BZ, a powerful hallucinogenic nerve agent." + gas_type = GAS_BZ + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#9b5d7f#d0d2a0" /obj/machinery/portable_atmospherics/canister/carbon_dioxide name = "co2 canister" desc = "Carbon dioxide. What the fuck is carbon dioxide?" - icon_state = "black" gas_type = GAS_CO2 + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#4e4c48" + +/* +/obj/machinery/portable_atmospherics/canister/freon + name = "Freon canister" + desc = "Freon. Can absorb heat" + gas_type = /datum/gas/freon + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#6696ee#fefb30" -/obj/machinery/portable_atmospherics/canister/toxins - name = "plasma canister" - desc = "Plasma. The reason YOU are here. Highly toxic." - icon_state = "orange" - gas_type = GAS_PLASMA +/obj/machinery/portable_atmospherics/canister/halon + name = "Halon canister" + desc = "Halon, removes oxygen from high temperature fires and cools down the area" + gas_type = /datum/gas/halon + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#9b5d7f#368bff" -/obj/machinery/portable_atmospherics/canister/bz - name = "\improper BZ canister" - desc = "BZ. A powerful hallucinogenic nerve agent." - icon_state = "purple" - gas_type = GAS_BZ +/obj/machinery/portable_atmospherics/canister/healium + name = "Healium canister" + desc = "Healium, causes deep sleep" + gas_type = /datum/gas/healium + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#009823#ff0e00" + +/obj/machinery/portable_atmospherics/canister/helium + name = "Helium canister" + desc = "Helium, inert gas" + gas_type = /datum/gas/helium + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#9b5d7f#368bff" +*/ + +/obj/machinery/portable_atmospherics/canister/hydrogen + name = "Hydrogen canister" + desc = "Hydrogen, highly flammable" + gas_type = GAS_HYDROGEN + filled = 1 + greyscale_config = /datum/greyscale_config/canister/stripe + greyscale_colors = "#bdc2c0#ffffff" + +/obj/machinery/portable_atmospherics/canister/methane + name = "methane canister" + desc = "Methane. The simplest of hydrocarbons. Non-toxic but highly flammable." + gas_type = GAS_METHANE + greyscale_config = /datum/greyscale_config/canister/triple_stripe + greyscale_colors = "#4E4E4E#C7C7C7#DA1010" + +/obj/machinery/portable_atmospherics/canister/methyl_bromide + name = "methyl bromide canister" + desc = "Methyl bromide. A potent toxin to most, essential for the Kharmaan to live." + gas_type = GAS_METHYL_BROMIDE + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#543568#179595" + +/obj/machinery/portable_atmospherics/canister/miasma + name = "Miasma canister" + desc = "Miasma. Makes you wish your nose was blocked." + gas_type = GAS_MIASMA + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#009823#f7d5d3" + +/obj/machinery/portable_atmospherics/canister/nitrogen + name = "Nitrogen canister" + desc = "Nitrogen gas. Reportedly useful for something." + gas_type = GAS_N2 + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#d41010" /obj/machinery/portable_atmospherics/canister/nitrous_oxide name = "n2o canister" desc = "Nitrous oxide. Known to cause drowsiness." - icon_state = "redws" gas_type = GAS_NITROUS + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#c63e3b#f7d5d3" -/obj/machinery/portable_atmospherics/canister/air - name = "air canister" - desc = "Pre-mixed air." - icon_state = "grey" - -/obj/machinery/portable_atmospherics/canister/tritium - name = "tritium canister" - desc = "Tritium. Inhalation might cause irradiation." - icon_state = "green" - gas_type = GAS_TRITIUM +/obj/machinery/portable_atmospherics/canister/nitryl + name = "Nitryl canister" + desc = "Nitryl gas. Feels great 'til the acid eats your lungs." + gas_type = GAS_NITRYL + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#7b4732" /obj/machinery/portable_atmospherics/canister/nob name = "hyper-noblium canister" desc = "Hyper-Noblium. More noble than all other gases." - icon_state = "freon" gas_type = GAS_HYPERNOB + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#6399fc#b2b2b2" -/obj/machinery/portable_atmospherics/canister/nitryl - name = "nitryl canister" - desc = "Nitryl. Feels great 'til the acid eats your lungs." - icon_state = "brown" - gas_type = GAS_NITRYL - -/obj/machinery/portable_atmospherics/canister/stimulum - name = "stimulum canister" - desc = "Stimulum. High energy gas, high energy people." - icon_state = "darkpurple" - gas_type = GAS_STIMULUM +/obj/machinery/portable_atmospherics/canister/oxygen + name = "Oxygen canister" + desc = "Oxygen. Necessary for human life." + gas_type = GAS_O2 + greyscale_config = /datum/greyscale_config/canister/stripe + greyscale_colors = "#2786e5#e8fefe" /obj/machinery/portable_atmospherics/canister/pluoxium name = "pluoxium canister" desc = "Pluoxium. Like oxygen, but more bang for your buck." - icon_state = "darkblue" gas_type = GAS_PLUOXIUM + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#2786e5" + +/* +/obj/machinery/portable_atmospherics/canister/proto_nitrate + name = "Proto Nitrate canister" + desc = "Proto Nitrate, reacts differently with various gases" + gas_type = /datum/gas/proto_nitrate + filled = 1 + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#008200#33cc33" +*/ + +/obj/machinery/portable_atmospherics/canister/stimulum + name = "Stimulum canister" + desc = "Stimulum. High energy gas, high energy people." + gas_type = GAS_STIMULUM + greyscale_config = /datum/greyscale_config/canister + greyscale_colors = "#9b5d7f" + +/obj/machinery/portable_atmospherics/canister/toxins + name = "Plasma canister" + desc = "Plasma gas. The reason YOU are here. Highly toxic." + gas_type = GAS_PLASMA + greyscale_config = /datum/greyscale_config/canister/hazard + greyscale_colors = "#f62800#000000" + +/obj/machinery/portable_atmospherics/canister/tritium + name = "Tritium canister" + desc = "Tritium. Inhalation might cause irradiation." + gas_type = GAS_TRITIUM + greyscale_config = /datum/greyscale_config/canister/hazard + greyscale_colors = "#3fcd40#000000" /obj/machinery/portable_atmospherics/canister/water_vapor - name = "water vapor canister" - desc = "Water vapor. We get it, you vape." - icon_state = "water_vapor" + name = "Water vapor canister" + desc = "Water Vapor. We get it, you vape." gas_type = GAS_H2O filled = 1 - -/obj/machinery/portable_atmospherics/canister/miasma - name = "miasma canister" - desc = "Miasma. Makes you wish your nose were blocked." - icon_state = "miasma" - gas_type = GAS_MIASMA + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#4c4e4d#f7d5d3" + +/* +/obj/machinery/portable_atmospherics/canister/zauker + name = "Zauker canister" + desc = "Zauker, highly toxic" + gas_type = /datum/gas/zauker filled = 1 - -/obj/machinery/portable_atmospherics/canister/methane - name = "methane canister" - desc = "Methane. The simplest of hydrocarbons. Non-toxic but highly flammable." - icon_state = "greyblackred" - gas_type = GAS_METHANE - -/obj/machinery/portable_atmospherics/canister/methyl_bromide - name = "methyl bromide canister" - desc = "Methyl bromide. A potent toxin to most, essential for the Kharmaan to live." - icon_state = "purplecyan" - gas_type = GAS_METHYL_BROMIDE + greyscale_config = /datum/greyscale_config/canister/double_stripe + greyscale_colors = "#009a00#006600" +*/ /obj/machinery/portable_atmospherics/canister/proc/get_time_left() if(timing) @@ -181,12 +279,12 @@ /obj/machinery/portable_atmospherics/canister/proto name = "prototype canister" - + greyscale_config = /datum/greyscale_config/prototype_canister + greyscale_colors = "#ffffff#a50021#ffffff" /obj/machinery/portable_atmospherics/canister/proto/default name = "prototype canister" desc = "The best way to fix an atmospheric emergency... or the best way to introduce one." - icon_state = "proto" volume = 5000 max_integrity = 300 temperature_resistance = 2000 + T0C @@ -197,7 +295,6 @@ /obj/machinery/portable_atmospherics/canister/proto/default/oxygen name = "prototype canister" desc = "A prototype canister for a prototype bike, what could go wrong?" - icon_state = "proto" gas_type = GAS_O2 filled = 1 release_pressure = ONE_ATMOSPHERE*2 @@ -232,18 +329,19 @@ /obj/machinery/portable_atmospherics/canister/update_overlays() . = ..() if(holding) - . += "can-open" + . += icon(canister_overlay_file, "can-open") if(connected_port) - . += "can-connector" - var/pressure = air_contents?.return_pressure() - if(pressure >= 40 * ONE_ATMOSPHERE) - . += "can-o3" - else if(pressure >= 10 * ONE_ATMOSPHERE) - . += "can-o2" - else if(pressure >= 5 * ONE_ATMOSPHERE) - . += "can-o1" - else if(pressure >= 10) - . += "can-o0" + . += icon(canister_overlay_file, "can-connector") + + switch(air_contents?.return_pressure()) + if((40 * ONE_ATMOSPHERE) to INFINITY) + . += icon(canister_overlay_file, "can-3") + if((10 * ONE_ATMOSPHERE) to (40 * ONE_ATMOSPHERE)) + . += icon(canister_overlay_file, "can-2") + if((5 * ONE_ATMOSPHERE) to (10 * ONE_ATMOSPHERE)) + . += icon(canister_overlay_file, "can-1") + if((10) to (5 * ONE_ATMOSPHERE)) + . += icon(canister_overlay_file, "can-0") /obj/machinery/portable_atmospherics/canister/temperature_expose(datum/gas_mixture/air, exposed_temperature, exposed_volume) if(exposed_temperature > temperature_resistance) @@ -301,15 +399,18 @@ holding.forceMove(T) holding = null + animate(src, 0.5 SECONDS, transform=turn(transform, rand(-179, 180)), easing=BOUNCE_EASING) + /obj/machinery/portable_atmospherics/canister/replace_tank(mob/living/user, close_valve) . = ..() - if(.) - if(close_valve) - valve_open = FALSE - update_icon() - investigate_log("Valve was closed by [key_name(user)].
", INVESTIGATE_ATMOS) - else if(valve_open && holding) - investigate_log("[key_name(user)] started a transfer into [holding].
", INVESTIGATE_ATMOS) + if(!.) + return + if(close_valve) + valve_open = FALSE + update_appearance() + investigate_log("Valve was closed by [key_name(user)].", INVESTIGATE_ATMOS) + else if(valve_open && holding) + investigate_log("[key_name(user)] started a transfer into [holding].", INVESTIGATE_ATMOS) /obj/machinery/portable_atmospherics/canister/process_atmos() ..() @@ -388,15 +489,17 @@ return switch(action) if("relabel") - var/label = input("New canister label:", name) as null|anything in sortList(label2types) + var/label = input("New canister label:", name) as null|anything in sortList(GLOB.gas_id_to_canister) if(label && !..()) - var/newtype = label2types[label] + var/newtype = GLOB.gas_id_to_canister[label] if(newtype) var/obj/machinery/portable_atmospherics/canister/replacement = newtype investigate_log("was relabelled to [initial(replacement.name)] by [key_name(usr)].", INVESTIGATE_ATMOS) name = initial(replacement.name) desc = initial(replacement.desc) icon_state = initial(replacement.icon_state) + base_icon_state = icon_state + set_greyscale(initial(replacement.greyscale_colors), initial(replacement.greyscale_config)) if("restricted") restricted = !restricted if(restricted) diff --git a/code/modules/tgui/states/greyscale_menu.dm b/code/modules/tgui/states/greyscale_menu.dm new file mode 100644 index 000000000000..9de6140e7092 --- /dev/null +++ b/code/modules/tgui/states/greyscale_menu.dm @@ -0,0 +1,14 @@ +/** + * tgui state: greyscale menu + * + * Checks that the target var of the greyscale menu meets the default can_use_topic criteria + */ + +GLOBAL_DATUM_INIT(greyscale_menu_state, /datum/ui_state/greyscale_menu_state, new) + +/datum/ui_state/greyscale_menu_state/can_use_topic(src_object, mob/user) + var/datum/greyscale_modify_menu/menu = src_object + if(!isatom(menu.target)) + return TRUE + + return GLOB.default_state.can_use_topic(menu.target, user) diff --git a/icons/Testing/greyscale_error.dmi b/icons/Testing/greyscale_error.dmi new file mode 100644 index 000000000000..6c781a70ad19 Binary files /dev/null and b/icons/Testing/greyscale_error.dmi differ diff --git a/icons/effects/shimmer.dmi b/icons/effects/shimmer.dmi new file mode 100644 index 000000000000..cf464371c815 Binary files /dev/null and b/icons/effects/shimmer.dmi differ diff --git a/icons/obj/atmos.dmi b/icons/obj/atmos.dmi index 719ed7222616..a280d0a4b9fc 100644 Binary files a/icons/obj/atmos.dmi and b/icons/obj/atmos.dmi differ diff --git a/icons/obj/atmospherics/canisters.dmi b/icons/obj/atmospherics/canisters.dmi new file mode 100644 index 000000000000..d7761182d8a5 Binary files /dev/null and b/icons/obj/atmospherics/canisters.dmi differ diff --git a/icons/obj/atmospherics/prototype_canister.dmi b/icons/obj/atmospherics/prototype_canister.dmi new file mode 100644 index 000000000000..fb73aa2ed6d2 Binary files /dev/null and b/icons/obj/atmospherics/prototype_canister.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 54a485b1a3eb..88525f772ac2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -160,6 +160,7 @@ #include "code\__DEFINES\dcs\flags.dm" #include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals.dm" +#include "code\__DEFINES\dcs\signals\signals_greyscale.dm" #include "code\__DEFINES\dcs\signals\signals_hud.dm" #include "code\__DEFINES\dcs\signals\signals_medical.dm" #include "code\__DEFINES\dcs\signals\signals_mod.dm" @@ -253,6 +254,7 @@ #include "code\_globalvars\lists\achievements.dm" #include "code\_globalvars\lists\client.dm" #include "code\_globalvars\lists\flavor_misc.dm" +#include "code\_globalvars\lists\icons.dm" #include "code\_globalvars\lists\keybindings.dm" #include "code\_globalvars\lists\loadout_categories.dm" #include "code\_globalvars\lists\maintenance_loot.dm" @@ -386,6 +388,7 @@ #include "code\controllers\subsystem\fire_burning.dm" #include "code\controllers\subsystem\fluid.dm" #include "code\controllers\subsystem\garbage.dm" +#include "code\controllers\subsystem\greyscale.dm" #include "code\controllers\subsystem\holodeck.dm" #include "code\controllers\subsystem\icon_smooth.dm" #include "code\controllers\subsystem\idlenpcpool.dm" @@ -724,6 +727,11 @@ #include "code\datums\elements\ventcrawling.dm" #include "code\datums\elements\weather_listener.dm" #include "code\datums\elements\wuv.dm" +#include "code\datums\greyscale\_greyscale_config.dm" +#include "code\datums\greyscale\json_reader.dm" +#include "code\datums\greyscale\layer.dm" +#include "code\datums\greyscale\config_types\greyscale_configs.dm" +#include "code\datums\greyscale\config_types\material_effects.dm" #include "code\datums\elements\screentips\contextual_screentip_bare_hands.dm" #include "code\datums\elements\screentips\contextual_screentip_item_typechecks.dm" #include "code\datums\elements\screentips\contextual_screentip_sharpness.dm" @@ -1486,6 +1494,7 @@ #include "code\modules\admin\create_turf.dm" #include "code\modules\admin\force_event.dm" #include "code\modules\admin\fun_balloon.dm" +#include "code\modules\admin\greyscale_modify_menu.dm" #include "code\modules\admin\holder2.dm" #include "code\modules\admin\ipintel.dm" #include "code\modules\admin\IsBanned.dm" @@ -3715,6 +3724,7 @@ #include "code\modules\tgui\states\deep_inventory.dm" #include "code\modules\tgui\states\default.dm" #include "code\modules\tgui\states\fun.dm" +#include "code\modules\tgui\states\greyscale_menu.dm" #include "code\modules\tgui\states\hands.dm" #include "code\modules\tgui\states\human_adjacent.dm" #include "code\modules\tgui\states\inventory.dm" diff --git a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx index 4866eba822d9..47d1d2febe98 100644 --- a/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx +++ b/tgui/packages/tgui/interfaces/GreyscaleModifyMenu.tsx @@ -5,20 +5,20 @@ import { Window } from '../layouts'; type ColorEntry = { index: Number; value: string; -} +}; type SpriteData = { icon_states: string[]; finished: string; steps: SpriteEntry[]; time_spent: Number; -} +}; type SpriteEntry = { layer: string; result: string; config_name: string; -} +}; type GreyscaleMenuData = { greyscale_config: string; @@ -26,31 +26,32 @@ type GreyscaleMenuData = { sprites: SpriteData; generate_full_preview: boolean; unlocked: boolean; + monitoring_files: boolean; sprites_dir: string; icon_state: string; refreshing: boolean; -} +}; enum Direction { - North = "north", - NorthEast = "northeast", - East = "east", - SouthEast = "southeast", - South = "south", - SouthWest = "southwest", - West = "west", - NorthWest = "northwest" + North = 'north', + NorthEast = 'northeast', + East = 'east', + SouthEast = 'southeast', + South = 'south', + SouthWest = 'southwest', + West = 'west', + NorthWest = 'northwest', } -const DirectionAbbreviation : Record = { - [Direction.North]: "N", - [Direction.NorthEast]: "NE", - [Direction.East]: "E", - [Direction.SouthEast]: "SE", - [Direction.South]: "S", - [Direction.SouthWest]: "SW", - [Direction.West]: "W", - [Direction.NorthWest]: "NW", +const DirectionAbbreviation: Record = { + [Direction.North]: 'N', + [Direction.NorthEast]: 'NE', + [Direction.East]: 'E', + [Direction.SouthEast]: 'SE', + [Direction.South]: 'S', + [Direction.SouthWest]: 'SW', + [Direction.West]: 'W', + [Direction.NorthWest]: 'NW', }; const ConfigDisplay = (props, context) => { @@ -59,13 +60,12 @@ const ConfigDisplay = (props, context) => {
-
); @@ -202,73 +199,77 @@ const PreviewDisplay = (props, context) => { - { - data.sprites?.finished - ? ( + {data.sprites?.finished ? ( + + + + ) : ( + + + + + + )} + + + {!!data.unlocked && `Time Spent: ${data.sprites.time_spent}ms`} + + {!data.refreshing && ( + + {!!data.generate_full_preview && data.sprites.steps !== null && ( + + + Layer Source + + + Step Layer + + + Step Result + + + )} + {!!data.generate_full_preview && + data.sprites.steps !== null && + data.sprites.steps.map((item) => ( + + + {item.config_name} + - + - ) - : ( - - - + - ) - } - -
- { - !!data.generate_full_preview - && `Time Spent: ${data.sprites.time_spent}ms` - } - - { - !data.refreshing - && ( - - { - !!data.generate_full_preview && data.sprites.steps !== null - && ( - - Layer Source - Step Layer - Step Result - - ) - } - { - !!data.generate_full_preview && data.sprites.steps !== null - && data.sprites.steps.map(item => ( - - {item.config_name} - - - - - - - - )) - } -
- ) - } + + ))} + + )} ); }; const SingleSprite = (props) => { - const { - source, - } = props; + const { source } = props; return ( ); }; @@ -284,33 +285,54 @@ const LoadingAnimation = () => { export const GreyscaleModifyMenu = (props, context) => { const { act, data } = useBackend(context); return ( - + - { - !!data.unlocked - &&