From 3fad0f3e36227f60dea79156161c8f415788ddd8 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 20 Dec 2023 16:24:43 -0500 Subject: [PATCH 01/35] iconforge beta --- Cargo.lock | 85 +++++++++++++++++- Cargo.toml | 15 ++++ src/error.rs | 3 + src/iconforge.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 +- 5 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 src/iconforge.rs diff --git a/Cargo.lock b/Cargo.lock index 98170f30..f43f7b72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,8 +686,13 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "dunce" version = "1.0.4" +======= +name = "dtoa" +version = "0.4.8" +>>>>>>> 47bfb12 (iconforge beta) source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" @@ -758,6 +763,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fdeflate" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -772,7 +786,7 @@ checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.5.3", ] [[package]] @@ -1696,8 +1710,22 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "indexmap" version = "2.1.0" +======= +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + +[[package]] +name = "instant" +version = "0.1.12" +>>>>>>> 47bfb12 (iconforge beta) source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ @@ -1990,6 +2018,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.8" @@ -2374,7 +2412,11 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", +<<<<<<< HEAD "miniz_oxide", +======= + "miniz_oxide 0.7.1", +>>>>>>> 47bfb12 (iconforge beta) ] [[package]] @@ -2767,7 +2809,6 @@ dependencies = [ "gix", "hex", "image", - "lazy_static", "md-5", "mysql", "noise", @@ -2786,7 +2827,12 @@ dependencies = [ "sha-1", "sha2", "thiserror", +<<<<<<< HEAD "toml 0.8.8", +======= + "toml", + "tracy_full", +>>>>>>> 47bfb12 (iconforge beta) "twox-hash", "url", "zip", @@ -2821,6 +2867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] +<<<<<<< HEAD name = "rustix" version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2831,6 +2878,14 @@ dependencies = [ "libc", "linux-raw-sys", "windows-sys 0.52.0", +======= +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +>>>>>>> 47bfb12 (iconforge beta) ] [[package]] @@ -3064,6 +3119,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -3408,6 +3469,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tracy-client-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db0b1cc1bb12a70457300d9affc07acb587390d971a796dac2f4d9bca8df776" +dependencies = [ + "cc", +] + +[[package]] +name = "tracy_full" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01aaff24a62ad715d80adcf28e47c228dbed3d6285fb85b55bfd9eb47fda4df" +dependencies = [ + "once_cell", + "rustc_version", + "tracy-client-sys", +] + [[package]] name = "try-lock" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 482865b1..bcde95c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } dmi = { version = "0.3.1", optional = true } +tracy_full = { version = "1.6.1", optional = true} [features] default = [ @@ -134,6 +135,7 @@ hash = [ "serde_json", ] pathfinder = ["num-integer", "pathfinding", "serde", "serde_json"] +iconforge = ["serde", "serde_json", "png", "image", "dep:dmi", "rayon", "tracy_full", "jobs", "once_cell"] redis_pubsub = ["flume", "redis", "serde", "serde_json"] redis_reliablequeue = ["flume", "redis", "serde", "serde_json"] unzip = ["zip", "jobs"] @@ -148,3 +150,16 @@ jobs = ["flume"] [dev-dependencies] regex = "1" + +# DEV ONLY. REMOVE THIS LATER +[profile.dev.package.dmi] +opt-level = 3 + +[profile.dev.package.image] +opt-level = 3 + +[profile.dev.package.serde] +opt-level = 3 + +[profile.dev.package.serde_json] +opt-level = 3 diff --git a/src/error.rs b/src/error.rs index d56cb5c3..90cf4b5a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,6 +61,9 @@ pub enum Error { #[cfg(feature = "hash")] #[error("Unable to decode hex value.")] HexDecode, + #[cfg(feature = "iconforge")] + #[error("IconState error: {0}")] + IconState(String), } impl From for Error { diff --git a/src/iconforge.rs b/src/iconforge.rs new file mode 100644 index 00000000..d2f25e79 --- /dev/null +++ b/src/iconforge.rs @@ -0,0 +1,223 @@ +// DMI spritesheet generator +// Developed by itsmeow +use crate::jobs; +use crate::error::Error; +use std::{ + fs::File, + io::BufReader, +}; +use dmi::icon::{Icon, IconState}; +use image::{DynamicImage, GenericImage, GenericImageView}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +//use raster::Image; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tracy_full::{zone, frame}; +use once_cell::sync::OnceCell; + +fn icon_file_to_icon() -> &'static Mutex> { + static INSTANCE: OnceCell>> = OnceCell::new(); + INSTANCE.get_or_init(|| Mutex::new(HashMap::new())) +} + +byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { + catch_panic(file_path, spritesheet_name, sprites).err() +}); + + +byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites) { + let file_path = file_path.to_owned(); + let spritesheet_name = spritesheet_name.to_owned(); + let sprites = sprites.to_owned(); + Some(jobs::start(move || { + match catch_panic(&file_path, &spritesheet_name, &sprites) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + } + })) +}); + +byond_fn!(fn iconforge_check(id) { + Some(jobs::check(id)) +}); + +#[derive(Serialize)] +struct Returned { + sizes: Vec, + sprites: HashMap, + error: String, +} + +#[derive(Serialize, Clone)] +struct SpritesheetEntry { + size_id: String, + position: u32, +} + +#[derive(Serialize, Deserialize, Clone)] +struct IconObject { + icon_file: String, + icon_state: String, + dir: u8, + frame: u32, + moving: u8, + transform: Vec +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type")] +enum Transform { + BlendColorTransform { + color: String, + blend_mode: u8, + }, + BlendIconTransform { + icon: IconObject, + blend_mode: u8, + }, + ScaleTransform { + width: u32, + height: u32, + }, + CropTransform { + x1: u32, + y1: u32, + x2: u32, + y2: u32, + } +} + +fn catch_panic(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { + let x = std::panic::catch_unwind(|| { + let result = generate_spritesheet(file_path, spritesheet_name, sprites); + frame!(); + return result; + }); + if x.is_err() { + match x.unwrap_err().downcast_ref::() { + Some(as_string) => { + return Err(Error::IconState(as_string.to_owned())) + } + None => { + return Err(Error::IconState("Failed to stringify panic".to_string())) + } + } + } + return x.ok().unwrap() +} + +fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { + zone!("generate_spritesheet"); + + let error: Arc>> = Arc::new(Mutex::new(Vec::new())); + + let size_to_images: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + let sprites_map: HashMap = serde_json::from_str::>(sprites)?; + let sprites_objects: Arc>> = Arc::new(Mutex::new(HashMap::new())); + + sprites_map.par_iter().for_each(|sprite_entry| { + zone!("sprite_to_icon"); + let (_, icon) = sprite_entry; + let icon_path = icon.icon_file.to_owned(); + if icon_file_to_icon().lock().unwrap().contains_key(&icon_path) { + return; + } + let reader = BufReader::new(File::open(&icon_path).unwrap()); + let icon: Option; + { + zone!("load_icon"); + icon = Icon::load(reader).ok(); + } + if icon.is_none() { + error.lock().unwrap().push(format!("Invalid DMI: {}", icon_path)); + return; + } + icon_file_to_icon().lock().unwrap().insert(icon_path, icon.unwrap().to_owned()); + }); + + sprites_map.par_iter().for_each(|sprite_entry| { + zone!("map_sprite"); + let (sprite_name, icon) = sprite_entry; + let parsed_icon = icon_file_to_icon().lock().unwrap().get(&icon.icon_file).unwrap().to_owned(); + let mut matched_state: Option = Option::None; + for icon_state in parsed_icon.states { + if icon_state.name == icon.icon_state { + matched_state = Option::Some(icon_state.clone()); + break; + } + } + if matched_state.is_none() { + error.lock().unwrap().push(format!("Could not find associated icon state {} for {}", icon.icon_state, sprite_name)); + return; + } + let state = matched_state.unwrap(); + if !( if icon.dir == 2 { state.dirs >= 1 } else { state.dirs >= 4 } && state.frames >= icon.frame ) { + error.lock().unwrap().push(format!("Could not find associated dir or frame dir: {} frame: {} in {} icon_state - dirs: {} frames: {}", icon.dir, icon.frame, sprite_name, state.dirs, state.frames)); + return; + } + let mut icon_idx: u32 = 0; + if state.dirs == 4 { + icon_idx = match icon.dir { + 2 => 0, // South + 1 => 1, // North + 4 => 2, // East + 8 => 3, // West + _ => 0, + } + } else if state.dirs != 1 { + error.lock().unwrap().push(format!("Unsupported dirs size of {} in {} state: {} for sprite {}", state.dirs, icon.icon_file, icon.icon_state, sprite_name)); + return; + } + if state.frames > 1 { + // Add one so zero scales properly + icon_idx = (icon_idx + 1) * icon.frame - 1 + } + let image: &DynamicImage = state.images.get(usize::try_from(icon_idx).unwrap()).unwrap(); + let cloned_image: DynamicImage = image.clone(); + // apply transforms here + let size_id = format!("{}x{}", cloned_image.width(), cloned_image.height()); + let mut size_map = size_to_images.lock().unwrap(); + let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); + vec.push(cloned_image); + sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { + size_id: size_id.to_owned(), + position: u32::try_from(vec.len()).unwrap() - 1 + }); + }); + + size_to_images.lock().unwrap().par_iter().for_each(|(size_id, images_list)| { + zone!("join_sprites"); + let file_path = format!("{}{}_{}.png", file_path, spritesheet_name, size_id); + let size_data: Vec<&str> = size_id.split("x").collect(); + let base_width = size_data.first().unwrap().to_string().parse::().unwrap(); + let base_height = size_data.last().unwrap().to_string().parse::().unwrap(); + + let image_count: u32 = u32::try_from(images_list.len()).unwrap(); + let mut final_image = DynamicImage::new_rgba8(base_width * image_count, base_height); + + for idx in 0..image_count { + zone!("join_sprite"); + let image: &DynamicImage = images_list.get::(usize::try_from(idx).unwrap()).unwrap(); + let base_x: u32 = base_width * idx; + for x in 0..image.width() { + for y in 0..image.height() { + final_image.put_pixel(base_x + x, y, image.get_pixel(x, y)) + } + } + } + { + zone!("write_spritesheet"); + final_image.save(file_path).err(); + } + }); + + let sizes: Vec = size_to_images.lock().unwrap().clone().into_keys().collect(); + + let returned = Returned { + sizes: sizes, + sprites: sprites_objects.lock().unwrap().to_owned(), + error: error.lock().unwrap().join("\n"), + }; + Ok(serde_json::to_string::(&returned).unwrap()) +} diff --git a/src/lib.rs b/src/lib.rs index 8b9fa90a..c6ce0ed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ pub mod git; pub mod hash; #[cfg(feature = "http")] pub mod http; +#[cfg(feature = "iconforge")] +pub mod iconforge; #[cfg(feature = "json")] pub mod json; #[cfg(feature = "log")] @@ -48,6 +50,3 @@ pub mod unzip; pub mod url; #[cfg(feature = "worleynoise")] pub mod worleynoise; - -#[cfg(not(target_pointer_width = "32"))] -compile_error!("rust-g must be compiled for a 32-bit target"); From 01da1b828fa8c1a6565990246587cf0cae45f77e Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 20 Dec 2023 19:26:06 -0500 Subject: [PATCH 02/35] Start blending --- src/iconforge.rs | 142 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 120 insertions(+), 22 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index d2f25e79..e014c18a 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -5,9 +5,10 @@ use crate::error::Error; use std::{ fs::File, io::BufReader, + num::ParseIntError, }; use dmi::icon::{Icon, IconState}; -use image::{DynamicImage, GenericImage, GenericImageView}; +use image::{DynamicImage, GenericImage, GenericImageView, Pixel, Rgba}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; //use raster::Image; use serde::{Serialize, Deserialize}; @@ -81,10 +82,10 @@ enum Transform { height: u32, }, CropTransform { - x1: u32, - y1: u32, - x2: u32, - y2: u32, + x1: i32, + y1: i32, + x2: i32, + y2: i32, } } @@ -117,25 +118,37 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) let sprites_objects: Arc>> = Arc::new(Mutex::new(HashMap::new())); sprites_map.par_iter().for_each(|sprite_entry| { - zone!("sprite_to_icon"); + zone!("sprite_to_icons"); let (_, icon) = sprite_entry; - let icon_path = icon.icon_file.to_owned(); - if icon_file_to_icon().lock().unwrap().contains_key(&icon_path) { - return; - } - let reader = BufReader::new(File::open(&icon_path).unwrap()); - let icon: Option; - { - zone!("load_icon"); - icon = Icon::load(reader).ok(); - } - if icon.is_none() { - error.lock().unwrap().push(format!("Invalid DMI: {}", icon_path)); - return; - } - icon_file_to_icon().lock().unwrap().insert(icon_path, icon.unwrap().to_owned()); + icon_to_icons(icon).par_iter().for_each(|icon| { + zone!("icon_to_dmi"); + let icon_path = icon.icon_file.to_owned(); + { + zone!("check_dmi_exists"); + // scope-in so the lock does not persist during DMI read + if icon_file_to_icon().lock().unwrap().contains_key(&icon_path) { + return; + } + } + let reader = BufReader::new(File::open(&icon_path).unwrap()); + let dmi: Option; + { + zone!("parse_dmi"); + dmi = Icon::load(reader).ok(); + } + if dmi.is_none() { + error.lock().unwrap().push(format!("Invalid DMI: {}", icon_path)); + return; + } + { + zone!("insert_dmi"); + icon_file_to_icon().lock().unwrap().insert(icon_path, dmi.unwrap().to_owned()); + } + }); }); + + sprites_map.par_iter().for_each(|sprite_entry| { zone!("map_sprite"); let (sprite_name, icon) = sprite_entry; @@ -174,8 +187,40 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) icon_idx = (icon_idx + 1) * icon.frame - 1 } let image: &DynamicImage = state.images.get(usize::try_from(icon_idx).unwrap()).unwrap(); - let cloned_image: DynamicImage = image.clone(); + let mut cloned_image: DynamicImage = image.clone(); // apply transforms here + + for transform in &icon.transform { + match transform { + Transform::BlendColorTransform { color, blend_mode } => { + let mutator = mutate(*blend_mode); + let color_parts = decode_hex(color).unwrap(); + for x in 0..cloned_image.width() { + for y in 0..cloned_image.height() { + let rgba = cloned_image.get_pixel(x, y).to_rgba(); + cloned_image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) + } + } + }, + Transform::BlendIconTransform { icon, blend_mode } => { + let mutator = mutate(*blend_mode); + let color_parts = decode_hex(color).unwrap(); + for x in 0..cloned_image.width() { + for y in 0..cloned_image.height() { + let rgba = cloned_image.get_pixel(x, y).to_rgba(); + cloned_image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) + } + } + }, + Transform::ScaleTransform { width, height } => { + cloned_image.resize_exact(*width, *height, image::imageops::FilterType::Nearest); + } + Transform::CropTransform { x1, y1, x2, y2 } => { + //cloned_image = cloned_image.crop_imm(x1, y1, x2 - x1, y2 - y1) + } + } + } + let size_id = format!("{}x{}", cloned_image.width(), cloned_image.height()); let mut size_map = size_to_images.lock().unwrap(); let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); @@ -221,3 +266,56 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) }; Ok(serde_json::to_string::(&returned).unwrap()) } + +fn icon_to_icons(icon: &IconObject) -> Vec { + let mut icons: Vec = Vec::new(); + icons.push(icon.to_owned()); + for transform in &icon.transform { + match transform { + Transform::BlendIconTransform { icon, .. } => { + let nested = icon_to_icons(&icon); + icons.extend(nested.to_owned()); + } + _ => {} + } + } + return icons; +} + +fn mutate(blend_mode: u8) -> fn(u8, u8) -> u8 { + return match blend_mode { + 0 => {|a: u8, b: u8| cap(a as u32 + b as u32)} + 2 => {|a: u8, b: u8| cap(a as u32 * b as u32)} + 3 => {|a: u8, b: u8| { + if a < 128 { + return cap(2 * a as u32 * b as u32); + } else { + return cap(255 - 510 * (255 - a as u32) * (255 - b as u32)); + } + }} + _ => {|a: u8, _: u8| a} + }; +} + +fn blend(rgba_src: Rgba, rgba_dst: [u8; 3], mutator_rgb: fn(u8, u8) -> u8) -> Rgba { + let r = mutator_rgb(rgba_src.0[0], rgba_dst[0]); + let g = mutator_rgb(rgba_src.0[1], rgba_dst[1]); + let b = mutator_rgb(rgba_src.0[2], rgba_dst[2]); + let a = rgba_src.0[3]; + return Rgba::( [r, g, b, a] ) +} + +fn decode_hex(s: &str) -> Result, ParseIntError> { + (1..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) + .collect() +} + +fn cap(val: u32) -> u8 { + if val > 255 { + return 255; + } else { + return u8::try_from(val).unwrap(); + } +} From 88055c64d0f631dd3571d7acf5104539500e58b2 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Thu, 21 Dec 2023 11:50:21 -0500 Subject: [PATCH 03/35] Huge cleanup --- src/iconforge.rs | 242 +++++++++++++++++++++++++++++++---------------- 1 file changed, 158 insertions(+), 84 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index e014c18a..143998f8 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -15,12 +15,26 @@ use serde::{Serialize, Deserialize}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tracy_full::{zone, frame}; -use once_cell::sync::OnceCell; +use once_cell::sync::Lazy; +static ICON_FILES: Lazy>> = Lazy::new(Mutex::default); +static ICON_STATES: Lazy>> = Lazy::new(Mutex::default); + +const SOUTH: u8 = 2; +const NORTH: u8 = 1; +const EAST: u8 = 4; +const WEST: u8 = 8; +const FOUR_DIRS: [u8; 4] = [SOUTH, NORTH, EAST, WEST]; +const SOUTHEAST: u8 = SOUTH | EAST; // 6 +const SOUTHWEST: u8 = SOUTH | WEST; // 10 +const NORTHEAST: u8 = NORTH | EAST; // 5 +const NORTHWEST: u8 = NORTH | WEST; // 9 +const EIGHT_DIRS: [u8; 8] = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST]; + +const DMI_ORDERING: [u8; 8] = EIGHT_DIRS; +// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = DMI_ORDERING.indexof(DIR) +// 255 is invalid. +const DIR_TO_INDEX: [u8; 11] = [255, 1, 0, 255, 2, 6, 4, 255, 3, 7, 5]; -fn icon_file_to_icon() -> &'static Mutex> { - static INSTANCE: OnceCell>> = OnceCell::new(); - INSTANCE.get_or_init(|| Mutex::new(HashMap::new())) -} byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { catch_panic(file_path, spritesheet_name, sprites).err() @@ -28,9 +42,10 @@ byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites) { - let file_path = file_path.to_owned(); - let spritesheet_name = spritesheet_name.to_owned(); - let sprites = sprites.to_owned(); + // Take ownership before passing + let file_path = file_path; + let spritesheet_name = spritesheet_name; + let sprites = sprites; Some(jobs::start(move || { match catch_panic(&file_path, &spritesheet_name, &sprites) { Ok(o) => o.to_string(), @@ -66,6 +81,16 @@ struct IconObject { transform: Vec } +trait IcoString { + fn to_icostring() -> String; +} + +impl IcoString for IconObject { + fn to_icostring() -> String { + return "".to_string(); // TODO implement this as as unique ID. Transforms need another trait + } +} + #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] enum Transform { @@ -113,81 +138,35 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) let error: Arc>> = Arc::new(Mutex::new(Vec::new())); - let size_to_images: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + let size_to_images: Arc>>> = Arc::new(Mutex::new(HashMap::new())); let sprites_map: HashMap = serde_json::from_str::>(sprites)?; let sprites_objects: Arc>> = Arc::new(Mutex::new(HashMap::new())); + // Pre-load all the DMIs now. sprites_map.par_iter().for_each(|sprite_entry| { zone!("sprite_to_icons"); let (_, icon) = sprite_entry; icon_to_icons(icon).par_iter().for_each(|icon| { - zone!("icon_to_dmi"); - let icon_path = icon.icon_file.to_owned(); - { - zone!("check_dmi_exists"); - // scope-in so the lock does not persist during DMI read - if icon_file_to_icon().lock().unwrap().contains_key(&icon_path) { - return; - } - } - let reader = BufReader::new(File::open(&icon_path).unwrap()); - let dmi: Option; - { - zone!("parse_dmi"); - dmi = Icon::load(reader).ok(); - } - if dmi.is_none() { - error.lock().unwrap().push(format!("Invalid DMI: {}", icon_path)); + if let Err(err) = icon_to_dmi(icon) { + error.lock().unwrap().push(err); return; } - { - zone!("insert_dmi"); - icon_file_to_icon().lock().unwrap().insert(icon_path, dmi.unwrap().to_owned()); - } }); }); - - + // Pick the specific icon states out of the DMI sprites_map.par_iter().for_each(|sprite_entry| { zone!("map_sprite"); let (sprite_name, icon) = sprite_entry; - let parsed_icon = icon_file_to_icon().lock().unwrap().get(&icon.icon_file).unwrap().to_owned(); - let mut matched_state: Option = Option::None; - for icon_state in parsed_icon.states { - if icon_state.name == icon.icon_state { - matched_state = Option::Some(icon_state.clone()); - break; - } - } - if matched_state.is_none() { - error.lock().unwrap().push(format!("Could not find associated icon state {} for {}", icon.icon_state, sprite_name)); - return; - } - let state = matched_state.unwrap(); - if !( if icon.dir == 2 { state.dirs >= 1 } else { state.dirs >= 4 } && state.frames >= icon.frame ) { - error.lock().unwrap().push(format!("Could not find associated dir or frame dir: {} frame: {} in {} icon_state - dirs: {} frames: {}", icon.dir, icon.frame, sprite_name, state.dirs, state.frames)); - return; - } - let mut icon_idx: u32 = 0; - if state.dirs == 4 { - icon_idx = match icon.dir { - 2 => 0, // South - 1 => 1, // North - 4 => 2, // East - 8 => 3, // West - _ => 0, - } - } else if state.dirs != 1 { - error.lock().unwrap().push(format!("Unsupported dirs size of {} in {} state: {} for sprite {}", state.dirs, icon.icon_file, icon.icon_state, sprite_name)); + + // get DynamicImage + let image_result = icon_to_image(icon, sprite_name); + if image_result.is_err() { + error.lock().unwrap().push(image_result.unwrap_err()); return; } - if state.frames > 1 { - // Add one so zero scales properly - icon_idx = (icon_idx + 1) * icon.frame - 1 - } - let image: &DynamicImage = state.images.get(usize::try_from(icon_idx).unwrap()).unwrap(); - let mut cloned_image: DynamicImage = image.clone(); + let image = image_result.unwrap(); + // apply transforms here for transform in &icon.transform { @@ -195,36 +174,38 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) Transform::BlendColorTransform { color, blend_mode } => { let mutator = mutate(*blend_mode); let color_parts = decode_hex(color).unwrap(); - for x in 0..cloned_image.width() { - for y in 0..cloned_image.height() { - let rgba = cloned_image.get_pixel(x, y).to_rgba(); - cloned_image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) + for x in 0..image.width() { + for y in 0..image.height() { + let rgba = image.get_pixel(x, y).to_rgba(); + image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) } } }, Transform::BlendIconTransform { icon, blend_mode } => { + /* let mutator = mutate(*blend_mode); let color_parts = decode_hex(color).unwrap(); - for x in 0..cloned_image.width() { - for y in 0..cloned_image.height() { - let rgba = cloned_image.get_pixel(x, y).to_rgba(); - cloned_image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) + for x in 0..image.width() { + for y in 0..image.height() { + let rgba = image.get_pixel(x, y).to_rgba(); + image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) } } + */ }, Transform::ScaleTransform { width, height } => { - cloned_image.resize_exact(*width, *height, image::imageops::FilterType::Nearest); + *image = image.resize_exact(*width, *height, image::imageops::FilterType::Nearest); } Transform::CropTransform { x1, y1, x2, y2 } => { - //cloned_image = cloned_image.crop_imm(x1, y1, x2 - x1, y2 - y1) + //*image = image.crop_imm(x1, y1, x2 - x1, y2 - y1) } } } - let size_id = format!("{}x{}", cloned_image.width(), cloned_image.height()); + let size_id = format!("{}x{}", image.width(), image.height()); let mut size_map = size_to_images.lock().unwrap(); let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); - vec.push(cloned_image); + vec.push(image); sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { size_id: size_id.to_owned(), position: u32::try_from(vec.len()).unwrap() - 1 @@ -257,7 +238,7 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) } }); - let sizes: Vec = size_to_images.lock().unwrap().clone().into_keys().collect(); + let sizes: Vec = size_to_images.lock().unwrap().iter().map(|(k, _v)| k).cloned().collect(); let returned = Returned { sizes: sizes, @@ -267,14 +248,17 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) Ok(serde_json::to_string::(&returned).unwrap()) } -fn icon_to_icons(icon: &IconObject) -> Vec { - let mut icons: Vec = Vec::new(); - icons.push(icon.to_owned()); +/// Takes in an icon and gives a list of nested icons. Also returns a reference to the provided icon in the list. +fn icon_to_icons(icon: &IconObject) -> Vec<&IconObject> { + let mut icons: Vec<&IconObject> = Vec::new(); + icons.push(icon); for transform in &icon.transform { match transform { Transform::BlendIconTransform { icon, .. } => { - let nested = icon_to_icons(&icon); - icons.extend(nested.to_owned()); + let nested = icon_to_icons(icon); + for icon in nested { + icons.push(icon) + } } _ => {} } @@ -282,6 +266,96 @@ fn icon_to_icons(icon: &IconObject) -> Vec { return icons; } +/// Given an IconObject, returns a DMI Icon structure and caches it. +fn icon_to_dmi(icon: &IconObject) -> Result<&Icon, String> { + zone!("icon_to_dmi"); + let icon_path: String = icon.icon_file; + { + zone!("check_dmi_exists"); + // scope-in so the lock does not persist during DMI read + let found_icon = ICON_FILES.lock().unwrap().get(&icon_path); + if found_icon.is_some() { + return Ok(found_icon.unwrap()); + } + } + let reader = BufReader::new(File::open(&icon_path).unwrap()); + let dmi: Option; + { + zone!("parse_dmi"); + dmi = Icon::load(reader).ok(); + } + if dmi.is_none() { + return Err(format!("Invalid DMI: {}", icon_path)); + } + { + zone!("insert_dmi"); + let my_dmi = dmi.unwrap(); + // cache it for later. + // Ownership is given to the hashmap + ICON_FILES.lock().unwrap().insert(icon_path,my_dmi); + return Ok(&my_dmi); + } +} + +fn icon_to_image<'a>(icon: &'a IconObject, sprite_name: &String) -> Result<&'a mut DynamicImage, String> { + { + zone!("check_dynamicimage_exists"); + // scope-in so the lock does not persist during DMI read + let found_icon = ICON_STATES.lock().unwrap().get(&icon.to_icostring()); + if found_icon.is_some() { + return Ok(*found_icon.unwrap()) + } + } + let result = icon_to_dmi(icon); + if result.is_err() { + return Err(result.unwrap_err()); + } + let dmi = result.unwrap(); + let mut matched_state: Option<&IconState> = Option::None; + { + zone!("match_icon_state"); + for icon_state in dmi.states { + if icon_state.name == icon.icon_state { + matched_state = Option::Some(&icon_state); + break; + } + } + } + if matched_state.is_none() { + return Err(format!("Could not find associated icon state {} for {}", icon.icon_state, sprite_name)); + } + let state = matched_state.unwrap(); + { + zone!("determine_icon_state_validity"); + if state.frames < icon.frame { + return Err(format!("Could not find associated frame: {} in {} icon_state {} - dirs: {} frames: {}", icon.frame, sprite_name, icon.icon_state, state.dirs, state.frames)); + } + if (state.dirs == 1 && icon.dir != SOUTH) + || (state.dirs == 4 && !FOUR_DIRS.contains(&icon.dir)) + || (state.dirs == 8 && !EIGHT_DIRS.contains(&icon.dir)) { + return Err(format!("Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name)); + } + + } + let icon_index = DIR_TO_INDEX.get(icon.dir as usize); + if icon_index.is_none() || *icon_index.unwrap() == 255 { + return Err(format!("Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name)); + } + let mut icon_idx: u32 = *icon_index.unwrap() as u32; + if icon.frame > 1 { + // Add one so zero scales properly + icon_idx = (icon_idx + 1) * icon.frame - 1 + } + let image: &mut DynamicImage = state.images.get_mut(icon_idx as usize).unwrap(); + { + zone!("insert_dynamicimage"); + // cache it for later. + // Ownership is given to the hashmap + ICON_STATES.lock().unwrap().insert(icon.to_icostring(), image); + } + return Ok(image); +} + fn mutate(blend_mode: u8) -> fn(u8, u8) -> u8 { return match blend_mode { 0 => {|a: u8, b: u8| cap(a as u32 + b as u32)} From ef602382c302ef9fb29a523582ecb414f0281812 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Thu, 21 Dec 2023 16:02:18 -0500 Subject: [PATCH 04/35] Finish optimizing the thing --- src/hash.rs | 4 +-- src/iconforge.rs | 88 ++++++++++++++++++++++++++---------------------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/hash.rs b/src/hash.rs index ee959fe0..8353ae5b 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -76,11 +76,11 @@ fn hash_algorithm>(name: &str, bytes: B) -> Result { } } -fn string_hash(algorithm: &str, string: &str) -> Result { +pub fn string_hash(algorithm: &str, string: &str) -> Result { hash_algorithm(algorithm, string) } -fn file_hash(algorithm: &str, path: &str) -> Result { +pub fn file_hash(algorithm: &str, path: &str) -> Result { let mut bytes: Vec = Vec::new(); let mut file = BufReader::new(File::open(path)?); file.read_to_end(&mut bytes)?; diff --git a/src/iconforge.rs b/src/iconforge.rs index 143998f8..a8853f5a 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1,6 +1,7 @@ // DMI spritesheet generator // Developed by itsmeow use crate::jobs; +use crate::hash::string_hash; use crate::error::Error; use std::{ fs::File, @@ -16,8 +17,8 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tracy_full::{zone, frame}; use once_cell::sync::Lazy; -static ICON_FILES: Lazy>> = Lazy::new(Mutex::default); -static ICON_STATES: Lazy>> = Lazy::new(Mutex::default); +static ICON_FILES: Lazy>>> = Lazy::new(Mutex::default); +static ICON_STATES: Lazy>> = Lazy::new(Mutex::default); const SOUTH: u8 = 2; const NORTH: u8 = 1; @@ -43,9 +44,9 @@ byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites) { // Take ownership before passing - let file_path = file_path; - let spritesheet_name = spritesheet_name; - let sprites = sprites; + let file_path = file_path.to_owned(); + let spritesheet_name = spritesheet_name.to_owned(); + let sprites = sprites.to_owned(); Some(jobs::start(move || { match catch_panic(&file_path, &spritesheet_name, &sprites) { Ok(o) => o.to_string(), @@ -81,13 +82,9 @@ struct IconObject { transform: Vec } -trait IcoString { - fn to_icostring() -> String; -} - -impl IcoString for IconObject { - fn to_icostring() -> String { - return "".to_string(); // TODO implement this as as unique ID. Transforms need another trait +impl IconObject { + fn to_icostring(&self) -> Result { + return string_hash("xxh64", &serde_json::to_string(self).unwrap()); } } @@ -138,7 +135,7 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) let error: Arc>> = Arc::new(Mutex::new(Vec::new())); - let size_to_images: Arc>>> = Arc::new(Mutex::new(HashMap::new())); + let size_to_icon_objects: Arc>>> = Arc::new(Mutex::new(HashMap::new())); let sprites_map: HashMap = serde_json::from_str::>(sprites)?; let sprites_objects: Arc>> = Arc::new(Mutex::new(HashMap::new())); @@ -165,7 +162,8 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) error.lock().unwrap().push(image_result.unwrap_err()); return; } - let image = image_result.unwrap(); + + let mut image = image_result.unwrap(); // apply transforms here @@ -194,7 +192,7 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) */ }, Transform::ScaleTransform { width, height } => { - *image = image.resize_exact(*width, *height, image::imageops::FilterType::Nearest); + image = image.resize_exact(*width, *height, image::imageops::FilterType::Nearest); } Transform::CropTransform { x1, y1, x2, y2 } => { //*image = image.crop_imm(x1, y1, x2 - x1, y2 - y1) @@ -202,29 +200,38 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) } } + // Generate the metadata used by the game let size_id = format!("{}x{}", image.width(), image.height()); - let mut size_map = size_to_images.lock().unwrap(); + return_image(image, icon); + let mut size_map = size_to_icon_objects.lock().unwrap(); let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); - vec.push(image); + vec.push(icon); + sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { size_id: size_id.to_owned(), position: u32::try_from(vec.len()).unwrap() - 1 }); }); - size_to_images.lock().unwrap().par_iter().for_each(|(size_id, images_list)| { + size_to_icon_objects.lock().unwrap().par_iter().for_each(|(size_id, icon_objects)| { zone!("join_sprites"); let file_path = format!("{}{}_{}.png", file_path, spritesheet_name, size_id); let size_data: Vec<&str> = size_id.split("x").collect(); let base_width = size_data.first().unwrap().to_string().parse::().unwrap(); let base_height = size_data.last().unwrap().to_string().parse::().unwrap(); - let image_count: u32 = u32::try_from(images_list.len()).unwrap(); + let image_count: u32 = u32::try_from(icon_objects.len()).unwrap(); let mut final_image = DynamicImage::new_rgba8(base_width * image_count, base_height); for idx in 0..image_count { zone!("join_sprite"); - let image: &DynamicImage = images_list.get::(usize::try_from(idx).unwrap()).unwrap(); + let icon = icon_objects.get::(usize::try_from(idx).unwrap()).unwrap(); + let image_result = icon_to_image(icon, &"N/A, in final generation stage".to_string()); + if image_result.is_err() { + error.lock().unwrap().push(image_result.unwrap_err()); + continue; + } + let image = image_result.unwrap(); let base_x: u32 = base_width * idx; for x in 0..image.width() { for y in 0..image.height() { @@ -238,7 +245,7 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) } }); - let sizes: Vec = size_to_images.lock().unwrap().iter().map(|(k, _v)| k).cloned().collect(); + let sizes: Vec = size_to_icon_objects.lock().unwrap().iter().map(|(k, _v)| k).cloned().collect(); let returned = Returned { sizes: sizes, @@ -267,18 +274,19 @@ fn icon_to_icons(icon: &IconObject) -> Vec<&IconObject> { } /// Given an IconObject, returns a DMI Icon structure and caches it. -fn icon_to_dmi(icon: &IconObject) -> Result<&Icon, String> { +fn icon_to_dmi(icon: &IconObject) -> Result, String> { zone!("icon_to_dmi"); - let icon_path: String = icon.icon_file; + let icon_path: &String = &icon.icon_file; { zone!("check_dmi_exists"); + let map = ICON_FILES.lock().unwrap(); // scope-in so the lock does not persist during DMI read - let found_icon = ICON_FILES.lock().unwrap().get(&icon_path); + let found_icon = map.get(icon_path); if found_icon.is_some() { - return Ok(found_icon.unwrap()); + return Ok(found_icon.unwrap().clone()); } } - let reader = BufReader::new(File::open(&icon_path).unwrap()); + let reader = BufReader::new(File::open(icon_path).unwrap()); let dmi: Option; { zone!("parse_dmi"); @@ -289,21 +297,21 @@ fn icon_to_dmi(icon: &IconObject) -> Result<&Icon, String> { } { zone!("insert_dmi"); - let my_dmi = dmi.unwrap(); // cache it for later. // Ownership is given to the hashmap - ICON_FILES.lock().unwrap().insert(icon_path,my_dmi); - return Ok(&my_dmi); + let dmi = ICON_FILES.lock().unwrap().insert(icon_path.to_owned(), Arc::new(dmi.unwrap())); + return Ok(dmi.unwrap().clone()); } } -fn icon_to_image<'a>(icon: &'a IconObject, sprite_name: &String) -> Result<&'a mut DynamicImage, String> { +/// Gives ownership over the image. Please return when you are done <3 +fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result { { zone!("check_dynamicimage_exists"); // scope-in so the lock does not persist during DMI read - let found_icon = ICON_STATES.lock().unwrap().get(&icon.to_icostring()); + let found_icon = ICON_STATES.lock().unwrap().remove(&icon.to_icostring().unwrap()); if found_icon.is_some() { - return Ok(*found_icon.unwrap()) + return Ok(found_icon.unwrap()) } } let result = icon_to_dmi(icon); @@ -314,7 +322,7 @@ fn icon_to_image<'a>(icon: &'a IconObject, sprite_name: &String) -> Result<&'a m let mut matched_state: Option<&IconState> = Option::None; { zone!("match_icon_state"); - for icon_state in dmi.states { + for icon_state in &dmi.states { if icon_state.name == icon.icon_state { matched_state = Option::Some(&icon_state); break; @@ -346,16 +354,16 @@ fn icon_to_image<'a>(icon: &'a IconObject, sprite_name: &String) -> Result<&'a m // Add one so zero scales properly icon_idx = (icon_idx + 1) * icon.frame - 1 } - let image: &mut DynamicImage = state.images.get_mut(icon_idx as usize).unwrap(); - { - zone!("insert_dynamicimage"); - // cache it for later. - // Ownership is given to the hashmap - ICON_STATES.lock().unwrap().insert(icon.to_icostring(), image); - } + let image: DynamicImage = state.images.get(icon_idx as usize).unwrap().clone(); return Ok(image); } +// Gives an image back to the cache, after it is done being altered. +fn return_image(image: DynamicImage, icon: &IconObject) { + zone!("insert_dynamicimage"); + ICON_STATES.lock().unwrap().insert(icon.to_icostring().unwrap(), image); +} + fn mutate(blend_mode: u8) -> fn(u8, u8) -> u8 { return match blend_mode { 0 => {|a: u8, b: u8| cap(a as u32 + b as u32)} From 03963ecda4103f59954a20535e1b57be72e3d1c7 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 01:16:06 -0500 Subject: [PATCH 05/35] Finish the thing!! --- Cargo.lock | 73 ++------ Cargo.toml | 27 ++- src/iconforge.rs | 440 ++++++++++++++++++++++++++++++++--------------- src/lib.rs | 1 + 4 files changed, 328 insertions(+), 213 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f43f7b72..8771a4e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,13 +686,8 @@ dependencies = [ ] [[package]] -<<<<<<< HEAD name = "dunce" version = "1.0.4" -======= -name = "dtoa" -version = "0.4.8" ->>>>>>> 47bfb12 (iconforge beta) source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" @@ -763,15 +758,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fdeflate" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" -dependencies = [ - "simd-adler32", -] - [[package]] name = "fixedbitset" version = "0.4.2" @@ -786,7 +772,7 @@ checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide 0.5.3", + "miniz_oxide", ] [[package]] @@ -1710,22 +1696,8 @@ dependencies = [ ] [[package]] -<<<<<<< HEAD name = "indexmap" version = "2.1.0" -======= -name = "inflate" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" -dependencies = [ - "adler32", -] - -[[package]] -name = "instant" -version = "0.1.12" ->>>>>>> 47bfb12 (iconforge beta) source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ @@ -2018,16 +1990,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", - "simd-adler32", -] - [[package]] name = "mio" version = "0.8.8" @@ -2412,11 +2374,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", -<<<<<<< HEAD "miniz_oxide", -======= - "miniz_oxide 0.7.1", ->>>>>>> 47bfb12 (iconforge beta) ] [[package]] @@ -2809,6 +2767,7 @@ dependencies = [ "gix", "hex", "image", + "lazy_static", "md-5", "mysql", "noise", @@ -2827,12 +2786,8 @@ dependencies = [ "sha-1", "sha2", "thiserror", -<<<<<<< HEAD "toml 0.8.8", -======= - "toml", "tracy_full", ->>>>>>> 47bfb12 (iconforge beta) "twox-hash", "url", "zip", @@ -2867,7 +2822,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -<<<<<<< HEAD +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] name = "rustix" version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2878,14 +2841,6 @@ dependencies = [ "libc", "linux-raw-sys", "windows-sys 0.52.0", -======= -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", ->>>>>>> 47bfb12 (iconforge beta) ] [[package]] @@ -3119,12 +3074,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index bcde95c0..3f7ffc7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } dmi = { version = "0.3.1", optional = true } tracy_full = { version = "1.6.1", optional = true} +#tracy_full = { version = "1.6.1", optional = true, features = ["enable"]} [features] default = [ @@ -135,7 +136,18 @@ hash = [ "serde_json", ] pathfinder = ["num-integer", "pathfinding", "serde", "serde_json"] -iconforge = ["serde", "serde_json", "png", "image", "dep:dmi", "rayon", "tracy_full", "jobs", "once_cell"] +iconforge = [ + "serde", + "serde_json", + "png", + "image", + "dep:dmi", + "rayon", + "tracy_full", + "jobs", + "once_cell", + "dashmap" +] redis_pubsub = ["flume", "redis", "serde", "serde_json"] redis_reliablequeue = ["flume", "redis", "serde", "serde_json"] unzip = ["zip", "jobs"] @@ -150,16 +162,3 @@ jobs = ["flume"] [dev-dependencies] regex = "1" - -# DEV ONLY. REMOVE THIS LATER -[profile.dev.package.dmi] -opt-level = 3 - -[profile.dev.package.image] -opt-level = 3 - -[profile.dev.package.serde] -opt-level = 3 - -[profile.dev.package.serde_json] -opt-level = 3 diff --git a/src/iconforge.rs b/src/iconforge.rs index a8853f5a..e9de08fb 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -4,21 +4,26 @@ use crate::jobs; use crate::hash::string_hash; use crate::error::Error; use std::{ - fs::File, - io::BufReader, - num::ParseIntError, + fs::{File, OpenOptions}, + io::{BufReader, Write}, + cell::RefCell, }; use dmi::icon::{Icon, IconState}; -use image::{DynamicImage, GenericImage, GenericImageView, Pixel, Rgba}; +use image::{DynamicImage, GenericImage, GenericImageView, Pixel, ImageBuffer}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; //use raster::Image; use serde::{Serialize, Deserialize}; use std::collections::HashMap; +use dashmap::DashMap; use std::sync::{Arc, Mutex}; use tracy_full::{zone, frame}; use once_cell::sync::Lazy; -static ICON_FILES: Lazy>>> = Lazy::new(Mutex::default); -static ICON_STATES: Lazy>> = Lazy::new(Mutex::default); +use std::backtrace::Backtrace; +static ICON_FILES: Lazy>> = Lazy::new(DashMap::new); +static ICON_STATES: Lazy> = Lazy::new(DashMap::new); +thread_local! { + static LAST_BACKTRACE: RefCell> = RefCell::new(None); +} const SOUTH: u8 = 2; const NORTH: u8 = 1; @@ -29,16 +34,22 @@ const SOUTHEAST: u8 = SOUTH | EAST; // 6 const SOUTHWEST: u8 = SOUTH | WEST; // 10 const NORTHEAST: u8 = NORTH | EAST; // 5 const NORTHWEST: u8 = NORTH | WEST; // 9 +// This is ordered by how DMIs internally place dirs into the PNG const EIGHT_DIRS: [u8; 8] = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST]; -const DMI_ORDERING: [u8; 8] = EIGHT_DIRS; -// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = DMI_ORDERING.indexof(DIR) +// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = EIGHT_DIRS.indexof(DIR) // 255 is invalid. const DIR_TO_INDEX: [u8; 11] = [255, 1, 0, 255, 2, 6, 4, 255, 3, 7, 5]; byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { - catch_panic(file_path, spritesheet_name, sprites).err() + let file_path = file_path.to_owned(); + let spritesheet_name = spritesheet_name.to_owned(); + let sprites = sprites.to_owned(); + Some(match catch_panic(&file_path, &spritesheet_name, &sprites) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }) }); @@ -84,26 +95,27 @@ struct IconObject { impl IconObject { fn to_icostring(&self) -> Result { - return string_hash("xxh64", &serde_json::to_string(self).unwrap()); + zone!("to_icostring"); + string_hash("xxh64", &serde_json::to_string(self).unwrap()) } } #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] enum Transform { - BlendColorTransform { + BlendColor { color: String, blend_mode: u8, }, - BlendIconTransform { + BlendIcon { icon: IconObject, blend_mode: u8, }, - ScaleTransform { + Scale { width: u32, height: u32, }, - CropTransform { + Crop { x1: i32, y1: i32, x2: i32, @@ -112,22 +124,36 @@ enum Transform { } fn catch_panic(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { + std::panic::set_hook(Box::new(|panic_info| { + LAST_BACKTRACE.set(Option::Some(Backtrace::capture())); + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open("iconforge-error.log") + .unwrap(); + file.write_all(Backtrace::capture().to_string().as_bytes()).expect("Fail backtrace"); + file.write_all(panic_info.payload().downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| { + panic_info.payload().downcast_ref::().cloned() + }).unwrap().as_bytes()).expect("Fail payload"); + })); let x = std::panic::catch_unwind(|| { let result = generate_spritesheet(file_path, spritesheet_name, sprites); frame!(); - return result; + result }); - if x.is_err() { - match x.unwrap_err().downcast_ref::() { - Some(as_string) => { - return Err(Error::IconState(as_string.to_owned())) - } - None => { - return Err(Error::IconState("Failed to stringify panic".to_string())) - } - } + if let Err(err) = x { + let message: Option = err + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| { + err.downcast_ref::().cloned() + }); + return Err(Error::IconState(format!("{}\n\nBACKTRACE:\n{}", message.unwrap().to_owned(), LAST_BACKTRACE.take().unwrap_or(Backtrace::capture()).to_string()))) } - return x.ok().unwrap() + x.ok().unwrap() } fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { @@ -146,7 +172,6 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) icon_to_icons(icon).par_iter().for_each(|icon| { if let Err(err) = icon_to_dmi(icon) { error.lock().unwrap().push(err); - return; } }); }); @@ -156,67 +181,37 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) zone!("map_sprite"); let (sprite_name, icon) = sprite_entry; - // get DynamicImage + // get DynamicImage, applying transforms as well let image_result = icon_to_image(icon, sprite_name); - if image_result.is_err() { - error.lock().unwrap().push(image_result.unwrap_err()); + if let Err(err) = image_result { + error.lock().unwrap().push(err); return; } + let image = image_result.unwrap(); - let mut image = image_result.unwrap(); - - // apply transforms here - - for transform in &icon.transform { - match transform { - Transform::BlendColorTransform { color, blend_mode } => { - let mutator = mutate(*blend_mode); - let color_parts = decode_hex(color).unwrap(); - for x in 0..image.width() { - for y in 0..image.height() { - let rgba = image.get_pixel(x, y).to_rgba(); - image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) - } - } - }, - Transform::BlendIconTransform { icon, blend_mode } => { - /* - let mutator = mutate(*blend_mode); - let color_parts = decode_hex(color).unwrap(); - for x in 0..image.width() { - for y in 0..image.height() { - let rgba = image.get_pixel(x, y).to_rgba(); - image.put_pixel(x, y, blend(rgba, [color_parts[0], color_parts[1], color_parts[2]], mutator)) - } - } - */ - }, - Transform::ScaleTransform { width, height } => { - image = image.resize_exact(*width, *height, image::imageops::FilterType::Nearest); - } - Transform::CropTransform { x1, y1, x2, y2 } => { - //*image = image.crop_imm(x1, y1, x2 - x1, y2 - y1) - } - } + { + zone!("create_game_metadata"); + // Generate the metadata used by the game + let size_id = format!("{}x{}", image.width(), image.height()); + return_image(image, icon); + let mut size_map = size_to_icon_objects.lock().unwrap(); + let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); + vec.push(icon); + + sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { + size_id: size_id.to_owned(), + position: u32::try_from(vec.len()).unwrap() - 1 + }); } - - // Generate the metadata used by the game - let size_id = format!("{}x{}", image.width(), image.height()); - return_image(image, icon); - let mut size_map = size_to_icon_objects.lock().unwrap(); - let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); - vec.push(icon); - - sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { - size_id: size_id.to_owned(), - position: u32::try_from(vec.len()).unwrap() - 1 - }); }); + // all images have been returned now, so continue... + + // Get all the sprites and spew them onto a spritesheet. size_to_icon_objects.lock().unwrap().par_iter().for_each(|(size_id, icon_objects)| { zone!("join_sprites"); let file_path = format!("{}{}_{}.png", file_path, spritesheet_name, size_id); - let size_data: Vec<&str> = size_id.split("x").collect(); + let size_data: Vec<&str> = size_id.split('x').collect(); let base_width = size_data.first().unwrap().to_string().parse::().unwrap(); let base_height = size_data.last().unwrap().to_string().parse::().unwrap(); @@ -227,8 +222,8 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) zone!("join_sprite"); let icon = icon_objects.get::(usize::try_from(idx).unwrap()).unwrap(); let image_result = icon_to_image(icon, &"N/A, in final generation stage".to_string()); - if image_result.is_err() { - error.lock().unwrap().push(image_result.unwrap_err()); + if let Err(err) = image_result { + error.lock().unwrap().push(err); continue; } let image = image_result.unwrap(); @@ -247,8 +242,9 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) let sizes: Vec = size_to_icon_objects.lock().unwrap().iter().map(|(k, _v)| k).cloned().collect(); + // Collect the game metadata and any errors. let returned = Returned { - sizes: sizes, + sizes, sprites: sprites_objects.lock().unwrap().to_owned(), error: error.lock().unwrap().join("\n"), }; @@ -257,20 +253,18 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) /// Takes in an icon and gives a list of nested icons. Also returns a reference to the provided icon in the list. fn icon_to_icons(icon: &IconObject) -> Vec<&IconObject> { + zone!("icon_to_icons"); let mut icons: Vec<&IconObject> = Vec::new(); icons.push(icon); for transform in &icon.transform { - match transform { - Transform::BlendIconTransform { icon, .. } => { - let nested = icon_to_icons(icon); - for icon in nested { - icons.push(icon) - } + if let Transform::BlendIcon { icon, .. } = transform { + let nested = icon_to_icons(icon); + for icon in nested { + icons.push(icon) } - _ => {} } } - return icons; + icons } /// Given an IconObject, returns a DMI Icon structure and caches it. @@ -279,14 +273,17 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { let icon_path: &String = &icon.icon_file; { zone!("check_dmi_exists"); - let map = ICON_FILES.lock().unwrap(); // scope-in so the lock does not persist during DMI read - let found_icon = map.get(icon_path); - if found_icon.is_some() { - return Ok(found_icon.unwrap().clone()); + let found_icon = ICON_FILES.get(icon_path); + if let Some(found) = found_icon { + return Ok(found.clone()); } } - let reader = BufReader::new(File::open(icon_path).unwrap()); + let icon_file = File::open(icon_path); + if icon_file.is_err() { + return Err(format!("No such DMI file: {}", icon_path)) + } + let reader = BufReader::new(icon_file.unwrap()); let dmi: Option; { zone!("parse_dmi"); @@ -297,34 +294,34 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { } { zone!("insert_dmi"); + let dmi_arc = Arc::new(dmi.unwrap()); + let other_arc = dmi_arc.clone(); // cache it for later. // Ownership is given to the hashmap - let dmi = ICON_FILES.lock().unwrap().insert(icon_path.to_owned(), Arc::new(dmi.unwrap())); - return Ok(dmi.unwrap().clone()); + ICON_FILES.insert(icon_path.to_owned(), dmi_arc); + Ok(other_arc) } } -/// Gives ownership over the image. Please return when you are done <3 +// Takes an IconObject, gets its DMI, then picks out a DynamicImage for the IconState, as well as transforms the DynamicImage. +// Gives ownership over the image. Please return when you are done <3 fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result { + zone!("icon_to_image"); { zone!("check_dynamicimage_exists"); // scope-in so the lock does not persist during DMI read - let found_icon = ICON_STATES.lock().unwrap().remove(&icon.to_icostring().unwrap()); - if found_icon.is_some() { - return Ok(found_icon.unwrap()) + let found_icon = ICON_STATES.remove(&icon.to_icostring().unwrap()); + if let Some(found) = found_icon { + return Ok(found.1) } } - let result = icon_to_dmi(icon); - if result.is_err() { - return Err(result.unwrap_err()); - } - let dmi = result.unwrap(); + let dmi = icon_to_dmi(icon)?; let mut matched_state: Option<&IconState> = Option::None; { zone!("match_icon_state"); for icon_state in &dmi.states { if icon_state.name == icon.icon_state { - matched_state = Option::Some(&icon_state); + matched_state = Option::Some(icon_state); break; } } @@ -355,49 +352,218 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result fn(u8, u8) -> u8 { - return match blend_mode { - 0 => {|a: u8, b: u8| cap(a as u32 + b as u32)} - 2 => {|a: u8, b: u8| cap(a as u32 * b as u32)} - 3 => {|a: u8, b: u8| { - if a < 128 { - return cap(2 * a as u32 * b as u32); - } else { - return cap(255 - 510 * (255 - a as u32) * (255 - b as u32)); +// Applies transforms to a DynamicImage. +fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &String) -> (DynamicImage, String) { + zone!("transform_image"); + let mut image = image_in; + let mut error: Vec = Vec::new(); + for transform in &icon.transform { + match transform { + Transform::BlendColor { color, blend_mode } => { + zone!("blend_color"); + let mut hex: String = color.to_owned(); + if hex.starts_with('#') { + hex = hex[1..].to_string(); + } + if hex.len() == 6 { + hex = format!("{}ff", hex); + } + let mut color2: [u8; 4] = [0, 0, 0, 255]; + hex::decode_to_slice(hex, &mut color2).expect(&format!("Decoding hex color {} failed", color)); + for x in 0..image.width() { + for y in 0..image.height() { + let px = image.get_pixel(x, y); + let pixel = px.channels(); + let blended = blend(pixel, &color2, *blend_mode); + + image.put_pixel(x, y, + image::Rgba::(blended), + ); + } + } + }, + Transform::BlendIcon { icon, blend_mode } => { + zone!("blend_icon"); + let image_result = icon_to_image(icon, &format!("Transform blend_icon of {}", sprite_name)); + if let Err(err) = image_result { + error.push(err); + continue; + } + + let other_image = image_result.unwrap(); + + for x in 0..image.width() { + if x >= other_image.width() { + break; // undefined behavior in DM :) + } + for y in 0..image.height() { + if y >= other_image.height() { + break; // undefined behavior in DM :) + } + let px1 = image.get_pixel(x, y); + let px2 = other_image.get_pixel(x, y); + let pixel_1 = px1.channels(); + let pixel_2 = px2.channels(); + + let blended = blend(pixel_1, pixel_2, *blend_mode); + + image.put_pixel(x, y, + image::Rgba::(blended), + ); + } + } + return_image(other_image, icon); + + }, + Transform::Scale { width, height } => { + zone!("scale"); + let x_ratio = image.width() as f32 / *width as f32; + let y_ratio = image.height() as f32 / *height as f32; + let mut new_image = DynamicImage::new_rgba8(*width, *height); + for x in 0..*width { + for y in 0..*height { + let old_x: u32 = ( x as f32 * x_ratio ).floor() as u32; + let old_y: u32 = ( y as f32 * y_ratio ).floor() as u32; + let pixel = image.get_pixel(old_x, old_y); + new_image.put_pixel(x, y, pixel); + } + } + image = new_image; } - }} - _ => {|a: u8, _: u8| a} - }; -} + Transform::Crop { x1, y1, x2, y2 } => { + zone!("crop"); + let i_width = image.width(); + let i_height = image.height(); + let mut x1 = *x1; + let mut y1 = *y1; + let mut x2 = *x2; + let mut y2 = *y2; + if x2 <= x1 || y2 <= y1 { + error.push(format!("Invalid bounds {} {} to {} {} from sprite {}", x1, y1, x2, y2, sprite_name)); + continue; + } -fn blend(rgba_src: Rgba, rgba_dst: [u8; 3], mutator_rgb: fn(u8, u8) -> u8) -> Rgba { - let r = mutator_rgb(rgba_src.0[0], rgba_dst[0]); - let g = mutator_rgb(rgba_src.0[1], rgba_dst[1]); - let b = mutator_rgb(rgba_src.0[2], rgba_dst[2]); - let a = rgba_src.0[3]; - return Rgba::( [r, g, b, a] ) + // convert from BYOND (0,0 is bottom left) to Rust (0,0 is top left) + let y2_old = y2; + y2 = i_height as i32 - y1; + y1 = i_height as i32 - y2_old; + + let mut width = x2 - x1; + let mut height = y2 - y1; + + if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { + //continue; + let mut blank_img = ImageBuffer::from_fn(width as u32, height as u32, |_x, _y| image::Rgba([0, 0, 0, 0])); + image::imageops::overlay( + &mut blank_img, + &image, + if x1 < 0 { (x1).abs() as i64 } else { 0 } - if x1 > i_width as i32 { (x1 - i_width as i32) as i64 } else { 0 }, + if y1 < 0 { (y1).abs() as i64 } else { 0 } - if x1 > i_width as i32 { (x1 - i_width as i32) as i64} else { 0 }, + ); + image = DynamicImage::new_rgba8(width as u32, height as u32); + let error_i = image.copy_from(&blank_img, 0, 0); + if let Err(err) = error_i { + error.push(err.to_string()); + continue; + } + assert_eq!(image.width(), width as u32); + assert_eq!(image.height(), height as u32); + if x1 < 0 { + x1 = 0; + } + if x2 > i_width as i32 { + x2 = i_width as i32; + } + if y1 < 0 { + y1 = 0; + } + if y2 > i_height as i32 { + y2 = i_height as i32; + } + width = x2 - x1; + height = y2 - y1; + } + image = image.crop_imm(x1 as u32, y1 as u32, width as u32, height as u32); + } + } + } + (image, error.join("\n")) } -fn decode_hex(s: &str) -> Result, ParseIntError> { - (1..s.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) - .collect() +// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. +fn blend(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { + match blend_mode { + 0 => [ + strict_f32_to_u8(color2[0] as f32 + color[0] as f32), + strict_f32_to_u8(color2[1] as f32 + color[1] as f32), + strict_f32_to_u8(color2[2] as f32 + color[2] as f32), + if color2[3] > color[3] {color[3]} else {color2[3]} + ], + 1 => [ + strict_f32_to_u8(color2[0] as f32 - color[0] as f32), + strict_f32_to_u8(color2[1] as f32 - color[1] as f32), + strict_f32_to_u8(color2[2] as f32 - color[2] as f32), + if color2[3] > color[3] {color[3]} else {color2[3]} + ], + 2 => [ + strict_f32_to_u8((color[0] as f32) * (color2[0] as f32) / 255.0f32), + strict_f32_to_u8((color[1] as f32) * (color2[1] as f32) / 255.0f32), + strict_f32_to_u8((color[2] as f32) * (color2[2] as f32) / 255.0f32), + strict_f32_to_u8((color[3] as f32) * (color2[3] as f32) / 255.0f32) + ], + 3 => { + let mut high = color2[3]; + let mut low = color[3]; + if high < low { + high = color[3]; + low = color2[3]; + } + [ + strict_f32_to_u8(color[0] as f32 + (color2[0] as f32 - color[0] as f32) * color2[3] as f32 / 255.0f32), + strict_f32_to_u8(color[1] as f32 + (color2[1] as f32 - color[1] as f32) * color2[3] as f32 / 255.0f32), + strict_f32_to_u8(color[2] as f32 + (color2[2] as f32 - color[2] as f32) * color2[3] as f32 / 255.0f32), + strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0)) + ] + }, + 6 => { + let mut high = color[3]; + let mut low = color2[3]; + if high < low { + high = color2[3]; + low = color[3]; + } + [ + strict_f32_to_u8(color2[0] as f32 + (color[0] as f32 - color2[0] as f32) * color[3] as f32 / 255.0f32), + strict_f32_to_u8(color2[1] as f32 + (color[1] as f32 - color2[1] as f32) * color[3] as f32 / 255.0f32), + strict_f32_to_u8(color2[2] as f32 + (color[2] as f32 - color2[2] as f32) * color[3] as f32 / 255.0f32), + strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0f32)) + ] + }, + _ => [color[0], color[1], color[2], color[3]], + } } -fn cap(val: u32) -> u8 { - if val > 255 { - return 255; - } else { - return u8::try_from(val).unwrap(); +// caps an f32 into u8 ranges, rounds it to the nearest integer, then truncates to a u8. +fn strict_f32_to_u8(x: f32) -> u8 { + if x < u8::MIN as f32 { + return 0; + } + if x > u8::MAX as f32 { + return u8::MAX; } + x.round().trunc() as u8 } diff --git a/src/lib.rs b/src/lib.rs index c6ce0ed8..2b4cb700 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_op_in_unsafe_fn)] +#![feature(local_key_cell_methods)] #[macro_use] mod byond; From d8a64bc50e691573f2ff8e09de929c94c8b35fef Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 04:34:38 -0500 Subject: [PATCH 06/35] Clean up a bit --- Cargo.toml | 3 ++- src/byond.rs | 24 ++++++++++++++++++++++++ src/iconforge.rs | 30 +++++------------------------- src/lib.rs | 1 - 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3f7ffc7b..c7e4f0fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,7 +146,8 @@ iconforge = [ "tracy_full", "jobs", "once_cell", - "dashmap" + "dashmap", + "hash", ] redis_pubsub = ["flume", "redis", "serde", "serde_json"] redis_reliablequeue = ["flume", "redis", "serde", "serde_json"] diff --git a/src/byond.rs b/src/byond.rs index 385d5251..6b60facb 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -4,8 +4,13 @@ use std::{ ffi::{CStr, CString}, os::raw::{c_char, c_int}, slice, + sync::Once, + fs::OpenOptions, + io::Write, + backtrace::Backtrace, }; +static SET_HOOK: Once = Once::new(); static EMPTY_STRING: c_char = 0; thread_local! { static RETURN_STRING: RefCell = RefCell::new(CString::default()); @@ -50,6 +55,7 @@ macro_rules! byond_fn { pub unsafe extern "C" fn $name( _argc: ::std::os::raw::c_int, _argv: *const *const ::std::os::raw::c_char ) -> *const ::std::os::raw::c_char { + $crate::byond::set_panic_hook(); let closure = || ($body); $crate::byond::byond_return(closure().map(From::from)) } @@ -84,3 +90,21 @@ byond_fn!( Some(env!("CARGO_PKG_VERSION")) } ); + +// Print any panics before exiting. +pub fn set_panic_hook() { + SET_HOOK.call_once(|| std::panic::set_hook(Box::new(|panic_info| { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open("rustg-panic.log") + .unwrap(); + file.write_all(Backtrace::capture().to_string().as_bytes()).expect("Failed to extract error backtrace"); + file.write_all(panic_info.payload().downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| { + panic_info.payload().downcast_ref::().cloned() + }).unwrap().as_bytes()).expect("Failed to extract error payload"); + }))); +} diff --git a/src/iconforge.rs b/src/iconforge.rs index e9de08fb..d66bf4fa 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -4,26 +4,21 @@ use crate::jobs; use crate::hash::string_hash; use crate::error::Error; use std::{ - fs::{File, OpenOptions}, - io::{BufReader, Write}, - cell::RefCell, + fs::File, + io::BufReader, + sync::{Arc, Mutex}, + collections::HashMap, }; use dmi::icon::{Icon, IconState}; use image::{DynamicImage, GenericImage, GenericImageView, Pixel, ImageBuffer}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; //use raster::Image; use serde::{Serialize, Deserialize}; -use std::collections::HashMap; use dashmap::DashMap; -use std::sync::{Arc, Mutex}; use tracy_full::{zone, frame}; use once_cell::sync::Lazy; -use std::backtrace::Backtrace; static ICON_FILES: Lazy>> = Lazy::new(DashMap::new); static ICON_STATES: Lazy> = Lazy::new(DashMap::new); -thread_local! { - static LAST_BACKTRACE: RefCell> = RefCell::new(None); -} const SOUTH: u8 = 2; const NORTH: u8 = 1; @@ -124,21 +119,6 @@ enum Transform { } fn catch_panic(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { - std::panic::set_hook(Box::new(|panic_info| { - LAST_BACKTRACE.set(Option::Some(Backtrace::capture())); - let mut file = OpenOptions::new() - .write(true) - .append(true) - .create(true) - .open("iconforge-error.log") - .unwrap(); - file.write_all(Backtrace::capture().to_string().as_bytes()).expect("Fail backtrace"); - file.write_all(panic_info.payload().downcast_ref::<&'static str>() - .map(|payload| payload.to_string()) - .or_else(|| { - panic_info.payload().downcast_ref::().cloned() - }).unwrap().as_bytes()).expect("Fail payload"); - })); let x = std::panic::catch_unwind(|| { let result = generate_spritesheet(file_path, spritesheet_name, sprites); frame!(); @@ -151,7 +131,7 @@ fn catch_panic(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::r .or_else(|| { err.downcast_ref::().cloned() }); - return Err(Error::IconState(format!("{}\n\nBACKTRACE:\n{}", message.unwrap().to_owned(), LAST_BACKTRACE.take().unwrap_or(Backtrace::capture()).to_string()))) + return Err(Error::IconState(message.unwrap().to_owned())); } x.ok().unwrap() } diff --git a/src/lib.rs b/src/lib.rs index 2b4cb700..c6ce0ed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #![forbid(unsafe_op_in_unsafe_fn)] -#![feature(local_key_cell_methods)] #[macro_use] mod byond; From f48c32e2988891fbbb2b563c123653f1892192b3 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 04:45:30 -0500 Subject: [PATCH 07/35] Re-add 32-bit thing --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c6ce0ed8..891d03dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,3 +50,6 @@ pub mod unzip; pub mod url; #[cfg(feature = "worleynoise")] pub mod worleynoise; + +#[cfg(not(target_pointer_width = "32"))] +compile_error!("rust-g must be compiled for a 32-bit target"); From e585301d3e922c1e2feba0f8da0094880d93180c Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 04:55:26 -0500 Subject: [PATCH 08/35] Fix TOML sorting --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c7e4f0fd..e2d8ecee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ all = [ "file", "git", "http", + "iconforge", "json", "log", "noise", @@ -135,7 +136,6 @@ hash = [ "serde", "serde_json", ] -pathfinder = ["num-integer", "pathfinding", "serde", "serde_json"] iconforge = [ "serde", "serde_json", @@ -149,6 +149,7 @@ iconforge = [ "dashmap", "hash", ] +pathfinder = ["num-integer", "pathfinding", "serde", "serde_json"] redis_pubsub = ["flume", "redis", "serde", "serde_json"] redis_reliablequeue = ["flume", "redis", "serde", "serde_json"] unzip = ["zip", "jobs"] From b1c1761b15ad91d6906274859583aced55e51f8c Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 05:06:41 -0500 Subject: [PATCH 09/35] Add dmsrc --- dmsrc/iconforge.dm | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 dmsrc/iconforge.dm diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm new file mode 100644 index 00000000..67d1dc88 --- /dev/null +++ b/dmsrc/iconforge.dm @@ -0,0 +1,3 @@ +#define rustg_iconforge_generate(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites) +#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites) +#define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") From 65f96f1febf06c1b3b8d4d78c5fde811ef690ad5 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 05:09:36 -0500 Subject: [PATCH 10/35] Fix clippy suggestions --- src/iconforge.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index d66bf4fa..e39c4b90 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -175,7 +175,7 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) let size_id = format!("{}x{}", image.width(), image.height()); return_image(image, icon); let mut size_map = size_to_icon_objects.lock().unwrap(); - let vec = (*size_map).entry(size_id.to_owned()).or_insert(Vec::new()); + let vec = (*size_map).entry(size_id.to_owned()).or_default(); vec.push(icon); sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { @@ -363,7 +363,9 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri hex = format!("{}ff", hex); } let mut color2: [u8; 4] = [0, 0, 0, 255]; - hex::decode_to_slice(hex, &mut color2).expect(&format!("Decoding hex color {} failed", color)); + if let Err(err) = hex::decode_to_slice(hex, &mut color2) { + error.push(format!("Decoding hex color {} failed: {}", color, err.to_string())); + } for x in 0..image.width() { for y in 0..image.height() { let px = image.get_pixel(x, y); From bc55eb0e04daa3cea025eafdf35618813051c2ff Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 05:21:23 -0500 Subject: [PATCH 11/35] Clippy.. stop being mean --- src/iconforge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index e39c4b90..c2025069 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -364,7 +364,7 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri } let mut color2: [u8; 4] = [0, 0, 0, 255]; if let Err(err) = hex::decode_to_slice(hex, &mut color2) { - error.push(format!("Decoding hex color {} failed: {}", color, err.to_string())); + error.push(format!("Decoding hex color {} failed: {}", color, err)); } for x in 0..image.width() { for y in 0..image.height() { From bc1a389adf6782214614a63dfa9ecb74c6d6b7a8 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 18:45:15 -0500 Subject: [PATCH 12/35] Cargo fmt + doc comments --- src/byond.rs | 42 +++--- src/iconforge.rs | 337 +++++++++++++++++++++++++++++------------------ 2 files changed, 231 insertions(+), 148 deletions(-) diff --git a/src/byond.rs b/src/byond.rs index 6b60facb..d25507cb 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -1,13 +1,13 @@ use std::{ + backtrace::Backtrace, borrow::Cow, cell::RefCell, ffi::{CStr, CString}, + fs::OpenOptions, + io::Write, os::raw::{c_char, c_int}, slice, sync::Once, - fs::OpenOptions, - io::Write, - backtrace::Backtrace, }; static SET_HOOK: Once = Once::new(); @@ -93,18 +93,26 @@ byond_fn!( // Print any panics before exiting. pub fn set_panic_hook() { - SET_HOOK.call_once(|| std::panic::set_hook(Box::new(|panic_info| { - let mut file = OpenOptions::new() - .write(true) - .append(true) - .create(true) - .open("rustg-panic.log") - .unwrap(); - file.write_all(Backtrace::capture().to_string().as_bytes()).expect("Failed to extract error backtrace"); - file.write_all(panic_info.payload().downcast_ref::<&'static str>() - .map(|payload| payload.to_string()) - .or_else(|| { - panic_info.payload().downcast_ref::().cloned() - }).unwrap().as_bytes()).expect("Failed to extract error payload"); - }))); + SET_HOOK.call_once(|| { + std::panic::set_hook(Box::new(|panic_info| { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open("rustg-panic.log") + .unwrap(); + file.write_all(Backtrace::capture().to_string().as_bytes()) + .expect("Failed to extract error backtrace"); + file.write_all( + panic_info + .payload() + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()) + .unwrap() + .as_bytes(), + ) + .expect("Failed to extract error payload"); + })) + }); } diff --git a/src/iconforge.rs b/src/iconforge.rs index c2025069..f7c7b93f 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1,22 +1,21 @@ // DMI spritesheet generator // Developed by itsmeow -use crate::jobs; -use crate::hash::string_hash; use crate::error::Error; +use crate::hash::string_hash; +use crate::jobs; +use dashmap::DashMap; +use dmi::icon::{Icon, IconState}; +use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Pixel}; +use once_cell::sync::Lazy; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, fs::File, io::BufReader, sync::{Arc, Mutex}, - collections::HashMap, }; -use dmi::icon::{Icon, IconState}; -use image::{DynamicImage, GenericImage, GenericImageView, Pixel, ImageBuffer}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -//use raster::Image; -use serde::{Serialize, Deserialize}; -use dashmap::DashMap; -use tracy_full::{zone, frame}; -use once_cell::sync::Lazy; +use tracy_full::{frame, zone}; static ICON_FILES: Lazy>> = Lazy::new(DashMap::new); static ICON_STATES: Lazy> = Lazy::new(DashMap::new); @@ -29,13 +28,15 @@ const SOUTHEAST: u8 = SOUTH | EAST; // 6 const SOUTHWEST: u8 = SOUTH | WEST; // 10 const NORTHEAST: u8 = NORTH | EAST; // 5 const NORTHWEST: u8 = NORTH | WEST; // 9 -// This is ordered by how DMIs internally place dirs into the PNG -const EIGHT_DIRS: [u8; 8] = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST]; -// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = EIGHT_DIRS.indexof(DIR) -// 255 is invalid. -const DIR_TO_INDEX: [u8; 11] = [255, 1, 0, 255, 2, 6, 4, 255, 3, 7, 5]; +/// This is ordered by how DMIs internally place dirs into the PNG +const EIGHT_DIRS: [u8; 8] = [ + SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST, +]; +/// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = EIGHT_DIRS.indexof(DIR) +/// 255 is invalid. +const DIR_TO_INDEX: [u8; 11] = [255, 1, 0, 255, 2, 6, 4, 255, 3, 7, 5]; byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { let file_path = file_path.to_owned(); @@ -47,7 +48,6 @@ byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { }) }); - byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites) { // Take ownership before passing let file_path = file_path.to_owned(); @@ -80,12 +80,12 @@ struct SpritesheetEntry { #[derive(Serialize, Deserialize, Clone)] struct IconObject { - icon_file: String, - icon_state: String, - dir: u8, - frame: u32, - moving: u8, - transform: Vec + icon_file: String, + icon_state: String, + dir: u8, + frame: u32, + moving: u8, + transform: Vec, } impl IconObject { @@ -98,27 +98,17 @@ impl IconObject { #[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] enum Transform { - BlendColor { - color: String, - blend_mode: u8, - }, - BlendIcon { - icon: IconObject, - blend_mode: u8, - }, - Scale { - width: u32, - height: u32, - }, - Crop { - x1: i32, - y1: i32, - x2: i32, - y2: i32, - } + BlendColor { color: String, blend_mode: u8 }, + BlendIcon { icon: IconObject, blend_mode: u8 }, + Scale { width: u32, height: u32 }, + Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, } -fn catch_panic(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { +fn catch_panic( + file_path: &str, + spritesheet_name: &str, + sprites: &str, +) -> std::result::Result { let x = std::panic::catch_unwind(|| { let result = generate_spritesheet(file_path, spritesheet_name, sprites); frame!(); @@ -128,22 +118,27 @@ fn catch_panic(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::r let message: Option = err .downcast_ref::<&'static str>() .map(|payload| payload.to_string()) - .or_else(|| { - err.downcast_ref::().cloned() - }); + .or_else(|| err.downcast_ref::().cloned()); return Err(Error::IconState(message.unwrap().to_owned())); } x.ok().unwrap() } -fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) -> std::result::Result { +fn generate_spritesheet( + file_path: &str, + spritesheet_name: &str, + sprites: &str, +) -> std::result::Result { zone!("generate_spritesheet"); let error: Arc>> = Arc::new(Mutex::new(Vec::new())); - let size_to_icon_objects: Arc>>> = Arc::new(Mutex::new(HashMap::new())); - let sprites_map: HashMap = serde_json::from_str::>(sprites)?; - let sprites_objects: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let size_to_icon_objects: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); + let sprites_map: HashMap = + serde_json::from_str::>(sprites)?; + let sprites_objects: Arc>> = + Arc::new(Mutex::new(HashMap::new())); // Pre-load all the DMIs now. sprites_map.par_iter().for_each(|sprite_entry| { @@ -178,49 +173,75 @@ fn generate_spritesheet(file_path: &str, spritesheet_name: &str, sprites: &str) let vec = (*size_map).entry(size_id.to_owned()).or_default(); vec.push(icon); - sprites_objects.lock().unwrap().insert(sprite_name.to_owned(), SpritesheetEntry { - size_id: size_id.to_owned(), - position: u32::try_from(vec.len()).unwrap() - 1 - }); + sprites_objects.lock().unwrap().insert( + sprite_name.to_owned(), + SpritesheetEntry { + size_id: size_id.to_owned(), + position: u32::try_from(vec.len()).unwrap() - 1, + }, + ); } }); // all images have been returned now, so continue... // Get all the sprites and spew them onto a spritesheet. - size_to_icon_objects.lock().unwrap().par_iter().for_each(|(size_id, icon_objects)| { - zone!("join_sprites"); - let file_path = format!("{}{}_{}.png", file_path, spritesheet_name, size_id); - let size_data: Vec<&str> = size_id.split('x').collect(); - let base_width = size_data.first().unwrap().to_string().parse::().unwrap(); - let base_height = size_data.last().unwrap().to_string().parse::().unwrap(); - - let image_count: u32 = u32::try_from(icon_objects.len()).unwrap(); - let mut final_image = DynamicImage::new_rgba8(base_width * image_count, base_height); - - for idx in 0..image_count { - zone!("join_sprite"); - let icon = icon_objects.get::(usize::try_from(idx).unwrap()).unwrap(); - let image_result = icon_to_image(icon, &"N/A, in final generation stage".to_string()); - if let Err(err) = image_result { - error.lock().unwrap().push(err); - continue; - } - let image = image_result.unwrap(); - let base_x: u32 = base_width * idx; - for x in 0..image.width() { - for y in 0..image.height() { - final_image.put_pixel(base_x + x, y, image.get_pixel(x, y)) + size_to_icon_objects + .lock() + .unwrap() + .par_iter() + .for_each(|(size_id, icon_objects)| { + zone!("join_sprites"); + let file_path = format!("{}{}_{}.png", file_path, spritesheet_name, size_id); + let size_data: Vec<&str> = size_id.split('x').collect(); + let base_width = size_data + .first() + .unwrap() + .to_string() + .parse::() + .unwrap(); + let base_height = size_data + .last() + .unwrap() + .to_string() + .parse::() + .unwrap(); + + let image_count: u32 = u32::try_from(icon_objects.len()).unwrap(); + let mut final_image = DynamicImage::new_rgba8(base_width * image_count, base_height); + + for idx in 0..image_count { + zone!("join_sprite"); + let icon = icon_objects + .get::(usize::try_from(idx).unwrap()) + .unwrap(); + let image_result = + icon_to_image(icon, &"N/A, in final generation stage".to_string()); + if let Err(err) = image_result { + error.lock().unwrap().push(err); + continue; + } + let image = image_result.unwrap(); + let base_x: u32 = base_width * idx; + for x in 0..image.width() { + for y in 0..image.height() { + final_image.put_pixel(base_x + x, y, image.get_pixel(x, y)) + } } } - } - { - zone!("write_spritesheet"); - final_image.save(file_path).err(); - } - }); + { + zone!("write_spritesheet"); + final_image.save(file_path).err(); + } + }); - let sizes: Vec = size_to_icon_objects.lock().unwrap().iter().map(|(k, _v)| k).cloned().collect(); + let sizes: Vec = size_to_icon_objects + .lock() + .unwrap() + .iter() + .map(|(k, _v)| k) + .cloned() + .collect(); // Collect the game metadata and any errors. let returned = Returned { @@ -237,7 +258,7 @@ fn icon_to_icons(icon: &IconObject) -> Vec<&IconObject> { let mut icons: Vec<&IconObject> = Vec::new(); icons.push(icon); for transform in &icon.transform { - if let Transform::BlendIcon { icon, .. } = transform { + if let Transform::BlendIcon { icon, .. } = transform { let nested = icon_to_icons(icon); for icon in nested { icons.push(icon) @@ -261,7 +282,7 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { } let icon_file = File::open(icon_path); if icon_file.is_err() { - return Err(format!("No such DMI file: {}", icon_path)) + return Err(format!("No such DMI file: {}", icon_path)); } let reader = BufReader::new(icon_file.unwrap()); let dmi: Option; @@ -283,8 +304,8 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { } } -// Takes an IconObject, gets its DMI, then picks out a DynamicImage for the IconState, as well as transforms the DynamicImage. -// Gives ownership over the image. Please return when you are done <3 +/// Takes an IconObject, gets its DMI, then picks out a DynamicImage for the IconState, as well as transforms the DynamicImage. +/// Gives ownership over the image. Please return when you are done <3 fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result { zone!("icon_to_image"); { @@ -292,7 +313,7 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result Result 1 { @@ -340,14 +373,18 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result (DynamicImage, String) { +/// Applies transforms to a DynamicImage. +fn transform_image( + image_in: DynamicImage, + icon: &IconObject, + sprite_name: &String, +) -> (DynamicImage, String) { zone!("transform_image"); let mut image = image_in; let mut error: Vec = Vec::new(); @@ -372,15 +409,14 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri let pixel = px.channels(); let blended = blend(pixel, &color2, *blend_mode); - image.put_pixel(x, y, - image::Rgba::(blended), - ); + image.put_pixel(x, y, image::Rgba::(blended)); } } - }, + } Transform::BlendIcon { icon, blend_mode } => { zone!("blend_icon"); - let image_result = icon_to_image(icon, &format!("Transform blend_icon of {}", sprite_name)); + let image_result = + icon_to_image(icon, &format!("Transform blend_icon of {}", sprite_name)); if let Err(err) = image_result { error.push(err); continue; @@ -403,14 +439,11 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri let blended = blend(pixel_1, pixel_2, *blend_mode); - image.put_pixel(x, y, - image::Rgba::(blended), - ); + image.put_pixel(x, y, image::Rgba::(blended)); } } return_image(other_image, icon); - - }, + } Transform::Scale { width, height } => { zone!("scale"); let x_ratio = image.width() as f32 / *width as f32; @@ -418,8 +451,8 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri let mut new_image = DynamicImage::new_rgba8(*width, *height); for x in 0..*width { for y in 0..*height { - let old_x: u32 = ( x as f32 * x_ratio ).floor() as u32; - let old_y: u32 = ( y as f32 * y_ratio ).floor() as u32; + let old_x: u32 = (x as f32 * x_ratio).floor() as u32; + let old_y: u32 = (y as f32 * y_ratio).floor() as u32; let pixel = image.get_pixel(old_x, old_y); new_image.put_pixel(x, y, pixel); } @@ -435,7 +468,10 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri let mut x2 = *x2; let mut y2 = *y2; if x2 <= x1 || y2 <= y1 { - error.push(format!("Invalid bounds {} {} to {} {} from sprite {}", x1, y1, x2, y2, sprite_name)); + error.push(format!( + "Invalid bounds {} {} to {} {} from sprite {}", + x1, y1, x2, y2, sprite_name + )); continue; } @@ -449,12 +485,25 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { //continue; - let mut blank_img = ImageBuffer::from_fn(width as u32, height as u32, |_x, _y| image::Rgba([0, 0, 0, 0])); + let mut blank_img = + ImageBuffer::from_fn(width as u32, height as u32, |_x, _y| { + image::Rgba([0, 0, 0, 0]) + }); image::imageops::overlay( - &mut blank_img, - &image, - if x1 < 0 { (x1).abs() as i64 } else { 0 } - if x1 > i_width as i32 { (x1 - i_width as i32) as i64 } else { 0 }, - if y1 < 0 { (y1).abs() as i64 } else { 0 } - if x1 > i_width as i32 { (x1 - i_width as i32) as i64} else { 0 }, + &mut blank_img, + &image, + if x1 < 0 { (x1).abs() as i64 } else { 0 } + - if x1 > i_width as i32 { + (x1 - i_width as i32) as i64 + } else { + 0 + }, + if y1 < 0 { (y1).abs() as i64 } else { 0 } + - if x1 > i_width as i32 { + (x1 - i_width as i32) as i64 + } else { + 0 + }, ); image = DynamicImage::new_rgba8(width as u32, height as u32); let error_i = image.copy_from(&blank_img, 0, 0); @@ -486,26 +535,34 @@ fn transform_image(image_in: DynamicImage, icon: &IconObject, sprite_name: &Stri (image, error.join("\n")) } -// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. +/// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. fn blend(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { match blend_mode { 0 => [ strict_f32_to_u8(color2[0] as f32 + color[0] as f32), strict_f32_to_u8(color2[1] as f32 + color[1] as f32), strict_f32_to_u8(color2[2] as f32 + color[2] as f32), - if color2[3] > color[3] {color[3]} else {color2[3]} + if color2[3] > color[3] { + color[3] + } else { + color2[3] + }, ], 1 => [ strict_f32_to_u8(color2[0] as f32 - color[0] as f32), strict_f32_to_u8(color2[1] as f32 - color[1] as f32), strict_f32_to_u8(color2[2] as f32 - color[2] as f32), - if color2[3] > color[3] {color[3]} else {color2[3]} + if color2[3] > color[3] { + color[3] + } else { + color2[3] + }, ], 2 => [ strict_f32_to_u8((color[0] as f32) * (color2[0] as f32) / 255.0f32), strict_f32_to_u8((color[1] as f32) * (color2[1] as f32) / 255.0f32), strict_f32_to_u8((color[2] as f32) * (color2[2] as f32) / 255.0f32), - strict_f32_to_u8((color[3] as f32) * (color2[3] as f32) / 255.0f32) + strict_f32_to_u8((color[3] as f32) * (color2[3] as f32) / 255.0f32), ], 3 => { let mut high = color2[3]; @@ -515,12 +572,21 @@ fn blend(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { low = color2[3]; } [ - strict_f32_to_u8(color[0] as f32 + (color2[0] as f32 - color[0] as f32) * color2[3] as f32 / 255.0f32), - strict_f32_to_u8(color[1] as f32 + (color2[1] as f32 - color[1] as f32) * color2[3] as f32 / 255.0f32), - strict_f32_to_u8(color[2] as f32 + (color2[2] as f32 - color[2] as f32) * color2[3] as f32 / 255.0f32), - strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0)) + strict_f32_to_u8( + color[0] as f32 + + (color2[0] as f32 - color[0] as f32) * color2[3] as f32 / 255.0f32, + ), + strict_f32_to_u8( + color[1] as f32 + + (color2[1] as f32 - color[1] as f32) * color2[3] as f32 / 255.0f32, + ), + strict_f32_to_u8( + color[2] as f32 + + (color2[2] as f32 - color[2] as f32) * color2[3] as f32 / 255.0f32, + ), + strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0)), ] - }, + } 6 => { let mut high = color[3]; let mut low = color2[3]; @@ -529,17 +595,26 @@ fn blend(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { low = color[3]; } [ - strict_f32_to_u8(color2[0] as f32 + (color[0] as f32 - color2[0] as f32) * color[3] as f32 / 255.0f32), - strict_f32_to_u8(color2[1] as f32 + (color[1] as f32 - color2[1] as f32) * color[3] as f32 / 255.0f32), - strict_f32_to_u8(color2[2] as f32 + (color[2] as f32 - color2[2] as f32) * color[3] as f32 / 255.0f32), - strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0f32)) + strict_f32_to_u8( + color2[0] as f32 + + (color[0] as f32 - color2[0] as f32) * color[3] as f32 / 255.0f32, + ), + strict_f32_to_u8( + color2[1] as f32 + + (color[1] as f32 - color2[1] as f32) * color[3] as f32 / 255.0f32, + ), + strict_f32_to_u8( + color2[2] as f32 + + (color[2] as f32 - color2[2] as f32) * color[3] as f32 / 255.0f32, + ), + strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0f32)), ] - }, + } _ => [color[0], color[1], color[2], color[3]], } } -// caps an f32 into u8 ranges, rounds it to the nearest integer, then truncates to a u8. +/// caps an f32 into u8 ranges, rounds it to the nearest integer, then truncates to a u8. fn strict_f32_to_u8(x: f32) -> u8 { if x < u8::MIN as f32 { return 0; From 9ace09a196196081d32a1a5ae19a247a04eb1561 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 19:25:23 -0500 Subject: [PATCH 13/35] Code cleanup --- src/iconforge.rs | 206 +++++++++++++++++++++++------------------------ 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index f7c7b93f..6dd2e94f 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -131,14 +131,11 @@ fn generate_spritesheet( ) -> std::result::Result { zone!("generate_spritesheet"); - let error: Arc>> = Arc::new(Mutex::new(Vec::new())); + let error = Arc::new(Mutex::new(Vec::::new())); - let size_to_icon_objects: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); - let sprites_map: HashMap = - serde_json::from_str::>(sprites)?; - let sprites_objects: Arc>> = - Arc::new(Mutex::new(HashMap::new())); + let size_to_icon_objects = Arc::new(Mutex::new(HashMap::>::new())); + let sprites_map = serde_json::from_str::>(sprites)?; + let sprites_objects = Arc::new(Mutex::new(HashMap::::new())); // Pre-load all the DMIs now. sprites_map.par_iter().for_each(|sprite_entry| { @@ -177,7 +174,7 @@ fn generate_spritesheet( sprite_name.to_owned(), SpritesheetEntry { size_id: size_id.to_owned(), - position: u32::try_from(vec.len()).unwrap() - 1, + position: vec.len() as u32 - 1, }, ); } @@ -207,14 +204,12 @@ fn generate_spritesheet( .parse::() .unwrap(); - let image_count: u32 = u32::try_from(icon_objects.len()).unwrap(); + let image_count = icon_objects.len() as u32; let mut final_image = DynamicImage::new_rgba8(base_width * image_count, base_height); for idx in 0..image_count { zone!("join_sprite"); - let icon = icon_objects - .get::(usize::try_from(idx).unwrap()) - .unwrap(); + let icon = icon_objects.get::(idx as usize).unwrap(); let image_result = icon_to_image(icon, &"N/A, in final generation stage".to_string()); if let Err(err) = image_result { @@ -407,7 +402,7 @@ fn transform_image( for y in 0..image.height() { let px = image.get_pixel(x, y); let pixel = px.channels(); - let blended = blend(pixel, &color2, *blend_mode); + let blended = blend_u8(pixel, &color2, *blend_mode); image.put_pixel(x, y, image::Rgba::(blended)); } @@ -437,7 +432,7 @@ fn transform_image( let pixel_1 = px1.channels(); let pixel_2 = px2.channels(); - let blended = blend(pixel_1, pixel_2, *blend_mode); + let blended = blend_u8(pixel_1, pixel_2, *blend_mode); image.put_pixel(x, y, image::Rgba::(blended)); } @@ -484,7 +479,6 @@ fn transform_image( let mut height = y2 - y1; if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { - //continue; let mut blank_img = ImageBuffer::from_fn(width as u32, height as u32, |_x, _y| { image::Rgba([0, 0, 0, 0]) @@ -506,25 +500,14 @@ fn transform_image( }, ); image = DynamicImage::new_rgba8(width as u32, height as u32); - let error_i = image.copy_from(&blank_img, 0, 0); - if let Err(err) = error_i { + if let Err(err) = image.copy_from(&blank_img, 0, 0) { error.push(err.to_string()); continue; } - assert_eq!(image.width(), width as u32); - assert_eq!(image.height(), height as u32); - if x1 < 0 { - x1 = 0; - } - if x2 > i_width as i32 { - x2 = i_width as i32; - } - if y1 < 0 { - y1 = 0; - } - if y2 > i_height as i32 { - y2 = i_height as i32; - } + x1 = std::cmp::max(0, x1); + x2 = std::cmp::min(i_width as i32, x2); + y1 = std::cmp::max(0, y1); + y2 = std::cmp::min(i_height as i32, y2); width = x2 - x1; height = y2 - y1; } @@ -535,82 +518,99 @@ fn transform_image( (image, error.join("\n")) } +struct Rgba { + r: f32, + g: f32, + b: f32, + a: f32, +} + +impl Rgba { + fn into_array(self) -> [u8; 4] { + [ + strict_f32_to_u8(self.r), + strict_f32_to_u8(self.g), + strict_f32_to_u8(self.b), + strict_f32_to_u8(self.a), + ] + } + + fn from_array(rgba: &[u8]) -> Rgba { + Rgba { + r: rgba[0] as f32, + g: rgba[1] as f32, + b: rgba[2] as f32, + a: rgba[3] as f32, + } + } + + fn map_each( + color: Rgba, + color2: Rgba, + rgb_fn: &dyn Fn(f32, f32) -> f32, + a_fn: &dyn Fn(f32, f32) -> f32, + ) -> Rgba { + Rgba { + r: rgb_fn(color.r, color2.r), + g: rgb_fn(color.g, color2.g), + b: rgb_fn(color.b, color2.b), + a: a_fn(color.a, color2.a), + } + } + + fn map_each_a( + color: Rgba, + color2: Rgba, + rgb_fn: &dyn Fn(f32, f32, f32, f32) -> f32, + a_fn: &dyn Fn(f32, f32) -> f32, + ) -> Rgba { + Rgba { + r: rgb_fn(color.r, color2.r, color.a, color2.a), + g: rgb_fn(color.g, color2.g, color.a, color2.a), + b: rgb_fn(color.b, color2.b, color.a, color2.a), + a: a_fn(color.a, color2.a), + } + } +} + +fn blend_u8(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { + blend( + Rgba::from_array(color), + Rgba::from_array(color2), + blend_mode, + ) + .into_array() +} + /// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. -fn blend(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { +fn blend(color: Rgba, color2: Rgba, blend_mode: u8) -> Rgba { match blend_mode { - 0 => [ - strict_f32_to_u8(color2[0] as f32 + color[0] as f32), - strict_f32_to_u8(color2[1] as f32 + color[1] as f32), - strict_f32_to_u8(color2[2] as f32 + color[2] as f32), - if color2[3] > color[3] { - color[3] - } else { - color2[3] + 0 => Rgba::map_each(color, color2, &|c1, c2| c1 + c2, &f32::min), + 1 => Rgba::map_each(color, color2, &|c1, c2| c2 - c1, &f32::min), + 2 => Rgba::map_each(color, color2, &|c1, c2| c1 * c2 / 255.0, &|a1, a2| { + a1 * a2 / 255.0 + }), + 3 => Rgba::map_each_a( + color, + color2, + &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a, + &|a1, a2| { + let high = f32::max(a1, a2); + let low = f32::min(a1, a2); + high + (high * low / 255.0) }, - ], - 1 => [ - strict_f32_to_u8(color2[0] as f32 - color[0] as f32), - strict_f32_to_u8(color2[1] as f32 - color[1] as f32), - strict_f32_to_u8(color2[2] as f32 - color[2] as f32), - if color2[3] > color[3] { - color[3] - } else { - color2[3] + ), + 6 => Rgba::map_each_a( + color2, + color, + &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a, + &|a1, a2| { + let high = f32::max(a1, a2); + let low = f32::min(a1, a2); + high + (high * low / 255.0) }, - ], - 2 => [ - strict_f32_to_u8((color[0] as f32) * (color2[0] as f32) / 255.0f32), - strict_f32_to_u8((color[1] as f32) * (color2[1] as f32) / 255.0f32), - strict_f32_to_u8((color[2] as f32) * (color2[2] as f32) / 255.0f32), - strict_f32_to_u8((color[3] as f32) * (color2[3] as f32) / 255.0f32), - ], - 3 => { - let mut high = color2[3]; - let mut low = color[3]; - if high < low { - high = color[3]; - low = color2[3]; - } - [ - strict_f32_to_u8( - color[0] as f32 - + (color2[0] as f32 - color[0] as f32) * color2[3] as f32 / 255.0f32, - ), - strict_f32_to_u8( - color[1] as f32 - + (color2[1] as f32 - color[1] as f32) * color2[3] as f32 / 255.0f32, - ), - strict_f32_to_u8( - color[2] as f32 - + (color2[2] as f32 - color[2] as f32) * color2[3] as f32 / 255.0f32, - ), - strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0)), - ] - } - 6 => { - let mut high = color[3]; - let mut low = color2[3]; - if high < low { - high = color2[3]; - low = color[3]; - } - [ - strict_f32_to_u8( - color2[0] as f32 - + (color[0] as f32 - color2[0] as f32) * color[3] as f32 / 255.0f32, - ), - strict_f32_to_u8( - color2[1] as f32 - + (color[1] as f32 - color2[1] as f32) * color[3] as f32 / 255.0f32, - ), - strict_f32_to_u8( - color2[2] as f32 - + (color[2] as f32 - color2[2] as f32) * color[3] as f32 / 255.0f32, - ), - strict_f32_to_u8(high as f32 + (high as f32 * low as f32 / 255.0f32)), - ] - } - _ => [color[0], color[1], color[2], color[3]], + ), + _ => color, } } From 156a42e0766a53ba102e99549aeeae9e79203b88 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 20:25:23 -0500 Subject: [PATCH 14/35] More cleanup, remove most unsafe unwrap()s, use Match syntax. --- src/error.rs | 4 +- src/iconforge.rs | 243 ++++++++++++++++++++++++++++------------------- 2 files changed, 147 insertions(+), 100 deletions(-) diff --git a/src/error.rs b/src/error.rs index 90cf4b5a..6f8cd736 100644 --- a/src/error.rs +++ b/src/error.rs @@ -62,8 +62,8 @@ pub enum Error { #[error("Unable to decode hex value.")] HexDecode, #[cfg(feature = "iconforge")] - #[error("IconState error: {0}")] - IconState(String), + #[error("IconForge error: {0}")] + IconForge(String), } impl From for Error { diff --git a/src/iconforge.rs b/src/iconforge.rs index 6dd2e94f..7da27372 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -42,7 +42,7 @@ byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { let file_path = file_path.to_owned(); let spritesheet_name = spritesheet_name.to_owned(); let sprites = sprites.to_owned(); - Some(match catch_panic(&file_path, &spritesheet_name, &sprites) { + Some(match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites) { Ok(o) => o.to_string(), Err(e) => e.to_string() }) @@ -54,7 +54,7 @@ byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites) { let spritesheet_name = spritesheet_name.to_owned(); let sprites = sprites.to_owned(); Some(jobs::start(move || { - match catch_panic(&file_path, &spritesheet_name, &sprites) { + match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites) { Ok(o) => o.to_string(), Err(e) => e.to_string() } @@ -66,7 +66,7 @@ byond_fn!(fn iconforge_check(id) { }); #[derive(Serialize)] -struct Returned { +struct SpritesheetResult { sizes: Vec, sprites: HashMap, error: String, @@ -91,7 +91,7 @@ struct IconObject { impl IconObject { fn to_icostring(&self) -> Result { zone!("to_icostring"); - string_hash("xxh64", &serde_json::to_string(self).unwrap()) + string_hash("xxh64", &serde_json::to_string(self)?) } } @@ -104,24 +104,29 @@ enum Transform { Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, } -fn catch_panic( +fn generate_spritesheet_safe( file_path: &str, spritesheet_name: &str, sprites: &str, ) -> std::result::Result { - let x = std::panic::catch_unwind(|| { + match std::panic::catch_unwind(|| { let result = generate_spritesheet(file_path, spritesheet_name, sprites); frame!(); result - }); - if let Err(err) = x { - let message: Option = err - .downcast_ref::<&'static str>() - .map(|payload| payload.to_string()) - .or_else(|| err.downcast_ref::().cloned()); - return Err(Error::IconState(message.unwrap().to_owned())); + }) { + Ok(o) => o, + Err(e) => { + let message: Option = e + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| e.downcast_ref::().cloned()); + Err(Error::IconForge( + message + .unwrap_or("Failed to stringify panic! Check rustg-panic.log".to_string()) + .to_owned(), + )) + } } - x.ok().unwrap() } fn generate_spritesheet( @@ -138,6 +143,7 @@ fn generate_spritesheet( let sprites_objects = Arc::new(Mutex::new(HashMap::::new())); // Pre-load all the DMIs now. + // This is much faster than doing it as we go (tested!), because sometimes multiple parallel iterators need the DMI. sprites_map.par_iter().for_each(|sprite_entry| { zone!("sprite_to_icons"); let (_, icon) = sprite_entry; @@ -148,40 +154,55 @@ fn generate_spritesheet( }); }); - // Pick the specific icon states out of the DMI + // Pick the specific icon states out of the DMI, also generating their transforms, build the spritesheet metadata. sprites_map.par_iter().for_each(|sprite_entry| { zone!("map_sprite"); let (sprite_name, icon) = sprite_entry; // get DynamicImage, applying transforms as well - let image_result = icon_to_image(icon, sprite_name); - if let Err(err) = image_result { - error.lock().unwrap().push(err); - return; - } - let image = image_result.unwrap(); + let image = match icon_to_image(icon, sprite_name) { + Ok(image) => image, + Err(err) => { + error.lock().unwrap().push(err); + return; + } + }; { zone!("create_game_metadata"); // Generate the metadata used by the game let size_id = format!("{}x{}", image.width(), image.height()); - return_image(image, icon); - let mut size_map = size_to_icon_objects.lock().unwrap(); - let vec = (*size_map).entry(size_id.to_owned()).or_default(); - vec.push(icon); - - sprites_objects.lock().unwrap().insert( - sprite_name.to_owned(), - SpritesheetEntry { - size_id: size_id.to_owned(), - position: vec.len() as u32 - 1, - }, - ); + if let Err(err) = return_image(image, icon) { + error.lock().unwrap().push(err.to_string()); + } + let icon_position; + { + zone!("insert_into_size_map"); + // This scope releases the lock on size_to_icon_objects + let mut size_map = size_to_icon_objects.lock().unwrap(); + let vec = (*size_map).entry(size_id.to_owned()).or_default(); + icon_position = vec.len() as u32; + vec.push(icon); + } + + { + zone!("insert_into_sprite_objects"); + sprites_objects.lock().unwrap().insert( + sprite_name.to_owned(), + SpritesheetEntry { + size_id: size_id.to_owned(), + position: icon_position, + }, + ); + } } }); // all images have been returned now, so continue... + // cache this here so we don't generate the same string 5000 times + let sprite_name = "N/A, in final generation stage".to_string(); + // Get all the sprites and spew them onto a spritesheet. size_to_icon_objects .lock() @@ -204,20 +225,19 @@ fn generate_spritesheet( .parse::() .unwrap(); - let image_count = icon_objects.len() as u32; - let mut final_image = DynamicImage::new_rgba8(base_width * image_count, base_height); + let mut final_image = + DynamicImage::new_rgba8(base_width * icon_objects.len() as u32, base_height); - for idx in 0..image_count { + for (idx, icon) in icon_objects.iter().enumerate() { zone!("join_sprite"); - let icon = icon_objects.get::(idx as usize).unwrap(); - let image_result = - icon_to_image(icon, &"N/A, in final generation stage".to_string()); - if let Err(err) = image_result { - error.lock().unwrap().push(err); - continue; - } - let image = image_result.unwrap(); - let base_x: u32 = base_width * idx; + let image = match icon_to_image(icon, &sprite_name) { + Ok(image) => image, + Err(err) => { + error.lock().unwrap().push(err); + return; + } + }; + let base_x: u32 = base_width * idx as u32; for x in 0..image.width() { for y in 0..image.height() { final_image.put_pixel(base_x + x, y, image.get_pixel(x, y)) @@ -239,12 +259,12 @@ fn generate_spritesheet( .collect(); // Collect the game metadata and any errors. - let returned = Returned { + let returned = SpritesheetResult { sizes, sprites: sprites_objects.lock().unwrap().to_owned(), error: error.lock().unwrap().join("\n"), }; - Ok(serde_json::to_string::(&returned).unwrap()) + Ok(serde_json::to_string::(&returned)?) } /// Takes in an icon and gives a list of nested icons. Also returns a reference to the provided icon in the list. @@ -266,69 +286,76 @@ fn icon_to_icons(icon: &IconObject) -> Vec<&IconObject> { /// Given an IconObject, returns a DMI Icon structure and caches it. fn icon_to_dmi(icon: &IconObject) -> Result, String> { zone!("icon_to_dmi"); - let icon_path: &String = &icon.icon_file; + let icon_path = &icon.icon_file; { zone!("check_dmi_exists"); - // scope-in so the lock does not persist during DMI read - let found_icon = ICON_FILES.get(icon_path); - if let Some(found) = found_icon { + if let Some(found) = ICON_FILES.get(icon_path) { return Ok(found.clone()); } } - let icon_file = File::open(icon_path); - if icon_file.is_err() { - return Err(format!("No such DMI file: {}", icon_path)); - } - let reader = BufReader::new(icon_file.unwrap()); - let dmi: Option; + let icon_file = match File::open(icon_path) { + Ok(icon_file) => icon_file, + Err(_) => { + return Err(format!("No such DMI file: {}", icon_path)); + } + }; + let reader = BufReader::new(icon_file); + let dmi: Icon; { zone!("parse_dmi"); - dmi = Icon::load(reader).ok(); - } - if dmi.is_none() { - return Err(format!("Invalid DMI: {}", icon_path)); + dmi = match Icon::load(reader) { + Ok(dmi) => dmi, + Err(_) => { + return Err(format!("Invalid DMI: {}", icon_path)); + } + }; } { zone!("insert_dmi"); - let dmi_arc = Arc::new(dmi.unwrap()); + let dmi_arc = Arc::new(dmi); let other_arc = dmi_arc.clone(); - // cache it for later. - // Ownership is given to the hashmap + // Cache it for later, saving future DMI parsing operations, which are very slow. ICON_FILES.insert(icon_path.to_owned(), dmi_arc); Ok(other_arc) } } /// Takes an IconObject, gets its DMI, then picks out a DynamicImage for the IconState, as well as transforms the DynamicImage. -/// Gives ownership over the image. Please return when you are done <3 +/// Gives ownership over the image. Please return when you are done <3 (via return_image) fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result { zone!("icon_to_image"); { zone!("check_dynamicimage_exists"); - // scope-in so the lock does not persist during DMI read - let found_icon = ICON_STATES.remove(&icon.to_icostring().unwrap()); - if let Some(found) = found_icon { - return Ok(found.1); + let ico_string = match icon.to_icostring() { + Ok(ico_string) => ico_string, + Err(err) => { + return Err(err.to_string()); + } + }; + if let Some((_key, value)) = ICON_STATES.remove(&ico_string) { + return Ok(value); } } let dmi = icon_to_dmi(icon)?; - let mut matched_state: Option<&IconState> = Option::None; + let mut matched_state: Option<&IconState> = None; { zone!("match_icon_state"); for icon_state in &dmi.states { if icon_state.name == icon.icon_state { - matched_state = Option::Some(icon_state); + matched_state = Some(icon_state); break; } } } - if matched_state.is_none() { - return Err(format!( - "Could not find associated icon state {} for {}", - icon.icon_state, sprite_name - )); - } - let state = matched_state.unwrap(); + let state = match matched_state { + Some(state) => state, + None => { + return Err(format!( + "Could not find associated icon state {} for {}", + icon.icon_state, sprite_name + )); + } + }; { zone!("determine_icon_state_validity"); if state.frames < icon.frame { @@ -347,19 +374,34 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result { + return Err(format!( + "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", + icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name + )); + } + Some(idx) => *idx as u32, + None => { + return Err(format!( + "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", + icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name + )); + } + }; if icon.frame > 1 { // Add one so zero scales properly icon_idx = (icon_idx + 1) * icon.frame - 1 } - let image: DynamicImage = state.images.get(icon_idx as usize).unwrap().clone(); + let image = match state.images.get(icon_idx as usize) { + Some(image) => image.clone(), + None => { + return Err( + format!("Out of bounds index {} in icon_state {} for sprite {} - Maximum index: {} (frames: {}, dirs: {})", + icon_idx, icon.icon_state, sprite_name, state.images.len(), state.dirs, state.frames + )); + } + }; // Apply transforms let (transformed_image, errors) = transform_image(image, icon, sprite_name); if !errors.is_empty() { @@ -369,9 +411,10 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result Result<(), Error> { zone!("insert_dynamicimage"); - ICON_STATES.insert(icon.to_icostring().unwrap(), image); + ICON_STATES.insert(icon.to_icostring()?, image); + Ok(()) } /// Applies transforms to a DynamicImage. @@ -410,14 +453,16 @@ fn transform_image( } Transform::BlendIcon { icon, blend_mode } => { zone!("blend_icon"); - let image_result = - icon_to_image(icon, &format!("Transform blend_icon of {}", sprite_name)); - if let Err(err) = image_result { - error.push(err); - continue; - } - - let other_image = image_result.unwrap(); + let other_image = match icon_to_image( + icon, + &format!("Transform blend_icon of {}", sprite_name), + ) { + Ok(other_image) => other_image, + Err(err) => { + error.push(err); + continue; + } + }; for x in 0..image.width() { if x >= other_image.width() { @@ -437,7 +482,9 @@ fn transform_image( image.put_pixel(x, y, image::Rgba::(blended)); } } - return_image(other_image, icon); + if let Err(err) = return_image(other_image, icon) { + error.push(err.to_string()); + } } Transform::Scale { width, height } => { zone!("scale"); From 3dbe3a0f85fe0472e358bd132d08f9fded2a6e62 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 20:45:44 -0500 Subject: [PATCH 15/35] Remove unneccesarily verbose casting --- src/iconforge.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index 7da27372..80e6e8f6 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -575,10 +575,10 @@ struct Rgba { impl Rgba { fn into_array(self) -> [u8; 4] { [ - strict_f32_to_u8(self.r), - strict_f32_to_u8(self.g), - strict_f32_to_u8(self.b), - strict_f32_to_u8(self.a), + self.r.round() as u8, + self.g.round() as u8, + self.b.round() as u8, + self.a.round() as u8, ] } @@ -660,14 +660,3 @@ fn blend(color: Rgba, color2: Rgba, blend_mode: u8) -> Rgba { _ => color, } } - -/// caps an f32 into u8 ranges, rounds it to the nearest integer, then truncates to a u8. -fn strict_f32_to_u8(x: f32) -> u8 { - if x < u8::MIN as f32 { - return 0; - } - if x > u8::MAX as f32 { - return u8::MAX; - } - x.round().trunc() as u8 -} From 1edb38481d28f13c90a4e4d524a4063fcf61671d Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 23 Dec 2023 20:51:52 -0500 Subject: [PATCH 16/35] Fix overlay blending --- src/iconforge.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index 80e6e8f6..29064a6e 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -640,7 +640,7 @@ fn blend(color: Rgba, color2: Rgba, blend_mode: u8) -> Rgba { 3 => Rgba::map_each_a( color, color2, - &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a, + &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, &|a1, a2| { let high = f32::max(a1, a2); let low = f32::min(a1, a2); @@ -650,7 +650,7 @@ fn blend(color: Rgba, color2: Rgba, blend_mode: u8) -> Rgba { 6 => Rgba::map_each_a( color2, color, - &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a, + &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, &|a1, a2| { let high = f32::max(a1, a2); let low = f32::min(a1, a2); From 6fd5902315bda7c1996c4ce194c98463e9d5f902 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sun, 24 Dec 2023 01:14:44 -0500 Subject: [PATCH 17/35] Cleanup with new DMI version --- Cargo.lock | 358 ++++++++++++++++++++++++----------------------- Cargo.toml | 2 +- src/iconforge.rs | 37 +++-- 3 files changed, 203 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8771a4e8..636ceb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,13 +47,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -138,9 +139,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.68.1" +version = "0.69.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" dependencies = [ "bitflags 2.4.1", "cexpr", @@ -191,47 +192,26 @@ dependencies = [ [[package]] name = "borsh" -version = "0.10.3" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.3" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" dependencies = [ + "once_cell", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.42", + "syn_derive", ] [[package]] @@ -241,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", - "regex-automata 0.4.3", + "regex-automata", "serde", ] @@ -352,6 +332,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.31" @@ -446,9 +432,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -675,10 +661,11 @@ dependencies = [ [[package]] name = "dmi" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c191706391473812b2c9f11befada49b65e094a19d3d422985f0d1e2daf900" +checksum = "754c4784da61ad948b8bc868b3f639b102f798567576044fe67f5b7c70044c8f" dependencies = [ + "bitflags 2.4.1", "deflate", "image", "inflate", @@ -877,9 +864,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", ] @@ -892,20 +879,9 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.42", -] +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-sink" @@ -915,19 +891,18 @@ checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-io", - "futures-macro", "futures-task", "memchr", "pin-project-lite", @@ -971,9 +946,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gix" @@ -1483,9 +1458,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1493,7 +1468,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -1515,7 +1490,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.6", ] [[package]] @@ -1562,9 +1537,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1573,9 +1548,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1596,9 +1571,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1611,7 +1586,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1620,9 +1595,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", @@ -1685,16 +1660,6 @@ dependencies = [ "png", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.1.0" @@ -1744,9 +1709,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" @@ -1992,9 +1957,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2024,7 +1989,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "socket2 0.5.4", + "socket2 0.5.5", "twox-hash", "url", "webpki", @@ -2221,9 +2186,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2253,9 +2218,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -2305,7 +2270,7 @@ checksum = "f6f4a3f5089b981000cb50ec24320faf7a19649a45e8730e4adf49f78f066528" dependencies = [ "deprecate-until", "fixedbitset", - "indexmap 2.1.0", + "indexmap", "integer-sqrt", "num-traits", "rustc-hash", @@ -2391,21 +2356,21 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ - "toml 0.5.11", + "once_cell", + "toml_edit 0.19.15", ] [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "toml_edit 0.20.7", ] [[package]] @@ -2626,38 +2591,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", + "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" - [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rend" @@ -2704,7 +2663,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.2", + "webpki-roots 0.25.3", "winreg", ] @@ -2718,11 +2677,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom 0.2.11", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rkyv" version = "0.7.43" @@ -2786,7 +2759,7 @@ dependencies = [ "sha-1", "sha2", "thiserror", - "toml 0.8.8", + "toml", "tracy_full", "twox-hash", "url", @@ -2795,9 +2768,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.32.0" +version = "1.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" dependencies = [ "arrayvec", "borsh", @@ -2845,21 +2818,21 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", - "rustls-webpki 0.101.6", + "ring 0.17.7", + "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64", ] @@ -2870,18 +2843,18 @@ version = "0.100.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -2922,12 +2895,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -3101,9 +3074,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", @@ -3174,6 +3147,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.42", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3216,9 +3201,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -3300,9 +3285,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -3310,7 +3295,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.4", + "socket2 0.5.5", "windows-sys 0.48.0", ] @@ -3326,9 +3311,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3338,15 +3323,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.8" @@ -3374,7 +3350,18 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", "toml_datetime", "winnow", ] @@ -3385,7 +3372,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap 2.1.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -3400,20 +3387,19 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -3440,9 +3426,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" @@ -3451,7 +3437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", - "rand 0.7.3", + "rand 0.8.5", "static_assertions", ] @@ -3503,6 +3489,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -3590,9 +3582,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -3631,9 +3623,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3641,12 +3633,12 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -3660,9 +3652,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "winapi" @@ -3873,6 +3865,26 @@ dependencies = [ "tap", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.42", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index e2d8ecee..c27de75c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ rayon = { version = "1.8", optional = true } dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } -dmi = { version = "0.3.1", optional = true } +dmi = { version = "0.3.3", optional = true } tracy_full = { version = "1.6.1", optional = true} #tracy_full = { version = "1.6.1", optional = true, features = ["enable"]} diff --git a/src/iconforge.rs b/src/iconforge.rs index 29064a6e..d73a553c 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -4,7 +4,10 @@ use crate::error::Error; use crate::hash::string_hash; use crate::jobs; use dashmap::DashMap; -use dmi::icon::{Icon, IconState}; +use dmi::{ + icon::{Icon, IconState}, + dirs::Dirs, +}; use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Pixel}; use once_cell::sync::Lazy; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; @@ -19,22 +22,7 @@ use tracy_full::{frame, zone}; static ICON_FILES: Lazy>> = Lazy::new(DashMap::new); static ICON_STATES: Lazy> = Lazy::new(DashMap::new); -const SOUTH: u8 = 2; -const NORTH: u8 = 1; -const EAST: u8 = 4; -const WEST: u8 = 8; -const FOUR_DIRS: [u8; 4] = [SOUTH, NORTH, EAST, WEST]; -const SOUTHEAST: u8 = SOUTH | EAST; // 6 -const SOUTHWEST: u8 = SOUTH | WEST; // 10 -const NORTHEAST: u8 = NORTH | EAST; // 5 -const NORTHWEST: u8 = NORTH | WEST; // 9 - -/// This is ordered by how DMIs internally place dirs into the PNG -const EIGHT_DIRS: [u8; 8] = [ - SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST, -]; - -/// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = EIGHT_DIRS.indexof(DIR) +/// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = dmi::dirs::DIR_ORDERING.indexof(DIR) /// 255 is invalid. const DIR_TO_INDEX: [u8; 11] = [255, 1, 0, 255, 2, 6, 4, 255, 3, 7, 5]; @@ -364,9 +352,18 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result dir, + None => { + return Err(format!( + "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", + icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name + )); + } + }; + if (state.dirs == 1 && dir != Dirs::SOUTH) + || (state.dirs == 4 && !dmi::dirs::ORDINAL_DIRS.contains(&dir)) + || (state.dirs == 8 && !dmi::dirs::ALL_DIRS.contains(&dir)) { return Err(format!( "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", From 93dada61ab4550f6702529f0389550af8b6f2e5b Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sun, 24 Dec 2023 02:17:02 -0500 Subject: [PATCH 18/35] Cargo fmt --- src/iconforge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index d73a553c..e44ed981 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -5,8 +5,8 @@ use crate::hash::string_hash; use crate::jobs; use dashmap::DashMap; use dmi::{ - icon::{Icon, IconState}, dirs::Dirs, + icon::{Icon, IconState}, }; use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Pixel}; use once_cell::sync::Lazy; From 04722d568851eb5dfce79528b042d4b866e8a453 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sun, 24 Dec 2023 15:03:06 -0500 Subject: [PATCH 19/35] Leaf test, DynamicImage->RgbaImage, better Error handling, DashMap, and cleanup command --- Cargo.lock | 2 + Cargo.toml | 2 +- dmsrc/iconforge.dm | 24 ++ src/iconforge.rs | 617 ++++++++++++++++++++++++++++++++------------- 4 files changed, 463 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 636ceb18..dbbbf567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -592,6 +592,8 @@ dependencies = [ "lock_api", "once_cell", "parking_lot_core", + "rayon", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c27de75c..56344e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ serde_json = { version = "1.0", optional = true } lazy_static = { version = "1.4", optional = true } once_cell = { version = "1.19", optional = true } mysql = { version = "24.0", default_features = false, optional = true } -dashmap = { version = "5.5", optional = true } +dashmap = { version = "5.5", optional = true, features = ["rayon", "serde"]} zip = { version = "0.6", optional = true } rand = { version = "0.8", optional = true } toml-dep = { version = "0.8.8", package = "toml", optional = true } diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index 67d1dc88..9a9e2cce 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -1,3 +1,27 @@ +/// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].png +/// Spritesheet will contain all sprites listed within "sprites". +/// Sprite object format: list( +/// icon_file = 'icons/path_to/an_icon.dmi', +/// icon_state = "some_icon_state", +/// dir = SOUTH, +/// frame = 1, +/// transform = list(transform_object, ...) +/// ) +/// transform_object format: +/// list("type" = "Color", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) +/// list("type" = "Icon", "icon" = sprite_object, "blend_mode" = ICON_OVERLAY) +/// list("type" = "Scale", "width" = 32, "height" = 32) +/// list("type" = "Crop", "x1" = 0, "y1" = 0, "x2" = 32, "y2" = 32) +/// Returns a SpritesheetResult as JSON, containing fields: +/// sizes: list("32x32", "64x64", ...etc) +/// sprites: list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...) +/// error: A string, empty if there were no errors. +/// In the event of an unrecoverable error, where the spritesheet could not even generate, returns a string containing the error. #define rustg_iconforge_generate(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites) +/// Returns a job_id for use with rustg_iconforge_check() #define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites) +/// Returns the status of a job_id #define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") +/// Clears all cached DMIs and images, freeing up memory. +/// This should be used after spritesheets are done being generated. +#define rustg_iconforge_cleanup() RUSTG_CALL(RUST_G, "iconforge_cleanup")() diff --git a/src/iconforge.rs b/src/iconforge.rs index e44ed981..8a1a6e1e 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -8,9 +8,11 @@ use dmi::{ dirs::Dirs, icon::{Icon, IconState}, }; -use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, Pixel}; +use image::{Pixel, RgbaImage}; use once_cell::sync::Lazy; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use rayon::iter::{ + IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator, +}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -20,7 +22,7 @@ use std::{ }; use tracy_full::{frame, zone}; static ICON_FILES: Lazy>> = Lazy::new(DashMap::new); -static ICON_STATES: Lazy> = Lazy::new(DashMap::new); +static ICON_STATES: Lazy> = Lazy::new(DashMap::new); /// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = dmi::dirs::DIR_ORDERING.indexof(DIR) /// 255 is invalid. @@ -53,10 +55,18 @@ byond_fn!(fn iconforge_check(id) { Some(jobs::check(id)) }); +byond_fn!( + fn iconforge_cleanup() { + ICON_FILES.clear(); + ICON_STATES.clear(); + Some("Ok") + } +); + #[derive(Serialize)] struct SpritesheetResult { sizes: Vec, - sprites: HashMap, + sprites: DashMap, error: String, } @@ -66,25 +76,73 @@ struct SpritesheetEntry { position: u32, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Clone, Eq, PartialEq, Hash)] struct IconObject { icon_file: String, icon_state: String, dir: u8, frame: u32, - moving: u8, transform: Vec, + icostring: String, +} + +#[derive(Serialize, Deserialize)] +struct IconObjectIO { + icon_file: String, + icon_state: String, + dir: u8, + frame: u32, + transform: Vec, +} + +impl std::fmt::Display for IconObject { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "IconObject(icon_file={}, icon_state={}, dir={}, frame={})", + self.icon_file, self.icon_state, self.dir, self.frame + ) + } } impl IconObject { - fn to_icostring(&self) -> Result { - zone!("to_icostring"); - string_hash("xxh64", &serde_json::to_string(self)?) + fn to_base(&self) -> Result { + zone!("to_base"); + string_hash( + "xxh64", + &format!( + "{}.{}.{}.{}", + self.icon_file, self.icon_state, self.dir, self.frame + ), + ) + } + + fn gen_icostring_input(&self, transform: &[Transform]) -> Result { + zone!("gen_icostring_input"); + Ok(format!( + "{}-{}", + self.to_base()?, + serde_json::to_string(transform)? + )) + } + + fn gen_icostring(&mut self) -> Result<(), Error> { + zone!("gen_icostring"); + self.icostring = string_hash("xxh64", &self.gen_icostring_input(&self.transform)?)?; + Ok(()) } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize)] #[serde(tag = "type")] +enum TransformIO { + BlendColor { color: String, blend_mode: u8 }, + BlendIcon { icon: IconObjectIO, blend_mode: u8 }, + Scale { width: u32, height: u32 }, + Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, +} + +#[derive(Serialize, Clone, Eq, PartialEq, Hash)] enum Transform { BlendColor { color: String, blend_mode: u8 }, BlendIcon { icon: IconObject, blend_mode: u8 }, @@ -127,28 +185,121 @@ fn generate_spritesheet( let error = Arc::new(Mutex::new(Vec::::new())); let size_to_icon_objects = Arc::new(Mutex::new(HashMap::>::new())); - let sprites_map = serde_json::from_str::>(sprites)?; - let sprites_objects = Arc::new(Mutex::new(HashMap::::new())); + let sprites_objects = DashMap::::new(); + + let tree_bases = Arc::new(Mutex::new( + HashMap::>::new(), + )); + let input; + { + zone!("from_json"); + input = serde_json::from_str::>(sprites)?; + } + let mut sprites_map = HashMap::::new(); + { + zone!("io_to_mem"); + sprites_map.extend( + input + .into_par_iter() + .map(|(sprite_name, icon)| (sprite_name, icon_from_io(icon))) + .collect::>(), + ); + } // Pre-load all the DMIs now. // This is much faster than doing it as we go (tested!), because sometimes multiple parallel iterators need the DMI. - sprites_map.par_iter().for_each(|sprite_entry| { + sprites_map.par_iter().for_each(|(sprite_name, icon)| { zone!("sprite_to_icons"); - let (_, icon) = sprite_entry; - icon_to_icons(icon).par_iter().for_each(|icon| { + + icon_to_icons(icon).into_par_iter().for_each(|icon| { if let Err(err) = icon_to_dmi(icon) { error.lock().unwrap().push(err); } }); + + { + zone!("map_to_base"); + let base = match icon.to_base() { + Ok(base) => base, + Err(err) => { + error.lock().unwrap().push(err.to_string()); + return; + } + }; + tree_bases + .lock() + .unwrap() + .entry(base) + .or_default() + .push((sprite_name, icon)); + } }); + // cache this here so we don't generate the same string 5000 times + let sprite_name = "N/A, in tree generation stage".to_string(); + + // Map duplicate transform sets into a tree. + // This is beneficial in the case where we have the same base image, and the same set of transforms, but change 1 or 2 things at the end. + // We can greatly reduce the amount of RgbaImages created by first finding these. + tree_bases + .lock() + .unwrap() + .par_iter() + .for_each(|(_, icons)| { + zone!("transform_trees"); + let first_icon = match icons.first() { + Some((_, icon)) => icon, + None => { + error + .lock() + .unwrap() + .push("Somehow found no icon for a tree.".to_string()); + return; + } + }; + let (base_image, _) = match icon_to_image(first_icon, &sprite_name, false, false) { + Ok(image) => image, + Err(err) => { + error.lock().unwrap().push(err); + return; + } + }; + let unique_icons = DashMap::, &IconObject>::new(); + { + zone!("map_unique"); + icons.iter().for_each(|(_, icon)| { + // This will ensure we only map unique transform sets. This also means each IconObject is guaranteed a unique IcoString + // Since all icons share the same 'base'. + // Also check to see if the icon is already cached. If so, we can ignore this transform chain. + if !ICON_STATES.contains_key(&icon.icostring) { + unique_icons.insert(icon.transform.clone(), icon); + } + }); + } + if let Some(entry) = unique_icons.get(&Vec::new()) { + if let Err(err) = return_image(base_image.clone(), entry.value()) { + error.lock().unwrap().push(err.to_string()); + } + } + { + zone!("transform_all_leaves"); + if let Err(err) = transform_leaves( + &unique_icons.into_iter().map(|(_, v)| v).collect(), + base_image, + 0, + ) { + error.lock().unwrap().push(err); + } + } + }); + // Pick the specific icon states out of the DMI, also generating their transforms, build the spritesheet metadata. sprites_map.par_iter().for_each(|sprite_entry| { zone!("map_sprite"); let (sprite_name, icon) = sprite_entry; - // get DynamicImage, applying transforms as well - let image = match icon_to_image(icon, sprite_name) { + // get RgbaImage, it should already be transformed, so it must be cached. + let (image, _) = match icon_to_image(icon, sprite_name, true, true) { Ok(image) => image, Err(err) => { error.lock().unwrap().push(err); @@ -175,7 +326,7 @@ fn generate_spritesheet( { zone!("insert_into_sprite_objects"); - sprites_objects.lock().unwrap().insert( + sprites_objects.insert( sprite_name.to_owned(), SpritesheetEntry { size_id: size_id.to_owned(), @@ -214,12 +365,12 @@ fn generate_spritesheet( .unwrap(); let mut final_image = - DynamicImage::new_rgba8(base_width * icon_objects.len() as u32, base_height); + RgbaImage::new(base_width * icon_objects.len() as u32, base_height); for (idx, icon) in icon_objects.iter().enumerate() { zone!("join_sprite"); - let image = match icon_to_image(icon, &sprite_name) { - Ok(image) => image, + let image = match icon_to_image(icon, &sprite_name, true, true) { + Ok((image, _)) => image, Err(err) => { error.lock().unwrap().push(err); return; @@ -228,9 +379,12 @@ fn generate_spritesheet( let base_x: u32 = base_width * idx as u32; for x in 0..image.width() { for y in 0..image.height() { - final_image.put_pixel(base_x + x, y, image.get_pixel(x, y)) + final_image.put_pixel(base_x + x, y, *image.get_pixel(x, y)) } } + if let Err(err) = return_image(image, icon) { + error.lock().unwrap().push(err.to_string()); + } } { zone!("write_spritesheet"); @@ -249,18 +403,114 @@ fn generate_spritesheet( // Collect the game metadata and any errors. let returned = SpritesheetResult { sizes, - sprites: sprites_objects.lock().unwrap().to_owned(), + sprites: sprites_objects, error: error.lock().unwrap().join("\n"), }; Ok(serde_json::to_string::(&returned)?) } +/// Given an array of 'transform arrays' onto from a shared IconObject base, +/// recursively applies transforms in a tree structure. Maximum transform depth is 128. +fn transform_leaves(icons: &Vec<&IconObject>, image: RgbaImage, depth: u8) -> Result<(), String> { + zone!("transform_leaf"); + if depth > 128 { + return Err( + "Transform depth exceeded 128. https://www.youtube.com/watch?v=CUjrySBwi5Q".to_string(), + ); + } + let next_transforms = DashMap::>::new(); + let errors = Mutex::new(Vec::::new()); + + { + zone!("get_next_transforms"); + icons.par_iter().for_each(|icon| { + zone!("collect_icon_transforms"); + if let Some(transform) = icon.transform.get(depth as usize) { + next_transforms + .entry(transform.clone()) + .or_default() + .push(icon); + } + }); + } + + { + zone!("do_next_transforms"); + next_transforms + .into_par_iter() + .for_each(|(transform, mut associated_icons)| { + let mut altered_image; + { + zone!("clone_image"); + altered_image = image.clone(); + } + if let Err(err) = transform_image(&mut altered_image, &transform) { + errors.lock().unwrap().push(err); + } + { + zone!("filter_associated_icons"); + associated_icons + .clone() + .into_iter() + .enumerate() + .for_each(|(idx, icon)| { + if icon.transform.len() as u8 == depth + 1 + && *icon.transform.last().unwrap() == transform + { + associated_icons.swap_remove(idx); + if let Err(err) = return_image(altered_image.clone(), icon) { + errors.lock().unwrap().push(err.to_string()); + } + } + }); + } + if let Err(err) = transform_leaves(&associated_icons, altered_image, depth + 1) { + errors.lock().unwrap().push(err); + } + }); + } + + if !errors.lock().unwrap().is_empty() { + return Err(errors.lock().unwrap().join("\n")); + } + Ok(()) +} + +/// Converts an IO icon to one with icostrings +fn icon_from_io(icon_in: IconObjectIO) -> IconObject { + zone!("icon_from_io"); + let mut result = IconObject { + icon_file: icon_in.icon_file, + icon_state: icon_in.icon_state, + dir: icon_in.dir, + frame: icon_in.frame, + transform: icon_in + .transform + .into_iter() + .map(|transform_in| match transform_in { + TransformIO::BlendColor { color, blend_mode } => { + Transform::BlendColor { color, blend_mode } + } + TransformIO::BlendIcon { icon, blend_mode } => Transform::BlendIcon { + icon: icon_from_io(icon), + blend_mode, + }, + TransformIO::Crop { x1, y1, x2, y2 } => Transform::Crop { x1, y1, x2, y2 }, + TransformIO::Scale { width, height } => Transform::Scale { width, height }, + }) + .collect(), + icostring: String::new(), + }; + result.gen_icostring().unwrap(); // unsafe but idc + result +} + /// Takes in an icon and gives a list of nested icons. Also returns a reference to the provided icon in the list. -fn icon_to_icons(icon: &IconObject) -> Vec<&IconObject> { +fn icon_to_icons(icon_in: &IconObject) -> Vec<&IconObject> { zone!("icon_to_icons"); let mut icons: Vec<&IconObject> = Vec::new(); - icons.push(icon); - for transform in &icon.transform { + icons.push(icon_in); + for transform in &icon_in.transform { if let Transform::BlendIcon { icon, .. } = transform { let nested = icon_to_icons(icon); for icon in nested { @@ -283,8 +533,8 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { } let icon_file = match File::open(icon_path) { Ok(icon_file) => icon_file, - Err(_) => { - return Err(format!("No such DMI file: {}", icon_path)); + Err(err) => { + return Err(format!("Failed to open DMI '{}' - {}", icon_path, err)); } }; let reader = BufReader::new(icon_file); @@ -293,8 +543,8 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { zone!("parse_dmi"); dmi = match Icon::load(reader) { Ok(dmi) => dmi, - Err(_) => { - return Err(format!("Invalid DMI: {}", icon_path)); + Err(err) => { + return Err(format!("DMI '{}' failed to parse - {}", icon_path, err)); } }; } @@ -308,20 +558,29 @@ fn icon_to_dmi(icon: &IconObject) -> Result, String> { } } -/// Takes an IconObject, gets its DMI, then picks out a DynamicImage for the IconState, as well as transforms the DynamicImage. +/// Takes an IconObject, gets its DMI, then picks out a RgbaImage for the IconState. +/// Returns with True if the RgbaImage is pre-cached (and shouldn't have new transforms applied) /// Gives ownership over the image. Please return when you are done <3 (via return_image) -fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result { +fn icon_to_image( + icon: &IconObject, + sprite_name: &String, + cached: bool, + must_be_cached: bool, +) -> Result<(RgbaImage, bool), String> { zone!("icon_to_image"); - { - zone!("check_dynamicimage_exists"); - let ico_string = match icon.to_icostring() { - Ok(ico_string) => ico_string, - Err(err) => { - return Err(err.to_string()); - } - }; - if let Some((_key, value)) = ICON_STATES.remove(&ico_string) { - return Ok(value); + if cached { + zone!("check_rgba_image_exists"); + if icon.icostring.is_empty() { + return Err(format!( + "No icostring generated for {} {}", + icon, sprite_name + )); + } + if let Some(entry) = ICON_STATES.get(&icon.icostring) { + return Ok((entry.value().clone(), true)); + } + if must_be_cached { + return Err("Image not found in cache!".to_string()); } } let dmi = icon_to_dmi(icon)?; @@ -362,7 +621,7 @@ fn icon_to_image(icon: &IconObject, sprite_name: &String) -> Result Result image.clone(), + Ok(match state.images.get(icon_idx as usize) { + Some(image) => (image.to_rgba8(), false), None => { return Err( format!("Out of bounds index {} in icon_state {} for sprite {} - Maximum index: {} (frames: {}, dirs: {})", icon_idx, icon.icon_state, sprite_name, state.images.len(), state.dirs, state.frames )); } - }; - // Apply transforms - let (transformed_image, errors) = transform_image(image, icon, sprite_name); - if !errors.is_empty() { - return Err(errors); - } - Ok(transformed_image) + }) } /// Gives an image back to the cache, after it is done being used. -fn return_image(image: DynamicImage, icon: &IconObject) -> Result<(), Error> { - zone!("insert_dynamicimage"); - ICON_STATES.insert(icon.to_icostring()?, image); +fn return_image(image: RgbaImage, icon: &IconObject) -> Result<(), Error> { + zone!("insert_rgbaimage"); + if icon.icostring.is_empty() { + return Err(Error::IconForge(format!( + "No icostring generated for {}", + icon + ))); + } + ICON_STATES.insert(icon.icostring.to_owned(), image); Ok(()) } -/// Applies transforms to a DynamicImage. -fn transform_image( - image_in: DynamicImage, - icon: &IconObject, - sprite_name: &String, -) -> (DynamicImage, String) { +fn apply_all_transforms(image: &mut RgbaImage, transforms: &Vec) -> Result<(), String> { + let mut errors = Vec::::new(); + for transform in transforms { + if let Err(error) = transform_image(image, transform) { + errors.push(error); + } + } + if !errors.is_empty() { + return Err(errors.join("\n")); + } + Ok(()) +} + +/// Applies transforms to a RgbaImage. +fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), String> { zone!("transform_image"); - let mut image = image_in; - let mut error: Vec = Vec::new(); - for transform in &icon.transform { - match transform { - Transform::BlendColor { color, blend_mode } => { - zone!("blend_color"); + match transform { + Transform::BlendColor { color, blend_mode } => { + zone!("blend_color"); + let mut color2: [u8; 4] = [0, 0, 0, 255]; + { + zone!("from_hex"); let mut hex: String = color.to_owned(); if hex.starts_with('#') { hex = hex[1..].to_string(); } if hex.len() == 6 { - hex = format!("{}ff", hex); + hex += "ff"; } - let mut color2: [u8; 4] = [0, 0, 0, 255]; - if let Err(err) = hex::decode_to_slice(hex, &mut color2) { - error.push(format!("Decoding hex color {} failed: {}", color, err)); - } - for x in 0..image.width() { - for y in 0..image.height() { - let px = image.get_pixel(x, y); - let pixel = px.channels(); - let blended = blend_u8(pixel, &color2, *blend_mode); - image.put_pixel(x, y, image::Rgba::(blended)); - } + if let Err(err) = hex::decode_to_slice(hex, &mut color2) { + return Err(format!("Decoding hex color {} failed: {}", color, err)); } } - Transform::BlendIcon { icon, blend_mode } => { - zone!("blend_icon"); - let other_image = match icon_to_image( - icon, - &format!("Transform blend_icon of {}", sprite_name), - ) { - Ok(other_image) => other_image, - Err(err) => { - error.push(err); - continue; - } - }; - - for x in 0..image.width() { - if x >= other_image.width() { - break; // undefined behavior in DM :) - } - for y in 0..image.height() { - if y >= other_image.height() { - break; // undefined behavior in DM :) - } - let px1 = image.get_pixel(x, y); - let px2 = other_image.get_pixel(x, y); - let pixel_1 = px1.channels(); - let pixel_2 = px2.channels(); - - let blended = blend_u8(pixel_1, pixel_2, *blend_mode); + for x in 0..image.width() { + for y in 0..image.height() { + let px = image.get_pixel_mut(x, y); + let pixel = px.channels(); + let blended = blend_u8(pixel, &color2, *blend_mode); - image.put_pixel(x, y, image::Rgba::(blended)); - } - } - if let Err(err) = return_image(other_image, icon) { - error.push(err.to_string()); + *px = image::Rgba::(blended); } } - Transform::Scale { width, height } => { - zone!("scale"); - let x_ratio = image.width() as f32 / *width as f32; - let y_ratio = image.height() as f32 / *height as f32; - let mut new_image = DynamicImage::new_rgba8(*width, *height); - for x in 0..*width { - for y in 0..*height { - let old_x: u32 = (x as f32 * x_ratio).floor() as u32; - let old_y: u32 = (y as f32 * y_ratio).floor() as u32; - let pixel = image.get_pixel(old_x, old_y); - new_image.put_pixel(x, y, pixel); - } + } + Transform::BlendIcon { icon, blend_mode } => { + zone!("blend_icon"); + let (mut other_image, cached) = + icon_to_image(icon, &format!("Transform blend_icon {}", icon), true, false)?; + + if !cached { + apply_all_transforms(&mut other_image, &icon.transform)?; + }; + for x in 0..std::cmp::min(image.width(), other_image.width()) { + for y in 0..std::cmp::min(image.width(), other_image.width()) { + let px1 = image.get_pixel_mut(x, y); + let px2 = other_image.get_pixel(x, y); + let pixel_1 = px1.channels(); + let pixel_2 = px2.channels(); + + let blended = blend_u8(pixel_1, pixel_2, *blend_mode); + + *px1 = image::Rgba::(blended); } - image = new_image; } - Transform::Crop { x1, y1, x2, y2 } => { - zone!("crop"); - let i_width = image.width(); - let i_height = image.height(); - let mut x1 = *x1; - let mut y1 = *y1; - let mut x2 = *x2; - let mut y2 = *y2; - if x2 <= x1 || y2 <= y1 { - error.push(format!( - "Invalid bounds {} {} to {} {} from sprite {}", - x1, y1, x2, y2, sprite_name - )); - continue; + if let Err(err) = return_image(other_image, icon) { + return Err(err.to_string()); + } + } + Transform::Scale { width, height } => { + zone!("scale"); + let old_width = image.width() as usize; + let old_height = image.height() as usize; + let x_ratio = old_width as f32 / *width as f32; + let y_ratio = old_height as f32 / *height as f32; + let mut new_image = RgbaImage::new(*width, *height); + for x in 0..(*width) { + for y in 0..(*height) { + let old_x = (x as f32 * x_ratio).floor() as u32; + let old_y = (y as f32 * y_ratio).floor() as u32; + new_image.put_pixel(x, y, *image.get_pixel(old_x, old_y)); } + } + *image = new_image; + } + Transform::Crop { x1, y1, x2, y2 } => { + zone!("crop"); + let i_width = image.width(); + let i_height = image.height(); + let mut x1 = *x1; + let mut y1 = *y1; + let mut x2 = *x2; + let mut y2 = *y2; + if x2 <= x1 || y2 <= y1 { + return Err(format!( + "Invalid bounds {} {} to {} {} in crop transform", + x1, y1, x2, y2 + )); + } - // convert from BYOND (0,0 is bottom left) to Rust (0,0 is top left) - let y2_old = y2; - y2 = i_height as i32 - y1; - y1 = i_height as i32 - y2_old; - - let mut width = x2 - x1; - let mut height = y2 - y1; - - if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { - let mut blank_img = - ImageBuffer::from_fn(width as u32, height as u32, |_x, _y| { - image::Rgba([0, 0, 0, 0]) - }); - image::imageops::overlay( - &mut blank_img, - &image, - if x1 < 0 { (x1).abs() as i64 } else { 0 } - - if x1 > i_width as i32 { - (x1 - i_width as i32) as i64 - } else { - 0 - }, - if y1 < 0 { (y1).abs() as i64 } else { 0 } - - if x1 > i_width as i32 { - (x1 - i_width as i32) as i64 - } else { - 0 - }, - ); - image = DynamicImage::new_rgba8(width as u32, height as u32); - if let Err(err) = image.copy_from(&blank_img, 0, 0) { - error.push(err.to_string()); - continue; - } - x1 = std::cmp::max(0, x1); - x2 = std::cmp::min(i_width as i32, x2); - y1 = std::cmp::max(0, y1); - y2 = std::cmp::min(i_height as i32, y2); - width = x2 - x1; - height = y2 - y1; - } - image = image.crop_imm(x1 as u32, y1 as u32, width as u32, height as u32); + // convert from BYOND (0,0 is bottom left) to Rust (0,0 is top left) + let y2_old = y2; + y2 = i_height as i32 - y1; + y1 = i_height as i32 - y2_old; + + let mut width = x2 - x1; + let mut height = y2 - y1; + + if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { + let mut blank_img: image::ImageBuffer, Vec> = + RgbaImage::from_fn(width as u32, height as u32, |_x, _y| { + image::Rgba([0, 0, 0, 0]) + }); + image::imageops::overlay( + &mut blank_img, + image, + if x1 < 0 { (x1).abs() as i64 } else { 0 } + - if x1 > i_width as i32 { + (x1 - i_width as i32) as i64 + } else { + 0 + }, + if y1 < 0 { (y1).abs() as i64 } else { 0 } + - if x1 > i_width as i32 { + (x1 - i_width as i32) as i64 + } else { + 0 + }, + ); + *image = blank_img; + x1 = std::cmp::max(0, x1); + x2 = std::cmp::min(i_width as i32, x2); + y1 = std::cmp::max(0, y1); + y2 = std::cmp::min(i_height as i32, y2); + width = x2 - x1; + height = y2 - y1; } + *image = + image::imageops::crop_imm(image, x1 as u32, y1 as u32, width as u32, height as u32) + .to_image(); } } - (image, error.join("\n")) + Ok(()) } struct Rgba { From ae8fe6140320b3336c8abef30b5cee4321779ec6 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sun, 24 Dec 2023 16:20:40 -0500 Subject: [PATCH 20/35] Fix --- src/iconforge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index 8a1a6e1e..e8f09475 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -11,7 +11,7 @@ use dmi::{ use image::{Pixel, RgbaImage}; use once_cell::sync::Lazy; use rayon::iter::{ - IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator, + IntoParallelIterator, IntoParallelRefIterator, ParallelIterator, }; use serde::{Deserialize, Serialize}; use std::{ From 0e5b59bbe0917e6d0f2b4fa8990d0c0ee07ff7d5 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 25 Dec 2023 15:26:06 -0500 Subject: [PATCH 21/35] Further tree optimizations, hashing optimization, cache icostrings more effectively. --- Cargo.toml | 22 +++++------ src/iconforge.rs | 97 +++++++++++++++++++++++++++--------------------- 2 files changed, 65 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 56344e64..0e3c016b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ serde_json = { version = "1.0", optional = true } lazy_static = { version = "1.4", optional = true } once_cell = { version = "1.19", optional = true } mysql = { version = "24.0", default_features = false, optional = true } -dashmap = { version = "5.5", optional = true, features = ["rayon", "serde"]} +dashmap = { version = "5.5", optional = true, features = ["rayon", "serde"] } zip = { version = "0.6", optional = true } rand = { version = "0.8", optional = true } toml-dep = { version = "0.8.8", package = "toml", optional = true } @@ -63,8 +63,8 @@ dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } dmi = { version = "0.3.3", optional = true } -tracy_full = { version = "1.6.1", optional = true} -#tracy_full = { version = "1.6.1", optional = true, features = ["enable"]} +tracy_full = { version = "1.6.1", optional = true } +#tracy_full = { version = "1.6.1", optional = true, features = ["enable"] } [features] default = [ @@ -137,17 +137,17 @@ hash = [ "serde_json", ] iconforge = [ - "serde", - "serde_json", - "png", - "image", + "dashmap", "dep:dmi", - "rayon", - "tracy_full", + "image", "jobs", "once_cell", - "dashmap", - "hash", + "png", + "rayon", + "serde", + "serde_json", + "tracy_full", + "twox-hash", ] pathfinder = ["num-integer", "pathfinding", "serde", "serde_json"] redis_pubsub = ["flume", "redis", "serde", "serde_json"] diff --git a/src/iconforge.rs b/src/iconforge.rs index e8f09475..6b8418eb 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1,7 +1,6 @@ // DMI spritesheet generator // Developed by itsmeow use crate::error::Error; -use crate::hash::string_hash; use crate::jobs; use dashmap::DashMap; use dmi::{ @@ -10,19 +9,21 @@ use dmi::{ }; use image::{Pixel, RgbaImage}; use once_cell::sync::Lazy; -use rayon::iter::{ - IntoParallelIterator, IntoParallelRefIterator, ParallelIterator, -}; +use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fs::File, + hash::BuildHasherDefault, io::BufReader, sync::{Arc, Mutex}, }; use tracy_full::{frame, zone}; -static ICON_FILES: Lazy>> = Lazy::new(DashMap::new); -static ICON_STATES: Lazy> = Lazy::new(DashMap::new); +use twox_hash::XxHash64; +static ICON_FILES: Lazy, BuildHasherDefault>> = + Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::::default())); +static ICON_STATES: Lazy>> = + Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::::default())); /// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = dmi::dirs::DIR_ORDERING.indexof(DIR) /// 255 is invalid. @@ -66,7 +67,7 @@ byond_fn!( #[derive(Serialize)] struct SpritesheetResult { sizes: Vec, - sprites: DashMap, + sprites: DashMap>, error: String, } @@ -83,7 +84,8 @@ struct IconObject { dir: u8, frame: u32, transform: Vec, - icostring: String, + transform_hash_input: String, + icon_hash_input: String, } #[derive(Serialize, Deserialize)] @@ -108,27 +110,27 @@ impl std::fmt::Display for IconObject { impl IconObject { fn to_base(&self) -> Result { zone!("to_base"); - string_hash( - "xxh64", - &format!( - "{}.{}.{}.{}", - self.icon_file, self.icon_state, self.dir, self.frame - ), - ) - } - - fn gen_icostring_input(&self, transform: &[Transform]) -> Result { - zone!("gen_icostring_input"); - Ok(format!( - "{}-{}", - self.to_base()?, - serde_json::to_string(transform)? - )) + // This is a micro-op that ends up saving a lot of time. format!() is quite slow when you get down to microseconds. + let mut str_buf = String::with_capacity(self.icon_file.len() + self.icon_state.len() + 4); + str_buf.push_str(&self.icon_file); + str_buf.push_str(&self.icon_state); + str_buf.push_str(&self.dir.to_string()); + str_buf.push_str(&self.frame.to_string()); + Ok(str_buf) } - fn gen_icostring(&mut self) -> Result<(), Error> { - zone!("gen_icostring"); - self.icostring = string_hash("xxh64", &self.gen_icostring_input(&self.transform)?)?; + fn gen_icon_hash_input(&mut self) -> Result<(), Error> { + zone!("gen_icon_hash_input"); + let base = self.to_base()?; + { + zone!("transform_to_json"); + let transform_str = serde_json::to_string(&self.transform)?; + self.transform_hash_input = transform_str; + } + let mut str_buf = String::with_capacity(base.len() + self.transform_hash_input.len()); + str_buf.push_str(&base); + str_buf.push_str(&self.transform_hash_input); + self.icon_hash_input = str_buf; Ok(()) } } @@ -185,11 +187,18 @@ fn generate_spritesheet( let error = Arc::new(Mutex::new(Vec::::new())); let size_to_icon_objects = Arc::new(Mutex::new(HashMap::>::new())); - let sprites_objects = DashMap::::new(); + let sprites_objects = + DashMap::>::with_hasher( + BuildHasherDefault::::default(), + ); - let tree_bases = Arc::new(Mutex::new( - HashMap::>::new(), - )); + let tree_bases = Arc::new(Mutex::new(HashMap::< + String, + Vec<(&String, &IconObject)>, + BuildHasherDefault, + >::with_hasher( + BuildHasherDefault::::default() + ))); let input; { zone!("from_json"); @@ -268,10 +277,11 @@ fn generate_spritesheet( { zone!("map_unique"); icons.iter().for_each(|(_, icon)| { - // This will ensure we only map unique transform sets. This also means each IconObject is guaranteed a unique IcoString + // This will ensure we only map unique transform sets. This also means each IconObject is guaranteed a unique icon_hash // Since all icons share the same 'base'. // Also check to see if the icon is already cached. If so, we can ignore this transform chain. - if !ICON_STATES.contains_key(&icon.icostring) { + if !ICON_STATES.contains_key(&icon.icon_hash_input) { + // TODO, try to make a faster hash for this. Can probably generate a unique hash for transforms during the IO conversion step. unique_icons.insert(icon.transform.clone(), icon); } }); @@ -476,7 +486,7 @@ fn transform_leaves(icons: &Vec<&IconObject>, image: RgbaImage, depth: u8) -> Re Ok(()) } -/// Converts an IO icon to one with icostrings +/// Converts an IO icon to one with icon_hash_input fn icon_from_io(icon_in: IconObjectIO) -> IconObject { zone!("icon_from_io"); let mut result = IconObject { @@ -499,9 +509,10 @@ fn icon_from_io(icon_in: IconObjectIO) -> IconObject { TransformIO::Scale { width, height } => Transform::Scale { width, height }, }) .collect(), - icostring: String::new(), + transform_hash_input: String::new(), + icon_hash_input: String::new(), }; - result.gen_icostring().unwrap(); // unsafe but idc + result.gen_icon_hash_input().unwrap(); // unsafe but idc result } @@ -570,13 +581,13 @@ fn icon_to_image( zone!("icon_to_image"); if cached { zone!("check_rgba_image_exists"); - if icon.icostring.is_empty() { + if icon.icon_hash_input.is_empty() { return Err(format!( - "No icostring generated for {} {}", + "No icon_hash generated for {} {}", icon, sprite_name )); } - if let Some(entry) = ICON_STATES.get(&icon.icostring) { + if let Some(entry) = ICON_STATES.get(&icon.icon_hash_input) { return Ok((entry.value().clone(), true)); } if must_be_cached { @@ -662,14 +673,14 @@ fn icon_to_image( /// Gives an image back to the cache, after it is done being used. fn return_image(image: RgbaImage, icon: &IconObject) -> Result<(), Error> { - zone!("insert_rgbaimage"); - if icon.icostring.is_empty() { + zone!("insert_rgba_image"); + if icon.icon_hash_input.is_empty() { return Err(Error::IconForge(format!( - "No icostring generated for {}", + "No icon_hash_input generated for {}", icon ))); } - ICON_STATES.insert(icon.icostring.to_owned(), image); + ICON_STATES.insert(icon.icon_hash_input.to_owned(), image); Ok(()) } From cd968634bf022fc6183e5edea1206a8f64055cfe Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 25 Dec 2023 15:33:00 -0500 Subject: [PATCH 22/35] Optimize unique_icons insertion a little --- src/iconforge.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index 6b8418eb..07ae9be6 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -273,7 +273,8 @@ fn generate_spritesheet( return; } }; - let unique_icons = DashMap::, &IconObject>::new(); + let mut no_transforms = Option::<&IconObject>::None; + let unique_icons = DashMap::::new(); { zone!("map_unique"); icons.iter().for_each(|(_, icon)| { @@ -281,13 +282,15 @@ fn generate_spritesheet( // Since all icons share the same 'base'. // Also check to see if the icon is already cached. If so, we can ignore this transform chain. if !ICON_STATES.contains_key(&icon.icon_hash_input) { - // TODO, try to make a faster hash for this. Can probably generate a unique hash for transforms during the IO conversion step. - unique_icons.insert(icon.transform.clone(), icon); + unique_icons.insert(icon.icon_hash_input.clone(), icon); + } + if icon.transform.is_empty() { + no_transforms = Some(icon); } }); } - if let Some(entry) = unique_icons.get(&Vec::new()) { - if let Err(err) = return_image(base_image.clone(), entry.value()) { + if let Some(entry) = no_transforms { + if let Err(err) = return_image(base_image.clone(), entry) { error.lock().unwrap().push(err.to_string()); } } From 3d47ac79e9049724275335cb8106539cbd9c52c6 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 25 Dec 2023 15:56:31 -0500 Subject: [PATCH 23/35] Fix macro --- dmsrc/iconforge.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index 9a9e2cce..e57cddea 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -24,4 +24,4 @@ #define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") /// Clears all cached DMIs and images, freeing up memory. /// This should be used after spritesheets are done being generated. -#define rustg_iconforge_cleanup() RUSTG_CALL(RUST_G, "iconforge_cleanup")() +#define rustg_iconforge_cleanup RUSTG_CALL(RUST_G, "iconforge_cleanup") From 2d7865e7a57640238c4cf0a36f152c3410fe610b Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 25 Dec 2023 16:55:07 -0500 Subject: [PATCH 24/35] Little more cleanup --- dmsrc/iconforge.dm | 32 +++++++++++++++++++------------- src/hash.rs | 4 ++-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index e57cddea..d8f2ed1b 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -1,21 +1,27 @@ /// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].png /// Spritesheet will contain all sprites listed within "sprites". -/// Sprite object format: list( -/// icon_file = 'icons/path_to/an_icon.dmi', -/// icon_state = "some_icon_state", -/// dir = SOUTH, -/// frame = 1, -/// transform = list(transform_object, ...) -/// ) -/// transform_object format: -/// list("type" = "Color", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) -/// list("type" = "Icon", "icon" = sprite_object, "blend_mode" = ICON_OVERLAY) +/// "sprites" format: +/// list( +/// "sprite_name" = list( +/// icon_file = 'icons/path_to/an_icon.dmi', +/// icon_state = "some_icon_state", +/// dir = SOUTH, +/// frame = 1, +/// transform = list([TRANSFORM_OBJECT], ...) +/// ), +/// ..., +/// ) +/// TRANSFORM_OBJECT format: +/// list("type" = "BlendColor", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) +/// list("type" = "BlendIcon", "icon" = sprite_object, "blend_mode" = ICON_OVERLAY) /// list("type" = "Scale", "width" = 32, "height" = 32) /// list("type" = "Crop", "x1" = 0, "y1" = 0, "x2" = 32, "y2" = 32) /// Returns a SpritesheetResult as JSON, containing fields: -/// sizes: list("32x32", "64x64", ...etc) -/// sprites: list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...) -/// error: A string, empty if there were no errors. +/// list( +/// "sizes" = list("32x32", "64x64", ...etc) +/// "sprites" = list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...) +/// "error" = "[A string, empty if there were no errors.]" +/// ) /// In the event of an unrecoverable error, where the spritesheet could not even generate, returns a string containing the error. #define rustg_iconforge_generate(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites) /// Returns a job_id for use with rustg_iconforge_check() diff --git a/src/hash.rs b/src/hash.rs index 8353ae5b..ee959fe0 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -76,11 +76,11 @@ fn hash_algorithm>(name: &str, bytes: B) -> Result { } } -pub fn string_hash(algorithm: &str, string: &str) -> Result { +fn string_hash(algorithm: &str, string: &str) -> Result { hash_algorithm(algorithm, string) } -pub fn file_hash(algorithm: &str, path: &str) -> Result { +fn file_hash(algorithm: &str, path: &str) -> Result { let mut bytes: Vec = Vec::new(); let mut file = BufReader::new(File::open(path)?); file.read_to_end(&mut bytes)?; From 557294d7f6442f8057afa593db65ccd41897782c Mon Sep 17 00:00:00 2001 From: itsmeow Date: Mon, 25 Dec 2023 23:12:20 -0500 Subject: [PATCH 25/35] Add to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3490242b..49757e38 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ The default features are: Additional features are: * batchnoise: Discrete Batched Perlin-like Noise, fast and multi-threaded - sent over once instead of having to query for every tile. * hash: Faster replacement for `md5`, support for SHA-1, SHA-256, and SHA-512. Requires OpenSSL on Linux. +* iconforge: A much faster replacement for the spritesheet generation system used by [/tg/station]. * pathfinder: An a* pathfinder used for finding the shortest path in a static node map. Not to be used for a non-static map. * redis_pubsub: Library for sending and receiving messages through Redis. * redis_reliablequeue: Library for using a reliable queue pattern through Redis. From a8d1bf13c5fa177c6eabafe249f33e4086972496 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Tue, 26 Dec 2023 20:17:15 -0500 Subject: [PATCH 26/35] Update dmi, add caching logic. --- Cargo.lock | 4 +- Cargo.toml | 7 +- dmsrc/iconforge.dm | 40 +++++- src/byond.rs | 6 +- src/hash.rs | 9 +- src/iconforge.rs | 327 ++++++++++++++++++++++++++++++++++----------- 6 files changed, 299 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbbbf567..823bccfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "dmi" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754c4784da61ad948b8bc868b3f639b102f798567576044fe67f5b7c70044c8f" +checksum = "a99b0d2dae3ac6a37fe16dae423852e78b4d3279f86fe0958bd0549540952ffc" dependencies = [ "bitflags 2.4.1", "deflate", diff --git a/Cargo.toml b/Cargo.toml index 0e3c016b..b961771f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,9 +62,9 @@ rayon = { version = "1.8", optional = true } dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } -dmi = { version = "0.3.3", optional = true } -tracy_full = { version = "1.6.1", optional = true } -#tracy_full = { version = "1.6.1", optional = true, features = ["enable"] } +dmi = { version = "0.3.4", optional = true } +#tracy_full = { version = "1.6.1", optional = true } +tracy_full = { version = "1.6.1", optional = true, features = ["enable"] } [features] default = [ @@ -139,6 +139,7 @@ hash = [ iconforge = [ "dashmap", "dep:dmi", + "hash", "image", "jobs", "once_cell", diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index d8f2ed1b..acb52f6d 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -1,8 +1,14 @@ /// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].png +/// 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. +/// /// Spritesheet will contain all sprites listed within "sprites". /// "sprites" format: /// list( -/// "sprite_name" = list( +/// "sprite_name" = list( // <--- this list is a [SPRITE_OBJECT] /// icon_file = 'icons/path_to/an_icon.dmi', /// icon_state = "some_icon_state", /// dir = SOUTH, @@ -13,21 +19,41 @@ /// ) /// TRANSFORM_OBJECT format: /// list("type" = "BlendColor", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) -/// list("type" = "BlendIcon", "icon" = sprite_object, "blend_mode" = ICON_OVERLAY) +/// list("type" = "BlendIcon", "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY) /// list("type" = "Scale", "width" = 32, "height" = 32) /// list("type" = "Crop", "x1" = 0, "y1" = 0, "x2" = 32, "y2" = 32) +/// /// Returns a SpritesheetResult as JSON, containing fields: /// list( -/// "sizes" = list("32x32", "64x64", ...etc) -/// "sprites" = list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...) +/// "sizes" = list("32x32", "64x64", ...), +/// "sprites" = list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...), +/// "dmi_hashes" = list("icons/path_to/an_icon.dmi" = "d6325c5b4304fb03", ...), +/// "sprites_hash" = "a2015e5ff403fb5c", // This is the xxh64 hash of the INPUT field "sprites". /// "error" = "[A string, empty if there were no errors.]" /// ) -/// In the event of an unrecoverable error, where the spritesheet could not even generate, returns a string containing the error. -#define rustg_iconforge_generate(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites) +/// 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]") /// Returns a job_id for use with rustg_iconforge_check() -#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites) +#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]") /// Returns the status of a job_id #define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") /// Clears all cached DMIs and images, freeing up memory. /// This should be used after spritesheets are done being generated. #define rustg_iconforge_cleanup RUSTG_CALL(RUST_G, "iconforge_cleanup") +/// Takes in a set of hashes, generate inputs, and DMI filepaths, and compares them to determine cache validity. +/// input_hash: xxh64 hash of "sprites" from the cache. +/// dmi_hashes: xxh64 hashes of the DMIs in a spritesheet, given by `rustg_iconforge_generate` with `hash_icons` enabled. From the cache. +/// sprites: The new input that will be passed to rustg_iconforge_generate(). +/// Returns a CacheResult with the following structure: list( +/// "result": "1" (if cache is valid) or "0" (if cache is invalid) +/// "fail_reason": "" (emtpy string if valid, otherwise a string containing the invalidation reason or an error with ERROR: prefixed.) +/// ) +/// In the case of an unrecoverable panic from within Rust, this function ONLY returns a string containing the error. +#define rustg_iconforge_cache_valid(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid")(input_hash, dmi_hashes, sprites) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid_async")(input_hash, dmi_hashes, sprites) + +#define RUSTG_ICONFORGE_BLEND_COLOR "BlendColor" +#define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon" +#define RUSTG_ICONFORGE_CROP "Crop" +#define RUSTG_ICONFORGE_SCALE "Scale" diff --git a/src/byond.rs b/src/byond.rs index d25507cb..8a79a6c1 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -91,7 +91,7 @@ byond_fn!( } ); -// Print any panics before exiting. +/// Print any panics before exiting. pub fn set_panic_hook() { SET_HOOK.call_once(|| { std::panic::set_hook(Box::new(|panic_info| { @@ -101,8 +101,6 @@ pub fn set_panic_hook() { .create(true) .open("rustg-panic.log") .unwrap(); - file.write_all(Backtrace::capture().to_string().as_bytes()) - .expect("Failed to extract error backtrace"); file.write_all( panic_info .payload() @@ -113,6 +111,8 @@ pub fn set_panic_hook() { .as_bytes(), ) .expect("Failed to extract error payload"); + file.write_all(Backtrace::capture().to_string().as_bytes()) + .expect("Failed to extract error backtrace"); })) }); } diff --git a/src/hash.rs b/src/hash.rs index ee959fe0..1c19ef3a 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -70,17 +70,22 @@ fn hash_algorithm>(name: &str, bytes: B) -> Result { let mut hasher = XxHash64::with_seed(XXHASH_SEED); hasher.write(bytes.as_ref()); Ok(format!("{:x}", hasher.finish())) + }, + "xxh64_fixed" => { + let mut hasher = XxHash64::with_seed(17479268743136991876); + hasher.write(bytes.as_ref()); + Ok(format!("{:x}", hasher.finish())) } "base64" => Ok(base64::prelude::BASE64_STANDARD.encode(bytes.as_ref())), _ => Err(Error::InvalidAlgorithm), } } -fn string_hash(algorithm: &str, string: &str) -> Result { +pub fn string_hash(algorithm: &str, string: &str) -> Result { hash_algorithm(algorithm, string) } -fn file_hash(algorithm: &str, path: &str) -> Result { +pub fn file_hash(algorithm: &str, path: &str) -> Result { let mut bytes: Vec = Vec::new(); let mut file = BufReader::new(File::open(path)?); file.read_to_end(&mut bytes)?; diff --git a/src/iconforge.rs b/src/iconforge.rs index 07ae9be6..ac132cbe 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1,7 +1,10 @@ // DMI spritesheet generator // Developed by itsmeow -use crate::error::Error; use crate::jobs; +use crate::{ + error::Error, + hash::{file_hash, string_hash}, +}; use dashmap::DashMap; use dmi::{ dirs::Dirs, @@ -11,6 +14,8 @@ use image::{Pixel, RgbaImage}; use once_cell::sync::Lazy; use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::sync::RwLock; use std::{ collections::HashMap, fs::File, @@ -20,32 +25,39 @@ use std::{ }; use tracy_full::{frame, zone}; use twox_hash::XxHash64; +type SpriteJsonMap = HashMap, BuildHasherDefault>; +/// This is used to save time decoding 'sprites' between the cache step and the generate step. +static SPRITES_TO_JSON: Lazy>> = Lazy::new(|| { + Arc::new(Mutex::new(HashMap::with_hasher(BuildHasherDefault::< + XxHash64, + >::default()))) +}); +/// A cache of DMI filepath -> Icon objects. static ICON_FILES: Lazy, BuildHasherDefault>> = Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::::default())); +/// A cache of icon_hash_input to RgbaImage (with transforms applied! This can only contain COMPLETED sprites). static ICON_STATES: Lazy>> = Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::::default())); -/// This is an array mapping the DIR number from above to a position in DMIs, such that DIR_TO_INDEX[DIR] = dmi::dirs::DIR_ORDERING.indexof(DIR) -/// 255 is invalid. -const DIR_TO_INDEX: [u8; 11] = [255, 1, 0, 255, 2, 6, 4, 255, 3, 7, 5]; - -byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites) { +byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites, hash_icons) { let file_path = file_path.to_owned(); let spritesheet_name = spritesheet_name.to_owned(); let sprites = sprites.to_owned(); - Some(match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites) { + let hash_icons = hash_icons.to_owned(); + Some(match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites, &hash_icons) { Ok(o) => o.to_string(), Err(e) => e.to_string() }) }); -byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites) { +byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons) { // Take ownership before passing let file_path = file_path.to_owned(); let spritesheet_name = spritesheet_name.to_owned(); let sprites = sprites.to_owned(); + let hash_icons = hash_icons.to_owned(); Some(jobs::start(move || { - match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites) { + match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites, &hash_icons) { Ok(o) => o.to_string(), Err(e) => e.to_string() } @@ -64,10 +76,34 @@ byond_fn!( } ); +byond_fn!(fn iconforge_cache_valid(input_hash, dmi_hashes, sprites) { + let input_hash = input_hash.to_owned(); + let dmi_hashes = dmi_hashes.to_owned(); + let sprites = sprites.to_owned(); + Some(match cache_valid_safe(&input_hash, &dmi_hashes, &sprites) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }) +}); + +byond_fn!(fn iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) { + let input_hash = input_hash.to_owned(); + let dmi_hashes = dmi_hashes.to_owned(); + let sprites = sprites.to_owned(); + Some(jobs::start(move || { + match cache_valid_safe(&input_hash, &dmi_hashes, &sprites) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + } + })) +}); + #[derive(Serialize)] struct SpritesheetResult { sizes: Vec, sprites: DashMap>, + dmi_hashes: DashMap, + sprites_hash: String, error: String, } @@ -88,7 +124,7 @@ struct IconObject { icon_hash_input: String, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] struct IconObjectIO { icon_file: String, icon_state: String, @@ -135,7 +171,7 @@ impl IconObject { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] #[serde(tag = "type")] enum TransformIO { BlendColor { color: String, blend_mode: u8 }, @@ -152,13 +188,148 @@ enum Transform { Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, } +fn cache_valid_safe( + input_hash: &str, + dmi_hashes: &str, + sprites: &str, +) -> std::result::Result { + match std::panic::catch_unwind(|| { + let result = cache_valid(input_hash, dmi_hashes, sprites); + frame!(); + result + }) { + Ok(o) => o, + Err(e) => { + let message: Option = e + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| e.downcast_ref::().cloned()); + Err(Error::IconForge( + message + .unwrap_or( String::from("Failed to stringify panic! Check rustg-panic.log")) + .to_owned(), + )) + } + } +} + +#[derive(Serialize)] +struct CacheResult { + result: String, + fail_reason: String, +} + +fn cache_valid(input_hash: &str, dmi_hashes_in: &str, sprites_in: &str) -> Result { + zone!("cache_valid"); + let sprites_hash = string_hash("xxh64_fixed", sprites_in)?; + if sprites_hash != input_hash { + return Ok(serde_json::to_string::(&CacheResult { + result: String::from("0"), + fail_reason: String::from("Input hash did not match."), + })?); + } + let dmi_hashes: DashMap; + { + zone!("from_json_hashes"); + dmi_hashes = serde_json::from_str::>(dmi_hashes_in)?; + } + let mut sprites_json = SPRITES_TO_JSON.lock().unwrap(); + let sprites = match sprites_json.get(&sprites_hash) { + Some(sprites) => sprites, + None => { + zone!("from_json_sprites"); + { + sprites_json.insert( + sprites_hash.clone(), + serde_json::from_str::>(sprites_in)?, + ); + } + sprites_json.get(&sprites_hash).unwrap() + } + }; + + let dmis: HashSet; + + { + zone!("collect_dmis"); + dmis = sprites + .par_iter() + .flat_map(|(_, icon)| { + icon_to_icons_io(icon) + .into_iter() + .map(|icon| icon.icon_file.clone()) + .collect::>() + }) + .collect(); + } + + drop(sprites_json); + + if dmis.len() > dmi_hashes.len() { + return Ok(serde_json::to_string::(&CacheResult { + result: String::from("0"), + fail_reason: format!("Input hash matched, but more DMIs exist than DMI hashes provided ({} DMIs, {} DMI hashes).", dmis.len(), dmi_hashes.len()), + })?); + } + + let fail_reason: Arc>> = Arc::new(RwLock::new(None)); + { + zone!("check_dmis"); + dmis.into_par_iter().for_each(|dmi_path| { + zone!("check_dmi"); + if fail_reason.read().unwrap().is_some() { + return; + } + match dmi_hashes.get(&dmi_path) { + Some(hash) => { + zone!("hash_dmi"); + match file_hash("xxh64_fixed", &dmi_path) { + Ok(new_hash) => { + zone!("check_match"); + if new_hash != *hash { + if fail_reason.read().unwrap().is_some() { + return; + } + *fail_reason.write().unwrap() = Some(format!("Input hash matched, but dmi_hash was invalid DMI: '{}' (stored hash: {}, new hash: {})", dmi_path, hash.clone(), new_hash)); + } + }, + Err(err) => { + if fail_reason.read().unwrap().is_some() { + return; + } + *fail_reason.write().unwrap() = Some(format!("ERROR: Error while hashing dmi_path '{}': {}", dmi_path, err.to_string())); + } + } + } + None => { + if fail_reason.read().unwrap().is_some() { + return; + } + *fail_reason.write().unwrap() = Some(format!("Input hash matched, but no dmi_hash existed for DMI: '{}'", dmi_path)); + } + } + }); + } + if let Some(err) = fail_reason.read().unwrap().clone() { + return Ok(serde_json::to_string::(&CacheResult { + result: String::from("0"), + fail_reason: err, + })?); + } + Ok(serde_json::to_string::(&CacheResult { + result: String::from("1"), + fail_reason: String::from(""), + })?) +} + fn generate_spritesheet_safe( file_path: &str, spritesheet_name: &str, sprites: &str, + hash_icons: &str, ) -> std::result::Result { match std::panic::catch_unwind(|| { - let result = generate_spritesheet(file_path, spritesheet_name, sprites); + let result = generate_spritesheet(file_path, spritesheet_name, sprites, hash_icons); frame!(); result }) { @@ -170,7 +341,7 @@ fn generate_spritesheet_safe( .or_else(|| e.downcast_ref::().cloned()); Err(Error::IconForge( message - .unwrap_or("Failed to stringify panic! Check rustg-panic.log".to_string()) + .unwrap_or( String::from("Failed to stringify panic! Check rustg-panic.log")) .to_owned(), )) } @@ -181,10 +352,12 @@ fn generate_spritesheet( file_path: &str, spritesheet_name: &str, sprites: &str, + hash_icons: &str, ) -> std::result::Result { zone!("generate_spritesheet"); - + let hash_icons: bool = hash_icons == "1"; let error = Arc::new(Mutex::new(Vec::::new())); + let dmi_hashes = DashMap::::new(); let size_to_icon_objects = Arc::new(Mutex::new(HashMap::>::new())); let sprites_objects = @@ -199,11 +372,18 @@ fn generate_spritesheet( >::with_hasher( BuildHasherDefault::::default() ))); - let input; + let sprites_hash; { - zone!("from_json"); - input = serde_json::from_str::>(sprites)?; + zone!("compute_sprites_hash"); + sprites_hash = string_hash("xxh64_fixed", sprites)?; } + let input = match SPRITES_TO_JSON.lock().unwrap().get(&sprites_hash) { + Some(sprites) => sprites.clone(), + None => { + zone!("from_json_sprites"); // byondapi, save us + serde_json::from_str::>(sprites)? + } + }; let mut sprites_map = HashMap::::new(); { zone!("io_to_mem"); @@ -220,11 +400,25 @@ fn generate_spritesheet( sprites_map.par_iter().for_each(|(sprite_name, icon)| { zone!("sprite_to_icons"); - icon_to_icons(icon).into_par_iter().for_each(|icon| { - if let Err(err) = icon_to_dmi(icon) { - error.lock().unwrap().push(err); - } - }); + icon_to_icons(icon) + .into_par_iter() + .for_each(|icon| match icon_to_dmi(icon) { + Ok(_) => { + if hash_icons { + zone!("hash_dmi"); + match file_hash("xxh64_fixed", &icon.icon_file) { + Ok(hash) => { + zone!("insert_dmi_hash"); + dmi_hashes.insert(icon.icon_file.clone(), hash); + } + Err(err) => { + error.lock().unwrap().push(err.to_string()); + } + }; + } + } + Err(err) => error.lock().unwrap().push(err), + }); { zone!("map_to_base"); @@ -245,7 +439,7 @@ fn generate_spritesheet( }); // cache this here so we don't generate the same string 5000 times - let sprite_name = "N/A, in tree generation stage".to_string(); + let sprite_name = String::from("N/A, in tree generation stage"); // Map duplicate transform sets into a tree. // This is beneficial in the case where we have the same base image, and the same set of transforms, but change 1 or 2 things at the end. @@ -262,7 +456,7 @@ fn generate_spritesheet( error .lock() .unwrap() - .push("Somehow found no icon for a tree.".to_string()); + .push( String::from("Somehow found no icon for a tree.")); return; } }; @@ -353,7 +547,7 @@ fn generate_spritesheet( // all images have been returned now, so continue... // cache this here so we don't generate the same string 5000 times - let sprite_name = "N/A, in final generation stage".to_string(); + let sprite_name = String::from("N/A, in final generation stage"); // Get all the sprites and spew them onto a spritesheet. size_to_icon_objects @@ -417,6 +611,8 @@ fn generate_spritesheet( let returned = SpritesheetResult { sizes, sprites: sprites_objects, + dmi_hashes, + sprites_hash, error: error.lock().unwrap().join("\n"), }; Ok(serde_json::to_string::(&returned)?) @@ -428,7 +624,7 @@ fn transform_leaves(icons: &Vec<&IconObject>, image: RgbaImage, depth: u8) -> Re zone!("transform_leaf"); if depth > 128 { return Err( - "Transform depth exceeded 128. https://www.youtube.com/watch?v=CUjrySBwi5Q".to_string(), + String::from("Transform depth exceeded 128. https://www.youtube.com/watch?v=CUjrySBwi5Q"), ); } let next_transforms = DashMap::>::new(); @@ -492,6 +688,9 @@ fn transform_leaves(icons: &Vec<&IconObject>, image: RgbaImage, depth: u8) -> Re /// Converts an IO icon to one with icon_hash_input fn icon_from_io(icon_in: IconObjectIO) -> IconObject { zone!("icon_from_io"); + // TODO: can probably convert this function to just lazily attaching icostring to a RefCell<> or something + // This alternative type system is too verbose and wasteful of processing time. + // https://doc.rust-lang.org/reference/interior-mutability.html let mut result = IconObject { icon_file: icon_in.icon_file, icon_state: icon_in.icon_state, @@ -535,6 +734,22 @@ fn icon_to_icons(icon_in: &IconObject) -> Vec<&IconObject> { icons } +/// icon_to_icons but for IO icons. +fn icon_to_icons_io(icon_in: &IconObjectIO) -> Vec<&IconObjectIO> { + zone!("icon_to_icons_io"); + let mut icons: Vec<&IconObjectIO> = Vec::new(); + icons.push(icon_in); + for transform in &icon_in.transform { + if let TransformIO::BlendIcon { icon, .. } = transform { + let nested = icon_to_icons_io(icon); + for icon in nested { + icons.push(icon) + } + } + } + icons +} + /// Given an IconObject, returns a DMI Icon structure and caches it. fn icon_to_dmi(icon: &IconObject) -> Result, String> { zone!("icon_to_dmi"); @@ -594,7 +809,7 @@ fn icon_to_image( return Ok((entry.value().clone(), true)); } if must_be_cached { - return Err("Image not found in cache!".to_string()); + return Err( String::from("Image not found in cache!")); } } let dmi = icon_to_dmi(icon)?; @@ -617,59 +832,17 @@ fn icon_to_image( )); } }; - { - zone!("determine_icon_state_validity"); - if state.frames < icon.frame { - return Err(format!( - "Could not find associated frame: {} in {} icon_state {} - dirs: {} frames: {}", - icon.frame, sprite_name, icon.icon_state, state.dirs, state.frames - )); - } - let dir = match dmi::dirs::Dirs::from_bits(icon.dir) { - Some(dir) => dir, - None => { - return Err(format!( - "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", - icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name - )); - } - }; - if (state.dirs == 1 && dir != Dirs::SOUTH) - || (state.dirs == 4 && !dmi::dirs::CARDINAL_DIRS.contains(&dir)) - || (state.dirs == 8 && !dmi::dirs::ALL_DIRS.contains(&dir)) - { - return Err(format!( - "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", - icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name - )); - } - } - let mut icon_idx = match DIR_TO_INDEX.get(icon.dir as usize) { - Some(idx) if *idx == 255 => { - return Err(format!( - "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", - icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name - )); - } - Some(idx) => *idx as u32, + + let dir = match Dirs::from_bits(icon.dir) { + Some(dir) => dir, None => { - return Err(format!( - "Invalid dir {} or size of dirs {} in {} state: {} for sprite {}", - icon.dir, state.dirs, icon.icon_file, icon.icon_state, sprite_name - )); + return Err(format!("Invalid dir number {} for {}", icon.dir, sprite_name)); } }; - if icon.frame > 1 { - // Add one so zero scales properly - icon_idx = (icon_idx + 1) * icon.frame - 1 - } - Ok(match state.images.get(icon_idx as usize) { - Some(image) => (image.to_rgba8(), false), - None => { - return Err( - format!("Out of bounds index {} in icon_state {} for sprite {} - Maximum index: {} (frames: {}, dirs: {})", - icon_idx, icon.icon_state, sprite_name, state.images.len(), state.dirs, state.frames - )); + Ok(match state.get_image(&dir, icon.frame) { + Ok(image) => (image.to_rgba8(), false), + Err(err) => { + return Err(format!("Error getting image for {}: {}", sprite_name, err)); } }) } From 41d5bcaaf77b9a4ef342eea0c771ed2cf54bd379 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Tue, 26 Dec 2023 20:30:04 -0500 Subject: [PATCH 27/35] Address reviews --- Cargo.toml | 3 +- src/byond.rs | 2 +- src/hash.rs | 2 +- src/iconforge.rs | 131 +++++++++++++++++++++++++---------------------- 4 files changed, 72 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b961771f..a9d11da3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,8 +63,7 @@ dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } dmi = { version = "0.3.4", optional = true } -#tracy_full = { version = "1.6.1", optional = true } -tracy_full = { version = "1.6.1", optional = true, features = ["enable"] } +tracy_full = { version = "1.6.1", optional = true } [features] default = [ diff --git a/src/byond.rs b/src/byond.rs index 8a79a6c1..9412c101 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -112,7 +112,7 @@ pub fn set_panic_hook() { ) .expect("Failed to extract error payload"); file.write_all(Backtrace::capture().to_string().as_bytes()) - .expect("Failed to extract error backtrace"); + .expect("Failed to extract error backtrace"); })) }); } diff --git a/src/hash.rs b/src/hash.rs index 1c19ef3a..7980e741 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -70,7 +70,7 @@ fn hash_algorithm>(name: &str, bytes: B) -> Result { let mut hasher = XxHash64::with_seed(XXHASH_SEED); hasher.write(bytes.as_ref()); Ok(format!("{:x}", hasher.finish())) - }, + } "xxh64_fixed" => { let mut hasher = XxHash64::with_seed(17479268743136991876); hasher.write(bytes.as_ref()); diff --git a/src/iconforge.rs b/src/iconforge.rs index ac132cbe..d8bd6385 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -206,7 +206,9 @@ fn cache_valid_safe( .or_else(|| e.downcast_ref::().cloned()); Err(Error::IconForge( message - .unwrap_or( String::from("Failed to stringify panic! Check rustg-panic.log")) + .unwrap_or(String::from( + "Failed to stringify panic! Check rustg-panic.log", + )) .to_owned(), )) } @@ -341,7 +343,9 @@ fn generate_spritesheet_safe( .or_else(|| e.downcast_ref::().cloned()); Err(Error::IconForge( message - .unwrap_or( String::from("Failed to stringify panic! Check rustg-panic.log")) + .unwrap_or(String::from( + "Failed to stringify panic! Check rustg-panic.log", + )) .to_owned(), )) } @@ -456,7 +460,7 @@ fn generate_spritesheet( error .lock() .unwrap() - .push( String::from("Somehow found no icon for a tree.")); + .push(String::from("Somehow found no icon for a tree.")); return; } }; @@ -547,7 +551,7 @@ fn generate_spritesheet( // all images have been returned now, so continue... // cache this here so we don't generate the same string 5000 times - let sprite_name = String::from("N/A, in final generation stage"); + let sprite_name = String::from("N/A, in final generation stage"); // Get all the sprites and spew them onto a spritesheet. size_to_icon_objects @@ -623,9 +627,9 @@ fn generate_spritesheet( fn transform_leaves(icons: &Vec<&IconObject>, image: RgbaImage, depth: u8) -> Result<(), String> { zone!("transform_leaf"); if depth > 128 { - return Err( - String::from("Transform depth exceeded 128. https://www.youtube.com/watch?v=CUjrySBwi5Q"), - ); + return Err(String::from( + "Transform depth exceeded 128. https://www.youtube.com/watch?v=CUjrySBwi5Q", + )); } let next_transforms = DashMap::>::new(); let errors = Mutex::new(Vec::::new()); @@ -809,7 +813,7 @@ fn icon_to_image( return Ok((entry.value().clone(), true)); } if must_be_cached { - return Err( String::from("Image not found in cache!")); + return Err(String::from("Image not found in cache!")); } } let dmi = icon_to_dmi(icon)?; @@ -836,7 +840,10 @@ fn icon_to_image( let dir = match Dirs::from_bits(icon.dir) { Some(dir) => dir, None => { - return Err(format!("Invalid dir number {} for {}", icon.dir, sprite_name)); + return Err(format!( + "Invalid dir number {} for {}", + icon.dir, sprite_name + )); } }; Ok(match state.get_image(&dir, icon.frame) { @@ -898,7 +905,7 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S for y in 0..image.height() { let px = image.get_pixel_mut(x, y); let pixel = px.channels(); - let blended = blend_u8(pixel, &color2, *blend_mode); + let blended = Rgba::blend_u8(pixel, &color2, *blend_mode); *px = image::Rgba::(blended); } @@ -919,7 +926,7 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S let pixel_1 = px1.channels(); let pixel_2 = px2.channels(); - let blended = blend_u8(pixel_1, pixel_2, *blend_mode); + let blended = Rgba::blend_u8(pixel_1, pixel_2, *blend_mode); *px1 = image::Rgba::(blended); } @@ -1004,6 +1011,7 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S Ok(()) } +#[derive(Clone)] struct Rgba { r: f32, g: f32, @@ -1022,7 +1030,7 @@ impl Rgba { } fn from_array(rgba: &[u8]) -> Rgba { - Rgba { + Self { r: rgba[0] as f32, g: rgba[1] as f32, b: rgba[2] as f32, @@ -1030,12 +1038,11 @@ impl Rgba { } } - fn map_each( - color: Rgba, - color2: Rgba, - rgb_fn: &dyn Fn(f32, f32) -> f32, - a_fn: &dyn Fn(f32, f32) -> f32, - ) -> Rgba { + fn map_each(color: &Rgba, color2: &Rgba, rgb_fn: F, a_fn: T) -> Rgba + where + F: Fn(f32, f32) -> f32, + T: Fn(f32, f32) -> f32, + { Rgba { r: rgb_fn(color.r, color2.r), g: rgb_fn(color.g, color2.g), @@ -1044,12 +1051,11 @@ impl Rgba { } } - fn map_each_a( - color: Rgba, - color2: Rgba, - rgb_fn: &dyn Fn(f32, f32, f32, f32) -> f32, - a_fn: &dyn Fn(f32, f32) -> f32, - ) -> Rgba { + fn map_each_a(color: &Rgba, color2: &Rgba, rgb_fn: F, a_fn: T) -> Rgba + where + F: Fn(f32, f32, f32, f32) -> f32, + T: Fn(f32, f32) -> f32, + { Rgba { r: rgb_fn(color.r, color2.r, color.a, color2.a), g: rgb_fn(color.g, color2.g, color.a, color2.a), @@ -1057,45 +1063,46 @@ impl Rgba { a: a_fn(color.a, color2.a), } } -} -fn blend_u8(color: &[u8], color2: &[u8], blend_mode: u8) -> [u8; 4] { - blend( - Rgba::from_array(color), - Rgba::from_array(color2), - blend_mode, - ) - .into_array() -} + /// Takes two [u8; 4]s, converts them to Rgba structs, then blends them according to blend_mode by calling blend(). + fn blend_u8(color: &[u8], other_color: &[u8], blend_mode: u8) -> [u8; 4] { + Rgba::from_array(color) + .blend(&Rgba::from_array(other_color), blend_mode) + .into_array() + } -/// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. -fn blend(color: Rgba, color2: Rgba, blend_mode: u8) -> Rgba { - match blend_mode { - 0 => Rgba::map_each(color, color2, &|c1, c2| c1 + c2, &f32::min), - 1 => Rgba::map_each(color, color2, &|c1, c2| c2 - c1, &f32::min), - 2 => Rgba::map_each(color, color2, &|c1, c2| c1 * c2 / 255.0, &|a1, a2| { - a1 * a2 / 255.0 - }), - 3 => Rgba::map_each_a( - color, - color2, - &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, - &|a1, a2| { - let high = f32::max(a1, a2); - let low = f32::min(a1, a2); - high + (high * low / 255.0) - }, - ), - 6 => Rgba::map_each_a( - color2, - color, - &|c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, - &|a1, a2| { - let high = f32::max(a1, a2); - let low = f32::min(a1, a2); - high + (high * low / 255.0) - }, - ), - _ => color, + /// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. + fn blend(&self, other_color: &Rgba, blend_mode: u8) -> Rgba { + match blend_mode { + 0 => Rgba::map_each(self, other_color, |c1, c2| c1 + c2, f32::min), + 1 => Rgba::map_each(self, other_color, |c1, c2| c2 - c1, f32::min), + 2 => Rgba::map_each( + self, + other_color, + |c1, c2| c1 * c2 / 255.0, + |a1: f32, a2: f32| a1 * a2 / 255.0, + ), + 3 => Rgba::map_each_a( + self, + other_color, + |c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, + |a1, a2| { + let high = f32::max(a1, a2); + let low = f32::min(a1, a2); + high + (high * low / 255.0) + }, + ), + 6 => Rgba::map_each_a( + other_color, + self, + |c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, + |a1, a2| { + let high = f32::max(a1, a2); + let low = f32::min(a1, a2); + high + (high * low / 255.0) + }, + ), + _ => self.clone(), + } } } From a50e8f194a50344e21db06444b00e7a8f22e2477 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Tue, 26 Dec 2023 20:46:30 -0500 Subject: [PATCH 28/35] Cleanup panic unwind --- src/byond.rs | 25 ++++++++++++++ src/error.rs | 2 ++ src/iconforge.rs | 87 +++++++++++------------------------------------- 3 files changed, 47 insertions(+), 67 deletions(-) diff --git a/src/byond.rs b/src/byond.rs index 9412c101..ec04ce81 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -1,3 +1,4 @@ +use crate::error::Error; use std::{ backtrace::Backtrace, borrow::Cow, @@ -116,3 +117,27 @@ pub fn set_panic_hook() { })) }); } + +/// Utility for BYOND functions to catch panic unwinds safely and return a Result, as expected. +/// Usage: catch_panic(|| internal_safe_function(arguments)) +pub fn catch_panic(f: F) -> Result +where + F: FnOnce() -> Result + std::panic::UnwindSafe, +{ + match std::panic::catch_unwind(|| f()) { + Ok(o) => o, + Err(e) => { + let message: Option = e + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| e.downcast_ref::().cloned()); + Err(Error::Panic( + message + .unwrap_or(String::from( + "Failed to stringify panic! Check rustg-panic.log!", + )) + .to_owned(), + )) + } + } +} diff --git a/src/error.rs b/src/error.rs index 6f8cd736..6c3dbc5d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,6 +64,8 @@ pub enum Error { #[cfg(feature = "iconforge")] #[error("IconForge error: {0}")] IconForge(String), + #[error("Panic during function execution: {0}")] + Panic(String), } impl From for Error { diff --git a/src/iconforge.rs b/src/iconforge.rs index d8bd6385..74ee0e4f 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1,9 +1,10 @@ // DMI spritesheet generator // Developed by itsmeow -use crate::jobs; use crate::{ + byond::catch_panic, error::Error, hash::{file_hash, string_hash}, + jobs, }; use dashmap::DashMap; use dmi::{ @@ -26,7 +27,7 @@ use std::{ use tracy_full::{frame, zone}; use twox_hash::XxHash64; type SpriteJsonMap = HashMap, BuildHasherDefault>; -/// This is used to save time decoding 'sprites' between the cache step and the generate step. +/// This is used to save time decoding 'sprites' a second time between the cache step and the generate step. static SPRITES_TO_JSON: Lazy>> = Lazy::new(|| { Arc::new(Mutex::new(HashMap::with_hasher(BuildHasherDefault::< XxHash64, @@ -44,23 +45,26 @@ byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites, hash_icons let spritesheet_name = spritesheet_name.to_owned(); let sprites = sprites.to_owned(); let hash_icons = hash_icons.to_owned(); - Some(match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites, &hash_icons) { + let result = Some(match catch_panic(|| generate_spritesheet(&file_path, &spritesheet_name, &sprites, &hash_icons)) { Ok(o) => o.to_string(), Err(e) => e.to_string() - }) + }); + frame!(); + result }); byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons) { - // Take ownership before passing let file_path = file_path.to_owned(); let spritesheet_name = spritesheet_name.to_owned(); let sprites = sprites.to_owned(); let hash_icons = hash_icons.to_owned(); Some(jobs::start(move || { - match generate_spritesheet_safe(&file_path, &spritesheet_name, &sprites, &hash_icons) { + let result = match catch_panic(|| generate_spritesheet(&file_path, &spritesheet_name, &sprites, &hash_icons)) { Ok(o) => o.to_string(), Err(e) => e.to_string() - } + }; + frame!(); + result })) }); @@ -80,22 +84,26 @@ byond_fn!(fn iconforge_cache_valid(input_hash, dmi_hashes, sprites) { let input_hash = input_hash.to_owned(); let dmi_hashes = dmi_hashes.to_owned(); let sprites = sprites.to_owned(); - Some(match cache_valid_safe(&input_hash, &dmi_hashes, &sprites) { + let result = Some(match catch_panic(|| cache_valid(&input_hash, &dmi_hashes, &sprites)) { Ok(o) => o.to_string(), Err(e) => e.to_string() - }) + }); + frame!(); + result }); byond_fn!(fn iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) { let input_hash = input_hash.to_owned(); let dmi_hashes = dmi_hashes.to_owned(); let sprites = sprites.to_owned(); - Some(jobs::start(move || { - match cache_valid_safe(&input_hash, &dmi_hashes, &sprites) { + let result = Some(jobs::start(move || { + match catch_panic(|| cache_valid(&input_hash, &dmi_hashes, &sprites)) { Ok(o) => o.to_string(), Err(e) => e.to_string() } - })) + })); + frame!(); + result }); #[derive(Serialize)] @@ -188,33 +196,6 @@ enum Transform { Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, } -fn cache_valid_safe( - input_hash: &str, - dmi_hashes: &str, - sprites: &str, -) -> std::result::Result { - match std::panic::catch_unwind(|| { - let result = cache_valid(input_hash, dmi_hashes, sprites); - frame!(); - result - }) { - Ok(o) => o, - Err(e) => { - let message: Option = e - .downcast_ref::<&'static str>() - .map(|payload| payload.to_string()) - .or_else(|| e.downcast_ref::().cloned()); - Err(Error::IconForge( - message - .unwrap_or(String::from( - "Failed to stringify panic! Check rustg-panic.log", - )) - .to_owned(), - )) - } - } -} - #[derive(Serialize)] struct CacheResult { result: String, @@ -324,34 +305,6 @@ fn cache_valid(input_hash: &str, dmi_hashes_in: &str, sprites_in: &str) -> Resul })?) } -fn generate_spritesheet_safe( - file_path: &str, - spritesheet_name: &str, - sprites: &str, - hash_icons: &str, -) -> std::result::Result { - match std::panic::catch_unwind(|| { - let result = generate_spritesheet(file_path, spritesheet_name, sprites, hash_icons); - frame!(); - result - }) { - Ok(o) => o, - Err(e) => { - let message: Option = e - .downcast_ref::<&'static str>() - .map(|payload| payload.to_string()) - .or_else(|| e.downcast_ref::().cloned()); - Err(Error::IconForge( - message - .unwrap_or(String::from( - "Failed to stringify panic! Check rustg-panic.log", - )) - .to_owned(), - )) - } - } -} - fn generate_spritesheet( file_path: &str, spritesheet_name: &str, From bfd48f15a25d3e3179378ee71eae59474f40c970 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Tue, 26 Dec 2023 21:11:26 -0500 Subject: [PATCH 29/35] Fix lint failure --- src/byond.rs | 2 +- src/iconforge.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/byond.rs b/src/byond.rs index ec04ce81..0629d27e 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -124,7 +124,7 @@ pub fn catch_panic(f: F) -> Result where F: FnOnce() -> Result + std::panic::UnwindSafe, { - match std::panic::catch_unwind(|| f()) { + match std::panic::catch_unwind(f) { Ok(o) => o, Err(e) => { let message: Option = e diff --git a/src/iconforge.rs b/src/iconforge.rs index 74ee0e4f..0b2804f9 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -280,7 +280,7 @@ fn cache_valid(input_hash: &str, dmi_hashes_in: &str, sprites_in: &str) -> Resul if fail_reason.read().unwrap().is_some() { return; } - *fail_reason.write().unwrap() = Some(format!("ERROR: Error while hashing dmi_path '{}': {}", dmi_path, err.to_string())); + *fail_reason.write().unwrap() = Some(format!("ERROR: Error while hashing dmi_path '{}': {}", dmi_path, err)); } } } From 98beb2b498339cf89efd9546ec5226f7f016fba2 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 29 Dec 2023 00:27:33 -0500 Subject: [PATCH 30/35] Fix bounds expansion crops, and properly index crops from 1,1 --- dmsrc/iconforge.dm | 2 +- src/iconforge.rs | 77 ++++++++++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index acb52f6d..b8e18ed5 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -21,7 +21,7 @@ /// list("type" = "BlendColor", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) /// list("type" = "BlendIcon", "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY) /// list("type" = "Scale", "width" = 32, "height" = 32) -/// list("type" = "Crop", "x1" = 0, "y1" = 0, "x2" = 32, "y2" = 32) +/// list("type" = "Crop", "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // (BYOND icons index from 1,1 to the upper bound, inclusive) /// /// Returns a SpritesheetResult as JSON, containing fields: /// list( diff --git a/src/iconforge.rs b/src/iconforge.rs index 0b2804f9..4be498ab 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -906,12 +906,17 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S } Transform::Crop { x1, y1, x2, y2 } => { zone!("crop"); + let i_width = image.width(); let i_height = image.height(); let mut x1 = *x1; let mut y1 = *y1; let mut x2 = *x2; let mut y2 = *y2; + // BYOND indexes from 1,1! how silly of them. We'll just fix this here. + // Crop(1,1,1,1) is a valid statement. Save us. + y1 -= 1; + x1 -= 1; if x2 <= x1 || y2 <= y1 { return Err(format!( "Invalid bounds {} {} to {} {} in crop transform", @@ -919,46 +924,58 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S )); } - // convert from BYOND (0,0 is bottom left) to Rust (0,0 is top left) + // Convert from BYOND (0,0 is bottom left) to Rust (0,0 is top left) + // BYOND also includes the upper bound let y2_old = y2; y2 = i_height as i32 - y1; y1 = i_height as i32 - y2_old; - let mut width = x2 - x1; - let mut height = y2 - y1; - + // Check for silly expansion crops and add transparency in the gaps. if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { + // The amount the blank icon's size should increase by. + let mut width_inc: u32 = (x2 - i_width as i32).max(0) as u32; + let mut height_inc: u32 = (y2 - i_height as i32).max(0) as u32; + // Where to position the icon within our blank space. + let mut x_offset: u32 = 0; + let mut y_offset: u32 = 0; + // Make room to place the image further in, and change our bounds to match. + if x1 < 0 { + x2 += x1.abs(); + x_offset += x1.unsigned_abs(); + width_inc += x1.unsigned_abs(); + x1 = 0; + } + if y1 < 0 { + y2 += y1.abs(); + y_offset += y1.unsigned_abs(); + height_inc += y1.unsigned_abs(); + y1 = 0; + } let mut blank_img: image::ImageBuffer, Vec> = - RgbaImage::from_fn(width as u32, height as u32, |_x, _y| { + RgbaImage::from_fn(i_width + width_inc, i_height + height_inc, |_x, _y| { image::Rgba([0, 0, 0, 0]) }); - image::imageops::overlay( - &mut blank_img, + + image::imageops::overlay(&mut blank_img, image, x_offset as i64, y_offset as i64); + *image = image::imageops::crop_imm( + &blank_img, + x1 as u32, + y1 as u32, + (x2 - x1) as u32, + (y2 - y1) as u32, + ) + .to_image(); + } else { + // Normal bounds crop. Hooray! + *image = image::imageops::crop_imm( image, - if x1 < 0 { (x1).abs() as i64 } else { 0 } - - if x1 > i_width as i32 { - (x1 - i_width as i32) as i64 - } else { - 0 - }, - if y1 < 0 { (y1).abs() as i64 } else { 0 } - - if x1 > i_width as i32 { - (x1 - i_width as i32) as i64 - } else { - 0 - }, - ); - *image = blank_img; - x1 = std::cmp::max(0, x1); - x2 = std::cmp::min(i_width as i32, x2); - y1 = std::cmp::max(0, y1); - y2 = std::cmp::min(i_height as i32, y2); - width = x2 - x1; - height = y2 - y1; + x1 as u32, + y1 as u32, + (x2 - x1) as u32, + (y2 - y1) as u32, + ) + .to_image(); } - *image = - image::imageops::crop_imm(image, x1 as u32, y1 as u32, width as u32, height as u32) - .to_image(); } } Ok(()) From 5d7a25ee32482eab0aad81b4c7f746706385c6de Mon Sep 17 00:00:00 2001 From: itsmeow Date: Fri, 29 Dec 2023 01:16:06 -0500 Subject: [PATCH 31/35] Don't multiply by alpha if the base alpha is 0 --- src/iconforge.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index 4be498ab..b477a487 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -956,7 +956,7 @@ fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), S image::Rgba([0, 0, 0, 0]) }); - image::imageops::overlay(&mut blank_img, image, x_offset as i64, y_offset as i64); + image::imageops::replace(&mut blank_img, image, x_offset as i64, y_offset as i64); *image = image::imageops::crop_imm( &blank_img, x1 as u32, @@ -1055,7 +1055,12 @@ impl Rgba { 3 => Rgba::map_each_a( self, other_color, - |c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, + |c1, c2, c1_a, c2_a| { + if c1_a == 0.0 { + return c2; + } + c1 + (c2 - c1) * c2_a / 255.0 + }, |a1, a2| { let high = f32::max(a1, a2); let low = f32::min(a1, a2); @@ -1065,7 +1070,12 @@ impl Rgba { 6 => Rgba::map_each_a( other_color, self, - |c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, + |c1, c2, c1_a, c2_a| { + if c1_a == 0.0 { + return c2; + } + c1 + (c2 - c1) * c2_a / 255.0 + }, |a1, a2| { let high = f32::max(a1, a2); let low = f32::min(a1, a2); From 9724ab947b9ba8b8089aa98d674604ca073fe2f3 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 10 Jan 2024 01:11:46 -0600 Subject: [PATCH 32/35] Fix subtract blending --- src/iconforge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index b477a487..ab866c72 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -1045,7 +1045,7 @@ impl Rgba { fn blend(&self, other_color: &Rgba, blend_mode: u8) -> Rgba { match blend_mode { 0 => Rgba::map_each(self, other_color, |c1, c2| c1 + c2, f32::min), - 1 => Rgba::map_each(self, other_color, |c1, c2| c2 - c1, f32::min), + 1 => Rgba::map_each(self, other_color, |c1, c2| c1 - c2, f32::min), 2 => Rgba::map_each( self, other_color, From ff2d74c8ab28ac91b9d906b010660e1e33bc6f09 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 10 Jan 2024 02:00:04 -0600 Subject: [PATCH 33/35] Don't hash the same DMI 500 times --- src/iconforge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iconforge.rs b/src/iconforge.rs index ab866c72..4aad44ab 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -361,7 +361,7 @@ fn generate_spritesheet( .into_par_iter() .for_each(|icon| match icon_to_dmi(icon) { Ok(_) => { - if hash_icons { + if hash_icons && !dmi_hashes.contains_key(&icon.icon_file) { zone!("hash_dmi"); match file_hash("xxh64_fixed", &icon.icon_file) { Ok(hash) => { From 7b99cba2d59c787adccb95834c9ee3439c734b5f Mon Sep 17 00:00:00 2001 From: itsmeow Date: Tue, 16 Apr 2024 15:50:20 -0500 Subject: [PATCH 34/35] Address reviews --- dmsrc/iconforge.dm | 10 +++++----- src/hash.rs | 2 +- src/iconforge.rs | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm index b8e18ed5..5f6d9bf0 100644 --- a/dmsrc/iconforge.dm +++ b/dmsrc/iconforge.dm @@ -18,10 +18,10 @@ /// ..., /// ) /// TRANSFORM_OBJECT format: -/// list("type" = "BlendColor", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) -/// list("type" = "BlendIcon", "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY) -/// list("type" = "Scale", "width" = 32, "height" = 32) -/// list("type" = "Crop", "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // (BYOND icons index from 1,1 to the upper bound, inclusive) +/// 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_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) /// /// Returns a SpritesheetResult as JSON, containing fields: /// list( @@ -35,7 +35,7 @@ #define rustg_iconforge_generate(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites, "[hash_icons]") /// 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]") -/// Returns the status of a job_id +/// 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. /// This should be used after spritesheets are done being generated. diff --git a/src/hash.rs b/src/hash.rs index 228011ef..eba83f0f 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -72,7 +72,7 @@ fn hash_algorithm>(name: &str, bytes: B) -> Result { Ok(format!("{:x}", hasher.finish())) } "xxh64_fixed" => { - let mut hasher = XxHash64::with_seed(17479268743136991876); + let mut hasher = XxHash64::with_seed(17479268743136991876); // this seed is just a random number that should stay the same between builds and runs hasher.write(bytes.as_ref()); Ok(format!("{:x}", hasher.finish())) } diff --git a/src/iconforge.rs b/src/iconforge.rs index 4aad44ab..40145dea 100644 --- a/src/iconforge.rs +++ b/src/iconforge.rs @@ -671,7 +671,9 @@ fn icon_from_io(icon_in: IconObjectIO) -> IconObject { transform_hash_input: String::new(), icon_hash_input: String::new(), }; - result.gen_icon_hash_input().unwrap(); // unsafe but idc + // This line can panic, but I consider that acceptable considering how annoying "proper" error handling would be + // especially when this failing basically breaks the entire program. The panic will be caught and written to logs anyway. + result.gen_icon_hash_input().unwrap(); result } From b8afdb3d9ed6fe7abac59e73b2a5230601793166 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Sat, 20 Apr 2024 14:26:48 -0500 Subject: [PATCH 35/35] Clippy fix --- src/byond.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/byond.rs b/src/byond.rs index 0629d27e..5c489037 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -97,7 +97,6 @@ pub fn set_panic_hook() { SET_HOOK.call_once(|| { std::panic::set_hook(Box::new(|panic_info| { let mut file = OpenOptions::new() - .write(true) .append(true) .create(true) .open("rustg-panic.log")