Skip to content

Commit 6dc6264

Browse files
committed
IconForge: Headless Icon Generation
1 parent ca4586e commit 6dc6264

File tree

9 files changed

+510
-71
lines changed

9 files changed

+510
-71
lines changed

dmsrc/iconforge.dm

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@
4646
#define rustg_iconforge_generate(file_path, spritesheet_name, sprites, hash_icons, generate_dmi, flatten) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites, "[hash_icons]", "[generate_dmi]", "[flatten]")
4747
/// Returns a job_id for use with rustg_iconforge_check()
4848
#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons, generate_dmi, flatten) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites, "[hash_icons]", "[generate_dmi]", "[flatten]")
49+
/// Creates a single DMI or PNG using 'sprites' as a list of icon states / images.
50+
/// This function is intended for generating icons with only a few states that have little in common with each other, and only one size.
51+
/// For icons with a large number of states, potentially variable sizes, that re-use sets of transforms more than once, or that benefit from caching, use rustg_iconforge_generate.
52+
/// sprites - follows the same format as rustg_iconforge_generate.
53+
/// file_path - the full relative path at which the PNG or DMI will be written. It must be a full filepath such as tmp/my_icon.dmi or my_icon.png
54+
/// flatten - boolean (0 or 1) determines if the DMI output will be flattened to a single frame/dir if unscoped (null/0 dir or frame values).
55+
///
56+
/// Returns a HeadlessResult, decoded to a BYOND list (always, it's not possible for this to panic unless rustg itself has an issue) containing the following fields:
57+
/// list(
58+
/// "file_path" = "tmp/my_icon.dmi" // [whatever you input returned back to you, null if there was a fatal error]
59+
/// "width" = 32 // the width, which is determined by the first entry of 'sprites', null if there was a fatal error
60+
/// "height" = 32 // the height, which is determined by the first entry of 'sprites', null if there was a fatal error
61+
/// "error" = "[A string, null if there were no errors.]"
62+
/// )
63+
#define rustg_iconforge_generate_headless(file_path, sprites, flatten) json_decode(RUSTG_CALL(RUST_G, "iconforge_generate_headless")(file_path, sprites, "[flatten]"))
4964
/// Returns the status of an async job_id, or its result if it is completed. See RUSTG_JOB DEFINEs.
5065
#define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]")
5166
/// Clears all cached DMIs and images, freeing up memory.

