BG3 Osiris Scripting Introduction

Contents:
Run your first Osiris script in the toolkit
Run your first Osiris script with manual mod making
Responding to events
Adding actions
Types
Adding conditions to listeners
Comments
Databases

Run your first Osiris script in the toolkit

The toolkit has sections called INIT, KB, EXIT - these are equivalent to INITSECTION, KBSECTION, and EXITSECTION in this guide.

Run your first Osiris script with manual mod making

An osiris file contains INITSECTION, KBSECTION, and EXITSECTION, these may otherwise be referred to as KB, INIT and EXIT in this guide.

Responding to events

Osiris is an event based system - everything you do is in response to some event or trigger.

We can listen for an event with
IF
EVENT(...Parameters...)
If an event has 2 parameters, like LevelGameplayStarted((STRING)_LevelName, (INTEGER)_IsEditorMode), you must state 2 parameters. These can be a specific value such a "WLD_Main_A" which says that our block will only respond when that parameters matches the value, a named parameter such as _Level which captures the value for later, or we can ignore that parameter with _. Parameter names start with an underscore, and the first letter is capitalized. Names can be anything and don't have to match those used in references.
IF
EVENT("WLD_Main_A", _)

IF
EVENT(_Level, _)

IF
EVENT(_, _)



A few starter events:

event LevelGameplayStarted((STRING)_LevelName, (INTEGER)_IsEditorMode)
Gameplay just started - useful for things you want to do when the game starts

event CharacterJoinedParty((CHARACTER)_Character)
A character joined the active party - useful to apply effects that you want to apply to all party members

event StatusApplied((GUIDSTRING)_Object, (STRING)_Status, (GUIDSTRING)_Causee, (INTEGER)_StoryActionID)
A status was applied to an object or character. Apply a status in stats, then listen to it in Osiris to trigger Osiris scripts.

event CastedSpell((GUIDSTRING)_Caster, (STRING)_Spell, (STRING)_SpellType, (STRING)_SpellElement, (INTEGER)_StoryActionID)
A character finished casting a spell

References for more events:
https://docs.baldursgate3.game/index.php?title=Category:Osiris_Events
https://mod.io/g/baldursgate3/r/osiris-events

Adding actions

Blocks end with THEN followed by 1 or more actions. Each action ends with a ;

IF
CastedSpell(_Caster, _, _, _, _)
THEN
ApplyStatus(_Caster, "AID", 6.0);

This is our first complete block - this will grant the AID status for 1 turn to anyone who finishes casting a spell.

[Note: the duration in ApplyStatus is a REAL (a.k.a. Float), it must have the .0 to be read as a REAL not an INTEGER, and the duration is measured in seconds. 1 turn = 6 seconds.]

References for Osiris calls, which can be used here:
https://docs.baldursgate3.game/index.php?title=Category:Osiris_Calls
https://mod.io/g/baldursgate3/r/osiris-calls

Adding conditions to listeners

We'll often want more rules about when we respond to an event, which we can do with conditions. These are specified with AND followed by a condition after the event, with no semi-colon.

IF
CastedSpell(_Caster, _, _, _, _)
AND
IsInCombat(_Caster, 1)
THEN
ApplyStatus(_Caster, "AID", 6.0);

With the condition the block only completes if the caster is in combat.

IsInCombat([in](GUIDSTRING)_Entity, [out](INTEGER)_Bool)
The IsInCombat query checks whether a character is combat. It has an [in] parameter, which we must provide, and out [out] parameter. Like with events we can require the OUT parameter to have a specific value, store it in a named variable, or ignore it.

Here the out parameter tells us whether the provided character was in combat, 1 for yes, 0 for no, so we'll just specify that the result must be 1.

References for Osiris queries, which can be used here:
https://docs.baldursgate3.game/index.php?title=Category:Osiris_Queries
https://mod.io/g/baldursgate3/r/osiris-queries

Types

Osiris Calls/Queries/Events accept parameters of certain types, and each variable will have a type that is automatically inferred if not specified.

INTEGER - a whole number
REAL - a number with a decimal point, write these like 1.2
STRING - text. Strings are wrapped in quotes like "Foo"
GUIDSTRING - A UUID, optionally with some description text preceeding it, such as S_Player_Astarion_c7c13742-bacd-460a-8f65-f864fe41f255. GUIDSTRINGS are not wrapped in quotes.
    GUIDSTRING subtypes:
    CHARACTER - a character in the world
    ITEM - an item in the world
    ITEMROOT - an item template

CharacterJoinedParty((CHARACTER)_Character) gives a CHARACTER, while ApplyStatus((GUIDSTRING)_Object, (STRING)_Status, (REAL)_Duration) takes a GUIDSTRING.

Because a CHARACTER is a type of GUIDSTRING, we can write code like this:

