Difference between revisions of "Osiris Overview"

From Divinity Engine Wiki
Jump to: navigation, search
m (Program Execution)
m (Program Execution)
 
(One intermediate revision by the same user not shown)
Line 334: Line 334:
 
Osiris execution is completely event-driven:
 
Osiris execution is completely event-driven:
 
* Rules can only be triggered when an event is thrown, or when a database is defined or removed by the action block of a procedure, user query or rule, or by the INIT/FINI section of a goal
 
* Rules can only be triggered when an event is thrown, or when a database is defined or removed by the action block of a procedure, user query or rule, or by the INIT/FINI section of a goal
* Procedures and user queries can only be called from action blocks of other procedures, user queries or rules.
+
* User queries can only be called from action blocks of other procedures, user queries or rules. Procedures can also be called from INIT and FINI sections of a goal.
  
This means that there is no ''main'' or other procedure at which an Osiris story (= program) starts executing. Story merely reacts to events thrown by the game engine and actions by the player in the game world. That said, the game engine does throw events such as [[Osiris/API/RegionStarted|RegionStarted]] and [[Osiris/API/GameStarted|GameStarted]] that signify the game may have entered a different phase.
+
This means that there is no ''main'' or other procedure at which an Osiris story (= program) starts executing. Story primarily reacts to events thrown by the game engine and actions by the player in the game world. That said, the game engine does throw events such as [[Osiris/API/RegionStarted|RegionStarted]] and [[Osiris/API/GameStarted|GameStarted]] that signify the game may have entered a different phase. Additionally, the top-level goals (see next section) are intialised as soon as a story becomes active. Note that this happens before the game has fully initialised, so do no perform any game-related queries at that point.
  
 
== Goal Initialisation and Completion ==
 
== Goal Initialisation and Completion ==

Latest revision as of 09:02, 22 February 2019

Introduction

Osiris is a mostly declarative programming language, similar to Prolog in some ways. While it is somewhat counter-intuitive at first if you are not used to declarative programming, the language itself is quite simple and only has a few constructs. Its power mainly stems from its access to a very large number of APIs (note: work-in-progress; there are 879 Osiris APIs at the time of writing). These APIs allow you to react to things that happen (events), to obtain information about the current game state (queries), and to change the game state (calls).

If you are familiar with databases, you can think of an Osiris program as a collection of rules that operate on a single, game-wide database. The rules dynamically add and remove tables and rows in reaction to other database entries getting modified. Additionally, they can also react to game state changes, query the game state, and change the game state. Note that the tables are actually referred to as "databases" in Osiris, so below we will talk about defining databases rather than tables. Adding a row to a table is generally referred to as "defining a fact", although "defining a database" or "defining a database entry" is also used from time to time.

Here is an example Osiris code fragment:

INIT
	DB_MyPrefix_Fruit("Apple");		// Defines the database DB_MyPrefix_Fruit with one column of type STRING, and adds a fact consisting of the string "Apple" to it.
	DB_MyPrefix_Fruit("Pear");		// Adds another fact to the DB_MyPrefix_Fruit database.
	DB_MyPrefix_Fruit("Banana");		// And one more.
KB
	IF					// Osiris rules always start with IF
	DB_MyPrefix_Fruit(_SomeFruit)		// This rule will fire when any database with the name "DB_MyPrefix_Fruit" gets defined.
						// The name of the fruit will be stored in (bound to) the _SomeFruit variable.
	THEN					// "THEN" indicates that the rule conditions have finished, and the rule actions follow.
	DB_MyPrefix_AtLeastOneFruit(1);		// We define a new database with the name "DB_MyPrefix_AtLeastOneFruit" and type INTEGER,
						// and add a fact with the integer value 1 to it. Since there are three rows in the
						// DB_MyPrefix_Fruit database, this action will execute three times. However, as the value
						// is always the same (1), in the end the DB_MyPrefix_AtLeastOneFruit database will
						// contain only a single fact, which consists of the value 1.

	IF
	DB_MyPrefix_Fruit("Pear")		// This rule will fire when DB_MyPrefix_Fruit("Pear") gets defined.
	AND					// Osiris rule conditions can only be combined with AND, not with OR. There is
						// another ways to implement OR-conditions though, explained below (user-defined queries).
	NOT DB_MyPrefix_Fruit("Lemon")		// This rule's actions will only execute if no DB_MyPrefix_Fruit("Lemon") entry exists.
	THEN
	DB_MyPrefix_PearNoLemon(1);

