How a Shot Combo Is Built in GPC

GPC Programmingpublished September 4, 2026 · 11 min read

A worked example of the GPC behind a 2K shot mod: a hold-then-release auto-green combo, a rhythm-style right-stick sweep, a menu page that toggles a value and saves it with set_pvar, and an OLED page that prints it.

A 2K shot mod is four small GPC pieces: a combo that presses and releases the shot button on a timer, a second combo that moves the right stick through a few positions, a menu block in main that changes a value and saves it with set_pvar, and an OLED routine that prints that value. This page builds each piece and explains the GPC construct behind it. The reference mod set is the yew2K NBA 2K27 Cronus Zen script; its ranges, defaults, and triggers are used as the worked example. The code is not yew2K's source. yew2K is generated per buyer from the buyer's settings, and its exact curves and guards are not published. Constants that are not in the tables below are illustrative.

The four constructs

ConstructWhat it doesWhere it lives
combo Name { ... }A named timed sequence. Runs alongside main once started.Top level
wait(ms)Ends the current combo step and holds its outputs for ms (1–32767).Inside a combo only
set_val(ID, v)Writes an output value for a button (0–100) or stick axis (-100–100) for the current cycle.main or a combo
event_press(ID) / event_release(ID)True for one cycle when the input changes state.main
set_pvar(SPVAR_n, v) / get_pvar(SPVAR_n, min, max, def)Save and load a slot-private persistent value with bounds.main / init
cls_oled, putc_oled, puts_oled, printfClear the Zen OLED, buffer characters, draw them.A user function called from main

Full signatures: GPC combos, wait, and call, controller input and output, persistent variables, OLED functions.

Example 1: hold-then-release (Auto-Green style)

Auto-Green on yew2K fires when you click R3 with the right stick inside the centre. The script holds Square for the page's ms value, then releases. The right stick is held neutral the whole time. It is a timing mod, not a rhythm sweep. Pressing Cross bails any running shot combo. The left stick is never driven by the script.

SettingValue
TriggerR3 click, stick centred
Range300–900 ms, 5 ms steps
Shipped default660 ms
yew's Sept 3 value710 ms
BailCross
Tuning ruleEarly → raise the value. Late → lower it.

The GPC:

example.gpc
define AG_MIN = 300;
define AG_MAX = 900;

int ag_on;      // page toggle
int ag_hold;    // ms Square is held

init {
    ag_on   = get_pvar(SPVAR_1, 0, 1, 0);
    ag_hold = get_pvar(SPVAR_2, AG_MIN, AG_MAX, 660);
}

main {
    if (ag_on && event_press(PS4_R3) && stick_centred()
        && !combo_running(AutoGreen)) {
        combo_run(AutoGreen);
    }
    if (combo_running(AutoGreen) && get_ival(PS4_CROSS)) {
        combo_stop(AutoGreen);      // Cross bails the shot
    }
}

combo AutoGreen {
    set_val(PS4_SQUARE, 100);
    set_val(PS4_RX, 0);
    set_val(PS4_RY, 0);             // timing only, no stick motion
    wait(ag_hold);
    set_val(PS4_SQUARE, 0);
    set_val(PS4_RX, 0);
    set_val(PS4_RY, 0);
    wait(random(40, 90));           // illustrative gap, see below
}

// 20 is an illustrative dead zone
function stick_centred() {
    return abs(get_ival(PS4_RX)) < 20
        && abs(get_ival(PS4_RY)) < 20;
}

What each line is doing:

  • event_press(PS4_R3) is true for exactly one cycle when the stick is clicked. Using get_val here would restart the combo on every cycle the click is held.
  • stick_centred() reads get_ival, the physical input before any script output, so the check cannot be fooled by the combo's own set_val(PS4_RY, 0).
  • combo_running(AutoGreen) stops a second click from restarting a shot already in flight.
  • Inside the combo, every set_val before a wait is held for that whole step. That is why RX and RY are written to 0 in both steps: the stick must read neutral for the full hold and release, or the game grades it as a stick shot.
  • wait(ag_hold) takes a variable. The menu changes ag_hold in 5 ms steps; the combo picks up the new value on the next run.
  • The trailing wait(random(40, 90)) is a short randomised gap. It keeps the combo alive for a few cycles after release so a second click on the same frame cannot start a new shot instantly. random is Zen-only. The 40–90 window is this example's choice, not a yew2K value.

