Files
Bubberstation/code/datums/elements/immerse.dm
SmArtKar 5ee4ab33c6 Fixes immerse element runtimes (#93229)
## About The Pull Request

The runtimes themselves don't actually cause any issues in-game, as they
occur when an object without an overlay (due to flying, being thrown,
etc) does something that should modify the overlay. The easiest way to
handle this is to just check if its immersed in the signal receiver
itself, otherwise it'll get pretty messy with trying to keep track of
what is in what state of immersion.

## Changelog
🆑
fix: Fixed harmless runtimes when a flying object tried to cross lava
/🆑
2025-10-02 16:10:55 +02:00

310 lines
14 KiB
Plaintext

/// A list of movables that shouldn't be affected by the element, either because it'd look bad or barely perceptible
GLOBAL_LIST_INIT(immerse_ignored_movable, typecacheof(list(
/obj/effect,
/mob/dead,
/obj/projectile,
)))
/// A visual element that makes movables entering the attached turfs look immersed into that turf.
/// May the gods forgive me for the bullshit you're about to witness
/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_turf_contents = list()
/// A list of generated immersion masks based on object width, height and whever they're fully immersed underwater
var/list/immersion_masks = list()
/// An assoc list of instances of /atom/movable/immerse_mask used as abstract effect relays, because god is dead
var/list/generated_visual_overlays = list()
/// icon_state used as a mask by our turf
var/mask_icon = "immerse"
/// Alpha of the mask, to make the liquid partially transparent
var/alpha = 180
/datum/element/immerse/Attach(turf/target, mask_icon = "immerse", alpha = 180)
. = ..()
if(!isturf(target) || !mask_icon)
return ELEMENT_INCOMPATIBLE
src.mask_icon = mask_icon
src.alpha = alpha
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_turf_contents += source
for(var/atom/movable/movable as anything in source)
if(!(movable.flags_1 & INITIALIZED_1) || movable.invisibility >= INVISIBILITY_OBSERVER)
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_turf_contents[source])
remove_from_element(source, movable)
attached_turf_contents -= 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 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(QDELETED(movable))
return
if(HAS_TRAIT(movable, TRAIT_IMMERSED) || HAS_TRAIT(movable, TRAIT_WALLMOUNTED))
return
if(!ISINRANGE(PLANE_TO_TRUE(movable.plane), FLOOR_PLANE, GAME_PLANE))
return
// First, floor plane objects use TOPDOWN_LAYER, second this check shouldn't apply to them anyway.
var/layer_to_check = IS_TOPDOWN_PLANE(source.plane) ? TOPDOWN_ABOVE_WATER_LAYER : ABOVE_ALL_MOB_LAYER
if(movable.layer >= layer_to_check)
return
if(is_type_in_typecache(movable, GLOB.immerse_ignored_movable))
return
var/atom/movable/buckled = null
if(isliving(movable))
var/mob/living/living_mob = movable
buckled = living_mob.buckled
RegisterSignal(living_mob, COMSIG_LIVING_SET_BUCKLED, PROC_REF(on_set_buckled))
RegisterSignal(living_mob, COMSIG_LIVING_UPDATE_OFFSETS, PROC_REF(on_update_offsets))
RegisterSignal(movable, COMSIG_LIVING_POST_UPDATE_TRANSFORM, PROC_REF(on_update_transform))
RegisterSignal(movable, COMSIG_ATOM_SPIN_ANIMATION, PROC_REF(on_spin_animation))
RegisterSignal(movable, COMSIG_QDELETING, PROC_REF(on_movable_qdel))
try_immerse(movable, buckled)
LAZYADD(attached_turf_contents[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)
/**
* 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)
return
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)
return
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(!attached_turf_contents[exited.loc])
remove_from_element(source, exited)
return
LAZYREMOVE(attached_turf_contents[source], exited)
LAZYADD(attached_turf_contents[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 = null
if(isliving(movable))
var/mob/living/living_mob = movable
buckled = living_mob.buckled
try_unimmerse(movable, buckled)
LAZYREMOVE(attached_turf_contents[source], movable)
UnregisterSignal(movable, list(COMSIG_LIVING_SET_BUCKLED, COMSIG_QDELETING, COMSIG_LIVING_UPDATE_OFFSETS, COMSIG_ATOM_SPIN_ANIMATION, COMSIG_LIVING_POST_UPDATE_TRANSFORM))
REMOVE_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src))
/// Generate a mask filter mutable to use as render_source for the alpha filter based on provided width, height and immersion state
/datum/element/immerse/proc/generate_immerse_mask(width, height, is_below_water)
var/clean_height = height
width = ceil(width / ICON_SIZE_X) * ICON_SIZE_X
height = ceil(height / ICON_SIZE_Y) * ICON_SIZE_Y
var/mask_key = "[width]-[height]-[is_below_water]"
var/mutable_appearance/target_mask = immersion_masks[mask_key]
if (target_mask)
return target_mask
if (width == ICON_SIZE_X && height == ICON_SIZE_Y)
target_mask = mutable_appearance('icons/effects/effects.dmi', mask_icon, alpha = alpha)
immersion_masks[mask_key] = target_mask
return target_mask
var/icon/column_icon = icon('icons/effects/effects.dmi', mask_icon)
var/y_tiles = 1
if (height != ICON_SIZE_Y)
column_icon.Crop(1, 1, ICON_SIZE_X, ICON_SIZE_Y) // Use base icon and crop it out so animation frames respect dmi's delays
y_tiles = ceil((height / ICON_SIZE_Y - 1) / 2) + 1
column_icon.Scale(ICON_SIZE_X, y_tiles * ICON_SIZE_Y)
var/icon/effect_icon = icon('icons/effects/effects.dmi', mask_icon)
var/icon/fill_icon = icon('icons/effects/alphacolors.dmi', "white")
for (var/y_tile in 1 to y_tiles - 1)
column_icon.Blend(fill_icon, ICON_OVERLAY, 1, 1 + (y_tile - 1) * ICON_SIZE_Y)
column_icon.Blend(effect_icon, ICON_OVERLAY, 1, 1 + (y_tiles - 1) * ICON_SIZE_Y)
var/icon/immerse_icon = null
if (width == ICON_SIZE_X)
immerse_icon = column_icon
else
immerse_icon = icon('icons/effects/effects.dmi', mask_icon) // Use base icon and crop it out so animation frames respect dmi's delays
immerse_icon.Crop(1, 1, ICON_SIZE_X, ICON_SIZE_Y)
immerse_icon.Scale(ceil(width / ICON_SIZE_X) * ICON_SIZE_X, ceil(height / ICON_SIZE_Y) * ICON_SIZE_Y)
for (var/x_tile in 1 to ceil(width / ICON_SIZE_X))
immerse_icon.Blend(column_icon, ICON_OVERLAY, 1 + (x_tile - 1) * ICON_SIZE_X, 1)
target_mask = mutable_appearance(immerse_icon)
target_mask.alpha = alpha
target_mask.pixel_y = -(y_tiles - 1) * ICON_SIZE_Y + floor((clean_height - ICON_SIZE_Y) / 2)
immersion_masks[mask_key] = target_mask
return target_mask
/datum/element/immerse/proc/add_immerse_overlay(atom/movable/movable)
// This determines if the overlay should cover the entire surface of the object or not
var/layer_to_check = IS_TOPDOWN_PLANE(movable.plane) ? TOPDOWN_WATER_LEVEL_LAYER : WATER_LEVEL_LAYER
var/is_below_water = (movable.layer < layer_to_check) ? "underwater-" : ""
// Tall mobs still only get covered to their feet, unless they're offset down
var/mutable_appearance/immerse_mask = generate_immerse_mask(movable.get_cached_width(), max(ICON_SIZE_Y - movable.pixel_z, ICON_SIZE_Y), is_below_water)
var/atom/movable/immerse_mask/effect_relay = generated_visual_overlays[movable]
if (!effect_relay)
effect_relay = new(movable)
movable.vis_contents += effect_relay
generated_visual_overlays[movable] = effect_relay
var/mutable_appearance/mask_copy = new(immerse_mask)
effect_relay.appearance = mask_copy
effect_relay.render_target = "*immerse_[REF(movable)]"
SEND_SIGNAL(movable, COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY, effect_relay)
// Should always render above any other filters that could be adding visuals
movable.add_filter("immerse_mask", INFINITY, alpha_mask_filter(y = -floor((movable.get_cached_height() - ICON_SIZE_Y) / 2) - movable.pixel_z, render_source = effect_relay.render_target, flags = MASK_INVERSE))
/datum/element/immerse/proc/remove_immerse_overlay(atom/movable/movable, deleting = TRUE)
movable.remove_filter("immerse_mask")
if (!deleting)
return
var/atom/movable/immerse_mask/mask = generated_visual_overlays[movable]
movable.vis_contents -= mask
generated_visual_overlays -= movable
QDEL_NULL(mask)
/// 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/atom/movable/immerse_mask/effect_relay = generated_visual_overlays[source]
if (!effect_relay)
return
var/matrix/new_transform = matrix()
new_transform.Scale(1 / source.current_size)
new_transform.Turn(-new_lying_angle)
var/mutable_appearance/relay_appearance = new(effect_relay.appearance)
relay_appearance.transform = new_transform
effect_relay.appearance = relay_appearance
/// 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_mask/immerse_mask = generated_visual_overlays[source]
if (immerse_mask)
immerse_mask.do_spin_animation(speed, loops, segments, -segment)
/datum/element/immerse/proc/on_update_offsets(mob/living/source, new_x, new_y, new_w, new_z, animate)
SIGNAL_HANDLER
if (!generated_visual_overlays[source])
return
var/old_height = ceil(max(ICON_SIZE_Y - source.pixel_z, ICON_SIZE_Y) / ICON_SIZE_Y)
var/new_height = ceil(max(ICON_SIZE_Y - new_z, ICON_SIZE_Y) / ICON_SIZE_Y)
if (old_height != new_height)
remove_immerse_overlay(source, FALSE)
add_immerse_overlay(source)
if (source.pixel_z == new_z)
return
if (animate)
source.transition_filter("immerse_mask", list("y" = -floor((source.get_cached_height() - ICON_SIZE_Y) / 2) - new_z), time = UPDATE_TRANSFORM_ANIMATION_TIME)
else
source.modify_filter("immerse_mask", list("y" = -floor((source.get_cached_height() - ICON_SIZE_Y) / 2) - new_z))
/atom/movable/immerse_mask
appearance_flags = RESET_TRANSFORM|RESET_COLOR|RESET_ALPHA|KEEP_APART
vis_flags = VIS_HIDE