Osiris: Using the Story Editor

From Baldur's Gate 3 Modding

In this guide, we’ll cover how to get started with the Story Editor (also known as the Osiris Editor), including how to build and reload your own script, handle build errors, and do some basic debugging.

Before starting with this guide, we recommend reading Introduction to Osiris to gain an understanding of how Osiris works and learn about concepts like goals and databases.

Opening the Story Editor

In the Editor’s main toolbar, click on the book icon to open the Story Editor. Alternatively, if the viewport is selected, you can use the keyboard shortcut Ctrl+X.

Opening the Story Editor.

The Story Editor is a tool designed to create and modify story goals.

The Story Editor.

Creating a Script File

Let’s start by understanding the system we use to organise and load scripts.

The goal hierarchy in the Story Editor.

The panel on the left side of the Story Editor is the goal hierarchy. This panel shows you all of the goals used in Baldur’s Gate 3 and the structure we use for them. Notice that some of the goals in the list are collapsible – this means that they have sub-goals.

There are a couple of important rules to understand before you place a goal.

Firstly, goals are executed from top to bottom. This is a rule for Osiris as a whole, not just goals. For example, in the screenshot above, GLO_HagCombatStates will execute before GLO_HagDoubles, which will in turn execute before GLO_HagMaskedVictims, and so on.

Secondly, parent goals execute first, and will only activate their children once that parent goal is complete. For example, the parent goal GLO_Hag will execute first, and only after it finishes with a GoalCompleted event will all of its children starting from GLO_HagCombatStates be activated. We’ll delve into GoalCompleted events later.

When adding a new goal, it’s important to choose the right location in the hierarchy.

Example

Let’s say we want to make a kobold that enjoys stealing apples and put it in the Lower City of Baldur’s Gate. First of all, we need to find when the Lower City is actually loaded up and ready to go. In this case, that’s Act3b.

ModWrapper_Gustav contains all of the quest logic for Baldur’s Gate 3 – if it’s a quest that comes up in your journal, it should be here. Systemics (like crime), achievements, spells, savegame patching, and tutorials all live outside of this goal.

ModWrapper_Gustav, which contains all the quest logic.

To ensure that this goal will only start when the player has reached the Lower City, we’ll want to make it a sub-goal of Act3b. Right-click on the Act3b goal and select Add New Sub Item.

Adding a new sub-goal in Act3b.

ⓘ Note that you must first save after creating new script items before you can rename them. If you've renamed a newly created script item, you must again save before you can rename it again. The save icon is in the top left corner of the Story Editor. If the new name still doesn't stick, try closing and reopening the Story Editor.

This brings up the New Script dialogue. Select the mod project to add this new script to, and name it. In our example, the mod project name is NewScript. As a broad rule of thumb, our naming conventions tend to follow the Act_Region_Situation structure. Following this guideline, we’ll call our hypothetical new script Act3b_LOW_Applesnatcher.

Naming the new script in the New Script pop-up dialogue.

This creates the new goal under Act3b, and also opens the new script on the right-hand side of the Story Editor.

The new goal and its empty script.

Sections

On the right-hand side, you’ll see three sections: INIT, KB, and EXIT. These are the sections we talked about in Introduction to Osiris.

A closer look at the INIT, KB, and EXIT sections in the Story Editor.

INIT is the initialisation field, and will run only once when the goal activates. This is used for script you wish to establish at the beginning of a goal, such as setting up characters.

KB is where the bulk of the script goes. The script here is assessed while the goal is active, and covers almost everything that needs to happen for your situation or mod. In our Act3b_LOW_Applesnatcher example, if we want Applesnatcher to respond to the player throwing an apple at him, this is where we would write the relevant script: Applesnatcher listening for such an event, and reacting to such an event.

EXIT contains logic that will only execute when GoalCompleted is called. Once the EXIT section runs, the logic from your KB section will no longer be evaluated by the game. Most commonly in the EXIT section, you’ll see script like SysClear("NameOfDatabaseGoesHere", Number), which tidies up databases (DBs) we no longer need.

There is also a panel at the bottom of the Story Editor for Errors. This is a debug field that will tell you of any errors encountered with your script when you build it. Please note that this reports from every goal file, not just the one you’re working on! We’ll go a bit deeper into debugging later in this guide.

Building and Reloading Your Script

Generate Definitions, Build and Reload

Once you have written your script, it is time to build it. By building story, the game will be able to evaluate and execute your script.

Using the Generate Definitions, Build and Reload option (Ctrl+F7) allows you to easily and comprehensively build and reload story, along with making it easier to find any new objects you have created.

The "Generate Definitions, Build and Reload" option in the Story Editor.

Once this has completed without any errors, you can test your script in-game. However, if any warnings showed up, you’ll need to fix them first. We'll cover common errors in the Common Build Errors and Warnings section below.

Advanced Build Options

