Android: initial working version

This commit is contained in:
Berserker
2026-03-26 07:58:11 +01:00
parent fb45a2f87e
commit 07e6be7c93
15 changed files with 87 additions and 24 deletions

View File

@@ -650,7 +650,7 @@ if __name__ == '__main__':
import atexit
confirmation = atexit.register(input, "Press enter to close.")
erargs, seed = main()
from Main import main as ERmain
from generate_lib import main as ERmain
multiworld = ERmain(erargs, seed)
if __debug__:
import gc

View File

@@ -30,7 +30,7 @@ if __name__ == "__main__":
import settings
import Utils
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
user_path)
user_path, is_mobile)
if __name__ == "__main__":
init_logging('Launcher')
@@ -495,8 +495,7 @@ def main(args: argparse.Namespace | dict | None = None):
elif not args["update_settings"]:
run_gui(args.get("launch_components", None), args.get("args", ()))
if __name__ == '__main__':
def run():
multiprocessing.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
parser = argparse.ArgumentParser(
@@ -519,3 +518,22 @@ if __name__ == '__main__':
# we await all child processes to close before we tear down the process host
# this makes it feel like each one is its own program, as the Launcher is closed now
process.join()
if is_mobile:
allowed_names = {
"Launcher",
"Text Client",
"APQuest Client",
"Archipelago Website",
"Discord Server",
"Unrated/18+ Discord Server",
}
components[:] = [c for c in components if c.display_name in allowed_names]
logging.info(f"Loaded {len(components)} components.")
if __name__ == '__main__':
run()

View File

@@ -20,7 +20,8 @@ elif not (3, 11, 0) <= sys.version_info < (3, 14, 0):
_skip_update = bool(
getattr(sys, "frozen", False) or
multiprocessing.parent_process() or
os.environ.get("SKIP_REQUIREMENTS_UPDATE", "").lower() in ("1", "true", "yes")
os.environ.get("SKIP_REQUIREMENTS_UPDATE", "").lower() in ("1", "true", "yes") or
sys.platform in ("ios", "android")
)
update_ran = _skip_update

View File

@@ -1168,8 +1168,8 @@ class ConnectionsMeta(AssembleOptions):
attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"]))
assert "exits" in attrs, f"Please define valid exits for {name}"
attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"]))
if "__doc__" not in attrs:
attrs["__doc__"] = PlandoConnections.__doc__
if "__doc__" not in attrs:
attrs["__doc__"] = PlandoConnections.__doc__
cls = super().__new__(mcs, name, bases, attrs)
return cls

View File

@@ -58,6 +58,9 @@ version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
is_macos = sys.platform == "darwin"
is_windows = sys.platform in ("win32", "cygwin", "msys")
is_android = sys.platform == "android"
is_ios = sys.platform == "ios"
is_mobile = is_android or is_ios
def int16_as_bytes(value: int) -> typing.List[int]:
@@ -684,13 +687,18 @@ def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P
def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit: typing.Optional[int] = None) \
-> typing.List[typing.Tuple[str, int]]:
import jellyfish
if is_mobile:
def get_fuzzy_ratio(word1: str, word2: str) -> float:
length = min(len(word1), len(word2))
return sum(word1[i] == word2[i] for i in range(length)) / min(1, length)
def get_fuzzy_ratio(word1: str, word2: str) -> float:
if word1 == word2:
return 1.01
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
else:
import jellyfish
def get_fuzzy_ratio(word1: str, word2: str) -> float:
if word1 == word2:
return 1.01
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
limit = limit if limit else len(word_list)
return list(

View File

@@ -13,7 +13,7 @@ from pony.orm import commit, db_session
from BaseClasses import get_seed, seeddigits
from Generate import PlandoOptions, handle_name, mystery_argparse
from Main import main as ERmain
from generate_lib import main as ERmain
from Utils import __version__, restricted_dumps, DaemonThreadPoolExecutor
from WebHostLib import app
from settings import ServerOptions, GeneratorOptions

32
buildozer.spec Normal file
View File

@@ -0,0 +1,32 @@
[app]
title = Archipelago
package.name = archipelago
package.domain = gg.archipelago
source.dir = .
source.include_exts = py,png,jpg,kv,atlas,json,yml,txt,lua
source.include_patterns = data/*, *.kv, *.py
source.exclude_dirs = factorio,test,docs,.github,.git, deploy, bin, build, __pycache__
source.exclude_patterns = test,*.pyc,*.pyo,__pycache__,*.egg-info,*.dist-info,docs,examples,build,dist,.git,.github
version = 0.6.7
# Requirements/Python
p4a.branch = develop
p4a.setup_py = false
python_flags = -O
requirements = python3==3.11.14, hostpython3==3.11.14,pip==24.3.1, kivy==2.3.1,kivymd@git+https://github.com/kivymd/KivyMD@365aa9b,materialyoucolor>=3.0.2,asynckivy>=0.10.0.dev1,asyncgui>=0.10.0.dev0,colorama==0.4.6,websockets==13.0.1,PyYAML>=6.0.3,jinja2>=3.1.6,schema>=0.7.8,bsdiff4>=1.2.6,platformdirs>=4.5.0,certifi>=2025.11.12,cython>=3.2.1,cymem>=2.0.13,orjson>=3.11.4,typing_extensions>=4.15.0,pyshortcuts>=1.9.6,pathspec>=0.12.1
# Android settings
orientation = portrait, landscape, portrait-reverse, landscape-reverse
fullscreen = 0
android.gradle_properties = org.gradle.jvmargs=-Xmx8192m -XX:MaxMetaspaceSize=512m
android.permissions = INTERNET,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE
android.api = 34
android.minapi = 24
android.ndk = 25b
android.archs = arm64-v8a
android.copy_libs = 1
# Icons/presplash
icon.filename = data/icon.png
presplash.filename = data/icon.png

View File

@@ -57,7 +57,7 @@ for classobj in SoundLoader._classes:
# .extensions(), which e.g. in audio_sdl2.pyx then calls a function called "mix_init()"
classobj.extensions()
from kivymd.uix.divider import MDDivider
from kivy.core.window import Window
from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel
@@ -86,7 +86,7 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.navigationbar import MDNavigationBar, MDNavigationItem
from kivymd.uix.screen import MDScreen
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.uix.divider import MDDivider
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.menu.menu import MDDropdownTextItem
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText

4
main.py Normal file
View File

@@ -0,0 +1,4 @@
import Launcher
if __name__ == "__main__":
Launcher.run()

View File

@@ -14,7 +14,7 @@ orjson>=3.11.4
typing_extensions>=4.15.0
pyshortcuts>=1.9.6
pathspec>=0.12.1
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
kivymd @ git+https://github.com/kivymd/KivyMD@365aa9b
kivymd>=2.0.1.dev0
# Legacy world dependencies that custom worlds rely on

View File

@@ -26,7 +26,7 @@ def _generate_local_inner(games: Iterable[str],
with TemporaryDirectory() as players_dir:
with TemporaryDirectory() as output_dir:
import Generate
import Main
import generate_lib
for n, game in enumerate(games, 1):
player_path = Path(players_dir) / f"{n}.yaml"

View File

@@ -9,7 +9,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory
import Generate
import Main
import generate_lib
class TestGenerateMain(unittest.TestCase):
@@ -59,7 +59,7 @@ class TestGenerateMain(unittest.TestCase):
'--player_files_path', str(self.abs_input_dir),
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
Main.main(*Generate.main())
generate_lib.main(*Generate.main())
self.assertOutput(self.output_tempdir.name)
@@ -68,7 +68,7 @@ class TestGenerateMain(unittest.TestCase):
'--player_files_path', str(self.rel_input_dir),
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
Main.main(*Generate.main())
generate_lib.main(*Generate.main())
self.assertOutput(self.output_tempdir.name)
@@ -87,7 +87,7 @@ class TestGenerateMain(unittest.TestCase):
sys.argv = [sys.argv[0], '--seed', '0',
'--outputpath', self.output_tempdir.name]
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
Main.main(*Generate.main())
generate_lib.main(*Generate.main())
finally:
user_path.cached_path = user_path_backup

View File

@@ -5,7 +5,7 @@ import weakref
from enum import Enum, auto
from typing import Optional, Callable, List, Iterable, Tuple
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore, is_mobile
class Type(Enum):

View File

@@ -1191,7 +1191,7 @@ def patch_rom(world: "YoshisIslandWorld", rom: LocalRom, player: int) -> None:
rom.write_bytes(0x1153F6, bytearray([0x16, 0x28, 0x10, 0x0C, 0x10, 0x4E, 0x1E, 0x10, 0x08, 0x04, 0x08, 0x24, 0x36, 0x82, 0x83, 0x83, 0x34, 0x84, 0x85, 0x85])) # Luigi piece clear text
rom.write_bytes(0x06FC86, bytearray([0xFF])) # Boss clear goal = 255, renders bowser inaccessible
from Main import __version__
from generate_lib import __version__
rom.name = bytearray(f'YOSHIAP{__version__.replace(".", "")[0:3]}_{player}_{world.multiworld.seed:11}\0', "utf8")[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x007FC0, rom.name)