Files
Bubberstation/code/datums/components/reflection.dm
Bloop b77cdda8a4 Fix hard del/race condition in reflections (#93328)
## About The Pull Request

Tin, the proc that's managing references should remove the thing from
the list before calling qdel on it. Also, it should not be calling qdel
on something that's already being qdeleted (which could happen before if
`nuke_reflection()` was reached via the `COMSIG_QDELING` signal).

<img width="1675" height="480" alt="firefox_xIIRCIbgdU"
src="https://github.com/user-attachments/assets/246be9de-6ff0-46a5-bf5e-1d0895fd9f55"
/>


## Why It's Good For The Game

Less hard deletes

## Changelog

Not player facing/should get the hard deletes tag
2025-10-10 03:59:59 +02:00

224 lines
9.5 KiB
Plaintext

/**
* A simple-ish component that reflects the icons of movables on the parent like a mirror.
* Sadly, there's no easy way to make the SOUTH dir reflection flip the visual so that you can see
* the back NORTH dir of a target while it's facing SOUTH beside adding the VIS_INHERIT_DIR flag
* to the target movable, which I'm not doing to spare eventual issues with other vis overlays in the future.
*/
/datum/component/reflection
/**
* The direction from which the component gets its visual overlays.
* The visuals are also flipped horizontally or vertically based on it.
*/
var/reflected_dir
/// the movable which the reflected movables are attached to, in turn added to the vis contents of the parent.
VAR_PRIVATE/obj/effect/abstract/reflection_holder
/**
* A lazy assoc list that keeps track of all movables in range that either could be reflected or are reflected.
*
* The key is the movable, and the value is the reflection object (or null - the reflection object is also lazy loaded).
*/
VAR_PRIVATE/list/reflected_movables
/// A callback used check to know which movables should be reflected and which not.
var/datum/callback/can_reflect
///the base matrix used by reflections
var/matrix/reflection_matrix
///the filter data added to reflection holder.
var/list/reflection_filter
///the transparency channel value of the reflection holder.
var/alpha
///A list of signals that when sent to the parent, will force the comp to recalculate the reflected movables.
var/list/update_signals
///List of signals registered on reflected atoms to update their reflections.
var/list/check_reflect_signals
/**
* Init Args
* * set_reflected_dir: Optional: What dir to reflect. If not provided, uses (and updates to) the parent's dir.
* * reflection_filter: Optional: A list of filters to apply to the reflection.
* * reflection_matrix: Optional: A matrix to apply as the transform of the reflection.
* * can_reflect: Optional: A callback to check if a movable should be reflected.
* * alpha: The transparency of the reflection holder.
* * update_signals: Optional: Additional signals to provide to update_signals (to check for when to recalculate all reflections).
* * check_reflect_signals: Optional: Additional signals to provide to check_reflect_signals (to check for when to update a single reflection).
*/
/datum/component/reflection/Initialize(set_reflected_dir, list/reflection_filter, matrix/reflection_matrix, datum/callback/can_reflect, alpha = 150, list/update_signals, list/check_reflect_signals)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
var/static/list/connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_movable_entered_or_initialized),
COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON = PROC_REF(on_movable_entered_or_initialized),
COMSIG_ATOM_EXITED = PROC_REF(on_movable_exited)
)
AddComponent(/datum/component/connect_range, parent, connections, 1, works_in_containers = FALSE)
// Always supplied to check_reflect_signals
var/list/default_can_reflect_signals = list(
COMSIG_ATOM_POST_DIR_CHANGE,
COMSIG_ATOM_UPDATED_ICON,
COMSIG_CARBON_APPLY_OVERLAY,
COMSIG_CARBON_REMOVE_OVERLAY,
COMSIG_LIVING_POST_UPDATE_TRANSFORM,
)
// Always supplied to update_signals
var/list/default_update_signals = list(
COMSIG_MOVABLE_MOVED,
)
src.reflection_matrix = reflection_matrix
src.reflection_filter = reflection_filter
src.can_reflect = can_reflect
src.check_reflect_signals = (check_reflect_signals || list()) + default_can_reflect_signals
reflection_holder = new(parent)
reflection_holder.alpha = alpha
reflection_holder.appearance_flags = KEEP_TOGETHER
reflection_holder.vis_flags = VIS_INHERIT_ID
reflection_holder.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
if(reflection_filter)
reflection_holder.add_filter("reflection", 1, reflection_filter)
var/atom/movable/mov_parent = parent
mov_parent.vis_contents += reflection_holder
set_reflection(set_reflected_dir || REVERSE_DIR(mov_parent.dir))
if(!set_reflected_dir)
RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_dir_change))
RegisterSignals(parent, (update_signals || list()) + default_update_signals, PROC_REF(get_reflection_targets))
/datum/component/reflection/Destroy(force)
for(var/atom/movable/tracked in reflected_movables)
nuke_reflection(tracked)
QDEL_NULL(reflection_holder)
can_reflect = null
return ..()
///Called when the parent changes its direction.
/datum/component/reflection/proc/on_dir_change(atom/movable/source, old_dir, new_dir)
SIGNAL_HANDLER
set_reflection(REVERSE_DIR(new_dir))
///Turns the allowed reflected direction alongside the parent's dir. then calls get_reflection_targets.
/datum/component/reflection/proc/set_reflection(new_dir = SOUTH)
if(reflected_dir == new_dir)
return
reflected_dir = new_dir
get_reflection_targets(parent)
///Unsets the old reflected movables and sets it with new ones.
/datum/component/reflection/proc/get_reflection_targets(atom/movable/source)
SIGNAL_HANDLER
// clean slate
for(var/atom/movable/tracked in reflected_movables)
nuke_reflection(tracked)
// find anything adjacent, if we can't see it that's fine (we check view later)
for(var/atom/movable/target in range(1, source))
track_reflection(target)
///Checks if the target movable can be reflected or not.
/datum/component/reflection/proc/check_can_reflect(atom/movable/target)
if(target == parent || !(target in view(1, parent)))
return FALSE
var/atom/movable/mov_parent = parent
if(target.loc != mov_parent.loc && get_dir(mov_parent, target) != reflected_dir)
return FALSE
if(can_reflect && !can_reflect.Invoke(target))
return FALSE
return TRUE
///Called when a movable enters a turf within the connected range
/datum/component/reflection/proc/on_movable_entered_or_initialized(atom/movable/source, atom/movable/arrived)
SIGNAL_HANDLER
track_reflection(arrived)
/datum/component/reflection/proc/track_reflection(atom/movable/target, check_view = TRUE)
// this stuff really shouldn't be tracked
if(QDELETED(target) || target == parent || target.loc == parent)
return
// i don't really want to do this but there's a bunch of abstract effects we should ignore...
// we can revisit this later when we actually have an object that wants to reflect this stuff
if(iseffect(target))
return
if(!LAZYFIND(reflected_movables, target)) // not lazyaccess - value may be null
LAZYSET(reflected_movables, target, null)
RegisterSignals(target, check_reflect_signals, PROC_REF(update_reflection))
RegisterSignals(target, COMSIG_QDELETING, PROC_REF(nuke_reflection))
update_reflection(target)
/datum/component/reflection/proc/nuke_reflection(atom/movable/target)
SIGNAL_HANDLER
var/atom/movable/reflection = LAZYACCESS(reflected_movables, target)
LAZYREMOVE(reflected_movables, target)
if(!QDELETED(reflection))
qdel(reflection)
UnregisterSignal(target, check_reflect_signals)
UnregisterSignal(target, COMSIG_QDELETING)
///Called when a movable exits a turf within the connected range
/datum/component/reflection/proc/on_movable_exited(atom/movable/source, atom/movable/gone)
SIGNAL_HANDLER
if(!LAZYFIND(reflected_movables, gone)) // not lazyaccess - value may be null
return
if(check_can_reflect(gone))
return
nuke_reflection(gone)
/// Handles updating the appearance of the reflection to match the target movable.
/datum/component/reflection/proc/copy_appearance_to_reflection(obj/effect/abstract/reflection, atom/movable/target)
reflection.appearance = copy_appearance_filter_overlays(target.appearance)
reflection.vis_flags = VIS_INHERIT_ID
reflection.transform = reflection_matrix || matrix()
// updating the dir so facing towards / away from it correctly faces the reflection away / towards it,
// while facing left / right will correctly face the reflection left / right
// there's probably a more intelligent way to tackle this but this is more readable, i guess
if(reflected_dir & EAST)
if(target.dir & NORTH)
reflection.dir = WEST
else if(target.dir & SOUTH)
reflection.dir = EAST
else
reflection.dir = REVERSE_DIR(target.dir)
else if(reflected_dir & WEST)
if(target.dir & NORTH)
reflection.dir = EAST
else if(target.dir & SOUTH)
reflection.dir = WEST
else
reflection.dir = REVERSE_DIR(target.dir)
else if(reflected_dir & SOUTH)
// east/west is the same, makes it easy on us
reflection.dir = (target.dir & (EAST|WEST)) ? target.dir : REVERSE_DIR(target.dir)
else if(reflected_dir & NORTH)
reflection.dir = (target.dir & (NORTH|SOUTH)) ? target.dir : REVERSE_DIR(target.dir)
// north needs snowflake handling to make a more... "understandable" reflection
reflection.transform = reflection.transform.Turn(180)
reflection.pixel_y += 5
// purely for vv
reflection.name = "[target.name]'s reflection"
///Called when the target movable changes its appearance or dir.
/datum/component/reflection/proc/update_reflection(atom/movable/source)
SIGNAL_HANDLER
var/obj/effect/abstract/reflection = LAZYACCESS(reflected_movables, source)
if(!check_can_reflect(source))
// temporarily hide any reflection
reflection?.vis_flags |= VIS_HIDE
return
// Lazy init the reflection
if(!reflection)
// If the loc is null, only a black (or grey depending on alpha) silhouette of the target will be rendered
// Just putting this information here in case you want something like that in the future.
reflection = new(parent)
reflection_holder.vis_contents += reflection
LAZYSET(reflected_movables, source, reflection)
// technically redundant (...because copying appearance copies vis flags), but good to be explicit
reflection.vis_flags &= ~VIS_HIDE
copy_appearance_to_reflection(reflection, source)