Osiris Overview

From Divinity Engine Wiki
Revision as of 21:52, 16 September 2017 by Tinkerer (talk | contribs) (Rule execution, goal initialisation/completion, rule order)
Jump to: navigation, search

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.

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.

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.

And to return to 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

Frame Delays