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.
On this page
- The pipeline: your controller, the script, the console
- Reading inputs: get_val, get_ival, and get_lval
- Value ranges by input type
- Edges versus levels: event_press and event_release
- Measuring time: get_ptime
- Writing outputs: set_val, blocking, and swapping
- FAQ
- What is the difference between get_val and set_val in GPC?
- What is the difference between get_val and get_ival?
- Why does set_val only work while the button condition is true?
- When should I use event_press instead of get_val?
- Can a GPC script press a button the player never touched?
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:
- Your controller reports its full state — every button, trigger, and stick axis.
- The Zen runs your
mainblock once against that snapshot. - 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()inmainshapes 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 effectivelyset_valon 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.
