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
- In the Toolkit go to Story Editor
- File > Generated definitions (you only need to do this once)
- File > New, give it a name
- In KB insert IFGainedControl(_Character)THENApplyStatus(_Character, "AID", 6.0);
- File > Build and Reload
- Test it in the game preview by selecting characters (Control+Shift+1 to spawn an extra character)
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
- Create a .txt file in Mods/<YourMod>/Story/RawFiles/Goals/
- Add the skeleton code Version 1SubGoalCombiner SGC_ANDINITSECTIONKBSECTIONEXITSECTIONENDEXITSECTION
- Add a rule in the KBSECTION so your code looks like Version 1SubGoalCombiner SGC_ANDINITSECTIONKBSECTIONIFGainedControl(_Character,)THENApplyStatus(_Character, "AID", 6.0);EXITSECTIONENDEXITSECTION
- .pak it and test it in the game by selecting characters.
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 withIf 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.IFEVENT(...Parameters...)
IFEVENT("WLD_Main_A", _)
IFEVENT(_Level, _)
IFEVENT(_, _)
A few starter events:
Gameplay just started - useful for things you want to do when the game startsevent LevelGameplayStarted((STRING)_LevelName, (INTEGER)_IsEditorMode)
A character joined the active party - useful to apply effects that you want to apply to all party membersevent CharacterJoinedParty((CHARACTER)_Character)
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 StatusApplied((GUIDSTRING)_Object, (STRING)_Status, (GUIDSTRING)_Causee, (INTEGER)_StoryActionID)
A character finished casting a spellevent CastedSpell((GUIDSTRING)_Caster, (STRING)_Spell, (STRING)_SpellType, (STRING)_SpellElement, (INTEGER)_StoryActionID)
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 ;
IFCastedSpell(_Caster, _, _, _, _)THENApplyStatus(_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.
IFCastedSpell(_Caster, _, _, _, _)ANDIsInCombat(_Caster, 1)THENApplyStatus(_Caster, "AID", 6.0);
With the condition the block only completes if the caster is in combat.
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.IsInCombat([in](GUIDSTRING)_Entity, [out](INTEGER)_Bool)
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:
IFCharacterJoinedParty(_Character)THENApplyStatus(_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.
IFCastedSpell(_Caster, _, _, _, _)ANDIsCharacter(_Caster, 1)THENSetImmortal(_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:
IFCastedSpell(_Caster, _, _, _, _)ANDIsCharacter((CHARACTER)_Caster, 1)THENSetImmortal(_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:
IFCastedSpell(_, _Spell, _, _, _)THENDB_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:
IFCastedSpell(_Caster, _Spell, _, _, _)ANDDB_MY_Spells(_Spell)THENApplyStatus(_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:
INITSECTIONDB_MY_Targets(S_Player_Astarion_c7c13742-bacd-460a-8f65-f864fe41f255);DB_MY_Targets(S_Player_Laezel_58a69333-40bf-8358-1d17-fff240d7fb12);KBSECTIONIFCastSpell(_, "Target_Light", _, _, _)ANDDB_MY_Targets(_Target)THENApplyStatus(_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.:
INITSECTIONDB_MY_Targets(S_Player_Astarion_c7c13742-bacd-460a-8f65-f864fe41f255);DB_MY_Targets(S_Player_Laezel_58a69333-40bf-8358-1d17-fff240d7fb12);KBSECTIONIFCastSpell(_, "Target_Light", _, _, _)ANDDB_MY_Targets(_Target)ANDDB_PartyMembers(_Target)THENApplyStatus(_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);KBSECTIONIFCastSpell(_Caster, "Target_Light", _, _, _)ANDIsCharacter((CHARACTER)_Caster, 1)ANDDB_MY_Companions(_Target, _Gold)ANDDB_PartyMembers(_Target)THENAddGold(_Caster, _Gold);