src/byond.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ pub fn set_panic_hook() {
123123
#[allow(dead_code)] // Used depending on feature set
124124
/// Utility for BYOND functions to catch panic unwinds safely and return a Result<String, Error>, as expected.
125125
/// Usage: catch_panic(|| internal_safe_function(arguments))
126-
pub fn catch_panic<F>(f: F) -> Result<String, Error>
126+
pub fn catch_panic<F, R>(f: F) -> Result<R, Error>
127127
where
128-
F: FnOnce() -> Result<String, Error> + std::panic::UnwindSafe,
128+
F: FnOnce() -> R + std::panic::UnwindSafe,
129129
{
130130
match std::panic::catch_unwind(f) {
131-
Ok(o) => o,
131+
Ok(o) => Ok(o),
132132
Err(e) => {
133133
let message: Option<String> = e
134134
.downcast_ref::<&'static str>()

src/iconforge/byond.rs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{gags, image_cache, spritesheet};
2-
use crate::{byond::catch_panic, jobs};
2+
use crate::{byond::catch_panic, iconforge::spritesheet::HeadlessResult, jobs};
33
use tracy_full::frame;
44

55
byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites, hash_icons, generate_dmi, flatten) {
@@ -10,7 +10,10 @@ byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites, hash_icons
1010
let generate_dmi = generate_dmi.to_owned();
1111
let flatten = flatten.to_owned();
1212
let result = Some(match catch_panic(|| spritesheet::generate_spritesheet(&file_path, &spritesheet_name, &sprites, &hash_icons, &generate_dmi, &flatten)) {
13-
Ok(o) => o.to_string(),
13+
Ok(o) => match o {
14+
Ok(o) => o,
15+
Err(e) => e.to_string()
16+
},
1417
Err(e) => e.to_string()
1518
});
1619
frame!();
@@ -26,14 +29,40 @@ byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites, hash
2629
let flatten = flatten.to_owned();
2730
Some(jobs::start(move || {
2831
let result = match catch_panic(|| spritesheet::generate_spritesheet(&file_path, &spritesheet_name, &sprites, &hash_icons, &generate_dmi, &flatten)) {
29-
Ok(o) => o.to_string(),
32+
Ok(o) => match o {
33+
Ok(o) => o,
34+
Err(e) => e.to_string()
35+
},
3036
Err(e) => e.to_string()
3137
};
3238
frame!();
3339
result
3440
}))
3541
});
3642

43+
byond_fn!(fn iconforge_generate_headless(file_path, sprites, flatten) {
44+
let file_path = file_path.to_owned();
45+
let sprites = sprites.to_owned();
46+
let flatten = flatten.to_owned();
47+
let result = Some(match catch_panic::<_, HeadlessResult>(|| spritesheet::generate_headless(&file_path, &sprites, &flatten)) {
48+
Ok(o) => match serde_json::to_string::<HeadlessResult>(&o) {
49+
Ok(o) => o,
50+
Err(_) => String::from("{\"error\":\"Serde serialization error\"}") // nigh impossible but whatever
51+
},
52+
Err(e) => match serde_json::to_string::<HeadlessResult>(&HeadlessResult {
53+
file_path: None,
54+
width: None,
55+
height: None,
56+
error: Some(e.to_string()),
57+
}) {
58+
Ok(o) => o,
59+
Err(_) => String::from("{\"error\":\"Serde serialization error\"}")
60+
}
61+
});
62+
frame!();
63+
result
64+
});
65+
3766
byond_fn!(fn iconforge_check(id) {
3867
Some(jobs::check(id))
3968
});
@@ -51,7 +80,10 @@ byond_fn!(fn iconforge_cache_valid(input_hash, dmi_hashes, sprites) {
5180
let dmi_hashes = dmi_hashes.to_owned();
5281
let sprites = sprites.to_owned();
5382
let result = Some(match catch_panic(|| spritesheet::cache_valid(&input_hash, &dmi_hashes, &sprites)) {
54-
Ok(o) => o.to_string(),
83+
Ok(o) => match o {
84+
Ok(o) => o,
85+
Err(e) => e.to_string()
86+
},
5587
Err(e) => e.to_string()
5688
});
5789
frame!();
@@ -64,7 +96,10 @@ byond_fn!(fn iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) {
6496
let sprites = sprites.to_owned();
6597
let result = Some(jobs::start(move || {
6698
match catch_panic(|| spritesheet::cache_valid(&input_hash, &dmi_hashes, &sprites)) {
67-
Ok(o) => o.to_string(),
99+
Ok(o) => match o {
100+
Ok(o) => o,
101+
Err(e) => e.to_string()
102+
},
68103
Err(e) => e.to_string()
69104
}
70105
}));
@@ -77,7 +112,10 @@ byond_fn!(fn iconforge_load_gags_config(config_path, config_json, config_icon_pa
77112
let config_json = config_json.to_owned();
78113
let config_icon_path = config_icon_path.to_owned();
79114
let result = Some(match catch_panic(|| gags::load_gags_config(&config_path, &config_json, &config_icon_path)) {
80-
Ok(o) => o.to_string(),
115+
Ok(o) => match o {
116+
Ok(o) => o,
117+
Err(e) => e.to_string()
118+
},
81119
Err(e) => e.to_string()
82120
});
83121
frame!();
@@ -90,7 +128,10 @@ byond_fn!(fn iconforge_load_gags_config_async(config_path, config_json, config_i
90128
let config_icon_path = config_icon_path.to_owned();
91129
Some(jobs::start(move || {
92130
let result = match catch_panic(|| gags::load_gags_config(&config_path, &config_json, &config_icon_path)) {
93-
Ok(o) => o.to_string(),
131+
Ok(o) => match o {
132+
Ok(o) => o,
133+
Err(e) => e.to_string()
134+
},
94135
Err(e) => e.to_string()
95136
};
96137
frame!();
@@ -103,7 +144,10 @@ byond_fn!(fn iconforge_gags(config_path, colors, output_dmi_path) {
103144
let colors = colors.to_owned();
104145
let output_dmi_path = output_dmi_path.to_owned();
105146
let result = Some(match catch_panic(|| gags::gags(&config_path, &colors, &output_dmi_path)) {
106-
Ok(o) => o.to_string(),
147+
Ok(o) => match o {
148+
Ok(o) => o,
149+
Err(e) => e.to_string()
150+
},
107151
Err(e) => e.to_string()
108152
});
109153
frame!();
@@ -116,7 +160,10 @@ byond_fn!(fn iconforge_gags_async(config_path, colors, output_dmi_path) {
116160
let output_dmi_path = output_dmi_path.to_owned();
117161
Some(jobs::start(move || {
118162
let result = match catch_panic(|| gags::gags(&config_path, &colors, &output_dmi_path)) {
119-
Ok(o) => o.to_string(),
163+
Ok(o) => match o {
164+
Ok(o) => o,
165+
Err(e) => e.to_string()
166+
},
120167
Err(e) => e.to_string()
121168
};
122169
frame!();

src/iconforge/icon_operations.rs

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -553,15 +553,19 @@ pub fn blend_images_other_universal(
553553
.clone()
554554
.unwrap_or(vec![1.0; frames_out as usize]);
555555
let delay_diff = frames_out as i32 - new_delays.len() as i32;
556-
// Extend the number of delays to match frames by copying the first delay
557-
if delay_diff > 0 {
558-
new_delays.extend(vec![
559-
*new_delays.first().unwrap_or(&1.0);
560-
delay_diff as usize
561-
]);
562-
} else if delay_diff < 0 {
563-
// sometimes DMIs can contain more delays than frames because they retain old data
564-
new_delays = new_delays[0..frames_out as usize].to_vec()
556+
match delay_diff.cmp(&0) {
557+
std::cmp::Ordering::Greater => {
558+
// Extend the number of delays to match frames by copying the first delay
559+
new_delays.extend(vec![
560+
*new_delays.first().unwrap_or(&1.0);
561+
delay_diff as usize
562+
]);
563+
}
564+
std::cmp::Ordering::Less => {
565+
// sometimes DMIs can contain more delays than frames because they retain old data
566+
new_delays = new_delays[0..frames_out as usize].to_vec()
567+
}
568+
_ => {}
565569
}
566570
delay_out = Some(new_delays);
567571
} else {
@@ -691,15 +695,20 @@ pub fn blend_images_other(
691695
.clone()
692696
.unwrap_or(vec![1.0; base_icon_state.frames as usize]);
693697
let delay_diff = base_icon_state.frames as i32 - new_delays.len() as i32;
694-
// Extend the number of delays to match frames by copying the first delay
695-
if delay_diff > 0 {
696-
new_delays.extend(vec![
697-
*new_delays.first().unwrap_or(&1.0);
698-
delay_diff as usize
699-
]);
700-
} else if delay_diff < 0 {
701-
// sometimes DMIs can contain more delays than frames because they retain old data
702-
new_delays = new_delays[0..base_icon_state.frames as usize].to_vec()
698+
699+
match delay_diff.cmp(&0) {
700+
std::cmp::Ordering::Greater => {
701+
// Extend the number of delays to match frames by copying the first delay
702+
new_delays.extend(vec![
703+
*new_delays.first().unwrap_or(&1.0);
704+
delay_diff as usize
705+
]);
706+
}
707+
std::cmp::Ordering::Less => {
708+
// sometimes DMIs can contain more delays than frames because they retain old data
709+
new_delays = new_delays[0..base_icon_state.frames as usize].to_vec()
710+
}
711+
_ => {}
703712
}
704713
base_icon_state.delay = Some(new_delays);
705714
} else {
@@ -788,7 +797,7 @@ impl Transform {
788797
frames = new_out.frames;
789798
dirs = new_out.dirs;
790799
delay = new_out.delay;
791-
image_cache::cache_transformed_images(icon, other_image_data);
800+
image_cache::cache_transformed_images(icon, other_image_data, flatten);
792801
}
793802
Transform::Scale { width, height } => {
794803
images = image_data.map_cloned_images(|image| scale(image, *width, *height));
@@ -888,7 +897,7 @@ impl Transform {
888897
}
889898

890899
/// Applies a list of Transforms to UniversalIconData immediately and sequentially, while handling any errors. Optionally flattens to only the first dir and frame.
891-
fn apply_all_transforms(
900+
pub fn apply_all_transforms(
892901
image_data: Arc<UniversalIconData>,
893902
transforms: &Vec<Transform>,
894903
flatten: bool,

src/iconforge/image_cache.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@ static ICON_STATES: Lazy<
1515
DashMap<UniversalIcon, Arc<UniversalIconData>, BuildHasherDefault<XxHash64>>,
1616
> = Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::<XxHash64>::default()));
1717

18-
pub fn image_cache_contains(icon: &UniversalIcon) -> bool {
19-
ICON_STATES.contains_key(icon)
18+
static ICON_STATES_FLAT: Lazy<
19+
DashMap<UniversalIcon, Arc<UniversalIconData>, BuildHasherDefault<XxHash64>>,
20+
> = Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::<XxHash64>::default()));
21+
22+
pub fn image_cache_contains(icon: &UniversalIcon, flatten: bool) -> bool {
23+
if flatten {
24+
ICON_STATES_FLAT.contains_key(icon)
25+
} else {
26+
ICON_STATES.contains_key(icon)
27+
}
2028
}
2129

2230
pub fn image_cache_clear() {
2331
ICON_STATES.clear();
32+
ICON_STATES_FLAT.clear();
2433
}
2534

2635
impl UniversalIcon {
@@ -37,7 +46,11 @@ impl UniversalIcon {
3746
zone!("universal_icon_to_image_data");
3847
if cached {
3948
zone!("check_image_cache");
40-
if let Some(entry) = ICON_STATES.get(self) {
49+
if let Some(entry) = if flatten {
50+
ICON_STATES_FLAT.get(self)
51+
} else {
52+
ICON_STATES.get(self)
53+
} {
4154
return Ok((entry.value().to_owned(), true));
4255
}
4356
if must_be_cached {
@@ -162,9 +175,17 @@ impl UniversalIcon {
162175
}
163176
}
164177

165-
pub fn cache_transformed_images(uni_icon: &UniversalIcon, image_data: Arc<UniversalIconData>) {
178+
pub fn cache_transformed_images(
179+
uni_icon: &UniversalIcon,
180+
image_data: Arc<UniversalIconData>,
181+
flatten: bool,
182+
) {
166183
zone!("cache_transformed_images");
167-
ICON_STATES.insert(uni_icon.to_owned(), image_data.to_owned());
184+
if flatten {
185+
ICON_STATES_FLAT.insert(uni_icon.to_owned(), image_data.to_owned());
186+
} else {
187+
ICON_STATES.insert(uni_icon.to_owned(), image_data.to_owned());
188+
}
168189
}
169190

170191
/* ---- DMI CACHING ---- */

0 commit comments

Comments
 (0)