CronusZenWiki
Learn GPC Scripting

GPC Combos and Macros: Timing, Structure, Worked Example

How GPC combo blocks work on the Cronus Zen: the wait() timing model, run-once triggers, interrupting combos, and a worked macro built step by step.

Updated September 2, 20265 min readPPeepsIntermediate⏱ 20 min

A GPC combo is a named block of timed, simulated inputs, press this, hold it 50 milliseconds, release, press that, which the Cronus Zen plays back on its own clock while your main loop keeps running. Combos are how GPC expresses everything with duration: macros, skill-move sequences, drop shots, double taps. This tutorial explains the timing model precisely, then builds a complete one-button macro step by step, a fighting-game-style quarter-circle-forward plus attack, including the trigger, an abort button, and a reusable sub-combo.

You should already be comfortable with the basics from the first script tutorial; the syntax reference has the full combo function table.

How GPC combos actually run

The one rule that explains all combo behavior: each wait(ms) holds everything the statements since the previous wait have set, for that many milliseconds. A combo is not a list of instantaneous actions, it is a list of windows, each defined by the set_val lines above a wait and lasting for that wait's duration.

Take this combo:

// Timing-model illustration (complete script).
main {
    if (event_press(PS4_L1)) {
        combo_run(Windows);
    }
}

combo Windows {
    set_val(PS4_CROSS, 100);    // window 1
    wait(80);
    set_val(PS4_CROSS, 0);      // window 2
    set_val(PS4_CIRCLE, 100);
    wait(120);
}

What the console receives, window by window:

Window Duration Forced by the combo Everything else
1 80 ms Cross held at 100 Passes through from your hands
2 120 ms Cross forced to 0, Circle held at 100 Passes through
after , Nothing; combo has ended All controls back to physical

Three more facts complete the model. A running combo continues on its own timeline even if the condition that started it is long gone. Controls the combo does not mention are untouched, you keep moving and aiming while it plays. And wait() accepts 1 to 32767 milliseconds, so a single window can span half a minute if needed.

Step 1: Sketch the sequence before you code it

Our worked example: in many fighting games, a special move is quarter-circle-forward plus an attack button, stick down, roll to down-forward, then forward, then punch. Before writing GPC, write the human action as a timed table:

Phase Stick / button Roughly
1 Left stick down 45 ms
2 Left stick down-forward (both axes) 45 ms
3 Left stick forward 45 ms
4 Square (attack) 60 ms

This sketch step is not optional busywork, every combo bug is easier to find when there is a table stating what should be happening in each window. Remember GPC's axis polarity: on the Y axis, +100 is fully down; on the X axis, +100 is fully right (we will treat right as "forward", real fighting games need a facing check that inputs alone cannot see, which is a fundamental GPC limitation worth knowing early).

Step 2: Write the combo block

Translate the table directly, one window per phase:

// Step 2: the sequence itself, on a test trigger (complete script).

define STEP_MS = 45;    // per-window stick time
define HIT_MS  = 60;    // attack button hold

main {
    if (event_press(PS4_L1)) {
        combo_run(QuarterCircle);
    }
}

combo QuarterCircle {
    set_val(PS4_LY, 100);        // window 1: stick down
    wait(STEP_MS);

    set_val(PS4_LY, 100);        // window 2: down-forward , 
    set_val(PS4_LX, 100);        // both axes held together
    wait(STEP_MS);

    set_val(PS4_LX, 100);        // window 3: forward only
    wait(STEP_MS);

    set_val(PS4_SQUARE, 100);    // window 4: attack
    wait(HIT_MS);
}

Note that window 2 restates PS4_LY, set values do not carry across wait() boundaries; every window declares its own contents. Forgetting this is the most common combo bug: the stick snaps back to your physical position for a window and the game reads a broken motion.

Step 3: Choose the trigger condition

How a combo is triggered matters as much as its contents:

  • event_press for run-once. True only on the pass where the button becomes pressed, one press, one macro. This is what we want here.
  • get_val for run-while-held. Since combo_run() does nothing while the combo is already running, a held condition makes the combo loop back-to-back, right for rapid fire, wrong for a special move.
  • Add a guard when re-entry would hurt. event_press(PS4_L1) && !combo_running(QuarterCircle) refuses a second press until the first sequence finishes. With event_press this is belt-and-suspenders, but it documents intent and protects you if the trigger later changes.

