CronusZenWiki
GPC Scripting

GPC Inputs Explained: The get_val and set_val Model

How Cronus Zen scripts read and modify controller input with get_val, set_val, event_press, and get_ptime — value ranges plus worked example patterns.

Updated September 2, 20266 min readCECZW Editorial

Every Cronus Zen script is built on one mechanism: the get_val and set_val model. Each time your controller sends a report, the Zen runs your main block; get_val() reads what your hands are doing, set_val() decides what the console sees for that instant, and anything you do not touch passes through unchanged. Once this model clicks, every GPC feature — events, timers, combos, remaps — turns out to be a small variation on it. This page explains the pipeline precisely, with complete example scripts for each pattern.

If you have not written any GPC yet, the first script tutorial is a friendlier entry point; this explainer goes deeper on the model behind it.

The pipeline: your controller, the script, the console

Think of the Zen as a checkpoint on the wire. The flow for every single input report:

  1. Your controller reports its full state — every button, trigger, and stick axis.
  2. The Zen runs your main block once against that snapshot.
  3. Whatever values remain after your set_val() calls are forwarded to the console.

Three rules fall out of this, and they explain nearly all beginner confusion:

  • Reads are snapshots. get_val() tells you about this report. Next pass, everything is re-read from scratch.
  • Writes are per-pass. A set_val() in main shapes only the current outgoing report. If the condition around it stops being true, you stop writing, and the physical value flows through again on the next pass. Nothing is "stuck" — persistence requires either writing every pass or a combo, which is effectively set_val on a timer.
  • Untouched controls pass through. A script that mentions only the right trigger has zero effect on anything else.

There is no game-facing magic in any of this: the console receives what looks like ordinary controller input. That is both the power and the hard limit of the model — a script can reshape your inputs, but it cannot see the screen or know anything the controller does not report. The wider capabilities question is covered in what is GPC.

Reading inputs: get_val, get_ival, and get_lval

GPC gives you three reads on every control, differing only in which snapshot they consult:

Function Answers the question
get_val(id) What is this control's value right now, in the current processing chain?
get_ival(id) What is the player physically doing, before the script modified anything?
get_lval(id) What was this control's value on the previous pass?

get_val is the everyday workhorse. get_ival matters once your script starts overwriting a control: after set_val(XB1_RT, 0), later logic reading get_val(XB1_RT) sees the modified 0, while get_ival(XB1_RT) still sees the player's actual pull — essential when one part of a script suppresses an input that another part still needs to observe. get_lval lets you compare against the previous pass, which is how you detect change by hand — though for the common cases, the event functions below do it for you.

In conditions, any nonzero value is true, so if (get_val(PS4_R2)) means "pressed at all." For analog controls, prefer explicit thresholds — if (get_val(PS4_R2) > 90) reads as "deliberate full pull" and ignores feather touches.

Value ranges by input type

All values are integers representing a percentage of full deflection:

Input type Examples Range Meaning
Face buttons, bumpers, D-pad PS4_CROSS, XB1_RB, XB1_UP 0 to 100 0 released, 100 pressed
Triggers PS4_R2, XB1_RT 0 to 100 Analog pull depth
Stick axes PS4_LX, PS4_RY, XB1_LX -100 to +100 Negative is up/left, positive is down/right, 0 centered
Stick clicks PS4_L3, XB1_RS 0 to 100 Digital press

The PS4_ and XB1_ identifier families alias the same internal slots, so either vocabulary drives either console. The full identifier list and the rest of the language are in the GPC syntax reference.

Edges versus levels: event_press and event_release

get_val answers "is it pressed?" — a level. Very often you need "did it just become pressed?" — an edge. That distinction is the single most common source of beginner bugs, because main runs continuously: a condition on get_val is true for every pass of a hold, potentially hundreds of times for one human press.

  • event_press(id) is true only on the exact pass where the control goes from released to pressed.
  • event_release(id) is true only on the pass where it goes from pressed to released.

The classic demonstration is a toggle:

// Edge vs. level: a toggle done right (complete script).
// Tap Options to enable/disable; the flag flips exactly once per tap.

int enabled;    // 0 = off, 1 = on

main {
    if (event_press(PS4_OPTIONS)) {   // edge: one flip per press
        enabled = !enabled;
    }

    // Level: while enabled AND the trigger is held, act every pass.
    if (enabled && get_val(PS4_R2)) {
        set_val(PS4_R1, 100);         // e.g. hold R1 alongside R2
    }
}

