BSL:Manual

From OniGalore
Revision as of 20:41, 4 December 2015 by Iritscen (talk | contribs) (new idea: merge all BSL syntax documentation onto one long page; this should be easier than browsing around several interconnected pages)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

This page discusses more details of BSL syntax than BSL:Introduction.

Syntax

Like any typical scripting or programming language, BSL scripts consist of commented plain-text files which employ branching logic and various operators according to a strict syntax to process data using functions that act upon variables. Click one of those words to jump to the brief explanations of those terms below.

Statements

Statement separators

BSL accepts 2 statement separators: the semicolon (;) and the linebreak. The following 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 in old-style syntax, which is discussed under "Old vs. new syntax" below.

Compound statements

Compound statements are groups of statements held together by a pair of curly braces:

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

You can string them one that uses the semicolon:

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

The purpose of doing this is to group some statements under a header, which can be either a function declaration (see "Declaration" below) or an "if" statement (see "Conditional" below).

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 = 0; # this global is also modified in my_cutscenes.bsl

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

# The following block of code calculates the meaning of life
if [block of code begins here]

Do not using trailing comments when not ending statements with semicolons (see "Old vs. new syntax" below for explanation).

In documentation outside of source code or script files, such as this page, comments are sometimes used to tell the reader something in a way that doesn't break the actual code, if the user should type it in exactly as it appears, 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":

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 syntax. For instance, if you try to end a function call with a semicolon, but you don't use a C-style function call...

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:

a = 3 # this line is valid old-style syntax...
var int a = 3 # ...this line is not (even without this comment!)

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.

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. You can also omit function types (with "void" being assumed in their absence). You also do not need to enclose code after an if/else statement in curly braces if there is only a single line. None of these syntax choices are in conflict with the "new style" syntax.

Additionally, you can use whitespace 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
}

Reserved words

Here are the keywords that have special meaning in BSL.

Declaration

var

See "Variables" below for details on using variables.

func

See "Functions" below for details on using functions.

Types

See BSL:Types for details.

void, bool, int, float, string

Besides "void", used in function definitions to indicate "no type", these are the types of data that can be assigned to variables, passed to functions, and returned by functions.

Conditional

if

The only conditional statement is "if", with an optional "else" follow-up.

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

The condition in the parentheses needs to evaluate to "true" or "false". For examples of operators, see the "Operators" section.

The condition can technically also evaluate to an int value, which then is converted to true/false:

if (8 - 8) # evaluates to false because it is zero
if (8 - 7) # evaluates to true because it is non-zero
if (7 - 8) # evaluates to true because it is non-zero

Be aware that BSL does not properly respect scope for blocks of code under "if" statements, so some actions like setting variables and using "return" will occur even if the surrounding "if" condition is false (see "Flow interrupts" below for an example). A workaround for setting variables conditionally is to make the variable global, and under the "if" statement call a function that modifies that variable; the function call will not fire unless the "if" condition is true.

The logical operators "and" and "or" can be used to string together multiple conditions:

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

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.

Flow interrupts

There is no "goto" statement in BSL, nor any loop controls like "continue" or "break" (since there are no proper loops!). There are, however, "return", and, arguably, "sleep".

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?
}

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.

sleep

You can also delay a script using "sleep", passing it a number in ticks:

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

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

Loops

BSL has no "for" or "while" statement, but "schedule-repeat-every" (see "Multi-threading" below) can be used as a poor-man's loop (as long as you realize that there's no way to exit early).

Multi-threading

fork, schedule-at, schedule-repeat-every

See "Functions" below 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, it doesn't do anything.

Operators

Here are all the operators you can use in BSL.

Assignment

Symbol Name Description
= Equals Sets the left-hand entity to the right-hand value.

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

Arithmetical

Symbol Name Description
+ Add Sums two numbers.
- Subtract Deducts one number from another.

You'll note that there is no 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.

Go to BSL:Types to learn the details of how operators work between different data types.

Relational

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 a number is greater than another.
< Less than Tests if a number is smaller than another.
>= 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: eq and ne work for strings too.

Logical

Symbol Name Description
! Not Logical Not. Gives the inverse of a bool. The same as saying "some_bool ne true".
and And Logical And.
or Or Logical Or.

Data types

When declaring a variable or a function (more on this under the "Functions" and "Variables" sections below), you must specify the type of data it contains, accepts, or returns:

var int x;
func int my_func(void)

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).

void

See "Functions" below. Variables cannot be type "void".

bool

A 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 bool are_we_there_yet(void)

Giving a bool any value other than 0 will set it to 1. For instance, assigning a float to a bool will make the bool "true" unless the float value is 0.0, which will become "false".

int

A 32-bit signed 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.

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

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...

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

...will print "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. BSL was not designed for performing serious math, so floats are somewhat lacking in precision.

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

Adding a value below the sixth decimal place to a float will have no effect, e.g. 3.0 plus 0.0000001.

Adding an int (or a bool) 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 ASCII characters, for storing text in memory.

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 together simply crashes the game, rather than concatenating them. 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).

"(null)" is the value that strings assume when they have not been given a value yet; it cannot be assigned to a string manually. The other, numerical data types assume zero.

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

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 they do not need to do either of those things in order to be useful.

Built-in vs. script-defined

There are two classes of functions: user-defined and built-in.

Built-in

Built-in functions are hardcoded into Oni, that is, they were written in C along with the rest of the engine rather than being written in BSL, so you cannot inspect their code. Built-in functions are available to be called from BSL at all times, and are listed here. Guidance on how to use them is found in the Scripting tasks category. You manually call from the developer console any built-in function.

Script-defined

You can manually call from the developer console any functions you declared in a script, however, you cannot actually define functions through the console.

Defining

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

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

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.

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 simply 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

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

some_function(3, "Jojo");

In this case, we are passing a constant value, three, into some_function, where 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";
some_function(num_heroes, enemy);

Multi-threading

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() is waiting for a second on its own thread. 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
}

You can use forked functions to print delayed messages using "sleep" without holding up other scripted actions, as well as use built-in functions that hold up BSL, like chr_wait_animation to wait for a condition to be true before performing some action. However, do not "fork" call the same function again before it finishes running, as this will have undesired effects. Using "sleep" before each call can prevent this overlapping execution.

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 can simulate a "for" loop with these keyword sets.

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;

Recursive calling

A function can call itself, which is useful for various looping behavior, but a function can only call itself up to about four times.

Returning

As mentioned under "Flow interrupts" above, "return" exits the function at that point. Because of the bug documented in that section, you cannot exit early from a function under some 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":

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

You would then use this function to set a variable, as follows:

some_number = add_ten(enemy_count);

Function return values can also be used in "if" statements to provide a true/false condition:

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;
}
if (is_it_safe())
   dprint("It is safe.");
else
   dprint("It is not safe.");

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.

Built-in vs. script-defined

There are two classes of variables: user-defined and built-in.

Built-in

Built-in variables are hardcoded into Oni, that is, they were declared in C along with the rest of the engine code, rather than being declared in BSL. The built-in variables are available for accessing and changing their values from BSL at all times, and are listed here. Guidance on how to use them is found in the Scripting tasks category. You manually get and set the value of any built-in variable from the developer console.

Script-defined

You can use the developer console to get and set the value of a global variable that you declared in a script, but you cannot actually declare variables through the console.

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;