Skip to content

Borderlands 3 Hotfix Modding

CJ Kucera edited this page Oct 26, 2022 · 17 revisions

This page is intended to go over some of the basics of working with Borderlands 3 and Wonderlands hotfixes for modding purposes. This will be written primarily from the perspective of someone who's already reasonably familiar with modding in BL2/TPS. Prior modding knowledge will serve you well, since a lot of it is fairly transferrable. People who have some knowledge of Unreal Engine 4 may have a leg up as well -- there are various components here which will probably be familiar to someone with UE4 experience. As always, clarifications and corrections to this wiki are welcomed, especially if we have any UE4 details wrong!

Getting Started

Here are some things which you should probably have available and be at least somewhat familiar with before you actually get going with writing BL3 hotfixes:

General Hotfix Info

In BL2/TPS, SparkPatchEntry hotfixes don't work at all due to the nature of BL2/TPS hotfixing, but you'll want to use them if doing hotfix modding in BL3/WL. These get applied as soon as the game loads in hotfixes, so they will basically be the equivalent of the BL2/TPS set command. (BL3's console doesn't have set, so that's the best we can do.) Using getall on the console can help you determine if an object is loaded or not.

If you want to trigger a hotfix reload, simply go out to the main menu, select "Quit," and then choose "Title Screen" instead of "Desktop." When the camera scrolls back down, a hotfix reload will be taking place in the background.

There are some noticeable differences in what data is available at what point during the game, compared to BL2 and TPS. This will affect what hotfix operation you'll use. For instance:

  • Unlike in BL2/TPS, you cannot specify a "global" or "wildcard" level-based hotfix. SparkLevelPatchEntry hotfixes which don't have a level name specified will simply never activate, so you may have to use a longish list of otherwise-identical hotfixes to achieve similar ends.
  • All character-specific modding statements seem to use SparkPatchEntry. The character data for all chars seems to be available right at the main menu, as opposed to having to use BL2/TPS's OnDemand hotfix type (which doesn't exist in BL3).
  • Hotfixes on enemy data generally has to be done with a SparkCharacterLoadedEntry hotfix, keyed off of the enemy's BPChar_* name. They're loaded on-demand as you progress through the level, so you can't generally use SparkLevelPatchEntry for them (and you won't even see the objects in the console until they've been loaded dynamically).
  • Vehicle data doesn't load until you actually spawn the vehicles in question. So far nobody's taken the time to track down exactly how to hotfix that data, but it seems possible that SparkStreamedPackageEntry could be used for that. So far, no Gearbox-provided hotfix uses SparkStreamedPackageEntry though, so it's hard to tell. Some vehicle-related objects tend to be loaded on level load, though, so they can be hotfixed that way (such as the parts lists for spawned vehicles).
  • Very often, other objects which are referenced by characters/vehicles/etc are only loaded when they are, or on the levels in which they appear. This can be rather surprising sometimes. For instance, the main ItemPoolList used by standard enemies -- /Game/GameData/Loot/ItemPools/ItemPoolList_StandardEnemyGunsandGear -- does not exist until those enemies are loaded. So if you want to edit that particular ItemPoolList, you need to use a series of otherwise-identical SparkCharacterLoadedEntry hotfixes keyed to every enemy you want to apply it to. You could alternatively use the special target MatchAll, instead of the actual BPChar_* name, to have a hotfix apply to all characters.
    • Other ItemPools and ItemPoolLists may be loaded all the time, though, so you'll have to check. For instance, basically all of the item pools referenced by that ItemPoolList_StandardEnemyGunsandGear object will exist right at the main menu, even if the ItemPoolList itself doesn't. Once again, using getall from the console helps to figure that out.
  • If you're looking to do map tweaks, remember that there are two types of level hotfixes: SparkLevelPatchEntry and SparkEarlyLevelPatchEntry. Some of the statements required to alter vehicle-spawning parts have to use the Early version in order to work. I'd recommend trying the non-Early hotfix first, though, when testing out functionality.

Hotfix Target Wildcards

In BL2/TPS, if you were using a Level hotfix, you could leave the "target" blank to have it apply to all levels. In BL3/WL hotfixes, that'd be the fourth and final field inside the opening parenthetical statement at the beginning of the hotifx, after the three numbers.

