make balatro (steam version) run on grapheneos pixel 8
  • Python 48%
  • Shell 45.4%
  • Lua 6.6%
Find a file
Fabian Missbrenner 59587b2e8e build-apk.sh: add --joker-display (bake JokerDisplay overlay, no runtime loader)
Adds tools/jokerdisplay_bake.py, a build-time bundler for the JokerDisplay
overlay, and a --joker-display flag that bakes the mod into the APK. Lovely
and Steamodded have no Android build, so instead of a runtime loader the
mod's Lua tree (src/, definitions/, localization/, config.lua) is bundled
into the staged game.love under JokerDisplay/ and loaded by a Steamodded-free
port of its own standalone loader (tools/jokerdisplay_baked_boot.lua). The
baker injects require("JokerDisplay.baked_boot") immediately before the
bare-line set_profile_progress() anchor in game.lua (exact trimmed match,
match_indent parity) and fails loudly when the anchor is absent (a
version-drift signal). The overlay shows live mult/chips/xmult on scaling
jokers and the active suit or rank on suit- and rank-dependent jokers.

JokerDisplay is bakeable (unlike a content framework such as Pokermon)
because it is Lovely-only-capable: its lovely.lua is wholly gated behind
'if not SMODS' and every SMODS reference in its source is nil-guarded with a
vanilla fallback, so no neutralization is needed. The in-game config tab is
not baked (it returns nil without SMODS); overlay defaults are used and
tap-to-collapse still works.

Both inspirational mods are now pinned git submodules under vendor/:
sticky-fingers @ 4a73d33 and JokerDisplay @ f3f3f98 (v1.10.6). --touch is
rewired to bake from the vendor/sticky-fingers submodule rather than a
build-time clone (single-source commit lock); --touch-ref and the
TOUCH_MOD_URL/TOUCH_REF variables are removed, while --touch-mod-dir remains
as the advanced local-checkout override. --joker-display composes with
--touch and --unlock-all (all edit disjoint parts of game.love).

