Understanding Item and Weapon Parts Definitions - BLCM/BLCMods GitHub Wiki

Understanding the structures which define item/weapon parts isn't terribly difficult, though there's one or two wrinkles which make it not entirely straightfoward. Before you continue, it's probably best to be at least somewhat familiar with the BaseValueConstant/BaseValueAttribute/InitializationDefinition/BaseValueScaleConstant tuple described in Understanding Borderlands Weight and Probability Values.

The Short Version

This page goes into perhaps agonizing detail about item/weapon parts. If you just want the short version, here goes:

  1. Whenever you see a parts list variable named like *Index, the number it's actually looking for is in the ConsolidatedAttributeInitData at the bottom of the object dump, not the actual number you see right there.
  2. See the Manufacturers Summary table for how the Manufacturers field impacts what weight is used when spawning parts.

See, could've saved myself some typing, probably. On to the wordy bits!

Where to Find Part Definitions

Weapons

Weapons and items both define their parts using very similar parts lists objects. For weapons, these can be found by looking at the WeaponBalanceDefinition for the weapon in question. For instance, purple-rarity Hyperion SMGs (GD_Weap_SMG.A_Weapons.SMG_Hyperion_4_VeryRare) have this right at the top of that object:

WeaponPartListCollection=WeaponPartListCollectionDefinition'GD_Weap_SMG.A_Weapons.SMG_Hyperion_4_VeryRare:PartList'
RuntimePartListCollection=WeaponPartListCollectionDefinition'GD_Weap_SMG.A_Weapons.SMG_Hyperion_4_VeryRare:WeaponPartListCollectionDefinition_160'

In general, for weapons, you're only going to be interested in the RuntimePartListCollection object. So in this case, you'd want to look at WeaponPartListCollectionDefinition_160 and not worry about PartList. (Though sometimes the two will point to the same place.)

Shields

For items, it can depend on the item in question. For shields, it'll be found in the ShieldDefinition for the shield, for instance on Booster shields (GD_Shields.A_Item.Shield_Booster) there's this:

AlphaParts=ItemPartListDefinition'GD_Shields.Body.PartsList_Body_Booster'
BetaParts=ItemPartListDefinition'GD_Shields.Battery.PartsList_Battery_Standard'
GammaParts=ItemPartListDefinition'GD_Shields.Capacitor.PartsList_Capacitor_Standard'
DeltaParts=ItemPartListDefinition'GD_Shields.Accessory.PartsList_Accessory_Booster'
MaterialParts=ItemPartListDefinition'GD_Shields.Material.PartsList_Materials_Booster'

Grenades and Relics

For grenade mods and relics, it's found in the InventoryBalanceDefinition. For instance, blue-rarity MIRV grenades (GD_GrenadeMods.A_Item.GM_Mirv_3_Rare) have this:

PartListCollection=ItemPartListCollectionDefinition'GD_GrenadeMods.PartLists.Parts_Mirv_3_Rare'

Class Mods

For class mods, they're found in ClassModDefinition objects. For instance, Siren Cat COMs (GD_ClassMods.A_Item_Siren.ClassMod_Siren_Cat) have these:

AlphaParts=ItemPartListDefinition'GD_ClassMods.Specialization.Z_PartsList_SpecializationParts'
BetaParts=ItemPartListDefinition'GD_ClassMods.StatPrimary.Z_PartsList_PrimaryStatParts'
GammaParts=ItemPartListDefinition'GD_ClassMods.StatPrimary02.Z_PartsList_PrimaryStat02Parts'
MaterialParts=ItemPartListDefinition'GD_ClassMods.StatPenalty.Z_PartsList_StatPenaltyParts'

How To Read Part Data

Let's take white-rarity Hyperion SMGs as an example, whose runtime partlist is defined in the object GD_Weap_SMG.A_Weapons.SMG_Hyperion:PartList.

The main body of the object looks pretty straightforward. There's only one possible body for the weapon, as defined here:

BodyPartData=( 
    bEnabled=True, 
    WeightedParts=( 
        ( 
            Part=WeaponPartDefinition'GD_Weap_SMG.Body.SMG_Body_Hyperion', 
            Manufacturers=, 
            MinGameStageIndex=0, 
            MaxGameStageIndex=1, 
            DefaultWeightIndex=2 
        ) 
    ) 
)

Whereas there are three possible barrels:

