diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 5a1d4a99f9f..201227ed2c5 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -1005,6 +1005,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai */ #define TRAIT_UNIQUE_IMMERSE "unique_immerse" + // unique trait sources, still defines #define EMP_TRAIT "emp_trait" #define STATUE_MUTE "statue" @@ -1087,6 +1088,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define PAI_FOLDED "pai-folded" /// Trait applied to brain mobs when they lack external aid for locomotion, such as being inside a mech. #define BRAIN_UNAIDED "brain-unaided" +/// Trait applied to a mob when it gets a required "operational datum" (components/elements). Sends out the source as the type of the element. +#define TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM "element-required" /// Trait applied by MODsuits. #define MOD_TRAIT "mod" /// Trait applied by element diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index df15d9442c3..54b916400d6 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -64,7 +64,8 @@ multiple modular subtrees with behaviors if(idle_behavior) idle_behavior = new idle_behavior() - PossessPawn(new_pawn) + if(!isnull(new_pawn)) // unit tests need the ai_controller to exist in isolation due to list schenanigans i hate it here + PossessPawn(new_pawn) /datum/ai_controller/Destroy(force, ...) set_ai_status(AI_STATUS_OFF) @@ -148,6 +149,9 @@ multiple modular subtrees with behaviors ///Proc for deinitializing the pawn to the old controller /datum/ai_controller/proc/UnpossessPawn(destroy) + if(isnull(pawn)) + return // instantiated without an applicable pawn, fine + UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE)) if(ai_movement.moving_controllers[src]) ai_movement.stop_moving_towards(src) @@ -155,7 +159,6 @@ multiple modular subtrees with behaviors pawn = null if(destroy) qdel(src) - return ///Returns TRUE if the ai controller can actually run at the moment. /datum/ai_controller/proc/able_to_run() diff --git a/code/datums/ai/_ai_planning_subtree.dm b/code/datums/ai/_ai_planning_subtree.dm index 6560e91c00f..b3f0ae9fb53 100644 --- a/code/datums/ai/_ai_planning_subtree.dm +++ b/code/datums/ai/_ai_planning_subtree.dm @@ -1,6 +1,9 @@ ///A subtree is attached to a controller and is occasionally called by /ai_controller/SelectBehaviors(), this mainly exists to act as a way to subtype and modify SelectBehaviors() without needing to subtype the ai controller itself /datum/ai_planning_subtree - + /// A list of typepaths of "operational datums" (elements/components) we absolutely NEED to run. Checked in unit tests, as well as be a nice reminder to developers that such a thing might be needed. + /// Note that in the Attach/Inititalize/New (or any future equivalent for these procs), you will need to add the trait TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM to the mob in question + /// in order for unit tests to succeed. This will break obviously enough if you don't do this and declare the required datum here however. + var/list/operational_datums = null ///Determines what behaviors should the controller try processing; if this returns SUBTREE_RETURN_FINISH_PLANNING then the controller won't go through the other subtrees should multiple exist in controller.planning_subtrees /datum/ai_planning_subtree/proc/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm index 5db0486a43a..46325a37b9f 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm @@ -12,8 +12,8 @@ /datum/ai_planning_subtree/basic_melee_attack_subtree/average_speed melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/average_speed -//If you give this to something without the element you are a dumbass. /datum/ai_planning_subtree/basic_ranged_attack_subtree + operational_datums = list(/datum/element/ranged_attacks) var/datum/ai_behavior/basic_ranged_attack/ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack /datum/ai_planning_subtree/basic_ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm index ccb740c9980..2d553fe4b2f 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm @@ -1,5 +1,6 @@ /// Sets the BB target to a mob which you can see and who has recently attacked you /datum/ai_planning_subtree/target_retaliate + operational_datums = list(/datum/element/ai_retaliate, /datum/component/ai_retaliate_advanced) /// Blackboard key which tells us how to select valid targets var/targetting_datum_key = BB_TARGETTING_DATUM /// Blackboard key in which to store selected target diff --git a/code/datums/components/ai_retaliate_advanced.dm b/code/datums/components/ai_retaliate_advanced.dm index 87e7442832d..46557b0ef2f 100644 --- a/code/datums/components/ai_retaliate_advanced.dm +++ b/code/datums/components/ai_retaliate_advanced.dm @@ -13,6 +13,8 @@ src.post_retaliate_callback = post_retaliate_callback parent.AddElement(/datum/element/relay_attackers) + ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + /datum/component/ai_retaliate_advanced/RegisterWithParent() RegisterSignal(parent, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) diff --git a/code/datums/elements/ai_retaliate.dm b/code/datums/elements/ai_retaliate.dm index 3fb8494a99e..4cca4089633 100644 --- a/code/datums/elements/ai_retaliate.dm +++ b/code/datums/elements/ai_retaliate.dm @@ -12,6 +12,8 @@ target.AddElement(/datum/element/relay_attackers) RegisterSignal(target, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + ADD_TRAIT(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + /datum/element/ai_retaliate/Detach(datum/source, ...) . = ..() UnregisterSignal(source, COMSIG_ATOM_WAS_ATTACKED) diff --git a/code/datums/elements/ranged_attacks.dm b/code/datums/elements/ranged_attacks.dm index abef602632f..2df0a9189ff 100644 --- a/code/datums/elements/ranged_attacks.dm +++ b/code/datums/elements/ranged_attacks.dm @@ -20,6 +20,8 @@ if(casingtype && projectiletype) CRASH("Set both casing type and projectile type in [target]'s ranged attacks element! uhoh! stinky!") + ADD_TRAIT(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + /datum/element/ranged_attacks/Detach(datum/target) UnregisterSignal(target, COMSIG_MOB_ATTACK_RANGED) return ..() diff --git a/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm b/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm index a09107aebf4..7108983c310 100644 --- a/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm +++ b/code/modules/mob/living/basic/space_fauna/spider/spiderlings/spiderling.dm @@ -38,6 +38,7 @@ AddComponent(/datum/component/swarming) AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW, volume = 0.2) // they're small but you can hear 'em AddElement(/datum/element/web_walker, /datum/movespeed_modifier/spiderling_web) + AddElement(/datum/element/ai_retaliate) // keep in mind we have infinite range (the entire pipenet is our playground, it's just a matter of random choice as to where we end up) so lower and upper both have their gives and takes. // but, also remember the more time we aren't in a vent, the more susceptible we are to dying to anything and everything. diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 9cc43e4ce22..29f24a6cfbd 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -130,6 +130,7 @@ #include "dynamic_ruleset_sanity.dm" #include "egg_glands.dm" #include "emoting.dm" +#include "ensure_subtree_operational_datum.dm" #include "explosion_action.dm" #include "fish_unit_tests.dm" #include "focus_only_tests.dm" diff --git a/code/modules/unit_tests/ensure_subtree_operational_datum.dm b/code/modules/unit_tests/ensure_subtree_operational_datum.dm new file mode 100644 index 00000000000..9ca78fbc674 --- /dev/null +++ b/code/modules/unit_tests/ensure_subtree_operational_datum.dm @@ -0,0 +1,64 @@ +/// The subtree that requires the operational datum. +#define REQUIRED_SUBTREE "required_subtree" +/// The list of typepaths of applicable operational datums that would satisfy the requirement. +#define REQUIRED_OPERATIONAL_DATUMS "required_operational_datums" + +/// Unit Test that ensure that if we add a specific planning subtree to a basic mob's planning tree, that we also have the operational datum needed for it (component/element). +/// This can be extended to other "mandatory" operational datums for certain subtrees to work. +/datum/unit_test/ensure_subtree_operational_datum + /// Associated list of mobs that we need to test this on. Key is the typepath of the mob, value is a list of the planning subtree and the operational datums that are required for it. + var/list/testable_mobs = list() + +/datum/unit_test/ensure_subtree_operational_datum/Run() + gather_testable_mobs() + test_applicable_mobs() + +/// First, look for all mobs that have a planning subtree that requires an element, then add it to the list for stuff to test afterwards. Done like this to not have one mumbo proc that's hard to read. +/datum/unit_test/ensure_subtree_operational_datum/proc/gather_testable_mobs() + for(var/mob/living/basic/checkable_mob as anything in subtypesof(/mob/living/basic)) + var/datum/ai_controller/testable_controller = initial(checkable_mob.ai_controller) + if(isnull(testable_controller)) + continue + + // we can't do inital() memes on lists so it's allocation time + testable_controller = allocate(testable_controller) + var/list/ai_planning_subtrees = testable_controller.planning_subtrees // list of instantiated datums. easy money + if(!length(ai_planning_subtrees)) + continue + + for(var/datum/ai_planning_subtree/testable_subtree as anything in ai_planning_subtrees) + var/list/necessary_datums = testable_subtree.operational_datums + if(isnull(necessary_datums)) + continue + + testable_mobs[checkable_mob] = list( + REQUIRED_OPERATIONAL_DATUMS = necessary_datums, + REQUIRED_SUBTREE = testable_subtree.type, + ) + +/// Then, test the mobs that we've found +/datum/unit_test/ensure_subtree_operational_datum/proc/test_applicable_mobs() + for(var/mob/living/basic/checkable_mob as anything in testable_mobs) + var/list/checkable_mob_data = testable_mobs[checkable_mob] + checkable_mob = allocate(checkable_mob) + + var/datum/ai_planning_subtree/test_subtree = checkable_mob_data[REQUIRED_SUBTREE] + var/list/trait_sources = GET_TRAIT_SOURCES(checkable_mob, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM) + if(!length(trait_sources)) // yes yes we could use `COUNT_TRAIT_SOURCES` but why invoke the same macro twice + TEST_FAIL("The mob [checkable_mob] ([checkable_mob.type]) does not have ANY instances of TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, but has a planning subtree ([test_subtree]) that requires it!") + continue + + var/has_element = FALSE + var/list/testable_operational_datums = checkable_mob_data[REQUIRED_OPERATIONAL_DATUMS] + for(var/iterable in trait_sources) + if(iterable in testable_operational_datums) + has_element = TRUE + break + + if(!has_element) + var/list/message_list = list("The mob [checkable_mob] ([checkable_mob.type]) has a planning subtree ([test_subtree]) that requires a component/element, but does not have any!") + message_list += "Needs one of the following to satisfy the requirement: ([testable_operational_datums.Join(", ")])" + TEST_FAIL(message_list.Join(" ")) + +#undef REQUIRED_SUBTREE +#undef REQUIRED_OPERATIONAL_DATUMS