Baldur's Gate 3 Mod Manager for macOS
This mod manager is currently in active development and may not function as expected. Please submit issues for the most recent build available.
To download the latest version, check Releases.
For pre-development planning, see the Documentation section.
- How It Works
- File Management
- XML-LSX Parsing
- APFS Permissions
- Mod Types
- System Requirements
- Resources
- Acknowledgements
- SwiftData implementation
- JSON mod metadata parsing (
info.json
) - NavigationView (master)
- Add ModItem to SwiftData store
- Delete ModItem from SwiftData store
- Drag/drop ModItem to set load order
- ModItemDetailView (detail)
- Populate with metadata from parsed JSON
- Toggle modItem's
isEnabled
state
- File management
- UserSettings: Option for copy or move on mod import
- Copy/move mod folder to Application Support/Documents on import
- Handling of
.pak
file location based onisEnabled
status - Remove mod folder contents on Delete
-
modsettings.lsx
- modsettings XML version/build check on launch
- Backup default modsettings file for restore (remove all mods) functionality
- Use latest XML version/build tags for generation
- Mod load order XML generation based on
isEnabled
status - Save Load Order button action → backup lsx (rename), generate new lsx
- modsettings XML version/build check on launch
Upon downloading a mod package, you'll be given a mod folder with two files: info.json
and some .pak
file. Simply drag and drop that mod folder into the app to get started...
If the info.json
file can be parsed (it contains the required fields Name, Folder, UUID, MD5
) then the mod folder will be accepted.
From here, the mod folder will be stored in the app's Application Support/
directory. Simultaneously, a new entry will be added to the app's local database that will include a reference to the mod folder directory, as well as the metadata parsed from the JSON file. Each new entry will also be added to the end of the load order list and given an order number based on its position in the list.
Rearranging mods in the sidebar will update the order number of each mod, respective to their new position in the list.
Newly added mods are disabled by default.
Enabling a mod will move that mod's .pak
file to the BG3 Mods/
directory. It will also queue the metadata (parsed from the JSON file) to be added to the modsettings.lsx
file.
Disabling a mod will move the .pak
file back to the mod folder (in the app's Application Support/
directory), and will remove its associated metadata from the modsettings.lsx
queue.
Save Mod Settings will backup the existing modsettings.lsx
file and replace it with a new, identical file that includes the metadata of the enabled mods in your load order. The order in which these mods are added will depend on their order in the list. This new modsettings file will be given permissions that mimic the system's file-locking functionality, as seen in the Finder app.
Adding new mods, enabling/disabling existings mods and/or modifying the load order–followed by Save Mod Settings–will simply replace the existing modsettings.lsx
file with a newly generated one.
Restore Mod Settings will replace any existing modsettings.lsx
file with the one that was initially backed up from the first time you saved mod settings.
Mod path management, async file transfers, version control, etc.
BaldursModManager/
default.store
UserBackups/
modsettings.lsx {timestamp}
UserMods/
mod-folder/
info.json
mod-file.pak
Alongside creating a backup of each modsettings.lsx file, we also need to parse its contents for attribute values. We not only need to do this on first backup, but every backup henceforth due to the possibility that BG3 may update these values with future patches. Finally, we also need to replicate the GustavDev embedded attributes within the ModuleShortDesc as its values may change as well–this goes especially for associated values to attribute IDs UUID and Version64.
This current version of the parser is extremely hacky and specifically designed to work with the modsettings.lsx
file structure. I welcome any help on this front, as I'm no XML parsing expert. For the meantime, this solution should at least work for our purposes.
Input sample (default) modsettings.lsx
from BG3 version 4.1.1.4251417:
<?xml version="1.0" encoding="UTF-8"?>
<save>
<version major="4" minor="4" revision="0" build="300"/>
<region id="ModuleSettings">
<node id="root">
<children>
<node id="ModOrder"/>
<node id="Mods">
<children>
<node id="ModuleShortDesc">
<attribute id="Folder" type="LSString" value="GustavDev"/>
<attribute id="MD5" type="LSString" value=""/>
<attribute id="Name" type="LSString" value="GustavDev"/>
<attribute id="UUID" type="FixedString" value="28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8"/>
<attribute id="Version64" type="int64" value="36028797018963968"/>
</node>
</children>
</node>
</children>
</node>
</region>
</save>
We'll need to create our own XMLAttributes structure to store these values:
struct XMLAttributes {
var version: Version
var moduleShortDesc: ModuleShortDesc
struct Version {
var majorString: String
var minorString: String
var revisionString: String
var buildString: String
}
struct ModuleShortDesc {
var folder: Attribute
var md5: Attribute
var name: Attribute
var uuid: Attribute
var version64: Attribute
struct Attribute {
var typeString: String
var valueString: String
}
}
}
Our LsxParserDelegate class will then extract this data, storing them as (kinda) "type-safe(ish)" variables. From there, we can call them as such to help re-generate the modsettings.lsx file anew:
let majorVersion = xmlAttrs.version.majorString
let minorVersion = xmlAttrs.version.minorString
let revisionVersion = xmlAttrs.version.revisionString
let buildVersion = xmlAttrs.version.buildString
let versionXmlString =
"""
<version major="\(majorVersion)" minor="\(minorVersion)" revision="\(revisionVersion)" build="\(buildVersion)"/>
"""
print(versionXmlString)
Output:
<version major="4" minor="4" revision="0" build="300"/>
let gustavDevGeneratedAttributes =
"""
<attribute id="Folder" type="\(gustavDevModule.folder.typeString)" value="\(gustavDevModule.folder.valueString)"/>
<attribute id="MD5" type="\(gustavDevModule.md5.typeString)" value="\(gustavDevModule.md5.valueString)"/>
<attribute id="Name" type="\(gustavDevModule.name.typeString)" value="\(gustavDevModule.name.valueString)"/>
<attribute id="UUID" type="\(gustavDevModule.uuid.typeString)" value="\(gustavDevModule.uuid.valueString)"/>
<attribute id="Version64" type="\(gustavDevModule.version64.typeString)" value="\(gustavDevModule.version64.valueString)"/>
"""
print(gustavDevGeneratedAttributes)
Output:
<attribute id="Folder" type="LSString" value="GustavDev"/>
<attribute id="MD5" type="LSString" value=""/>
<attribute id="Name" type="LSString" value="GustavDev"/>
<attribute id="UUID" type="FixedString" value="28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8"/>
<attribute id="Version64" type="int64" value="36028797018963968"/>
Refer to the specific pull request for more details on this implementation.
As of macOS 14, file management is still possible outside of the App Sandbox. However, PermissionsView.swift has been implemented for the case that individual file and folder permissions are required in the future.
Initially, this mod manager will feature support for downloadable mod folders that contain a .pak
file and an info.json
file (.pakFileWithJson
). This section is mostly for potential future plans.
enum ModType {
case pakFile
case pakFileWithUuid
case pakFileWithJson
case replaceFileStructure
}
.pakFile
ie. Baldur's Gate 3 Mod Fixer
- Mod contents: PAK file
- PAK file simply needs to be placed in
Mods/
folder for it to work
.pakFileWithUuid
ie. UnlockLevelCurve
- Mod contents: PAK file
- Mod description contains UUID values per associated PAK file
- PAK file needs to be placed in
Mods/
folder; UUID key-value must be added to modsettings.lsx
.pakFileWithJson
ie. Faces of Faerun
- Mod contents: PAK file, info.json
- PAK file needs to be placed in
Mods/
folder; JSON contents must be parsed and added to modsettings.lsx
.replaceFileStructure
ie. Level 20 (Multiclass)
- Mod contents: file-folder structure that mimics game data files (
{MOD}/Data/Public/.../file
) - Files need to replace existing files at their exact locations
{MOD}/Data/Public/.../file
→{GAME}/Data/Public/.../file
Requires macOS 14+ (Sonoma and later).
This requirement is mostly due to the SwiftData implementation. This was to ensure that the underlying code will be compatible with macOS for as long as possible. Due to the open source nature of this project, anyone is more than welcome to implement backwards compatibility with the Core Data framework.
- Fix Stuck Loading Main Menu - Fake GustavDev (Nexus)
- Manual Modding in BG3, specifically for MacOS users by u/Dapper-Ad3707 (Reddit)
u/TheMetaHorde
for walking me through mod fundamentals, providing niche cases and being very friendly :)u/Dapper-Ad3707
for some very helpful technical writeups on manual modding methodology