EXIT
	NOT DB_MyPrefix_Fruit("Apple");		// Remove the "Apple" fact from the DB_MyPrefix_Fruit when the goal completes.
	NOT DB_MyPrefix_Fruit("Pear");		// Removing databases that are no longer used anywhere else after a goal completes
	NOT DB_MyPrefix_Fruit("Banana");	// reduces savegame sizes and speeds up the game.

	NOT DB_MyPrefix_AtLeastOneFruit(1);	// Even if some of these databases would not exist, removing them would not result
	NOT DB_MyPrefix_AtLeastOneFruit(1);	// in an error. Removing a non-existent database is simply ignored.

Note that you do not type INIT, KB and EXIT in the story editor. These sections are represented by the separate panes that you see when opening a goal for editing.

Program Structure

An Osiris program is called a Story. A story consists of all (story) Goals for the current mod and its dependencies. E.g., the main game mod (DivinityOrigins) depends on the Shared mod, so the story of the main game includes all goals from both mods.

A goal is a plain text file with three sections:

INIT 
The INIT section contains actions that are executed when the goal initialises.
KB 
The KB section, or knowledge base, contains rules that become active as soon as the goal starts initialising. This means that the rules in the KB section can react to changes made in the INIT section of the same goal. The KB section cannot contain actions outside rule bodies.
EXIT 
The EXIT section contains actions that are executed when the goal completes.

Types

Osiris supports ten different types:

INTEGER 
A 32-bit integer number. Examples: -4, 0, 10.
INTEGER64 
A 64-bit integer number. Examples: -99999999999, -4, 0, 10, 12345678901.
REAL 
A single precision floating point number. Examples: -10.0, -0.1, 0.0, 0.5, 100.123.
STRING 
A character string. Examples: "A", "ABC", "_This is a string_".
GUIDSTRING 
A GUID that refers to an object, or sometimes root template, in the game. This object can be any of the specialised GUID-types below. Examples: 123e4567-e89b-12d3-a456-426655440000, MyObjectName_123e4567-e89b-12d3-a456-426655440000, CHARACTERGUID_MyObjectName_123e4567-e89b-12d3-a456-426655440000. Osiris in fact only uses the GUID itself. Anything before it (as long as it does not contain "-") gets ignored by the compiler, and is only there to improve the readability of the code. This ensures that if you rename an object in game, its related scripting won't break.
CHARACTERGUID 
A GUIDSTRING referring to a character object in the game. Examples: (CHARACTERGUID)123e4567-e89b-12d3-a456-426655440000, (CHARACTERGUID)MyObjectName_123e4567-e89b-12d3-a456-426655440000, (CHARACTERGUID)CHARACTERGUID_MyObjectName_123e4567-e89b-12d3-a456-426655440000
ITEMGUID 
A GUIDSTRING referring to an item object in the game.
TRIGGERGUID 
A GUIDSTRING referring to a trigger object in the game.
SPLINEGUID 
A GUIDSTRING referring to a spline object in the game.
LEVELTEMPLATEGUID 
A GUIDSTRING referring to a level template object in the game.

The specialised GUID-types all inherit from GUIDSTRING, and you can cast a GUIDSTRING to any of the specific types by adding (ITEMGUID) and the like in front of it. Note that while code-completion will prepend the object type to the object name in the form of ITEMGUID_, this does not define or change the type in any way to the Osiris compiler.

Warningred.png
When you wish to refer to objects/local instances from script or elsewhere, make sure to follow the correct naming convention.

Databases