In BL3, leaving that blank doesn't work for SparkCharacterLoadedEntry, SparkLevelPatchEntry, or SparkEarlyLevelPatchEntry hotfixes, but you can instead specify the special keyword MatchAll. So, go ahead and use that when you need to apply the same hotfix to a number of levels or characters.

Basic Object References

In UE4, objects are specified with a path-like object, such as /Game/GameData/Loot/ItemPools/ItemPoolList_StandardEnemyGunsandGear. When referencing these objects in hotfixes, though, just that name isn't usually enough. The system expects a second identifier after a dot in the name, so to edit that ItemPoolList, you'd need to reference it as /Game/GameData/Loot/ItemPools/ItemPoolList_StandardEnemyGunsandGear.ItemPoolList_StandardEnemyGunsandGear.

For a lot of objects, it's that simple -- just repeat the very last path component. There's also a lot of objects for which that's not the case, though, and only looking through similar hotfixes and doing experimentation will get you used to tracking down the correct syntax.

One common reference you'll see is the addition of _C at the end of the "extra" object name, such as this object name related to Zane's "Violent Violence" skill:

/Game/PlayerCharacters/Operative/_Shared/_Design/Passives/DroneTree/ViolentViolence/Init_Operative_ViolentViolence_Calc.Init_Operative_ViolentViolence_Calc_C

