CronusZenWiki
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 readCECZW EditorialIntermediate20 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.