Osiris databases consist of a database name, which must start with DB_, and one or more typed columns. As there is only one namespace (= all database names defined in all goals from all mods must be unique), it is a good idea to follow the DB_ by a prefix that is unique to your mod or goal. Every entry that gets added to a database is called a fact. The structure of a fact definition is

DB_Prefix_DatabaseName(TypedValue1[,TypedValue2..]);

Examples:

// Type: String
DB_Overview_StringDB("SomeString");
DB_Overview_StringDB("AnotherString");

// Type: float
DB_Overview_FloatDB(1.0);

// Type: Integer, String
DB_Overview_IntegerStringDB(0, "String0");
DB_Overview_IntegerStringDB(1, "String1");
DB_Overview_IntegerStringDB(1, "String1");

// Type: GUIDSTRING (not CHARACTERGUID, because no typecast)
DB_Overview_Origins(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f);
DB_Overview_Origins(CHARACTERGUID_S_Player_Beast_f25ca124-a4d2-427b-af62-df66df41a978);
DB_Overview_Origins(CHARACTERGUID_S_Player_Lohse_bb932b13-8ebf-4ab4-aac0-83e6924e4295);
DB_Overview_Origins(CHARACTERGUID_S_Player_RedPrince_a26a1efb-cdc8-4cf3-a7b2-b2f9544add6f);
DB_Overview_Origins(CHARACTERGUID_S_Player_Sebille_c8d55eaf-e4eb-466a-8f0d-6a9447b5b24c);
DB_Overview_Origins(CHARACTERGUID_S_Player_Fane_02a77f1f-872b-49ca-91ab-32098c443beb);

// Type: CHARACTERGUID, String
DB_Overview_Origins((CHARACTERGUID)CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, "IFAN");
DB_Overview_Origins(CHARACTERGUID_S_Player_Beast_f25ca124-a4d2-427b-af62-df66df41a978, "BEAST");
DB_Overview_Origins(CHARACTERGUID_S_Player_Lohse_bb932b13-8ebf-4ab4-aac0-83e6924e4295, "LOHSE");
DB_Overview_Origins(CHARACTERGUID_S_Player_RedPrince_a26a1efb-cdc8-4cf3-a7b2-b2f9544add6f, "RED PRINCE");
DB_Overview_Origins(CHARACTERGUID_S_Player_Sebille_c8d55eaf-e4eb-466a-8f0d-6a9447b5b24c, "SEBILLE");
DB_Overview_Origins(CHARACTERGUID_S_Player_Fane_02a77f1f-872b-49ca-91ab-32098c443beb, "FANE");

As the last example shows it is possible to overload databases: you can have multiple databases with the same name. The only requirement is that they have a different number of columns (parameters). Also note that this last example only includes a (CHARACTERGUID) typecast for the first row. That is sufficient because the compiler uses the first occurrence of a database in story to determine the types. It will then typecast the values in any further occurrences of this database to the type it determined from the first declaration. Since a GUIDSTRING can be typecasted into a CHARACTERGUID, this works fine here. This is an important rule to keep in mind, as it is a frequent source of type errors when you're just starting out.

Removing database facts can be done using the NOT-operator:

NOT DB_Overview_StringDB("SomeString");
NOT DB_Overview_StringDB("AnotherString");

Rules

A Rule consist of one or more trigger conditions, a number of optional extra conditions, and finally one or more actions. A rule is evaluated whenever one of its trigger conditions becomes fulfilled. If all trigger and extra conditions are fulfilled at that point, the actions of this rule are executed.

The structure of a rule is as follows:

IF
TriggerCondition
[
AND
TriggerCondition | ExtraCondition
]
[
AND
TriggerCondition | ExtraCondition
..
]
THEN
Action1;
[
Action2;
..
]

You can have multiple rules that check on the same trigger conditions. They will all be executed when the trigger conditions are satisfied.

Trigger Conditions

There are two categories of trigger conditions:

Osiris Event 
The game informs Osiris that a particular event has occurred. Warning: an event can only be used as the first trigger condition. Putting it at any other place in a rule condition will result in a compilation error.
Database 
A new database fact has been added. Removing a database fact can be a trigger condition, but not the first one. Database fact trigger conditions can appear anywhere in a condition.