As you may have noticed, there are a variety of other options for building story.

  • Build: This compiles your script and allows the game to read it and evaluate it.
  • Reload Story: This reloads all goal files and executes their INIT sections in the Editor. Keep in mind that it only reloads script and does not restore interactive items, characters and such to their previous positions. See also the Reloading Script section below.
  • Generate Definitions: This generates and updates definitions. In practical terms, this allows Osiris to know of any new objects, queries (QRYs), procedures (PROCs), and databases (DBs) to autocomplete. It also updates the technical names for any objects that maintain the same GUID. This can be useful when your script is not yet ready to be built.

Reloading Script

Option 1: Reload Level and Story

Reload Level and Story (Ctrl+F8) in the main Editor not only reloads your script, but also reloads the objects in your current level. This is helpful when testing – for example, if characters have been defeated or items were destroyed. Reloading the level reverts all objects back to their original setups, except for items in your character’s inventory.

ⓘ Reloading the level takes significantly more time than just reloading story.

The "Reload level and story" option in the main Editor.

Option 2: Reload Story

If you made no changes to your script, and do not need to reload the level, you can save some time by choosing to only reload story. Select the Reload Story (F8) option in the Story Editor.

The "Reload Story" option in the Story Editor.

Common Build Errors and Warnings

This section will guide you through how to fix some common build errors and warnings.

Note that the example code snippets in this section are specifically for demonstrating bugs. They are not examples of well-written Osiris code.

Conflict with function definition

"Conflict with function definition" error.

If you see an error like this, it means the Story Editor has been told a single variable has multiple types. In the example above, the _Player variable in the FlagSet event is sent as a GUIDSTRING type, but the DB_Players database we are checking contains variables as a CHARACTER type. Fortunately, fixing these is easy.

We can recast the variable type in the Story Editor by adding the expected type in ()s before the variable name. In this case, we just change the DB_Players check to DB_Players((CHARACTER)_Player).

If we then build again, the error should be gone.

"Conflict with function definition" error resolved.

We could achieve the same result by recasting the variable in the initial FlagSet line. Either approach is correct, but it is best to avoid recasting the type of a variable multiple times in the same code block.

Database checked but never defined

"Database checked but never defined" error.

This occurs if you have left a reference to a DB somewhere in the KB section of your script, but you never defined the DB within the INIT section of the script, nor as part of a THEN block. Either remove the check or define the DB to fix the error.

Parameter X is an unbound variable

"Parameter X is an unbound variable" error.

This occurs when you named a variable but Osiris can’t find any context for it.

Often, this occurs when you copy-pasted some logic from one area to another and forgot to update your variable names to be consistent with your new logic. In the above example, the Story Editor is able to bind _Player to a value because it is being sent that value by the StatusRemoved event, but _Character isn’t bound to anything, so the IsOnStage query doesn’t know who it should be checking for.

In this case, since we want IsOnStage to be checking the same character that the status was removed from, we just need to replace the unbound _Character with _Player so the variable names match up, and the script should then work as intended.

Auto-define Osiris query X failed: type of parameter X unknown

"Auto-define Osiris query X failed: type of parameter X unknown" error.

This occurs when you try and check a DB for more values than it stores.

If we look at the example above, we first define DB_BuffedPlayers as containing a single value: _Player. However, in the next block of code we look in that same DB for two pieces of information: _Player and _Duration. Because the DB has never been defined to contain 2 values for a single entry, the build fails.

Could not find any complete/correct definition of Osiris User Query/Procedure

"Could not find any complete/correct definition of Osiris User Query/Procedure" error.

Much like the error above, this occurs if you try to add more variables to a query or proc than it was configured to handle.

In this example, the IsOnStage query is only configured to take 2 variables, a GUIDTSTRING and an INTEGER. When we try to put an additional parameter in, in this case _Level, the Story Editor doesn't know what to do with the extra parameter, and cannot build.

It is possible to write your own queries so that they can accept different numbers of parameters.

This error could also be thrown if you have a typo in your query/proc name, as shown below:

A typo ("PROC_CharcterMoveTo") causing the error to get thrown.

In the example above, PROC_CharacterMoveTo is misspelled as PROC_CharcterMoveTo.

Generally, misspellings are pretty easy to catch in the Story Editor. A properly spelled query will have a brick red name, and procs will have a bright green one. In the above example, once the spelling is fixed, the proc name immediately turns green and story successfully builds.

The correctly spelled proc now showing up in green.

Conflict with function definition: parameter X type mismatch

"Conflict with function definition: parameter X type mismatch" error.

This occurs when you pass the correct number of values to a query or procedure, but they’re the wrong datatypes. In this case, IsOnStage is expecting to see a GUIDSTRING in the first slot, but it’s being passed an INTEGER. Passing the correct datatypes will resolve the error.

To learn about the basics of debugging using the Osiris log, have a look at Osiris: Introduction to Debugging.