Action language

Actions (the building bricks of a configuration) are actually written in a simple programming language reminiscent of C++. After enabling “Developer mode” in the preferences, you’ll be able to create custom actions, for which you’ll have to write your own logic, and convert builtin actions to custom ones. This is a very short overview of this language.

Builtin types

The only builtin types are int (signed, 32 bits) and float.

Builtin global variables

There are builtin global variables for the current input report’s state. Boolean variables (like buttons) are actually integer ones, with values 0 or not 0 for false/true. Those variables are:

  • LpadX, RPadX, LPadY, RPadY: Analog pad values, 0..255
  • Hat: DPad state. 8 means released; 0 means up, 1 up+right, 2 right, etc, in clockwise order
  • Square, Cross, Circle, Triangle, L1, L2, L3, R1, R2, R3, Share, Options, TPad, PS are the buttons states
  • L2Value, R2Value are the trigger values, 0..255
  • IMUX, IMUY, IMUZ (read-only) are the Euler angles for the current controller orientation, in degrees.
  • ACCELX, ACCELY, ACCELZ (read-only) are the angular accelerations from the gyro, in degrees/second.
  • DELTA is the number of milliseconds elapsed since the last input report

Structures

You can define structured types, with methods:

struct Point {
  int x;
  int y;

  void init() {
    x = 0;
    y = 0;
  }

  void advance() {
    x += 10;
    y += 10;
  }
};

Point startPoint;

They behave just like C++ classes. There is no constructor/destructor; initial values for members are undetermined.

Functions

You can declare functions just like in C

int addup(int a, int b) {
  return a + b;
}

Note

Functions cannot be forward declared. This is by design. It prevents recursivity, which is desirable because the stack size must be computed at compile time.

States

States are special objects, not exactly types. The program must be in a single state at any point in time. The default state (entry point) is named idle. Any state may have

  • An enter method, which will be executed when entering the state
  • A method named after the state itself, which will be executed on each new input report

States may have members (with the same lifetime as the state itself). You can change state using the go keyword.

state idle {
  idle() {
    if (L2 && R2)
      go pressing;
  }
};

state pressing {
  int count;

  enter() {
    count = 0;
  }

  pressing() {
    count++;
    if (!(L2 && R2))
      go idle;
  }
};

Note that state “methods” do not have arguments or return values.

Flow control

A subset of the usual flow control instructions is available: if / else and while. Two flow control instructions are particular:

  • go is used to change the current state
  • yield is used to wait for the next input report

A state’s main method contains an implicit yield at the end, so

state idle {
  idle() {
    // do something
  }
};

is actually equivalent to

state idle {
  idle() {
    while (1) {
      // do something
      yield;
    }
  }
};

Example

Here is a program that swaps the right and left pads:

state idle {
  idle() {
    int tmp = LPadX;
    LPadX = RPadX;
    RPadX = tmp;
    tmp = LPadY;
    LPadY = RPadY;
    RPadY = tmp;
  }
};

Here is the full code for gyro aiming on the right pad when pressing L2 and R2 (as used in the builtin Horizon: Zero Dawn configuration):

#define DEADZONE 50
#define ACCEL_FACTOR 5

int rpad_delta() {
  return (RPadX - 128) * (RPadX - 128) + (RPadY - 128) * (RPadY - 128);
}

int should_aim() {
  return L2 && R2;
}

state idle {
  idle() {
    if (should_aim()) {
      if (rpad_delta() < DEADZONE) {
        go gyro_aiming;
      } else {
        go manual_aiming;
      }
    }
  }
};

state manual_aiming {
  manual_aiming() {
    if (!should_aim()) {
      go idle;
    }

    if (rpad_delta() < DEADZONE) {
      go gyro_aiming;
    }
  }
};

state gyro_aiming {
  gyro_aiming() {
    if (!should_aim()) {
      go idle;
    }

    if (rpad_delta() >= DEADZONE) {
      go manual_aiming;
    }

    RPadX = ACCEL_FACTOR * ACCELY + 0x80;
    RPadY = ACCEL_FACTOR * ACCELX + 0x80;
  }
};

Debugging

Right now there is no debugging options, so you’re probably in for a world of pain. Compilation error reporting is very rough and the messages are unreadable for anyone who didn’t write the parser or spent time studying it. A future version may include a symbolic debugger and simulation options.