Figuring out what exactly to use is a bit of a guessing game sometimes. All the various data introspection methods you have available might lead to clues. The name to use should always show up in the strings output of the relevant .uasset file, so you can check that for ideas if you're otherwise lost. If you're lucky, there will already be a working hotfix available (whether via Gearbox or Apoc's mods, for instance) which touches something similar to what you want to touch, and you can sort of go from there.

Attribute Array Double-References

One thing which I still don't have a great handle on is that when specifying a specific item in an array, as the attribute name, you'll often see the attribute name specified twice, such as this attribute referenced in a Gearbox hotfix:

Actions.Actions[0].bEnabled

I believe that that double-reference may not be required, or at least may not be required in all cases, but it's something to take note of, in case an array reference you're trying to work with isn't working right.

DataTables

UE4 includes a new kind of object called a DataTable, which is essentially like little mini spreadsheets embedded in the data, which other objects can then reference. They are super handy for modding purposes because they're easy to edit and can often let you make fairly sweeping changes to the game pretty easily.

For instance, one of Apocalyptech's earliest BL3 hotfix mods was a version of Early Bloomer, which unlocks all weapon/item types and parts (including elements, etc) from the very beginning of the game. In the BL2/TPS versions of that mod, all sorts of different objects had to be touched throughout the game's data, to reset MinGameStage attributes to 1 to unlock them. In BL3, fortunately, it turned out that the vast majority of unlocks that were needed for the mod's functionality were just available inside a single DataTable, /Game/GameData/Loot/LootSchedule/DataTable_GameStage_Schedule.

JWP can serialize DataTable objects really easily, so it's a simple matter to just loop through all the rows inside a DataTable making changes in one centralized location. DataTables are used for a lot of weapon/item balancing, enemy stats, and all sorts of things.

One weird thing about DataTables is that the "column" names are kind of strange, and include a random-looking 32-character string at the end. For instance, in the Early Bloomer mod, editing the DataTable_GameStage_Schedule object, the column that was altered is named MinGameStage_17_2500317646FAD2F4916D158835B29E83. You'll end up seeing the same column names used in various tables throughout the game -- for instance, other tables which deal with vehicle part locks will also use that exact same column name. Functionally, that's not something you've got to worry about, though. Just copy the whole column name when adding it to your hotfixes and you'll be golden.

Sometimes you might come across a DataTable reference which does not specify a ValueName parameter. In these cases, it seems to default to using the "column" name Value instead. You can see this in a reference to DataTable_Siren_ConstantValues inside the object /Game/PlayerCharacters/SirenBrawler/_Shared/_Design/Passives/BrawlTree/HelpingHands/PassiveSkill_Siren_HelpingHands, for instance.

One final thing to note about DataTables is that they're not always consistently used by the game data. For instance, there's a table which is used to specify drop rates for specific legendary item drops for bosses in the game, at /Game/GameData/Loot/ItemPools/Table_LegendarySpecificLootOdds. This is used pretty consistently by the original base-game data, but when Gearbox added in drop sources for all legendary gear, basically none of enemies which acquired brand new legendary drops used a DataTable for those drop rates -- instead, they were just hardcoded by the addition objects. So you can't always count on DataTables providing complete solutions to your problems.

Specific Object Type Differences

Here are some objects which exist in BL2/TPS as well as BL3, and some differences between them

BaseValueConstant Differences

In BL2/TPS, a common structure to see when dealing with numbers was a collection of BaseValueConstant, BaseValueAttribute, InitializationDefinition, and BaseValueScaleConstant. You can read about in our page on Understanding Borderlands Weight and Probability Values.

This structure makes an appearance in BL3 as well, though it's been slightly altered. A full structure in BL3 will look like so:

(
    BaseValueConstant=1.000000,
    DataTableValue=(DataTable=None,RowName="",ValueName=""),
    BaseValueAttribute=None,
    AttributeInitializer=None,
    BaseValueScale=1.000000
)

As you can see, instead of an InitializationDefinition attribute, there's AttributeInitializer, BaseValueScaleConstant has become just BaseValueScale, and there's a brand new attribute called DataTableValue. That's the one used to look up data from tables. The RowName and ValueName attributes in there are what's used to look up the exact "cell" inside the DataTable that you're looking for. The value taken from the DataTable will overwrite the BaseValueConstant entirely.

Implied/Default Values

One thing you'll often see in existing hotfixes, especially when setting those BaseValueConstant-based structures, is that the game will often provide sensible defaults if you leave off various attributes. For instance, when setting an object with a BaseValueConstant, you'll often see the data specified just like this:

(BaseValueConstant=5)

So all the other attributes in there are left blank. BaseValueScale will be set to 1, and the rest will be None and won't affect the math at all.

It's worth noting that if you're setting an existing BVC structure, it's possible that specifying only a single attribute like that might only update the exact attribute you've specified, leaving all the others the way they were previously. (The author has yet to do any tests to confirm/deny this.) So in some cases you may want to specify the full structure regardless.

BalancedItems Differences

BalancedItems still form the basis for weapon/item selection in the game, and they work pretty much just like their BL2/TPS counterparts, though there's one notable difference: they've got an additional ResolvedInventoryBalanceData attribute. An entry in a BalancedItems array will look something like this:

(
    InventoryBalanceData=/Game/Gear/Shields/_Design/_Uniques/Ward/Balance/InvBalD_Shield_Ward.InvBalD_Shield_Ward,
    ResolvedInventoryBalanceData=InventoryBalanceData'"/Game/Gear/Shields/_Design/_Uniques/Ward/Balance/InvBalD_Shield_Ward.InvBalD_Shield_Ward"',
    Weight=(BaseValueConstant=0.500000)
)

As you can see, the InventoryBalanceData attribute is "bare" and just includes the path to the relevant balance. The ResolvedInventoryBalanceData attribute is qualified with a class type (InventoryBalanceData in basically all cases). So far, all the instances I've seen of this specify the same path in both attributes, so it's a simple matter of duplicating the balance in both.

Quoting Objects

As seen in the BalancedItems entry above, there's one small difference in regards to quoting objects which include the object type. In BL2/TPS, this was always just done with a single quote mark ('), but in many official GBX hotfixes you'll now see that there's an additional double-quote (") inside that. I think that that's not actually necessary most of the time, but I've continued to use it regardless, since that's what the GBX hotfixes do.

ItemPoolList Differences

ItemPoolList objects are used in BL3 just like they were in BL2/TPS: to provide a list of ItemPool objects dropped by enemies and the like. For instance, nearly every "standard" enemy references the object /Game/GameData/Loot/ItemPools/ItemPoolList_StandardEnemyGunsandGear. These structures are nearly identical to their BL2/TPS counterparts, but have one new handy attribte: NumberOfTimesToSelectFromThisPool. For instance, if you wanted to drop three items from a given pool, you could do something like:

(
    ItemPool=ItemPoolData'"/Game/PatchDLC/Raid1/GameData/Loot/ItemPools/ItemPool_IndoTyrant.ItemPool_IndoTyrant"',
    PoolProbability=(BaseValueConstant=1),
    NumberOfTimesToSelectFromThisPool=(BaseValueConstant=3)
)

This effect is in addition to the Quantity attribute which still exists in the ItemPool objects themselves. In BL2/TPS, to drop more than once from a given pool, you'd most likely use Quantity, but in BL3 you've got a choice of doing it this way as well. The nice thing about NumberOfTimesToSelectFromThisPool is that it can be defined on a per-drop basis, so you can have one enemy drop 2 items from a pool, but another enemy might drop 4 from the same pool, whereas in BL2/TPS you'd have to set the Quantity on the pool itself, and both sources would drop the same amount.

Note that NumberOfTimesToSelectFromThisPool is processed after the PoolProbability is used to determine if the pool will be used or not. So an entry with PoolProbability of 0.5 and a NumberOfTimesToSelectFromThisPool of 2, will only ever drop exactly two items from the pool, or exactly zero.

Blueprint Object References

UE4 has the concept of a "Blueprint," which I have not investigated closely at all (someone familiar with UE4, feel free to expand on this section), but it seems to be a fancy way to generate objects using templates or code or something, and you'll see them used pretty frequently throughout the data. All enemies are named like BPChar_Foo for instance, the BP in the name standing for Blueprint. There are a lot of them out there; if you run a getall blueprintgeneratedclass ingame, you'll get thousands of results.

How this often ends up looking in-game, is that you can find both the Blueprint object itself, and also all the objects which were generated with that blueprint. For instance, you can run this getall statement to find the main BPChar class for the Gunner class:

getall blueprintgeneratedclass foo name=bpchar_gunner_c

Which will return /Game/PlayerCharacters/Gunner/_Shared/_Design/Character/BPChar_Gunner.BPChar_Gunner_C. That's not actually the BPChar which is in-use if you're playing the game as a Gunner, though. Instead, you can do a getall like so:

getall bpchar_gunner_c

If you're in Sanctuary, playing a Gunner, that getall statement will return /Game/Maps/Sanctuary3/Sanctuary3_P.Sanctuary3_P:PersistentLevel.BPChar_Gunner_C_0. Presumably if you had a friend also playing a Gunner in the map with you, you'd also see a _1 object as well. So the blueprint was used to generate the real objects used at the time.

That's a little bit of an aside, really, but it does mean that generally when you want to edit those objects, you'll specify the Blueprint class itself, but use a very different-looking additional text on the object reference.. For instance, to edit the movement characteristics of any Gunner in your game, you'd reference the following object:

/Game/PlayerCharacters/Gunner/_Shared/_Design/Character/BPChar_Gunner.Default__BPChar_Gunner_C:CharMoveComp

Of note specifically there is the Default__ prefix on the extra bit, which is nearly always coupled with the _C suffix. You won't always see a colon with extra text, as you do in this example, but the Default__ is something you'll see pretty frequently. This kind of thing is one of the hardest object reference names to figure out when modding, and you'll often end up needing to use a combination of JohnWickParse serializations (when possible), strings output (which can give you some name components that JWP won't), getall references from the console, and, if you're lucky, some already-existing hotfix from Gearbox or Apoc's mods which already touch something very similar to what you're looking for.

