├── 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 | [![Video tut](misc/video-thumb-1.png)](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 | --------------------------------------------------------------------------------