IF
CharacterJoinedParty(_Character)
THEN
ApplyStatus(_Character, "JOINED_PARTY", 60.0);

However if we were to combine CastedSpell((GUIDSTRING)_Caster, (STRING)_Spell, (STRING)_SpellType, (STRING)_SpellElement, (INTEGER)_StoryActionID), which provides a GUIDSTRING with SetImmortal((CHARACTER)_Character, (INTEGER)_Bool) which requires a CHARACTER we would get a compile error. Not all GUIDSTRINGs are CHARACTERs. Logically we can resolve this with an IsCharacter([in](GUIDSTRING)_Object, [out](INTEGER)_Bool) check.

IF
CastedSpell(_Caster, _, _, _, _)
AND
IsCharacter(_Caster, 1)
THEN
SetImmortal(_Caster, 1);

However this won't fix the build issue. _Caster has been inferred as a GUIDSTRING, and our check did not change, so the compiler will complain about using it with SetImmortal. Instead we can override the type of _Caster with a (CHARACTER) annotation. This overrules the type of the variable, but has no actual effect on the behaviour of the code. By combining the IsCharacater check and the type annotation together we get code that does both:

IF
CastedSpell(_Caster, _, _, _, _)
AND
IsCharacter((CHARACTER)_Caster, 1)
THEN
SetImmortal(_Caster, 1);

The type annotation can be placed before any _Caster instance here and have the same effect - but I'm choosing to put it with the check that is actually ensuring that it is a CHARACTER.

Comments

Osiris scripts accept comments by writing // comment - these are ignored by the compiler, letting you add notes for later.

Databases

Databases are quite a powerful feature that unlock a lot of capability. A database stores a set of rows of a certain shape.

We can populate a database with values in the INIT section

DB_MY_Spells("Target_Light");
DB_MY_Spells("Shout_Shillelagh");
DB_MY_Spells("Target_TrueStrike");

We can also populate DBs as actions:

IF
CastedSpell(_, _Spell, _, _, _)
THEN
DB_MY_Spells(_Spell);

Once populated we can check for the existence of matching rows, and retrieve their data.

In this example we check that he spell casted is in the DB - if it is found, the code continues, otherwise it does not:

IF
CastedSpell(_Caster, _Spell, _, _, _)
AND
DB_MY_Spells(_Spell)
THEN
ApplyStatus(_Caster, "AID", 6.0);

If you use an unbound variable in a database query, instead of searching for a value, it will retrieve values. When a DB call matches multiple rows, the code will fork, and the will code will run separately for each match. This code applies the LIGHT status to Astarion and Lae'zel when the Target_Light spell is cast:

INITSECTION
DB_MY_Targets(S_Player_Astarion_c7c13742-bacd-460a-8f65-f864fe41f255);
DB_MY_Targets(S_Player_Laezel_58a69333-40bf-8358-1d17-fff240d7fb12);
KBSECTION
IF
CastSpell(_, "Target_Light", _, _, _)
AND
DB_MY_Targets(_Target)
THEN
ApplyStatus(_Target, "LIGHT", 6.0);

We can add conditions or further DB checks after a DB call to narrow down the results. DB_PartyMembers is a game DB containing all current party members. The call to DB_MY_Targets with an unbound variable _Target will fill _Target with values from that DB, and fork the code. The following call to DB_PartyMembers will use the now-bound variable _Target to check that that character is in the party. This code only applies LIGHT to Astarion and/or Lae'zel if they are in the party.:

INITSECTION
DB_MY_Targets(S_Player_Astarion_c7c13742-bacd-460a-8f65-f864fe41f255);
DB_MY_Targets(S_Player_Laezel_58a69333-40bf-8358-1d17-fff240d7fb12);
KBSECTION
IF
CastSpell(_, "Target_Light", _, _, _)
AND
DB_MY_Targets(_Target)
AND
DB_PartyMembers(_Target)
THEN
ApplyStatus(_Target, "LIGHT", 6.0);

Databases can have multiple fields, and bound and unbound variables can be combined to search for specific entries, and get the rest of the data related to them.

This code will retrieve the number associated with companion, check whether they are in the party, and if so, award the caster that much gold.

INITSECTION
// specifying that these are CHARACTERs here to make them compatible with DB_PartyMembers()
DB_MY_Companions((CHARACTER)S_Player_Astarion_c7c13742-bacd-460a-8f65-f864fe41f255, 50);
DB_MY_Companions((CHARACTER)S_Player_Laezel_58a69333-40bf-8358-1d17-fff240d7fb12, 100);
KBSECTION
IF
CastSpell(_Caster, "Target_Light", _, _, _)
AND
IsCharacter((CHARACTER)_Caster, 1)
AND
DB_MY_Companions(_Target, _Gold)
AND
DB_PartyMembers(_Target)
THEN
AddGold(_Caster, _Gold);