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.
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.
- Update: since OXCE 7.0 strings are also supported, loops are supported too
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.
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. (that's not true, scripts do not override each other). Script offsets cannot be 100 or greater. Up to two decimals can be added. This means that 99.99 is the highest possible value for the offset.
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.
Each global script can be named and updated by other mods (available in OXCE version 7.1.12).
extended: scripts: createUnit: - new: TEST_SCRIPT_NAME offset: 2 code: | debug_log "Old script"; return;
And in other mod:
extended: scripts: createUnit: - override: TEST_SCRIPT_NAME offset: 2 code: | debug_log "New script"; return;
Names of new yaml nodes available and its functions:
|new||require new unique name, throw error when it detect duplicate|
|delete||delete old, if not exists only leave massage in log|
|override||require old and update to new value, throw exception if old not exists|
|update||if old exists update it, if not log message and skip|
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
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).
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
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;
|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.
|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|
Scripts also supports basic loops like this:
var int loopCount 10; loop var x loopCount; debug_log x; end;
This will print values from 0 to 9 (to one less than given count 10).
This is added in OXCE version 7.0.0
When you need temporary variable you can use block that define new scope
begin; var int x 10; end; # x is not accessible here
New variables are possible for if and loop blocks too.
This is added in OXCE version 7.0.0