Files
RoxyandGitHub 2138af3a6f Fix duplicate signal registration with mining mob respawners (#96827)
## About The Pull Request

So it's possible for a mining mob respawner to be spawned by
`/datum/biome/populate_turfs`, which calls `setup()` on the respawner,
and if the mob being passed is a random spawner, it'll register
`COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON` on the turf to wait for
that random spawner to spawn something. Since this is happening prior to
atom subsystem init, that spawn won't happen until later. Once the atom
subsystem starts Initializing stuff, if the mining respawner gets
Initialized before the random spawner, it'll call `spawn_mob` which, if
it happens to choose the same turf its on, will register
`COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON` again and runtime.

## Why It's Good For The Game

<img width="1388" height="310" alt="image"
src="https://github.com/user-attachments/assets/90bc2030-b8e4-4e1a-92cd-bbd399359684"
/>

## Changelog

N/A
2026-07-07 09:22:28 +02:00

184 lines
6.9 KiB
Plaintext

/// Respawns a mob if it's died and there's a storm
/obj/effect/mining_mob_respawner
icon = 'icons/turf/overlays.dmi'
icon_state = "explodable"
invisibility = INVISIBILITY_ABSTRACT
/// Do we check the outdoorsness of our spawn tile?
var/outdoor_only = TRUE
/// Are we waiting for a mob to spawn so we can link to it?
var/registered_spawn_signal = FALSE
// Spawn somewhere in an area around the spawner rather than dead on it
var/respawn_range = 3
/// Min time from storm to spawn a mob
var/min_delay = 1 SECONDS
/// Max time from storm to spawn a mob
var/max_delay = 10 SECONDS
/// Our currently spawned mob
var/datum/weakref/our_mob
/// Weighted list of things we can spawn
var/list/valid_mobs = list()
/// List of things we want to remove from our spawner list before using it
var/list/invalid_mobs = list(
SPAWN_MEGAFAUNA,
/obj/effect/spawner/random/lavaland_mob/raptor,
/mob/living/basic/mining/tendril,
)
/// Types of storm that respawn mobs, we might as well make it static because it's only used on init so there's no point in it supporting VV edits
var/static/list/valid_weather = list(
/datum/weather/particle/ash_storm,
/datum/weather/sand_storm, // We don't have any mining areas with sand but someone might start one there with an anomaly
/datum/weather/snow_storm,
)
/obj/effect/mining_mob_respawner/Initialize(mapload)
. = ..()
if (mapload && length(valid_mobs))
var/list/filtered_mobs = list()
for (var/path in valid_mobs)
if (!(path in invalid_mobs))
filtered_mobs += path
valid_mobs = filtered_mobs
if (!our_mob?.resolve() && !registered_spawn_signal)
make_mob()
// We're just going to go ahead and assume these won't move after being spawned
for (var/weather in valid_weather)
RegisterSignal(get_area(src), COMSIG_WEATHER_BEGAN_IN_AREA(weather), PROC_REF(on_storm_event))
/// Let the map generator system pass us a list of mobs
/obj/effect/mining_mob_respawner/proc/setup(list/valid_mobs, atom/initial_spawn)
src.valid_mobs = valid_mobs
if (istype(initial_spawn, /obj/effect/spawner/random))
RegisterSignal(get_turf(src), COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, PROC_REF(get_spawned_mob))
registered_spawn_signal = TRUE
else
register_spawn(src, initial_spawn)
/// Record something as our spawn, so we don't need to spawn something else
/obj/effect/mining_mob_respawner/proc/register_spawn(atom/source, atom/new_spawn)
SIGNAL_HANDLER
if (!new_spawn)
return
if (isliving(new_spawn))
our_mob = WEAKREF(new_spawn)
/// Make a guy and subscribe to see if they die
/obj/effect/mining_mob_respawner/proc/make_mob()
if (!length(valid_mobs))
return
var/spawn_path = pick(valid_mobs)
var/list/valid_locations = list()
for(var/turf/turf_in_view in view(respawn_range, get_turf(src)))
if (isclosedturf(turf_in_view) || (isgroundlessturf(turf_in_view)))
continue
if (outdoor_only)
var/area/turf_area = get_area(turf_in_view)
if (!turf_area.outdoors) // Don't spawn inside a miner's little pod house
continue
valid_locations += turf_in_view
if (!length(valid_locations))
return
var/turf/spawn_turf = pick(valid_locations)
// Bit roundabout but it's the only way of intercepting mob spawners
RegisterSignal(spawn_turf, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, PROC_REF(get_spawned_mob))
registered_spawn_signal = TRUE
new spawn_path(spawn_turf)
/// Intercept the next mob spawned on the turf, because we might have spawned an object which spawns a mob instead
/obj/effect/mining_mob_respawner/proc/get_spawned_mob(datum/source, atom/new_spawn)
SIGNAL_HANDLER
if (!isliving(new_spawn))
return
UnregisterSignal(source, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON)
registered_spawn_signal = FALSE
register_spawn(source, new_spawn)
play_spawn_animation(new_spawn, source)
/// When a storm hits & if our mob is dead, make a new one
/obj/effect/mining_mob_respawner/proc/on_storm_event()
SIGNAL_HANDLER
var/mob/living/resolved = our_mob?.resolve()
if (!resolved || resolved.stat == DEAD)
addtimer(CALLBACK(src, PROC_REF(make_mob)), rand(min_delay, max_delay), TIMER_DELETE_ME)
/// Play an awesome animation
/obj/effect/mining_mob_respawner/proc/play_spawn_animation(mob/living/spawned, turf/spawned_turf)
if (HAS_TRAIT(spawned, TRAIT_MOVE_FLYING))
new /obj/effect/fly_down(spawned_turf, spawned)
else
new /obj/effect/unburrow(spawned_turf, spawned)
spawned_turf.Shake(pixelshiftx = 0.5, pixelshifty = 0.5, duration = 2 SECONDS)
/// A "good enough" alpha mask effect that looks like something is coming out of the ground
/// Not guaranteed to look right on things taller than 32 pixels because i didnt want to make a really massive alpha mask
/obj/effect/unburrow
appearance_flags = parent_type::appearance_flags | KEEP_TOGETHER
/// Time in which to animate emerging from the ground
var/animate_time = 2 SECONDS
/obj/effect/unburrow/Initialize(mapload, atom/movable/thing)
. = ..()
if (QDELETED(thing))
return INITIALIZE_HINT_QDEL
thing.forceMove(src)
setup(thing)
/obj/effect/unburrow/proc/setup(atom/movable/thing)
vis_contents += thing
thing.pixel_y -= 32
animate(thing, pixel_y = 32, time = animate_time, flags = ANIMATION_PARALLEL | ANIMATION_RELATIVE)
thing.Shake(pixelshiftx = 1, pixelshifty = 0.5, duration = animate_time)
thing.dir = pick(GLOB.cardinals)
add_filter("alpha_mask", 2, alpha_mask_filter(icon = icon('icons/effects/64x64.dmi', "alpha_unburrow"), y = 16))
var/filter = get_filter("alpha_mask")
animate(filter, y = -16, delay = animate_time, time = 0.5 SECONDS, flags = ANIMATION_PARALLEL | ANIMATION_RELATIVE)
addtimer(CALLBACK(src, PROC_REF(release), thing), animate_time + 0.5 SECONDS, TIMER_DELETE_ME)
/obj/effect/unburrow/proc/release(atom/movable/thing)
if (!QDELETED(thing))
thing.forceMove(drop_location())
qdel(src)
/// An effect that shows an object swooping to the ground from above
/obj/effect/fly_down
alpha = 0
appearance_flags = parent_type::appearance_flags | KEEP_TOGETHER
/// Time in which to animate appearing
var/animate_time = 0.6 SECONDS
/obj/effect/fly_down/Initialize(mapload, atom/movable/thing)
. = ..()
if (!thing)
thing = new /mob/living/basic/mining/watcher(loc)
if (QDELETED(thing))
return INITIALIZE_HINT_QDEL
thing.forceMove(src)
setup(thing)
/obj/effect/fly_down/proc/setup(atom/movable/thing)
vis_contents += thing
thing.pixel_y += 64
var/direction = pick(list(1, -1))
var/anim_offset = 32 * direction
thing.pixel_x += anim_offset
thing.dir = direction < 0 ? EAST : WEST
animate(src, alpha = 255, time = 0.4 SECONDS, flags = ANIMATION_PARALLEL)
animate(thing, pixel_y = -64, time = animate_time, flags = ANIMATION_PARALLEL | ANIMATION_RELATIVE)
animate(thing, pixel_x = -anim_offset, time = animate_time, flags = ANIMATION_PARALLEL | ANIMATION_RELATIVE, easing = QUAD_EASING | EASE_IN)
addtimer(CALLBACK(src, PROC_REF(release), thing), animate_time, TIMER_DELETE_ME)
/obj/effect/fly_down/proc/release(atom/movable/thing)
if (!QDELETED(thing))
thing.forceMove(drop_location())
qdel(src)