diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm index d2928195a50..048ea0bd0b7 100644 --- a/code/__DEFINES/rust_g.dm +++ b/code/__DEFINES/rust_g.dm @@ -138,6 +138,36 @@ */ #define rustg_dmi_icon_states(fname) RUSTG_CALL(RUST_G, "dmi_icon_states")(fname) +/** + * The below functions involve dmi metadata represented in the following format: + * list( + * "width": number, + * "height": number, + * "states": list([STATE_DATA], ...) + * ) + * + * STATE_DATA format: + * list( + * "name": string, + * "dirs": 1 | 4 | 8, + * "delays"?: list(number, ...), + * "rewind"?: TRUE | FALSE, + * "movement"?: TRUE | FALSE, + * "loop"?: number + * ) + */ + +/** + * Get the dmi metadata of the file located at `fname`. + * Returns a list in the metadata format listed above, or an error message. + */ +#define rustg_dmi_read_metadata(fname) json_decode(RUSTG_CALL(RUST_G, "dmi_read_metadata")(fname)) +/** + * Inject dmi metadata into a png file located at `path`. + * `metadata` must be a json_encode'd list in the metadata format listed above. + */ +#define rustg_dmi_inject_metadata(path, metadata) RUSTG_CALL(RUST_G, "dmi_inject_metadata")(path, metadata) + #define rustg_file_read(fname) RUSTG_CALL(RUST_G, "file_read")(fname) #define rustg_file_exists(fname) (RUSTG_CALL(RUST_G, "file_exists")(fname) == "true") #define rustg_file_write(text, fname) RUSTG_CALL(RUST_G, "file_write")(text, fname) @@ -198,13 +228,21 @@ #define rustg_http_request_blocking(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_blocking")(method, url, body, headers, options) #define rustg_http_request_async(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_async")(method, url, body, headers, options) #define rustg_http_check_request(req_id) RUSTG_CALL(RUST_G, "http_check_request")(req_id) +/// This is basically just `rustg_http_request_async` if you don't care about the response. +/// This will either return "ok" or an error, as this does not create a job. +#define rustg_http_request_fire_and_forget(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_fire_and_forget")(method, url, body, headers, options) -/// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].png +/// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].[png or dmi] /// The resulting spritesheet arranges icons in a random order, with the position being denoted in the "sprites" return value. /// All icons have the same y coordinate, and their x coordinate is equal to `icon_width * position`. /// /// hash_icons is a boolean (0 or 1), and determines if the generator will spend time creating hashes for the output field dmi_hashes. -/// These hashes can be heplful for 'smart' caching (see rustg_iconforge_cache_valid), but require extra computation. +/// These hashes can be helpful for 'smart' caching (see rustg_iconforge_cache_valid), but require extra computation. +/// +/// generate_dmi is a boolean (0 or 1), and determines if the generator will save the sheet as a DMI or stripped PNG file. +/// DMI files can be used to replace bulk Insert() operations, PNGs are more useful for asset transport or UIs. DMI generation is slower due to more metadata. +/// flatten is a boolean (0 or 1), and determines if the DMI output will be flattened to a single frame/dir if unscoped (null/0 dir or frame values). +/// PNGs are always flattened, regardless of argument. /// /// Spritesheet will contain all sprites listed within "sprites". /// "sprites" format: @@ -220,9 +258,15 @@ /// ) /// TRANSFORM_OBJECT format: /// list("type" = RUSTG_ICONFORGE_BLEND_COLOR, "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) -/// list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY) +/// list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY, "x" = 1, "y" = 1) // offsets optional /// list("type" = RUSTG_ICONFORGE_SCALE, "width" = 32, "height" = 32) /// list("type" = RUSTG_ICONFORGE_CROP, "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // (BYOND icons index from 1,1 to the upper bound, inclusive) +/// list("type" = RUSTG_ICONFORGE_MAP_COLORS, "rr" = 0.5, "rg" = 0.5, "rb" = 0.5, "ra" = 1, "gr" = 1, "gg" = 1, "gb" = 1, "ga" = 1, ...) // alpha arguments and rgba0 optional +/// list("type" = RUSTG_ICONFORGE_FLIP, "dir" = SOUTH) +/// list("type" = RUSTG_ICONFORGE_TURN, "angle" = 90.0) +/// list("type" = RUSTG_ICONFORGE_SHIFT, "dir" = EAST, "offset" = 10, "wrap" = FALSE) +/// list("type" = RUSTG_ICONFORGE_SWAP_COLOR, "src_color" = "#ff0000", "dst_color" = "#00ff00") // alpha bits supported +/// list("type" = RUSTG_ICONFORGE_DRAW_BOX, "color" = "#ff0000", "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // alpha bits supported. color can be null/omitted for transparency. x2 and y2 will default to x1 and y1 if omitted /// /// Returns a SpritesheetResult as JSON, containing fields: /// list( @@ -233,9 +277,9 @@ /// "error" = "[A string, empty if there were no errors.]" /// ) /// In the case of an unrecoverable panic from within Rust, this function ONLY returns a string containing the error. -#define rustg_iconforge_generate(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites, "[hash_icons]") +#define rustg_iconforge_generate(file_path, spritesheet_name, sprites, hash_icons, generate_dmi, flatten) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites, "[hash_icons]", "[generate_dmi]", "[flatten]") /// Returns a job_id for use with rustg_iconforge_check() -#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites, "[hash_icons]") +#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons, generate_dmi, flatten) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites, "[hash_icons]", "[generate_dmi]", "[flatten]") /// Returns the status of an async job_id, or its result if it is completed. See RUSTG_JOB DEFINEs. #define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") /// Clears all cached DMIs and images, freeing up memory. @@ -256,7 +300,7 @@ /// Provided a /datum/greyscale_config typepath, JSON string containing the greyscale config, and path to a DMI file containing the base icons, /// Loads that config into memory for later use by rustg_iconforge_gags(). The config_path is the unique identifier used later. /// JSON Config schema: https://hackmd.io/@tgstation/GAGS-Layer-Types -/// Unsupported features: color_matrix layer type, 'or' blend_mode. May not have BYOND parity with animated icons or varying dirs between layers. +/// Adding dirs or frames (via blending larger icons) to icons with more than 1 dir or 1 frame is not supported. /// Returns "OK" if successful, otherwise, returns a string containing the error. #define rustg_iconforge_load_gags_config(config_path, config_json, config_icon_path) RUSTG_CALL(RUST_G, "iconforge_load_gags_config")("[config_path]", config_json, config_icon_path) /// Given a config_path (previously loaded by rustg_iconforge_load_gags_config), and a string of hex colors formatted as "#ff00ff#ffaa00" @@ -272,6 +316,12 @@ #define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon" #define RUSTG_ICONFORGE_CROP "Crop" #define RUSTG_ICONFORGE_SCALE "Scale" +#define RUSTG_ICONFORGE_MAP_COLORS "MapColors" +#define RUSTG_ICONFORGE_FLIP "Flip" +#define RUSTG_ICONFORGE_TURN "Turn" +#define RUSTG_ICONFORGE_SHIFT "Shift" +#define RUSTG_ICONFORGE_SWAP_COLOR "SwapColor" +#define RUSTG_ICONFORGE_DRAW_BOX "DrawBox" #define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET" #define RUSTG_JOB_NO_SUCH_JOB "NO SUCH JOB" @@ -298,6 +348,39 @@ */ #define rustg_noise_poisson_map(seed, width, length, radius) RUSTG_CALL(RUST_G, "noise_poisson_map")(seed, width, length, radius) +/** + * Register a list of nodes into a rust library. This list of nodes must have been serialized in a json. + * Node {// Index of this node in the list of nodes + * unique_id: usize, + * // Position of the node in byond + * x: usize, + * y: usize, + * z: usize, + * // Indexes of nodes connected to this one + * connected_nodes_id: Vec} + * It is important that the node with the unique_id 0 is the first in the json, unique_id 1 right after that, etc. + * It is also important that all unique ids follow. {0, 1, 2, 4} is not a correct list and the registering will fail + * Nodes should not link across z levels. + * A node cannot link twice to the same node and shouldn't link itself either + */ +#define rustg_register_nodes_astar(json) RUSTG_CALL(RUST_G, "register_nodes_astar")(json) + +/** + * Add a new node to the static list of nodes. Same rule as registering_nodes applies. + * This node unique_id must be equal to the current length of the static list of nodes + */ +#define rustg_add_node_astar(json) RUSTG_CALL(RUST_G, "add_node_astar")(json) + +/** + * Remove every link to the node with unique_id. Replace that node by null + */ +#define rustg_remove_node_astar(unique_id) RUSTG_CALL(RUST_G, "remove_node_astar")("[unique_id]") + +/** + * Compute the shortest path between start_node and goal_node using A*. Heuristic used is simple geometric distance + */ +#define rustg_generate_path_astar(start_node_id, goal_node_id) RUSTG_CALL(RUST_G, "generate_path_astar")("[start_node_id]", "[goal_node_id]") + /* * Takes in a string and json_encode()"d lists to produce a sanitized string. * This function operates on whitelists, there is currently no way to blacklist. @@ -362,8 +445,17 @@ #define rustg_time_milliseconds(id) text2num(RUSTG_CALL(RUST_G, "time_milliseconds")(id)) #define rustg_time_reset(id) RUSTG_CALL(RUST_G, "time_reset")(id) +/// Returns the current timestamp (in local time), formatted with the given format string. +/// See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for documentation on the formatting syntax. +#define rustg_formatted_timestamp(format) RUSTG_CALL(RUST_G, "formatted_timestamp")(format) + +/// Returns the current timestamp (with the given UTC offset in hours), formatted with the given format string. +/// See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for documentation on the formatting syntax. +#define rustg_formatted_timestamp_tz(format, offset) RUSTG_CALL(RUST_G, "formatted_timestamp")(format, offset) + /// Returns the timestamp as a string -#define rustg_unix_timestamp(...) (RUSTG_CALL(RUST_G, "unix_timestamp")()) +/proc/rustg_unix_timestamp() + return RUSTG_CALL(RUST_G, "unix_timestamp")() #define rustg_raw_read_toml_file(path) json_decode(RUSTG_CALL(RUST_G, "toml_file_to_json")(path) || "null") @@ -391,10 +483,3 @@ #define url_decode(text) rustg_url_decode(text) #endif -/// Returns the current timestamp (in local time), formatted with the given format string. -/// See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for documentation on the formatting syntax. -#define rustg_formatted_timestamp(format) RUSTG_CALL(RUST_G, "formatted_timestamp")(format) - -/// Returns the current timestamp (with the given UTC offset in hours), formatted with the given format string. -/// See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for documentation on the formatting syntax. -#define rustg_formatted_timestamp_tz(format, offset) RUSTG_CALL(RUST_G, "formatted_timestamp")(format, offset) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 86817a55440..ba9d701b03a 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1171,27 +1171,60 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects) animate(pixel_x = initialpixelx + rand(-pixelshiftx,pixelshiftx), pixel_y = initialpixely + rand(-pixelshifty,pixelshifty), time = shake_interval) animate(pixel_x = initialpixelx, pixel_y = initialpixely, time = shake_interval) +/// Returns rustg-parsed metadata for an icon, universal icon, or DMI file, using cached values where possible +/// Returns null if passed object is not a filepath or icon with a valid DMI file +/proc/icon_metadata(file) + var/static/list/icon_metadata_cache = list() + if(istype(file, /datum/universal_icon)) + var/datum/universal_icon/u_icon = file + file = u_icon.icon_file + var/file_string = "[file]" + if(!istext(file) && !(isfile(file) && length(file_string))) + return null + var/list/cached_metadata = icon_metadata_cache[file_string] + if(islist(cached_metadata)) + return cached_metadata + var/list/metadata_result = rustg_dmi_read_metadata(file_string) + if(!islist(metadata_result) || !length(metadata_result)) + CRASH("Error while reading DMI metadata for path '[file_string]': [metadata_result]") + else + icon_metadata_cache[file_string] = metadata_result + return metadata_result + /// Checks whether a given icon state exists in a given icon file. If `file` and `state` both exist, /// this will return `TRUE` - otherwise, it will return `FALSE`. /// /// If you want a stack trace to be output when the given state/file doesn't exist, use /// `/proc/icon_exists_or_scream()`. /proc/icon_exists(file, state) - var/static/list/icon_states_cache = list() if(isnull(file) || isnull(state)) return FALSE //This is common enough that it shouldn't panic, imo. - if(isnull(icon_states_cache[file])) - icon_states_cache[file] = list() - var/file_string = "[file]" - if(isfile(file) && length(file_string)) // ensure that it's actually a file, and not a runtime icon - for(var/istate in json_decode(rustg_dmi_icon_states(file_string))) - icon_states_cache[file][istate] = TRUE - else // Otherwise, we have to use the slower BYOND proc - for(var/istate in icon_states(file)) - icon_states_cache[file][istate] = TRUE + if(isnull(GLOB.icon_states_cache_lookup[file])) + compile_icon_states_cache(file) + return !isnull(GLOB.icon_states_cache_lookup[file][state]) - return !isnull(icon_states_cache[file][state]) +/// Cached, rustg-based alternative to icon_states() +/proc/icon_states_fast(file) + if(isnull(file)) + return null + if(isnull(GLOB.icon_states_cache[file])) + compile_icon_states_cache(file) + return GLOB.icon_states_cache[file] + +/proc/compile_icon_states_cache(file) + GLOB.icon_states_cache[file] = list() + GLOB.icon_states_cache_lookup[file] = list() + // Try to use rustg first + var/list/metadata = icon_metadata(file) + if(islist(metadata) && islist(metadata["states"])) + for(var/list/state_data as anything in metadata["states"]) + GLOB.icon_states_cache[file] += state_data["name"] + GLOB.icon_states_cache_lookup[file][state_data["name"]] = TRUE + else // Otherwise, we have to use the slower BYOND proc + for(var/istate in icon_states(file)) + GLOB.icon_states_cache[file] += istate + GLOB.icon_states_cache_lookup[file][istate] = TRUE /// Functions the same as `/proc/icon_exists()`, but with the addition of a stack trace if the /// specified file or state doesn't exist. @@ -1238,6 +1271,31 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects) /// Returns a list containing the width and height of an icon file /proc/get_icon_dimensions(icon_path) + if(istype(icon_path, /datum/universal_icon)) + var/datum/universal_icon/u_icon = icon_path + icon_path = u_icon.icon_file + // Icons can be a real file(), a rsc backed file(), a dynamic rsc (dyn.rsc) reference (known as a cache reference in byond docs), or an /icon which is pointing to one of those. + // Runtime generated dynamic icons are an unbounded concept cache identity wise, the same icon can exist millions of ways and holding them in a list as a key can lead to unbounded memory usage if called often by consumers. + // Check distinctly that this is something that has this unspecified concept, and thus that we should not cache. + if (!istext(icon_path) && (!isfile(icon_path) || !length("[icon_path]"))) + var/icon/my_icon = icon(icon_path) + return list("width" = my_icon.Width(), "height" = my_icon.Height()) + if (isnull(GLOB.icon_dimensions[icon_path])) + // Used cached icon metadata + var/list/metadata = icon_metadata(icon_path) + var/list/result = null + if(islist(metadata) && isnum(metadata["width"]) && isnum(metadata["height"])) + result = list("width" = metadata["width"], "height" = metadata["height"]) + // Otherwise, we have to use the slower BYOND proc + else + var/icon/my_icon = icon(icon_path) + result = list("width" = my_icon.Width(), "height" = my_icon.Height()) + GLOB.icon_dimensions[icon_path] = result + + return GLOB.icon_dimensions[icon_path] + +/// Returns a list containing the width and height of an icon file, without using rustg for pure function calls +/proc/get_icon_dimensions_pure(icon_path) // Icons can be a real file(), a rsc backed file(), a dynamic rsc (dyn.rsc) reference (known as a cache reference in byond docs), or an /icon which is pointing to one of those. // Runtime generated dynamic icons are an unbounded concept cache identity wise, the same icon can exist millions of ways and holding them in a list as a key can lead to unbounded memory usage if called often by consumers. // Check distinctly that this is something that has this unspecified concept, and thus that we should not cache. diff --git a/code/__HELPERS/turfs.dm b/code/__HELPERS/turfs.dm index 1aa7fcd65d0..902bfbf585e 100644 --- a/code/__HELPERS/turfs.dm +++ b/code/__HELPERS/turfs.dm @@ -236,7 +236,7 @@ Turf and target are separate in case you want to teleport some distance from a t var/pixel_y_offset = checked_atom.pixel_y + checked_atom.pixel_z + atom_matrix.get_y_shift() //Irregular objects - var/list/icon_dimensions = get_icon_dimensions(checked_atom.icon) + var/list/icon_dimensions = get_icon_dimensions_pure(checked_atom.icon) var/checked_atom_icon_height = icon_dimensions["height"] var/checked_atom_icon_width = icon_dimensions["width"] if(checked_atom_icon_height != ICON_SIZE_Y || checked_atom_icon_width != ICON_SIZE_X) diff --git a/code/_globalvars/lists/icons.dm b/code/_globalvars/lists/icons.dm index ff60e6bc8d9..4d0b6279945 100644 --- a/code/_globalvars/lists/icons.dm +++ b/code/_globalvars/lists/icons.dm @@ -1,2 +1,6 @@ /// Cache of the width and height of icon files, to avoid repeating the same expensive operation GLOBAL_LIST_EMPTY(icon_dimensions) +/// Cache of the states of icon files +GLOBAL_LIST_EMPTY(icon_states_cache) +/// Cache of the states of icon files, stored associatively with TRUE for lookup +GLOBAL_LIST_EMPTY(icon_states_cache_lookup) diff --git a/code/controllers/subsystem/processing/greyscale.dm b/code/controllers/subsystem/processing/greyscale.dm index 4b77aa3e7b4..f88b960e869 100644 --- a/code/controllers/subsystem/processing/greyscale.dm +++ b/code/controllers/subsystem/processing/greyscale.dm @@ -1,7 +1,6 @@ /// Disable to use builtin DM-based generation. /// IconForge is 250x times faster but requires storing the icons in tmp/ and may result in higher asset transport. /// Note that the builtin GAGS editor still uses the 'legacy' generation to allow for debugging. -/// IconForge also does not support the color matrix layer type or the 'or' blend_mode, however both are currently unused. #define USE_RUSTG_ICONFORGE_GAGS PROCESSING_SUBSYSTEM_DEF(greyscale) diff --git a/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm b/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm index 9256231c0e8..a5a636327f5 100644 --- a/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm +++ b/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm @@ -189,10 +189,10 @@ 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) + job_id = rustg_iconforge_generate_async("data/spritesheets/", name, entries_json, do_cache, FALSE, TRUE) UNTIL((data_out = rustg_iconforge_check(job_id)) != RUSTG_JOB_NO_RESULTS_YET) else - data_out = rustg_iconforge_generate("data/spritesheets/", name, entries_json, do_cache) + data_out = rustg_iconforge_generate("data/spritesheets/", name, entries_json, do_cache, FALSE, TRUE) if (data_out == RUSTG_JOB_ERROR) CRASH("Spritesheet [name] JOB PANIC") else if(!findtext(data_out, "{", 1, 2)) diff --git a/code/modules/asset_cache/spritesheet/batched/universal_icon.dm b/code/modules/asset_cache/spritesheet/batched/universal_icon.dm index 3f06001900b..0ecdd1ea349 100644 --- a/code/modules/asset_cache/spritesheet/batched/universal_icon.dm +++ b/code/modules/asset_cache/spritesheet/batched/universal_icon.dm @@ -10,7 +10,7 @@ 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) +/datum/universal_icon/New(icon/icon_file, icon_state="", dir=null, frame=null, 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" || !length("[icon_file]")) @@ -44,10 +44,10 @@ transform.blend_color(color, blend_mode) return src -/datum/universal_icon/proc/blend_icon(datum/universal_icon/icon_object, blend_mode) +/datum/universal_icon/proc/blend_icon(datum/universal_icon/icon_object, blend_mode, x=1, y=1) if(!transform) transform = new - transform.blend_icon(icon_object, blend_mode) + transform.blend_icon(icon_object, blend_mode, x, y) return src /datum/universal_icon/proc/scale(width, height) @@ -62,14 +62,116 @@ transform.crop(x1, y1, x2, y2) return src -/// Internally performs a crop. -/datum/universal_icon/proc/shift(dir, amount, icon_width, icon_height) +/datum/universal_icon/proc/flip(dir) if(!transform) transform = new - var/list/offsets = dir2offset(dir) - var/shift_x = -offsets[1] * amount - var/shift_y = -offsets[2] * amount - transform.crop(1 + shift_x, 1 + shift_y, icon_width + shift_x, icon_height + shift_y) + transform.flip(dir) + return src + +/datum/universal_icon/proc/rotate(angle) + if(!transform) + transform = new + transform.rotate(angle) + return src + +/datum/universal_icon/proc/shift(dir, offset, wrap=0) + if(!transform) + transform = new + transform.shift(dir, offset, wrap) + return src + +/datum/universal_icon/proc/swap_color(src_color, dst_color) + if(!transform) + transform = new + transform.swap_color(src_color, dst_color) + return src + +/datum/universal_icon/proc/draw_box(color, x1, y1, x2=x1, y2=y1) + if(!transform) + transform = new + transform.draw_box(color, x1, y1, x2, y2) + return src + +/datum/universal_icon/proc/map_colors_inferred(list/color_args) + var/num_args = length(color_args) + if(num_args <= 20 || num_args >= 16) + src.map_colors_rgba(arglist(color_args)) + else if(num_args <= 12 || num_args >= 9) + src.map_colors_rgb(arglist(color_args)) + else if(num_args == 5) + src.map_colors_rgba_hex(arglist(color_args)) + else if(num_args == 4) + // is there alpha in the hex? + if(length(color_args[3]) == 7 || length(color_args[3]) == 4) + src.map_colors_rgb_hex(arglist(color_args)) + else + src.map_colors_rgba_hex(arglist(color_args)) + else if(num_args == 3) + src.map_colors_rgb_hex(arglist(color_args)) + +/datum/universal_icon/proc/map_colors_rgba(rr, rg, rb, ra, gr, gg, gb, ga, br, bg, bb, ba, ar, ag, ab, aa, r0=0, g0=0, b0=0, a0=0) + if(!transform) + transform = new + transform.map_colors(rr, rg, rb, ra, gr, gg, gb, ga, br, bg, bb, ba, ar, ag, ab, aa, r0, g0, b0, a0) + return src + +/datum/universal_icon/proc/map_colors_rgb(rr, rg, rb, gr, gg, gb, br, bg, bb, r0=0, g0=0, b0=0) + if(!transform) + transform = new + transform.map_colors(rr, rg, rb, 0, gr, gg, gb, 0, br, bg, bb, 0, 0, 0, 0, 1, r0, g0, b0, 0) + return src + +/datum/universal_icon/proc/map_colors_rgb_hex(r_rgb, g_rgb, b_rgb, rgb0=rgb(0,0,0)) + if(!transform) + transform = new + var/rr = hex2num(copytext(r_rgb, 2, 4)) / 255 + var/rg = hex2num(copytext(r_rgb, 4, 6)) / 255 + var/rb = hex2num(copytext(r_rgb, 6, 8)) / 255 + + var/gr = hex2num(copytext(g_rgb, 2, 4)) / 255 + var/gg = hex2num(copytext(g_rgb, 4, 6)) / 255 + var/gb = hex2num(copytext(g_rgb, 6, 8)) / 255 + + var/br = hex2num(copytext(b_rgb, 2, 4)) / 255 + var/bg = hex2num(copytext(b_rgb, 4, 6)) / 255 + var/bb = hex2num(copytext(b_rgb, 6, 8)) / 255 + + var/r0 = hex2num(copytext(rgb0, 2, 4)) / 255 + var/b0 = hex2num(copytext(rgb0, 4, 6)) / 255 + var/g0 = hex2num(copytext(rgb0, 6, 8)) / 255 + + transform.map_colors(rr, rg, rb, 0, gr, gg, gb, 0, br, bg, bb, 0, 0, 0, 0, 1, r0, b0, g0, 0) + return src + +/datum/universal_icon/proc/map_colors_rgba_hex(r_rgba, g_rgba, b_rgba, a_rgba, rgba0="#00000000") + if(!transform) + transform = new + var/rr = hex2num(copytext(r_rgba, 2, 4)) / 255 + var/rg = hex2num(copytext(r_rgba, 4, 6)) / 255 + var/rb = hex2num(copytext(r_rgba, 6, 8)) / 255 + var/ra = hex2num(copytext(r_rgba, 8, 10)) / 255 + + var/gr = hex2num(copytext(g_rgba, 2, 4)) / 255 + var/gg = hex2num(copytext(g_rgba, 4, 6)) / 255 + var/gb = hex2num(copytext(g_rgba, 6, 8)) / 255 + var/ga = hex2num(copytext(g_rgba, 8, 10)) / 255 + + var/br = hex2num(copytext(b_rgba, 2, 4)) / 255 + var/bg = hex2num(copytext(b_rgba, 4, 6)) / 255 + var/bb = hex2num(copytext(b_rgba, 6, 8)) / 255 + var/ba = hex2num(copytext(b_rgba, 8, 10)) / 255 + + var/ar = hex2num(copytext(a_rgba, 2, 4)) / 255 + var/ag = hex2num(copytext(a_rgba, 4, 6)) / 255 + var/ab = hex2num(copytext(a_rgba, 6, 8)) / 255 + var/aa = hex2num(copytext(a_rgba, 8, 10)) / 255 + + var/r0 = hex2num(copytext(rgba0, 2, 4)) / 255 + var/b0 = hex2num(copytext(rgba0, 4, 6)) / 255 + var/g0 = hex2num(copytext(rgba0, 6, 8)) / 255 + var/a0 = hex2num(copytext(rgba0, 8, 10)) / 255 + + transform.map_colors(rr, rg, rb, ra, gr, gg, gb, ga, br, bg, bb, ba, ar, ag, ab, aa, r0, b0, g0, a0) return src /// Internally performs a color blend. @@ -118,11 +220,29 @@ if(!istype(icon_object)) stack_trace("Invalid icon found in icon transformer during apply()! [icon_object]") continue - target.Blend(icon_object.to_icon(), transform["blend_mode"]) + target.Blend(icon_object.to_icon(), transform["blend_mode"], transform["x"], transform["y"]) if(RUSTG_ICONFORGE_SCALE) target.Scale(transform["width"], transform["height"]) if(RUSTG_ICONFORGE_CROP) target.Crop(transform["x1"], transform["y1"], transform["x2"], transform["y2"]) + if(RUSTG_ICONFORGE_MAP_COLORS) + target.MapColors( + transform["rr"], transform["rg"], transform["rb"], transform["ra"], + transform["gr"], transform["gg"], transform["gb"], transform["ga"], + transform["br"], transform["bg"], transform["bb"], transform["ba"], + transform["ar"], transform["ag"], transform["ab"], transform["aa"], + transform["r0"], transform["g0"], transform["b0"], transform["a0"], + ) + if(RUSTG_ICONFORGE_FLIP) + target.Flip(transform["dir"]) + if(RUSTG_ICONFORGE_TURN) + target.Turn(transform["angle"]) + if(RUSTG_ICONFORGE_SHIFT) + target.Shift(transform["dir"], transform["offset"], transform["wrap"]) + if(RUSTG_ICONFORGE_SWAP_COLOR) + target.SwapColor(transform["src_color"], transform["dst_color"]) + if(RUSTG_ICONFORGE_DRAW_BOX) + target.DrawBox(transform["color"], transform["x1"], transform["y1"], transform["x2"], transform["y2"]) return target /datum/icon_transformer/proc/copy() @@ -142,13 +262,17 @@ #endif 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) +/datum/icon_transformer/proc/blend_icon(datum/universal_icon/icon_object, blend_mode, x=1, y=1) #ifdef UNIT_TESTS // icon_object's type is checked later in to_list if(!isnum(blend_mode)) CRASH("Invalid blend_mode provided to blend_icon: [blend_mode]") + if(!isnum(x)) + CRASH("Invalid x offset provided to blend_icon: [x]") + if(!isnum(y)) + CRASH("Invalid y offset provided to blend_icon: [y]") #endif - transforms += list(list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = icon_object, "blend_mode" = blend_mode)) + transforms += list(list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = icon_object, "blend_mode" = blend_mode, "x" = x, "y" = y)) /datum/icon_transformer/proc/scale(width, height) #ifdef UNIT_TESTS @@ -164,6 +288,51 @@ #endif transforms += list(list("type" = RUSTG_ICONFORGE_CROP, "x1" = x1, "y1" = y1, "x2" = x2, "y2" = y2)) +/datum/icon_transformer/proc/flip(dir) + #ifdef UNIT_TESTS + if(!isnum(dir)) + CRASH("Invalid arguments provided to flip: [dir]") + #endif + transforms += list(list("type" = RUSTG_ICONFORGE_FLIP, "dir" = dir)) + +/datum/icon_transformer/proc/rotate(angle) + #ifdef UNIT_TESTS + if(!isnum(angle)) + CRASH("Invalid arguments provided to rotate: [angle]") + #endif + transforms += list(list("type" = RUSTG_ICONFORGE_TURN, "angle" = angle)) + +/datum/icon_transformer/proc/shift(dir, offset, wrap=FALSE) + #ifdef UNIT_TESTS + if(!isnum(dir) || !isnum(offset) || (wrap != FALSE && wrap != TRUE)) + CRASH("Invalid arguments provided to shift: [dir],[offset],[wrap]") + #endif + transforms += list(list("type" = RUSTG_ICONFORGE_SHIFT, "dir" = dir, "offset" = offset, "wrap" = wrap)) + +/datum/icon_transformer/proc/swap_color(src_color, dst_color) + #ifdef UNIT_TESTS + if(!istext(src_color) || !istext(dst_color)) + CRASH("Invalid arguments provided to swap_color: [src_color],[dst_color]") + #endif + transforms += list(list("type" = RUSTG_ICONFORGE_SWAP_COLOR, "src_color" = src_color, "dst_color" = dst_color)) + +/datum/icon_transformer/proc/draw_box(color, x1, y1, x2=x1, y2=y1) + #ifdef UNIT_TESTS + if(!istext(color) || !isnum(x1) || !isnum(y1) || !isnum(x2) || !isnum(y2)) + CRASH("Invalid arguments provided to draw_box: [color],[x1],[y1],[x2],[y2]") + #endif + transforms += list(list("type" = RUSTG_ICONFORGE_DRAW_BOX, "color" = color, "x1" = x1, "y1" = y1, "x2" = x2, "y2" = y2)) + +/datum/icon_transformer/proc/map_colors(rr, rg, rb, ra, gr, gg, gb, ga, br, bg, bb, ba, ar, ag, ab, aa, r0=0, g0=0, b0=0, a0=0) + transforms += list(list( + "type" = RUSTG_ICONFORGE_MAP_COLORS, + "rr" = rr, "rg" = rg, "rb" = rb, "ra" = ra, + "gr" = gr, "gg" = gg, "gb" = gb, "ga" = ga, + "br" = br, "bg" = bg, "bb" = bb, "ba" = ba, + "ar" = ar, "ag" = ag, "ab" = ab, "aa" = aa, + "r0" = r0, "g0" = g0, "b0" = b0, "a0" = a0, + )) + /// Recursively converts all contained [/datum/universal_icon]s and their associated [/datum/icon_transformer]s into list form so the transforms can be JSON encoded. /datum/icon_transformer/proc/to_list() RETURN_TYPE(/list) @@ -218,21 +387,19 @@ /proc/get_display_icon_for(atom/atom_path) if (!ispath(atom_path, /atom)) return FALSE - var/icon_file = initial(atom_path.icon) - var/icon_state = initial(atom_path.icon_state) - if(initial(atom_path.greyscale_config) && initial(atom_path.greyscale_colors)) + var/icon_file = atom_path::icon + var/icon_state = atom_path::icon_state + if(atom_path::greyscale_config && atom_path::greyscale_colors) return gags_to_universal_icon(atom_path) if(ispath(atom_path, /obj)) var/obj/obj_path = atom_path - if(initial(obj_path.icon_state_preview)) - icon_state = initial(obj_path.icon_state_preview) - return uni_icon(icon_file, icon_state, color=initial(atom_path.color)) + if(obj_path::icon_state_preview) + icon_state = obj_path::icon_state_preview + return uni_icon(icon_file, icon_state, color=atom_path::color) /// getFlatIcon for [/datum/universal_icon]s -/// Only supports 32x32 icons facing south -/// Tough luck if you want anything else /// Still fairly slow for complex appearances due to filesystem operations. Try to avoid using it -/proc/get_flat_uni_icon(image/appearance, deficon, defstate, defblend, start = TRUE, parentcolor) +/proc/get_flat_uni_icon(image/appearance, defdir, deficon, defstate, defblend, start = TRUE, parentcolor) // Loop through the underlays, then overlays, sorting them into the layers list #define PROCESS_OVERLAYS_OR_UNDERLAYS(flat, process, base_layer) \ for (var/i in 1 to process.len) { \ @@ -283,7 +450,7 @@ var/curstate = appearance.icon_state || defstate // Filter out 'runtime' icons (server-generated RSC cache icons) // Write the icon to the filesystem so it can be used by iconforge - if(!isfile(curicon) || string_curicon == "/icon" || string_curicon == "/image" || !length(string_curicon)) + if(!isfile(curicon) || !length(string_curicon)) var/file_path_tmp = "tmp/uni_icon-tmp-[rand(1, 999)].dmi" // this filename is temporary. fcopy(curicon, file_path_tmp) var/file_hash = rustg_hash_file(RUSTG_HASH_MD5, file_path_tmp) @@ -293,21 +460,51 @@ fdel(file_path_tmp) // delete the old one curicon = file(file_path) - var/curblend = appearance.blend_mode || defblend - var/list/curstates = icon_states(curicon) - if(!(curstate in curstates)) - if("" in curstates) // BYOND defaulting functionality + if(!icon_exists(curicon, curstate)) + if("" in icon_states_fast(curicon)) // BYOND defaulting functionality curstate = "" else should_display = FALSE + var/curdir = (!appearance.dir || appearance.dir == SOUTH) ? defdir : appearance.dir + var/base_icon_dir //We'll use this to get the icon state to display if not null BUT NOT pass it to overlays as the dir we have + + if(should_display) + //Determines if there're directionals. + if (curdir != SOUTH) + // icon states either have 1, 4 or 8 dirs. We only have to check + // one of NORTH, EAST or WEST to know that this isn't a 1-dir icon_state since they just have SOUTH. + var/list/metadata = icon_metadata(curicon) + if(islist(metadata)) + for(var/list/state_data as anything in metadata["states"]) + var/name = state_data["name"] + if(name != curstate) + continue + var/dir_count = state_data["dirs"] + if(dir_count == 1) + base_icon_dir = SOUTH + else if(!length(icon_states(icon(curicon, curstate, NORTH)))) + base_icon_dir = SOUTH + + var/list/icon_dimensions = get_icon_dimensions(curicon) + var/icon_width = icon_dimensions["width"] + var/icon_height = icon_dimensions["height"] + if(icon_width != 32 || icon_height != 32) + flat.scale(icon_width, icon_height) + + if(!base_icon_dir) + base_icon_dir = curdir + + var/curblend = appearance.blend_mode || defblend + + if(appearance.overlays.len || appearance.underlays.len) // Layers will be a sorted list of icons/overlays, based on the order in which they are displayed var/list/layers = list() var/image/copy if(should_display) // Add the atom's icon itself, without pixel_x/y offsets. - copy = image(icon=curicon, icon_state=curstate, layer=appearance.layer, dir=SOUTH) + copy = image(icon=curicon, icon_state=curstate, layer=appearance.layer, dir=base_icon_dir) copy.color = appearance.color copy.alpha = appearance.alpha copy.blend_mode = curblend @@ -318,15 +515,26 @@ var/datum/universal_icon/add // Icon of overlay being added + var/list/flat_dimensions = get_icon_dimensions(flat) + var/flatX1 = 1 + var/flatX2 = flat_dimensions["width"] + var/flatY1 = 1 + var/flatY2 = flat_dimensions["height"] + + var/addX1 = 0 + var/addX2 = 0 + var/addY1 = 0 + var/addY2 = 0 + if(appearance.color) if(islist(appearance.color)) - stack_trace("Unsupported color map appearance provided to get_flat_uni_icon, ignoring it.") + flat.map_colors_inferred(appearance.color) else flat.blend_color(appearance.color, ICON_MULTIPLY) if(parentcolor && !(appearance.appearance_flags & RESET_COLOR)) if(islist(parentcolor)) - stack_trace("Unsupported color map appearance provided to get_flat_uni_icon, ignoring it.") + flat.map_colors_inferred(parentcolor) else flat.blend_color(parentcolor, ICON_MULTIPLY) @@ -338,19 +546,45 @@ if(layer_image == copy && length("[layer_image.icon]")) // 'layer_image' is an /image based on the object being flattened, and isn't a 'runtime' icon. curblend = BLEND_OVERLAY - add = uni_icon(layer_image.icon, layer_image.icon_state, SOUTH) + add = uni_icon(layer_image.icon, layer_image.icon_state, base_icon_dir) if(appearance.color) if(islist(appearance.color)) - stack_trace("Unsupported color map appearance provided to get_flat_uni_icon, ignoring it.") + add.map_colors_inferred(appearance.color) else add.blend_color(appearance.color, ICON_MULTIPLY) else // 'layer_image' is an appearance object. - add = get_flat_uni_icon(layer_image, curicon, curstate, curblend, FALSE, next_parentcolor) + add = get_flat_uni_icon(layer_image, curdir, curicon, curstate, curblend, FALSE, next_parentcolor) if(!add || !length(add.icon_file)) continue + // Find the new dimensions of the flat icon to fit the added overlay + var/list/add_dimensions = get_icon_dimensions(add) + addX1 = min(flatX1, layer_image.pixel_x + layer_image.pixel_w + 1) + addX2 = max(flatX2, layer_image.pixel_x + layer_image.pixel_w + add_dimensions["width"]) // assuming 32x32 + addY1 = min(flatY1, layer_image.pixel_y + layer_image.pixel_z + 1) + addY2 = max(flatY2, layer_image.pixel_y + layer_image.pixel_z + add_dimensions["height"]) + + if ( + addX1 != flatX1 \ + && addX2 != flatX2 \ + && addY1 != flatY1 \ + && addY2 != flatY2 \ + ) + // Resize the flattened icon so the new icon fits + flat.crop( + addX1 - flatX1 + 1, + addY1 - flatY1 + 1, + addX2 - flatX1 + 1, + addY2 - flatY1 + 1 + ) + + flatX1 = addX1 + flatX2 = addY1 + flatY1 = addX2 + flatY2 = addY2 + // Blend the overlay into the flattened icon - flat.blend_icon(add, blendMode2iconMode(curblend)) + flat.blend_icon(add, blendMode2iconMode(curblend), layer_image.pixel_x + layer_image.pixel_w + 2 - flatX1, layer_image.pixel_y + layer_image.pixel_z + 2 - flatY1) if(appearance.alpha < 255) flat.blend_color(rgb(255, 255, 255, appearance.alpha), ICON_MULTIPLY) @@ -358,14 +592,14 @@ return flat else if(should_display) // There's no overlays. - var/datum/universal_icon/final_icon = uni_icon(curicon, curstate, SOUTH) + var/datum/universal_icon/final_icon = uni_icon(curicon, curstate, base_icon_dir) if (appearance.alpha < 255) final_icon.blend_color(rgb(255,255,255, appearance.alpha), ICON_MULTIPLY) if (appearance.color) if (islist(appearance.color)) - stack_trace("Unsupported color map appearance provided to get_flat_uni_icon, ignoring it.") + final_icon.map_colors_inferred(appearance.color) else final_icon.blend_color(appearance.color, ICON_MULTIPLY) diff --git a/code/modules/client/preferences/species_features/basic.dm b/code/modules/client/preferences/species_features/basic.dm index 876d3a3c45d..b33f2818bda 100644 --- a/code/modules/client/preferences/species_features/basic.dm +++ b/code/modules/client/preferences/species_features/basic.dm @@ -10,7 +10,7 @@ var/datum/universal_icon/head_accessory_icon = uni_icon(sprite_accessory.icon, sprite_accessory.icon_state) if(y_offset) - head_accessory_icon.shift(NORTH, y_offset, ICON_SIZE_X, ICON_SIZE_Y) + head_accessory_icon.shift(NORTH, y_offset) head_accessory_icon.blend_color(COLOR_DARK_BROWN, ICON_MULTIPLY) final_icon.blend_icon(head_accessory_icon, ICON_OVERLAY) diff --git a/dependencies.sh b/dependencies.sh index c89c1d8597a..e685a8aed3b 100644 --- a/dependencies.sh +++ b/dependencies.sh @@ -8,7 +8,7 @@ export BYOND_MAJOR=516 export BYOND_MINOR=1659 #rust_g git tag -export RUST_G_VERSION=3.9.0 +export RUST_G_VERSION=4.0.0 # node version export NODE_VERSION_LTS=22.11.0 diff --git a/icons/map_icons/clothing/accessory.dmi b/icons/map_icons/clothing/accessory.dmi index 1f048116d9a..1719b8a73bb 100644 Binary files a/icons/map_icons/clothing/accessory.dmi and b/icons/map_icons/clothing/accessory.dmi differ diff --git a/icons/map_icons/clothing/head/_head.dmi b/icons/map_icons/clothing/head/_head.dmi index 4032ed9f102..b05d7b9b050 100644 Binary files a/icons/map_icons/clothing/head/_head.dmi and b/icons/map_icons/clothing/head/_head.dmi differ diff --git a/icons/map_icons/clothing/head/beret.dmi b/icons/map_icons/clothing/head/beret.dmi index 9fb1cf8fa9e..975bbf80cf2 100644 Binary files a/icons/map_icons/clothing/head/beret.dmi and b/icons/map_icons/clothing/head/beret.dmi differ diff --git a/icons/map_icons/clothing/mask.dmi b/icons/map_icons/clothing/mask.dmi index 2651067be63..e8fa3c8594c 100644 Binary files a/icons/map_icons/clothing/mask.dmi and b/icons/map_icons/clothing/mask.dmi differ diff --git a/icons/map_icons/clothing/neck.dmi b/icons/map_icons/clothing/neck.dmi index 10a1bd2ddfe..a477d15da68 100644 Binary files a/icons/map_icons/clothing/neck.dmi and b/icons/map_icons/clothing/neck.dmi differ diff --git a/icons/map_icons/clothing/shoes.dmi b/icons/map_icons/clothing/shoes.dmi index b73fd466d3f..1381efbf904 100644 Binary files a/icons/map_icons/clothing/shoes.dmi and b/icons/map_icons/clothing/shoes.dmi differ diff --git a/icons/map_icons/clothing/suit/_suit.dmi b/icons/map_icons/clothing/suit/_suit.dmi index 43b0400ab55..484ecf79317 100644 Binary files a/icons/map_icons/clothing/suit/_suit.dmi and b/icons/map_icons/clothing/suit/_suit.dmi differ diff --git a/icons/map_icons/clothing/suit/costume.dmi b/icons/map_icons/clothing/suit/costume.dmi index 7dbba8d3e16..3db98406230 100644 Binary files a/icons/map_icons/clothing/suit/costume.dmi and b/icons/map_icons/clothing/suit/costume.dmi differ diff --git a/icons/map_icons/clothing/under/_under.dmi b/icons/map_icons/clothing/under/_under.dmi index 7549dc9baa5..82a09867c53 100644 Binary files a/icons/map_icons/clothing/under/_under.dmi and b/icons/map_icons/clothing/under/_under.dmi differ diff --git a/icons/map_icons/clothing/under/color.dmi b/icons/map_icons/clothing/under/color.dmi index c091de182d6..aa09a947f9d 100644 Binary files a/icons/map_icons/clothing/under/color.dmi and b/icons/map_icons/clothing/under/color.dmi differ diff --git a/icons/map_icons/clothing/under/costume.dmi b/icons/map_icons/clothing/under/costume.dmi index 4402789a4c5..bd69a61ff4d 100644 Binary files a/icons/map_icons/clothing/under/costume.dmi and b/icons/map_icons/clothing/under/costume.dmi differ diff --git a/icons/map_icons/clothing/under/dress.dmi b/icons/map_icons/clothing/under/dress.dmi index 6e6a9883a12..fde8dfbebaa 100644 Binary files a/icons/map_icons/clothing/under/dress.dmi and b/icons/map_icons/clothing/under/dress.dmi differ diff --git a/icons/map_icons/items/_item.dmi b/icons/map_icons/items/_item.dmi index 9e3ce568af0..c81bfa7d4f4 100644 Binary files a/icons/map_icons/items/_item.dmi and b/icons/map_icons/items/_item.dmi differ diff --git a/icons/map_icons/items/encryptionkey.dmi b/icons/map_icons/items/encryptionkey.dmi index c44fac3a26e..be7514ad789 100644 Binary files a/icons/map_icons/items/encryptionkey.dmi and b/icons/map_icons/items/encryptionkey.dmi differ diff --git a/icons/map_icons/items/pda.dmi b/icons/map_icons/items/pda.dmi index 6ba874d57fb..488e0ea2e7a 100644 Binary files a/icons/map_icons/items/pda.dmi and b/icons/map_icons/items/pda.dmi differ diff --git a/icons/map_icons/objects.dmi b/icons/map_icons/objects.dmi index 8fc20d26983..faefc9cb0d6 100644 Binary files a/icons/map_icons/objects.dmi and b/icons/map_icons/objects.dmi differ diff --git a/rust_g.dll b/rust_g.dll index 4401dd48583..ee66aafe9d3 100644 Binary files a/rust_g.dll and b/rust_g.dll differ