Borderlands 3 Hotfixes - BLCM/BLCMods GitHub Wiki
This page intends to document what we know about the hotfix system in use by Borderlands 3. If you're interested in tracking changes to the hotfixes that GBX provides for Borderlands 3, a github repo was set up which checks for hotfixes hourly and uploads new versions where appropriate. Feel free to check it out over here: https://github.com/BLCM/bl3hotfixes
The official GBX hotfixes have been getting uploaded to a Google Sheet, which makes analyzing some of their structure a bit more easily. This has been getting updated pretty regularly, though make sure you check the last-updated date in the document title: BL3 Hotfixes on Google Sheets.
Hotfix Keys/Operations
BL3 currently has had five hotfix operations which have been used in official hotfixes:
SparkPatchEntry
- Globally-applicable hotfixes which get applied at the main menu, when hotfixes are loaded. This hotfix type should be all right for most objects which already exist at the main menu. Unlike in BL2/TPS, most Player Character related objects can make use of this type.SparkLevelPatchEntry
- Hotfixes which activate on level load. This hotfix type requires specifying a level name, but you can also useMatchAll
to apply to all levels. (MatchAll
is equivalent to the BL2/TPS method of leaving the level name blank.)SparkEarlyLevelPatchEntry
- This is similar toSparkLevelPatchEntry
, but kicks off earlier in the level-loading sequence. This is required for some kinds of map-related edits, such as altering container contents and the like. It's also needed for some Mayhem Modifier changes. In general, though, this hotfix type isn't necessary, and aSparkLevelPatchEntry
would be sufficient.SparkCharacterLoadedEntry
- Hotfixes which activate when aBPChar
loads, primarily used for enemy modifications. Enemies tend to get loaded dynamically as the engine requires them, so hotfixes to enemy data often can't use patch or level-based hotfixes. Requires specifying aBPChar_*
name, though like level-based hotfixes, you can useMatchAll
to have it run for any character load.SparkSaveGameEntry
- Used to tweak incorrect values in savegames. This syntax hasn't really been fully investigated, since it's of more use to GBX than ourselves.
Additionally, there appear to be two other kinds of hotfix operations which have not been seen yet, so it's a bit difficult to know exactly how they would be used:
SparkPostLoadedEntry
SparkStreamedPackageEntry
Hotfix Values
The hotfixes used in BL3 are at least slightly different from the ones used in both BL2 and TPS, though the general syntax is still the same. Here's an example of a very simple hotfix:
(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE,bEnableUpdateRateOptimizations,4,True,False
Two things will be obvious to folks already familiar with BL2/TPS hotfixes: the "package" field has been expanded, and there's an extra number inbetween the attribute and the "old" value. For clarity's sake, here's the fields split out:
(1,1,1,Crypt_P)
/Game/Cinematics/_Design/NPCs/BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE
bEnableUpdateRateOptimizations
4
True
False
The extra number field (field 4 in the list above) is easy enough to
explain: it's simply the string length of the "old" value. The string
True
has four characters, so the number is 4. This was presumably added
in because that way it's easy to include old values which contain commas,
without having to worry about quoting the values properly, etc. If the
game knows the string length, it can just read that many bytes and then
proceed to process the rest of the hotfix. To construct a hotfix which
doesn't check for existing values (ie: a set
rather than set_cmp
, in
OpenBLCMM parlance), set the "from" length to zero and leave the next field
blank, as usual.
The package field has been expanded to include quite a bit more information. I've been calling it a "package tuple".
The first of the numbers is always 1
, and the game will reject a
hotfix which has any other value.
The second of the numbers determines the type of hotfix, and so far we've
seen a few values for that: 1
, 2
, 4
, 6
, 7
, 8
, and 11
. This number
can have a big effect on anything looking to process hotfixes, and can alter
the number of fields that show up in the rest of the hotfixes. I've been
calling it a "type," though that term's unlikely to be what GBX calls
it. See the next section for information on that.
The third number is technically a bitfield, though only bit 0 is being checked
currently, so this value will always be 0
or 1
for the moment. It's used
internally to specify whether the modified object should be notified when the
hotfix is applied or removed. The circumstances under which you'd want to use
one or the other remains fairly opaque. In practice, 0
seems to work most of
the time, though there are plenty of GBX-provided hotfixes which use 1
The fourth field in there is the "target" of the hotfix operation, and is
only seen in SparkLevelPatchEntry
, SparkEarlyLevelPatchEntry
, and
SparkCharacterLoadedEntry
hotfixes. This is where you specify the level in
which to run the hotfix, or the character whose load will trigger the hotfix.
You can use the special keyword MatchAll
to have a hotfix operate on all
level loads, or on all character loads. (The BL2/TPS method of leaving that
identifier blank does not work in BL3.)
Hotfix Types
The second number in the "package" field determines the type of the hotfix, which will alter how the hotfix looks.
Hotfix Type 1: Regular
This is the most familiar-looking hotfix, such as the example above. It alters the named attribuet which exists right in the object in question. The field list for this kind of hotfix is:
- Package Tuple
- Object Name
- Attribute Name
- "From" Length
- "From" Value
- "To" Value
Hotfix Type 2: DataTable
BL3 has a new data structure used in a variety of circumstances which is called a DataTable. These tables seem to have "rows" which can be referenced by name, and hotfix type 2 will let you specify which row to act on.
- Package Tuple
- Object Name
- Row Name
- Attribute Name
- "From" Length
- "From" Value
- "To" Value
For instance, here's one currently-active hotfix:
(1,2,0,),/Game/GameData/Modifiers/DataTable_Mayhem_CoreMods_Easy.DataTable_Mayhem_CoreMods_Easy,ExpGain_CombatOnly,MinValue,8,2.000000,0.100000
- Package:
(1,2,0,)
- Object:
/Game/GameData/Modifiers/DataTable_Mayhem_CoreMods_Easy.DataTable_Mayhem_CoreMods_Easy
- Row Name:
ExpGain_CombatOnly
- Attribute:
MinValue
- "From" Length:
8
- "From" Value:
2.000000
- "To" Value:
0.100000
Hotfix Type 3: CVar
This hotfix type is used to set engine-level CVars. It's unknown what kinds of things can usefully be edited with these. Gearbox themselves never used this hotfix type until November 2023. As of July 2024, the two Gearbox examples we have are:
(1,3,1,),0,UnlockWarchestHeroes,0,1
(1,3,1,),0,UnlockWarchestVillains,0,1
The syntax is technically unknown, though we can make a few guesses:
- Package:
(1,3,1,)
- Unknown Number:
0
- CVar name:
UnlockWarchestHeroes
- "From" value, maybe?:
0
- "To" value, probably:
1
Hotfix Type 4: Savegame MissionObjective
This type has only been seen inside SparkSaveGameEntry
hotfixes so
far, of which we've only seen two examples. The currently-active one looks
like:
(1,4,0,),/Game/Missions/Plot/Mission_Ep02_Sacrifice.Mission_Ep02_Sacrifice_C,1,/Game/Missions/Plot/Mission_Ep02_Sacrifice.Set_WatchMouthpieceMovie_ObjectiveSet,(8,9),(1,0),(2,2)
As suggested by the MissionObjective
name, this is used to alter saved
mission objectives, to tweak values which might have been blocking progress
or something. I haven't taken the time to try and figure out what most of
those fields mean:
- Package
- Mission Object Name
- Unknown Number 1
- Objective Object Name
- Unknown two-digit tuple 1
- Unknown two-digit tuple 2 ("from" value?)
- Unknown two-digit tuple 3 ("to" value?)
This type is unlikely to be useful to modders anyway, of course.
Hotfix Type 5: Savegame MissionObjectiveSet
Like type 4, this has only been seen in conjunction with
SparkSaveGameEntry
hotfixes, so it's clearly something savegame-related,
and additionally is also related to mission objectives. The currently-active
one looks like:
(1,5,0,),/Game/Missions/Plot/Mission_Ep05_OvercomeHQBlockade.Mission_Ep05_OvercomeHQBlockade_C,1,/Game/Missions/Plot/Mission_Ep05_OvercomeHQBlockade.SET_ContactAtlas_RhysConversation_ObjectiveSet,53,1,/Game/Missions/Plot/Mission_Ep05_OvercomeHQBlockade.SET_TalkToLorelai_Monocycle_ObjectiveSet
I haven't taken the time to try and figure out what most of those fields mean:
- Package
- Mission Object Name
- Unknown Number 1
- ObjectiveSet Object Name
- Unknown Number 2
- Unknown Number 3
- Another ObjectiveSet Object Name
Hotfix Type 6: SpawnMesh
This hotfix type is used to alter some ingame mesh information, so these are altering level geometry in some ways. The values set will include a pipe-and-comma-delimited array of numbers. Here's one currently-active example:
(1,6,0,AtlasHQ_P),/Game/Maps/Zone_1/AtlasHQ,/Game/LevelArt/Environments/Promethea/AtlasHQ/Architecture/Pillars/Model/Meshes,SM_AtlasHQ_Pillar_V1,90,"-9392.000000,-2797.000000,928.000000|0.000000,0.000319,0.000000|1.500000,0.750000,1.500000",0
- Package
- Path to map where the hotfix will be applied, without the actual map object name
- Path to
SpawnMesh
object (without the actual mesh object name) - Object name of the
SpawnMesh
object to add, without the path component - Length of the next field
- Pipe-and-comma-delimited array of numbers.
Format:
Location x,y,z|Rotation pitch,yaw,roll|Scale x,y,z
- Opacity:
0
= visible,1
= clear/see through.
For the rotation values, positive X is considered "forward," so pitch
will
rotate around the Y axis, yaw
will rotate around the Z axis, and roll
will
rotate around the X axis. Many (though not all) StaticMeshes visually "look"
like they're facing positive X, which is a help.
Note that using these hotfixes, you can only use meshes which are already
loaded by the level, so you can't pull in entirely arbitrary meshes using
only this hotfix type. If you can cause some new meshes to be referenced
by other objects, though, before this hotfix is read, you can pull in
arbitrary meshes. The bl3hotfixmod
hotfix-writing helper library now
includes a method to do this transparently in the background.
Hotfix Type 7: Blueprint Bytecode
This type alters the bytecodes which are generated as the result of Unreal Engine Blueprint compilation, which is responsible for some of the fancier behavior in objects in the game. Here's an example of a hotfix from GBX:
(1,7,0,),/Game/PlayerCharacters/Beastmaster/_Shared/_Design/Passives/Ranged1/Passive_Beastmaster_Ranged1.Passive_Beastmaster_Ranged1_C,0,1,ExecuteUbergraph_Passive_Beastmaster_Ranged1,1,312,8:0.100000,3:2.0
The fields found in the hotfix:
- Package
- Object Name (basically always with a
_C
suffix) - Unknown Number 1 - always
0
so far - Unknown Number 2 - always
1
so far - Export/function/sub-object in which to apply the hotfix
- Number of indexes/offsets to alter in the hotfix -- most often just
1
- (can be repeated if #6 is more than 1!) Index/Offset(s) to alter
- "From" Value, prefixed with the length of the "From" string by use of a colon
- "To" Value, prefixed with the length of the "To" string by use of a colon
An example of a hotfix which alters more than one index, from GBX:
(1,7,0,),/Game/PatchDLC/Dandelion/Gear/Weapon/_Unique/IonLaser/BPWepFireBeam_IonLaser.BPWepFireBeam_IonLaser_C,0,1,ExecuteUbergraph_BPWepFireBeam_IonLaser,2,665,1249,1:6,1:8
The main challenge with using this hotfix type is data inspection -- neither JohnWickParse nor FModel report anything about blueprint bytecode. Fortunately, a project called UAssetGUI/UAssetAPI does this handily. When serializing the data, UAssetGUI will show you the bytecode contents as some fairly straightforward JSON, which takes a bit of getting used to, but isn't awful once you're used to it.
Apocalyptech's fork of UAssetAPI
is recommended because it includes the indexes/offsets usable in hotfixes,
next to any data structures which we're capable of modifying with hotfixes.
You can download an updated UAssetAPI.dll
from that fork and then copy it
over the "vanilla" one in your UAssetGUI installation, if you're using the
GUI to look at the serializations. Alternatively, that fork also includes
a CLI utility to do serializations without a GUI (very similarly to how
JohnWickParse is used for
"regular" serializations), and another to generate graphs of the bytecode
structure.
Note that we can probably only edit "literal" values in the bytecode. Specifically, it looks like the following data types are what we've got to work with:
- ByteConst
- False
- FloatConst
- InstanceDelegate
- Int64Const
- IntConst
- IntConstByte
- IntOne
- IntZero
- NameConst
- ObjectConst
- RotationConst
- TransformConst
- True
- UInt64Const
- VectorConst
So, in general, raw numbers (like changing 0.1
to 2.0
in the first example, or
6
to 8
in the second), or change which object is being referenced, but we don't
seem to be able to edit things like local variables, "Jump" offsets, or completely
overwrite raw bytecode opcodes.
There seems to be some weirdness with doing multiple bytecode edits to the same function, as well. If you're modifying the same from/to values more than once in a single function, you may need to do so with the "combined" multi-index syntax, rather than doing separate hotfixes. In some circumstances it might not work even with that. Some creativity in figuring out what works and what doesn't will be required.
Hotfix Type 8: MaterialInterface
This type of hotfix was first seen on the June 11, 2020 hotfix update (which added the Guardian Takedown). Its exact fields aren't known, but there aren't too many fields in there. There's an example:
(1,8,1,GuardianTakedown_P),/Game/PatchDLC/Takedown2/Maps/GuardianTakedown_Temple.GuardianTakedown_Temple:PersistentLevel.IO_CryptPillar_2.StaticMesh1,0,/GbxSharedBlockoutAssets/_Shared/Model/Materials/Grayscale/MI_GrayScale_10.MI_GrayScale_10,MI_Eridian_Wall_Atlas_V2
- Package
- Object Name
- Unknown Number 1
- Material Interface Path
- material Interface Name
Hotfix Type 9: Unknown
Nothing much to say about this. We don't even have a label for them, let alone examples.
Hotfix Type 10: Unknown
Nothing much to say about this. We don't even have a label for them, let alone examples.
Hotfix Type: 11: Stream Blueprint
This is used to add new streaming Blueprint based objects to the map, which includes things like Interactive Objects -- its use for some time has been to just add in the "Hotfixes Applied" sign on the main menu, but it's theoretically possible to add things like vending machines, respec machines, and maybe even Catch-A-Rides.
This type comes with a bunch of caveats, though, and seems to be pretty
fragile. Apart from the "Hotfixes Applied" hotfixes (added on 2019-11-21),
GBX attempted to use it to add another Black Market machine to Neon Arterial
on the 2021-09-09 hotfix update, which didn't actually work properly. The
main limitation is that objects added via this method will always end up at
the "origin" point on the map, or (0, 0, 0)
, and must be moved to their
proper place via other methods. Those methods seem to not always work properly,
though, and not in ways which are yet predictable.
Still, here's an example of the hotfix in action. It follows an extremely similar syntax to Type 6 hotfixes (SpawnMesh additions):
(1,11,0,MenuMap_P),/Game/Maps/MenuMap,/Game/Patch/MicropatchApplied,IO_MainMenu_HotfixIndicator,80,"0.000000,0.000000,0.000000|0.000000,0.000000,0.000000|1.000000,1.000000,1.000000"
- Package
- Path to map where the hotfix will be applied, without the actual map object name
- Path to object to add (without the actual object name)
- Object name of the object to add, without the path component
- Length of the next field
- Pipe-and-comma-delimited array of numbers. This is theoretically a compilation of location, rotation, and scale, like the Type 6 hotfix type, but these values appear to be ignored when placing the object. It will instead always appear at the origin, with no rotation or scaling.
In order to move placed objects around, you'll have to hotfix their location after the fact, using their dynamically-generated object names to get at the location parameters. For instance, you can use this to add a new Black Market machine to The Droughts:
SparkEarlyLevelPatchEntry,(1,11,0,Prologue_P),/Game/Maps/Zone_0/Prologue,/Game/PatchDLC/Ixora2/InteractiveObjects/GameSystemMachines/VendingMachine/_Shared,BP_VendingMachine_BlackMarket,80,"0.000000,0.000000,0.000000|0.000000,0.000000,0.000000|1.000000,1.000000,1.000000"
The machine itself will end up with an object path of /Game/Maps/Zone_0/Prologue/Prologue_P.Prologue_P:PersistentLevel.BP_VendingMachine_BlackMarket_C_0
.
The name appears to be pretty deterministic -- it should live under the map
object specified in the hotfix, with its eventual last component of the name
suffixed by _C_0
, with 0
being the first one added, _1
being the next,
etc. These numbers might be placed before any already-existing objects of
that sort on the map. For instance, adding that Black Market machine will push
the existing machines in The Droughts up to _1
, _2
, etc. Note that this
has implications for having multiple mods editing the same maps -- if more than
one mod adds the same kind of object to the same map, neither will be able
to know what suffixes to properly use.
Regardless, to move the machine to a location near the "Highway" fast travel in that map, you could try a hotfix like:
SparkEarlyLevelPatchEntry,(1,1,1,Prologue_P),/Game/Maps/Zone_0/Prologue/Prologue_P.Prologue_P:PersistentLevel.BP_VendingMachine_BlackMarket_C_0.RootComponent,RelativeLocation,0,,(X=46488.000000,Y=22942.000000,Z=-3379.000000)
Specifically, going after its RootComponent
subobject, and setting the
RelativeLocation
attribute in there. Unfortunately, it looks like the object
itself might not exist yet, when that hotfix is activated. Sometimes,
depending on placement, you might end up with a machine where you expect it,
but other times it appears that the object's created after the hotfix fires, so
its RelativeLocation
remains just at the origin. This appears to be a timing
issue - if the positioning hotfix happens too soon, the object doesn't actually
exist yet. One way to introduce some artificial timing is to force some SpawnMeshes
to load inbetween the Type 11 hotfix and the positioning one; in testing, it looks
like two SpawnMesh loads are often enough to get it to work.
One other caveat: the created Blueprints will always live inside the "basic"
Levelname_P
object, but it will not work if there are already objects of that
type living inside Levelname_P
. For instance, if you want to spawn in a new
ammo vending machine, and the existing machines all live inside Levelname_Dynamic
or Levelname_Combat
, for instance, you'll be good to go. If there are any
inside Levelname_P
, though, it just doesn't seem possible.
Anyway, all of this is to say that there's still some unknowns with this hotfix type, and it's possible that it's not really suitable for modding in the end, if you care about mod stability.
Conclusion
That's it, so far! As more hotfixes get rolled out, some of the mysteries here could get cleared up. Some mysteries would probably be cleared up by just taking a closer look at the GBX-provided changelogs and matching them up to the downloaded hotfixes. One other thing that would probably help would be to have a good set of dumped data from the game, though BL3 makes that rather more complicated than the easier situation we had with BL2/TPS.