BPD classroom - BLCM/BLCMods GitHub Wiki
Almost everything in here is courtesy of LightChaosMan so go say thanks, that said don't annoy him about this stuff since its complicated.
- Table of contents
Welcome to the BPD-classroom. Here we will be explaining how BPDs (Behavior Provider Definition) work, and how you can change them, without crashing your game, as well as the parts that are currently unknown.
There are a couple of tools which may come in useful as you start diving into BPDs.
- OpenBLCMM is the main tool which you should already be quite familiar with. Its Object Explorer will be invaluable in taking a look at BPDs and navigating around to find out how things work. (And, of course, you'll be using it to make whatever changes you want to make.)
- FT/BLCMM Explorer is a tree-based view into the same data available in OpenBLCMM's Object Explorer, and is sometimes useful as a higher-level overview. OpenBLCMM's OE should still be your first stop for data, but it may be useful to have this as well (it's useful for getting a nice visual list of all the Behaviors inside a BPD, for instance)
- BPD/Kismet Grapher (sourcecode) is an online tool to generate nice visual graphs of BPDs (and Kismet sequences, though we won't be talking about those here). These graphs are just about invaluable to get a feel for how a BPD actually flows, without having to do a ton of legwork yourself.
- BPD Number Calculator is a little page to automatically convert the strange numbers you'll encounter inside BPDs, which are talked about in Lesson Three.
Additionally, some older tools are still available:
- Adudney's Behavior Tool can also be used to get a feel for the structure of BPDs, though it's been largely superceded by the BPD/Kismet Grapher.
- Two's Compliment Calculator can be used to interpret some of the strange numbers found in BPDs, though the BPD Number Calculator is custom-made for the calculations in BL2 and TPS.
BPDs, along with Kismets, govern some of the most interesting stuff in the game.
NPC's transfering during quests, the Butcher ammo gimmick, intersting skills like blood soaked shields, all action skills, amp shields, enemies spawning attached to each other, and 100s of other things in the game.
Borderlands has a bunch of Behavior_*
objects which do very simple activities. A Behavior_SimpleAnimPlay
behavior will start an animation, a Behavior_SpawnItems
behavior will spawn some items int he world, and a Behavior_TriggerDialogEvent
behavior will trigger some dialog, for instance.
BPDs are attached to specific objects in the game (like an enemy, a projectile, a container, etc) and collect a group of these individual Behavior_*
objects together using a bunch of logic, so that more complex things can happen. If you need more than one Behavior to accomplish your goal, a BPD comes into play.
For example, for amp shields, the BPD more or less translates to:
- When a shot is fired
- If the current shield value is at maximum, then damage the shield
- Play a sound effect.
The graph for this, via the BPD/Kismet Grapher, looks like this:
There are a total of four Behavior
objects referenced in there, and the logic of the BPD is what ties them together and ensures that things happen in the proper order.
Using BPDs, you can create complicated if-then-else constructs, with random chance, delay and whatnot in between. In the end, it's about as close as you can get to actual programming inside Borderlands modding.
If you want an example of a full blown web of behaviors, have a look at the BPD of an action skill.
Alright, so we know it does cool stuff, but what does it actually look like?
We'll be using one main BPD during this tutorial -- the BPD for amp shields: GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0
Dumping that object name inside OpenBLCMM's Object Explorer will yield:
All BPDs contain an array BehaviorSequence
. For simpler BPDs, this array will usually only contain a single entry, which is the actual BehaviorSequence
we're interested in. More complicated BPDs may have many more.
When referring to a BPD in the rest of class, we will be referring to whatever is inside this BehaviorSequence
.
Each BehaviorSequence contains a couple of fields of its own. The parts that are interesting to us are EventData2
, BehaviorData2
, ConsolidatedOutputLinkData
, ConsolidatedVariableLinkData
and ConsolidatedLinkedVariables
.
As the name implies, a BehaviorSequence
, is actually a sequence of behaviors. That is, it is a load of behaviors linked together, in some way.
The actual behaviors that are being linked are the ones listed in BehaviorData2
.
How they are linked, is not clear at all, at least not at first glance. You'll notice throughout the entire BPD, there's all these wierd looking numbers, that seem completely random.
Well, they're not random. These numbers are actually the things that connect everything together, in a 'connect the dots' kind of fashion.
Alright, let's start with the cornerstone of BPDs, the magic that's used to connect everything: what do all those weird looking numbers mean?
These numbers, are actually two numbers merged together, for efficiency and stuff. This is implied by the names of the fields that precede these weird numbers, like LinkIdAndLinkedBehavior
or ArrayIndexAndLength
.
Okay, so how are there two numbers in there? The answer is binary numbers. Borderlands uses 32 bit integers for most things in the game, and these weird numbers are integers.
So how do you put 2 numbers into a single 32 bit number? There's a few ways, but one way is to give each number 16 bits, and then just push them together.
So, lets look at a specific example, the first weird number in the BPD, in the eventdata
of our example: OutputLinks=(ArrayIndexAndLength=196609)
.
If we convert 196609
to binary we find 00000000 00000011 00000000 00000001
.
Since this is just two 16 bit numbers pushed together, we can split them by taking the rightmost 16 bits and whatever is leftover.
This would turn 0000000000000011 0000000000000001
into 11
and 1
, which are both binary numbers, representing 3
and 1
in base 10.
Combining this with the name ArrayIndexAndLength
, it makes sense that it means that the number represents an array index of 3
, and a length of 1
, for whatever that may be used.
There's a custom-built calculator for doing these calculations both ways, which should greatly simplify the process. If you want to take a look at the actual binary values involved, a more generic calculator is available online as well.
There's another class of compound number which we haven't run into yet but will soon, called LinkIdAndLinkedBehavior
. This is a compound number just like ArrayIndexAndLength
, but rather than being split right down the middle, it's chopped up a little more finely.
A number that we'll see shortly is -16777214
, which translates in two's-compliment binary to 11111111 00000000 00000000 00000010
. If we chopped that directly in half like we did for ArrayIndexAndLength
, we'd end up with a very weird -256
on the left-hand side. Instead, numbers in LinkIdAndLinkedBehavior
seem to have the "Link ID" be just the first 8 bits, and then the "Linked Behavior" is the last 16. That second byte in there just gets ignored (that's been confirmed for all data in BL2 and TPS).
So, for LinkIdAndLinkedBehavior
, you take the first byte for Link ID, as a signed two's compliment byte, which means the Link ID in this case is -1
, and then the behavior is binary 10
in this case, which is 2
. So -1
and 2
. As with ArrayIndexAndLength
, you can use the BPD Number Calculator online to do all these calculations for you.
("Two's Compliment" is the style of binary encoding which allows negative numbers. The two's compliment calculator mentioned above can show you the binary values involved, though the custom-build BPD Number Calculator means that you shouldn't have to actually care. Be sure to use the 32 bit
options at the generic calculator, if you do use it.)
Now that we know how to read the weird numbers, lets see how a BPD uses them to 'connect the dots'.
Every sequence needs a start, so let's start there, how do we find the first behavior in the sequence?
Naturally, the behavior in question is somewhere in BehaviorData2
. The first weird number that we need is the OutputLinks
of the event that triggers this sequence, which happens to be the number that we analyzed above.
We know that this number, 196609
, actually means 3
and 1
.
You might think: "alright, so the first behavior is the one at index 3
of BehaviorData2
", but, you'd be wrong.
All pointers towards behaviors are actually rerouted trough ConsolidatedOutputLinkData
. This means that the thing we just found is whatever the things are at index 3 through 3
(length 1
) of ConsolidatedOutputLinkData
, which is the single element LinkIdAndLinkedBehavior=0,ActivateDelay=0.000000
.
Now, LinkIdAndLinkedBehavior=0
is one of those compound numbers, except this time it represents a LinkId
of 0
and a linkedBehavior
of 0
, which combines to a total number of 0
.
For now, ignore LinkId
, the thing we we are interested in the second number, the LinkedBehavior
, which happens to also be 0
.
This means that the first behavior in this sequence is the behavior at index 0
of the BehaviorData2
array.
This happens to be
(
Behavior =
Behavior_SetShieldTriggeredState'GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0.Behavior_SetShieldTriggeredState_35',
LinkedVariables =
(ArrayIndexAndLength=1),
OutputLinks =
(ArrayIndexAndLength=2)
)
We now know the starting point of our sequence. All that's left is to figure out how the rest of the behaviors are linked together.
Luckily, if you were able to follow along this far, that part is relatively easy. Each entry in the BehaviorData2
array, contains a field OutputLinks
.
This field tells us which behaviors follow this one. Again, this is one of these compound numbers, and since it is used to eventually point us to other behaviors, it will first point us to ConsolidatedOutputLinkData
, just like with the initial link.
In this case, our compound number is 2
, which translates to an ArrayIndex
of 0
and a Length
of 2
, after doing the binary conversion.
This means we look at the two entries (length 2
) starting at index 0
, so concretely, we look at the elements at index 0
and index 1
.
These are the following two elements: LinkIdAndLinkedBehavior=-16777214,ActivateDelay=0.000000
and LinkIdAndLinkedBehavior=-16777213,ActivateDelay=0.000000
.
As stated above, -16777214
translates to 11111111 00000000 00000000 00000010
in binary, and since LinkIdAndLinkedBehavior
variables aren't just chopped in half, it translates to -1
and 2
. Similarly, -16777213
translates to -1
and 3
.
As before, we'll disregard the Link IDs for now, and look at just the LinkedBehaviors
, for which we have found the indices 2
and 3
.
So, the two behaviors following the initial behavior are those stored at index 2
and 3
of BehaviorData2
, or the third and fourth element, since in computer science, we count starting from 0.
These are the Behavior_CompareFloat
and Behavior_PostAkEvent
behaviors. Following the same procedure, we find that the Behavior_PostAkEvent
isn't followed by anything, and Behavior_CompareFloat
is followed by the Behavior_SimpleMath
, trough the third element of ConsolidatedOutputLinkData
.
So, we now know how the behaviors are linked together, but how do the behaviors know what to do?
Some behaviors work fine as a standalone behavior, like Behavior_Explode
. Others, like Behavior_SimpleMath
require some sort of input/output to work their magic.
For the amp shield example we're using, shield stats are of particular interest. Such data is conveyed trough variables
, or properties
if you will.
Each entry in the BehaviorData2
array has a field LinkedVariables
. Using the same compound numbers as before, this points towards sub-arrays of ConsolidatedVariableLinkData
.
So, for example, Behavior_SimpleMath_4
has a field LinkedVariables
equal to ArrayIndexAndLength=196611
, which translates to an Array index of 3
and a length of 3
.
So, that means that Behavior_SimpleMath_4
uses the entries 3
, 4
and 5
of ConsolidatedVariableLinkData
, or
(PropertyName="A",VariableLinkType=BVARLINK_Input,ConnectionIndex=0,LinkedVariables=(ArrayIndexAndLength=131073),CachedProperty=None),
(PropertyName="B",VariableLinkType=BVARLINK_Input,ConnectionIndex=0,LinkedVariables=(ArrayIndexAndLength=262145),CachedProperty=None),
(PropertyName="Result",VariableLinkType=BVARLINK_Output,ConnectionIndex=0,LinkedVariables=(ArrayIndexAndLength=327681),CachedProperty=None)
Like earlier with behaviors, this is just an intermediate step.
From here we follow the next LinkedVariables
field, which is another ArrayIndexAndLength
.
As far as we've yet to find an exception, the Length part of the numbers stored in these particular LinkedVariables
is always equal to 1
, since it points to a single entry in the ConsolidatedLinkedVariables
array.
Let's take the first entry from above as an example, which has LinkedVariables=(ArrayIndexAndLength=131073)
, which translates to index 2
and length 1
.
Similarly, the numbers corresponding with B
are index 4
and length 1
, and result maps to index 5
and length 1
.
From these 3 indices (2
,4
and 5
) (and yes, they are actually just indices, since they are all of length 1
), we go to ConsolidatedLinkedVariables
, which is equal to (0
,0
,1
,0
,2
,1
,0
,3
,1
,0
,0
) for our BPD.
Indices 2
, 4
and 5
point towards the numbers 1
, 2
and 1
respectively. This makes sense, as that behavior is the thing that actually causes amp drain, so 1
corresponds to current shield value, and 2
corresponds to the amp drain of the shield.
It then performs the calculation 1
-2
, and writes it to its BVARLINK_Output
variable Result
, which happens to be the same variable as A
, or the current shield value.
Finally, these numbers in ConsolidatedLinkedVariables
are actually another layer of indices. This time, they point towards VariableData
of the BPD.
Concretely, this means that the first (Name=,Type=BVAR_Attribute)
you see in there is the current shield value, the second one is the amp drain, and, as you would discover trough more digging, the third is the max shield value.
This was a humongous wall of text, but that is how the dots of a BPD are connected. Once you understand what's going on, this image should help you to keep the things organized:
We've been ignoring the "Link ID" part of LinkIdAndLinkedBehavior
variables for now, and the exact specifics of how to use them are still a little bit vague (though as more people get into modding BPDs, our understanding is growing). In some cases they seem to be used to control what Behaviors actually get called. For instance, Behavior_CompareInt
/Behavior_CompareFloat
/etc behaviors seem to use the Link ID to know which links to follow, depending on the results of the comparison. Likewise, that seems to be the case in Behavior_RandomBranch
behaviors, which are seen frequently throughout the data.
The case of having a Link ID of -1
is still a little bit strange; it's possible that maybe that means that the link should be followed regardless of the results of any comparisons or the like being done in the Behavior. In terms of frequency, -1
is seen just slightly less than a Link ID of 0
. A full breakdown of the distribution of Link IDs across BL2 and TPS, can be found here. (Note that that page is showing 255
instead of -1
. The binary value 11111111
can be interpreted as either 255
or -1
, depending on what makes sense, and the assumption while creating that page is different than what I think now.)
So, you've managed to get this far without being intimidated by a wall of text? Good on ya! Grab a beer or something to celebrate.
Once you understand it till here, it will only get easier.
Next on the agenda is actually modifying a BPD.
Unlike most things in this game, behaviors and BPDs are NOT static. That is, changes you make to them can be overwritten, seemingly at random sometimes.
Remember those Behavior_Delay
things that you tried to change once with no luck?
That is because Behavior_Delay
uses input variables, and each time the BPD is executed, the value inside the Behavior_Delay
is overwritten with this new input.
The main thing to keep in mind is that we cannot, with our current understanding of BPDs, modify anything about ConsolidatedVariableLinkData
and VariableData
.
-
ConsolidatedVariableLinkData
is modified by the game each time the BPD is executed, itsCachedProperty
is updated. -
We have no clue where the game is pulling the actual values from. By trial and error, and dumping in-game, one can usually piece together what a
(Name=,Type=BVAR_Attribute)
actually is, likemax shield
, for example.
Since we don't even know how the game is getting all these values, we can't hope to modify them with our current understanding.
It is probably related to the BVAR_Object
and BVARLINK_Context
parts in the BPD, or possible it's coming from somewhere outside of the BPD, like maybe LockedSkillDetailStr=<Locked>
in GD_Shields.Skills.Impact_Shield_Skill
, or who knows where.
Fortunately, most of the rest of a BPD structure is fair game for editing, though juggling all the various variables, indexes, and two's-compliment numbers can make it a sometimes bewildering practice. Note that all BPD edits must be done via Hotfix. That's actually a boon, in the end, because it allows you to specify the exact value you want to edit rather than having to specify the whole BehaviorSequence
at once.
As with regular Borderlands modding (at least in BL2), one limitation is that we can't add elements to an existing array without redefining the whole array. If you wanted to do something like add some new elements to ConsolidatedOutputLinkData
, to link Behaviors differently than they already are, you may want to first set up a hotfix which just copies the "stock" array but adds a bunch of empty elements at the end (with their values set to 0
), and then use targetted hotfixes to set the values when appropriate. Ideally you could do that alongside comments and folders inside your mod, so that it's clear what various parts of your edits are meant to do. Merely setting the whole array all at once would be a little less clear to anyone else looking at the code (or even to yourself, a day or two after you're done dealing with it).
There are multiple ways we can change things. We'll start with unlinking variables.
A few paragraphs above, Behavior_Delay
was used as an example of a Behavior whose value you can't directly alter. The Behavior tends to use input variables, so each time the Behavior is executed, any changes you tried to make earlier will get overwritten. This is controlled by the LinkedVariables.ArrayIndexAndLength
property inside the BehaviorData2
array.
Well, we can't change ConsolidatedVariableLinkData
or VariableData
, but we can alter the LinkedVariables.ArrayIndexAndLength
property inside the BehaviorData2
array. So if you wanted to set a static custom delay right on a Behavior_Delay
object, just find its entry inside BehaviorData2
and use a hotfix like the following:
set GD_Whatever.Foo:BehaviorProviderDefinition_0 BehaviorSequences[X].BehaviorData2[Y].LinkedVariables.ArrayIndexAndLength 0
Substituting the proper object name, and X
and Y
for the proper indexes. Then the behavior won't have a linked variable anymore, and you can hardcode whatever you like.
Note that some behaviors may not work properly with their variables de-linked, but it can be useful in a variety of contexts.
To make amp shields drain 100% of shield on each amp shot, we could replace the variable that is now using the amp drain variable, by the one that uses the current shield value. The new shield value will then always evaulate to current shield - current shield = 0
.
From above, we know that the second operand of the Behavior_SimpleMath
uses the variable at index 4
of ConsolidatedLinkedVariables
, and we know that the first operand is current shield
, which is variable 1
.
To realize completely draining amps we would wanna make something like
set GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0 BehaviorSequences[0].ConsolidatedLinkedVariables[4] 1
Similarly, one can plug in different variables into other behaviors, like in the Behavior_CompareFloat
.
If Behavior_CompareFloat
does not return true
(it checks if current shield == max shield
), the behavior sequence is halted.
So, if you plug in two of the same variables into the Behavior_CompareFloat
, it will always return true, and hence, the BPD will continue execution no matter the current shield
value.
Notice that, to make continously draining amp shields, more needs to be done outside of the BPD, which we won't go over here.
We can alter Behavior_Delay
objects by unlinking their variable bindings (or by changing the binding to some other variable in the BPD), but we can also specify our own delays on any behavior link in the BPD, by modifying ConsolidatedOutputLinkData
elements.
Each entry of this array has two fields: LinkIdAndLinkedBehavior
, which points towards a Behavior object, and ActivateDelay
, which is a number of seconds the BPD waits before executing the linked behavior. Since this number must be positive, all we can do is make behaviors execute later.
One could use this to make amp shields drain 1 second after firing, allowing you to get off multiple amp shots, and probably getting multiple drains afterwards, which would look like this
set GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0 BehaviorSequences[0].ConsolidatedOutputLinkData[3].ActivateDelay 1
This method alone can't be used to remove delays completely, unless there is some variable in the BPD that seems to always be equal to 0. For instance, the Landscaper only has fixed variables at 0.2 seconds, so you can make the pellets explode quickly, but not instantly with this method.
Next on the agenda is actually rewiring a BPD, removing and adding of behaviors.
We'll start with removing a behavior. Say you have a Behavior_CompareXXX
or Behavior_Delay
, Behavior_RandomBranch
or whatever else is getting in the way of executing your BPD all the way trough.
You'll wanna remove that behavior.
The way we remove the behavior, is not by actually removing the behavior from BehaviorData2
, but by redirecting every behavior that links towards this blocking behavior towards the behavior the blocking behavior points to.
For this example, we'll cut out the Behavior_CompareFloat
from the sequence, which will effectively achieve the same result as we got by remapping the variables.
First off, we need to know what points to the Behavior_CompareFloat
.
As we discovered above, Behavior_CompareFloat
is one of the two behaviors following the Behavior_SetShieldTriggeredState
, where Behavior_PostAkEvent
is the other behavior following Behavior_SetShieldTriggeredState
. The goal now becomes to make Behavior_SetShieldTriggeredState
point directly to Behavior_PostAkEvent
and Behavior_SimpleMath
, which is the single behavior following Behavior_CompareFloat
.
From above we know that the OutputLinks of the Behavior_SetShieldTriggeredState
points towards the two (LinkIdAndLinkedBehavior=-16777214,ActivateDelay=0.000000)
and (LinkIdAndLinkedBehavior=-16777213,ActivateDelay=0.000000)
entries in ConsolidatedOutputLinkData
, which in turn mean linkID -256
(or 255
, depending on how you want to interpet Link IDs) with LinkedBehaviors 2
and 3
.
We want the linkedBehaviors
to now be 1
and 3
, since 1
is Behavior_SimpleMath
, and 2
is Behavior_CompareFloat
. Converting a linkID
of -256
and a LinkedBehavior
to a single number (via the Two's Compliment Calculator) yields -16777215
.
The final code for this rewiring thus becomes:
set GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0 BehaviorSequences[0].ConsolidatedOutputLinkData[0].LinkIdAndLinkedBehavior -16777215
The procedure for adding behaviors is similar, but, we have far less experience with that, and from what we know from @FromDarkHell
and @the_Nocturni
, it's much more .... volatile, from the attempts they had with it.
To add a new behavior to the BPD, the first step is to add it to the BehaviorData2
array. This is again done trough hotfix, but you'll need to copy-paste the entire old array, and append the new behaviors to it.
As mentioned above, when adding a new behavior, the cleanest way to do it is to leave LinkedVariables
and OutputLinks
both being equal to (ArrayIndexAndLength=0)
, and give them the proper values in a later hotfix. That way your edits can be as small as possible, and organized neatly in your mod, with comments and categories to make it clear what you're changing and where.
For this it is important to realize that, just like regular set commands, the order of hotfixes do matter. Hotfixes are executed in the order they are shown in OpenBLCMM. (top -> down)
This means you can first create a big array
, and in a later hotfix, modifify what you just created.
After you have the behavior plugged in, you're gonna need one or more entries in ConsolidatedOutputLinkData
that point towards your new behavior. It is usually good practice to give these new links a unique LinkID
, and naturally, have the LinkedBehavior
be the index
of your newly added behavior. Furthermore, if you have a single behavior linking to multiple next behaviors, you'll wanna have all those links have the same LinkID
. Something we ignored up till now
After that it's a matter of having an existing behavior in the BPD link towards the new ConsolidatedOutputLinkData
entry, and it shouldtm be done.
All the BPDs in the vanilla BL2/TPS data keep all their Behaviors as sub-objects which live directly underneath the BPD itself. For instance, GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0.Behavior_SimpleMath_4
lives "inside" GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0
. There's no actual requirement that the behaviors live underneath the BPD like that, though, so you can (with restrictions) make use of Behaviors from other BPDs if you want.
The main restriction here is that the Behavior generally needs to live in the same top-level package as the BPD itself. So, for instance, if you're altering a rocket launcher BPD and need a spare Behavior_Explode
Behavior, you could use GD_Weap_Launchers.Projectiles.Projectile_Rocket_Creamer:BehaviorProviderDefinition_0.Behavior_Explode_7
even if you're not editing the Creamer. You would not be able to use GD_Weap_SniperRifles.FiringModes.Bullet_Sniper_Volcano:Behavior_Explode_5
, though, because that Behavior lives in the sniper rifle package. 55tumbl's Elemental Banshee Maya makes use of this pretty frequently.
Remember that if you alter a Behavior that's used by some other BPD, you'll also be changing it for that other BPD, so be careful when doing that. In some cases you may be able to do some rewriting on the other BPD to mitigate that -- for instance with the Behavior_Explode
on the Creamer, above, that particular BPD is pretty easy to rewrite so that the Creamer only needs one Behavior_Explode
instead of two, so you can free up the one for your own use. (Doing so would require that either the Creamer's main projectile gets nerfed or the two child projectiles get buffed, but the difference there is pretty minor.)
As mentioned above near the beginning of Lesson Four, we can't modify anything about ConsolidatedVariableLinkData
and VariableData
, at least with our current understanding of how BPDs work.
Consequently, when adding behaviors to a BPD, you are inherently limited to behaviors that do not require inputs/outputs, or at least only those inputs/outputs that are already present in the BPD you are changing.
Having said that, there's probably also some cross-DLC mixing issues, like some of our gunsmiths have experienced (the reason why the OM, pimpernel and sandhawk are popular ones to replace).
All in all, with these 3 methods of changing BPD's, we're more or less free to modify a BPD any way we want in it's original sandbox, and if we're lucky we can bring in a new toy, but nothing too spectacular.
As a conclusion to this class, we'll consider a different example: custom delay spikers. We could use the "Unlinking Variables" method to disable the Delay behavior, but in this example we'll instead bypass the Delay behavior entirely, to give an example about how that would work. (This is the technique you'd be using to make more advanced BPD edits.)
You know those spiker pistols, which take 1.8
or so seconds to detonate, meaning that enemies don't die when you want them to?
We can change that with BPD edits.
First off, for those reading alone, the BPD in question is GD_Weap_Pistol.Projectiles.Projectile_Spiker:BehaviorProviderDefinition_0
, and it looks like this:
The relevant bit on a graph for the BPD looks like this:
So, the problem in this BPD would be our friend Behavior_Delay
. Step one will be to get rid of that one. Behavior_Delay
is the 6th element, so it has index 5
in BehaviorData2
.
It's output link has ArrayIndexAndLength=65537
, which translates to an ArrayIndex
of 1
and a length
of 1
in ConsolidatedOutputLinkData
. Element 1
of ConsolidatedOutputLinkData
is (LinkIdAndLinkedBehavior=-16777214,ActivateDelay=0.000000)
, which is a LinkID
of -256
and a LinkedBehavior
of 2
, which is the ProjectileBehavior_Detonate
.
Next we see which of the other behaviors point towards Behavior_Delay
, by inspecting all of the elements of ConsolidatedOutputLinkData
, and see which have a LinkedBehavior
of 5
. The ones that have this are those on index 0
and index 2
, which are -16777211
and -16777211
, which both translate to -256
and 5
.
Then, the next question we wanna solve is, which behaviors link to a subarray that contains index 0
or 2
? Both of these are only used by a single behavior, which each only link to the delay behavior, namely ProjectileBehavior_Attach_6
and ProjectileBehavior_Attach_7
.
These behaviors are on index 4
and 7
of BehaviorData2
.
We're now ready to redirect these behaviors directly to ProjectileBehavior_Detonate
, by using the OutputLink
of Behavior_Delay
, which is 65537
.
This results in the following code:
set GD_Weap_Pistol.Projectiles.Projectile_Spiker:BehaviorProviderDefinition_0 BehaviorSequences[0].BehaviorData2[4].OutputLinks.ArrayIndexAndLength 65537
set GD_Weap_Pistol.Projectiles.Projectile_Spiker:BehaviorProviderDefinition_0 BehaviorSequences[0].BehaviorData2[7].OutputLinks.ArrayIndexAndLength 65537
Next, we need to add in our own delay, to replace the one we just removed. We do this by modifying the element with index 1
of ConsolidatedOutputLinkData
, the one that points to ProjectileBehavior_Detonate
and is used by both ProjectileBehavior_Attach
now.
set GD_Weap_Pistol.Projectiles.Projectile_Spiker:BehaviorProviderDefinition_0 BehaviorSequences[0].ConsolidatedOutputLinkData[1].ActivateDelay 0.345
And with that, we have custom-delay spikers.
Even with all this knowledge & stuff, it's very easy to make a mistake, do something the game doesn't like, or otherwise crash your game.
BPD's are sensitive bastards, so if you're gonna attempt to edit them, don't get mad if your game decides to crash on you, multiple times. Especially, since a core part of it is inherently still just trail and error.
And, that's basically it folks. Nothing too complicated, right?
If you made it this far, good job. If anything is unclear, try reading that part again, or ask a question in the discord.