IconForge: Antag and species icons, greyscale previews optimization (#94954)

## About The Pull Request

Converts species and antagonist icon generation to the batched
spritesheet system using IconForge, thanks to the new
`get_flat_uni_icon` implementation. Unfortunately the cost of *building*
the sprite is still expensive (GFI is always expensive, even a fancy
list-based one), but the generation is SIGNIFICANTLY faster. We will see
evidence of parity in the screenshot tests. but here:

<img width="892" height="634" alt="image"
src="https://github.com/user-attachments/assets/2a17f2e3-c024-41f6-9d1e-c2cb70642a81"
/>

The main advantage is that species and antag icons can now take
advantage of the development-time smart cache which invalidates
automatically. On the server this PR does very little except make antag
icon generation a little bit more likely to find and announce errors
(BYOND has a habit of silently eating weird icon proc calls).

Also optimizes the greyscale preview generator from #90940 (~2x speedup)
using `rustg_iconforge_generate_headless` instead of `Insert()` to build
the resulting sheets. This can be further optimized in the future by
implementing a smart cache, like batched spritesheets, and storing it in
the repo, but for now it's not important/slow enough to be worth the
effort. Also fixes a silent compilation error that would always happen
outside unit tests, but for some reason doesn't appear on local? Notice
how `map_icon_key` is not a defined variable anywhere. That's because
`USE_RUSTG_ICONFORGE_GAGS` is *never* defined at this point, so it was
always using the 'slow' generation.

I also took the liberty of cleaning up the cultist and heretic icon
generation randomly initializing a blade object when it could just use a
static access.

## Why It's Good For The Game

The subsystem timing may not be much faster, but the interactivity
benefits during spritesheet realization are undeniable. Opening the
preferences menu during init on local is orders of magnitude faster.

**Old**
Early Assets: 5.02 seconds
Greyscale Previews: 1.38 seconds

**Fresh (No Cache)**
Early Assets: 4.21 seconds
Greyscale Previews: 0.5 seconds

**Cache Invalidated**
Early Assets: 4.27 seconds

**Cache Hit**
Early Assets: 4.05~4.2 seconds

**Preferences lag:**
~6 sec to open to ~2 sec to open due to caching in dev

## Changelog

🆑
code: Optimized species and antagonist icon loading in the preferences
menu on local, speeding up time to open in development.
fix: GAGS map preview generation no longer silently errors outside of
unit tests due to a compilation error.
/🆑
This commit is contained in:
itsmeow
2026-03-02 17:25:16 -05:00
committed by GitHub
parent e1d8e983f4
commit 57144e0243
48 changed files with 214 additions and 184 deletions
+9 -9
View File
@@ -397,24 +397,24 @@ Space Ninja, for example, looks like:
preview_outift = /datum/outfit/ninja
```
However, if you want to get creative, you can override `/get_preview_icon()`. This proc should return an icon of size `ANTAGONIST_PREVIEW_ICON_SIZE`x`ANTAGONIST_PREVIEW_ICON_SIZE`.
However, if you want to get creative, you can override `/get_preview_icon()`. This proc should return a /datum/universal_icon of size `ANTAGONIST_PREVIEW_ICON_SIZE`x`ANTAGONIST_PREVIEW_ICON_SIZE`.
There are some helper procs you can use as well. `render_preview_outfit(outfit_type)` will take an outfit and give you an icon of someone wearing those clothes. `finish_preview_outfit` will, given an icon, resize it appropriately and zoom in on the head. Note that this will look bad on anything that isn't a human, so if you have a non-human antagonist (such as sentient disease), just run `icon.Scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE)`.
There are some helper procs you can use as well. `render_preview_outfit(outfit_type)` will take an outfit and give you a /datum/universal_icon of someone wearing those clothes (using get_flat_uni_icon). `finish_preview_outfit` will, given an icon, resize it appropriately and zoom in on the head. Note that this will look bad on anything that isn't a human, so if you have a non-human antagonist (such as sentient disease), just run `icon.scale(ANTAGONIST_PREVIEW_ICON_SIZE, ANTAGONIST_PREVIEW_ICON_SIZE)`.
For inspiration, here is changeling's:
```dm
/datum/antagonist/changeling/get_preview_icon()
var/icon/final_icon = render_preview_outfit(/datum/outfit/changeling)
var/icon/split_icon = render_preview_outfit(/datum/outfit/job/engineer)
var/datum/universal_icon/final_icon = render_preview_outfit(/datum/outfit/changeling)
var/datum/universal_icon/split_icon = render_preview_outfit(/datum/outfit/job/engineer)
final_icon.Shift(WEST, ICON_SIZE_X / 2)
final_icon.Shift(EAST, ICON_SIZE_X / 2)
final_icon.shift(WEST, ICON_SIZE_X / 2)
final_icon.shift(EAST, ICON_SIZE_X / 2)
split_icon.Shift(EAST, ICON_SIZE_X / 2)
split_icon.Shift(WEST, ICON_SIZE_X / 2)
split_icon.shift(EAST, ICON_SIZE_X / 2)
split_icon.shift(WEST, ICON_SIZE_X / 2)
final_icon.Blend(split_icon, ICON_OVERLAY)
final_icon.blend_icon(split_icon, ICON_OVERLAY)
return finish_preview_icon(final_icon)
```
@@ -36,7 +36,7 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list(
/datum/preference_middleware/antags/get_ui_assets()
return list(
get_asset_datum(/datum/asset/spritesheet/antagonists),
get_asset_datum(/datum/asset/spritesheet_batched/antagonists),
)
/datum/preference_middleware/antags/proc/set_antags(list/params, mob/user)
@@ -136,14 +136,14 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list(
return get_remaining_days(antag_time_limits[checked_antag_flag] || 0)
/// Sprites generated for the antagonists panel
/datum/asset/spritesheet/antagonists
/datum/asset/spritesheet_batched/antagonists
name = "antagonists"
early = TRUE
/// Mapping of spritesheet keys -> icons
/// List of keys -> universal icon datums
var/list/antag_icons = list()
/datum/asset/spritesheet/antagonists/create_spritesheets()
/datum/asset/spritesheet_batched/antagonists/create_spritesheets()
var/list/antagonists = GLOB.non_ruleset_antagonists.Copy()
for (var/datum/dynamic_ruleset/ruleset as anything in subtypesof(/datum/dynamic_ruleset))
@@ -166,10 +166,13 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list(
if (!isnull(generated_icons[antagonist_type]))
antag_icons[spritesheet_key] = generated_icons[antagonist_type]
// make sure IconForge also knows to generate this duplicate for the given key
// internal caching will prevent double generation of the same icon
insert_icon(spritesheet_key, generated_icons[antagonist_type])
continue
var/datum/antagonist/antagonist = new antagonist_type
var/icon/preview_icon = antagonist.get_preview_icon()
var/datum/universal_icon/preview_icon = antagonist.get_preview_icon()
if (isnull(preview_icon))
continue
@@ -179,11 +182,12 @@ GLOBAL_LIST_INIT(non_ruleset_antagonists, list(
// preview_icons are not scaled at this stage INTENTIONALLY.
// If an icon is not prepared to be scaled to that size, it looks really ugly, and this
// makes it harder to figure out what size it *actually* is.
generated_icons[antagonist_type] = preview_icon
antag_icons[spritesheet_key] = preview_icon
for (var/spritesheet_key in antag_icons)
Insert(spritesheet_key, antag_icons[spritesheet_key])
// make sure no antagonists are rebuilt
generated_icons[antagonist_type] = preview_icon
// store for screenshot tests
antag_icons[spritesheet_key] = preview_icon
insert_icon(spritesheet_key, preview_icon)
/// Serializes an antag name to be used for preferences UI
/proc/serialize_antag_name(antag_name)
@@ -3,16 +3,14 @@
/datum/preference_middleware/species/get_ui_assets()
return list(
get_asset_datum(/datum/asset/spritesheet/species),
get_asset_datum(/datum/asset/spritesheet_batched/species),
)
/datum/asset/spritesheet/species
/datum/asset/spritesheet_batched/species
name = "species"
early = TRUE
/datum/asset/spritesheet/species/create_spritesheets()
var/list/to_insert = list()
/datum/asset/spritesheet_batched/species/create_spritesheets()
for (var/species_id in get_selectable_species())
var/datum/species/species_type = GLOB.species_list[species_id]
@@ -21,13 +19,10 @@
dummy.equipOutfit(/datum/outfit/job/assistant/consistent, visuals_only = TRUE)
dummy.dna.species.prepare_human_for_preview(dummy)
var/icon/dummy_icon = getFlatIcon(dummy)
dummy_icon.Scale(64, 64)
dummy_icon.Crop(15, 64 - 31, 15 + 31, 64)
dummy_icon.Scale(64, 64)
to_insert[sanitize_css_class_name(initial(species_type.name))] = dummy_icon
var/datum/universal_icon/dummy_icon = get_flat_uni_icon(dummy)
dummy_icon.scale(64, 64)
dummy_icon.crop(15, 64 - 31, 15 + 31, 64)
dummy_icon.scale(64, 64)
insert_icon(sanitize_css_class_name(initial(species_type.name)), dummy_icon)
SSatoms.prepare_deletion(dummy)
for (var/spritesheet_key in to_insert)
Insert(spritesheet_key, to_insert[spritesheet_key])