Note that you can check multiple databases as part of your trigger conditions, and the rule's trigger conditions will be considered as fulfilled as soon as all database checks succeed. Example:

IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)	// Trigger condition
AND
DB_Dead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)	// Trigger condition
THEN
DB_IfanIsDeadAsAPlayer(1);

This rule will trigger when DB_IsPlayer() is defined for Ifan first and then his DB_Dead(), but also if they are defined in the opposite order. Second example:

IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)	// Trigger condition
AND
NOT DB_Dead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)	// Trigger condition
THEN
DB_IfanIsAliveAsAPlayer(1);

This rule will trigger when DB_IsPlayer() is defined for Ifan while no DB_Dead() fact was defined for him. However, it will also trigger if initially both DB_IsPlayer() and DB_Dead() were defined for him, and later on the DB_Dead() fact gets removed.

Extra Conditions

Extra conditions are any conditions that are checked as part of a rule, but that are not trigger conditions:

Osiris Query 
Osiris can query the game engine about many aspects of the current game state, or the state of an object.
User Query 
You can also define your own queries in Osiris. This way you can implement OR-conditions.
Comparison
You can use the comparison operators == (equals), != (different), <=, <, > and >= as a condition. GUIDSTRING and its descendent types can only be compared for (non-)equality, while the other types can also be used with the other comparison operators.
Any condition following a game engine event 
Events are sent by the game engine to Osiris whenever specific things happen (a character dies, and item has been picked up, ...). Only when this happens, the extra conditions below that event get checked.

Examples:

IF
CharacterDied(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)		// Trigger condition
AND
NOT DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)	// Extra condition
THEN
DB_IfanDiedAsAPlayer(1);

In this case, DB_IfanDiedAsAPlayer(1) will only be defined if the CharacterDied event arrives while Ifan was not in the DB_IsPlayer database. If you remove him from that database after he died the rule will not re-evaluated, because only the CharacterDied event is part of the trigger conditions (and this event will not be sent again if he is already dead).

IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)		// Trigger condition
AND
CharacterIsDead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, 0)	// Extra condition
AND
DB_Avatars(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)            // Trigger condition (!)
THEN
DB_IfanIsAnAvatarPlayerAndNotDead(1);

CharacterIsDead is a query that takes two parameters: a CHARACTERGUID and an integer. The integer indicates whether the character is dead (1) or not (0). If all parameters match, the query condition will succeed. This means that the above rule succeeds in case CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f is not dead at the time he first appears in both the DB_IsPlayer and DB_Avatars databases. As mentioned earlier, it does not matter in which order he gets added to these databases. As soon as both facts are defined, the rule triggers and the extra condition(s) will be evaluated. If these extra conditions succeed, the actions in the rules section will be executed.

On the other hand, if both database facts would already be defined and then Ifan gets resurrected (so CharacterIsDead(IFan, 0) would now succeed if it were called), the rule will not trigger. The reason is that it only get (re)evaluated when all of the trigger conditions become fulfilled (in this case, Ifan gets added to both databases). This also demonstrates the main difference between queries and events:

  • Events are triggered when the game state changes, but you cannot call them yourself.
  • Queries allow you to request information about the game state at any point, but they never get automatically triggered/(re)evaluated.

Actions

All statements in the INIT and FINI sections must be actions. Additionally, at the end of a rule you must also put at least one action. Every action ends in a semi-colon (;).

Actions can make changes to both the Osiris program state and the game state:

Osiris Call 
Osiris can call into the game engine to perform all kinds of changes to the current game state, either globally or specific to certain objects.
Procedure 
You can group conditions and actions in a procedure and call that from multiple locations.
Database 
As shown in the examples above, a new database fact can be added in an action by placing it there, followed by a semi-colon. You can also remove database facts by prepending them with NOT.

Example:

IF
DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f)		// Trigger condition
AND
CharacterIsDead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f, 1)	// Extra condition
THEN
CharacterResurrect(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f);

