From 8da1bae8e7f18eea0b9c7dcf0afca022a924a904 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Sun, 26 Feb 2023 22:37:49 +0000 Subject: [PATCH] Refactor mob spawner component so that it is independent (#73645) ## About The Pull Request Went down a rabbit hole with touching the migo files... I noticed that all of those mobs had a reference to `/datum/component/spawner` on them which looked a bit off. After investigation it seems like this component is breaking the prinicple of using an ECS system by assigning a reference to itself on every mob it creates? There doesn't seem to be a good reason to do that, as we can just use signals. This also doesn't work for basic mobs, because most of them don't _have_ this reference to assign to. If we don't want to add it to every basic mob (and why would we?) it would make more and more converted mobs invalid for spawners. Also it means that it has never been valid to create a Monkey spawner, which seems like a big oversight. I replaced all of the parts dependent on telling the mob where it was spawned from with signals. Megafauna seemed to have a reasonable amount of code related to "not straying a certain distance from what spawned them", but as far as I can tell unless someone varedited one onto a map we have never had a spawner which creates megafauna (nor would we want one? That would virtually always cause it to respawn instantly after being killed). ## Why It's Good For The Game Improves future maintainability Brings implementation up to current standards. Makes the code work the way I would have assumed it already worked in the first place. ## Changelog :cl: fix: Mob spawners will no longer break if instructed to spawn certain kinds of basic mob, or monkeys. /:cl: --- code/datums/components/spawner.dm | 92 ++++++++++++------- code/game/objects/structures/spawner.dm | 2 +- .../space_fauna/netherworld/blankbody.dm | 8 -- .../basic/space_fauna/netherworld/creature.dm | 10 +- .../basic/space_fauna/netherworld/migo.dm | 8 -- .../hostile/megafauna/_megafauna.dm | 9 -- .../hostile/mining_mobs/hivelord.dm | 9 +- .../mob/living/simple_animal/simple_animal.dm | 9 -- 8 files changed, 68 insertions(+), 79 deletions(-) diff --git a/code/datums/components/spawner.dm b/code/datums/components/spawner.dm index 7cc18937127..cd0eb754305 100644 --- a/code/datums/components/spawner.dm +++ b/code/datums/components/spawner.dm @@ -1,25 +1,27 @@ /datum/component/spawner - var/mob_types = list(/mob/living/basic/carp) - var/spawn_time = 300 //30 seconds default + /// Time to wait between spawns + var/spawn_time + /// Maximum number of mobs we can have active at one time + var/max_mobs + /// Visible message to show when a mob spawns + var/spawn_text + /// List of mob types to spawn, picked randomly + var/list/mob_types + /// Faction to grant to mobs + var/list/faction + /// List of weak references to mobs we have already created var/list/spawned_mobs = list() - var/spawn_delay = 0 - var/max_mobs = 5 - var/spawn_text = "emerges from" - var/list/faction = list("mining") + /// Time until we next spawn + COOLDOWN_DECLARE(spawn_delay) - - -/datum/component/spawner/Initialize(_mob_types, _spawn_time, _faction, _spawn_text, _max_mobs) - if(_spawn_time) - spawn_time=_spawn_time - if(_mob_types) - mob_types=_mob_types - if(_faction) - faction=_faction - if(_spawn_text) - spawn_text=_spawn_text - if(_max_mobs) - max_mobs=_max_mobs +/datum/component/spawner/Initialize(mob_types = list(), spawn_time = 30 SECONDS, max_mobs = 5, faction = list("mining"), spawn_text = "emerges from") + if (!length(mob_types)) + CRASH("No types of mob to spawn specified for spawner component!") + src.spawn_time = spawn_time + src.mob_types = mob_types + src.faction = faction + src.spawn_text = spawn_text + src.max_mobs = max_mobs RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(stop_spawning)) START_PROCESSING(SSprocessing, src) @@ -27,27 +29,49 @@ /datum/component/spawner/process() try_spawn_mob() - +/// Stop spawning mobs /datum/component/spawner/proc/stop_spawning(force) SIGNAL_HANDLER STOP_PROCESSING(SSprocessing, src) - for(var/mob/living/simple_animal/L in spawned_mobs) - if(L.nest == src) - L.nest = null - spawned_mobs = null + spawned_mobs = list() +/// Try to create a new mob /datum/component/spawner/proc/try_spawn_mob() - var/atom/P = parent - if(spawned_mobs.len >= max_mobs) + if(!COOLDOWN_FINISHED(src, spawn_delay)) return - if(spawn_delay > world.time) + validate_references() + if(length(spawned_mobs) >= max_mobs) return - spawn_delay = world.time + spawn_time + var/atom/spawner = parent + COOLDOWN_START(src, spawn_delay, spawn_time) + var/chosen_mob_type = pick(mob_types) - var/mob/living/simple_animal/L = new chosen_mob_type(P.loc) - L.flags_1 |= (P.flags_1 & ADMIN_SPAWNED_1) - spawned_mobs += L - L.nest = src - L.faction = src.faction - P.visible_message(span_danger("[L] [spawn_text] [P].")) + var/mob/living/created = new chosen_mob_type(spawner.loc) + created.flags_1 |= (spawner.flags_1 & ADMIN_SPAWNED_1) + spawned_mobs += WEAKREF(created) + created.faction = src.faction + spawner.visible_message(span_danger("[created] [spawn_text] [spawner].")) + + RegisterSignal(created, COMSIG_PARENT_QDELETING, PROC_REF(mob_deleted)) + RegisterSignal(created, COMSIG_MOB_STATCHANGE, PROC_REF(mob_stat_changed)) + +/// Remove weakrefs to mobs which have been killed or deleted without us picking it up somehow +/datum/component/spawner/proc/validate_references() + for (var/datum/weakref/weak_mob as anything in spawned_mobs) + var/mob/living/previously_spawned = weak_mob.resolve() + if (previously_spawned && previously_spawned.stat != DEAD) + continue + spawned_mobs -= weak_mob + +/// Called when a mob we spawned is deleted, remove it from the list +/datum/component/spawner/proc/mob_deleted(mob/living/source) + SIGNAL_HANDLER + spawned_mobs -= WEAKREF(source) + +/// Called when a mob we spawned dies, remove it from the list and unregister signals +/datum/component/spawner/proc/mob_stat_changed(mob/living/source) + if (source.stat != DEAD) + return + spawned_mobs -= WEAKREF(source) + UnregisterSignal(source, list(COMSIG_PARENT_QDELETING, COMSIG_MOB_STATCHANGE)) diff --git a/code/game/objects/structures/spawner.dm b/code/game/objects/structures/spawner.dm index a505952666b..64c58d2421f 100644 --- a/code/game/objects/structures/spawner.dm +++ b/code/game/objects/structures/spawner.dm @@ -17,7 +17,7 @@ /obj/structure/spawner/Initialize(mapload) . = ..() - AddComponent(spawner_type, mob_types, spawn_time, faction, spawn_text, max_mobs) + AddComponent(spawner_type, mob_types, spawn_time, max_mobs, faction, spawn_text) /obj/structure/spawner/attack_animal(mob/living/simple_animal/user, list/modifiers) if(faction_check(faction, user.faction, FALSE) && !user.client) diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm index 08332d8f529..32ce7717b94 100644 --- a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm +++ b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm @@ -26,8 +26,6 @@ lighting_cutoff_blue = 40 ai_controller = /datum/ai_controller/basic_controller/blankbody - /// Used for mobs that get spawned in a spawner appearently. - var/datum/component/spawner/nest /mob/living/basic/blankbody/Initialize(mapload) . = ..() @@ -61,12 +59,6 @@ melee_damage_lower = 10 melee_damage_upper = 20 -/mob/living/basic/blankbody/Destroy() - if(nest) - nest.spawned_mobs -= src - nest = null - return ..() - /datum/ai_controller/basic_controller/blankbody blackboard = list( BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm index 3e1271c415a..568f92703ea 100644 --- a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm +++ b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm @@ -21,7 +21,7 @@ unsuitable_atmos_damage = 0 unsuitable_cold_damage = 0 unsuitable_heat_damage = 0 - // Green and blue, bit dim cause yaknow morphlike + // Green and blue, bit dim cause yaknow morphlike lighting_cutoff_red = 5 lighting_cutoff_green = 25 lighting_cutoff_blue = 15 @@ -29,8 +29,6 @@ ai_controller = /datum/ai_controller/basic_controller/creature /// Used for checking if the mob is phased or not. var/is_phased = FALSE - /// Used for mobs that get spawned in a spawner appearently. - var/datum/component/spawner/nest /mob/living/basic/creature/Initialize(mapload) . = ..() @@ -123,12 +121,6 @@ holder = new /obj/effect/dummy/phased_mob(owner_turf, owner_mob) owner_mob.is_phased = TRUE -/mob/living/basic/creature/Destroy() - if(nest) - nest.spawned_mobs -= src - nest = null - return ..() - /datum/ai_controller/basic_controller/creature blackboard = list( BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm index ca642350431..651ff8bdada 100644 --- a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm +++ b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm @@ -31,8 +31,6 @@ var/static/list/migo_sounds /// Odds migo will dodge var/dodge_prob = 10 - /// Used for mobs that get spawned in a spawner appearently. - var/datum/component/spawner/nest /mob/living/basic/migo/Initialize(mapload) . = ..() @@ -96,12 +94,6 @@ if(!.)//Can't dodge there so we just carry on . = Move(moving_to, move_direction) -/mob/living/basic/migo/Destroy() - if(nest) - nest.spawned_mobs -= src - nest = null - return ..() - /datum/ai_controller/basic_controller/migo blackboard = list( BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm index c87dc31e3bf..1320c5e4bb9 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm @@ -46,8 +46,6 @@ var/recovery_time = 0 /// If this is a megafauna that is real (has achievements, gps signal) var/true_spawn = TRUE - /// Range the megafauna can move from their nest (if they have one - var/nest_range = 10 /// The chosen attack by the megafauna var/chosen_attack = 1 /// Attack actions, sets chosen_attack to the number in the action @@ -74,13 +72,6 @@ //Safety check if(!loc) return ..() - if(nest && nest.parent && get_dist(nest.parent, src) > nest_range) - var/turf/closest = get_turf(nest.parent) - for(var/i = 1 to nest_range) - closest = get_step(closest, get_dir(closest, src)) - forceMove(closest) // someone teleported out probably and the megafauna kept chasing them - LoseTarget() - return return ..() /mob/living/simple_animal/hostile/megafauna/death(gibbed, list/force_grant) diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm index 15741c8d38f..c5d6a0d4486 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm @@ -282,7 +282,14 @@ /mob/living/simple_animal/hostile/big_legion/Initialize(mapload) .=..() - AddComponent(/datum/component/spawner, list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion), 200, faction, "peels itself off from", 3) + AddComponent(\ + /datum/component/spawner,\ + mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion),\ + spawn_time = 20 SECONDS,\ + max_mobs = 3,\ + spawn_text = "peels itself off from",\ + faction = faction,\ + ) // Snow Legion /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 189c15aa617..ad7c164a5fd 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -127,8 +127,6 @@ ///If the mob can be spawned with a gold slime core. HOSTILE_SPAWN are spawned with plasma, FRIENDLY_SPAWN are spawned with blood. var/gold_core_spawnable = NO_SPAWN - var/datum/component/spawner/nest - ///Sentience type, for slime potions. var/sentience_type = SENTIENCE_ORGANIC @@ -225,10 +223,6 @@ GLOB.simple_animals[AIStatus] -= src SSnpcpool.currentrun -= src - if(nest) - nest.spawned_mobs -= src - nest = null - var/turf/T = get_turf(src) if (T && AIStatus == AI_Z_OFF) SSidlenpcpool.idle_mobs_by_zlevel[T.z] -= src @@ -459,9 +453,6 @@ new i(loc) /mob/living/simple_animal/death(gibbed) - if(nest) - nest.spawned_mobs -= src - nest = null drop_loot() if(dextrous) drop_all_held_items()