Verified: --joker-display and --joker-display --touch --unlock-all both
produce a signed dist/balatro.apk whose shipped assets/game.love carries all
26 JokerDisplay/* files and exactly one require at the set_profile_progress()
seam; the composition build also carries the unlock-all switches, the touch
neutralization, and the CRT fract fix, all on disjoint lines. The bootstrap
and every bundled mod file parse under luajit. The overlay was confirmed
working on a device.
2026-06-20 11:07:40 +02:00
tools build-apk.sh: add --joker-display (bake JokerDisplay overlay, no runtime loader) 2026-06-20 11:07:40 +02:00
vendor build-apk.sh: add --joker-display (bake JokerDisplay overlay, no runtime loader) 2026-06-20 11:07:40 +02:00
.gitignore Add macOS Apple Silicon build helper (build-apk.sh) + ignore build/ and dist/ 2026-06-17 21:25:18 +02:00
.gitmodules build-apk.sh: add --joker-display (bake JokerDisplay overlay, no runtime loader) 2026-06-20 11:07:40 +02:00
build-apk.sh build-apk.sh: add --joker-display (bake JokerDisplay overlay, no runtime loader) 2026-06-20 11:07:40 +02:00
README.md build-apk.sh: add --joker-display (bake JokerDisplay overlay, no runtime loader) 2026-06-20 11:07:40 +02:00

balatro → APK (macOS, Apple Silicon)

Build a sideloadable Balatro APK from your own Steam copy and install it on Android (including GrapheneOS, which the Play Store gates out). Wraps blake502/balatro-mobile-maker (pinned as a submodule) and drives it non-interactively with a GrapheneOS/Pixel-friendly patch recipe.

TL;DR

git clone --recurse-submodules <this-repo> balatro && cd balatro
brew install dotnet@8        # one-time; the script finds it even though it is keg-only
./build-apk.sh               # -> dist/balatro.apk

Then copy dist/balatro.apk to the phone and open it in Files (allow the source when prompted), or:

adb install dist/balatro.apk   # USB debugging on, phone unlocked

Requirements

  • macOS on Apple Silicon.
  • .NET 8 SDK: brew install dotnet@8.
  • Rosetta (apktool's aapt): softwareupdate --install-rosetta --agree-to-license.
  • A legitimate Steam copy of Balatro. Network is required on the first run (the maker downloads its own JDK, apktool, signer, and LÖVE runtime into build/).

Options

--game PATH      Balatro.love, Balatro.exe, Balatro.app, or the game folder (default: auto-locate Steam install)
--fps VALUE      integer (default 60), "refresh" (device rate), or "off"
--no-landscape   skip the landscape patch
--no-highdpi     skip the high-DPI patch
--disable-crt    turn the CRT shader off (default: keep it on + Pixel GPU noise fix)
--unlock-all     unlock all decks/jokers/vouchers/editions/etc. + all stakes + challenges
--touch          bake Sticky Fingers drag-touch into the APK from the pinned vendor/sticky-fingers submodule (build-time Lovely resolution, no Steamodded)
--touch-mod-dir P  use a local Sticky Fingers checkout at P instead of the submodule (advanced)
--joker-display  bake the JokerDisplay overlay (live joker mult/chips/xmult + active suit/rank); bundled into game.love with a Steamodded-free bootstrap, no runtime loader
--joker-display-mod-dir P  use a local JokerDisplay checkout at P instead of the submodule (advanced)
--verbose        verbose maker + build output
--clean          wipe build/ first (forces fresh tool downloads)
-h, --help

Output is dist/balatro.apk: signed, embeds game.love, ships arm64-v8a + armeabi-v7a.

Notes

  • macOS build is APK-only. The maker's auto-install and save-transfer paths are Windows-only, so installation and save copying are manual here. Steam Cloud is unavailable and Steam achievements are disabled on mobile.
  • A Console.ReadKey exception ("Abort trap: 6") prints at the very end. It is expected under piped input and harmless; success is determined by the produced APK, not the exit code.
  • If the log shows Unable to find patch location, Balatro has changed since the pinned maker release and a newer maker is needed.
  • Update the maker: git submodule update --remote vendor/balatro-mobile-maker, then commit the moved pointer.
  • --unlock-all flips two of Balatro's own unlock switches in game.lua, so everything is available on each launch (overriding saved progress): TESTHELPER_unlocks (all centers/blinds/tags discovered + unlocked) and the profile's all_unlocked flag (all 8 stakes selectable on every deck, plus challenges). It is baked into the APK; reinstall to apply or remove it. Both edits were verified present and syntactically clean in the packed game.love; the in-game result was not exercised on a device by this build.
  • --touch adds drag-to-sell (jokers/consumables), drag-to-buy (shop/vouchers/boosters), and drag-to-select (pack cards) by baking the Sticky Fingers mod (pinned vendor/sticky-fingers submodule) into the APK. Lovely and Steamodded have no Android build, so instead of a runtime loader the mod's Lovely patches are resolved against the game source at build time (tools/lovely_bake.py) and the one hard Steamodded dependency is neutralized — the shipped game.love is self-contained, with no in-game Mods menu and no way to add other mods without rebuilding. The mod's optional vanilla_joker_sell settings toggle is dropped (defaults stay on). It is version-fragile: a pattern not found failure means Balatro or the mod updated — re-pin vendor/sticky-fingers (or pass --touch-mod-dir).
  • --joker-display overlays live joker information under each joker: the running +Mult/+Chips/Xmult on scaling jokers (Green Joker, Ride the Bus, and similar) and the active suit or rank on suit- and rank-dependent jokers (Ancient Joker, The Idol, and similar) by baking the JokerDisplay mod (pinned vendor/JokerDisplay submodule) into the APK. Lovely and Steamodded have no Android build, so the mod's Lua tree is bundled into game.love under JokerDisplay/ and loaded by a port of its own if not SMODS standalone loader (tools/jokerdisplay_baked_boot.lua), with require("JokerDisplay.baked_boot") injected at the set_profile_progress() seam in game.lua (tools/jokerdisplay_bake.py). It covers vanilla jokers only and ships no in-game config tab: the overlay defaults are used, and tapping a display to collapse or expand it still works. It is version-fragile: an anchor-not-found failure means Balatro has drifted from the pinned mod — re-pin vendor/JokerDisplay. Composes with --touch and --unlock-all (all edit disjoint parts of game.love).
  • The CRT shader is kept on by default with a Pixel GPU fix. On Pixel/Tensor (and some other) devices the stock shader's noise term trips a driver bug in the GLSL mod() builtin: at reduced mobile precision the two tiny-divisor calls mod(tc.x, 0.002) and mod(tc.y, 0.0013) return NaN, which propagates through the shader's final * mask and paints part of the screen as a black void (desaturating the rest) — which is why the upstream maker simply disables CRT there. Instead, build-apk.sh rewrites those two calls in resources/shaders/CRT.fs to the mathematically exact GLSL identity b * fract(a / b), which Pixel evaluates correctly. The substitution is exact in real arithmetic; the noise is a chaotic per-pixel hash, so individual grain pixels differ only at the floating-point-rounding level (as they already do between any two GPUs), but the grain is visually identical and the full effect renders everywhere rather than being removed. This exact fix was verified on a Pixel 9 by the balatro-mobile-maker community (PR #212, @Neulus); the larger-divisor mod() calls later in the hash do not trigger the bug and are left unchanged. This build verified end-to-end that the fix lands in the packed game.love inside the produced APK — the CRT.fs noise line carries the fract form with no mod(tc.*, …) call left, CRT stays enabled (crt = 70, and game.lua still applies G.SHADERS['CRT']) — but the on-device render on Pixel hardware was not exercised here. Pass --disable-crt to turn the effect off entirely instead.

Layout

  • build-apk.sh — the driver.
  • tools/lovely_bake.py — build-time Lovely (pattern+copy) patch resolver used by --touch.
  • tools/jokerdisplay_bake.py — build-time JokerDisplay bundler used by --joker-display.
  • tools/jokerdisplay_baked_boot.lua — Steamodded-free JokerDisplay bootstrap baked into game.love.
  • vendor/balatro-mobile-maker — upstream maker (submodule, pinned).
  • vendor/sticky-fingers — Sticky Fingers touch mod (submodule, pinned).
  • vendor/JokerDisplay — JokerDisplay overlay mod (submodule, pinned).
  • build/, dist/ — generated, git-ignored.