# The Attack Chain The vast majority of things that happen when a player performs an action on an object or another player occurs within the _attack chain_. The attack chain is the set of procs and signals that determine what should happen when an action is performed, if an attack should occur, and what to do afterwards. ## Overview The attack chain is made up of multiple procs and signals which are expected to respect each other's responses as to how the attack chain should be executed. For most mobs, this begins in [`/mob/proc/ClickOn`][clickon]. First, any click performed with a modifier key such as `SHIFT` or `ALT` is handled first in separate procs. A handful of global use cases are handled next, such as if the user is in a mech, if they are restrained, if they are throwing an item, or if the thing they're interacting with is an SSD player. The core of the attack chain commences: 1. If the user is holding the item and clicking on it, `/obj/item/activate_self()` is called. This sends `COMSIG_ACTIVATE_SELF` and cancels the rest of the attack chain if `COMPONENT_CANCEL_ATTACK_CHAIN` is returned by any listeners. 2. If the user can reach the item or it is in relatively accessible inventory (three levels deep), the melee attack chain is called via `/obj/item/melee_attack_chain()`. 3. If the user is on HELP intent and the item is a tool, the various `multitool_act()`/`welder_act()`/`screwdriver_act()`/etc. methods are called depending on the tool type. This typically causes the attack chain to end. 4. Several signals are sent: `COMSIG_INTERACT_TARGET`, `COMSIG_INTERACTING`, and `COMSIG_INTERACT_USER`. If any listeners request it (usually by returning a non-null value), the attack chain may end here. 5. If the target implements `item_interaction()`, it is called here, and can either return `ITEM_INTERACT_COMPLETE` to end the attack chain, or `ITEM_INTERACT_SKIP_TO_AFTER_ATTACK` to skip all phases of the attack chain except for after-attack. 6. If the item being used on the target implements `interact_with_atom()`, it is called here, and can either return `ITEM_INTERACT_COMPLETE` to end the attack chain, or `ITEM_INTERACT_SKIP_TO_AFTER_ATTACK` to skip all phases of the attack chain except for after-attack. The above steps can generally be considered the "item interaction phase", when the action is not meant to cause in-game harm to the target. If the attack chain has not been ended, this means we are in the "attack phase": 1. `pre_attack()` is called on the used item, which sends `COMSIG_PRE_ATTACK`, and cancels the rest of the attack chain if any listeners return `COMPONENT_CANCEL_ATTACK_CHAIN`. 2. `attack_by()` is called on the target. This sends `COMSIG_ATTACKBY`, and cancels the rest of the attack chain if any listeners return `COMPONENT_SKIP_AFTERATTACK`. 3. `attacked_by()` is called on the target. 4. `COMSIG_AFTER_ATTACK` is sent on the used item, and `COMSIG_AFTER_ATTACKED_BY` is sent on the target. The benefits of this approach is that it allows an enormous amount of flexibility when it comes to dealing with attacks, while ensuring that behaviors that are always expected occur consistently. For a high-level flowchart of the attack chain, see below. Note that this flowchart may not be 100% accurate/up-to-date. When in doubt, check the implementation. ![](./images/attack_chain_flowchart.png) [clickon]: https://codedocs.paradisestation.org/mob.html#proc/ClickOn ## Why? A reasonable question to ask would be, why do we need all of these procs and signals? A good way to think of the attack chain is as a series of suggestions, rather than a series of instructions. If a player attacks a mob with an object, there are many things in the game world that may want to have a say in whether that will happen, and how it will happen. For example, there may be a component attached to the player that wants to intercept whenever an attack is attempted in order to cancel it or substitute its own action. The item being used to attack may want to cancel the attack based on its own internal state. The mob or object being attacked may have specific ways to react to the attack. By having as many procs and signals as we do, we're allowing all involved objects and any attached components or elements to contribute their own behavior into the attack chain. ### ITEM_INTERACT flags One may also ask why the `ITEM_INTERACT_SKIP_TO_AFTER_ATTACK` flag is necessary. Pre-migration, a common pattern was for an object to skip certain items in its `attackby`, and let those items handle the interaction in their `afterattack`. Some examples of this include: - Mountable frames being "ignored" in `/turf/attackby`, in order to let `/obj/item/mounted/frame/afterattack` handle its specific behavior. - Reagent containers being "ignored" in various machines' `attackby`, in order to let the container's `afterattack` handle reagent transfer or other specific behavior. ## Attack Chain Refactor The attack chain was overhauled in [#26834][]. This overhaul introduced several safeties, renamed many procs and signals, and helped to ensure consistent handling of signals in order to help make the attack chain more reliable. [#26834]: https://github.com/ParadiseSS13/Paradise/pull/26834 Prior to the attack chain refactoring, this system was disorganized and its behavior was not unified. Some procs would call their parent procs, some wouldn't. Some would send out signals at the right time, some wouldn't. The attack chain refactor unified all this behavior, but it did not do it across the entire codebase, all at once. Instead, a separate codepath was introduced, and all existing uses of the attack chain were placed in separate procs. These are easily identified by procs which contain `legacy__attackchain` in the name. The goal is to move all uses of the legacy attack chain onto the new one, but it would be infeasible to attempt to do this all at once. Anyone can choose to migrate attack chains if they so desire to help complete the migration. > [!NOTE] > > If you are working in code that touches the legacy attack chain, it is > expected that you migrate the code to the new attack chain first. ## Performing Migrations Procs with the `__legacy__attackchain` suffix must be carefully understood in order to migrate them properly. This is not just a matter of renaming procs; it is expected that a migration preserve all existing behavior while fixing any potential bugs that were a result of the original implementation. There are several important points: 1. Any subtype which is being migrated must have all of its parent types (up to but not including `/obj/item`) migrated as well. If you are migrating `/obj/item/foo/bar/baz/proc/attacked_by__legacy__attackchain()`, then you must also migrate `/obj/item/foo/bar/proc/attacked_by__legacy__attackchain()` and `/obj/item/foo/proc/attacked_by__legacy__attackchain()`. 2. Once a tree of items has been updated to the new attack chain, its `new_attack_chain` var must be set to `TRUE`. 3. All legacy attack chain procs in an object tree must be migrated at once. While this may seem overwhelming, the good news is that most migrations are straightforward, and because you are only migrating a small part of the codebase at a time, it is easy to test the results. In order to make this process easier, we'll examine some sample migrations that have already been performed. ### `attackby` `attackby` is used in cases when an item needs to respond to another item being used on it. These can be fairly straightforward if the type tree is shallow and the number of interactions is small. Something to note is that `attackby`, despite its name, rarely has behavior that is designed to respond to combat attacks. Most `attackby` methods you will find are simple item interactions; specific behavior the objects want to intercept before allowing the attack phase to begin. Our example migration is `/obj/vehicle`. This type tree only requires migrating: - `/obj/vehicle/proc/attackby__legacy__attackchain` - `/obj/vehicle/janicart/proc/attackby__legacy__attackchain` First, let's look at `/obj/vehicle/attackby__legacy__attackchain`: ```dm /obj/vehicle/attackby__legacy__attackchain(obj/item/I, mob/user, params) if(key_type && !is_key(inserted_key) && is_key(I)) if(user.drop_item()) I.forceMove(src) to_chat(user, "You insert [I] into [src].") if(inserted_key) //just in case there's an invalid key inserted_key.forceMove(drop_location()) inserted_key = I else to_chat(user, "[I] seems to be stuck to your hand!") return if(istype(I, /obj/item/borg/upgrade/vtec) && vehicle_move_delay > 1) vehicle_move_delay = 1 qdel(I) to_chat(user, "You upgrade [src] with [I].") return return ..() ``` The logic here is pretty straightforward. We check to see if the user is attempting to insert a key, if there's already one in the vehicle, and if the key can be dropped by the user. We also check to see if the user is attempting to install the VTEC upgrade. Otherwise we return control to the parent. Now let's look at `/obj/vehicle/janicart/attackby__legacy__attackchain`: ```dm /obj/vehicle/janicart/attackby(obj/item/I, mob/user, params) var/fail_msg = "There is already one of those in [src]." if(istype(I, /obj/item/storage/bag/trash)) if(mybag) to_chat(user, fail_msg) return if(!user.drop_item()) return to_chat(user, "You hook [I] onto [src].") I.forceMove(src) mybag = I update_icon(UPDATE_OVERLAYS) return if(istype(I, /obj/item/borg/upgrade/floorbuffer)) if(buffer_installed) to_chat(user, fail_msg) return buffer_installed = TRUE qdel(I) to_chat(user,"You upgrade [src] with [I].") update_icon(UPDATE_OVERLAYS) return if(istype(I, /obj/item/borg/upgrade/vtec) && floorbuffer) floorbuffer = FALSE vehicle_move_delay -= buffer_delay return ..() //VTEC installation is handled in parent attackby, so we're returning to it early if(mybag && user.a_intent == INTENT_HELP && !is_key(I)) mybag.attackby(I, user) else return ..() ``` Here the logic is a bit more complex, but has a basic structure: we check to see what kind of thing the janicart is being attacked with. If it's a trash bag or floor buffer, we attach it. If it's VTEC upgrade we remove the floorbuffer and return control to the parent for installing the VTEC. If there's a bag and the user is clicking on it with anything else with help intent, attempt to put it in the bag. Otherwise, return control to the parent. Most of this logic will work just fine as is. However, none of this is combat-related, so we should pull it out of `attack_by` and substitute in `item_interaction`. This ensures that all the code involving specific behavior when clicking on the janicart will run before the attack phase, and not get in its way. Note that while `item_interaction` does not require a parent call, in this case it is useful to us because we want to handle the janicart-specific interactions before handling the vehicle-specific interactions. We change all the return statements to return one of the `ITEM_INTERACT_` flags at each junction whenever we have handled the item interaction. ```diff -/obj/vehicle/janicart/attackby(obj/item/I, mob/user, params) +/obj/vehicle/janicart/item_interaction(mob/living/user, obj/item/I, list/modifiers) var/fail_msg = "There is already one of those in [src]." if(istype(I, /obj/item/storage/bag/trash)) if(mybag) to_chat(user, fail_msg) - return + return ITEM_INTERACT_COMPLETE if(!user.drop_item()) - return + return ITEM_INTERACT_COMPLETE to_chat(user, "You hook [I] onto [src].") I.forceMove(src) mybag = I update_icon(UPDATE_OVERLAYS) - return + return ITEM_INTERACT_COMPLETE + if(istype(I, /obj/item/borg/upgrade/floorbuffer)) if(buffer_installed) to_chat(user, fail_msg) - return + return ITEM_INTERACT_COMPLETE buffer_installed = TRUE qdel(I) to_chat(user,"You upgrade [src] with [I].") update_icon(UPDATE_OVERLAYS) - return - if(istype(I, /obj/item/borg/upgrade/vtec) && floorbuffer) + return ITEM_INTERACT_COMPLETE + + if(mybag && user.a_intent == INTENT_HELP && !is_key(I)) + mybag.attackby__legacy__attackchain(I, user) + return ITEM_INTERACT_COMPLETE + + return ..() ``` We also refactor the code regarding VTEC installation: because one subtype does something different in reaction to the installation, we will pull that into its own proc, so that the parent interaction can handle that behavior. ```diff +/obj/vehicle/janicart/install_vtec(obj/item/borg/upgrade/vtec/vtec, mob/user) + if(..() && floorbuffer) floorbuffer = FALSE vehicle_move_delay -= buffer_delay - return ..() //VTEC installation is handled in parent attackby, so we're returning to it early - if(mybag && user.a_intent == INTENT_HELP && !is_key(I)) - mybag.attackby(I, user) - else - return ..() + + return TRUE ``` This allows us to keep the VTEC-specific behavior separate. ```diff +/obj/vehicle/janicart/install_vtec(obj/item/borg/upgrade/vtec/vtec, mob/user) + if(..() && floorbuffer) + floorbuffer = FALSE + vehicle_move_delay -= buffer_delay + + return TRUE ``` That is: if the VTEC installation was successful, we disable the floorbuffer and its delay. We want to return `TRUE` at the end no matter what, because this is the indication not that the VTEC installation was succesful, but that it was attempted, and thus the rest of the attack chain is not necessary. We'll take the opportunity to rename the passed in argument from `I` to `used` to make the code clearer, as well: ```diff -/obj/vehicle/janicart/item_interaction(mob/living/user, obj/item/I, list/modifiers) +/obj/vehicle/janicart/item_interaction(mob/living/user, obj/item/used, list/modifiers) // etc... ``` Finally, set `new_attack_chain = TRUE` on `/obj/vehicle.` > [!NOTE] > > An advantage of migrating the attack chain procs piecemeal is that each PR > requires less testing relative to the whole, but this testing must still > occur. > > It is important to come up with a comprehensive list of things to test for > each migration PR. For the above migration, an example set of tasks might > include: > > - Testing that bags can be attached to janicarts > - Testing that players can get on and off all vehicles > - Testing that keys can be inserted into vehicles > - Testing that only the correct keys can be inserted into vehicles > - Testing attacking vehicles with other objects > - Testing adding the floor buffer to janicarts > - Testing adding VTEC to vehicles > - Come up with your own test cases! > > The valuable thing about keys and attacks with other objects is because > they're not part of the new attack chain yet. This helps to ensure the legacy > and new attack chains are interacting with each other properly, as well. ### `attack_self` `attack_self` is, typically, not part of the chain's attack phase at all. In the new attack chain, the proc is called `activate_self` to reflect this. Let's examine the case of airlock electronics, `/obj/item/airlock_electronics`. This is a good example because it has no parent types we need to migrate. Here is the proc before the migration: ```dm /obj/item/airlock_electronics/attack_self__legacy__attackchain(mob/user) if(!ishuman(user) && !isrobot(user)) return ..() if(ishuman(user)) var/mob/living/carbon/human/H = user if(H.getBrainLoss() >= max_brain_damage) to_chat(user, "You forget how to use [src].") return ui_interact(user) ``` There's a couple things to note here: 1. Currently, the parent proc is only called if the player is in a mob that isn't meant to interact with the electronics; which means the signal `COMSIG_ACTIVATE_SELF` is only sent if the electronics _aren't_ activated by the user! 2. The behavior regarding brain damage and being unable to use the electronics seems like it would be much more useful if generalized into a component, but we can ignore that for now. The first thing we do is ensure that the parent proc is called at the correct time, and correctly respond to its requests if the interaction should be cancelled: ```diff /obj/item/airlock_electronics/attack_self__legacy__attackchain(mob/user) + if(..()) + return if(!ishuman(user) && !isrobot(user)) return ..() ``` Secondly, we can pull the other guard clause into the conditional, since it returns in the same manner: ```diff /obj/item/airlock_electronics/attack_self__legacy__attackchain(mob/user) - if(..()) + if(..() || (!ishuman(user) && !isrobot(user))) return - if(!ishuman(user) && !isrobot(user)) - return ..() ``` Then, we rename the proc: ```diff -/obj/item/airlock_electronics/attack_self__legacy__attackchain(mob/user) +/obj/item/airlock_electronics/activate_self(mob/user) ``` And, importantly, we change the value of `var/new_attack_chain` in the object declaration to let the attack chain know to use the new proc: ```diff /obj/item/airlock_electronics name = "airlock electronics" icon = 'icons/obj/doors/door_assembly.dmi' // ... + new_attack_chain = TRUE ``` The migration is complete. The proc now looks like this: ```dm /obj/item/airlock_electronics/activate_self(mob/user) if(..() || (!ishuman(user) && !isrobot(user))) return if(ishuman(user)) var/mob/living/carbon/human/H = user if(H.getBrainLoss() >= max_brain_damage) to_chat(user, "You forget how to use [src].") return ui_interact(user) ``` ### `attack` Let's now look at a more complex example, the cult dagger, `/obj/item/melee/cultblade/dagger`. This is the code as it exists before the migration: ```dm /obj/item/melee/cultblade/dagger/attack__legacy__attackchain(mob/living/M, mob/living/user) if(IS_CULTIST(M)) if(M.reagents && M.reagents.has_reagent("holywater")) //allows cultists to be rescued from the clutches of ordained religion if(M == user) // Targeting yourself to_chat(user, "You can't remove holy water from yourself!") else // Targeting someone else to_chat(user, "You remove the taint from [M].") to_chat(M, "[user] removes the taint from your body.") M.reagents.del_reagent("holywater") add_attack_logs(user, M, "Hit with [src], removing the holy water from them") return FALSE else var/datum/status_effect/cult_stun_mark/S = M.has_status_effect(STATUS_EFFECT_CULT_STUN) if(S) S.trigger() . = ..() ``` Because the dagger has a parent proc, let's also examine that: ```dm /obj/item/melee/cultblade/attack__legacy__attackchain(mob/living/target, mob/living/carbon/human/user) if(!IS_CULTIST(user)) user.Weaken(10 SECONDS) user.unEquip(src, 1) user.visible_message("A powerful force shoves [user] away from [target]!", "\"You shouldn't play with sharp things. You'll poke someone's eye out.\"") if(ishuman(user)) var/mob/living/carbon/human/H = user H.apply_damage(rand(force/2, force), BRUTE, pick("l_arm", "r_arm")) else user.adjustBruteLoss(rand(force/2, force)) return if(!IS_CULTIST(target)) var/datum/status_effect/cult_stun_mark/S = target.has_status_effect(STATUS_EFFECT_CULT_STUN) if(S) S.trigger() ..() ``` There are several codepaths happening here: 1. If a non-cultist attacks with the dagger, they are forced to drop it, gain several status effects, and receive brute damage. 2. If a cultist attacks another cultist, it removes holy water from the target. If the target has no holy water in them, it does nothing. 3. If a cultist attacks a non-cultist, and the non-cultist has a cult-stun status effect, it is prolonged. Several issues should become apparent while reading through this. First let's determine when the root `attack()` proc is actually called: 1. In `cultblade/dagger/attack()`, there is an early return if the target is a cultist. 2. In `cultblade/attack()`, there is an early return if the user is _not_ a cultist. This means that we only wish to follow through with an attack if the user is a cultist and the target is not. This suggests to us that the code for the first two codepaths above should come before the `attack()`, logically, the `pre_attack()`. (You may also notice a bug in the code. We'll point it out below but see if you can spot it yourself.) The first thing we'll do is handle the first codepath, in the dagger's parent type: ```diff -/obj/item/melee/cultblade/attack__legacy__attackchain(mob/living/target, mob/living/carbon/human/user) +/obj/item/melee/cultblade/pre_attack(atom/target, mob/living/user, params) + if(..()) + return FINISH_ATTACK if(!IS_CULTIST(user)) // ... + return FINISH_ATTACK ``` We return early if our parent proc asks us to, and we return early if the user isn't a cultist, because the user can't perform an attack. We adjust the attack proc itself to check its parent, and perform the cult-stun trigger. We return nothing to let the attack chain know to continue: ```diff +/obj/item/melee/cultblade/attack(mob/living/target, mob/living/carbon/human/user) + if(..()) + return FINISH_ATTACK + if(!IS_CULTIST(target)) var/datum/status_effect/cult_stun_mark/S = target.has_status_effect(STATUS_EFFECT_CULT_STUN) if(S) S.trigger() - ..() ``` Then we'll handle the dagger itself. Again, we want to cancel the attack chain if a cultist user is attacking a cultist target, one way or another: ```diff -/obj/item/melee/cultblade/dagger/attack(mob/living/M, mob/living/user) - if(IS_CULTIST(M)) - if(M.reagents && M.reagents.has_reagent("holywater")) - if(M == user) // Targeting yourself +/obj/item/melee/cultblade/dagger/pre_attack(atom/target, mob/living/user, params) + if(..()) + return FINISH_ATTACK + + if(IS_CULTIST(target)) + if(target.reagents && target.reagents.has_reagent("holywater")) + if(target == user) // Targeting yourself // ... + return FINISH_ATTACK ``` By regularly calling the parent proc first, it's easier to think through the process of what's happening. It's much easier to tell that the parent proc handles failed attacks by non-cultists first, and only if that's not the case does the holy-water removal behavior run. Since we know we're not a non-cultist at this point, we don't need to perform a second check for that, either. This also fixes a bug in the original code. Because the code to re-trigger cult-stuns on targets was duplicated in both attack procs, it was being called twice. It's now much easier to tell when that is happening. The resultant code looks like this: ```dm /obj/item/melee/cultblade/pre_attack(atom/target, mob/living/user, params) if(..()) return FINISH_ATTACK if(!IS_CULTIST(user)) user.Weaken(10 SECONDS) user.unEquip(src, 1) user.visible_message("A powerful force shoves [user] away from [target]!", "\"You shouldn't play with sharp things. You'll poke someone's eye out.\"") if(ishuman(user)) var/mob/living/carbon/human/H = user H.apply_damage(rand(force/2, force), BRUTE, pick("l_arm", "r_arm")) else user.adjustBruteLoss(rand(force/2, force)) return FINISH_ATTACK /obj/item/melee/cultblade/attack(mob/living/target, mob/living/carbon/human/user) if(..()) return FINISH_ATTACK if(!IS_CULTIST(target)) var/datum/status_effect/cult_stun_mark/S = target.has_status_effect(STATUS_EFFECT_CULT_STUN) if(S) S.trigger() /obj/item/melee/cultblade/dagger/pre_attack(atom/target, mob/living/user, params) if(..()) return FINISH_ATTACK if(IS_CULTIST(target)) if(target.reagents && target.reagents.has_reagent("holywater")) //allows cultists to be rescued from the clutches of ordained religion if(target == user) // Targeting yourself to_chat(user, "You can't remove holy water from yourself!") else // Targeting someone else to_chat(user, "You remove the taint from [target].") to_chat(target, "[user] removes the taint from your body.") target.reagents.del_reagent("holywater") add_attack_logs(user, target, "Hit with [src], removing the holy water from them") return FINISH_ATTACK ``` Not only is it much easier to read and understand what is happening, it is also divided up into smaller, more manageable procs with clear names to explain the sequence of events. Finally, because we constantly check the parent proc, all signals that are expected to be sent, are, so any other components or listeners can take appropriate action and cancel the attack chain themselves, if requested. ### Cancelling All Behavior Frequently, a subtype will want to completely prevent any of its parent type behavior from running. Examples may be a holofloor, which should prevent any attempts to deconstruct it, or a destroyed variant of an object, which cancels out the existing functionality of the parent type. Attack chain methods must always call their parent procs, so this presents a problem. In order to implement behavior such as this, the child type should register to listen for the signal that applies to the attack chain proc, and respond by calling one of the procs which return a signal preventing the rest of the attack chain from running. For `attack_by` prevention, this proc is [/datum/proc/signal_cancel_attack_by][]. For `activate_self` prevention, this proc is [/datum/proc/signal_cancel_activate_self][]. [/datum/proc/signal_cancel_attack_by]: https://codedocs.paradisestation.org/datum.html#proc/signal_cancel_attack_by [/datum/proc/signal_cancel_activate_self]: https://codedocs.paradisestation.org/datum.html#proc/signal_cancel_activate_self For example, when we migrated the airlock electronics above, we neglected to handle the `/destroyed` subtype, which prevents any interaction via `activate_self`. To ensure this, we make the following change: ```diff +/obj/item/airlock_electronics/destroyed/Initialize(mapload) + . = ..() + RegisterSignal(src, COMSIG_ACTIVATE_SELF, TYPE_PROC_REF(/datum, signal_cancel_activate_self)) ``` ## Migration Helpers There are two important tools which can help make the migration process easier: the _migration plan checker_ and the _attack chain CI checks_. ### Migration Plan Checker If you are making a code change and need to update the attack chain on an object, the migration plan checker will tell you what other types will need to be migrated in the same PR. For example, if I wanted to migrate `/turf/simulated/wall/cult`, I could run the migration plan checker at the command line: > [!NOTE] > > When running the migration plan checker, be sure to run it from the root > directory of your repository (`\Paradise`) and to use the version of Python > provided by the bootstrap module (`tools\bootstrap\python`). If you know > specifically that you are running in PowerShell, use the appropriate command > (`tools\bootstrap\python_.ps1`). ``` $ tools\bootstrap\python .\tools\migrate_attack_chain.py /turf/simulated/wall/cult Migration Plan for Path /turf/simulated/wall/cult Required Additional Migrations: /turf /turf/simulated/floor /turf/simulated/floor/chasm /turf/simulated/floor/grass /turf/simulated/floor/holofloor /turf/simulated/floor/indestructible /turf/simulated/floor/lava /turf/simulated/floor/lava/lava_land_surface/plasma /turf/simulated/floor/light /turf/simulated/floor/mineral/bananium /turf/simulated/floor/mineral/plasma /turf/simulated/floor/mineral/uranium /turf/simulated/floor/plating /turf/simulated/floor/plating/asteroid /turf/simulated/floor/plating/metalfoam /turf/simulated/floor/vines /turf/simulated/mineral /turf/simulated/mineral/ancient /turf/simulated/mineral/ancient/lava_land_surface_hard /turf/simulated/mineral/ancient/outer /turf/simulated/wall /turf/simulated/wall/indestructible /turf/simulated/wall/mineral/plasma /turf/simulated/wall/mineral/uranium /turf/simulated/wall/mineral/wood /turf/simulated/wall/r_wall /turf/space /turf/space/transit Toggle `new_attack_chain = TRUE` on: /turf ``` You can see the output shows two things: 1. The list of other types that will need to be migrated at the same time, and 2. The type that `new_attack_chain` should be set to TRUE after the migration is complete. This means that instead of just migrating `/turf/simulated/wall/cult`, we will be migrating 28 types. This is a lot! A migration of this size is not recommended for new contributors. On the other hand, let us examine migrating wirecutters: ``` $ tools\bootstrap\python .\tools\migrate_attack_chain.py /obj/item/wirecutters Migration Plan for Path /obj/item/wirecutters Required Additional Migrations: /obj/item/wirecutters /obj/item/wirecutters/power Toggle `new_attack_chain = TRUE` on: /obj/item/wirecutters ``` If we wanted to migrate `/obj/item/wirecutters`, we only need to migrate that type and one other, the power wirecutters. This is a much more manageable migration for novice contributors. ### Attack Chain CI Checks The attack chain CI checks are run when a pull request is opened or modified. It provides a high-level overview of how complete a migration is. These checks will: - Ensure that anything marked with `new_attack_chain = TRUE` does not override any legacy attack chain procs - Ensure that all of the objects required in a specific type migration have been migrated - Ensure that any other types that call attack chain methods on migrated types no longer call legacy attack chain procs. Let's look at our wirecutters example again. If we were simply to set `new_attack_chain = TRUE` on `/obj/item/wirecutters` without making any other code changes, the CI check will return something like this: ``` check_legacy_attack_chain started new_attack_chain on /obj/item/wirecutters still has legacy procs: attack__legacy__attackchain new_attack_chain on /obj/item/wirecutters/power still has legacy procs: attack_self__legacy__attackchain check_legacy_attack_chain tests completed in 5.06s ``` This makes it clear that both types in the type chain marked as new still have legacy procs that need to be migrated. Let's return to our `/turf` example. You can see in the output from the Migration Plan Checker that if we were to migrate `/turf/simulated/wall/cult` that `new_attack_chain` should be set to `TRUE` on `/turf` itself. What if we were to ignore that, and not make any other changes but to set `new_attack_chain` to `TRUE` on `/turf/simulated/wall`? The CI check will return something like this: ``` check_legacy_attack_chain started new_attack_chain on /turf/simulated/mineral still has legacy procs: attackby__legacy__attackchain new_attack_chain on /turf/simulated/mineral but related type /turf is not Call sites requiring migration: code\game\turfs\simulated\minerals.dm:139: /turf/simulated/mineral/Bumped(...) calls attackby__legacy__attackchain(...) on var /turf/simulated/mineral/src code\game\turfs\simulated\minerals.dm:133: /turf/simulated/mineral/Bumped(...) calls attackby__legacy__attackchain(...) on var /turf/simulated/mineral/src code\game\turfs\simulated\minerals.dm:131: /turf/simulated/mineral/Bumped(...) calls attackby__legacy__attackchain(...) on var /turf/simulated/mineral/src new_attack_chain on /turf/simulated/mineral/ancient still has legacy procs: attackby__legacy__attackchain new_attack_chain on /turf/simulated/mineral/ancient but related type /turf is not new_attack_chain on /turf/simulated/mineral/ancient/lava_land_surface_hard still has legacy procs: attackby__legacy__attackchain new_attack_chain on /turf/simulated/mineral/ancient/lava_land_surface_hard but related type /turf is not new_attack_chain on /turf/simulated/mineral/ancient/outer still has legacy procs: attackby__legacy__attackchain new_attack_chain on /turf/simulated/mineral/ancient/outer but related type /turf is not new_attack_chain on /turf/simulated/mineral/clown but related type /turf is not new_attack_chain on /turf/simulated/mineral/random but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/high_chance but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/high_chance/clown but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/high_chance/volcanic but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/labormineral but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/low_chance but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/volcanic but related type /turf is not new_attack_chain on /turf/simulated/mineral/random/volcanic/labormineral but related type /turf is not new_attack_chain on /turf/simulated/mineral/volcanic but related type /turf is not new_attack_chain on /turf/simulated/mineral/volcanic/clown but related type /turf is not new_attack_chain on /turf/simulated/mineral/volcanic/lava_land_surface but related type /turf is not check_legacy_attack_chain tests completed in 5.51s ``` There is a lot of output here, but it should be straightforward to understand: 1. First, it tells you that `/turf/simulated/wall` (the type we changed to `new_attack_chain`) still has legacy procs. That check is straightforward. 2. Then, it tells you that a parent type, `/turf`, is not on the new attack chain, despite one of its subtypes being so. 3. There is a set of "call sites requiring migration": these let you know that there are procs that make calls to legacy attack chain procs on a type that we've marked as migrated. In this case, they are all on turf instances, but they may be anywhere in the codebase. 4. Finally, it performs the same checks as above to other related types. This makes it clear that there are many more steps that must be performed before the migration is complete.