// spritesheet implementation - coalesces various icons into a single .png file
// and uses CSS to select icons out of that file - saves on transferring some
// 1400-odd individual PNG files
#define SPR_SIZE 1
#define SPR_IDX 2
#define SPRSZ_COUNT 1
#define SPRSZ_ICON 2
#define SPRSZ_STRIPPED 3
/// Deprecated: Use /datum/asset/spritesheet_batched where possible
/datum/asset/spritesheet
_abstract = /datum/asset/spritesheet
cross_round_cachable = TRUE
var/name
/// List of arguments to pass into queuedInsert
/// Exists so we can queue icon insertion, mostly for stuff like preferences
var/list/to_generate = list()
var/list/sizes = list() // "32x32" -> list(10, icon/normal, icon/stripped)
var/list/sprites = list() // "foo_bar" -> list("32x32", 5)
var/list/cached_spritesheets_needed
var/generating_cache = FALSE
var/fully_generated = FALSE
/// If this asset should be fully loaded on new
/// Defaults to false so we can process this stuff nicely
var/load_immediately = FALSE
// Kept in state so that the result is the same, even when the files are created, for this run
VAR_PRIVATE/should_refresh = null
/datum/asset/spritesheet/proc/should_load_immediately()
#ifdef DO_NOT_DEFER_ASSETS
return TRUE
#else
return load_immediately
#endif
/datum/asset/spritesheet/should_refresh()
if (..())
return TRUE
if (isnull(should_refresh))
// `fexists` seems to always fail on static-time
should_refresh = !fexists(css_cache_filename()) || !fexists(data_cache_filename())
return should_refresh
/datum/asset/spritesheet/unregister()
SSassets.transport.unregister_asset("spritesheet_[name].css")
if(length(sizes))
for(var/size_id in sizes)
SSassets.transport.unregister_asset("[name]_[size_id].png")
else
for(var/sheet in cached_spritesheets_needed)
SSassets.transport.unregister_asset(sheet)
/datum/asset/spritesheet/regenerate()
unregister()
sprites = list()
fdel("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css")
for(var/sheet in cached_spritesheets_needed)
fdel("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[sheet].png")
fdel("data/spritesheets/spritesheet_[name].css")
for(var/size_id in sizes)
fdel("data/spritesheets/[name]_[size_id].png")
sizes = list()
to_generate = list()
cached_serialized_url_mappings = null
cached_serialized_url_mappings_transport_type = null
fully_generated = FALSE
var/old_load = load_immediately
load_immediately = TRUE
create_spritesheets()
realize_spritesheets(yield = FALSE)
load_immediately = old_load
/datum/asset/spritesheet/register()
SHOULD_NOT_OVERRIDE(TRUE)
if (!name)
CRASH("spritesheet [type] cannot register without a name")
if (!should_refresh() && read_from_cache())
fully_generated = TRUE
return
// If it's cached, may as well load it now, while the loading is cheap
if(CONFIG_GET(flag/cache_assets) && cross_round_cachable)
load_immediately = TRUE
create_spritesheets()
if(should_load_immediately())
realize_spritesheets(yield = FALSE)
else
SSasset_loading.queue_asset(src)
/datum/asset/spritesheet/proc/realize_spritesheets(yield)
if(fully_generated)
return
while(length(to_generate))
var/list/stored_args = to_generate[to_generate.len]
to_generate.len--
queuedInsert(arglist(stored_args))
if(yield && TICK_CHECK)
return
ensure_stripped()
for(var/size_id in sizes)
var/size = sizes[size_id]
var/file_path = size[SPRSZ_STRIPPED]
var/file_hash = rustg_hash_file(RUSTG_HASH_MD5, file_path)
SSassets.transport.register_asset("[name]_[size_id].png", file_path, file_hash=file_hash)
var/css_name = "spritesheet_[name].css"
var/file_directory = "data/spritesheets/[css_name]"
fdel(file_directory)
var/css = generate_css()
rustg_file_write(css, file_directory)
var/css_hash = rustg_hash_string(RUSTG_HASH_MD5, css)
SSassets.transport.register_asset(css_name, fcopy_rsc(file_directory), file_hash=css_hash)
if(CONFIG_GET(flag/save_spritesheets))
save_to_logs(file_name = css_name, file_location = file_directory)
fdel(file_directory)
if (CONFIG_GET(flag/cache_assets) && cross_round_cachable)
write_to_cache()
fully_generated = TRUE
// If we were ever in there, remove ourselves
SSasset_loading.dequeue_asset(src)
/datum/asset/spritesheet/queued_generation()
realize_spritesheets(yield = TRUE)
/datum/asset/spritesheet/ensure_ready()
if(!fully_generated)
realize_spritesheets(yield = FALSE)
return ..()
/datum/asset/spritesheet/send(client/client)
if (!name)
return
if (!should_refresh())
return send_from_cache(client)
var/all = list("spritesheet_[name].css")
for(var/size_id in sizes)
all += "[name]_[size_id].png"
. = SSassets.transport.send_assets(client, all)
/datum/asset/spritesheet/get_url_mappings()
if (!name)
return
if (!should_refresh())
return get_cached_url_mappings()
. = list("spritesheet_[name].css" = SSassets.transport.get_asset_url("spritesheet_[name].css"))
for(var/size_id in sizes)
.["[name]_[size_id].png"] = SSassets.transport.get_asset_url("[name]_[size_id].png")
/datum/asset/spritesheet/proc/ensure_stripped(sizes_to_strip = sizes)
for(var/size_id in sizes_to_strip)
var/size = sizes[size_id]
if (size[SPRSZ_STRIPPED])
continue
// save flattened version
var/png_name = "[name]_[size_id].png"
var/file_directory = "data/spritesheets/[png_name]"
fcopy(size[SPRSZ_ICON], file_directory)
var/error = rustg_dmi_strip_metadata(file_directory)
if(length(error))
stack_trace("Failed to strip [png_name]: [error]")
size[SPRSZ_STRIPPED] = icon(file_directory)
// this is useful here for determining if weird sprite issues (like having a white background) are a cause of what we're doing DM-side or not since we can see the full flattened thing at-a-glance.
if(CONFIG_GET(flag/save_spritesheets))
save_to_logs(file_name = png_name, file_location = file_directory)
fdel(file_directory)
/datum/asset/spritesheet/proc/generate_css()
var/list/out = list()
for (var/size_id in sizes)
var/size = sizes[size_id]
var/list/dimensions = get_icon_dimensions(size[SPRSZ_ICON])
out += ".[name][size_id]{display:inline-block;width:[dimensions["width"]]px;height:[dimensions["height"]]px;background-image:url('[get_background_url("[name]_[size_id].png")]');background-repeat:no-repeat;}"
for (var/sprite_id in sprites)
var/sprite = sprites[sprite_id]
var/size_id = sprite[SPR_SIZE]
var/idx = sprite[SPR_IDX]
var/size = sizes[size_id]
var/list/tiny_dimensions = get_icon_dimensions(size[SPRSZ_ICON])
var/icon/big = size[SPRSZ_STRIPPED]
// big width won't be cached ever
var/per_line = big.Width() / tiny_dimensions["width"]
var/x = (idx % per_line) * tiny_dimensions["width"]
var/y = round(idx / per_line) * tiny_dimensions["height"]
out += ".[name][size_id].[sprite_id]{background-position:-[x]px -[y]px;}"
return out.Join("\n")
/datum/asset/spritesheet/proc/css_cache_filename()
return "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css"
/datum/asset/spritesheet/proc/data_cache_filename()
return "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].json"
/datum/asset/spritesheet/proc/read_from_cache()
return read_css_from_cache() && read_data_from_cache()
/datum/asset/spritesheet/proc/read_css_from_cache()
var/replaced_css = rustg_file_read(css_cache_filename())
var/regex/find_background_urls = regex(@"background-image:url\('%(.+?)%'\)", "g")
while (find_background_urls.Find(replaced_css))
var/asset_id = find_background_urls.group[1]
var/file_path = "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[asset_id]"
// Hashing it here is a *lot* faster.
var/hash = rustg_hash_file(RUSTG_HASH_MD5, file_path)
var/asset_cache_item = SSassets.transport.register_asset(asset_id, file_path, file_hash=hash)
var/asset_url = SSassets.transport.get_asset_url(asset_cache_item = asset_cache_item)
replaced_css = replacetext(replaced_css, find_background_urls.match, "background-image:url('[asset_url]')")
LAZYADD(cached_spritesheets_needed, asset_id)
var/finalized_name = "spritesheet_[name].css"
var/replaced_css_filename = "data/spritesheets/[finalized_name]"
var/css_hash = rustg_hash_string(RUSTG_HASH_MD5, replaced_css)
rustg_file_write(replaced_css, replaced_css_filename)
SSassets.transport.register_asset(finalized_name, replaced_css_filename, file_hash=css_hash)
if(CONFIG_GET(flag/save_spritesheets))
save_to_logs(file_name = finalized_name, file_location = replaced_css_filename)
fdel(replaced_css_filename)
return TRUE
/datum/asset/spritesheet/proc/read_data_from_cache()
var/json = json_decode(rustg_file_read(data_cache_filename()))
if (islist(json["sprites"]))
sprites = json["sprites"]
return TRUE
/datum/asset/spritesheet/proc/send_from_cache(client/client)
if (isnull(cached_spritesheets_needed))
stack_trace("cached_spritesheets_needed was null when sending assets from [type] from cache")
cached_spritesheets_needed = list()
return SSassets.transport.send_assets(client, cached_spritesheets_needed + "spritesheet_[name].css")
/// Returns the URL to put in the background:url of the CSS asset
/datum/asset/spritesheet/proc/get_background_url(asset)
if (generating_cache)
return "%[asset]%"
else
return SSassets.transport.get_asset_url(asset)
/datum/asset/spritesheet/proc/write_to_cache()
write_css_to_cache()
write_data_to_cache()
/datum/asset/spritesheet/proc/write_css_to_cache()
for (var/size_id in sizes)
fcopy(SSassets.cache["[name]_[size_id].png"].resource, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name]_[size_id].png")
generating_cache = TRUE
var/mock_css = generate_css()
generating_cache = FALSE
rustg_file_write(mock_css, css_cache_filename())
/datum/asset/spritesheet/proc/write_data_to_cache()
rustg_file_write(json_encode(list(
"sprites" = sprites,
)), data_cache_filename())
/datum/asset/spritesheet/proc/get_cached_url_mappings()
var/list/mappings = list()
mappings["spritesheet_[name].css"] = SSassets.transport.get_asset_url("spritesheet_[name].css")
for (var/asset_name in cached_spritesheets_needed)
mappings[asset_name] = SSassets.transport.get_asset_url(asset_name)
return mappings
/// Override this in order to start the creation of the spritehseet.
/// This is where all your Insert, InsertAll, etc calls should be inside.
/datum/asset/spritesheet/proc/create_spritesheets()
CRASH("create_spritesheets() not implemented for [type]!")
/datum/asset/spritesheet/proc/Insert(sprite_name, icon/inserted_icon, icon_state="", dir=SOUTH, frame=1, moving=FALSE)
if(should_load_immediately())
queuedInsert(sprite_name, inserted_icon, icon_state, dir, frame, moving)
else
to_generate += list(args.Copy())
/datum/asset/spritesheet/proc/queuedInsert(sprite_name, icon/inserted_icon, icon_state="", dir=SOUTH, frame=1, moving=FALSE)
#ifdef UNIT_TESTS
if (inserted_icon && icon_state && !icon_exists(inserted_icon, icon_state)) // check the base icon prior to extracting the state we want
stack_trace("Tried to insert nonexistent icon_state '[icon_state]' from [inserted_icon] into spritesheet [name] ([type])")
return
#endif
inserted_icon = icon(inserted_icon, icon_state=icon_state, dir=dir, frame=frame, moving=moving)
if (!inserted_icon || !length(icon_states(inserted_icon))) // that direction or state doesn't exist
return
var/start_usage = world.tick_usage
//any sprite modifications we want to do (aka, coloring a greyscaled asset)
inserted_icon = ModifyInserted(inserted_icon)
var/list/dimensions = get_icon_dimensions(inserted_icon)
var/size_id = "[dimensions["width"]]x[dimensions["height"]]"
var/size = sizes[size_id]
if (sprites[sprite_name])
CRASH("duplicate sprite \"[sprite_name]\" in sheet [name] ([type])")
if (size)
var/position = size[SPRSZ_COUNT]++
// Icons are essentially representations of files + modifications
// Because of this, byond keeps them in a cache. It does this in a really dumb way tho
// It's essentially a FIFO queue. So after we do icon() some amount of times, our old icons go out of cache
// When this happens it becomes impossible to modify them, trying to do so will instead throw a
// "bad icon" error.
// What we're doing here is ensuring our icon is in the cache by refreshing it, so we can modify it w/o runtimes.
var/icon/sheet = size[SPRSZ_ICON]
var/icon/sheet_copy = icon(sheet)
size[SPRSZ_STRIPPED] = null
sheet_copy.Insert(inserted_icon, icon_state=sprite_name)
size[SPRSZ_ICON] = sheet_copy
sprites[sprite_name] = list(size_id, position)
else
sizes[size_id] = size = list(1, inserted_icon, null)
sprites[sprite_name] = list(size_id, 0)
SSblackbox.record_feedback("tally", "spritesheet_queued_insert_time", TICK_USAGE_TO_MS(start_usage), name)
/**
* A simple proc handing the Icon for you to modify before it gets turned into an asset.
*
* Arguments:
* * I: icon being turned into an asset
*/
/datum/asset/spritesheet/proc/ModifyInserted(icon/pre_asset)
return pre_asset
/datum/asset/spritesheet/proc/InsertAll(prefix, icon/inserted_icon, list/directions)
if (length(prefix))
prefix = "[prefix]-"
if (!directions)
directions = list(SOUTH)
for (var/icon_state_name in icon_states(inserted_icon))
for (var/direction in directions)
var/prefix2 = (directions.len > 1) ? "[dir2text(direction)]-" : ""
Insert("[prefix][prefix2][icon_state_name]", inserted_icon, icon_state=icon_state_name, dir=direction)
/datum/asset/spritesheet/proc/css_tag()
return {""}
/datum/asset/spritesheet/proc/css_filename()
return SSassets.transport.get_asset_url("spritesheet_[name].css")
/datum/asset/spritesheet/proc/icon_tag(sprite_name)
var/sprite = sprites[sprite_name]
if (!sprite)
return null
var/size_id = sprite[SPR_SIZE]
return {""}
/datum/asset/spritesheet/proc/icon_class_name(sprite_name)
var/sprite = sprites[sprite_name]
if (!sprite)
return null
var/size_id = sprite[SPR_SIZE]
return {"[name][size_id] [sprite_name]"}
/**
* Returns the size class (ex design32x32) for a given sprite's icon
*
* Arguments:
* * sprite_name - The sprite to get the size of
*/
/datum/asset/spritesheet/proc/icon_size_id(sprite_name)
var/sprite = sprites[sprite_name]
if (!sprite)
return null
var/size_id = sprite[SPR_SIZE]
return "[name][size_id]"
#undef SPR_SIZE
#undef SPR_IDX
#undef SPRSZ_COUNT
#undef SPRSZ_ICON
#undef SPRSZ_STRIPPED
/// Spritesheet that only uses simple PNGs and CSS keys. See `assets` variable.
/// Deprecated: Use /datum/asset/spritesheet_batched where possible
/datum/asset/spritesheet/simple
_abstract = /datum/asset/spritesheet/simple
/// Associative list of icon keys (CSS class names) -> PNG filepaths (single quote!)
/// File paths MUST be PNGs
var/list/assets
/datum/asset/spritesheet/simple/create_spritesheets()
for (var/key in assets)
Insert(key, assets[key])