If Ifan is added to the DB_IsPlayer database while he was already dead, he will be resurrected.

Variables

All previous examples use concrete values to define and test database contents. However, in many cases you do not know the actual value, or you want a rule to be more general. In this case, you use variables. Like in most declarative programming languages, you do not declare these variables. Their type is defined by their first use, and their lifetime and scope is always limited to a single rule, procedure or query.

Example:

IF
DB_IsPlayer(_Player)
AND
CharacterIsDead(_Player, 1)
THEN
CharacterResurrect(_Player)

Rather than only resurrecting Ifan in case he is dead when he gets added to the DB_IsPlayer database, any dead character that gets added to the DB_IsPlayer database will be resurrected at that point.

More powerful uses of variables are demonstrated in the section on Rule Matching below.

Subroutines

There are two kinds of user-defined subroutines in Osiris: queries and procedures.

Queries

Queries are new compared to DOS1. They are very handy to implement OR-conditions. The format of a query is:

QRY
QRY_MyPrefix_QueryName((TYPE1)_Param1[,(TYPE2)_Param2..])
[
AND
ExtraCondition1
..
]
THEN
Action1;
[
Action2;
..
]

Just like databases, queries can be overloaded with different numbers of parameters. And just like with rules, you can define the same query multiple times and every definition of that same query will be executed when it is called. When a user-defined query is called in a rule, it is considered to have succeeded if all conditions of at least one definition of this query succeeded (and hence its actions were executed).

Example:

QRY
QRY_Overview_CharacterIsIncapacitated((CHARACTERGUID)_Char)
AND
HasActiveStatus(_Char,"FROZEN",1)
THEN
DB_NOOP(1);

QRY
QRY_Overview_CharacterIsIncapacitated(_Char)
AND
HasActiveStatus(_Char,"PETRIFIED",1)
THEN
DB_NOOP(1);

QRY
QRY_Overview_CharacterIsIncapacitated(_Char)
AND
HasActiveStatus(_Char,"KNOCKED_DOWN",1)
THEN
DB_NOOP(1);

IF
CharacterReceivedDamage(_Char)
AND
// check whether _Char is FROZEN, PETRIFIED or KNOCKED_DOWN
QRY_Overview_CharacterIsIncapacitated(_Char)
THEN
DB_Overview_CowardAttackingIncapacitatedCharacter(1);

Our QRY_Overview_CharacterIsIncapacitated query does not actually need to do anything if its conditions succeed. However, you cannot have condition checks without an action. For this reason, we define a dummy database fact (DB_NOOP(1) by convention) if all rules succeed. So, when our rule calls QRY_Overview_CharacterIsIncapacitated(_Char), Osiris will evaluate all definitions of this query in the entire story (so not just in the current goal!) and if one of them succeeds, it will consider this query/extra condition to have succeeded.

Just like with databases, only the first encountered definition of a query needs to specify the parameter type(s). It does not hurt to repeat them though. You can also see that parameters use the same syntax as variables.

Note that Osiris has a built-in query to check whether a character is incapacitated (which checks more statuses than the ones listed above), so you won't need to implement the above query yourself.

Procedures

Procedures are collections of actions that you may want to execute from multiple action blocks. Or, they may group a number of related actions of which some must only be executed under certain conditions. In this sense, they allow you to have extra condition checks in an action block.

The format of a procedure is:

PROC
PROC_MyPrefix_ProcName((TYPE1)_Param1[,(TYPE2)_Param2..])
[
AND
ExtraCondition1
..
]
THEN
Action1;
[
Action2;
..
]

Just like with queries, you can have multiple definitions of the same procedure (even in different goals). When you invoke a procedure, all of those procedures will be executed. Unlike queries, procedures have no result, since statements in an action block do not return anything.

Example:

PROC
PROC_Overview_TeleportAlive((CHARATERGUID)_Char, (GUIDSTRING)_Destination)
AND
CharacterIsDead(_Char, 1)
THEN
CharacterResurrect(_Char);

