diff --git a/Docs/API.md b/Docs/API.md
index 427dfe6a..9ba86e4a 100644
--- a/Docs/API.md
+++ b/Docs/API.md
@@ -1,44 +1,59 @@
-
-### BG3SE Lua API v22 Documentation
-
-### Table of Contents
- - [Getting Started](#getting-started)
- - [Bootstrap Scripts](#bootstrap-scripts)
- - [Client / Server States](#client-server)
- - [Persistent Variables](#persistent-vars)
- - [Console](#console)
- - [Multiline Mode](#multiline-mode)
- - [Saving output to a file](#save-console-output)
-- [General Lua Rules](#lua-general)
- * [Object Scopes](#lua-scopes)
- * [Object Behavior](#lua-objects)
- * [Parameter Passing](#lua-parameters)
- * [Enumerations](#lua-enumerations)
- * [Bitfields](#lua-bitfields)
- * [Events](#lua-events)
- - [Calling Osiris from Lua](#calling-osiris-from-lua)
- * [Calls](#o2l_calls)
- * [Queries](#o2l_queries)
- * [Events](#o2l_events)
- * [PROCs](#o2l_procs)
- * [User Queries](#o2l_qrys)
- * [Databases](#o2l_dbs)
- - [Calling Lua from Osiris](#calling-lua-from-osiris)
- * [Capturing Events/Calls](#l2o_captures)
- - [Stats](#stats)
- - [ECS](#ecs)
- - [Networking](#networking)
- * [NetChannel API](#net-channel-api)
- * [NetChannel Examples](#net-channel-examples)
- * [Utility functions](#net-utils)
- - [Noesis UI](#noesis-ui)
- * [Custom ViewModels](#noesis-viewmodels)
- - [User Variables](#user-variables)
- - [Utility functions](#ext-utility)
- - [JSON Support](#json-support)
- - [Mod Info](#mod-info)
- - [Math Library](#math)
- - [Engine Events](#engine-events)
+# BG3SE Lua API v30 Documentation
+
+## Table of Contents
+- [Getting Started](#getting-started)
+ - [Bootstrap Scripts](#bootstrap-scripts)
+- [Client / Server States](#client-server)
+- [SE Console](#console)
+ - [Multiline Mode](#multiline-mode)
+ - [Saving output to a file](#save-console-output)
+- [General SE Lua Rules](#lua-general)
+ - [Object Scopes](#lua-scopes)
+ - [Object Behavior](#lua-objects)
+ - [Parameter Passing](#lua-parameters)
+ - [Enumerations](#lua-enumerations)
+ - [Bitfields](#lua-bitfields)
+ - [SE Events](#lua-events)
+- [Calling Osiris from Lua](#calling-osiris-from-lua)
+ - [Osiris Calls](#o2l_calls)
+ - [Osiris Queries](#o2l_queries)
+ - [Osiris Events](#o2l_events)
+ - [PROCs](#o2l_procs)
+ - [User Queries](#o2l_qrys)
+ - [Databases](#o2l_dbs)
+- [Calling Lua from Osiris](#calling-lua-from-osiris)
+ - [Capturing Events/Calls](#l2o_captures)
+- [Persistence](#persistence)
+ - [User Variables](#user-variables)
+ - [Mod Variables](#mod-variables)
+ - [Synchronization](#synchronization)
+ - [Caching Behavior](#caching-behavior)
+- [Entity Class](#entity-class)
+ - [Entity subscriptions](#entity-subscriptions)
+- [Helper/Aliased Functions](#helper-functions)
+- [Networking](#networking)
+ - [NetChannel API](#net-channel-api)
+ - [NetChannel Examples](#net-channel-examples)
+ - [Utility functions](#net-utils)
+- [Noesis UI](#noesis-ui)
+ - [Custom ViewModels](#noesis-viewmodels)
+ - [UI input and world interaction](#ui-input-and-world-interaction)
+- [Stats](#stats)
+ - [Stats Objects](#stats-objects)
+ - [Reading/Writing Stat Attributes](#reading-writing-stats)
+- [Input/Output (IO)](#io)
+- [Timers](#timers)
+- [JSON Support](#json-support)
+- [Mod Info](#mod-info)
+- [Utils](#utils)
+- [Audio](#audio)
+- [Localization](#loca)
+- [Templates](#templates)
+- [Static Data](#static-data)
+- [Resources](#resources)
+- [Level](#level)
+- [Math Library](#math)
+- [Engine Events](#engine-events)
## Getting Started
@@ -48,7 +63,7 @@ To start using the extension in your mod, a configuration file must be created t
Create a file at `Mods\YourMod\ScriptExtender\Config.json` with the following contents, then tweak the values as desired:
```json
{
- "RequiredVersion": 1,
+ "RequiredVersion": 29,
"ModTable": "YOUR_MOD_NAME_HERE",
"FeatureFlags": ["Lua"]
}
@@ -59,7 +74,7 @@ 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. |
+| `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.
It has no relation with the `Folder` value from your meta.lsx. |
| `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`:
@@ -68,14 +83,19 @@ The following features are accepted in `FeatureFlags`:
|--|--|
| `Lua` | Enables Lua scripting |
+
+
+
-### Bootstrap Scripts
+### Bootstrap Scripts
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\\ScriptExtender\Lua\` folder.
-```
-```
-**Required Scripts**
+The `Ext.Require` function is the extender's version of the Lua built-in `require` function.
+The function checks if the file at `Mods//ScriptExtender/Lua/` 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.
+
+**Required Scripts**
| Name | State |
|--|--|
| `BootstrapServer.lua` | Server Side |
@@ -103,32 +123,8 @@ Because they run in different environments, server and client states can access
- **S** - The function is only available on the server
- **R** - Restricted; the function is only callable in special contexts/locations
-
-### Persistent Variables
-
-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:
-```lua
-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)
-```
-
-
-## Console
+
+## SE Console
The extender allows commands to be entered to the console window.
@@ -149,7 +145,7 @@ 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`).
+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).
@@ -172,7 +168,7 @@ end
### Saving the console output to a file
-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).
+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:
@@ -192,7 +188,7 @@ The saved file will be located in the Script Extender folder, typically found at
This method provides a convenient way to store and analyze complex console output, allowing for easier debugging and analysis outside of the console environment.
-## General Lua Rules
+## General SE Lua Rules
### Object Scopes
@@ -203,7 +199,7 @@ Example of possible crash:
```lua
local spells = Ext.Entity.Get(...).SpellBook.Spells
-Ext.OnNextTick(function (...)
+Ext.OnNextTick(function (...)
-- Spell might get deleted beforehand
-- POSSIBLE CRASH!
local uuid = spells[2].SpellUUID
@@ -215,7 +211,7 @@ To fix these issues, most `userdata` types are now bound to their enclosing *ext
```lua
local spellbook = Ext.Entity.Get(...).SpellBook
-Ext.OnNextTick(function (...)
+Ext.OnNextTick(function (...)
-- Throws "Attempted to read object of type 'SpellBookEntry' whose lifetime has expired"
local uuid = spellbook.Spells[2].SpellUUID
end)
@@ -226,11 +222,11 @@ This rule also applies to objects you fetch manually during a listener:
```lua
local spellbook
-Ext.Events.SessionLoaded:Subscribe(function (event)
+Ext.Events.SessionLoaded:Subscribe(function (event)
spellbook = Ext.Entity.Get(...).SpellBook
end)
-Ext.OnNextTick(function (...)
+Ext.OnNextTick(function (...)
-- Throws "Attempted to read object of type 'SpellBookEntry' whose lifetime has expired"
local uuid = spellbook.Spells[2].SpellUUID
end)
@@ -283,7 +279,7 @@ end
### Parameter Passing
- 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" for `PropertyContext`)
- String (i.e. `"Target"`) - note that this only supports passing a single value!
@@ -377,8 +373,8 @@ _D(af.WebImmunity) -- false
Bitfields support table-like iteration (i.e. `pairs`/`ipairs`):
```lua
-for k,v in pairs(af) do
- print(k,v)
+for k,v in pairs(af) do
+ print(k,v)
end
-- 1 BleedingImmunity
-- 2 DrunkImmunity
@@ -447,7 +443,7 @@ table.insert(af, "WebImmunity")
```
-## Events
+### SE Events
Subscribing to engine events can be done through the `Ext.Events` table.
@@ -481,20 +477,20 @@ 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.
-### Calls
+### Osiris Calls
Simply call the method from Lua with the same parameters:
```lua
--- Built-in functions are in the global table (_G)
+-- Built-in functions are in each mod's global table (_G). This will not work in the console!
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.
+
-### Queries
+### Osiris Queries
The query behavior is a mirror of the one described in the [Exporting Lua functions to Osiris](#exporting-lua-functions-to-osiris) chapter.
@@ -503,7 +499,7 @@ For queries with zero OUT arguments, the function will return a boolean indicati
local succeeded = SysIsCompleted("TestGoal")
```
-Queries with OUT arguments will have a number of return values corresponding to the number of OUT arguments.
+Queries with OUT arguments will have a number of return values corresponding to the number of OUT arguments.
```lua
-- Single return value
local player = GetHostCharacter()
@@ -512,7 +508,7 @@ local x, y, z = GetPosition(player)
```
-### Events
+### Osiris Events
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.
```lua
@@ -579,7 +575,7 @@ Osi.DB_GiveTemplateFromNpcToPlayerDialogEvent:Delete("CON_Drink_Cup_A_Tea_080d0e
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:
+Parameters:
- `name` is the function or database name
- `arity` is the number of columns for DBs or the number of parameters (both IN and OUT) for functions
- `event` is the type of event to capture; possible values:
@@ -596,276 +592,427 @@ Ext.Osiris.RegisterListener("TurnEnded", 1, "after", function (characterGuid)
end)
```
-
-## Stats (Ext.Stats module)
-
-
-### Ext.Stats.GetStats(type: string): string[]
-
-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`
-
-
-
-## Stats Objects
+## Persistence
-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.
+The Lua state and all local variables are reset after each game reload. To persist data across multiple play sessions, use ModVars, UserVars, or [MCMVars](https://wiki.bg3.community/Tutorials/Mod-Frameworks/mod-configuration-menu#mcm-api-functions). PersistentVars are deprecated and should not be used; prefer one of the aforementioned alternatives instead.
+
+### User variables
-### Ext.Stats.GetStatsLoadedBefore(modGuid: string, type: string): string[]
+v10 adds support for attaching custom properties to entities. These properties support automatic network synchronization between server and clients as well as savegame persistence.
-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"`).
+To use custom variables, the variable name must first be registered with the variable manager:
+```lua
+Ext.Vars.RegisterUserVariable("NRD_Whatever", {
+ Server = true,
+ Client = true,
+ SyncToClient = true
+})
+```
-### Ext.Stats.Create(name: string, type: string, template: string?): StatEntry
+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) |
-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()`.
+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` or `BootstrapClient.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.
- - `name` is the name of stats entry to create; it should be globally unique
- - `type` 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:
+After registration, custom variables can be read/written through the `Vars` property on entities:
```lua
-local stat = Ext.Stats.CreateStat("NRD_Dynamic_Skill", "SkillData", "Rain_Water")
-stat.RainEffect = "RS3_FX_Environment_Rain_Fire_01"
-stat.SurfaceType = "Fire"
-stat:Sync()
+_C().Vars.NRD_Whatever = 123
+Ext.Print(_C().Vars.NRD_Whatever)
```
-### Ext.Stats.Get(stat, [level], [warnOnError]): StatEntry
-
-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.
+
+### Mod variables
-The behavior of getting a table entry is identical to that of `StatGetAttribute` and setting a table entry is identical to `StatSetAttribute`.
+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.
-The `StatSetAttribute` example rewritten using `Stats.Get`:
+To use a mod variable, the variable must first be registered with the variable manager:
```lua
--- 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
+Ext.Vars.RegisterModVariable(ModuleUUID, "VariableName", {
+ Server = true, Client = true, SyncToClient = true
+})
```
-### Reading stat attributes
-
-Stat attributes can be retrieved by reading the appropriate property of the StatEntry object:
+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)`:
```lua
-local spell = Ext.Stats.Get("Shout_FlameBlade")
-local useCosts = spell.UseCosts
+local vars = Ext.Vars.GetModVariables(ModuleUUID)
+Ext.Print(vars.VariableName)
+vars.VariableName = 123
```
-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`.
+`Ext.Vars.SyncModVariables([moduleUuid])` can be called to perform an immediate synchronization of all mod variable changes.
-*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!
+### Synchronization
-### Writing stat attributes
+A variable is only eligible for synchronization if:
+ - Both `Server` and `Client` flags are set
+ - For server to client synchronization, both `WriteableOnServer` and `SyncToClient` flags are set
+ - For client to server synchronization, both `WriteableOnClient` and `SyncToServer` flags are set
-Stat attributes can be updated using simple table assignment:
+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:
+```lua
+_C().Vars.NRD_Whatever = "asd"
+```
+Note: Writes to subproperties of complex types (i.e. tables etc) will not trigger this mechanism! Example:
```lua
-local spell = Ext.Stats.Get("Shout_FlameBlade")
-spell.UseCosts = "BonusActionPoint:1;SpellSlot:1:1:2"
+_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:
+```lua
+local v = _C().Vars.NRD_Whatever
+v.SomeProperty = 123
+_C().Vars.NRD_Whatever = v
```
-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.
+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.
-*Technical note:* The StatEntry object has a `__newindex` metamethod that performs validation and updates the real stats entry in the background.
+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
-Example usage of stats read/write (Disable autocast on all spells):
-```lua
-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
-```
+### Caching behavior
-**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:
+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:
```lua
-local requirements = spell.Requirements
-table.insert(requirements, {Name = "Immobile", Param = -1, Not = false})
--- Reassign table to update Requirements
-spell.Requirements = requirements
+local t1 = _C().Vars.NRD_Whatever
+local t2 = _C().Vars.NRD_Whatever
+t1.Name = "test"
+_D(t2.Name) -- prints "test"
```
-### Stat property type notes
-
-For a list of enumeration types and their possible values see `Public\Shared\Stats\Generated\Structure\Base\ValueLists.txt` or `Enumerations.xml`.
+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:
+```lua
+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()
-#### Flags
+-- 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
+```
-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.
+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:
-Reading flags:
```lua
-local spell = Ext.Stats.Get("Shout_ArmorOfAgathys")
-_D(spell.SpellFlags)
--- Prints:
--- ["HasSomaticComponent", "HasVerbalComponent", "IsSpell"]
+local t1 = _C().Vars.NRD_Whatever
+local t2 = _C().Vars.NRD_Whatever
+t1.Name = "test"
+_D(t2.Name) -- prints nil
```
-Writing flags:
+Variables are immediately serialized to JSON when a `Vars` write occurs; this means that changes to the original reference have no effect after assignment.
+
```lua
-local spell = Ext.Stats.Get("Shout_ArmorOfAgathys")
-spell.SpellFlags = {"HasVerbalComponent", "IsSpell"}
+local t1 = { Name = "t1" }
+_C().Vars.NRD_Whatever = t1
+t1.Name = "t2"
+_D(_C().Vars.NRD_Whatever.Name) -- prints "t1"
```
-#### Requirements
+This also means that changing the value returned from a `Vars` fetch will not affect the stored value:
-`Requirements` and `MemorizationRequirements` are returned in the following format:
-```js
-[
- {
- "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"
- }
-]
+```lua
+local t1 = _C().Vars.NRD_Whatever
+t1.Name = "t1"
+_D(_C().Vars.NRD_Whatever.Name) -- prints "t1"
```
-#### StatsFunctors
-**StatsFunctors are not supported as of v11.**
+
+Deprecated: PersistentVars
-### Ext.Stats.ExtraData
+### Persistent Variables
-`Ext.ExtraData` is an object containing all entries from `Data.txt`.
+PersistentVars store 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*: It is possible to add custom `ExtraData` keys by adding a new `Data.txt` to the mod and then retrieve them using Lua.
+(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:
```lua
-Ext.Utils.Print(Ext.Stats.ExtraData.WisdomTierHigh)
+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)
```
+
+
+
+---
+
+> [!IMPORTANT]
+> The following sections will go over the different modules provided by the Script Extender. Note that this documentation is not exhaustive and may not cover all features.
+>
+> Please refer to the [ExtIdeHelpers](https://github.com/Norbyte/bg3se/blob/main/BG3Extender/IdeHelpers/ExtIdeHelpers.lua) for a more comprehensive and systematic definition/reference of the API.
## ECS
-### TODO - WIP
+Entity and component APIs are available on both client and server.
-## Networking
-
-Deprecated: NetMessages API
+
+## Entity class - `Ext.Entity`
-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.
+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.).
-
-### Sending NetMessages
-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:
+*Technical note:* For a somewhat more detailed description of the ECS system see:
+ - [Entities, components and systems](https://medium.com/ingeniouslysimple/entities-components-and-systems-89c31464240d)
+ - [The Entity-Component-System - An awesome game-design pattern in C++ ](https://www.gamasutra.com/blogs/TobiasStein/20171122/310172/The_EntityComponentSystem__An_awesome_gamedesign_pattern_in_C_Part_1.php)
-**Sending data from the server to the client(s) :**
-```lua
---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)
+### Entity:GetAllComponentNames() : string[]
---If we want to send the message to ALL the clients
-Ext.ServerNet.BroadcastMessage(channel, payload)
+Returns all engine component types (native C++ class names) that the entity has.
---If we wanted to send the message to a specific userId
-local somePeer = 9999
-Ext.ServerNet.PostMessageToUser(somePeer, channel, payload)
+Example:
+```lua
+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)",
+-- ...
+```
---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)
-```
+### Entity:GetAllComponents() : Component[]
-**Sending data from the client to the server :**
+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:
```lua
-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)
+local entity = Ext.Entity.Get(GetHostCharacter())
+_D(entity:GetAllComponents())
+-- Prints:
+-- {
+-- "ActionResources" :
+-- {
+-- "Entity" : "Entity (02c0000100000180)",
+-- "GetReplicationFlags" : "function: 00007FFDE482D5E0",
+-- ...
```
+### Entity:GetComponent(name) : Component?
-
-### Listening for NetMessages
+Returns the specified component if it is attached to the entity. If the component is not present the method returns `nil`.
-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.
+*Note:* This method only returns components whose structure is known to the Script Extender. Components with unknown structure are not returned.
-**Listening for a Message from the Server in the Client Context :**
+*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:
```lua
---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)
+local entity = Ext.Entity.Get(GetHostCharacter())
+_D(entity:GetComponent("DisplayName"))
+-- Prints:
+-- {
+-- "Entity" : "Entity (02c0000100000180)",
+-- "Name" : "Tav",
+-- "NameKey" : "ResStr_669727657",
+-- ...
```
-**Alternatively :**
+The `__index` metamethod of the Entity object is a shorthand for `GetComponent`:
```lua
---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)
+local entity = Ext.Entity.Get(GetHostCharacter())
+-- The two below are equivalent
+local displayName = entity:GetComponent("DisplayName")
+local displayName = entity.DisplayName
```
-*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:
+### Entity:CreateComponent(name) : Component
-```lua
-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)
-```
-
+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.
+
+
+### Entity:Replicate(component)
+
+Marks a component as changed so replication can propagate it.
+
+### Entity:SetReplicationFlags(component, flags, word)
+
+Sets replication behavior flags for a component.
+
+### Entity:GetReplicationFlags(component, word) : flags
+
+Reads replication behavior flags for a component.
+
+### Entity subscriptions
+
+The following methods allow subscribing to component lifecycle events (creation, destruction, modification) and ECS system updates.
+This allows you to react to changes in the entities and execute code based on those changes.
+
+*Note:* normal subscriptions fire immediately. 'Deferred' variants fire at the end of the tick.
+
+### Ext.Entity.Subscribe(componentName, callback, [entity], [order])
+
+Generic component-change subscription.
+
+ - `componentName`: component type (`ExtComponentType`)
+ - `callback`: function called on matching changes
+ - `entity` (optional): restrict to one entity
+ - `order` (optional): ordering key for subscription processing
+
+Returns a subscription id (`uint64`) that can be passed to `Unsubscribe`.
+
+### Ext.Entity.Unsubscribe(subscriptionId)
+
+Unregisters a subscription created by any method below. Returns `true` if removed.
+
+### Ext.Entity.OnChange(...)
+
+Alias? of `Subscribe` for component updates.
+
+`OnChange(componentName, callback, [entity], [order])`
+
+### Ext.Entity.OnCreate(...)
+
+`OnCreate(componentName, callback, [entity], [opt1], [opt2])`
+
+Called when matching component is created/attached.
+
+### Ext.Entity.OnCreateOnce(...)
+
+`OnCreateOnce(componentName, callback, [entity])`
+
+One-shot create subscription; auto-unsubscribes after first match.
+
+### Ext.Entity.OnCreateDeferred(...)
+
+`OnCreateDeferred(componentName, callback, [entity])`
+
+Like `OnCreate`, but callback runs deferred.
+
+### Ext.Entity.OnCreateDeferredOnce(...)
+
+`OnCreateDeferredOnce(componentName, callback, [entity])`
+
+Deferred + one-shot create subscription.
+
+### Ext.Entity.OnDestroy(...)
+
+`OnDestroy(componentName, callback, [entity], [opt1], [opt2])`
+
+Called when matching component is removed/destroyed.
+
+### Ext.Entity.OnDestroyOnce(...)
+
+`OnDestroyOnce(componentName, callback, [entity])`
+
+One-shot destroy subscription; auto-unsubscribes after first match.
-## NetChannel API
+### Ext.Entity.OnDestroyDeferred(...)
+
+`OnDestroyDeferred(componentName, callback, [entity])`
+
+Like `OnDestroy`, but callback runs deferred.
+
+### Ext.Entity.OnDestroyDeferredOnce(...)
+
+`OnDestroyDeferredOnce(componentName, callback, [entity])`
+
+Deferred + one-shot destroy subscription.
+
+### Ext.Entity.OnSystemUpdate(...)
+
+`OnSystemUpdate(systemType, callback, [once])`
+
+Subscribes to ECS system update hooks.
+
+### Ext.Entity.OnSystemPostUpdate(...)
+
+`OnSystemPostUpdate(systemType, callback, [once])`
+
+TODO.
+
+### Entity:IsAlive() : boolean
+
+Returns whether the entity still exists.
+
+### Entity:GetEntityType() : integer
+
+Returns the numeric type ID of the entity.
+(For development purposes only.)
+
+### Entity:GetSalt() : integer
+
+Returns the salt value of the entity handle.
+(For development purposes only.)
+
+### Entity:GetIndex() : integer
+
+Returns the entity index of the entity handle.
+(For development purposes only.)
+
+
+### Helper/aliased functions
+
+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.)
+
+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.
+ - `_D()`: Equivalent to `Ext.Dump()`, an utility function for dumping an expression to console; supports hierarchical dumping of tables and userdata (engine) objects
+ - `_P()`: Equivalent to `Ext.Utils.Print()`
+ - `_C()`: Equivalent to `Ext.Entity.Get(Osi.GetHostCharacter())`
+
+## Networking
+
+Mods can exchange data between the server and client(s) using the NetChannel API.
+
+Note that there is no external networking capability in the Script Extender. SE mods cannot communicate with external servers or clients.
+
+
+### NetChannel API
> This section documents the new **NetChannel API**, which supersedes the legacy/deprecated NetMessage approach.
The NetChannel API provides a small, structured abstraction for request/response and message broadcasting and handling between server/client peers. It makes asynchronous requests easier to write and reason about, and can attach message handlers directly to the named channels.
-### Why NetChannel is better than the legacy approach
+#### Why NetChannel is better than the legacy approach
NetChannel improves ergonomics and safety compared to the deprecated NetMessage API:
@@ -874,7 +1021,7 @@ NetChannel improves ergonomics and safety compared to the deprecated NetMessage
- **Faster local client requests** - old NetMessages were delayed by 1 frame even if the target was the local client (e.g. in single-player).
-### Quick concepts
+#### Quick concepts
* **Channel**: a named communication channel (string identifier).
* **Request / reply**: send a request and receive a response via a callback.
@@ -884,7 +1031,7 @@ NetChannel improves ergonomics and safety compared to the deprecated NetMessage
---
-### API type annotation reference
+#### API type annotation reference
```lua
--- Sets a handler for incoming messages (fire-and-forget)
@@ -908,17 +1055,22 @@ function NetChannel:RequestToClient(data, user, replyCallback) end
```
-### Usage patterns and examples
+#### Usage patterns and examples
This section provides some pseudo-code examples of how to use the NetChannel API under different scenarios.
+It is recommended to create the same channels in both contexts, e.g. within Shared files that both server and client contexts import before their main logic.
-#### 1) Server-side handler that calls Osiris using data from the payload
+##### 1) Server-side handler that calls Osiris using data from the payload
```lua
--- Server side: handle requests
+-- Shared: create channels for both server and client
Channels = {}
-Channels.TemplateAddTo = Net.CreateChannel(ModuleUUID, "TemplateAddTo")
+Channels.TemplateAddTo = Ext.Net.CreateChannel(ModuleUUID, "TemplateAddTo")
+return Channels
+
+---
+-- Server side: handle requests
-- Using SetHandler: note there's no reply callback
Channels.TemplateAddTo:SetHandler(function(data, user)
for _, v in pairs(data.Items) do
@@ -928,6 +1080,8 @@ Channels.TemplateAddTo:SetHandler(function(data, user)
end
end)
+---
+
-- Client side: send message to server
Channels.TemplateAddTo:SendToServer({
Items = { {"item-template-guid-1", 1}, {"item-template-guid-2", 2} },
@@ -935,7 +1089,9 @@ Channels.TemplateAddTo:SendToServer({
})
```
-#### 2) Request / reply (client requests some data from the server)
+In the following examples, channel creation may be partially omitted for brevity.
+
+##### 2) Request / reply (client requests some data from the server)
```lua
-- Server side: handle requests
@@ -953,11 +1109,12 @@ end)
This pattern allows the caller to perform work after the reply arrives without storing temporary state elsewhere, or defining multiple NetMessage channels (old API).
-#### 3) Broadcast & sync (server pushes global state)
+##### 3) Broadcast & sync (server pushes global state)
```lua
+Channels.SyncSettings = Ext.Net.CreateChannel(ModuleUUID, "SyncSettings")
+
-- Client side: message handler for SyncSettings
-Channels.SyncSettings = Net.CreateChannel(ModuleUUID, "SyncSettings")
Channels.SyncSettings:SetHandler(function(data, user)
ModSettings = data.Settings
_P("Received mod settings sync from server")
@@ -967,11 +1124,12 @@ end)
Channels.SyncSettings:Broadcast({ Settings = MCM.GetCurrentSettings() })
```
-#### 4) Targeted messages (server → specific client)
+##### 4) Targeted messages (server → specific client)
```lua
+Channels.ChangeAppearance = Ext.Net.CreateChannel(ModuleUUID, "ChangeAppearance")
+
-- Server side: send data to a specific client (determined by clientId)
-Channels.ChangeAppearance = Net.CreateChannel(ModuleUUID, "ChangeAppearance")
local clientId = ...
Channels.ChangeAppearance:SendToClient({ CCAData = {...} }, clientId)
```
@@ -979,126 +1137,111 @@ Channels.ChangeAppearance:SendToClient({ CCAData = {...} }, clientId)
Only the client of id `clientId` will receive the message.
-
-### Utility functions
-
-### Ext.Net.IsHost()
-
-Returns true if the client it was called from is the host, always return true from the server context.
-
-## Entity class
-
-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.).
+
+Deprecated: NetMessages API
-*Technical note:* For a somewhat more detailed description of the ECS system see:
- - [Entities, components and systems](https://medium.com/ingeniouslysimple/entities-components-and-systems-89c31464240d)
- - [The Entity-Component-System - An awesome game-design pattern in C++ ](https://www.gamasutra.com/blogs/TobiasStein/20171122/310172/The_EntityComponentSystem__An_awesome_gamedesign_pattern_in_C_Part_1.php)
+NOTE: The NetMessages API is deprecated; it is strongly recommended to use the NetChannel API instead.
+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.
-### Entity:GetAllComponentNames() : string[]
+
-Returns all engine component types (native C++ class names) that the entity has.
+### Sending NetMessages
+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:
-Example:
+**Sending data from the server to the client(s) :**
```lua
-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)",
--- ...
-```
+--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)
-### Entity:GetAllComponents() : Component[]
+--If we want to send the message to ALL the clients
+Ext.ServerNet.BroadcastMessage(channel, payload)
-Returns all components that are attached to the entity.
+--If we wanted to send the message to a specific userId
+local somePeer = 9999
+Ext.ServerNet.PostMessageToUser(somePeer, channel, payload)
-*Note:* This method only returns components whose structure is known to the Script Extender. Components with unknown structure are not returned.
+--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)
-Example:
+```
+
+**Sending data from the client to the server :**
```lua
-local entity = Ext.Entity.Get(GetHostCharacter())
-_D(entity:GetAllComponents())
--- Prints:
--- {
--- "ActionResources" :
--- {
--- "Entity" : "Entity (02c0000100000180)",
--- "GetReplicationFlags" : "function: 00007FFDE482D5E0",
--- ...
+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)
```
-### Entity:GetComponent(name) : Component?
-Returns the specified component if it is attached to the entity. If the component is not present the method returns `nil`.
+
+### Listening for NetMessages
-*Note:* This method only returns components whose structure is known to the Script Extender. Components with unknown structure are not returned.
+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.
-*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.
+**Listening for a Message from the Server in the Client Context :**
-Example:
```lua
-local entity = Ext.Entity.Get(GetHostCharacter())
-_D(entity:GetComponent("DisplayName"))
--- Prints:
--- {
--- "Entity" : "Entity (02c0000100000180)",
--- "Name" : "Tav",
--- "NameKey" : "ResStr_669727657",
--- ...
+--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)
```
-The `__index` metamethod of the Entity object is a shorthand for `GetComponent`:
+**Alternatively :**
```lua
-local entity = Ext.Entity.Get(GetHostCharacter())
--- The two below are equivalent
-local displayName = entity:GetComponent("DisplayName")
-local displayName = entity.DisplayName
+--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.*
-### Entity:CreateComponent(name) : Component
-
-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.
-
-
-### Entity:Replicate(component)
-### Entity:SetReplicationFlags(component, flags, word)
-### Entity:GetReplicationFlags(component, word) : flags
-
-FIXME - DOCUMENT
-
-### Entity:IsAlive() : boolean
-
-Returns whether the entity still exists.
-
-### Entity:GetEntityType() : integer
-
-Returns the numeric type ID of the entity.
-(For development purposes only.)
-
-### Entity:GetSalt() : integer
+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:
-Returns the salt value of the entity handle.
-(For development purposes only.)
+```lua
+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)
+```
+
-### Entity:GetIndex() : integer
+### Utility functions
+
-Returns the entity index of the entity handle.
-(For development purposes only.)
+#### Ext.Net.IsHost()
+Returns true if the client it was called from is the host, always return true from the server context.
-## Noesis UI
-
+## Noesis UI - `Ext.UI`
### Custom ViewModels
@@ -1185,7 +1328,7 @@ local mainMenu = Ext.UI.GetRoot():Find("ContentRoot"):VisualChild(1)
-- Create a wrapper around the original main menu DataContext
local ctx = Ext.UI.Instantiate("se::SAMPLE_MainMenuCtx", mainMenu.DataContext)
-ctx.StartGameCommand:SetHandler(function ()
+ctx.StartGameCommand:SetHandler(function ()
print("do stuff")
end)
@@ -1193,203 +1336,269 @@ end)
mainMenu.DataContext = ctx
```
+### UI input and world interaction
+
+UI is inherently client-side, therefore these APIs are client-side only.
+
+#### Ext.UI.GetPickingHelper(playerId): EclPlayerPickingHelper
+
+Returns the picking helper for `playerId`. Useful for point-and-click logic and world cursor targeting.
+
+Some useful fields:
+ - `Selection`: currently selected in-range entity under cursor;
+ - `Inner.WorldPosition`: world-space hit position;
+ - `WindowCursorPos`: cursor position in window coordinates;
+
+#### Ext.UI.GetCursorControl(): EclCursorControl
+
+TODO:
+
+#### Ext.UI.GetDragDrop(playerId: uint16): EclPlayerDragData
+
+Returns drag-and-drop state for `playerId`.
+
+Some useful fields:
+ - `IsDragging`;
+ - `ScreenPosition`;
+
+
+## Stats - `Ext.Stats`
+
+
+### Ext.Stats.GetStats(type: string): string[]
+
+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`
+
+
+
+### Stats Objects
+
+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.
+
+
+#### Ext.Stats.GetStatsLoadedBefore(modGuid: string, type: string): string[]
+
+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"`).
+
+#### Ext.Stats.Create(name: string, type: string, template: string?): StatEntry
+
+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 unique
+ - `type` 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:
+```lua
+local stat = Ext.Stats.Create("NRD_Dynamic_Skill", "SkillData", "Rain_Water")
+stat.RainEffect = "RS3_FX_Environment_Rain_Fire_01"
+stat.SurfaceType = "Fire"
+stat:Sync()
+```
+
+#### Ext.Stats.Get(stat, [level], [warnOnError]): StatEntry
+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.
-
-## User variables
-
-v10 adds support for attaching custom properties to entities. These properties support automatic network synchronization between server and clients as well as savegame persistence.
+The behavior of getting a table entry is identical to that of `StatGetAttribute` and setting a table entry is identical to `StatSetAttribute`.
-To use custom variables, the variable name must first be registered with the variable manager:
+The `StatSetAttribute` example rewritten using `Stats.Get`:
```lua
-Ext.Vars.RegisterUserVariable("NRD_Whatever", {
- Server = true,
- Client = true,
- SyncToClient = true
-})
+-- 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
```
-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` or `BootstrapClient.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.
+
+#### Reading stat attributes
+Stat attributes can be retrieved by reading the appropriate property of the StatEntry object:
-After registration, custom variables can be read/written through the `Vars` property on entities:
```lua
-_C().Vars.NRD_Whatever = 123
-Ext.Print(_C().Vars.NRD_Whatever)
+local spell = Ext.Stats.Get("Shout_FlameBlade")
+local useCosts = spell.UseCosts
```
-### Synchronization
+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`.
-A variable is only eligible for synchronization if:
- - Both `Server` and `Client` flags are set
- - For server to client synchronization, both `WriteableOnServer` and `SyncToClient` flags are set
- - For client to server synchronization, both `WriteableOnClient` and `SyncToServer` flags are set
+*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!
+
+#### Writing stat attributes
+
+Stat attributes can be updated using simple table assignment:
-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:
```lua
-_C().Vars.NRD_Whatever = "asd"
+local spell = Ext.Stats.Get("Shout_FlameBlade")
+spell.UseCosts = "BonusActionPoint:1;SpellSlot:1:1:2"
```
-Note: Writes to subproperties of complex types (i.e. tables etc) will not trigger this mechanism! Example:
+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):
```lua
-_C().Vars.NRD_Whatever.SomeProperty = 123
+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
```
-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:
+
+**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:
```lua
-local v = _C().Vars.NRD_Whatever
-v.SomeProperty = 123
-_C().Vars.NRD_Whatever = v
+local requirements = spell.Requirements
+table.insert(requirements, {Name = "Immobile", Param = -1, Not = false})
+-- Reassign table to update Requirements
+spell.Requirements = requirements
```
-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
+#### Stat property type notes
+For a list of enumeration types and their possible values see `Public\Shared\Stats\Generated\Structure\Base\ValueLists.txt` or `Enumerations.xml`.
-### Caching behavior
+#### Flags
-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.
+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.
-Example:
+Reading flags:
```lua
-local t1 = _C().Vars.NRD_Whatever
-local t2 = _C().Vars.NRD_Whatever
-t1.Name = "test"
-_D(t2.Name) -- prints "test"
+local spell = Ext.Stats.Get("Shout_ArmorOfAgathys")
+_D(spell.SpellFlags)
+-- Prints:
+-- ["HasSomaticComponent", "HasVerbalComponent", "IsSpell"]
```
-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:
+Writing flags:
```lua
-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
+local spell = Ext.Stats.Get("Shout_ArmorOfAgathys")
+spell.SpellFlags = {"HasVerbalComponent", "IsSpell"}
```
-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:
+##### Requirements
-```lua
-local t1 = _C().Vars.NRD_Whatever
-local t2 = _C().Vars.NRD_Whatever
-t1.Name = "test"
-_D(t2.Name) -- prints nil
+`Requirements` and `MemorizationRequirements` are returned in the following format:
+```js
+[
+ {
+ "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"
+ }
+]
```
-Variables are immediately serialized to JSON when a `Vars` write occurs; this means that changes to the original reference have no effect after assignment.
+##### StatsFunctors
-```lua
-local t1 = { Name = "t1" }
-_C().Vars.NRD_Whatever = t1
-t1.Name = "t2"
-_D(_C().Vars.NRD_Whatever.Name) -- prints "t1"
-```
+**StatsFunctors are not supported as of v29.**
-This also means that changing the value returned from a `Vars` fetch will not affect the stored value:
+#### Ext.Stats.ExtraData
+
+`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:
```lua
-local t1 = _C().Vars.NRD_Whatever
-t1.Name = "t1"
-_D(_C().Vars.NRD_Whatever.Name) -- prints "t1"
+Ext.Utils.Print(Ext.Stats.ExtraData.WisdomTierHigh)
```
-### Mod variables
+
+## I/O - `Ext.IO`
-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.
+Server and client filesystem helpers.
-To use a mod variable, the variable must first be registered with the variable manager:
-```lua
-Ext.Vars.RegisterModVariable(ModuleUUID, "VariableName", {
- Server = true, Client = true, SyncToClient = true
-})
-```
+### Methods
-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.
+- `Ext.IO.LoadFile(path, [context]): string?`
+ Reads file contents. Returns `nil` if the file cannot be read.
+- `Ext.IO.SaveFile(path, content): boolean`
+ Writes content to a file. Creates missing parent directories.
+- `Ext.IO.AddPathOverride(originalPath, newPath)`
+ Redirects game file access from `originalPath` to `newPath`.
+- `Ext.IO.GetPathOverride(path): string?`
+ Returns active override target for `path`, if any.
-The variables for a mod can be accessed by calling `Ext.Vars.GetModVariables(ModuleUUID)`:
+`AddPathOverride` should be called as early as possible (typically `ModuleLoadStarted`), before the original resource is loaded.
+
+Example:
```lua
-local vars = Ext.Vars.GetModVariables(ModuleUUID)
-Ext.Print(vars.VariableName)
-vars.VariableName = 123
+Ext.IO.AddPathOverride("Public/Game/GUI/enemyHealthBar.swf", "Public/YourMod/GUI/enemyHealthBar.swf")
```
-`Ext.Vars.SyncModVariables([moduleUuid])` can be called to perform an immediate synchronization of all mod variable changes.
+
+## Timers - `Ext.Timer`
+Timer and clock helpers.
-
-## Utility functions
+### Delayed execution
-#### Ext.Require(path) R
+#### `Ext.Timer.WaitFor(ms, callback)`
+Uses game clock (pauses when game pauses).
-The `Ext.Require` function is the extender's version of the Lua built-in `require` function.
-The function checks if the file at `Mods//ScriptExtender/Lua/` 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.
+#### `Ext.Timer.WaitForRealtime(ms, callback)`
+Uses OS clock.
-#### Ext.Utils.Print(...)
+Most of the time they are the same, but there are cases when the game timer is paused and time doesn't "progress".
+Game timer can also be affected by the tick throttling logic if the framerate drops too low.
-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.
+#### `Ext.Timer.WaitForPersistent(ms, name, callback)`
+Creates a persistent handle that is written to the savegame so your timer survives a save/reload.
-#### Ext.IO.AddPathOverride(originalPath, newPath)
+#### `Ext.Timer.MonotonicTime()`
+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!)
-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.
+### Timer handle control
-Example:
-```lua
-Ext.IO.AddPathOverride("Public/Game/GUI/enemyHealthBar.swf", "Public/YourMod/GUI/enemyHealthBar.swf")
-```
+- `Ext.Timer.Cancel(handle): boolean`
+- `Ext.Timer.Pause(handle): boolean`
+- `Ext.Timer.Resume(handle): boolean`
+- `Ext.Timer.IsPaused(handle): boolean`
+- `Ext.Timer.RegisterPersistentHandler(name, callback)`
-#### Ext.Utils.MonotonicTime()
+### Clock helpers
-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!)
+- `Ext.Timer.GameTime(): number`
+- `Ext.Timer.MicrosecTime(): number`
+- `Ext.Timer.MonotonicTime(): int64`
+- `Ext.Timer.ClockTime(): string`
+- `Ext.Timer.ClockEpoch(): int64`
+
+`Ext.Timer.MonotonicTime()` returns monotonic system time (ms). Decent for profiling. It is not synchronized between peers.
Example:
```lua
-local startTime = Ext.Utils.MonotonicTime()
+local startTime = Ext.Timer.MonotonicTime()
DoLongTask()
-local endTime = Ext.Utils.MonotonicTime()
+local endTime = Ext.Timer.MonotonicTime()
_P("Took: " .. tostring(endTime - startTime) .. " ms")
```
-### Helper functions
-
-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 to `Ext.Dump()`, an utility function for dumping an expression to console; supports hierarchical dumping of tables and userdata (engine) objects
- - `_P()`: Equivalent to `Ext.Utils.Print()`
- - `_C()`: Equivalent to `Ext.Entity.Get(Osi.GetHostCharacter())`
-
-
-## JSON Support
+
+## JSON support - `Ext.Json`
Two functions are provided for parsing and building JSON documents, `Ext.Json.Parse` and `Ext.Json.Stringify`.
@@ -1456,7 +1665,7 @@ Ext.Json.Stringify(val, {
```
-## Mod Info
+## Mod info - `Ext.Mod`
### IsModLoaded(modGuid)
@@ -1473,216 +1682,255 @@ end
Returns the list of loaded module UUIDs in the order they're loaded in.
-### GetModInfo(modGuid)
+### GetBaseMod()
+
+TODO.
+
+### GetMod(modGuid)
+
+Returns detailed information about the specified loaded module.
+
+Returned object type is `Module`:
+- `Info: ModuleInfo`
+- `Dependencies: ModuleShortDesc[]`
+- `Addons: ModuleShortDesc[]`
+- `ModConflicts: ModuleShortDesc[]`
+
+`ModuleInfo` includes fields such as `Name`, `Author`, `Description`, `Directory`, `ModuleUUID`, and `ModVersion`.
-Returns detailed information about the specified (loaded) module.
Example:
```lua
local loadOrder = Ext.Mod.GetLoadOrder()
for k,uuid in pairs(loadOrder) do
- local mod = Ext.Mod.GetModInfo(uuid)
+ local mod = Ext.Mod.GetMod(uuid)
_D(mod)
end
```
-
-## Math library
-
-The extender math library `Ext.Math` contains following functions:
-
-##### Add(a: any, b: any)
-
-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`).
-
-##### Sub(a: any, b: any)
-
-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`).
-
-##### Mul(a: any, b: any)
-
-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`).
-
-##### Div(a: any, b: any)
-
-Divides the two operands. All math types (number/vec3/vec4/mat3x3/mat4x4) are supported.
-
-##### vec3|vec4 Reflect(I: vec3|vec4, N: vec3|vec4)
-
-For the incident vector `I` and surface orientation `N`, returns the reflection direction: `result = I - 2.0 * dot(N, I) * N`.
-
-##### float Angle(a: vec3|vec4, b: vec3|vec4)
-
-Returns the absolute angle between two vectors. Parameters need to be normalized.
-
-##### vec3 Cross(x: vec3, y: vec3)
-
-Returns the cross product of x and y.
-
-##### float Distance(p0: vec3, p1: vec3)
-
-Returns the distance between p0 and p1, i.e., `length(p0 - p1)`.
-
-##### float Dot(x: vec3, y: vec3)
-
-Returns the dot product of x and y.
-
-##### float Length(x: vec3|vec4)
-
-Returns the length of x, i.e., `sqrt(x * x)`.
-
-##### vec3|vec4 Normalize(x: vec3|vec4)
-
-Returns a vector in the same direction as x but with length of 1.
-
-##### float Determinant(x: mat3|mat4)
+### GetModManager(): ModManager
-Return the determinant of a matrix.
+Provides access to the engine's internal ModManager. Useful to get information about load order, dependencies, conflicts, etc.
-##### mat3|mat4 Inverse(x: mat3|mat4)
+`ModManager` commonly used fields:
+- `AvailableMods: Module[]`
+- `LoadOrderedModules: Module[]`
+- `BaseModule: Module`
+- `Settings: ModuleSettings`
-Return the inverse of a matrix.
+
+## Utils - `Ext.Utils`
-##### mat3|mat4 Transpose(x: mat3|mat4)
+General utility helpers.
-Returns the transposed matrix of `x`.
+### Common methods
-##### mat3|mat4 OuterProduct(c: vec3|vec4, r: vec3|vec4)
-
-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`.
-
-##### void Rotate(m: mat3|mat4, angle: float, axis: vec4)
-
-Builds a rotation matrix created from an axis of 3 scalars and an angle expressed in radians.
-
-##### void Translate(m: mat4, translation: vec3)
-
-Transforms a matrix with a translation 4 * 4 matrix created from a vector of 3 components.
-
-##### void Scale(m: mat4, translation: vec3)
-
-Transforms a matrix with a scale 4 * 4 matrix created from a vector of 3 components.
-
-##### mat4 BuildRotation4(v: vec3, angle: float)
-
-Builds a rotation 4 * 4 matrix created from an axis of 3 scalars and an angle expressed in radians.
-
-##### mat3 BuildRotation3(v: vec3, angle: float)
-
-Builds a rotation 3 * 3 matrix created from an axis of 3 scalars and an angle expressed in radians.
+- `Ext.Utils.Version(): int32` - Script Extender API version.
+- `Ext.Utils.GameVersion(): string?` - game version string.
+- `Ext.Utils.GetGameState()` - current game state enum.
+- `Ext.Utils.GetGlobalSwitches(): GlobalSwitches` - exposes a large settings object (`GlobalSwitches`) including fields like `AiEnableSwarm`, `CanAutoSave`, `NrOfAutoSaves`, etc.
+- `Ext.Utils.GetCommandLineParams(): string[]` - CLI arguments used to launch the game, e.g.:
+```[
+ "\"..\\bin\\bg3_dx11.exe\"",
+ "--skip-launcher",
+ "-continueGame",
+ "-externalcrashhandler",
+ "-stats",
+ "0",
+ "-modded",
+ "1"
+]
+```
+- `Ext.Utils.HandleToInteger(handle): int64`
+- `Ext.Utils.IntegerToHandle(value): EntityHandle`
+- `Ext.Utils.IsValidHandle(handle): boolean`
+- `Ext.Utils.ProfileBegin(name)` / `Ext.Utils.ProfileEnd()`
-##### mat4 BuildTranslation(v: vec3)
-Builds a translation 4 * 4 matrix created from a vector of 3 components.
+
+## Audio - `Ext.Audio`
-##### mat4 BuildScale(v: vec3)
+Client-side audio control API (banks, events, RTPC, switches, states).
-Builds a scale 4 * 4 matrix created from 3 scalars.
+### Methods
-##### vec3 ExtractEulerAngles(m: mat3|mat4)
+- `Ext.Audio.LoadBank(bankName): boolean`
+- `Ext.Audio.UnloadBank(bankName): boolean`
+- `Ext.Audio.PrepareBank(bankName): boolean`
+- `Ext.Audio.UnprepareBank(bankName): boolean`
+- `Ext.Audio.LoadEvent(eventName): boolean`
+- `Ext.Audio.UnloadEvent(eventName): boolean`
+- `Ext.Audio.PostEvent(objectHandle, eventName, [flags]): boolean`
+- `Ext.Audio.Stop([objectHandle])`
+- `Ext.Audio.SetRTPC(objectHandle, rtpcName, value, [skipInterpolation]): boolean`
+- `Ext.Audio.GetRTPC(objectHandle, rtpcName): number`
+- `Ext.Audio.ResetRTPC(objectHandle, rtpcName)`
+- `Ext.Audio.SetSwitch(objectHandle, switchGroup, switchState): boolean`
+- `Ext.Audio.SetState(stateGroup, state): boolean`
+- `Ext.Audio.PlayExternalSound(objectHandle, pathOrName, resourceName, codec, [volume]): boolean`
+- `Ext.Audio.PauseAllSounds()`
+- `Ext.Audio.ResumeAllSounds()`
-Extracts the `(X * Y * Z)` Euler angles from the rotation matrix M.
+
+## Localization - `Ext.Loca`
-##### mat4 BuildFromEulerAngles4(angles: vec3)
+Methods for reading and writing localization entries (loca) at runtime.
-Creates a 3D 4 * 4 homogeneous rotation matrix from euler angles `(X * Y * Z)`.
+### Methods
-##### mat3 BuildFromEulerAngles3(angles: vec3)
+- `Ext.Loca.GetTranslatedString(handle, [fallbackText]): string`
+ Returns localized text for a localization handle.
+- `Ext.Loca.UpdateTranslatedString(handle, text): boolean`
+ Updates/overrides translated text for a handle at runtime.
-Creates a 3D 3 * 3 homogeneous rotation matrix from euler angles `(X * Y * Z)`.
+Example:
+```lua
+local text = Ext.Loca.GetTranslatedString("h1234567890abcdef1234567890abcdefg")
+Ext.Loca.UpdateTranslatedString("h1234567890abcdef1234567890abcdefg", text .. " (modified)")
+```
-##### void Decompose(m: mat4, scale: vec3, yawPitchRoll: vec3, translation: vec3)
+
+## Templates - `Ext.Template`
-Decomposes a model matrix to translations, rotation and scale components.
+Template lookup API (e.g. character/item root templates).
-##### float ExtractAxisAngle(m: mat3|mat4, axis: vec3)
+### Client template methods
-Decomposes a model matrix to translations, rotation and scale components.
+- `Ext.Template.GetTemplate(templateId): GameObjectTemplate`
+- `Ext.Template.GetRootTemplate(templateId): GameObjectTemplate`
+- `Ext.Template.GetAllRootTemplates(): table`
-##### mat3 BuildFromAxisAngle3(axis: vec3, angle: float)
-##### mat4 BuildFromAxisAngle4(axis: vec3, angle: float)
+### Server-only additional template methods
-Build a matrix from axis and angle.
+- `Ext.Template.GetLocalTemplate(templateId): GameObjectTemplate`
+- `Ext.Template.GetLocalCacheTemplate(templateId): GameObjectTemplate`
+- `Ext.Template.GetCacheTemplate(templateId): GameObjectTemplate`
+- `Ext.Template.GetAllLocalTemplates(): table`
+- `Ext.Template.GetAllLocalCacheTemplates(): table`
+- `Ext.Template.GetAllCacheTemplates(): table`
-##### vec3|vec4 Perpendicular(x: vec3|vec4, normal: vec3|vec4)
+
+## Static Data - `Ext.StaticData`
-Projects `x` on a perpendicular axis of `normal`.
+Access to static game resources such as Races, Classes, and other UUID-based engine definitions, via resource manager type (`ExtResourceManagerType`).
-##### vec3|vec4 Project(x: vec3|vec4, normal: vec3|vec4)
+### Methods
-Projects `x` on `normal`.
+- `Ext.StaticData.Get(resourceGuid, managerType)`
+- `Ext.StaticData.GetAll(managerType): Guid[]`
+- `Ext.StaticData.GetByModId(managerType, modGuid): Guid[]`
+- `Ext.StaticData.GetSources(managerType): table`
+- `Ext.StaticData.Create(managerType, [resourceGuid])`
-##### float Fract(x: float)
+Common manager types include: `ClassDescription`, `Progression`, `Feat`, `Race`, `Background`, `God`, etc.
-Return `x - floor(x).`
+
+## Resources - `Ext.Resource`
-##### float Trunc(x: float)
+Access to visual resources including Meshes, Materials, and Textures, via resource bank (`ResourceBankType`).
-Returns a value equal to the nearest integer to x whose absolute value is not larger than the absolute value of x.
+### Methods
-##### float Sign(x: float)
+- `Ext.Resource.Get(resourceId, bankType)`
+- `Ext.Resource.GetAll(bankType): FixedString[]`
-Returns 1.0 if `x > 0`, 0.0 if `x == 0`, or -1.0 if `x < 0`.
+Common bank types include `Visual`, `Material`, `Texture`, `Animation`, `Effect`, `Sound`, `Script`, etc.
-##### float Clamp(val: float, minVal: float, maxVal: float)
+
+## Levels, Pathfinding & Physics - `Ext.Level`
-Returns `min(max(x, minVal), maxVal)` for each component in x using the floating-point values minVal and maxVal.
+Contains logic for Raycasting, Pathfinding, and checking entity/tile physics data.
-##### float Lerp(x: float, y: float, a: float)
+### Pathfinding
-Returns `x * (1.0 - a) + y * a`, i.e., the linear blend of x and y using the floating-point value a.
+- `Ext.Level.BeginPathfinding(entity, targetPos, opts): AiPath`
+- `Ext.Level.BeginPathfindingImmediate(entity, targetPos): AiPath`
+- `Ext.Level.FindPath(path): boolean`
+- `Ext.Level.ReleasePath(path)`
+- `Ext.Level.GetPathById(pathId): AiPath`
+- `Ext.Level.GetActivePathfindingRequests(): AiPath[]`
-##### float Acos(x: float)
+### Tile and height queries
-Arc cosine. Returns an angle whose sine is x.
+- `Ext.Level.GetEntitiesOnTile(position): EntityHandle[]`
+- `Ext.Level.GetHeightsAt(x, z): number[]`
+- `Ext.Level.GetTileDebugInfo(position): AiGridLuaTile`
-##### float Asin(x: float)
+### Physics queries
-Arc sine. Returns an angle whose sine is x.
+- Raycasts:
+ - `Ext.Level.RaycastAny(from, to, physicsType, collidesWith, ignoredGroups, maxHits): boolean`
+ - `Ext.Level.RaycastClosest(...): PhxPhysicsHit`
+ - `Ext.Level.RaycastAll(...): PhxPhysicsHitAll`
+- Sweeps:
+ - `Ext.Level.SweepBoxClosest(...)`, `Ext.Level.SweepBoxAll(...)`
+ - `Ext.Level.SweepSphereClosest(...)`, `Ext.Level.SweepSphereAll(...)`
+ - `Ext.Level.SweepCapsuleClosest(...)`, `Ext.Level.SweepCapsuleAll(...)`
+- Overlap tests:
+ - `Ext.Level.TestBox(...) : PhxPhysicsHitAll`
+ - `Ext.Level.TestSphere(...) : PhxPhysicsHitAll`
-##### float Atan(y_over_x: float)
+`PhxPhysicsHit` includes `Position`, `Normal`, `Distance`, `Shape`, `PhysicsGroup`.
- Arc tangent. Returns an angle whose tangent is `y_over_x`.
+
+## Math library - `Ext.Math`
-##### float Atan2(x: float, y: float)
+The extender math library `Ext.Math` contains following functions:
-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.
+| Function | Parameters | Return Type | Description |
+|----------|-----------|-------------|-------------|
+| `Add` | `a: any, b: any` | `any` | Adds two operands. Supports `number`/`vec3`/`vec4`/`mat3x3`/`mat4x4` with mixed types. |
+| `Sub` | `a: any, b: any` | `any` | Subtracts two operands. Supports `number`/`vec3`/`vec4`/`mat3x3`/`mat4x4` with mixed types. |
+| `Mul` | `a: any, b: any` | `any` | Multiplies two operands. Supports `number`/`vec3`/`vec4`/`mat3x3`/`mat4x4` with mixed types. |
+| `Div` | `a: any, b: any` | `any` | Divides two operands. Supports `number`/`vec3`/`vec4`/`mat3x3`/`mat4x4`. |
+| `Reflect` | `I: vec3\|vec4, N: vec3\|vec4` | `vec3\|vec4` | Returns reflection direction: `result = I - 2.0 * dot(N, I) * N`. |
+| `Angle` | `a: vec3\|vec4, b: vec3\|vec4` | `float` | Returns absolute angle between two normalized vectors. |
+| `Cross` | `x: vec3, y: vec3` | `vec3` | Returns the cross product of x and y. |
+| `Distance` | `p0: vec3, p1: vec3` | `float` | Returns distance between p0 and p1: `length(p0 - p1)`. |
+| `Dot` | `x: vec3, y: vec3` | `float` | Returns the dot product of x and y. |
+| `Length` | `x: vec3\|vec4` | `float` | Returns the length of x: `sqrt(x * x)`. |
+| `Normalize` | `x: vec3\|vec4` | `vec3\|vec4` | Returns a vector in the same direction as x with length 1. |
+| `Determinant` | `x: mat3\|mat4` | `float` | Returns the determinant of a matrix. |
+| `Inverse` | `x: mat3\|mat4` | `mat3\|mat4` | Returns the inverse of a matrix. |
+| `Transpose` | `x: mat3\|mat4` | `mat3\|mat4` | Returns the transposed matrix of x. |
+| `OuterProduct` | `c: vec3\|vec4, r: vec3\|vec4` | `mat` | Treats c as column vector and r as row vector, returns `c * r`. |
+| `Rotate` | `m: mat3\|mat4, angle: float, axis: vec4` | `void` | Builds a rotation matrix from axis and angle in radians. |
+| `Translate` | `m: mat4, translation: vec3` | `void` | Transforms a matrix with translation. |
+| `Scale` | `m: mat4, translation: vec3` | `void` | Transforms a matrix with scale. |
+| `BuildRotation4` | `v: vec3, angle: float` | `mat4` | Builds a 4×4 rotation matrix from axis and angle in radians. |
+| `BuildRotation3` | `v: vec3, angle: float` | `mat3` | Builds a 3×3 rotation matrix from axis and angle in radians. |
+| `BuildTranslation` | `v: vec3` | `mat4` | Builds a 4×4 translation matrix from a vector. |
+| `BuildScale` | `v: vec3` | `mat4` | Builds a 4×4 scale matrix from 3 scalars. |
+| `ExtractEulerAngles` | `m: mat3\|mat4` | `vec3` | Extracts `(X * Y * Z)` Euler angles from rotation matrix. |
+| `BuildFromEulerAngles4` | `angles: vec3` | `mat4` | Creates a 4×4 rotation matrix from `(X * Y * Z)` Euler angles. |
+| `BuildFromEulerAngles3` | `angles: vec3` | `mat3` | Creates a 3×3 rotation matrix from `(X * Y * Z)` Euler angles. |
+| `Decompose` | `m: mat4, scale: vec3, yawPitchRoll: vec3, translation: vec3` | `void` | Decomposes a model matrix into translation, rotation, and scale. |
+| `ExtractAxisAngle` | `m: mat3\|mat4, axis: vec3` | `float` | Extracts axis-angle representation from a matrix. |
+| `BuildFromAxisAngle3` | `axis: vec3, angle: float` | `mat3` | Builds a 3×3 rotation matrix from axis and angle. |
+| `BuildFromAxisAngle4` | `axis: vec3, angle: float` | `mat4` | Builds a 4×4 rotation matrix from axis and angle. |
+| `Perpendicular` | `x: vec3\|vec4, normal: vec3\|vec4` | `vec3\|vec4` | Projects x on a perpendicular axis of normal. |
+| `Project` | `x: vec3\|vec4, normal: vec3\|vec4` | `vec3\|vec4` | Projects x on normal. |
+| `Fract` | `x: float` | `float` | Returns `x - floor(x)`. |
+| `Trunc` | `x: float` | `float` | Returns nearest integer to x with absolute value not larger than x. |
+| `Sign` | `x: float` | `float` | Returns `1.0` if `x > 0`, `0.0` if `x == 0`, `-1.0` if `x < 0`. |
+| `Clamp` | `val: float, minVal: float, maxVal: float` | `float` | Returns `min(max(val, minVal), maxVal)`. |
+| `Lerp` | `x: float, y: float, a: float` | `float` | Returns `x * (1.0 - a) + y * a` (linear blend). |
+| `Acos` | `x: float` | `float` | Arc cosine. Returns angle whose cosine is x. |
+| `Asin` | `x: float` | `float` | Arc sine. Returns angle whose sine is x. |
+| `Atan` | `y_over_x: float` | `float` | Arc tangent. Returns angle whose tangent is `y_over_x`. |
+| `Atan2` | `x: float, y: float` | `float` | Arc tangent. Returns angle whose tangent is `y / x`. |
-# Engine Events
-
-
-## Load Events
-
-### ModuleLoadStarted
-
-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
-
-`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
-
-`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
-
-`SessionLoaded` is thrown when the game session was set up.
-
-### ResetCompleted
-
-The `ResetCompleted` event is thrown when a `reset` console command completes on the client or server, indicating that the Lua state was reloaded.
-
-### GameStateChanged
-
-The `GameStateChanged` event indicates that the server/client game state changed (eg. paused, etc).
-
-### Tick
-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
+## Engine and SE Events - `Ext.Events`
-### OnResetCompleted
+You can listen to SE and engine events with `Ext.Events.:Subscribe(fun)`:
-Thrown when a console `reset` command or an `NRD_LuaReset` Osiris call completes.
+| Event | Description |
+|-------|-------------|
+| `ModuleLoadStarted` | Thrown when the engine has started loading mods.
Mod data (stats, localization, root templates, etc.) is not yet loaded, so most mod editing functionality (e.g., `Ext.StatSetAttribute`) is inaccessible.
Purpose: Allow adding filesystem-level hooks using `Ext.IO.AddPathOverride` before mod data is loaded. |
+| `StatsLoaded` | 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` | Thrown when the engine has started setting up a game session (i.e., new game, loading a savegame, or joining a multiplayer game). |
+| `SessionLoaded` | Thrown when the game session was set up. |
+| `ResetCompleted` | Thrown when `Ext.Debug.Reset()` or `reset` console command completes on the client or server.
Indicates that the Lua state was reloaded. |
+| `GameStateChanged` | Indicates that the server/client game state changed (e.g., loading save, paused, main menu, etc.). |
+| `Tick` | 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.
Helper: `Ext.OnNextTick(fun)` registers a handler that is only called on the next tick and is unregistered afterwards. |
diff --git a/Docs/ReleaseNotes.md b/Docs/ReleaseNotes.md
index a4993ada..226fadbb 100644
--- a/Docs/ReleaseNotes.md
+++ b/Docs/ReleaseNotes.md
@@ -1,5 +1,62 @@
# Release Notes
+## v30
+
+### Materials
+- Added support for applying base/overlay materials
+- Added support for reading current instance parameter values
+- Added support for setting parameters on queued materials
+- Added support for setting virtual texture and texture2D material parameters
+- Fixed bug where updating a material with no CB would allocate in an incorrectly sized CB
+
+### Genome
+- Added support for queuing Genome events
+- Added support for more Genome value types: Bool, Float, FloatSet, IntSet, ShortNameSet, StringSet, FixedStringSet
+- Added support for writing Genome variants
+- Fixed crash when reassigning Genome values
+- Fixed AnimationBlueprintSystem mapping
+
+### Mappings
+- Added support for creating non-immediate components
+- Added animation blueprint systems and components
+- Added combat turn blueprint systems and components
+- Added level state components
+- Added effect manager and client effect components
+- Added light system
+- Added camera global switches and improved game camera mappings
+- Improved mapping of visual state types
+- Added visual systems: VisualSystem, VisualChangedSystem, VisualChangeRequestSystem
+- Fixed mappings for various components and systems
+
+### Debugger
+- Fixed crash when running multiple DAP instances
+- Fixed DAP not reporting startup errors to the client
+- Fixed debugger disconnect message spam
+- Fixed race condition when fetching source list from debugger
+- Fixed source reply being sent twice
+
+### UI
+- Added support for reading Noesis types CornerRadius, ICommand, GridLength
+- Fixed crash when DX11 renderer is reinitialized while a frame is being rendered
+- Fixed issue where IMGUI InputText text buffer did not grow after a certain size
+- Fixed crash when reading Noesis symbol properties
+
+### ECS
+- Fixed crash when unregistering client replication handlers
+- Fixed missing type mapping for some components
+- Fixed some logic errors in internal ECS validation
+- Optimized component fetch
+- Worked around VS2026 compiler bug when fetching components
+
+### Misc
+- Fixed validation errors caused by jank UUIDs
+- Added Ext.Stats.GetCachedBoost
+- Use AVX2 & fast float in release builds
+
+## v29
+
+?
+
## v28
- Added support for separate client/server reset to `Ext.Utils.Reset()`
@@ -15,7 +72,7 @@
## v27.1
-- Hotfix 34 InputManager compatibility
+- Hotfix 34 InputManager compatibility
## v27
@@ -131,7 +188,7 @@
### Entity system
- Added `Ext.Entity.Create`, `Destroy`, `RemoveComponent`
- It is now possible to retrieve and enumerate one-frame components that were created in the current frame
-
+
### Input
- Added support for programmatic triggering of input events via `Ext.Input.InjectKeyPress`, `InjectKeyDown`, `InjectKeyUp`