Gotcha from the yew2K menu: if SQ Remap is set to R3, the Auto-Green click moves to L3. In GPC terms the trigger identifier becomes a variable rather than a constant.

Example 2: the stick sweep (Button Rhythm style)

Button Rhythm turns a Square shot into a rhythm shot. In yew's words: when you press Square, the right stick gets pulled down on the software output level; when you release, it smoothly flicks the stick up. The ms value is how long the upward flick lasts. It is the centre step of a 6-point ease-in-out sweep. 2K27 grades stick tempo, and a snap shrinks the window, which is why the release is eased rather than instant. Button Rhythm does not time the shot. It makes the green window bigger, reduces the shot-contest penalty, and improves consistency.

SettingValue
TriggerPress and release Square
Range8–80 ms, 1 ms steps
Shipped default35 ms
yew's Sept 3 value23 ms
Raw-shot escapeHold the SQ Remap button (L3 default) while pressing Square

The GPC:

example.gpc
int br_on;
int br_ms;                              // 8–80
int sweep;                              // br_ms after context multipliers

init {
    br_on = get_pvar(SPVAR_3, 0, 1, 0);
    br_ms = get_pvar(SPVAR_4, 8, 80, 35);
}

main {
    // L3 held = plain Square shot (SQ Remap)
    if (br_on && !get_val(PS4_L3)) {
        // pull the stick down while Square is held
        if (get_val(PS4_SQUARE) && !combo_running(RhythmUp)) {
            set_val(PS4_RY, 100);
        }
        if (event_release(PS4_SQUARE)) {
            sweep = br_ms;
            // context multipliers go here, see the table
            combo_run(RhythmUp);
        }
    }
}

combo RhythmUp {
    set_val(PS4_RY, 100);  wait(sweep / 2);   // ease in
    set_val(PS4_RY, 85);   wait(sweep / 2);
    set_val(PS4_RY, 55);   wait(sweep);       // centre step
    set_val(PS4_RY, 25);   wait(sweep);
    set_val(PS4_RY, 8);    wait(sweep / 2);   // ease out
    set_val(PS4_RY, 0);    wait(sweep / 2);
}

What each line is doing:

  • In GPC, positive RY is down. set_val(PS4_RY, 100) in main pins the stick down on the output side for as long as Square is held. Your physical stick does not move.
  • event_release(PS4_SQUARE) fires once. That is the moment the sweep starts.
  • The combo writes six stick positions. Each wait holds the previous set_val. Small moves with short holds at the ends and the largest moves in the middle give the ease-in-out shape. The positions and the half/full split are this example's curve, not yew2K's.
  • wait(sweep / 2) is integer division. With br_ms = 8 the outer steps are 4 ms, which is still a legal wait.
  • The !combo_running(RhythmUp) guard matters. Without it, main would overwrite the combo's RY value with 100 on every cycle the button is still read as held.
  • !get_val(PS4_L3) is the SQ Remap escape hatch. Holding the remap button while pressing Square skips the rhythm logic and sends a plain shot. If the remap is moved to R1, L1, or R3, this identifier changes with it.

yew2K scales the sweep by shot context. The multipliers are automatic; the player never sets them.

ContextSweep change16-bit integer form
Turbo fade (R2 + fade)~18% slowersweep = sweep * 118 / 100;
Insta-Stop or Quick Stop shot~10% fastersweep = sweep * 90 / 100;
Shot on the move~8% fastersweep = sweep * 92 / 100;
Within 400 ms of leaving the move stickslightly fasterif (get_brtime(PS4_LX) < 400) ...

Multiply before you divide. GPC variables are signed 16-bit, so 80 * 118 (9440) is safe; 80 / 100 * 118 is 0.

Example 3: a menu page that toggles a value and saves it

