├── balatromobile
├── __version__.py
├── __init__.py
├── artifacts
│ ├── nunito-font.tty
│ ├── apksigner.jar
│ ├── APKEditor-1.3.7.jar
│ ├── uber-debug.keystore
│ ├── zipalign-linux-amd64
│ ├── zipalign-linux-arm64
│ ├── res
│ │ ├── drawable-hdpi
│ │ │ └── love.png
│ │ ├── drawable-mdpi
│ │ │ └── love.png
│ │ ├── drawable-xhdpi
│ │ │ └── love.png
│ │ ├── drawable-xxhdpi
│ │ │ └── love.png
│ │ └── drawable-xxxhdpi
│ │ │ └── love.png
│ ├── zipalign-android-arm64
│ ├── zipalign-darwin-amd64
│ ├── zipalign-darwin-arm64
│ ├── zipalign-windows-amd64
│ ├── zipalign-windows-arm64
│ ├── love-11.5-SAF-android-embed.apk
│ └── AndroidManifest.xml
├── patches
│ ├── max-volume.toml
│ ├── nunito-font.toml
│ ├── external-storage.toml
│ ├── fps.toml
│ ├── landscape-hidpi.toml
│ ├── landscape.toml
│ ├── no-background.toml
│ ├── shaders-flames.toml
│ ├── simple-fx.toml
│ ├── basic.toml
│ ├── no-crt.toml
│ ├── fix-beta-langs.toml
│ ├── square-display.toml
│ └── fps-settings.toml
├── utils.py
├── resources.py
├── patcher.py
└── gamble.py
├── .gitignore
├── misc
├── icon.png
├── logo.xcf
└── video-thumb-1.png
├── .gitattributes
├── pyproject.toml
├── .github
└── workflows
│ └── pypi-publish.yml
├── LICENSE.txt
└── README.md
/balatromobile/__version__.py:
--------------------------------------------------------------------------------
1 | __version__="0.6.4"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | __pycache__/
3 | *.pyc
4 | Balatro-*/
5 | Balatro*.exe
6 | balatro*.apk
7 | dist/
8 | Balatro/
9 |
--------------------------------------------------------------------------------
/balatromobile/__init__.py:
--------------------------------------------------------------------------------
1 | """balatromobile: build your mobile version of Balatro"""
2 | from .__version__ import __version__
--------------------------------------------------------------------------------
/balatromobile/artifacts/nunito-font.tty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antipatico/balatromobile/HEAD/balatromobile/artifacts/nunito-font.tty
--------------------------------------------------------------------------------
/misc/icon.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:a6bf3fe0a51a708ed94d3d4c1d10185e0526531a5d35c4038d8dcadc903927f4
3 | size 22131
4 |
--------------------------------------------------------------------------------
/misc/logo.xcf:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:2c8cce09e4c3f8dc9e88fb82451769577d74f4cc0fe22a39cc47de1eff60c008
3 | size 610426
4 |
--------------------------------------------------------------------------------
/misc/video-thumb-1.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:c7b409598a16995aa870c7e93586e116c4da15b6048749b0710ab1596246c22e
3 | size 37090
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/apksigner.jar:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:3f24d15016f70bfc85fd70fb41a2f882acbc4e09a385ff0ea4161ccfa8fcb3db
3 | size 3869836
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/APKEditor-1.3.7.jar:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:f199893941e87da61fdaee5e30270e326cf828abe147d6216612eba936de3fae
3 | size 6865566
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/uber-debug.keystore:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:161a0018f33277e842534f5443fdd36683359b4950f4dc006c1648895cb073b2
3 | size 2224
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-linux-amd64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:7de4a1ce5cfcc0e6472d2190b4690dc26866c53459dc54d1853fb41a3c9c9214
3 | size 1818776
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-linux-arm64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:922b6b17c8fffda2fc9328e604b4a8575f00d6c051c303f0a4b7eb0710062815
3 | size 1900696
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/res/drawable-hdpi/love.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:54c86d5fbc432e0f631a2ac624894042c1857fc8bf945176a05c96c5901902f4
3 | size 19206
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/res/drawable-mdpi/love.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:54c86d5fbc432e0f631a2ac624894042c1857fc8bf945176a05c96c5901902f4
3 | size 19206
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/res/drawable-xhdpi/love.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:54c86d5fbc432e0f631a2ac624894042c1857fc8bf945176a05c96c5901902f4
3 | size 19206
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-android-arm64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:de5fab73587010f3cba04e3117045eb7468cec925000f0fea1a52305686b486b
3 | size 2031937
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-darwin-amd64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:815bc85fb4c2f46ac3cab68298e18ae7723423a11fb5d81c33e77d213c68be09
3 | size 1863312
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-darwin-arm64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:176c87a0388ae827563f65a124cc282574ae869199eee0943b63590d3ef04e91
3 | size 1857602
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-windows-amd64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:f5305e50ac97ab6436f220cc7479ad405382a4214a557bb5f8ae95a83ffad57f
3 | size 1955328
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/zipalign-windows-arm64:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:9465ddce66e996c3117bb503724ce20b443a253b47b333a69a33980f220ca935
3 | size 1903616
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/res/drawable-xxhdpi/love.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:54c86d5fbc432e0f631a2ac624894042c1857fc8bf945176a05c96c5901902f4
3 | size 19206
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/res/drawable-xxxhdpi/love.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:54c86d5fbc432e0f631a2ac624894042c1857fc8bf945176a05c96c5901902f4
3 | size 19206
4 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/love-11.5-SAF-android-embed.apk:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:485f07086642e72d12dc8f55e6865f8ca85eb20d92b5bc04ba5058edf5011db7
3 | size 7574843
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.apk filter=lfs diff=lfs merge=lfs -text
2 | *.jar filter=lfs diff=lfs merge=lfs -text
3 | *.png filter=lfs diff=lfs merge=lfs -text
4 | *.xcf filter=lfs diff=lfs merge=lfs -text
5 | zipalign-* filter=lfs diff=lfs merge=lfs -text
6 | balatromobile/artifacts/uber-debug.keystore filter=lfs diff=lfs merge=lfs -text
7 |
--------------------------------------------------------------------------------
/balatromobile/patches/max-volume.toml:
--------------------------------------------------------------------------------
1 | description = "Set master volume to 100 by default"
2 | authors = ["SBence"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "globals.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "volume = 50"
12 | patch_content = """
13 | volume = 100,
14 | """
--------------------------------------------------------------------------------
/balatromobile/patches/nunito-font.toml:
--------------------------------------------------------------------------------
1 | description = "Replace the main font used with nunito, optimized for smaller displays. From PortMaster"
2 | authors = ["nkahoang", "rancossack"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "resources/fonts/m6x11plus.ttf"
10 | [[patch_lists.patch_files.patches]]
11 | artifact = "nunito-font.tty"
--------------------------------------------------------------------------------
/balatromobile/patches/external-storage.toml:
--------------------------------------------------------------------------------
1 | description = "Save game files under /sdcard/Android [Works well for Android < 13]"
2 | authors = ["blake502"]
3 | supported_platforms = ["android"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "conf.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "function love.conf(t)"
12 | patch_content = """
13 | function love.conf(t)
14 | t.externalstorage = true
15 | """
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "balatromobile"
3 | authors = [
4 | {name = "antipatico", email = "code@bootkit.dev"},
5 | ]
6 | readme = "README.md"
7 | classifiers = [
8 | "License :: OSI Approved :: MIT License",
9 | ]
10 | requires-python = ">=3.11"
11 | dynamic = ["version", "description"]
12 | dependencies = [
13 | "tabulate"
14 | ]
15 |
16 | [project.scripts]
17 | balatromobile = "balatromobile.gamble:main"
18 |
19 | [build-system]
20 | requires = ["flit_core >=3.2,<4"]
21 | build-backend = "flit_core.buildapi"
--------------------------------------------------------------------------------
/balatromobile/patches/fps.toml:
--------------------------------------------------------------------------------
1 | description = "Cap the FPS limit to the FPS limit of the screen"
2 | authors = ["PGgamer2"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "main.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "G.FPS_CAP = G.FPS_CAP or"
12 | patch_content = """
13 | p_ww, p_hh, p_wflags = love.window.getMode()
14 | G.FPS_CAP = p_wflags['refreshrate']
15 | """
--------------------------------------------------------------------------------
/balatromobile/patches/landscape-hidpi.toml:
--------------------------------------------------------------------------------
1 | description = "Forces the game to always stay in landscape mode and apply hidpi fix for iOS"
2 | authors = ["blake502"]
3 | supported_platforms = ["ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "main.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "local os = love.system.getOS()"
12 | patch_content = """
13 | local os = love.system.getOS()
14 | love.window.setMode(2, 1, {highdpi = true})
15 | """
--------------------------------------------------------------------------------
/balatromobile/patches/landscape.toml:
--------------------------------------------------------------------------------
1 | description = "Forces the game to always stay in landscape mode, ignoring the screeen orentation of the device"
2 | authors = ["blake502"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "main.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "local os = love.system.getOS()"
12 | patch_content = """
13 | local os = love.system.getOS()
14 | love.window.setMode(2, 1)
15 | """
--------------------------------------------------------------------------------
/balatromobile/patches/no-background.toml:
--------------------------------------------------------------------------------
1 | description = "Disable background animations and effects. From PortMaster"
2 | authors = ["nkahoang","rancossack"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 |
9 | [[patch_lists.patch_files]]
10 | target_file = "globals.lua"
11 | [[patch_lists.patch_files.patches]]
12 | search_string = "self.DEBUG = false"
13 | patch_content = """
14 | self.DEBUG = false
15 | self.debug_background_toggle = true
16 | """
17 |
18 | [[patch_lists.patch_files]]
19 | target_file = "game.lua"
20 | [[patch_lists.patch_files.patches]]
21 | search_string = "love.graphics.clear({0,1,0,1})"
22 | patch_content = """
23 | love.graphics.clear(mix_colours(G.C.DYN_UI.DARK, {0.17,0.35,0.26,1}, 0.4))
24 | """
25 |
--------------------------------------------------------------------------------
/balatromobile/utils.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | from os import environ
3 | from pathlib import Path
4 |
5 | DEBUG = (debug := environ.get("BALATROMOBILE_DEBUG")) and (debug.lower() != "false")
6 |
7 | def run_silent(what: list[str], **kwargs):
8 | outpipe = subprocess.DEVNULL
9 |
10 | if DEBUG:
11 | from sys import stderr
12 | print(f"[DEBUG] `run_silent`: {what=}", file=stderr)
13 | outpipe = stderr
14 |
15 | subprocess.run(
16 | what,
17 | stdin=subprocess.DEVNULL,
18 | stdout=outpipe,
19 | stderr=outpipe,
20 | check=True,
21 | **kwargs
22 | )
23 |
24 | def is_java_installed() -> bool:
25 | try:
26 | run_silent(["java", "-version"])
27 | return True
28 | except FileNotFoundError:
29 | return False
30 |
31 | def get_balatro_version(balatro: Path) -> str:
32 | return (balatro / "version.jkr").read_text().splitlines()[0]
33 |
--------------------------------------------------------------------------------
/.github/workflows/pypi-publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package to PyPi
2 |
3 |
4 | on:
5 | push:
6 | branches:
7 | - master # or a pattern like "*" for including all branches
8 | paths:
9 | - balatromobile/__version__.py
10 |
11 | jobs:
12 | deploy:
13 | runs-on: ubuntu-latest
14 | environment:
15 | name: pypi
16 | url: https://pypi.org/p/balatromobile
17 | permissions:
18 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | lfs: true
23 | - name: Set up Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: '3.x'
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install flit
31 | - name: Build package
32 | run: |
33 | flit build
34 | - name: Publish package distributions to PyPI
35 | uses: pypa/gh-action-pypi-publish@release/v1
36 |
--------------------------------------------------------------------------------
/balatromobile/patches/shaders-flames.toml:
--------------------------------------------------------------------------------
1 | description = "Fix the flames shaders for mobile"
2 | authors = ["PGgamer2"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "resources/shaders/flame.fs"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "#define MY_HIGHP_OR_MEDIUMP highp"
12 | patch_content = """
13 | #define MY_HIGHP_OR_MEDIUMP highp
14 | precision highp float;
15 | """
16 | [[patch_lists.patch_files.patches]]
17 | search_string = "#define MY_HIGHP_OR_MEDIUMP mediump"
18 | patch_content = """
19 | #define MY_HIGHP_OR_MEDIUMP mediump
20 | precision mediump float;
21 | """
22 | [[patch_lists.patch_files.patches]]
23 | search_string = "vec4 effect( vec4 colour, Image texture, vec2 texture_coords, vec2 screen_coords )"
24 | patch_content = "mediump vec4 effect( mediump vec4 colour, Image texture, mediump vec2 texture_coords, mediump vec2 screen_coords )"
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 antipatico
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/balatromobile/patches/simple-fx.toml:
--------------------------------------------------------------------------------
1 | description = "Disable gameplay visible behind menu background, shadows, and bloom effects. From PortMaster"
2 | authors = ["nkahoang","rancossack"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 |
9 | [[patch_lists.patch_files]]
10 | target_file = "globals.lua"
11 | [[patch_lists.patch_files.patches]]
12 | search_string = "bloom = 1"
13 | patch_content = """
14 | bloom = 0,
15 | """
16 | [[patch_lists.patch_files.patches]]
17 | search_string = "shadows = 'On'"
18 | patch_content = """
19 | shadows = 'Off',
20 | """
21 | [[patch_lists.patch_files.patches]]
22 | search_string = "self.F_HIDE_BG = false"
23 | patch_content = """
24 | self.F_HIDE_BG = true
25 | """
26 | [[patch_lists.patch_files.patches]]
27 | search_string = "self.TILE_W = self.F_MOBILE_UI and 11.5 or 20"
28 | patch_content = """
29 | self.TILE_W = 20
30 | """
31 | [[patch_lists.patch_files.patches]]
32 | search_string = "self.TILE_H = self.F_MOBILE_UI and 20 or 11.5"
33 | patch_content = """
34 | self.TILE_H = 11.5
35 | """
36 |
--------------------------------------------------------------------------------
/balatromobile/patches/basic.toml:
--------------------------------------------------------------------------------
1 | description = "Basic set of patches needed to make the game run on mobile"
2 | authors = ["blake502", "TheCatRiX", "PGgamer2"]
3 | supported_platforms = ["android"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | supported_game_versions = ["1.0.1o-FULL", "1.0.1n-FULL", "1.0.1m-FULL", "1.0.1g-FULL", "1.0.1f-FULL", "1.0.1e-FULL", "1.0.1c-FULL"]
8 | [[patch_lists.patch_files]]
9 | target_file = "globals.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "loadstring"
12 | patch_content = """
13 | if love.system.getOS() == 'Android' then
14 | self.F_DISCORD = true
15 | self.F_NO_ACHIEVEMENTS = true
16 | self.F_SOUND_THREAD = true
17 | self.F_VIDEO_SETTINGS = false
18 | self.F_QUIT_BUTTON = false
19 | self.F_MOBILE_UI = true
20 | end
21 | """
22 |
23 | [[patch_lists.patch_files]]
24 | target_file = "functions/button_callbacks.lua"
25 | [[patch_lists.patch_files.patches]]
26 | search_string = "G.CONTROLLER.text_input_hook == e and G.CONTROLLER.HID.controller"
27 | patch_content = """
28 | if G.CONTROLLER.text_input_hook == e and (G.CONTROLLER.HID.controller or G.CONTROLLER.HID.touch) then
29 | """
30 |
--------------------------------------------------------------------------------
/balatromobile/patches/no-crt.toml:
--------------------------------------------------------------------------------
1 | description = "Disable CRT effect [Fixes blackscreen bug on Pixels and other devices]"
2 | authors = ["blake502", "SBence"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "globals.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "crt = "
12 | patch_content = """
13 | crt = 0,
14 | """
15 | [[patch_lists.patch_files]]
16 | target_file = "game.lua"
17 | [[patch_lists.patch_files.patches]]
18 | search_string = "G.SHADERS['CRT'])"
19 | # patch_content = ""
20 |
21 | [[patch_lists.patch_files]]
22 | target_file = "functions/UI_definitions.lua"
23 | [[patch_lists.patch_files.patches]]
24 | search_string = "create_slider({label = localize('b_set_CRT'),w = 4, h = 0.4, ref_table = G.SETTINGS.GRAPHICS, ref_value = 'crt', min = 0, max = 100})"
25 | # patch_content = ""
26 |
27 | [[patch_lists.patch_files]]
28 | target_file = "functions/UI_definitions.lua"
29 | [[patch_lists.patch_files.patches]]
30 | search_string = "create_option_cycle({w = 4,scale = 0.8, label = localize(\"b_set_CRT_bloom\"),options = localize('ml_bloom_opt'), opt_callback = 'change_crt_bloom', current_option = G.SETTINGS.GRAPHICS.bloom})"
31 | # patch_content = ""
--------------------------------------------------------------------------------
/balatromobile/patches/fix-beta-langs.toml:
--------------------------------------------------------------------------------
1 | description = "Make beta langs selectable on mobile"
2 | authors = ["SBence", "antipatico"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = all NOT TESTED
8 |
9 | [[patch_lists.patch_files]]
10 | target_file = "functions/button_callbacks.lua"
11 | [[patch_lists.patch_files.patches]]
12 | search_string = "if (_infotip_object.config.set ~= e.config.ref_table.label) and (not G.F_NO_ACHIEVEMENTS) then"
13 | patch_content = "if (_infotip_object.config.set ~= e.config.ref_table.label) then"
14 |
15 | ### Alternative patch (removes alert):
16 |
17 | # target_file = "functions/UI_definitions.lua"
18 | # [[patch_lists.patch_files.patches]]
19 | # search_string = """_row[#_row+1] = {n=G.UIT.C, config={align = "cm", func = 'beta_lang_alert', padding = 0.05, r = 0.1, minh = 0.7, minw = 4.5, button = v.beta and 'warn_lang' or 'change_lang', ref_table = v, colour = v.beta and G.C.RED or G.C.BLUE, hover = true, shadow = true, focus_args = {snap_to = (k == 1)}}, nodes={"""
20 | # patch_content = """
21 | # _row[#_row+1] = {n=G.UIT.C, config={align = "cm", func = 'beta_lang_alert', padding = 0.05, r = 0.1, minh = 0.7, minw = 4.5, button = 'change_lang', ref_table = v, colour = v.beta and G.C.RED or G.C.BLUE, hover = true, shadow = true, focus_args = {snap_to = (k == 1)}}, nodes={
22 | # """
--------------------------------------------------------------------------------
/balatromobile/patches/square-display.toml:
--------------------------------------------------------------------------------
1 | description = "Optimize for square and square-like displays. From PortMaster"
2 | authors = ["nkahoang", "rancossack"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | # supported_game_versions = ALL
8 | [[patch_lists.patch_files]]
9 | target_file = "functions/common_events.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "bloom = 1"
12 | patch_content = """"
13 | bloom = 0
14 | """
15 |
16 | # move the hands a bit to the right
17 | [[patch_lists.patch_files.patches]]
18 | search_string = "G.hand.T.x = G.TILE_W - G.hand.T.w - 2.85"
19 | patch_content = """
20 | G.hand.T.x = G.TILE_W - G.hand.T.w - 1
21 | """
22 |
23 | # then move the playing area up
24 | [[patch_lists.patch_files.patches]]
25 | search_string = "G.play.T.y = G.hand.T.y - 3.6"
26 | patch_content = """
27 | G.play.T.y = G.hand.T.y - 4.5
28 | """
29 |
30 | # move the decks to the right
31 | [[patch_lists.patch_files.patches]]
32 | search_string = "G.deck.T.x = G.TILE_W - G.deck.T.w - 0.5"
33 | patch_content = """
34 | G.deck.T.x = G.TILE_W - G.deck.T.w + 0.85
35 | """
36 |
37 | # move the jokers to the left
38 | [[patch_lists.patch_files.patches]]
39 | search_string = "G.jokers.T.x = G.hand.T.x - 0.1"
40 | patch_content = """
41 | G.jokers.T.x = G.hand.T.x - 0.2
42 | """
43 |
--------------------------------------------------------------------------------
/balatromobile/resources.py:
--------------------------------------------------------------------------------
1 | from argparse import Namespace
2 | import importlib.resources
3 | import platform
4 | from pathlib import Path
5 |
6 | def get_resorce(basepath: str | Path, name: str | Path):
7 | with importlib.resources.as_file(importlib.resources.files(__package__)) as f:
8 | res = f / basepath / name
9 | if name is None or not res.exists():
10 | raise Exception(f'Missing resource: "{name}" in "{res.absolute()}"')
11 | return res
12 |
13 | def get_artifact(name: str | Path) -> Path:
14 | return get_resorce("artifacts", name)
15 |
16 | def get_patch(name: str | Path) -> Path:
17 | return get_resorce("patches", name)
18 |
19 | def list_patches() -> list[str]:
20 | with importlib.resources.as_file(importlib.resources.files(__package__)) as f:
21 | return [f.stem for f in (f / "patches").glob("**/*.toml")]
22 |
23 | def all_artifacts():
24 | os = platform.system().lower()
25 | arch = ({
26 | "x86_64": "amd64",
27 | "amd64": "amd64",
28 | "aarch64": "arm64",
29 | "arm64": "arm64",
30 | })[platform.machine().lower()]
31 | return Namespace(
32 | apk_editor = get_artifact("APKEditor-1.3.7.jar"),
33 | love_apk = get_artifact("love-11.5-SAF-android-embed.apk"),
34 | android_manifest = get_artifact("AndroidManifest.xml"),
35 | android_res = get_artifact("res"),
36 | zipalign = get_artifact(f"zipalign-{os}-{arch}"),
37 | uber_keystore = get_artifact("uber-debug.keystore"),
38 | apksigner = get_artifact("apksigner.jar"),
39 | )
40 |
--------------------------------------------------------------------------------
/balatromobile/patches/fps-settings.toml:
--------------------------------------------------------------------------------
1 | description = "Adds an FPS limit option to the graphics settings menu"
2 | authors = ["janw4ld"]
3 | supported_platforms = ["android", "ios"]
4 |
5 | [[patch_lists]]
6 | version = 0
7 | supported_game_versions = ["1.0.1o-FULL", "1.0.1n-FULL"]
8 | [[patch_lists.patch_files]]
9 | target_file = "functions/UI_definitions.lua"
10 | [[patch_lists.patch_files.patches]]
11 | search_string = "G.UIDEF = {}"
12 | patch_content = """
13 | G.UIDEF = {}
14 | local _, _, __balatromobile_fps_cap_p_wflags = love.window.getMode()
15 | local __balatromobile_fps_cap_options = {
16 | labels = {"Monitor", 30, 60, 90, 120, 144, 240, "Max"},
17 | reverse_lookup = {
18 | ["Monitor"] = 1,
19 | [30] = 2,
20 | [60] = 3,
21 | [90] = 4,
22 | [120] = 5,
23 | [144] = 6,
24 | [240] = 7,
25 | ["Max"] = 8,
26 | },
27 | values = {
28 | ["Monitor"] = __balatromobile_fps_cap_p_wflags.refreshrate,
29 | [30] = 30,
30 | [60] = 60,
31 | [90] = 90,
32 | [120] = 120,
33 | [144] = 144,
34 | [240] = 240,
35 | ["Max"] = 500,
36 | },
37 | }
38 | G.FUNCS.__balatromobile_fps_cap_callback = function(option)
39 | G.SETTINGS.__balatromobile_FPS_CAP = option.to_val
40 | G.FPS_CAP = __balatromobile_fps_cap_options.values[option.to_val]
41 | end
42 | G.FPS_CAP = G.FUNCS.__balatromobile_fps_cap_callback {
43 | to_val = G.SETTINGS.__balatromobile_FPS_CAP or "Monitor"
44 | }
45 | """
46 | [[patch_lists.patch_files.patches]]
47 | search_string = "create_option_cycle({w = 4,scale = 0.8, label = localize(\"b_set_CRT_bloom\"),options = localize('ml_bloom_opt'), opt_callback = 'change_crt_bloom', current_option = G.SETTINGS.GRAPHICS.bloom}),"
48 | patch_content = """
49 | create_option_cycle({w = 4,scale = 0.8, label = localize("b_set_CRT_bloom"),options = localize('ml_bloom_opt'), opt_callback = 'change_crt_bloom', current_option = G.SETTINGS.GRAPHICS.bloom}),
50 | {n = G.UIT.R, config = {align = "cm", r = 0}, nodes = {create_option_cycle{
51 | label = "FPS Limit",
52 | w = 4,
53 | scale = 0.8,
54 | options = __balatromobile_fps_cap_options.labels,
55 | opt_callback = '__balatromobile_fps_cap_callback',
56 | current_option =
57 | __balatromobile_fps_cap_options.reverse_lookup[G.SETTINGS.__balatromobile_FPS_CAP],
58 | }}},
59 | """
60 |
--------------------------------------------------------------------------------
/balatromobile/artifacts/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/balatromobile/patcher.py:
--------------------------------------------------------------------------------
1 | import tomllib
2 | from pathlib import Path
3 | from .resources import get_patch, get_artifact, list_patches
4 |
5 | DEFAULT_PATCHES = "basic,landscape,no-crt,fps,external-storage,shaders-flames,fix-beta-langs,max-volume"
6 |
7 |
8 | class Patch:
9 | def __init__(self, patch: dict):
10 | self.search_string = patch.get("search_string", None)
11 | self.content = patch.get("patch_content", "")
12 | artifact_name = patch.get("artifact", None)
13 | self.artifact = get_artifact(artifact_name) if artifact_name is not None else None
14 | if self.search_string == self.artifact == None:
15 | raise Exception(f"Empty patches are not allowed")
16 |
17 | def apply(self, target_file: Path):
18 | target = target_file
19 | if self.artifact is not None:
20 | target.write_bytes(self.artifact.read_bytes())
21 | return
22 | patched = "\n".join([l if self.search_string not in l else self.content for l in target.read_text(encoding="utf-8").splitlines()])
23 | target.write_text(patched, encoding="utf-8")
24 |
25 |
26 | class PatchFile:
27 | def __init__(self, patch_file: dict):
28 | self.target_file = Path(patch_file["target_file"])
29 | self.patches = [Patch(p) for p in patch_file["patches"]]
30 |
31 | def apply_all(self, balatro: Path):
32 | [p.apply(balatro / self.target_file) for p in self.patches]
33 |
34 |
35 | class PatchList:
36 | def __init__(self, patch_list: dict):
37 | # Must be unique across patch lists to differentiate them
38 | self.version = patch_list['version']
39 | # If not defined, skip version checking
40 | self.supported_game_versions = patch_list.get('supported_game_versions', None)
41 | self.patch_files = [PatchFile(p) for p in patch_list["patch_files"]]
42 |
43 | def is_compatible(self, version: str):
44 | return self.supported_game_versions is None or version in self.supported_game_versions
45 |
46 | def apply_all(self, balatro: Path):
47 | [p.apply_all(balatro) for p in self.patch_files]
48 |
49 | def __lt__(self, other):
50 | return self.version < other.version
51 |
52 |
53 | class VersionedPatch:
54 | def __init__(self, name: str):
55 | self.path = get_patch(f"{name}.toml")
56 | with open(self.path,"rb") as f:
57 | toml = tomllib.load(f)
58 | self.name : str = name
59 | self.description : str = toml["description"]
60 | self.authors : list = toml["authors"]
61 | self.supported_platforms : list = toml["supported_platforms"]
62 | self.patch_lists = sorted([PatchList(p) for p in toml['patch_lists']], reverse=True)
63 |
64 | def supports_android(self) -> bool:
65 | return "android" in self.supported_platforms
66 |
67 | def supports_ios(self) -> bool:
68 | return "ios" in self.supported_platforms
69 |
70 | def __str__(self) -> str:
71 | return f'VersionedPatch(name="{self.name}", description="{self.description}", supported_platforms=[{",".join(self.supported_platforms)}])'
72 |
73 | def __repr__(self) -> str:
74 | return str(self)
75 |
76 | def __lt__(self, other) -> str:
77 | return self.name < other.name
78 |
79 | def apply(self, balatro: Path, version: str, force: bool = False) -> int:
80 | # TODO: allow user to specify specific patch version
81 | for p in self.patch_lists:
82 | if p.is_compatible(version):
83 | p.apply_all(balatro)
84 | return p.version
85 | if force:
86 | p = self.patch_lists[0]
87 | print(f'WARNING: applying incompatible patch of "{self.name}" patch, selected version "{p.version}"')
88 | p.apply_all(balatro)
89 | return p.version
90 | raise Exception(f'Cannot find any compatible Patch version of "{self.name}" for given Balatro.exe having game version "{version}"')
91 |
92 |
93 | def all_patches() -> list[VersionedPatch]:
94 | return sorted([VersionedPatch(p) for p in list_patches()])
95 |
96 |
97 | def select_patches(patches: str) -> list[VersionedPatch]:
98 | desired_patches = patches.split(",")
99 | patch_files : list[VersionedPatch] = list(filter(lambda p: p.name in desired_patches, all_patches()))
100 | if len(desired_patches) != len(patch_files):
101 | missing_patches = [p for p in desired_patches if p not in [P.name for P in patch_files]]
102 | raise Exception(f'One or more patches not found: {",".join(missing_patches)}')
103 | return patch_files
--------------------------------------------------------------------------------
/balatromobile/gamble.py:
--------------------------------------------------------------------------------
1 | from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter
2 | from pathlib import Path
3 | from tempfile import TemporaryDirectory
4 | import sys
5 | import shutil
6 | from zipfile import ZipFile
7 | from tabulate import tabulate
8 |
9 | from .resources import all_artifacts
10 | from .patcher import all_patches, select_patches, DEFAULT_PATCHES
11 | from .utils import get_balatro_version, is_java_installed, run_silent
12 | from .__version__ import __version__
13 |
14 |
15 | def main():
16 | #TODO: iOS
17 | args = parse_args()
18 | if args.command == "android":
19 | android(args)
20 | elif args.command == "list-patches":
21 | list_patches(args)
22 |
23 | def android(args: Namespace):
24 | balatro_exe = Path(args.BALATRO_EXE)
25 | artifacts = all_artifacts()
26 | patches = select_patches(args.patches)
27 | if sys.version_info.major == 3 and sys.version_info.minor < 11:
28 | print("WARNING: Python version < 3.11 is not tested and may not be supported")
29 | if not is_java_installed():
30 | print("ERROR: Java is not installed. Install Java-JRE before running this script")
31 | sys.exit(1)
32 | if not balatro_exe.is_file():
33 | print("ERROR: invalid Balatro.exe")
34 | sys.exit(1)
35 | with TemporaryDirectory() as d:
36 | balatro = Path(d) / "Balatro"
37 | with ZipFile(balatro_exe, "r") as z:
38 | z.extractall(balatro)
39 | balatro_version = get_balatro_version(balatro)
40 | for patch in patches:
41 | patch.apply(balatro, balatro_version, args.force)
42 | app = Path(d) / "balatro_app"
43 | run_silent(["java", "-jar", artifacts.apk_editor.absolute(), "d", "-i", artifacts.love_apk.absolute(), "-o", app.absolute()])
44 | manifest_tpl = artifacts.android_manifest.read_text()
45 | manifest = manifest_tpl.format(package=args.package_name, version=balatro_version, label=args.display_name)
46 | (app / "AndroidManifest.xml").write_text(manifest)
47 | shutil.copytree(artifacts.android_res, app / "resources" / "package_1" / "res", dirs_exist_ok=True)
48 | shutil.make_archive((Path(d) / "game.love").absolute(), "zip", balatro.absolute())
49 | shutil.move(Path(d) / "game.love.zip", (app / "root" / "assets" / "game.love"))
50 | apk = Path(d) / "balatro.apk"
51 | run_silent(["java", "-jar", artifacts.apk_editor.absolute(), "b", "-i", app.absolute(), "-o", apk.absolute()])
52 | output_apk = Path(args.output) if args.output else Path(f"balatro-{balatro_version}.apk")
53 | if args.skip_sign:
54 | shutil.move(apk.absolute(), output_apk.absolute())
55 | else:
56 | zipaligned_apk = Path(d) / "balatro-aligned.apk"
57 | run_silent([artifacts.zipalign.absolute(), "-i", apk.absolute(), "-o", zipaligned_apk.absolute()])
58 | signed_apk = Path(d) / "balatro-aligned-debugSigned.apk"
59 | run_silent([
60 | "java", "-jar", artifacts.apksigner.absolute(), "sign",
61 | "--ks-key-alias", "androiddebugkey",
62 | "--ks", artifacts.uber_keystore.absolute(),
63 | "--ks-pass", "pass:android",
64 | "--out", signed_apk.absolute(),
65 | zipaligned_apk.absolute()
66 | ])
67 | shutil.move(signed_apk, output_apk)
68 |
69 | def list_patches(args: Namespace):
70 | print(tabulate(
71 | headers=["Name","Platforms", "Description", "Authors"],
72 | tabular_data=[[p.name, ",".join(p.supported_platforms), p.description, ",".join(p.authors)] for p in all_patches()]
73 | ))
74 | pass
75 |
76 | def parse_args() -> Namespace:
77 | parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
78 | parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
79 | commands = parser.add_subparsers(title='Commands', dest='command', required=True)
80 | # android
81 | android = commands.add_parser('android', help='Create an Android APK file')
82 | android.add_argument("BALATRO_EXE", help="Path to Balatro.exe file")
83 | android.add_argument("--output", "-o", required=False, help="Output path for apk (default: balatro-GAME_VERSION.apk)")
84 | android.add_argument("--patches", "-p", default=DEFAULT_PATCHES, help="Comma-separated list of patches to apply (default: %(default)s)")
85 | android.add_argument("--skip-sign", "-s", action="store_true", help="Skip signing the apk file with Uber Apk Signer (default: %(default)s)")
86 | android.add_argument("--display-name", default="Balatro Mobile (unofficial)", help="Change application display name (default: %(default)s)")
87 | android.add_argument("--package-name", default="dev.bootkit.balatro", help="Change application package name (default: %(default)s)")
88 | android.add_argument("--force", "-f", action="store_true", help="Force apply patches even if not compatible with supplied Balatro.exe version (default: %(default)s)")
89 | # list-patches
90 | android = commands.add_parser('list-patches', help='List available patches')
91 | return parser.parse_args()
92 |
93 | if __name__=="__main__":
94 | main()
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Balatro Mobile
2 |
3 | Create a mobile version of Balatro from the Windows base version of the game.
4 |
5 | Python rewrite of [balatro-apk-maker](https://github.com/blake502/balatro-apk-maker) by [blake502](https://github.com/blake502) and friends. Compared to the original one, it is *NIX friendly, more modular, has patch versioning and does not try to download and install tools from the internet.
6 |
7 | As of today, it only supports Android. Extending it to iOS should be trivial, if anyone is interested feel free to drop a PR!
8 |
9 |
10 | ## Prerequisites
11 | This script comes pre-bundled with all the tools needed to make it work. The following list of programs must be installed to make the script work:
12 | * [Java-JRE](https://www.java.com/en/download/manual.jsp)
13 | * [Python >= 3.11](https://www.python.org/)
14 |
15 | Moreover, you will need a copy of the game (buy it on [Steam](https://store.steampowered.com/app/2379780/Balatro/))
16 |
17 | ## Installation
18 | Open a terminal and run
19 | ```bash
20 | python3 -m pip install balatromobile
21 | ```
22 |
23 | ## Usage
24 | ```bash
25 | balatromobile android Balatro.exe
26 | ```
27 | This command will output an APK that is ready to be installed on your Android device.
28 |
29 | ## Save files
30 | If your device is running Android 12 or prior, you will find your save files in your sdcard, more specifically under:
31 | ```
32 | /sdcard/Android/data/dev.bootkit.balatro/
33 | ```
34 |
35 | If your device is running Android 13 or later, you cannot access directly your files from your computer. Fortunately, you can use applications which supports the Storage Access Framework API (such as [FX File Explorer](https://play.google.com/store/apps/details?id=nextapp.fx) and [Material Files](https://play.google.com/store/apps/details?id=me.zhanghai.android.files)) to access the files from your device:
36 |
37 | [](https://vimeo.com/939997099 "Click to Watch!")
38 |
39 | If you disable the `external-storage` patch, you can browse the game files using `run-as` in `adb`, for example:
40 | ```bash
41 | adb shell run-as dev.bootkit.balatro ls
42 | ```
43 | This is finnicky and error prone and not reccomended.
44 |
45 | ## Patches
46 | Every patch is versioned, allowing the upkeeping of different patches for different versions of the game.
47 | As of today, the platform check is disabled (since only android is supported anyway).
48 | You can force the patching of unsupported game versions by supplying the `--force` flag.
49 |
50 | You can list the available patches using the `list-patches` command:
51 |
52 | ```console
53 | $ balatromobile list-patches
54 | Name Platforms Description Authors
55 | ---------------- ----------- ----------------------------------------------------------------------------------------------- ---------------------------
56 | basic android Basic set of patches needed to make the game run on mobile blake502,TheCatRiX,PGgamer2
57 | external-storage android Save game files under /sdcard/Android [Works well for Android < 13] blake502
58 | fix-beta-langs android,ios Make beta langs selectable on mobile SBence,antipatico
59 | fps android,ios Cap the FPS limit to the FPS limit of the screen PGgamer2
60 | fps-settings android,ios Adds an FPS limit option to the graphics settings menu janw4ld
61 | landscape android,ios Forces the game to always stay in landscape mode, ignoring the screeen orentation of the device blake502
62 | landscape-hidpi ios Forces the game to always stay in landscape mode and apply hidpi fix for iOS blake502
63 | max-volume android,ios Set master volume to 100 by default SBence
64 | no-background android,ios Disable background animations and effects. From PortMaster nkahoang,rancossack
65 | no-crt android,ios Disable CRT effect [Fixes blackscreen bug on Pixels and other devices] blake502,SBence
66 | nunito-font android,ios Replace the main font used with nunito, optimized for smaller displays. From PortMaster nkahoang,rancossack
67 | shaders-flames android,ios Fix the flames shaders for mobile PGgamer2
68 | simple-fx android,ios Disable gameplay visible behind menu background, shadows, and bloom effects. From PortMaster nkahoang,rancossack
69 | square-display android,ios Optimize for square and square-like displays. From PortMaster nkahoang,rancossack
70 | ```
71 |
72 | It is possible to specify the list of patches you want to apply by supplying a comma-separated list of patches, for example:
73 | ```bash
74 | balatromobile android Balatro.exe --patches basic,landscape,external-storage
75 | ```
76 |
77 | ## Supported Game Versions
78 | * `1.0.1o-FULL`
79 | * `1.0.1n-FULL`
80 | * `1.0.1m-FULL`
81 | * `1.0.1g-FULL`
82 | * `1.0.1f-FULL`
83 | * `1.0.1e-FULL` (public beta)
84 | * `1.0.1c-FULL` (public beta)
85 |
86 | ## Advanced Usage
87 | ```
88 | $ balatromobile android -h
89 | usage: balatromobile android [-h] [--output OUTPUT] [--patches PATCHES] [--skip-sign] [--display-name DISPLAY_NAME] [--package-name PACKAGE_NAME] [--force]
90 | BALATRO_EXE
91 |
92 | positional arguments:
93 | BALATRO_EXE Path to Balatro.exe file
94 |
95 | options:
96 | -h, --help show this help message and exit
97 | --output OUTPUT, -o OUTPUT
98 | Output path for apk (default: balatro-GAME_VERSION.apk)
99 | --patches PATCHES, -p PATCHES
100 | Comma-separated list of patches to apply (default: basic,landscape,no-crt,fps,external-storage,shaders-flames,fix-beta-langs,
101 | max-volume)
102 | --skip-sign, -s Skip signing the apk file with Uber Apk Signer (default: False)
103 | --display-name DISPLAY_NAME
104 | Change application display name (default: Balatro Mobile (unofficial))
105 | --package-name PACKAGE_NAME
106 | Change application package name (default: dev.bootkit.balatro)
107 | --force, -f Force apply patches even if not compatible with supplied Balatro.exe version (default: False)
108 | ```
109 |
110 | ## Credits
111 | This software is a rewrite of [balatro-apk-maker](https://github.com/blake502/balatro-apk-maker). It uses [APKEditor](https://github.com/REAndroid/APKEditor), [Love Android](https://github.com/love2d/love-android) and [Nunito Font](https://fonts.google.com/specimen/Nunito). Moreover, some patches were ported from [nkaHong's fork of PortMaster](https://github.com/nkahoang/PortMaster-nkaHoang).
112 |
113 | Thanks for everybody contributing to this project.
114 |
--------------------------------------------------------------------------------