Osiris Overview
Contents
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 are // multiple ways to implement OR-conditions though, as explained below. 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.
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; .. ]
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) AND DB_Dead(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) 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:
- Query : Osiris can query the game engine about many aspects of the current game state, or the state of an object.
- 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.
Example
IF CharacterDied(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) AND DB_IsPlayer(CHARACTERGUID_S_Player_Ifan_ad9a3327-4456-42a7-9bf4-7ad60cc9e54f) THEN DB_IfanDiedAsAPlayer(1);
In this case, DB_IfanDiedAsAPlayer(1) will only be defined if the CharacterDied event arrives while he was already in the DB_IsPlayer. If you make him a player after he died, no new CharacterDied event will be sent and hence the rule will not re-evaluated.