PROC
PROC_Overview_TeleportAlive(_Char, _Destination)
THEN
TeleportTo(_Char, _Destination);

This procedure will first check whether the character to be teleported is dead. If so, the character will be resurrected. Next, the character (which is guaranteed to be alive now) is teleported to its destination, which can be a trigger, item, or other character.

This way you can check extra conditions in an actions block, by delegating the checks to a procedure.

Program Execution

Osiris execution is completely event-driven:

  • Rules can only be triggered when an event is thrown, or when a database is defined or removed by the action block of a procedure, user query or rule, or by the INIT/FINI section of a goal
  • User queries can only be called from action blocks of other procedures, user queries or rules. Procedures can also be called from INIT and FINI sections of a goal.

This means that there is no main or other procedure at which an Osiris story (= program) starts executing. Story primarily reacts to events thrown by the game engine and actions by the player in the game world. That said, the game engine does throw events such as RegionStarted and GameStarted that signify the game may have entered a different phase. Additionally, the top-level goals (see next section) are intialised as soon as a story becomes active. Note that this happens before the game has fully initialised, so do no perform any game-related queries at that point.

Goal Initialisation and Completion

As mentioned earlier, when a goal initialises its INIT section gets executed and the rules in its KB section become active. Additionally, QRYs and PROCs defined in this goal's KB become usable by this and other goals. This means that as long as a goal has not been initialised, or after it has been completed, none of its defined procedures and queries will be found/executed anymore by other goals.

Now, when does a goal initialise? Let's have a look at the left pane of the following screenshot of the story editor:

StoryEditor.png

At the top level, you see a goal named Start. Under it, there are two other goals: AtaraxianArtifacts and FTJ_Origins_Ifan. Top-level goals are initialised as soon as the mod is loaded. In case those three goals were the only ones in a mod, that would mean that only the Start goal would initialise when the mod gets loaded.

Completing, or finalising a goal happens by placing a GoalCompleted; action in any action block. At that point,

  • all subgoals of the current goal will be initialised;
  • the EXIT section of the current goal will be executed;
  • none of the rules, queries or procedures in the current goal will be active/available anymore;
  • databases set in the completed goal, even in its init section, will remain active and available, unless they are deleted by the goal's EXIT code.

However, the action block in which GoalCompleted; was called will still be executed till the end. This means that you can insert calls after that statement, even to procedures inside subgoals as these have been initialised once the GoalCompleted; statement has finished executing. Just make sure you do not rely on any databases that may be deleted by the EXIT code of the current goal.

Rule Order

As mentioned in the program structure section, all goals of a mod and of the mods it depends on are merged into a single story. The question then is: in what order are the merged rules evaluated? This can be very important in case one rule defines databases upon which other rules may depend. For example, the DB_DialogNPCs database is defined by a goal in the Shared mod whenever a DialogStarted event fires. So if you also catch the DialogStarted event in your mod, can you rely on this database existing already or not at that time? In other words, how can you know whether the DialogStarted-rule from the Shared mod executes before or after the one in your mod?

The answer is both simple and possibly a bit unsettling: rules are executed from top to bottom in the single story that contains all goals from all mods that are currently active. Inside this story, all goals are sorted alphabetically on goal name. This sorting disregards both parent/subgoal relations and mod dependencies. So a goal named FooBar in your mod, even if it is a subgoal located at five levels deep, will always be placed in story before e.g., the GLO_Follower of the Shared mod.

Note that, as mentioned in the previous section, only rules/procedures/queries from active goals will be found. So as long as the FooBar goal from your mod has not be initialised, none of its rules/procedures/queries will execute at all, no matter where they are placed in story.

Returning to the specific case of the DB_DialogNPCs database: these are defined in a goal of the Shared mod with name _AAA_FirstGoal. While it is technically not the very first goal in the story, it does appear before any goal that uses this database. And if you wish to use this database in a mod of your own in a DialogStarted event, make sure that goal's name comes after _AAA_FirstGoal when sorted alphabetically (the _ character is sorted before any letter or number).

