Flow Control

BREAK

Breaks out of a loop:

SET X TO 1.
UNTIL 0 {
    SET X TO X + 1.
    IF X > 10 { BREAK. } // Exits the loop when
                         // X is greater than 10
}

IF / ELSE

Checks if the expression supplied returns true. If it does, IF executes the following command block. Can also have an optional ELSE to execute when the IF condition is not true. ELSE can have another IF after it, to make a chain of IF/ELSE conditions:

SET X TO 1.
IF X = 1 { PRINT "X equals one.". }     // Prints "X equals one."
IF X > 10 { PRINT "X is greater than ten.". }  // Does nothing

// IF-ELSE structure:
IF X > 10 { PRINT "X is large".  } ELSE { PRINT "X is small".  }

// An if-else ladder:
IF X = 0 {
    PRINT "zero".
} ELSE IF X < 0 {
    PRINT "negative".
} ELSE {
    PRINT "positive".
}

Note

The period (.) is optional after the end of a set of curly braces like so:

// both of these lines are fine
IF TRUE { PRINT "Hello". }
IF TRUE { PRINT "Hello". }.

In the case where you are using the ELSE keyword, you must not end the previous IF body with a period, as that terminates the IF command and causes the ELSE keyword to be without a matching IF:

// works:
IF X > 10 { PRINT "Large". }  ELSE { PRINT "Small". }.

// syntax error - ELSE without IF.
IF X > 10 { PRINT "Large". }. ELSE { PRINT "Small". }.

CHOOSE (Ternary operator)

An expression that evalualtes to one of two choices depending on a conditional check:

CHOOSE expression1 IF condition ELSE expression2

Note this is NOT a statement. This is an expression that can be embedded inside other statements, like so:

SET X TO CHOOSE expression1 IF condition ELSE expression2. PRINT CHOOSE “High” IF altitude > 20000 ELSE “Low”.

The reason to use the CHOOSE operator instead of an IF/ELSE statement is that IF/ELSE won’t return a value, while this does, and thus this can be embedded inside other expressions.

(This is similar to the ? operator in languages like “C” and its derivatives, except it puts the “true” choice first, then the conditional check, then the “false” choice.)

LOCK

Locks an identifier to an expression. Each time the identifier is used in an expression, its value will be re-calculated on the fly:

SET X TO 1.
LOCK Y TO X + 2.
PRINT Y.         // Outputs 3
SET X TO 4.
PRINT Y.         // Outputs 6

LOCK follows the same scoping rules as the SET command. If the variable name used already exists in local scope, then the lock command creates a lock function that only lasts as long as the current scope and then becomes unreachable after that. If the variable name used does not exist in local scope, then LOCK will create it as a global variable, unless @LAZYGLOBAL is set to off, in which case it will be an error.

Note that a LOCK expression is extremely similar to a user function. Every time you read the value of the “variable”, it executes the expression again.

Note

If a LOCK expression is used with a flight control such as THROTTLE or STEERING, then it will get repeatedly evaluated in each physics tick. When used with a flight control variable, a LOCK actually becomes a trigger.

UNLOCK

Releases a lock on a variable. See LOCK:

UNLOCK X.    // Releases a lock on variable X
UNLOCK ALL.  // Releases ALL locks

UNTIL loop

Performs a loop until a certain condition is met:

SET X to 1.
UNTIL X > 10 {      // Prints the numbers 1-10
    PRINT X.
    SET X to X + 1.
}

Note

If you are writing an UNTIL loop that looks much like the example above, consider the possibility of writing it as a FROM loop instead.

Note that if you are creating a loop in which you are watching a physical value that you expect to change each iteration, it’s vital that you insert a small WAIT at the bottom of the loop like so:

SET PREV_TIME to TIME:SECONDS.
SET PREV_VEL to SHIP:VELOCITY.
SET ACCEL to V(9999,9999,9999).
PRINT "Waiting for accellerations to stop.".
UNTIL ACCEL:MAG < 0.5 {
    SET ACCEL TO (SHIP:VELOCITY - PREV_VEL) / (TIME:SECONDS - PREV_TIME).
    SET PREV_TIME to TIME:SECONDS.
    SET PREV_VEL to SHIP:VELOCITY.

    WAIT 0.001.  // This line is Vitally Important.
}

The full explanation why is in the CPU hardware description page.

FOR loop

Loops over a list collection, letting you access one element at a time. Syntax:

