BSL:Manual: Difference between revisions

From OniGalore
Jump to navigation Jump to search
(improving instructions on function calling; correcting "some_function" to "prepare_fight")
(elaboration on recursion and forking; brought over warning about dprint/dmsg from BSL:Functions)
 
(27 intermediate revisions by 3 users not shown)
Line 1: Line 1:
Oni's level scripts are written in BFW Scripting Language, or [[BSL]]. A scripting language allows you to create a set of plain-text files which employ branching logic and various operators according to certain rules of syntax in order to process data using functions that act upon variables. If any of those terms are not familiar to you now, keep reading. If you found that sentence boringly obvious, then you should be experienced enough to get by with the [[BSL:Introduction|Introduction]] page, a sort of quick reference. This Manual page, by contrast, provides both a thorough and newbie-friendly introduction to the language and also documents the quirks of the language, which a modder writing a serious script mod will want to know about.
Oni's level scripts are written in {{Hover|BFW|text=BungieFrameWork, the underlying layer of the game engine.}} Scripting Language, or [[BSL]]. A scripting language allows you to create a set of plain-text files which employ branching logic and various operators according to certain rules of syntax in order to process data using functions that act upon variables. If any of those terms are not familiar to you now, keep reading. If you found that sentence boringly obvious, then you should be experienced enough to get by with the [[BSL:Introduction|Introduction]] page, a sort of quick reference. This Manual page, by contrast, provides both a thorough and newbie-friendly introduction to the language and also documents every known bug in the language, which a modder writing a complex script will want to know about.
{{TOClimit|3}}
{{TOClimit|3}}
==Files==
==Script files==
When Oni loads a level's resources, it also loads and parses all .bsl files in the level's corresponding folder in [[IGMD]] (the name of this folder being specified by the level's [[ONLV]] resource). The code automatically begins running with the function called "main", regardless of which .bsl file it is found in (Oni's scripts always place it in a file ending in "_main.bsl"). The code then flows through whichever functions are called by "main". You can also manually call any of those functions from the [[Developer Mode|developer console]] -- however, you cannot actually define variables or functions through the console, so scripts cannot be written this way.
When Oni loads a level's resources, it also loads and parses all .bsl files in the level's designated [[IGMD]] folder (the name of this folder is specified by [[ONLV]]). The code automatically begins running with the function called "main", regardless of which .bsl file it is found in (Oni's scripts always place it in a file ending in "_main.bsl"). The code then flows through whichever functions are called by "main". You can also manually call any of those functions from the [[Developer Mode|developer console]] however, you cannot define variables or functions through the console.


Besides the automatic starting point represented by the "main" function, there are types of game data which specify function names that are to be called upon certain events. For instance, characters can call script functions upon death, and trigger volumes can call functions when they are entered or exited. See [[CHAR]], [[OBD:BINA/OBJC/CONS|CONS (BINA)]], [[OBD:BINA/OBJC/DOOR|DOOR (BINA)]], [[NEUT]], and [[TRGV]] for all known places in the game data that can trigger BSL functions.
Besides the automatic starting point represented by the "main" function, there are types of game data which specify the names of functions to be called upon certain events. For instance, characters can trigger script functions upon their death, and trigger volumes can call functions when they are entered or exited. See [[CHAR]], [[OBD:BINA/OBJC/CONS|BINA/CONS]], [[OBD:BINA/OBJC/DOOR|BINA/DOOR]], [[NEUT]], and [[TRGV]] for all known places in the game data that can trigger BSL functions.


Note that the optional [[IGMD/global|global]] folder is also loaded for all levels, but any function found in a global .bsl file will be stranded, only accessible by console, unless you edit a function in some level's existing BSL file so that it calls your global function.
Note that the optional [[IGMD/global|global folder]] is also loaded for all levels, but if you create global/ and add a .bsl file to it, there is nothing to run the code unless you call one of its functions from the console or edit one of the existing levels' BSL files to call it.
 
==Built-in commands==
The documentation on this page centers around creating your own functions and variables. But Oni contains about 500 built-in functions and variables, referred to collectively as commands. It's the calls to these commands which are responsible for driving level scripts. These calls are organized and managed using functions and variables of your own creation, which you'll learn how to make by reading this page.
 
Looking at a BSL script, there is no immediate way to tell whether a function or variable is built-in or defined elsewhere in the scripts for that level. For instance "you_win" is a function defined in various scripts and "win" is a built-in function, and they look the same when called. The answer can only be found by searching the level's scripts for a definition of the function/variable or by knowing the names of the built-in ones. You will find the built-in commands listed on [[BSL:Functions]] and [[BSL:Variables]].
 
Unlike the code in Oni's .bsl files, built-in functions and variables such as [[ai2_allpassive]] and [[chr_big_head]] are hardcoded into Oni, that is, they were defined in the engine code and then registered as accessible from BSL. This means that they are essentially [[wp:Black box|black boxes]]: you are only given an external description of the input they take and the outcome of this input, but you cannot see the actual code behind them.
 
When using the developer console, you can manually call a built-in function, and can get and set the value of a built-in variable. However, you cannot actually define functions or declare variables through the console.


==Syntax==
==Syntax==
Line 29: Line 38:
  dmsg("Statement 2")
  dmsg("Statement 2")


The third example is partly in old-style syntax, which is discussed under [[#Old vs. new syntax|Old vs. new syntax]].
The third example is partly in old-style syntax, which is discussed under {{SectionLink||Old vs. new syntax}}.


===Compound statements===
===Compound statements===
Line 40: Line 49:
  <b>}</b>
  <b>}</b>


The purpose of doing this is to place statements under either a function declaration (see [[#Declaration|Declaration]]) or an "if" statement (see [[#Conditional|Conditional]]). Such a group of statements is referred to as a "block", and sometimes also defines a "scope". Traditionally in programming, anything inside a certain scope can be seen from elsewhere within that scope, or from within a scope inside that scope, but not from a scope outside that scope. This allows careful management of which code can alter which data. In BSL's case, however, there are certain issues with scope not being respected (see the [[#if|if statement]] section). Variables outside of all scopes (including functions) are referred to as "globals".
The purpose of doing this is to place statements under either a function declaration (see {{SectionLink||Declaration}}) or an "if" statement (see {{SectionLink||Conditional}}). Such a group of statements is referred to as a "block", and sometimes also defines a "scope". Traditionally in programming, anything inside a certain scope can be seen from elsewhere within that scope, or from within a scope inside that scope, but not from a scope outside that scope. This allows careful management of which code can alter which data. In BSL's case, however, there are certain issues with scope not being respected (see {{SectionLink||if}}). Variables outside of all scopes (including functions) are referred to as "globals".


===Comments===
===Comments===
Line 55: Line 64:
  }
  }


Do not use a trailing comment unless you end the statement with a semicolon (see [[#Old vs. new syntax|Old vs. new syntax]] for explanation).
Do not use a trailing comment unless you end the statement with a semicolon (see {{SectionLink||Old vs. new syntax}} for explanation).


In documentation outside of source code or script files, such as this page's BSL samples, comments are sometimes used to tell the reader something in a way that won't break the actual code if the user copies the whole block of text into a script file, comments and all.
In documentation outside of source code or script files, such as this page's BSL samples, comments are sometimes used to tell the reader something in a way that won't break the actual code if the user copies the whole block of text into a script file, comments and all.
Line 62: Line 71:
BSL allows two kinds of syntax to be used somewhat interchangeably, one of which is lighter on punctuation requirements. There's the simpler style, called "old style":
BSL allows two kinds of syntax to be used somewhat interchangeably, one of which is lighter on punctuation requirements. There's the simpler style, called "old style":


  dprint "Hello"
  [[dprint]] "Hello"


and the more strict style, called "new style", which resembles the syntax of [[wikipedia:C_(programming_language)|C]]:
and the more strict style, called "new style", which resembles the syntax of the [[wp:C (programming language)|C language]]:


  dprint("Hello");
  dprint("Hello");
Line 79: Line 88:
...and the fact that semicolons are always needed at the end of variable declarations:
...and the fact that semicolons are always needed at the end of variable declarations:


  a = 3 # this line is valid old-style syntax (except for this comment)...
  # The following line is valid old-style syntax...
  var int a = 3 # ...this line is not (even without this comment!)
  a = 3
# ...but the following line is not
var int a = 3


Thus, it's recommended to consistently use the "new" style of BSL. It requires a bit more typing, but it creates safer and more readable code. For consistency, all code on this page is in new-style syntax besides the old-style examples above.
Thus, it's recommended to consistently use the "new" style of BSL. It requires a bit more typing, but it creates safer and more readable code. For consistency, the rest of the code on this page is written in new-style syntax.


===Coding style===
===Coding style===
There are other aspects of the language which are flexible, and yet not connected to the old-style/new-style division. For instance, the quotes around "Hello" can be left out in both syntax versions of the above calls to dprint() and dmsg(). You can also omit stating the type and parameters of a function, with "void" being assumed in their absence (see [[#Functions|Functions]] if you need further explanation). You also do not need to use curly braces to enclose code that falls under an if/else statement if there is only a single line of code intended to be in that scope (see [[#Conditional|Conditional]] for examples). None of these syntax choices are in conflict with the "new style" syntax.
There are other aspects of the language which are flexible, and yet not connected to the old-style/new-style division. For instance, the quotes around "Hello" can be left out in both syntax versions of the above calls to dprint() and dmsg(). You can also omit stating the type and parameters of a function, with "void" being assumed in their absence (see {{SectionLink||Functions}} if you need further explanation). You also do not need to use curly braces to enclose code that falls under an if/else statement if there is only a single line of code intended to be in that scope (see {{SectionLink||Conditional}} for examples). None of these syntax choices are in conflict with the "new style" syntax.


Additionally, you can use whitespace and newlines in different ways:
Additionally, you can use whitespace and newlines in different ways:
Line 106: Line 117:
  }
  }


==Reserved words==
==Variables==
Here are the keywords that have special meaning in BSL. If you attempt to use these words as names for variables or functions, you will confuse the BSL parser and your script will fail.
A variable is a named storage space in memory for a value that you will want to access at different times, and perhaps change over time as well. The name of the variable is expected to start with a letter, but it can contain numbers and the underscore (<code>_</code>) at any point after that. A variable name cannot be the same as one of BSL's [[#Reserved words|reserved words]], e.g. an int named "return", and obviously it cannot share a name with any built-in or declared function either.


===Declaration===
===Declaring===
Before using a variable for the first time, you need to declare it with the "var" keyword. Function definitions must start with the "func" keyword.
When declaring a variable, the statement must begin with "var" and the type of the variable:


====var====
  var int i = 0; # declare a variable named "i" and initialize it to the value zero
  '''var''' int a = 0;


After this, you can refer to the variable merely as "a". See [[#Variables|Variables]] for details on declaring variables.
You don't have to initialize variables (set them to a value when declaring them), but it's usually a good idea. If you don't initialize, the engine will use a default value: "false" for bools, "0" for ints and floats, and "" (an empty string) for strings.


====func====
===Accessing===
'''func''' void spawn_team(void)
After a variable has been declared, you don't use "var" or the type of the variable when getting or setting its value:
{
    ...
}


Besides this, you can call the function merely by writing "spawn_team", but the proper new-style syntax is "spawn_team();". See [[#Functions|Functions]] for details on declaring functions.
i = 4;
if (i eq 0)
    [...]


===Type specification===
==Data types==
When declaring a variable or defining a function, you need to state the type of data that is contained in that variable or that is returned by the function.
When declaring a variable (more on this under {{SectionLink||Variables}}), you must specify the type of data it contains. When declaring a function (more on this under {{SectionLink||Functions}}), you can optionally specify the type of data it accepts and returns. You can choose from "bool" (can be "true" or "false", but see warning under {{SectionLink||bool}}), "int" (can be any whole number), "float" (a value with a decimal point), or "string" (some text).


====void, bool, int, float, string====
  var '''int''' x;
  var '''int''' a = 0;
  var '''bool''' happy_bomber = true;
  func '''int''' spawn_team('''int''' flag)
func '''string''' my_func(void)
  {
  {
     ...
     ...
  }
  }


Besides "void", which is only used in function definitions (indicating that no data is returned), these are the types of data that can be assigned to variables, passed into functions, and returned by functions. See [[#Data types|Data types]] for details.
Upon declaration, variables are automatically initialized to zero if no value is assigned explicitly, as in the above example with "x", or to a blank string in the case of a string variable.


===Conditional===
===void===
====if====
See {{SectionLink||Functions}} for the meaning of "void". Variables cannot be of type "void".
The only conditional statement in BSL is "if", with optional follow-up statements "else if" and "else", discussed below.
 
===bool===
'''(Note: See warning at bottom of this section; the bool type is not recommended for variables.)''' A [[wp:Boolean data type|Boolean]] number has one of two possible states. Thus, a bool can be assigned a value with the keywords "true" and "false".


  if (a operator b)
  var '''bool''' example = true;
func '''bool''' are_we_there_yet(void)
  {
  {
     # ...
     ...
  }
  }


For examples of operators, see [[#Operators|Operators]]. A typical example would be:
Giving a bool any value other than 0 or "false" will set it to 1 or "true". For instance, assigning a float to a bool will make the bool "true" or "1" unless the float value is "0.0", which will become "false". On Macs, you can make reference to bools without any relational operator. The following statements are equivalent in Mac Oni:


  if (kills < 10)
  if (example)
if (example eq true)


The result of the operation within the parentheses will always boil down to either "true" or "false" (a "yes" or a "no"), even if you would think that the result would be a number. For instance:
However, Windows Oni will fail to evaluate bare references to bool variables, and testing indicates the possibility of other bugs existing in Windows Oni's handling of bools. Additionally, Bungie West uses virtually no bool variables in Oni's scripts (though they do use built-in functions that return bool values). Since ints are handled much more reliably, it's recommended to use int for variables instead of bool; the values 1 and 0 can be used to represent "true" and "false".


if (8 - 8) # evaluates to false because 8 minus 8 is zero
===int===
if (8 - 7) # evaluates to true because 8 minus 7 is non-zero
A 32-bit signed [[wp:Integer|whole number]]; it can be positive, negative or zero, with a maximum value of 2,147,483,647 and a minimum value of -2,147,483,648.
 
Braces are optional when you only have one line of code under the "if" (this is also true for "else" and "else if", discussed below):
 
if (a > 0)
    spawn_enemy_at_flag(a);
spawn_considered = true;
 
Here, "spawn_considered" will be set to true regardless of whether the "if" condition was true and spawn_enemy_at_flag() was run. (The indentation has no effect on how the code runs; it's simply to guide the reader.) Though it's nice to save the vertical space by omitting the braces, the danger is in writing something like this:
 
if (a > 0)
    spawn_enemy_at_flag(a);
    spawn_considered = true;
 
That indentation on "spawn_considered" is misleading; when glancing at this code, you might expect that "spawn_considered" only is changed if "a" is greater than zero. It could be the script's writer really did want that line to be subject to the "if" statement, but he forgot to add braces. Always using braces around an "if" statement's body, even when it's one line long, will prevent that mistake.
 
Unfortunately, even if the programmer added braces around the two lines above, they would not run as intended. That's because, even ''with'' braces, BSL does not always respect scope for blocks of code under "if" statements; for instance, a "return" statement will fire even when the surrounding "if" condition is false (see [[#Flow interrupt|Flow interrupt]] for an example). An even more alarming example of bad scoping is this:


  func void broken_if(void)
var '''int''' example = -1000;
  func '''int''' get_enemy_count(void)
  {
  {
     var int one = 1;
     ...
    var int two = 2;
    var bool one_equals_two = false;
    if (one eq two) # this condition will evaluate to false, but...
      one_equals_two = true; # ...this will still run
    if (one_equals_two)
      dprint("Uh-oh.");
    else
      dprint("Phew.");
  }
  }


Bungie West was aware of this bug, and would work around it in Oni's level scripting by making a global variable and then calling a function to modify that variable. This works because function calls will not fire out of scope:
Note that the value wraps around, which means that once it passes its maximum or minimum value, it starts over from the other end. For instance, the function...


var bool one_equals_two;
  func void overflow_test(void)
  func void fixed_if(void)
  {
  {
     var int one = 1;
     var int overflow = -2147483648;
     var int two = 2;
     overflow = overflow - 1;
    set_oet(false);
    if (one eq two)
      set_oet(true);
    if (one_equals_two)
      dprint("Uh-oh.");
    else
      dprint("Phew.");
}
func void set_oet(bool input)
{
    one_equals_two = input;
  }
  }


Another workaround to this bug is too avoid local variables and boolean variables (as both seem to be buggy by default), so instead of using local variables you should use global variables and instead of boolean variables you should use int variables. The int variables can act as boolean values, you can use 1 to "true and 0 to "false".
...will print the positive number "int32: 2147483647" to screen when it runs.
 
Adding a float or a bool to an int yields odd and very high results, even if the float or bool could in theory be seamlessly converted to an int. Assigning a float to an int correctly assigns the truncated integer to the int. Assigning a bool to an int works correctly, the bool being treated as either 0 or 1.


Here's the code above working without the conditions bug, using the workaround with global variables and int variable acting as boolean:
===float===
A [[wp:Floating-point arithmetic|floating-point]] number. Bungie West actually never used floats in their scripts for Oni, which explains why there are certain rough aspects to them. For instance, floats are limited to six decimal places of precision:


  var int one = 1;
  var '''float''' good_example = 10.5; # returns as "10.500000"
  var int two = 2;
  var '''float''' too_precise = 3.141592653589793; # returns as "3.141593"
var int one_equals_two = 0;
func void fixed_if(void)
{
   
    if (one eq two) # this condition will evaluate to false, but...
      one_equals_two = 1; # ...now this DOES'T run! Yeahhh!
    if (one_equals_two eq 1)
      dprint("Uh-oh.");
    else
      dprint("Phew.");
}


One very handy feature that you won't see used in Oni's game scripts is that the logical operators "and" and "or" can string together multiple conditions:
Thus, adding a value below the sixth decimal place to a float will have no effect, e.g. 3.000000 + 0.0000001 = 3.000000. Additionally, adding an int value (or a bool value) to a float has no effect:


  if ((a < b) and (c > d))
  func float test_float_addition(void)
  {
  {
     # ...
     var float add_to_me = 10.5;
    add_to_me = add_to_me + '''1''';
    return add_to_me; '''# returns "float: 10.500000"'''
  }
  }


If you want to express the opposite of some condition, you can use the "!" operator (pronounced "not", and also unused by Bungie West). The following two statements have the same meaning:
As opposed to proper float math:


  if (!one_equals_two)
  func float float_test(void)
if (one_equals_two eq false)
 
There's no functional need for the "!"; it just saves space.
 
====else====
When an "if" statement evaluates as false, you can use "else" to perform an alternate action:
 
var int one = 1;
var int two = 2;
if (one eq two)
  {
  {
     # these commands will not run
     var float add_to_me = 10.5;
}
    add_to_me = add_to_me + '''1.0''';
else
     return add_to_me; '''# returns "float: 11.500000"'''
{
     # these are the commands that will run
  }
  }


Though "else" is not used in Oni's existing BSL scripts, it seems to work fine.
===string===
A sequence of textual characters. BSL does not have any built-in way of performing the string manipulations that you would expect from a scripting language, such as concatenation and trimming, though see below for a simple trimming hack. It also does not provide a reliable method of string comparison (see [[BSL:String comparison|HERE]] for a detailed examination).


====else if====
var '''string''' example = "hello";
As is common in other programming languages, you can also specify an "if" condition that should only be evaluated if the previous condition was false, by writing "else if":


if (error eq 0)
Trying to give a string a non-textual value (bool/int/float) gives the error "illegal type convertion" (sic). Trying to add two strings with a '+' operator crashes the game, rather than concatenating them as it would in some other languages. Adding a float to a string crashes too. Bools have no effect whatsoever.
    dprint("Everything is fine.");
else if (error eq 1)
    dprint("There are too many enemies to add one more.");
else # handle all other possibilities
    dprint("Unknown error code!");


Programmers commonly use "else if" statements to save some CPU cycles. For instance, if you had a long string of "if" statements to handle different possible values for a variable, why should they all be evaluated once you have already found the one that is true? Imagine the above example, but with 30 possible values for "error". In reality, you won't find much benefit in trying to save cycles with BSL since typical game scripts are not computationally expensive. However, "else if" can also simplify the flow of logic:
Trying to add an int value to a string creates some cool and unexpected results. For instance, adding a number between 5 and 11 removes the first character from the string. A number greater than or equal to 12 removes the first two, and so on. Subtraction does not yield the reverse effect, even if you try it with the maximum int value (in which case Oni crashes). Subtracting numbers can, however, result in printing random strings from Oni's memory, such as BSL function names!


if (d eq 0)
"(null)" is the value that strings assume when they have not been given a value yet; it cannot be assigned to a string manually.
    ...
if (d eq 1)
    ...
if ((n eq d) and (n > 0) and (d > 1))
    ...
if ((n eq 0) and (d > 1))
    ...


In checking the condition of "n" and "d" in this example, we have to keep checking after the second statement whether "d" is greater than 1; otherwise, multiple "if" statements could evaluate to true and execute their code, and we only want to act on one of these possible situations. Using "else if" allows us to be confident that only one statement can run:
  func string string_test(void)
 
  if (d eq 0)
    ...
else if (d eq 1)
    ...
else if (n eq d)
    ...
else if (n eq 0)
    ...
 
Notice how we can shorten the logical tests without losing anything. "d" is still implicitly required to be non-zero in statements 2-4, or else they will not even be evaluated.
 
===Flow interrupt===
Use these keywords to interrupt the execution of BSL.
 
====return====
On its own, "return" exits the function early. You can also place a variable or constant after the keyword to pass that value back to a calling function. The fact that "return" is not used in Oni's scripts might explain this bug:
 
var int one = 1;
var int two = 2;
if (one eq two)
  {
  {
     return; # this could not possibly be reached... or could it?
     var string example;
}
     return example; # prints "string: (null)"
else
{
     # this code should run, right?
  }
  }
Yes, "return" is also subject to the scope bug discussed in the [[#Conditional|Conditional]] section above. The statements under "else" will never run. The "return" will actually kick in and the function will exit, even though the surrounding control statement did not evaluate to true.
However, "return" is still useful at the end of a function for returning data to the function that called that one; see the section on [[#Returning|returning function values]].
====sleep====
You can also delay execution of a script using "sleep", passing it a number in sixtieths of a second:
sleep(60); # pause execution of BSL at this point for one second
Sometimes you will see in the original scripts a call to sleep with a 'f' character before the sixtieths of a second like this: sleep(f60), but apparently there is no difference by using this extra character.
Note that you cannot have a sleep statement in a function that returns a value, probably to avoid variables being changed at unexpected times.
===Loop===
It is generally desirable in any program to be able to run the same code over and over, such as repeatedly spawning enemies. This is one of BSL's biggest limitations, as it has no conventional loop statement like C's "for" or "while", but there are two indirect methods for looping: (1) use "fork" on a function (see the section on [[#Looping|looping functions]]), and (2) "schedule-at"/"schedule-repeat-every" (see [[#Concurrency|Concurrency]]).
===Multi-threading===
====fork, schedule-at, schedule-repeat-every====
See [[#Concurrency|Concurrency]] for the use of these keywords.
===Obsolete===
====iterate over ... using ...====
An unfinished feature, "iterate" was going to utilize "iterator variables", but these don't exist.
====for====
Whatever Bungie West's intention for this keyword may have been, it doesn't do anything.


==Operators==
==Operators==
Line 352: Line 241:
|}
|}


Note that "=" is '''not''' for checking if one entity is equal to another (see "eq" below for that).
Note that "=" is '''not''' for checking if one entity is equal to another (see "eq" under {{SectionLink||Relational}} for that).


===Arithmetical===
===Arithmetical===
These operations do not change the numbers that the operators act upon; they merely provide a resulting number for your use. Note that there is no operator for multiplication or division. Bungie West was only concerned with creating enough scripting power to drive Oni, and they did not need "higher math" for this. See [[#Data types|Data types]] to learn the details of how math works between different data types.
These operations do not change the numbers that the operators act upon; they merely provide a resulting number for your use. Note that there is no operator for multiplication or division. Bungie West was only concerned with creating enough scripting power to drive Oni, and they did not need "higher math" for this. See {{SectionLink||Data types}} to learn the details of how math works between different data types.


{| class="wikitable"   
{| class="wikitable"   
Line 393: Line 282:
  }
  }


Alternatively, you can use the sum operator, which doesn't suffer from this issue:
Alternatively you can use the sum operator, which doesn't suffer from this issue:


  if (5+-6 eq -1)
  if (5+-6 eq -1)
Line 405: Line 294:


===Relational===
===Relational===
These statements return true/false values for use in "if" statements.
These operators return true/false values for use in "if" statements.


{| class="wikitable"   
{| class="wikitable"   
Line 437: Line 326:
|}
|}


Note: Although most of these operators are intended for use with numbers, ''eq'' and ''ne'' work for strings too.
Note: These operators are intended for use with numbers; strings cannot make reliable use of comparators, as explained under {{SectionLink||string}}.


===Logical===
===Logical===
Line 447: Line 336:
| !
| !
| Not
| Not
| Logical Not. Reverses the result of a test. "!some_bool" is the same as saying "some_bool eq false" or "some_bool ne true".
| Logical Not. Reverses the result of a test.
|-
|-
| and
| and
Line 458: Line 347:
|}
|}


See the [[#if|"if" section]] to learn how to use the logical operators.
To illustrate the '!' ("not") operator, the following three lines are equivalent:
!(some_bool eq true)
some_bool eq false
some_bool ne true


==Data types==
See {{SectionLink||if}} for detailed examples of using the logical operators.
When declaring a variable or a function (more on this under the [[#Functions|Functions]] and [[#Variables|Variables]] sections), you must specify the type of data it contains, accepts, or returns. You can choose from "bool" (can be true/false), "int" (can be any whole number), "float" (a value with a decimal point), or "string" (some text).


var '''int''' x;
==Reserved words==
var '''bool''' happy_bomber = true;
Here are the keywords that have special meaning in BSL. If you attempt to use these words as names for variables or functions, you will confuse the BSL parser and your script will fail.
func '''string''' my_func(void)
{
    ...
}


Upon declaration, variables are automatically initialized to zero if no value is assigned explicitly, as in the above example with "x", or to a blank string in the case of a string variable.
===Declaration===
Before using a variable for the first time, you need to declare it with the "var" keyword. Function definitions must start with the "func" keyword.


===void===
====var====
See [[#Functions|Functions]]. Variables cannot be type "void".
'''var''' int a = 0;


===bool===
After this, you refer to the variable merely as "a". See {{SectionLink||Variables}} for details on declaring variables.
A [[wikipedia:Boolean data type|Boolean]] number has one of two possible states. Thus, a bool can be either 1 or 0. You can also use the keywords "true" and "false".


var bool example = 1;
====func====
  func bool are_we_there_yet(void)
  '''func''' void spawn_team(void)
  {
  {
     ...
     ...
  }
  }


Giving a bool any value other than 0/false will set it to 1/true. For instance, assigning a float to a bool will make the bool "true" or "1" unless the float value is "0.0", which will become "false".
After this, you call the function merely by writing its name; "spawn_team" would work, but the proper new-style syntax is "spawn_team();". See {{SectionLink||Functions}} for details on declaring functions.


===int===
===Type specification===
A 32-bit signed [[wikipedia:integer|whole number]]; that is, it can be positive, negative or zero, with a maximum value of 2,147,483,647 and a minimum value of -2,147,483,648.
When declaring a variable or defining a function, you need to state the type of data that is contained in that variable or that is returned by the function.


  var int example = -1000;
====void, bool, int, float, string====
  func int get_enemy_count(void)
  var '''int''' a = 0;
  func '''int''' spawn_team('''int''' flag)
  {
  {
     ...
     ...
  }
  }


Note that the number wraps around, which means that once it passes its maximum or minimum value, it starts over from the other end. For instance, the function...
Besides "void", which is only used in function definitions (indicating that no data is returned), these are the types of data that can be assigned to variables, passed into functions, and returned by functions. See {{SectionLink||Data types}} for details.
 
===Conditional===
====if====
The only conditional statement in BSL is "if", with optional follow-up statements "else if" and "else", discussed below.


  func void overflow_test(void)
  if (a operator b)
  {
  {
     var int overflow = -2147483648;
     # ...
    overflow = overflow - 1;
  }
  }


...will print "int32: 2147483647" to screen when it runs.
For examples of operators, see {{SectionLink||Operators}}. A typical example would be:
 
if (kills < 10)
 
The result of the operation within the parentheses will always boil down to either "true" or "false" (a "yes" or a "no"), even if you would think that the result would be a number. For instance:
 
if (8 - 8) # evaluates as false because 8 minus 8 is zero
if (8 - 7) # evaluates as true because 8 minus 7 is non-zero
 
Braces are optional when you only have one line of code under the "if" (this is also true for "else" and "else if", discussed below):
 
if (a > 0)
    spawn_enemy_at_flag(a);
spawn_considered = 1;
 
Here, "spawn_considered" will be set to 1 regardless of whether the "if" condition was true and spawn_enemy_at_flag() was run. (The indentation has no effect on how the code runs; it's simply to guide the reader.) Though it's nice to save the vertical space by omitting the braces, the danger is in writing something like this:


Adding a float or a bool to an int yields odd and very high results, even if the float or bool could in theory be seamlessly converted to an int. Assigning a float to an int correctly assigns the truncated integer to the int. Assigning a bool to an int works correctly, the bool being treated as either 0 or 1.
if (a > 0)
    spawn_enemy_at_flag(a);
    spawn_considered = 1;


===float===
That indentation on "spawn_considered" is misleading; when glancing at this code, you might expect that "spawn_considered" only is changed if "a" is greater than zero. It could be the script's writer really did want that line to be subject to the "if" statement, but he forgot to add braces. Always using braces around an "if" statement's body, even when it's one line long, will prevent that mistake.
A [[wikipedia:floating point|floating-point]] number. Bungie West actually never used floats in their scripts for Oni, which explains why there are certain rough aspects to them. For instance, floats are limited to six decimal places of precision:


var float good_example = 10.5; # returns as "10.500000"
Unfortunately, even ''with'' braces, BSL does not always respect scope for blocks of code under "if" statements; for instance, a "return" statement will fire even when the surrounding "if" condition is false (see {{SectionLink||Flow interrupt}} for an example). An even more alarming example of bad scoping is this:
var float too_precise = 3.141592653589793; # returns as "3.141593"


Thus, adding a value below the sixth decimal place to a float will have no effect, e.g. 3.000000 + 0.0000001 = 3.000000. Additionally, adding an int (or a bool) to a float has no effect:
func void broken_if(void)
{
    var int one = 1;
    var int two = 2;
    var int one_equals_two = 0;
    if (one eq two) # this condition will evaluate to false, but...
      one_equals_two = 1; # ...this will still run
    if (one_equals_two eq 1)
      dprint("Uh-oh.");
    else
      dprint("Phew.");
}


  func float test_float_addition(void)
Bungie West was aware of this bug, and avoided it by never setting variables under "if" statements. However, you can safely do so as long as you use a global variable. For some reason, assigning a global variable under an "if" will not fire out of scope:
 
var int one_equals_two = 0;
  func void fixed_if(void)
{
    var int one = 1;
    var int two = 2;
    if (one eq two)
      one_equals_two = 1;
    if (one_equals_two eq 1)
      dprint("Uh-oh.");
    else
      dprint("Phew.");
}
 
One very handy feature that you won't see used in Oni's game scripts is that the logical operators "and" and "or" can string together multiple conditions:
 
if ((a < b) and (c > d))
  {
  {
     var float add_to_me = 10.5;
     # ...
    add_to_me = add_to_me + '''1''';
    return add_to_me; '''# returns "float: 10.500000"'''
  }
  }


As opposed to proper float math:
Another unused feature of BSL is that you can express the opposite of some condition by using the "!" operator (pronounced "not"). The following two statements have the same meaning:
if (!(one_equals_two eq 1))
if (one_equals_two eq 0)
 
====else====
When an "if" statement evaluates as false, you can use "else" to perform an alternate action:


  func float float_test(void)
  var int one = 1;
var int two = 2;
if (one eq two)
{
    # these commands will not run
}
else
  {
  {
     var float add_to_me = 10.5;
     # these are the commands that will run
    add_to_me = add_to_me + '''1.0''';
    return add_to_me; '''# returns "float: 11.500000"'''
  }
  }


===string===
Though "else" is not used in Oni's existing BSL scripts, it seems to work fine.
A sequence of textual characters. BSL does not provide any explicit means for manipulating strings (concatenation, trimming, etc.), though see below for a simple trimming hack.
 
====else if====
As is common in other programming languages, you can also specify an "if" condition that should only be evaluated if the previous condition was false, by writing "else if":
 
if (error eq 0)
    dprint("Everything is fine.");
else if (error eq 1)
    dprint("There are too many enemies to add one more.");
else # handle all other possibilities
    dprint("Unknown error code!");
 
Programmers commonly use "else if" statements to save some CPU cycles. For instance, if you had a long string of "if" statements to handle different possible values for a variable, why should they all be evaluated once you have already found the one that is true? Imagine the above example, but with 30 possible values for "error". In reality, you won't find much benefit in trying to save cycles with BSL since typical game scripts are not computationally expensive. However, "else if" can also simplify the flow of logic:
 
if (d eq 10)
    # special-case code for d being 10
if ((d ne 10) and (d eq 3))
    # special-case code for d being 3
if ((d ne 10) and (d ne 3) and (d > 0))
    # code for handling any other positive value for d


var string example = "hello";
In checking the condition of "d" in this example, we have to keep checking after the first statement whether our previous conditions were not true; if we didn't, multiple "if" statements could evaluate to true and execute their code, and we only want to act on one of these possible situations. Using "else if" allows us to be confident that only one statement can run:


Trying to give a string a non-textual value (bool/int/float) gives the error "illegal type convertion" (sic). Trying to add two strings together simply crashes the game, rather than concatenating them as it would in some other languages. Adding a float to a string crashes too. Bools have no effect whatsoever.
if (d eq 10)
    # special-case code for d being 10
else if (d eq 3)
    # special-case code for d being 3
else if (d > 0)
    # code for handling any other positive value for d


Trying to add an int value to a string creates some cool and unexpected results. For instance, adding a number between 5 and 11 removes the first character from the string. A number greater than or equal to 12 removes the first two, and so on. Subtraction does not yield the reverse effect, even if you try it with the maximum int value (in which case, Oni crashes). Subtracting numbers can, however, result in printing random strings from Oni's memory, such as BSL function names!
Thanks to the "else" keyword, "d" is implicitly guaranteed to not be 10 if statement 2's code is being run, and to not be 10 or 3 if statement 3's code is being run.


"(null)" is the value that strings assume when they have not been given a value yet; it cannot be assigned to a string manually.
===Flow interrupt===
Use the keywords below to interrupt the execution of BSL. Note that Bungie West also frequently used these built-in commands to halt the execution of a script until the current or previous command finished: [[chr_animate_block]], [[chr_envanim_block]], [[chr_playback_block]], [[cm_anim_block]], [[cm_interpolate_block]], [[cm_orbit_block]], [[env_anim_block]], [[env_setanim_block]], [[sound_dialog_play_block]].
 
====return====
On its own, "return" exits the function early. You can also place a variable or constant after the keyword to pass that value back to a calling function. The fact that "return" is not used in Oni's scripts might explain this bug:


  func string string_test(void)
  var int one = 1;
var int two = 2;
if (one eq two)
{
    return; # this could not possibly be reached... or could it?
}
else
  {
  {
     var string example;
     # this code should run, right?
    return example; # prints "string: (null)"
  }
  }
Yes, "return" is also subject to the scope bug discussed under {{SectionLink||Conditional}}. The statements under "else" will never run. The "return" will actually kick in and the function will exit, even though the surrounding control statement did not evaluate to true.
However, "return" is still useful at the end of a function for returning data to the function that called that one; see {{SectionLink||Returning}}.
====sleep====
You can also delay execution of a script using "sleep", passing it a number in sixtieths of a second. This keyword was heavily used by Bungie West and could be considered their primary method of executing events with the right timing. Its usage is simple:
sleep(60); # pause execution of BSL at this point for one second
In Oni's level scripts, you will often see a call to "sleep" with an 'f' preceding the sixtieths of a second like this: "sleep(f60)". There is no change in functionality when using this character (it may be a relic of an earlier version of "sleep"), so you can leave it out.
Note that you cannot have a sleep statement in a function that returns a value, probably to avoid variables being changed at unexpected times.
===Loop===
It is generally desirable in any program to be able to run the same code over and over, such as repeatedly spawning enemies. This is one of BSL's biggest limitations, as it has no conventional loop statement like C's "for" or "while", but there are two indirect methods for looping: (1) use "fork" on a function (see {{SectionLink||Looping}}), and (2) "schedule-at"/"schedule-repeat-every" (see {{SectionLink||Concurrency}}).
===Multi-threading===
====fork, schedule-at, schedule-repeat-every====
See {{SectionLink||Concurrency}} for the use of these keywords.
===Obsolete===
====iterate over ... using ...====
An unfinished feature, "iterate" was going to utilize "iterator variables", but these don't exist.
====for====
Bungie West may have once intended to create a C-like "for loop", but this keyword doesn't do anything.


==Functions==
==Functions==
A function is a block of code that can be accessed repeatedly whenever you want to perform a certain task. As in mathematics, functions can be passed input and can return output, though unlike in mathematics, they do not need to do either of those things in order to be useful, as BSL functions can affect external data by modifying global variables and by calling built-in functions which affect the game environment (see [[#Built-in functions and variables|Built-in functions and variables]] section).
A function is a block of code that can be accessed repeatedly whenever you want to perform a certain task. As in mathematics, functions can be passed input and can return output, though unlike in mathematics, they do not need to do either of those things in order to be useful, as BSL functions can affect external data by modifying global variables and by calling built-in functions which affect the game environment (see {{SectionLink||Built-in commands}}).


===Defining===
===Defining===
When beginning to define a function, you must begin the line with "func"...
When beginning to define a function, you must start the line with "func"...


  func int prepare_fight(int count, string enemy_name)
  func int prepare_fight(int count, string enemy_name)
Line 559: Line 565:
  }
  }


The input which the function accepts is listed between the parentheses, and separated by commas. First the type of the input is given ("int" in the first case), then the name of that input is given, which will be used within the function to refer to that input.
This line is known as the function signature. The type of the function comes after the "func" keyword, in this case "int", which means it can return a value of type "int". The input which the function accepts is listed between the parentheses and separated by commas. First the type of the input is given ("int" in the first case), then the name of that input is given; this name will be used within the function to refer to that input but has no meaning outside of it. These inputs are known as "parameters" or "arguments". BSL functions cannot accept more than eight parameters.


====void====
====void====
Functions do not need to accept or return data. When they don't, you use "void" to indicate this. Note that both "void"s can be omitted and will be assumed implicitly.
Functions do not need to accept or return data. When they don't, you use "void" to indicate this. Note that both "void"s can be omitted and will be assumed implicitly.


  func void func_start(string ai_name) # Returns nothing to calling function, but accepts a string
  func void func_start(string ai_name) # returns nothing to calling function, but accepts a string
  {
  {
     ...
     ...
  }
  }


  func void music_force_stop(void) # Neither returns nor accepts any data
  func void music_force_stop(void) # neither returns nor accepts any data
  {
  {
     ...
     ...
  }
  }


  func music_force_stop # Same signature as previous function
  func music_force_stop # same signature as previous function
  {
  {
     ...
     ...
Line 580: Line 586:


===Calling===
===Calling===
When you call a function it will execute its statements from top to bottom or until it hits a "[[#Returning|return]]" statement. This means that when you call a second function from inside another function, the second function will be executed until its end and then the rest of the first function will be executed. You can think of the function calls as nested parentheses, where the innermost statement must close before the next-innermost level can close, and so on. (For example, here is a parenthetical statement (and another one (and one more!) inside of it)).
You do not use "func" when calling a function. You simply use its name:
You do not use "func" when calling a function. You simply use its name:


Line 597: Line 605:


===Recursive calling===
===Recursive calling===
A function can call itself, which is useful for various looping behavior, but a function can only call itself recursively up to about four times. Note that this limit is bypassed if you use "[[#Fork|fork]]" to call a function from within itself, but this no longer counts as recursing because the logic will not complete in a predictable inside-to-outside order. For a non-recursive way to loop a function, see [[#Looping|Looping]].
A function can call itself by invoking its own name:
 
var int global_iterator = 0;
func void bad_loop_idea(void)
{
    global_iterator = global_iterator + 1
    dmsg("Calling myself...");
    bad_loop_idea();
}
 
This might seem useful for producing a loop, but a function can only call itself recursively about four times before you hit a limit in BSL and the script fails silently. Open a level with the above code placed in its level script and call bad_loop_idea() from the dev console. After you stop seeing the "Calling myself..." messages, run the command "global_iterator" to query its value and you'll probably get "5", meaning the recursion failed between the fourth and fifth levels.
 
This limit can be bypassed if you use "[[#fork|fork]]" when calling a function from within itself, though this is not technically recursing: the logic will not complete in a predictable inside-to-outside order like our nested parentheses example in the previous section; instead it will run concurrently through all of the forked function calls. This can be very bad if you're trying to get reliable results, however there are ways to mitigate this. For a discussion of looping reliably with "fork", see {{SectionLink||Looping}}.


===Returning===
===Returning===
As mentioned under [[#Flow interrupt|Flow interrupt]], "return" exits the function at that point. Because of the bug documented in that section, you cannot exit early from a function under some logical condition. Since "return" can thus only be placed at the end of a function, there is no point in using it at all unless you are going to pass back a value by placing a variable name after "return":
As mentioned under {{SectionLink||Flow interrupt}}, "return" allows you to exit the function at that point. But because of the bug documented in that section you cannot use "return" conditionally, that is, exit early from a function depending on some logical condition ("if"). Since "return" can thus only be placed at the end of a function, there is no need to use it at all unless you are passing back a value with it like so:


  func int add_ten(int input)
  func int add_ten(int input)
Line 608: Line 628:
  }
  }


You would then use this function to set a variable, as follows:
When a function returns a value, you can use the function to set a variable as follows:


  some_number = add_ten(enemy_count);
  some_number = add_ten(enemy_count);


Function return values can also be used in "if" statements to provide a true/false condition. If you have a function that returns a bool...
Function return values can also be used in "if" statements. If you have a function that returns a bool...


  func bool is_it_safe(void)
  func bool is_it_safe(void)
Line 623: Line 643:
...then it could be called this way in some other function:
...then it could be called this way in some other function:


  if (is_it_safe())
  if (is_it_safe() eq true)
     dprint("It is safe.");
     dprint("It is safe.");
  else
  else
     dprint("It is not safe.");
     dprint("It is not safe.");
Bizarre bug warning: Calling dprint() and dmsg() more than once within a function will produce unexpected results. One of the dprint()/dmsg() calls may become the return value for the function (even if it it's type "void"), or create a return value of 0, and more than two calls also might be ignored.


===Looping===
===Looping===
The following function creates a loop using "[[#Fork|fork]]".
The following function creates a loop using "[[#fork|fork]]".


  var int counter;
  var int counter;
Line 646: Line 668:
  }
  }


Note that calling the same function again before it finishes running the first time can have undesired effects. Using "sleep" before each call can prevent this overlapping execution. A short function like the example above should not need a "sleep" statement, as the remainder of the function after the "fork" call will complete in the same tick. But the more complex the function, the more ticks you will need to allow for it to complete, requiring "sleep(1);", "sleep(3);", etc. before the "fork" call.
Note that calling the same function again before it finishes running the first time can have undesired effects. Imagine if you have another thread in the code accessing "counter" while increment() is still running in a loop; what value will it have at that moment? Real-world scripting scenarios will be more complex and thus more problematic if multiple concurrent calls to a function are in play at the same time. Using "sleep" before each fork call can prevent this overlapping execution. A short function like the example above should not need a "sleep" statement as the remainder of the function after the "fork" call will complete in the same tick. But the more complex the function, the more ticks you will need to allow for it to complete, requiring "sleep(1);", "sleep(3);", etc. before the "fork" call.
 
An alternate looping method: See the two variants of the "schedule" keyword under "Concurrency" below.


===Concurrency===
===Concurrency===
Line 652: Line 676:


====fork====
====fork====
If you call a function with "fork" before its name, the code below the function call continues without waiting for the function to return. If you place this sample code in a script and enter "fork_test" on the dev console, you will immediately see the output "int: 3" even though wait_for_a_second() is waiting for a second on its own thread. You can remove the "fork" keyword to see the difference.
If you call a function with "fork" before its name, the code below the function call continues without waiting for the function to return. If you place this sample code in a script and enter "fork_test" on the dev console, you will immediately see the output "int: 3" even though wait_for_a_second() was called before the value of "enemies" was printed to screen. You can remove the "fork" keyword to see the difference.


  func void fork_test(void)
  func void fork_test(void)
Line 670: Line 694:
  {
  {
     sleep(60);
     sleep(60);
    dprint("Done waiting.");
  }
  }


As you can see, you can use forked functions to perform delayed actions without holding up the rest of the script; this not only allows the use of "sleep", but also any built-in functions that hold up BSL by design, like [[chr_wait_animation]].
As you can see, fork-called functions can delay actions within themselves without holding up the rest of the script; this not only allows the use of "sleep" without affecting outside functions, but also any built-in functions that hold up BSL by design, like [[chr_wait_animation]].


Also be aware that accidentally calling a nonexistent function using "fork" will crash Oni; when "fork" is not used, BSL will not recognize that the unknown function name represents a function, and thus will not attempt to call it and end up crashing.
Be aware that accidentally calling a nonexistent function using "fork" will crash Oni; when "fork" is not used, BSL will successfully recognize it as an unknown function and thus will not attempt to call it.
 
Below are two types of uses for the "schedule" keyword. By scheduling a function call that operates on global variables and doesn't run unless a condition is true, you could simulate a slow-acting loop with these keyword sets.


====schedule ... at ...====
====schedule ... at ...====
Line 695: Line 718:
  schedule dprint("Is this annoying yet?") repeat 50 every 20;
  schedule dprint("Is this annoying yet?") repeat 50 every 20;


This is equivalent to the following loop in a C-style programming language:
This is equivalent to the following loop in a C-style language:


  int i = 0;
  int i = 0;
Line 709: Line 732:
  schedule dprint("Is this annoying yet?") repeat 0 every 20; # repeats forever
  schedule dprint("Is this annoying yet?") repeat 0 every 20; # repeats forever


"schedule-repeat-every" can be used in place of recursive functions that call themselves ''n'' times using "fork". For example,
"schedule-repeat-every" can be used in place of recursive functions that call themselves ''n'' times using "fork". For example…


  func void hey(void)
  func void hey(void)
  {
  {
     dmsg("hey");
     dmsg("hey");
     sleep(1);
     sleep(60);
     fork hey();
     fork hey();
  }
  }
Line 723: Line 746:
  }
  }


can be replaced by:
…can be replaced by…


  func void hey(void)
  func void hey(void)
Line 731: Line 754:
   
   
  schedule hey() repeat 0 every 60;
  schedule hey() repeat 0 every 60;
Note the significant difference between the built-in "sleep" function, which operates in whole seconds, and the "schedule" commands, which operate in ticks (1/60 of a second). Using "schedule" gives you much greater control over the timing of BSL execution than a "fork"+"sleep" loop would.
==Variables==
A variable is a storage space in memory for a value that you will want to access at different times, and perhaps change over time as well.
===Declaring===
When declaring a variable, the statement must begin with "var" and the type of the variable...
var int i = 0;
===Accessing===
...but not when getting or setting its value later:
i = 4;
var int c = i;
==Built-in functions and variables==
The above documentation for functions and variables explains how to create your own functions and variables. But, like any game, Oni provides access to a number of built-in functions and variables; these are used to write level scripts in BSL.
Built-in functions and variables such as "ai2_allpassive" and "ai2_blind" are hardcoded into Oni, that is, they were defined in C along with the rest of the engine code and then registered as accessible from BSL. This means that they are essentially [[wikipedia:Black_box|black boxes]]: you are only given an external description of the input they take and the outcome of this input, but you cannot see the actual code behind them.
When using the developer console, you can manually call a built-in function, and can get and set the value of a built-in variable. However, you cannot actually define functions or declare variables through the console.
Built-in functions and variables are listed on [[BSL:List]]. They are grouped together by subject matter in the [[:Category:Scripting tasks|Scripting tasks]] category.


[[Category:BSL docs]]
[[Category:BSL docs]]

Latest revision as of 03:58, 14 December 2023

Oni's level scripts are written in BFWBungieFrameWork, the underlying layer of the game engine. Scripting Language, or BSL. A scripting language allows you to create a set of plain-text files which employ branching logic and various operators according to certain rules of syntax in order to process data using functions that act upon variables. If any of those terms are not familiar to you now, keep reading. If you found that sentence boringly obvious, then you should be experienced enough to get by with the Introduction page, a sort of quick reference. This Manual page, by contrast, provides both a thorough and newbie-friendly introduction to the language and also documents every known bug in the language, which a modder writing a complex script will want to know about.

Script files

When Oni loads a level's resources, it also loads and parses all .bsl files in the level's designated IGMD folder (the name of this folder is specified by ONLV). The code automatically begins running with the function called "main", regardless of which .bsl file it is found in (Oni's scripts always place it in a file ending in "_main.bsl"). The code then flows through whichever functions are called by "main". You can also manually call any of those functions from the developer console – however, you cannot define variables or functions through the console.

Besides the automatic starting point represented by the "main" function, there are types of game data which specify the names of functions to be called upon certain events. For instance, characters can trigger script functions upon their death, and trigger volumes can call functions when they are entered or exited. See CHAR, BINA/CONS, BINA/DOOR, NEUT, and TRGV for all known places in the game data that can trigger BSL functions.

Note that the optional global folder is also loaded for all levels, but if you create global/ and add a .bsl file to it, there is nothing to run the code unless you call one of its functions from the console or edit one of the existing levels' BSL files to call it.

Built-in commands

The documentation on this page centers around creating your own functions and variables. But Oni contains about 500 built-in functions and variables, referred to collectively as commands. It's the calls to these commands which are responsible for driving level scripts. These calls are organized and managed using functions and variables of your own creation, which you'll learn how to make by reading this page.

Looking at a BSL script, there is no immediate way to tell whether a function or variable is built-in or defined elsewhere in the scripts for that level. For instance "you_win" is a function defined in various scripts and "win" is a built-in function, and they look the same when called. The answer can only be found by searching the level's scripts for a definition of the function/variable or by knowing the names of the built-in ones. You will find the built-in commands listed on BSL:Functions and BSL:Variables.

Unlike the code in Oni's .bsl files, built-in functions and variables such as ai2_allpassive and chr_big_head are hardcoded into Oni, that is, they were defined in the engine code and then registered as accessible from BSL. This means that they are essentially black boxes: you are only given an external description of the input they take and the outcome of this input, but you cannot see the actual code behind them.

When using the developer console, you can manually call a built-in function, and can get and set the value of a built-in variable. However, you cannot actually define functions or declare variables through the console.

Syntax

As with natural languages like English, there are rules about punctuation, spacing, and word order in BSL.

Statements

All code consists of discrete statements. These statements can be gathered into larger structures like "blocks" and "functions" to create more efficient programs.

Statement separators

BSL accepts two statement separators: the semicolon (;) and the line-break. The following three pieces of code are equivalent:

dmsg("Statement 1"); dmsg("Statement 2");

and

dmsg("Statement 1");
dmsg("Statement 2");

and

dmsg("Statement 1")
dmsg("Statement 2")

The third example is partly in old-style syntax, which is discussed under § Old vs. new syntax.

Compound statements

Compound statements are series of statements grouped together by a pair of curly braces:

{
   dmsg("Statement 1");
   dmsg("Statement 2");
   dmsg("Statement 3");
}

The purpose of doing this is to place statements under either a function declaration (see § Declaration) or an "if" statement (see § Conditional). Such a group of statements is referred to as a "block", and sometimes also defines a "scope". Traditionally in programming, anything inside a certain scope can be seen from elsewhere within that scope, or from within a scope inside that scope, but not from a scope outside that scope. This allows careful management of which code can alter which data. In BSL's case, however, there are certain issues with scope not being respected (see § if). Variables outside of all scopes (including functions) are referred to as "globals".

Comments

Comments are notes from the programmer to explain some code. In BSL, the comment marker is the "#". Any text after that character is not interpreted as BSL. Comments are supposed to be placed after a line of code...

var int a = 4; # here is a trailing comment explaining why 'a' is 4

...or above a line or block of code:

# The following block of code calculates the meaning of life
if (...)
{
   ...
}

Do not use a trailing comment unless you end the statement with a semicolon (see § Old vs. new syntax for explanation).

In documentation outside of source code or script files, such as this page's BSL samples, comments are sometimes used to tell the reader something in a way that won't break the actual code if the user copies the whole block of text into a script file, comments and all.

Old vs. new syntax

BSL allows two kinds of syntax to be used somewhat interchangeably, one of which is lighter on punctuation requirements. There's the simpler style, called "old style":

dprint "Hello"

and the more strict style, called "new style", which resembles the syntax of the C language:

dprint("Hello");

This dual syntax can make it more complicated to talk about the language, and you can also generate errors if you accidentally mix syntaxes. For instance, if you try to end a function call with a semicolon, but you don't use the new-style parentheses around the function arguments...

dprint "Hello";

...you'll get an error mentioning an "illegal token" (the semicolon) in an "old style" function call. There are also other potential pitfalls with "old style" syntax, such as the fact that trailing comments don't work:

dmsg "Hello" # this comment will disable the line below it
a = 4

...and the fact that semicolons are always needed at the end of variable declarations:

# The following line is valid old-style syntax...
a = 3
# ...but the following line is not
var int a = 3

Thus, it's recommended to consistently use the "new" style of BSL. It requires a bit more typing, but it creates safer and more readable code. For consistency, the rest of the code on this page is written in new-style syntax.

Coding style

There are other aspects of the language which are flexible, and yet not connected to the old-style/new-style division. For instance, the quotes around "Hello" can be left out in both syntax versions of the above calls to dprint() and dmsg(). You can also omit stating the type and parameters of a function, with "void" being assumed in their absence (see § Functions if you need further explanation). You also do not need to use curly braces to enclose code that falls under an if/else statement if there is only a single line of code intended to be in that scope (see § Conditional for examples). None of these syntax choices are in conflict with the "new style" syntax.

Additionally, you can use whitespace and newlines in different ways:

if (counter eq 3) {
   # do something
} else {
   # do something else
}

The above is three lines shorter than the below, but look at the difference in readability with this standard style, used throughout most of Oni's BSL scripts:

if (counter eq 3)
{
   # do something
}
else
{
   # do something else
}

Variables

A variable is a named storage space in memory for a value that you will want to access at different times, and perhaps change over time as well. The name of the variable is expected to start with a letter, but it can contain numbers and the underscore (_) at any point after that. A variable name cannot be the same as one of BSL's reserved words, e.g. an int named "return", and obviously it cannot share a name with any built-in or declared function either.

Declaring

When declaring a variable, the statement must begin with "var" and the type of the variable:

var int i = 0; # declare a variable named "i" and initialize it to the value zero

You don't have to initialize variables (set them to a value when declaring them), but it's usually a good idea. If you don't initialize, the engine will use a default value: "false" for bools, "0" for ints and floats, and "" (an empty string) for strings.

Accessing

After a variable has been declared, you don't use "var" or the type of the variable when getting or setting its value:

i = 4;
if (i eq 0)
   [...]

Data types

When declaring a variable (more on this under § Variables), you must specify the type of data it contains. When declaring a function (more on this under § Functions), you can optionally specify the type of data it accepts and returns. You can choose from "bool" (can be "true" or "false", but see warning under § bool), "int" (can be any whole number), "float" (a value with a decimal point), or "string" (some text).

var int x;
var bool happy_bomber = true;
func string my_func(void)
{
   ...
}

Upon declaration, variables are automatically initialized to zero if no value is assigned explicitly, as in the above example with "x", or to a blank string in the case of a string variable.

void

See § Functions for the meaning of "void". Variables cannot be of type "void".

bool

(Note: See warning at bottom of this section; the bool type is not recommended for variables.) A Boolean number has one of two possible states. Thus, a bool can be assigned a value with the keywords "true" and "false".

var bool example = true;
func bool are_we_there_yet(void)
{
   ...
}

Giving a bool any value other than 0 or "false" will set it to 1 or "true". For instance, assigning a float to a bool will make the bool "true" or "1" unless the float value is "0.0", which will become "false". On Macs, you can make reference to bools without any relational operator. The following statements are equivalent in Mac Oni:

if (example)
if (example eq true)

However, Windows Oni will fail to evaluate bare references to bool variables, and testing indicates the possibility of other bugs existing in Windows Oni's handling of bools. Additionally, Bungie West uses virtually no bool variables in Oni's scripts (though they do use built-in functions that return bool values). Since ints are handled much more reliably, it's recommended to use int for variables instead of bool; the values 1 and 0 can be used to represent "true" and "false".

int

A 32-bit signed whole number; it can be positive, negative or zero, with a maximum value of 2,147,483,647 and a minimum value of -2,147,483,648.

var int example = -1000;
func int get_enemy_count(void)
{
   ...
}

Note that the value wraps around, which means that once it passes its maximum or minimum value, it starts over from the other end. For instance, the function...

func void overflow_test(void)
{
   var int overflow = -2147483648;
   overflow = overflow - 1;
}

...will print the positive number "int32: 2147483647" to screen when it runs.

Adding a float or a bool to an int yields odd and very high results, even if the float or bool could in theory be seamlessly converted to an int. Assigning a float to an int correctly assigns the truncated integer to the int. Assigning a bool to an int works correctly, the bool being treated as either 0 or 1.

float

A floating-point number. Bungie West actually never used floats in their scripts for Oni, which explains why there are certain rough aspects to them. For instance, floats are limited to six decimal places of precision:

var float good_example = 10.5; # returns as "10.500000"
var float too_precise = 3.141592653589793; # returns as "3.141593"

Thus, adding a value below the sixth decimal place to a float will have no effect, e.g. 3.000000 + 0.0000001 = 3.000000. Additionally, adding an int value (or a bool value) to a float has no effect:

func float test_float_addition(void)
{
   var float add_to_me = 10.5;
   add_to_me = add_to_me + 1;
   return add_to_me; # returns "float: 10.500000"
}

As opposed to proper float math:

func float float_test(void)
{
   var float add_to_me = 10.5;
   add_to_me = add_to_me + 1.0;
   return add_to_me; # returns "float: 11.500000"
}

string

A sequence of textual characters. BSL does not have any built-in way of performing the string manipulations that you would expect from a scripting language, such as concatenation and trimming, though see below for a simple trimming hack. It also does not provide a reliable method of string comparison (see HERE for a detailed examination).

var string example = "hello";

Trying to give a string a non-textual value (bool/int/float) gives the error "illegal type convertion" (sic). Trying to add two strings with a '+' operator crashes the game, rather than concatenating them as it would in some other languages. Adding a float to a string crashes too. Bools have no effect whatsoever.

Trying to add an int value to a string creates some cool and unexpected results. For instance, adding a number between 5 and 11 removes the first character from the string. A number greater than or equal to 12 removes the first two, and so on. Subtraction does not yield the reverse effect, even if you try it with the maximum int value (in which case Oni crashes). Subtracting numbers can, however, result in printing random strings from Oni's memory, such as BSL function names!

"(null)" is the value that strings assume when they have not been given a value yet; it cannot be assigned to a string manually.

func string string_test(void)
{
   var string example;
   return example; # prints "string: (null)"
}

Operators

Here are all the operators you can use in BSL.

Assignment

Symbol Name Description
= Equals Sets the variable on the left-hand side to the right-hand value.

Note that "=" is not for checking if one entity is equal to another (see "eq" under § Relational for that).

Arithmetical

These operations do not change the numbers that the operators act upon; they merely provide a resulting number for your use. Note that there is no operator for multiplication or division. Bungie West was only concerned with creating enough scripting power to drive Oni, and they did not need "higher math" for this. See § Data types to learn the details of how math works between different data types.

Symbol Name Description
+ Add Provides the sum of two numbers.
- Subtract Gives the result of subtracting the second number from the first.

A quirk of the subtraction operator is that it is sensitive to whitespace. For instance, the following code will print "NOT -1":

if (5-6 eq -1)
{
   dmsg("-1");
}
else
{
   dmsg("NOT -1"); # this code will run
}

In order for the operator "-" to work correctly, you need to have a space between it and the numbers being subtracted, otherwise it may return unexpected results! So to fix the code above, you should write it like this:

if (5 - 6 eq -1)
{
   dmsg("-1"); # this code will run
}
else
{
   dmsg("NOT -1");
}

Alternatively you can use the sum operator, which doesn't suffer from this issue:

if (5+-6 eq -1)
{
   dmsg("-1"); # this code will run
}
else
{
   dmsg("NOT -1");
}

Relational

These operators return true/false values for use in "if" statements.

Symbol Name Description
eq Equal to Tests if two values are equal.
ne Not equal to Tests if two values are different.
> Greater than Tests if first number is greater than second one.
< Less than Tests if first number is smaller than second one.
>= Greater than or equal to Tests if a number is greater than, or equal to, another.
<= Less than or equal to Tests if a number is smaller than, or equal to, another.

Note: These operators are intended for use with numbers; strings cannot make reliable use of comparators, as explained under § string.

Logical

Symbol Name Description
! Not Logical Not. Reverses the result of a test.
and And Logical And.
or Or Logical Or.

To illustrate the '!' ("not") operator, the following three lines are equivalent:

!(some_bool eq true)
some_bool eq false
some_bool ne true

See § if for detailed examples of using the logical operators.

Reserved words

Here are the keywords that have special meaning in BSL. If you attempt to use these words as names for variables or functions, you will confuse the BSL parser and your script will fail.

Declaration

Before using a variable for the first time, you need to declare it with the "var" keyword. Function definitions must start with the "func" keyword.

var

var int a = 0;

After this, you refer to the variable merely as "a". See § Variables for details on declaring variables.

func

func void spawn_team(void)
{
   ...
}

After this, you call the function merely by writing its name; "spawn_team" would work, but the proper new-style syntax is "spawn_team();". See § Functions for details on declaring functions.

Type specification

When declaring a variable or defining a function, you need to state the type of data that is contained in that variable or that is returned by the function.

void, bool, int, float, string

var int a = 0;
func int spawn_team(int flag)
{
   ...
}

Besides "void", which is only used in function definitions (indicating that no data is returned), these are the types of data that can be assigned to variables, passed into functions, and returned by functions. See § Data types for details.

Conditional

if

The only conditional statement in BSL is "if", with optional follow-up statements "else if" and "else", discussed below.

if (a operator b)
{
   # ...
}

For examples of operators, see § Operators. A typical example would be:

if (kills < 10)

The result of the operation within the parentheses will always boil down to either "true" or "false" (a "yes" or a "no"), even if you would think that the result would be a number. For instance:

if (8 - 8) # evaluates as false because 8 minus 8 is zero
if (8 - 7) # evaluates as true because 8 minus 7 is non-zero

Braces are optional when you only have one line of code under the "if" (this is also true for "else" and "else if", discussed below):

if (a > 0)
   spawn_enemy_at_flag(a);
spawn_considered = 1;

Here, "spawn_considered" will be set to 1 regardless of whether the "if" condition was true and spawn_enemy_at_flag() was run. (The indentation has no effect on how the code runs; it's simply to guide the reader.) Though it's nice to save the vertical space by omitting the braces, the danger is in writing something like this:

if (a > 0)
   spawn_enemy_at_flag(a);
   spawn_considered = 1;

That indentation on "spawn_considered" is misleading; when glancing at this code, you might expect that "spawn_considered" only is changed if "a" is greater than zero. It could be the script's writer really did want that line to be subject to the "if" statement, but he forgot to add braces. Always using braces around an "if" statement's body, even when it's one line long, will prevent that mistake.

Unfortunately, even with braces, BSL does not always respect scope for blocks of code under "if" statements; for instance, a "return" statement will fire even when the surrounding "if" condition is false (see § Flow interrupt for an example). An even more alarming example of bad scoping is this:

func void broken_if(void)
{
   var int one = 1;
   var int two = 2;
   var int one_equals_two = 0;

   if (one eq two) # this condition will evaluate to false, but...
      one_equals_two = 1; # ...this will still run

   if (one_equals_two eq 1)
      dprint("Uh-oh.");
   else
      dprint("Phew.");
}

Bungie West was aware of this bug, and avoided it by never setting variables under "if" statements. However, you can safely do so as long as you use a global variable. For some reason, assigning a global variable under an "if" will not fire out of scope:

var int one_equals_two = 0;

func void fixed_if(void)
{
   var int one = 1;
   var int two = 2;

   if (one eq two)
      one_equals_two = 1;

   if (one_equals_two eq 1)
      dprint("Uh-oh.");
   else
      dprint("Phew.");
}

One very handy feature that you won't see used in Oni's game scripts is that the logical operators "and" and "or" can string together multiple conditions:

if ((a < b) and (c > d))
{
   # ...
}

Another unused feature of BSL is that you can express the opposite of some condition by using the "!" operator (pronounced "not"). The following two statements have the same meaning:

if (!(one_equals_two eq 1))
if (one_equals_two eq 0)

else

When an "if" statement evaluates as false, you can use "else" to perform an alternate action:

var int one = 1;
var int two = 2;
if (one eq two)
{
   # these commands will not run
}
else
{
   # these are the commands that will run
}

Though "else" is not used in Oni's existing BSL scripts, it seems to work fine.

else if

As is common in other programming languages, you can also specify an "if" condition that should only be evaluated if the previous condition was false, by writing "else if":

if (error eq 0)
   dprint("Everything is fine.");
else if (error eq 1)
   dprint("There are too many enemies to add one more.");
else # handle all other possibilities
   dprint("Unknown error code!");

Programmers commonly use "else if" statements to save some CPU cycles. For instance, if you had a long string of "if" statements to handle different possible values for a variable, why should they all be evaluated once you have already found the one that is true? Imagine the above example, but with 30 possible values for "error". In reality, you won't find much benefit in trying to save cycles with BSL since typical game scripts are not computationally expensive. However, "else if" can also simplify the flow of logic:

if (d eq 10)
   # special-case code for d being 10
if ((d ne 10) and (d eq 3))
   # special-case code for d being 3
if ((d ne 10) and (d ne 3) and (d > 0))
   # code for handling any other positive value for d

In checking the condition of "d" in this example, we have to keep checking after the first statement whether our previous conditions were not true; if we didn't, multiple "if" statements could evaluate to true and execute their code, and we only want to act on one of these possible situations. Using "else if" allows us to be confident that only one statement can run:

if (d eq 10)
   # special-case code for d being 10
else if (d eq 3)
   # special-case code for d being 3
else if (d > 0)
   # code for handling any other positive value for d

Thanks to the "else" keyword, "d" is implicitly guaranteed to not be 10 if statement 2's code is being run, and to not be 10 or 3 if statement 3's code is being run.

Flow interrupt

Use the keywords below to interrupt the execution of BSL. Note that Bungie West also frequently used these built-in commands to halt the execution of a script until the current or previous command finished: chr_animate_block, chr_envanim_block, chr_playback_block, cm_anim_block, cm_interpolate_block, cm_orbit_block, env_anim_block, env_setanim_block, sound_dialog_play_block.

return

On its own, "return" exits the function early. You can also place a variable or constant after the keyword to pass that value back to a calling function. The fact that "return" is not used in Oni's scripts might explain this bug:

var int one = 1;
var int two = 2;
if (one eq two)
{
   return; # this could not possibly be reached... or could it?
}
else
{
   # this code should run, right?
}

Yes, "return" is also subject to the scope bug discussed under § Conditional. The statements under "else" will never run. The "return" will actually kick in and the function will exit, even though the surrounding control statement did not evaluate to true.

However, "return" is still useful at the end of a function for returning data to the function that called that one; see § Returning.

sleep

You can also delay execution of a script using "sleep", passing it a number in sixtieths of a second. This keyword was heavily used by Bungie West and could be considered their primary method of executing events with the right timing. Its usage is simple:

sleep(60); # pause execution of BSL at this point for one second

In Oni's level scripts, you will often see a call to "sleep" with an 'f' preceding the sixtieths of a second like this: "sleep(f60)". There is no change in functionality when using this character (it may be a relic of an earlier version of "sleep"), so you can leave it out.

Note that you cannot have a sleep statement in a function that returns a value, probably to avoid variables being changed at unexpected times.

Loop

It is generally desirable in any program to be able to run the same code over and over, such as repeatedly spawning enemies. This is one of BSL's biggest limitations, as it has no conventional loop statement like C's "for" or "while", but there are two indirect methods for looping: (1) use "fork" on a function (see § Looping), and (2) "schedule-at"/"schedule-repeat-every" (see § Concurrency).

Multi-threading

fork, schedule-at, schedule-repeat-every

See § Concurrency for the use of these keywords.

Obsolete

iterate over ... using ...

An unfinished feature, "iterate" was going to utilize "iterator variables", but these don't exist.

for

Bungie West may have once intended to create a C-like "for loop", but this keyword doesn't do anything.

Functions

A function is a block of code that can be accessed repeatedly whenever you want to perform a certain task. As in mathematics, functions can be passed input and can return output, though unlike in mathematics, they do not need to do either of those things in order to be useful, as BSL functions can affect external data by modifying global variables and by calling built-in functions which affect the game environment (see § Built-in commands).

Defining

When beginning to define a function, you must start the line with "func"...

func int prepare_fight(int count, string enemy_name)
{
   ...
}

This line is known as the function signature. The type of the function comes after the "func" keyword, in this case "int", which means it can return a value of type "int". The input which the function accepts is listed between the parentheses and separated by commas. First the type of the input is given ("int" in the first case), then the name of that input is given; this name will be used within the function to refer to that input but has no meaning outside of it. These inputs are known as "parameters" or "arguments". BSL functions cannot accept more than eight parameters.

void

Functions do not need to accept or return data. When they don't, you use "void" to indicate this. Note that both "void"s can be omitted and will be assumed implicitly.

func void func_start(string ai_name) # returns nothing to calling function, but accepts a string
{
   ...
}
func void music_force_stop(void) # neither returns nor accepts any data
{
   ...
}
func music_force_stop # same signature as previous function
{
   ...
}

Calling

When you call a function it will execute its statements from top to bottom or until it hits a "return" statement. This means that when you call a second function from inside another function, the second function will be executed until its end and then the rest of the first function will be executed. You can think of the function calls as nested parentheses, where the innermost statement must close before the next-innermost level can close, and so on. (For example, here is a parenthetical statement (and another one (and one more!) inside of it)).

You do not use "func" when calling a function. You simply use its name:

music_force_stop();

Since no parameters are expected by music_force_stop(), we call it with empty parentheses. However, if parameters are expected, it looks like this:

prepare_fight(3, "Jojo");

In this case, we are passing a constant value of three into prepare_fight() (the function defined at the start of this section), inside of which it will be known as "count", and we are passing a constant string in as "enemy_name", but we could also have passed in variables by name:

var int num_heroes = 3;
var string enemy = "Jojo";
prepare_fight(num_heroes, enemy);

The values of these variables will still be referred to as "count" and "enemy_name" inside prepare_fight().

Recursive calling

A function can call itself by invoking its own name:

var int global_iterator = 0;
func void bad_loop_idea(void)
{
   global_iterator = global_iterator + 1
   dmsg("Calling myself...");
   bad_loop_idea();
}

This might seem useful for producing a loop, but a function can only call itself recursively about four times before you hit a limit in BSL and the script fails silently. Open a level with the above code placed in its level script and call bad_loop_idea() from the dev console. After you stop seeing the "Calling myself..." messages, run the command "global_iterator" to query its value and you'll probably get "5", meaning the recursion failed between the fourth and fifth levels.

This limit can be bypassed if you use "fork" when calling a function from within itself, though this is not technically recursing: the logic will not complete in a predictable inside-to-outside order like our nested parentheses example in the previous section; instead it will run concurrently through all of the forked function calls. This can be very bad if you're trying to get reliable results, however there are ways to mitigate this. For a discussion of looping reliably with "fork", see § Looping.

Returning

As mentioned under § Flow interrupt, "return" allows you to exit the function at that point. But because of the bug documented in that section you cannot use "return" conditionally, that is, exit early from a function depending on some logical condition ("if"). Since "return" can thus only be placed at the end of a function, there is no need to use it at all unless you are passing back a value with it like so:

func int add_ten(int input)
{
   var int the_result = input + 10;
   return(the_result);
}

When a function returns a value, you can use the function to set a variable as follows:

some_number = add_ten(enemy_count);

Function return values can also be used in "if" statements. If you have a function that returns a bool...

func bool is_it_safe(void)
{
   var bool result = false;
   # do some investigation here and set result to true if it is safe
   return(result);
}

...then it could be called this way in some other function:

if (is_it_safe() eq true)
   dprint("It is safe.");
else
   dprint("It is not safe.");

Bizarre bug warning: Calling dprint() and dmsg() more than once within a function will produce unexpected results. One of the dprint()/dmsg() calls may become the return value for the function (even if it it's type "void"), or create a return value of 0, and more than two calls also might be ignored.

Looping

The following function creates a loop using "fork".

var int counter;

func void increment_counter_to(int limit)
{
   counter = 0;
   fork increment(limit);
}

func void increment(int limit)
{
   counter = counter + 1;
   if (counter < limit)
      fork increment(limit);
}

Note that calling the same function again before it finishes running the first time can have undesired effects. Imagine if you have another thread in the code accessing "counter" while increment() is still running in a loop; what value will it have at that moment? Real-world scripting scenarios will be more complex and thus more problematic if multiple concurrent calls to a function are in play at the same time. Using "sleep" before each fork call can prevent this overlapping execution. A short function like the example above should not need a "sleep" statement as the remainder of the function after the "fork" call will complete in the same tick. But the more complex the function, the more ticks you will need to allow for it to complete, requiring "sleep(1);", "sleep(3);", etc. before the "fork" call.

An alternate looping method: See the two variants of the "schedule" keyword under "Concurrency" below.

Concurrency

When you call a function, all the code inside that function will finish running before giving control back to the calling function; that is, unless you use one of the following keyword sets to run parallel "threads" of BSL at the same time. Unlike robust programming languages, there is very little control over BSL's version of "multi-threading", so see the caveats at the end of the "fork" section.

fork

If you call a function with "fork" before its name, the code below the function call continues without waiting for the function to return. If you place this sample code in a script and enter "fork_test" on the dev console, you will immediately see the output "int: 3" even though wait_for_a_second() was called before the value of "enemies" was printed to screen. You can remove the "fork" keyword to see the difference.

func void fork_test(void)
{
   var int counter = 0;
   fork wait_for_a_second();
   var int enemies = count_enemies();
   enemies;
}

func int count_enemies(void)
{
   return(3);
}

func void wait_for_a_second(void)
{
   sleep(60);
   dprint("Done waiting.");
}

As you can see, fork-called functions can delay actions within themselves without holding up the rest of the script; this not only allows the use of "sleep" without affecting outside functions, but also any built-in functions that hold up BSL by design, like chr_wait_animation.

Be aware that accidentally calling a nonexistent function using "fork" will crash Oni; when "fork" is not used, BSL will successfully recognize it as an unknown function and thus will not attempt to call it.

schedule ... at ...

Allows you to schedule a function call a certain number of ticks in the future. Unused in Oni's scripts, but here's a working example:

schedule dmsg("BOOM") at 300;
schedule dmsg("1...") at 240;
schedule dmsg("2...") at 180;
schedule dmsg("3...") at 120;
schedule dmsg("4...") at 60;
dmsg("5...");

You are not allowed to set a variable to the return value of a scheduled function call, just like you cannot use "sleep" in a function that returns a value. This is presumably to avoid unpredictable changes being made to variables that share scope with other code.

schedule ... repeat ... every ...

Allows you to schedule a function call to repeat x times at a rate of y ticks. Unused in Oni's scripts, but here's a working example:

schedule dprint("Is this annoying yet?") repeat 50 every 20;

This is equivalent to the following loop in a C-style language:

int i = 0;
do
{
   print("Is this annoying yet?");
   wait(20);
   i++;
} while (i != 50); 

To create an infinite loop, you can use "0" or a negative number as the repeat value:

schedule dprint("Is this annoying yet?") repeat 0 every 20; # repeats forever

"schedule-repeat-every" can be used in place of recursive functions that call themselves n times using "fork". For example…

func void hey(void)
{
   dmsg("hey");
   sleep(60);
   fork hey();
}

func void main(void)
{
   fork hey();
}

…can be replaced by…

func void hey(void)
{
   dmsg("hey");
}

schedule hey() repeat 0 every 60;