- Getting Started
- Client / Server States
- Console
- General Lua Rules
- Calling Osiris from Lua
- Calling Lua from Osiris
- Stats
- ECS
- Networking
- Custom Variables
- Utility functions
- JSON Support
- Mod Info
- Math Library
- Engine Events
To start using the extension in your mod, a configuration file must be created that describes what features are utilized by your mod.
Create a file at Mods\YourMod\ScriptExtender\Config.json
with the following contents, then tweak the values as desired:
{
"RequiredVersion": 1,
"ModTable": "YOUR_MOD_NAME_HERE",
"FeatureFlags": ["Lua"]
}
Meaning of configuration keys:
Key | Meaning |
---|---|
RequiredVersion |
Osiris Extender version required to run the mod. It is recommended to use the version number of the Script Extender you used for developing the mod since the behavior of new features and backwards compatibility functions depends on this version number. |
ModTable |
Name of the mod in the global mod table (Mods ) when using Lua. This name is required to use Lua scripting, and must be unique. |
FeatureFlags |
A list of features that the mod is using. For performance reasons it is recommended to only list features that are actually in use. |
The following features are accepted in FeatureFlags
:
Value | Meaning |
---|---|
Lua |
Enables Lua scripting |
If Lua is enabled for the mod, the extender will attempt to load BootstrapServer.lua
on the server side, and BootstrapClient.lua
on the client side. These scripts should be created in the Mods\<YOURMOD>\ScriptExtender\Lua\
folder.
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:
BootstrapClient.lua
BootstrapServer.lua
Server/SkillMechanics.lua
BootstrapServer would load SkillMechanics.lua
with Ext.Require("Server/SkillMechanics.lua")
. Script loading only needs to happen once.
See below for further information on the client/server states, as certain scripting functions are only available on a specific side (i.e. only Osiris functions work on the server-side, in unrestricted contexts).
Internally the game is split into two components, a client and a server component. When a new game is started/loaded, a new server is created and client connect to this server. The server component is only created on the host; client components are created on both the host and all peers. Because of this, the game technically always runs in multiplayer. Single player is just a special form of multiplayer where only one local peer is connected to the server.
Osiris and behavior scripts (gamescripts) always run on the server. Since Lua has access to features that require client-side code (UI modification, level scaling formulas, status chances, skill damage calculation, etc.) the extender keeps multiple Lua states: one state for the server and one for each client (including the single player "fake client"). These states are completely separated from each other and cannot access the internal state of each other (Lua globals, functions, variables).
Because they run in different environments, server and client states can access a different set of features. Functions/classes in this document are annotated with the following letters, which indicate where they are available:
- C - The function is only available on the client
- S - The function is only available on the server
- R - Restricted; the function is only callable in special contexts/locations
The Lua state and all local variables are reset after each game reload. For keeping data through multiple play sessions it is possible to store them in the savegame by storing them in the mod-local table Mods[ModTable].PersistentVars
. By default the table is nil
, i.e. a mod should create the table and populate it with data it wishes to store in the savegame. The contents of PersistentVars
is saved when a savegame is created, and restored before the SessionLoaded
event is triggered.
(Note: There is no global PersistentVars
table, i.e. mods that haven't set their ModTable
won't be able to use this feature).
Example:
PersistentVars = {}
...
-- Variable will be restored after the savegame finished loading
function doStuff()
PersistentVars['Test'] = 'Something to keep'
end
function OnSessionLoaded()
-- Persistent variables are only available after SessionLoaded is triggered!
_P(PersistentVars['Test'])
end
Ext.Events.SessionLoaded:Subscribe(OnSessionLoaded)
The extender allows commands to be entered to the console window.
Press <enter>
to enter console mode; in this mode the normal log output is disabled to avoid log spam while typing commands.
Client/server context can be selected by typing client
or server
. This selects in which Lua environment the console commands will execute. By default the console uses the server context.
The reset
command reinitializes the server and client Lua VM.
Typing exit
returns to log mode.
Commands prefixed by a !
will trigger callbacks registered via the RegisterConsoleCommand
function.
Example:
local function testCmd(cmd, a1, a2, ...)
_P("Cmd: " .. cmd .. ", args: ", a1, ", ", a2);
end
Ext.RegisterConsoleCommand("test", testCmd);
The command !test 123 456
will call testCmd("test", 123, 456)
and prints Cmd: test, args: 123, 456
.
Anything else typed in the console will be executed as Lua code in the current context. (eg. typing _P(1234)
will print 123
).
The console has full access to the underlying Lua state, i.e. server console commands can also call builtin/custom Osiris functions, so Osiris calls like AddExplorationExperience(GetHostCharacter(), 100)
are possible using the console.
Variables can be used just like in Lua, i.e. variable in one command can later on be used in another console command. Be careful, console code runs in global context, so make sure console variable names don't conflict with globals (i.e. Mods
, Ext
, etc.)! Don't use local
for console variables, since the lifetime of the local will be one console command. (Each console command is technically a separate chunk).
One unique feature of the console is its multiline mode, which facilitates the input of multiple lines of code at once. To enter multiline mode, start your input with "--[[" followed by pressing the enter key. This signals the console to expect multiple lines of code. Once you've entered all your lines, close the multiline mode with "]]--".
Here's an example demonstrating the usage of multiline mode:
--[[
someTable={1,2,3,4,5}
for key,value in pairs(someTable) do
print(value)
end
]]--
Sometimes, the output of a command in the console might be too lengthy or complex to analyze effectively within the console interface. In such cases, you might prefer to save the output to a file for easier review. This can be accomplished using a combination of two functions: Ext.IO.SaveFile(filename, content) and Ext.DumpExport(object).
Here's how it works:
Ext.IO.SaveFile(filename, content)
: This function allows you to save content to a file with a specified filename. You need to provide the filename and the content you want to save. If the directory specified in the filename does not exist, SaveFile
will automatically create it.
Ext.DumpExport(object)
: This function serializes the given object into a string, which can then be saved to a file using Ext.IO.SaveFile
.
Here's an example illustrating how to save console output to a file:
entity=Ext.Entity.Get("0133f2ad-e121-4590-b5f0-a79413919805") --Wither's UUID, for demonstration purposes
Ext.IO.SaveFile("output.json", Ext.DumpExport(entity:GetAllComponents()))
The saved file will be located in the Script Extender folder, typically found at %localappdata%\Larian Studios\Baldur's Gate 3\Script Extender
This method provides a convenient way to store and analyze complex console output, allowing for easier debugging and analysis outside of the console environment.
Previously, a userdata
(game object passed from the extender to Lua, i.e. Character
, Status
, etc.) returned from an API call or passed via a parameter was valid for an infinite duration. This meant that the object could be accessed anytime, potentially well after the actual object in the engine was destroyed, leading to strange crashes.
Example of possible crash:
local spells = Ext.Entity.Get(...).SpellBook.Spells
Ext.OnNextTick(function (...)
-- Spell might get deleted beforehand
-- POSSIBLE CRASH!
local uuid = spells[2].SpellUUID
end)
To fix these issues, most userdata
types are now bound to their enclosing extender scope. Since the engine always deletes game objects at the end of the game loop, it is guaranteed that eg. a Status or Character won't disappear during a Lua call, but they may be gone afterwards. To rectify this, "smuggling" objects outside of listeners is no longer allowed. Example:
local spellbook = Ext.Entity.Get(...).SpellBook
Ext.OnNextTick(function (...)
-- Throws "Attempted to read object of type 'SpellBookEntry' whose lifetime has expired"
local uuid = spellbook.Spells[2].SpellUUID
end)
This rule also applies to objects you fetch manually during a listener:
local spellbook
Ext.Events.SessionLoaded:Subscribe(function (event)
spellbook = Ext.Entity.Get(...).SpellBook
end)
Ext.OnNextTick(function (...)
-- Throws "Attempted to read object of type 'SpellBookEntry' whose lifetime has expired"
local uuid = spellbook.Spells[2].SpellUUID
end)
Subproperties inherit the lifetime of their parent object, eg. if you keep a reference to one specific spell (local spellbook = Ext.Entity.Get(...).SpellBook.Spells[2]
), its lifetime will expire at the same time as the characters'.
This ensures that no potentially deleted objects are not accessed in risky contexts.
Attempting to read or write properties that don't exist on an object class now lead to a Lua error:
local spellbook = Ext.Entity.Get(...).SpellBook
-- Object of type 'SpellBookComponent' has no property named 'PreparedSpells'
_P(spellbook.PreparedSpells)
The properties and methods of all engine objects can be read using iteration (metatables now support __pairs
):
local spell = Ext.Entity.Get(...).SpellBook.Spells[1]
for property, value in pairs(spell) do
_P(property, value)
end
Stringifying an engine object returns its class and instance ID (i.e. metatables now support __tostring
):
-- Prints "SpellBookEntry (00000209C32D16F0)"
_P(tostring(Ext.Entity.Get(GetHostCharacter()).SpellBook.Spells[1]))
Equality checks on engine objects return whether the two references point to the same object:
_P(Ext.Entity.Get(GetHostCharacter()) == Ext.Entity.Get(GetHostCharacter()))
Array-like engine objects support iteration via ipairs()
and their length can be read using the #
operator (i.e. the __len
and __pairs
metamethods are now supported):
local tags = _C().Tag.Tags
_P("Number of tags: ", #tags)
for i, tag in ipairs(tags) do
_P(i, tag)
end
-
Numeric enum values and numeric bitmask values passed to API calls are validated; a Lua error is thrown if an unsupported enum label or bitfield value is passed.
-
All bitmask parameters (eg.
PropertyContext
) support passing numeric values, strings and tables to specify the flags, i.e. the allowed ways to pass bitmasks are:- Integer (i.e.
3
means "Target and AoE" forPropertyContext
) - String (i.e.
"Target"
) - note that this only supports passing a single value! - Table (i.e.
{"Target", "AoE"}
)
- Integer (i.e.
Enum values returned from functions and enum properties are returned as userdata
(lightcppobject) values instead of string
.
_D(type(_C().CurrentTemplate.BloodSurfaceType)) -- "userdata"
Enum values have Label
, Value
and EnumName
properties that can be queried to fetch the textual name, numeric value and enumeration name respectively.
local bt = _C().CurrentTemplate.BloodSurfaceType
_D(bt.Label) -- "Blood"
_D(bt.Value) -- 16
_D(bt.EnumName) -- "SurfaceType"
Enum values implement __tostring
for backwards compatibility with old string enums
print(_C().CurrentTemplate.BloodSurfaceType) -- "Blood"
Enum values support comparison with other enum values, enum labels (names) and numeric values:
local bt = _C().CurrentTemplate.BloodSurfaceType
_D(bt) -- "Blood"
_D(bt == "Blood") -- true
_D(bt == "something else") -- false
_D(bt == 16) -- true
_D(bt == 15) -- false
_D(bt == Ext.Enums.SurfaceType.Blood) -- true
_D(bt == Ext.Enums.SurfaceType.Web) -- false
Enum properties support assignment of other enum values, enum labels (names) and numeric values:
-- assignment by enum label
_C().CurrentTemplate.BloodSurfaceType = "Blood"
-- assignment by enum value
_C().CurrentTemplate.BloodSurfaceType = 16
-- assignment by enum object
_C().CurrentTemplate.BloodSurfaceType = Ext.Enums.SurfaceType.Blood
Using enum values as table keys turns them into strings for backwards compatibility reasons:
local t = { Web = 123 }
_D(t[Ext.Enums.SurfaceType.Web]) -- prints 123
JSON serialization turns enum values into their string representation.
print(Ext.Json.Stringify(Ext.Enums.SurfaceType.Web))
-- "Web"
Bitfields returned from functions and enum properties are userdata
(lightcppobject) values instead of table
.
_D(type(_C().Stats.AttributeFlags)) -- "userdata"
Bitfields have __Labels
, __Value
and __EnumName
properties that can be queried to fetch a table containing all textual names, a numeric value representing all values and the enumeration name respectively.
local af = _C().Stats.AttributeFlags
_D(af) -- ["SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"]
_D(af.__Labels) -- ["SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"]
_D(af.__Value) -- 137440004096
_D(af.__EnumName) -- "StatAttributeFlags"
They also support querying the state of each bitfield flag (either by label or by numeric value):
local af = _C().Stats.AttributeFlags
_D(af.DrunkImmunity) -- true
_D(af.WebImmunity) -- false
Bitfields support table-like iteration (i.e. pairs
/ipairs
):
for k,v in pairs(af) do
print(k,v)
end
-- 1 BleedingImmunity
-- 2 DrunkImmunity
-- 3 SuffocatingImmunity
Bitfields implement __tostring
that returns a string containing the enum type and all labels:
-- "StatAttributeFlags(SuffocatingImmunity,BleedingImmunity,DrunkImmunity)"
print(_C().Stats.AttributeFlags)
Bitfields support the ~
(bitwise negate) unary operator and the |
(bitwise or), &
(bitwise and) and ~
(bitwise xor) binary operators. All binary operators support bitfields, string bitfield labels, tables of bitfield labels and numeric values as their second operand:
local af = _C().Stats.AttributeFlags
_D(af) -- ["SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"]
_D(~af) -- ["Unstorable", "DisarmedImmunity", "PoisonImmunity", "HastedImmunity", ...]
_D(af & {"DrunkImmunity", "BleedingImmunity"}) -- ["BleedingImmunity", "DrunkImmunity"]
_D(af & Ext.Enums.StatAttributeFlags.DrunkImmunity) -- ["DrunkImmunity"]
_D(af | "FreezeImmunity") -- ["FreezeImmunity", "SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"]
_D(af ~ 0x802) -- ["SuffocatingImmunity", "DrunkImmunity", "BurnImmunity"]
Bitfields support comparison with other bitfields, singular bitfield labels, tables of bitfield labels and numeric values:
local af = _C().Stats.AttributeFlags
_D(af) -- ["SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"]
_D(af == {"SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"}) -- true
_D(af == {"SuffocatingImmunity", "BleedingImmunity"}) -- false
_D(af == 137440004096) -- true
_D(af == 1234) -- false
Bitfields support assignment of other enum values, enum labels (names) and numeric values:
-- assignment by enum label
_C().Stats.AttributeFlags = {"SuffocatingImmunity", "BleedingImmunity"}
-- assignment by enum value
_C().Stats.AttributeFlags = 137440004096
-- assignment by enum object
_C().Stats.AttributeFlags = Ext.Enums.StatAttributeFlags.WebImmunity
-- assigning result of bitfield operation
_C().Stats.AttributeFlags = _C().Stats.AttributeFlags | "WebImmunity"
JSON serialization turns bitfields into an array of textual labels.
print(Ext.Json.Stringify(_C().Stats.AttributeFlags))
-- ["SuffocatingImmunity", "BleedingImmunity", "DrunkImmunity"]
It should be noted that bitfields are always passed by value, so appending or removing elements from them like a table is not possible:
local af = _C().Stats.AttributeFlags
-- throws "attempt to index a userdata value (global 'af')"
af.SuffocatingImmunity = false
-- throws "bad argument #1 to 'insert' (table expected, got light C++ object)"
table.insert(af, "WebImmunity")
Subscribing to engine events can be done through the Ext.Events
table.
Example:
Ext.Events.GameStateChanged:Subscribe(function (e)
_P("State change from " .. e.FromState .. " to " .. e.ToState)
end)
The Subscribe()
method accepts an optional options table that contains additional settings:
Ext.Events.GameStateChanged:Subscribe(handler, {
Priority = 50,
Once = true
})
The Priority
setting determines the order in which subscribers are called; subscribers with lower priority are called first. The default priority is 100.
If the Once
flag is set, the event is only triggered once and the handler is automatically unsubscribed after the first call.
The Subscribe()
method returns a handler index that can be used to cancel the subscription later on:
local handlerId = Ext.Events.GameStateChanged:Subscribe(handler)
...
Ext.Events.GameStateChanged:Unsubscribe(handlerId)
Lua server contexts have a special global table called Osi
that contains every Osiris symbol. In addition, built-in engine functions (calls, queries, events) are also added to the global table.
Simply call the method from Lua with the same parameters:
-- Built-in functions are in the global table (_G)
CharacterResetCooldowns(player)
-- Equivalent to the above
Osi.CharacterResetCooldowns(player)
Implementation detail: Technically, name resolution is only performed when the function is called, since Osiris allows multiple overloads of the same name and the function to call is resolved based on the number of arguments. Because of this, getting any key from the Osi
table will return an object, even if no function with that name exists. Therefore, Osi.Something ~= nil
and similar checks cannot be used to determine whether a given Osiris symbol exists.
The query behavior is a mirror of the one described in the Exporting Lua functions to Osiris chapter.
For queries with zero OUT arguments, the function will return a boolean indicating whether the query succeeded (true) or failed (false).
local succeeded = SysIsCompleted("TestGoal")
Queries with OUT arguments will have a number of return values corresponding to the number of OUT arguments.
-- Single return value
local player = GetHostCharacter()
-- Multiple return values
local x, y, z = GetPosition(player)
Osiris events can be triggered by calling them like a function. Events are not buffered and the event is triggered synchronously, i.e. the function call returns when every Osiris rule that handles the event has finished.
StoryEvent(player, "event name")
Calling PROCs is equivalent to built-in calls, however they are not added to the global table.
Osi.Proc_CharacterFullRestore(player)
User queries (QRY
) behave just like built-in queries do. Since they can't have OUT arguments (i.e. can't return values), the function will just return a boolean indicating whether the query succeeded or not. User queries are not added to the global table.
local succeeded = Osi.Qry_IsHealingStatus("DAMAGE")
Databases can be read using the Get
method. The method checks its parameters against the database and only returns rows that match the query.
The number of parameters passed to Get
must be equivalent to the number of columns in the target database. Each parameter defines an (optional) filter on the corresponding column; if the parameter is nil
, the column is not filtered (equivalent to passing _
in Osiris). If the parameter is not nil
, only rows with matching values will be returned.
Example:
-- Fetch all rows from DB_GiveTemplateFromNpcToPlayerDialogEvent
local rows = Osi.DB_GiveTemplateFromNpcToPlayerDialogEvent:Get(nil, nil, nil)
-- Fetch rows where the first column is CON_Drink_Cup_A_Tea_080d0e93-12e0-481f-9a71-f0e84ac4d5a9
local rows = Osi.DB_GiveTemplateFromNpcToPlayerDialogEvent:Get("CON_Drink_Cup_A_Tea_080d0e93-12e0-481f-9a71-f0e84ac4d5a9", nil, nil)
It is possible to insert new tuples to Osiris databases by calling the DB like a function.
Osi.DB_CharacterAllCrimesDisabled(player)
The Delete
method can be used to delete rows from databases. The number of parameters passed to Delete
must be equivalent to the number of columns in the target database.
Each parameter defines an (optional) filter on the corresponding column; if the parameter is nil
, the column is not filtered (equivalent to passing _
in Osiris). If the parameter is not nil
, only rows with matching values will be deleted.
Example:
-- Delete all rows from DB_GiveTemplateFromNpcToPlayerDialogEvent
Osi.DB_GiveTemplateFromNpcToPlayerDialogEvent:Delete(nil, nil, nil)
-- Delete rows where the first column is CON_Drink_Cup_A_Tea_080d0e93-12e0-481f-9a71-f0e84ac4d5a9
Osi.DB_GiveTemplateFromNpcToPlayerDialogEvent:Delete("CON_Drink_Cup_A_Tea_080d0e93-12e0-481f-9a71-f0e84ac4d5a9", nil, nil)
The Ext.Osiris.RegisterListener(name, arity, event, handler)
function registers a listener that is called in response to Osiris events.
It currently supports capturing events, built-in queries, databases, user-defined PROCs and user-defined QRYs. Capture support for built-in calls will be added in a later version.
Parameters:
name
is the function or database namearity
is the number of columns for DBs or the number of parameters (both IN and OUT) for functionsevent
is the type of event to capture; possible values:before
- Trigger event before a call/DB insert is performedafter
- Trigger event after a call/DB insert is performedbeforeDelete
- Trigger event before a DB delete is performed (databases only!)afterDelete
- Trigger event after a DB delete is performed (databases only!)
handler
is a Lua function that is called when the specified event is triggered. The function receives all parameters of the original DB/function.
Example:
Ext.Osiris.RegisterListener("TurnEnded", 1, "after", function (characterGuid)
_P("TurnEnded- " .. characterGuid)
end)
Returns a table with the names of all stat entries.
When the optional parameter type
is specified, it'll only return stats with the specified type.
The following types are supported: StatusData
, SpellData
, PassiveData
, Armor
, Weapon
, Character
, Object
, SpellSet
, EquipmentSet
, TreasureTable
, TreasureCategory
, ItemGroup
, NameGroup
The following functions are only usable for Spell, Status, Passive, Interrupt, Armor, Weapon, Character and Object stats entries. Other stats types (eg. ItemGroups, TreasureTables) have their own separate sections in the docs and cannot be manipulated using these functions.
Returns a table with the names of all stat entries that were loaded before the specified mod.
This function is useful for retrieving stats that can be overridden by a mod according to the module load order.
When the optional parameter type
is specified, it'll only return stats with the specified type. (The type of a stat entry is specified in the stat .txt file itself (eg. type "StatusData"
).
Creates a new stats entry.
If a stat object with the same name already exists, the specified modifier type is invalid or the specified template doesn't exist, the function returns nil
.
After all stat properties were initialized, the stats entry must be synchronized by calling stat:Sync()
.
name
is the name of stats entry to create; it should be globally uniquetype
is the stats entry type (eg.SkillData
,StatusData
,Weapon
, etc.)- If the
template
parameter is not null, stats properties are copied from the template entry to the newly created entry - If the entry was created on the server,
stat:Sync()
will replicate the stats entry to all clients. If the entry was created on the client,stat:Sync()
will only update it locally.
Example:
local stat = Ext.Stats.CreateStat("NRD_Dynamic_Skill", "SkillData", "Rain_Water")
stat.RainEffect = "RS3_FX_Environment_Rain_Fire_01"
stat.SurfaceType = "Fire"
stat:Sync()
Returns the specified stats entry as an object for easier manipulation.
If the level
argument is not specified or is nil
, the table will contain stat values as specified in the stat entry.
If the level
argument is not nil
, the table will contain level-scaled values for the specified level. A level
value of -1
will use the level specified in the stat entry.
The behavior of getting a table entry is identical to that of StatGetAttribute
and setting a table entry is identical to StatSetAttribute
.
The StatSetAttribute
example rewritten using Stats.Get
:
-- Swap DamageType from Poison to Air on all skills
for i,name in pairs(Ext.Stats.GetStats("SkillData")) do
local stat = Ext.Stats.Get(name)
if stat.DamageType == "Poison" then
stat.DamageType = "Air"
end
end
Stat attributes can be retrieved by reading the appropriate property of the StatEntry object:
local spell = Ext.Stats.Get("Shout_FlameBlade")
local useCosts = spell.UseCosts
If the stat entry doesn't have the specified attribute or the attribute is not supported, nil
is returned.
The list of attributes each stat type supports can be found in Public\Shared\Stats\Generated\Structure\Modifiers.txt
.
Technical note: The StatEntry object has an __index
metamethod that retrieves the stats property; the StatEntry is not a Lua table and shouldn't be treated as such!
Stat attributes can be updated using simple table assignment:
local spell = Ext.Stats.Get("Shout_FlameBlade")
spell.UseCosts = "BonusActionPoint:1;SpellSlot:1:1:2"
This essentially allows on-the-fly changing of data loaded from stat .txt files without having to override the whole stat entry.
If the function is called while the module is loading (i.e. from a ModuleLoading
/StatsLoaded
listener) no additional synchronization is needed. If the function is called after module load, the stats entry must be synchronized with the client via the StatEntry:Sync()
call.
Technical note: The StatEntry object has a __newindex
metamethod that performs validation and updates the real stats entry in the background.
Example usage of stats read/write (Disable autocast on all spells):
for i,name in pairs(Ext.Stats.GetStats("SpellData")) do
local spell = Ext.Stats.Get(name)
if spell.Autocast == "Yes" then
spell.Autocast = "No"
end
end
Note: When modifying stat attributes that are tables (i.e. Requirements
, SpellSuccess
, SpellProperties
etc.) it is not sufficient to just modify the table, the modified table must be reassigned to the stat property:
local requirements = spell.Requirements
table.insert(requirements, {Name = "Immobile", Param = -1, Not = false})
-- Reassign table to update Requirements
spell.Requirements = requirements
For a list of enumeration types and their possible values see Public\Shared\Stats\Generated\Structure\Base\ValueLists.txt
or Enumerations.xml
.
The AttributeFlags
, SpellFlagList
, WeaponFlags
, ResistanceFlags
, PassiveFlags
, ProficiencyGroupFlags
, StatsFunctorContext
, StatusEvent
, StatusPropertyFlags
, StatusGroupFlags
and LineOfSightFlags
enumerations are flags; this means that multiple enumeration values may be assigned to a stats property.
Reading flags:
local spell = Ext.Stats.Get("Shout_ArmorOfAgathys")
_D(spell.SpellFlags)
-- Prints:
-- ["HasSomaticComponent", "HasVerbalComponent", "IsSpell"]
Writing flags:
local spell = Ext.Stats.Get("Shout_ArmorOfAgathys")
spell.SpellFlags = {"HasVerbalComponent", "IsSpell"}
Requirements
and MemorizationRequirements
are returned in the following format:
[
{
"Not" : true, // Negated condition?
"Param" : "Tag", // Parameter; number for ability/attribute level, string for Tag
"Requirement" : "TADPOLE_POWERS_BLOCKED" // Requirement name
},
{
"Not" : true,
"Param" : -1,
"Requirement" : "Immobile"
}
]
StatsFunctors are not supported as of v11.
Ext.ExtraData
is an object containing all entries from Data.txt
.
Note: It is possible to add custom ExtraData
keys by adding a new Data.txt
to the mod and then retrieve them using Lua.
Example:
Ext.Utils.Print(Ext.Stats.ExtraData.WisdomTierHigh)
To exchange data between the server and client(s), we use NetMessages. These can be sent and received from either context to facilitate communication. This allows us to share data between the server and the client(s) and vice versa.
NetMessages can be sent from either the server or client. They consist of a channel and a payload. The channel is a string used to distinguish your messages from others, and the payload is the data being sent. Currently, the payload must be a string. Here are some examples:
Sending data from the server to the client(s) :
--Server context
local channel = "MyModChannel_SomethingSpecific"
local payload = {["somedata"] = somevalue, ["supertable"]={1,2,3,4,5}}
--We need to stringify our payload in this case since it is a table and not a string
payload=Ext.Json.Stringify(payload)
--If we want to send the message to ALL the clients
Ext.ServerNet.BroadcastMessage(channel, payload)
--If we wanted to send the message to a specific userId
local somePeer = 9999
Ext.ServerNet.PostMessageToUser(somePeer, channel, payload)
--If we wanted to send the message to the client controlling a specific character
local someUUID = "c774d764-4a17-48dc-b470-32ace9ce447d" -- Wyll's uuid
Ext.ServerNet.PostMessageToClient(characterUUID, channel, payload)
Sending data from the client to the server :
local channel = "MyModChannel_SomethingSpecific"
local payload = "I'm a cute message"
--No need to stringify since we're sending a simple string
Ext.ClientNet.PostMessageToServer(channel, payload)
To handle incoming messages, we can listen to a channel on either side and use the received data. If the payload was stringified, use Ext.Json.Parse
to convert it back into a table; otherwise, it remains a string.
Listening for a Message from the Server in the Client Context :
--Client context
local channel = "MyModChannel_SomethingSpecific"
Ext.Events.NetMessage:Subscribe(function(data)
if data.Channel == channel then
--Parse the string back into a table if it was stringified
local data = Ext.Json.Parse(data.Payload)
--Do whatever you want with the data in the client context
someFunction(data)
end
end)
Alternatively :
--wrapper for Ext.Events.NetMessage:Subscribe(function(data) ...end)
--which removes the need to check for the channel
Ext.RegisterNetListener(channel, function(channel, payload, userID)
--Parse the string back into a table
local data = Ext.Json.Parse(payload)
--Do whatever you want with the data in the client context
someFunction(data)
end)
The code to listen for messages from the client on the server context would be similar.
Note that the userId
in these examples is actually a peerId. Osiris functions usually expect a different userId, which is typically peerId + 1
. Use the following function to convert between the peerId used by network functions and the userId expected by Osiris functions:
function PeerToUserID(peerID)
-- usually just userid+1
return (u & 0xffff0000) | 0x0001
end
--Example usage, Server context, pretend the client just sent something on the whatever channel
--And that we need to get which character they're controlling
Ext.Events.NetMessage:Subscribe(function(data)
if data.Channel == "whatever" then
local character = Osi.GetCurrentCharacter(PeerToUserID(data.UserID)) -- returns the character the client was using when the client sent the message
_P(character ) --Prints the character of the user the message originates from
end
end)
Returns true if the client it was called from is the host, always return true from the server context.
Game objects in BG3 are called entities. Each entity consists of multiple components that describes certain properties or behaviors of the entity.
The Lua Entity
class is the represntation of an ingame object (eg. character, item, trigger, etc.).
Technical note: For a somewhat more detailed description of the ECS system see:
- Entities, components and systems
- The Entity-Component-System - An awesome game-design pattern in C++
Returns all engine component types (native C++ class names) that the entity has.
Example:
local char = Ext.Entity.Get(GetHostCharacter())
_D(char:GetAllComponentNames())
-- Prints:
-- {
-- "eoc::ActionResourcesComponent" : "eoc::ActionResourcesComponent Object (1c4000010000039e)",
-- "eoc::BackgroundComponent" : "eoc::BackgroundComponent Object (1e000001000003ff)",
-- "eoc::BackgroundPassivesComponent" : "eoc::BackgroundPassivesComponent Object (66c00001000003ff)",
-- ...
Returns all components that are attached to the entity.
Note: This method only returns components whose structure is known to the Script Extender. Components with unknown structure are not returned.
Example:
local entity = Ext.Entity.Get(GetHostCharacter())
_D(entity:GetAllComponents())
-- Prints:
-- {
-- "ActionResources" :
-- {
-- "Entity" : "Entity (02c0000100000180)",
-- "GetReplicationFlags" : "function: 00007FFDE482D5E0",
-- ...
Returns the specified component if it is attached to the entity. If the component is not present the method returns nil
.
Note: This method only returns components whose structure is known to the Script Extender. Components with unknown structure are not returned.
Note: Although the type (character, item, etc.) of the entity cannot be determined directly, it can be inferred from the components that are attached to the entity.
Eg. to check if the entity is a character, an entity:GetComponent("ServerCharacter") ~= nil
check can be used.
Example:
local entity = Ext.Entity.Get(GetHostCharacter())
_D(entity:GetComponent("DisplayName"))
-- Prints:
-- {
-- "Entity" : "Entity (02c0000100000180)",
-- "Name" : "Tav",
-- "NameKey" : "ResStr_669727657",
-- ...
The __index
metamethod of the Entity object is a shorthand for GetComponent
:
local entity = Ext.Entity.Get(GetHostCharacter())
-- The two below are equivalent
local displayName = entity:GetComponent("DisplayName")
local displayName = entity.DisplayName
Attaches a new empty copy of the specified component type to the entity, if one does not exist. The function returns the newly created component.
Note: This method only works for components whose structure is known to the Script Extender. Components with unknown structure are not returned.
FIXME - DOCUMENT
Returns whether the entity still exists.
Returns the numeric type ID of the entity. (For development purposes only.)
Returns the salt value of the entity handle. (For development purposes only.)
Returns the entity index of the entity handle. (For development purposes only.)
v10 adds support for attaching custom properties to entities. These properties support automatic network synchronization between server and clients as well as savegame persistence.
To use custom variables, the variable name must first be registered with the variable manager:
Ext.Vars.RegisterUserVariable("NRD_Whatever", {
Server = true,
Client = true,
SyncToClient = true
})
The RegisterUserVariable
method accepts two parameters, a variable name and an optional list of settings.
The following settings are supported:
Setting | Default | Meaning |
---|---|---|
Server |
true | Variable is present on server entities |
Client |
false | Variable is present on client entities |
WriteableOnServer |
true | Variable can be modified on server side |
WriteableOnClient |
false | Variable can be modified on client side |
Persistent |
true | Variable is written to/restored from savegames |
SyncToClient |
false | Server-side changes to the variable are synced to all clients |
SyncToServer |
false | Client-side changes to the variable are synced to the server |
SyncOnTick |
true | Client-server sync is performed once per game loop tick |
SyncOnWrite |
false | Client-server sync is performed immediately when the variable is written. This is disabled by default for performance reasons. |
DontCache |
false | Disable Lua caching of variable values (see below) |
Usage notes:
- Since variable prototypes are used for savegame serialization, network syncing, etc., they must be registered before the savegame is loaded and every time the Lua context is reset; performing the registration when
BootstrapServer.lua
orBootstrapClient.lua
is loaded is recommended - Although the variables registered server-side and client-side can differ, it is recommended to register all variables on both sides (even if they're server-only or client-only) for consistency
- Variable names, much like Osiris DB names are global; it is recommended to prefix them with your mod name to ensure they're unique
- Variables must be registered with the same settings on both client and server, otherwise various synchronization issues may occur.
- Client-only variables cannot be persistent.
After registration, custom variables can be read/written through the Vars
property on entities:
_C().Vars.NRD_Whatever = 123
Ext.Print(_C().Vars.NRD_Whatever)
A variable is only eligible for synchronization if:
- Both
Server
andClient
flags are set - For server to client synchronization, both
WriteableOnServer
andSyncToClient
flags are set - For client to server synchronization, both
WriteableOnClient
andSyncToServer
flags are set
For a variable to be synchronized, it must be dirtied first. The most straightforward way to perform this is by doing a direct write to the variable:
_C().Vars.NRD_Whatever = "asd"
Note: Writes to subproperties of complex types (i.e. tables etc) will not trigger this mechanism! Example:
_C().Vars.NRD_Whatever.SomeProperty = 123
Since the __newindex
metamethod of the Vars
object is not called, the variable manager does not detect that a change was performed. A simple fix is to reassign the property after modifications were made:
local v = _C().Vars.NRD_Whatever
v.SomeProperty = 123
_C().Vars.NRD_Whatever = v
On each tick of the game loop, variables that were changed during the current tick are collected and sent to the client/server in a batch. Unless configured otherwise (i.e. the SyncOnTick
setting is disabled), this is the default synchronization method.
If a change to a user variable must be visible by the peer before the end of the current tick:
- The
SyncOnWrite
flag can be enabled which ensures that the write is immediately sent to client/server without additional wait time. Ext.Vars.SyncUserVariables()
can be called, which synchronizes all user variable changes that were done up to that point
The variable manager keeps a Lua copy of table variables for performance reasons. This means that instead of unserializing the table from JSON each time the property is accessed, the cached Lua version is returned after the first access. This means that subsequent accesses to the property will return the same reference and writes to the property.
Example:
local t1 = _C().Vars.NRD_Whatever
local t2 = _C().Vars.NRD_Whatever
t1.Name = "test"
_D(t2.Name) -- prints "test"
Cached variables are serialized to JSON when they are first sent to the client/server or when a savegame is created. This means that all changes to a dirtied variable up to the next synchronization point will be visible to peers despite no explicit write being performed to Vars
. Example:
local v = _C().Vars.NRD_Whatever
v.SomeProperty = 123
-- variable is dirtied here
_C().Vars.NRD_Whatever = v
v.SomeProperty = 456
-- client will receive 456
Ext.Vars.SyncUserVariables()
-- client will NOT receive this change since the NRD_Whatever variable is no longer dirtied after sync;
-- another explicit write to Vars.NRD_Whatever must be performed
v.SomeProperty = 789
Variable caching can be disabled by passing the DontCache
flag to RegisterUserVariable
. Uncached variables are unserialized from JSON each time the property is accessed, so each access returns a different copy:
local t1 = _C().Vars.NRD_Whatever
local t2 = _C().Vars.NRD_Whatever
t1.Name = "test"
_D(t2.Name) -- prints nil
Variables are immediately serialized to JSON when a Vars
write occurs; this means that changes to the original reference have no effect after assignment.
local t1 = { Name = "t1" }
_C().Vars.NRD_Whatever = t1
t1.Name = "t2"
_D(_C().Vars.NRD_Whatever.Name) -- prints "t1"
This also means that changing the value returned from a Vars
fetch will not affect the stored value:
local t1 = _C().Vars.NRD_Whatever
t1.Name = "t1"
_D(_C().Vars.NRD_Whatever.Name) -- prints "t1"
Mod variables are the equivalent of user variables for mods; i.e. they store and synchronize a set of variables for each mod. Mod variables are mostly functionally identical to user variables, so only the differences are highlighted here.
To use a mod variable, the variable must first be registered with the variable manager:
Ext.Vars.RegisterModVariable(ModuleUUID, "VariableName", {
Server = true, Client = true, SyncToClient = true
})
Mod variable registrations are kept separate for each mod UUID, so there is no need to use unique prefixes for variables since a mod registering a variable in its own table will have no effect on other mods.
The variables for a mod can be accessed by calling Ext.Vars.GetModVariables(ModuleUUID)
:
local vars = Ext.Vars.GetModVariables(ModuleUUID)
Ext.Print(vars.VariableName)
vars.VariableName = 123
Ext.Vars.SyncModVariables([moduleUuid])
can be called to perform an immediate synchronization of all mod variable changes.
The Ext.Require
function is the extender's version of the Lua built-in require
function.
The function checks if the file at Mods/<ModuleUUID>/ScriptExtender/Lua/<path>
was already loaded; if not, it'll load the file, store the return value of the main chunk and return it to the caller. If the file was already loaded, it'll return the stored return value.
Note: Ext.Require
should only be called during module startup (i.e. when loading BootstrapClient.lua
or BoostrapServer.lua
). Loading Lua files after module startup is deprecated.
Prints the specified value(s) to the debug console. Works similarly to the built-in Lua print()
, except that it also logs the printed messages to the editor messages pane.
Redirects file access from originalPath
to newPath
. This is useful for overriding built-in files or resources that are otherwise not moddable.
Make sure that the override is added as early as possible (preferably in StatsLoaded
), as adding path overrides after the game has already loaded the resource has no effect.
Example:
Ext.IO.AddPathOverride("Public/Game/GUI/enemyHealthBar.swf", "Public/YourMod/GUI/enemyHealthBar.swf")
Returns a monotonic value representing the current system time in milliseconds. Useful for performance measurements / measuring real world time. (Note: This value is not synchronized between peers and different clients may report different time values!)
Example:
local startTime = Ext.Utils.MonotonicTime()
DoLongTask()
local endTime = Ext.Utils.MonotonicTime()
_P("Took: " .. tostring(endTime - startTime) .. " ms")
Some helper functions were added to aid in development. (Please note that using them in mod code is not recommended, they are designed for developer use only.)
_D()
: Equivalent toExt.Dump()
, an utility function for dumping an expression to console; supports hierarchical dumping of tables and userdata (engine) objects_P()
: Equivalent toExt.Utils.Print()
_C()
: Equivalent toExt.Entity.Get(Osi.GetHostCharacter())
Two functions are provided for parsing and building JSON documents, Ext.Json.Parse
and Ext.Json.Stringify
.
Lua types are encoded in JSON (and vice versa) using the following table:
Lua Type | JS Type |
---|---|
nil |
null |
boolean |
boolean |
number (integer) |
number |
number (float) |
number |
string |
string |
table (sequential keys) |
array |
table (non-sequential) |
object |
It is not possible to stringify/parse lightuserdata
, userdata
, function
and thread
values.
Since JSON only supports string object keys, Lua number
(integer/float) keys are saved as string
.
Usage example:
local tab = {
asd = 1234,
arr = {
"ab", "bc", 44
}
}
local json = Ext.Json.Stringify(tab)
_P(json)
local decoded = Ext.Json.Parse(json)
_P(decoded.arr[1])
Expected output:
{
"arr": [
"ab",
"bc",
44
],
"asd" : 1234
}
ab
- The
Stringify
function accepts an optional settings tableStringify(value, [options])
.options
is a table that supports the following keys:Beautify
(bool) - Generate human-readable JSON (i.e. add indents and linebreaks to the output)StringifyInternalTypes
(bool) - Save engine types (handles, coroutines, etc.) as strings instead of throwing an errorIterateUserdata
(bool) - Dump engine objects similarly to tables instead of throwing an error- NOTE: Due to the nature of these objects, neither internal types nor userdata types can be unserialized from a JSON; parsing a JSON with userdata objects will return them as normal tables
AvoidRecursion
(bool) - If an userdata or table is seen multiple times, further instances will be serialized as"*RECURSION*"
; this is helpful when dumping objectsMaxDepth
(int) - Maximum iteration depth
Example:
Ext.Json.Stringify(val, {
Beautify = true,
MaxDepth = 4
})
Returns whether the module with the specified GUID is loaded.
Example:
if (Ext.Mod.IsModLoaded("5cc23efe-f451-c414-117d-b68fbc53d32d"))
_P("Mod loaded")
end
Returns the list of loaded module UUIDs in the order they're loaded in.
Returns detailed information about the specified (loaded) module. Example:
local loadOrder = Ext.Mod.GetLoadOrder()
for k,uuid in pairs(loadOrder) do
local mod = Ext.Mod.GetModInfo(uuid)
_D(mod)
end
The extender math library Ext.Math
contains following functions:
Adds the two operands. All math types (number/vec3/vec4/mat3x3/mat4x4) are supported. Mixing different operand types works in if a reasonable implementation is available (eg. number + vec3
).
Subtracts the two operands. All math types (number/vec3/vec4/mat3x3/mat4x4) are supported. Mixing different operand types works in if a reasonable implementation is available (eg. vec3 - number
).
Multiplies the two operands. All math types (number/vec3/vec4/mat3x3/mat4x4) are supported. Mixing different operand types works in if a reasonable implementation is available (eg. mat3x3 * vec3
).
Divides the two operands. All math types (number/vec3/vec4/mat3x3/mat4x4) are supported.
For the incident vector I
and surface orientation N
, returns the reflection direction: result = I - 2.0 * dot(N, I) * N
.
Returns the absolute angle between two vectors. Parameters need to be normalized.
Returns the cross product of x and y.
Returns the distance between p0 and p1, i.e., length(p0 - p1)
.
Returns the dot product of x and y.
Returns the length of x, i.e., sqrt(x * x)
.
Returns a vector in the same direction as x but with length of 1.
Return the determinant of a matrix.
Return the inverse of a matrix.
Returns the transposed matrix of x
.
Treats the first parameter c
as a column vector and the second parameter r
as a row vector and does a linear algebraic matrix multiply c * r
.
Builds a rotation matrix created from an axis of 3 scalars and an angle expressed in radians.
Transforms a matrix with a translation 4 * 4 matrix created from a vector of 3 components.
Transforms a matrix with a scale 4 * 4 matrix created from a vector of 3 components.
Builds a rotation 4 * 4 matrix created from an axis of 3 scalars and an angle expressed in radians.
Builds a rotation 3 * 3 matrix created from an axis of 3 scalars and an angle expressed in radians.
Builds a translation 4 * 4 matrix created from a vector of 3 components.
Builds a scale 4 * 4 matrix created from 3 scalars.
Extracts the (X * Y * Z)
Euler angles from the rotation matrix M.
Creates a 3D 4 * 4 homogeneous rotation matrix from euler angles (X * Y * Z)
.
Creates a 3D 3 * 3 homogeneous rotation matrix from euler angles (X * Y * Z)
.
Decomposes a model matrix to translations, rotation and scale components.
Decomposes a model matrix to translations, rotation and scale components.
Build a matrix from axis and angle.
Projects x
on a perpendicular axis of normal
.
Projects x
on normal
.
Return x - floor(x).
Returns a value equal to the nearest integer to x whose absolute value is not larger than the absolute value of x.
Returns 1.0 if x > 0
, 0.0 if x == 0
, or -1.0 if x < 0
.
Returns min(max(x, minVal), maxVal)
for each component in x using the floating-point values minVal and maxVal.
Returns x * (1.0 - a) + y * a
, i.e., the linear blend of x and y using the floating-point value a.
Arc cosine. Returns an angle whose sine is x.
Arc sine. Returns an angle whose sine is x.
Arc tangent. Returns an angle whose tangent is y_over_x
.
Arc tangent. Returns an angle whose tangent is y / x
. The signs of x and y are used to determine what quadrant the angle is in.
The ModuleLoadStarted
event is thrown when the engine has started loading mods. Mod data (stats, localization, root templates, etc.) is not yet loaded when this listener is called, so most mod editing functionality (eg. Ext.StatSetAttribute
) is inaccessible.
The purpose of this event is to allow adding filesystem-level hooks using Ext.IO.AddPathOverride
before mod data is loaded.
StatsLoaded
is thrown after stats entries (weapons, skills, etc.) were cleared and subsequently reloaded. Stat modifications that are valid for every game session should be applied here.
SessionLoading
is thrown when the the engine has started setting up a game session (i.e. new game, loading a savegame or joining a multiplayer game).
SessionLoaded
is thrown when the game session was set up.
The ResetCompleted
event is thrown when a reset
console command completes on the client or server, indicating that the Lua state was reloaded.
The GameStateChanged
event indicates that the server/client game state changed (eg. paused, etc).
The Tick
event is thrown after each game engine tick on both the client and the server. Server logic runs at ~30hz, so this event is thrown roughly every 33ms.
- The Ext.OnNextTick(fun)
helper registers a handler that is only called on the next tick and is unregistered afterwards
Thrown when a console reset
command or an NRD_LuaReset
Osiris call completes.