-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Borderlands 3 Hotfix Modding
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
- General Hotfix Info
- Hotfix Target Wildcards
- Basic Object References
- DataTables
- Specific Object Type Differences
- Blueprint Object References
- Reading JohnWickParse Serializations
- Complex Object/Attribute References
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:
- You'll want to be able to access as much BL3/WL data as possible. The wiki page on Accessing Borderlands 3 Data details all the methods currently known for doing so.
- A method of running mods:
- apple1417's OpenHotfixLoader is the latest method, and as of Oct 26, 2022, is the only method to support Tiny Tina's Wonderlands.
- c0dycode's B3HM project has been around for longer, and still works fine for most BL3 mods.
- You can find plenty of information about how to run both at borderlandsmodding.com.
- A legacy version of hotfix injection (and the only real option for users on Linux) uses mitmproxy. Some information on that method can be found at apocalyptech's bl3hotfixmodding repo.
- The BL3/WL hotfix syntax differs from BL2/TPS in a number of ways. You should probably look over our page on Borderlands 3 Hotfixes to familiarize yourself with them.
- Looking through existing hotfixes is a great way to see what's possible,
and use as templates for making your own changes. There's two good
sources for that at the moment:
- First, official Gearbox-provided hotfixes. There's a "bl3hotfixes" project on Github
which collects all the official hotfixes, and a Google Sheets page
which collates them all into a single spreadsheet.
- Wonderlands versions: wlhotfixes project at Github
- Secondly, a new bl3mods github repo has
been started to collect community mods. There's also a
ModCabinet wiki which organizes
those mods in a slightly easier-to-browse way.
- Wonderlands versions: wlmods github repo, and Wonderlands ModCabinet.
- First, official Gearbox-provided hotfixes. There's a "bl3hotfixes" project on Github
which collects all the official hotfixes, and a Google Sheets page
which collates them all into a single spreadsheet.
- Apocalyptech's Borderlands 3 Object Refs (and Wonderlands Object Refs) page can be handy for quickly figuring out how objects relate to each other (though it doesn't replace other tricks like JohnWickParse, from the Accessing Data page).
- The Level Name Reference page has all the BL3/WL level names populated.
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'sOnDemand
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'sBPChar_*
name. They're loaded on-demand as you progress through the level, so you can't generally useSparkLevelPatchEntry
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 usesSparkStreamedPackageEntry
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-identicalSparkCharacterLoadedEntry
hotfixes keyed to every enemy you want to apply it to. You could alternatively use the special targetMatchAll
, instead of the actualBPChar_*
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, usinggetall
from the console helps to figure that out.
- 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
- If you're looking to do map tweaks, remember that there are two types of
level hotfixes:
SparkLevelPatchEntry
andSparkEarlyLevelPatchEntry
. Some of the statements required to alter vehicle-spawning parts have to use theEarly
version in order to work. I'd recommend trying the non-Early
hotfix first, though, when testing out functionality.
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.
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.
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.
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.
Here are some objects which exist in BL2/TPS as well as BL3, and some differences between them
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.
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
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.
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
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.
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.
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.
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.
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.