Tip

While tuning, wire the combo to a button you never use in-game. Once the timings are right, move it to its real trigger. Retuning timing and trigger simultaneously doubles the variables.

Step 4: Interrupts and sub-combos

Two refinements turn a demo into a usable macro.

An abort button. A 200 ms sequence is an eternity if it starts by accident. combo_stop() halts a combo instantly and returns all controls to your hands:

// Inside main, alongside the trigger:
if (event_press(PS4_CIRCLE)) {
    combo_stop(QuarterCircle);   // panic button: abort mid-sequence
}

Reuse with call(). Valid only inside combos, call(Other) suspends the current combo, plays Other to completion, then resumes. If several macros end with the same attack, extract it once:

combo Attack {
    set_val(PS4_SQUARE, 100);    // shared finisher
    wait(60);
}

Any combo can then end with call(Attack); instead of duplicating those lines, one place to retune the hit timing for every macro that uses it.

The finished macro

Everything assembled, commented, and tunable from the top:

// ============================================================
// qcf_macro.gpc, quarter-circle-forward + attack on one button.
// Press L1 to play the sequence. Press Circle to abort it.
// Assumes the character faces right; retune STEP_MS per game.
// ============================================================

define STEP_MS = 45;    // stick time per window
define HIT_MS  = 60;    // attack button hold

main {
    // Run-once trigger with a re-entry guard.
    if (event_press(PS4_L1) && !combo_running(QuarterCircle)) {
        combo_run(QuarterCircle);
    }

    // Abort at any point in the sequence.
    if (event_press(PS4_CIRCLE)) {
        combo_stop(QuarterCircle);
    }
}

combo QuarterCircle {
    set_val(PS4_LY, 100);        // down
    wait(STEP_MS);

    set_val(PS4_LY, 100);        // down-forward (restate LY)
    set_val(PS4_LX, 100);
    wait(STEP_MS);

    set_val(PS4_LX, 100);        // forward
    wait(STEP_MS);

    call(Attack);                // shared finisher, then done
}

combo Attack {
    set_val(PS4_SQUARE, 100);    // attack press
    wait(HIT_MS);
}

Flash it, test it against a training-mode dummy, and expect to tune. Combo timings are tuned against a specific game build; games buffer inputs differently, and a patch can change the windows without notice. There is no universal set of numbers, which is why every timing above is a define at the top of the file.

Common combo mistakes

Symptom Cause Fix
Game reads one long press instead of two taps No release window between presses Insert a set_val(id, 0); wait(...) window
Motion input drops halfway A held value not restated after a wait() Every window redeclares all values it needs
Macro fires repeatedly while button held Triggered with get_val instead of event_press Use event_press for run-once macros
Sequence works in menus, fails in-game Windows shorter than the game's input polling Lengthen wait() values in steps of 10 ms
Combo will not restart quickly combo_run ignored while running Use combo_restart() if a hard reset is intended
Stick fights your movement mid-macro Physical and scripted axis values overlap by design Expected: mentioned controls are overridden until the combo ends

For deeper background on why overrides behave this way, read the inputs and events explainer, combos are just set_val on a timer, and the same pipeline rules apply.

FAQ

What is the difference between a combo and a macro in GPC?

Nothing formal, "macro" is the gaming term for an automated input sequence, and a combo block is GPC's construct for building one. Every GPC macro is a combo, though combos also serve smaller jobs like a single timed press.

How do I stop a GPC combo mid-run?

combo_stop(Name) halts it immediately and returns all controls to physical input. Wire it to an abort button with event_press, as in the worked example above.

Why does my combo restart from the beginning instead of continuing?

Something is calling combo_restart(), or the combo ended and was re-triggered. Note the opposite is the default: combo_run() on an already-running combo does nothing, so an accidental restart usually means an explicit combo_restart call.

Can one combo run another combo?

Yes, with call(Other), the current combo pauses, the called combo plays to completion, and the caller resumes at the next statement. call is only valid inside combo blocks. Starting a combo from main always uses combo_run().

How long can a wait() be in a combo?

From 1 to 32767 milliseconds per wait(), about 32 seconds. Chain multiple waits for longer holds. Very long combos keep overriding their mentioned controls the whole time, so pair them with an abort button.