-
Notifications
You must be signed in to change notification settings - Fork 2.2k
BPD classroom
Almost everything in here is courtesy of LightChaosMan so go say thanks, that said dont annoy him about this stuff since its complicated.
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.
BPDs govern 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 more things probably.
What does a BPD do?
- Well, it's all in the name, it provides behavior to whatever it is it is attached to. Some things that have just a single behavior, like splash damage, can use the Behavior_xxxxx directly.
If you need more than one Behavior to accomplish your goal, a BPD comes into play.
A BPD will combine multiple Behaviors into a single neat package, that works on it's own. It can be thought of as a different level of code inside the Borderlands code.
This is because you can make the separate Behaviors in a BPD affect one another.
For example, with amp shields, the BPD more or less translates to
If current shield == max shield
then damage shield
and play sound effect
All 3 code blocks above are separate Behaviors, and the latter two will only be executed if they get the okay sign from the first.
Like this, you can create complicated if-then-else things, with random chance, delay and whatnot in between, hence my comparison to a different level of code.
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, which is the one chaos used to teach myself how they work, the BPD for amp shields. GD_Shields.Skills.Impact_Shield_Skill:BehaviorProviderDefinition_0
Using a certain awesome tool, you can make it look somewhat organized, as shown here
Like in the example above, all BPDs contain an array BehaviorSequence
. This array will usually only contain a single entry, which is the actual BehaviorSequence
we're interested in.
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?
Well, you 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 110000000000000001
.
Since this is just two 16 bit numbers pushed together, we can split them by taking the rightmost 16 bits and whatever is leftover (since leading zer0's aren't shown).
This would turn 11 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.
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, 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, this being a great example, LinkIdAndLinkedBehavior=0
is actually 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
.
Starting with -16777214
, which translates to 1111111100000000 0000000000000010
, which translates to -256
and 2
. Similarly, -16777213
translates to -256
and 3
.
This would be the moment to tell that we're using so called 2's complement to convert from and to binary notation, which is what enables negative numbers.
To convert them yourself, you can use something like this.
Whenever converting to binary, always use 32 bit
options, since that's what BL2 uses to generate these numbers in the first place.
When converting back, leave in the leading zer0s, they are important for 2's complement.
As before, we disregard the (negative) linkIDs
, 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 the performs 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: