Lua Setup - LaughingLeader-DOS2-Mods/LeaderLib GitHub Wiki
- Prerequisites
- Scripting Setup
- Testing a Script
- Using the Extender Console
- Setting up a LeaderLib Dependency
This tutorial assumes you have generated a new mod project using the Divinity Engine 2 editor tool.
VSCode is the preferred editor for Divinity scripting. Install the following:
Create a new text file somewhere appropriate (wherever you want to store your workspace files outside of your mod folders, such as C:\DOS2Modding\Workspaces
), then rename it to MyModName.code-workspace
, replacing MyModName with whatever your mod's name is.
Edit it with your preferred text editor (Notepad, Notepad++, etc. VSCode may try and open it as a workspace) and enter the following:
{
"folders": [
{
"path": "C:\\Games\\Steam\\steamapps\\common\\Divinity Original Sin 2\\DefEd\\Data\\Mods\\ModName_UUID",
"name": "Mods"
},
{
"path": "C:\\Games\\Steam\\steamapps\\common\\Divinity Original Sin 2\\DefEd\\Data\\Public\\ModName_UUID",
"name": "Public"
},
{
"path": "C\\DOS2Modding\\ExtenderScripts",
"name": "Extender"
}
],
"settings": {
"search.exclude": {
"**/*.raw": true,
"**/Story/*.div": true,
"**/*.osi": true,
"**/*.dat": true
},
"Lua.workspace.library": [
"C:\\DOS2Modding\\ExtenderScripts"
],
"Lua.diagnostics.libraryFiles": "Disable",
"Lua.diagnostics.workspaceRate": 25,
"Lua.diagnostics.workspaceDelay": 5000,
"Lua.workspace.preloadFileSize": 10000,
"Lua.diagnostics.globals": [
"Osi"
]
}
}
This is a basic workspace file that includes two folders - Your mod's Mods\ and Public\ folders. Be sure to replace the path values with the actual path to your mod's folders, replacing \
with \\
.
The last folder, "Extender", should be a folder you create external to your actual mod. We'll use that folder to store extender reference scripts. Including this folder in the workspace allows jumping to a definition/source when CTRL + left clicking functions or types, allowing us to easily view properties/definitions.
This folder must be included in the Lua.workspace.library
setting as well, so the global variables in the extender automatically show up in every lua environment. Without this setting, you will get warnings of an "undefined global" when trying to use the Ext table, and you won't have auto-completion.
Each individual folder in a workspace is treated as its own isolated lua environment. The Lua.workspace.library
setting is what allows Lua globals, from the included folders, to show up these isolated environments. Therefore, if you want a mod or the extender's globals to show up when working on a mod, folders need to be added to Lua.workspace.library
, as adding a folder to the workspace itself won't work for this purpose.
The folder you add extender references to (see Extender References) should be included in this Lua.workspace.library
setting, if you want extender auto-completion.
The search.exclude
setting lets us ignore the various osi compilation files from searching, so if you look for an Osiris function, it should either show your code or story_header.div.
The Lua.diagnostics.workspaceRate
and Lua.diagnostics.workspaceDelay
settings reduce the extension's CPU usage, for better overall performance, though this can be tweaked or removed as desired.
Lua.workspace.preloadFileSize
is needed if your references folder includes the various definitions for Osiris functions as well (all usable in lua).
Lua.diagnostics.globals
is an array of variables to consider as global, even without definition from a script. Including "Osi" here allows you to use it without being warned of an "Undefined global", even if you choose to not include the annotations for the Osi table.
Lua scripting requires the Script Extender, a project that extends scripting in Divinity: Original Sin 2 Definitive Edition to new capabilities. Out of the box it also enables achievements, reduces mod loading times by removing unnecessary mod hashing, fixes some crash-related bugs, and adds a console window (a must-have tool for development), among many other features.
Grab the latest Script Extender Updater if you haven't already. Put DXGI.dll in your Divinity Original Sin 2\DefEd\bin
folder and The Divinity Engine 2\DefEd
folders. Launch the game/editor once so the updater will download the latest extender release.
This file tells the script extender which features to enable for your mod project. You can either create the file manually, or grab the sample.
The file sits in your mod's Mods\ folder, like so:
Divinity Original Sin 2\DefEd\Data\Mods\ModName_UUID\OsiToolsConfig.json
What we want to enable lua scripting are the following settings:
{
"RequiredExtensionVersion": 58,
"ModTable": "REPLACE_ME_WITH_YOUR_MOD_ID",
"FeatureFlags": [
"Lua"
]
}
Replace the ModTable
value with a unique name for your mod. This will be the global table key the extender stores all of your mod's global variables (your mod's 'environment'), so it's very important to set this to a unique name.
With this file set, the extender will enable lua scripting for your mod, and look for the related bootstrap scripts to load.
Property | Description |
---|---|
RequiredExtensionVersion | This is the required extender version, and is used to enable some additional features, so generally this is always the latest. |
ModTable | The table key name used for your mod's scripting environment. Must be unique, and can be accessed in scripting within the global Mods table (Mods.LeaderLib for instance). |
FeatureFlags | An array of specific flags to enable for the mod. |
Feature Flags
ID | Description |
---|---|
Lua | Simply enables Lua scripting. Must have a ModTable set. |
OsirisExtensions | Enables the various NRD Osiris functions. You can use these in lua too, though most things have a better lua equivalent now. |
Preprocessor | This allows you to define blocks of rules as extender-only or no-extender in Osiris scripting. Useful when you need to define Osiris rules that should be disabled/enabled in a no extender/extender context, such as making the extender optional for a mod. |
OsirisExtenderSettings.json is a config file from the user side of the script extender. We want to set this up to enable the console window and developer mode.
Create a new text file and rename it to OsirisExtenderSettings.json
in either the game's bin folder, or the editor folder, in the same folder you placed DXGI.dll.
Place the following text inside:
{
"CreateConsole": true,
"DeveloperMode": true,
"EnableLuaDebugger": true,
"EnableLogging": true,
"LogCompile": true,
"LogFailedCompile": true,
"LogRuntime": true
}
These are some basic settings that will allow better development with the extender. Logging is enabled so we have files we can check when debugging issues or looking at the various rules that can happen.
Property | Description |
---|---|
CreateConsole | Enables the script extender console window, which will output console text and allow running lua code directly in the console. |
DeveloperMode | Can be checked in scripts to enable developer features (Ext.IsDeveloperMode()). |
EnableLuaDebugger | Allows hooking the lua debugger to the game with vscode. |
EnableLogging | Enables logging the various aspects of the game. Keep in mind logs can get fairly big, so keep an eye on your logs folder. Default directory is Documents\Larian Studios\Divinity Original Sin 2 Definitive Edition\Extender Logs
|
LogCompile | Enables logging for the Osiris compiler when it's merging story scripts. Useful to see issues that cause working story errors. |
LogFailedCompile | Enables logging for the Osiris compiler when compiling fails (may be redundant if LogCompile is enabled). |
LogRuntime | Enables logging for the console window / lua scripting. |
LogDirectory | For changing the directory logs are stored in. Make sure to escape \ in json (\\ ). |
Finally with our workspace and the script extender all set up, we can begin to set up actual lua scripting.
First grab copies of the various extender scripts here, and place them in a folder external to your mod (such as C\\DOS2Modding\\ExtenderScripts
). We don't want to include these in our mod, just use them in vscode for referencing and auto-completion.
-
ExtIdeHelpers.lua
The most important script reference, as it includes all the various extender functions, type information, etc. -
Game.Math.lua
Math functions used to calculate things like skill damage and skill damage tooltips, or the function that processes hits, all transcribed in lua. These are usable via the Game.Math global. -
Game.Tooltip.lua
Contains the public API for tooltips listeners, which allow you to manipulate tooltips. If you plan on using LeaderLib, skip this, as LeaderLib has an extended version with more tooltip support (including controller support).
With the release of v56 of the extender, this file can now be generated.
Generate the file in-game by entering this in the extender console window:
Ext.Types.GenerateIdeHelpers("ExtIdeHelpers.lua")
With the above line, ExtIdeHelpers.lua will be created in this folder:
Documents\Larian Studios\Divinity Original Sin 2 Definitive Edition\Osiris Data\ExtIdeHelpers.lua
If you want Osiris scripting definitions in Lua, grab the following reference scripts (again, not including these in your mod at all):
-
calls.lua
All the Osiris calls, such as CharacterAddSkill. -
queries.lua
All the Osiris queries, such as CharacterHasSkill. -
nrd.lua
All the various NRD (Script Extender) Osiris functions, whether they be a call or query.
Custom procs/queries can also be used, but Osiris-related calls/queries/procs must be used in an Osiris script somewhere for Lua to be able to use it, even if the Osiris script in question never actually activates. LeaderLib already handles making everything Osiris-related work in Lua.
To finally begin lua scripting, we need to create the two script files the extender looks for. These are our entry points into lua scripting, and from there we can load other scripts.
Create the Lua script folder:
Mods\ModName_UUID\Story\RawFiles\Lua\
Then create two text files, and rename them to BootstrapServer.lua and BootstrapClient.lua:
Mods\ModName_UUID\Story\RawFiles\Lua\BootstrapServer.lua
Mods\ModName_UUID\Story\RawFiles\Lua\BootstrapClient.lua
The extender will attempt to load BootstrapServer.lua
on the server side, and BootstrapClient.lua
on the client side. In a multiplayer context, the host is both the server and a client, and all players connecting are clients. In singleplayer, both the server and client sides co-exist.
Required Scripts
Name | State |
---|---|
BootstrapServer.lua |
Server Side |
BootstrapClient.lua |
Client Side |
From here, these scripts can load other scripts with Ext.Require
. The path to scripts are relative to the Lua folder, so if you had a file setup like this:
Mods\ModName_UUID\Story\RawFiles\Lua\BootstrapClient.lua
Mods\ModName_UUID\Story\RawFiles\Lua\BootstrapServer.lua
Mods\ModName_UUID\Story\RawFiles\Lua\Client\Tooltips.lua
Mods\ModName_UUID\Story\RawFiles\Lua\Server\SkillMechanics.lua
BootstrapServer would load SkillMechanics.lua
with Ext.Require("Server/SkillMechanics.lua")
, and BootstrapClient would load Tooltips.lua
with Ext.Require("Client/Tooltips.lua")
. Script loading only needs to happen once.
I recommend creating a script both bootstraps load:
Mods\ModName_UUID\Story\RawFiles\Lua\Shared.lua
Ext.Require("Shared.lua")
With this script created, let's put some code inside so we can make sure our setup works:
Ext.Events.StatsLoaded:Subscribe(function()
local info = Ext.Mod.GetModInfo(ModuleUUID)
Ext.Utils.Print(string.format("[%s][%s] StatsLoaded running. [%s]", info.Name, ModuleUUID, Ext.IsClient() and "CLIENT" or "SERVER"))
end)
This code registers a function to the "StatsLoaded" listener, which runs when you're loading into the main menu on the client, and when you're loading a save or starting a new game on the server.
ModuleUUID
is a global variable the extender automatically sets for each mod. This is your mod's UUID, stored in your mod's individual scripting environment.
Run the game with your mod active, and you should see text like this in the extender console:
[LeaderLib - Definitive Edition][7e737d2f-31d2-4751-963f-be6ccc59cd0c] StatsLoaded running. [CLIENT]
Next, load a save or start a new game, and let's get accustomed to the workflow of lua scripting.
Once in-game, let's modify our Shared.lua
script and add a new listener functions:
Ext.Events.SessionLoaded:Subscribe(function(e)
Ext.Utils.Print(string.format("[%s] SessionLoaded running. [%s]", Ext.Mod.GetModInfo(ModuleUUID).Name, Ext.IsClient() and "CLIENT" or "SERVER"))
local mods = {}
for i,v in ipairs(Ext.Mod.GetLoadOrder()) do
local info = Ext.Mod.GetModInfo(v)
if info then
table.insert(mods, {
Index = i,
UUID = v,
Name = info.Name
})
end
end
Ext.Utils.Print("Loaded mods:")
Ext.Dump(mods)
end)
This will print the list of active mods to the console. Ext.Dump
in particular prints a table or object to the console, in json format.
Now for a very important step.
- Go to the extender console window, and hit enter to enable text input.
- Next, type
silence off
and hit enter to send the command. This will allow print messages to appear in the window while in input mode. 3a. If LeaderLib is active, type!luareset
in the console window. This will reset all scripts after a delay. 3b. If LeaderLib not active, use the default extender command,reset
.
All lua scripts will be reloaded again, and a SessionLoaded event will fire once more. You should see the mod load order get printed on both the server and client sides, like so:
Resetting lua scripts allows you to avoid having to toggle a gift bag or load a save to reload changes, and saves you precious time in the long run.
If using LeaderLib as a dependency, you can register a function that will get called after lua is reset with the !luareset
command. This is useful to run any relevant code that needs to re-run after lua data is reset, such as reinitializing UIs.
Mods.LeaderLib.Events.BeforeLuaReset:Subscribe(function (e)
-- Before lua is reset. Destroy your UI here for instance.
end)
Mods.LeaderLib.Events.LuaReset:Subscribe(function (e)
-- Lua was reset. Re-initialize your UI here.
end)
The script extender consoles allows you to run lua code directly. This is extremely useful as you can use it to test things, or acquire more information.
Let's use the console to add a skill, then change the skill on the fly.
In the console window, enter the following:
CharacterAddSkill(CharacterGetHostCharacter(), "Projectile_EnemyFireball", 1)
This will add the enemy version of Fireball to whichever character we're currently controlling.
Next let's tweak Fireball:
local id = "Projectile_EnemyFireball"; local stat = Ext.Stats.Get(id); stat.DamageType = "Water"; stat.Cooldown = 0; stat.Template = "f9ac4646-243c-4793-b34f-77ab8c79d31c"; Ext.Stats.Sync(id, false)
This will make Fireball do Water damage, and use a different projectile, while having no cooldown.
Let's break down the code:
local id = "Projectile_EnemyFireball";
local stat = Ext.Stats.Get(id);
Here we're assigning a local variable to the skill ID, so we don't have to type out the long name in the other parts of the code. Ext.GetStat returns a stat object for the ID specified.
stat.DamageType = "Water"
stat.Cooldown = 0
stat.Template = "f9ac4646-243c-4793-b34f-77ab8c79d31c"
Ext.Stats.Sync(id, false)
Here we're modifying the skill's attributes, then finally syncing it with Ext.SyncStat. Syncing is necessary for the changes to show up on the client-side as well. The second parameter, false, tells the extender we don't want it to store our changes in the save. We're just testing the skill for now.
All of this allows you to quickly test different skill changes without needing to reload all stats via gift bag toggling or restarting the game, making iterating skill changes easier.
Saving an object's properties to a file is an easy way to analyze data and learn function/variable names.
Ext.DumpExport
is similar to Ext.Dump
, except it outputs the result to a string. From there we can save that string to a json file, like so:
Ext.IO.SaveFile("Dumps/EsvCharacter.json", Ext.DumpExport(Ext.GetCharacter(CharacterGetHostCharacter())))
If you wish to use the various helpers/systems LeaderLib provides, you'll need to set up a dependency.
- Open your meta.lsx file directly in
Mods\ModName_UUID\meta.lsx
. You can open this file in any text editor, such as VSCode. - Locate your Dependencies node in the file, and add the following LeaderLib node to the children, like so:
<node id="Dependencies">
<children>
<node id="ModuleShortDesc">
<attribute id="Folder" value="LeaderLib_543d653f-446c-43d8-8916-54670ce24dd9" type="30" />
<attribute id="MD5" value="" type="23" />
<attribute id="Name" value="LeaderLib - Definitive Edition" type="22" />
<attribute id="UUID" value="7e737d2f-31d2-4751-963f-be6ccc59cd0c" type="22" />
<attribute id="Version" value="387186699" type="4" />
</node>
</children>
</node>
- Save the file.
This will ensure the game loads LeaderLib before you mod, so you can use everything from the Lua scripts immediately.
From here, this dependency will work as long as you have a LeaderLib pak in your Mods folder, even if testing your mod in the editor. Using LeaderLib as a dependency from pak is the recommended method, as it's easier to stay up-to-date via workshop updates, since assets are omitted from the repository.
The next step is to use a LeaderLib helper to configure your mod's environment to use LeaderLib's globals. This makes auto-completion easier, as you'll be able to use the LeaderLib globals directly, instead of through Mods.LeaderLib.
In a script loaded by both client and server (such as "Shared.lua", as detailed below), add the following line of code after defining your mod's globals:
Mods.LeaderLib.Import(Mods.MyModTable)
Replace MyModTable
with the ModTable value you used in OsiToolsConfig.json
. The Import function will setup your mod's scripting environment to check inside LeaderLib's environment when you use a global key that doesn't exist in your mod, such as GameHelpers.
With that, you can use the various LeaderLib helpers directly.
Example from Weapon Expansion:
Notice that the Import function is used after several lines of declaring globals. This is to ensure we don't accidentally overwrite a LeaderLib global using the same name.
To get auto-completion working with LeaderLib's helpers, we'll need to configure a folder for it in the VSCode workspace, just like the extender scripts.
Either clone the LeaderLib repository with git somewhere, or download it directly here:
https://github.com/LaughingLeader-DOS2-Mods/LeaderLib/archive/refs/heads/master.zip
If you're familiar with git, the git cloning method is preferred, as that will allow you to update your local copy with a command.
With the repository copied locally, add the following folder to your VSCode workspace:
Mods\LeaderLib_543d653f-446c-43d8-8916-54670ce24dd9\Story\RawFiles\Lua
Adding this to the .code-workspace file directly may look like this:
{
"path": "G:\\Modding\\DOS2DE\\ExternalMods\\LeaderLib\\Mods\\LeaderLib_543d653f-446c-43d8-8916-54670ce24dd9\\Story\\RawFiles\\Lua",
"name": "LeaderLib"
}
Just like with the extender scripts, we don't want this folder to be included in our mod directly. It's just a reference, so the Lua extension parses it for auto-completion.
You may also wish to add this folder to the global Lua.workspace.library
setting in your VSCode user settings. Press CTRL + Shift + P in VSCode to open the command interface at the top, then enter Open Settings (JSON)
to open your user settings file. Locate the Lua.workspace.library
setting, or add it if it doesn't exist.
Add LeaderLib's Lua folder to the library, like so:
"Lua.workspace.library": [
"G:\\Modding\\DOS2DE\\ExtenderScripts",
"G:\\Modding\\DOS2DE\\ExternalMods\\LeaderLib\\Mods\\LeaderLib_543d653f-446c-43d8-8916-54670ce24dd9\\Story\\RawFiles\\Lua"
]
With these two changes, you may need to restart VSCode. You then should get auto-completion with LeaderLib's globals, such as GameHelpers
, Common
, SkillManager
, StatusManager
, and more.