The yew2K menu opens with L2 held + Options and draws on the Zen's OLED, not the TV. With L2 still held, D-pad Left/Right changes page, Up/Down toggles On/Off, and Cross + Up/Down changes the ms value on pages that have one. The menu ignores the D-pad if L2 is released. Values persist on the Zen and save automatically.

In GPC that is a block in main:

example.gpc
define PAGE_RHYTHM    = 1;
define PAGE_AUTOGREEN = 2;
define PAGE_COUNT     = 2;

int menu_open;
int page = 1;
int dirty;                              // 1 when the OLED needs a redraw

main {
    if (get_val(PS4_L2) && event_press(PS4_OPTIONS)) {
        menu_open = !menu_open;
        dirty = TRUE;
    }

    // D-pad is ignored once L2 is released
    if (menu_open && get_val(PS4_L2)) {
        if (event_press(PS4_RIGHT)) {
            page = page + 1;
            if (page > PAGE_COUNT) page = 1;
            dirty = TRUE;
        }
        if (event_press(PS4_LEFT)) {
            page = page - 1;
            if (page < 1) page = PAGE_COUNT;
            dirty = TRUE;
        }

        if (page == PAGE_AUTOGREEN) {
            if (get_val(PS4_CROSS)) {
                // fine-tune: Cross + Up/Down, 5 ms steps
                if (event_press(PS4_UP)) {
                    ag_hold = clamp(ag_hold + 5, AG_MIN, AG_MAX);
                    set_pvar(SPVAR_2, ag_hold);
                    dirty = TRUE;
                }
                if (event_press(PS4_DOWN)) {
                    ag_hold = clamp(ag_hold - 5, AG_MIN, AG_MAX);
                    set_pvar(SPVAR_2, ag_hold);
                    dirty = TRUE;
                }
            } else if (event_press(PS4_UP) || event_press(PS4_DOWN)) {
                // Up/Down alone toggles On/Off
                ag_on = !ag_on;
                set_pvar(SPVAR_1, ag_on);
                dirty = TRUE;
            }
        }
        block_all_inputs();     // menu presses never reach the game
    }
}

What each line is doing:

  • get_val(PS4_L2) && event_press(PS4_OPTIONS) is the open/close chord. event_press makes it fire once per press.
  • The whole menu body is inside if (menu_open && get_val(PS4_L2)). Let go of L2 and the D-pad is ordinary D-pad again. That is the same rule yew2K enforces.
  • clamp keeps the value inside 300–900 and steps it by 5. Button Rhythm's page would use clamp(br_ms + 1, 8, 80) because that page steps by 1.
  • set_pvar is called only inside the branch that changed something. Writing persistent memory every cycle is wasteful and a common mistake.
  • get_pvar in init (see Examples 1 and 2) reads the saved value back with the same bounds and a default. A fresh slot gets the default; a tuned slot gets what you left.
  • block_all_inputs() drops this cycle's controller report so the D-pad and Cross taps do not leak into the game while you are in the menu. Zen-only.

SPVARs are private to the memory slot. Sixty-four positions per slot is plenty for a menu this size; yew2K's menu has eleven pages with an ms value plus the toggles.

Example 4: an OLED page that prints the value

The page needs a title, the current value, On/Off, and the page counter yew2K shows bottom-right as "n / total". putc_oled fills a character buffer one position at a time; puts_oled draws that buffer at a coordinate. printf draws a string stored in the data section. Numbers are turned into characters by adding 48 (ASCII "0") to each digit.

example.gpc
data (65, 117, 116, 111, 45, 71, 114, 101, 101, 110, 0)   // "Auto-Green", null-terminated

function put_digits(n) {                // 3 digits, right-aligned
    putc_oled(1, 48 + n / 100);
    putc_oled(2, 48 + (n / 10) % 10);
    putc_oled(3, 48 + n % 10);
}

function draw_autogreen_page() {
    cls_oled(OLED_BLACK);

    // title: string at data offset 0
    printf(2, 2, OLED_FONT_MEDIUM, OLED_WHITE, 0);

    // value, e.g. "710"
    put_digits(ag_hold);
    puts_oled(2, 26, OLED_FONT_LARGE, 3, OLED_WHITE);

    // "On " or "Off"
    putc_oled(1, 79);
    if (ag_on) { putc_oled(2, 110); putc_oled(3, 32); }
    else       { putc_oled(2, 102); putc_oled(3, 102); }
    puts_oled(90, 26, OLED_FONT_MEDIUM, 3, OLED_WHITE);

    // page counter bottom-right, e.g. "2/2"
    putc_oled(1, 48 + page);
    putc_oled(2, 47);
    putc_oled(3, 48 + PAGE_COUNT);
    puts_oled(100, 54, OLED_FONT_SMALL, 3, OLED_WHITE);
}

main {
    // ... menu block from Example 3 ...
    if (menu_open && dirty && page == PAGE_AUTOGREEN) {
        draw_autogreen_page();
        dirty = FALSE;
    }
}

What each line is doing:

  • cls_oled(OLED_BLACK) wipes the display. Without it the old digits stay under the new ones.
  • put_digits does integer division and modulo to peel off hundreds, tens, and ones. For Button Rhythm (max 80) two digits are enough.
  • putc_oled(index, char) is 1-based. puts_oled(x, y, font, length, color) draws length characters from that buffer. The x/y here are illustrative positions; check the current OLED reference for font heights.
  • printf is the Zen OLED string call, not a console logger. Its string lives in the data section as bytes with a 0 terminator, and the last argument is the offset into that data.
  • The dirty flag means the page is redrawn only when something changed. Redrawing the whole display every cycle is the fastest way to make a menu feel laggy.

On boot, yew2K shows its logo during a 1500 ms menu lockout. In GPC that is a boot combo that draws the logo and waits before menu_open is allowed to go true.

Where each piece goes in the file

BlockContents
define / dataRanges, page numbers, OLED strings
int globalsag_on, ag_hold, br_on, br_ms, menu_open, page, dirty
initget_pvar every setting with bounds and a default; cls_oled
mainKill switch, menu block, triggers (event_press/event_release), held-stick logic, redraw when dirty
combo blocksAutoGreen, RhythmUp, one per shot-pack page
function blocksstick_centred, put_digits, the OLED page drawers

Order matters for the compiler: declarations before runtime blocks, and main is mandatory. See GPC script structure.

Tuning the values once it compiles

PageRangeStepShipped defaultyew's Sept 3 value
Button Rhythm8–80 ms1 ms3523
Auto-Green (standstill)300–900 ms5 ms660710

The rule for every auto-green: early → raise the value; late → lower the value. Button Rhythm has no formula; try fades and stand-stills at each value in online MyCourt and keep what feels best. Two in-game settings are required before any of this works: Shot Timing set to Shots and Layups, and Shot Meter Off. Offline MyCourt timing differs from online, so test online. yew's framing for Auto-Green is about 70% with good values; nothing here guarantees a green.

What this page is not

It is not yew2K's source. yew2K ships as a generator build: the buyer's settings are compiled into the GPC on yew.gg, updates ship weekly through the 2K27 cycle, and support is on yew.gg/discord. The step curve, the guards around fades and Insta-Stop, the shot-pack pages (Left Fade, Right Fade, Catch Shoot, Step Back, Quick Stop), and the kill switch are all real yew2K behaviour that this page only sketches. The point is the GPC: a shot is a combo, a rhythm is a sweep, a setting is a persistent variable, and a menu is a few event_press checks gated on a held trigger.

Frequently asked questions

Why is the hold a combo instead of a wait in main?

wait is illegal in main. main must finish every cycle so the Zen can keep reading the controller. A combo owns its own timeline and runs alongside main, which is exactly what a hold-then-release needs.

Why write the stick to 0 in the Auto-Green combo at all?

Because a combo step holds every set_val written before its wait. If you only wrote Square, a thumb resting on the stick would still reach the game and the shot would grade as a stick shot. Writing RX and RY to 0 makes the release a pure timing release.

Does set_pvar save immediately?

Yes. Call it when the value changes and the new value is there after a power cycle. Do not call it every cycle. Read it back in init with get_pvar and the same bounds.

Can I print the value with printf?

printf draws a string from the data section, so it is the right call for a fixed title. For a changing number, build the digits with putc_oled and draw them with puts_oled.