FOR variable1 IN variable2 { use variable1 here. }

Where:

  • variable1 is a variable to hold each element one at a time.
  • variable2 is a LIST variable to iterate over.

Example:

PRINT "Counting flamed out engines:".
SET numOUT to 0.
LIST ENGINES IN MyList.
FOR eng IN MyList {
    IF ENG:FLAMEOUT {
        set numOUT to numOUT + 1.
    }
}
PRINT "There are " + numOut + "Flamed out engines.".

Note

If you are an experienced programmer looking for something more like the for-loop from C, with its 3-part clauses of init, check, and increment in the header, see the FROM loop description. The kerboscript ‘for’ loop is more like a ‘foreach’ loop from other modern languages like C#.

FROM loop

Identical to the UNTIL loop, except that it also contains an explicit initializer and incrementer section in the header.

Syntax:

FROM { one or more statements } UNTIL Boolean_expression STEP { one or more statements } DO one statement or a block of statements inside braces ‘{}’

Quick Example:

print "Countdown initiated:".
FROM {local x is 10.} UNTIL x = 0 STEP {set x to x-1.} DO {
  print "T -" + x.
}

Note

If you are an experienced programmer, you can think of the FROM loop as just being Kerboscript’s version of the generic 3-part for-loop for( int x=10; x > 0; --x ) {...} that first appeared in C and is now so common to many programming languages, except that its Boolean check uses the reverse of that logic because it’s based on UNTIL loops instead of WHILE loops.

What the parts mean

  • FROM { one or more statements }
    • Perform these statements at the beginning before starting the first pass through the loop. They may contain local declarations of new variables. If they do, then the variables will be local to the body of the loop and won’t be visible outside the loop. In this case the braces { and } are mandatory even when there is only one statement present. To create a a null FROM clause, give it an empty set of braces.
  • UNTIL expression
    • Exactly like the UNTIL loop. The loop will run this expression at the start of each pass through the loop body, and if it’s true, it will abort and stop running the loop. It checks before the initial first pass of the loop as well, so it’s possible for the check to prevent the loop body from even executing once. Braces {..``}`` are not used here because this is not technically a complete statement. It is just an expression that evaluates to a value.
  • STEP { one or more statements }
    • Perform these statements at the bottom of each loop pass. The purpose is typically to increment or decrement the variable you declared in your FROM clause to get it ready for the next loop pass. In this case the braces { and } are mandatory even when there is only one statement present. To create a null FROM clause, give it an empty set of braces.
  • DO one statement or a block of statements inside braxes {..``}``:
    • This is where the loop body gets put. Much like with the UNTIL and FOR loops, these braces are not mandatory when there is only exactly one statement in the body, but are a very good idea to have anyway.

Why some braces are mandatory

Some braces are mandatory (for the FROM and STEP clauses) even when there is only one statement inside them, because the period that ends a single statement would look like it’s terminating the entire FROM loop if it was open and bare. Wrapping it inside braces makes it more visually obvious that it’s not the end of the FROM loop.

Why DO is mandatory

Other loop types don’t require a keyword to begin the loop body. You can just start in with the opening left-brace {. The reason the additional DO keyword exists in the FROM loop is because otherwise you’d have two back-to-back brace sections (The end of the STEP clause would abut against the start of the loop body) without any punctuation between them, and that would look too much like it was starting a brand new thing from scratch.

Other formatting examples

// prints a count from 1 to 10:
FROM {local x is 1.} UNTIL x > 10 STEP {set x to x+1.} DO { print x.}

// Entire header in one line, body indented:
// --------------------------------------------
FROM {local x is 1.} UNTIL x > 10 STEP {set x to x+1.} DO {
  print x.
}

// Each header part on its own line, body indented:
// --------------------------------------------
FROM {local x is 1.}
UNTIL x > 10
STEP {set x to x+1.}
DO {
  print x.
}

// Fully exploded out: Each header part on its own line,
//  each clause indented separately:
// --------------------------------------------
FROM
{
  local x is 1.  // x will count upward from 1.
  local y is 10. // while y is counting downward from 10.
}
UNTIL
  x > 10 or y = 0
STEP
{
  set x to x+1.
  set y to y-1.
}
DO
{
  print "x is " + x + ", y is " + y.
}

// ETC.

Any such combination of indenting styles, or mix and match of them, is understood by the compiler. The compiler ignores the spacing and indenting. It is recommended that you pick just two of them and stick with them - one compact one to use for short headers, and one longer exploded one to use for more wordy headers when you have to split it up across lines.

The literal meaning of FROM

If you have a FROM loop, it ends up being exactly identical to an UNTIL loop written as follows:

If we assume that AAAA, BBBB, CCCC, and DDDD are placeholders referring to the actual script syntax, then in the generic case, the following is how all FROM loops work:

FROM loop:

FROM { AAAA } UNTIL BBBB STEP { CCCC } DO { DDDD }

Is exactly the same as doing this:

{ // start a brace to keep the scope of AAAA local to the loop.
    AAAA
    UNTIL BBBB {
        DDDD

        CCCC
    }
} // end a brace to throw away the local scope of AAAA

An example of why the FROM loop is useful

Given that the FROM loop is really just an alternate way to write a certain format of UNTIL loop, you might ask why bother having it. The reason is that in the long run it makes your script easier to edit and maintain. It makes things more self-contained and cut-and-pasteable:

Above, in the documentation for UNTIL loops, this example was given:

SET X to 1.
UNTIL X > 10 {      // Prints the numbers 1-10
    PRINT X.
    SET X to X + 1.
}

The same example, expressed as a FROM loop is this:

FROM {SET X to 1.} UNTIL X > 10 {SET X to X + 1.} DO {
    PRINT X.
}

Kerboscript FROM loop provides a way to place those sections in the loop header so they are declared up front and let people see the layout of how the loop iterates, leaving the body to just contain the statements to be done for that iteration.

If you are editing your script and need to cut a loop section and move it elsewhere, the FROM loop makes it more visually obvious how to cut that loop and move it. It makes the important parts of the loop be self contained in the header, so you don’t leave the initializer behind when moving the loop.

WAIT

Halts execution for a specified amount of time, or until a specific set of criteria are met. Note that running a WAIT UNTIL statement can hang the machine forever if the criteria are never met. Examples:

WAIT 6.2.                     // Wait 6.2 seconds
WAIT UNTIL X > 40.            // Wait until X is greater than 40
WAIT UNTIL APOAPSIS > 150000. // You can see where this is going

Note that any WAIT statement, no matter what the actual expression is, will always result in a wait time that lasts at least one physics tick.

Difference between wait in mainline code and trigger code

When called from your mainline code, the WAIT command causes mainline code to be suspended, but does not stop triggers from interrupting this waiting period. Triggers will continue to fire off during the time that mainline code is stuck on a wait.

But when a WAIT is used in a trigger’s body (A “trigger” is any WHEN, or ON statement, or the expression in a steering control lock like lock throttle to mythrottlefunction().), it actually causes all execution except for other triggers that are of higher priority to get stuck until the wait is done. Because of this, while it is allowed, it is usually a bad idea to use WAIT inside a trigger.

Boolean Operators

All conditional statements, like IF, can make use of boolean operators. The order of operations is as follows:

  • = < > <= >= <>
  • AND
  • OR
  • NOT

Boolean is a type that can be stored in a variable and used that way as well. The constants True and False (case insensitive) may be used as values for boolean variables. If a number is used as if it was a Boolean variable, it will be interpreted in the standard way (zero means false, anything else means true):

IF X = 1 AND Y > 4 { PRINT "Both conditions are true". }
IF X = 1 OR Y > 4 { PRINT "At least one condition is true". }
IF NOT (X = 1 or Y > 4) { PRINT "Neither condition is true". }
IF X <> 1 { PRINT "X is not 1". }
SET MYCHECK TO NOT (X = 1 or Y > 4).
IF MYCHECK { PRINT "mycheck is true." }
LOCK CONTINUOUSCHECK TO X < 0.
WHEN CONTINUOUSCHECK THEN { PRINT "X has just become negative.". }
IF True { PRINT "This statement happens unconditionally." }
IF False { PRINT "This statement never happens." }
IF 1 { PRINT "This statement happens unconditionally." }
IF 0 { PRINT "This statement never happens." }
IF count { PRINT "count isn't zero.". }

DECLARE FUNCTION

Covered in more depth elsewhere in the documentation, the DECLARE FUNCTION statement creates a user-defined function that you can then call elsewhere in the code.

RETURN

Covered in more depth elsewhere in the documentation, the RETURN statement causes a user function, or a trigger body, to end, and chooses what the calling part of the program will see if it reads the value of the function.

WHEN / THEN statements, and ON statements

Note

Before going too far into this explanation, be aware that the WHEN and ON statements are rather advanced topics for a new programmer and if you’re just getting a feel for how programming works, and are using kOS as a first gentle introduction to writing programs, you might want to avoid using them until you’re more comfortable with the other features of kOS first.

The WHEN and the ON statement are very similar to each other, and so they are documented together here.

See also

General Guidelines for kOS Scripts
Before you continue, be aware that there is also a page in the tutorials section describing the best practices to use with these statements, including minimizing how long trigger bodies take. and minimizing how many trigger conditions are active. It would be a good idea to read that documentation after reading this section.

WHEN and ON both begin checking in the background for a condition that will cause some code to execute some statements later on. They do NOT cause the code to necessarily get run right now. The check will occur at regular fast intervals in the background, and the code will trigger whenever kOS next notices that the check happens to be true.

kOS has a feature known as a trigger, and a WHEN or an ON statement are two of the ways to create one. Any time you make a section of program that is meant to repeatedly run a check in the background while the main program continues on, that is called a trigger in kOS terminology. You may see the term trigger mentioned in many places in this documentation.

Syntax examples:

When and On side by side
WHEN .. THEN syntax ON syntax
WHEN boolean_expression THEN {

statements go here

}
ON any_expression {

statements go here

}
WHEN boolean_expression THEN single_statement. ON any_expression single_statement.

For historical reasons, the THEN keyword is needed for WHEN statements but not for ON statements.

Here is the difference between them:

  • WHEN statement: When kOS checks it in the background, if it notices the condition is true, the trigger fires and it performs the statements. The condition to check for must be a boolean expression.
  • ON statement: When kOS checks it in the backround, if it notices the expression is now different from what it was the last time it checked, the trigger fires and it performs the statements. The condition to check for can be any expression for which it is possible to test equality. It can be a boolean, a scalar, etc. All that matters is that kOS needs to be able to check if its new value is equal to its previous value or not.

Other than that, the two work the same way, and follow the same rules.

WHEN example:

// This example will eventually print the message
// once enough time has passed:

SET tenSecondsLater to TIME:SECONDS + 10.
WHEN TIME:SECONDS > tenSecondsLater THEN {
  PRINT "Ten seconds have passed.".
}

PRINT "now checking in the background to see if 10 seconds have passed yet.".

WAIT UNTIL FALSE. // Wait forever.  You have to end with Control-C
                  // The trigger will interrupt this waiting when it
                  // notices it should.

ON example. This style is frequently used with action groups in kOS. KSP’s action groups actually toggle from true to false or from false to true each time you press the key:

// This example will print a message whenever you toggle
// the lights, or press the '1' key.

ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  PRINT "No longer paying attention.".
}

