-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Borderlands 3 Item and Weapon Parts
Weapons and Items in Borderlands 3 are constructed a little differently than their BL2/TPS counterparts. BL2 and TPS had very distinct part slots/categories which were the same for all weapons, or the same for all shields, etc, whereas BL3 supports a much more dynamic system which allows for any number of part categories. This is how CoV guns have a separate category for their engine-starter, and Vladof guns have a separate category for their underbarrel attachment.
Additionally, for modding purposes at least, the part lists are all defined in one easy object, instead of potentially being split out across multiple objects (as they were for shields, for instance). The definitions for gear are also specified the same way regardless of whether it's a weapon or an item, which is nice from a parsing perspective.
Most of this is pretty straightforward and intuitive -- for the most part you can probably just start looking at the data and get a feel for what's there. A few of the interactions between the various objects may not be obvious, though, so it makes sense to go through it all anyway.
- Accessing this Data
- InventoryBalanceData Objects
- PartSet Objects
- Part Objects: Dependencies and Excluders
- Anointments / Generic Part List Attributes
- Manufacturers
- Summary / How Gear is Built
- Extracted Data
- How PartSet Data Is Turned Into Balances
- Wonderlands Expansion Objects
- Miscellaneous
The data that this page uses has been taken from JohnWickParse serializations of unpacked BL3 data. The wiki page on Accessing Borderlands 3 Data describes how to do the unpacking and get the serializations, in case you wanted to look through yourself. Fortunately, the objects that we need to look at for gear construction tend to serialize pretty well.
The "starting point" for digging into how gear is generated, and the main
object that you'd need to be editing to do modding-type activity on the
gear, is the InventoryBalanceData
object. This object contains the runtime parts
list for the weapon or item, as well as the categories which all the parts
are sorted into. This page will mostly be looking at data from the Lyuda
sniper rifle, whose Balance can be found at
/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/_Unique/Lyuda/Balance/Balance_VLA_SR_Lyuda
.
The main attribute that we're generally concerned with is
RuntimePartList
, which has a few sub-attributes of its own. One of them,
AllParts
, contains the full list of parts which are valid for the given
balance. For instance, the Lyuda's RuntimePartList.AllParts
list starts
out like this (though I've trimmed out some unnecessary info and simplified
the Weight
attribute):
[
{
"PartData": [
"Part_SR_VLA_Body",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body"
],
"Weight": 1,
},
{
"PartData": [
"Part_SR_VLA_Body_A",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body_A"
],
"Weight": 1,
},
{
"PartData": [
"Part_SR_VLA_Body_B",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body_B"
],
"Weight": 1,
},
{
"PartData": [
"Part_SR_VLA_Body_C",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body_C"
],
"Weight": 1,
},
{
"PartData": [
"Part_SR_VLA_Barrel_Lyuda",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/_Unique/Lyuda/Parts/Part_SR_VLA_Barrel_Lyuda"
],
"Weight": 1,
},
{
"PartData": [
"Part_SR_VLA_Barrel_03_C",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Barrel/Barrel_03/Part_SR_VLA_Barrel_03_C"
],
"Weight": 1,
},
...
]
So the possible parts include a standard Vladof body, three Vladof body augments/mods, the Lyuda barrel (which provides the Lyuda's special abilities, just like BL2/TPS barrels generally do), and one possible barrel augment/mod.
The RuntimePartList.AllParts
list itself doesn't actually contain any
information about how those parts should be grouped, though. We can look
at it as a human and see some obvious patterns, but the game itself needs
things laid out a bit more explicitly.
The structure which does that is also in RuntimePartList
, and is called
PartTypeTOC
. This Table of Contents attribute starts out like so:
[
{
"StartIndex": 0,
"NumParts": 1
},
{
"StartIndex": 1,
"NumParts": 3
},
{
"StartIndex": 4,
"NumParts": 1
},
{
"StartIndex": 5,
"NumParts": 1
},
...
]
Arrays/lists in BL3 start with an index of 0
, just like they did in
BL2/TPS. So that first StartIndex
/NumParts
pair is saying that
starting with the first part in the list, the category is comprised of a
single part. That'll be the Vladof body we mentioned above.
The next section starts at index 1
(so, the second entry in
RuntimePartList.AllParts
), and contains three total parts. So, those are
the body mods/augments that we mentioned above. Then so on down the list:
one barrel, one barrel mod, and so on.
So, now we've got a method to parse out the parts and find out what
groupings there are. However, the Balance object does not give us
information about how those parts are selected, such as how many parts are
allowed to be selected in the category, if the Weight
attributes should
be considered, etc. For that, we need to look at a PartSet
object.
In the Balance object, you'll see an attribute named PartSetData
, which
in the Lyuda's case points us to the object
/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/_Unique/Lyuda/Balance/PartSet_VLA_SR_Lyuda
.
This is the PartSet object which provides the extra information that we'll
need.
If we take a look at the JWP serialization for that object, one attribute
in particular stands out: ActorPartLists
. If we take a look at the
serialization for that object (again, with various things trimmed out and
simplified for clarity's sake), it looks like this:
[
{
"PartType": 0,
"bCanSelectMultipleParts": false,
"bUseWeightWithMultiplePartSelection": false,
"MultiplePartSelectionRange": { "Min": 0, "Max": 0 },
"bEnabled": true,
"Parts": [
{
"PartData": [
"Part_SR_VLA_Body",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body"
],
"Weight": 1
}
]
},
{
"PartType": 1,
"bCanSelectMultipleParts": true,
"bUseWeightWithMultiplePartSelection": false,
"MultiplePartSelectionRange": { "Min": 3, "Max": 3 },
"bEnabled": true,
"Parts": [
{
"PartData": [
"Part_SR_VLA_Body_A",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body_A"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Body_B",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body_B"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Body_C",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Body/Part_SR_VLA_Body_C"
],
"Weight": 1
}
]
},
{
"PartType": 2,
"bCanSelectMultipleParts": false,
"bUseWeightWithMultiplePartSelection": false,
"MultiplePartSelectionRange": { "Min": 0, "Max": 0 },
"bEnabled": true,
"Parts": [
{
"PartData": [
"Part_SR_VLA_Barrel_Lyuda",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/_Unique/Lyuda/Parts/Part_SR_VLA_Barrel_Lyuda"
],
"Weight": 1
}
]
},
{
"PartType": 3,
"bCanSelectMultipleParts": true,
"bUseWeightWithMultiplePartSelection": false,
"MultiplePartSelectionRange": { "Min": 2, "Max": 2 },
"bEnabled": true,
"Parts": [
{
"PartData": [
"Part_SR_VLA_Barrel_03_C",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Barrel/Barrel_03/Part_SR_VLA_Barrel_03_C"
],
"Weight": 1
}
]
},
...
]
A few things stand out right away. One is that it looks like this is a
more convenient way to look at the valid parts: they're already arranged
into groups in a nice convenient way. Unfortunately, the actual parts
listed inside the PartSet
object are basically ignored by the game by
the time we can look at them in-game.
From a technical perspective, what happens when the game loads these objects is
that the PartSet
is used to dynamically generate the RuntimePartList
attribute of the Balance. The on-disk versions of the RuntimePartList
arrays
happen to match the results of this process in 99% of cases, so they can
generally be trusted. By the time we have any access to the objects ingame
(such as via getall
on the console, or with hotfix modding), this dynamic
generation has already taken place, so altering the Parts
lists inside
the PartSet
object doesn't actually accomplish anything -- at that point,
you've got to take a look at RuntimePartList
on the Balance instead. See
below for some details on the generation process, if you're interested,
because it jumps through a few hoops to do so.
The other thing that probably stands out is that the PartSet
does
contain all the extra informaion that we'd need about the groupings. For
instance, the Body and Barrel categories both specify a
bCanSelectMultipleParts
of False
, meaning that only one part can be
selected from that group. (In this case, there's only one part in the
category anyway, but for other guns/categories there could be more.)
Additionally, all the categories shown above have
bUseWeightWithMultiplePartSelection
set to False
, meaning that the
weights specified on the parts are completely ignored when selecting
multiple parts. In the Lyuda's case this wouldn't matter anyway, since
everything has a weight of 1
, but in some cases it could be important.
Remember that when the part weights are used, they're using the weight
found in the Balance
, not the PartSet
. The PartSet
part lists
are always ignored by the game.
Next up, there's MultiplePartSelectionRange
, which tells the game how
many parts from the category can be selected. Note that the same part
cannot be chosen twice out of the pool. So even though the barrel mod
category says that there should be exactly 2 parts chosen from the
category, the gun will still only receive one Part_SR_VLA_Barrel_03_C
.
Likewise, the Lyuda will always receive one of each body mod/augment,
because the MultiplePartSelectionRange
specifies that there must be
exactly 3 parts, and there are only 3 parts in that parts list. If
bCanSelectMultipleParts
is False
for a category, then this
MultiplePartSelectionRange
is ignored.
Items which do show up as having more than one of the same part while in-game (such as grenade mods and shields) do this by actually specifying the same parts more than once in the category. So a shield which has three "Brimming" aguments actually had that attribute three times in the part pool.
You may notice the bEnabled
flag in there as well, but that flag is only
used by the engine while the PartSet data is getting transformed into the
Balance's RuntimePartList
attribute, as the objects get loaded. Changing
this value with hotfix modding or the like won't actually accomplish anything,
since that process has already finished by that time.
So, we nearly know everything we need to know about how gear is constructed, but there's one more wrinkle, in the part objects themselves.
The final wrinkle to gear creation is that each Part
object might specify
some additional requirements in order to be added (or not added) to a
weapon or item. As one example, we can look down to the Lyuda's Scope
Accessory category, which has six total options in it. The entry in the
PartSet
looks like this (the parts listed in the Balance
are identical,
so we're just showing the PartSet
for simplicity's sake):
{
"PartType": 8,
"bCanSelectMultipleParts": true,
"bUseWeightWithMultiplePartSelection": false,
"MultiplePartSelectionRange": { "Min": 2, "Max": 2 },
"bEnabled": true,
"Parts": [
{
"PartData": [
"Part_SR_VLA_Scope_01_A",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_01/Part_SR_VLA_Scope_01_A"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Scope_01_B",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_01/Part_SR_VLA_Scope_01_B"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Scope_02_A",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_02/Part_SR_VLA_Scope_02_A"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Scope_02_B",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_02/Part_SR_VLA_Scope_02_B"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Scope_03_A",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_03/Part_SR_VLA_Scope_03_A"
],
"Weight": 1
},
{
"PartData": [
"Part_SR_VLA_Scope_03_B",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_03/Part_SR_VLA_Scope_03_B"
],
"Weight": 1
}
]
},
At first glance, it would look like there's lots of possible combinations
there. The category specifies exactly two parts, and there's six total,
which would lead to fifteen total combinations of parts. However, if we
look at the serialization for, say, Part_SR_VLA_Scope_01_A
, we'll see
this among its attributes:
"Dependencies": [
[
"Part_SR_VLA_Scope_01",
"/Game/Gear/Weapons/SniperRifles/Vladof/_Shared/_Design/Parts/Scope/Scope_01/Part_SR_VLA_Scope_01"
]
],
This means that Part_SR_VLA_Scope_01_A
will only ever spawn if the gun
already has the part Part_SR_VLA_Scope_01
. The other scope accessory
objects call have the same situation: accessories with 01
require the
01
scope, accessories with 02
require the 02
scope, and so on. So
really, for each scope that the Lyuda can spawn with, there's only one
possible combination of scope accessories, since there are two per scope,
and the ActorPartLists
object wants exactly two. This is and extremely
common scenario which you'll see pretty frequently.
The other attribute which might show up in a Part
object is Excluders
,
which does the opposite: if a part in its Excluders
list is already
assigned to the weapon/item, then that part will not be a valid part on
that bit of gear. The Lyuda technically has one part which does, this.
Specifically, its single barrel accessory Part_SR_VLA_Barrel_03_C
has the
following three parts in its Excluders
list:
Part_SR_VLA_Barrel_01
Part_SR_VLA_Barrel_02
Part_SR_VLA_Barrel_ETech
Those Excluders
will never come into play on the Lyuda itself, because
the only valid barrel for a Lyuda is Part_SR_VLA_Barrel_Lyuda
. This does
bring up a point worth mentioning, though: Dependencies
and Excluders
are defined on the parts themselves, and the parts might show up in many
different guns. So the Dependencies
and Excluders
you see on a part
may not ever apply on a specific gun, as with this example.
Anointments on gear don't show up anywhere in the attributes we've talked
about already. Instead, those are defined in a special attribute on the
Balance
object named RuntimeGenericPartList
. That will basically have
a big ol' PartList
sub-attribute which is a list of all the anointments
which can exist on the weapon/item. The PartSet
object also has a
GenericParts
attribute which often contains the anointments as well, but
as with the standard gun/item parts, only the Balance
part definitions
matter here. The first part in this structure is generally always
Att_EndGame_NoneChanceGuns
, which is the part which means that the item
will have no anointment.
Weights for the anointments are somewhat interesting. All generic
(non-character-specific) anointments have a weight of 1
, and all
character-specific anointments have a weight of 0.15
. When a character
joins the game, though (such as the character you're playing, or when a
co-op partner joins), the character-specific anointment weight matching
that character goes up by 0.85
. So if you're playing a solo Siren, the
Siren-specific anointments will be as likely to spawn as a generic
anointment, and the other characters' anointments will be less likely. If
two Sirens are present in the game, I believe the weight of the Siren
anoints will be 1.85
, so they'll be more likely than the generic ones.
The weights of the no-anointment part has been in flux for awhile now --
current hotfixed valuse set them pretty low: from 2.3
when playing
without Mayhem mode, to 0.5
when playing in Mayhem 3 or 4. Those values
seem likely to change again at some point in the future, so don't take them
as gospel.
As BL3 gets patched, Gearbox sometimes adds in more anointments, like they
did during the Bloody Harvest event, or the new anointments added by the
Maliwan Takedown / Mayhem 4 update. These are all defined in
GPartExpansion
objects. We won't go into great detail about them here,
other than to say that they specify more anointments and get applied to
nearly all gear which is ordinarily able to have anointments. (There are a
few exceptions -- in order to trace those out you'd have to loop through
the InventoryBalanceCollection
and ParentCollection
links in the
expansion objects, and make a note of any gear not found via that
method.)
The expansion objects currently in-use are:
/Game/PatchDLC/Raid1/Gear/_GearExtension/GParts/GPartExpansion_Weapons_Raid1
/Game/PatchDLC/Raid1/Gear/_GearExtension/GParts/GPartExpansion_Shields_Raid1
/Game/PatchDLC/Raid1/Gear/_GearExtension/GParts/GPartExpansion_Grenades_Raid1
The anointments added by the Revenge of the Cartels event were made permanent additions by Gearbox, so the following expansion objects are also in-use:
/Game/PatchDLC/Event2/Gear/_Design/_GearExtension/GParts/GPartExpansion_Weapons_Event2
/Game/PatchDLC/Event2/Gear/_Design/_GearExtension/GParts/GPartExpansion_Shields_Event2
/Game/PatchDLC/Event2/Gear/_Design/_GearExtension/GParts/GPartExpansion_Grenades_Event2
The Bloody Harvest expansions (which are only active while the event is active) are:
/Game/PatchDLC/BloodyHarvest/Gear/_Design/_GearExtension/GParts/GPartExpansion_Weapons_BloodyHarvest
/Game/PatchDLC/BloodyHarvest/Gear/_Design/_GearExtension/GParts/GPartExpansion_Shields_BloodyHarvest
/Game/PatchDLC/BloodyHarvest/Gear/_Design/_GearExtension/GParts/GPartExpansion_Grenades_BloodyHarvest
There are similar objects for each released story DLC (Moxxi's Heist; Guns, Love, and Tentacles; Bounty of Blood), but they don't actually add any new anointment parts, so they can be safely ignored.
The Balance
object also has a Manufacturers
attribute. For nearly all
gear, this will just contain a single manufacturer, but grenade mods can
sometimes spawn in a variety of manufacturers. This appears to be just a
straightforward weight-based pool, so not much more needs to be said about
that.
So, after all that, here's a more streamlined and general version of how the game engine builds gear. The order of the main steps might not be accurate, of course, and it's probably a bit more streamlined than I've written down here. (It probably just parses the structures as it goes along, for instance, rather than doing it first.)
- Pick a manufacturer from the
Balance
'sManufacturers
list. - Parse the
Balance
'sRuntimePartList.PartTypeTOC
structure, in conjunction withRuntimePartList.AllParts
, to know what part categories are available. - Associate those categories with the extra category information found in
the
PartSetData
object - Take a look at the first category, and trim out the parts whose
Excluders
andDependencies
aren't valid currently. - Then use the
PartSet
parameters to pick however many parts from the category are required. - Repeat starting at step 4 for each remaining part category. (So Barrel Augments will always end up being a category after Barrels, so that their dependencies can be processed.)
- Choose a part from the
Balance
'sRuntimeGenericPartList
; this will be the anointment.
There's a couple Google Sheets out there which have made use of all this to programmatically extract a bunch of data from the game. These should include the last-updated-date in both the sheet name, and a changelog on their main sheet, so you can tell if they've been updated for recent patches or not.
As mentioned above, in nearly all cases, you can look at the on-disk
JWP serializations of the InventoryBalanceData
objects, specifically in
the RuntimePartList
attribute, to know what parts can spawn on a gun.
That attribute is technically reconstructed at runtime as the objects
are loaded into the game, though, and there are a couple of cases where
the on-disk JWP serializations don't match what the game actually uses.
Specifically:
- On the June 25, 2020 update (with the third story DLC, Bounty of Blood), Gearbox added some new parts for Class Mods which buff up Action Skill Damage. These parts will not show up in the disk serializations of the Balances.
- It turns out that the Balance references to a couple of Artifact parts
are wrong. Specifically, some of them reference
Artifact_Part_Stats_FireDamage
and/orArtifact_Part_Stats_CryoDamage
, but the actual object name for those both have a_2
suffix at the end (Artifact_Part_Stats_FireDamage_2
, for instance).
As of July 16, 2020, those are the only differences we're aware of, but
if you're looking to programmatically look at part data, you might need to
know how to construct the "proper" part lists out of the PartSet
objects,
just like BL3 does when loading the objects. As before, altering the
part lists found in the PartSet
objects is pointless, since the engine
has already done the translation, but it may help to know anyway. (The
spreadsheets listed above this section needed to know this to accurately
report on the parts, for instance.)
So far, we've only been looking at a single InventoryBalanceData
object,
which points us at a single PartSet
object. In reality, there's a bit
of a tree structure in place, which is important if you're trying to
replicate the RuntimePartList
construction behavior. Specifically, the
InventoryBalanceData
is likely to have a BaseSelectionData
attribute
which will point you at another InventoryBalanceData
object. This can
chain multiple times, though I don't think the game ever goes beyond three
total Balance objects. Weapons, in general, don't seem to do this very
often, but other item types do. For instance, if we take a look at the
Back Ham shield Balance, we'll find that it "chains" to one additional
Balance. The two will be:
/Game/Gear/Shields/_Design/_Uniques/BackHam/Balance/InvBalD_Shield_BackHam
/Game/Gear/Shields/_Design/InvBalance/InvBalD_Shield_Anshin
Legendary class mods will often be three deep, such as the Phasezerker Balance, which looks like this:
/Game/PatchDLC/Raid1/Gear/ClassMods/Siren/InvBalD_ClassMod_Siren_Phasezerker
/Game/Gear/ClassMods/_Design/BalanceDefs/InvBalD_ClassMod_Siren
/Game/Gear/ClassMods/_Design/BalanceDefs/InvBalD_ClassMod
As in the beginning, when we associated a Balance object to its PartSet,
each of these balances will have a PartSetData
attribute which points at
a PartSet. For the Back Ham, they'd be:
/Game/Gear/Shields/_Design/_Uniques/BackHam/Balance/PartSet_Shield_BackHam
/Game/Gear/Shields/_Design/PartSets/PartSet_Shield_Anshin
Or for that Phasezerker COM, we'd be looking at:
/Game/PatchDLC/Raid1/Gear/ClassMods/Siren/PartSet_ClassMod_Siren_Phasezerker
/Game/Gear/ClassMods/_Design/PartSets/PartSet_ClassMod_Siren
/Game/Gear/ClassMods/_Design/PartSets/PartSet_ClassMod
Now that we've got a list of all the PartSet objects which are used to
construct the Balance's eventual RuntimePartList
attribute, we've got to
loop through them and process their ActorPartLists
attributes. We'd do
so by starting at the "bottom" PartSet and moving up. So for instance on
the Phasezerker COM, we'd start with PartSet_ClassMod
first, then
PartSet_ClassMod_Siren
, and finally PartSet_ClassMod_Siren_Phasezerker
.
One important attribute to look at first is ActorPartReplacementMode
, a
top-level attribute inside the PartSet. That will be one of three values,
which determines how exactly to process the ActorPartLists
array:
-
Complete - In this case, the PartSet completely defines the set of
parts, and will totally overwrite any prior PartSets which have been
processed before this point. If an
ActorPartLists
entry hasbEnabled
ofFalse
at this point, that part category will just be an empty list which won't have parts in it. -
Selective - In this case, each
ActorPartLists
entry will overwrite previously-seen categories, but only if itsbEnabled
attribute isTrue
. If a category'sbEnabled
isFalse
at this point, any previous parts listed in that category will remain in place. -
Additive - In this case, if an
ActorPartLists
entry'sbEnabled
isTrue
, any parts specified will be added to any previous parts lists defined in this category.
So, knowing the mode, you'd start at the "bottom" PartSet object and
start adding parts to the part categories. Then take the next PartSet and
alter your part category lists as instructed by the ActorPartReplacementMode
.
And just keep going until you've done the "final" PartSet.
Keep in mind that the part weights are part of this as well, so a Selective PartSet might change the weight of a previously-defined part in its category.
Once you go through this process of looping through all relevant PartSets,
you'll have the collection of parts which the game will put into the Balance's
RuntimePartList
attribute (dynamically constructing the PartTypeTOC
in
addition to the AllParts
list). As mentioned above, currently there's only
a few cases where this process will give you different data than just looking
at the cached RuntimePartList
attributes on-disk, but if you want to be
100% sure you've got the right parts, just from the on-disk data, you'll have
to do this processing.
You might wonder, after going through the PartSet-to-Balance conversion process,
about the other ActorPartLists
attributes like bCanSelectMultipleParts
,
bUseWeightWithMultiplePartSelection
, and MultiplePartSelectionRange
, and if
the game does similar processing to find those. Fortunately, the game only
appears to look at the "top" level PartSet when looking for those attributes.
So for that Phasezerker COM, even though there are three PartSet objects involved
in the construction of the Balance's RuntimePartList
, only the top-level
InvBalD_ClassMod_Siren_Phasezerker
object is used by the game to determine
how the multi-part selection is processed.
Unrelatedly, the Artifact_Part_Stats_FireDamage_2
and Artifact_Part_Stats_CryoDamage_2
artifact attributes mentioned above continue to be a little bit weird, even
after going through this process. When looking at Artifacts' cached (on-disk)
RuntimePartList
attributes, they will basically all omit the _2
suffixes,
so they'll be technically incorrect. If you go through this PartSet-to-Balance
construction process, nearly all of those errors will be fixed, except for
two artifacts: Unleash the Dragon and Phoenix Tears. Something inside the game
engine "fixes" those dynamically, though, to include the _2
suffix. So that's
one hardcoded fix you'll have to remember to make.
Tiny Tina's Wonderlands introduces one further wrinkle to the gear-construction
system, namely InventoryPartSetExpansionData
and InventoryExcludersExpansionData
objects. These were introduced with DLC4 (Shattering Spectreglass), to handle
the new parts required to support the Blightcaller class (specifically for Armor
and Amulets).
These expansion objects aren't actually linked-to by anything; the engine must just
know to load them and process them dynamically, which means that anyone looking
through game data for Balance/PartSet info just has to be aware that they exist.
As of August 2022, all known examples of these objects start with EXPD_
in their
object name, and live under one of these four paths:
/Game/PatchDLC/Indigo4/Gear/_Design/Amulets/_Shared/_Design/PartSet/ExpansionData
/Game/PatchDLC/Indigo4/Gear/Pauldrons/_Shared/_Design/Parts/Passive/Other/Skills
/Game/PatchDLC/Indigo4/Gear/Pauldrons/_Shared/_Design/Parts/PlayerStat/Other
/Game/PatchDLC/Indigo4/Gear/Pauldrons/_Shared/_Design/PartSet/ExpansionData
There are also some ItemPoolExpansionData
objects which are prefixed by EXPD_
,
which have been used across all Wonderlands DLC to expand itempools, but those
obviously don't have any bearing on gear construction.
These objects are straightforward enough -- they simply add more Dependencies or
Excluders to a specified set of parts. These expansion objects will have an array
named TargetParts
which defines which parts the expansion applies to, and then
a Dependencies
and/or Excluders
attribute which lists the extra constraints.
As mentioned above, there's no link from the Balance/PartSet/Part over to these
expansion objects, so you'll just have to look through them to find out if any
of them apply.
These are objects which alter the part selection for gear, and they end up having
a pretty noticeable change on gear definitions, for modders. They're basically
used to add in the necessary Blightcaller parts to existing armor and amulets.
As with InventoryExcludersExpansionData
objects, there isn't a link from the
Balance/PartSet over to these expansion objects, so you'll just have to look through
them all to know whether one applies or not.
The objects themselves look fairly innocuous -- there's a top-level InventoryPartSet
attribute which defines which PartSet the expansion acts on, and then a PartLists
structure which is identical to the ActorPartLists
structure found in regular
PartSet objects. Inside each of the categories is a Parts
array which might define
more parts to be added in to that category. The structure does include all the other
usual ActorPartLists
attributes like bCanSelectMultipleParts
, MultiplePartSelectionRange
,
etc, but those appear to be ignored by the game. Some attributes like PartTypeEnum
and PartType
might still be required for the structure to work properly, but the only
one that's important to look at for our purposes is Parts
.
The main wrinkle that these introduce is that for objects with InventoryPartSetExpansionData
expansions, the Balance
itself ends up being useless for hotfix modding (as opposed
to ordinarily, where the Balance
is the only thing useful for hotfix modding, for
part lists). Basically, the early object-loading workflow ends up looking like this:
- Objects get loaded by the game, and the PartSet's
ActorPartLists
struct gets processed over to the Balance'sRuntimePartList
struct as usual. - Hotfixes are processed
-
InventoryPartSetExpansionData
objects get processed, which has the following effects:- The PartSet's
ActorPartLists[x].Parts
structures are merged with the expansion object'sPartLists[x].Parts
into the a newActorPartLists[x].RuntimeParts
on the PartSet itself - The Balance's
RuntimePartList
might be updated as well, again, but that sort of doesn't matter anymore.
- The PartSet's
- When gear is dropped, if
ActorPartLists[x].RuntimeParts
exists on the PartSet, the Balance is completely ignored for part-picking purposes, and only the PartSet'sActorpartLists[x].RuntimeParts
is used. (IfRuntimeParts
is absent, then the Balance is used for part lists, as described above.)
It's actually a more sensible approach than the usual method -- this way, all the decisions about which parts to spawn on a bit of gear come from the PartSet, instead of splitting it up between the Balance (for the part list) and PartSet (for all the "meta" params about how to pick the parts). It does lead for a somewhat frustrating situation where the behavior depends entirely on whether or not one of these expansion objects is in effect, though, and there's no way to really know that without looping through all available expansion objects to find out if any apply.
In terms of modding, for InventoryPartSetExpansionData
-expanded gear, you can
use hotfixes on both the PartSet and InventoryPartSetExpansionData objects
themselves without problems, due to the order of operations when applying
hotfixes. So the Balance
object can be completely ignored for these. One
potential wrinkle is that you might have to set the entire ActorPartLists
structure rather than cherry-picking individual components. Some testing
indicates that drilling in too far might lead to the changes not applying
properly.
One final point of interest here: this exact same system is what's used
to spawn enemy vehicles in maps, though the actual object attributes are
sometimes differently-named. But, for instance, SpawnOptions_CotV_Outrunner
has an associated Outrunner_VehiclePartSet_Enemy_COTV
, and the relationship
between the SpawnOptions' eventual RuntimePartList
attribute and the
PartSet ActorPartLists
attribute is just the same as it is for items.