mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-10-20 00:00:39 +00:00
1.7.32 ba_data update
This commit is contained in:
parent
bf2f252ee5
commit
15393d5461
144 changed files with 4296 additions and 2411 deletions
37
dist/ba_data/python/babase/__init__.py
vendored
37
dist/ba_data/python/babase/__init__.py
vendored
|
|
@ -27,7 +27,10 @@ from _babase import (
|
|||
apptime,
|
||||
apptimer,
|
||||
AppTimer,
|
||||
can_toggle_fullscreen,
|
||||
fullscreen_control_available,
|
||||
fullscreen_control_get,
|
||||
fullscreen_control_key_shortcut,
|
||||
fullscreen_control_set,
|
||||
charstr,
|
||||
clipboard_get_text,
|
||||
clipboard_has_text,
|
||||
|
|
@ -58,10 +61,8 @@ from _babase import (
|
|||
in_logic_thread,
|
||||
increment_analytics_count,
|
||||
is_os_playing_music,
|
||||
is_running_on_fire_tv,
|
||||
is_xcode_build,
|
||||
lock_all_input,
|
||||
mac_music_app_get_library_source,
|
||||
mac_music_app_get_playlists,
|
||||
mac_music_app_get_volume,
|
||||
mac_music_app_init,
|
||||
|
|
@ -72,7 +73,10 @@ from _babase import (
|
|||
music_player_set_volume,
|
||||
music_player_shutdown,
|
||||
music_player_stop,
|
||||
native_review_request,
|
||||
native_review_request_supported,
|
||||
native_stack_trace,
|
||||
open_file_externally,
|
||||
print_load_info,
|
||||
pushcall,
|
||||
quit,
|
||||
|
|
@ -82,7 +86,6 @@ from _babase import (
|
|||
screenmessage,
|
||||
set_analytics_screen,
|
||||
set_low_level_config_value,
|
||||
set_stress_testing,
|
||||
set_thread_name,
|
||||
set_ui_input_device,
|
||||
show_progress_bar,
|
||||
|
|
@ -114,6 +117,11 @@ from babase._apputils import (
|
|||
AppHealthMonitor,
|
||||
)
|
||||
from babase._cloud import CloudSubsystem
|
||||
from babase._devconsole import (
|
||||
DevConsoleTab,
|
||||
DevConsoleTabEntry,
|
||||
DevConsoleSubsystem,
|
||||
)
|
||||
from babase._emptyappmode import EmptyAppMode
|
||||
from babase._error import (
|
||||
print_exception,
|
||||
|
|
@ -146,9 +154,8 @@ from babase._general import (
|
|||
getclass,
|
||||
get_type_name,
|
||||
)
|
||||
from babase._keyboard import Keyboard
|
||||
from babase._language import Lstr, LanguageSubsystem
|
||||
from babase._login import LoginAdapter
|
||||
from babase._login import LoginAdapter, LoginInfo
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
# (PyCharm inspection bug?)
|
||||
|
|
@ -157,6 +164,7 @@ from babase._mgen.enums import (
|
|||
SpecialChar,
|
||||
InputType,
|
||||
UIScale,
|
||||
QuitType,
|
||||
)
|
||||
from babase._math import normalized_color, is_point_in_box, vec3validate
|
||||
from babase._meta import MetadataSubsystem
|
||||
|
|
@ -194,7 +202,10 @@ __all__ = [
|
|||
'apptimer',
|
||||
'AppTimer',
|
||||
'Call',
|
||||
'can_toggle_fullscreen',
|
||||
'fullscreen_control_available',
|
||||
'fullscreen_control_get',
|
||||
'fullscreen_control_key_shortcut',
|
||||
'fullscreen_control_set',
|
||||
'charstr',
|
||||
'clipboard_get_text',
|
||||
'clipboard_has_text',
|
||||
|
|
@ -206,6 +217,9 @@ __all__ = [
|
|||
'ContextError',
|
||||
'ContextRef',
|
||||
'DelegateNotFoundError',
|
||||
'DevConsoleTab',
|
||||
'DevConsoleTabEntry',
|
||||
'DevConsoleSubsystem',
|
||||
'DisplayTime',
|
||||
'displaytime',
|
||||
'displaytimer',
|
||||
|
|
@ -244,14 +258,12 @@ __all__ = [
|
|||
'is_browser_likely_available',
|
||||
'is_os_playing_music',
|
||||
'is_point_in_box',
|
||||
'is_running_on_fire_tv',
|
||||
'is_xcode_build',
|
||||
'Keyboard',
|
||||
'LanguageSubsystem',
|
||||
'lock_all_input',
|
||||
'LoginAdapter',
|
||||
'LoginInfo',
|
||||
'Lstr',
|
||||
'mac_music_app_get_library_source',
|
||||
'mac_music_app_get_playlists',
|
||||
'mac_music_app_get_volume',
|
||||
'mac_music_app_init',
|
||||
|
|
@ -264,10 +276,13 @@ __all__ = [
|
|||
'music_player_set_volume',
|
||||
'music_player_shutdown',
|
||||
'music_player_stop',
|
||||
'native_review_request',
|
||||
'native_review_request_supported',
|
||||
'native_stack_trace',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
'NotFoundError',
|
||||
'open_file_externally',
|
||||
'Permission',
|
||||
'PlayerNotFoundError',
|
||||
'Plugin',
|
||||
|
|
@ -278,6 +293,7 @@ __all__ = [
|
|||
'print_load_info',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'QuitType',
|
||||
'reload_media',
|
||||
'request_permission',
|
||||
'safecolor',
|
||||
|
|
@ -287,7 +303,6 @@ __all__ = [
|
|||
'SessionTeamNotFoundError',
|
||||
'set_analytics_screen',
|
||||
'set_low_level_config_value',
|
||||
'set_stress_testing',
|
||||
'set_thread_name',
|
||||
'set_ui_input_device',
|
||||
'show_progress_bar',
|
||||
|
|
|
|||
89
dist/ba_data/python/babase/_accountv2.py
vendored
89
dist/ba_data/python/babase/_accountv2.py
vendored
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, assert_never
|
||||
|
||||
from efro.call import tpartial
|
||||
from efro.error import CommunicationError
|
||||
|
|
@ -16,7 +16,7 @@ import _babase
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from babase._login import LoginAdapter
|
||||
from babase._login import LoginAdapter, LoginInfo
|
||||
|
||||
|
||||
DEBUG_LOG = False
|
||||
|
|
@ -27,10 +27,12 @@ class AccountV2Subsystem:
|
|||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.accounts'.
|
||||
Access the single shared instance of this class at 'ba.app.plus.accounts'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
from babase._login import LoginAdapterGPGS, LoginAdapterGameCenter
|
||||
|
||||
# Whether or not everything related to an initial login
|
||||
# (or lack thereof) has completed. This includes things like
|
||||
# workspace syncing. Completion of this is what flips the app
|
||||
|
|
@ -45,16 +47,13 @@ class AccountV2Subsystem:
|
|||
self._implicit_state_changed = False
|
||||
self._can_do_auto_sign_in = True
|
||||
|
||||
if _babase.app.classic is None:
|
||||
raise RuntimeError('Needs updating for no-classic case.')
|
||||
|
||||
if (
|
||||
_babase.app.classic.platform == 'android'
|
||||
and _babase.app.classic.subplatform == 'google'
|
||||
):
|
||||
from babase._login import LoginAdapterGPGS
|
||||
|
||||
self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS()
|
||||
adapter: LoginAdapter
|
||||
if _babase.using_google_play_game_services():
|
||||
adapter = LoginAdapterGPGS()
|
||||
self.login_adapters[adapter.login_type] = adapter
|
||||
if _babase.using_game_center():
|
||||
adapter = LoginAdapterGameCenter()
|
||||
self.login_adapters[adapter.login_type] = adapter
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Should be called at standard on_app_loading time."""
|
||||
|
|
@ -62,10 +61,6 @@ class AccountV2Subsystem:
|
|||
for adapter in self.login_adapters.values():
|
||||
adapter.on_app_loading()
|
||||
|
||||
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||
"""Set credentials for the primary app account."""
|
||||
raise NotImplementedError('This should be overridden.')
|
||||
|
||||
def have_primary_credentials(self) -> bool:
|
||||
"""Are credentials currently set for the primary app account?
|
||||
|
||||
|
|
@ -80,10 +75,6 @@ class AccountV2Subsystem:
|
|||
"""The primary account for the app, or None if not logged in."""
|
||||
return self.do_get_primary()
|
||||
|
||||
def do_get_primary(self) -> AccountV2Handle | None:
|
||||
"""Internal - should be overridden by subclass."""
|
||||
return None
|
||||
|
||||
def on_primary_account_changed(
|
||||
self, account: AccountV2Handle | None
|
||||
) -> None:
|
||||
|
|
@ -142,6 +133,8 @@ class AccountV2Subsystem:
|
|||
"""An implicit sign-in happened (called by native layer)."""
|
||||
from babase._login import LoginAdapter
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
with _babase.ContextRef.empty():
|
||||
self.login_adapters[login_type].set_implicit_login_state(
|
||||
LoginAdapter.ImplicitLoginState(
|
||||
|
|
@ -151,6 +144,7 @@ class AccountV2Subsystem:
|
|||
|
||||
def on_implicit_sign_out(self, login_type: LoginType) -> None:
|
||||
"""An implicit sign-out happened (called by native layer)."""
|
||||
assert _babase.in_logic_thread()
|
||||
with _babase.ContextRef.empty():
|
||||
self.login_adapters[login_type].set_implicit_login_state(None)
|
||||
|
||||
|
|
@ -192,9 +186,10 @@ class AccountV2Subsystem:
|
|||
cfgkey = 'ImplicitLoginStates'
|
||||
cfgdict = _babase.app.config.setdefault(cfgkey, {})
|
||||
|
||||
# Store which (if any) adapter is currently implicitly signed in.
|
||||
# Making the assumption there will only ever be one implicit
|
||||
# adapter at a time; may need to update this if that changes.
|
||||
# Store which (if any) adapter is currently implicitly signed
|
||||
# in. Making the assumption there will only ever be one implicit
|
||||
# adapter at a time; may need to revisit this logic if that
|
||||
# changes.
|
||||
prev_state = cfgdict.get(login_type.value)
|
||||
if state is None:
|
||||
self._implicit_signed_in_adapter = None
|
||||
|
|
@ -205,18 +200,26 @@ class AccountV2Subsystem:
|
|||
state.login_id
|
||||
)
|
||||
|
||||
# Special case: if the user is already signed in but not with
|
||||
# this implicit login, we may want to let them know that the
|
||||
# 'Welcome back FOO' they likely just saw is not actually
|
||||
# accurate.
|
||||
# Special case: if the user is already signed in but not
|
||||
# with this implicit login, let them know that the 'Welcome
|
||||
# back FOO' they likely just saw is not actually accurate.
|
||||
if (
|
||||
self.primary is not None
|
||||
and not self.login_adapters[login_type].is_back_end_active()
|
||||
):
|
||||
service_str: Lstr | None
|
||||
if login_type is LoginType.GPGS:
|
||||
service_str = Lstr(resource='googlePlayText')
|
||||
else:
|
||||
elif login_type is LoginType.GAME_CENTER:
|
||||
# Note: Apparently Game Center is just called 'Game
|
||||
# Center' in all languages. Can revisit if not true.
|
||||
# https://developer.apple.com/forums/thread/725779
|
||||
service_str = Lstr(value='Game Center')
|
||||
elif login_type is LoginType.EMAIL:
|
||||
# Not possible; just here for exhaustive coverage.
|
||||
service_str = None
|
||||
else:
|
||||
assert_never(login_type)
|
||||
if service_str is not None:
|
||||
_babase.apptimer(
|
||||
2.0,
|
||||
|
|
@ -259,6 +262,14 @@ class AccountV2Subsystem:
|
|||
# We may want to auto-sign-in based on this new state.
|
||||
self._update_auto_sign_in()
|
||||
|
||||
def do_get_primary(self) -> AccountV2Handle | None:
|
||||
"""Internal - should be overridden by subclass."""
|
||||
raise NotImplementedError('This should be overridden.')
|
||||
|
||||
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||
"""Set credentials for the primary app account."""
|
||||
raise NotImplementedError('This should be overridden.')
|
||||
|
||||
def _update_auto_sign_in(self) -> None:
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
|
@ -266,7 +277,7 @@ class AccountV2Subsystem:
|
|||
# If implicit state has changed, try to respond.
|
||||
if self._implicit_state_changed:
|
||||
if self._implicit_signed_in_adapter is None:
|
||||
# If implicit back-end is signed out, follow suit
|
||||
# If implicit back-end has signed out, we follow suit
|
||||
# immediately; no need to wait for network connectivity.
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -286,9 +297,8 @@ class AccountV2Subsystem:
|
|||
# Consider this an 'explicit' sign in because the
|
||||
# implicit-login state change presumably was triggered
|
||||
# by some user action (signing in, signing out, or
|
||||
# switching accounts via the back-end).
|
||||
# NOTE: should test case where we don't have
|
||||
# connectivity here.
|
||||
# switching accounts via the back-end). NOTE: should
|
||||
# test case where we don't have connectivity here.
|
||||
if plus.cloud.is_connected():
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -419,14 +429,11 @@ class AccountV2Handle:
|
|||
used with some operations such as cloud messaging.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tag = '?'
|
||||
|
||||
self.workspacename: str | None = None
|
||||
self.workspaceid: str | None = None
|
||||
|
||||
# Login types and their display-names associated with this account.
|
||||
self.logins: dict[LoginType, str] = {}
|
||||
accountid: str
|
||||
tag: str
|
||||
workspacename: str | None
|
||||
workspaceid: str | None
|
||||
logins: dict[LoginType, LoginInfo]
|
||||
|
||||
def __enter__(self) -> None:
|
||||
"""Support for "with" statement.
|
||||
|
|
|
|||
416
dist/ba_data/python/babase/_app.py
vendored
416
dist/ba_data/python/babase/_app.py
vendored
|
|
@ -1,12 +1,10 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the high level state of the app."""
|
||||
# pylint: disable=too-many-lines
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import logging
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
|
@ -24,6 +22,7 @@ from babase._appcomponent import AppComponentSubsystem
|
|||
from babase._appmodeselector import AppModeSelector
|
||||
from babase._appintent import AppIntentDefault, AppIntentExec
|
||||
from babase._stringedit import StringEditSubsystem
|
||||
from babase._devconsole import DevConsoleSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
|
|
@ -57,6 +56,8 @@ class App:
|
|||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# A few things defined as non-optional values but not actually
|
||||
# available until the app starts.
|
||||
plugins: PluginSubsystem
|
||||
lang: LanguageSubsystem
|
||||
health_monitor: AppHealthMonitor
|
||||
|
|
@ -71,7 +72,7 @@ class App:
|
|||
|
||||
# The app has not yet begun starting and should not be used in
|
||||
# any way.
|
||||
NOT_RUNNING = 0
|
||||
NOT_STARTED = 0
|
||||
|
||||
# The native layer is spinning up its machinery (screens,
|
||||
# renderers, etc.). Nothing should happen in the Python layer
|
||||
|
|
@ -91,13 +92,23 @@ class App:
|
|||
# All pieces are in place and the app is now doing its thing.
|
||||
RUNNING = 4
|
||||
|
||||
# The app is backgrounded or otherwise suspended.
|
||||
PAUSED = 5
|
||||
# Used on platforms such as mobile where the app basically needs
|
||||
# to shut down while backgrounded. In this state, all event
|
||||
# loops are suspended and all graphics and audio must cease
|
||||
# completely. Be aware that the suspended state can be entered
|
||||
# from any other state including NATIVE_BOOTSTRAPPING and
|
||||
# SHUTTING_DOWN.
|
||||
SUSPENDED = 5
|
||||
|
||||
# The app is shutting down.
|
||||
# The app is shutting down. This process may involve sending
|
||||
# network messages or other things that can take up to a few
|
||||
# seconds, so ideally graphics and audio should remain
|
||||
# functional (with fades or spinners or whatever to show
|
||||
# something is happening).
|
||||
SHUTTING_DOWN = 6
|
||||
|
||||
# The app has completed shutdown.
|
||||
# The app has completed shutdown. Any code running here should
|
||||
# be basically immediate.
|
||||
SHUTDOWN_COMPLETE = 7
|
||||
|
||||
class DefaultAppModeSelector(AppModeSelector):
|
||||
|
|
@ -140,9 +151,9 @@ class App:
|
|||
def __init__(self) -> None:
|
||||
"""(internal)
|
||||
|
||||
Do not instantiate this class; access the single shared instance
|
||||
of it as 'app' which is available in various Ballistica
|
||||
feature-set modules such as babase.
|
||||
Do not instantiate this class. You can access the single shared
|
||||
instance of it through various high level packages: 'babase.app',
|
||||
'bascenev1.app', 'bauiv1.app', etc.
|
||||
"""
|
||||
|
||||
# Hack for docs-generation: we can be imported with dummy modules
|
||||
|
|
@ -151,32 +162,35 @@ class App:
|
|||
return
|
||||
|
||||
self.env: babase.Env = _babase.Env()
|
||||
self.state = self.State.NOT_RUNNING
|
||||
self.state = self.State.NOT_STARTED
|
||||
|
||||
# Default executor which can be used for misc background
|
||||
# processing. It should also be passed to any additional asyncio
|
||||
# loops we create so that everything shares the same single set
|
||||
# of worker threads.
|
||||
self.threadpool = ThreadPoolExecutor(thread_name_prefix='baworker')
|
||||
self.threadpool = ThreadPoolExecutor(
|
||||
thread_name_prefix='baworker',
|
||||
initializer=self._thread_pool_thread_init,
|
||||
)
|
||||
|
||||
self.meta = MetadataSubsystem()
|
||||
self.net = NetworkSubsystem()
|
||||
self.workspaces = WorkspaceSubsystem()
|
||||
self.components = AppComponentSubsystem()
|
||||
self.stringedit = StringEditSubsystem()
|
||||
self.devconsole = DevConsoleSubsystem()
|
||||
|
||||
# This is incremented any time the app is backgrounded or
|
||||
# foregrounded; can be a simple way to determine if network data
|
||||
# should be refreshed/etc.
|
||||
self.fg_state = 0
|
||||
self.config_file_healthy: bool = False
|
||||
|
||||
self._subsystems: list[AppSubsystem] = []
|
||||
self._native_bootstrapping_completed = False
|
||||
self._init_completed = False
|
||||
self._meta_scan_completed = False
|
||||
self._native_start_called = False
|
||||
self._native_paused = False
|
||||
self._native_suspended = False
|
||||
self._native_shutdown_called = False
|
||||
self._native_shutdown_complete_called = False
|
||||
self._initial_sign_in_completed = False
|
||||
|
|
@ -194,8 +208,11 @@ class App:
|
|||
self._mode_selector: babase.AppModeSelector | None = None
|
||||
self._shutdown_task: asyncio.Task[None] | None = None
|
||||
self._shutdown_tasks: list[Coroutine[None, None, None]] = [
|
||||
self._wait_for_shutdown_suppressions()
|
||||
self._wait_for_shutdown_suppressions(),
|
||||
self._fade_and_shutdown_graphics(),
|
||||
self._fade_and_shutdown_audio(),
|
||||
]
|
||||
self._pool_thread_count = 0
|
||||
|
||||
def postinit(self) -> None:
|
||||
"""Called after we've been inited and assigned to babase.app.
|
||||
|
|
@ -212,6 +229,15 @@ class App:
|
|||
self.lang = LanguageSubsystem()
|
||||
self.plugins = PluginSubsystem()
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
"""Whether the app is currently front and center.
|
||||
|
||||
This will be False when the app is hidden, other activities
|
||||
are covering it, etc. (depending on the platform).
|
||||
"""
|
||||
return _babase.app_is_active()
|
||||
|
||||
@property
|
||||
def aioloop(self) -> asyncio.AbstractEventLoop:
|
||||
"""The logic thread's asyncio event loop.
|
||||
|
|
@ -311,7 +337,7 @@ class App:
|
|||
def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
|
||||
"""Add a task to be run on app shutdown.
|
||||
|
||||
Note that tasks will be killed after
|
||||
Note that shutdown tasks will be canceled after
|
||||
App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
|
||||
"""
|
||||
if (
|
||||
|
|
@ -385,18 +411,18 @@ class App:
|
|||
self._native_bootstrapping_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_pause(self) -> None:
|
||||
"""Called by the native layer when the app pauses."""
|
||||
def on_native_suspend(self) -> None:
|
||||
"""Called by the native layer when the app is suspended."""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._native_paused # Should avoid redundant calls.
|
||||
self._native_paused = True
|
||||
assert not self._native_suspended # Should avoid redundant calls.
|
||||
self._native_suspended = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_resume(self) -> None:
|
||||
"""Called by the native layer when the app resumes."""
|
||||
def on_native_unsuspend(self) -> None:
|
||||
"""Called by the native layer when the app suspension ends."""
|
||||
assert _babase.in_logic_thread()
|
||||
assert self._native_paused # Should avoid redundant calls.
|
||||
self._native_paused = False
|
||||
assert self._native_suspended # Should avoid redundant calls.
|
||||
self._native_suspended = False
|
||||
self._update_state()
|
||||
|
||||
def on_native_shutdown(self) -> None:
|
||||
|
|
@ -415,7 +441,7 @@ class App:
|
|||
"""(internal)"""
|
||||
from babase._appconfig import read_app_config
|
||||
|
||||
self._config, self.config_file_healthy = read_app_config()
|
||||
self._config = read_app_config()
|
||||
|
||||
def handle_deep_link(self, url: str) -> None:
|
||||
"""Handle a deep link URL."""
|
||||
|
|
@ -493,7 +519,7 @@ class App:
|
|||
except Exception:
|
||||
logging.exception('Error setting app intent to %s.', intent)
|
||||
_babase.pushcall(
|
||||
tpartial(self._apply_intent_error, intent),
|
||||
tpartial(self._display_set_intent_error, intent),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
|
|
@ -538,10 +564,11 @@ class App:
|
|||
'Error handling intent %s in app-mode %s.', intent, mode
|
||||
)
|
||||
|
||||
def _apply_intent_error(self, intent: AppIntent) -> None:
|
||||
def _display_set_intent_error(self, intent: AppIntent) -> None:
|
||||
"""Show the *user* something went wrong setting an intent."""
|
||||
from babase._language import Lstr
|
||||
|
||||
del intent # Unused.
|
||||
del intent
|
||||
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
|
|
@ -564,19 +591,6 @@ class App:
|
|||
self._aioloop = _asyncio.setup_asyncio()
|
||||
self.health_monitor = AppHealthMonitor()
|
||||
|
||||
# Only proceed if our config file is healthy so we don't
|
||||
# overwrite a broken one or whatnot and wipe out data.
|
||||
if not self.config_file_healthy:
|
||||
if self.classic is not None:
|
||||
handled = self.classic.show_config_error_window()
|
||||
if handled:
|
||||
return
|
||||
|
||||
# For now on other systems we just overwrite the bum config.
|
||||
# At this point settings are already set; lets just commit
|
||||
# them to disk.
|
||||
_appconfig.commit_app_config(force=True)
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
|
|
@ -726,15 +740,15 @@ class App:
|
|||
_babase.lifecyclelog('app state shutting down')
|
||||
self._on_shutting_down()
|
||||
|
||||
elif self._native_paused:
|
||||
# Entering paused state:
|
||||
if self.state is not self.State.PAUSED:
|
||||
self.state = self.State.PAUSED
|
||||
self._on_pause()
|
||||
elif self._native_suspended:
|
||||
# Entering suspended state:
|
||||
if self.state is not self.State.SUSPENDED:
|
||||
self.state = self.State.SUSPENDED
|
||||
self._on_suspend()
|
||||
else:
|
||||
# Leaving paused state:
|
||||
if self.state is self.State.PAUSED:
|
||||
self._on_resume()
|
||||
# Leaving suspended state:
|
||||
if self.state is self.State.SUSPENDED:
|
||||
self._on_unsuspend()
|
||||
|
||||
# Entering or returning to running state
|
||||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
|
|
@ -768,7 +782,7 @@ class App:
|
|||
self.state = self.State.NATIVE_BOOTSTRAPPING
|
||||
_babase.lifecyclelog('app state native bootstrapping')
|
||||
else:
|
||||
# Only logical possibility left is NOT_RUNNING, in which
|
||||
# Only logical possibility left is NOT_STARTED, in which
|
||||
# case we should not be getting called.
|
||||
logging.warning(
|
||||
'App._update_state called while in %s state;'
|
||||
|
|
@ -780,6 +794,7 @@ class App:
|
|||
async def _shutdown(self) -> None:
|
||||
import asyncio
|
||||
|
||||
_babase.lock_all_input()
|
||||
try:
|
||||
async with asyncio.TaskGroup() as task_group:
|
||||
for task_coro in self._shutdown_tasks:
|
||||
|
|
@ -809,33 +824,33 @@ class App:
|
|||
try:
|
||||
await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS)
|
||||
except Exception:
|
||||
logging.exception('Error in shutdown task.')
|
||||
logging.exception('Error in shutdown task (%s).', coro)
|
||||
|
||||
def _on_pause(self) -> None:
|
||||
"""Called when the app goes to a paused state."""
|
||||
def _on_suspend(self) -> None:
|
||||
"""Called when the app goes to a suspended state."""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Pause all app subsystems in the opposite order they were inited.
|
||||
# Suspend all app subsystems in the opposite order they were inited.
|
||||
for subsystem in reversed(self._subsystems):
|
||||
try:
|
||||
subsystem.on_app_pause()
|
||||
subsystem.on_app_suspend()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_pause for subsystem %s.', subsystem
|
||||
'Error in on_app_suspend for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def _on_resume(self) -> None:
|
||||
"""Called when resuming."""
|
||||
def _on_unsuspend(self) -> None:
|
||||
"""Called when unsuspending."""
|
||||
assert _babase.in_logic_thread()
|
||||
self.fg_state += 1
|
||||
|
||||
# Resume all app subsystems in the same order they were inited.
|
||||
# Unsuspend all app subsystems in the same order they were inited.
|
||||
for subsystem in self._subsystems:
|
||||
try:
|
||||
subsystem.on_app_resume()
|
||||
subsystem.on_app_unsuspend()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_resume for subsystem %s.', subsystem
|
||||
'Error in on_app_unsuspend for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def _on_shutting_down(self) -> None:
|
||||
|
|
@ -875,10 +890,45 @@ class App:
|
|||
import asyncio
|
||||
|
||||
# Spin and wait for anything blocking shutdown to complete.
|
||||
starttime = _babase.apptime()
|
||||
_babase.lifecyclelog('shutdown-suppress wait begin')
|
||||
while _babase.shutdown_suppress_count() > 0:
|
||||
await asyncio.sleep(0.001)
|
||||
_babase.lifecyclelog('shutdown-suppress wait end')
|
||||
duration = _babase.apptime() - starttime
|
||||
if duration > 1.0:
|
||||
logging.warning(
|
||||
'Shutdown-suppressions lasted longer than ideal '
|
||||
'(%.2f seconds).',
|
||||
duration,
|
||||
)
|
||||
|
||||
async def _fade_and_shutdown_graphics(self) -> None:
|
||||
import asyncio
|
||||
|
||||
# Kick off a short fade and give it time to complete.
|
||||
_babase.lifecyclelog('fade-and-shutdown-graphics begin')
|
||||
_babase.fade_screen(False, time=0.15)
|
||||
await asyncio.sleep(0.15)
|
||||
|
||||
# Now tell the graphics system to go down and wait until
|
||||
# it has done so.
|
||||
_babase.graphics_shutdown_begin()
|
||||
while not _babase.graphics_shutdown_is_complete():
|
||||
await asyncio.sleep(0.01)
|
||||
_babase.lifecyclelog('fade-and-shutdown-graphics end')
|
||||
|
||||
async def _fade_and_shutdown_audio(self) -> None:
|
||||
import asyncio
|
||||
|
||||
# Tell the audio system to go down and give it a bit of
|
||||
# time to do so gracefully.
|
||||
_babase.lifecyclelog('fade-and-shutdown-audio begin')
|
||||
_babase.audio_shutdown_begin()
|
||||
await asyncio.sleep(0.15)
|
||||
while not _babase.audio_shutdown_is_complete():
|
||||
await asyncio.sleep(0.01)
|
||||
_babase.lifecyclelog('fade-and-shutdown-audio end')
|
||||
|
||||
def _threadpool_no_wait_done(self, fut: Future) -> None:
|
||||
try:
|
||||
|
|
@ -888,243 +938,7 @@ class App:
|
|||
'Error in work submitted via threadpool_submit_no_wait()'
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# THE FOLLOWING ARE DEPRECATED AND WILL BE REMOVED IN A FUTURE UPDATE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def build_number(self) -> int:
|
||||
"""Integer build number.
|
||||
|
||||
This value increases by at least 1 with each release of the engine.
|
||||
It is independent of the human readable babase.App.version string.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.build_number is deprecated; use app.env.build_number',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.build_number
|
||||
|
||||
@property
|
||||
def device_name(self) -> str:
|
||||
"""Name of the device running the app."""
|
||||
warnings.warn(
|
||||
'app.device_name is deprecated; use app.env.device_name',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.device_name
|
||||
|
||||
@property
|
||||
def config_file_path(self) -> str:
|
||||
"""Where the app's config file is stored on disk."""
|
||||
warnings.warn(
|
||||
'app.config_file_path is deprecated;'
|
||||
' use app.env.config_file_path',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.config_file_path
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Human-readable engine version string; something like '1.3.24'.
|
||||
|
||||
This should not be interpreted as a number; it may contain
|
||||
string elements such as 'alpha', 'beta', 'test', etc.
|
||||
If a numeric version is needed, use `build_number`.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.version is deprecated; use app.env.version',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.version
|
||||
|
||||
@property
|
||||
def debug_build(self) -> bool:
|
||||
"""Whether the app was compiled in debug mode.
|
||||
|
||||
Debug builds generally run substantially slower than non-debug
|
||||
builds due to compiler optimizations being disabled and extra
|
||||
checks being run.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.debug_build is deprecated; use app.env.debug',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.debug
|
||||
|
||||
@property
|
||||
def test_build(self) -> bool:
|
||||
"""Whether the app was compiled in test mode.
|
||||
|
||||
Test mode enables extra checks and features that are useful for
|
||||
release testing but which do not slow the game down significantly.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.test_build is deprecated; use app.env.test',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.test
|
||||
|
||||
@property
|
||||
def data_directory(self) -> str:
|
||||
"""Path where static app data lives."""
|
||||
warnings.warn(
|
||||
'app.data_directory is deprecated; use app.env.data_directory',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.data_directory
|
||||
|
||||
@property
|
||||
def python_directory_user(self) -> str | None:
|
||||
"""Path where the app expects its user scripts (mods) to live.
|
||||
|
||||
Be aware that this value may be None if ballistica is running in
|
||||
a non-standard environment, and that python-path modifications may
|
||||
cause modules to be loaded from other locations.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.python_directory_user is deprecated;'
|
||||
' use app.env.python_directory_user',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.python_directory_user
|
||||
|
||||
@property
|
||||
def python_directory_app(self) -> str | None:
|
||||
"""Path where the app expects its bundled modules to live.
|
||||
|
||||
Be aware that this value may be None if Ballistica is running in
|
||||
a non-standard environment, and that python-path modifications may
|
||||
cause modules to be loaded from other locations.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.python_directory_app is deprecated;'
|
||||
' use app.env.python_directory_app',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.python_directory_app
|
||||
|
||||
@property
|
||||
def python_directory_app_site(self) -> str | None:
|
||||
"""Path where the app expects its bundled pip modules to live.
|
||||
|
||||
Be aware that this value may be None if Ballistica is running in
|
||||
a non-standard environment, and that python-path modifications may
|
||||
cause modules to be loaded from other locations.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.python_directory_app_site is deprecated;'
|
||||
' use app.env.python_directory_app_site',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.python_directory_app_site
|
||||
|
||||
@property
|
||||
def api_version(self) -> int:
|
||||
"""The app's api version.
|
||||
|
||||
Only Python modules and packages associated with the current API
|
||||
version number will be detected by the game (see the ba_meta tag).
|
||||
This value will change whenever substantial backward-incompatible
|
||||
changes are introduced to ballistica APIs. When that happens,
|
||||
modules/packages should be updated accordingly and set to target
|
||||
the newer API version number.
|
||||
"""
|
||||
warnings.warn(
|
||||
'app.api_version is deprecated; use app.env.api_version',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.api_version
|
||||
|
||||
@property
|
||||
def on_tv(self) -> bool:
|
||||
"""Whether the app is currently running on a TV."""
|
||||
warnings.warn(
|
||||
'app.on_tv is deprecated; use app.env.tv',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.tv
|
||||
|
||||
@property
|
||||
def vr_mode(self) -> bool:
|
||||
"""Whether the app is currently running in VR."""
|
||||
warnings.warn(
|
||||
'app.vr_mode is deprecated; use app.env.vr',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.vr
|
||||
|
||||
# __SPINOFF_REQUIRE_UI_V1_BEGIN__
|
||||
|
||||
@property
|
||||
def toolbar_test(self) -> bool:
|
||||
"""(internal)."""
|
||||
warnings.warn(
|
||||
'app.toolbar_test is deprecated; use app.ui_v1.use_toolbars',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.ui_v1.use_toolbars
|
||||
|
||||
# __SPINOFF_REQUIRE_UI_V1_END__
|
||||
|
||||
@property
|
||||
def arcade_mode(self) -> bool:
|
||||
"""Whether the app is currently running on arcade hardware."""
|
||||
warnings.warn(
|
||||
'app.arcade_mode is deprecated; use app.env.arcade',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.arcade
|
||||
|
||||
@property
|
||||
def headless_mode(self) -> bool:
|
||||
"""Whether the app is running headlessly."""
|
||||
warnings.warn(
|
||||
'app.headless_mode is deprecated; use app.env.headless',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.headless
|
||||
|
||||
@property
|
||||
def demo_mode(self) -> bool:
|
||||
"""Whether the app is targeting a demo experience."""
|
||||
warnings.warn(
|
||||
'app.demo_mode is deprecated; use app.env.demo',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.env.demo
|
||||
|
||||
# __SPINOFF_REQUIRE_SCENE_V1_BEGIN__
|
||||
|
||||
@property
|
||||
def protocol_version(self) -> int:
|
||||
"""(internal)."""
|
||||
# pylint: disable=cyclic-import
|
||||
import bascenev1
|
||||
|
||||
warnings.warn(
|
||||
'app.protocol_version is deprecated;'
|
||||
' use bascenev1.protocol_version()',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return bascenev1.protocol_version()
|
||||
|
||||
# __SPINOFF_REQUIRE_SCENE_V1_END__
|
||||
def _thread_pool_thread_init(self) -> None:
|
||||
# Help keep things clear in profiling tools/etc.
|
||||
self._pool_thread_count += 1
|
||||
_babase.set_thread_name(f'ballistica worker-{self._pool_thread_count}')
|
||||
|
|
|
|||
41
dist/ba_data/python/babase/_appconfig.py
vendored
41
dist/ba_data/python/babase/_appconfig.py
vendored
|
|
@ -101,15 +101,13 @@ class AppConfig(dict):
|
|||
self.commit()
|
||||
|
||||
|
||||
def read_app_config() -> tuple[AppConfig, bool]:
|
||||
def read_app_config() -> AppConfig:
|
||||
"""Read the app config."""
|
||||
import os
|
||||
import json
|
||||
|
||||
config_file_healthy = False
|
||||
|
||||
# NOTE: it is assumed that this only gets called once and the
|
||||
# config object will not change from here on out
|
||||
# NOTE: it is assumed that this only gets called once and the config
|
||||
# object will not change from here on out
|
||||
config_file_path = _babase.app.env.config_file_path
|
||||
config_contents = ''
|
||||
try:
|
||||
|
|
@ -119,20 +117,16 @@ def read_app_config() -> tuple[AppConfig, bool]:
|
|||
config = AppConfig(json.loads(config_contents))
|
||||
else:
|
||||
config = AppConfig()
|
||||
config_file_healthy = True
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Error reading config file at time %.3f: '%s'.",
|
||||
"Error reading config file '%s' at time %.3f.\n"
|
||||
"Backing up broken config to'%s.broken'.",
|
||||
config_file_path,
|
||||
_babase.apptime(),
|
||||
config_file_path,
|
||||
)
|
||||
|
||||
# Whenever this happens lets back up the broken one just in case it
|
||||
# gets overwritten accidentally.
|
||||
logging.info(
|
||||
"Backing up current config file to '%s.broken'", config_file_path
|
||||
)
|
||||
try:
|
||||
import shutil
|
||||
|
||||
|
|
@ -141,23 +135,10 @@ def read_app_config() -> tuple[AppConfig, bool]:
|
|||
logging.exception('Error copying broken config.')
|
||||
config = AppConfig()
|
||||
|
||||
# Now attempt to read one of our 'prev' backup copies.
|
||||
prev_path = config_file_path + '.prev'
|
||||
try:
|
||||
if os.path.exists(prev_path):
|
||||
with open(prev_path, encoding='utf-8') as infile:
|
||||
config_contents = infile.read()
|
||||
config = AppConfig(json.loads(config_contents))
|
||||
else:
|
||||
config = AppConfig()
|
||||
config_file_healthy = True
|
||||
logging.info('Successfully read backup config.')
|
||||
except Exception:
|
||||
logging.exception('Error reading prev backup config.')
|
||||
return config, config_file_healthy
|
||||
return config
|
||||
|
||||
|
||||
def commit_app_config(force: bool = False) -> None:
|
||||
def commit_app_config() -> None:
|
||||
"""Commit the config to persistent storage.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
|
@ -167,10 +148,4 @@ def commit_app_config(force: bool = False) -> None:
|
|||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
if not _babase.app.config_file_healthy and not force:
|
||||
logging.warning(
|
||||
'Current config file is broken; '
|
||||
'skipping write to avoid losing settings.'
|
||||
)
|
||||
return
|
||||
plus.mark_config_dirty()
|
||||
|
|
|
|||
1
dist/ba_data/python/babase/_appmode.py
vendored
1
dist/ba_data/python/babase/_appmode.py
vendored
|
|
@ -31,6 +31,7 @@ class AppMode:
|
|||
AppExperience associated with the AppMode must be supported by
|
||||
the current app and runtime environment.
|
||||
"""
|
||||
# FIXME: check AppExperience.
|
||||
return cls._supports_intent(intent)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
4
dist/ba_data/python/babase/_appsubsystem.py
vendored
4
dist/ba_data/python/babase/_appsubsystem.py
vendored
|
|
@ -39,10 +39,10 @@ class AppSubsystem:
|
|||
def on_app_running(self) -> None:
|
||||
"""Called when the app reaches the running state."""
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
def on_app_suspend(self) -> None:
|
||||
"""Called when the app enters the paused state."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
def on_app_unsuspend(self) -> None:
|
||||
"""Called when the app exits the paused state."""
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
|
|
|
|||
20
dist/ba_data/python/babase/_apputils.py
vendored
20
dist/ba_data/python/babase/_apputils.py
vendored
|
|
@ -64,7 +64,9 @@ def get_remote_app_name() -> babase.Lstr:
|
|||
|
||||
def should_submit_debug_info() -> bool:
|
||||
"""(internal)"""
|
||||
return _babase.app.config.get('Submit Debug Info', True)
|
||||
val = _babase.app.config.get('Submit Debug Info', True)
|
||||
assert isinstance(val, bool)
|
||||
return val
|
||||
|
||||
|
||||
def handle_v1_cloud_log() -> None:
|
||||
|
|
@ -323,7 +325,7 @@ def dump_app_state(
|
|||
)
|
||||
|
||||
|
||||
def log_dumped_app_state() -> None:
|
||||
def log_dumped_app_state(from_previous_run: bool = False) -> None:
|
||||
"""If an app-state dump exists, log it and clear it. No-op otherwise."""
|
||||
|
||||
try:
|
||||
|
|
@ -350,8 +352,13 @@ def log_dumped_app_state() -> None:
|
|||
|
||||
metadata = dataclass_from_json(DumpedAppStateMetadata, appstatedata)
|
||||
|
||||
header = (
|
||||
'Found app state dump from previous app run'
|
||||
if from_previous_run
|
||||
else 'App state dump'
|
||||
)
|
||||
out += (
|
||||
f'App state dump:\nReason: {metadata.reason}\n'
|
||||
f'{header}:\nReason: {metadata.reason}\n'
|
||||
f'Time: {metadata.app_time:.2f}'
|
||||
)
|
||||
tbpath = os.path.join(
|
||||
|
|
@ -381,9 +388,10 @@ class AppHealthMonitor(AppSubsystem):
|
|||
|
||||
def on_app_loading(self) -> None:
|
||||
# If any traceback dumps happened last run, log and clear them.
|
||||
log_dumped_app_state()
|
||||
log_dumped_app_state(from_previous_run=True)
|
||||
|
||||
def _app_monitor_thread_main(self) -> None:
|
||||
_babase.set_thread_name('ballistica app-monitor')
|
||||
try:
|
||||
self._monitor_app()
|
||||
except Exception:
|
||||
|
|
@ -441,10 +449,10 @@ class AppHealthMonitor(AppSubsystem):
|
|||
|
||||
self._first_check = False
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
def on_app_suspend(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = False
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
def on_app_unsuspend(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = True
|
||||
|
|
|
|||
5
dist/ba_data/python/babase/_cloud.py
vendored
5
dist/ba_data/python/babase/_cloud.py
vendored
|
|
@ -26,6 +26,11 @@ DEBUG_LOG = False
|
|||
class CloudSubsystem(AppSubsystem):
|
||||
"""Manages communication with cloud components."""
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Property equivalent of CloudSubsystem.is_connected()."""
|
||||
return self.is_connected()
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Return whether a connection to the cloud is present.
|
||||
|
||||
|
|
|
|||
188
dist/ba_data/python/babase/_devconsole.py
vendored
Normal file
188
dist/ba_data/python/babase/_devconsole.py
vendored
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Dev-Console functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any, Literal
|
||||
|
||||
|
||||
class DevConsoleTab:
|
||||
"""Defines behavior for a tab in the dev-console."""
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Called when the tab should refresh itself."""
|
||||
|
||||
def request_refresh(self) -> None:
|
||||
"""The tab can call this to request that it be refreshed."""
|
||||
_babase.dev_console_request_refresh()
|
||||
|
||||
def button(
|
||||
self,
|
||||
label: str,
|
||||
pos: tuple[float, float],
|
||||
size: tuple[float, float],
|
||||
call: Callable[[], Any] | None = None,
|
||||
h_anchor: Literal['left', 'center', 'right'] = 'center',
|
||||
label_scale: float = 1.0,
|
||||
corner_radius: float = 8.0,
|
||||
style: Literal['normal', 'dark'] = 'normal',
|
||||
) -> None:
|
||||
"""Add a button to the tab being refreshed."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
_babase.dev_console_add_button(
|
||||
label,
|
||||
pos[0],
|
||||
pos[1],
|
||||
size[0],
|
||||
size[1],
|
||||
call,
|
||||
h_anchor,
|
||||
label_scale,
|
||||
corner_radius,
|
||||
style,
|
||||
)
|
||||
|
||||
def text(
|
||||
self,
|
||||
text: str,
|
||||
pos: tuple[float, float],
|
||||
h_anchor: Literal['left', 'center', 'right'] = 'center',
|
||||
h_align: Literal['left', 'center', 'right'] = 'center',
|
||||
v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
|
||||
scale: float = 1.0,
|
||||
) -> None:
|
||||
"""Add a button to the tab being refreshed."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
_babase.dev_console_add_text(
|
||||
text, pos[0], pos[1], h_anchor, h_align, v_align, scale
|
||||
)
|
||||
|
||||
def python_terminal(self) -> None:
|
||||
"""Add a Python Terminal to the tab being refreshed."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
_babase.dev_console_add_python_terminal()
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
"""Return the current tab width. Only call during refreshes."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
return _babase.dev_console_tab_width()
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
"""Return the current tab height. Only call during refreshes."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
return _babase.dev_console_tab_height()
|
||||
|
||||
@property
|
||||
def base_scale(self) -> float:
|
||||
"""A scale value set depending on the app's UI scale.
|
||||
|
||||
Dev-console tabs can incorporate this into their UI sizes and
|
||||
positions if they desire. This must be done manually however.
|
||||
"""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
return _babase.dev_console_base_scale()
|
||||
|
||||
|
||||
class DevConsoleTabPython(DevConsoleTab):
|
||||
"""The Python dev-console tab."""
|
||||
|
||||
def refresh(self) -> None:
|
||||
self.python_terminal()
|
||||
|
||||
|
||||
class DevConsoleTabTest(DevConsoleTab):
|
||||
"""Test dev-console tab."""
|
||||
|
||||
def refresh(self) -> None:
|
||||
import random
|
||||
|
||||
self.button(
|
||||
f'FLOOP-{random.randrange(200)}',
|
||||
pos=(10, 10),
|
||||
size=(100, 30),
|
||||
h_anchor='left',
|
||||
label_scale=0.6,
|
||||
call=self.request_refresh,
|
||||
)
|
||||
self.button(
|
||||
f'FLOOP2-{random.randrange(200)}',
|
||||
pos=(120, 10),
|
||||
size=(100, 30),
|
||||
h_anchor='left',
|
||||
label_scale=0.6,
|
||||
style='dark',
|
||||
)
|
||||
self.text(
|
||||
'TestText',
|
||||
scale=0.8,
|
||||
pos=(15, 50),
|
||||
h_anchor='left',
|
||||
h_align='left',
|
||||
v_align='none',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevConsoleTabEntry:
|
||||
"""Represents a distinct tab in the dev-console."""
|
||||
|
||||
name: str
|
||||
factory: Callable[[], DevConsoleTab]
|
||||
|
||||
|
||||
class DevConsoleSubsystem:
|
||||
"""Subsystem for wrangling the dev console.
|
||||
|
||||
The single instance of this class can be found at
|
||||
babase.app.devconsole. The dev-console is a simple always-available
|
||||
UI intended for use by developers; not end users. Traditionally it
|
||||
is available by typing a backtick (`) key on a keyboard, but now can
|
||||
be accessed via an on-screen button (see settings/advanced to enable
|
||||
said button).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# All tabs in the dev-console. Add your own stuff here via
|
||||
# plugins or whatnot.
|
||||
self.tabs: list[DevConsoleTabEntry] = [
|
||||
DevConsoleTabEntry('Python', DevConsoleTabPython)
|
||||
]
|
||||
if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
|
||||
self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
|
||||
self.is_refreshing = False
|
||||
|
||||
def do_refresh_tab(self, tabname: str) -> None:
|
||||
"""Called by the C++ layer when a tab should be filled out."""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# FIXME: We currently won't handle multiple tabs with the same
|
||||
# name. We should give a clean error or something in that case.
|
||||
tab: DevConsoleTab | None = None
|
||||
for tabentry in self.tabs:
|
||||
if tabentry.name == tabname:
|
||||
tab = tabentry.factory()
|
||||
break
|
||||
|
||||
if tab is None:
|
||||
logging.error(
|
||||
'DevConsole got refresh request for tab'
|
||||
" '%s' which does not exist.",
|
||||
tabname,
|
||||
)
|
||||
return
|
||||
|
||||
self.is_refreshing = True
|
||||
try:
|
||||
tab.refresh()
|
||||
finally:
|
||||
self.is_refreshing = False
|
||||
11
dist/ba_data/python/babase/_env.py
vendored
11
dist/ba_data/python/babase/_env.py
vendored
|
|
@ -40,6 +40,11 @@ def on_native_module_import() -> None:
|
|||
if envconfig.log_handler is not None:
|
||||
_feed_logs_to_babase(envconfig.log_handler)
|
||||
|
||||
# Also let's name the log-handler thread to help in profiling.
|
||||
envconfig.log_handler.call_in_thread(
|
||||
lambda: _babase.set_thread_name('ballistica logging')
|
||||
)
|
||||
|
||||
env = _babase.pre_env()
|
||||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
|
|
@ -180,10 +185,8 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
|
|||
def _on_log(entry: LogEntry) -> None:
|
||||
# Forward this along to the engine to display in the in-app
|
||||
# console, in the Android log, etc.
|
||||
_babase.display_log(
|
||||
name=entry.name,
|
||||
level=entry.level.name,
|
||||
message=entry.message,
|
||||
_babase.emit_log(
|
||||
name=entry.name, level=entry.level.name, message=entry.message
|
||||
)
|
||||
|
||||
# We also want to feed some logs to the old v1-cloud-log system.
|
||||
|
|
|
|||
61
dist/ba_data/python/babase/_hooks.py
vendored
61
dist/ba_data/python/babase/_hooks.py
vendored
|
|
@ -33,18 +33,47 @@ def reset_to_main_menu() -> None:
|
|||
logging.warning('reset_to_main_menu: no-op due to classic not present.')
|
||||
|
||||
|
||||
def set_config_fullscreen_on() -> None:
|
||||
def get_v2_account_id() -> str | None:
|
||||
"""Return the current V2 account id if signed in, or None if not."""
|
||||
try:
|
||||
plus = _babase.app.plus
|
||||
if plus is not None:
|
||||
account = plus.accounts.primary
|
||||
if account is not None:
|
||||
accountid = account.accountid
|
||||
# (Avoids mypy complaints when plus is not present)
|
||||
assert isinstance(accountid, (str, type(None)))
|
||||
return accountid
|
||||
return None
|
||||
except Exception:
|
||||
logging.exception('Error fetching v2 account id.')
|
||||
return None
|
||||
|
||||
|
||||
def store_config_fullscreen_on() -> None:
|
||||
"""The OS has changed our fullscreen state and we should take note."""
|
||||
_babase.app.config['Fullscreen'] = True
|
||||
_babase.app.config.commit()
|
||||
|
||||
|
||||
def set_config_fullscreen_off() -> None:
|
||||
def store_config_fullscreen_off() -> None:
|
||||
"""The OS has changed our fullscreen state and we should take note."""
|
||||
_babase.app.config['Fullscreen'] = False
|
||||
_babase.app.config.commit()
|
||||
|
||||
|
||||
def set_config_fullscreen_on() -> None:
|
||||
"""Set and store fullscreen state"""
|
||||
_babase.app.config['Fullscreen'] = True
|
||||
_babase.app.config.apply_and_commit()
|
||||
|
||||
|
||||
def set_config_fullscreen_off() -> None:
|
||||
"""The OS has changed our fullscreen state and we should take note."""
|
||||
_babase.app.config['Fullscreen'] = False
|
||||
_babase.app.config.apply_and_commit()
|
||||
|
||||
|
||||
def not_signed_in_screen_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
|
|
@ -111,6 +140,14 @@ def error_message() -> None:
|
|||
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def success_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
if _babase.app.env.gui:
|
||||
_babase.getsimplesound('dingSmall').play()
|
||||
_babase.screenmessage(Lstr(resource='successText'), color=(0, 1, 0))
|
||||
|
||||
|
||||
def purchase_not_valid_error() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
|
|
@ -300,6 +337,7 @@ def implicit_sign_in(
|
|||
from bacommon.login import LoginType
|
||||
|
||||
assert _babase.app.plus is not None
|
||||
|
||||
_babase.app.plus.accounts.on_implicit_sign_in(
|
||||
login_type=LoginType(login_type_str),
|
||||
login_id=login_id,
|
||||
|
|
@ -372,3 +410,22 @@ def string_edit_adapter_can_be_replaced(adapter: StringEditAdapter) -> bool:
|
|||
|
||||
assert isinstance(adapter, StringEditAdapter)
|
||||
return adapter.can_be_replaced()
|
||||
|
||||
|
||||
def get_dev_console_tab_names() -> list[str]:
|
||||
"""Return the current set of dev-console tab names."""
|
||||
return [t.name for t in _babase.app.devconsole.tabs]
|
||||
|
||||
|
||||
def unsupported_controller_message(name: str) -> None:
|
||||
"""Print a message when an unsupported controller is connected."""
|
||||
from babase._language import Lstr
|
||||
|
||||
# Ick; this can get called early in the bootstrapping process
|
||||
# before we're allowed to load assets. Guard against that.
|
||||
if _babase.asset_loads_allowed():
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='unsupportedControllerText', subs=[('${NAME}', name)]),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
|
|
|||
59
dist/ba_data/python/babase/_login.py
vendored
59
dist/ba_data/python/babase/_login.py
vendored
|
|
@ -20,6 +20,13 @@ if TYPE_CHECKING:
|
|||
DEBUG_LOG = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoginInfo:
|
||||
"""Basic info about a login available in the app.plus.accounts section."""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class LoginAdapter:
|
||||
"""Allows using implicit login types in an explicit way.
|
||||
|
||||
|
|
@ -138,7 +145,7 @@ class LoginAdapter:
|
|||
is actually being used by the app. It should therefore register
|
||||
unlocked achievements, leaderboard scores, allow viewing native
|
||||
UIs, etc. When not active it should ignore everything and behave
|
||||
as if logged out, even if it technically is still logged in.
|
||||
as if signed out, even if it technically is still signed in.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
del active # Unused.
|
||||
|
|
@ -149,7 +156,7 @@ class LoginAdapter:
|
|||
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
|
||||
description: str,
|
||||
) -> None:
|
||||
"""Attempt an explicit sign in via this adapter.
|
||||
"""Attempt to sign in via this adapter.
|
||||
|
||||
This can be called even if the back-end is not implicitly signed in;
|
||||
the adapter will attempt to sign in if possible. An exception will
|
||||
|
|
@ -161,7 +168,7 @@ class LoginAdapter:
|
|||
# Have been seeing multiple sign-in attempts come through
|
||||
# nearly simultaneously which can be problematic server-side.
|
||||
# Let's error if a sign-in attempt is made within a few seconds
|
||||
# of the last one to address this.
|
||||
# of the last one to try and address this.
|
||||
now = time.monotonic()
|
||||
appnow = _babase.apptime()
|
||||
if self._last_sign_in_time is not None:
|
||||
|
|
@ -229,6 +236,7 @@ class LoginAdapter:
|
|||
def _got_sign_in_response(
|
||||
response: bacommon.cloud.SignInResponse | Exception,
|
||||
) -> None:
|
||||
# This likely means we couldn't communicate with the server.
|
||||
if isinstance(response, Exception):
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -239,20 +247,18 @@ class LoginAdapter:
|
|||
)
|
||||
_babase.pushcall(Call(result_cb, self, response))
|
||||
else:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got successful'
|
||||
' sign-in response',
|
||||
self.login_type.name,
|
||||
)
|
||||
# This means our credentials were explicitly rejected.
|
||||
if response.credentials is None:
|
||||
result2: LoginAdapter.SignInResult | Exception = (
|
||||
RuntimeError(
|
||||
'No credentials returned after'
|
||||
' submitting sign-in-token.'
|
||||
)
|
||||
RuntimeError('Sign-in-token was rejected.')
|
||||
)
|
||||
else:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got successful'
|
||||
' sign-in response',
|
||||
self.login_type.name,
|
||||
)
|
||||
result2 = self.SignInResult(
|
||||
credentials=response.credentials
|
||||
)
|
||||
|
|
@ -269,7 +275,7 @@ class LoginAdapter:
|
|||
on_response=_got_sign_in_response,
|
||||
)
|
||||
|
||||
# Kick off the process by fetching a sign-in token.
|
||||
# Kick off the sign-in process by fetching a sign-in token.
|
||||
self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
|
||||
|
||||
def is_back_end_active(self) -> bool:
|
||||
|
|
@ -282,11 +288,10 @@ class LoginAdapter:
|
|||
"""Get a sign-in token from the adapter back end.
|
||||
|
||||
This token is then passed to the master-server to complete the
|
||||
login process.
|
||||
The adapter can use this opportunity to bring up account creation
|
||||
UI, call its internal sign_in function, etc. as needed.
|
||||
The provided completion_cb should then be called with either a token
|
||||
or None if sign in failed or was cancelled.
|
||||
sign-in process. The adapter can use this opportunity to bring
|
||||
up account creation UI, call its internal sign_in function, etc.
|
||||
as needed. The provided completion_cb should then be called with
|
||||
either a token or None if sign in failed or was cancelled.
|
||||
"""
|
||||
from babase._general import Call
|
||||
|
||||
|
|
@ -295,7 +300,7 @@ class LoginAdapter:
|
|||
|
||||
def _update_implicit_login_state(self) -> None:
|
||||
# If we've received an implicit login state, schedule it to be
|
||||
# sent along to the app. We wait until on-app-launch has been
|
||||
# sent along to the app. We wait until on-app-loading has been
|
||||
# called so that account-client-v2 has had a chance to load
|
||||
# any existing state so it can properly respond to this.
|
||||
if self._implicit_login_state_dirty and self._on_app_loading_called:
|
||||
|
|
@ -340,8 +345,8 @@ class LoginAdapter:
|
|||
class LoginAdapterNative(LoginAdapter):
|
||||
"""A login adapter that does its work in the native layer."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(LoginType.GPGS)
|
||||
def __init__(self, login_type: LoginType) -> None:
|
||||
super().__init__(login_type)
|
||||
|
||||
# Store int ids for in-flight attempts since they may go through
|
||||
# various platform layers and back.
|
||||
|
|
@ -375,3 +380,13 @@ class LoginAdapterNative(LoginAdapter):
|
|||
|
||||
class LoginAdapterGPGS(LoginAdapterNative):
|
||||
"""Google Play Game Services adapter."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(LoginType.GPGS)
|
||||
|
||||
|
||||
class LoginAdapterGameCenter(LoginAdapterNative):
|
||||
"""Apple Game Center adapter."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(LoginType.GAME_CENTER)
|
||||
|
|
|
|||
35
dist/ba_data/python/babase/_meta.py
vendored
35
dist/ba_data/python/babase/_meta.py
vendored
|
|
@ -24,6 +24,8 @@ if TYPE_CHECKING:
|
|||
# instead of these or to make the meta system aware of arbitrary classes.
|
||||
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
|
||||
'plugin': 'babase.Plugin',
|
||||
# DEPRECATED as of 12/2023. Currently am warning if finding these
|
||||
# but should take this out eventually.
|
||||
'keyboard': 'babase.Keyboard',
|
||||
}
|
||||
|
||||
|
|
@ -414,30 +416,27 @@ class DirectoryScan:
|
|||
if export_class_name is not None:
|
||||
classname = modulename + '.' + export_class_name
|
||||
|
||||
# Since we'll soon have multiple versions of 'game'
|
||||
# classes we need to migrate people to using base
|
||||
# class names for them.
|
||||
if exporttypestr == 'game':
|
||||
# Migrating away from the 'keyboard' name shortcut
|
||||
# since it's specific to bauiv1; warn if we find it.
|
||||
if exporttypestr == 'keyboard':
|
||||
logging.warning(
|
||||
"metascan: %s:%d: '# ba_meta export"
|
||||
" game' tag should be replaced by '# ba_meta"
|
||||
" export bascenev1.GameActivity'.",
|
||||
" keyboard' tag should be replaced by '# ba_meta"
|
||||
" export bauiv1.Keyboard'.",
|
||||
subpath,
|
||||
lindex + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
else:
|
||||
# If export type is one of our shortcuts, sub in the
|
||||
# actual class path. Otherwise assume its a classpath
|
||||
# itself.
|
||||
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(
|
||||
exporttypestr
|
||||
)
|
||||
if exporttype is None:
|
||||
exporttype = exporttypestr
|
||||
self.results.exports.setdefault(exporttype, []).append(
|
||||
classname
|
||||
)
|
||||
|
||||
# If export type is one of our shortcuts, sub in the
|
||||
# actual class path. Otherwise assume its a classpath
|
||||
# itself.
|
||||
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
|
||||
if exporttype is None:
|
||||
exporttype = exporttypestr
|
||||
self.results.exports.setdefault(exporttype, []).append(
|
||||
classname
|
||||
)
|
||||
|
||||
def _get_export_class_name(
|
||||
self, subpath: Path, lines: list[str], lindex: int
|
||||
|
|
|
|||
21
dist/ba_data/python/babase/_mgen/enums.py
vendored
21
dist/ba_data/python/babase/_mgen/enums.py
vendored
|
|
@ -38,6 +38,27 @@ class InputType(Enum):
|
|||
DOWN_RELEASE = 26
|
||||
|
||||
|
||||
class QuitType(Enum):
|
||||
"""Types of input a controller can send to the game.
|
||||
|
||||
Category: Enums
|
||||
|
||||
'soft' may hide/reset the app but keep the process running, depending
|
||||
on the platform.
|
||||
|
||||
'back' is a variant of 'soft' which may give 'back-button-pressed'
|
||||
behavior depending on the platform. (returning to some previous
|
||||
activity instead of dumping to the home screen, etc.)
|
||||
|
||||
'hard' leads to the process exiting. This generally should be avoided
|
||||
on platforms such as mobile.
|
||||
"""
|
||||
|
||||
SOFT = 0
|
||||
BACK = 1
|
||||
HARD = 2
|
||||
|
||||
|
||||
class UIScale(Enum):
|
||||
"""The overall scale the UI is being rendered for. Note that this is
|
||||
independent of pixel resolution. For example, a phone and a desktop PC
|
||||
|
|
|
|||
20
dist/ba_data/python/babase/_plugin.py
vendored
20
dist/ba_data/python/babase/_plugin.py
vendored
|
|
@ -170,23 +170,23 @@ class PluginSubsystem(AppSubsystem):
|
|||
|
||||
_error.print_exception('Error in plugin on_app_running()')
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
def on_app_suspend(self) -> None:
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_pause()
|
||||
plugin.on_app_suspend()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_pause()')
|
||||
_error.print_exception('Error in plugin on_app_suspend()')
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
def on_app_unsuspend(self) -> None:
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_resume()
|
||||
plugin.on_app_unsuspend()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_resume()')
|
||||
_error.print_exception('Error in plugin on_app_unsuspend()')
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
for plugin in self.active_plugins:
|
||||
|
|
@ -327,11 +327,11 @@ class Plugin:
|
|||
def on_app_running(self) -> None:
|
||||
"""Called when the app reaches the running state."""
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app is switching to a paused state."""
|
||||
def on_app_suspend(self) -> None:
|
||||
"""Called when the app enters the suspended state."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Called when the app is resuming from a paused state."""
|
||||
def on_app_unsuspend(self) -> None:
|
||||
"""Called when the app exits the suspended state."""
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called when the app is beginning the shutdown process."""
|
||||
|
|
|
|||
4
dist/ba_data/python/babase/modutils.py
vendored
4
dist/ba_data/python/babase/modutils.py
vendored
|
|
@ -104,8 +104,8 @@ def show_user_scripts() -> None:
|
|||
|
||||
_error.print_exception('error writing about_this_folder stuff')
|
||||
|
||||
# On a few platforms we try to open the dir in the UI.
|
||||
if app.classic is not None and app.classic.platform in ['mac', 'windows']:
|
||||
# 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue