GPC Syntax Reference: Variables, Main Loop, Operators
Complete GPC syntax reference for Cronus Zen scripting: script sections, variables, operators, the main loop, combo timing rules, and core functions.
On this page
- How a GPC script is laid out
- GPC syntax basics: statements, comments, and variables
- Operators
- The main loop, init, and conditionals
- Combos and the wait() timing model
- Core function reference
- Identifiers and value ranges
- FAQ
- Is GPC syntax the same as C?
- Where can I use wait() in a GPC script?
- What is the difference between define and int in GPC?
- How fast does the GPC main loop run?
- Why does my script compile but a combo never fires?
This page is a working GPC syntax reference for the Cronus Zen: the script sections in their required shape, statement and variable rules, operators, the main-loop execution model, combo timing semantics, and a table of the core built-in functions. It is meant to be kept open in a second tab while you write. For a gentler on-ramp, the first script tutorial builds a real script line by line; for the conceptual model behind input reading and writing, see the inputs and events explainer.
How a GPC script is laid out
A .gpc file is a plain-text file whose sections conventionally appear in this order:
| Section | Keyword | Runs | Purpose |
|---|---|---|---|
| Constants | define |
compile time | Named values for timings and thresholds |
| Globals | int |
— | Variables that persist across loop passes |
| Data | data(...) |
— | Read-only data tables (advanced; see Cronus's docs) |
| Init | init { } |
once, at script start | One-time setup |
| Main | main { } |
continuously | The loop; reacts to every controller report |
| Combos | combo Name { } |
when triggered | Timed input sequences |
| Functions | function name(...) { } |
when called | Reusable helpers |
A compact skeleton showing every common section, as a complete script:
// Skeleton: every common GPC section in conventional order.
define TAP_MS = 60; // named constant
int presses; // global variable (integers only)
init {
presses = 0; // runs once when the script loads
}
main {
// Runs once per controller report while the script is active.
if (event_press(PS4_CROSS)) {
presses = bump(presses); // call a user function
combo_run(TapCircle); // start a timed sequence
}
}
combo TapCircle {
set_val(PS4_CIRCLE, 100); // held during the wait below
wait(TAP_MS);
set_val(PS4_CIRCLE, 0); // released during this wait
wait(TAP_MS);
}
function bump(n) {
return n + 1; // user-defined function with return
}
The execution model in one sentence: init runs once, then main re-runs for every input report from your controller, and any set_val you perform applies to that report on its way to the console.
GPC syntax basics: statements, comments, and variables
Statements end with a semicolon. The closing brace of a block implies one, so the last line before a } does not strictly need its own — but writing the semicolon anyway is harmless and easier to keep consistent. Braces { } group blocks; a single-statement if body can technically omit them, but braces-always avoids a classic class of bugs.
Comments come in two forms:
// Single line: everything to the end of the line is ignored.
/* Multi-line: everything until the closing
marker is ignored, across lines. */
main {
// A complete (if idle) script, to keep the example runnable.
}
Constants use define, evaluated at compile time:
define FIRE_HOLD = 40; // milliseconds
main {
// Constants substitute anywhere a number is expected.
if (get_val(XB1_RT) > 90) {
combo_run(Pulse);
}
}
combo Pulse {
set_val(XB1_RT, 100);
wait(FIRE_HOLD);
set_val(XB1_RT, 0);
wait(FIRE_HOLD);
}
Variables are declared with int — integers are the only variable type. Globals persist between passes of main, which is how scripts remember toggle states and counters. Declare globals outside all blocks; initialize them in init or rely on their starting value of zero. There are no strings and no floating point; timing math is done in whole milliseconds and stick math in whole percentage points.
Names (variables, combos, functions) follow C rules: letters, digits, and underscores, not starting with a digit. Published Cronus examples use built-in identifiers in both cases (XB1_RT and xb1_rt); pick one style and stay consistent within a script.
Operators
| Category | Operators | Notes |
|---|---|---|
| Arithmetic | + - * / % |
Integer math; division truncates |
| Comparison | == != < <= > >= |
Result is true/false |
| Logical | && || ! |
Combine conditions; !x also flips a 0/1 flag |
| Assignment | = |
x = x + 1; — write increments explicitly |
The ! operator doubles as the standard toggle idiom: flag = !flag; flips a variable between 0 and 1. GPC also has bitwise operators for packing flags into a single integer; the exact set is in Cronus's language reference — verify there before relying on them, since day-to-day scripts rarely need any.
The main loop, init, and conditionals
main is not a function you call; it is the body the Zen executes on every controller report, indefinitely. Three consequences shape all GPC code:
- Never busy-wait in
main. There is no sleeping insidemain;wait()is only valid inside combos. Anything with duration belongs in a combo. - Locals do not carry over. Only global
intvariables persist from one pass to the next. set_valis per-pass. A value you set inmainapplies to the current report only; to hold it, either set it every pass while a condition holds or use a combo.
Flow control inside main is if / else if / else:
main {
if (get_val(PS4_L2) && get_val(PS4_R2)) {
// both triggers held
set_val(PS4_R1, 100);
} else if (get_val(PS4_L2)) {
// only the left trigger
set_val(PS4_R1, 0);
} else {
// neither — pass through untouched
}
}
The loop itself is your iteration construct; GPC scripting does not revolve around while/for loops the way desktop C does, and a loop that never yields would stall input. If you think you need a loop, you almost always need a combo or a counter that increments once per pass.
Combos and the wait() timing model
A combo is a named block of set_val and wait statements. The timing rule that governs everything: each wait(ms) holds whatever the statements since the previous wait (or the combo's start, or the last call) have set, for that many milliseconds. When the combo ends, the script stops overriding those controls.
// Timing model demo: what the console sees, window by window.
main {
if (event_press(XB1_Y)) {
combo_run(Demo);
}
}
combo Demo {
set_val(XB1_A, 100); // window 1: A held...
wait(500); // ...for 500 ms
set_val(XB1_A, 0); // window 2: A forced released,
set_val(XB1_B, 100); // B held...
wait(200); // ...for 200 ms
call(Tail); // pause here, run Tail, then resume
}
combo Tail {
set_val(XB1_X, 100); // X held for 150 ms
wait(150);
}
Key facts, per Cronus's combo documentation:
wait()accepts 1 to 32767 milliseconds.call(OtherCombo)is valid only inside combos; it suspends the current combo, runs the called one to completion, then resumes.combo_run(X)on an already-running combo has no effect; usecombo_restart(X)to start it over.- A combo continues to completion on its own timeline even if the triggering condition in
mainhas ended, unless something callscombo_stop(X).
Worked combo construction, including interrupts and retrigger guards, is the subject of the combos and macros tutorial.
Core function reference
The functions below cover the overwhelming majority of real scripts. Value semantics: buttons and triggers read 0 to 100; stick axes read -100 to +100.
| Function | Signature | What it does |
|---|---|---|
get_val |
get_val(id) |
Current value of a control in this pass |
get_ival |
get_ival(id) |
The raw input value, before script modification |
get_lval |
get_lval(id) |
The control's value in the previous pass |
get_ptime |
get_ptime(id) |
Milliseconds since the control last changed state (0–32767) |
event_press |
event_press(id) |
True only on the pass where the control becomes pressed |
event_release |
event_release(id) |
True only on the pass where the control becomes released |
set_val |
set_val(id, v) |
Sets the value sent to the console this pass |
swap |
swap(id1, id2) |
Exchanges two controls' values (on-the-fly remap) |
block |
block(id, ms) |
Suppresses forwarding of a control for 20–4000 ms |
sensitivity |
sensitivity(id, mid, ratio) |
Rescales an analog control around a midpoint |
deadzone |
deadzone(idx, idy, dzx, dzy) |
Adjusts stick deadzone on two axes |
combo_run |
combo_run(Name) |
Starts a combo (no effect if already running) |
combo_running |
combo_running(Name) |
True while the named combo runs |
combo_stop |
combo_stop(Name) |
Halts a running combo immediately |
combo_restart |
combo_restart(Name) |
Restarts a combo from its first statement |
wait |
wait(ms) |
Combo-only: hold current window for 1–32767 ms |
call |
call(Name) |
Combo-only: run another combo, then resume |
Beyond these, GPC includes device functions (LED control, rumble, slot management, battery queries) whose exact signatures are best taken from Cronus's current GPC reference rather than memorized from forum posts.
Identifiers and value ranges
Every physical control has named identifiers. The XB1_ and PS4_ families are aliases for the same internal slots, so scripts written with either work across consoles.
| Group | Examples | Range |
|---|---|---|
| Face buttons | XB1_A, PS4_CROSS, PS4_SQUARE |
0 to 100 |
| Shoulder bumpers | XB1_RB, PS4_R1 |
0 to 100 |
| Triggers (analog) | XB1_RT, PS4_R2 |
0 to 100 |
| D-pad | XB1_UP, PS4_LEFT |
0 to 100 |
| Stick axes | XB1_LX, XB1_LY, PS4_RX, PS4_RY |
-100 to +100 |
| Stick clicks | XB1_LS, PS4_R3 |
0 to 100 |
| Trace channels | TRACE_1 … TRACE_6 |
script-defined; shown in the device monitor |
Axis polarity: negative is up/left, positive is down/right. Each named identifier maps to a number under the hood (PS4_CROSS is 19, for example) and raw numbers are accepted by the compiler, but names are self-documenting — always prefer them. The full identifier list, including console-specific extras like the PS4 touchpad, is in Cronus's identifier documentation.
FAQ
Is GPC syntax the same as C?
The surface is C: braces, semicolons, // comments, the same operator symbols. The differences are structural — a perpetually looping main instead of a run-once program, int as the only variable type, no standard library, and combo blocks with wait() as the timing mechanism.
Where can I use wait() in a GPC script?
Only inside combo blocks. main must finish each pass quickly so the next controller report can be processed; anything that needs duration gets a combo, triggered from main with combo_run().
What is the difference between define and int in GPC?
define creates a compile-time constant — a name substituted for a fixed number, unchangeable at runtime. int declares a variable that occupies memory and can be reassigned every pass. Use define for tunable timings, int for state such as toggles and counters.
How fast does the GPC main loop run?
Once per controller input report, which on the Zen is on the order of every few milliseconds. Cronus's introductory documentation does not pin an exact figure, so never design logic that assumes a precise loop period — measure durations with get_ptime() or structure timing inside combos instead.
Why does my script compile but a combo never fires?
The three usual causes: the triggering condition uses event_press on a control you are holding rather than tapping; the combo is already running so combo_run does nothing (add a combo_running() check while debugging); or the wrong memory slot is selected on the device.
