Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/__DEFINES/dcs/signals/signals_greyscale.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#define COMSIG_GREYSCALE_CONFIG_REFRESHED "greyscale_config_refreshed"
1 change: 1 addition & 0 deletions code/__DEFINES/subsystems.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions code/__DEFINES/vv.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions code/__HELPERS/icons.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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]
2 changes: 2 additions & 0 deletions code/_globalvars/lists/icons.dm
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions code/_globalvars/regexes.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
50 changes: 50 additions & 0 deletions code/controllers/subsystem/greyscale.dm
Original file line number Diff line number Diff line change
@@ -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]]"
73 changes: 73 additions & 0 deletions code/datums/components/gags_recolorable.dm
Original file line number Diff line number Diff line change
@@ -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)
112 changes: 112 additions & 0 deletions code/datums/greyscale/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading