;;;; raylib [core] example - input gamepad ;;;; ;;;; examples/core/core_input_gamepad.c. Needed set-config-flags (and the ;;;; FLAG_ constants), both now bound. ;;;; ;;;; Three deliberate differences from the C, all of them things the port ;;;; could not or should not follow. ;;;; ;;;; **The pad artwork is gone, and with it the xbox/PS branches.** The C draws ;;;; `resources/ps3.png` or `resources/xbox.png` and picks between them by ;;;; matching `GetGamepadName` against four substrings. Neither image is in ;;;; this repository, and importing two binaries for one example is worse than ;;;; the alternative: the C has a third branch, for a pad it does not ;;;; recognise, which draws the whole thing out of rounded rectangles and ;;;; circles and needs no asset at all. That branch is what this ports, in ;;;; full. It is also the branch a Linux desktop usually takes anyway. ;;;; ;;;; **The name is drawn and not matched on.** get-gamepad-name copies ;;;; raylib's static text into the context allocator, which this program never ;;;; frees — so the name is read once per frame into the frame arena, and the ;;;; branches that matched on it are the artwork's and are gone with it. ;;;; ;;;; **The VIBRATE button is drawn and inert.** SetGamepadVibration is the one ;;;; call in the C this refuses to bind, and vendor/raylib/raylib.fln already ;;;; carries the paragraph saying why: its arity differs between raylib ;;;; versions with no 5.5 header here to settle it, so a guess is a corrupted ;;;; stack frame rather than a link error — and the symbol in libraylib.so.550 ;;;; disassembles to a single TraceLog call and a jump. Binding it would be ;;;; binding a warning. The button is drawn because taking it out would hide ;;;; the finding; it is labelled so. ;;;; ;;;; With no pad attached this shows "GP0: NOT DETECTED" and nothing else, ;;;; which is the C's behaviour minus the greyed-out artwork. import rl "vendor:raylib" import d "digits.fln" const screen-width = 800 const screen-height = 450 const stick-deadzone: f32 = 0.1 const trigger-deadzone: f32 = -0.9 once gamepad: i32 ;; The C's deadzone test, which is a band around zero and not a clamp: inside ;; it the axis reads exactly 0, outside it the raw value passes through ;; unscaled. That is a step at the edge of the band and the C has it too. fn deadzone(v: f32, limit: f32) -> f32 if v > 0.0 - limit and v < limit then 0.0 else v ;; The triggers rest at -1 rather than 0, which raylib does not normalise — ;; see the note on GamepadAxis in vendor/raylib/raylib.fln. So their deadzone ;; is a floor near the resting end and not a band around the middle. fn trigger-deadzoned(v: f32) -> f32 if v < trigger-deadzone then -1.0 else v ;; The C's read-out loop is `for (i = 0; i < GetGamepadAxisCount(gamepad); i++) ;; DrawText(TextFormat("AXIS %i: %.02f", i, GetGamepadAxisMovement(gamepad, i)))` ;; — an axis selected by a loop variable, which is what this is: an integer ;; converts to an enum when the conversion is written by name. ;; ;; It used to be a six-armed cond over the members, because an enum is its own ;; type in the checker and a bare i32 does not fit one: ;; ;; expected rl/GamepadAxis, found i32 ;; ;; That rule is unchanged and should be. It is what makes a misspelled ;; `:lft-x` an error *here*, at the call site, rather than a wrong axis read ;; later — which is the whole reason an enum is a type at all rather than a ;; pile of i32 constants. What changed is that the conversion can be said out ;; loud: the rule was never "an integer is dangerous", it was "an integer must ;; not arrive silently", and (rl/GamepadAxis index) is not silent. ;; ;; (The other escape stays closed, and should: a second declare-c of ;; GetGamepadAxisMovement with an i32 face is still refused — one declare-c ;; per C function — and a defn wrapper still cannot retype a parameter. It is ;; no longer needed.) ;; ;; An index raylib does not name converts too, rather than being refused, and ;; raylib bounds-checks its own axis and answers 0.0. So the (min 6 ...) clamp ;; at the call site is what keeps the read-out to the six axes raylib names — ;; load-bearing now rather than cosmetic, since it replaced the :else arm. fn axis-at(pad: i32, index: i32) -> f32 rl/get-gamepad-axis-movement(pad, rl/GamepadAxis(index)) fn draw-pad-background() -> () rl/draw-rectangle-rounded(rl/Rectangle{.x 175.0 .y 110.0 .width 460.0 .height 220.0}, 0.3, 16, rl/darkgray) ;; The three middle buttons and the four face buttons, as outlines. The ;; filled overlays go on top only while the button is down. rl/draw-circle(365, 170, 12.0, rl/raywhite) rl/draw-circle(405, 170, 12.0, rl/raywhite) rl/draw-circle(445, 170, 12.0, rl/raywhite) rl/draw-circle(516, 191, 17.0, rl/raywhite) rl/draw-circle(551, 227, 17.0, rl/raywhite) rl/draw-circle(587, 191, 17.0, rl/raywhite) rl/draw-circle(551, 155, 17.0, rl/raywhite) ;; The d-pad cross: a light plate with a dark one inset, so a pressed ;; direction has something to show up against. rl/draw-rectangle(245, 145, 28, 88, rl/raywhite) rl/draw-rectangle(215, 174, 88, 29, rl/raywhite) rl/draw-rectangle(247, 147, 24, 84, rl/black) rl/draw-rectangle(217, 176, 84, 25, rl/black) rl/draw-rectangle-rounded(rl/Rectangle{.x 215.0 .y 98.0 .width 100.0 .height 10.0}, 0.5, 16, rl/darkgray) rl/draw-rectangle-rounded(rl/Rectangle{.x 495.0 .y 98.0 .width 100.0 .height 10.0}, 0.5, 16, rl/darkgray) fn draw-pad-buttons() -> () if rl/is-gamepad-button-down(gamepad, :button-middle-left) rl/draw-circle(365, 170, 10.0, rl/red) if rl/is-gamepad-button-down(gamepad, :button-middle) rl/draw-circle(405, 170, 10.0, rl/green) if rl/is-gamepad-button-down(gamepad, :button-middle-right) rl/draw-circle(445, 170, 10.0, rl/blue) if rl/is-gamepad-button-down(gamepad, :button-right-face-left) rl/draw-circle(516, 191, 15.0, rl/gold) if rl/is-gamepad-button-down(gamepad, :button-right-face-down) rl/draw-circle(551, 227, 15.0, rl/blue) if rl/is-gamepad-button-down(gamepad, :button-right-face-right) rl/draw-circle(587, 191, 15.0, rl/green) if rl/is-gamepad-button-down(gamepad, :button-right-face-up) rl/draw-circle(551, 155, 15.0, rl/red) if rl/is-gamepad-button-down(gamepad, :button-left-face-up) rl/draw-rectangle(247, 147, 24, 29, rl/red) if rl/is-gamepad-button-down(gamepad, :button-left-face-down) rl/draw-rectangle(247, 201, 24, 30, rl/red) if rl/is-gamepad-button-down(gamepad, :button-left-face-left) rl/draw-rectangle(217, 176, 30, 25, rl/red) if rl/is-gamepad-button-down(gamepad, :button-left-face-right) rl/draw-rectangle(271, 176, 30, 25, rl/red) if rl/is-gamepad-button-down(gamepad, :button-left-trigger-1) rl/draw-rectangle-rounded(rl/Rectangle{.x 215.0 .y 98.0 .width 100.0 .height 10.0}, 0.5, 16, rl/red) if rl/is-gamepad-button-down(gamepad, :button-right-trigger-1) rl/draw-rectangle-rounded(rl/Rectangle{.x 495.0 .y 98.0 .width 100.0 .height 10.0}, 0.5, 16, rl/red) fn draw-stick(cx: i32, cy: i32, ax: f32, ay: f32, thumb-down: bool) -> () rl/draw-circle(cx, cy, 40.0, rl/black) rl/draw-circle(cx, cy, 35.0, rl/lightgray) rl/draw-circle(cx + i32(ax * 20.0), cy + i32(ay * 20.0), 25.0, if thumb-down then rl/red else rl/black) fn main() -> () ;; Before init-window, and it has to be: raylib reads the flags while it is ;; creating the context, so the same call afterwards is accepted, logged, and ;; has no effect on the window that already exists. rl/set-config-flags(rl/flag-msaa-4x-hint) rl/init-window(screen-width, screen-height, "raylib [core] example - input gamepad") defer rl/close-window() rl/set-target-fps(60) until rl/window-should-close() ;; The frame arena holds the pad's name for one frame. free-all(context/temp) ;; Update if rl/is-key-pressed(:key-left) and gamepad > 0 gamepad -= 1 if rl/is-key-pressed(:key-right) gamepad += 1 let axis-count = min(6, rl/get-gamepad-axis-count(gamepad)) let vibrate-rect = rl/Rectangle{.x 10.0, .y 90.0 + 20.0 * f32(axis-count), .width 75.0, .height 24.0} ;; Draw rl/with-drawing: rl/clear-background(rl/raywhite) if rl/is-gamepad-available(gamepad) ;; The C draws `TextFormat("GP%d: %s", gamepad, GetGamepadName(...))`. ;; TextFormat is variadic and unbound, so the pieces are drawn in turn. rl/draw-text("GP", 10, 10, 10, rl/black) let x = 10 + rl/measure-text("GP", 10) let x = x + d/draw-int(gamepad, x, 10, 10, rl/black) rl/draw-text(": ", x, 10, 10, rl/black) rl/draw-text(with-allocator(context/temp, rl/get-gamepad-name(gamepad)), x + rl/measure-text(": ", 10), 10, 10, rl/black) let lx = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-left-x), stick-deadzone) let ly = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-left-y), stick-deadzone) let rx = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-right-x), stick-deadzone) let ry = deadzone(rl/get-gamepad-axis-movement(gamepad, :axis-right-y), stick-deadzone) let lt = trigger-deadzoned(rl/get-gamepad-axis-movement(gamepad, :axis-left-trigger)) let rt = trigger-deadzoned(rl/get-gamepad-axis-movement(gamepad, :axis-right-trigger)) draw-pad-background() draw-pad-buttons() draw-stick(345, 260, lx, ly, rl/is-gamepad-button-down(gamepad, :button-left-thumb)) draw-stick(465, 260, rx, ry, rl/is-gamepad-button-down(gamepad, :button-right-thumb)) ;; The triggers as bars filling upward from a grey track. The +1 ;; and the halving turn raylib's [-1, 1] into [0, 1]. rl/draw-rectangle(151, 110, 15, 70, rl/gray) rl/draw-rectangle(644, 110, 15, 70, rl/gray) rl/draw-rectangle(151, 110, 15, i32((1.0 + lt) / 2.0 * 70.0), rl/red) rl/draw-rectangle(644, 110, 15, i32((1.0 + rt) / 2.0 * 70.0), rl/red) rl/draw-text("DETECTED AXIS [", 10, 50, 10, rl/maroon) let x = 10 + rl/measure-text("DETECTED AXIS [", 10) rl/draw-text("]:", x + d/draw-int(axis-count, x, 50, 10, rl/maroon), 50, 10, rl/maroon) for i in range(axis-count) rl/draw-text("AXIS ", 20, 70 + 20 * i, 10, rl/darkgray) let x = 20 + rl/measure-text("AXIS ", 10) y = 70 + 20 * i x += d/draw-int(i, x, y, 10, rl/darkgray) rl/draw-text(": ", x, y, 10, rl/darkgray) x += rl/measure-text(": ", 10) ;; The raw reading, not the deadzoned one — the C shows the raw ;; value here too, which is what makes the deadzone visible as a ;; difference between this row and the stick. d/draw-f32(axis-at(gamepad, i), 2, x, y, 10, rl/darkgray) ;; Drawn and inert. See the header: SetGamepadVibration is a stub in ;; this raylib and is not bound. rl/draw-rectangle-rec(vibrate-rect, rl/skyblue) rl/draw-text("VIBRATE", i32(vibrate-rect.x) + 14, i32(vibrate-rect.y) + 1, 10, rl/darkgray) rl/draw-text("(not bound: raylib stub)", 95, i32(vibrate-rect.y) + 7, 10, rl/gray) ;; -1 when nothing is pressed, which is why the binding answers an ;; i32 and not a GamepadButton. ;; draw-int answers the width it drew, so a branch that ends in one ;; has type i32 while its sibling has type () and the `if` will not ;; typecheck — "expected i32, found ()". Both arms end in a ;; draw-text here, which is the tidy way out; where that is awkward a ;; trailing `()` is the other. let b = rl/get-gamepad-button-pressed() if b >= 0 rl/draw-text("DETECTED BUTTON: ", 10, 430, 10, rl/red) d/draw-int(b, 10 + rl/measure-text("DETECTED BUTTON: ", 10), 430, 10, rl/red) () else rl/draw-text("DETECTED BUTTON: NONE", 10, 430, 10, rl/gray) else rl/draw-text("GP", 10, 10, 10, rl/gray) let x = 10 + rl/measure-text("GP", 10) rl/draw-text(": NOT DETECTED", x + d/draw-int(gamepad, x, 10, 10, rl/gray), 10, 10, rl/gray) rl/draw-text("left/right arrows select another pad", 10, 30, 10, rl/lightgray)