GPC combos and wait(): timed button sequences that don't block main
How GPC combos work: declaring a combo, combo_run, combo_stop, combo_restart and combo_running, why wait() only belongs inside combos, calling one combo from another, and how to keep a sequence from misfiring.
- Updated

Quick answer
A GPC combo is a top-level block that sets outputs and pauses with wait() between steps while main keeps running every cycle. Start one with combo_run, stop it with combo_stop, restart it with combo_restart, and check it with combo_running. wait() only works inside a combo; a delay loop in main would freeze the controller.
Why can't I just delay in main?
main runs once per VM cycle, every few milliseconds, and must finish quickly so the Zen can send the next report to the console. There is no sleep. If you tried to hold a button for 80 ms by looping in main, the controller would freeze for those 80 ms and every other input would be lost.
A combo solves this. It is a separate block that the compiler schedules across cycles. Each wait() inside a combo says "hold these outputs for this many milliseconds, then continue", and while it waits, main keeps running normally.
How do I declare and run a combo?
The official example taps Square once:
main {
if(event_press(PS5_CROSS)) combo_run(TapSquare);
if(event_press(PS5_CIRCLE)) combo_stop(TapSquare);
}
combo TapSquare {
set_val(PS5_SQUARE, 100);
wait(80);
set_val(PS5_SQUARE, 0);
wait(80);
}Reading it line by line:
combo TapSquare { ... }declares the block at the top level of the file, outsidemain.set_val(PS5_SQUARE, 100); wait(80);presses Square and holds it for 80 ms.set_val(PS5_SQUARE, 0); wait(80);releases it and waits another 80 ms, so a following press is seen as a new press rather than one long hold.- In
main,event_pressstarts the combo on the cycle Cross is tapped. Because it isevent_press, a held Cross does not restart it every cycle.
Combo names live in their own namespace, separate from variables and definitions, though keywords remain reserved.
What do the control functions do?
| Function | Behaviour |
|---|---|
combo_run(Name) | Starts the combo if it is not already running. Repeated calls do nothing. |
combo_restart(Name) | Starts the combo from the first line even if it is mid-run. |
combo_stop(Name) | Stops it immediately. No effect if idle. |
combo_running(Name) | Returns TRUE while the combo is active. |
combo_run versus combo_restart is the choice that catches people. If a player taps Cross twice quickly, combo_run ignores the second tap while the first tap's combo is still going; combo_restart would cut the sequence short and begin again. Decide which you want deliberately.
How do I repeat a combo while a button is held?
Use get_val rather than event_press, and let combo_run's idle check do the rate limiting:
main {
if(get_val(PS5_R2)) combo_run(TapSquare);
}Every cycle R2 is held, main asks to run the combo. It only starts when the previous run has finished, so the result is a steady tap at the rate set by the two wait() values. This is the structure behind rapid fire in GPC, and also behind the honest warning that comes with it.
How do I call one combo from another?
call(OtherCombo) inside a combo pauses the parent until the child finishes, then continues. It keeps long sequences readable:
combo Reload {
set_val(PS5_SQUARE, 100);
wait(60);
set_val(PS5_SQUARE, 0);
wait(40);
}
combo ReloadThenCrouch {
call(Reload);
set_val(PS5_R3, 100);
wait(60);
set_val(PS5_R3, 0);
}The official guide warns against recursive or cyclic calls: a combo that calls itself, or two that call each other, will not behave. wait and call must both be at the root of the combo, not inside an if.
What does wait() actually mean?
wait(ms) holds the outputs set so far for ms milliseconds. Two details:
- A
waitat the end of a combo defines the gap before the combo can start again. Leave it out and a repeated combo may run its last output straight into the first, which some games read as one long press. - Wait values are not frame-locked. A 16 ms wait will not perfectly align to a 60 Hz game frame. Pick values with margin.
How do I test a combo?
- Use
TRACE_1toTRACE_6inside the combo (set_val(TRACE_1, 1)and so on) and watch them in Zen Studio Legacy's Device Monitor. Traces do not reach the console. - Test the first run after loading the script. Uninitialised state shows up there.
- Test stop mid-run. If
combo_stopfires with Square still set to 100, the button releases immediately because outputs reset every cycle. - Time it. A combo with waits summing to 160 ms should repeat at roughly six per second.
Which patterns should I avoid?
- Blocking loops in
mainfor timing. The official architecture guide has a page titled "timing without blocking loops"; the answer is always a combo or a timer variable. - Restarting on every cycle with
combo_restartunderget_val. The combo never gets past its first line. - Deep call chains for what should be one combo.
- Forgetting the trailing wait.
Where next?
Combos produce output over time; the other half of a useful script is remembering settings between sessions. Continue with persistent variables with SPVAR, or go back to reading values with get_val if the input side is still hazy.
Frequently asked questions
Can I use wait() in main?
What happens if I call combo_run on a combo that is already running?
Do outputs set in a combo persist after it ends?
What is fcombo?
Sources
Every link was live when this guide was last updated.
This guide is independent and unofficial; product names belong to their owners. Found an error? Use the contact page and we will correct it and note the change.