Script (OpenXcom)

From UFOpaedia
Jump to navigation Jump to search

Scripting Basics

Scripts for OXCE are able to change the behaviour of the game while it is running.

They are not based on any pre-existing programming language but borrows from assembler syntax. The language was created by Yankes, so for any details of the implementation you might want to ask him.

Introduction (for non-programmers)

The scripts are implementations of so called script hooks. Meaning, that the game pauses whatever it does to execute the script and wait for it to return. Every script is a function being called. A function can return some values to the caller (the game) which would be the return values (or output parameters). The function can also receive some values from the caller (the game) which would be the input parameters.

Every function must end with a "return" statement, returning the flow of execution back to the game and thus exiting your script. You can also exit earlier from your script by calling the "return" statement earlier in your code.

Script API

Since the script feature is a work in progress, the API can change heavily between OXCE versions. Fortunately the game includes code to dump the currently active API. In order to acquire the API Documentation you will have to set the following options in your options.cfg

  debug: true
  verboseLogging: true

Then start up the game and it should generate quite a large log file in openxcom.log.

The documentation starts at a line that looks like this:

[20-12-2019_21-14-46]	[DEBUG]	Available built-in script operations:

The API Documentation includes the names of all available script hooks, their input/output parameters and variables and all the available script commands, objects and their functions.

Currently the script engine only supports integer as a data type. So no strings, no lists, etc. Also loops are not yet implemented.

Note: ohartenstein23 also maintains a copy of the API reference here. Might be enough for a quick peek.

Note 2: You might want to disable verboseLogging after dumping the API documentation since your logfile will become pretty big otherwise.

Overview of available Script hooks

There is a short reference available on Ruleset Reference page.

Scope

Scripts can be created in local or global scope. The Syntax for them differs slightly.

Global Script example

extended:
  scripts:
    createUnit:
      - offset: 2
        code: |
          return;

Local Script example

armors:
  - type: MIMIC_ARMOR
    scripts:
      createUnit: |
        return;

So while global scripts have to be under the top-level name "extended", local scripts are attached directly to a gameobject.

Scripts are executed in order of their offsets. So a script with offset 10 will be executed after a script with offset 5. When scripts with the same name define the same offset one overrides the other, as with every other value in OXC rulesets.

Under the hood, the global scripts will later be "sorted" into the respective local ones. Internally, local scripts always have offset 0. So a global script with offset -1 would run before the local one. Offsets are also a valuable tool make sure two mods and their scripts don't conflict with each other.

While global scripts execute for every gameobject, local ones only run for the gameobjects they are bound to.

The name of the script is pre-defined. You can find a complete list of all script hooks in the script API.

Syntax

The syntax relies on Polish notation for its operations. Keywords are derived from assembler and every statement must end with a semicolon (";").

The following is valid script code:

var int x; # declare variable x as integer type
var int y; # declare variable y as integer type
set x 3;   # set variable x to the value 3, so x=3 is true afterwards
set y 2;   # set variable y to the value 2, so y=2 is true afterwards
add x y;   # add the value of variable y to variable x and store the result in variable x
debug_log "X is now" x; # will print the string "X is now" followed by the value of x which is now 5

This would only execute if embedded into a ruleset like this:

extended:
  scripts:
    createUnit:
      - offset: 2
        code: |
          var int x;
          var int y;
          set x 3;
          set y 2;
          add x y;
          debug_log "X is now" x;
          return;

This would execute for every unit created at the start of a mission. So if you have 10 soldiers and fight 5 aliens this would print "X is now 5" 15 times into your openxcom.log file.

Debugging / Output

The keyword "debug_log" can be used to produce a line in the openxcom.log file. It will simply output everything given to it as a parameter separated by a space. In contrast to (currently) most other keywords, this one can handle strings and even objects.

Objects / Pointers

Most of the scripts have access to many in-game variables. Let's have a look at the input parameters createUnit receives:

Name: battle_game                             ptre      BattleGame
Name: null                                              null     
Name: rules                                   ptr       RuleMod  
Name: turn                                              int      
Name: unit                                    ptre      BattleUnit

This is directly from the API Documentation (2019-29-11).

We can see five variables: battle_game, null, rules, turn and unit.

turn is the easy one. It just has the current turn number stored as an integer. We could output it to log with

debug_log turn;

battle_game, rules and unit have a more complex data type. They are pointers to objects. That means we can access more information about these objects by querying them. This is done with the so called "dot-notation".

The object of type BattleGame contains global information about the game, for example the difficulty level the player has chosen at the start of the game. If we wanted to access that (and output it to the log) we'd type:

extended:
  scripts:
    createUnit:
      - offset: 25
        code: |
          var int diff;                           # define a variable called diff of type integer, we store the difficulty here
          battle_game.difficultyLevel diff;       # get the difficulty value from the battle_game object and store it in diff
          debug_log "Current difficulty:" diff;   # output the value to logfile
          return;                                 # return control to the game

You can find the definitions of all the objects in the API Documentation. You have to search for the data type of the object (BattleGame here) in order to find something. An interesting search is "BattleGame.get" it yields the following results:

Name: BattleGame.getAnimFrame        Args: [ptr BattleGame] [var int]                        
Name: BattleGame.getTag              Args: [ptr BattleGame] [var int] [BattleGame.Tag] Desc: Get tag of BattleGame
Name: BattleGame.getTurn             Args: [ptr BattleGame] [var int]                        

Again, directly from the API Documentation (2019-29-11).

We can see, that we could have queried the battle_game for the turn we are on. Also note, that some functions have a description string attached to them which helps clarify their usage.

ptr vs. ptre

The script engine exposes two different kind of object pointers to the script hooks. Read-only pointers (=ptr) and writable pointers (=ptre).

The difference will become obvious once you have a look at "get" vs. "set" functions. If we want to change the amount of ammunition of a BattleItem we would have a look at the API Documentation and find the following line:

Name: BattleItem.setAmmoQuantity              Args: [ptre BattleItem] [int]

This tells us, that we would need a writable pointer (=ptre) in order to use this function. Some scripts receive such a pointer as part of their input parameters. The createItem script is one of them. We can find the following line among it's definition:

Name: item                                    ptre      BattleItem

That tells us, that on item creation, we would have the ability to change the ammunition count. Which would look like this:

extended:
  scripts:
    createItem:
      - offset: 25
        code: |
          item.setAmmoQuantity 0;   # let them start without any ammunition ;)
          return;                   # return control to the game

This would run for every item at the start of a battlescape mission. You could make this only change the ammunition of specific items, if you made it into a local script instead.

This could be used for a weapon which drains ammo every turn (only possible because newTurnItem also receives a writable pointer to the item).

Tags

One of the most important part of the scripts are the tags. They enable modders to attach custom values to gameobjects and manipulate them via scripts.

You can define tags like this:

extended:
  tags:
    BattleUnit:
      LIGHTNING_REFLEXES_STATE: int
      STASIS_STATE: int  # 0 = no stasis, 1 = stasis active until next alien turn, 2 = stasis active until next xcom turn
    RuleSoldierBonus:
      LIGHTNING_REFLEXES: int
      UNTOUCHABLE: int
    RuleItem:
      SWORD_TYPE: int

That would tell the game, that BatleUnits (all units in the BattleScape, like soldiers, aliens and civilians) can have two tags of type integer. They are named LIGHTNING_REFLEXES_STATE and STASIS_STATE.

Currently OXCE only supports integer data types, so the part after the name will currently alway be "int". The data type tells the engine what kind of data is stored there. Integers are just whole numbers (eg. 0, 1, 56 or -3).

After defining a tag, you can apply it in a ruleset like this:

items:
  - type: STR_ALLOY_SWORD
    tags:
      SWORD_TYPE: 1

The tag SWORD_TYPE used with the item STR_ALLOY_SWORD, setting a value of 1 for the tag.

And then scripts can access them on objects of the correct type with getTag and setTag.

item.getTag sword_type Tag.SWORD_TYPE;     # getting the SWORD_TYPE tag value of item
item.setTag Tag.SWORD_TYPE 2;              # setting a value of 1 for the SWORD_TYPE tag

Control Flow

If / Else

Scripts currently support if/else as a major part of the control flow structures. The syntax is as follows:

if gt x 0;
  add x 1;
end;

"else" branches can be written like this:

if gt x 0;
  add x 1;
else gt y 0;
  add x y;
end;
Comparison Operators
eq x 0 x == 0 x is equal to zero
neq x 0 x != 0 x is not equal to zero
gt x 0 x > 0 x is greater than zero
ge x 0 x >= 0 x is greater than or equal to zero
lt x 0 x < 0 x is less than zero
le x 0 x <= 0 x is less than or equal to zero

Conjunctions (And / Or)

Scripts support and & or. Like all other operations in scripts, the parameters are passed in polish notation.


Conjunctions Operators
and eq x 0 eq y 0 (x == 0) && (y == 0) x is equal to zero and y is equal to zero
or eq x 0 eq y 0 (y == 0) || (y == 0) x is equal to zero or y is equal to zero

Labels / Goto

Scripts also supports basic labels and jumping like this:

mylabel:
add x 1;
debug_log x;
goto mylabel; 

Example Scripts on the Forum