Difference between revisions of "Osiris Overview"

From Divinity Engine Wiki
Jump to: navigation, search
(Rule execution, goal initialisation/completion, rule order)
(Finished the program execution section)
Line 203: Line 203:
  
 
If Ifan is added to the ''DB_IsPlayer'' database while he was already dead, he will be resurrected.
 
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, [[#Procedures|procedure]] or [[#Query|query]].
 +
 +
Example:
 +
<pre>
 +
IF
 +
DB_IsPlayer(_Player)
 +
AND
 +
CharacterIsDead(_Player, 1)
 +
THEN
 +
CharacterResurrect(_Player)
 +
</pre>
 +
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|Rule Matching]] below.
  
 
== Subroutines ==
 
== Subroutines ==
Line 258: Line 274:
 
</pre>
 
</pre>
  
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, our rule calls ''QRY_Overview_CharacterIsIncapacitated(_Char)'' then Osiris will evaluate all definitions of this query in the entire story (so not just in the current [[#Program_Structure|goal]!]) and if one of them succeeds, it will consider this query/extra condition to have succeeded..
+
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, our rule calls ''QRY_Overview_CharacterIsIncapacitated(_Char)'' then Osiris will evaluate all definitions of this query in the entire story (so not just in the current [[#Program_Structure|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.
+
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|variables]].
  
 
Note that Osiris has a [[Osiris/API/CharacterIsIncapacitated|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.
 
Note that Osiris has a [[Osiris/API/CharacterIsIncapacitated|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.
Line 334: Line 350:
 
Note that, as mentioned in the [[#Goal_Initialisation_and_Completion|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.
 
Note that, as mentioned in the [[#Goal_Initialisation_and_Completion|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.
  
And to return to the [[Osiris/Shared/DB_DialogNPCs|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).
+
Returning to the specific case of the [[Osiris/Shared/DB_DialogNPCs|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).
  
 
== Rule Matching ==
 
== Rule Matching ==
 +
As explained in the section on [[#Variables|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 [[#Databases|as above]], consider the following rule:
 +
<pre>
 +
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");
 +
</pre>
 +
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:
 +
<pre>
 +
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");
 +
</pre>
 +
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:
 +
<pre>
 +
IF
 +
DB_Overview_KillOriginByName(_Name)
 +
AND
 +
DB_Overview_Origins(_Origin, _Name)
 +
THEN
 +
NOT DB_Overview_KillOriginByName(_Name); // added
 +
CharacterDie(_Origin, 0, "DoT");
 +
</pre>
 +
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.
 +
 +
=== Rule Trigger Evaluation ===
 +
Osiris uses a similar mechanic to the [[#Rule_Evaluation_State|Rule Evaluation State]] at the top-level, when determining which rules to execute. When a [[#Trigger_Conditions|trigger condition]] occurs, Osiris immediately collects all rules in all active goals that match this 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 trigger condition or rendering it false will not prevent subsequent rules from executing.
 +
 +
Example:
 +
<pre>
 +
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_ResurrectOriginByName(_Name)
 +
AND
 +
DB_Overview_Origins(_Origin, _Name)
 +
AND
 +
ObjectGetFlag(_Origin,"Overview_ShouldBeResurrectedAfterDying")
 +
THEN
 +
CharacterResurrect(_Origin);
 +
</pre>
 +
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 the triggering event.
 +
 +
== 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|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 ==
 
== 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 [[Osiris/API/CharacterMoveTo|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: if you [[Osiris/API/ObjectSetFlag|set an Object flag]] in Osiris while a dialog is active, the flag will only be visible in the dialog once the dialog proceeds to its own next frame. Usually the dialog manager processes one dialog node per frame, but there is one exception: an answer node followed by a question node. The reason is that these two nodes are shown at the same time (the NPC says something and the player can answer). You can often solve this by adding an empty answer node in between: these will not be displayed, but they will force the dialog manager to wait until the next Osiris frame before checking flags.

Revision as of 22:26, 16 September 2017

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, 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 a fact to one-column "DB_MyPrefix_Fruit" database.
						// gets added. The name of the fruit will be bound to the _SomeFruit variable.
	THEN					// "THEN" indicates that the rule conditions have finished, and the rule actions start.
	DB_MyPrefix_AtLeastOneFruitDefined(1);	// We define a new database with the name "DB_MyPrefix_AtLeastOneFruitDefined",

	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_AtLeastOneFruitDefined",
						// 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_AtLeastOneFruitDefined 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 FINI 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 referring 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 referring an item object in the game.
TRIGGERGUID 
A GUIDSTRING referring to referring a trigger object in the game.
SPLINEGUID 
A GUIDSTRING referring to referring a spline object in the game.
LEVELTEMPLATEGUID 
A GUIDSTRING referring to referring 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.

Databases

Osiris databases consist of a database name, which must start with DB_, and one or more typed columns. In practice, since there is only one namespace, 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 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 at least one trigger condition, a number of optional extra conditions, and finally one or more actions. A rule is evaluated whenever one of its trigger conditions get 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
TriggerCondition1
[
AND
TriggerCondition2
..
]
[
AND
ExtraCondition1
..
]
THEN
Action1;
[
Action2;
..
]

You can have multiple rules that check on the same trigger condition. They will all be executed when the trigger condition is 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 should result in a compilation error (and it usually will, but there are some known bugs in the compiler where it will not print an error and generate non-working code instead).
Database 
A new database fact has been added. Conversely, removing a database fact never results in any kind of event that can be caught as a trigger event.

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 if DB_IsPlayer() is defined for Ifan first and then his DB_Dead(), but also if they are defined in the opposite order.

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.
Database 
Any database checks after an event trigger condition, or after a query, becomes part of the extra conditions. This means that defining such a database will not trigger this rule's evaluation. Only when this rule is triggered via one of its trigger conditions, this database will be consulted. On the other hand, this means that you can also use negative database fact checks as an extra condition.

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 he was not in the DB_IsPlayer database. If you remove him from that database after he died, no new CharacterDied event will be sent and hence the rule will not re-evaluated.

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
THEN
DB_IfanIsAPlayerAndNotDead(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 gets added to the DB_IsPlayer database.

Actions

All statements in the INIT and FINI sections must be actions. Additionally, at the end of 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 it 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 the conditions of at least one definition of this query all 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, our rule calls QRY_Overview_CharacterIsIncapacitated(_Char) then Osiris will evaluate all definitions of this query in the entire story (so not just in the current [[#Program_Structure|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 be 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 [[#Program_Structure|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 by the action block of a procedure, user query or rule
  • Procedures and user queries can only be called from action blocks of other procedures, user queries or rules.

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 RegionStarted and GameStarted that signify the game may have entered a different phase.

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 all initialised as soon as the mod is loaded. In case those three goals were the only ones in a mod, that would means that when loaded on the Start mod would initialise.

Completing, or finalising a mod 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 that 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).

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.

Rule Trigger Evaluation

Osiris uses a similar mechanic to 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 this 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 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_ResurrectOriginByName(_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 the triggering event.

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: if you set an Object flag in Osiris while a dialog is active, the flag will only be visible in the dialog once the dialog proceeds to its own next frame. Usually the dialog manager processes one dialog node per frame, but there is one exception: an answer node followed by a question node. The reason is that these two nodes are shown at the same time (the NPC says something and the player can answer). You can often solve this by adding an empty answer node in between: these will not be displayed, but they will force the dialog manager to wait until the next Osiris frame before checking flags.