mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 09:42:29 +00:00
* Adds a color matrix layer to GAGS (#63957) * Adds a color matrix layer to GAGS * Fixes default row value * Passes along the last viable icon for color matrix use * Removes stray nocache reference * Adds a color matrix layer to GAGS Co-authored-by: Emmett Gaines <ninjanomnom@gmail.com>
307 lines
11 KiB
Plaintext
307 lines
11 KiB
Plaintext
#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(!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(delta_time)
|
|
if(!Refresh(loadFromDisk=TRUE))
|
|
return
|
|
if(!live_edit_types)
|
|
return
|
|
for(var/atom/thing in world)
|
|
if(live_edit_types[thing.type])
|
|
thing.update_greyscale()
|
|
|
|
/datum/greyscale_config/proc/EnableAutoRefresh(live_type)
|
|
message_admins("Config auto refresh has been enabled for '[live_type]' with configuration [DebugName()]. Expect heavy lag.")
|
|
if(live_type)
|
|
if(!live_edit_types)
|
|
live_edit_types = list()
|
|
live_edit_types += typecacheof(live_type)
|
|
START_PROCESSING(SSgreyscale, src)
|
|
|
|
/datum/greyscale_config/proc/DisableAutoRefresh(live_type, remove_all=FALSE)
|
|
if(!remove_all && !(live_type in live_edit_types))
|
|
return
|
|
message_admins("Config auto refresh has been disabled for '[live_type]' with configuration [DebugName()]")
|
|
if(remove_all)
|
|
live_edit_types = null
|
|
else if(live_type && live_edit_types)
|
|
live_edit_types -= typecacheof(live_type)
|
|
if(!length(live_edit_types))
|
|
live_edit_types = null
|
|
STOP_PROCESSING(SSgreyscale, src)
|
|
|
|
/// Call this proc to handle all the data extraction from the json configuration. Can be forced to load values from disk instead of memory.
|
|
/datum/greyscale_config/proc/Refresh(loadFromDisk=FALSE)
|
|
if(loadFromDisk)
|
|
var/changed = FALSE
|
|
|
|
json_config = file(string_json_config)
|
|
var/json_hash = md5asfile(json_config)
|
|
if(json_config_hash != json_hash)
|
|
json_config_hash = json_hash
|
|
changed = TRUE
|
|
|
|
icon_file = file(string_icon_file)
|
|
var/icon_hash = md5asfile(icon_file)
|
|
if(icon_file_hash != icon_hash)
|
|
icon_file_hash = icon_hash
|
|
changed = TRUE
|
|
|
|
for(var/datum/greyscale_layer/layer as anything in flat_all_layers)
|
|
if(layer.DiskRefresh())
|
|
changed = TRUE
|
|
|
|
if(!changed)
|
|
return FALSE
|
|
|
|
var/list/raw = json_decode(file2text(json_config))
|
|
ReadIconStateConfiguration(raw)
|
|
|
|
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["layer_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/icon/source = icon(icon_file)
|
|
height = source.Height()
|
|
width = source.Width()
|
|
|
|
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
|