WAIT UNTIL FALSE. // Wait forever.  You have to end with Control-C
                  // The trigger will interrupt this waiting when it
                  // notices it should.

For either WHEN or ON triggers, the check to see if it’s time to trigger, and the subsequent run of the statements if they do trigger, interrupts the normal flow of the program. The normal program flow will continue from where it left off, after the trigger finishes its work.

In a sense, a trigger is a bit like a user function you created and then asked the kOS system to please keep running it again and again in the background until it finally says that it fired off. In fact, it is implemented much like your own user functions.

If you run the above examples, you will see that they actually only happen once, and then stop happening again. In the ON AG1 example, it will only fire off once, no matter how many times you press the ‘1’ key. More will be covered about how to change that further down.

Warning

Do not make the body of a WHEN/THEN take a long time to execute. If you attempt to run code that lasts too long in the body of your WHEN/THEN statement, it will cause the main line code, and all other triggers (WHEN, ON, and cooked steering locks) to be stuck unable to continue until it finishes. You also probably should not make the system execute a WAIT command when inside the body of a WHEN/THEN statement.

kOS has a mechanism in place that allows triggers to interrupt mainline code that is stuck in a wait. It does not have a mechanism to go the other way around and have a trigger get interrupted. Triggers are meant to run quickly and finish so the system can get back to the mainline code.

Don’t let triggers bog down the code

If you are going to make extensive use of WHEN/THEN triggers, it’s important to understand more details of how they work in the kOS CPU.

Most importantly, be aware that since they get checked again and again in the background, having too many triggers that are “too expensive” can starve your main code of its use of the CPU, and thus slow down your program’s rate of running.

By default triggers only run once, but this can be changed