Reading JohnWickParse Serializations

In general, having a JWP .json file is pretty easy to understand, though there's one bit of structure which might not be obvious but is pretty handy (especially if you start to dive into the next section).

UE4 object files specify a list of "exports", which often correspond to the actual individual objects that you'll see in-game from the console. The JWP JSON files will be a list/array at the top level, and each item in the array is an export provided by the object. For instance, if you serialize any item pool with JWP, you'll see a serialization which looks a lot like this:

[
  {
    "export_type": "ItemPoolData",
    "BalancedItems": [
      ...
    ]
  }
]

So there's one export, with a type of ItemPoolData, and a BalancedItems array which will look familiar to many BL2/TPS modders. It's the export_type line in there that I wanted to draw specific attention to, because that will tell you the class of object which you'd want to use as part of a getall statement. That'll tell you that you can run a getall ItemPoolData from the console, and this object should show up in the list so long as it's loaded into memory.

Other objects more complex than item pools will often have more than one export, so you'll see multiple structures inside the top-level list, each with their own export_type. I'm unsure how to tell which export is considered the "main" one, which doesn't require any extra shenanigans to reference in hotfixes. When JWP reads in the data, there's an internal variable which flags whether or not an export is an "asset," and that does often line up with what the main export ends up being, but not always (and you can't see that variable in the serializations, anyway).

Other non-main exports are often the sub-objects that you'll see with colons in their names, like you'd see in BL2 with an object like GD_Weap_Pistol.A_Weapons_Legendary.Pistol_Bandit_5_Gub:WeaponPartListCollectionDefinition_100, for the runtime part list collection for the Gub pistol. If that were a BL3 object, I'm pretty sure that the main Pistol_Bandit_5_Gub would be the "main" export in the serialization, and then there would be another export in there for the WeaponPartListCollectionDefinition.

In the end, there's still a number of unknowns (for myself, anyway), but that should hopefully help a bit.

Complex Object/Attribute References

One thing you might notice in existing hotfixes is some fairly wild-looking attribute names such as the following:

AspectList.AspectList[0].Object..WeaponUseComponent.Object...RefundAmmoCount

What in the world is going on there? Well, I don't absolutely know for certain, but I suspect that it's related to tracing down into those sub-objects that we mentioned before. I believe that this fancy Object.. syntax is a way to get around having to reference the exact object name specifically.

Namely, the main object you're referencing refers to one of those sub-objects in an attribute, and you can simply follow the object chain by using the Object.. syntax, which happens to leap through the exports which are enumerated by a JWP serialization. It would be like being able to reference something like RuntimePartListCollection.Object..GripPartData from that main Gub BalanceDefinition, to modify the Gub's possible grips, instead of having to know that the sub-object was named WeaponPartListCollectionDefinition_100. This is all somewhat conjecture on my part, but I think it mostly works out.

An Example

Constructing this kind of object reference chain yourself for modding can be quite tricky without the aid of JohnWickParse, whose serializations can often be real helpful. By way of illustration, we'll walk through an object which controls whether or not the player is allowed to use the Eridian Resonator (the thing which lets you smash Eridium crystal growths), /Game/Gear/Game/Resonator/_Design/MeleeData_Resonator. This is an object which JWP is able to fully serialize, which helps a lot. Here's the serialization of that object, with some extra data manually added in using the prefix _apoc: MeleeData_Resonator.json

When looking at the serializations, you'd want to start numbering the exports starting with 1 from the top (as opposed to using 0, which might come more naturally to someone used to programming). I've numbered those with _apoc_export_idx. Then whenever you see an export attribute in there, it'll be pointing to a number, which references the numbers that you assigned to those exports. I've added in my own _apoc_export_type attribute in there which references the export_type of the export in question.

In this object, the "main" export happens to be the very first one, so you can reference the OverrideCondition attribute in a hotfix directly, without any extra fuss. That OverrideCondition points at export 5, which happens to be a GbxCondition_List object. That export contains a Conditions array, which happens to define the conditions which are required in order to melee using the Resonator. So, if you wanted to change that condition list in any way, you could reference this attribute in your hotfix:

OverrideCondition.Object..Conditions

In fact, one simple way to make the Resonator available from the beginning of the game is to redefine that Conditions array to only include the Condition_CompareDistance_C condition. You can see a hotfix which does that in Apocalyptech's eridian_unlocks.txt mod. (The absolute name of the CompareDistance comparison was discovered using a big chain of getall commands from the console.)

You can trace the chain further along using the same trick, if you want. For instance, export 7 defines a specific mission objective which has to be complete in order to use the Resonator (specifically one from story mission chapter 10). If you keep tracing through those export links in the serialization, you can successfully change the required mission objective by referencing the following attribute:

OverrideCondition.Object..Conditions[0].Object..Conditions[0].Object..ObjectiveRef

Nothing to it, eh? It's definitely a lot of trial and error, but if you've got a mostly-complete JWP serialization, you'll have a pretty decent chance of getting there in the end.

Clone this wiki locally