Ports Iconforge, uses for kitchen recipes (#16747)

* Port iconforge

* Convert kitchen_icons for the microwave to iconforge

* Fix to pass lint

* brings thsoe back

* Re-enable cross round caching for normal spritesheets

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
ShadowLarkens
2024-12-29 05:39:40 -08:00
committed by GitHub
parent 845d0caffe
commit 6ab8ea768d
23 changed files with 838 additions and 153 deletions
@@ -0,0 +1,356 @@
#define SPR_SIZE "size_id"
#define SPR_IDX "position"
#define CACHE_WAIT "wait"
#define CACHE_INVALID TRUE
#define CACHE_VALID FALSE
#define WHATTHEFUCKAMIDOING_FILE "data/WHATTHEFUCKFILE.json"
/datum/asset/spritesheet_batched
_abstract = /datum/asset/spritesheet_batched
var/name
/// list("32x32")
var/list/sizes = list()
/// "foo_bar" -> list("32x32", 5, entry_obj)
var/list/sprites = list()
// "foo_bar" -> entry_obj
var/list/entries = list()
/// JSON encoded version of entries.
var/entries_json = null
/// If this spritesheet exists in a completed state.
var/fully_generated = FALSE
/// If this asset should be fully loaded on new
/// Defaults to false so we can process this stuff nicely
var/load_immediately = FALSE
/// If we should avoid propogating 'invalid dir' errors from rust-g. Because sometimes, you just don't know what dirs are valid.
var/ignore_dir_errors = FALSE
/// Avoid propogating 'Could not find associated icon state' errors because we know our input data fucking sucks
var/ignore_associated_icon_state_errors = FALSE
/// Forces use of the smart cache. This is for unit tests, please respect the config <3
var/force_cache = FALSE
/// If there is currently an async job, its ID
var/job_id = null
/// If there is currently an async cache job, its ID.
var/cache_job_id = null
// Fields to store async cache task inputs.
var/cache_data = null
var/cache_sizes_data = null
var/cache_sprites_data = null
var/cache_input_hash = null
var/cache_dmi_hashes = null
var/cache_dmi_hashes_json = null
/// Used to prevent async cache refresh jobs from looping on failure.
var/cache_result = null
/datum/asset/spritesheet_batched/proc/should_load_immediately()
#ifdef DO_NOT_DEFER_ASSETS
return TRUE
#else
return load_immediately
#endif
/// Returns true if the cache should be invalidated/doesn't exist.
/datum/asset/spritesheet_batched/should_refresh(yield)
. = CACHE_INVALID // in the case of any errors, we need to regenerate.
if(!fexists("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.[name].json"))
return CACHE_INVALID
if(!fexists("data/spritesheets/spritesheet_[name].css"))
return CACHE_INVALID
if(isnull(cache_data) || isnull(cache_dmi_hashes_json))
cache_data = rustg_file_read("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.[name].json")
if(!findtext(cache_data, "{", 1, 2)) // cache isn't valid JSON
log_asset("Cache for spritesheet_[name] was not valid JSON. This is abnormal. Likely tampered with or IO failure.")
return CACHE_INVALID
var/cache_json = json_decode(cache_data)
// Best to invalidate if rustg updates, since the way icons are handled can change.
var/cached_rustg_version = cache_json["rustg_version"]
if(isnull(cached_rustg_version))
log_asset("Cache for spritesheet_[name] did not contain a rustg_version!")
return CACHE_INVALID
var/rustg_version = rustg_get_version()
if(cached_rustg_version != rustg_version)
log_asset("Invalidated cache for spritesheet_[name] due to rustg updating from [cached_rustg_version] to [rustg_version].")
return CACHE_INVALID
cache_sizes_data = cache_json["sizes"]
cache_sprites_data = cache_json["sprites"]
cache_input_hash = cache_json["input_hash"]
cache_dmi_hashes = cache_json["dmi_hashes"]
if(!length(cache_dmi_hashes) || !length(cache_input_hash) || !length(cache_sizes_data) || !length(cache_sprites_data))
log_asset("Cache for spritesheet_[name] did not contain the correct data. This is abnormal. Likely tampered with.")
return CACHE_INVALID
cache_dmi_hashes_json = json_encode(cache_dmi_hashes)
var/data_out
if(yield || !isnull(cache_job_id))
if(isnull(cache_job_id))
cache_job_id = rustg_iconforge_cache_valid_async(cache_input_hash, cache_dmi_hashes_json, entries_json)
. = CACHE_WAIT // if we return during this, WAIT!!
UNTIL((data_out = rustg_iconforge_check(cache_job_id)) != RUSTG_JOB_NO_RESULTS_YET)
cache_job_id = null
. = CACHE_INVALID // reset back to normal, invalid on CRASH
else
data_out = rustg_iconforge_cache_valid(cache_input_hash, cache_dmi_hashes_json, entries_json)
if (data_out == RUSTG_JOB_ERROR)
CRASH("Spritesheet [name] cache JOB PANIC")
else if(!findtext(data_out, "{", 1, 2))
rustg_file_write(cache_data, "[log_path]-spritesheet_cache_debug.[name].json")
rustg_file_write(entries_json, "[log_path]-spritesheet_debug_[name].json")
CRASH("Spritesheet [name] cache check UNKNOWN ERROR: [data_out]")
var/result = json_decode(data_out)
var/fail = result["fail_reason"]
if(length(fail) || result["result"] != "1")
if(findtextEx(fail, "ERROR:"))
CRASH("Spritesheet [name] cache check UNKNOWN [fail]")
log_asset("Invalidated cache for spritesheet_[name]: [fail]")
return CACHE_INVALID
// Populate the sizes and sprites list.
sizes = cache_sizes_data
sprites = cache_sprites_data
log_asset("Validated cache for spritesheet_[name]!")
return CACHE_VALID
/datum/asset/spritesheet_batched/proc/insert_icon(sprite_name, datum/universal_icon/entry)
if(!istext(sprite_name) || !length(sprite_name))
CRASH("Invalid sprite_name \"[sprite_name]\" given to insert_icon()! Providing non-strings will break icon generation.")
entries[sprite_name] = entry.to_list()
/datum/asset/spritesheet_batched/register()
SHOULD_NOT_OVERRIDE(TRUE)
if (!name)
CRASH("spritesheet [type] cannot register without a name")
// Create our input data first, so we can compare to the cache.
create_spritesheets()
if(should_load_immediately())
realize_spritesheets(yield = FALSE)
else
SSasset_loading.queue_asset(src)
/datum/asset/spritesheet_batched/unregister()
CRASH("unregister() called on batched spritesheet! Bad!")
/// Call insert_icon or insert_all_icons here, building a spritesheet!
/datum/asset/spritesheet_batched/proc/create_spritesheets()
SHOULD_CALL_PARENT(FALSE)
CRASH("create_spritesheets() not implemented for [type]!")
/datum/asset/spritesheet_batched/proc/insert_all_icons(prefix, icon/I, list/directions, prefix_with_dirs = TRUE)
if (length(prefix))
prefix = "[prefix]-"
if (!directions)
directions = list(SOUTH)
for (var/icon_state_name in icon_states(I))
if(icon_state_name == "")
icon_state_name = "byond-default"
for (var/direction in directions)
var/prefix2 = (directions.len > 1 && prefix_with_dirs) ? "[dir2text(direction)]-" : ""
insert_icon("[prefix][prefix2][icon_state_name]", uni_icon(I, icon_state_name, direction))
/datum/asset/spritesheet_batched/proc/realize_spritesheets(yield)
if(fully_generated)
return
if(!length(entries))
CRASH("Spritesheet [name] ([type]) is empty! What are you doing?")
if(isnull(entries_json))
entries_json = json_encode(entries)
if(isnull(cache_result))
cache_result = should_refresh(yield)
if(cache_result == CACHE_WAIT) // sleep interrupted by MC. We'll get queried again later.
cache_result = null
return
// read_from_cache returns false if config is disabled, otherwise it fully loads the spritesheet.
if (cache_result == CACHE_VALID && read_from_cache())
SSasset_loading.dequeue_asset(src)
fully_generated = TRUE
return
// Remove the cache, since it's invalid if we get to this point.
fdel("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.[name].json")
var/do_cache = CONFIG_GET(flag/smart_cache_assets) || force_cache
var/data_out
if(yield || !isnull(job_id))
if(isnull(job_id))
job_id = rustg_iconforge_generate_async("data/spritesheets/", name, entries_json, do_cache)
UNTIL((data_out = rustg_iconforge_check(job_id)) != RUSTG_JOB_NO_RESULTS_YET)
else
//rustg_file_write(entries_json, "fuckoff.json")
data_out = rustg_iconforge_generate("data/spritesheets/", name, entries_json, do_cache)
if (data_out == RUSTG_JOB_ERROR)
CRASH("Spritesheet [name] JOB PANIC")
else if(!findtext(data_out, "{", 1, 2))
rustg_file_write(entries_json, "[log_path]-spritesheet_debug_[name].json")
CRASH("Spritesheet [name] UNKNOWN ERROR: [data_out]")
var/data = json_decode(data_out)
sizes = data["sizes"]
sprites = data["sprites"]
var/input_hash = data["sprites_hash"]
var/dmi_hashes = data["dmi_hashes"] // this only contains values if do_cache is TRUE.
for(var/size_id in sizes)
var/file_path = "data/spritesheets/[name]_[size_id].png"
var/file_hash = rustg_hash_file("md5", file_path)
SSassets.transport.register_asset("[name]_[size_id].png", fcopy_rsc(file_path), file_hash)
var/res_name = "spritesheet_[name].css"
var/fname = "data/spritesheets/[res_name]"
fdel(fname)
var/css = generate_css()
rustg_file_write(css, fname)
var/css_hash = rustg_hash_string("md5", css)
SSassets.transport.register_asset(res_name, fcopy_rsc(fname), file_hash=css_hash)
if (do_cache)
write_cache_meta(input_hash, dmi_hashes)
fully_generated = TRUE
// If we were ever in there, remove ourselves
SSasset_loading.dequeue_asset(src)
if(data["error"])
var/err = data["error"]
if(ignore_dir_errors && findtext(err, "is not in the set of valid dirs"))
return
if(ignore_associated_icon_state_errors && findtext(err, "Could not find associated icon state"))
return
CRASH("Error during spritesheet generation for [name]: [err]")
/datum/asset/spritesheet_batched/queued_generation()
realize_spritesheets(yield = TRUE)
/datum/asset/spritesheet_batched/ensure_ready()
if(!fully_generated)
realize_spritesheets(yield = FALSE)
return ..()
/datum/asset/spritesheet_batched/send(client/client)
if (!name)
return
var/all = list("spritesheet_[name].css")
for(var/size_id in sizes)
all += "[name]_[size_id].png"
. = SSassets.transport.send_assets(client, all)
/datum/asset/spritesheet_batched/get_url_mappings()
if (!name)
return
. = list("spritesheet_[name].css" = SSassets.transport.get_asset_url("spritesheet_[name].css"))
for(var/size_id in sizes)
.["[name]_[size_id].png"] = SSassets.transport.get_asset_url("[name]_[size_id].png")
/datum/asset/spritesheet_batched/proc/generate_css()
var/list/out = list()
for (var/size_id in sizes)
var/size_split = splittext(size_id, "x")
var/width = text2num(size_split[1])
var/height = text2num(size_split[2])
out += ".[name][size_id]{display:inline-block;width:[width]px;height:[height]px;background:url('[get_background_url("[name]_[size_id].png")]') no-repeat;}"
for (var/sprite_id in sprites)
var/sprite = sprites[sprite_id]
var/size_id = sprite[SPR_SIZE]
var/idx = sprite[SPR_IDX]
var/size_split = splittext(size_id, "x")
var/width = text2num(size_split[1])
var/x = idx * width
var/y = 0
out += ".[name][size_id].[sprite_id]{background-position:-[x]px -[y]px;}"
return out.Join("\n")
/datum/asset/spritesheet_batched/proc/read_from_cache()
if(!CONFIG_GET(flag/smart_cache_assets) && !force_cache)
return FALSE
// this is already guaranteed to exist.
var/css_fname = "data/spritesheets/spritesheet_[name].css"
// sizes gets filled during should_refresh()
for(var/size_id in sizes)
var/fname = "data/spritesheets/[name]_[size_id].png"
if(!fexists(fname))
return FALSE
var/css_hash = rustg_hash_file("md5", css_fname)
SSassets.transport.register_asset("spritesheet_[name].css", fcopy_rsc(css_fname), file_hash=css_hash)
for(var/size_id in sizes)
var/fname = "data/spritesheets/[name]_[size_id].png"
var/hash = rustg_hash_file("md5", fname)
SSassets.transport.register_asset("[name]_[size_id].png", fcopy_rsc(fname), file_hash=hash)
return TRUE
/// Returns the URL to put in the background:url of the CSS asset
/datum/asset/spritesheet_batched/proc/get_background_url(asset)
return SSassets.transport.get_asset_url(asset)
/datum/asset/spritesheet_batched/proc/write_cache_meta(input_hash, dmi_hashes)
var/list/cache_data = list(
"input_hash" = input_hash,
"dmi_hashes" = dmi_hashes,
"sizes" = sizes,
"sprites" = sprites,
"rustg_version" = rustg_get_version()
)
rustg_file_write(json_encode(cache_data), "[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.[name].json")
/**
* Third party helpers
* ===================
*/
/datum/asset/spritesheet_batched/proc/css_tag()
return {"<link rel="stylesheet" href="[css_filename()]" />"}
/datum/asset/spritesheet_batched/proc/css_filename()
return SSassets.transport.get_asset_url("spritesheet_[name].css")
/datum/asset/spritesheet_batched/proc/icon_tag(sprite_name)
var/sprite = sprites[sprite_name]
if (!sprite)
return null
var/size_id = sprite[SPR_SIZE]
return "<span class='[name][size_id] [sprite_name]'></span>"
/datum/asset/spritesheet_batched/proc/icon_class_name(sprite_name)
var/sprite = sprites[sprite_name]
if (!sprite)
return null
var/size_id = sprite[SPR_SIZE]
return "[name][size_id] [sprite_name]"
/**
* Returns the size class (ex design32x32) for a given sprite's icon
*
* Arguments:
* * sprite_name - The sprite to get the size of
*/
/datum/asset/spritesheet_batched/proc/icon_size_id(sprite_name)
var/sprite = sprites[sprite_name]
if (!sprite)
return null
var/size_id = sprite[SPR_SIZE]
return "[name][size_id]"
#undef SPR_SIZE
#undef SPR_IDX
#undef CACHE_WAIT
#undef CACHE_INVALID
#undef CACHE_VALID
#undef WHATTHEFUCKAMIDOING_FILE
@@ -0,0 +1,186 @@
/// This is intended to replace /icon, allowing rustg to generate icons much faster than DM can at scale.
/// Construct these with the uni_icon() proc, in the same manner as BYOND's icon() proc.
/// Additionally supports a number of transform procs (lowercase, rather than BYOND's uppercase)
/// such as Crop, Scale, and Blend (as blend_icon/blend_color).
/datum/universal_icon
var/icon/icon_file
var/icon_state
var/dir
var/frame
var/datum/icon_transformer/transform
/// Don't instantiate these yourself, use uni_icon.
/datum/universal_icon/New(icon/icon_file, icon_state="", dir=SOUTH, frame=1, datum/icon_transformer/transform=null, color=null)
#ifdef UNIT_TESTS
// This check is kinda slow and shouldn't fail unless a developer makes a mistake. So it'll get caught in unit tests.
if(!isicon(icon_file) || !isfile(icon_file) || "[icon_file]" == "/icon")
// bad! use 'icons/path_to_dmi.dmi' format only
CRASH("FATAL: universal_icon was provided icon_file: [icon_file] - icons provided to batched spritesheets MUST be DMI files, they cannot be /image, /icon, or other runtime generated icons.")
#endif
src.icon_file = icon_file
src.icon_state = icon_state
src.dir = dir
src.frame = frame
if(isnull(transform) && !isnull(color) && uppertext(color) != "#FFFFFF")
var/datum/icon_transformer/T = new()
if(color)
T.blend_color(color, ICON_MULTIPLY)
src.transform = T
else if(!isnull(transform))
src.transform = transform
else // null = empty list
src.transform = null
/datum/universal_icon/proc/copy()
RETURN_TYPE(/datum/universal_icon)
var/datum/universal_icon/new_icon = new(icon_file, icon_state, dir, frame)
if(!isnull(src.transform))
new_icon.transform = src.transform.copy()
return new_icon
/datum/universal_icon/proc/blend_color(color, blend_mode)
if(!transform)
transform = new
transform.blend_color(color, blend_mode)
return src
/datum/universal_icon/proc/blend_icon(datum/universal_icon/icon_object, blend_mode)
if(!transform)
transform = new
transform.blend_icon(icon_object, blend_mode)
return src
/datum/universal_icon/proc/scale(width, height)
if(!transform)
transform = new
transform.scale(width, height)
return src
/datum/universal_icon/proc/crop(x1, y1, x2, y2)
if(!transform)
transform = new
transform.crop(x1, y1, x2, y2)
return src
/datum/universal_icon/proc/to_list()
RETURN_TYPE(/list)
return list("icon_file" = "[icon_file]", "icon_state" = icon_state, "dir" = dir, "frame" = frame, "transform" = !isnull(transform) ? transform.to_list() : list())
/proc/universal_icon_from_list(list/input_in)
RETURN_TYPE(/datum/universal_icon)
var/list/input = input_in.Copy() // copy, since icon_transformer_from_list will mutate the list.
return uni_icon(input["icon_file"], input["icon_state"], input["dir"], input["frame"], icon_transformer_from_list(input["transform"]))
/datum/universal_icon/proc/to_json()
return json_encode(to_list())
/datum/universal_icon/proc/to_icon()
RETURN_TYPE(/icon)
var/icon/self = icon(src.icon_file, src.icon_state, dir=src.dir, frame=src.frame)
if(istype(src.transform))
src.transform.apply(self)
return self
/datum/icon_transformer
var/list/transforms = null
/datum/icon_transformer/New()
transforms = list()
/// Applies the contained set of transforms to an icon
/datum/icon_transformer/proc/apply(icon/target)
RETURN_TYPE(/icon)
for(var/transform in src.transforms)
switch(transform["type"])
if(RUSTG_ICONFORGE_BLEND_COLOR)
target.Blend(target["color"], target["blend_mode"])
if(RUSTG_ICONFORGE_BLEND_ICON)
var/datum/universal_icon/icon_object = target["icon"]
if(!istype(icon_object))
stack_trace("Invalid icon found in icon transformer during apply()! [icon_object]")
continue
target.Blend(icon_object.to_icon(), target["blend_mode"])
if(RUSTG_ICONFORGE_SCALE)
target.Scale(target["width"], target["height"])
if(RUSTG_ICONFORGE_CROP)
target.Crop(target["x1"], target["y1"], target["x2"], target["y2"])
return target
/datum/icon_transformer/proc/copy()
RETURN_TYPE(/datum/icon_transformer)
var/datum/icon_transformer/new_transformer = new()
new_transformer.transforms = list()
for(var/transform in src.transforms)
new_transformer.transforms += list(deep_copy_list_alt(transform))
return new_transformer
/datum/icon_transformer/proc/blend_color(color, blend_mode)
transforms += list(list("type" = RUSTG_ICONFORGE_BLEND_COLOR, "color" = color, "blend_mode" = blend_mode))
/datum/icon_transformer/proc/blend_icon(datum/universal_icon/icon_object, blend_mode)
transforms += list(list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = icon_object, "blend_mode" = blend_mode))
/datum/icon_transformer/proc/scale(width, height)
transforms += list(list("type" = RUSTG_ICONFORGE_SCALE, "width" = width, "height" = height))
/datum/icon_transformer/proc/crop(x1, y1, x2, y2)
transforms += list(list("type" = RUSTG_ICONFORGE_CROP, "x1" = x1, "y1" = y1, "x2" = x2, "y2" = y2))
/datum/icon_transformer/proc/to_list()
RETURN_TYPE(/list)
var/list/transforms_out = list()
for(var/transform in src.transforms.Copy())
var/this_transform = transform
if(transform["type"] == RUSTG_ICONFORGE_BLEND_ICON)
var/datum/universal_icon/icon_object = this_transform["icon"]
if(!istype(icon_object))
stack_trace("Invalid icon found in icon transformer during to_list()! [icon_object]")
continue
this_transform["icon"] = icon_object.to_list()
transforms_out += list(this_transform)
return transforms_out
/proc/icon_transformer_from_list(list/input)
RETURN_TYPE(/datum/icon_transformer)
var/list/transforms = list()
for(var/transform in input)
var/this_transform = transform
if(transform["type"] == RUSTG_ICONFORGE_BLEND_ICON)
this_transform["icon"] = universal_icon_from_list(transform["icon"])
transforms += list(this_transform)
var/datum/icon_transformer/transformer = new()
transformer.transforms = transforms
return transformer
/// Constructs a transformer, with optional color multiply pre-added.
/proc/color_transform(color=null)
RETURN_TYPE(/datum/icon_transformer)
var/datum/icon_transformer/transform = new()
if(color)
transform.blend_color(color, ICON_MULTIPLY)
return transform
/// Converts a GAGS item to a universal icon by generating blend operations.
// /proc/gags_to_universal_icon(obj/item/path)
// RETURN_TYPE(/datum/universal_icon)
// if(!ispath(path, /obj/item) || !initial(path.greyscale_config) || !initial(path.greyscale_colors))
// CRASH("gags_to_universal_icon() received an invalid path!")
// var/datum/greyscale_config/config = initial(path.greyscale_config)
// var/colors = initial(path.greyscale_colors)
// var/datum/universal_icon/entry = SSgreyscale.GetColoredIconEntryByType(config, colors, initial(path.icon_state))
// return entry
/// Gets the relevant universal icon for an atom, when displayed in TGUI. (see: icon_state_preview)
/// Supports GAGS items and colored items.
/proc/get_display_icon_for(atom/A)
if (!ispath(A, /atom))
return FALSE
var/icon_file = initial(A.icon)
var/icon_state = initial(A.icon_state)
// if(ispath(A, /obj/item))
// var/obj/item/I = A
// if(initial(I.icon_state_preview))
// icon_state = initial(I.icon_state_preview)
// if(initial(I.greyscale_config) && initial(I.greyscale_colors))
// return gags_to_universal_icon(I)
return uni_icon(icon_file, icon_state, color=initial(A.color))