Replace that event_press with get_val and the flag flips every pass while Options is held — the toggle lands on an effectively random state. Rule of thumb: edges for decisions, levels for conditions. One-shot actions (toggles, starting a run-once combo, counters) key off event_press/event_release; continuous behavior (rapid fire while held, suppression while aiming) keys off get_val.

Measuring time: get_ptime

get_ptime(id) returns the milliseconds since a control last changed state, from 0 to 32767. Combined with levels and edges, it distinguishes taps from holds:

// Tap vs. hold on one button (complete script).
// Tap Square (under 300 ms): passes through as a normal press.
// Hold Square 300 ms or more: also presses R3 on release.

define HOLD_MS = 300;

main {
    // At the release edge, get_ptime reports how long it was held.
    if (event_release(PS4_SQUARE) && get_ptime(PS4_SQUARE) >= HOLD_MS) {
        combo_run(LongPressBonus);
    }
}

combo LongPressBonus {
    set_val(PS4_R3, 100);   // the "hold" action
    wait(80);
    set_val(PS4_R3, 0);
    wait(20);
}

The same tool works mid-hold: if (get_val(XB1_A) && get_ptime(XB1_A) > 200) is true from the 200 ms mark of a hold onward — useful for "held long enough" gates without any combo at all.

Writing outputs: set_val, blocking, and swapping

set_val(id, value) is the only way a script changes what the console sees, and three idioms cover most uses.

Amplify or add. Write a value alongside or instead of what the player did:

// Full-deflection assist: past 95, snap the axis to exactly 100.
main {
    if (get_val(PS4_RX) > 95) {
        set_val(PS4_RX, 100);
    }
}

Suppress. Writing 0 hides a physical input from the game:

// While holding L2, completely hide R3 (no more accidental
// stick-click melee while aiming).
main {
    if (get_val(PS4_L2)) {
        set_val(PS4_R3, 0);
    }
}

For time-boxed suppression there is also block(id, ms), which suppresses a control's forwarding for 20 to 4000 milliseconds, and swap(id1, id2), which exchanges two controls' values for on-the-fly remapping — both documented in the syntax reference.

Adjust relative to the player. Read, modify, write — with explicit bounds, since axes cap at ±100:

// Conceptual recoil-style compensation: while aiming and firing,
// add a small constant downward push to the right stick.
// Strength varies per game, weapon, and patch — expect to tune,
// and expect patches to silently change what feels right.

define PULL = 18;    // downward push, in axis percent

int newy;            // scratch variable for the clamped result

main {
    if (get_val(PS4_L2) && get_val(PS4_R2)) {
        newy = get_val(PS4_RY) + PULL;   // +100 is fully down
        if (newy > 100) {
            newy = 100;                  // stay inside the valid range
        }
        set_val(PS4_RY, newy);
    }
}

This read-modify-write shape — physical value in, arithmetic, bounded value out — is the backbone of every input-shaping script, from sensitivity tweaks to recoil compensation.

FAQ

What is the difference between get_val and set_val in GPC?

get_val(id) reads a control's current value for this pass of the main loop; set_val(id, value) writes the value the console will receive for this pass. Reads observe, writes override, and both last exactly one pass.

What is the difference between get_val and get_ival?

get_ival returns the raw physical input before any script modification; get_val returns the value as it currently stands in the processing chain, including your own earlier set_val calls. They differ only after the script has modified that control in the same pass.

Why does set_val only work while the button condition is true?

Because set_val shapes a single outgoing report. The next pass, main re-runs from scratch; if no code writes the control, the physical value passes through. To hold a value over time, write it every pass while a condition holds, or use a combo, which holds values across wait() windows for you.

When should I use event_press instead of get_val?

Whenever the action should happen once per physical press: toggles, counters, launching run-once combos. event_press is true for exactly one pass per press, while get_val stays true for the entire hold — use it for continuous conditions instead.

Can a GPC script press a button the player never touched?

Yes — set_val(id, 100) outputs a press regardless of physical state, and combos sequence such presses over time. The console cannot distinguish scripted from physical input in the report itself, though games' terms of service generally prohibit automated input, and publishers can analyze input patterns server-side.