mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 10:12:45 +00:00
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
837 lines
30 KiB
Plaintext
837 lines
30 KiB
Plaintext
#define TO_HEX_DIGIT(n) ascii2text((n&15) + ((n&15)<10 ? 48 : 87))
|
|
|
|
/icon/proc/MakeLying()
|
|
var/icon/I = new(src,dir=SOUTH)
|
|
I.BecomeLying()
|
|
return I
|
|
|
|
/icon/proc/BecomeLying()
|
|
Turn(90)
|
|
Shift(SOUTH,6)
|
|
Shift(EAST,1)
|
|
|
|
// Multiply all alpha values by this float
|
|
/icon/proc/ChangeOpacity(opacity = 1.0)
|
|
MapColors(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,opacity, 0,0,0,0)
|
|
|
|
// Convert to grayscale
|
|
/icon/proc/GrayScale()
|
|
MapColors(0.3,0.3,0.3, 0.59,0.59,0.59, 0.11,0.11,0.11, 0,0,0)
|
|
|
|
/icon/proc/ColorTone(tone)
|
|
GrayScale()
|
|
|
|
var/list/TONE = rgb2num(tone)
|
|
var/gray = round(TONE[1]*0.3 + TONE[2]*0.59 + TONE[3]*0.11, 1)
|
|
|
|
var/icon/upper = (255-gray) ? new(src) : null
|
|
|
|
if(gray)
|
|
MapColors(255/gray,0,0, 0,255/gray,0, 0,0,255/gray, 0,0,0)
|
|
Blend(tone, ICON_MULTIPLY)
|
|
else SetIntensity(0)
|
|
if(255-gray)
|
|
upper.Blend(rgb(gray,gray,gray), ICON_SUBTRACT)
|
|
upper.MapColors((255-TONE[1])/(255-gray),0,0,0, 0,(255-TONE[2])/(255-gray),0,0, 0,0,(255-TONE[3])/(255-gray),0, 0,0,0,0, 0,0,0,1)
|
|
Blend(upper, ICON_ADD)
|
|
|
|
// Take the minimum color of two icons; combine transparency as if blending with ICON_ADD
|
|
/icon/proc/MinColors(icon)
|
|
var/icon/I = new(src)
|
|
I.Opaque()
|
|
I.Blend(icon, ICON_SUBTRACT)
|
|
Blend(I, ICON_SUBTRACT)
|
|
|
|
// Take the maximum color of two icons; combine opacity as if blending with ICON_OR
|
|
/icon/proc/MaxColors(icon)
|
|
var/icon/I
|
|
if(isicon(icon))
|
|
I = new(icon)
|
|
else
|
|
// solid color
|
|
I = new(src)
|
|
I.Blend("#000000", ICON_OVERLAY)
|
|
I.SwapColor("#000000", null)
|
|
I.Blend(icon, ICON_OVERLAY)
|
|
var/icon/J = new(src)
|
|
J.Opaque()
|
|
I.Blend(J, ICON_SUBTRACT)
|
|
Blend(I, ICON_OR)
|
|
|
|
// make this icon fully opaque--transparent pixels become black
|
|
/icon/proc/Opaque(background = "#000000")
|
|
SwapColor(null, background)
|
|
MapColors(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,0, 0,0,0,1)
|
|
|
|
// Change a grayscale icon into a white icon where the original color becomes the alpha
|
|
// I.e., black -> transparent, gray -> translucent white, white -> solid white
|
|
/icon/proc/BecomeAlphaMask()
|
|
SwapColor(null, "#000000ff") // don't let transparent become gray
|
|
MapColors(0,0,0,0.3, 0,0,0,0.59, 0,0,0,0.11, 0,0,0,0, 1,1,1,0)
|
|
|
|
/icon/proc/UseAlphaMask(mask)
|
|
Opaque()
|
|
AddAlphaMask(mask)
|
|
|
|
/icon/proc/AddAlphaMask(mask)
|
|
var/icon/M = new(mask)
|
|
M.Blend("#ffffff", ICON_SUBTRACT)
|
|
// apply mask
|
|
Blend(M, ICON_ADD)
|
|
|
|
/proc/BlendRGB(rgb1, rgb2, amount)
|
|
var/list/RGB1 = ReadRGB(rgb1)
|
|
var/list/RGB2 = ReadRGB(rgb2)
|
|
|
|
// add missing alpha if needed
|
|
if(!RGB1 || !RGB2) //Don't blend if we don't two! No use blending what we don't have!
|
|
return
|
|
if(RGB1.len < RGB2.len) RGB1 += 255
|
|
else if(RGB2.len < RGB1.len) RGB2 += 255
|
|
var/usealpha = RGB1.len > 3
|
|
|
|
var/r = round(RGB1[1] + (RGB2[1] - RGB1[1]) * amount, 1)
|
|
var/g = round(RGB1[2] + (RGB2[2] - RGB1[2]) * amount, 1)
|
|
var/b = round(RGB1[3] + (RGB2[3] - RGB1[3]) * amount, 1)
|
|
var/alpha = usealpha ? round(RGB1[4] + (RGB2[4] - RGB1[4]) * amount, 1) : null
|
|
|
|
return isnull(alpha) ? rgb(r, g, b) : rgb(r, g, b, alpha)
|
|
|
|
// Ported from /tg/station
|
|
// Creates a single icon from a given /atom or /image. Only the first argument is required.
|
|
// appearance_flags indicates whether appearance_flags should be respected (at the cost of about 10-20% perf)
|
|
/proc/getFlatIcon(image/appearance, defdir, deficon, defstate, defblend, start = TRUE, no_anim = FALSE, force_south = FALSE, appearance_flags = FALSE)
|
|
// Loop through the underlays, then overlays, sorting them into the layers list
|
|
#define PROCESS_OVERLAYS_OR_UNDERLAYS(flat, process, base_layer) \
|
|
for (var/i in 1 to process.len) { \
|
|
var/image/current = process[i]; \
|
|
if (!current) { \
|
|
continue; \
|
|
} \
|
|
if (current.plane != FLOAT_PLANE && current.plane != appearance.plane) { \
|
|
continue; \
|
|
} \
|
|
var/current_layer = current.layer; \
|
|
if (current_layer < 0) { \
|
|
if (current_layer <= -1000) { \
|
|
return flat; \
|
|
} \
|
|
current_layer = base_layer + appearance.layer + current_layer / 1000; \
|
|
} \
|
|
for (var/index_to_compare_to in 1 to layers.len) { \
|
|
var/compare_to = layers[index_to_compare_to]; \
|
|
if (current_layer < layers[compare_to]) { \
|
|
layers.Insert(index_to_compare_to, current); \
|
|
break; \
|
|
} \
|
|
} \
|
|
layers[current] = current_layer; \
|
|
}
|
|
|
|
var/static/icon/flat_template = icon('icons/effects/effects.dmi', "nothing")
|
|
|
|
if(!appearance || appearance.alpha <= 0)
|
|
return icon(flat_template)
|
|
if(start)
|
|
if(!defdir)
|
|
defdir = appearance.dir
|
|
if(!deficon)
|
|
deficon = appearance.icon
|
|
if(!defstate)
|
|
defstate = appearance.icon_state
|
|
if(!defblend)
|
|
defblend = appearance.blend_mode
|
|
|
|
var/curicon = appearance.icon || deficon
|
|
var/curstate = appearance.icon_state || defstate
|
|
var/curdir = (!appearance.dir || appearance.dir == SOUTH) ? defdir : appearance.dir
|
|
|
|
if(force_south)
|
|
curdir = SOUTH
|
|
|
|
var/render_icon = curicon
|
|
|
|
if (render_icon)
|
|
var/curstates = icon_states(curicon)
|
|
if(!(curstate in curstates))
|
|
if ("" in curstates)
|
|
curstate = ""
|
|
else
|
|
render_icon = FALSE
|
|
|
|
var/base_icon_dir //We'll use this to get the icon state to display if not null BUT NOT pass it to overlays as the dir we have
|
|
|
|
//Try to remove/optimize this section ASAP, CPU hog.
|
|
//Determines if there's directionals.
|
|
if(render_icon && curdir != SOUTH)
|
|
if (
|
|
!length(icon_states(icon(curicon, curstate, NORTH))) \
|
|
&& !length(icon_states(icon(curicon, curstate, EAST))) \
|
|
&& !length(icon_states(icon(curicon, curstate, WEST))) \
|
|
)
|
|
base_icon_dir = SOUTH
|
|
|
|
if(!base_icon_dir)
|
|
base_icon_dir = curdir
|
|
|
|
var/curblend = appearance.blend_mode || defblend
|
|
|
|
if(appearance.overlays.len || appearance.underlays.len)
|
|
var/icon/flat = icon(flat_template)
|
|
// Layers will be a sorted list of icons/overlays, based on the order in which they are displayed
|
|
var/list/layers = list()
|
|
var/image/copy
|
|
// Add the atom's icon itself, without pixel_x/y offsets.
|
|
if(render_icon)
|
|
copy = image(icon=curicon, icon_state=curstate, layer=appearance.layer, dir=base_icon_dir)
|
|
copy.color = appearance.color
|
|
copy.alpha = appearance.alpha
|
|
copy.blend_mode = curblend
|
|
layers[copy] = appearance.layer
|
|
|
|
PROCESS_OVERLAYS_OR_UNDERLAYS(flat, appearance.underlays, 0)
|
|
PROCESS_OVERLAYS_OR_UNDERLAYS(flat, appearance.overlays, 1)
|
|
|
|
var/icon/add // Icon of overlay being added
|
|
|
|
var/flatX1 = 1
|
|
var/flatX2 = flat.Width()
|
|
var/flatY1 = 1
|
|
var/flatY2 = flat.Height()
|
|
|
|
var/addX1 = 0
|
|
var/addX2 = 0
|
|
var/addY1 = 0
|
|
var/addY2 = 0
|
|
|
|
for(var/image/layer_image as anything in layers)
|
|
if(layer_image.alpha == 0)
|
|
continue
|
|
|
|
// variables only relevant when accounting for appearance_flags:
|
|
var/apply_color = TRUE
|
|
var/apply_alpha = TRUE
|
|
|
|
if(layer_image == copy) // 'layer_image' is an /image based on the object being flattened.
|
|
curblend = BLEND_OVERLAY
|
|
add = icon(layer_image.icon, layer_image.icon_state, base_icon_dir)
|
|
else // 'I' is an appearance object.
|
|
var/image/layer_as_image = image(layer_image)
|
|
if(appearance_flags)
|
|
if(layer_as_image.appearance_flags & RESET_COLOR)
|
|
apply_color = FALSE
|
|
if(layer_as_image.appearance_flags & RESET_ALPHA)
|
|
apply_alpha = FALSE
|
|
add = getFlatIcon(layer_as_image, curdir, curicon, curstate, curblend, FALSE, no_anim, force_south, appearance_flags)
|
|
if(!add)
|
|
continue
|
|
|
|
// Find the new dimensions of the flat icon to fit the added overlay
|
|
addX1 = min(flatX1, layer_image.pixel_x + 1)
|
|
addX2 = max(flatX2, layer_image.pixel_x + add.Width())
|
|
addY1 = min(flatY1, layer_image.pixel_y + 1)
|
|
addY2 = max(flatY2, layer_image.pixel_y + add.Height())
|
|
|
|
if (
|
|
addX1 != flatX1 \
|
|
|| addX2 != flatX2 \
|
|
|| addY1 != flatY1 \
|
|
|| addY2 != flatY2 \
|
|
)
|
|
// Resize the flattened icon so the new icon fits
|
|
flat.Crop(
|
|
addX1 - flatX1 + 1,
|
|
addY1 - flatY1 + 1,
|
|
addX2 - flatX1 + 1,
|
|
addY2 - flatY1 + 1
|
|
)
|
|
|
|
flatX1 = addX1
|
|
flatX2 = addX2
|
|
flatY1 = addY1
|
|
flatY2 = addY2
|
|
|
|
if(appearance_flags)
|
|
// apply parent's color/alpha to the added layers if the layer didn't opt
|
|
if(apply_color && appearance.color)
|
|
if(islist(appearance.color))
|
|
add.MapColors(arglist(appearance.color))
|
|
else
|
|
add.Blend(appearance.color, ICON_MULTIPLY)
|
|
|
|
if(apply_alpha && appearance.alpha < 255)
|
|
add.Blend(rgb(255, 255, 255, appearance.alpha), ICON_MULTIPLY)
|
|
|
|
// Blend the overlay into the flattened icon
|
|
flat.Blend(add, blendMode2iconMode(curblend), layer_image.pixel_x + 2 - flatX1, layer_image.pixel_y + 2 - flatY1)
|
|
|
|
if(!appearance_flags)
|
|
// If we didn't apply parent colors individually per layer respecting appearance_flags, then do it just the one time now
|
|
if(appearance.color)
|
|
if(islist(appearance.color))
|
|
flat.MapColors(arglist(appearance.color))
|
|
else
|
|
flat.Blend(appearance.color, ICON_MULTIPLY)
|
|
|
|
if(appearance.alpha < 255)
|
|
flat.Blend(rgb(255, 255, 255, appearance.alpha), ICON_MULTIPLY)
|
|
|
|
if(no_anim)
|
|
//Clean up repeated frames
|
|
var/icon/cleaned = new /icon()
|
|
cleaned.Insert(flat, "", SOUTH, 1, 0)
|
|
return cleaned
|
|
else
|
|
return icon(flat, "", SOUTH)
|
|
else if (render_icon) // There's no overlays.
|
|
var/icon/final_icon = icon(icon(curicon, curstate, base_icon_dir), "", SOUTH, no_anim ? TRUE : null)
|
|
|
|
if (appearance.alpha < 255)
|
|
final_icon.Blend(rgb(255,255,255, appearance.alpha), ICON_MULTIPLY)
|
|
|
|
if (appearance.color)
|
|
if (islist(appearance.color))
|
|
final_icon.MapColors(arglist(appearance.color))
|
|
else
|
|
final_icon.Blend(appearance.color, ICON_MULTIPLY)
|
|
|
|
return final_icon
|
|
|
|
#undef PROCESS_OVERLAYS_OR_UNDERLAYS
|
|
|
|
/proc/getIconMask(atom/A)//By yours truly. Creates a dynamic mask for a mob/whatever. /N
|
|
var/icon/alpha_mask = new(A.icon,A.icon_state)//So we want the default icon and icon state of A.
|
|
for(var/I in A.overlays)//For every image in overlays. var/image/I will not work, don't try it.
|
|
if(I:layer>A.layer) continue//If layer is greater than what we need, skip it.
|
|
var/icon/image_overlay = new(I:icon,I:icon_state)//Blend only works with icon objects.
|
|
//Also, icons cannot directly set icon_state. Slower than changing variables but whatever.
|
|
alpha_mask.Blend(image_overlay,ICON_OR)//OR so they are lumped together in a nice overlay.
|
|
return alpha_mask//And now return the mask.
|
|
|
|
//getFlatIcon but generates an icon that can face ALL four directions. The only four.
|
|
/proc/getCompoundIcon(atom/A)
|
|
var/icon/north = getFlatIcon(A,defdir=NORTH)
|
|
var/icon/south = getFlatIcon(A,defdir=SOUTH)
|
|
var/icon/east = getFlatIcon(A,defdir=EAST)
|
|
var/icon/west = getFlatIcon(A,defdir=WEST)
|
|
|
|
//Starts with a blank icon because of byond bugs.
|
|
var/icon/full = icon('icons/effects/effects.dmi', "icon_state"="nothing")
|
|
|
|
full.Insert(north,dir=NORTH)
|
|
full.Insert(south,dir=SOUTH)
|
|
full.Insert(east,dir=EAST)
|
|
full.Insert(west,dir=WEST)
|
|
qdel(north)
|
|
qdel(south)
|
|
qdel(east)
|
|
qdel(west)
|
|
return full
|
|
|
|
/proc/downloadImage(atom/A, dir)
|
|
var/icon/this_icon = getFlatIcon(A,defdir=dir)
|
|
|
|
usr << ftp(this_icon,"[A.name].png")
|
|
|
|
/mob/proc/AddCamoOverlay(atom/A)//A is the atom which we are using as the overlay.
|
|
var/icon/opacity_icon = new(A.icon, A.icon_state)//Don't really care for overlays/underlays.
|
|
//Now we need to culculate overlays+underlays and add them together to form an image for a mask.
|
|
//var/icon/alpha_mask = getFlatIcon(src)//Accurate but SLOW. Not designed for running each tick. Could have other uses I guess.
|
|
var/icon/alpha_mask = getIconMask(src)//Which is why I created that proc. Also a little slow since it's blending a bunch of icons together but good enough.
|
|
opacity_icon.AddAlphaMask(alpha_mask)//Likely the main source of lag for this proc. Probably not designed to run each tick.
|
|
opacity_icon.ChangeOpacity(0.4)//Front end for MapColors so it's fast. 0.5 means half opacity and looks the best in my opinion.
|
|
for(var/i=0,i<5,i++)//And now we add it as overlays. It's faster than creating an icon and then merging it.
|
|
var/image/I = image("icon" = opacity_icon, "icon_state" = A.icon_state, "layer" = layer+0.8)//So it's above other stuff but below weapons and the like.
|
|
switch(i)//Now to determine offset so the result is somewhat blurred.
|
|
if(1) I.pixel_x--
|
|
if(2) I.pixel_x++
|
|
if(3) I.pixel_y--
|
|
if(4) I.pixel_y++
|
|
overlays += I//And finally add the overlay.
|
|
|
|
/proc/getHologramIcon(icon/A, safety=1, no_color = FALSE)//If safety is on, a new icon is not created.
|
|
var/icon/flat_icon = safety ? A : new(A)//Has to be a new icon to not constantly change the same icon.
|
|
/* VOREStation Removal - For AI Vore effects
|
|
if(!no_color)
|
|
flat_icon.ColorTone(rgb(125,180,225))//Let's make it bluish.
|
|
flat_icon.ChangeOpacity(0.5)//Make it half transparent.
|
|
*/ //VOREStation Removal End
|
|
var/icon/alpha_mask = new('icons/effects/effects.dmi', "scanline")//Scanline effect.
|
|
flat_icon.AddAlphaMask(alpha_mask)//Finally, let's mix in a distortion effect.
|
|
return flat_icon
|
|
|
|
//For photo camera.
|
|
/proc/build_composite_icon(atom/A)
|
|
var/icon/composite = icon(A.icon, A.icon_state, A.dir, 1)
|
|
for(var/image/I as anything in A.overlays)
|
|
composite.Blend(icon(I.icon, I.icon_state, I.dir, 1), ICON_OVERLAY)
|
|
return composite
|
|
|
|
GLOBAL_LIST_EMPTY(icon_state_lists)
|
|
/proc/cached_icon_states(var/icon/I)
|
|
if(!I)
|
|
return list()
|
|
var/key = I
|
|
var/returnlist = GLOB.icon_state_lists[key]
|
|
if(!returnlist)
|
|
returnlist = icon_states(I)
|
|
if(isfile(I)) // It's something that will stick around
|
|
GLOB.icon_state_lists[key] = returnlist
|
|
return returnlist
|
|
|
|
/proc/expire_states_cache(var/key)
|
|
if(GLOB.icon_state_lists[key])
|
|
GLOB.icon_state_lists -= key
|
|
return TRUE
|
|
return FALSE
|
|
|
|
GLOBAL_LIST_EMPTY(cached_examine_icons)
|
|
/proc/set_cached_examine_icon(var/atom/A, var/icon/I, var/expiry = 12000)
|
|
GLOB.cached_examine_icons[WEAKREF(A)] = I
|
|
if(expiry)
|
|
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(uncache_examine_icon), WEAKREF(A)), expiry, TIMER_UNIQUE)
|
|
|
|
/proc/get_cached_examine_icon(var/atom/A)
|
|
var/datum/weakref/WR = WEAKREF(A)
|
|
return GLOB.cached_examine_icons[WR]
|
|
|
|
/proc/uncache_examine_icon(var/datum/weakref/WR)
|
|
GLOB.cached_examine_icons -= WR
|
|
|
|
/proc/adjust_brightness(var/color, var/value)
|
|
if (!color) return "#FFFFFF"
|
|
if (!value) return color
|
|
|
|
var/list/RGB = rgb2num(color)
|
|
RGB[1] = CLAMP(RGB[1]+value,0,255)
|
|
RGB[2] = CLAMP(RGB[2]+value,0,255)
|
|
RGB[3] = CLAMP(RGB[3]+value,0,255)
|
|
return rgb(RGB[1],RGB[2],RGB[3])
|
|
|
|
/proc/sort_atoms_by_layer(var/list/atoms)
|
|
// Comb sort icons based on levels
|
|
var/list/result = atoms.Copy()
|
|
var/gap = result.len
|
|
var/swapped = 1
|
|
while (gap > 1 || swapped)
|
|
swapped = 0
|
|
if(gap > 1)
|
|
gap = round(gap / 1.3) // 1.3 is the emperic comb sort coefficient
|
|
if(gap < 1)
|
|
gap = 1
|
|
for(var/i = 1; gap + i <= result.len; i++)
|
|
var/atom/l = result[i] //Fucking hate
|
|
var/atom/r = result[gap+i] //how lists work here
|
|
if(l.layer > r.layer) //no "result[i].layer" for me
|
|
result.Swap(i, gap + i)
|
|
swapped = 1
|
|
return result
|
|
|
|
/proc/gen_hud_image(var/file, var/person, var/state, var/plane)
|
|
var/image/img = image(file, person, state)
|
|
img.plane = plane //Thanks Byond.
|
|
img.layer = MOB_LAYER-0.2
|
|
img.appearance_flags = APPEARANCE_UI
|
|
return img
|
|
|
|
/**
|
|
* Animate a 'halo' around an object.
|
|
*
|
|
* This proc is not exactly cheap. You'd be well advised to set up many-loops rather than call this super-often. getCompoundIcon is
|
|
* mostly to blame for this. If Byond ever implements a way to get something's icon more 'gently' than this, do that instead.
|
|
*
|
|
* @param A This is the atom to put the halo on
|
|
* @param simple_icons If set to TRUE, will just perform a very basic icon and icon_state steal. DO USE when possible.
|
|
* @param color This is the color for the halo
|
|
* @param anim_duration This decides how fast (or slow) the animation plays
|
|
* @param offset Mysterious variable that determines size of the halo's gap from icon
|
|
* @param loops How many times the animation loops
|
|
* @param grow_to Relative to the size of the icon, how big the halo grows while fading (don't use negatives for inward halos, use < 1)
|
|
* @param pixel_scale If you'd like the halo to use pixel scale or the default 'fuzzy' scale
|
|
*/
|
|
/proc/animate_aura(var/atom/A, var/simple_icons, var/color = "#00FF22", var/anim_duration = 5, var/offset = 1, var/loops = 1, var/grow_to = 2, var/pixel_scale = FALSE)
|
|
ASSERT(A)
|
|
|
|
//Take a guess at this, if they didn't set it
|
|
if(isnull(simple_icons))
|
|
if(ismob(A))
|
|
simple_icons = FALSE
|
|
else
|
|
simple_icons = TRUE
|
|
|
|
//Get their icon
|
|
var/icon/hole
|
|
|
|
if(simple_icons)
|
|
hole = icon(A.icon, A.icon_state)
|
|
else
|
|
hole = getCompoundIcon(A)
|
|
|
|
hole.MapColors(0,0,0, 0,0,0, 0,0,0, 1,1,1) //White.
|
|
|
|
//Make a bigger version
|
|
var/icon/grower = new(hole)
|
|
var/orig_width = grower.Width()
|
|
var/orig_height = grower.Height()
|
|
var/end_width = orig_width+(offset*2)
|
|
var/end_height = orig_height+(offset*2)
|
|
var/half_diff_width = (end_width-orig_width)*0.5
|
|
var/half_diff_height = (end_height-orig_height)*0.5
|
|
|
|
//Make icon black
|
|
grower.SwapColor("#FFFFFF","#000000") //Black.
|
|
|
|
//Scale both icons big so we don't have to deal with low-pixel garbage issues
|
|
grower.Scale(orig_width*10,orig_height*10)
|
|
hole.Scale(orig_width*9,orig_height*9)
|
|
|
|
//Blend the hole in
|
|
grower.Blend(hole,ICON_OVERLAY, x = ((orig_width*10-orig_width*9)*0.5)+1, y = ((orig_height*10-orig_height*9)*0.5)+1)
|
|
|
|
//Swap white to zero alpha
|
|
grower.SwapColor("#FFFFFF","#00000000")
|
|
|
|
//Color it
|
|
grower.SwapColor("#000000",color)
|
|
|
|
//Scale it to final height
|
|
grower.Scale(end_width,end_height)
|
|
|
|
//Flick it onto them
|
|
var/image/img = image(grower,A)
|
|
if(pixel_scale)
|
|
img.appearance_flags |= PIXEL_SCALE
|
|
img.pixel_x = half_diff_width*-1
|
|
img.pixel_y = half_diff_height*-1
|
|
flick_overlay_view(img, A, anim_duration*loops, TRUE)
|
|
|
|
//Animate it growing
|
|
animate(img, alpha = 0, transform = matrix()*grow_to, time = anim_duration, loop = loops)
|
|
|
|
/// generates a filename for a given asset.
|
|
/// like generate_asset_name(), except returns the rsc reference and the rsc file hash as well as the asset name (sans extension)
|
|
/// used so that certain asset files dont have to be hashed twice
|
|
/proc/generate_and_hash_rsc_file(file, dmi_file_path)
|
|
var/rsc_ref = fcopy_rsc(file)
|
|
var/hash
|
|
//if we have a valid dmi file path we can trust md5'ing the rsc file because we know it doesnt have the bug described in http://www.byond.com/forum/post/2611357
|
|
if(dmi_file_path)
|
|
hash = md5(rsc_ref)
|
|
else //otherwise, we need to do the expensive fcopy() workaround
|
|
hash = md5asfile(rsc_ref)
|
|
|
|
return list(rsc_ref, hash, "asset.[hash]")
|
|
|
|
/// Gets a dummy savefile for usage in icon generation.
|
|
/// Savefiles generated from this proc will be empty.
|
|
/proc/get_dummy_savefile(from_failure = FALSE)
|
|
var/static/next_id = 0
|
|
if(next_id++ > 9)
|
|
next_id = 0
|
|
var/savefile_path = "tmp/dummy-save-[next_id].sav"
|
|
try
|
|
if(fexists(savefile_path))
|
|
fdel(savefile_path)
|
|
return new /savefile(savefile_path)
|
|
catch(var/exception/error)
|
|
// if we failed to create a dummy once, try again; maybe someone slept somewhere they shouldnt have
|
|
if(from_failure) // this *is* the retry, something fucked up
|
|
CRASH("get_dummy_savefile failed to create a dummy savefile: '[error]'")
|
|
return get_dummy_savefile(from_failure = TRUE)
|
|
|
|
/// Generate a filename for this asset
|
|
/// The same asset will always lead to the same asset name
|
|
/// (Generated names do not include file extention.)
|
|
/proc/generate_asset_name(file)
|
|
return "asset.[md5(fcopy_rsc(file))]"
|
|
|
|
/**
|
|
* Converts an icon to base64. Operates by putting the icon in the iconCache savefile,
|
|
* exporting it as text, and then parsing the base64 from that.
|
|
* (This relies on byond automatically storing icons in savefiles as base64)
|
|
*/
|
|
/proc/icon2base64(icon/icon)
|
|
if (!isicon(icon))
|
|
return FALSE
|
|
var/savefile/dummySave = get_dummy_savefile()
|
|
WRITE_FILE(dummySave["dummy"], icon)
|
|
var/iconData = dummySave.ExportText("dummy")
|
|
var/list/partial = splittext(iconData, "{")
|
|
return replacetext(copytext_char(partial[2], 3, -5), "\n", "") //if cleanup fails we want to still return the correct base64
|
|
|
|
///given a text string, returns whether it is a valid dmi icons folder path
|
|
/proc/is_valid_dmi_file(icon_path)
|
|
if(!istext(icon_path) || !length(icon_path))
|
|
return FALSE
|
|
|
|
var/is_in_icon_folder = findtextEx(icon_path, "icons/")
|
|
var/is_dmi_file = findtextEx(icon_path, ".dmi")
|
|
|
|
if(is_in_icon_folder && is_dmi_file)
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/// given an icon object, dmi file path, or atom/image/mutable_appearance, attempts to find and return an associated dmi file path.
|
|
/// a weird quirk about dm is that /icon objects represent both compile-time or dynamic icons in the rsc,
|
|
/// but stringifying rsc references returns a dmi file path
|
|
/// ONLY if that icon represents a completely unchanged dmi file from when the game was compiled.
|
|
/// so if the given object is associated with an icon that was in the rsc when the game was compiled, this returns a path. otherwise it returns ""
|
|
/proc/get_icon_dmi_path(icon/icon)
|
|
/// the dmi file path we attempt to return if the given object argument is associated with a stringifiable icon
|
|
/// if successful, this looks like "icons/path/to/dmi_file.dmi"
|
|
var/icon_path = ""
|
|
|
|
if(isatom(icon) || istype(icon, /image) || istype(icon, /mutable_appearance))
|
|
var/atom/atom_icon = icon
|
|
icon = atom_icon.icon
|
|
//atom icons compiled in from 'icons/path/to/dmi_file.dmi' are weird and not really icon objects that you generate with icon().
|
|
//if theyre unchanged dmi's then they're stringifiable to "icons/path/to/dmi_file.dmi"
|
|
|
|
if(isicon(icon) && isfile(icon))
|
|
//icons compiled in from 'icons/path/to/dmi_file.dmi' at compile time are weird and arent really /icon objects,
|
|
///but they pass both isicon() and isfile() checks. theyre the easiest case since stringifying them gives us the path we want
|
|
var/icon_ref = "\ref[icon]"
|
|
var/locate_icon_string = "[locate(icon_ref)]"
|
|
|
|
icon_path = locate_icon_string
|
|
|
|
else if(isicon(icon) && "[icon]" == "/icon")
|
|
// icon objects generated from icon() at runtime are icons, but they ARENT files themselves, they represent icon files.
|
|
// if the files they represent are compile time dmi files in the rsc, then
|
|
// the rsc reference returned by fcopy_rsc() will be stringifiable to "icons/path/to/dmi_file.dmi"
|
|
var/rsc_ref = fcopy_rsc(icon)
|
|
|
|
var/icon_ref = "\ref[rsc_ref]"
|
|
|
|
var/icon_path_string = "[locate(icon_ref)]"
|
|
|
|
icon_path = icon_path_string
|
|
|
|
else if(istext(icon))
|
|
var/rsc_ref = fcopy_rsc(icon)
|
|
//if its the text path of an existing dmi file, the rsc reference returned by fcopy_rsc() will be stringifiable to a dmi path
|
|
|
|
var/rsc_ref_ref = "\ref[rsc_ref]"
|
|
var/rsc_ref_string = "[locate(rsc_ref_ref)]"
|
|
|
|
icon_path = rsc_ref_string
|
|
|
|
if(is_valid_dmi_file(icon_path))
|
|
return icon_path
|
|
|
|
return FALSE
|
|
|
|
/// Asks the user for an icon (either from file or as a path) and offers to customize it if possible (e.g. setting icon_state)
|
|
/proc/pick_and_customize_icon(mob/user, pick_only=FALSE)
|
|
var/icon/icon_result = null
|
|
if(!user)
|
|
user = usr
|
|
|
|
var/icon_from_file = tgui_alert(user, "Do you wish to pick an icon from file?", "File picker icon", list("Yes", "No"))
|
|
if(isnull(icon_from_file))
|
|
return null
|
|
if(icon_from_file == "Yes")
|
|
icon_result = input(user, "Pick icon:", "Icon") as null|icon
|
|
if(!icon_result)
|
|
return null
|
|
else if(icon_from_file == "No")
|
|
var/new_icon = tgui_input_text(user, "Pick icon path", "icon path")
|
|
if(isnull(new_icon))
|
|
return null
|
|
var/regex/regex = regex(@"^.+icons/")
|
|
new_icon = regex.Replace(replacetext(new_icon, @"\", "/"), "icons/")
|
|
icon_result = fcopy_rsc(new_icon)
|
|
if(!icon_result)
|
|
to_chat(user, span_warning("'[new_icon]' is an invalid icon path!"))
|
|
return null
|
|
|
|
var/dmi_path = get_icon_dmi_path(icon_result)
|
|
if(!dmi_path || pick_only)
|
|
return icon_result
|
|
|
|
var/custom = tgui_alert(user, "Do you wish to specify any arguments for the icon?", "Customize Icon", list("Yes", "No"))
|
|
if(isnull(custom))
|
|
return null
|
|
if(custom == "Yes")
|
|
var/new_icon_state = tgui_input_text(user, "Pick icon_state", "icon_state")
|
|
if(isnull(new_icon_state))
|
|
return null
|
|
var/new_icon_dir = tgui_input_list(user, "Pick icon dir", "dir", list("North", "East", "South", "West"), default="South")
|
|
if(isnull(new_icon_dir))
|
|
return null
|
|
var/new_icon_frame = tgui_input_number(user, "Pick icon frame", "frame", min_value=0, round_value=TRUE)
|
|
if(isnull(new_icon_frame))
|
|
return null
|
|
var/new_icon_moving = tgui_input_list(user, "Pick icon moving", "moving", list("Both", "Movement only", "Non-Movement Only"), default="Both")
|
|
switch(new_icon_moving)
|
|
if("Both")
|
|
new_icon_moving = null
|
|
if("Movement only")
|
|
new_icon_moving = 1
|
|
if("Non-Movement Only")
|
|
new_icon_moving = 0
|
|
else
|
|
return null
|
|
icon_result = new(dmi_path, new_icon_state, text2dir(new_icon_dir), new_icon_frame, new_icon_moving)
|
|
|
|
return icon_result
|
|
|
|
/**
|
|
* generate an asset for the given icon or the icon of the given appearance for [thing], and send it to any clients in target.
|
|
* Arguments:
|
|
* * thing - either a /icon object, or an object that has an appearance (atom, image, mutable_appearance).
|
|
* * target - either a reference to or a list of references to /client's or mobs with clients
|
|
* * icon_state - string to force a particular icon_state for the icon to be used
|
|
* * dir - dir number to force a particular direction for the icon to be used
|
|
* * frame - what frame of the icon_state's animation for the icon being used
|
|
* * moving - whether or not to use a moving state for the given icon
|
|
* * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players
|
|
* * extra_clases - string of extra css classes to use when returning the icon string
|
|
*/
|
|
/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null) //CHOMPEdit
|
|
if (!thing)
|
|
return
|
|
//if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
|
|
//return
|
|
|
|
var/key
|
|
var/icon/icon2collapse = thing
|
|
|
|
if (!target)
|
|
return
|
|
if (target == world)
|
|
target = GLOB.clients
|
|
|
|
var/list/targets
|
|
if (!islist(target))
|
|
targets = list(target)
|
|
else
|
|
targets = target
|
|
if(!length(targets))
|
|
return
|
|
|
|
//check if the given object is associated with a dmi file in the icons folder. if it is then we dont need to do a lot of work
|
|
//for asset generation to get around byond limitations
|
|
var/icon_path = get_icon_dmi_path(thing)
|
|
|
|
if (!isicon(icon2collapse))
|
|
if (isfile(thing)) //special snowflake
|
|
var/name = SANITIZE_FILENAME("[generate_asset_name(thing)].png")
|
|
if (!SSassets.cache[name])
|
|
SSassets.transport.register_asset(name, thing)
|
|
for (var/thing2 in targets)
|
|
SSassets.transport.send_assets(thing2, name)
|
|
if(sourceonly)
|
|
return SSassets.transport.get_asset_url(name)
|
|
return "<img class='[extra_classes] icon icon-misc' src='[SSassets.transport.get_asset_url(name)]'>"
|
|
|
|
//its either an atom, image, or mutable_appearance, we want its icon var
|
|
icon2collapse = thing.icon
|
|
|
|
if (isnull(icon_state))
|
|
icon_state = thing.icon_state
|
|
//Despite casting to atom, this code path supports mutable appearances, so let's be nice to them
|
|
//if(isnull(icon_state) || (isatom(thing) && thing.flags_1 & HTML_USE_INITAL_ICON_1))
|
|
if(isnull(icon_state) && isatom(thing))
|
|
icon_state = initial(thing.icon_state)
|
|
if (isnull(dir))
|
|
dir = initial(thing.dir)
|
|
|
|
if (isnull(dir))
|
|
dir = thing.dir
|
|
|
|
if (ishuman(thing)) // Shitty workaround for a BYOND issue.
|
|
var/icon/temp = icon2collapse
|
|
icon2collapse = icon()
|
|
icon2collapse.Insert(temp, dir = SOUTH)
|
|
dir = SOUTH
|
|
else
|
|
if (isnull(dir))
|
|
dir = SOUTH
|
|
if (isnull(icon_state))
|
|
icon_state = ""
|
|
|
|
icon2collapse = icon(icon2collapse, icon_state, dir, frame, moving)
|
|
|
|
var/list/name_and_ref = generate_and_hash_rsc_file(icon2collapse, icon_path)//pretend that tuples exist
|
|
|
|
var/rsc_ref = name_and_ref[1] //weird object thats not even readable to the debugger, represents a reference to the icons rsc entry
|
|
var/file_hash = name_and_ref[2]
|
|
key = "[name_and_ref[3]].png"
|
|
|
|
if(!SSassets.cache[key])
|
|
SSassets.transport.register_asset(key, rsc_ref, file_hash, icon_path)
|
|
for (var/client_target in targets)
|
|
SSassets.transport.send_assets(client_target, key)
|
|
if(sourceonly)
|
|
return SSassets.transport.get_asset_url(key)
|
|
return "<img class='[extra_classes] icon icon-[icon_state]' src='[SSassets.transport.get_asset_url(key)]'>"
|
|
|
|
/proc/icon2base64html(target, var/custom_classes = "")
|
|
if (!target)
|
|
return
|
|
var/static/list/bicon_cache = list()
|
|
if (isicon(target))
|
|
var/icon/target_icon = target
|
|
var/icon_base64 = icon2base64(target_icon)
|
|
|
|
if (target_icon.Height() > world.icon_size || target_icon.Width() > world.icon_size)
|
|
var/icon_md5 = md5(icon_base64)
|
|
icon_base64 = bicon_cache[icon_md5]
|
|
if (!icon_base64) // Doesn't exist yet, make it.
|
|
bicon_cache[icon_md5] = icon_base64 = icon2base64(target_icon)
|
|
|
|
|
|
return "<img class='icon icon-misc [custom_classes]' src='data:image/png;base64,[icon_base64]'>"
|
|
|
|
// Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with.
|
|
var/atom/target_atom = target
|
|
var/key = "[istype(target_atom.icon, /icon) ? "[REF(target_atom.icon)]" : target_atom.icon]:[target_atom.icon_state]"
|
|
|
|
|
|
if (!bicon_cache[key]) // Doesn't exist, make it.
|
|
var/icon/target_icon = icon(target_atom.icon, target_atom.icon_state, SOUTH, 1)
|
|
if (ishuman(target)) // Shitty workaround for a BYOND issue.
|
|
var/icon/temp = target_icon
|
|
target_icon = icon()
|
|
target_icon.Insert(temp, dir = SOUTH)
|
|
|
|
bicon_cache[key] = icon2base64(target_icon)
|
|
|
|
return "<img class='icon icon-[target_atom.icon_state] [custom_classes]' src='data:image/png;base64,[bicon_cache[key]]'>"
|
|
|
|
//Costlier version of icon2html() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs.
|
|
/proc/costly_icon2html(thing, target, sourceonly = FALSE, force_south = FALSE)
|
|
if (!thing)
|
|
return
|
|
//if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
|
|
//return
|
|
|
|
if (isicon(thing))
|
|
return icon2html(thing, target)
|
|
|
|
var/icon/I = getFlatIcon(thing, force_south = force_south)
|
|
return icon2html(I, target, sourceonly = sourceonly)
|
|
|
|
/// Checks whether a given icon state exists in a given icon file. If `file` and `state` both exist,
|
|
/// this will return `TRUE` - otherwise, it will return `FALSE`.
|
|
///
|
|
/// If you want a stack trace to be output when the given state/file doesn't exist, use
|
|
/// `/proc/icon_exists_or_scream()`.
|
|
/proc/icon_exists(file, state)
|
|
var/static/list/icon_states_cache = list()
|
|
if(isnull(file) || isnull(state))
|
|
return FALSE //This is common enough that it shouldn't panic, imo.
|
|
|
|
if(isnull(icon_states_cache[file]))
|
|
icon_states_cache[file] = list()
|
|
var/file_string = "[file]"
|
|
if(isfile(file) && length(file_string)) // ensure that it's actually a file, and not a runtime icon
|
|
for(var/istate in json_decode(rustg_dmi_icon_states(file_string)))
|
|
icon_states_cache[file][istate] = TRUE
|
|
else // Otherwise, we have to use the slower BYOND proc
|
|
for(var/istate in icon_states(file))
|
|
icon_states_cache[file][istate] = TRUE
|
|
|
|
return !isnull(icon_states_cache[file][state])
|