BarrelPartData=( 
    bEnabled=True, 
    WeightedParts=( 
        ( 
            Part=WeaponPartDefinition'GD_Weap_SMG.Barrel.SMG_Barrel_Tediore', 
            Manufacturers=, 
            MinGameStageIndex=0, 
            MaxGameStageIndex=1, 
            DefaultWeightIndex=2 
        ), 
        ( 
            Part=WeaponPartDefinition'GD_Weap_SMG.Barrel.SMG_Barrel_Bandit', 
            Manufacturers=, 
            MinGameStageIndex=0, 
            MaxGameStageIndex=1, 
            DefaultWeightIndex=2 
        ), 
        ( 
            Part=WeaponPartDefinition'GD_Weap_SMG.Barrel.SMG_Barrel_Maliwan', 
            Manufacturers=, 
            MinGameStageIndex=0, 
            MaxGameStageIndex=1, 
            DefaultWeightIndex=2 
        ) 
    ) 
)

Likewise, there's five possible grips, one option for sights (specifically not having one), five stocks, and nothing of anything else.

MinGameStageIndex defines the minimum "game stage" where that part is allowed to spawn (which will roughly coincide with the level of the missions you're running in-game), whereas MaxGameStageIndex defines the maximum. DefaultWeightIndex determines the "weight" of the part (as described in Understanding Borderlands Weight and Probability Values), so some parts can be more or less likely than others.

Those numbers given by the *Index variables don't make a lot of sense on their own, though. If the Min/Max stage for all those parts is 0 and 1, how do any of them actually spawn at all? The answer is that Borderlands uses a separate lookup table, found further down in the object dump, to get the real numbers.

ConsolidatedAttributeInitData

Specifically, the actual numbers are found in the ConsolidatedAttributeInitData near the bottom of the dump. Using that white-level Hyperion SMG object as an example again, that whole array looks like this:

ConsolidatedAttributeInitData(0)=( 
    BaseValueConstant=1.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(1)=( 
    BaseValueConstant=100.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(2)=( 
    BaseValueConstant=0.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)

We'll refer to that as CAID from now on... So in this case, CAID[0] evaluates to 1, CAID[1] evaluates to 100, and CAID[2] evaluates to 0.

So when all those parts up above were referring to MinGameStageIndex=0 and MaxGameStageIndex=1, to define the min/max levels at which those parts are allowed to spawn, they were actually referring to CAID[0] and CAID[1], which is 1 and 100. So those parts can start spawning at level one, and will continue to work fine through level 100.

(Incidentally, this is one reason why c0dycode's Hex Multitool doesn't recommend setting the maximum game level to more than 92. At Level 92 OP8, enemies will be at level 100, and level 100 is basically always the maximum spawn level for weapon parts. Level 101 enemies wouldn't have any valid guns to use, or drop!)

The CAID[2] value of 0 is a bit weird, since that's what's used for the DefaultWeightIndex in our example, but we'll get to that in the "Advanced" section. For now, just keep in mind that whenever you see Index in a variable name in a parts list, it's referring to the CAID array at the bottom of the object dump.

A Slightly More Complex Example

Let's take a look at the parts for purple-rarity Hyperion SMGs, in the object GD_Weap_SMG.A_Weapons.SMG_Hyperion_4_VeryRare:WeaponPartListCollectionDefinition_160. We won't paste the entire structure here, and we're omitting a couple of things for clarity, but the first few elements of its ElementalPartData array look like this:

( 
	Part=WeaponPartDefinition'GD_Weap_SMG.elemental.SMG_Elemental_None', 
	...
	MinGameStageIndex=0, 
	MaxGameStageIndex=1, 
	DefaultWeightIndex=3 
), 
( 
	Part=WeaponPartDefinition'GD_Weap_SMG.elemental.SMG_Elemental_Fire', 
	...
	MinGameStageIndex=4, 
	MaxGameStageIndex=1, 
	DefaultWeightIndex=5 
), 
( 
	Part=WeaponPartDefinition'GD_Weap_SMG.elemental.SMG_Elemental_Corrosive', 
	...
	MinGameStageIndex=6, 
	MaxGameStageIndex=1, 
	DefaultWeightIndex=5 
),
...

Looking at the CAID structure at the bottom, the relevant entries are:

ConsolidatedAttributeInitData(0)=( 
    BaseValueConstant=1.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(1)=( 
    BaseValueConstant=100.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(3)=( 
    BaseValueConstant=0.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=AttributeInitializationDefinition'GD_Balance.Weighting.Weight_1_Common', 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(4)=( 
    BaseValueConstant=7.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(5)=( 
    BaseValueConstant=0.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=AttributeInitializationDefinition'GD_Balance.Weighting.Weight_2_Uncommon', 
    BaseValueScaleConstant=1.000000 
)
ConsolidatedAttributeInitData(6)=( 
    BaseValueConstant=13.000000, 
    BaseValueAttribute=None, 
    InitializationDefinition=None, 
    BaseValueScaleConstant=1.000000 
)

So starting with the min/max game stages, all the elemental choices will spawn up to level 100, as we'd expect. The Minimum value changes depending on the element, though. At level 1 through 6, the only possibility would be no element at all. Once you hit level 7, though, (via CAID[4]), Fire becomes a possibility. Then once you get to level 13 (via CAID[6]), corrosive is available as well. This mechanism is most often used to lock elemental types to levels.

The other interesting thing is that the "No Element" option has a weight of Common (via CAID[3]), and the other elements have a weight of Uncommon (via CAID[5]). "Common" evaluates to 100, and "Uncommon" evaluates to 10. The other two elements we didn't paste here also use Uncommon, so in this case, purple Hyperion SMGs will have a 71% chance of spawning without an element, and 7.1% chance of spawning in each of the other elements.

So keep the CAID array in mind when looking at these objects, and you should be mostly fine!

Advanced: Manufacturer Interactions

(Many thanks to LightChaosman for figuring out the specifics of this! The rest of us had just been sort of waving our hands at it for ages.)

The only other real thing of note which still needs mentioning is exactly how the game deals with the Manufacturers field. Remember the white-rarity Hyperion SMGs above, whose parts all had a CAID reference which ended up being 0? Well, it turns out that if a part's Manufacturers field is completely empty, the DefaultWeightIndex is ignored, and the part is assigned a weight of 100. So, rather than having weights of 0, which wouldn't make a lot of sense, the parts all have a weight of 100.

Things get a little trickier when there is a Manufacturer. The second case is when there is something in Manufacturers but the manufacturer is set to None. This is actually the case in the purple-rarity Hyperion SMG element list which we'd omitted above. The full definition for fire element on that gun is:

( 
	Part=WeaponPartDefinition'GD_Weap_SMG.elemental.SMG_Elemental_Fire', 
	Manufacturers=( 
		( 
			Manufacturer=None, 
			DefaultWeightIndex=1 
		) 
	), 
	MinGameStageIndex=4, 
	MaxGameStageIndex=1, 
	DefaultWeightIndex=5 
), 

In this case, the main DefaultWeightIndex is used (and the Manufacturer weight ignored), so what we said earlier about the purple Hyperion SMG elemental weights was not a lie. :)

Finally, there are cases where an actual Manufacturer is defined. In that case, the DefaultWeightIndex inside the Manufacturer index is used. This doesn't happen much (or even at all?) on weapons, but it's pretty common in things like grenade mods. For instance, the MIRV grenade part list (at GD_GrenadeMods.PartLists.Parts_Mirv) has this as its first delivery type part option (in BetaPartData):

( 
	Part=GrenadeModPartDefinition'GD_GrenadeMods.Delivery.Delivery_Lob', 
	Manufacturers=( 
		( 
			Manufacturer=ManufacturerDefinition'GD_Manufacturers.Manufacturers.Torgue', 
			DefaultWeightIndex=3 
		), 
		( 
			Manufacturer=ManufacturerDefinition'GD_Manufacturers.Manufacturers.Bandit', 
			DefaultWeightIndex=3 
		) 
	), 
	MinGameStageIndex=0, 
	MaxGameStageIndex=1, 
	DefaultWeightIndex=2 
), 

So there's actually two options for lobbed delivery in a MIRV grenade: Torgue and Bandit, and they each use a weight defined in CAID[3] instead of the weight defined in CAID[2].

Manufacturers Summary

Manufacturers State Result
No Manufacturers data at all Weight is 100
Has Manufacturers but Manufacturer is None Use CAID[DefaultWeightIndex] of the main part itself
Has actual Manufacturer data Use CAID[Manufacturer.DefaultWeightIndex] from the Manufacturers section

Sanity Check Considerations

When Borderlands loads a savegame, it checks all of that save's items/weapons against the defined parts lists using a "sanity check." If an item is found which has a part not defined in the parts list, the item will get silently deleted from the user's inventory. If you notice this and hit Alt-F4 (or otherwise force-quit the game) before it next autosaves, the item will remain in the savegame, but otherwise it'll end up getting lost.

So, when editing parts lists for weapons, it's good to make sure you don't inadvertently cause the sanity check to delete things you don't want it to. Like if you want to lock a gun to a specific element, don't just remove all the other element parts from the parts list -- doing so would make any existing guns in the other elements disappear. Instead, either play with the (Min|Max)GameStageIndex fields so that the weapon will never actually spawn, or if possible just adjust the weights of the other parts so that they'll never spawn that way.

Likewise, if you want to add parts to a gun (like if you wanted to add shock to the Volcano or something), keep in mind that if someone with a shock Volcano launches their game without having executed your mod first, that shock Volcano will end up getting sanity checked and deleted. You may want to make sure that's clear in your README or whatever.

Note that c0dycode's Hex Multitool can hex-edit BL2/TPS so that the sanity check is disabled, which will prevent users from running up against these kinds of issues.

Conclusion

That's it! Pretty straightforward, in the end, apart from some weirdness with the Manufacturers list. Feel free to hop into the classroom Discord channel if you've got any other questions.