-
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_01Part_SR_VLA_Barrel_02Part_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'sManufacturerslist. - Parse the
Balance'sRuntimePartList.PartTypeTOCstructure, in conjunction withRuntimePartList.AllParts, to know what part categories are available. - Associate those categories with the extra category information found in
the
PartSetDataobject - Take a look at the first category, and trim out the parts whose
ExcludersandDependenciesaren't valid currently. - Then use the
PartSetparameters 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_FireDamageand/orArtifact_Part_Stats_CryoDamage, but the actual object name for those both have a_2suffix 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
ActorPartListsentry hasbEnabledofFalseat this point, that part category will just be an empty list which won't have parts in it. -
Selective - In this case, each
ActorPartListsentry will overwrite previously-seen categories, but only if itsbEnabledattribute isTrue. If a category'sbEnabledisFalseat this point, any previous parts listed in that category will remain in place. -
Additive - In this case, if an
ActorPartListsentry'sbEnabledisTrue, 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
ActorPartListsstruct gets processed over to the Balance'sRuntimePartListstruct as usual. - Hotfixes are processed
-
InventoryPartSetExpansionDataobjects get processed, which has the following effects:- The PartSet's
ActorPartLists[x].Partsstructures are merged with the expansion object'sPartLists[x].Partsinto the a newActorPartLists[x].RuntimePartson the PartSet itself - The Balance's
RuntimePartListmight be updated as well, again, but that sort of doesn't matter anymore.
- The PartSet's
- When gear is dropped, if
ActorPartLists[x].RuntimePartsexists on the PartSet, the Balance is completely ignored for part-picking purposes, and only the PartSet'sActorpartLists[x].RuntimePartsis used. (IfRuntimePartsis 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.