In summary:

  • All event handlers in top-level goals become available when the game starts.
  • Goals are initialised/activated in alphabetical order, so do not depend on PROCs/QRYs/events (including reacting to database facts) from other goals in your goal initialisation code.
  • Completing a goal renders it inactive, but activates all of its subgoals.

Rule Matching

As explained in the section on variables, you can use variables to make rules generic. There are many more powerful uses of variables though, with associated gotchas.

Iterating Database Contents

You can use variables to iterate all values in a database. Assuming the DB_Overview_Origins has been declared as above, consider the following rule:

IF
DB_Overview_KillAllOrigins(1)
AND
DB_Overview_Origins(_Origin)
THEN
// Cast _Origin to CHARACTERGUID because that's what CharacterDie expects, and because the
// first DB_Overview_Origins() database fact has been declared without a specific type
// (hence defaulting to GUIDSTRING)
CharacterDie((CHARACTERGUID)_Origin, 0, "DoT");

This rule means that as soon as the DB_Overview_KillAllOrigins(1) fact has been defined, we will iterate over all currently defined DB_Overview_Origins facts with one column. For each of those facts we will sequentially assign the actual value to the _Origin variable, and then evaluate the subsequent conditions (if any) for that value. If they succeed, we will perform the actions, again with this value.

Looking Up Facts (Associative Arrays)

We can also use this technique to look up facts in a database:

IF
DB_Overview_KillOriginByName(_Name)
AND
DB_Overview_Origins(_Origin, _Name)
THEN
// No need for typecast, as the first DB_Overview_Origins() database fact with two columns
// has been declared as having CHARACTERGUIDs in the first column
CharacterDie(_Origin, 0, "DoT");

Here we react to a database fact being set containing the name of the origin we want to kill. We then iterate over all DB_Overview_Origins facts with two columns, whereby the second column matches the name specified in the newly defined DB_Overview_ResurrectOriginByName fact. If one or more such entries are found, we will then kill them.

Rule Evaluation State

But what if we want to be able to perform the above multiple times for the same origin? Once a database fact has been defined, redefining it again with the same value will not result in any event triggering. After all, the database fact already existed and the event only triggers when a new fact gets added. To solve this, we can simply remove the fact in the rules section:

IF
DB_Overview_KillOriginByName(_Name)
AND
DB_Overview_Origins(_Origin, _Name)
THEN
NOT DB_Overview_KillOriginByName(_Name); // added
CharacterDie(_Origin, 0, "DoT");

You may wonder whether removing the DB_Overview_KillOriginByName(_Name) will abort further execution of the rule in case there would be multiple DB_Overview_Origins facts for which _Name matches. That is not the case. The reason is that the execution of a rule occurs by generating a temporary internal table of the valid possibilities every time a condition is evaluated. Afterwards, Osiris keeps referring to this table.

For example, in the above rule, at the second step Osiris will create an internal table that contains all matching DB_Overview_Origins(_Origin, _Name) facts for the specified _Name. Then it will start evaluating all subsequent conditions (and actions) using this temporary table, rather than starting at the first condition again every time. The same happens with the second, third, ... condition.

Rule Trigger Evaluation

Osiris uses a similar mechanism as the Rule Evaluation State at the top-level, when determining which rules to execute. When a trigger condition occurs, Osiris immediately collects all rules in all active goals that match the first trigger condition. Next, it will start executing these rules one by one with the matched parameter values. This means that even if you ensure that your rule triggers first, removing the first trigger condition or rendering it false will not prevent subsequent rules from executing.

Example:

IF
DB_Overview_KillOriginByName(_Name)
AND
DB_Overview_Origins(_Origin, _Name)
THEN
NOT DB_Overview_KillOriginByName(_Name); // added
CharacterDie(_Origin, 0, "DoT");

IF
DB_Overview_KillOriginByName(_Name)
AND
DB_Overview_Origins(_Origin, _Name)
AND
ObjectGetFlag(_Origin,"Overview_ShouldBeResurrectedAfterDying")
THEN
CharacterResurrect(_Origin);