The original intention of the WHEN and ON triggers was that although they check the condition repeatedly, once the condition is found to be true, they execute the body just once and then stop checking the condition.

They were intended for things like only running a piece of code when you break a threshold altitude, or detect that you’ve landed, etc.

So the default way they behave is that once the body of the trigger happens the first time, the trigger will never be checked again, and is now effectively dead for the rest of the program.

Obviously, that’s probably not the behavior you always want. Sometimes you will want them to keep repeatedly happening, as a frequent background check. One obvious example comes from the ON AG1 example above. You probably want a program that can keep re-checking to see if the action group button has been hit again and again, not just notice it once and then quit looking for it.

There are two ways to do this - the new (better) way with the return statement, and the older way, kept around for backward compatibility, of using the preserve keyword.

Preserving with return

Triggers are essentialy functions that don’t quite look like functions. They are frequently called, but they’re not called by you. They’re called by the Kerbal Operating System itself. So you can tell the Kerbal Operating System what your intentions were by simply deciding to return either a false or a true boolean value from the body of the trigger. This tells kOS if you wanted to keep the trigger around or let it get deleted.

  • return true. to tell kOS to preserve the trigger and keep checking it again next time.
  • return false. to tell kOS to disable the trigger after this check, and never use it again.

Therefore, if you want to have the ON AG1 example always respond to the keypress from now on, then change this:

ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  PRINT "No longer paying attention.".
}

To this instead:

ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  RETURN true.
}

Or, for a more complex example, if you want it to only respond to the first 5 times you press the key and then stop after that, you can conditionally decide what return value to use, like so:

SET count TO 5.
ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  SET count TO count - 1.
  PRINT "I will only pay attention " + count + " more times.".
  if count > 0
    RETURN true. // will keep the trigger alive.
  else
    RETURN false. // will let the trigger die.
}

There is an alternate, older syntax you can use to do the same thing, called the preserve keyword. You may see it used in a lot of older scripts, but the new way using the return keyword is cleaner.

If you never mention either a true or a false return value, the default is to behave as if you had returned false, and delete the trigger. This works because of the sort-of-secret fact that in kOS, all functions return zero if you don’t mention the return value explicitly.

They don’t last past the end of the program

A WHEN/THEN or ON trigger gets removed when the program that created it exits, even if it has not occurred yet.

PRESERVE

PRESERVE is a command keyword that is only valid inside of WHEN/THEN and ON code blocks.

When a WHEN/THEN or ON condition is triggered, the default behavior is to execute the code block body exactly once and only once, and then the trigger condition is removed and the trigger will never occur again.

To alter this, a new ability was added in kOS 0.19.3 and above to have triggers simply return a true or false value to determine if they wish to be preserved.

But prior to kOS 0.19.3, the only way to do it in kerboscript was with the PRESERVE keyword, which will likely remain in kerboscript for quite some time because it has a lot of backward compatibility legacy.

If you execute the PRESERVE command anywhere within the body of a trigger, it tells kOS that you wish the trigger to remain present and not get deleted. Choosing not to execute it, and just letting the execution fall through to the bottom of the body, has the default behavior of causing the trigger to get deleted.

For example, this:

ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  RETURN true.
}

could also be expressed this way:

ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  PRESERVE.
}

And this:

SET count TO 5.
ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  SET count TO count - 1.
  PRINT "I will only pay attention " + count + " more times.".
  if count > 0
    RETURN true. // will keep the trigger alive.
  else
    RETURN false. // will let the trigger die.
}

could also be expressed this way:

SET count TO 5.
ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  SET count TO count - 1.
  PRINT "I will only pay attention " + count + " more times.".
  if count > 0
    PRESERVE.
}

Also note that unlike using RETURN, the PRESERVE statement doesn’t actually cause the trigger to abort and return at that point. It just sets a flag for what the intended return value will be, without actually returning yet. Therefore it doesn’t actually matter where within the block of code it happens, it has the same effect.

this:

ON AG1 {
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
  PRESERVE.
}

has the same effect as this:

ON AG1 {
  PRESERVE. // <-- Doesn't matter where you PRESERVE within the body.
  PRINT "You pressed '1', causing action group 1 to toggle.".
  PRINT "Action group 1 is now " + AG1.
}

(If you attempt to BOTH execute PRESERVE. and provide a RETURN false. statement that contradicts it, the RETURN statement will end up overriding the effect of the PRESERVE.)