Modding:Working with LSX files

From bg3.wiki
Revision as of 14:17, 26 September 2023 by HINK (talk | contribs)
Jump to navigation Jump to search

I'll try to give people a head start here in working with lsx files, which are just xml files, and getting a head start with modifying Progressions.lsx. This gives you the power to modify & add classes & what they get on level up.

Folder structure

There's already tutorials on here on how to setup the folder structure, overall it's very beneficial to package your mod as .pak instead of an archive which the user extracts in to their data folder. BG3 uses an override system which I will get into more detail with later on, extracting everything in the data folder seems to somewhat circumvent this behavior since one seems to be forced to put stuff in some variation of Gustav, GustavDev, Shared, SharedDev. Be sure to really take a look at the folder structure because missing one little detail will make your mod not load & it's overall a tedious process to debug.

Working with .lsx, or rather .xml files

Depending on experience level & what your goals are different strategies can be taken to modify the files. For my mod, which is doing sweeping changes I use python. It also helps me to query the data & find what I'm looking for directly instead of trying to look through the same file in multiple locations. The advantage of doing it programmatically is that you can just re-run the script if a patch updates the same nodes you've changed. At the bottom I've included the script I use to generate my mod, python is a bit outside the scope of this tutorial but it should give you a skeleton & the general workflow for how to work with the files. The mod in question adds level 13 to all base classes, this makes it so you can take level 12 in a mod which increases max level without the game crashing when you level up next time. It gives feat on every level, and triples the spell slots/sorcery points/ki points etc on level up.

How does BG3 load resources?

As hinted on earlier it's very important to learn how resources are loaded and overridden. Imagine you extract the game .paks, you look through the folder structure, and inside Data/Public/Shared/Progressions/Progressions.lsx you find xml data for levels. You modify it to your hearts content, perhaps you remove stuff, add some stuff, and then you just let it sit there inside that folder hierarchy. What will happen is that it will completely replace that file as seen in the .pak file. Perhaps you noticed that there's only level 1-6 in this file, and you thought it smart to add levels 7-12. But the problem is that level 7-12 is another Progressions.lsx inside the SharedDev subfolder structure, and that one is loaded after the one in Shared and just overwrote your 7-12. While this seems like a downer it should also give you a hint on how the system works. There's a load order, even inside the shipped .paks with the game, and you generally don't want to mess with that, because the patches are also playing ball with that load order. Generally, whenever you see Gustav/GustavDev/Shared/SharedDev in paths you should replace that with your mod name when you copy it over to your mod project. Generally you should only include sections of the files which you're actually changing, exclude everything else, and they will instead be filled by the game. This gives you the best chance that your mod is still working whenever a patch arrives, or if another mod is overriding stuff in the same file.

So how are these files merged? It seems to be different depending on files, .txt files such as Data.txt merge based on the key of the key / value pair. .lsx which I'll focus on however are merged base on their UUID, so when you look over the xml whatever group has an UUID child is the granularity that you will be overriding on.

Progressions.lsx

