make balatro (steam version) run on grapheneos pixel 8
- Python 48%
- Shell 45.4%
- Lua 6.6%
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.
|
||
|---|---|---|
| tools | ||
| vendor | ||
| .gitignore | ||
| .gitmodules | ||
| build-apk.sh | ||
| README.md | ||
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.ReadKeyexception ("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-allflips two of Balatro's own unlock switches ingame.lua, so everything is available on each launch (overriding saved progress):TESTHELPER_unlocks(all centers/blinds/tags discovered + unlocked) and the profile'sall_unlockedflag (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 packedgame.love; the in-game result was not exercised on a device by this build.--touchadds drag-to-sell (jokers/consumables), drag-to-buy (shop/vouchers/boosters), and drag-to-select (pack cards) by baking the Sticky Fingers mod (pinnedvendor/sticky-fingerssubmodule) 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 shippedgame.loveis self-contained, with no in-game Mods menu and no way to add other mods without rebuilding. The mod's optionalvanilla_joker_sellsettings toggle is dropped (defaults stay on). It is version-fragile: apattern not foundfailure means Balatro or the mod updated — re-pinvendor/sticky-fingers(or pass--touch-mod-dir).--joker-displayoverlays live joker information under each joker: the running+Mult/+Chips/Xmulton 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 (pinnedvendor/JokerDisplaysubmodule) into the APK. Lovely and Steamodded have no Android build, so the mod's Lua tree is bundled intogame.loveunderJokerDisplay/and loaded by a port of its ownif not SMODSstandalone loader (tools/jokerdisplay_baked_boot.lua), withrequire("JokerDisplay.baked_boot")injected at theset_profile_progress()seam ingame.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-pinvendor/JokerDisplay. Composes with--touchand--unlock-all(all edit disjoint parts ofgame.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 callsmod(tc.x, 0.002)andmod(tc.y, 0.0013)returnNaN, which propagates through the shader's final* maskand 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.shrewrites those two calls inresources/shaders/CRT.fsto the mathematically exact GLSL identityb * 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-divisormod()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 packedgame.loveinside the produced APK — theCRT.fsnoise line carries thefractform with nomod(tc.*, …)call left, CRT stays enabled (crt = 70, andgame.luastill appliesG.SHADERS['CRT']) — but the on-device render on Pixel hardware was not exercised here. Pass--disable-crtto 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 intogame.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.