Oathbreaker Events Quick Guide - BG3-Community-Library-Team/OathFramework GitHub Wiki
Intro
This is a quick summary on how to set up a listener for custom Oath Breaking/Redemption events. This guide involves a bit of Lua know-how, but will attempt to explain the general set-up for the Lua.
There are a couple common acronym's I'll be using:
- Script Extender (SE)
- Community Library (CL)
- Oath Framework (OF)
Useful Resources
These are a few links that will help you further understand how to utilize Script Extender
- Script Extender API documentation
- LaughingLeader's SE Development Set-up Guide
- The #bg3-mods-scripting channel on the official Larian Discord
- CL's Script Extender mod template
Basic Set-up
First Steps
- You'll want to start with a folder structure like this, subbing
MODNAME
with the name of your mod.
Mods/MODNAME/ScriptExtender/
Config.json
OathFrameworkConfig.json
Lua/
BoostrapServer.lua
Globals.lua
Listeners/
_init.lua
DescriptiveNameForListener.lua
- Be sure to do some of the basic setup for your
Config.json
as described in the official SE Documentation as well as the setup described in registering your Subclass forOathFrameworkConfig.json
before proceeding.
Setting up Requirements
Note: From this point onward, we're going to assume the ScriptExtender/Lua
folder to be our root folder.
By default, SE only looks at BootstrapServer.lua.
We need to make sure that our scripts are getting loaded. SE has a a function, Ext.Require()
, which does this for us.
- Add the following lines to
BootstrapServer.lua
:
Ext.Require("Globals.lua")
Ext.Require("Listeners/_init.lua")
- Add the following lines to
Listeners/_init.lua
, replacingFILENAME
with the name of the other lua file in the folder. Be sure to add a line like this to_init.lua
for every .lua file within theListeners
folder *except for_init.lua
:
Ext.Require("Listeners/FILENAME.lua")
Initializing Globals
There are a few values that are always going to remain the same, and that we'll want to access in multiple files. Instead of repeating them, we're going to create a Global Variable. These variables are values we can reference anywhere in our code.
- Add the following to your
Globals.lua
file:
ModuleUUID = "your-mods-uuid-value-here"
PaladinTagId = "PALADIN_6d85ab2d-5c23-498c-a61e-98f05a00177a"
SubclassTagId = "SUBCLASS_TAGNAME_your-subclass-tag-uuid-value"
Defining your Listener
Note: When you see --
in any lua code, that means that anything on that line will be considered a comment, and won't be executed. It's great for notes.
Next, we need to watch for certain events to happen before our Oath Break/Redemption happens.
1/ In your Listeners/FILENAME
file, add the following:
-- Osiris Listener for a Flag being set - convention is RegisterListener(function name as a string, number of parameters,
-- before or after, and an anonymous function containing the logic)
Ext.Osiris.RegisterListener("FlagSet", 3, "after", function (flag, _, dialogInstance)
-- If the Flag set equals the flag for the Hag being given mercy, then...
if flag == "HAG_Hag_State_HagGivenMercy_a3c0a36a-ccce-4f35-7b54-9ab22d6ac534" then
-- Loop through the characters within the specific dialog instance
for _, character in pairs(Osi.DB_DialogPlayers:Get(dialogInstance, _)) do
-- If the character found is tagged as a Devotion Paladin and a Paladin...
if IsTagged(character[1], SubclassTagId) and
IsTagged(character[1], PaladinTagId) then
-- Call the Oath Framework API passing in our mod's UUID, the character ID, their subclass tag, and whether to Break or Redeem them.
Mods.OF.Api.ModifyOath({
modGuids = {ModuleUUID},
CharacterId = character[1],
SubclassTagId = SubclassTagId,
EventType = "Break"})
end
end
end
end)
This effectively watches for the flag indicating that the Hag from Act 1 has been granted mercy, checks to make sure that a player is involved in the event, and if they have the Paladin and Subclass tags, calls the Oath Framework to break the character's Oath.
Improved Method
The above guide is a quick way of doing it, but not ideal if you need multiple events. Here's an example of what you would want to do:
File Structure
The above file structure is good, but we'll want to add a new section to our code, Actions. This section is going to hold functions that handle logic for specific events. This keeps our code cleaner, more understandable, and easier to maintain.
- In your
ScriptExtender/Lua
folder, add a subfolderActions
, and create these two files:_init.lua
andMercyToHag.lua
. Your folder structure should now look like this:
Mods/MODNAME/ScriptExtender/
Config.json
OathFrameworkConfig.json
Lua/
BoostrapServer.lua
Globals.lua
Actions/
_init.lua
MercyToHag.lua
Listeners/
_init.lua
DescriptiveNameForListener.lua
- Modify your
BootstrapServer.lua
file to look like this:
Ext.Require("Globals.lua")
Ext.Require("Actions/_init.lua")
Ext.Require("Listeners/_init.lua")
- In your
Actions/_init.lua
file, add this:
-- We want Actions to be in its own category/table, so it's easier to reference things
Actions = {}
Ext.Require("Actions/MercyToHag.lua")
You'll notice we've added an Actions
variable. We've defined this as a Table - think of it like a section of code that contains other things. We'll be putting our Action functions within this table, but more on that later.
Adding to our Globals
To make things easier, we're going to create another table, this time in our Globals file. This table is going to contain a quick, referenceable Variable that contains the flags we want to look for.
- Add the following to
Globals.lua
:
OF_Flags = {
MercyToHag = "HAG_Hag_State_HagGivenMercy_a3c0a36a-ccce-4f35-7b54-9ab22d6ac534"
}
This Global Variable is a Table, like Actions
. Within it is a Key/Value Pair - the Key is on the left side of the equals sign, while then Value is on the right. Our key, MercyToHag
is identical to the filename of our Action - that's on purpose. You may also notice that the value is the same as that flag we were checking for in our Listener file.
Reorganizing our Listener
Now we need to change how we're handling our Listener. We're going to remove the logic from our listener, and instead have it search through every flag we've defined in our new OF_Flags
Global Variable. Whenever the flag matches, it'll execute the relevant Action function.
-
Rename your listener file to
Listeners/FlagSet.lua
. -
Change the reference to the listener file in
Listeners/_init.lua
to match the new filename. -
Modify
Listeners/FlagSet.lua
to look like this:
-- Osiris Listener for a Flag being set - convention is RegisterListener(function name as a string, number of parameters,
-- before or after, and an anonymous function containing the logic)
Ext.Osiris.RegisterListener("FlagSet", 3, "after", function (flag, _, dialogInstance)
-- Loop through all flags we want to support in our Flags global
for flagKey, supportedFlag in pairs(OF_Flags) do
-- if the supported flag matches the flag our listener hears, call a function to handle the specific flag
if flag == supportedFlag then
Actions[flagKey](dialogInstance)
end
end
end)
Setting up our Actions
Now, whenever our FlagSet
listener detects the MercyToHag flag being set, it will attempt to fire our Action Function. We previously got out Actions/_init.lua
file set up, and created our MercyToHag.lua
file, but we didn't actually create the action, so right now, it'll create an error. Let's fix that. We're going to replicate the logic from our old version of the listener, but we don't need to Register a new listener, or check if the flag matches anymore, because we're handling that within the new listener already.
- In
Actions/MercyToHag.lua
, add this:
-- We've already checked if the flag matches, so we only need the dialog instance
function Actions.MercyToHag(dialogInstance)
-- Loop through the characters within the specific dialog instance
for _, character in pairs(Osi.DB_DialogPlayers:Get(dialogInstance, _) do
-- If the character found is tagged as a Devotion Paladin and a Paladin...
if IsTagged(character[1], SubclassTagId) and
IsTagged(character[1], PaladinTagId) then
-- Call the Oath Framework API passing in our mod's UUID, the character ID, their subclass tag, and whether to Break or Redeem them.
Mods.OF.Api.ModifyOath({
modGuids = {ModuleUUID},
CharacterId = character[1],
SubclassTagId = SubclassTagId,
EventType = "Break"})
end
end
end
By prefixing our function's name with Actions.
, we're creating it within the Actions table, which is what allows our listener to call the Action. The name, as well, must match the Key of the value that we've added to our OF_Flags
Global Table.
Adding additional Oath Break Events
Now that we have this set-up, adding additional Oath Break Events is much simpler. You only need to do 2 things:
-
Add a new entry to
OF_Flags
. -
Add a new
Actions/FILENAME.lua
, replacingFILENAME
with theOF_Flags
entry's Key, which will perform the Oath Break/Redeem event.
Be sure to add an entry to Actions/_init.lua
for each new Action Function you create.
Flags
There is no comprehensive list of flags in the game yet, but there are a few ways to find them.
- Community Library is in the process of indexing flags for easy reference.
Shared
,SharedDev
,Gustav
, andGustavDev
have aPublic/ModuleName/Flags
folder that you can search through manually.- Unpacking
Mods/GustavDev/Story/story.div.osi
using LSLib's story tools, and looking through the story goals.