So finally we'll get to the structure of this file which is integral to all the level ups in the game. It's important to know that this file is split into multiple locations, for the base game without patches it's split between Shared/Public/Shared/Progressions/Progressions.lsx, which contains level 1-6 & I presume the EA state of the game. 7-12 and modifications to the progression is instead in Shared/Public/SharedDev/Progressions/Progressions.lsx. Patches which changes progression have their own versions which takes precedence. As a rule of thumb I would never change the type attribute of any nodes, although I haven't really tried it. Lets break down a level 1 of a class, in this case I'll take Cleric, because it has a lot of variation, even if it's very large:

   <node id="Progression">
       <attribute id="Boosts" type="LSString" value="Proficiency(LightArmor);Proficiency(MediumArmor);Proficiency(Shields);Proficiency(Morningstars);Proficiency(Flails)"/>
       <attribute id="IsMulticlass" type="bool" value="true"/>
       <attribute id="Level" type="uint8" value="1"/>
       <attribute id="Name" type="LSString" value="Cleric"/>
       <attribute id="PassivesAdded" type="LSString" value="DomainSpells;UnlockedSpellSlotLevel1"/>
       <attribute id="ProgressionType" type="uint8" value="0"/>
       <attribute id="Selectors" type="LSString" value="SelectSpells(2f43a103-5bf1-4534-b14f-663decc0c525,3,0,,,,AlwaysPrepared);AddSpells(269d1a3b-eed8-4131-8901-a562238f5289)"/>
       <attribute id="TableUUID" type="guid" value="64474b62-b4f5-46b3-b94a-c676c6da3116"/>
       <attribute id="UUID" type="guid" value="366cde9c-db0c-43ce-a49c-fb140e084b3c"/>
       <children>
           <node id="SubClasses">
               <children>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="4b5da2f5-b999-4623-8bff-a63df5560fb3"/>
                   </node>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="c54d7591-b305-4f22-b2a7-1bf5c4a3470a"/>
                   </node>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="f013d01b-3310-43f7-81bf-a51130442b5e"/>
                   </node>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="ebe18794-b5e1-41c4-befa-4b9d6922b0ec"/>
                   </node>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="6dec76d0-df22-411c-8a78-3d6fb843ae50"/>
                   </node>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="89bacf1b-8f15-4972-ada7-bf59c7c78441"/>
                   </node>
                   <node id="SubClass">
                       <attribute id="Object" type="guid" value="b9ccf90e-b35b-4b73-b896-8ed2d32ae8c6"/>
                   </node>
               </children>
           </node>
       </children>
   </node>

The first attribute, which has the id Boosts, is one of the most important ones. It's a series of function calls which declares what the you actually get when you reach this particular level. In this case getting level 1 Cleric gives us a series of proficiencies, what can be specified here is also currently outside the scope of this tutorial, but it's actually pretty straightforward & easy to deduce. If you want your modified level up get something else, look at the level in the file for another level up which has what you want & append it to yours.

The second attribute, which has the attribute IsMulticlass, is optional & defaults to false, but in this case it's true. What that means is that this level up is actually for when multi classing into Cleric, this is not the specification for a level 1 character starting with Cleric.

The third attribute, Level, simply specifies that this declaration concerns level 1. The fourth attribute, Name, is simply the identifier for the class. The fifth attribute I haven't deduced yet, but surely related to passives granted.

The sixth attribute, ProgressionType, is an integer which decides what type of progression it is. 0 means that it's a base class, 1 means that it's a sub class, and 3 means that it's related to a race.

The seventh attribute, Selectors, decides what you can choose from / get for this level up. SelectSpells(2f43a103-5bf1-4534-b14f-663decc0c525,3,0,,,,AlwaysPrepared); for example, starts with a UUID into SpellLists.lsx, which is a set of the level 1 cantrips for for Cleric in this case. 3 means that you get to choose 3, the other values haven't been deduced yet but AlwaysPrepared probably does what it says on the tin. AddSpells(269d1a3b-eed8-4131-8901-a562238f5289) is also an UUID into SpellLists.lsx, this is the Cleric level 1 spells.

The eighth attribute, TableUUID, is an UUID which essentially ties together everything for the progression. This is also a bit outside of the scope of this tutorial and something I haven't fully deduced, but the starting point is inside ClassDescriptions.lsx which associates a table with the general description for a class.

The ninth, UUID, is simply the unique identifier for this level up, if you want to override something you must have an identical UUID to it, and if you want to add something it in turn must have a freshly generated UUID.

Lastly we have a child structure which is merely an array of subclasses, if you want to add a new sub class copy the structure & change the UUID value, the UUID maps to an entry inside ClassDescriptions.lsx.

In addition to the base classes you'd expect there's also MulticlassSpellSlots, this seems to just be a placeholder which kicks in when you multiclass with spell casters to make sure they get extra spell slots dependent on general character level instead.


You might also run across an attribute with an id of AllowImprovement, adding this to a base class level & setting it to true makes sure that you get a feat for that level up.

Python example for working with .lsx