Files
Bubberstation/code/datums/elements/immerse.dm
Ghom 0e3c83ff3a Revert "Makes immerse use weakrefs", prioritizing some ref removal lines instead. (#80707)
## About The Pull Request
For a moment, I had forgot about saying I'd take a look into it, but it
seems the new fix to hard dels is causing some runtimes with empty
weakrefs. Beside, WEAKREF() doesn't work well with qdeleting atoms (so
you'd have to access the weak_reference var directly).
How immersion works is quite confusing even for me who coded it, trying
to work around some of the hefty limitations of the engine truly blows.
I could even ask MrMelbert to make a proc-chain chart for it.

But yeah, long story short, I only have a bare idea where the uncleared
refs would be. I suspect it could be `immersed_movables`. It's totally
possible since the proc can early return in a few cases, thus skipping
the ref removal, hence the title.

## Why It's Good For The Game
I didn't like the PR that implemented weakref usage into the element,
but I let it pass because "hard dels = bad". However, the runtimes
aren't that much more pleasant either.

## Changelog
N/A
2024-01-17 04:38:13 -05:00

412 lines
19 KiB
Plaintext

/**
* A visual element that makes movables entering the attached turfs look immersed into that turf.
*
* Abandon all hope, ye who read forth, for this immerse works on mind-numbing workarounds,
*/
/datum/element/immerse
element_flags = ELEMENT_DETACH_ON_HOST_DESTROY | ELEMENT_BESPOKE
argument_hash_start_idx = 2
///An association list of turfs that have this element attached and their affected contents.
var/list/attached_turfs_and_movables = list()
/**
* A list of movables that shouldn't be affected by the element, either because it'd look bad
* or barely perceptible.
*/
var/static/list/movables_to_ignore
///A list of icons generated from a target and a mask, later used as appearances for the overlays.
var/static/list/generated_immerse_icons = list()
///A list of instances of /atom/movable/immerse_overlay then used as visual overlays for the immersed movables.
var/list/generated_visual_overlays = list()
///An association list of movables as key and overlays as assoc.
var/list/immersed_movables
var/icon
var/icon_state
var/mask_icon
var/color
var/alpha
/datum/element/immerse/Attach(turf/target, icon, icon_state, mask_icon, color = "#777777", alpha = 180)
. = ..()
if(!isturf(target) || !icon || !icon_state || !mask_icon)
return ELEMENT_INCOMPATIBLE
if(isnull(movables_to_ignore))
movables_to_ignore = typecacheof(list(
/obj/effect,
/mob/dead,
/obj/projectile,
))
movables_to_ignore += GLOB.WALLITEMS_INTERIOR
movables_to_ignore += GLOB.WALLITEMS_EXTERIOR
src.icon = icon
src.icon_state = icon_state
src.color = color
src.alpha = alpha
src.mask_icon = mask_icon
/**
* Hello, you may be wondering why we're blending icons and not simply
* overlaying one mutable appearance with the blend multiply on another.
* Well, the latter option doesn't work as neatly when added
* to an atom with the KEEP_TOGETHER appearance flag, with the mask icon also
* showing on said atom, while we don't want it to.
*
* Also using KEEP_APART isn't an option, because unless it's drawn as one with
* its visual loation, the whole plane the atom belongs to will count as part of the
* mask of the final visual overlay since that's how the BLEND_INSET_OVERLAY blend mode works here.
* In layman terms, with KEEP_APART on, if a flying monkey gets nears an immersed
* human, the visual overlay will appear on the flying monkey even if it shouldn't.
*/
var/icon/immerse_icon = generated_immerse_icons["[icon]-[icon_state]-[mask_icon]"]
if(!immerse_icon)
immerse_icon = icon(icon, icon_state)
var/icon/sub_mask = icon('icons/effects/effects.dmi', mask_icon)
immerse_icon.Blend(sub_mask, ICON_MULTIPLY)
immerse_icon = fcopy_rsc(immerse_icon)
generated_immerse_icons["[icon]-[icon_state]-[mask_icon]"] = immerse_icon
RegisterSignal(target, SIGNAL_ADDTRAIT(TRAIT_IMMERSE_STOPPED), PROC_REF(stop_immersion))
RegisterSignal(target, SIGNAL_REMOVETRAIT(TRAIT_IMMERSE_STOPPED), PROC_REF(start_immersion))
if(!HAS_TRAIT(target, TRAIT_IMMERSE_STOPPED))
start_immersion(target)
/datum/element/immerse/Detach(turf/source)
UnregisterSignal(source, list(SIGNAL_ADDTRAIT(TRAIT_IMMERSE_STOPPED), SIGNAL_REMOVETRAIT(TRAIT_IMMERSE_STOPPED)))
if(!HAS_TRAIT(source, TRAIT_IMMERSE_STOPPED))
stop_immersion(source)
return ..()
///Makes the element start affecting the turf and its contents. Called on Attach() or when TRAIT_IMMERSE_STOPPED is removed.
/datum/element/immerse/proc/start_immersion(turf/source)
SIGNAL_HANDLER
RegisterSignals(source, list(COMSIG_ATOM_ABSTRACT_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON), PROC_REF(on_init_or_entered))
RegisterSignal(source, COMSIG_ATOM_ABSTRACT_EXITED, PROC_REF(on_atom_exited))
attached_turfs_and_movables += source
for(var/atom/movable/movable as anything in source)
if(!(movable.flags_1 & INITIALIZED_1))
continue
on_init_or_entered(source, movable)
///Stops the element from affecting on the turf and its contents. Called on Detach() or when TRAIT_IMMERSE_STOPPED is added.
/datum/element/immerse/proc/stop_immersion(turf/source)
SIGNAL_HANDLER
UnregisterSignal(source, list(COMSIG_ATOM_ABSTRACT_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, COMSIG_ATOM_ABSTRACT_EXITED))
for(var/atom/movable/movable as anything in attached_turfs_and_movables[source])
remove_from_element(source, movable)
attached_turfs_and_movables -= source
/**
* If the movable is within the right layers and planes, not in the list of movable types to ignore,
* or already affected by the element for that matter, Signals will be registered and,
* unless the movable (or whatever it's buckled to) is flying, it'll appear as if immersed in that water.
*/
/datum/element/immerse/proc/on_init_or_entered(turf/source, atom/movable/movable)
SIGNAL_HANDLER
if(HAS_TRAIT(movable, TRAIT_IMMERSED))
return
if(movable.layer >= ABOVE_ALL_MOB_LAYER || !ISINRANGE(movable.plane, MUTATE_PLANE(FLOOR_PLANE, source), MUTATE_PLANE(GAME_PLANE, source)))
return
if(is_type_in_typecache(movable, movables_to_ignore))
return
var/atom/movable/buckled
if(isliving(movable))
var/mob/living/living_mob = movable
RegisterSignal(living_mob, COMSIG_LIVING_SET_BUCKLED, PROC_REF(on_set_buckled))
buckled = living_mob.buckled
try_immerse(movable, buckled)
RegisterSignal(movable, COMSIG_QDELETING, PROC_REF(on_movable_qdel))
LAZYADD(attached_turfs_and_movables[source], movable)
ADD_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src))
/datum/element/immerse/proc/on_movable_qdel(atom/movable/source)
SIGNAL_HANDLER
remove_from_element(source.loc, source)
/**
* The main proc, which adds a visual overlay to the movable that has entered the turf to make it look immersed.
* It's kind of iffy but basically, we want the overlay to cover as much area as needed to
* avoid the movable's icon from spilling horizontally or below.
* Also, while these visual overlays are mainly cached movables, for certain movables, such as living mobs,
* we want them to have their own unique vis overlay with additional signals registered.
* This allows the vis overlay to look more or less unchanged while its owner is spinning or resting
* without otherwise affecting other movables with identical overlays.
*/
/datum/element/immerse/proc/add_immerse_overlay(atom/movable/movable)
var/list/icon_dimensions = get_icon_dimensions(movable.icon)
var/width = icon_dimensions["width"] || world.icon_size
var/height = icon_dimensions["height"] || world.icon_size
var/is_below_water = movable.layer < WATER_LEVEL_LAYER ? "underwater-" : ""
var/atom/movable/immerse_overlay/vis_overlay = generated_visual_overlays["[is_below_water][width]x[height]"]
if(!vis_overlay) //create the overlay if not already done.
vis_overlay = generate_vis_overlay(width, height, is_below_water)
ADD_KEEP_TOGETHER(movable, ELEMENT_TRAIT(src))
/**
* Let's give an unique immerse visual only to those movables that would
* benefit from this the most, for the sake of a smidge of lightweightness.
*/
if(HAS_TRAIT(movable, TRAIT_UNIQUE_IMMERSE))
var/atom/movable/immerse_overlay/original_vis_overlay = vis_overlay
vis_overlay = new(null)
vis_overlay.appearance = original_vis_overlay
vis_overlay.extra_width = original_vis_overlay.extra_width
vis_overlay.extra_height = original_vis_overlay.extra_height
vis_overlay.overlay_appearance = original_vis_overlay.overlay_appearance
SEND_SIGNAL(movable, COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY, vis_overlay)
RegisterSignal(movable, COMSIG_ATOM_SPIN_ANIMATION, PROC_REF(on_spin_animation))
RegisterSignal(movable, COMSIG_LIVING_POST_UPDATE_TRANSFORM, PROC_REF(on_update_transform))
movable.vis_contents |= vis_overlay
LAZYSET(immersed_movables, movable, vis_overlay)
///Initializes and caches a new visual overlay given parameters such as width, height and whether it should appear fully underwater.
/datum/element/immerse/proc/generate_vis_overlay(width, height, is_below_water)
var/atom/movable/immerse_overlay/vis_overlay = new(null, src)
/**
* vis contents spin around the center of the icon of their vis locs
* but since we want the appearance to stay where it should be,
* we have to counteract this one.
*/
var/extra_width = (width - world.icon_size) * 0.5
var/extra_height = (height - world.icon_size) * 0.5
var/mutable_appearance/overlay_appearance = new()
var/icon/immerse_icon = generated_immerse_icons["[icon]-[icon_state]-[mask_icon]"]
var/last_i = width/world.icon_size
for(var/i in -1 to last_i)
var/mutable_appearance/underwater = mutable_appearance(icon, icon_state)
underwater.pixel_x = world.icon_size * i - extra_width
underwater.pixel_y = -world.icon_size - extra_height
overlay_appearance.overlays += underwater
var/mutable_appearance/water_level = is_below_water ? underwater : mutable_appearance(immerse_icon)
water_level.pixel_x = world.icon_size * i - extra_width
water_level.pixel_y = -extra_height
overlay_appearance.overlays += water_level
vis_overlay.color = color
vis_overlay.alpha = alpha
vis_overlay.overlays = list(overlay_appearance)
vis_overlay.extra_width = extra_width
vis_overlay.extra_height = extra_height
vis_overlay.overlay_appearance = overlay_appearance
generated_visual_overlays["[is_below_water][width]x[height]"] = vis_overlay
return vis_overlay
///This proc removes the vis_overlay, the keep together trait and some signals from the movable.
/datum/element/immerse/proc/remove_immerse_overlay(atom/movable/movable)
var/atom/movable/immerse_overlay/vis_overlay = LAZYACCESS(immersed_movables, movable)
LAZYREMOVE(immersed_movables, movable)
REMOVE_KEEP_TOGETHER(movable, ELEMENT_TRAIT(src))
movable.vis_contents -= vis_overlay
if(HAS_TRAIT(movable, TRAIT_UNIQUE_IMMERSE))
UnregisterSignal(movable, list(COMSIG_ATOM_SPIN_ANIMATION, COMSIG_LIVING_POST_UPDATE_TRANSFORM, COMSIG_QDELETING))
if(!QDELETED(vis_overlay))
qdel(vis_overlay)
/**
* Called by init_or_entered() and on_set_buckled().
* This applies the overlay if neither the movable or whatever is buckled to (exclusive to living mobs) are flying
* as well as movetype signals when the movable isn't buckled.
*/
/datum/element/immerse/proc/try_immerse(atom/movable/movable, atom/movable/buckled)
var/atom/movable/to_check = buckled || movable
if(!(to_check.movement_type & MOVETYPES_NOT_TOUCHING_GROUND) && !movable.throwing)
add_immerse_overlay(movable)
if(!buckled)
RegisterSignal(movable, COMSIG_MOVETYPE_FLAG_ENABLED, PROC_REF(on_move_flag_enabled))
RegisterSignal(movable, COMSIG_MOVETYPE_FLAG_DISABLED, PROC_REF(on_move_flag_disabled))
RegisterSignal(movable, COMSIG_MOVABLE_POST_THROW, PROC_REF(on_throw))
RegisterSignal(movable, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(on_throw_landed))
/**
* Called by on_set_buckled() and remove_from_element().
* This removes the filter and signals from the movable unless it doesn't have them.
*/
/datum/element/immerse/proc/try_unimmerse(atom/movable/movable, atom/movable/buckled)
var/atom/movable/to_check = buckled || movable
if(!(to_check.movement_type & MOVETYPES_NOT_TOUCHING_GROUND) && !movable.throwing)
remove_immerse_overlay(movable)
if(!buckled)
UnregisterSignal(movable, list(COMSIG_MOVETYPE_FLAG_ENABLED, COMSIG_MOVETYPE_FLAG_DISABLED, COMSIG_MOVABLE_POST_THROW, COMSIG_MOVABLE_THROW_LANDED))
/datum/element/immerse/proc/on_set_buckled(mob/living/source, atom/movable/new_buckled)
SIGNAL_HANDLER
try_unimmerse(source, source.buckled)
try_immerse(source, new_buckled)
///Removes the overlay from mob and bucklees is flying.
/datum/element/immerse/proc/on_move_flag_enabled(atom/movable/source, flag, old_movement_type)
SIGNAL_HANDLER
if(!(flag & MOVETYPES_NOT_TOUCHING_GROUND) || (old_movement_type & MOVETYPES_NOT_TOUCHING_GROUND) || source.throwing)
return
remove_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
remove_immerse_overlay(buckled_mob)
///Works just like on_move_flag_enabled, except it only has to check that movable isn't flying
/datum/element/immerse/proc/on_throw(atom/movable/source)
SIGNAL_HANDLER
if(source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return
remove_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
remove_immerse_overlay(buckled_mob)
///Readds the overlay to the mob and bucklees if no longer flying.
/datum/element/immerse/proc/on_move_flag_disabled(atom/movable/source, flag, old_movement_type)
SIGNAL_HANDLER
if(!(flag & MOVETYPES_NOT_TOUCHING_GROUND) || (source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND) || source.throwing)
return
add_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
add_immerse_overlay(buckled_mob)
///Works just like on_move_flag_disabled, except it only has to check that movable isn't flying
/datum/element/immerse/proc/on_throw_landed(atom/movable/source)
SIGNAL_HANDLER
if(source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return
add_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
add_immerse_overlay(buckled_mob)
/**
* Called when a movable exits the turf. If its new location is not in the list of turfs with this element,
* Remove the movable from the element.
*/
/datum/element/immerse/proc/on_atom_exited(turf/source, atom/movable/exited, direction)
SIGNAL_HANDLER
if(!(exited.loc in attached_turfs_and_movables))
remove_from_element(source, exited)
else
LAZYREMOVE(attached_turfs_and_movables[source], exited)
LAZYADD(attached_turfs_and_movables[exited.loc], exited)
///Remove any signal, overlay, trait given to the movable and reference to it within the element.
/datum/element/immerse/proc/remove_from_element(turf/source, atom/movable/movable)
var/atom/movable/buckled
if(isliving(movable))
var/mob/living/living_mob = movable
buckled = living_mob.buckled
try_unimmerse(movable, buckled)
LAZYREMOVE(attached_turfs_and_movables[source], movable)
UnregisterSignal(movable, list(COMSIG_LIVING_SET_BUCKLED, COMSIG_QDELETING))
REMOVE_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src))
/// A band-aid to keep the (unique) visual overlay from scaling and rotating along with its owner. I'm sorry.
/datum/element/immerse/proc/on_update_transform(mob/living/source, resize, new_lying_angle, is_opposite_angle)
SIGNAL_HANDLER
var/matrix/new_transform = matrix()
new_transform.Scale(1/source.current_size)
new_transform.Turn(-new_lying_angle)
var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[source]
if(is_opposite_angle)
vis_overlay.transform = new_transform
vis_overlay.adjust_living_overlay_offset(source)
return
/**
* Here, we temporarily switch from the offset of the mutable appearance to one for movable used as visual overlay.
* Why? While visual overlays can be animated, their fixed point stays at the center of the icon of the atom
* they're attached to and not theirs, which can make manipulating the transform var a pain, but because
* we cannot do that with normal overlay or filters (reliably), we have to bend a knee and try to compensate it.
*/
vis_overlay.overlays = list(vis_overlay.overlay_appearance)
/// Oh, yeah, didn't I mention turning a visual overlay affects its pixel x/y/w/z too? Yeah, it sucks.
var/new_x = vis_overlay.extra_width
var/new_y = vis_overlay.extra_height
var/old_div = source.current_size/resize
var/offset_lying = source.rotate_on_lying ? PIXEL_Y_OFFSET_LYING : source.get_pixel_y_offset_standing(source.current_size/resize)
switch(source.lying_prev)
if(270)
vis_overlay.pixel_x += -offset_lying / old_div
if(90)
vis_overlay.pixel_x += offset_lying / old_div
if(0)
vis_overlay.pixel_y += -source.get_pixel_y_offset_standing(source.current_size/resize) / old_div
switch(new_lying_angle)
if(270)
new_x += -source.body_position_pixel_y_offset / source.current_size
if(90)
new_x += source.body_position_pixel_y_offset / source.current_size
if(0)
new_y += -source.body_position_pixel_y_offset / source.current_size
animate(vis_overlay, transform = new_transform, pixel_x = new_x, pixel_y = new_y, time = UPDATE_TRANSFORM_ANIMATION_TIME, easing = (EASE_IN|EASE_OUT))
addtimer(CALLBACK(vis_overlay, TYPE_PROC_REF(/atom/movable/immerse_overlay, adjust_living_overlay_offset), source), UPDATE_TRANSFORM_ANIMATION_TIME)
///Spin the overlay in the opposite direction so it doesn't look like it's spinning at all.
/datum/element/immerse/proc/on_spin_animation(atom/source, speed, loops, segments, segment)
SIGNAL_HANDLER
var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[source]
vis_overlay.do_spin_animation(speed, loops, segments, -segment)
///We need to make sure to remove hard refs from the element when deleted.
/datum/element/immerse/proc/clear_overlay_refs(atom/movable/immerse_overlay/source)
//Assume that every vis loc is also in the immersed_movables list
for(var/atom/movable/vis_loc as anything in source.vis_locs)
remove_from_element(vis_loc.loc, vis_loc)
LAZYREMOVE(generated_visual_overlays, source)
source.overlay_appearance = null
///The not-quite-perfect movable used by the immerse element for its nefarious deeds.
/atom/movable/immerse_overlay
appearance_flags = RESET_TRANSFORM|RESET_COLOR|RESET_ALPHA|KEEP_TOGETHER
vis_flags = VIS_INHERIT_PLANE|VIS_INHERIT_ID
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
blend_mode = BLEND_INSET_OVERLAY
layer = WATER_VISUAL_OVERLAY_LAYER
plane = FLOAT_PLANE
/**
* The actual overlay used to make the mob look like it's half-covered in water.
*
* For visual overlays, pixel y/x/w/z are amplified by the a, b, d, e variables
* of the transform matrix of the movable they're attached to.
* For example, if a mob is twice its normal size (a = 2, e = 2),
* offsetting the movable used as visual overlay by 4 pixels to the right will result
* in the visual overlay moving 8 pixels to the right.
*
* This however, doesn't extend to the overlays of our visual overlay. which is why there's
* a mutable appearance variable that we use for those pixel offsets that really shouldn't be affected
* by the transform of our vis loc(s) in the first place.
*/
var/mutable_appearance/overlay_appearance
///The base pixel x offset of this movable
var/extra_width = 0
///The base pixel y offset of this movable
var/extra_height = 0
/atom/movable/immerse_overlay/Initialize(mapload, datum/element/immerse/element)
. = ..()
verbs.Cut() //"Cargo cultttttt" or something. Either way, they're better off without verbs.
element?.RegisterSignal(src, COMSIG_QDELETING, TYPE_PROC_REF(/datum/element/immerse, clear_overlay_refs))
///Called by COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY for living mobs and a few procs from the immerse element.
/atom/movable/immerse_overlay/proc/adjust_living_overlay_offset(mob/living/source)
pixel_x = extra_width
pixel_y = extra_height
overlay_appearance.pixel_y = -source.body_position_pixel_y_offset
overlays = list(overlay_appearance)
overlay_appearance.pixel_y = 0