Source code for babase.modutils

# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to modding."""
from __future__ import annotations

from typing import TYPE_CHECKING
import os

import _babase
from babase._logging import applog

if TYPE_CHECKING:
    from typing import Sequence


[docs] def get_human_readable_user_scripts_path() -> str: """Return a human readable location of user-scripts. This is NOT a valid filesystem path; may be something like "(SD Card)". """ app = _babase.app path: str | None = app.env.python_directory_user if path is None: return '<Not Available>' # These days, on Android, we use getExternalFilesDir() as the base of our # app's user-scripts dir, which gives us paths like: # /storage/emulated/0/Android/data/net.froemling.bombsquad/files # Userspace apps tend to show that as: # Android/data/net.froemling.bombsquad/files # We'd like to display it that way, but I'm not sure if there's a clean # way to get the root of the external storage area (/storage/emulated/0) # so that we could strip it off. There is # Environment.getExternalStorageDirectory() but that is deprecated. # So for now let's just be conservative and trim off recognized prefixes # and show the whole ugly path as a fallback. # Note that we used to use externalStorageText resource but gonna try # without it for now. (simply 'foo' instead of <External Storage>/foo). if app.classic is not None and app.classic.platform == 'android': for pre in ['/storage/emulated/0/']: if path.startswith(pre): path = path.removeprefix(pre) break return path
def _request_storage_permission() -> bool: """If needed, requests storage permission from the user (& return true).""" from babase._language import Lstr from babase._mgen.enums import Permission if not _babase.have_permission(Permission.STORAGE): _babase.getsimplesound('error').play() _babase.screenmessage( Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0) ) _babase.apptimer( 1.0, lambda: _babase.request_permission(Permission.STORAGE) ) return True return False
[docs] def show_user_scripts() -> None: """Open or nicely print the location of the user-scripts directory.""" app = _babase.app env = app.env # First off, if we need permission for this, ask for it. if _request_storage_permission(): return # If we're running in a nonstandard environment its possible this is unset. if env.python_directory_user is None: _babase.screenmessage('<unset>') return # Secondly, if the dir doesn't exist, attempt to make it. if not os.path.exists(env.python_directory_user): os.makedirs(env.python_directory_user) # On android, attempt to write a file in their user-scripts dir telling # them about modding. This also has the side-effect of allowing us to # media-scan that dir so it shows up in android-file-transfer, since it # doesn't seem like there's a way to inform the media scanner of an empty # directory, which means they would have to reboot their device before # they can see it. if app.classic is not None and app.classic.platform == 'android': try: usd: str | None = env.python_directory_user if usd is not None and os.path.isdir(usd): file_name = usd + '/about_this_folder.txt' with open(file_name, 'w', encoding='utf-8') as outfile: outfile.write( 'You can drop files in here to mod the game.' ' See settings/advanced in the game for more info.' ) except Exception: applog.exception('Error writing about_this_folder stuff.') # On platforms that support it, open the dir in the UI. if _babase.supports_open_dir_externally(): _babase.open_dir_externally(env.python_directory_user) # Otherwise we just print a pretty version of it. else: _babase.screenmessage(get_human_readable_user_scripts_path())
[docs] def create_user_system_scripts() -> None: """Set up a copy of Ballistica app scripts under user scripts dir. (for editing and experimenting) """ import shutil app = _babase.app env = app.env # First off, if we need permission for this, ask for it. if _request_storage_permission(): return # Its possible these are unset in non-standard environments. if env.python_directory_user is None: raise RuntimeError('user python dir unset') if env.python_directory_app is None: raise RuntimeError('app python dir unset') path = ( f'{env.python_directory_user}/sys/' f'{env.engine_version}_{env.engine_build_number}' ) pathtmp = path + '_tmp' if os.path.exists(path): print('Delete Existing User Scripts first!') _babase.screenmessage( 'Delete Existing User Scripts first!', color=(1, 0, 0), ) return if os.path.exists(pathtmp): shutil.rmtree(pathtmp) def _ignore_filter(src: str, names: Sequence[str]) -> Sequence[str]: del src, names # Unused # We simply skip all __pycache__ directories. (the user would have # to blow them away anyway to make changes; # See https://github.com/efroemling/ballistica/wiki # /Knowledge-Nuggets#python-cache-files-gotcha return ('__pycache__',) print(f'COPYING "{env.python_directory_app}" -> "{pathtmp}".') shutil.copytree(env.python_directory_app, pathtmp, ignore=_ignore_filter) print(f'MOVING "{pathtmp}" -> "{path}".') shutil.move(pathtmp, path) print( f"Created system scripts at :'{path}" f"'\nRestart {_babase.appname()} to use them." f' (use babase.quit() to exit the game)' ) _babase.screenmessage('Created User System Scripts', color=(0, 1, 0)) if app.classic is not None and app.classic.platform == 'android': print( 'Note: the new files may not be visible via ' 'android-file-transfer until you restart your device.' )
[docs] def delete_user_system_scripts() -> None: """Clean out the scripts created by create_user_system_scripts().""" import shutil env = _babase.app.env if env.python_directory_user is None: raise RuntimeError('user python dir unset') path = ( f'{env.python_directory_user}/sys/' f'{env.engine_version}_{env.engine_build_number}' ) if os.path.exists(path): shutil.rmtree(path) print('User system scripts deleted.') _babase.screenmessage('Deleted User System Scripts', color=(0, 1, 0)) _babase.screenmessage( f'Closing {_babase.appname()} to make changes.', color=(0, 1, 0) ) _babase.apptimer(2.0, _babase.quit) else: print(f"User system scripts not found at '{path}'.") _babase.screenmessage('User Scripts Not Found', color=(1, 0, 0)) # If the sys path is empty, kill it. dpath = env.python_directory_user + '/sys' if os.path.isdir(dpath) and not os.listdir(dpath): os.rmdir(dpath)
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum