1.7.32 ba_data update

This commit is contained in:
Ayush Saini 2023-12-21 15:55:50 +05:30
parent bf2f252ee5
commit 15393d5461
144 changed files with 4296 additions and 2411 deletions

View file

@ -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',

View file

@ -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.

View file

@ -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}')

View file

@ -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()

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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.

View 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

View file

@ -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.

View file

@ -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),
)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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."""

View file

@ -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.