First of all, keep in mind that rules are executed in the order they appear in story, and fully execute before any rule after them gets executed. This means that we will first kill all origin characters whose name column in the DB_Overview_Origins database matches the specified name. We will also remove the DB_Overview_KillOriginByName(_Name) fact. Next the second rule will still execute, because Osiris has already previously determined that this rule gets activated by the triggering event.

Conversely, removing (or adding) DB_Overview_Origins() facts in the first rule would affect the execution of the second rule. The reason is that only the first trigger gets cached while selecting which rules to evaluate.

Osiris Frames

The evaluation of Osiris events is separated into frames. Everything that happens in a single frame happens atomically: it is not possible for a player to save in the middle of a frame, nor to abort part of the actions of a frame. Either a frame starts and runs to the finish, or does not start at all. In other words, frames are atomic, non-interruptible blocks of rule evaluations and actions.

Whenever an event will execute a number of rules, as explained in the Rule Trigger Evaluation section, all of these rules will be evaluated and their actions executed in the same frame. Furthermore, any procedures or queries called by these rules will also execute in the same frame.

Frame Delays

While all Osiris API and user-defined procedure calls are guaranteed to execute in the same frame during which the corresponding rule's event was triggered, their effects often will not happen instantaneously. This can be obvious, as for example a CharacterMoveTo operation will probably take at least a few seconds complete. However, there are also a few less obvious cases. The most important one is flag visibility in dialogs.

Take for example the following dialog:
Dialog-framedelay.png

The following flags are set/checked in the nodes marked with with the blue squares:

Ask for blackroot Had blackroot Had no blackroot
Dialog-framedelay-askblackroot.png
Dialog-framedelay-hadblackroot2.png
Dialog-framedelay-noblackroot.png

This is combined with the example Osiris code from DB_GiveTemplateFromPlayerDialogEvent:

// if you set the character flag "MYPRE_give_blackroot" on an NPC in this dialog, and this NPC has blackroot in his inventory, then
// it will give one blackroot to the first player in that dialog and the flag "MYPRE_had_blackroot" will be set on the NPC.
DB_GiveTemplateFromPlayerDialogEvent("QUEST_Blackroot_843689bf-6498-45ac-98ba-b375bdbbb189", "MYPRE_give_blackroot", "MYPRE_had_blackroot");

Assume that frame 1 is when the player clicks to get the blackroot, then the following happens:

Dialog Osiris
Frame 1
  • The flag MYPRE_give_blackroot is set on the NPC.
  • The dialog manager loads the following node(s) ("Frame delay node" in our example) and evaluates its (their) flag conditions, if any (none in our example).
  • Osiris gets an ObjectFlagSet event for the MYPRE_give_blackroot flag that was set on the NPC.
  • In combination with the DB_GiveTemplateFromPlayerDialogEvent definition above, this will result in a check whether the NPC has any instance of the QUEST_Blackroot_843689bf-6498-45ac-98ba-b375bdbbb189 root template in its inventory, and if so transfer one such instance to the first player in the dialog and the set the MYPRE_had_blackroot flag on the NPC.
Frame 2
  • The frame delay node is shown.
  • The dialog manager loads the following node(s) (Had blackroot and Had no blackroot in our example) and evaluates its (their flag conditions) (check whether MYPRE_had_blackroot is set on NPC in our example).
  • Nothing relevant to the dialog.
Frame 3
  • Depending on whether or not Osiris set the MYPRE_had_blackroot flag in Frame 1, the Had blackroot or Had no blackroot node will be shown.
  • If the Had blackroot node got selected, the MYPRE_give_blackroot flag will be cleared again from the NPC.
  • Nothing relevant to the dialog.

If we did not add the Frame delay node between Ask blackroot and the Had (no) Blackroot, then the dialog manager would check the MYPRE_had_blackroot flag before Osiris had set it, and hence the check would always fail. If you do not want any actual dialog text in between, you can make the Frame delay node a completely empty node (without a text block). Such nodes are automatically skipped immediately, but they still occupy a frame.